@exulu/backend 3.1.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,101 +1088,43 @@ 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
1130
  // src/exulu/auth/validate.ts
@@ -1431,14 +1273,14 @@ var authRegistry = {
1431
1273
  };
1432
1274
 
1433
1275
  // src/exulu/auth/flow.ts
1434
- import CryptoJS3 from "crypto-js";
1276
+ import CryptoJS2 from "crypto-js";
1435
1277
  import { createHash, randomBytes } from "crypto";
1436
1278
 
1437
1279
  // src/exulu/auth/credential-store.ts
1438
- import CryptoJS2 from "crypto-js";
1280
+ import CryptoJS from "crypto-js";
1439
1281
  var TABLE = "user_credentials";
1440
- var encrypt = (value) => CryptoJS2.AES.encrypt(value, process.env.NEXTAUTH_SECRET).toString();
1441
- var decrypt = (value) => CryptoJS2.AES.decrypt(value, process.env.NEXTAUTH_SECRET).toString(CryptoJS2.enc.Utf8);
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);
1442
1284
  async function get(provider, userId) {
1443
1285
  const { db: db2 } = await postgresClient();
1444
1286
  const row = await db2.from(TABLE).where({ provider, user_id: String(userId) }).first();
@@ -1517,12 +1359,12 @@ var fromBase64Url = (value) => {
1517
1359
  }
1518
1360
  return base64;
1519
1361
  };
