@exulu/backend 1.58.0 → 1.59.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/dist/catalog-EOKGOHTY.js +10 -0
- package/dist/{chunk-RVLZ5EL3.js → chunk-U36VJDZ7.js} +644 -66
- package/dist/chunk-YS27XOXI.js +62 -0
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-K4W6OJ3G.js → convert-exulu-tools-to-ai-sdk-tools-ZEECMX43.js} +1 -1
- package/dist/index.cjs +2606 -1236
- package/dist/index.d.cts +13 -14
- package/dist/index.d.ts +13 -14
- package/dist/index.js +1812 -1134
- package/ee/python/.litellm/config.yaml.example +64 -0
- package/ee/python/requirements.txt +15 -0
- package/ee/python/setup.sh +13 -0
- package/ee/workers.ts +15 -29
- package/package.json +3 -1
|
@@ -5,6 +5,32 @@ import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2, S3Service
|
|
|
5
5
|
import { tool } from "ai";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
|
+
// src/utils/sanitize-name.ts
|
|
9
|
+
var sanitizeName = (name) => {
|
|
10
|
+
return name.toLowerCase().replace(/ /g, "_")?.trim();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/exulu/tool.ts
|
|
14
|
+
import { randomUUID } from "crypto";
|
|
15
|
+
|
|
16
|
+
// src/exulu/app/singleton.ts
|
|
17
|
+
var instance = null;
|
|
18
|
+
var exuluApp = {
|
|
19
|
+
get: () => {
|
|
20
|
+
if (!instance) {
|
|
21
|
+
throw new Error("ExuluApp not initialized");
|
|
22
|
+
}
|
|
23
|
+
return instance;
|
|
24
|
+
},
|
|
25
|
+
set: (app) => {
|
|
26
|
+
instance = app;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/exulu/resolve-model.ts
|
|
31
|
+
import CryptoJS from "crypto-js";
|
|
32
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
33
|
+
|
|
8
34
|
// src/postgres/client.ts
|
|
9
35
|
import Knex from "knex";
|
|
10
36
|
import "knex";
|
|
@@ -72,26 +98,20 @@ async function postgresClient() {
|
|
|
72
98
|
database: dbName,
|
|
73
99
|
password: process.env.POSTGRES_DB_PASSWORD,
|
|
74
100
|
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
101
|
+
// TCP keepalive prevents idle sockets from being silently dropped by
|
|
102
|
+
// intermediate network devices (NAT, firewalls) between us and Hetzner.
|
|
103
|
+
keepAlive: true,
|
|
104
|
+
keepAliveInitialDelayMillis: 1e4,
|
|
75
105
|
connectionTimeoutMillis: 3e4,
|
|
76
|
-
// Increased from 10s to 30s to handle connection spikes
|
|
77
|
-
// PostgreSQL statement timeout (in milliseconds) - kills queries that run too long
|
|
78
|
-
// This prevents runaway queries from blocking connections
|
|
79
106
|
statement_timeout: 18e5,
|
|
80
|
-
// 30 minutes - should be longer than max job timeout (1200s = 20m)
|
|
81
|
-
// Connection idle timeout - how long pg client waits before timing out
|
|
82
107
|
query_timeout: 18e5
|
|
83
|
-
// 30 minutes
|
|
84
108
|
},
|
|
85
109
|
pool: {
|
|
86
110
|
min: 10,
|
|
87
|
-
// Minimum connections always ready
|
|
88
111
|
max: 300,
|
|
89
|
-
// Increased to support high worker concurrency (250+ concurrent jobs)
|
|
90
112
|
acquireTimeoutMillis: 12e4,
|
|
91
|
-
// 2 minutes - increased to handle high contention during bursts
|
|
92
113
|
createTimeoutMillis: 3e4,
|
|
93
|
-
idleTimeoutMillis:
|
|
94
|
-
// Keep connections alive for reuse
|
|
114
|
+
idleTimeoutMillis: 3e4,
|
|
95
115
|
reapIntervalMillis: 1e3,
|
|
96
116
|
createRetryIntervalMillis: 200,
|
|
97
117
|
// Enable propagateCreateError to properly handle connection creation failures
|
|
@@ -127,30 +147,530 @@ async function postgresClient() {
|
|
|
127
147
|
};
|
|
128
148
|
}
|
|
129
149
|
|
|
130
|
-
// src/
|
|
131
|
-
|
|
150
|
+
// src/utils/check-record-access.ts
|
|
151
|
+
var checkRecordAccessCache = /* @__PURE__ */ new Map();
|
|
152
|
+
var checkRecordAccess = async (record, request, user) => {
|
|
153
|
+
const setRecordAccessCache = (hasAccess2) => {
|
|
154
|
+
checkRecordAccessCache.set(`${record.id}-${request}-${user?.id}`, {
|
|
155
|
+
hasAccess: hasAccess2,
|
|
156
|
+
expiresAt: new Date(Date.now() + 1e3 * 60 * 1)
|
|
157
|
+
// 1 minute
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
const cachedAccess = checkRecordAccessCache.get(`${record.id}-${request}-${user?.id}`);
|
|
161
|
+
if (cachedAccess && cachedAccess.expiresAt > /* @__PURE__ */ new Date()) {
|
|
162
|
+
return cachedAccess.hasAccess;
|
|
163
|
+
}
|
|
164
|
+
const isPublic = record.rights_mode === "public";
|
|
165
|
+
const byUsers = record.rights_mode === "users";
|
|
166
|
+
const byRoles = record.rights_mode === "roles";
|
|
167
|
+
const createdBy = typeof record.created_by === "string" ? record.created_by : record.created_by?.toString();
|
|
168
|
+
const isCreator = user ? createdBy === user.id.toString() : false;
|
|
169
|
+
const isAdmin = user ? user.super_admin : false;
|
|
170
|
+
const isApi = user ? user.type === "api" : false;
|
|
171
|
+
const isAdminApi = isApi && (!user.scope_mode || user.scope_mode === "admin");
|
|
172
|
+
const isAgentsScopedApi = isApi && user.scope_mode === "agents" && request === "read" && Array.isArray(user.agent_ids) && user.agent_ids.includes(String(record.id));
|
|
173
|
+
let hasAccess = "none";
|
|
174
|
+
if (isPublic || isCreator || isAdmin || isAdminApi || isAgentsScopedApi) {
|
|
175
|
+
setRecordAccessCache(true);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (byUsers) {
|
|
179
|
+
if (!user) {
|
|
180
|
+
setRecordAccessCache(false);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
hasAccess = record.RBAC?.users?.find((x) => x.id === user.id)?.rights || "none";
|
|
184
|
+
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
185
|
+
console.error(
|
|
186
|
+
`[EXULU] Your current user ${user.id} does not have access to this record, current access type is: ${hasAccess}.`
|
|
187
|
+
);
|
|
188
|
+
setRecordAccessCache(false);
|
|
189
|
+
return false;
|
|
190
|
+
} else {
|
|
191
|
+
setRecordAccessCache(true);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (byRoles) {
|
|
196
|
+
if (!user) {
|
|
197
|
+
setRecordAccessCache(false);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
hasAccess = record.RBAC?.roles?.find((x) => x.id === user.role?.id)?.rights || "none";
|
|
201
|
+
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
202
|
+
console.error(
|
|
203
|
+
`[EXULU] Your current role ${user.role?.name} does not have access to this record, current access type is: ${hasAccess}.`
|
|
204
|
+
);
|
|
205
|
+
setRecordAccessCache(false);
|
|
206
|
+
return false;
|
|
207
|
+
} else {
|
|
208
|
+
setRecordAccessCache(true);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
setRecordAccessCache(false);
|
|
213
|
+
return false;
|
|
214
|
+
};
|
|
132
215
|
|
|
133
|
-
// src/
|
|
134
|
-
|
|
135
|
-
|
|
216
|
+
// src/exulu/litellm/supervisor.ts
|
|
217
|
+
import { spawn } from "child_process";
|
|
218
|
+
import { existsSync } from "fs";
|
|
219
|
+
import { resolve } from "path";
|
|
220
|
+
var MAX_CRASHES = 5;
|
|
221
|
+
var INITIAL_BACKOFF_MS = 1e3;
|
|
222
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
223
|
+
var READY_TIMEOUT_MS = 3e4;
|
|
224
|
+
var READY_POLL_INTERVAL_MS = 200;
|
|
225
|
+
var SHUTDOWN_GRACE_MS = 5e3;
|
|
226
|
+
var internal = {
|
|
227
|
+
child: void 0,
|
|
228
|
+
state: "idle",
|
|
229
|
+
crashCount: 0,
|
|
230
|
+
backoffMs: INITIAL_BACKOFF_MS,
|
|
231
|
+
readyPromise: void 0,
|
|
232
|
+
shutdownRequested: false
|
|
233
|
+
};
|
|
234
|
+
var isLiteLLMEnabled = () => process.env.EXULU_USE_LITELLM === "true";
|
|
235
|
+
var resolveConfig = (packageRoot) => {
|
|
236
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
237
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
238
|
+
const masterKey = process.env.LITELLM_MASTER_KEY;
|
|
239
|
+
const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve(packageRoot, "./config.litellm.yaml");
|
|
240
|
+
const venvBin = resolve(packageRoot, "ee/python/.venv/bin");
|
|
241
|
+
const venvPython = resolve(venvBin, "python");
|
|
242
|
+
const litellmBin = resolve(venvBin, "litellm");
|
|
243
|
+
return { host, port, masterKey, configPath, venvBin, venvPython, litellmBin };
|
|
244
|
+
};
|
|
245
|
+
var log = (line) => console.log(`[EXULU-LITELLM] ${line}`);
|
|
246
|
+
var pollHealth = async (host, port) => {
|
|
247
|
+
const url = `http://${host}:${port}/health/liveliness`;
|
|
248
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
249
|
+
while (Date.now() < deadline) {
|
|
250
|
+
try {
|
|
251
|
+
const res = await fetch(url, { method: "GET" });
|
|
252
|
+
if (res.ok) return;
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
|
|
256
|
+
}
|
|
257
|
+
throw new Error(
|
|
258
|
+
`LiteLLM did not become ready at ${url} within ${READY_TIMEOUT_MS}ms`
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
var spawnLiteLLM = (cfg) => {
|
|
262
|
+
log(
|
|
263
|
+
`Spawning LiteLLM: ${cfg.litellmBin} --config ${cfg.configPath} --port ${cfg.port} --host ${cfg.host}`
|
|
264
|
+
);
|
|
265
|
+
const { DEBUG: _debug, ...envWithoutDebug } = process.env;
|
|
266
|
+
const child = spawn(
|
|
267
|
+
cfg.litellmBin,
|
|
268
|
+
[
|
|
269
|
+
"--config",
|
|
270
|
+
cfg.configPath,
|
|
271
|
+
"--port",
|
|
272
|
+
cfg.port,
|
|
273
|
+
"--host",
|
|
274
|
+
cfg.host
|
|
275
|
+
],
|
|
276
|
+
{
|
|
277
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
278
|
+
env: envWithoutDebug
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
child.stdout?.on("data", (chunk) => {
|
|
282
|
+
chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(l));
|
|
283
|
+
});
|
|
284
|
+
child.stderr?.on("data", (chunk) => {
|
|
285
|
+
chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(`stderr: ${l}`));
|
|
286
|
+
});
|
|
287
|
+
return child;
|
|
288
|
+
};
|
|
289
|
+
var supervise = async (cfg) => {
|
|
290
|
+
while (!internal.shutdownRequested && internal.crashCount < MAX_CRASHES) {
|
|
291
|
+
internal.state = internal.crashCount === 0 ? "starting" : "respawning";
|
|
292
|
+
internal.child = spawnLiteLLM(cfg);
|
|
293
|
+
const exitPromise = new Promise((resolveFn) => {
|
|
294
|
+
internal.child.on("exit", (code2) => resolveFn(code2));
|
|
295
|
+
});
|
|
296
|
+
try {
|
|
297
|
+
await Promise.race([
|
|
298
|
+
pollHealth(cfg.host, cfg.port).then(() => "ready"),
|
|
299
|
+
exitPromise.then((code2) => ({ exited: code2 }))
|
|
300
|
+
]);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
log(`Readiness probe failed: ${err.message}`);
|
|
303
|
+
try {
|
|
304
|
+
internal.child?.kill("SIGTERM");
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!internal.child?.killed && internal.child?.exitCode === null) {
|
|
309
|
+
internal.state = "ready";
|
|
310
|
+
internal.crashCount = 0;
|
|
311
|
+
internal.backoffMs = INITIAL_BACKOFF_MS;
|
|
312
|
+
log("LiteLLM is ready.");
|
|
313
|
+
}
|
|
314
|
+
const code = await exitPromise;
|
|
315
|
+
internal.state = "respawning";
|
|
316
|
+
internal.child = void 0;
|
|
317
|
+
if (internal.shutdownRequested) {
|
|
318
|
+
log("Child exited during shutdown; supervisor stopping.");
|
|
319
|
+
internal.state = "stopped";
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
internal.crashCount += 1;
|
|
323
|
+
log(
|
|
324
|
+
`LiteLLM exited (code=${code}). Crash ${internal.crashCount}/${MAX_CRASHES}. Respawning in ${internal.backoffMs}ms.`
|
|
325
|
+
);
|
|
326
|
+
if (internal.crashCount >= MAX_CRASHES) {
|
|
327
|
+
log(
|
|
328
|
+
"LiteLLM keeps crashing \u2014 fix the config and restart Exulu. Giving up on respawn."
|
|
329
|
+
);
|
|
330
|
+
internal.state = "given_up";
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
await new Promise((r) => setTimeout(r, internal.backoffMs));
|
|
334
|
+
internal.backoffMs = Math.min(internal.backoffMs * 2, MAX_BACKOFF_MS);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
var _packageRoot;
|
|
338
|
+
var setLiteLLMPackageRoot = (root) => {
|
|
339
|
+
_packageRoot = root;
|
|
340
|
+
};
|
|
341
|
+
var startLiteLLMSupervisor = async (options = {}) => {
|
|
342
|
+
if (!isLiteLLMEnabled()) return;
|
|
343
|
+
if (internal.readyPromise) {
|
|
344
|
+
return internal.readyPromise;
|
|
345
|
+
}
|
|
346
|
+
if (!process.env.LITELLM_MASTER_KEY) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
"EXULU_USE_LITELLM is true but LITELLM_MASTER_KEY is not set. Set LITELLM_MASTER_KEY to a strong secret and restart Exulu."
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const packageRoot = options.packageRoot ?? _packageRoot;
|
|
352
|
+
if (!packageRoot) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
"LiteLLM supervisor: package root not set. Call setLiteLLMPackageRoot() from the boot path before starting the supervisor."
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const cfg = resolveConfig(packageRoot);
|
|
358
|
+
if (!existsSync(cfg.configPath)) {
|
|
359
|
+
log(
|
|
360
|
+
`LiteLLM config not found at ${cfg.configPath}. Copy ee/python/.litellm/config.yaml.example to that path, edit it, and restart Exulu. LiteLLM will NOT be started until then.`
|
|
361
|
+
);
|
|
362
|
+
internal.state = "given_up";
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (!existsSync(cfg.litellmBin)) {
|
|
366
|
+
log(
|
|
367
|
+
`LiteLLM binary not found at ${cfg.litellmBin}. The Python venv may not be set up. Run setupPythonEnvironment() from @exulu/backend, then restart.`
|
|
368
|
+
);
|
|
369
|
+
internal.state = "given_up";
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
internal.readyPromise = (async () => {
|
|
373
|
+
supervise(cfg);
|
|
374
|
+
const deadline = Date.now() + READY_TIMEOUT_MS + 5e3;
|
|
375
|
+
while (Date.now() < deadline) {
|
|
376
|
+
if (internal.state === "ready") return;
|
|
377
|
+
if (internal.state === "given_up") {
|
|
378
|
+
throw new Error("LiteLLM supervisor gave up before becoming ready.");
|
|
379
|
+
}
|
|
380
|
+
await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
|
|
381
|
+
}
|
|
382
|
+
throw new Error("Timed out waiting for LiteLLM supervisor readiness.");
|
|
383
|
+
})();
|
|
384
|
+
registerShutdownHandlers();
|
|
385
|
+
return internal.readyPromise;
|
|
386
|
+
};
|
|
387
|
+
var waitForLiteLLMReady = async () => {
|
|
388
|
+
if (!isLiteLLMEnabled()) return;
|
|
389
|
+
if (!internal.readyPromise) {
|
|
390
|
+
await startLiteLLMSupervisor();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
return internal.readyPromise;
|
|
394
|
+
};
|
|
395
|
+
var stopLiteLLM = (signal = "SIGTERM") => {
|
|
396
|
+
internal.shutdownRequested = true;
|
|
397
|
+
const child = internal.child;
|
|
398
|
+
if (!child) return;
|
|
399
|
+
try {
|
|
400
|
+
child.kill(signal);
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
setTimeout(() => {
|
|
404
|
+
try {
|
|
405
|
+
if (!child.killed && child.exitCode === null) {
|
|
406
|
+
child.kill("SIGKILL");
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}, SHUTDOWN_GRACE_MS).unref();
|
|
411
|
+
};
|
|
412
|
+
var shutdownHandlersRegistered = false;
|
|
413
|
+
var registerShutdownHandlers = () => {
|
|
414
|
+
if (shutdownHandlersRegistered) return;
|
|
415
|
+
shutdownHandlersRegistered = true;
|
|
416
|
+
process.on("SIGINT", () => stopLiteLLM("SIGTERM"));
|
|
417
|
+
process.on("SIGTERM", () => stopLiteLLM("SIGTERM"));
|
|
418
|
+
process.on("exit", () => stopLiteLLM("SIGTERM"));
|
|
136
419
|
};
|
|
137
420
|
|
|
138
|
-
// src/exulu/
|
|
139
|
-
|
|
421
|
+
// src/exulu/tags.ts
|
|
422
|
+
var MAX_LEN = 63;
|
|
423
|
+
function sanitizeTagValue(raw) {
|
|
424
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
425
|
+
const s = String(raw).normalize("NFKC").toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
426
|
+
return s.slice(0, MAX_LEN);
|
|
427
|
+
}
|
|
428
|
+
function buildTags(input) {
|
|
429
|
+
const candidates = [];
|
|
430
|
+
if (input.user_id) {
|
|
431
|
+
candidates.push("user_id_" + input.user_id);
|
|
432
|
+
}
|
|
433
|
+
if (input.user_name) {
|
|
434
|
+
candidates.push("user_name_" + input.user_name);
|
|
435
|
+
}
|
|
436
|
+
if (input.role_id) {
|
|
437
|
+
candidates.push("role_id_" + input.role_id);
|
|
438
|
+
}
|
|
439
|
+
if (input.role_name) {
|
|
440
|
+
candidates.push("role_name_" + input.role_name);
|
|
441
|
+
}
|
|
442
|
+
if (input.project_id) {
|
|
443
|
+
candidates.push("project_id_" + input.project_id);
|
|
444
|
+
}
|
|
445
|
+
if (input.project_name) {
|
|
446
|
+
candidates.push("project_name_" + input.project_name);
|
|
447
|
+
}
|
|
448
|
+
if (input.agent_id) {
|
|
449
|
+
candidates.push("agent_id_" + input.agent_id);
|
|
450
|
+
}
|
|
451
|
+
if (input.agent_name) {
|
|
452
|
+
candidates.push("agent_name_" + input.agent_name);
|
|
453
|
+
}
|
|
454
|
+
console.log("[EXULU] Candidates", candidates);
|
|
455
|
+
const out = [];
|
|
456
|
+
for (const candidate of candidates) {
|
|
457
|
+
if (candidate === void 0 || candidate === null) continue;
|
|
458
|
+
const value = sanitizeTagValue(candidate);
|
|
459
|
+
console.log("[EXULU] Sanitized tag value", value);
|
|
460
|
+
if (value === void 0 || value === "") continue;
|
|
461
|
+
out.push(value);
|
|
462
|
+
}
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
function decodeBody(body) {
|
|
466
|
+
if (typeof body === "string") return body;
|
|
467
|
+
if (body instanceof Uint8Array) return new TextDecoder().decode(body);
|
|
468
|
+
return void 0;
|
|
469
|
+
}
|
|
470
|
+
function stripContentLengthHeader(headers) {
|
|
471
|
+
if (!headers) return headers;
|
|
472
|
+
if (typeof headers.delete === "function") {
|
|
473
|
+
const clone = new globalThis.Headers(headers);
|
|
474
|
+
clone.delete("content-length");
|
|
475
|
+
return clone;
|
|
476
|
+
}
|
|
477
|
+
if (Array.isArray(headers)) {
|
|
478
|
+
return headers.filter(
|
|
479
|
+
([k]) => k.toLowerCase() !== "content-length"
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
const out = {};
|
|
483
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
484
|
+
if (k.toLowerCase() === "content-length") continue;
|
|
485
|
+
out[k] = v;
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
function createTaggedFetch(tags) {
|
|
490
|
+
const labeled = async (input, init) => {
|
|
491
|
+
try {
|
|
492
|
+
if (!init || !init.body) return globalThis.fetch(input, init);
|
|
493
|
+
const method = (init.method ?? "POST").toUpperCase();
|
|
494
|
+
if (method !== "POST" && method !== "PUT" && method !== "PATCH") {
|
|
495
|
+
return globalThis.fetch(input, init);
|
|
496
|
+
}
|
|
497
|
+
const decoded = decodeBody(init.body);
|
|
498
|
+
if (decoded === void 0) {
|
|
499
|
+
console.warn("[vertex-labels] unsupported body type, forwarding unchanged");
|
|
500
|
+
return globalThis.fetch(input, init);
|
|
501
|
+
}
|
|
502
|
+
let parsed;
|
|
503
|
+
try {
|
|
504
|
+
parsed = JSON.parse(decoded);
|
|
505
|
+
} catch {
|
|
506
|
+
console.warn("[vertex-labels] body is not JSON, forwarding unchanged");
|
|
507
|
+
return globalThis.fetch(input, init);
|
|
508
|
+
}
|
|
509
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
510
|
+
return globalThis.fetch(input, init);
|
|
511
|
+
}
|
|
512
|
+
const existing = parsed.metadata ?? {};
|
|
513
|
+
parsed.metadata = { ...existing, tags };
|
|
514
|
+
console.log("[EXULU] tags", parsed.metadata);
|
|
515
|
+
const nextInit = {
|
|
516
|
+
...init,
|
|
517
|
+
body: JSON.stringify(parsed),
|
|
518
|
+
headers: stripContentLengthHeader(init.headers)
|
|
519
|
+
};
|
|
520
|
+
return globalThis.fetch(input, nextInit);
|
|
521
|
+
} catch (err) {
|
|
522
|
+
console.warn("[vertex-labels] label injection failed, forwarding unchanged", err);
|
|
523
|
+
return globalThis.fetch(input, init);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
return labeled;
|
|
527
|
+
}
|
|
140
528
|
|
|
141
|
-
// src/exulu/
|
|
142
|
-
var
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
529
|
+
// src/exulu/resolve-model.ts
|
|
530
|
+
var LITELLM_PROVIDER_SENTINEL = new Proxy(
|
|
531
|
+
{},
|
|
532
|
+
{
|
|
533
|
+
get(_target, prop) {
|
|
534
|
+
if (prop === "id") return "litellm";
|
|
535
|
+
if (prop === Symbol.toPrimitive || prop === "toString") {
|
|
536
|
+
return () => "[LiteLLMProviderSentinel]";
|
|
537
|
+
}
|
|
538
|
+
console.error(`ExuluProvider.${String(prop)} is not available in LiteLLM mode. `, new Error().stack);
|
|
539
|
+
throw new Error(
|
|
540
|
+
`ExuluProvider.${String(prop)} is not available in LiteLLM mode. Code paths that depend on the in-code provider catalog must check isLiteLLMEnabled() and degrade.`
|
|
541
|
+
);
|
|
147
542
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
var ResolveModelError = class extends Error {
|
|
546
|
+
constructor(code, message) {
|
|
547
|
+
super(message);
|
|
548
|
+
this.code = code;
|
|
549
|
+
this.name = "ResolveModelError";
|
|
152
550
|
}
|
|
153
551
|
};
|
|
552
|
+
var _litellmProvider;
|
|
553
|
+
var getLiteLLMProvider = ({
|
|
554
|
+
user,
|
|
555
|
+
role,
|
|
556
|
+
project,
|
|
557
|
+
agent
|
|
558
|
+
}) => {
|
|
559
|
+
if (_litellmProvider) return _litellmProvider;
|
|
560
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
561
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
562
|
+
const masterKey = process.env.LITELLM_MASTER_KEY;
|
|
563
|
+
const tags = buildTags({
|
|
564
|
+
user,
|
|
565
|
+
role,
|
|
566
|
+
project,
|
|
567
|
+
agent
|
|
568
|
+
});
|
|
569
|
+
if (!masterKey) {
|
|
570
|
+
throw new ResolveModelError(
|
|
571
|
+
"LITELLM_NOT_CONFIGURED",
|
|
572
|
+
"LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
_litellmProvider = createOpenAICompatible({
|
|
576
|
+
name: "litellm",
|
|
577
|
+
baseURL: `http://${host}:${port}/v1`,
|
|
578
|
+
apiKey: masterKey,
|
|
579
|
+
fetch: createTaggedFetch(tags)
|
|
580
|
+
});
|
|
581
|
+
return _litellmProvider;
|
|
582
|
+
};
|
|
583
|
+
async function resolveModel(input) {
|
|
584
|
+
const { modelId, user, providers, agent, project, rbacBypass } = input;
|
|
585
|
+
const rbacRequest = input.rbacRequest ?? "read";
|
|
586
|
+
if (isLiteLLMEnabled()) {
|
|
587
|
+
try {
|
|
588
|
+
await waitForLiteLLMReady();
|
|
589
|
+
} catch (err) {
|
|
590
|
+
throw new ResolveModelError(
|
|
591
|
+
"LITELLM_NOT_READY",
|
|
592
|
+
`LiteLLM is not ready: ${err.message}`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
const litellm = getLiteLLMProvider({
|
|
596
|
+
user: user?.id,
|
|
597
|
+
role: user?.role?.id,
|
|
598
|
+
project: project?.id,
|
|
599
|
+
agent: agent?.id
|
|
600
|
+
});
|
|
601
|
+
const languageModel2 = litellm(modelId);
|
|
602
|
+
const syntheticModel = {
|
|
603
|
+
id: modelId,
|
|
604
|
+
name: modelId,
|
|
605
|
+
provider: modelId,
|
|
606
|
+
active: true,
|
|
607
|
+
rights_mode: "public",
|
|
608
|
+
created_by: "litellm"
|
|
609
|
+
};
|
|
610
|
+
return {
|
|
611
|
+
languageModel: languageModel2,
|
|
612
|
+
model: syntheticModel,
|
|
613
|
+
exuluProvider: LITELLM_PROVIDER_SENTINEL,
|
|
614
|
+
apiKey: void 0
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const { db: db2 } = await postgresClient();
|
|
618
|
+
const model = await db2.from("models").where({ id: modelId }).first();
|
|
619
|
+
if (!model) {
|
|
620
|
+
throw new ResolveModelError("MODEL_NOT_FOUND", `Model ${modelId} not found`);
|
|
621
|
+
}
|
|
622
|
+
if (!model.active) {
|
|
623
|
+
throw new ResolveModelError("MODEL_INACTIVE", `Model ${model.name} is inactive`);
|
|
624
|
+
}
|
|
625
|
+
if (!rbacBypass) {
|
|
626
|
+
const ok = await checkRecordAccess(model, rbacRequest, user);
|
|
627
|
+
if (!ok) {
|
|
628
|
+
throw new ResolveModelError(
|
|
629
|
+
"MODEL_FORBIDDEN",
|
|
630
|
+
`No ${rbacRequest} access to model ${model.name}`
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const exuluProvider = providers.find((p) => p.id === model.provider);
|
|
635
|
+
if (!exuluProvider) {
|
|
636
|
+
throw new ResolveModelError(
|
|
637
|
+
"PROVIDER_NOT_FOUND",
|
|
638
|
+
`ExuluProvider ${model.provider} (referenced by model ${model.name}) not registered in this instance`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
if (!exuluProvider.config?.model?.create) {
|
|
642
|
+
throw new ResolveModelError(
|
|
643
|
+
"PROVIDER_NO_MODEL",
|
|
644
|
+
`ExuluProvider ${exuluProvider.id} has no model.create()`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
let apiKey;
|
|
648
|
+
if (model.authvariable) {
|
|
649
|
+
const variable = await db2.from("variables").where({ name: model.authvariable }).first();
|
|
650
|
+
if (!variable) {
|
|
651
|
+
throw new ResolveModelError(
|
|
652
|
+
"AUTH_VAR_NOT_FOUND",
|
|
653
|
+
`Auth variable ${model.authvariable} (referenced by model ${model.name}) not found`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
if (!variable.encrypted) {
|
|
657
|
+
throw new ResolveModelError(
|
|
658
|
+
"AUTH_VAR_NOT_ENCRYPTED",
|
|
659
|
+
`Auth variable ${model.authvariable} must be encrypted`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
const bytes = CryptoJS.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
663
|
+
apiKey = bytes.toString(CryptoJS.enc.Utf8);
|
|
664
|
+
}
|
|
665
|
+
const languageModel = exuluProvider.config.model.create({
|
|
666
|
+
apiKey,
|
|
667
|
+
user: user?.id,
|
|
668
|
+
role: user?.role?.id,
|
|
669
|
+
project: project?.id,
|
|
670
|
+
agent: agent?.id
|
|
671
|
+
});
|
|
672
|
+
return { languageModel, model, exuluProvider, apiKey };
|
|
673
|
+
}
|
|
154
674
|
|
|
155
675
|
// src/exulu/tool.ts
|
|
156
676
|
var ExuluTool = class {
|
|
@@ -211,29 +731,19 @@ var ExuluTool = class {
|
|
|
211
731
|
if (!agent) {
|
|
212
732
|
throw new Error("Agent not found.");
|
|
213
733
|
}
|
|
214
|
-
const { db: db2 } = await postgresClient();
|
|
215
734
|
let providerapikey;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
providerapikey =
|
|
226
|
-
if (!variable.encrypted) {
|
|
227
|
-
throw new Error(
|
|
228
|
-
"Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys."
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
if (variable.encrypted) {
|
|
232
|
-
const bytes = CryptoJS.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
233
|
-
providerapikey = bytes.toString(CryptoJS.enc.Utf8);
|
|
234
|
-
}
|
|
735
|
+
if (agent.model) {
|
|
736
|
+
const providers = exuluApp.get().providers;
|
|
737
|
+
const resolved = await resolveModel({
|
|
738
|
+
modelId: agent.model,
|
|
739
|
+
user,
|
|
740
|
+
providers,
|
|
741
|
+
agent: { id: agent.id },
|
|
742
|
+
rbacBypass: true
|
|
743
|
+
});
|
|
744
|
+
providerapikey = resolved.apiKey;
|
|
235
745
|
}
|
|
236
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
746
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-ZEECMX43.js");
|
|
237
747
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
238
748
|
[this],
|
|
239
749
|
[],
|
|
@@ -560,7 +1070,7 @@ async function withRetry(generateFn, maxRetries = 3) {
|
|
|
560
1070
|
if (attempt === maxRetries) {
|
|
561
1071
|
throw error;
|
|
562
1072
|
}
|
|
563
|
-
await new Promise((
|
|
1073
|
+
await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) * 1e3));
|
|
564
1074
|
}
|
|
565
1075
|
}
|
|
566
1076
|
throw lastError;
|
|
@@ -929,7 +1439,7 @@ var uploadFile = async (file, fileName, config, options = {}, user, customBucket
|
|
|
929
1439
|
if (error.name === "SignatureDoesNotMatch" || error.name === "InvalidAccessKeyId" || error.name === "AccessDenied") {
|
|
930
1440
|
if (attempt < maxRetries) {
|
|
931
1441
|
const backoffMs = Math.pow(2, attempt) * 1e3;
|
|
932
|
-
await new Promise((
|
|
1442
|
+
await new Promise((resolve3) => setTimeout(resolve3, backoffMs));
|
|
933
1443
|
s3Client = void 0;
|
|
934
1444
|
getS3Client(config);
|
|
935
1445
|
continue;
|
|
@@ -2279,6 +2789,10 @@ var agentMessagesSchema = {
|
|
|
2279
2789
|
{
|
|
2280
2790
|
name: "session",
|
|
2281
2791
|
type: "text"
|
|
2792
|
+
},
|
|
2793
|
+
{
|
|
2794
|
+
name: "model",
|
|
2795
|
+
type: "text"
|
|
2282
2796
|
}
|
|
2283
2797
|
]
|
|
2284
2798
|
};
|
|
@@ -2459,6 +2973,11 @@ var agentsSchema = {
|
|
|
2459
2973
|
name: "feedback",
|
|
2460
2974
|
type: "boolean"
|
|
2461
2975
|
},
|
|
2976
|
+
{
|
|
2977
|
+
name: "suggestions_enabled",
|
|
2978
|
+
type: "boolean",
|
|
2979
|
+
default: false
|
|
2980
|
+
},
|
|
2462
2981
|
{
|
|
2463
2982
|
name: "description",
|
|
2464
2983
|
type: "text"
|
|
@@ -2477,11 +2996,7 @@ var agentsSchema = {
|
|
|
2477
2996
|
// allows selecting a exulu context as native memory for the agent
|
|
2478
2997
|
},
|
|
2479
2998
|
{
|
|
2480
|
-
name: "
|
|
2481
|
-
type: "text"
|
|
2482
|
-
},
|
|
2483
|
-
{
|
|
2484
|
-
name: "provider",
|
|
2999
|
+
name: "model",
|
|
2485
3000
|
type: "text"
|
|
2486
3001
|
},
|
|
2487
3002
|
{
|
|
@@ -2511,6 +3026,59 @@ var agentsSchema = {
|
|
|
2511
3026
|
}
|
|
2512
3027
|
]
|
|
2513
3028
|
};
|
|
3029
|
+
var modelsSchema = {
|
|
3030
|
+
type: "models",
|
|
3031
|
+
name: {
|
|
3032
|
+
plural: "models",
|
|
3033
|
+
singular: "model"
|
|
3034
|
+
},
|
|
3035
|
+
RBAC: true,
|
|
3036
|
+
fields: [
|
|
3037
|
+
{
|
|
3038
|
+
name: "name",
|
|
3039
|
+
type: "text",
|
|
3040
|
+
required: true
|
|
3041
|
+
},
|
|
3042
|
+
{
|
|
3043
|
+
name: "description",
|
|
3044
|
+
type: "text"
|
|
3045
|
+
},
|
|
3046
|
+
{
|
|
3047
|
+
name: "provider",
|
|
3048
|
+
type: "text",
|
|
3049
|
+
required: true
|
|
3050
|
+
},
|
|
3051
|
+
{
|
|
3052
|
+
name: "authvariable",
|
|
3053
|
+
type: "text"
|
|
3054
|
+
},
|
|
3055
|
+
{
|
|
3056
|
+
name: "active",
|
|
3057
|
+
type: "boolean",
|
|
3058
|
+
default: true
|
|
3059
|
+
},
|
|
3060
|
+
{
|
|
3061
|
+
name: "requests_per_window",
|
|
3062
|
+
type: "number"
|
|
3063
|
+
},
|
|
3064
|
+
{
|
|
3065
|
+
name: "window_seconds",
|
|
3066
|
+
type: "number"
|
|
3067
|
+
},
|
|
3068
|
+
{
|
|
3069
|
+
name: "token_budget",
|
|
3070
|
+
type: "number"
|
|
3071
|
+
},
|
|
3072
|
+
{
|
|
3073
|
+
name: "cost_budget_usd",
|
|
3074
|
+
type: "number"
|
|
3075
|
+
},
|
|
3076
|
+
{
|
|
3077
|
+
name: "budget_window",
|
|
3078
|
+
type: "text"
|
|
3079
|
+
}
|
|
3080
|
+
]
|
|
3081
|
+
};
|
|
2514
3082
|
var usersSchema = {
|
|
2515
3083
|
type: "users",
|
|
2516
3084
|
name: {
|
|
@@ -2806,6 +3374,7 @@ var coreSchemas = {
|
|
|
2806
3374
|
agentsSchema: () => addCoreFields(agentsSchema),
|
|
2807
3375
|
agentMessagesSchema: () => addCoreFields(agentMessagesSchema),
|
|
2808
3376
|
agentSessionsSchema: () => addCoreFields(agentSessionsSchema),
|
|
3377
|
+
modelsSchema: () => addCoreFields(modelsSchema),
|
|
2809
3378
|
projectsSchema: () => addCoreFields(projectsSchema),
|
|
2810
3379
|
usersSchema: () => addCoreFields(usersSchema),
|
|
2811
3380
|
skillsSchema: () => addCoreFields(skillsSchema),
|
|
@@ -5667,14 +6236,14 @@ import {
|
|
|
5667
6236
|
SandboxManager
|
|
5668
6237
|
} from "@anthropic-ai/sandbox-runtime";
|
|
5669
6238
|
import { mkdir as mkdir2, rm, writeFile as writeFile2, readFile as fsReadFile, readdir, stat } from "fs/promises";
|
|
5670
|
-
import { existsSync as
|
|
5671
|
-
import { join as join3, dirname, resolve, relative, posix } from "path";
|
|
5672
|
-
import { exec as exec2, spawn } from "child_process";
|
|
6239
|
+
import { existsSync as existsSync3 } from "fs";
|
|
6240
|
+
import { join as join3, dirname, resolve as resolve2, relative, posix } from "path";
|
|
6241
|
+
import { exec as exec2, spawn as spawn2 } from "child_process";
|
|
5673
6242
|
import { promisify as promisify2 } from "util";
|
|
5674
6243
|
|
|
5675
6244
|
// src/exulu/system-dependencies.ts
|
|
5676
6245
|
import { exec } from "child_process";
|
|
5677
|
-
import { existsSync } from "fs";
|
|
6246
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5678
6247
|
import { join as join2 } from "path";
|
|
5679
6248
|
import { promisify } from "util";
|
|
5680
6249
|
var execAsync = promisify(exec);
|
|
@@ -5740,7 +6309,7 @@ async function probeDependency(dep) {
|
|
|
5740
6309
|
case "npm-global": {
|
|
5741
6310
|
const root = await getNpmGlobalRoot();
|
|
5742
6311
|
if (!root) return false;
|
|
5743
|
-
return
|
|
6312
|
+
return existsSync2(join2(root, dep.check.packageName));
|
|
5744
6313
|
}
|
|
5745
6314
|
}
|
|
5746
6315
|
}
|
|
@@ -5840,7 +6409,7 @@ async function downloadSkill(skill, skillsDirectory, config) {
|
|
|
5840
6409
|
}
|
|
5841
6410
|
}
|
|
5842
6411
|
function isArtifactPath(absPath, sessionDir) {
|
|
5843
|
-
const resolved =
|
|
6412
|
+
const resolved = resolve2(absPath);
|
|
5844
6413
|
const rel = relative(sessionDir, resolved);
|
|
5845
6414
|
if (!rel || rel.startsWith("..")) return false;
|
|
5846
6415
|
const first = rel.split("/")[0];
|
|
@@ -5899,7 +6468,7 @@ async function restoreArtifactsFromS3(sessionDir, sessionId, userId, config) {
|
|
|
5899
6468
|
async function downloadKeyIntoSandbox(opts) {
|
|
5900
6469
|
const { sessionId, userId, fullS3Key, config } = opts;
|
|
5901
6470
|
const sessionDir = join3("/tmp", "exulu-sessions", sessionId);
|
|
5902
|
-
if (!
|
|
6471
|
+
if (!existsSync3(sessionDir)) {
|
|
5903
6472
|
return { written: false };
|
|
5904
6473
|
}
|
|
5905
6474
|
const userPrefix = `user_${userId}/sessions/${sessionId}/`;
|
|
@@ -5933,7 +6502,7 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
5933
6502
|
return cached.handle;
|
|
5934
6503
|
}
|
|
5935
6504
|
const sessionDir = join3("/tmp", "exulu-sessions", sessionId);
|
|
5936
|
-
const dirExisted =
|
|
6505
|
+
const dirExisted = existsSync3(sessionDir);
|
|
5937
6506
|
await mkdir2(sessionDir, { recursive: true });
|
|
5938
6507
|
const skillsDirectory = join3(sessionDir, "skills");
|
|
5939
6508
|
const installedSkills = /* @__PURE__ */ new Map();
|
|
@@ -6040,7 +6609,7 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
6040
6609
|
if (!persistenceEnabled || !isArtifactPath(absPath, sessionDir)) {
|
|
6041
6610
|
return {};
|
|
6042
6611
|
}
|
|
6043
|
-
const rel = relative(sessionDir,
|
|
6612
|
+
const rel = relative(sessionDir, resolve2(absPath));
|
|
6044
6613
|
const s3Key = artifactS3Key(sessionId, rel);
|
|
6045
6614
|
const out = {};
|
|
6046
6615
|
try {
|
|
@@ -6085,7 +6654,7 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
6085
6654
|
sessionSandboxConfig
|
|
6086
6655
|
);
|
|
6087
6656
|
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
6088
|
-
const child =
|
|
6657
|
+
const child = spawn2("/bin/bash", ["-c", wrapped]);
|
|
6089
6658
|
let stderr = "";
|
|
6090
6659
|
child.stderr.on("data", (chunk) => {
|
|
6091
6660
|
stderr += chunk.toString();
|
|
@@ -6595,9 +7164,18 @@ export {
|
|
|
6595
7164
|
postgresClient,
|
|
6596
7165
|
authentication,
|
|
6597
7166
|
STATISTICS_TYPE_ENUM,
|
|
7167
|
+
isLiteLLMEnabled,
|
|
7168
|
+
setLiteLLMPackageRoot,
|
|
7169
|
+
startLiteLLMSupervisor,
|
|
7170
|
+
waitForLiteLLMReady,
|
|
6598
7171
|
sanitizeName,
|
|
7172
|
+
checkRecordAccess,
|
|
6599
7173
|
checkLicense,
|
|
6600
7174
|
exuluApp,
|
|
7175
|
+
buildTags,
|
|
7176
|
+
createTaggedFetch,
|
|
7177
|
+
ResolveModelError,
|
|
7178
|
+
resolveModel,
|
|
6601
7179
|
updateStatistic,
|
|
6602
7180
|
createProjectItemsRetrievalTool,
|
|
6603
7181
|
sanitizeToolName,
|