@exulu/backend 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/{chunk-KFL7HIID.js → chunk-CVQTDG37.js} +1738 -781
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-YY2WIMMJ.js → convert-exulu-tools-to-ai-sdk-tools-QG7E6UX5.js} +1 -1
- package/dist/index.cjs +6947 -6530
- package/dist/index.d.cts +325 -369
- package/dist/index.d.ts +325 -369
- package/dist/index.js +3000 -3691
- package/ee/agentic-retrieval/pipeline/hyde.test.ts +1 -1
- package/ee/agentic-retrieval/pipeline/hyde.ts +6 -3
- package/ee/agentic-retrieval/pipeline/index.test.ts +1 -1
- package/ee/agentic-retrieval/pipeline/index.ts +0 -1
- package/ee/agentic-retrieval/pipeline/memory.test.ts +5 -1
- package/ee/agentic-retrieval/pipeline/memory.ts +71 -104
- package/ee/agentic-retrieval/pipeline/micro-call.test.ts +112 -0
- package/ee/agentic-retrieval/pipeline/micro-call.ts +98 -0
- package/ee/agentic-retrieval/pipeline/prefilter.test.ts +1 -0
- package/ee/agentic-retrieval/pipeline/prefilter.ts +11 -20
- package/ee/agentic-retrieval/pipeline/routing.test.ts +44 -1
- package/ee/agentic-retrieval/pipeline/routing.ts +31 -56
- package/ee/python/documents/processing/doc_processor.ts +0 -1
- package/ee/queues/queues.ts +13 -0
- package/ee/schemas.ts +11 -10
- package/ee/workers.ts +42 -56
- package/package.json +1 -1
- package/ee/workers.flow.test.ts +0 -236
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-7CCMW3IW.js";
|
|
5
5
|
|
|
6
6
|
// src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
|
|
7
|
-
import { S3Client as
|
|
7
|
+
import { S3Client as S3Client3, PutObjectCommand as PutObjectCommand3, S3ServiceException } from "@aws-sdk/client-s3";
|
|
8
8
|
|
|
9
9
|
// src/exulu/tool.ts
|
|
10
10
|
import { tool } from "ai";
|
|
@@ -33,210 +33,8 @@ var exuluApp = {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
// src/exulu/resolve-model.ts
|
|
36
|
-
import CryptoJS from "crypto-js";
|
|
37
36
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
38
37
|
|
|
39
|
-
// src/postgres/client.ts
|
|
40
|
-
import Knex from "knex";
|
|
41
|
-
import "knex";
|
|
42
|
-
import "pgvector/knex";
|
|
43
|
-
var db = {};
|
|
44
|
-
var databaseExistsChecked = false;
|
|
45
|
-
var getDbName = () => process.env.POSTGRES_DB_NAME || "exulu";
|
|
46
|
-
async function ensureDatabaseExists() {
|
|
47
|
-
const dbName = getDbName();
|
|
48
|
-
const defaultKnex = Knex({
|
|
49
|
-
client: "pg",
|
|
50
|
-
connection: {
|
|
51
|
-
host: process.env.POSTGRES_DB_HOST,
|
|
52
|
-
port: parseInt(process.env.POSTGRES_DB_PORT || "5432"),
|
|
53
|
-
user: process.env.POSTGRES_DB_USER,
|
|
54
|
-
database: "postgres",
|
|
55
|
-
// Connect to default database
|
|
56
|
-
password: process.env.POSTGRES_DB_PASSWORD,
|
|
57
|
-
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
58
|
-
connectionTimeoutMillis: 1e4
|
|
59
|
-
},
|
|
60
|
-
pool: {
|
|
61
|
-
min: 2,
|
|
62
|
-
max: 4,
|
|
63
|
-
acquireTimeoutMillis: 3e4,
|
|
64
|
-
createTimeoutMillis: 3e4,
|
|
65
|
-
idleTimeoutMillis: 3e4,
|
|
66
|
-
reapIntervalMillis: 1e3,
|
|
67
|
-
createRetryIntervalMillis: 200
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
try {
|
|
71
|
-
const result = await defaultKnex.raw(`
|
|
72
|
-
SELECT 1 FROM pg_database WHERE datname = '${dbName}'
|
|
73
|
-
`);
|
|
74
|
-
if (result.rows.length === 0) {
|
|
75
|
-
console.log(`[EXULU] Database '${dbName}' does not exist. Creating it...`);
|
|
76
|
-
await defaultKnex.raw(`CREATE DATABASE ${dbName}`);
|
|
77
|
-
console.log(`[EXULU] Database '${dbName}' created successfully.`);
|
|
78
|
-
} else {
|
|
79
|
-
console.log(`[EXULU] Database '${dbName}' already exists.`);
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.error(
|
|
83
|
-
"[EXULU] Error while checking to ensure the database exists, this could be if the user running the server does not have database admin rights, it is fine to ignore this if you are sure the database exists.",
|
|
84
|
-
error
|
|
85
|
-
);
|
|
86
|
-
return;
|
|
87
|
-
} finally {
|
|
88
|
-
await defaultKnex.destroy();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
async function postgresClient() {
|
|
92
|
-
if (!db["exulu"]) {
|
|
93
|
-
try {
|
|
94
|
-
if (!databaseExistsChecked) {
|
|
95
|
-
await ensureDatabaseExists();
|
|
96
|
-
databaseExistsChecked = true;
|
|
97
|
-
}
|
|
98
|
-
const knex = Knex({
|
|
99
|
-
client: "pg",
|
|
100
|
-
connection: {
|
|
101
|
-
host: process.env.POSTGRES_DB_HOST,
|
|
102
|
-
port: parseInt(process.env.POSTGRES_DB_PORT || "5432"),
|
|
103
|
-
user: process.env.POSTGRES_DB_USER,
|
|
104
|
-
database: getDbName(),
|
|
105
|
-
password: process.env.POSTGRES_DB_PASSWORD,
|
|
106
|
-
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
107
|
-
// TCP keepalive prevents idle sockets from being silently dropped by
|
|
108
|
-
// intermediate network devices (NAT, firewalls) between us and Hetzner.
|
|
109
|
-
keepAlive: true,
|
|
110
|
-
keepAliveInitialDelayMillis: 1e4,
|
|
111
|
-
connectionTimeoutMillis: 3e4,
|
|
112
|
-
statement_timeout: 18e5,
|
|
113
|
-
query_timeout: 18e5
|
|
114
|
-
},
|
|
115
|
-
pool: {
|
|
116
|
-
min: 10,
|
|
117
|
-
max: 300,
|
|
118
|
-
acquireTimeoutMillis: 12e4,
|
|
119
|
-
createTimeoutMillis: 3e4,
|
|
120
|
-
idleTimeoutMillis: 3e4,
|
|
121
|
-
reapIntervalMillis: 1e3,
|
|
122
|
-
createRetryIntervalMillis: 200,
|
|
123
|
-
// Enable propagateCreateError to properly handle connection creation failures
|
|
124
|
-
propagateCreateError: false,
|
|
125
|
-
// Log pool events to help debug connection issues
|
|
126
|
-
afterCreate: (conn, done) => {
|
|
127
|
-
console.log("[EXULU] New database connection created");
|
|
128
|
-
conn.query("SET statement_timeout = 1800000; SET hnsw.ef_search = 20", (err) => {
|
|
129
|
-
if (err) {
|
|
130
|
-
console.error("[EXULU] Error setting connection parameters:", err);
|
|
131
|
-
}
|
|
132
|
-
done(err, conn);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
try {
|
|
138
|
-
await knex.schema.createExtensionIfNotExists("vector");
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error(
|
|
141
|
-
"[EXULU] Error creating vector extension, this might be fine if you already activated the extension and the 'user' running this script does not have higher level database permissions.",
|
|
142
|
-
error
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
db["exulu"] = knex;
|
|
146
|
-
} catch (error) {
|
|
147
|
-
console.error("[EXULU] Error initializing exulu database.", error);
|
|
148
|
-
throw error;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return {
|
|
152
|
-
db: db["exulu"]
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// src/utils/check-record-access.ts
|
|
157
|
-
var checkRecordAccessCache = /* @__PURE__ */ new Map();
|
|
158
|
-
var checkRecordAccess = async (record, request, user) => {
|
|
159
|
-
const setRecordAccessCache = (hasAccess2) => {
|
|
160
|
-
checkRecordAccessCache.set(`${record.id}-${request}-${user?.id}`, {
|
|
161
|
-
hasAccess: hasAccess2,
|
|
162
|
-
expiresAt: new Date(Date.now() + 1e3 * 60 * 1)
|
|
163
|
-
// 1 minute
|
|
164
|
-
});
|
|
165
|
-
};
|
|
166
|
-
const cachedAccess = checkRecordAccessCache.get(`${record.id}-${request}-${user?.id}`);
|
|
167
|
-
if (cachedAccess && cachedAccess.expiresAt > /* @__PURE__ */ new Date()) {
|
|
168
|
-
return cachedAccess.hasAccess;
|
|
169
|
-
}
|
|
170
|
-
const isPublic = record.rights_mode === "public";
|
|
171
|
-
const byUsers = record.rights_mode === "users";
|
|
172
|
-
const byRoles = record.rights_mode === "roles";
|
|
173
|
-
const byTeams = record.rights_mode === "teams";
|
|
174
|
-
const createdBy = typeof record.created_by === "string" ? record.created_by : record.created_by?.toString();
|
|
175
|
-
const isCreator = user ? createdBy === user.id.toString() : false;
|
|
176
|
-
const isAdmin = user ? user.super_admin : false;
|
|
177
|
-
const isApi = user ? user.type === "api" : false;
|
|
178
|
-
const isAdminApi = isApi && (!user.scope_mode || user.scope_mode === "admin");
|
|
179
|
-
const isAgentsScopedApi = isApi && user.scope_mode === "agents" && request === "read" && Array.isArray(user.agent_ids) && user.agent_ids.includes(String(record.id));
|
|
180
|
-
let hasAccess = "none";
|
|
181
|
-
if (isPublic || isCreator || isAdmin || isAdminApi || isAgentsScopedApi) {
|
|
182
|
-
setRecordAccessCache(true);
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
if (byUsers) {
|
|
186
|
-
if (!user) {
|
|
187
|
-
setRecordAccessCache(false);
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
hasAccess = record.RBAC?.users?.find((x) => x.id === user.id)?.rights || "none";
|
|
191
|
-
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
192
|
-
console.error(
|
|
193
|
-
`[EXULU] Your current user ${user.id} does not have access to this record, current access type is: ${hasAccess}.`
|
|
194
|
-
);
|
|
195
|
-
setRecordAccessCache(false);
|
|
196
|
-
return false;
|
|
197
|
-
} else {
|
|
198
|
-
setRecordAccessCache(true);
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
if (byRoles) {
|
|
203
|
-
if (!user) {
|
|
204
|
-
setRecordAccessCache(false);
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
hasAccess = record.RBAC?.roles?.find((x) => x.id === user.role?.id)?.rights || "none";
|
|
208
|
-
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
209
|
-
console.error(
|
|
210
|
-
`[EXULU] Your current role ${user.role?.name} does not have access to this record, current access type is: ${hasAccess}.`
|
|
211
|
-
);
|
|
212
|
-
setRecordAccessCache(false);
|
|
213
|
-
return false;
|
|
214
|
-
} else {
|
|
215
|
-
setRecordAccessCache(true);
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (byTeams) {
|
|
220
|
-
if (!user) {
|
|
221
|
-
setRecordAccessCache(false);
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
hasAccess = record.RBAC?.teams?.find((x) => x.id === user.team?.id)?.rights || "none";
|
|
225
|
-
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
226
|
-
console.error(
|
|
227
|
-
`[EXULU] Your current team ${user.team?.name} does not have access to this record, current access type is: ${hasAccess}.`
|
|
228
|
-
);
|
|
229
|
-
setRecordAccessCache(false);
|
|
230
|
-
return false;
|
|
231
|
-
} else {
|
|
232
|
-
setRecordAccessCache(true);
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
setRecordAccessCache(false);
|
|
237
|
-
return false;
|
|
238
|
-
};
|
|
239
|
-
|
|
240
38
|
// src/exulu/litellm/supervisor.ts
|
|
241
39
|
import { spawn } from "child_process";
|
|
242
40
|
import { existsSync } from "fs";
|
|
@@ -616,6 +414,123 @@ function createTaggedFetch(tags) {
|
|
|
616
414
|
return labeled;
|
|
617
415
|
}
|
|
618
416
|
|
|
417
|
+
// src/postgres/client.ts
|
|
418
|
+
import Knex from "knex";
|
|
419
|
+
import "knex";
|
|
420
|
+
import "pgvector/knex";
|
|
421
|
+
var db = {};
|
|
422
|
+
var databaseExistsChecked = false;
|
|
423
|
+
var getDbName = () => process.env.POSTGRES_DB_NAME || "exulu";
|
|
424
|
+
async function ensureDatabaseExists() {
|
|
425
|
+
const dbName = getDbName();
|
|
426
|
+
const defaultKnex = Knex({
|
|
427
|
+
client: "pg",
|
|
428
|
+
connection: {
|
|
429
|
+
host: process.env.POSTGRES_DB_HOST,
|
|
430
|
+
port: parseInt(process.env.POSTGRES_DB_PORT || "5432"),
|
|
431
|
+
user: process.env.POSTGRES_DB_USER,
|
|
432
|
+
database: "postgres",
|
|
433
|
+
// Connect to default database
|
|
434
|
+
password: process.env.POSTGRES_DB_PASSWORD,
|
|
435
|
+
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
436
|
+
connectionTimeoutMillis: 1e4
|
|
437
|
+
},
|
|
438
|
+
pool: {
|
|
439
|
+
min: 2,
|
|
440
|
+
max: 4,
|
|
441
|
+
acquireTimeoutMillis: 3e4,
|
|
442
|
+
createTimeoutMillis: 3e4,
|
|
443
|
+
idleTimeoutMillis: 3e4,
|
|
444
|
+
reapIntervalMillis: 1e3,
|
|
445
|
+
createRetryIntervalMillis: 200
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
try {
|
|
449
|
+
const result = await defaultKnex.raw(`
|
|
450
|
+
SELECT 1 FROM pg_database WHERE datname = '${dbName}'
|
|
451
|
+
`);
|
|
452
|
+
if (result.rows.length === 0) {
|
|
453
|
+
console.log(`[EXULU] Database '${dbName}' does not exist. Creating it...`);
|
|
454
|
+
await defaultKnex.raw(`CREATE DATABASE ${dbName}`);
|
|
455
|
+
console.log(`[EXULU] Database '${dbName}' created successfully.`);
|
|
456
|
+
} else {
|
|
457
|
+
console.log(`[EXULU] Database '${dbName}' already exists.`);
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error(
|
|
461
|
+
"[EXULU] Error while checking to ensure the database exists, this could be if the user running the server does not have database admin rights, it is fine to ignore this if you are sure the database exists.",
|
|
462
|
+
error
|
|
463
|
+
);
|
|
464
|
+
return;
|
|
465
|
+
} finally {
|
|
466
|
+
await defaultKnex.destroy();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function postgresClient() {
|
|
470
|
+
if (!db["exulu"]) {
|
|
471
|
+
try {
|
|
472
|
+
if (!databaseExistsChecked) {
|
|
473
|
+
await ensureDatabaseExists();
|
|
474
|
+
databaseExistsChecked = true;
|
|
475
|
+
}
|
|
476
|
+
const knex = Knex({
|
|
477
|
+
client: "pg",
|
|
478
|
+
connection: {
|
|
479
|
+
host: process.env.POSTGRES_DB_HOST,
|
|
480
|
+
port: parseInt(process.env.POSTGRES_DB_PORT || "5432"),
|
|
481
|
+
user: process.env.POSTGRES_DB_USER,
|
|
482
|
+
database: getDbName(),
|
|
483
|
+
password: process.env.POSTGRES_DB_PASSWORD,
|
|
484
|
+
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
485
|
+
// TCP keepalive prevents idle sockets from being silently dropped by
|
|
486
|
+
// intermediate network devices (NAT, firewalls) between us and Hetzner.
|
|
487
|
+
keepAlive: true,
|
|
488
|
+
keepAliveInitialDelayMillis: 1e4,
|
|
489
|
+
connectionTimeoutMillis: 3e4,
|
|
490
|
+
statement_timeout: 18e5,
|
|
491
|
+
query_timeout: 18e5
|
|
492
|
+
},
|
|
493
|
+
pool: {
|
|
494
|
+
min: 10,
|
|
495
|
+
max: 300,
|
|
496
|
+
acquireTimeoutMillis: 12e4,
|
|
497
|
+
createTimeoutMillis: 3e4,
|
|
498
|
+
idleTimeoutMillis: 3e4,
|
|
499
|
+
reapIntervalMillis: 1e3,
|
|
500
|
+
createRetryIntervalMillis: 200,
|
|
501
|
+
// Enable propagateCreateError to properly handle connection creation failures
|
|
502
|
+
propagateCreateError: false,
|
|
503
|
+
// Log pool events to help debug connection issues
|
|
504
|
+
afterCreate: (conn, done) => {
|
|
505
|
+
console.log("[EXULU] New database connection created");
|
|
506
|
+
conn.query("SET statement_timeout = 1800000; SET hnsw.ef_search = 20", (err) => {
|
|
507
|
+
if (err) {
|
|
508
|
+
console.error("[EXULU] Error setting connection parameters:", err);
|
|
509
|
+
}
|
|
510
|
+
done(err, conn);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
try {
|
|
516
|
+
await knex.schema.createExtensionIfNotExists("vector");
|
|
517
|
+
} catch (error) {
|
|
518
|
+
console.error(
|
|
519
|
+
"[EXULU] Error creating vector extension, this might be fine if you already activated the extension and the 'user' running this script does not have higher level database permissions.",
|
|
520
|
+
error
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
db["exulu"] = knex;
|
|
524
|
+
} catch (error) {
|
|
525
|
+
console.error("[EXULU] Error initializing exulu database.", error);
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
db: db["exulu"]
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
619
534
|
// src/exulu/litellm/env.ts
|
|
620
535
|
var LiteLLMAdminError = class extends Error {
|
|
621
536
|
constructor(message, status) {
|
|
@@ -635,9 +550,9 @@ function litellmBase() {
|
|
|
635
550
|
}
|
|
636
551
|
|
|
637
552
|
// src/exulu/litellm/admin-client.ts
|
|
638
|
-
async function call(
|
|
553
|
+
async function call(path3, body) {
|
|
639
554
|
const { url, masterKey } = litellmBase();
|
|
640
|
-
const res = await fetch(`${url}${
|
|
555
|
+
const res = await fetch(`${url}${path3}`, {
|
|
641
556
|
method: "POST",
|
|
642
557
|
headers: {
|
|
643
558
|
Authorization: `Bearer ${masterKey}`,
|
|
@@ -648,7 +563,7 @@ async function call(path, body) {
|
|
|
648
563
|
if (!res.ok) {
|
|
649
564
|
const text = await res.text().catch(() => "");
|
|
650
565
|
throw new LiteLLMAdminError(
|
|
651
|
-
`LiteLLM ${
|
|
566
|
+
`LiteLLM ${path3} returned ${res.status}: ${text}`,
|
|
652
567
|
res.status
|
|
653
568
|
);
|
|
654
569
|
}
|
|
@@ -1112,21 +1027,6 @@ async function getUserBudgetView(userId) {
|
|
|
1112
1027
|
}
|
|
1113
1028
|
|
|
1114
1029
|
// src/exulu/resolve-model.ts
|
|
1115
|
-
var LITELLM_PROVIDER_SENTINEL = new Proxy(
|
|
1116
|
-
{},
|
|
1117
|
-
{
|
|
1118
|
-
get(_target, prop) {
|
|
1119
|
-
if (prop === "id") return "litellm";
|
|
1120
|
-
if (prop === Symbol.toPrimitive || prop === "toString") {
|
|
1121
|
-
return () => "[LiteLLMProviderSentinel]";
|
|
1122
|
-
}
|
|
1123
|
-
console.error(`ExuluProvider.${String(prop)} is not available in LiteLLM mode. `, new Error().stack);
|
|
1124
|
-
throw new Error(
|
|
1125
|
-
`ExuluProvider.${String(prop)} is not available in LiteLLM mode. Code paths that depend on the in-code provider catalog must check isLiteLLMEnabled() and degrade.`
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
);
|
|
1130
1030
|
var ResolveModelError = class extends Error {
|
|
1131
1031
|
constructor(code, message) {
|
|
1132
1032
|
super(message);
|
|
@@ -1188,166 +1088,171 @@ var getLiteLLMProvider = ({
|
|
|
1188
1088
|
});
|
|
1189
1089
|
};
|
|
1190
1090
|
async function resolveModel(input) {
|
|
1191
|
-
const { modelId, user,
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
try {
|
|
1195
|
-
await waitForLiteLLMReady();
|
|
1196
|
-
} catch (err) {
|
|
1197
|
-
throw new ResolveModelError(
|
|
1198
|
-
"LITELLM_NOT_READY",
|
|
1199
|
-
`LiteLLM is not ready: ${err.message}`
|
|
1200
|
-
);
|
|
1201
|
-
}
|
|
1202
|
-
if (user?.id) await provisionDefaultUserBudget(user.id);
|
|
1203
|
-
const litellm = getLiteLLMProvider({
|
|
1204
|
-
user,
|
|
1205
|
-
role: user?.role,
|
|
1206
|
-
// Fall back to the caller's own project (set on API keys) when no
|
|
1207
|
-
// explicit request project is supplied, so API-triggered requests are
|
|
1208
|
-
// attributed to the key's project.
|
|
1209
|
-
project: project ?? user?.project,
|
|
1210
|
-
agent,
|
|
1211
|
-
team: user?.team,
|
|
1212
|
-
routine
|
|
1213
|
-
});
|
|
1214
|
-
const languageModel2 = litellm(modelId);
|
|
1215
|
-
const syntheticModel = {
|
|
1216
|
-
id: modelId,
|
|
1217
|
-
name: modelId,
|
|
1218
|
-
provider: modelId,
|
|
1219
|
-
active: true,
|
|
1220
|
-
rights_mode: "public",
|
|
1221
|
-
created_by: "litellm"
|
|
1222
|
-
};
|
|
1223
|
-
return {
|
|
1224
|
-
languageModel: languageModel2,
|
|
1225
|
-
model: syntheticModel,
|
|
1226
|
-
exuluProvider: LITELLM_PROVIDER_SENTINEL,
|
|
1227
|
-
apiKey: void 0
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
const { db: db2 } = await postgresClient();
|
|
1231
|
-
const model = await db2.from("models").where({ id: modelId }).first();
|
|
1232
|
-
if (!model) {
|
|
1233
|
-
throw new ResolveModelError("MODEL_NOT_FOUND", `Model ${modelId} not found`);
|
|
1234
|
-
}
|
|
1235
|
-
if (!model.active) {
|
|
1236
|
-
throw new ResolveModelError("MODEL_INACTIVE", `Model ${model.name} is inactive`);
|
|
1237
|
-
}
|
|
1238
|
-
if (!rbacBypass) {
|
|
1239
|
-
const ok = await checkRecordAccess(model, rbacRequest, user);
|
|
1240
|
-
if (!ok) {
|
|
1241
|
-
throw new ResolveModelError(
|
|
1242
|
-
"MODEL_FORBIDDEN",
|
|
1243
|
-
`No ${rbacRequest} access to model ${model.name}`
|
|
1244
|
-
);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
const exuluProvider = providers.find((p) => p.id === model.provider);
|
|
1248
|
-
if (!exuluProvider) {
|
|
1249
|
-
throw new ResolveModelError(
|
|
1250
|
-
"PROVIDER_NOT_FOUND",
|
|
1251
|
-
`ExuluProvider ${model.provider} (referenced by model ${model.name}) not registered in this instance`
|
|
1252
|
-
);
|
|
1091
|
+
const { modelId, user, agent, project, routine } = input;
|
|
1092
|
+
if (!isLiteLLMEnabled()) {
|
|
1093
|
+
throw new Error("Litellm not configured or available.");
|
|
1253
1094
|
}
|
|
1254
|
-
|
|
1095
|
+
try {
|
|
1096
|
+
await waitForLiteLLMReady();
|
|
1097
|
+
} catch (err) {
|
|
1255
1098
|
throw new ResolveModelError(
|
|
1256
|
-
"
|
|
1257
|
-
`
|
|
1099
|
+
"LITELLM_NOT_READY",
|
|
1100
|
+
`LiteLLM is not ready: ${err.message}`
|
|
1258
1101
|
);
|
|
1259
1102
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
"AUTH_VAR_NOT_ENCRYPTED",
|
|
1272
|
-
`Auth variable ${model.authvariable} must be encrypted`
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
const bytes = CryptoJS.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
1276
|
-
apiKey = bytes.toString(CryptoJS.enc.Utf8);
|
|
1277
|
-
}
|
|
1278
|
-
const languageModel = exuluProvider.config.model.create({
|
|
1279
|
-
apiKey,
|
|
1280
|
-
user: user?.id,
|
|
1281
|
-
role: user?.role?.id,
|
|
1282
|
-
project: project?.id,
|
|
1283
|
-
agent: agent?.id
|
|
1103
|
+
if (user?.id) await provisionDefaultUserBudget(user.id);
|
|
1104
|
+
const litellm = getLiteLLMProvider({
|
|
1105
|
+
user,
|
|
1106
|
+
role: user?.role,
|
|
1107
|
+
// Fall back to the caller's own project (set on API keys) when no
|
|
1108
|
+
// explicit request project is supplied, so API-triggered requests are
|
|
1109
|
+
// attributed to the key's project.
|
|
1110
|
+
project: project ?? user?.project,
|
|
1111
|
+
agent,
|
|
1112
|
+
team: user?.team,
|
|
1113
|
+
routine
|
|
1284
1114
|
});
|
|
1285
|
-
|
|
1115
|
+
const languageModel = litellm(modelId);
|
|
1116
|
+
const syntheticModel = {
|
|
1117
|
+
id: modelId,
|
|
1118
|
+
name: modelId,
|
|
1119
|
+
provider: modelId,
|
|
1120
|
+
active: true,
|
|
1121
|
+
rights_mode: "public",
|
|
1122
|
+
created_by: "litellm"
|
|
1123
|
+
};
|
|
1124
|
+
return {
|
|
1125
|
+
languageModel,
|
|
1126
|
+
model: syntheticModel
|
|
1127
|
+
};
|
|
1286
1128
|
}
|
|
1287
1129
|
|
|
1288
|
-
// src/exulu/
|
|
1289
|
-
var
|
|
1130
|
+
// src/exulu/auth/validate.ts
|
|
1131
|
+
var OAUTH_REQUIRED_STRING_FIELDS = [
|
|
1290
1132
|
"authorizationUrl",
|
|
1291
1133
|
"tokenUrl",
|
|
1292
1134
|
"clientId",
|
|
1293
1135
|
"clientSecret"
|
|
1294
1136
|
];
|
|
1295
|
-
var
|
|
1296
|
-
|
|
1297
|
-
|
|
1137
|
+
var validateAuthConfig = (toolId, config) => {
|
|
1138
|
+
if (config.authType === "oauth") {
|
|
1139
|
+
for (const field of OAUTH_REQUIRED_STRING_FIELDS) {
|
|
1140
|
+
const value = config[field];
|
|
1141
|
+
if (!value || typeof value !== "string") {
|
|
1142
|
+
throw new Error(
|
|
1143
|
+
`ExuluTool "${toolId}": oauth.${field} is required and must be a non-empty string.`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (!Array.isArray(config.scopes)) {
|
|
1298
1148
|
throw new Error(
|
|
1299
|
-
`ExuluTool "${toolId}": oauth
|
|
1149
|
+
`ExuluTool "${toolId}": oauth.scopes must be an array of strings (use [] to request no scopes).`
|
|
1300
1150
|
);
|
|
1301
1151
|
}
|
|
1152
|
+
if (config.provider !== void 0) {
|
|
1153
|
+
if (typeof config.provider !== "string" || config.provider.length === 0 || config.provider.trim() !== config.provider) {
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
`ExuluTool "${toolId}": oauth.provider must be a non-empty string with no leading or trailing whitespace when set.`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (!process.env.BACKEND) {
|
|
1160
|
+
throw new Error(
|
|
1161
|
+
`ExuluTool "${toolId}": oauth requires the BACKEND environment variable (the backend's public base URL) to build the redirect URI.`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
return;
|
|
1302
1165
|
}
|
|
1303
|
-
if (
|
|
1304
|
-
throw new Error(
|
|
1305
|
-
`ExuluTool "${toolId}": oauth.scopes must be an array of strings (use [] to request no scopes).`
|
|
1306
|
-
);
|
|
1307
|
-
}
|
|
1308
|
-
if (config.provider !== void 0) {
|
|
1166
|
+
if (config.authType === "user_credentials") {
|
|
1309
1167
|
if (typeof config.provider !== "string" || config.provider.length === 0 || config.provider.trim() !== config.provider) {
|
|
1310
1168
|
throw new Error(
|
|
1311
|
-
`ExuluTool "${toolId}":
|
|
1169
|
+
`ExuluTool "${toolId}": user_credentials.provider must be a non-empty string with no leading or trailing whitespace.`
|
|
1312
1170
|
);
|
|
1313
1171
|
}
|
|
1172
|
+
if (!Array.isArray(config.fields) || config.fields.length === 0) {
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`ExuluTool "${toolId}": user_credentials.fields must contain at least one field.`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1178
|
+
for (let i = 0; i < config.fields.length; i++) {
|
|
1179
|
+
const field = config.fields[i];
|
|
1180
|
+
const name = field.name;
|
|
1181
|
+
if (typeof name !== "string" || name.length === 0 || name.trim() !== name) {
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
`ExuluTool "${toolId}": user_credentials.fields[${i}].name must be a non-empty string with no leading or trailing whitespace.`
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
if (seenNames.has(name)) {
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
`ExuluTool "${toolId}": user_credentials.fields has duplicate field name '${name}'.`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
seenNames.add(name);
|
|
1192
|
+
const type = field.type;
|
|
1193
|
+
if (type !== "text" && type !== "password") {
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`ExuluTool "${toolId}": user_credentials.fields[${i}].type must be 'text' or 'password' (got '${type}').`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (!process.env.BACKEND) {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`ExuluTool "${toolId}": user_credentials requires the BACKEND environment variable (the backend's public base URL) to build the credential submit URL.`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
return;
|
|
1314
1205
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
);
|
|
1319
|
-
}
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`ExuluTool "${toolId}": auth.authType '${config.authType}' is not supported.`
|
|
1208
|
+
);
|
|
1320
1209
|
};
|
|
1321
1210
|
|
|
1322
|
-
// src/exulu/
|
|
1211
|
+
// src/exulu/auth/provider-key.ts
|
|
1323
1212
|
var providerKeyFor = (toolId, config) => config.provider && config.provider.length > 0 ? config.provider : toolId;
|
|
1324
1213
|
|
|
1325
|
-
// src/exulu/
|
|
1214
|
+
// src/exulu/auth/registry.ts
|
|
1326
1215
|
var byProvider = /* @__PURE__ */ new Map();
|
|
1327
1216
|
var byTool = /* @__PURE__ */ new Map();
|
|
1328
|
-
var
|
|
1217
|
+
var STABLE_OAUTH_STRING_FIELDS = [
|
|
1329
1218
|
"authorizationUrl",
|
|
1330
1219
|
"tokenUrl",
|
|
1331
1220
|
"clientId",
|
|
1332
1221
|
"clientSecret"
|
|
1333
1222
|
];
|
|
1334
1223
|
var assertCompatible = (providerKey, toolId, existing, next) => {
|
|
1335
|
-
|
|
1336
|
-
|
|
1224
|
+
if (existing.authType !== next.authType) {
|
|
1225
|
+
throw new Error(
|
|
1226
|
+
`ExuluTool "${toolId}": auth.authType '${next.authType}' disagrees with another tool that shares provider "${providerKey}" using authType '${existing.authType}'.`
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
if (existing.authType === "oauth" && next.authType === "oauth") {
|
|
1230
|
+
for (const field of STABLE_OAUTH_STRING_FIELDS) {
|
|
1231
|
+
if (existing[field] !== next[field]) {
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
`ExuluTool "${toolId}": oauth.${field} disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must use identical authorizationUrl/tokenUrl/clientId/clientSecret.`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const a = new Set(existing.scopes);
|
|
1238
|
+
const b = new Set(next.scopes);
|
|
1239
|
+
if (a.size !== b.size || [...a].some((s) => !b.has(s))) {
|
|
1337
1240
|
throw new Error(
|
|
1338
|
-
`ExuluTool "${toolId}": oauth
|
|
1241
|
+
`ExuluTool "${toolId}": oauth.scopes disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must declare the same scope superset. Existing: [${[...a].sort().join(", ")}]. This tool: [${[...b].sort().join(", ")}].`
|
|
1339
1242
|
);
|
|
1340
1243
|
}
|
|
1244
|
+
return;
|
|
1341
1245
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1246
|
+
if (existing.authType === "user_credentials" && next.authType === "user_credentials") {
|
|
1247
|
+
if (JSON.stringify(existing.fields) !== JSON.stringify(next.fields)) {
|
|
1248
|
+
throw new Error(
|
|
1249
|
+
`ExuluTool "${toolId}": user_credentials.fields disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must declare structurally identical fields (same names, types, and order).`
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1348
1253
|
}
|
|
1349
1254
|
};
|
|
1350
|
-
var
|
|
1255
|
+
var authRegistry = {
|
|
1351
1256
|
register: (toolId, config) => {
|
|
1352
1257
|
const providerKey = providerKeyFor(toolId, config);
|
|
1353
1258
|
const existing = byProvider.get(providerKey);
|
|
@@ -1367,60 +1272,74 @@ var oauthRegistry = {
|
|
|
1367
1272
|
}
|
|
1368
1273
|
};
|
|
1369
1274
|
|
|
1370
|
-
// src/exulu/
|
|
1371
|
-
import
|
|
1275
|
+
// src/exulu/auth/flow.ts
|
|
1276
|
+
import CryptoJS2 from "crypto-js";
|
|
1372
1277
|
import { createHash, randomBytes } from "crypto";
|
|
1373
1278
|
|
|
1374
|
-
// src/exulu/
|
|
1375
|
-
import
|
|
1376
|
-
var TABLE = "
|
|
1377
|
-
var encrypt = (value) =>
|
|
1378
|
-
var decrypt = (value) =>
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
return null;
|
|
1385
|
-
}
|
|
1386
|
-
return {
|
|
1387
|
-
accessToken: decrypt(row.access_token),
|
|
1388
|
-
refreshToken: row.refresh_token ? decrypt(row.refresh_token) : null,
|
|
1389
|
-
tokenType: row.token_type ?? null,
|
|
1390
|
-
scopes: row.scopes ?? null,
|
|
1391
|
-
expiresAt: row.expires_at ? new Date(row.expires_at) : null
|
|
1392
|
-
};
|
|
1393
|
-
},
|
|
1394
|
-
// toolId is stored on every written row as an audit trail — which tool
|
|
1395
|
-
// triggered the last grant/refresh — but does NOT participate in the key.
|
|
1396
|
-
upsert: async (providerKey, userId, toolId, record) => {
|
|
1397
|
-
const { db: db2 } = await postgresClient();
|
|
1398
|
-
const existing = await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).first();
|
|
1399
|
-
const values = {
|
|
1400
|
-
provider: providerKey,
|
|
1401
|
-
tool_id: toolId,
|
|
1402
|
-
access_token: encrypt(record.accessToken),
|
|
1403
|
-
// Providers like Google only send a refresh_token on first consent;
|
|
1404
|
-
// never overwrite a stored one with nothing.
|
|
1405
|
-
refresh_token: record.refreshToken ? encrypt(record.refreshToken) : existing?.refresh_token ?? null,
|
|
1406
|
-
token_type: record.tokenType ?? null,
|
|
1407
|
-
scopes: record.scopes ?? null,
|
|
1408
|
-
expires_at: record.expiresAt ?? null,
|
|
1409
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1410
|
-
};
|
|
1411
|
-
if (existing) {
|
|
1412
|
-
await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).update(values);
|
|
1413
|
-
} else {
|
|
1414
|
-
await db2.from(TABLE).insert({ user_id: userId, ...values });
|
|
1415
|
-
}
|
|
1416
|
-
},
|
|
1417
|
-
delete: async (providerKey, userId) => {
|
|
1418
|
-
const { db: db2 } = await postgresClient();
|
|
1419
|
-
await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).del();
|
|
1279
|
+
// src/exulu/auth/credential-store.ts
|
|
1280
|
+
import CryptoJS from "crypto-js";
|
|
1281
|
+
var TABLE = "user_credentials";
|
|
1282
|
+
var encrypt = (value) => CryptoJS.AES.encrypt(value, process.env.NEXTAUTH_SECRET).toString();
|
|
1283
|
+
var decrypt = (value) => CryptoJS.AES.decrypt(value, process.env.NEXTAUTH_SECRET).toString(CryptoJS.enc.Utf8);
|
|
1284
|
+
async function get(provider, userId) {
|
|
1285
|
+
const { db: db2 } = await postgresClient();
|
|
1286
|
+
const row = await db2.from(TABLE).where({ provider, user_id: String(userId) }).first();
|
|
1287
|
+
if (!row) {
|
|
1288
|
+
return null;
|
|
1420
1289
|
}
|
|
1421
|
-
|
|
1290
|
+
return {
|
|
1291
|
+
provider,
|
|
1292
|
+
userId,
|
|
1293
|
+
authType: row.auth_type,
|
|
1294
|
+
data: JSON.parse(decrypt(row.data))
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
async function upsert(record) {
|
|
1298
|
+
const { db: db2 } = await postgresClient();
|
|
1299
|
+
const encrypted = encrypt(JSON.stringify(record.data));
|
|
1300
|
+
await db2.from(TABLE).insert({
|
|
1301
|
+
provider: record.provider,
|
|
1302
|
+
user_id: String(record.userId),
|
|
1303
|
+
auth_type: record.authType,
|
|
1304
|
+
data: encrypted,
|
|
1305
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
1306
|
+
}).onConflict(["provider", "user_id"]).merge({ auth_type: record.authType, data: encrypted, updated_at: /* @__PURE__ */ new Date() });
|
|
1307
|
+
}
|
|
1308
|
+
async function listByUser(userId) {
|
|
1309
|
+
const { db: db2 } = await postgresClient();
|
|
1310
|
+
const list = await db2.from(TABLE).where({ user_id: String(userId) }).orderBy("provider");
|
|
1311
|
+
return list.map((row) => ({
|
|
1312
|
+
provider: row.provider,
|
|
1313
|
+
authType: row.auth_type,
|
|
1314
|
+
createdAt: row.created_at,
|
|
1315
|
+
updatedAt: row.updated_at
|
|
1316
|
+
}));
|
|
1317
|
+
}
|
|
1318
|
+
async function del(provider, userId) {
|
|
1319
|
+
const { db: db2 } = await postgresClient();
|
|
1320
|
+
await db2.from(TABLE).where({ provider, user_id: String(userId) }).del();
|
|
1321
|
+
}
|
|
1322
|
+
var credentialStore = { get, upsert, listByUser, delete: del };
|
|
1422
1323
|
|
|
1423
|
-
// src/exulu/
|
|
1324
|
+
// src/exulu/auth/flow.ts
|
|
1325
|
+
function oauthRecordToBlob(r) {
|
|
1326
|
+
return {
|
|
1327
|
+
accessToken: r.accessToken,
|
|
1328
|
+
refreshToken: r.refreshToken ?? null,
|
|
1329
|
+
tokenType: r.tokenType ?? null,
|
|
1330
|
+
scopes: r.scopes ?? null,
|
|
1331
|
+
expiresAt: r.expiresAt ? r.expiresAt.toISOString() : null
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function oauthBlobToRecord(b) {
|
|
1335
|
+
return {
|
|
1336
|
+
accessToken: b.accessToken,
|
|
1337
|
+
refreshToken: b.refreshToken,
|
|
1338
|
+
tokenType: b.tokenType,
|
|
1339
|
+
scopes: b.scopes,
|
|
1340
|
+
expiresAt: b.expiresAt ? new Date(b.expiresAt) : null
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1424
1343
|
var OAUTH_CALLBACK_PATH = "/oauth/callback";
|
|
1425
1344
|
var STATE_TTL_MS = 10 * 60 * 1e3;
|
|
1426
1345
|
var EXPIRY_SKEW_MS = 30 * 1e3;
|
|
@@ -1440,12 +1359,12 @@ var fromBase64Url = (value) => {
|
|
|
1440
1359
|
}
|
|
1441
1360
|
return base64;
|
|
1442
1361
|
};
|
|
1443
|
-
var encryptOauthState = (state) => toBase64Url(
|
|
1362
|
+
var encryptOauthState = (state) => toBase64Url(CryptoJS2.AES.encrypt(JSON.stringify(state), process.env.NEXTAUTH_SECRET).toString());
|
|
1444
1363
|
var decryptOauthState = (value) => {
|
|
1445
1364
|
let json = "";
|
|
1446
1365
|
try {
|
|
1447
|
-
json =
|
|
1448
|
-
|
|
1366
|
+
json = CryptoJS2.AES.decrypt(fromBase64Url(value), process.env.NEXTAUTH_SECRET).toString(
|
|
1367
|
+
CryptoJS2.enc.Utf8
|
|
1449
1368
|
);
|
|
1450
1369
|
} catch {
|
|
1451
1370
|
throw new Error("[EXULU] Invalid OAuth state.");
|
|
@@ -1563,7 +1482,8 @@ var getValidAccessToken = async ({
|
|
|
1563
1482
|
toolId,
|
|
1564
1483
|
config
|
|
1565
1484
|
}) => {
|
|
1566
|
-
const
|
|
1485
|
+
const credRow = await credentialStore.get(providerKey, userId);
|
|
1486
|
+
const stored = credRow ? oauthBlobToRecord(credRow.data) : null;
|
|
1567
1487
|
if (!stored) {
|
|
1568
1488
|
return null;
|
|
1569
1489
|
}
|
|
@@ -1572,7 +1492,7 @@ var getValidAccessToken = async ({
|
|
|
1572
1492
|
return stored;
|
|
1573
1493
|
}
|
|
1574
1494
|
if (!stored.refreshToken) {
|
|
1575
|
-
await
|
|
1495
|
+
await credentialStore.delete(providerKey, userId);
|
|
1576
1496
|
return null;
|
|
1577
1497
|
}
|
|
1578
1498
|
try {
|
|
@@ -1580,20 +1500,127 @@ var getValidAccessToken = async ({
|
|
|
1580
1500
|
if (!refreshed.refreshToken) {
|
|
1581
1501
|
refreshed.refreshToken = stored.refreshToken;
|
|
1582
1502
|
}
|
|
1583
|
-
await
|
|
1503
|
+
await credentialStore.upsert({
|
|
1504
|
+
provider: providerKey,
|
|
1505
|
+
userId,
|
|
1506
|
+
authType: "oauth",
|
|
1507
|
+
data: oauthRecordToBlob(refreshed)
|
|
1508
|
+
});
|
|
1584
1509
|
return refreshed;
|
|
1585
1510
|
} catch (error) {
|
|
1586
1511
|
console.error(
|
|
1587
1512
|
`[EXULU] OAuth token refresh failed for provider "${providerKey}" tool "${toolId}" user ${userId}:`,
|
|
1588
1513
|
error
|
|
1589
1514
|
);
|
|
1590
|
-
await
|
|
1515
|
+
await credentialStore.delete(providerKey, userId);
|
|
1591
1516
|
return null;
|
|
1592
1517
|
}
|
|
1593
1518
|
};
|
|
1594
1519
|
|
|
1595
|
-
// src/exulu/
|
|
1596
|
-
|
|
1520
|
+
// src/exulu/auth/state.ts
|
|
1521
|
+
async function getValidUserCredentials(cfg, userId) {
|
|
1522
|
+
const row = await credentialStore.get(cfg.provider, userId);
|
|
1523
|
+
if (!row || row.authType !== "user_credentials") return null;
|
|
1524
|
+
const values = {};
|
|
1525
|
+
for (const [k, v] of Object.entries(row.data)) {
|
|
1526
|
+
if (typeof v !== "string") return null;
|
|
1527
|
+
values[k] = v;
|
|
1528
|
+
}
|
|
1529
|
+
return values;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// src/exulu/auth/credentials-request.ts
|
|
1533
|
+
var DEFAULT_TTL_SECONDS = 15 * 60;
|
|
1534
|
+
function buildCredentialRequest(cfg, opts) {
|
|
1535
|
+
const ttl = opts.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
1536
|
+
const claims = {
|
|
1537
|
+
provider: cfg.provider,
|
|
1538
|
+
userId: opts.userId,
|
|
1539
|
+
expiresAt: Math.floor(Date.now() / 1e3) + ttl
|
|
1540
|
+
};
|
|
1541
|
+
const nonce = encrypt(JSON.stringify(claims));
|
|
1542
|
+
return {
|
|
1543
|
+
provider: cfg.provider,
|
|
1544
|
+
fields: cfg.fields,
|
|
1545
|
+
submitUrl: `${opts.baseUrl.replace(/\/+$/, "")}/credentials/submit`,
|
|
1546
|
+
nonce
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
function verifyCredentialNonce(nonce) {
|
|
1550
|
+
let claims;
|
|
1551
|
+
try {
|
|
1552
|
+
claims = JSON.parse(decrypt(nonce));
|
|
1553
|
+
} catch {
|
|
1554
|
+
throw new Error("Invalid credential nonce");
|
|
1555
|
+
}
|
|
1556
|
+
if (typeof claims.provider !== "string" || typeof claims.userId !== "string" || typeof claims.expiresAt !== "number") {
|
|
1557
|
+
throw new Error("Malformed credential nonce claims");
|
|
1558
|
+
}
|
|
1559
|
+
if (claims.expiresAt < Math.floor(Date.now() / 1e3)) {
|
|
1560
|
+
throw new Error("Credential nonce expired");
|
|
1561
|
+
}
|
|
1562
|
+
return claims;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// src/exulu/auth/short-circuit.ts
|
|
1566
|
+
function credentialRequestResult(request) {
|
|
1567
|
+
return { credentialRequest: request, result: null };
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// src/exulu/auth/errors.ts
|
|
1571
|
+
var CredentialInvalidError = class extends Error {
|
|
1572
|
+
provider;
|
|
1573
|
+
reason;
|
|
1574
|
+
constructor(provider, reason) {
|
|
1575
|
+
super(reason ? `Credential invalid for provider '${provider}': ${reason}` : `Credential invalid for provider '${provider}'`);
|
|
1576
|
+
this.name = "CredentialInvalidError";
|
|
1577
|
+
this.provider = provider;
|
|
1578
|
+
this.reason = reason;
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1582
|
+
// src/exulu/auth/wrap-execute.ts
|
|
1583
|
+
var wrapExecuteWithAuth = (toolId, config, execute) => {
|
|
1584
|
+
if (config.authType === "oauth") {
|
|
1585
|
+
return wrapExecuteWithOauthInternal(toolId, config, execute);
|
|
1586
|
+
}
|
|
1587
|
+
if (config.authType === "user_credentials") {
|
|
1588
|
+
return wrapUserCredentials(toolId, config, execute);
|
|
1589
|
+
}
|
|
1590
|
+
throw new Error(`ExuluTool "${toolId}": unknown authType`);
|
|
1591
|
+
};
|
|
1592
|
+
var wrapUserCredentials = (toolId, config, execute) => {
|
|
1593
|
+
return async (inputs, options) => {
|
|
1594
|
+
const userId = inputs?.user?.id;
|
|
1595
|
+
if (!userId) {
|
|
1596
|
+
return {
|
|
1597
|
+
result: `The "${toolId}" tool requires user-supplied credentials, which needs a signed-in user. No user identity is available for this run.`
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
const baseUrl = (process.env.BACKEND ?? "").replace(/\/+$/, "");
|
|
1601
|
+
if (!baseUrl) {
|
|
1602
|
+
return {
|
|
1603
|
+
result: `The "${toolId}" tool requires the BACKEND env var to build the credential submit URL.`
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
const values = await getValidUserCredentials(config, userId);
|
|
1607
|
+
if (!values) {
|
|
1608
|
+
const request = buildCredentialRequest(config, { baseUrl, userId: String(userId) });
|
|
1609
|
+
return credentialRequestResult(request);
|
|
1610
|
+
}
|
|
1611
|
+
try {
|
|
1612
|
+
return await execute({ ...inputs, credentials: values }, options);
|
|
1613
|
+
} catch (e) {
|
|
1614
|
+
if (e instanceof CredentialInvalidError && e.provider === config.provider) {
|
|
1615
|
+
await credentialStore.delete(config.provider, userId);
|
|
1616
|
+
const request = buildCredentialRequest(config, { baseUrl, userId: String(userId) });
|
|
1617
|
+
return credentialRequestResult(request);
|
|
1618
|
+
}
|
|
1619
|
+
throw e;
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
};
|
|
1623
|
+
var wrapExecuteWithOauthInternal = (toolId, config, execute) => {
|
|
1597
1624
|
return async (inputs, options) => {
|
|
1598
1625
|
const userId = inputs?.user?.id;
|
|
1599
1626
|
if (!userId) {
|
|
@@ -1633,7 +1660,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1633
1660
|
type;
|
|
1634
1661
|
tool;
|
|
1635
1662
|
needsApproval;
|
|
1636
|
-
|
|
1663
|
+
authentication;
|
|
1637
1664
|
config;
|
|
1638
1665
|
constructor({
|
|
1639
1666
|
id,
|
|
@@ -1645,7 +1672,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1645
1672
|
execute,
|
|
1646
1673
|
config,
|
|
1647
1674
|
needsApproval,
|
|
1648
|
-
|
|
1675
|
+
authentication: authentication2
|
|
1649
1676
|
}) {
|
|
1650
1677
|
if (!PUBLIC_TOOL_TYPES.includes(type)) {
|
|
1651
1678
|
throw new Error(
|
|
@@ -1654,11 +1681,11 @@ var ExuluTool = class _ExuluTool {
|
|
|
1654
1681
|
)}. The "agent" and "context" types are managed by Exulu internally and cannot be set on a tool.`
|
|
1655
1682
|
);
|
|
1656
1683
|
}
|
|
1657
|
-
if (
|
|
1658
|
-
|
|
1659
|
-
|
|
1684
|
+
if (authentication2) {
|
|
1685
|
+
validateAuthConfig(id, authentication2);
|
|
1686
|
+
authRegistry.register(id, authentication2);
|
|
1660
1687
|
}
|
|
1661
|
-
this.
|
|
1688
|
+
this.authentication = authentication2;
|
|
1662
1689
|
this.id = id;
|
|
1663
1690
|
this.config = config;
|
|
1664
1691
|
this.needsApproval = needsApproval ?? true;
|
|
@@ -1670,7 +1697,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1670
1697
|
this.tool = tool({
|
|
1671
1698
|
description,
|
|
1672
1699
|
inputSchema: inputSchema || z.object({}),
|
|
1673
|
-
execute:
|
|
1700
|
+
execute: authentication2 ? wrapExecuteWithAuth(id, authentication2, execute) : execute
|
|
1674
1701
|
});
|
|
1675
1702
|
}
|
|
1676
1703
|
/**
|
|
@@ -1706,26 +1733,13 @@ var ExuluTool = class _ExuluTool {
|
|
|
1706
1733
|
if (!agent) {
|
|
1707
1734
|
throw new Error("Agent not found.");
|
|
1708
1735
|
}
|
|
1709
|
-
|
|
1710
|
-
if (agent.model) {
|
|
1711
|
-
const providers = exuluApp.get().providers;
|
|
1712
|
-
const resolved = await resolveModel({
|
|
1713
|
-
modelId: agent.model,
|
|
1714
|
-
user,
|
|
1715
|
-
providers,
|
|
1716
|
-
agent,
|
|
1717
|
-
rbacBypass: true
|
|
1718
|
-
});
|
|
1719
|
-
providerapikey = resolved.apiKey;
|
|
1720
|
-
}
|
|
1721
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-YY2WIMMJ.js");
|
|
1736
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-QG7E6UX5.js");
|
|
1722
1737
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
1723
1738
|
[this],
|
|
1724
1739
|
[],
|
|
1725
1740
|
[],
|
|
1726
1741
|
[],
|
|
1727
1742
|
agent.tools,
|
|
1728
|
-
providerapikey,
|
|
1729
1743
|
void 0,
|
|
1730
1744
|
user,
|
|
1731
1745
|
config,
|
|
@@ -1799,7 +1813,7 @@ var updateStatistic = async (statistic) => {
|
|
|
1799
1813
|
};
|
|
1800
1814
|
|
|
1801
1815
|
// src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
|
|
1802
|
-
import
|
|
1816
|
+
import CryptoJS4 from "crypto-js";
|
|
1803
1817
|
|
|
1804
1818
|
// src/templates/tools/session-items-retrieval-tool.ts
|
|
1805
1819
|
import { z as z2 } from "zod";
|
|
@@ -2230,11 +2244,14 @@ function buildProjectKbProfileDefaults(items) {
|
|
|
2230
2244
|
}
|
|
2231
2245
|
|
|
2232
2246
|
// ee/agentic-retrieval/pipeline/routing.ts
|
|
2233
|
-
import { generateText as generateText2, Output as Output2 } from "ai";
|
|
2234
2247
|
import { z as z5 } from "zod";
|
|
2235
2248
|
|
|
2249
|
+
// ee/agentic-retrieval/pipeline/micro-call.ts
|
|
2250
|
+
import { generateText, NoOutputGeneratedError, Output } from "ai";
|
|
2251
|
+
|
|
2236
2252
|
// src/utils/with-retry.ts
|
|
2237
|
-
async function withRetry(generateFn, maxRetries = 3) {
|
|
2253
|
+
async function withRetry(generateFn, maxRetries = 3, opts = {}) {
|
|
2254
|
+
const { shouldRetry, baseDelayMs = 1e3 } = opts;
|
|
2238
2255
|
let lastError;
|
|
2239
2256
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2240
2257
|
try {
|
|
@@ -2242,18 +2259,66 @@ async function withRetry(generateFn, maxRetries = 3) {
|
|
|
2242
2259
|
} catch (error) {
|
|
2243
2260
|
lastError = error;
|
|
2244
2261
|
console.error(`[EXULU] generateText attempt ${attempt} failed:`, error);
|
|
2245
|
-
if (attempt === maxRetries) {
|
|
2262
|
+
if (attempt === maxRetries || shouldRetry && !shouldRetry(error)) {
|
|
2246
2263
|
throw error;
|
|
2247
2264
|
}
|
|
2248
|
-
await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) *
|
|
2265
|
+
await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) * baseDelayMs));
|
|
2249
2266
|
}
|
|
2250
2267
|
}
|
|
2251
2268
|
throw lastError;
|
|
2252
2269
|
}
|
|
2253
2270
|
|
|
2271
|
+
// ee/agentic-retrieval/pipeline/micro-call.ts
|
|
2272
|
+
var MICRO_CALL_MAX_OUTPUT_TOKENS = 2e3;
|
|
2273
|
+
function microCallProviderOptions(model) {
|
|
2274
|
+
const modelId = typeof model === "string" ? model : model?.modelId;
|
|
2275
|
+
return typeof modelId === "string" && /gemini/i.test(modelId) ? { litellm: { reasoningEffort: "disable" } } : void 0;
|
|
2276
|
+
}
|
|
2277
|
+
async function microCall(args) {
|
|
2278
|
+
const {
|
|
2279
|
+
model,
|
|
2280
|
+
system,
|
|
2281
|
+
prompt,
|
|
2282
|
+
messages,
|
|
2283
|
+
schema,
|
|
2284
|
+
temperature = 0,
|
|
2285
|
+
maxOutputTokens = MICRO_CALL_MAX_OUTPUT_TOKENS,
|
|
2286
|
+
maxAttempts = 3,
|
|
2287
|
+
retryBaseDelayMs
|
|
2288
|
+
} = args;
|
|
2289
|
+
return withRetry(
|
|
2290
|
+
async () => {
|
|
2291
|
+
const result = await generateText({
|
|
2292
|
+
model,
|
|
2293
|
+
temperature,
|
|
2294
|
+
system,
|
|
2295
|
+
prompt,
|
|
2296
|
+
messages,
|
|
2297
|
+
...schema ? { output: Output.object({ schema }) } : {},
|
|
2298
|
+
maxOutputTokens,
|
|
2299
|
+
// withRetry owns retries. The SDK's internal retries on top of it
|
|
2300
|
+
// tripled request volume per attempt while the provider was already
|
|
2301
|
+
// rate-limiting.
|
|
2302
|
+
maxRetries: 0,
|
|
2303
|
+
providerOptions: microCallProviderOptions(model)
|
|
2304
|
+
});
|
|
2305
|
+
return {
|
|
2306
|
+
output: schema ? result.output : void 0,
|
|
2307
|
+
text: result.text
|
|
2308
|
+
};
|
|
2309
|
+
},
|
|
2310
|
+
maxAttempts,
|
|
2311
|
+
{
|
|
2312
|
+
// Empty output is deterministic for identical params — retrying only
|
|
2313
|
+
// added latency before the degraded path. Fail fast instead.
|
|
2314
|
+
shouldRetry: (error) => !NoOutputGeneratedError.isInstance(error),
|
|
2315
|
+
...retryBaseDelayMs !== void 0 ? { baseDelayMs: retryBaseDelayMs } : {}
|
|
2316
|
+
}
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2254
2320
|
// ee/agentic-retrieval/pipeline/prefilter.ts
|
|
2255
2321
|
import Fuse from "fuse.js";
|
|
2256
|
-
import { generateText, Output } from "ai";
|
|
2257
2322
|
import { z as z4 } from "zod";
|
|
2258
2323
|
|
|
2259
2324
|
// ee/agentic-retrieval/pipeline/text-utils.ts
|
|
@@ -2492,22 +2557,15 @@ async function resolveIdentifierPins({
|
|
|
2492
2557
|
identifierSets.map(async (set) => {
|
|
2493
2558
|
if (!set.contexts.length) return;
|
|
2494
2559
|
try {
|
|
2495
|
-
const { output } = await
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
matches: z4.array(z4.string()).optional()
|
|
2505
|
-
})
|
|
2506
|
-
}),
|
|
2507
|
-
maxOutputTokens: 300
|
|
2508
|
-
}),
|
|
2509
|
-
3
|
|
2510
|
-
);
|
|
2560
|
+
const { output } = await microCall({
|
|
2561
|
+
model,
|
|
2562
|
+
system: set.strategy === "exact" ? EXACT_EXTRACTION_PROMPT(set) : FUZZY_EXTRACTION_PROMPT(set),
|
|
2563
|
+
messages: [{ role: "user", content: question }],
|
|
2564
|
+
schema: z4.object({
|
|
2565
|
+
hasMatches: z4.boolean(),
|
|
2566
|
+
matches: z4.array(z4.string()).optional()
|
|
2567
|
+
})
|
|
2568
|
+
});
|
|
2511
2569
|
if (!output?.hasMatches || !output.matches?.length) return;
|
|
2512
2570
|
steps.push({ text: `Detected ${set.name} in the question: ${output.matches.join(", ")}` });
|
|
2513
2571
|
await Promise.all(
|
|
@@ -2605,24 +2663,17 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
|
|
|
2605
2663
|
const [docPageRaw, explicitKBRaw] = await Promise.all([
|
|
2606
2664
|
(async () => {
|
|
2607
2665
|
try {
|
|
2608
|
-
return await
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
pageNumber: z5.number().int().nullable().optional()
|
|
2620
|
-
})
|
|
2621
|
-
}),
|
|
2622
|
-
maxOutputTokens: 300
|
|
2623
|
-
}),
|
|
2624
|
-
3
|
|
2625
|
-
);
|
|
2666
|
+
return await microCall({
|
|
2667
|
+
model,
|
|
2668
|
+
system: buildDocPagePrompt(knownIdentifiers),
|
|
2669
|
+
messages: [{ role: "user", content: question }],
|
|
2670
|
+
schema: z5.object({
|
|
2671
|
+
hasFilenameHint: z5.boolean(),
|
|
2672
|
+
filenameHints: z5.array(z5.string()).optional(),
|
|
2673
|
+
hasPageHint: z5.boolean(),
|
|
2674
|
+
pageNumber: z5.number().int().nullable().optional()
|
|
2675
|
+
})
|
|
2676
|
+
});
|
|
2626
2677
|
} catch (err) {
|
|
2627
2678
|
steps.push({ text: "Doc/page detection failed \u2014 skipping filename and page hints." });
|
|
2628
2679
|
return {
|
|
@@ -2637,23 +2688,16 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
|
|
|
2637
2688
|
})(),
|
|
2638
2689
|
(async () => {
|
|
2639
2690
|
try {
|
|
2640
|
-
return await
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
explicitlyRequestedKnowledgeBases: z5.array(
|
|
2648
|
-
z5.enum(enabledContexts.map((c) => c.id))
|
|
2649
|
-
)
|
|
2650
|
-
})
|
|
2651
|
-
}),
|
|
2652
|
-
messages: [{ role: "user", content: question }],
|
|
2653
|
-
maxOutputTokens: 200
|
|
2691
|
+
return await microCall({
|
|
2692
|
+
model,
|
|
2693
|
+
system: kbSystemPrompt,
|
|
2694
|
+
schema: z5.object({
|
|
2695
|
+
explicitlyRequestedKnowledgeBases: z5.array(
|
|
2696
|
+
z5.enum(enabledContexts.map((c) => c.id))
|
|
2697
|
+
)
|
|
2654
2698
|
}),
|
|
2655
|
-
|
|
2656
|
-
);
|
|
2699
|
+
messages: [{ role: "user", content: question }]
|
|
2700
|
+
});
|
|
2657
2701
|
} catch (err) {
|
|
2658
2702
|
return { output: { explicitlyRequestedKnowledgeBases: [] } };
|
|
2659
2703
|
}
|
|
@@ -2736,22 +2780,15 @@ ${extraInstructions}
|
|
|
2736
2780
|
</instructions>`;
|
|
2737
2781
|
}
|
|
2738
2782
|
try {
|
|
2739
|
-
const { output: classified } = await
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
reason: z5.string()
|
|
2749
|
-
})
|
|
2750
|
-
}),
|
|
2751
|
-
maxOutputTokens: 200
|
|
2752
|
-
}),
|
|
2753
|
-
3
|
|
2754
|
-
);
|
|
2783
|
+
const { output: classified } = await microCall({
|
|
2784
|
+
model,
|
|
2785
|
+
system: classifyPrompt,
|
|
2786
|
+
messages: [{ role: "user", content: question }],
|
|
2787
|
+
schema: z5.object({
|
|
2788
|
+
ruleId: z5.enum(ruleIds),
|
|
2789
|
+
reason: z5.string()
|
|
2790
|
+
})
|
|
2791
|
+
});
|
|
2755
2792
|
const matchedRule = routingRules.find((r) => r.id === classified.ruleId);
|
|
2756
2793
|
if (matchedRule) {
|
|
2757
2794
|
const main = matchedRule.main.filter((id) => enabledIds.has(id));
|
|
@@ -2806,7 +2843,6 @@ ${extraInstructions}
|
|
|
2806
2843
|
}
|
|
2807
2844
|
|
|
2808
2845
|
// ee/agentic-retrieval/pipeline/memory.ts
|
|
2809
|
-
import { generateText as generateText3, Output as Output3 } from "ai";
|
|
2810
2846
|
import { z as z6 } from "zod";
|
|
2811
2847
|
|
|
2812
2848
|
// ee/agentic-retrieval/pipeline/multi-query.ts
|
|
@@ -3032,32 +3068,25 @@ async function runMemoryPhase({
|
|
|
3032
3068
|
`;
|
|
3033
3069
|
let relevantMemoryChunks = [];
|
|
3034
3070
|
try {
|
|
3035
|
-
const { output: output_relevant_memory } = await
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
role: "user",
|
|
3043
|
-
content: `
|
|
3071
|
+
const { output: output_relevant_memory } = await microCall({
|
|
3072
|
+
model,
|
|
3073
|
+
system: CHECK_MEMORIES_FOR_RELEVANT_INFORMATION,
|
|
3074
|
+
messages: [
|
|
3075
|
+
{
|
|
3076
|
+
role: "user",
|
|
3077
|
+
content: `
|
|
3044
3078
|
<user_question>${question}</user_question>
|
|
3045
3079
|
<relevant_keywords>${keywords.join(", ")}</relevant_keywords>
|
|
3046
3080
|
<important_keyword>${importantKeyword}</important_keyword>
|
|
3047
3081
|
`
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
}),
|
|
3057
|
-
maxOutputTokens: 400
|
|
3058
|
-
}),
|
|
3059
|
-
3
|
|
3060
|
-
);
|
|
3082
|
+
}
|
|
3083
|
+
],
|
|
3084
|
+
schema: z6.object({
|
|
3085
|
+
relevantChunkIds: z6.array(z6.string()).describe(
|
|
3086
|
+
"The chunk_ids (UUIDs at the start of each bullet) of chunks containing information relevant to the user's question. Empty array if none are relevant."
|
|
3087
|
+
)
|
|
3088
|
+
})
|
|
3089
|
+
});
|
|
3061
3090
|
const ids = new Set(output_relevant_memory?.relevantChunkIds ?? []);
|
|
3062
3091
|
relevantMemoryChunks = ids.size === 0 ? [] : retrieved_memory.filter((c) => ids.has(c.chunk_id));
|
|
3063
3092
|
} catch (e) {
|
|
@@ -3149,41 +3178,34 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
3149
3178
|
`;
|
|
3150
3179
|
const [overrideResult, fileResult, queryResult] = await Promise.all([
|
|
3151
3180
|
// Override check: strict gate to decide if memory should be authoritative
|
|
3152
|
-
memoryConfig.override ?
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
role: "user",
|
|
3160
|
-
content: `
|
|
3181
|
+
memoryConfig.override ? microCall({
|
|
3182
|
+
model,
|
|
3183
|
+
system: CHECK_MEMORY_OVERRIDE,
|
|
3184
|
+
messages: [
|
|
3185
|
+
{
|
|
3186
|
+
role: "user",
|
|
3187
|
+
content: `
|
|
3161
3188
|
<user_question>${question}</user_question>
|
|
3162
3189
|
<relevant_keywords>${keywords.join(", ")}</relevant_keywords>
|
|
3163
3190
|
<important_keyword>${importantKeyword}</important_keyword>
|
|
3164
3191
|
`
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
}),
|
|
3183
|
-
maxOutputTokens: 300
|
|
3184
|
-
}),
|
|
3185
|
-
3
|
|
3186
|
-
).catch(() => ({
|
|
3192
|
+
}
|
|
3193
|
+
],
|
|
3194
|
+
schema: z6.object({
|
|
3195
|
+
overrides: z6.boolean().describe(
|
|
3196
|
+
"True ONLY if a memory chunk directly and sufficiently answers the user's question and should be authoritative over the documents. Be strict; when unsure, false."
|
|
3197
|
+
),
|
|
3198
|
+
confidence: z6.enum(["high", "medium", "low"]).describe(
|
|
3199
|
+
"Confidence that the selected memory chunk(s) fully and directly answer the question."
|
|
3200
|
+
),
|
|
3201
|
+
authoritativeChunkIds: z6.array(z6.string()).describe(
|
|
3202
|
+
"The chunk_ids of the memory chunk(s) that directly answer the question. Empty if overrides is false."
|
|
3203
|
+
),
|
|
3204
|
+
reason: z6.string().describe(
|
|
3205
|
+
"One short sentence: why this memory does or does not directly answer the question."
|
|
3206
|
+
)
|
|
3207
|
+
})
|
|
3208
|
+
}).catch(() => ({
|
|
3187
3209
|
output: {
|
|
3188
3210
|
overrides: false,
|
|
3189
3211
|
confidence: "low",
|
|
@@ -3199,44 +3221,30 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
3199
3221
|
}
|
|
3200
3222
|
}),
|
|
3201
3223
|
// File prioritization: detect explicit document-pinning instructions in memory
|
|
3202
|
-
memoryConfig.filePrioritization ?
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
fileNameHints: z6.array(z6.string()).optional()
|
|
3212
|
-
})
|
|
3213
|
-
}),
|
|
3214
|
-
maxOutputTokens: 300
|
|
3215
|
-
}),
|
|
3216
|
-
3
|
|
3217
|
-
).catch(() => ({
|
|
3224
|
+
memoryConfig.filePrioritization ? microCall({
|
|
3225
|
+
model,
|
|
3226
|
+
system: "You are a helpful assistant that will strictly follow the user's instructions.",
|
|
3227
|
+
messages: [{ role: "user", content: PROMPT_EXTRACT_PRIORITIZED_FILES }],
|
|
3228
|
+
schema: z6.object({
|
|
3229
|
+
shouldPrioritizeFiles: z6.boolean(),
|
|
3230
|
+
fileNameHints: z6.array(z6.string()).optional()
|
|
3231
|
+
})
|
|
3232
|
+
}).catch(() => ({
|
|
3218
3233
|
output: { shouldPrioritizeFiles: false, fileNameHints: [] }
|
|
3219
3234
|
})) : Promise.resolve({
|
|
3220
3235
|
output: { shouldPrioritizeFiles: false, fileNameHints: [] }
|
|
3221
3236
|
}),
|
|
3222
3237
|
// Query augmentation: expand keywords with synonyms/abbreviations from memory
|
|
3223
|
-
memoryConfig.queryAugmentation && hasAugmentationContent ?
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
updatedImportantKeyword: z6.string()
|
|
3234
|
-
})
|
|
3235
|
-
}),
|
|
3236
|
-
maxOutputTokens: 600
|
|
3237
|
-
}),
|
|
3238
|
-
3
|
|
3239
|
-
).catch(() => ({
|
|
3238
|
+
memoryConfig.queryAugmentation && hasAugmentationContent ? microCall({
|
|
3239
|
+
model,
|
|
3240
|
+
system: "You are a helpful assistant that will strictly follow the user's instructions.",
|
|
3241
|
+
messages: [{ role: "user", content: QUERY_AUGMENTATION_PROMPT }],
|
|
3242
|
+
schema: z6.object({
|
|
3243
|
+
updatedUserQuestion: z6.string(),
|
|
3244
|
+
updatedRelevantKeywords: z6.array(z6.string()),
|
|
3245
|
+
updatedImportantKeyword: z6.string()
|
|
3246
|
+
})
|
|
3247
|
+
}).catch(() => ({
|
|
3240
3248
|
output: {
|
|
3241
3249
|
updatedUserQuestion: question,
|
|
3242
3250
|
updatedRelevantKeywords: [],
|
|
@@ -3318,7 +3326,6 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
3318
3326
|
}
|
|
3319
3327
|
|
|
3320
3328
|
// ee/agentic-retrieval/pipeline/hyde.ts
|
|
3321
|
-
import { generateText as generateText4 } from "ai";
|
|
3322
3329
|
var hydeCache = /* @__PURE__ */ new Map();
|
|
3323
3330
|
var HYDE_CACHE_MAX = 200;
|
|
3324
3331
|
function hydeCacheKey(originalQuestion, relevantKeywords, styleHint, importantKeyword) {
|
|
@@ -3387,11 +3394,11 @@ IMPORTANT:
|
|
|
3387
3394
|
prompt += `
|
|
3388
3395
|
Question: "${originalQuestion}"
|
|
3389
3396
|
Relevant keywords: ${relevantKeywords.join(", ")}`;
|
|
3390
|
-
const { text } = await
|
|
3397
|
+
const { text } = await microCall({
|
|
3391
3398
|
model,
|
|
3392
3399
|
prompt,
|
|
3393
3400
|
temperature: 0.3,
|
|
3394
|
-
|
|
3401
|
+
maxAttempts: 1
|
|
3395
3402
|
});
|
|
3396
3403
|
const passage = (text || "").trim();
|
|
3397
3404
|
return passage.length > 0 ? passage : null;
|
|
@@ -3839,7 +3846,6 @@ function createAgenticRetrievalTool(opts) {
|
|
|
3839
3846
|
const resolved = await resolveModel({
|
|
3840
3847
|
modelId: cfg.utilityModel,
|
|
3841
3848
|
user,
|
|
3842
|
-
providers: exuluApp.get().providers,
|
|
3843
3849
|
rbacBypass: true
|
|
3844
3850
|
});
|
|
3845
3851
|
utilityModel = resolved.languageModel ?? model;
|
|
@@ -4190,7 +4196,7 @@ function sanitizeToolName(name) {
|
|
|
4190
4196
|
}
|
|
4191
4197
|
|
|
4192
4198
|
// src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
|
|
4193
|
-
import { randomUUID as
|
|
4199
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
4194
4200
|
|
|
4195
4201
|
// types/enums/statistics.ts
|
|
4196
4202
|
var STATISTICS_TYPE_ENUM = {
|
|
@@ -4295,42 +4301,424 @@ var createNewMemoryItemTool = (agent, context) => {
|
|
|
4295
4301
|
}
|
|
4296
4302
|
}
|
|
4297
4303
|
}
|
|
4298
|
-
const newItem = {
|
|
4299
|
-
name,
|
|
4300
|
-
description: "Description: " + description + "\n\nSurrounding Context: " + surroundingContext,
|
|
4301
|
-
information: "Information: " + information,
|
|
4302
|
-
rights_mode: visibility === "private" ? "private" : "public",
|
|
4303
|
-
...extraFields
|
|
4304
|
-
};
|
|
4305
|
-
const { item: createdItem, job: createdJob } = await context.createItem(
|
|
4306
|
-
newItem,
|
|
4307
|
-
exuluConfig,
|
|
4308
|
-
user?.id,
|
|
4309
|
-
user?.role?.id,
|
|
4310
|
-
false
|
|
4311
|
-
);
|
|
4312
|
-
if (createdJob) {
|
|
4313
|
-
result = {
|
|
4314
|
-
result: `Created a Job to create the memory item with the following ID: ${createdJob}`
|
|
4315
|
-
};
|
|
4316
|
-
} else if (createdItem) {
|
|
4317
|
-
result = {
|
|
4318
|
-
result: `Created memory item with the following ID: ${createdItem.id}`
|
|
4319
|
-
};
|
|
4320
|
-
} else {
|
|
4321
|
-
result = {
|
|
4322
|
-
result: `Failed to create memory item`
|
|
4323
|
-
};
|
|
4304
|
+
const newItem = {
|
|
4305
|
+
name,
|
|
4306
|
+
description: "Description: " + description + "\n\nSurrounding Context: " + surroundingContext,
|
|
4307
|
+
information: "Information: " + information,
|
|
4308
|
+
rights_mode: visibility === "private" ? "private" : "public",
|
|
4309
|
+
...extraFields
|
|
4310
|
+
};
|
|
4311
|
+
const { item: createdItem, job: createdJob } = await context.createItem(
|
|
4312
|
+
newItem,
|
|
4313
|
+
exuluConfig,
|
|
4314
|
+
user?.id,
|
|
4315
|
+
user?.role?.id,
|
|
4316
|
+
false
|
|
4317
|
+
);
|
|
4318
|
+
if (createdJob) {
|
|
4319
|
+
result = {
|
|
4320
|
+
result: `Created a Job to create the memory item with the following ID: ${createdJob}`
|
|
4321
|
+
};
|
|
4322
|
+
} else if (createdItem) {
|
|
4323
|
+
result = {
|
|
4324
|
+
result: `Created memory item with the following ID: ${createdItem.id}`
|
|
4325
|
+
};
|
|
4326
|
+
} else {
|
|
4327
|
+
result = {
|
|
4328
|
+
result: `Failed to create memory item`
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4331
|
+
} catch (error) {
|
|
4332
|
+
console.error("[EXULU] Error creating memory item", error);
|
|
4333
|
+
result = {
|
|
4334
|
+
result: `Failed to create memory item: ${error instanceof Error ? error.message : String(error)}`
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
return result;
|
|
4338
|
+
}
|
|
4339
|
+
});
|
|
4340
|
+
};
|
|
4341
|
+
|
|
4342
|
+
// src/templates/tools/context-write-tools.ts
|
|
4343
|
+
import { z as z10 } from "zod";
|
|
4344
|
+
|
|
4345
|
+
// src/exulu/table-names.ts
|
|
4346
|
+
var getTableName = (id) => sanitizeName(id) + "_items";
|
|
4347
|
+
var getChunksTableName = (id) => sanitizeName(id) + "_chunks";
|
|
4348
|
+
|
|
4349
|
+
// src/utils/check-item-write-access.ts
|
|
4350
|
+
var checkItemWriteAccess = async (context, record, user) => {
|
|
4351
|
+
if (!user) {
|
|
4352
|
+
return false;
|
|
4353
|
+
}
|
|
4354
|
+
if (user.super_admin === true) {
|
|
4355
|
+
return true;
|
|
4356
|
+
}
|
|
4357
|
+
if (user.type === "api" && (!user.scope_mode || user.scope_mode === "admin")) {
|
|
4358
|
+
return true;
|
|
4359
|
+
}
|
|
4360
|
+
if (record.rights_mode === "public") {
|
|
4361
|
+
return true;
|
|
4362
|
+
}
|
|
4363
|
+
if (record.rights_mode === "private") {
|
|
4364
|
+
return record.created_by != null && String(record.created_by) === String(user.id);
|
|
4365
|
+
}
|
|
4366
|
+
const validRightsModes = ["users", "roles", "teams"];
|
|
4367
|
+
if (!validRightsModes.includes(record.rights_mode)) {
|
|
4368
|
+
return false;
|
|
4369
|
+
}
|
|
4370
|
+
const entity = getTableName(context.id);
|
|
4371
|
+
const { db: db2 } = await postgresClient();
|
|
4372
|
+
if (record.rights_mode === "users") {
|
|
4373
|
+
const grant = await db2.from("rbac").where({
|
|
4374
|
+
entity,
|
|
4375
|
+
target_resource_id: record.id,
|
|
4376
|
+
access_type: "User",
|
|
4377
|
+
user_id: user.id,
|
|
4378
|
+
rights: "write"
|
|
4379
|
+
}).first();
|
|
4380
|
+
return !!grant;
|
|
4381
|
+
}
|
|
4382
|
+
if (record.rights_mode === "roles") {
|
|
4383
|
+
const roleId = typeof user.role === "string" ? user.role : user.role?.id;
|
|
4384
|
+
if (!roleId) {
|
|
4385
|
+
return false;
|
|
4386
|
+
}
|
|
4387
|
+
const grant = await db2.from("rbac").where({
|
|
4388
|
+
entity,
|
|
4389
|
+
target_resource_id: record.id,
|
|
4390
|
+
access_type: "Role",
|
|
4391
|
+
role_id: roleId,
|
|
4392
|
+
rights: "write"
|
|
4393
|
+
}).first();
|
|
4394
|
+
return !!grant;
|
|
4395
|
+
}
|
|
4396
|
+
if (record.rights_mode === "teams") {
|
|
4397
|
+
const teamId = typeof user.team === "string" ? user.team : user.team?.id;
|
|
4398
|
+
if (!teamId) {
|
|
4399
|
+
return false;
|
|
4400
|
+
}
|
|
4401
|
+
const grant = await db2.from("rbac").where({
|
|
4402
|
+
entity,
|
|
4403
|
+
target_resource_id: record.id,
|
|
4404
|
+
access_type: "Team",
|
|
4405
|
+
team_id: teamId,
|
|
4406
|
+
rights: "write"
|
|
4407
|
+
}).first();
|
|
4408
|
+
return !!grant;
|
|
4409
|
+
}
|
|
4410
|
+
return false;
|
|
4411
|
+
};
|
|
4412
|
+
|
|
4413
|
+
// src/templates/tools/kb-editor-config.ts
|
|
4414
|
+
import { z as z9 } from "zod";
|
|
4415
|
+
var KB_EDITOR_TOOL_ID = "knowledge_base_editor";
|
|
4416
|
+
var permissionsSchema = z9.object({
|
|
4417
|
+
create: z9.boolean().catch(false).default(false),
|
|
4418
|
+
update: z9.boolean().catch(false).default(false)
|
|
4419
|
+
});
|
|
4420
|
+
var emptyConfig = () => ({
|
|
4421
|
+
enabled: false,
|
|
4422
|
+
knowledgeBases: {},
|
|
4423
|
+
skipApproval: false
|
|
4424
|
+
});
|
|
4425
|
+
var parseKbEditorConfig = (tools) => {
|
|
4426
|
+
let entries = tools;
|
|
4427
|
+
if (typeof entries === "string") {
|
|
4428
|
+
try {
|
|
4429
|
+
entries = JSON.parse(entries);
|
|
4430
|
+
} catch {
|
|
4431
|
+
return emptyConfig();
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
if (!Array.isArray(entries)) {
|
|
4435
|
+
return emptyConfig();
|
|
4436
|
+
}
|
|
4437
|
+
const entry = entries.find((t) => t?.id === KB_EDITOR_TOOL_ID);
|
|
4438
|
+
if (!entry) {
|
|
4439
|
+
return emptyConfig();
|
|
4440
|
+
}
|
|
4441
|
+
const rawValue = (name) => {
|
|
4442
|
+
const row = Array.isArray(entry.config) ? entry.config.find((c) => c?.name === name) : void 0;
|
|
4443
|
+
return row?.value ?? row?.variable ?? row?.default;
|
|
4444
|
+
};
|
|
4445
|
+
let kbsRaw = rawValue("knowledge_bases");
|
|
4446
|
+
if (typeof kbsRaw === "string" && kbsRaw) {
|
|
4447
|
+
try {
|
|
4448
|
+
kbsRaw = JSON.parse(kbsRaw);
|
|
4449
|
+
} catch {
|
|
4450
|
+
kbsRaw = {};
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
const knowledgeBases = {};
|
|
4454
|
+
if (kbsRaw && typeof kbsRaw === "object" && !Array.isArray(kbsRaw)) {
|
|
4455
|
+
for (const [contextId, value] of Object.entries(kbsRaw)) {
|
|
4456
|
+
const parsed = permissionsSchema.safeParse(value);
|
|
4457
|
+
if (parsed.success && (parsed.data.create || parsed.data.update)) {
|
|
4458
|
+
knowledgeBases[contextId] = parsed.data;
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
const skipRaw = rawValue("skip_approval");
|
|
4463
|
+
const skipApproval = skipRaw === true || skipRaw === "true" || skipRaw === 1;
|
|
4464
|
+
return { enabled: true, knowledgeBases, skipApproval };
|
|
4465
|
+
};
|
|
4466
|
+
|
|
4467
|
+
// src/templates/tools/context-write-tools.ts
|
|
4468
|
+
var MAX_CONTEXT_SEGMENT = 68;
|
|
4469
|
+
var RESERVED_INPUT_KEYS = /* @__PURE__ */ new Set([
|
|
4470
|
+
"model",
|
|
4471
|
+
"user",
|
|
4472
|
+
"contexts",
|
|
4473
|
+
"memory",
|
|
4474
|
+
"req",
|
|
4475
|
+
"upload",
|
|
4476
|
+
"sessionID",
|
|
4477
|
+
"sessionItems",
|
|
4478
|
+
"providerapikey",
|
|
4479
|
+
"allExuluTools",
|
|
4480
|
+
"currentTools",
|
|
4481
|
+
"exuluConfig",
|
|
4482
|
+
"toolVariablesConfig",
|
|
4483
|
+
"oauth"
|
|
4484
|
+
]);
|
|
4485
|
+
var buildWriteSchema = (context, mode) => {
|
|
4486
|
+
const shape = {};
|
|
4487
|
+
const contentKeys = [];
|
|
4488
|
+
const addContent = (key, schema, required) => {
|
|
4489
|
+
shape[key] = required && mode === "create" ? schema : schema.optional();
|
|
4490
|
+
contentKeys.push(key);
|
|
4491
|
+
};
|
|
4492
|
+
if (mode === "update") {
|
|
4493
|
+
shape["id"] = z10.string().optional().describe("The id of the item to update.");
|
|
4494
|
+
shape["external_id"] = z10.string().optional().describe("The external_id of the item to update, if the id is unknown. Lookup only \u2014 it is never changed.");
|
|
4495
|
+
}
|
|
4496
|
+
addContent("name", z10.string().describe("The name of the item."), true);
|
|
4497
|
+
addContent("description", z10.string().describe("A description of the item."), false);
|
|
4498
|
+
addContent("tags", z10.array(z10.string()).describe("Tags for the item."), false);
|
|
4499
|
+
if (mode === "create") {
|
|
4500
|
+
addContent(
|
|
4501
|
+
"external_id",
|
|
4502
|
+
z10.string().describe("An optional external identifier for the item, e.g. an id from a source system."),
|
|
4503
|
+
false
|
|
4504
|
+
);
|
|
4505
|
+
}
|
|
4506
|
+
for (const field of context.fields ?? []) {
|
|
4507
|
+
if (field.type === "file" || field.type === "uuid") continue;
|
|
4508
|
+
if (field.calculated === true || field.editable === false) continue;
|
|
4509
|
+
if (field.hidden === true) continue;
|
|
4510
|
+
if (RESERVED_INPUT_KEYS.has(field.name)) continue;
|
|
4511
|
+
let schema;
|
|
4512
|
+
switch (field.type) {
|
|
4513
|
+
case "enum":
|
|
4514
|
+
schema = z10.string().describe(
|
|
4515
|
+
`The ${field.name} of the item. Must be one of: ${(field.enumValues ?? []).join(", ")}`
|
|
4516
|
+
);
|
|
4517
|
+
break;
|
|
4518
|
+
case "json":
|
|
4519
|
+
schema = z10.string().describe(`The ${field.name} of the item, as a valid JSON string.`);
|
|
4520
|
+
break;
|
|
4521
|
+
case "markdown":
|
|
4522
|
+
schema = z10.string().describe(`The ${field.name} of the item, as a valid Markdown string.`);
|
|
4523
|
+
break;
|
|
4524
|
+
case "date":
|
|
4525
|
+
schema = z10.string().describe(`The ${field.name} of the item, as an ISO-8601 date string.`);
|
|
4526
|
+
break;
|
|
4527
|
+
case "number":
|
|
4528
|
+
schema = z10.number().describe(`The ${field.name} of the item.`);
|
|
4529
|
+
break;
|
|
4530
|
+
case "boolean":
|
|
4531
|
+
schema = z10.boolean().describe(`The ${field.name} of the item.`);
|
|
4532
|
+
break;
|
|
4533
|
+
default:
|
|
4534
|
+
schema = z10.string().describe(`The ${field.name} of the item.`);
|
|
4535
|
+
break;
|
|
4536
|
+
}
|
|
4537
|
+
addContent(field.name, schema, field.required === true);
|
|
4538
|
+
}
|
|
4539
|
+
return { shape, contentKeys };
|
|
4540
|
+
};
|
|
4541
|
+
var canonicalizeEnumFields = (context, params) => {
|
|
4542
|
+
for (const field of context.fields ?? []) {
|
|
4543
|
+
if (field.type !== "enum" || !field.enumValues?.length) continue;
|
|
4544
|
+
const raw = params[field.name];
|
|
4545
|
+
if (raw === void 0 || raw === null || raw === "") continue;
|
|
4546
|
+
const rawStr = String(raw);
|
|
4547
|
+
const canonical = field.enumValues.find((v) => v.toUpperCase() === rawStr.toUpperCase());
|
|
4548
|
+
if (canonical === void 0) {
|
|
4549
|
+
return `Invalid value "${rawStr}" for field "${field.name}". Allowed values: ${field.enumValues.join(", ")}.`;
|
|
4550
|
+
}
|
|
4551
|
+
params[field.name] = canonical;
|
|
4552
|
+
}
|
|
4553
|
+
return void 0;
|
|
4554
|
+
};
|
|
4555
|
+
var pickContent = (params, contentKeys) => {
|
|
4556
|
+
const item = {};
|
|
4557
|
+
for (const key of contentKeys) {
|
|
4558
|
+
if (params[key] !== void 0) {
|
|
4559
|
+
item[key] = params[key];
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
return item;
|
|
4563
|
+
};
|
|
4564
|
+
var jobNote = (job) => job ? ` Processing/embeddings queued (job: ${job}); changes become searchable when the job completes.` : "";
|
|
4565
|
+
var createContextWriteTools = (context, perms, skipApproval) => {
|
|
4566
|
+
const tools = [];
|
|
4567
|
+
const segment = sanitizeName(context.id).slice(0, MAX_CONTEXT_SEGMENT);
|
|
4568
|
+
const contextLabel = context.description ? ` ${context.description}` : "";
|
|
4569
|
+
if (perms.create) {
|
|
4570
|
+
const { shape, contentKeys } = buildWriteSchema(context, "create");
|
|
4571
|
+
tools.push(
|
|
4572
|
+
new ExuluTool({
|
|
4573
|
+
id: `create_${segment}_item`,
|
|
4574
|
+
name: `Create ${context.name} item`,
|
|
4575
|
+
category: "knowledge_base_editing",
|
|
4576
|
+
description: `Create a new item in the "${context.name}" knowledge base.${contextLabel}`,
|
|
4577
|
+
type: "function",
|
|
4578
|
+
inputSchema: z10.object(shape),
|
|
4579
|
+
config: [],
|
|
4580
|
+
needsApproval: !skipApproval,
|
|
4581
|
+
execute: async (params) => {
|
|
4582
|
+
const { user, exuluConfig } = params;
|
|
4583
|
+
if (!user?.id) {
|
|
4584
|
+
return { result: "Knowledge base writes require an authenticated user." };
|
|
4585
|
+
}
|
|
4586
|
+
try {
|
|
4587
|
+
const enumError = canonicalizeEnumFields(context, params);
|
|
4588
|
+
if (enumError) {
|
|
4589
|
+
return { result: enumError };
|
|
4590
|
+
}
|
|
4591
|
+
const item = pickContent(params, contentKeys);
|
|
4592
|
+
item.created_by = String(user.id);
|
|
4593
|
+
const { item: created, job } = await context.createItem(
|
|
4594
|
+
item,
|
|
4595
|
+
exuluConfig,
|
|
4596
|
+
user?.id,
|
|
4597
|
+
user?.role?.id,
|
|
4598
|
+
false
|
|
4599
|
+
);
|
|
4600
|
+
if (!created?.id) {
|
|
4601
|
+
return { result: `Failed to create item in "${context.name}".` };
|
|
4602
|
+
}
|
|
4603
|
+
return {
|
|
4604
|
+
result: `Created item ${created.id} in knowledge base "${context.name}".${jobNote(job)}`
|
|
4605
|
+
};
|
|
4606
|
+
} catch (error) {
|
|
4607
|
+
console.error(`[EXULU] Error creating item in context ${context.id}`, error);
|
|
4608
|
+
return {
|
|
4609
|
+
result: `Failed to create item in "${context.name}": ${error instanceof Error ? error.message : String(error)}`
|
|
4610
|
+
};
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
})
|
|
4614
|
+
);
|
|
4615
|
+
}
|
|
4616
|
+
if (perms.update) {
|
|
4617
|
+
const { shape, contentKeys } = buildWriteSchema(context, "update");
|
|
4618
|
+
const NOT_FOUND = `Item not found in "${context.name}" or you don't have write access to it.`;
|
|
4619
|
+
tools.push(
|
|
4620
|
+
new ExuluTool({
|
|
4621
|
+
id: `update_${segment}_item`,
|
|
4622
|
+
name: `Update ${context.name} item`,
|
|
4623
|
+
category: "knowledge_base_editing",
|
|
4624
|
+
description: `Update an existing item in the "${context.name}" knowledge base. Provide the item's id (or external_id) plus only the fields to change; omitted fields keep their values.`,
|
|
4625
|
+
type: "function",
|
|
4626
|
+
inputSchema: z10.object(shape),
|
|
4627
|
+
config: [],
|
|
4628
|
+
needsApproval: !skipApproval,
|
|
4629
|
+
execute: async (params) => {
|
|
4630
|
+
const { user, exuluConfig } = params;
|
|
4631
|
+
if (!user?.id) {
|
|
4632
|
+
return { result: "Knowledge base writes require an authenticated user." };
|
|
4633
|
+
}
|
|
4634
|
+
try {
|
|
4635
|
+
if (!params.id && !params.external_id) {
|
|
4636
|
+
return { result: "Provide the id or external_id of the item to update." };
|
|
4637
|
+
}
|
|
4638
|
+
const existing = await context.getItem({
|
|
4639
|
+
item: { id: params.id, external_id: params.external_id }
|
|
4640
|
+
});
|
|
4641
|
+
if (!existing?.id) {
|
|
4642
|
+
return { result: NOT_FOUND };
|
|
4643
|
+
}
|
|
4644
|
+
const allowed = await checkItemWriteAccess(context, existing, user);
|
|
4645
|
+
if (!allowed) {
|
|
4646
|
+
return { result: NOT_FOUND };
|
|
4647
|
+
}
|
|
4648
|
+
const enumError = canonicalizeEnumFields(context, params);
|
|
4649
|
+
if (enumError) {
|
|
4650
|
+
return { result: enumError };
|
|
4651
|
+
}
|
|
4652
|
+
const patch = pickContent(params, contentKeys);
|
|
4653
|
+
if (Object.keys(patch).length === 0) {
|
|
4654
|
+
return { result: "No fields to update were provided." };
|
|
4655
|
+
}
|
|
4656
|
+
patch.id = existing.id;
|
|
4657
|
+
const { job } = await context.updateItem(patch, exuluConfig, user?.id, user?.role?.id);
|
|
4658
|
+
const fresh = await context.getItem({ item: { id: existing.id } });
|
|
4659
|
+
const summary = { id: existing.id };
|
|
4660
|
+
for (const key of contentKeys) {
|
|
4661
|
+
if (fresh?.[key] !== void 0 && fresh?.[key] !== null) {
|
|
4662
|
+
summary[key] = fresh[key];
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
return {
|
|
4666
|
+
result: `Updated item ${existing.id} in knowledge base "${context.name}".${jobNote(job)}
|
|
4667
|
+
Current item: ${JSON.stringify(summary)}`
|
|
4668
|
+
};
|
|
4669
|
+
} catch (error) {
|
|
4670
|
+
console.error(`[EXULU] Error updating item in context ${context.id}`, error);
|
|
4671
|
+
return {
|
|
4672
|
+
result: `Failed to update item in "${context.name}": ${error instanceof Error ? error.message : String(error)}`
|
|
4673
|
+
};
|
|
4674
|
+
}
|
|
4324
4675
|
}
|
|
4325
|
-
}
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4676
|
+
})
|
|
4677
|
+
);
|
|
4678
|
+
}
|
|
4679
|
+
return tools;
|
|
4680
|
+
};
|
|
4681
|
+
var createKbEditorPickerTool = () => new ExuluTool({
|
|
4682
|
+
id: KB_EDITOR_TOOL_ID,
|
|
4683
|
+
name: "Knowledge base editor",
|
|
4684
|
+
category: "default",
|
|
4685
|
+
description: "Let this agent create or update items in selected knowledge bases during chat. Configure per knowledge base whether the agent may create and/or update items.",
|
|
4686
|
+
type: "function",
|
|
4687
|
+
inputSchema: z10.object({}),
|
|
4688
|
+
config: [
|
|
4689
|
+
{
|
|
4690
|
+
name: "knowledge_bases",
|
|
4691
|
+
description: "JSON record of context id to { create: boolean, update: boolean }. Contexts absent here get no write access.",
|
|
4692
|
+
type: "json"
|
|
4693
|
+
},
|
|
4694
|
+
{
|
|
4695
|
+
name: "skip_approval",
|
|
4696
|
+
description: "Run knowledge base writes without asking for approval in the chat.",
|
|
4697
|
+
type: "boolean",
|
|
4698
|
+
default: false
|
|
4699
|
+
}
|
|
4700
|
+
],
|
|
4701
|
+
execute: async () => ({
|
|
4702
|
+
result: "This entry is configuration-only; Exulu expands it into per-context create/update tools at runtime."
|
|
4703
|
+
})
|
|
4704
|
+
});
|
|
4705
|
+
var collectKbWriteTools = (agent, contexts) => {
|
|
4706
|
+
if (!agent?.tools || !contexts?.length) {
|
|
4707
|
+
return [];
|
|
4708
|
+
}
|
|
4709
|
+
const config = parseKbEditorConfig(agent.tools);
|
|
4710
|
+
if (!config.enabled) {
|
|
4711
|
+
return [];
|
|
4712
|
+
}
|
|
4713
|
+
const tools = [];
|
|
4714
|
+
for (const [contextId, perms] of Object.entries(config.knowledgeBases)) {
|
|
4715
|
+
const context = contexts.find((c) => c.id === contextId);
|
|
4716
|
+
if (!context) {
|
|
4717
|
+
continue;
|
|
4332
4718
|
}
|
|
4333
|
-
|
|
4719
|
+
tools.push(...createContextWriteTools(context, perms, config.skipApproval));
|
|
4720
|
+
}
|
|
4721
|
+
return tools;
|
|
4334
4722
|
};
|
|
4335
4723
|
|
|
4336
4724
|
// ee/invoke-skills/create-sandbox.ts
|
|
@@ -5468,8 +5856,8 @@ ${body}`
|
|
|
5468
5856
|
// ee/invoke-skills/create-sandbox.ts
|
|
5469
5857
|
import { createBashTool } from "bash-tool";
|
|
5470
5858
|
import { tool as tool2 } from "ai";
|
|
5471
|
-
import { z as
|
|
5472
|
-
import
|
|
5859
|
+
import { z as z11 } from "zod";
|
|
5860
|
+
import CryptoJS3 from "crypto-js";
|
|
5473
5861
|
var getAllExuluVariables = async () => {
|
|
5474
5862
|
const { db: db2 } = await postgresClient();
|
|
5475
5863
|
const rows = await db2.from("variables").select("*");
|
|
@@ -5481,8 +5869,8 @@ var getAllExuluVariables = async () => {
|
|
|
5481
5869
|
let value = row.value;
|
|
5482
5870
|
if (row.encrypted) {
|
|
5483
5871
|
try {
|
|
5484
|
-
const bytes =
|
|
5485
|
-
value = bytes.toString(
|
|
5872
|
+
const bytes = CryptoJS3.AES.decrypt(value, process.env.NEXTAUTH_SECRET);
|
|
5873
|
+
value = bytes.toString(CryptoJS3.enc.Utf8);
|
|
5486
5874
|
} catch (err) {
|
|
5487
5875
|
console.error(
|
|
5488
5876
|
`[VARIABLES] Failed to decrypt variable "${row.name}"; skipping.`,
|
|
@@ -5766,11 +6154,11 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
5766
6154
|
async executeCommand(command) {
|
|
5767
6155
|
return await runWrapped(command);
|
|
5768
6156
|
},
|
|
5769
|
-
async readFile(
|
|
5770
|
-
const { stdout, stderr, exitCode } = await runWrapped(`cat ${shellQuote(
|
|
6157
|
+
async readFile(path3) {
|
|
6158
|
+
const { stdout, stderr, exitCode } = await runWrapped(`cat ${shellQuote(path3)}`);
|
|
5771
6159
|
if (exitCode !== 0) {
|
|
5772
6160
|
throw new Error(
|
|
5773
|
-
`readFile ${
|
|
6161
|
+
`readFile ${path3} failed (exit ${exitCode}): ${stderr.trim() || "no stderr captured"}`
|
|
5774
6162
|
);
|
|
5775
6163
|
}
|
|
5776
6164
|
return stdout;
|
|
@@ -5886,12 +6274,12 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
5886
6274
|
});
|
|
5887
6275
|
const writeFileTool = tool2({
|
|
5888
6276
|
description: 'Write content to a file in the sandbox. Creates parent directories if needed. Paths are always resolved against the session sandbox root \u2014 both relative paths ("skills/foo.md") and leading-slash paths ("/skills/foo.md") work and reach the same file. When the path is under the session artifact tree, the file is also uploaded to S3 and a short-lived presigned URL is returned in the tool output.',
|
|
5889
|
-
inputSchema:
|
|
5890
|
-
path:
|
|
5891
|
-
content:
|
|
6277
|
+
inputSchema: z11.object({
|
|
6278
|
+
path: z11.string().describe("The path where the file should be written. Relative paths and leading-slash paths are both resolved against the session sandbox root."),
|
|
6279
|
+
content: z11.string().describe("The content to write to the file")
|
|
5892
6280
|
}),
|
|
5893
|
-
execute: async ({ path, content }) => {
|
|
5894
|
-
const resolvedPath = resolveSessionPath(
|
|
6281
|
+
execute: async ({ path: path3, content }) => {
|
|
6282
|
+
const resolvedPath = resolveSessionPath(path3, sessionDir);
|
|
5895
6283
|
const results = await writeFilesInternal([{ path: resolvedPath, content }]);
|
|
5896
6284
|
const result = results[0];
|
|
5897
6285
|
if (!result) {
|
|
@@ -5907,11 +6295,11 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
5907
6295
|
});
|
|
5908
6296
|
const readFileTool = tool2({
|
|
5909
6297
|
description: 'Read the contents of a file from the sandbox. Paths are always resolved against the session sandbox root \u2014 both relative paths ("skills/foo.md") and leading-slash paths ("/skills/foo.md") work and reach the same file. If the file does not exist, the error message is surfaced verbatim.',
|
|
5910
|
-
inputSchema:
|
|
5911
|
-
path:
|
|
6298
|
+
inputSchema: z11.object({
|
|
6299
|
+
path: z11.string().describe("The path of the file to read. Relative paths and leading-slash paths are both resolved against the session sandbox root.")
|
|
5912
6300
|
}),
|
|
5913
|
-
execute: async ({ path }) => {
|
|
5914
|
-
const resolvedPath = resolveSessionPath(
|
|
6301
|
+
execute: async ({ path: path3 }) => {
|
|
6302
|
+
const resolvedPath = resolveSessionPath(path3, sessionDir);
|
|
5915
6303
|
const content = await customSandbox.readFile(resolvedPath);
|
|
5916
6304
|
return { content };
|
|
5917
6305
|
}
|
|
@@ -5919,8 +6307,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
5919
6307
|
const originalBashTool = tools.bash;
|
|
5920
6308
|
const bashTool = tool2({
|
|
5921
6309
|
description: originalBashTool.description ?? "",
|
|
5922
|
-
inputSchema:
|
|
5923
|
-
command:
|
|
6310
|
+
inputSchema: z11.object({
|
|
6311
|
+
command: z11.string().describe("The bash command to execute.")
|
|
5924
6312
|
}),
|
|
5925
6313
|
execute: async (args, opts) => {
|
|
5926
6314
|
const before = persistenceEnabled ? await snapshotSessionArtifacts() : null;
|
|
@@ -5933,25 +6321,25 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
5933
6321
|
if (persistenceEnabled && before) {
|
|
5934
6322
|
const after = await snapshotSessionArtifacts();
|
|
5935
6323
|
const changedPaths = [];
|
|
5936
|
-
for (const [
|
|
5937
|
-
const beforeMtime = before.get(
|
|
6324
|
+
for (const [path3, mtime] of after) {
|
|
6325
|
+
const beforeMtime = before.get(path3);
|
|
5938
6326
|
if (beforeMtime === void 0 || beforeMtime < mtime) {
|
|
5939
|
-
changedPaths.push(
|
|
6327
|
+
changedPaths.push(path3);
|
|
5940
6328
|
}
|
|
5941
6329
|
}
|
|
5942
|
-
for (const
|
|
6330
|
+
for (const path3 of changedPaths) {
|
|
5943
6331
|
try {
|
|
5944
|
-
const content = await fsReadFile(
|
|
5945
|
-
const persisted = await persistArtifactToS3(
|
|
6332
|
+
const content = await fsReadFile(path3);
|
|
6333
|
+
const persisted = await persistArtifactToS3(path3, content);
|
|
5946
6334
|
artifacts.push({
|
|
5947
|
-
path,
|
|
5948
|
-
relativePath: relative(sessionDir,
|
|
6335
|
+
path: path3,
|
|
6336
|
+
relativePath: relative(sessionDir, path3),
|
|
5949
6337
|
key: persisted.key,
|
|
5950
6338
|
url: persisted.url
|
|
5951
6339
|
});
|
|
5952
6340
|
} catch (err) {
|
|
5953
6341
|
console.error(
|
|
5954
|
-
`[SKILLS] Failed to mirror bash-produced artifact ${
|
|
6342
|
+
`[SKILLS] Failed to mirror bash-produced artifact ${path3} to S3; continuing.`,
|
|
5955
6343
|
err
|
|
5956
6344
|
);
|
|
5957
6345
|
}
|
|
@@ -6167,8 +6555,27 @@ var guardExtractedFileText = async (filename, text, ctx) => {
|
|
|
6167
6555
|
${notice}`;
|
|
6168
6556
|
};
|
|
6169
6557
|
|
|
6558
|
+
// src/exulu/auth/scrub-text.ts
|
|
6559
|
+
var credentialScrubText = (provider, fieldLabels) => `A secure credential form for provider "${provider}"` + (fieldLabels.length ? ` (fields: ${fieldLabels.join(", ")})` : "") + ` is shown to the user in the chat UI. Never ask for these values in chat. After the user confirms saving, call the tool again.`;
|
|
6560
|
+
var SCRUBBED_CREDENTIAL_TEXT = "A secure credential form was shown to the user in the chat UI. Never ask for credential values in chat. After the user confirms saving, call the tool again.";
|
|
6561
|
+
var SCRUBBED_OAUTH_TEXT = "Authorization is required. A Connect button was shown to the user in the chat UI. Do not relay any URL in chat. After the user confirms connecting, call the tool again.";
|
|
6562
|
+
|
|
6563
|
+
// src/templates/tools/auth-tool-model-output.ts
|
|
6564
|
+
var buildAuthToolModelOutput = (tool3) => ({ output }) => {
|
|
6565
|
+
if (output && typeof output === "object" && output.credentialRequest) {
|
|
6566
|
+
const auth = tool3.authentication;
|
|
6567
|
+
const labels = auth?.authType === "user_credentials" ? auth.fields.map((f) => f.label) : [];
|
|
6568
|
+
const provider = output.credentialRequest.provider ?? (auth && "provider" in auth ? auth.provider : "unknown");
|
|
6569
|
+
return { type: "text", value: credentialScrubText(provider, labels) };
|
|
6570
|
+
}
|
|
6571
|
+
if (output && typeof output === "object" && output.oauth?.authorizationUrl) {
|
|
6572
|
+
return { type: "text", value: SCRUBBED_OAUTH_TEXT };
|
|
6573
|
+
}
|
|
6574
|
+
return { type: "json", value: output ?? null };
|
|
6575
|
+
};
|
|
6576
|
+
|
|
6170
6577
|
// src/templates/tools/session-file-read-tool.ts
|
|
6171
|
-
import { z as
|
|
6578
|
+
import { z as z12 } from "zod";
|
|
6172
6579
|
var DEFAULT_LIMIT = 250;
|
|
6173
6580
|
var MAX_CONTENT_CHARS = 16e3;
|
|
6174
6581
|
var createSessionFileReadTool = ({
|
|
@@ -6220,10 +6627,10 @@ var createSessionFileReadTool = ({
|
|
|
6220
6627
|
name: "read_session_file",
|
|
6221
6628
|
needsApproval: false,
|
|
6222
6629
|
description: "Read a line range from a file stored in this session's files \u2014 including offloaded tool outputs (tool-output-*.txt) and uploaded documents. Use offset (1-based line number) and limit to page through large files instead of reading everything at once.",
|
|
6223
|
-
inputSchema:
|
|
6224
|
-
filename:
|
|
6225
|
-
offset:
|
|
6226
|
-
limit:
|
|
6630
|
+
inputSchema: z12.object({
|
|
6631
|
+
filename: z12.string().describe('Exact session file name as referenced in a truncation notice, e.g. "tool-output-web_search-a1b2c3d4.txt"'),
|
|
6632
|
+
offset: z12.number().int().min(1).optional().describe("1-based first line to read (default 1)"),
|
|
6633
|
+
limit: z12.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT})`)
|
|
6227
6634
|
}),
|
|
6228
6635
|
type: "function",
|
|
6229
6636
|
category: "session",
|
|
@@ -6236,7 +6643,7 @@ var createSessionFileReadTool = ({
|
|
|
6236
6643
|
};
|
|
6237
6644
|
|
|
6238
6645
|
// src/templates/tools/parse-document-tool.ts
|
|
6239
|
-
import { z as
|
|
6646
|
+
import { z as z13 } from "zod";
|
|
6240
6647
|
import { extname } from "path";
|
|
6241
6648
|
import { parseOfficeAsync } from "officeparser";
|
|
6242
6649
|
|
|
@@ -6399,11 +6806,11 @@ ${text.trim()}`).join("\n");
|
|
|
6399
6806
|
name: "parse_document",
|
|
6400
6807
|
needsApproval: false,
|
|
6401
6808
|
description: `Extract the text of an uploaded PDF or Office document from this session's files, with "--- page N ---" markers for PDFs so you can locate content by page. Free and fast (no OCR): works only on documents with a real text layer. To SEE a page or an image inside a document, use view_document_page.`,
|
|
6402
|
-
inputSchema:
|
|
6403
|
-
filename:
|
|
6404
|
-
pages:
|
|
6405
|
-
offset:
|
|
6406
|
-
limit:
|
|
6809
|
+
inputSchema: z13.object({
|
|
6810
|
+
filename: z13.string().describe('Exact session file name, e.g. "report.pdf"'),
|
|
6811
|
+
pages: z13.string().optional().describe('PDF page or range to extract, e.g. "2" or "1-5" (default: all pages) (PDF only)'),
|
|
6812
|
+
offset: z13.number().int().min(1).optional().describe("1-based first output line to read (default 1)"),
|
|
6813
|
+
limit: z13.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT2})`)
|
|
6407
6814
|
}),
|
|
6408
6815
|
type: "function",
|
|
6409
6816
|
category: "session",
|
|
@@ -6416,7 +6823,7 @@ ${text.trim()}`).join("\n");
|
|
|
6416
6823
|
};
|
|
6417
6824
|
|
|
6418
6825
|
// src/templates/tools/view-document-page-tool.ts
|
|
6419
|
-
import { z as
|
|
6826
|
+
import { z as z14 } from "zod";
|
|
6420
6827
|
import { extname as extname3 } from "path";
|
|
6421
6828
|
|
|
6422
6829
|
// src/sessions/pdf-preview-cache.ts
|
|
@@ -6686,9 +7093,9 @@ var createViewDocumentPageTool = ({
|
|
|
6686
7093
|
name: "view_document_page",
|
|
6687
7094
|
needsApproval: false,
|
|
6688
7095
|
description: "LOOK at a page of an uploaded PDF/Office document, or at an uploaded image, from this session's files. The rendered image is attached as a user message directly after this tool result so you can visually analyze photos, charts, scans, and layouts. Use parse_document first to find which page you need. Requires a vision-capable model.",
|
|
6689
|
-
inputSchema:
|
|
6690
|
-
filename:
|
|
6691
|
-
page:
|
|
7096
|
+
inputSchema: z14.object({
|
|
7097
|
+
filename: z14.string().describe('Exact session file name, e.g. "report.pdf" or "screenshot.png"'),
|
|
7098
|
+
page: z14.number().int().min(1).optional().describe("Page number to render (default 1; ignored for image files)")
|
|
6692
7099
|
}),
|
|
6693
7100
|
type: "function",
|
|
6694
7101
|
category: "session",
|
|
@@ -6698,9 +7105,503 @@ var createViewDocumentPageTool = ({
|
|
|
6698
7105
|
});
|
|
6699
7106
|
};
|
|
6700
7107
|
|
|
7108
|
+
// src/exulu/audit/config.ts
|
|
7109
|
+
import os from "os";
|
|
7110
|
+
import path from "path";
|
|
7111
|
+
var normalizePrefix = (p) => {
|
|
7112
|
+
const raw = (p ?? "audit").trim().replace(/^\/+|\/+$/g, "");
|
|
7113
|
+
return `${raw || "audit"}/`;
|
|
7114
|
+
};
|
|
7115
|
+
var hasAllS3Fields = (t) => !!t && !!t.s3region && !!t.s3key && !!t.s3secret && !!t.s3Bucket;
|
|
7116
|
+
var resolveAuditConfig = (config) => {
|
|
7117
|
+
const a = config.audit;
|
|
7118
|
+
if (!a || a.enabled !== true) return null;
|
|
7119
|
+
const dedicated = hasAllS3Fields(a.s3);
|
|
7120
|
+
const source = dedicated ? a.s3 : config.fileUploads;
|
|
7121
|
+
if (!hasAllS3Fields(source)) {
|
|
7122
|
+
throw new Error(
|
|
7123
|
+
"[EXULU] audit.enabled is true but no S3 target is configured. Set config.audit.s3 or config.fileUploads."
|
|
7124
|
+
);
|
|
7125
|
+
}
|
|
7126
|
+
if (!Number.isInteger(a.retentionDays) || a.retentionDays <= 0) {
|
|
7127
|
+
throw new Error(`[EXULU] audit.retentionDays must be a positive integer, got ${a.retentionDays}.`);
|
|
7128
|
+
}
|
|
7129
|
+
const usingSharedFileUploadsBucket = !dedicated;
|
|
7130
|
+
return {
|
|
7131
|
+
target: {
|
|
7132
|
+
s3region: source.s3region,
|
|
7133
|
+
s3key: source.s3key,
|
|
7134
|
+
s3secret: source.s3secret,
|
|
7135
|
+
s3Bucket: source.s3Bucket,
|
|
7136
|
+
s3prefix: normalizePrefix(source.s3prefix),
|
|
7137
|
+
...source.s3endpoint ? { s3endpoint: source.s3endpoint } : {}
|
|
7138
|
+
},
|
|
7139
|
+
retentionDays: a.retentionDays,
|
|
7140
|
+
manageLifecycle: a.manageLifecycle ?? !usingSharedFileUploadsBucket,
|
|
7141
|
+
usingSharedFileUploadsBucket,
|
|
7142
|
+
spoolDir: a.spoolDir ?? path.join(os.tmpdir(), "exulu-audit-spool"),
|
|
7143
|
+
flush: {
|
|
7144
|
+
maxRecords: a.flush?.maxRecords ?? 100,
|
|
7145
|
+
maxIntervalMs: a.flush?.maxIntervalMs ?? 5e3
|
|
7146
|
+
},
|
|
7147
|
+
payload: {
|
|
7148
|
+
maxBytes: a.payload?.maxBytes ?? 32768,
|
|
7149
|
+
captureOutput: a.payload?.captureOutput ?? true,
|
|
7150
|
+
redactKeys: a.payload?.redactKeys ?? []
|
|
7151
|
+
},
|
|
7152
|
+
failureMode: a.failureMode ?? "open",
|
|
7153
|
+
toolCalls: {
|
|
7154
|
+
enabled: a.sources?.toolCalls?.enabled ?? true,
|
|
7155
|
+
include: a.sources?.toolCalls?.include ?? [],
|
|
7156
|
+
exclude: a.sources?.toolCalls?.exclude ?? []
|
|
7157
|
+
}
|
|
7158
|
+
};
|
|
7159
|
+
};
|
|
7160
|
+
|
|
7161
|
+
// src/exulu/audit/s3-writer.ts
|
|
7162
|
+
import {
|
|
7163
|
+
S3Client as S3Client2,
|
|
7164
|
+
PutObjectCommand as PutObjectCommand2,
|
|
7165
|
+
GetBucketLifecycleConfigurationCommand,
|
|
7166
|
+
PutBucketLifecycleConfigurationCommand
|
|
7167
|
+
} from "@aws-sdk/client-s3";
|
|
7168
|
+
var RETRYABLE = /* @__PURE__ */ new Set(["SignatureDoesNotMatch", "InvalidAccessKeyId", "AccessDenied"]);
|
|
7169
|
+
var buildAuditS3Client = (t) => new S3Client2({
|
|
7170
|
+
region: t.s3region,
|
|
7171
|
+
...t.s3endpoint ? { forcePathStyle: true, endpoint: t.s3endpoint } : {},
|
|
7172
|
+
credentials: { accessKeyId: t.s3key, secretAccessKey: t.s3secret },
|
|
7173
|
+
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
7174
|
+
responseChecksumValidation: "WHEN_REQUIRED"
|
|
7175
|
+
});
|
|
7176
|
+
var createAuditS3Writer = (target, client, opts) => {
|
|
7177
|
+
const c = client ?? buildAuditS3Client(target);
|
|
7178
|
+
const maxRetries = opts?.maxRetries ?? 3;
|
|
7179
|
+
const backoffMs = opts?.backoffMs ?? ((attempt) => Math.pow(2, attempt) * 1e3);
|
|
7180
|
+
const putNdjson = async (key, body) => {
|
|
7181
|
+
let lastError = null;
|
|
7182
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
7183
|
+
const command = new PutObjectCommand2({
|
|
7184
|
+
Bucket: target.s3Bucket,
|
|
7185
|
+
Key: key,
|
|
7186
|
+
Body: Buffer.from(body, "utf8"),
|
|
7187
|
+
ContentType: "application/x-ndjson"
|
|
7188
|
+
});
|
|
7189
|
+
try {
|
|
7190
|
+
await c.send(command);
|
|
7191
|
+
return;
|
|
7192
|
+
} catch (error) {
|
|
7193
|
+
lastError = error;
|
|
7194
|
+
if (RETRYABLE.has(error?.name) && attempt < maxRetries) {
|
|
7195
|
+
await new Promise((r) => setTimeout(r, backoffMs(attempt)));
|
|
7196
|
+
continue;
|
|
7197
|
+
}
|
|
7198
|
+
throw error;
|
|
7199
|
+
}
|
|
7200
|
+
}
|
|
7201
|
+
if (lastError) throw lastError;
|
|
7202
|
+
};
|
|
7203
|
+
const getLifecycle = async () => c.send(new GetBucketLifecycleConfigurationCommand({ Bucket: target.s3Bucket }));
|
|
7204
|
+
const putLifecycle = async (config) => {
|
|
7205
|
+
await c.send(
|
|
7206
|
+
new PutBucketLifecycleConfigurationCommand({
|
|
7207
|
+
Bucket: target.s3Bucket,
|
|
7208
|
+
LifecycleConfiguration: config
|
|
7209
|
+
})
|
|
7210
|
+
);
|
|
7211
|
+
};
|
|
7212
|
+
return { putNdjson, getLifecycle, putLifecycle };
|
|
7213
|
+
};
|
|
7214
|
+
|
|
7215
|
+
// src/exulu/audit/lifecycle.ts
|
|
7216
|
+
var AUDIT_LIFECYCLE_RULE_ID = "exulu-audit-retention";
|
|
7217
|
+
var buildRule = (prefix, retentionDays) => ({
|
|
7218
|
+
ID: AUDIT_LIFECYCLE_RULE_ID,
|
|
7219
|
+
Filter: { Prefix: prefix },
|
|
7220
|
+
Status: "Enabled",
|
|
7221
|
+
Expiration: { Days: retentionDays }
|
|
7222
|
+
});
|
|
7223
|
+
var applyRetentionLifecycle = async (writer, opts) => {
|
|
7224
|
+
const rule = buildRule(opts.prefix, opts.retentionDays);
|
|
7225
|
+
const config = { Rules: [rule] };
|
|
7226
|
+
if (!opts.manage) {
|
|
7227
|
+
console.warn(
|
|
7228
|
+
`[EXULU] audit retention: not managing the S3 lifecycle for this bucket. Apply this rule manually:
|
|
7229
|
+
${JSON.stringify(config, null, 2)}`
|
|
7230
|
+
);
|
|
7231
|
+
return;
|
|
7232
|
+
}
|
|
7233
|
+
try {
|
|
7234
|
+
let existing = [];
|
|
7235
|
+
try {
|
|
7236
|
+
const current = await writer.getLifecycle();
|
|
7237
|
+
existing = (current?.Rules ?? []).filter((r) => r.ID !== AUDIT_LIFECYCLE_RULE_ID);
|
|
7238
|
+
} catch (error) {
|
|
7239
|
+
if (error?.name !== "NoSuchLifecycleConfiguration") throw error;
|
|
7240
|
+
}
|
|
7241
|
+
await writer.putLifecycle({ Rules: [...existing, rule] });
|
|
7242
|
+
console.log(`[EXULU] audit retention: S3 lifecycle set to expire "${opts.prefix}" after ${opts.retentionDays} days.`);
|
|
7243
|
+
} catch (error) {
|
|
7244
|
+
console.warn(
|
|
7245
|
+
`[EXULU] audit retention: could not set the S3 lifecycle (${error?.name ?? "error"}). Apply this rule manually:
|
|
7246
|
+
${JSON.stringify(config, null, 2)}`
|
|
7247
|
+
);
|
|
7248
|
+
}
|
|
7249
|
+
};
|
|
7250
|
+
|
|
7251
|
+
// src/exulu/audit/sink.ts
|
|
7252
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
7253
|
+
import { promises as fs2 } from "fs";
|
|
7254
|
+
import path2 from "path";
|
|
7255
|
+
var createFsSpoolStore = (dir) => ({
|
|
7256
|
+
write: async (name, body) => {
|
|
7257
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
7258
|
+
await fs2.writeFile(path2.join(dir, name), body, "utf8");
|
|
7259
|
+
},
|
|
7260
|
+
list: async () => {
|
|
7261
|
+
try {
|
|
7262
|
+
return (await fs2.readdir(dir)).filter((f) => f.endsWith(".ndjson"));
|
|
7263
|
+
} catch {
|
|
7264
|
+
return [];
|
|
7265
|
+
}
|
|
7266
|
+
},
|
|
7267
|
+
read: async (name) => fs2.readFile(path2.join(dir, name), "utf8"),
|
|
7268
|
+
remove: async (name) => {
|
|
7269
|
+
await fs2.rm(path2.join(dir, name), { force: true });
|
|
7270
|
+
}
|
|
7271
|
+
});
|
|
7272
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
7273
|
+
var AuditSink = class {
|
|
7274
|
+
constructor(cfg, writer, spool, opts) {
|
|
7275
|
+
this.cfg = cfg;
|
|
7276
|
+
this.writer = writer;
|
|
7277
|
+
this.spool = spool;
|
|
7278
|
+
this.now = opts?.now ?? (() => /* @__PURE__ */ new Date());
|
|
7279
|
+
}
|
|
7280
|
+
buffer = [];
|
|
7281
|
+
timer = null;
|
|
7282
|
+
now;
|
|
7283
|
+
objectKey() {
|
|
7284
|
+
const d = this.now();
|
|
7285
|
+
const dt = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
|
|
7286
|
+
return `${this.cfg.target.s3prefix}dt=${dt}/${pad(d.getUTCHours())}/${d.getTime()}-${randomUUID4()}.ndjson`;
|
|
7287
|
+
}
|
|
7288
|
+
serialize(events) {
|
|
7289
|
+
return events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
7290
|
+
}
|
|
7291
|
+
record(event) {
|
|
7292
|
+
this.buffer.push(event);
|
|
7293
|
+
if (this.buffer.length >= this.cfg.flush.maxRecords) {
|
|
7294
|
+
void this.flush();
|
|
7295
|
+
} else if (!this.timer) {
|
|
7296
|
+
this.timer = setTimeout(() => void this.flush(), this.cfg.flush.maxIntervalMs);
|
|
7297
|
+
this.timer.unref?.();
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7300
|
+
async flush() {
|
|
7301
|
+
if (this.timer) {
|
|
7302
|
+
clearTimeout(this.timer);
|
|
7303
|
+
this.timer = null;
|
|
7304
|
+
}
|
|
7305
|
+
if (this.buffer.length === 0) return;
|
|
7306
|
+
const batch = this.buffer;
|
|
7307
|
+
this.buffer = [];
|
|
7308
|
+
const body = this.serialize(batch);
|
|
7309
|
+
try {
|
|
7310
|
+
await this.writer.putNdjson(this.objectKey(), body);
|
|
7311
|
+
await this.drainSpool();
|
|
7312
|
+
} catch (error) {
|
|
7313
|
+
const name = `${Date.now()}-${randomUUID4()}.ndjson`;
|
|
7314
|
+
try {
|
|
7315
|
+
await this.spool.write(name, body);
|
|
7316
|
+
console.warn(`[EXULU] audit: S3 write failed, spooled ${batch.length} record(s) to disk (${name}).`, error);
|
|
7317
|
+
} catch (spoolError) {
|
|
7318
|
+
console.error(`[EXULU] audit: S3 write AND local spool failed \u2014 ${batch.length} record(s) lost.`, spoolError);
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
async drainSpool() {
|
|
7323
|
+
const names = await this.spool.list();
|
|
7324
|
+
for (const name of names) {
|
|
7325
|
+
try {
|
|
7326
|
+
const body = await this.spool.read(name);
|
|
7327
|
+
await this.writer.putNdjson(this.objectKey(), body);
|
|
7328
|
+
await this.spool.remove(name);
|
|
7329
|
+
} catch {
|
|
7330
|
+
return;
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
7333
|
+
}
|
|
7334
|
+
async recordDurable(event) {
|
|
7335
|
+
await this.writer.putNdjson(this.objectKey(), this.serialize([event]));
|
|
7336
|
+
}
|
|
7337
|
+
async close() {
|
|
7338
|
+
await this.flush();
|
|
7339
|
+
}
|
|
7340
|
+
};
|
|
7341
|
+
|
|
7342
|
+
// src/exulu/audit/event.ts
|
|
7343
|
+
var AUDIT_EVENT_TYPES = {
|
|
7344
|
+
TOOL_CALL: "tool.call"
|
|
7345
|
+
};
|
|
7346
|
+
|
|
7347
|
+
// src/exulu/audit/redact.ts
|
|
7348
|
+
var SECRET_KEY_DENYLIST = [
|
|
7349
|
+
"oauth",
|
|
7350
|
+
"credentials",
|
|
7351
|
+
"accesstoken",
|
|
7352
|
+
"refreshtoken",
|
|
7353
|
+
"password",
|
|
7354
|
+
"secret",
|
|
7355
|
+
"token",
|
|
7356
|
+
"apikey",
|
|
7357
|
+
"authorization",
|
|
7358
|
+
"nonce"
|
|
7359
|
+
];
|
|
7360
|
+
var FRAMEWORK_INTERNAL_KEYS = /* @__PURE__ */ new Set([
|
|
7361
|
+
"req",
|
|
7362
|
+
"model",
|
|
7363
|
+
"contexts",
|
|
7364
|
+
"upload",
|
|
7365
|
+
"memory",
|
|
7366
|
+
"exuluConfig",
|
|
7367
|
+
"toolVariablesConfig",
|
|
7368
|
+
"allExuluTools",
|
|
7369
|
+
"currentTools",
|
|
7370
|
+
"sessionItems",
|
|
7371
|
+
"audit"
|
|
7372
|
+
]);
|
|
7373
|
+
var isSecretKey = (key, extra) => {
|
|
7374
|
+
const k = key.toLowerCase();
|
|
7375
|
+
if (extra.some((e) => k.includes(e.toLowerCase()))) return true;
|
|
7376
|
+
return SECRET_KEY_DENYLIST.some((term) => k.includes(term));
|
|
7377
|
+
};
|
|
7378
|
+
var redact = (value, redactKeys, seen) => {
|
|
7379
|
+
if (value === null || typeof value !== "object") return value;
|
|
7380
|
+
if (seen.has(value)) return "[circular]";
|
|
7381
|
+
seen.add(value);
|
|
7382
|
+
if (Array.isArray(value)) return value.map((v) => redact(v, redactKeys, seen));
|
|
7383
|
+
const out = {};
|
|
7384
|
+
for (const [key, val] of Object.entries(value)) {
|
|
7385
|
+
if (FRAMEWORK_INTERNAL_KEYS.has(key)) continue;
|
|
7386
|
+
if (isSecretKey(key, redactKeys)) {
|
|
7387
|
+
if (val !== null && typeof val === "object") {
|
|
7388
|
+
out[key] = "[redacted]";
|
|
7389
|
+
}
|
|
7390
|
+
continue;
|
|
7391
|
+
}
|
|
7392
|
+
out[key] = redact(val, redactKeys, seen);
|
|
7393
|
+
}
|
|
7394
|
+
return out;
|
|
7395
|
+
};
|
|
7396
|
+
var sanitizeData = (value, opts) => {
|
|
7397
|
+
let cleaned;
|
|
7398
|
+
try {
|
|
7399
|
+
cleaned = redact(value, opts.redactKeys ?? [], /* @__PURE__ */ new WeakSet());
|
|
7400
|
+
} catch {
|
|
7401
|
+
cleaned = "[unserializable]";
|
|
7402
|
+
}
|
|
7403
|
+
let serialized;
|
|
7404
|
+
try {
|
|
7405
|
+
serialized = JSON.stringify(cleaned) ?? "";
|
|
7406
|
+
} catch {
|
|
7407
|
+
return { value: "[unserializable]", truncated: false };
|
|
7408
|
+
}
|
|
7409
|
+
if (serialized.length <= opts.maxBytes) return { value: cleaned, truncated: false };
|
|
7410
|
+
return {
|
|
7411
|
+
value: { _truncated: true, preview: serialized.slice(0, opts.maxBytes) },
|
|
7412
|
+
truncated: true
|
|
7413
|
+
};
|
|
7414
|
+
};
|
|
7415
|
+
|
|
7416
|
+
// src/exulu/auth/describe.ts
|
|
7417
|
+
var describeCredentialIdentity = async (auth, userId, toolId) => {
|
|
7418
|
+
const provider = providerKeyFor(toolId ?? auth.provider, auth);
|
|
7419
|
+
const base = {
|
|
7420
|
+
provider,
|
|
7421
|
+
authType: auth.authType,
|
|
7422
|
+
account: String(userId)
|
|
7423
|
+
};
|
|
7424
|
+
if (auth.authType !== "oauth") return base;
|
|
7425
|
+
try {
|
|
7426
|
+
const row = await credentialStore.get(provider, userId);
|
|
7427
|
+
if (!row || row.authType !== "oauth") return base;
|
|
7428
|
+
const { scopes, expiresAt } = row.data;
|
|
7429
|
+
return {
|
|
7430
|
+
...base,
|
|
7431
|
+
...scopes ? { scopes: scopes.split(" ").filter(Boolean) } : {},
|
|
7432
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
7433
|
+
};
|
|
7434
|
+
} catch (error) {
|
|
7435
|
+
console.error(`[EXULU] describeCredentialIdentity failed for provider "${provider}":`, error);
|
|
7436
|
+
return base;
|
|
7437
|
+
}
|
|
7438
|
+
};
|
|
7439
|
+
|
|
7440
|
+
// src/exulu/audit/emitters/tool-call.ts
|
|
7441
|
+
var str = (v) => v === void 0 || v === null ? void 0 : String(v);
|
|
7442
|
+
var isAuthShortCircuit = (output) => {
|
|
7443
|
+
if (!output || typeof output !== "object") return false;
|
|
7444
|
+
const o = output;
|
|
7445
|
+
return !!o.credentialRequest || !!o.oauth?.authorizationUrl;
|
|
7446
|
+
};
|
|
7447
|
+
var buildToolCallEvent = async (ctx, opts) => {
|
|
7448
|
+
const nowIso = opts.nowIso ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
7449
|
+
const status = ctx.status === "error" ? "error" : isAuthShortCircuit(ctx.output) ? "auth_required" : "ok";
|
|
7450
|
+
const input = sanitizeData(ctx.input, { maxBytes: opts.maxBytes, redactKeys: opts.redactKeys });
|
|
7451
|
+
const data = { input: input.value };
|
|
7452
|
+
const truncated = {};
|
|
7453
|
+
if (input.truncated) truncated.input = true;
|
|
7454
|
+
if (opts.captureOutput && status !== "auth_required") {
|
|
7455
|
+
const output = sanitizeData(ctx.output, { maxBytes: opts.maxBytes, redactKeys: opts.redactKeys });
|
|
7456
|
+
data.output = output.value;
|
|
7457
|
+
if (output.truncated) truncated.output = true;
|
|
7458
|
+
}
|
|
7459
|
+
let credential;
|
|
7460
|
+
if (ctx.tool.authentication && ctx.user?.id != null) {
|
|
7461
|
+
credential = await describeCredentialIdentity(
|
|
7462
|
+
ctx.tool.authentication,
|
|
7463
|
+
Number(ctx.user.id),
|
|
7464
|
+
ctx.tool.id
|
|
7465
|
+
);
|
|
7466
|
+
}
|
|
7467
|
+
const err = ctx.error;
|
|
7468
|
+
return {
|
|
7469
|
+
v: 1,
|
|
7470
|
+
ts: nowIso(),
|
|
7471
|
+
type: AUDIT_EVENT_TYPES.TOOL_CALL,
|
|
7472
|
+
actor: {
|
|
7473
|
+
kind: "user",
|
|
7474
|
+
userId: str(ctx.user?.id),
|
|
7475
|
+
email: ctx.user?.email,
|
|
7476
|
+
roleId: str(ctx.user?.role?.id),
|
|
7477
|
+
projectId: ctx.projectId
|
|
7478
|
+
},
|
|
7479
|
+
context: {
|
|
7480
|
+
sessionId: ctx.sessionID,
|
|
7481
|
+
agentId: ctx.agent?.id,
|
|
7482
|
+
agentName: ctx.agent?.name,
|
|
7483
|
+
toolCallId: ctx.toolCallId
|
|
7484
|
+
},
|
|
7485
|
+
target: { kind: "tool", id: ctx.tool.id, name: ctx.tool.name, category: ctx.tool.category, builtin: ctx.builtin },
|
|
7486
|
+
...credential ? { credential } : {},
|
|
7487
|
+
status,
|
|
7488
|
+
...status === "error" ? { error: { name: err?.name, message: String(err?.message ?? err ?? "unknown error") } } : {},
|
|
7489
|
+
data,
|
|
7490
|
+
durationMs: ctx.durationMs,
|
|
7491
|
+
...Object.keys(truncated).length ? { truncated } : {}
|
|
7492
|
+
};
|
|
7493
|
+
};
|
|
7494
|
+
|
|
7495
|
+
// src/exulu/audit/logger.ts
|
|
7496
|
+
var noop = {
|
|
7497
|
+
enabled: false,
|
|
7498
|
+
failClosed: false,
|
|
7499
|
+
isBuiltin: () => false,
|
|
7500
|
+
shouldAuditTool: () => false,
|
|
7501
|
+
record: () => {
|
|
7502
|
+
},
|
|
7503
|
+
recordToolCall: async () => {
|
|
7504
|
+
},
|
|
7505
|
+
flush: async () => {
|
|
7506
|
+
},
|
|
7507
|
+
close: async () => {
|
|
7508
|
+
}
|
|
7509
|
+
};
|
|
7510
|
+
var RealAuditLogger = class {
|
|
7511
|
+
constructor(resolved, builtinToolIds) {
|
|
7512
|
+
this.resolved = resolved;
|
|
7513
|
+
this.builtinToolIds = builtinToolIds;
|
|
7514
|
+
this.failClosed = resolved.failureMode === "closed";
|
|
7515
|
+
const writer = createAuditS3Writer(resolved.target);
|
|
7516
|
+
this.sink = new AuditSink(resolved, writer, createFsSpoolStore(resolved.spoolDir));
|
|
7517
|
+
}
|
|
7518
|
+
enabled = true;
|
|
7519
|
+
failClosed;
|
|
7520
|
+
sink;
|
|
7521
|
+
lifecycleWriter() {
|
|
7522
|
+
return createAuditS3Writer(this.resolved.target);
|
|
7523
|
+
}
|
|
7524
|
+
isBuiltin(id) {
|
|
7525
|
+
return this.builtinToolIds.has(id);
|
|
7526
|
+
}
|
|
7527
|
+
shouldAuditTool(id) {
|
|
7528
|
+
const t = this.resolved.toolCalls;
|
|
7529
|
+
if (!t.enabled) return false;
|
|
7530
|
+
if (t.exclude.includes(id)) return false;
|
|
7531
|
+
if (t.include.length > 0) return t.include.includes(id);
|
|
7532
|
+
return true;
|
|
7533
|
+
}
|
|
7534
|
+
record(event) {
|
|
7535
|
+
this.sink.record(event);
|
|
7536
|
+
}
|
|
7537
|
+
async recordToolCall(ctx) {
|
|
7538
|
+
const event = await buildToolCallEvent(ctx, {
|
|
7539
|
+
maxBytes: this.resolved.payload.maxBytes,
|
|
7540
|
+
captureOutput: this.resolved.payload.captureOutput,
|
|
7541
|
+
redactKeys: this.resolved.payload.redactKeys
|
|
7542
|
+
});
|
|
7543
|
+
if (this.failClosed) await this.sink.recordDurable(event);
|
|
7544
|
+
else this.sink.record(event);
|
|
7545
|
+
}
|
|
7546
|
+
flush() {
|
|
7547
|
+
return this.sink.flush();
|
|
7548
|
+
}
|
|
7549
|
+
close() {
|
|
7550
|
+
return this.sink.close();
|
|
7551
|
+
}
|
|
7552
|
+
get resolvedConfig() {
|
|
7553
|
+
return this.resolved;
|
|
7554
|
+
}
|
|
7555
|
+
};
|
|
7556
|
+
var _instance;
|
|
7557
|
+
var _signalClose;
|
|
7558
|
+
var build = (config, builtinToolIds) => {
|
|
7559
|
+
const resolved = resolveAuditConfig(config);
|
|
7560
|
+
return resolved ? new RealAuditLogger(resolved, builtinToolIds) : noop;
|
|
7561
|
+
};
|
|
7562
|
+
var getAuditLogger = (config) => {
|
|
7563
|
+
if (!_instance) _instance = build(config, /* @__PURE__ */ new Set());
|
|
7564
|
+
return _instance;
|
|
7565
|
+
};
|
|
7566
|
+
var initAudit = async (config, opts) => {
|
|
7567
|
+
_instance = build(config, opts?.builtinToolIds ?? /* @__PURE__ */ new Set());
|
|
7568
|
+
if (_instance instanceof RealAuditLogger) {
|
|
7569
|
+
const r = _instance.resolvedConfig;
|
|
7570
|
+
await applyRetentionLifecycle(_instance.lifecycleWriter(), {
|
|
7571
|
+
prefix: r.target.s3prefix,
|
|
7572
|
+
retentionDays: r.retentionDays,
|
|
7573
|
+
manage: r.manageLifecycle
|
|
7574
|
+
});
|
|
7575
|
+
if (_signalClose) {
|
|
7576
|
+
process.off("SIGTERM", _signalClose);
|
|
7577
|
+
process.off("SIGINT", _signalClose);
|
|
7578
|
+
}
|
|
7579
|
+
const close = () => {
|
|
7580
|
+
void _instance?.close();
|
|
7581
|
+
};
|
|
7582
|
+
_signalClose = close;
|
|
7583
|
+
process.on("SIGTERM", close);
|
|
7584
|
+
process.on("SIGINT", close);
|
|
7585
|
+
}
|
|
7586
|
+
return _instance;
|
|
7587
|
+
};
|
|
7588
|
+
|
|
7589
|
+
// src/exulu/audit/emit-tool-call.ts
|
|
7590
|
+
var emitToolCallAudit = async (logger, ctx) => {
|
|
7591
|
+
if (!logger.shouldAuditTool(ctx.tool.id)) return;
|
|
7592
|
+
const full = { ...ctx, builtin: logger.isBuiltin(ctx.tool.id) };
|
|
7593
|
+
if (logger.failClosed) {
|
|
7594
|
+
await logger.recordToolCall(full);
|
|
7595
|
+
return;
|
|
7596
|
+
}
|
|
7597
|
+
logger.recordToolCall(full).catch(
|
|
7598
|
+
(error) => console.error(`[EXULU] audit: recordToolCall failed for tool "${ctx.tool.id}":`, error)
|
|
7599
|
+
);
|
|
7600
|
+
};
|
|
7601
|
+
|
|
6701
7602
|
// src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
|
|
6702
7603
|
var OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS = /* @__PURE__ */ new Set(["agentic_context_search"]);
|
|
6703
|
-
var generateS3Key = (filename) => `${
|
|
7604
|
+
var generateS3Key = (filename) => `${randomUUID5()}-${filename}`;
|
|
6704
7605
|
var s3Client2;
|
|
6705
7606
|
var getMimeType = (type) => {
|
|
6706
7607
|
switch (type) {
|
|
@@ -6789,8 +7690,8 @@ var hydrateVariables = async (tool3) => {
|
|
|
6789
7690
|
}
|
|
6790
7691
|
let value = variable.value;
|
|
6791
7692
|
if (variable.encrypted) {
|
|
6792
|
-
const bytes =
|
|
6793
|
-
value = bytes.toString(
|
|
7693
|
+
const bytes = CryptoJS4.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
7694
|
+
value = bytes.toString(CryptoJS4.enc.Utf8);
|
|
6794
7695
|
}
|
|
6795
7696
|
toolConfig.value = value;
|
|
6796
7697
|
return toolConfig;
|
|
@@ -6798,7 +7699,7 @@ var hydrateVariables = async (tool3) => {
|
|
|
6798
7699
|
await Promise.all(promises);
|
|
6799
7700
|
return tool3;
|
|
6800
7701
|
};
|
|
6801
|
-
var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs,
|
|
7702
|
+
var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems, contextWindow, disabledTools) => {
|
|
6802
7703
|
if (!currentTools) return {};
|
|
6803
7704
|
if (!allExuluTools) {
|
|
6804
7705
|
allExuluTools = [];
|
|
@@ -6863,6 +7764,11 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
6863
7764
|
currentTools.push(createNewMemoryTool);
|
|
6864
7765
|
}
|
|
6865
7766
|
}
|
|
7767
|
+
for (const kbWriteTool of collectKbWriteTools(agent, contexts)) {
|
|
7768
|
+
if (!disabled.has(kbWriteTool.id)) {
|
|
7769
|
+
currentTools.push(kbWriteTool);
|
|
7770
|
+
}
|
|
7771
|
+
}
|
|
6866
7772
|
console.log("[EXULU] Convert tools array to object, session items", sessionItems);
|
|
6867
7773
|
if (sessionItems) {
|
|
6868
7774
|
const sessionItemsRetrievalTool = await createSessionItemsRetrievalTool({
|
|
@@ -7015,6 +7921,10 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
7015
7921
|
// Vercel AI SDK uses the sanitized tool name as the key, so this matches.
|
|
7016
7922
|
needsApproval: approvedTools?.includes("tool-" + cur.name) || !cur.needsApproval ? false : true,
|
|
7017
7923
|
// todo make configurable
|
|
7924
|
+
// Auth-wrapped tools: the model sees scrub text instead of the
|
|
7925
|
+
// credentialRequest/oauth payload; the UI stream keeps the raw
|
|
7926
|
+
// output (spec 2026-07-22 §1.2).
|
|
7927
|
+
...cur.authentication ? { toModelOutput: buildAuthToolModelOutput(cur) } : {},
|
|
7018
7928
|
async *execute(inputs, options) {
|
|
7019
7929
|
console.log(
|
|
7020
7930
|
"[EXULU] Executing tool",
|
|
@@ -7024,124 +7934,162 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
7024
7934
|
"and options",
|
|
7025
7935
|
options
|
|
7026
7936
|
);
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
type
|
|
7051
|
-
}) => {
|
|
7052
|
-
const mime = getMimeType(type);
|
|
7053
|
-
const prefix = exuluConfig?.fileUploads?.s3prefix ? `${exuluConfig.fileUploads.s3prefix.replace(/\/$/, "")}/` : "";
|
|
7054
|
-
const key = `${prefix}${user?.id}/${generateS3Key(name)}${type}`;
|
|
7055
|
-
const command = new PutObjectCommand2({
|
|
7056
|
-
Bucket: exuluConfig?.fileUploads?.s3Bucket,
|
|
7057
|
-
Key: key,
|
|
7058
|
-
Body: data,
|
|
7059
|
-
ContentType: mime
|
|
7060
|
-
});
|
|
7061
|
-
try {
|
|
7062
|
-
if (!s3Client2) {
|
|
7063
|
-
throw new Error("S3 client not initialized");
|
|
7937
|
+
const __auditStart = Date.now();
|
|
7938
|
+
let __auditOutput;
|
|
7939
|
+
let __auditStatus = "ok";
|
|
7940
|
+
let __auditError;
|
|
7941
|
+
try {
|
|
7942
|
+
if (!cur.tool?.execute) {
|
|
7943
|
+
console.error("[EXULU] Tool execute function is undefined.", cur.tool);
|
|
7944
|
+
throw new Error("Tool execute function is undefined.");
|
|
7945
|
+
}
|
|
7946
|
+
if (toolVariableConfig) {
|
|
7947
|
+
toolVariableConfig = await hydrateVariables(toolVariableConfig || []);
|
|
7948
|
+
}
|
|
7949
|
+
let upload = void 0;
|
|
7950
|
+
if (exuluConfig?.fileUploads?.s3endpoint && exuluConfig?.fileUploads?.s3key && exuluConfig?.fileUploads?.s3secret && exuluConfig?.fileUploads?.s3Bucket) {
|
|
7951
|
+
s3Client2 ??= new S3Client3({
|
|
7952
|
+
region: exuluConfig?.fileUploads?.s3region,
|
|
7953
|
+
...exuluConfig?.fileUploads?.s3endpoint && {
|
|
7954
|
+
forcePathStyle: true,
|
|
7955
|
+
endpoint: exuluConfig?.fileUploads?.s3endpoint
|
|
7956
|
+
},
|
|
7957
|
+
credentials: {
|
|
7958
|
+
accessKeyId: exuluConfig?.fileUploads?.s3key ?? "",
|
|
7959
|
+
secretAccessKey: exuluConfig?.fileUploads?.s3secret ?? ""
|
|
7064
7960
|
}
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7961
|
+
});
|
|
7962
|
+
upload = async ({
|
|
7963
|
+
name,
|
|
7964
|
+
data,
|
|
7965
|
+
type
|
|
7966
|
+
}) => {
|
|
7967
|
+
const mime = getMimeType(type);
|
|
7968
|
+
const prefix = exuluConfig?.fileUploads?.s3prefix ? `${exuluConfig.fileUploads.s3prefix.replace(/\/$/, "")}/` : "";
|
|
7969
|
+
const key = `${prefix}${user?.id}/${generateS3Key(name)}${type}`;
|
|
7970
|
+
const command = new PutObjectCommand3({
|
|
7971
|
+
Bucket: exuluConfig?.fileUploads?.s3Bucket,
|
|
7972
|
+
Key: key,
|
|
7973
|
+
Body: data,
|
|
7974
|
+
ContentType: mime
|
|
7975
|
+
});
|
|
7976
|
+
try {
|
|
7977
|
+
if (!s3Client2) {
|
|
7978
|
+
throw new Error("S3 client not initialized");
|
|
7979
|
+
}
|
|
7980
|
+
await s3Client2.send(command);
|
|
7981
|
+
const bucket = exuluConfig?.fileUploads?.s3Bucket ?? "";
|
|
7982
|
+
const presignedUrl = await getPresignedUrl(bucket, key, exuluConfig);
|
|
7983
|
+
return { url: presignedUrl, key: `${bucket}/${key}` };
|
|
7984
|
+
} catch (caught) {
|
|
7985
|
+
if (caught instanceof S3ServiceException && caught.name === "EntityTooLarge") {
|
|
7986
|
+
throw new Error(`[EXULU] Error from S3 while uploading object to ${exuluConfig?.fileUploads?.s3Bucket}. The object was too large. To upload objects larger than 5GB, use the S3 console (160GB max) or the multipart upload API (5TB max).`);
|
|
7987
|
+
} else if (caught instanceof S3ServiceException) {
|
|
7988
|
+
throw new Error(
|
|
7989
|
+
`[EXULU] Error from S3 while uploading object to ${exuluConfig?.fileUploads?.s3Bucket}. ${caught.name}: ${caught.message}`
|
|
7990
|
+
);
|
|
7991
|
+
} else {
|
|
7992
|
+
throw caught;
|
|
7993
|
+
}
|
|
7078
7994
|
}
|
|
7079
|
-
}
|
|
7080
|
-
}
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7995
|
+
};
|
|
7996
|
+
}
|
|
7997
|
+
const contextsMap = contexts?.reduce((acc, curr) => {
|
|
7998
|
+
acc[curr.id] = curr;
|
|
7999
|
+
return acc;
|
|
8000
|
+
}, {});
|
|
8001
|
+
const toolVariablesConfigData = toolVariableConfig ? toolVariableConfig.config.reduce((acc, curr) => {
|
|
8002
|
+
acc[curr.name] = curr.value;
|
|
8003
|
+
return acc;
|
|
8004
|
+
}, {}) : {};
|
|
8005
|
+
const response = await cur.tool.execute(
|
|
8006
|
+
{
|
|
8007
|
+
...inputs,
|
|
8008
|
+
model,
|
|
8009
|
+
sessionID,
|
|
8010
|
+
sessionItems,
|
|
8011
|
+
memory: memoryItems,
|
|
8012
|
+
req,
|
|
8013
|
+
// Convert config to object format if a config object
|
|
8014
|
+
// is available, after we added the .value property
|
|
8015
|
+
// by hydrating it from the variables table.
|
|
8016
|
+
allExuluTools,
|
|
8017
|
+
currentTools,
|
|
8018
|
+
user,
|
|
8019
|
+
contexts: contextsMap,
|
|
8020
|
+
upload,
|
|
8021
|
+
exuluConfig,
|
|
8022
|
+
toolVariablesConfig: toolVariablesConfigData
|
|
8023
|
+
},
|
|
8024
|
+
options
|
|
8025
|
+
);
|
|
8026
|
+
await updateStatistic({
|
|
8027
|
+
name: "count",
|
|
8028
|
+
label: cur.name,
|
|
8029
|
+
type: STATISTICS_TYPE_ENUM.TOOL_CALL,
|
|
8030
|
+
trigger: "agent",
|
|
8031
|
+
count: 1,
|
|
8032
|
+
user: user?.id,
|
|
8033
|
+
role: user?.role?.id
|
|
8034
|
+
});
|
|
8035
|
+
const guardCtx = {
|
|
8036
|
+
toolName: cur.name,
|
|
8037
|
+
contextWindow,
|
|
7094
8038
|
sessionID,
|
|
7095
|
-
sessionItems,
|
|
7096
|
-
memory: memoryItems,
|
|
7097
|
-
req,
|
|
7098
|
-
// Convert config to object format if a config object
|
|
7099
|
-
// is available, after we added the .value property
|
|
7100
|
-
// by hydrating it from the variables table.
|
|
7101
|
-
providerapikey,
|
|
7102
|
-
allExuluTools,
|
|
7103
|
-
currentTools,
|
|
7104
8039
|
user,
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
exuluConfig
|
|
7127
|
-
};
|
|
7128
|
-
const offloadExempt = OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS.has(cur.id);
|
|
7129
|
-
if (response && typeof response === "object" && Symbol.asyncIterator in response) {
|
|
7130
|
-
let lastValue;
|
|
7131
|
-
for await (const value of response) {
|
|
7132
|
-
yield value;
|
|
7133
|
-
lastValue = value;
|
|
7134
|
-
}
|
|
7135
|
-
if (offloadExempt) return lastValue;
|
|
7136
|
-
const guarded = await guardToolOutput(lastValue, guardCtx);
|
|
7137
|
-
if (guarded !== lastValue) {
|
|
8040
|
+
exuluConfig
|
|
8041
|
+
};
|
|
8042
|
+
const offloadExempt = OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS.has(cur.id);
|
|
8043
|
+
if (response && typeof response === "object" && Symbol.asyncIterator in response) {
|
|
8044
|
+
let lastValue;
|
|
8045
|
+
for await (const value of response) {
|
|
8046
|
+
yield value;
|
|
8047
|
+
lastValue = value;
|
|
8048
|
+
}
|
|
8049
|
+
if (offloadExempt) {
|
|
8050
|
+
__auditOutput = lastValue;
|
|
8051
|
+
return lastValue;
|
|
8052
|
+
}
|
|
8053
|
+
const guarded = await guardToolOutput(lastValue, guardCtx);
|
|
8054
|
+
if (guarded !== lastValue) {
|
|
8055
|
+
yield guarded;
|
|
8056
|
+
}
|
|
8057
|
+
__auditOutput = guarded;
|
|
8058
|
+
return guarded;
|
|
8059
|
+
} else {
|
|
8060
|
+
const guarded = offloadExempt ? response : await guardToolOutput(response, guardCtx);
|
|
7138
8061
|
yield guarded;
|
|
8062
|
+
__auditOutput = guarded;
|
|
8063
|
+
return guarded;
|
|
8064
|
+
}
|
|
8065
|
+
} catch (error) {
|
|
8066
|
+
__auditStatus = "error";
|
|
8067
|
+
__auditError = error;
|
|
8068
|
+
throw error;
|
|
8069
|
+
} finally {
|
|
8070
|
+
const __auditLogger = getAuditLogger(exuluConfig ?? {});
|
|
8071
|
+
if (__auditLogger.shouldAuditTool(cur.id)) {
|
|
8072
|
+
const __emit = emitToolCallAudit(__auditLogger, {
|
|
8073
|
+
durationMs: Date.now() - __auditStart,
|
|
8074
|
+
agent: agent ? { id: agent.id, name: agent.name, slug: agent.slug } : void 0,
|
|
8075
|
+
tool: { id: cur.id, name: cur.name, category: cur.category, authentication: cur.authentication },
|
|
8076
|
+
user,
|
|
8077
|
+
projectId: project ? String(project) : void 0,
|
|
8078
|
+
sessionID,
|
|
8079
|
+
toolCallId: options?.toolCallId,
|
|
8080
|
+
input: inputs,
|
|
8081
|
+
output: __auditOutput,
|
|
8082
|
+
status: __auditStatus,
|
|
8083
|
+
error: __auditError
|
|
8084
|
+
});
|
|
8085
|
+
if (__auditLogger.failClosed) {
|
|
8086
|
+
await __emit;
|
|
8087
|
+
} else {
|
|
8088
|
+
__emit.catch(
|
|
8089
|
+
(error) => console.error(`[EXULU] audit: tool-call emit failed for "${cur.id}":`, error)
|
|
8090
|
+
);
|
|
8091
|
+
}
|
|
7139
8092
|
}
|
|
7140
|
-
return guarded;
|
|
7141
|
-
} else {
|
|
7142
|
-
const guarded = offloadExempt ? response : await guardToolOutput(response, guardCtx);
|
|
7143
|
-
yield guarded;
|
|
7144
|
-
return guarded;
|
|
7145
8093
|
}
|
|
7146
8094
|
}
|
|
7147
8095
|
}
|
|
@@ -7167,6 +8115,8 @@ export {
|
|
|
7167
8115
|
getS3SignedUploadUrl,
|
|
7168
8116
|
createUppyRoutes,
|
|
7169
8117
|
sanitizeName,
|
|
8118
|
+
getTableName,
|
|
8119
|
+
getChunksTableName,
|
|
7170
8120
|
LITELLM_UI_PATH,
|
|
7171
8121
|
isLiteLLMEnabled,
|
|
7172
8122
|
setLiteLLMPackageRoot,
|
|
@@ -7191,18 +8141,21 @@ export {
|
|
|
7191
8141
|
getUserBudgetView,
|
|
7192
8142
|
updateStatistic,
|
|
7193
8143
|
checkLicense,
|
|
7194
|
-
checkRecordAccess,
|
|
7195
8144
|
ResolveModelError,
|
|
7196
8145
|
resolveModel,
|
|
7197
8146
|
exuluApp,
|
|
7198
|
-
|
|
8147
|
+
authRegistry,
|
|
7199
8148
|
encrypt,
|
|
7200
8149
|
decrypt,
|
|
7201
|
-
|
|
8150
|
+
credentialStore,
|
|
7202
8151
|
OAUTH_CALLBACK_PATH,
|
|
7203
8152
|
decryptOauthState,
|
|
7204
8153
|
exchangeCodeForTokens,
|
|
8154
|
+
verifyCredentialNonce,
|
|
8155
|
+
CredentialInvalidError,
|
|
7205
8156
|
sanitizeToolName,
|
|
8157
|
+
KB_EDITOR_TOOL_ID,
|
|
8158
|
+
createKbEditorPickerTool,
|
|
7206
8159
|
reportSystemDependencies,
|
|
7207
8160
|
downloadKeyIntoSandbox,
|
|
7208
8161
|
truncateToolOutput,
|
|
@@ -7216,9 +8169,13 @@ export {
|
|
|
7216
8169
|
ContextCompactionRequiredError,
|
|
7217
8170
|
mapStreamErrorMessage,
|
|
7218
8171
|
guardExtractedFileText,
|
|
8172
|
+
SCRUBBED_CREDENTIAL_TEXT,
|
|
8173
|
+
SCRUBBED_OAUTH_TEXT,
|
|
7219
8174
|
PreviewRenderError,
|
|
7220
8175
|
getPdfPreviewBytes,
|
|
7221
8176
|
imageAttachmentGuard,
|
|
8177
|
+
getAuditLogger,
|
|
8178
|
+
initAudit,
|
|
7222
8179
|
hydrateVariables,
|
|
7223
8180
|
convertExuluToolsToAiSdkTools,
|
|
7224
8181
|
ExuluTool,
|