1520
- 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());
1521
1363
  var decryptOauthState = (value) => {
1522
1364
  let json = "";
1523
1365
  try {
1524
- json = CryptoJS3.AES.decrypt(fromBase64Url(value), process.env.NEXTAUTH_SECRET).toString(
1525
- CryptoJS3.enc.Utf8
1366
+ json = CryptoJS2.AES.decrypt(fromBase64Url(value), process.env.NEXTAUTH_SECRET).toString(
1367
+ CryptoJS2.enc.Utf8
1526
1368
  );
1527
1369
  } catch {
1528
1370
  throw new Error("[EXULU] Invalid OAuth state.");
@@ -1891,26 +1733,13 @@ var ExuluTool = class _ExuluTool {
1891
1733
  if (!agent) {
1892
1734
  throw new Error("Agent not found.");
1893
1735
  }
1894
- let providerapikey;
1895
- if (agent.model) {
1896
- const providers = exuluApp.get().providers;
1897
- const resolved = await resolveModel({
1898
- modelId: agent.model,
1899
- user,
1900
- providers,
1901
- agent,
1902
- rbacBypass: true
1903
- });
1904
- providerapikey = resolved.apiKey;
1905
- }
1906
- const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-FN6WZSIQ.js");
1736
+ const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-QG7E6UX5.js");
1907
1737
  const tools = await convertExuluToolsToAiSdkTools2(
1908
1738
  [this],
1909
1739
  [],
1910
1740
  [],
1911
1741
  [],
1912
1742
  agent.tools,
1913
- providerapikey,
1914
1743
  void 0,
1915
1744
  user,
1916
1745
  config,
@@ -1984,7 +1813,7 @@ var updateStatistic = async (statistic) => {
1984
1813
  };
1985
1814
 
1986
1815
  // src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
1987
- import CryptoJS5 from "crypto-js";
1816
+ import CryptoJS4 from "crypto-js";
1988
1817
 
1989
1818
  // src/templates/tools/session-items-retrieval-tool.ts
1990
1819
  import { z as z2 } from "zod";
@@ -4017,7 +3846,6 @@ function createAgenticRetrievalTool(opts) {
4017
3846
  const resolved = await resolveModel({
4018
3847
  modelId: cfg.utilityModel,
4019
3848
  user,
4020
- providers: exuluApp.get().providers,
4021
3849
  rbacBypass: true
4022
3850
  });
4023
3851
  utilityModel = resolved.languageModel ?? model;
@@ -4368,7 +4196,7 @@ function sanitizeToolName(name) {
4368
4196
  }
4369
4197
 
4370
4198
  // src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
4371
- import { randomUUID as randomUUID4 } from "crypto";
4199
+ import { randomUUID as randomUUID5 } from "crypto";
4372
4200
 
4373
4201
  // types/enums/statistics.ts
4374
4202
  var STATISTICS_TYPE_ENUM = {
@@ -6029,7 +5857,7 @@ ${body}`
6029
5857
  import { createBashTool } from "bash-tool";
6030
5858
  import { tool as tool2 } from "ai";
6031
5859
  import { z as z11 } from "zod";
6032
- import CryptoJS4 from "crypto-js";
5860
+ import CryptoJS3 from "crypto-js";
6033
5861
  var getAllExuluVariables = async () => {
6034
5862
  const { db: db2 } = await postgresClient();
6035
5863
  const rows = await db2.from("variables").select("*");
@@ -6041,8 +5869,8 @@ var getAllExuluVariables = async () => {
6041
5869
  let value = row.value;
6042
5870
  if (row.encrypted) {
6043
5871
  try {
6044
- const bytes = CryptoJS4.AES.decrypt(value, process.env.NEXTAUTH_SECRET);
6045
- value = bytes.toString(CryptoJS4.enc.Utf8);
5872
+ const bytes = CryptoJS3.AES.decrypt(value, process.env.NEXTAUTH_SECRET);
5873
+ value = bytes.toString(CryptoJS3.enc.Utf8);
6046
5874
  } catch (err) {
6047
5875
  console.error(
6048
5876
  `[VARIABLES] Failed to decrypt variable "${row.name}"; skipping.`,
@@ -6326,11 +6154,11 @@ Probe error: ${probe.reason ?? "(no detail)"}`
6326
6154
  async executeCommand(command) {
6327
6155
  return await runWrapped(command);
6328
6156
  },
6329
- async readFile(path) {
6330
- const { stdout, stderr, exitCode } = await runWrapped(`cat ${shellQuote(path)}`);
6157
+ async readFile(path3) {
6158
+ const { stdout, stderr, exitCode } = await runWrapped(`cat ${shellQuote(path3)}`);
6331
6159
  if (exitCode !== 0) {
6332
6160
  throw new Error(
6333
- `readFile ${path} failed (exit ${exitCode}): ${stderr.trim() || "no stderr captured"}`
6161
+ `readFile ${path3} failed (exit ${exitCode}): ${stderr.trim() || "no stderr captured"}`
6334
6162
  );
6335
6163
  }
6336
6164
  return stdout;
@@ -6450,8 +6278,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
6450
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."),
6451
6279
  content: z11.string().describe("The content to write to the file")
6452
6280
  }),
6453
- execute: async ({ path, content }) => {
6454
- const resolvedPath = resolveSessionPath(path, sessionDir);
6281
+ execute: async ({ path: path3, content }) => {
6282
+ const resolvedPath = resolveSessionPath(path3, sessionDir);
6455
6283
  const results = await writeFilesInternal([{ path: resolvedPath, content }]);
6456
6284
  const result = results[0];
6457
6285
  if (!result) {
@@ -6470,8 +6298,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
6470
6298
  inputSchema: z11.object({
6471
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.")
6472
6300
  }),
6473
- execute: async ({ path }) => {
6474
- const resolvedPath = resolveSessionPath(path, sessionDir);
6301
+ execute: async ({ path: path3 }) => {
6302
+ const resolvedPath = resolveSessionPath(path3, sessionDir);
6475
6303
  const content = await customSandbox.readFile(resolvedPath);
6476
6304
  return { content };
6477
6305
  }
@@ -6493,25 +6321,25 @@ Probe error: ${probe.reason ?? "(no detail)"}`
6493
6321
  if (persistenceEnabled && before) {
6494
6322
  const after = await snapshotSessionArtifacts();
6495
6323
  const changedPaths = [];
6496
- for (const [path, mtime] of after) {
6497
- const beforeMtime = before.get(path);
6324
+ for (const [path3, mtime] of after) {
6325
+ const beforeMtime = before.get(path3);
6498
6326
  if (beforeMtime === void 0 || beforeMtime < mtime) {
6499
- changedPaths.push(path);
6327
+ changedPaths.push(path3);
6500
6328
  }
6501
6329
  }
6502
- for (const path of changedPaths) {
6330
+ for (const path3 of changedPaths) {
6503
6331
  try {
6504
- const content = await fsReadFile(path);
6505
- const persisted = await persistArtifactToS3(path, content);
6332
+ const content = await fsReadFile(path3);
6333
+ const persisted = await persistArtifactToS3(path3, content);
6506
6334
  artifacts.push({
6507
- path,
6508
- relativePath: relative(sessionDir, path),
6335
+ path: path3,
6336
+ relativePath: relative(sessionDir, path3),
6509
6337
  key: persisted.key,
6510
6338
  url: persisted.url
6511
6339
  });
6512
6340
  } catch (err) {
6513
6341
  console.error(
6514
- `[SKILLS] Failed to mirror bash-produced artifact ${path} to S3; continuing.`,
6342
+ `[SKILLS] Failed to mirror bash-produced artifact ${path3} to S3; continuing.`,
6515
6343
  err
6516
6344
  );
6517
6345
  }
@@ -7277,9 +7105,503 @@ var createViewDocumentPageTool = ({
7277
7105
  });
7278
7106
  };
7279
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
+
7280
7602
  // src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
7281
7603
  var OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS = /* @__PURE__ */ new Set(["agentic_context_search"]);
7282
- var generateS3Key = (filename) => `${randomUUID4()}-${filename}`;
7604
+ var generateS3Key = (filename) => `${randomUUID5()}-${filename}`;
7283
7605
  var s3Client2;
7284
7606
  var getMimeType = (type) => {
7285
7607
  switch (type) {
@@ -7368,8 +7690,8 @@ var hydrateVariables = async (tool3) => {
7368
7690
  }
7369
7691
  let value = variable.value;
7370
7692
  if (variable.encrypted) {
7371
- const bytes = CryptoJS5.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
7372
- 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);
7373
7695
  }
7374
7696
  toolConfig.value = value;
7375
7697
  return toolConfig;
@@ -7377,7 +7699,7 @@ var hydrateVariables = async (tool3) => {
7377
7699
  await Promise.all(promises);
7378
7700
  return tool3;
7379
7701
  };
7380
- 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) => {
7381
7703
  if (!currentTools) return {};
7382
7704
  if (!allExuluTools) {
7383
7705
  allExuluTools = [];
@@ -7612,124 +7934,162 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
7612
7934
  "and options",
7613
7935
  options
7614
7936
  );
7615
- if (!cur.tool?.execute) {
7616
- console.error("[EXULU] Tool execute function is undefined.", cur.tool);
7617
- throw new Error("Tool execute function is undefined.");
7618
- }
7619
- if (toolVariableConfig) {
7620
- toolVariableConfig = await hydrateVariables(toolVariableConfig || []);
7621
- }
7622
- let upload = void 0;
7623
- if (exuluConfig?.fileUploads?.s3endpoint && exuluConfig?.fileUploads?.s3key && exuluConfig?.fileUploads?.s3secret && exuluConfig?.fileUploads?.s3Bucket) {
7624
- s3Client2 ??= new S3Client2({
7625
- region: exuluConfig?.fileUploads?.s3region,
7626
- ...exuluConfig?.fileUploads?.s3endpoint && {
7627
- forcePathStyle: true,
7628
- endpoint: exuluConfig?.fileUploads?.s3endpoint
7629
- },
7630
- credentials: {
7631
- accessKeyId: exuluConfig?.fileUploads?.s3key ?? "",
7632
- secretAccessKey: exuluConfig?.fileUploads?.s3secret ?? ""
7633
- }
7634
- });
7635
- upload = async ({
7636
- name,
7637
- data,
7638
- type
7639
- }) => {
7640
- const mime = getMimeType(type);
7641
- const prefix = exuluConfig?.fileUploads?.s3prefix ? `${exuluConfig.fileUploads.s3prefix.replace(/\/$/, "")}/` : "";
7642
- const key = `${prefix}${user?.id}/${generateS3Key(name)}${type}`;
7643
- const command = new PutObjectCommand2({
7644
- Bucket: exuluConfig?.fileUploads?.s3Bucket,
7645
- Key: key,
7646
- Body: data,
7647
- ContentType: mime
7648
- });
7649
- try {
7650
- if (!s3Client2) {
7651
- 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 ?? ""
7652
7960
  }
7653
- await s3Client2.send(command);
7654
- const bucket = exuluConfig?.fileUploads?.s3Bucket ?? "";
7655
- const presignedUrl = await getPresignedUrl(bucket, key, exuluConfig);
7656
- return { url: presignedUrl, key: `${bucket}/${key}` };
7657
- } catch (caught) {
7658
- if (caught instanceof S3ServiceException && caught.name === "EntityTooLarge") {
7659
- 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).`);
7660
- } else if (caught instanceof S3ServiceException) {
7661
- throw new Error(
7662
- `[EXULU] Error from S3 while uploading object to ${exuluConfig?.fileUploads?.s3Bucket}. ${caught.name}: ${caught.message}`
7663
- );
7664
- } else {
7665
- 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
+ }
7666
7994
  }
7667
- }
7668
- };
7669
- }
7670
- const contextsMap = contexts?.reduce((acc, curr) => {
7671
- acc[curr.id] = curr;
7672
- return acc;
7673
- }, {});
7674
- const toolVariablesConfigData = toolVariableConfig ? toolVariableConfig.config.reduce((acc, curr) => {
7675
- acc[curr.name] = curr.value;
7676
- return acc;
7677
- }, {}) : {};
7678
- const response = await cur.tool.execute(
7679
- {
7680
- ...inputs,
7681
- 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,
7682
8038
  sessionID,
7683
- sessionItems,
7684
- memory: memoryItems,
7685
- req,
7686
- // Convert config to object format if a config object
7687
- // is available, after we added the .value property
7688
- // by hydrating it from the variables table.
7689
- providerapikey,
7690
- allExuluTools,
7691
- currentTools,
7692
8039
  user,
7693
- contexts: contextsMap,
7694
- upload,
7695
- exuluConfig,
7696
- toolVariablesConfig: toolVariablesConfigData
7697
- },
7698
- options
7699
- );
7700
- await updateStatistic({
7701
- name: "count",
7702
- label: cur.name,
7703
- type: STATISTICS_TYPE_ENUM.TOOL_CALL,
7704
- trigger: "agent",
7705
- count: 1,
7706
- user: user?.id,
7707
- role: user?.role?.id
7708
- });
7709
- const guardCtx = {
7710
- toolName: cur.name,
7711
- contextWindow,
7712
- sessionID,
7713
- user,
7714
- exuluConfig
7715
- };
7716
- const offloadExempt = OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS.has(cur.id);
7717
- if (response && typeof response === "object" && Symbol.asyncIterator in response) {
7718
- let lastValue;
7719
- for await (const value of response) {
7720
- yield value;
7721
- lastValue = value;
7722
- }
7723
- if (offloadExempt) return lastValue;
7724
- const guarded = await guardToolOutput(lastValue, guardCtx);
7725
- 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);
7726
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
+ }
7727
8092
  }
7728
- return guarded;
7729
- } else {
7730
- const guarded = offloadExempt ? response : await guardToolOutput(response, guardCtx);
7731
- yield guarded;
7732
- return guarded;
7733
8093
  }
7734
8094
  }
7735
8095
  }
@@ -7781,7 +8141,6 @@ export {
7781
8141
  getUserBudgetView,
7782
8142
  updateStatistic,
7783
8143
  checkLicense,
7784
- checkRecordAccess,
7785
8144
  ResolveModelError,
7786
8145
  resolveModel,
7787
8146
  exuluApp,
@@ -7815,6 +8174,8 @@ export {
7815
8174
  PreviewRenderError,
7816
8175
  getPdfPreviewBytes,
7817
8176
  imageAttachmentGuard,
8177
+ getAuditLogger,
8178
+ initAudit,
7818
8179
  hydrateVariables,
7819
8180
  convertExuluToolsToAiSdkTools,
7820
8181
  ExuluTool,