@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.
- package/README.md +37 -0
- package/dist/{chunk-ZDH5S2WF.js → chunk-CVQTDG37.js} +832 -471
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-FN6WZSIQ.js → convert-exulu-tools-to-ai-sdk-tools-QG7E6UX5.js} +1 -1
- package/dist/index.cjs +3879 -5160
- package/dist/index.d.cts +312 -389
- package/dist/index.d.ts +312 -389
- package/dist/index.js +1815 -3507
- package/ee/agentic-retrieval/pipeline/index.test.ts +1 -1
- package/ee/agentic-retrieval/pipeline/index.ts +0 -1
- package/ee/python/documents/processing/doc_processor.ts +0 -1
- package/ee/queues/queues.ts +13 -0
- package/ee/schemas.ts +11 -10
- package/ee/workers.ts +42 -56
- package/package.json +1 -1
- package/ee/workers.flow.test.ts +0 -236
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-7CCMW3IW.js";
|
|
5
5
|
|
|
6
6
|
// src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
|
|
7
|
-
import { S3Client as
|
|
7
|
+
import { S3Client as S3Client3, PutObjectCommand as PutObjectCommand3, S3ServiceException } from "@aws-sdk/client-s3";
|
|
8
8
|
|
|
9
9
|
// src/exulu/tool.ts
|
|
10
10
|
import { tool } from "ai";
|
|
@@ -33,210 +33,8 @@ var exuluApp = {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
// src/exulu/resolve-model.ts
|
|
36
|
-
import CryptoJS from "crypto-js";
|
|
37
36
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
38
37
|
|
|
39
|
-
// src/postgres/client.ts
|
|
40
|
-
import Knex from "knex";
|
|
41
|
-
import "knex";
|
|
42
|
-
import "pgvector/knex";
|
|
43
|
-
var db = {};
|
|
44
|
-
var databaseExistsChecked = false;
|
|
45
|
-
var getDbName = () => process.env.POSTGRES_DB_NAME || "exulu";
|
|
46
|
-
async function ensureDatabaseExists() {
|
|
47
|
-
const dbName = getDbName();
|
|
48
|
-
const defaultKnex = Knex({
|
|
49
|
-
client: "pg",
|
|
50
|
-
connection: {
|
|
51
|
-
host: process.env.POSTGRES_DB_HOST,
|
|
52
|
-
port: parseInt(process.env.POSTGRES_DB_PORT || "5432"),
|
|
53
|
-
user: process.env.POSTGRES_DB_USER,
|
|
54
|
-
database: "postgres",
|
|
55
|
-
// Connect to default database
|
|
56
|
-
password: process.env.POSTGRES_DB_PASSWORD,
|
|
57
|
-
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
58
|
-
connectionTimeoutMillis: 1e4
|
|
59
|
-
},
|
|
60
|
-
pool: {
|
|
61
|
-
min: 2,
|
|
62
|
-
max: 4,
|
|
63
|
-
acquireTimeoutMillis: 3e4,
|
|
64
|
-
createTimeoutMillis: 3e4,
|
|
65
|
-
idleTimeoutMillis: 3e4,
|
|
66
|
-
reapIntervalMillis: 1e3,
|
|
67
|
-
createRetryIntervalMillis: 200
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
try {
|
|
71
|
-
const result = await defaultKnex.raw(`
|
|
72
|
-
SELECT 1 FROM pg_database WHERE datname = '${dbName}'
|
|
73
|
-
`);
|
|
74
|
-
if (result.rows.length === 0) {
|
|
75
|
-
console.log(`[EXULU] Database '${dbName}' does not exist. Creating it...`);
|
|
76
|
-
await defaultKnex.raw(`CREATE DATABASE ${dbName}`);
|
|
77
|
-
console.log(`[EXULU] Database '${dbName}' created successfully.`);
|
|
78
|
-
} else {
|
|
79
|
-
console.log(`[EXULU] Database '${dbName}' already exists.`);
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.error(
|
|
83
|
-
"[EXULU] Error while checking to ensure the database exists, this could be if the user running the server does not have database admin rights, it is fine to ignore this if you are sure the database exists.",
|
|
84
|
-
error
|
|
85
|
-
);
|
|
86
|
-
return;
|
|
87
|
-
} finally {
|
|
88
|
-
await defaultKnex.destroy();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
async function postgresClient() {
|
|
92
|
-
if (!db["exulu"]) {
|
|
93
|
-
try {
|
|
94
|
-
if (!databaseExistsChecked) {
|
|
95
|
-
await ensureDatabaseExists();
|
|
96
|
-
databaseExistsChecked = true;
|
|
97
|
-
}
|
|
98
|
-
const knex = Knex({
|
|
99
|
-
client: "pg",
|
|
100
|
-
connection: {
|
|
101
|
-
host: process.env.POSTGRES_DB_HOST,
|
|
102
|
-
port: parseInt(process.env.POSTGRES_DB_PORT || "5432"),
|
|
103
|
-
user: process.env.POSTGRES_DB_USER,
|
|
104
|
-
database: getDbName(),
|
|
105
|
-
password: process.env.POSTGRES_DB_PASSWORD,
|
|
106
|
-
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
107
|
-
// TCP keepalive prevents idle sockets from being silently dropped by
|
|
108
|
-
// intermediate network devices (NAT, firewalls) between us and Hetzner.
|
|
109
|
-
keepAlive: true,
|
|
110
|
-
keepAliveInitialDelayMillis: 1e4,
|
|
111
|
-
connectionTimeoutMillis: 3e4,
|
|
112
|
-
statement_timeout: 18e5,
|
|
113
|
-
query_timeout: 18e5
|
|
114
|
-
},
|
|
115
|
-
pool: {
|
|
116
|
-
min: 10,
|
|
117
|
-
max: 300,
|
|
118
|
-
acquireTimeoutMillis: 12e4,
|
|
119
|
-
createTimeoutMillis: 3e4,
|
|
120
|
-
idleTimeoutMillis: 3e4,
|
|
121
|
-
reapIntervalMillis: 1e3,
|
|
122
|
-
createRetryIntervalMillis: 200,
|
|
123
|
-
// Enable propagateCreateError to properly handle connection creation failures
|
|
124
|
-
propagateCreateError: false,
|
|
125
|
-
// Log pool events to help debug connection issues
|
|
126
|
-
afterCreate: (conn, done) => {
|
|
127
|
-
console.log("[EXULU] New database connection created");
|
|
128
|
-
conn.query("SET statement_timeout = 1800000; SET hnsw.ef_search = 20", (err) => {
|
|
129
|
-
if (err) {
|
|
130
|
-
console.error("[EXULU] Error setting connection parameters:", err);
|
|
131
|
-
}
|
|
132
|
-
done(err, conn);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
try {
|
|
138
|
-
await knex.schema.createExtensionIfNotExists("vector");
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error(
|
|
141
|
-
"[EXULU] Error creating vector extension, this might be fine if you already activated the extension and the 'user' running this script does not have higher level database permissions.",
|
|
142
|
-
error
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
db["exulu"] = knex;
|
|
146
|
-
} catch (error) {
|
|
147
|
-
console.error("[EXULU] Error initializing exulu database.", error);
|
|
148
|
-
throw error;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return {
|
|
152
|
-
db: db["exulu"]
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// src/utils/check-record-access.ts
|
|
157
|
-
var checkRecordAccessCache = /* @__PURE__ */ new Map();
|
|
158
|
-
var checkRecordAccess = async (record, request, user) => {
|
|
159
|
-
const setRecordAccessCache = (hasAccess2) => {
|
|
160
|
-
checkRecordAccessCache.set(`${record.id}-${request}-${user?.id}`, {
|
|
161
|
-
hasAccess: hasAccess2,
|
|
162
|
-
expiresAt: new Date(Date.now() + 1e3 * 60 * 1)
|
|
163
|
-
// 1 minute
|
|
164
|
-
});
|
|
165
|
-
};
|
|
166
|
-
const cachedAccess = checkRecordAccessCache.get(`${record.id}-${request}-${user?.id}`);
|
|
167
|
-
if (cachedAccess && cachedAccess.expiresAt > /* @__PURE__ */ new Date()) {
|
|
168
|
-
return cachedAccess.hasAccess;
|
|
169
|
-
}
|
|
170
|
-
const isPublic = record.rights_mode === "public";
|
|
171
|
-
const byUsers = record.rights_mode === "users";
|
|
172
|
-
const byRoles = record.rights_mode === "roles";
|
|
173
|
-
const byTeams = record.rights_mode === "teams";
|
|
174
|
-
const createdBy = typeof record.created_by === "string" ? record.created_by : record.created_by?.toString();
|
|
175
|
-
const isCreator = user ? createdBy === user.id.toString() : false;
|
|
176
|
-
const isAdmin = user ? user.super_admin : false;
|
|
177
|
-
const isApi = user ? user.type === "api" : false;
|
|
178
|
-
const isAdminApi = isApi && (!user.scope_mode || user.scope_mode === "admin");
|
|
179
|
-
const isAgentsScopedApi = isApi && user.scope_mode === "agents" && request === "read" && Array.isArray(user.agent_ids) && user.agent_ids.includes(String(record.id));
|
|
180
|
-
let hasAccess = "none";
|
|
181
|
-
if (isPublic || isCreator || isAdmin || isAdminApi || isAgentsScopedApi) {
|
|
182
|
-
setRecordAccessCache(true);
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
if (byUsers) {
|
|
186
|
-
if (!user) {
|
|
187
|
-
setRecordAccessCache(false);
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
hasAccess = record.RBAC?.users?.find((x) => x.id === user.id)?.rights || "none";
|
|
191
|
-
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
192
|
-
console.error(
|
|
193
|
-
`[EXULU] Your current user ${user.id} does not have access to this record, current access type is: ${hasAccess}.`
|
|
194
|
-
);
|
|
195
|
-
setRecordAccessCache(false);
|
|
196
|
-
return false;
|
|
197
|
-
} else {
|
|
198
|
-
setRecordAccessCache(true);
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
if (byRoles) {
|
|
203
|
-
if (!user) {
|
|
204
|
-
setRecordAccessCache(false);
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
hasAccess = record.RBAC?.roles?.find((x) => x.id === user.role?.id)?.rights || "none";
|
|
208
|
-
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
209
|
-
console.error(
|
|
210
|
-
`[EXULU] Your current role ${user.role?.name} does not have access to this record, current access type is: ${hasAccess}.`
|
|
211
|
-
);
|
|
212
|
-
setRecordAccessCache(false);
|
|
213
|
-
return false;
|
|
214
|
-
} else {
|
|
215
|
-
setRecordAccessCache(true);
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (byTeams) {
|
|
220
|
-
if (!user) {
|
|
221
|
-
setRecordAccessCache(false);
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
hasAccess = record.RBAC?.teams?.find((x) => x.id === user.team?.id)?.rights || "none";
|
|
225
|
-
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
226
|
-
console.error(
|
|
227
|
-
`[EXULU] Your current team ${user.team?.name} does not have access to this record, current access type is: ${hasAccess}.`
|
|
228
|
-
);
|
|
229
|
-
setRecordAccessCache(false);
|
|
230
|
-
return false;
|
|
231
|
-
} else {
|
|
232
|
-
setRecordAccessCache(true);
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
setRecordAccessCache(false);
|
|
237
|
-
return false;
|
|
238
|
-
};
|
|
239
|
-
|
|
240
38
|
// src/exulu/litellm/supervisor.ts
|
|
241
39
|
import { spawn } from "child_process";
|
|
242
40
|
import { existsSync } from "fs";
|
|
@@ -616,6 +414,123 @@ function createTaggedFetch(tags) {
|
|
|
616
414
|
return labeled;
|
|
617
415
|
}
|
|
618
416
|
|
|
417
|
+
// src/postgres/client.ts
|
|
418
|
+
import Knex from "knex";
|
|
419
|
+
import "knex";
|
|
420
|
+
import "pgvector/knex";
|
|
421
|
+
var db = {};
|
|
422
|
+
var databaseExistsChecked = false;
|
|
423
|
+
var getDbName = () => process.env.POSTGRES_DB_NAME || "exulu";
|
|
424
|
+
async function ensureDatabaseExists() {
|
|
425
|
+
const dbName = getDbName();
|
|
426
|
+
const defaultKnex = Knex({
|
|
427
|
+
client: "pg",
|
|
428
|
+
connection: {
|
|
429
|
+
host: process.env.POSTGRES_DB_HOST,
|
|
430
|
+
port: parseInt(process.env.POSTGRES_DB_PORT || "5432"),
|
|
431
|
+
user: process.env.POSTGRES_DB_USER,
|
|
432
|
+
database: "postgres",
|
|
433
|
+
// Connect to default database
|
|
434
|
+
password: process.env.POSTGRES_DB_PASSWORD,
|
|
435
|
+
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
436
|
+
connectionTimeoutMillis: 1e4
|
|
437
|
+
},
|
|
438
|
+
pool: {
|
|
439
|
+
min: 2,
|
|
440
|
+
max: 4,
|
|
441
|
+
acquireTimeoutMillis: 3e4,
|
|
442
|
+
createTimeoutMillis: 3e4,
|
|
443
|
+
idleTimeoutMillis: 3e4,
|
|
444
|
+
reapIntervalMillis: 1e3,
|
|
445
|
+
createRetryIntervalMillis: 200
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
try {
|
|
449
|
+
const result = await defaultKnex.raw(`
|
|
450
|
+
SELECT 1 FROM pg_database WHERE datname = '${dbName}'
|
|
451
|
+
`);
|
|
452
|
+
if (result.rows.length === 0) {
|
|
453
|
+
console.log(`[EXULU] Database '${dbName}' does not exist. Creating it...`);
|
|
454
|
+
await defaultKnex.raw(`CREATE DATABASE ${dbName}`);
|
|
455
|
+
console.log(`[EXULU] Database '${dbName}' created successfully.`);
|
|
456
|
+
} else {
|
|
457
|
+
console.log(`[EXULU] Database '${dbName}' already exists.`);
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error(
|
|
461
|
+
"[EXULU] Error while checking to ensure the database exists, this could be if the user running the server does not have database admin rights, it is fine to ignore this if you are sure the database exists.",
|
|
462
|
+
error
|
|
463
|
+
);
|
|
464
|
+
return;
|
|
465
|
+
} finally {
|
|
466
|
+
await defaultKnex.destroy();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function postgresClient() {
|
|
470
|
+
if (!db["exulu"]) {
|
|
471
|
+
try {
|
|
472
|
+
if (!databaseExistsChecked) {
|
|
473
|
+
await ensureDatabaseExists();
|
|
474
|
+
databaseExistsChecked = true;
|
|
475
|
+
}
|
|
476
|
+
const knex = Knex({
|
|
477
|
+
client: "pg",
|
|
478
|
+
connection: {
|
|
479
|
+
host: process.env.POSTGRES_DB_HOST,
|
|
480
|
+
port: parseInt(process.env.POSTGRES_DB_PORT || "5432"),
|
|
481
|
+
user: process.env.POSTGRES_DB_USER,
|
|
482
|
+
database: getDbName(),
|
|
483
|
+
password: process.env.POSTGRES_DB_PASSWORD,
|
|
484
|
+
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
485
|
+
// TCP keepalive prevents idle sockets from being silently dropped by
|
|
486
|
+
// intermediate network devices (NAT, firewalls) between us and Hetzner.
|
|
487
|
+
keepAlive: true,
|
|
488
|
+
keepAliveInitialDelayMillis: 1e4,
|
|
489
|
+
connectionTimeoutMillis: 3e4,
|
|
490
|
+
statement_timeout: 18e5,
|
|
491
|
+
query_timeout: 18e5
|
|
492
|
+
},
|
|
493
|
+
pool: {
|
|
494
|
+
min: 10,
|
|
495
|
+
max: 300,
|
|
496
|
+
acquireTimeoutMillis: 12e4,
|
|
497
|
+
createTimeoutMillis: 3e4,
|
|
498
|
+
idleTimeoutMillis: 3e4,
|
|
499
|
+
reapIntervalMillis: 1e3,
|
|
500
|
+
createRetryIntervalMillis: 200,
|
|
501
|
+
// Enable propagateCreateError to properly handle connection creation failures
|
|
502
|
+
propagateCreateError: false,
|
|
503
|
+
// Log pool events to help debug connection issues
|
|
504
|
+
afterCreate: (conn, done) => {
|
|
505
|
+
console.log("[EXULU] New database connection created");
|
|
506
|
+
conn.query("SET statement_timeout = 1800000; SET hnsw.ef_search = 20", (err) => {
|
|
507
|
+
if (err) {
|
|
508
|
+
console.error("[EXULU] Error setting connection parameters:", err);
|
|
509
|
+
}
|
|
510
|
+
done(err, conn);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
try {
|
|
516
|
+
await knex.schema.createExtensionIfNotExists("vector");
|
|
517
|
+
} catch (error) {
|
|
518
|
+
console.error(
|
|
519
|
+
"[EXULU] Error creating vector extension, this might be fine if you already activated the extension and the 'user' running this script does not have higher level database permissions.",
|
|
520
|
+
error
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
db["exulu"] = knex;
|
|
524
|
+
} catch (error) {
|
|
525
|
+
console.error("[EXULU] Error initializing exulu database.", error);
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
db: db["exulu"]
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
619
534
|
// src/exulu/litellm/env.ts
|
|
620
535
|
var LiteLLMAdminError = class extends Error {
|
|
621
536
|
constructor(message, status) {
|
|
@@ -635,9 +550,9 @@ function litellmBase() {
|
|
|
635
550
|
}
|
|
636
551
|
|
|
637
552
|
// src/exulu/litellm/admin-client.ts
|
|
638
|
-
async function call(
|
|
553
|
+
async function call(path3, body) {
|
|
639
554
|
const { url, masterKey } = litellmBase();
|
|
640
|
-
const res = await fetch(`${url}${
|
|
555
|
+
const res = await fetch(`${url}${path3}`, {
|
|
641
556
|
method: "POST",
|
|
642
557
|
headers: {
|
|
643
558
|
Authorization: `Bearer ${masterKey}`,
|
|
@@ -648,7 +563,7 @@ async function call(path, body) {
|
|
|
648
563
|
if (!res.ok) {
|
|
649
564
|
const text = await res.text().catch(() => "");
|
|
650
565
|
throw new LiteLLMAdminError(
|
|
651
|
-
`LiteLLM ${
|
|
566
|
+
`LiteLLM ${path3} returned ${res.status}: ${text}`,
|
|
652
567
|
res.status
|
|
653
568
|
);
|
|
654
569
|
}
|
|
@@ -1112,21 +1027,6 @@ async function getUserBudgetView(userId) {
|
|
|
1112
1027
|
}
|
|
1113
1028
|
|
|
1114
1029
|
// src/exulu/resolve-model.ts
|
|
1115
|
-
var LITELLM_PROVIDER_SENTINEL = new Proxy(
|
|
1116
|
-
{},
|
|
1117
|
-
{
|
|
1118
|
-
get(_target, prop) {
|
|
1119
|
-
if (prop === "id") return "litellm";
|
|
1120
|
-
if (prop === Symbol.toPrimitive || prop === "toString") {
|
|
1121
|
-
return () => "[LiteLLMProviderSentinel]";
|
|
1122
|
-
}
|
|
1123
|
-
console.error(`ExuluProvider.${String(prop)} is not available in LiteLLM mode. `, new Error().stack);
|
|
1124
|
-
throw new Error(
|
|
1125
|
-
`ExuluProvider.${String(prop)} is not available in LiteLLM mode. Code paths that depend on the in-code provider catalog must check isLiteLLMEnabled() and degrade.`
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
);
|
|
1130
1030
|
var ResolveModelError = class extends Error {
|
|
1131
1031
|
constructor(code, message) {
|
|
1132
1032
|
super(message);
|
|
@@ -1188,101 +1088,43 @@ var getLiteLLMProvider = ({
|
|
|
1188
1088
|
});
|
|
1189
1089
|
};
|
|
1190
1090
|
async function resolveModel(input) {
|
|
1191
|
-
const { modelId, user,
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
try {
|
|
1195
|
-
await waitForLiteLLMReady();
|
|
1196
|
-
} catch (err) {
|
|
1197
|
-
throw new ResolveModelError(
|
|
1198
|
-
"LITELLM_NOT_READY",
|
|
1199
|
-
`LiteLLM is not ready: ${err.message}`
|
|
1200
|
-
);
|
|
1201
|
-
}
|
|
1202
|
-
if (user?.id) await provisionDefaultUserBudget(user.id);
|
|
1203
|
-
const litellm = getLiteLLMProvider({
|
|
1204
|
-
user,
|
|
1205
|
-
role: user?.role,
|
|
1206
|
-
// Fall back to the caller's own project (set on API keys) when no
|
|
1207
|
-
// explicit request project is supplied, so API-triggered requests are
|
|
1208
|
-
// attributed to the key's project.
|
|
1209
|
-
project: project ?? user?.project,
|
|
1210
|
-
agent,
|
|
1211
|
-
team: user?.team,
|
|
1212
|
-
routine
|
|
1213
|
-
});
|
|
1214
|
-
const languageModel2 = litellm(modelId);
|
|
1215
|
-
const syntheticModel = {
|
|
1216
|
-
id: modelId,
|
|
1217
|
-
name: modelId,
|
|
1218
|
-
provider: modelId,
|
|
1219
|
-
active: true,
|
|
1220
|
-
rights_mode: "public",
|
|
1221
|
-
created_by: "litellm"
|
|
1222
|
-
};
|
|
1223
|
-
return {
|
|
1224
|
-
languageModel: languageModel2,
|
|
1225
|
-
model: syntheticModel,
|
|
1226
|
-
exuluProvider: LITELLM_PROVIDER_SENTINEL,
|
|
1227
|
-
apiKey: void 0
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
const { db: db2 } = await postgresClient();
|
|
1231
|
-
const model = await db2.from("models").where({ id: modelId }).first();
|
|
1232
|
-
if (!model) {
|
|
1233
|
-
throw new ResolveModelError("MODEL_NOT_FOUND", `Model ${modelId} not found`);
|
|
1234
|
-
}
|
|
1235
|
-
if (!model.active) {
|
|
1236
|
-
throw new ResolveModelError("MODEL_INACTIVE", `Model ${model.name} is inactive`);
|
|
1237
|
-
}
|
|
1238
|
-
if (!rbacBypass) {
|
|
1239
|
-
const ok = await checkRecordAccess(model, rbacRequest, user);
|
|
1240
|
-
if (!ok) {
|
|
1241
|
-
throw new ResolveModelError(
|
|
1242
|
-
"MODEL_FORBIDDEN",
|
|
1243
|
-
`No ${rbacRequest} access to model ${model.name}`
|
|
1244
|
-
);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
const exuluProvider = providers.find((p) => p.id === model.provider);
|
|
1248
|
-
if (!exuluProvider) {
|
|
1249
|
-
throw new ResolveModelError(
|
|
1250
|
-
"PROVIDER_NOT_FOUND",
|
|
1251
|
-
`ExuluProvider ${model.provider} (referenced by model ${model.name}) not registered in this instance`
|
|
1252
|
-
);
|
|
1091
|
+
const { modelId, user, agent, project, routine } = input;
|
|
1092
|
+
if (!isLiteLLMEnabled()) {
|
|
1093
|
+
throw new Error("Litellm not configured or available.");
|
|
1253
1094
|
}
|
|
1254
|
-
|
|
1095
|
+
try {
|
|
1096
|
+
await waitForLiteLLMReady();
|
|
1097
|
+
} catch (err) {
|
|
1255
1098
|
throw new ResolveModelError(
|
|
1256
|
-
"
|
|
1257
|
-
`
|
|
1099
|
+
"LITELLM_NOT_READY",
|
|
1100
|
+
`LiteLLM is not ready: ${err.message}`
|
|
1258
1101
|
);
|
|
1259
1102
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
"AUTH_VAR_NOT_ENCRYPTED",
|
|
1272
|
-
`Auth variable ${model.authvariable} must be encrypted`
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
const bytes = CryptoJS.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
1276
|
-
apiKey = bytes.toString(CryptoJS.enc.Utf8);
|
|
1277
|
-
}
|
|
1278
|
-
const languageModel = exuluProvider.config.model.create({
|
|
1279
|
-
apiKey,
|
|
1280
|
-
user: user?.id,
|
|
1281
|
-
role: user?.role?.id,
|
|
1282
|
-
project: project?.id,
|
|
1283
|
-
agent: agent?.id
|
|
1103
|
+
if (user?.id) await provisionDefaultUserBudget(user.id);
|
|
1104
|
+
const litellm = getLiteLLMProvider({
|
|
1105
|
+
user,
|
|
1106
|
+
role: user?.role,
|
|
1107
|
+
// Fall back to the caller's own project (set on API keys) when no
|
|
1108
|
+
// explicit request project is supplied, so API-triggered requests are
|
|
1109
|
+
// attributed to the key's project.
|
|
1110
|
+
project: project ?? user?.project,
|
|
1111
|
+
agent,
|
|
1112
|
+
team: user?.team,
|
|
1113
|
+
routine
|
|
1284
1114
|
});
|
|
1285
|
-
|
|
1115
|
+
const languageModel = litellm(modelId);
|
|
1116
|
+
const syntheticModel = {
|
|
1117
|
+
id: modelId,
|
|
1118
|
+
name: modelId,
|
|
1119
|
+
provider: modelId,
|
|
1120
|
+
active: true,
|
|
1121
|
+
rights_mode: "public",
|
|
1122
|
+
created_by: "litellm"
|
|
1123
|
+
};
|
|
1124
|
+
return {
|
|
1125
|
+
languageModel,
|
|
1126
|
+
model: syntheticModel
|
|
1127
|
+
};
|
|
1286
1128
|
}
|
|
1287
1129
|
|
|
1288
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
|
|
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
|
|
1280
|
+
import CryptoJS from "crypto-js";
|
|
1439
1281
|
var TABLE = "user_credentials";
|
|
1440
|
-
var encrypt = (value) =>
|
|
1441
|
-
var decrypt = (value) =>
|
|
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(
|
|
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 =
|
|
1525
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
6045
|
-
value = bytes.toString(
|
|
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(
|
|
6330
|
-
const { stdout, stderr, exitCode } = await runWrapped(`cat ${shellQuote(
|
|
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 ${
|
|
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(
|
|
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(
|
|
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 [
|
|
6497
|
-
const beforeMtime = before.get(
|
|
6324
|
+
for (const [path3, mtime] of after) {
|
|
6325
|
+
const beforeMtime = before.get(path3);
|
|
6498
6326
|
if (beforeMtime === void 0 || beforeMtime < mtime) {
|
|
6499
|
-
changedPaths.push(
|
|
6327
|
+
changedPaths.push(path3);
|
|
6500
6328
|
}
|
|
6501
6329
|
}
|
|
6502
|
-
for (const
|
|
6330
|
+
for (const path3 of changedPaths) {
|
|
6503
6331
|
try {
|
|
6504
|
-
const content = await fsReadFile(
|
|
6505
|
-
const persisted = await persistArtifactToS3(
|
|
6332
|
+
const content = await fsReadFile(path3);
|
|
6333
|
+
const persisted = await persistArtifactToS3(path3, content);
|
|
6506
6334
|
artifacts.push({
|
|
6507
|
-
path,
|
|
6508
|
-
relativePath: relative(sessionDir,
|
|
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 ${
|
|
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) => `${
|
|
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 =
|
|
7372
|
-
value = bytes.toString(
|
|
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,
|
|
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
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
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
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
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
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
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
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
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,
|