@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.
@@ -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 S3Client2, PutObjectCommand as PutObjectCommand2, S3ServiceException } from "@aws-sdk/client-s3";
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(path, body) {
553
+ async function call(path3, body) {
639
554
  const { url, masterKey } = litellmBase();
640
- const res = await fetch(`${url}${path}`, {
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 ${path} returned ${res.status}: ${text}`,
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, providers, agent, project, routine, rbacBypass } = input;
1192
- const rbacRequest = input.rbacRequest ?? "read";
1193
- if (isLiteLLMEnabled()) {
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
- if (!exuluProvider.config?.model?.create) {
1095
+ try {
1096
+ await waitForLiteLLMReady();
1097
+ } catch (err) {
1255
1098
  throw new ResolveModelError(
1256
- "PROVIDER_NO_MODEL",
1257
- `ExuluProvider ${exuluProvider.id} has no model.create()`
1099
+ "LITELLM_NOT_READY",
1100
+ `LiteLLM is not ready: ${err.message}`
1258
1101
  );
1259
1102
  }
1260
- let apiKey;
1261
- if (model.authvariable) {
1262
- const variable = await db2.from("variables").where({ name: model.authvariable }).first();
1263
- if (!variable) {
1264
- throw new ResolveModelError(
1265
- "AUTH_VAR_NOT_FOUND",
1266
- `Auth variable ${model.authvariable} (referenced by model ${model.name}) not found`
1267
- );
1268
- }
1269
- if (!variable.encrypted) {
1270
- throw new ResolveModelError(
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
- return { languageModel, model, exuluProvider, apiKey };
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/oauth/validate.ts
1289
- var REQUIRED_STRING_FIELDS = [
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 validateOauthConfig = (toolId, config) => {
1296
- for (const field of REQUIRED_STRING_FIELDS) {
1297
- if (!config[field] || typeof config[field] !== "string") {
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.${field} is required and must be a non-empty string.`
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 (!Array.isArray(config.scopes)) {
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}": oauth.provider must be a non-empty string with no leading or trailing whitespace when set.`
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
- if (!process.env.BACKEND) {
1316
- throw new Error(
1317
- `ExuluTool "${toolId}": oauth requires the BACKEND environment variable (the backend's public base URL) to build the redirect URI.`
1318
- );
1319
- }
1206
+ throw new Error(
1207
+ `ExuluTool "${toolId}": auth.authType '${config.authType}' is not supported.`
1208
+ );
1320
1209
  };
1321
1210
 
1322
- // src/exulu/oauth/provider-key.ts
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/oauth/registry.ts
1214
+ // src/exulu/auth/registry.ts
1326
1215
  var byProvider = /* @__PURE__ */ new Map();
1327
1216
  var byTool = /* @__PURE__ */ new Map();
1328
- var STABLE_STRING_FIELDS = [
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
- for (const field of STABLE_STRING_FIELDS) {
1336
- if (existing[field] !== next[field]) {
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.${field} disagrees with another tool that shares provider "${providerKey}". Every tool on the same provider must use identical authorizationUrl/tokenUrl/clientId/clientSecret.`
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
- const a = new Set(existing.scopes);
1343
- const b = new Set(next.scopes);
1344
- if (a.size !== b.size || [...a].some((s) => !b.has(s))) {
1345
- throw new Error(
1346
- `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(", ")}].`
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 oauthRegistry = {
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/oauth/flow.ts
1371
- import CryptoJS3 from "crypto-js";
1275
+ // src/exulu/auth/flow.ts
1276
+ import CryptoJS2 from "crypto-js";
1372
1277
  import { createHash, randomBytes } from "crypto";
1373
1278
 
1374
- // src/exulu/oauth/token-store.ts
1375
- import CryptoJS2 from "crypto-js";
1376
- var TABLE = "oauth_tokens";
1377
- var encrypt = (value) => CryptoJS2.AES.encrypt(value, process.env.NEXTAUTH_SECRET).toString();
1378
- var decrypt = (value) => CryptoJS2.AES.decrypt(value, process.env.NEXTAUTH_SECRET).toString(CryptoJS2.enc.Utf8);
1379
- var oauthTokenStore = {
1380
- get: async (providerKey, userId) => {
1381
- const { db: db2 } = await postgresClient();
1382
- const row = await db2.from(TABLE).where({ provider: providerKey, user_id: userId }).first();
1383
- if (!row) {
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/oauth/flow.ts
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(CryptoJS3.AES.encrypt(JSON.stringify(state), process.env.NEXTAUTH_SECRET).toString());
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 = CryptoJS3.AES.decrypt(fromBase64Url(value), process.env.NEXTAUTH_SECRET).toString(
1448
- CryptoJS3.enc.Utf8
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 stored = await oauthTokenStore.get(providerKey, userId);
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 oauthTokenStore.delete(providerKey, userId);
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 oauthTokenStore.upsert(providerKey, userId, toolId, refreshed);
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 oauthTokenStore.delete(providerKey, userId);
1515
+ await credentialStore.delete(providerKey, userId);
1591
1516
  return null;
1592
1517
  }
1593
1518
  };
1594
1519
 
1595
- // src/exulu/oauth/wrap-execute.ts
1596
- var wrapExecuteWithOauth = (toolId, config, execute) => {
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
- oauth;
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
- oauth
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 (oauth) {
1658
- validateOauthConfig(id, oauth);
1659
- oauthRegistry.register(id, oauth);
1684
+ if (authentication2) {
1685
+ validateAuthConfig(id, authentication2);
1686
+ authRegistry.register(id, authentication2);
1660
1687
  }
1661
- this.oauth = oauth;
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: oauth ? wrapExecuteWithOauth(id, oauth, execute) : 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
- let providerapikey;
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 CryptoJS5 from "crypto-js";
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) * 1e3));
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 withRetry(
2496
- () => generateText({
2497
- model,
2498
- temperature: 0,
2499
- system: set.strategy === "exact" ? EXACT_EXTRACTION_PROMPT(set) : FUZZY_EXTRACTION_PROMPT(set),
2500
- messages: [{ role: "user", content: question }],
2501
- output: Output.object({
2502
- schema: z4.object({
2503
- hasMatches: z4.boolean(),
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 withRetry(
2609
- () => generateText2({
2610
- model,
2611
- temperature: 0,
2612
- system: buildDocPagePrompt(knownIdentifiers),
2613
- messages: [{ role: "user", content: question }],
2614
- output: Output2.object({
2615
- schema: z5.object({
2616
- hasFilenameHint: z5.boolean(),
2617
- filenameHints: z5.array(z5.string()).optional(),
2618
- hasPageHint: z5.boolean(),
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 withRetry(
2641
- () => generateText2({
2642
- model,
2643
- temperature: 0,
2644
- system: kbSystemPrompt,
2645
- output: Output2.object({
2646
- schema: z5.object({
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
- 3
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 withRetry(
2740
- () => generateText2({
2741
- model,
2742
- temperature: 0,
2743
- system: classifyPrompt,
2744
- messages: [{ role: "user", content: question }],
2745
- output: Output2.object({
2746
- schema: z5.object({
2747
- ruleId: z5.enum(ruleIds),
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 withRetry(
3036
- () => generateText3({
3037
- model,
3038
- temperature: 0,
3039
- system: CHECK_MEMORIES_FOR_RELEVANT_INFORMATION,
3040
- messages: [
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
- output: Output3.object({
3051
- schema: z6.object({
3052
- relevantChunkIds: z6.array(z6.string()).describe(
3053
- "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."
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 ? withRetry(
3153
- () => generateText3({
3154
- model,
3155
- temperature: 0,
3156
- system: CHECK_MEMORY_OVERRIDE,
3157
- messages: [
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
- output: Output3.object({
3168
- schema: z6.object({
3169
- overrides: z6.boolean().describe(
3170
- "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."
3171
- ),
3172
- confidence: z6.enum(["high", "medium", "low"]).describe(
3173
- "Confidence that the selected memory chunk(s) fully and directly answer the question."
3174
- ),
3175
- authoritativeChunkIds: z6.array(z6.string()).describe(
3176
- "The chunk_ids of the memory chunk(s) that directly answer the question. Empty if overrides is false."
3177
- ),
3178
- reason: z6.string().describe(
3179
- "One short sentence: why this memory does or does not directly answer the question."
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 ? withRetry(
3203
- () => generateText3({
3204
- model,
3205
- temperature: 0,
3206
- system: "You are a helpful assistant that will strictly follow the user's instructions.",
3207
- messages: [{ role: "user", content: PROMPT_EXTRACT_PRIORITIZED_FILES }],
3208
- output: Output3.object({
3209
- schema: z6.object({
3210
- shouldPrioritizeFiles: z6.boolean(),
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 ? withRetry(
3224
- () => generateText3({
3225
- model,
3226
- temperature: 0,
3227
- system: "You are a helpful assistant that will strictly follow the user's instructions.",
3228
- messages: [{ role: "user", content: QUERY_AUGMENTATION_PROMPT }],
3229
- output: Output3.object({
3230
- schema: z6.object({
3231
- updatedUserQuestion: z6.string(),
3232
- updatedRelevantKeywords: z6.array(z6.string()),
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 generateText4({
3397
+ const { text } = await microCall({
3391
3398
  model,
3392
3399
  prompt,
3393
3400
  temperature: 0.3,
3394
- maxOutputTokens: 500
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 randomUUID4 } from "crypto";
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
- } catch (error) {
4326
- console.error("[EXULU] Error creating memory item", error);
4327
- result = {
4328
- result: `Failed to create memory item: ${error instanceof Error ? error.message : String(error)}`
4329
- };
4330
- }
4331
- return result;
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 z9 } from "zod";
5472
- import CryptoJS4 from "crypto-js";
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 = CryptoJS4.AES.decrypt(value, process.env.NEXTAUTH_SECRET);
5485
- value = bytes.toString(CryptoJS4.enc.Utf8);
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(path) {
5770
- const { stdout, stderr, exitCode } = await runWrapped(`cat ${shellQuote(path)}`);
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 ${path} failed (exit ${exitCode}): ${stderr.trim() || "no stderr captured"}`
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: z9.object({
5890
- path: z9.string().describe("The path where the file should be written. Relative paths and leading-slash paths are both resolved against the session sandbox root."),
5891
- content: z9.string().describe("The content to write to the file")
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(path, sessionDir);
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: z9.object({
5911
- path: z9.string().describe("The path of the file to read. Relative paths and leading-slash paths are both resolved against the session sandbox root.")
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(path, sessionDir);
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: z9.object({
5923
- command: z9.string().describe("The bash command to execute.")
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 [path, mtime] of after) {
5937
- const beforeMtime = before.get(path);
6324
+ for (const [path3, mtime] of after) {
6325
+ const beforeMtime = before.get(path3);
5938
6326
  if (beforeMtime === void 0 || beforeMtime < mtime) {
5939
- changedPaths.push(path);
6327
+ changedPaths.push(path3);
5940
6328
  }
5941
6329
  }
5942
- for (const path of changedPaths) {
6330
+ for (const path3 of changedPaths) {
5943
6331
  try {
5944
- const content = await fsReadFile(path);
5945
- const persisted = await persistArtifactToS3(path, content);
6332
+ const content = await fsReadFile(path3);
6333
+ const persisted = await persistArtifactToS3(path3, content);
5946
6334
  artifacts.push({
5947
- path,
5948
- relativePath: relative(sessionDir, path),
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 ${path} to S3; continuing.`,
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 z10 } from "zod";
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: z10.object({
6224
- filename: z10.string().describe('Exact session file name as referenced in a truncation notice, e.g. "tool-output-web_search-a1b2c3d4.txt"'),
6225
- offset: z10.number().int().min(1).optional().describe("1-based first line to read (default 1)"),
6226
- limit: z10.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_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 z11 } from "zod";
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: z11.object({
6403
- filename: z11.string().describe('Exact session file name, e.g. "report.pdf"'),
6404
- pages: z11.string().optional().describe('PDF page or range to extract, e.g. "2" or "1-5" (default: all pages) (PDF only)'),
6405
- offset: z11.number().int().min(1).optional().describe("1-based first output line to read (default 1)"),
6406
- limit: z11.number().int().min(1).max(1e3).optional().describe(`Number of lines to read (default ${DEFAULT_LIMIT2})`)
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 z12 } from "zod";
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: z12.object({
6690
- filename: z12.string().describe('Exact session file name, e.g. "report.pdf" or "screenshot.png"'),
6691
- page: z12.number().int().min(1).optional().describe("Page number to render (default 1; ignored for image files)")
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) => `${randomUUID4()}-${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 = CryptoJS5.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
6793
- value = bytes.toString(CryptoJS5.enc.Utf8);
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, providerapikey, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems, contextWindow, disabledTools) => {
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
- if (!cur.tool?.execute) {
7028
- console.error("[EXULU] Tool execute function is undefined.", cur.tool);
7029
- throw new Error("Tool execute function is undefined.");
7030
- }
7031
- if (toolVariableConfig) {
7032
- toolVariableConfig = await hydrateVariables(toolVariableConfig || []);
7033
- }
7034
- let upload = void 0;
7035
- if (exuluConfig?.fileUploads?.s3endpoint && exuluConfig?.fileUploads?.s3key && exuluConfig?.fileUploads?.s3secret && exuluConfig?.fileUploads?.s3Bucket) {
7036
- s3Client2 ??= new S3Client2({
7037
- region: exuluConfig?.fileUploads?.s3region,
7038
- ...exuluConfig?.fileUploads?.s3endpoint && {
7039
- forcePathStyle: true,
7040
- endpoint: exuluConfig?.fileUploads?.s3endpoint
7041
- },
7042
- credentials: {
7043
- accessKeyId: exuluConfig?.fileUploads?.s3key ?? "",
7044
- secretAccessKey: exuluConfig?.fileUploads?.s3secret ?? ""
7045
- }
7046
- });
7047
- upload = async ({
7048
- name,
7049
- data,
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
- await s3Client2.send(command);
7066
- const bucket = exuluConfig?.fileUploads?.s3Bucket ?? "";
7067
- const presignedUrl = await getPresignedUrl(bucket, key, exuluConfig);
7068
- return { url: presignedUrl, key: `${bucket}/${key}` };
7069
- } catch (caught) {
7070
- if (caught instanceof S3ServiceException && caught.name === "EntityTooLarge") {
7071
- 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).`);
7072
- } else if (caught instanceof S3ServiceException) {
7073
- throw new Error(
7074
- `[EXULU] Error from S3 while uploading object to ${exuluConfig?.fileUploads?.s3Bucket}. ${caught.name}: ${caught.message}`
7075
- );
7076
- } else {
7077
- throw caught;
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
- const contextsMap = contexts?.reduce((acc, curr) => {
7083
- acc[curr.id] = curr;
7084
- return acc;
7085
- }, {});
7086
- const toolVariablesConfigData = toolVariableConfig ? toolVariableConfig.config.reduce((acc, curr) => {
7087
- acc[curr.name] = curr.value;
7088
- return acc;
7089
- }, {}) : {};
7090
- const response = await cur.tool.execute(
7091
- {
7092
- ...inputs,
7093
- model,
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
- contexts: contextsMap,
7106
- upload,
7107
- exuluConfig,
7108
- toolVariablesConfig: toolVariablesConfigData
7109
- },
7110
- options
7111
- );
7112
- await updateStatistic({
7113
- name: "count",
7114
- label: cur.name,
7115
- type: STATISTICS_TYPE_ENUM.TOOL_CALL,
7116
- trigger: "agent",
7117
- count: 1,
7118
- user: user?.id,
7119
- role: user?.role?.id
7120
- });
7121
- const guardCtx = {
7122
- toolName: cur.name,
7123
- contextWindow,
7124
- sessionID,
7125
- user,
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
- oauthRegistry,
8147
+ authRegistry,
7199
8148
  encrypt,
7200
8149
  decrypt,
7201
- oauthTokenStore,
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,