@axiom-lattice/gateway 4.0.2 → 4.1.1
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/index.js +1235 -254
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1116 -138
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -69,8 +69,8 @@ function resolveLocalA2AWorkingDirectory(options) {
|
|
|
69
69
|
}
|
|
70
70
|
function validateWorkingDirectory(cwd) {
|
|
71
71
|
try {
|
|
72
|
-
const
|
|
73
|
-
if (!
|
|
72
|
+
const stat2 = statSync(cwd);
|
|
73
|
+
if (!stat2.isDirectory()) {
|
|
74
74
|
return `Working directory is not a directory: ${cwd}`;
|
|
75
75
|
}
|
|
76
76
|
return null;
|
|
@@ -846,8 +846,8 @@ async function pipeAsyncIterableToSse(raw, stream, options = {}) {
|
|
|
846
846
|
let closed = raw.destroyed || raw.writableEnded;
|
|
847
847
|
let returned = false;
|
|
848
848
|
let resolveClosed;
|
|
849
|
-
const closedPromise = new Promise((
|
|
850
|
-
resolveClosed =
|
|
849
|
+
const closedPromise = new Promise((resolve3) => {
|
|
850
|
+
resolveClosed = resolve3;
|
|
851
851
|
});
|
|
852
852
|
const returnConsumer = () => {
|
|
853
853
|
if (returned) return;
|
|
@@ -897,14 +897,14 @@ async function pipeAsyncIterableToSse(raw, stream, options = {}) {
|
|
|
897
897
|
async function writeSse(raw, value, closed) {
|
|
898
898
|
if (raw.destroyed || raw.writableEnded) return false;
|
|
899
899
|
if (raw.write(value)) return true;
|
|
900
|
-
return new Promise((
|
|
900
|
+
return new Promise((resolve3) => {
|
|
901
901
|
let settled = false;
|
|
902
902
|
const onDrain = () => finish(!raw.destroyed && !raw.writableEnded);
|
|
903
903
|
const finish = (writable) => {
|
|
904
904
|
if (settled) return;
|
|
905
905
|
settled = true;
|
|
906
906
|
raw.removeListener("drain", onDrain);
|
|
907
|
-
|
|
907
|
+
resolve3(writable);
|
|
908
908
|
};
|
|
909
909
|
raw.once("drain", onDrain);
|
|
910
910
|
void closed.then(() => finish(false));
|
|
@@ -1116,7 +1116,7 @@ var recoverRun = async (request, reply) => {
|
|
|
1116
1116
|
const { assistantId, threadId } = request.params;
|
|
1117
1117
|
const tenant_id = request.headers["x-tenant-id"];
|
|
1118
1118
|
const workspace_id = request.headers["x-workspace-id"] || "default";
|
|
1119
|
-
const project_id = request.headers["x-project-id"]
|
|
1119
|
+
const project_id = request.headers["x-project-id"];
|
|
1120
1120
|
const agent = agentInstanceManager2.getAgent({
|
|
1121
1121
|
assistant_id: assistantId,
|
|
1122
1122
|
thread_id: threadId,
|
|
@@ -1124,8 +1124,7 @@ var recoverRun = async (request, reply) => {
|
|
|
1124
1124
|
workspace_id,
|
|
1125
1125
|
project_id
|
|
1126
1126
|
});
|
|
1127
|
-
await agent.resumeTask();
|
|
1128
|
-
const status = await agent.getRunStatus();
|
|
1127
|
+
const { status } = await agent.resumeTask();
|
|
1129
1128
|
reply.status(200).send({
|
|
1130
1129
|
success: true,
|
|
1131
1130
|
threadId,
|
|
@@ -2015,7 +2014,8 @@ async function getHealth(request, reply) {
|
|
|
2015
2014
|
}
|
|
2016
2015
|
|
|
2017
2016
|
// src/services/skill_service.ts
|
|
2018
|
-
import { SandboxSkillStore } from "@axiom-lattice/core";
|
|
2017
|
+
import { SandboxSkillStore, PluginRegistry } from "@axiom-lattice/core";
|
|
2018
|
+
var PLUGIN_SKILL_SOURCE = "builtin-plugin";
|
|
2019
2019
|
function getTenantId4(request) {
|
|
2020
2020
|
return request.user?.tenantId || request.headers["x-tenant-id"] || "default";
|
|
2021
2021
|
}
|
|
@@ -2031,28 +2031,63 @@ function buildContext(request) {
|
|
|
2031
2031
|
projectId: getProjectId(request)
|
|
2032
2032
|
};
|
|
2033
2033
|
}
|
|
2034
|
+
function pluginSkillToSkill(item, tenantId, content) {
|
|
2035
|
+
return {
|
|
2036
|
+
id: item.id,
|
|
2037
|
+
tenantId,
|
|
2038
|
+
name: item.name,
|
|
2039
|
+
description: item.description,
|
|
2040
|
+
version: item.version,
|
|
2041
|
+
pluginType: item.pluginType,
|
|
2042
|
+
resourcePaths: item.resourcePaths,
|
|
2043
|
+
source: PLUGIN_SKILL_SOURCE,
|
|
2044
|
+
readOnly: true,
|
|
2045
|
+
content,
|
|
2046
|
+
createdAt: /* @__PURE__ */ new Date(0),
|
|
2047
|
+
updatedAt: /* @__PURE__ */ new Date(0)
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2034
2050
|
var SkillService = class {
|
|
2035
2051
|
constructor() {
|
|
2036
2052
|
this.store = new SandboxSkillStore();
|
|
2037
2053
|
}
|
|
2038
2054
|
async getAllSkills(request) {
|
|
2039
2055
|
const tenantId = getTenantId4(request);
|
|
2040
|
-
|
|
2056
|
+
const storeSkills = await this.store.getAllSkills(tenantId, buildContext(request));
|
|
2057
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2058
|
+
for (const skill of storeSkills) {
|
|
2059
|
+
merged.set(skill.id, skill);
|
|
2060
|
+
}
|
|
2061
|
+
for (const item of PluginRegistry.listSkills()) {
|
|
2062
|
+
merged.set(item.id, pluginSkillToSkill(item, tenantId));
|
|
2063
|
+
}
|
|
2064
|
+
return Array.from(merged.values());
|
|
2041
2065
|
}
|
|
2042
2066
|
async getSkillById(request, id) {
|
|
2043
2067
|
const tenantId = getTenantId4(request);
|
|
2068
|
+
const plugin = PluginRegistry.getSkill(id);
|
|
2069
|
+
if (plugin) {
|
|
2070
|
+
const { definition } = plugin;
|
|
2071
|
+
const item = PluginRegistry.listSkills().find((entry) => entry.id === id);
|
|
2072
|
+
if (item) {
|
|
2073
|
+
return pluginSkillToSkill(item, tenantId, definition.content);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2044
2076
|
return this.store.getSkillById(tenantId, id, buildContext(request));
|
|
2045
2077
|
}
|
|
2046
2078
|
async createSkill(request, id, data) {
|
|
2047
2079
|
const tenantId = getTenantId4(request);
|
|
2080
|
+
this.assertNotPluginSkill(id, "created");
|
|
2048
2081
|
return this.store.createSkill(tenantId, id, data, buildContext(request));
|
|
2049
2082
|
}
|
|
2050
2083
|
async updateSkill(request, id, updates) {
|
|
2051
2084
|
const tenantId = getTenantId4(request);
|
|
2085
|
+
this.assertNotPluginSkill(id, "modified");
|
|
2052
2086
|
return this.store.updateSkill(tenantId, id, updates, buildContext(request));
|
|
2053
2087
|
}
|
|
2054
2088
|
async deleteSkill(request, id) {
|
|
2055
2089
|
const tenantId = getTenantId4(request);
|
|
2090
|
+
this.assertNotPluginSkill(id, "deleted");
|
|
2056
2091
|
return this.store.deleteSkill(tenantId, id, buildContext(request));
|
|
2057
2092
|
}
|
|
2058
2093
|
async searchByMetadata(request, key, value) {
|
|
@@ -2067,6 +2102,16 @@ var SkillService = class {
|
|
|
2067
2102
|
const tenantId = getTenantId4(request);
|
|
2068
2103
|
return this.store.filterByLicense(tenantId, license, buildContext(request));
|
|
2069
2104
|
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Guard against mutating a registered plugin skill.
|
|
2107
|
+
* Message style mirrors SandboxSkillStore built-in errors so the controller's
|
|
2108
|
+
* existing "built-in skill" -> 403 mapping applies.
|
|
2109
|
+
*/
|
|
2110
|
+
assertNotPluginSkill(id, action) {
|
|
2111
|
+
if (PluginRegistry.getSkill(id)) {
|
|
2112
|
+
throw new Error(`Skill "${id}" is a built-in skill and cannot be ${action}`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2070
2115
|
};
|
|
2071
2116
|
|
|
2072
2117
|
// src/controllers/skills.ts
|
|
@@ -2081,6 +2126,11 @@ function serializeSkill(skill) {
|
|
|
2081
2126
|
metadata: skill.metadata || {},
|
|
2082
2127
|
content: skill.content,
|
|
2083
2128
|
subSkills: skill.subSkills,
|
|
2129
|
+
source: skill.source,
|
|
2130
|
+
pluginType: skill.pluginType,
|
|
2131
|
+
version: skill.version,
|
|
2132
|
+
resources: skill.resourcePaths,
|
|
2133
|
+
readOnly: skill.readOnly,
|
|
2084
2134
|
createdAt: skill.createdAt instanceof Date ? skill.createdAt.toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
2085
2135
|
updatedAt: skill.updatedAt instanceof Date ? skill.updatedAt.toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
2086
2136
|
};
|
|
@@ -3712,7 +3762,7 @@ var ExportImportService = class {
|
|
|
3712
3762
|
const files = /* @__PURE__ */ new Map();
|
|
3713
3763
|
let bundle;
|
|
3714
3764
|
const gunzip = zlib.createGunzip();
|
|
3715
|
-
await new Promise((
|
|
3765
|
+
await new Promise((resolve3, reject) => {
|
|
3716
3766
|
let settled = false;
|
|
3717
3767
|
let entryCount = 0;
|
|
3718
3768
|
let declaredTotalBytes = 0;
|
|
@@ -3832,7 +3882,7 @@ var ExportImportService = class {
|
|
|
3832
3882
|
parser.on("end", () => {
|
|
3833
3883
|
if (settled) return;
|
|
3834
3884
|
settled = true;
|
|
3835
|
-
|
|
3885
|
+
resolve3();
|
|
3836
3886
|
});
|
|
3837
3887
|
gunzip.pipe(parser);
|
|
3838
3888
|
gunzip.end(buffer);
|
|
@@ -4439,10 +4489,10 @@ async function importApply(request, reply) {
|
|
|
4439
4489
|
}
|
|
4440
4490
|
|
|
4441
4491
|
// src/controllers/middleware-types.ts
|
|
4442
|
-
import { PluginRegistry } from "@axiom-lattice/core";
|
|
4492
|
+
import { PluginRegistry as PluginRegistry2 } from "@axiom-lattice/core";
|
|
4443
4493
|
function middlewareTypesRoutes(fastify2) {
|
|
4444
4494
|
fastify2.get("/api/middleware-types", async (_request, reply) => {
|
|
4445
|
-
const metas =
|
|
4495
|
+
const metas = PluginRegistry2.listMeta();
|
|
4446
4496
|
return reply.send(metas);
|
|
4447
4497
|
});
|
|
4448
4498
|
}
|
|
@@ -4517,6 +4567,20 @@ var dataQuerySchema = {
|
|
|
4517
4567
|
};
|
|
4518
4568
|
|
|
4519
4569
|
// src/schemas/index.ts
|
|
4570
|
+
var downloadFolderSchema = {
|
|
4571
|
+
description: "Download a workspace project folder as a ZIP archive",
|
|
4572
|
+
tags: ["Workspace"],
|
|
4573
|
+
querystring: {
|
|
4574
|
+
type: "object",
|
|
4575
|
+
properties: {
|
|
4576
|
+
path: { type: "string", minLength: 1 },
|
|
4577
|
+
assistantId: { type: "string", minLength: 1 },
|
|
4578
|
+
tenantId: { type: "string", minLength: 1 }
|
|
4579
|
+
},
|
|
4580
|
+
required: ["path"],
|
|
4581
|
+
additionalProperties: false
|
|
4582
|
+
}
|
|
4583
|
+
};
|
|
4520
4584
|
var getAllMemoryItemsSchema = {
|
|
4521
4585
|
description: "Get all memory items for an assistant thread",
|
|
4522
4586
|
tags: ["Memory"],
|
|
@@ -5114,11 +5178,93 @@ function registerSandboxProxyRoutes(app2) {
|
|
|
5114
5178
|
// src/controllers/workspace.ts
|
|
5115
5179
|
import * as fs2 from "fs/promises";
|
|
5116
5180
|
import * as path2 from "path";
|
|
5181
|
+
import { constants as fsConstants } from "fs";
|
|
5117
5182
|
import * as os2 from "os";
|
|
5118
5183
|
import { getStoreLattice as getStoreLattice6 } from "@axiom-lattice/core";
|
|
5119
5184
|
import { SandboxFilesystem, FilesystemBackend } from "@axiom-lattice/core";
|
|
5120
5185
|
import { getSandBoxManager as getSandBoxManager3 } from "@axiom-lattice/core";
|
|
5121
5186
|
import { v4 as uuidv42 } from "uuid";
|
|
5187
|
+
var MAX_ZIP_ENTRIES = 1e4;
|
|
5188
|
+
var MAX_ZIP_FILE_SIZE = 100 * 1024 * 1024;
|
|
5189
|
+
var MAX_ZIP_TOTAL_SIZE = 512 * 1024 * 1024;
|
|
5190
|
+
var FolderDownloadError = class extends Error {
|
|
5191
|
+
constructor(statusCode, code, message) {
|
|
5192
|
+
super(message);
|
|
5193
|
+
this.statusCode = statusCode;
|
|
5194
|
+
this.code = code;
|
|
5195
|
+
this.name = "FolderDownloadError";
|
|
5196
|
+
}
|
|
5197
|
+
};
|
|
5198
|
+
function zipCrc32(data) {
|
|
5199
|
+
let crc = 4294967295;
|
|
5200
|
+
for (const byte of data) {
|
|
5201
|
+
crc ^= byte;
|
|
5202
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
5203
|
+
crc = crc >>> 1 ^ 3988292384 & -(crc & 1);
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
5206
|
+
return (crc ^ 4294967295) >>> 0;
|
|
5207
|
+
}
|
|
5208
|
+
function createZip(entries) {
|
|
5209
|
+
const localParts = [];
|
|
5210
|
+
const centralParts = [];
|
|
5211
|
+
let offset = 0;
|
|
5212
|
+
for (const entry of entries) {
|
|
5213
|
+
const name = Buffer.from(entry.name, "utf8");
|
|
5214
|
+
const data = entry.data;
|
|
5215
|
+
const crc = zipCrc32(data);
|
|
5216
|
+
const local = Buffer.alloc(30 + name.length + data.length);
|
|
5217
|
+
local.writeUInt32LE(67324752, 0);
|
|
5218
|
+
local.writeUInt16LE(20, 4);
|
|
5219
|
+
local.writeUInt16LE(0, 6);
|
|
5220
|
+
local.writeUInt16LE(2048, 8);
|
|
5221
|
+
local.writeUInt16LE(0, 10);
|
|
5222
|
+
local.writeUInt16LE(0, 12);
|
|
5223
|
+
local.writeUInt32LE(crc, 14);
|
|
5224
|
+
local.writeUInt32LE(data.length, 18);
|
|
5225
|
+
local.writeUInt32LE(data.length, 22);
|
|
5226
|
+
local.writeUInt16LE(name.length, 26);
|
|
5227
|
+
local.writeUInt16LE(0, 28);
|
|
5228
|
+
name.copy(local, 30);
|
|
5229
|
+
data.copy(local, 30 + name.length);
|
|
5230
|
+
localParts.push(local);
|
|
5231
|
+
const central = Buffer.alloc(46 + name.length);
|
|
5232
|
+
central.writeUInt32LE(33639248, 0);
|
|
5233
|
+
central.writeUInt16LE(20, 4);
|
|
5234
|
+
central.writeUInt16LE(20, 6);
|
|
5235
|
+
central.writeUInt16LE(0, 8);
|
|
5236
|
+
central.writeUInt16LE(2048, 10);
|
|
5237
|
+
central.writeUInt16LE(0, 12);
|
|
5238
|
+
central.writeUInt16LE(0, 14);
|
|
5239
|
+
central.writeUInt32LE(crc, 16);
|
|
5240
|
+
central.writeUInt32LE(data.length, 20);
|
|
5241
|
+
central.writeUInt32LE(data.length, 24);
|
|
5242
|
+
central.writeUInt16LE(name.length, 28);
|
|
5243
|
+
central.writeUInt16LE(0, 30);
|
|
5244
|
+
central.writeUInt16LE(0, 32);
|
|
5245
|
+
central.writeUInt16LE(0, 34);
|
|
5246
|
+
central.writeUInt16LE(entry.directory ? 16 : 0, 36);
|
|
5247
|
+
central.writeUInt32LE(entry.directory ? 16 : 0, 38);
|
|
5248
|
+
central.writeUInt32LE(offset, 42);
|
|
5249
|
+
name.copy(central, 46);
|
|
5250
|
+
centralParts.push(central);
|
|
5251
|
+
offset += local.length;
|
|
5252
|
+
}
|
|
5253
|
+
const centralDirectory = Buffer.concat(centralParts);
|
|
5254
|
+
const end = Buffer.alloc(22);
|
|
5255
|
+
end.writeUInt32LE(101010256, 0);
|
|
5256
|
+
end.writeUInt16LE(0, 4);
|
|
5257
|
+
end.writeUInt16LE(0, 6);
|
|
5258
|
+
end.writeUInt16LE(entries.length, 8);
|
|
5259
|
+
end.writeUInt16LE(entries.length, 10);
|
|
5260
|
+
end.writeUInt32LE(centralDirectory.length, 12);
|
|
5261
|
+
end.writeUInt32LE(offset, 16);
|
|
5262
|
+
return Buffer.concat([...localParts, centralDirectory, end]);
|
|
5263
|
+
}
|
|
5264
|
+
var CAPABILITY_BUNDLE_CONFIG_MESSAGE = "Use /api/projects/:projectId/capability-bundles to update capability bundle IDs";
|
|
5265
|
+
function writesCapabilityBundleIds(config) {
|
|
5266
|
+
return config !== void 0 && Object.prototype.hasOwnProperty.call(config, "capabilityBundleIds");
|
|
5267
|
+
}
|
|
5122
5268
|
var WorkspaceController = class {
|
|
5123
5269
|
constructor() {
|
|
5124
5270
|
this.workspaceStore = getStoreLattice6("default", "workspace").store;
|
|
@@ -5226,6 +5372,13 @@ var WorkspaceController = class {
|
|
|
5226
5372
|
const tenantId = this.getTenantId(request);
|
|
5227
5373
|
const { workspaceId } = request.params;
|
|
5228
5374
|
const data = request.body;
|
|
5375
|
+
if (writesCapabilityBundleIds(data.config)) {
|
|
5376
|
+
return reply.status(400).send({
|
|
5377
|
+
success: false,
|
|
5378
|
+
code: "INVALID_BUNDLE_CONFIG",
|
|
5379
|
+
message: CAPABILITY_BUNDLE_CONFIG_MESSAGE
|
|
5380
|
+
});
|
|
5381
|
+
}
|
|
5229
5382
|
const id = uuidv42();
|
|
5230
5383
|
const project = await this.projectStore.createProject(
|
|
5231
5384
|
tenantId,
|
|
@@ -5248,6 +5401,13 @@ var WorkspaceController = class {
|
|
|
5248
5401
|
const tenantId = this.getTenantId(request);
|
|
5249
5402
|
const { projectId } = request.params;
|
|
5250
5403
|
const updates = request.body;
|
|
5404
|
+
if (writesCapabilityBundleIds(updates.config)) {
|
|
5405
|
+
return reply.status(400).send({
|
|
5406
|
+
success: false,
|
|
5407
|
+
code: "INVALID_BUNDLE_CONFIG",
|
|
5408
|
+
message: CAPABILITY_BUNDLE_CONFIG_MESSAGE
|
|
5409
|
+
});
|
|
5410
|
+
}
|
|
5251
5411
|
const project = await this.projectStore.updateProject(
|
|
5252
5412
|
tenantId,
|
|
5253
5413
|
projectId,
|
|
@@ -5384,6 +5544,293 @@ var WorkspaceController = class {
|
|
|
5384
5544
|
return reply.status(502).send({ success: false, error: `Download proxy error: ${message}` });
|
|
5385
5545
|
}
|
|
5386
5546
|
}
|
|
5547
|
+
async downloadFolder(request, reply) {
|
|
5548
|
+
const tenantId = this.getTenantId(request);
|
|
5549
|
+
const { workspaceId, projectId } = request.params;
|
|
5550
|
+
const folderPath = request.query.path;
|
|
5551
|
+
const assistantId = request.query.assistantId;
|
|
5552
|
+
if (!folderPath || folderPath.includes("\\") || folderPath.split("/").includes("..")) {
|
|
5553
|
+
return reply.status(400).send({
|
|
5554
|
+
success: false,
|
|
5555
|
+
error: "Invalid folder path",
|
|
5556
|
+
code: "INVALID_PATH"
|
|
5557
|
+
});
|
|
5558
|
+
}
|
|
5559
|
+
try {
|
|
5560
|
+
const storedWorkspace = await this.workspaceStore.getWorkspaceById(tenantId, workspaceId);
|
|
5561
|
+
if (!storedWorkspace) {
|
|
5562
|
+
throw new FolderDownloadError(404, "WORKSPACE_NOT_FOUND", "Workspace not found");
|
|
5563
|
+
}
|
|
5564
|
+
const project = await this.projectStore.getProjectById(tenantId, projectId);
|
|
5565
|
+
if (!project || project.workspaceId !== workspaceId) {
|
|
5566
|
+
throw new FolderDownloadError(404, "PROJECT_NOT_FOUND", "Project not found");
|
|
5567
|
+
}
|
|
5568
|
+
if (storedWorkspace.storageType === "sandbox") {
|
|
5569
|
+
const sandboxManager = getSandBoxManager3();
|
|
5570
|
+
const volumeConfig = {
|
|
5571
|
+
assistant_id: assistantId || "",
|
|
5572
|
+
thread_id: "",
|
|
5573
|
+
tenantId,
|
|
5574
|
+
workspaceId,
|
|
5575
|
+
projectId
|
|
5576
|
+
};
|
|
5577
|
+
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
5578
|
+
if (!sandbox.packDirectory || !sandbox.file.deleteFile) {
|
|
5579
|
+
throw new FolderDownloadError(502, "BACKEND_UNSUPPORTED", "Backend does not support folder archive cleanup");
|
|
5580
|
+
}
|
|
5581
|
+
let packed;
|
|
5582
|
+
try {
|
|
5583
|
+
packed = await sandbox.packDirectory(folderPath);
|
|
5584
|
+
if (packed.size === void 0) {
|
|
5585
|
+
throw new FolderDownloadError(502, "BACKEND_UNSUPPORTED", "Backend does not report packed archive size");
|
|
5586
|
+
}
|
|
5587
|
+
if (!Number.isFinite(packed.size) || packed.size < 0 || packed.size > MAX_ZIP_TOTAL_SIZE) {
|
|
5588
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Packed folder exceeds download size limits");
|
|
5589
|
+
}
|
|
5590
|
+
const buffer = await sandbox.file.downloadFile({ file: packed.path });
|
|
5591
|
+
return reply.status(200).type("application/zip").header("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(packed.filename)}`).send(buffer);
|
|
5592
|
+
} finally {
|
|
5593
|
+
if (packed) {
|
|
5594
|
+
try {
|
|
5595
|
+
await sandbox.file.deleteFile(packed.path);
|
|
5596
|
+
} catch (cleanupError) {
|
|
5597
|
+
console.warn("Failed to clean up packed sandbox folder archive", cleanupError);
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
5602
|
+
const { backend, workspace } = await this.getBackend(
|
|
5603
|
+
tenantId,
|
|
5604
|
+
workspaceId,
|
|
5605
|
+
projectId,
|
|
5606
|
+
assistantId,
|
|
5607
|
+
folderPath
|
|
5608
|
+
);
|
|
5609
|
+
if (!backend.lsInfo) {
|
|
5610
|
+
throw new FolderDownloadError(502, "BACKEND_UNSUPPORTED", "Backend does not support folder downloads");
|
|
5611
|
+
}
|
|
5612
|
+
let listEntries2;
|
|
5613
|
+
if (workspace.storageType !== "sandbox") {
|
|
5614
|
+
const root = filesystemRootDirectory(tenantId, workspaceId, projectId);
|
|
5615
|
+
const target = path2.resolve(root, `.${folderPath.startsWith("/") ? folderPath : `/${folderPath}`}`);
|
|
5616
|
+
const contained = await resolvePathWithinRoot(root, target).catch((error) => {
|
|
5617
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5618
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5619
|
+
}
|
|
5620
|
+
throw error;
|
|
5621
|
+
});
|
|
5622
|
+
let stat2;
|
|
5623
|
+
try {
|
|
5624
|
+
stat2 = await fs2.stat(contained.targetPath);
|
|
5625
|
+
} catch (error) {
|
|
5626
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5627
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5628
|
+
}
|
|
5629
|
+
throw error;
|
|
5630
|
+
}
|
|
5631
|
+
if (!stat2.isDirectory()) {
|
|
5632
|
+
throw new FolderDownloadError(400, "NOT_A_FOLDER", "Path is not a folder");
|
|
5633
|
+
}
|
|
5634
|
+
} else if (folderPath) {
|
|
5635
|
+
const sandboxManager = getSandBoxManager3();
|
|
5636
|
+
const volumeConfig = {
|
|
5637
|
+
assistant_id: assistantId || "",
|
|
5638
|
+
thread_id: "",
|
|
5639
|
+
tenantId,
|
|
5640
|
+
workspaceId,
|
|
5641
|
+
projectId
|
|
5642
|
+
};
|
|
5643
|
+
const volume = await sandboxManager.getVolumeBackendForPath(volumeConfig, folderPath);
|
|
5644
|
+
if (!volume) {
|
|
5645
|
+
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
5646
|
+
try {
|
|
5647
|
+
await sandbox.file.listPath(folderPath, { recursive: false });
|
|
5648
|
+
} catch (error) {
|
|
5649
|
+
const code = error instanceof Error ? error.code : void 0;
|
|
5650
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5651
|
+
if (code === "ENOENT" || /not found|no such file|enoent/i.test(message)) {
|
|
5652
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5653
|
+
}
|
|
5654
|
+
throw error;
|
|
5655
|
+
}
|
|
5656
|
+
listEntries2 = async (directoryPath) => {
|
|
5657
|
+
const result = await sandbox.file.listPath(directoryPath, { recursive: false });
|
|
5658
|
+
return result.files;
|
|
5659
|
+
};
|
|
5660
|
+
} else {
|
|
5661
|
+
listEntries2 = async (directoryPath) => backend.lsInfo(directoryPath);
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
if (!listEntries2) {
|
|
5665
|
+
listEntries2 = async (directoryPath) => backend.lsInfo(directoryPath);
|
|
5666
|
+
}
|
|
5667
|
+
const folderName = path2.basename(folderPath.replace(/\/$/, "")) || "folder";
|
|
5668
|
+
const prefix = `${folderName}/`;
|
|
5669
|
+
const entries = [{ name: prefix, data: Buffer.alloc(0), directory: true }];
|
|
5670
|
+
let totalSize = 0;
|
|
5671
|
+
const filesystemRoot = workspace.storageType === "sandbox" ? void 0 : filesystemRootDirectory(tenantId, workspaceId, projectId);
|
|
5672
|
+
const addFile = (archivePath, data) => {
|
|
5673
|
+
if (data.length > MAX_ZIP_FILE_SIZE || totalSize + data.length > MAX_ZIP_TOTAL_SIZE) {
|
|
5674
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
5675
|
+
}
|
|
5676
|
+
totalSize += data.length;
|
|
5677
|
+
entries.push({ name: archivePath, data, directory: false });
|
|
5678
|
+
};
|
|
5679
|
+
const collectFilesystem = async (directoryPath, archivePath) => {
|
|
5680
|
+
let children;
|
|
5681
|
+
try {
|
|
5682
|
+
children = await fs2.readdir(directoryPath, { withFileTypes: true });
|
|
5683
|
+
} catch (error) {
|
|
5684
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5685
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5686
|
+
}
|
|
5687
|
+
throw error;
|
|
5688
|
+
}
|
|
5689
|
+
for (const child of children) {
|
|
5690
|
+
if (entries.length >= MAX_ZIP_ENTRIES) {
|
|
5691
|
+
throw new FolderDownloadError(400, "ENTRY_LIMIT", "Folder contains too many entries");
|
|
5692
|
+
}
|
|
5693
|
+
const childPath = path2.join(directoryPath, child.name);
|
|
5694
|
+
const archiveChildPath = `${archivePath}${child.name}`;
|
|
5695
|
+
let resolvedChild;
|
|
5696
|
+
try {
|
|
5697
|
+
resolvedChild = await resolvePathWithinRoot(filesystemRoot, childPath);
|
|
5698
|
+
} catch (error) {
|
|
5699
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5700
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5701
|
+
}
|
|
5702
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains a path outside the project");
|
|
5703
|
+
}
|
|
5704
|
+
let childStat;
|
|
5705
|
+
try {
|
|
5706
|
+
childStat = await fs2.lstat(resolvedChild.targetPath);
|
|
5707
|
+
} catch (error) {
|
|
5708
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5709
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5710
|
+
}
|
|
5711
|
+
throw error;
|
|
5712
|
+
}
|
|
5713
|
+
if (childStat.isSymbolicLink() || child.isSymbolicLink()) {
|
|
5714
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Symbolic links are not allowed in folder downloads");
|
|
5715
|
+
}
|
|
5716
|
+
if (child.isDirectory()) {
|
|
5717
|
+
entries.push({ name: `${archiveChildPath}/`, data: Buffer.alloc(0), directory: true });
|
|
5718
|
+
await collectFilesystem(resolvedChild.targetPath, `${archiveChildPath}/`);
|
|
5719
|
+
} else if (child.isFile()) {
|
|
5720
|
+
if (childStat.size > MAX_ZIP_FILE_SIZE || totalSize + childStat.size > MAX_ZIP_TOTAL_SIZE) {
|
|
5721
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
5722
|
+
}
|
|
5723
|
+
const flags = fsConstants.O_NOFOLLOW === void 0 ? fsConstants.O_RDONLY : fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW;
|
|
5724
|
+
let handle3;
|
|
5725
|
+
try {
|
|
5726
|
+
handle3 = await fs2.open(resolvedChild.targetPath, flags);
|
|
5727
|
+
} catch (error) {
|
|
5728
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5729
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5730
|
+
}
|
|
5731
|
+
throw error;
|
|
5732
|
+
}
|
|
5733
|
+
try {
|
|
5734
|
+
const openedStat = await handle3.stat();
|
|
5735
|
+
if (!openedStat.isFile() || openedStat.size > MAX_ZIP_FILE_SIZE || totalSize + openedStat.size > MAX_ZIP_TOTAL_SIZE) {
|
|
5736
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
5737
|
+
}
|
|
5738
|
+
const readLimit = MAX_ZIP_FILE_SIZE + 1;
|
|
5739
|
+
const dataBuffer = Buffer.alloc(Math.min(openedStat.size + 1, readLimit));
|
|
5740
|
+
let bytesRead = 0;
|
|
5741
|
+
while (bytesRead < dataBuffer.length) {
|
|
5742
|
+
const result = await handle3.read(
|
|
5743
|
+
dataBuffer,
|
|
5744
|
+
bytesRead,
|
|
5745
|
+
dataBuffer.length - bytesRead,
|
|
5746
|
+
bytesRead
|
|
5747
|
+
);
|
|
5748
|
+
if (result.bytesRead === 0) break;
|
|
5749
|
+
bytesRead += result.bytesRead;
|
|
5750
|
+
}
|
|
5751
|
+
const data = dataBuffer.subarray(0, bytesRead);
|
|
5752
|
+
const finalStat = await handle3.stat();
|
|
5753
|
+
if (finalStat.size > MAX_ZIP_FILE_SIZE || totalSize + finalStat.size > MAX_ZIP_TOTAL_SIZE || finalStat.size > data.length) {
|
|
5754
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
5755
|
+
}
|
|
5756
|
+
addFile(archiveChildPath, data);
|
|
5757
|
+
} finally {
|
|
5758
|
+
await handle3.close();
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
};
|
|
5763
|
+
const collect = async (currentPath, archivePath) => {
|
|
5764
|
+
let children;
|
|
5765
|
+
try {
|
|
5766
|
+
children = await listEntries2(currentPath);
|
|
5767
|
+
} catch (error) {
|
|
5768
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5769
|
+
if (/not found|enoent|no such file/i.test(message)) {
|
|
5770
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5771
|
+
}
|
|
5772
|
+
throw error;
|
|
5773
|
+
}
|
|
5774
|
+
for (const child of children) {
|
|
5775
|
+
if (entries.length >= MAX_ZIP_ENTRIES) {
|
|
5776
|
+
throw new FolderDownloadError(400, "ENTRY_LIMIT", "Folder contains too many entries");
|
|
5777
|
+
}
|
|
5778
|
+
const childPath = child.path;
|
|
5779
|
+
if (childPath.includes("\\") || childPath.split("/").includes("..") || !childPath.startsWith(folderPath.endsWith("/") ? folderPath : `${folderPath}/`)) {
|
|
5780
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains an unsafe child path");
|
|
5781
|
+
}
|
|
5782
|
+
if (workspace.storageType !== "sandbox") {
|
|
5783
|
+
const root = filesystemRootDirectory(tenantId, workspaceId, projectId);
|
|
5784
|
+
const virtualChildPath = childPath.startsWith("/") ? childPath : `/${childPath}`;
|
|
5785
|
+
const target = path2.resolve(root, `.${virtualChildPath}`);
|
|
5786
|
+
try {
|
|
5787
|
+
await resolvePathWithinRoot(root, target);
|
|
5788
|
+
const childStat = await fs2.lstat(target);
|
|
5789
|
+
if (childStat.isSymbolicLink()) {
|
|
5790
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Symbolic links are not allowed in folder downloads");
|
|
5791
|
+
}
|
|
5792
|
+
} catch (error) {
|
|
5793
|
+
if (error instanceof FolderDownloadError) throw error;
|
|
5794
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5795
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5796
|
+
}
|
|
5797
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains a path outside the project");
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5800
|
+
const childName = path2.basename(childPath.replace(/\/$/, ""));
|
|
5801
|
+
const childArchivePath = `${archivePath}${childName}`;
|
|
5802
|
+
if (child.is_dir) {
|
|
5803
|
+
entries.push({ name: `${childArchivePath}/`, data: Buffer.alloc(0), directory: true });
|
|
5804
|
+
await collect(childPath, `${childArchivePath}/`);
|
|
5805
|
+
} else {
|
|
5806
|
+
const size = child.size ?? 0;
|
|
5807
|
+
if (size > MAX_ZIP_FILE_SIZE || totalSize + size > MAX_ZIP_TOTAL_SIZE) {
|
|
5808
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
5809
|
+
}
|
|
5810
|
+
const data = await backend.readBinary(childPath);
|
|
5811
|
+
addFile(childArchivePath, data);
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
};
|
|
5815
|
+
if (workspace.storageType === "sandbox") {
|
|
5816
|
+
await collect(folderPath, prefix);
|
|
5817
|
+
} else {
|
|
5818
|
+
const root = filesystemRoot;
|
|
5819
|
+
const target = path2.resolve(root, `.${folderPath.startsWith("/") ? folderPath : `/${folderPath}`}`);
|
|
5820
|
+
await collectFilesystem(target, prefix);
|
|
5821
|
+
}
|
|
5822
|
+
const zip = createZip(entries);
|
|
5823
|
+
const downloadName = `${folderName}.zip`;
|
|
5824
|
+
return reply.status(200).type("application/zip").header("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(downloadName)}`).send(zip);
|
|
5825
|
+
} catch (error) {
|
|
5826
|
+
const folderError = error instanceof FolderDownloadError ? error : error instanceof PathOutsideRootError || error instanceof Error && ["ELOOP", "ENOTDIR", "EACCES"].includes(error.code || "") ? new FolderDownloadError(400, "INVALID_PATH", "Folder contains a path outside the project") : new FolderDownloadError(502, "DOWNLOAD_FAILED", error instanceof Error ? error.message : String(error));
|
|
5827
|
+
return reply.status(folderError.statusCode).send({
|
|
5828
|
+
success: false,
|
|
5829
|
+
error: folderError.message,
|
|
5830
|
+
code: folderError.code
|
|
5831
|
+
});
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5387
5834
|
/**
|
|
5388
5835
|
* View a file inline in the browser (not download).
|
|
5389
5836
|
* Returns file content with inline Content-Disposition for preview.
|
|
@@ -5676,6 +6123,11 @@ function registerWorkspaceRoutes(app2) {
|
|
|
5676
6123
|
"/api/workspaces/:workspaceId/projects/:projectId/downloadfile",
|
|
5677
6124
|
controller.downloadFile.bind(controller)
|
|
5678
6125
|
);
|
|
6126
|
+
app2.get(
|
|
6127
|
+
"/api/workspaces/:workspaceId/projects/:projectId/downloadfolder",
|
|
6128
|
+
{ schema: downloadFolderSchema },
|
|
6129
|
+
controller.downloadFolder.bind(controller)
|
|
6130
|
+
);
|
|
5679
6131
|
app2.get(
|
|
5680
6132
|
"/api/workspaces/:workspaceId/projects/:projectId/viewfile",
|
|
5681
6133
|
controller.viewFile.bind(controller)
|
|
@@ -5903,7 +6355,7 @@ async function testDatabaseConnection(request, reply) {
|
|
|
5903
6355
|
await db.connect();
|
|
5904
6356
|
await db.listTables();
|
|
5905
6357
|
const latency = Date.now() - startTime;
|
|
5906
|
-
await new Promise((
|
|
6358
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
5907
6359
|
await db.disconnect();
|
|
5908
6360
|
await sqlDatabaseManager.removeDatabase(tenantId, testKey);
|
|
5909
6361
|
return {
|
|
@@ -6556,7 +7008,7 @@ function registerMetricsServerConfigRoutes(app2) {
|
|
|
6556
7008
|
}
|
|
6557
7009
|
|
|
6558
7010
|
// src/controllers/connections.ts
|
|
6559
|
-
import { ConnectionRegistry, PluginRegistry as
|
|
7011
|
+
import { ConnectionRegistry, PluginRegistry as PluginRegistry3 } from "@axiom-lattice/core";
|
|
6560
7012
|
import { z } from "zod";
|
|
6561
7013
|
var createBody = z.object({
|
|
6562
7014
|
type: z.string().min(1),
|
|
@@ -6659,7 +7111,7 @@ function connectionRoutes(fastify2) {
|
|
|
6659
7111
|
fastify2.post("/api/connections/test", async (request, reply) => {
|
|
6660
7112
|
const { type } = request.query;
|
|
6661
7113
|
if (!type) return reply.status(400).send({ error: "query 'type' required" });
|
|
6662
|
-
const plugin =
|
|
7114
|
+
const plugin = PluginRegistry3.get(type);
|
|
6663
7115
|
if (!plugin?.connection?.test) {
|
|
6664
7116
|
return reply.status(400).send({ error: "plugin does not support connection testing" });
|
|
6665
7117
|
}
|
|
@@ -6687,7 +7139,7 @@ function connectionRoutes(fastify2) {
|
|
|
6687
7139
|
fastify2.post("/api/connections/discover", async (request, reply) => {
|
|
6688
7140
|
const { type } = request.query;
|
|
6689
7141
|
if (!type) return reply.status(400).send({ error: "query 'type' required" });
|
|
6690
|
-
const plugin =
|
|
7142
|
+
const plugin = PluginRegistry3.get(type);
|
|
6691
7143
|
if (!plugin?.connection?.discover) {
|
|
6692
7144
|
return reply.status(400).send({ error: "plugin does not support resource discovery" });
|
|
6693
7145
|
}
|
|
@@ -6725,6 +7177,7 @@ import {
|
|
|
6725
7177
|
LatticeEvalProject
|
|
6726
7178
|
} from "@axiom-lattice/core";
|
|
6727
7179
|
import { v4 as uuidv43 } from "uuid";
|
|
7180
|
+
var TERMINAL_PERSIST_ATTEMPTS = 2;
|
|
6728
7181
|
function mapLogs(logs) {
|
|
6729
7182
|
return logs.map((l) => ({
|
|
6730
7183
|
timestamp: l.ts,
|
|
@@ -6736,18 +7189,42 @@ function mapLogs(logs) {
|
|
|
6736
7189
|
var EvalRunner = class {
|
|
6737
7190
|
constructor() {
|
|
6738
7191
|
this.runs = /* @__PURE__ */ new Map();
|
|
7192
|
+
this.projectLeases = /* @__PURE__ */ new Map();
|
|
6739
7193
|
this.eventEmitter = new EventEmitter();
|
|
6740
7194
|
this.processStartedAt = /* @__PURE__ */ new Date();
|
|
6741
7195
|
}
|
|
6742
7196
|
getEventEmitter() {
|
|
6743
7197
|
return this.eventEmitter;
|
|
6744
7198
|
}
|
|
7199
|
+
reserveProjectDeletion(tenantId, projectId) {
|
|
7200
|
+
const leaseKey = `${tenantId}:${projectId}`;
|
|
7201
|
+
if (this.projectLeases.has(leaseKey) || [...this.runs.values()].some(
|
|
7202
|
+
(ctx) => ctx.tenantId === tenantId && ctx.projectId === projectId && !ctx.terminal.workerSettled
|
|
7203
|
+
)) return null;
|
|
7204
|
+
const leaseToken = Symbol(`delete:${leaseKey}`);
|
|
7205
|
+
this.projectLeases.set(leaseKey, leaseToken);
|
|
7206
|
+
return () => this.releaseProjectLease(leaseKey, leaseToken);
|
|
7207
|
+
}
|
|
6745
7208
|
async startRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId) {
|
|
7209
|
+
const leaseKey = `${tenantId}:${projectId}`;
|
|
7210
|
+
if (this.projectLeases.has(leaseKey)) {
|
|
7211
|
+
throw new Error("A run is already in progress for this project");
|
|
7212
|
+
}
|
|
7213
|
+
const leaseToken = Symbol(leaseKey);
|
|
7214
|
+
this.projectLeases.set(leaseKey, leaseToken);
|
|
7215
|
+
try {
|
|
7216
|
+
return await this.startLeasedRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId, leaseKey, leaseToken);
|
|
7217
|
+
} catch (error) {
|
|
7218
|
+
this.releaseProjectLease(leaseKey, leaseToken);
|
|
7219
|
+
throw error;
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
7222
|
+
async startLeasedRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId, leaseKey, leaseToken) {
|
|
6746
7223
|
const store = this.getEvalStore();
|
|
6747
7224
|
const project = await store.getProjectById(tenantId, projectId);
|
|
6748
7225
|
if (!project) throw new Error("Project not found");
|
|
6749
7226
|
for (const ctx of this.runs.values()) {
|
|
6750
|
-
if (ctx.projectId === projectId) {
|
|
7227
|
+
if (ctx.projectId === projectId && !ctx.terminal.persisted) {
|
|
6751
7228
|
throw new Error("A run is already in progress for this project");
|
|
6752
7229
|
}
|
|
6753
7230
|
}
|
|
@@ -6851,14 +7328,35 @@ var EvalRunner = class {
|
|
|
6851
7328
|
};
|
|
6852
7329
|
const abortController = new AbortController();
|
|
6853
7330
|
const stats = { passed: 0, failed: 0, interrupted: 0, totalScore: 0 };
|
|
7331
|
+
const terminal = { claiming: false, persisted: false, workerSettled: false };
|
|
7332
|
+
const terminalUnavailable = () => terminal.claiming || terminal.persisted;
|
|
7333
|
+
const persistTerminal = async (update, event) => {
|
|
7334
|
+
if (terminalUnavailable()) return false;
|
|
7335
|
+
terminal.claiming = true;
|
|
7336
|
+
try {
|
|
7337
|
+
let lastError;
|
|
7338
|
+
for (let attempt = 0; attempt < TERMINAL_PERSIST_ATTEMPTS; attempt++) {
|
|
7339
|
+
try {
|
|
7340
|
+
const persistedRun = await store.updateRunStatus(tenantId, runId, update);
|
|
7341
|
+
if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
|
|
7342
|
+
lastError = void 0;
|
|
7343
|
+
break;
|
|
7344
|
+
} catch (error) {
|
|
7345
|
+
lastError = error;
|
|
7346
|
+
}
|
|
7347
|
+
}
|
|
7348
|
+
if (lastError) throw lastError;
|
|
7349
|
+
terminal.persisted = true;
|
|
7350
|
+
this.eventEmitter.emit(`run:${runId}`, event);
|
|
7351
|
+
return true;
|
|
7352
|
+
} finally {
|
|
7353
|
+
terminal.claiming = false;
|
|
7354
|
+
}
|
|
7355
|
+
};
|
|
6854
7356
|
const onCaseComplete = async (result, suiteName) => {
|
|
7357
|
+
if (terminalUnavailable()) return;
|
|
6855
7358
|
const passed = result.result?.pass ?? false;
|
|
6856
7359
|
const score = result.result?.final_score ?? 0;
|
|
6857
|
-
if (passed) stats.passed++;
|
|
6858
|
-
else stats.failed++;
|
|
6859
|
-
stats.totalScore += score;
|
|
6860
|
-
if (result.interrupted) stats.interrupted++;
|
|
6861
|
-
const completedCount = stats.passed + stats.failed;
|
|
6862
7360
|
await store.createRunResult(tenantId, runId, uuidv43(), {
|
|
6863
7361
|
suiteName,
|
|
6864
7362
|
caseId: result.caseId,
|
|
@@ -6876,13 +7374,22 @@ var EvalRunner = class {
|
|
|
6876
7374
|
error: result.error,
|
|
6877
7375
|
interrupted: result.interrupted
|
|
6878
7376
|
});
|
|
7377
|
+
if (terminalUnavailable()) return;
|
|
7378
|
+
if (passed) stats.passed++;
|
|
7379
|
+
else stats.failed++;
|
|
7380
|
+
stats.totalScore += score;
|
|
7381
|
+
if (result.interrupted) stats.interrupted++;
|
|
7382
|
+
const completedCount = stats.passed + stats.failed;
|
|
7383
|
+
if (terminalUnavailable()) return;
|
|
6879
7384
|
const avgScore = completedCount > 0 ? stats.totalScore / completedCount : 0;
|
|
6880
|
-
await store.updateRunStatus(tenantId, runId, {
|
|
7385
|
+
const persistedRun = await store.updateRunStatus(tenantId, runId, {
|
|
6881
7386
|
passedCases: stats.passed,
|
|
6882
7387
|
failedCases: stats.failed,
|
|
6883
7388
|
interruptedCases: stats.interrupted,
|
|
6884
7389
|
avgScore
|
|
6885
7390
|
});
|
|
7391
|
+
if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
|
|
7392
|
+
if (terminalUnavailable()) return;
|
|
6886
7393
|
this.eventEmitter.emit(`run:${runId}`, {
|
|
6887
7394
|
type: "progress",
|
|
6888
7395
|
runId,
|
|
@@ -6894,65 +7401,86 @@ var EvalRunner = class {
|
|
|
6894
7401
|
const evalProject = new LatticeEvalProject(projectConfig, onCaseComplete);
|
|
6895
7402
|
const calibration = await evalProject.calibrateJudge();
|
|
6896
7403
|
if (!calibration.ok && !calibration.bypassed) {
|
|
6897
|
-
await store.updateRunStatus(tenantId, runId, {
|
|
6898
|
-
status: "failed",
|
|
6899
|
-
error: `Judge calibration failed: ${calibration.reason}`,
|
|
6900
|
-
completedAt: /* @__PURE__ */ new Date()
|
|
6901
|
-
});
|
|
6902
7404
|
throw new Error(`Judge calibration failed: ${calibration.reason}`);
|
|
6903
7405
|
}
|
|
6904
7406
|
if (calibration.bypassed) {
|
|
6905
7407
|
console.warn(`[eval-runner] Judge calibration bypassed \u2014 judge may be unreliable: ${calibration.reason}`);
|
|
6906
7408
|
}
|
|
6907
7409
|
const { report } = await evalProject.runAllSuitesBatch(concurrency, abortController.signal);
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
data: { passed: stats.passed, failed: stats.failed, avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0 }
|
|
6922
|
-
});
|
|
6923
|
-
}
|
|
7410
|
+
const completedCount = stats.passed + stats.failed;
|
|
7411
|
+
await persistTerminal({
|
|
7412
|
+
status: "completed",
|
|
7413
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
7414
|
+
passedCases: stats.passed,
|
|
7415
|
+
failedCases: stats.failed,
|
|
7416
|
+
interruptedCases: stats.interrupted,
|
|
7417
|
+
avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0
|
|
7418
|
+
}, {
|
|
7419
|
+
type: "completed",
|
|
7420
|
+
runId,
|
|
7421
|
+
data: { passed: stats.passed, failed: stats.failed, avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0 }
|
|
7422
|
+
});
|
|
6924
7423
|
return { report };
|
|
6925
7424
|
} catch (err) {
|
|
6926
7425
|
const errorMsg = err.message;
|
|
6927
|
-
|
|
6928
|
-
await
|
|
7426
|
+
try {
|
|
7427
|
+
await persistTerminal({
|
|
6929
7428
|
status: "failed",
|
|
6930
7429
|
error: errorMsg,
|
|
6931
7430
|
completedAt: /* @__PURE__ */ new Date()
|
|
6932
|
-
}
|
|
6933
|
-
this.eventEmitter.emit(`run:${runId}`, {
|
|
7431
|
+
}, {
|
|
6934
7432
|
type: "error",
|
|
6935
7433
|
runId,
|
|
6936
7434
|
data: { message: errorMsg }
|
|
6937
7435
|
});
|
|
7436
|
+
} catch {
|
|
6938
7437
|
}
|
|
6939
7438
|
throw err;
|
|
6940
7439
|
} finally {
|
|
6941
|
-
|
|
7440
|
+
terminal.workerSettled = true;
|
|
7441
|
+
if (terminal.persisted) {
|
|
7442
|
+
this.runs.delete(runId);
|
|
7443
|
+
this.releaseProjectLease(leaseKey, leaseToken);
|
|
7444
|
+
}
|
|
6942
7445
|
}
|
|
6943
7446
|
})();
|
|
6944
|
-
this.runs.set(runId, { runId, projectId, tenantId, abortController, promise: runPromise, taskId });
|
|
7447
|
+
this.runs.set(runId, { runId, projectId, tenantId, abortController, promise: runPromise, terminal, leaseKey, leaseToken, taskId });
|
|
6945
7448
|
runPromise.catch(() => {
|
|
6946
7449
|
});
|
|
6947
7450
|
return runId;
|
|
6948
7451
|
}
|
|
6949
|
-
async abortRun(runId) {
|
|
7452
|
+
async abortRun(runId, tenantId) {
|
|
6950
7453
|
const ctx = this.runs.get(runId);
|
|
6951
|
-
if (!ctx) return false;
|
|
7454
|
+
if (!ctx || tenantId !== void 0 && ctx.tenantId !== tenantId) return false;
|
|
7455
|
+
if (ctx.terminal.claiming || ctx.terminal.persisted) return false;
|
|
6952
7456
|
ctx.abortController.abort();
|
|
6953
7457
|
const store = this.getEvalStore();
|
|
6954
|
-
|
|
6955
|
-
|
|
7458
|
+
ctx.terminal.claiming = true;
|
|
7459
|
+
try {
|
|
7460
|
+
let lastError;
|
|
7461
|
+
for (let attempt = 0; attempt < TERMINAL_PERSIST_ATTEMPTS; attempt++) {
|
|
7462
|
+
try {
|
|
7463
|
+
const persistedRun = await store.updateRunStatus(ctx.tenantId, runId, { status: "aborted", completedAt: /* @__PURE__ */ new Date() });
|
|
7464
|
+
if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
|
|
7465
|
+
lastError = void 0;
|
|
7466
|
+
break;
|
|
7467
|
+
} catch (error) {
|
|
7468
|
+
lastError = error;
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
if (lastError) throw lastError;
|
|
7472
|
+
ctx.terminal.persisted = true;
|
|
7473
|
+
this.eventEmitter.emit(`run:${runId}`, {
|
|
7474
|
+
type: "error",
|
|
7475
|
+
runId,
|
|
7476
|
+
data: { message: "Run aborted" }
|
|
7477
|
+
});
|
|
7478
|
+
this.releaseProjectLease(ctx.leaseKey, ctx.leaseToken);
|
|
7479
|
+
if (ctx.terminal.workerSettled) this.runs.delete(runId);
|
|
7480
|
+
return true;
|
|
7481
|
+
} finally {
|
|
7482
|
+
ctx.terminal.claiming = false;
|
|
7483
|
+
}
|
|
6956
7484
|
}
|
|
6957
7485
|
async waitForRun(runId) {
|
|
6958
7486
|
const ctx = this.runs.get(runId);
|
|
@@ -6960,12 +7488,25 @@ var EvalRunner = class {
|
|
|
6960
7488
|
const { report } = await ctx.promise;
|
|
6961
7489
|
return report;
|
|
6962
7490
|
}
|
|
6963
|
-
|
|
6964
|
-
|
|
7491
|
+
/**
|
|
7492
|
+
* Whether the run can still be controlled or observed as a non-terminal run.
|
|
7493
|
+
* Acknowledged aborts return false even while their worker is unwinding.
|
|
7494
|
+
*/
|
|
7495
|
+
isRunning(runId, tenantId) {
|
|
7496
|
+
const ctx = this.runs.get(runId);
|
|
7497
|
+
return ctx !== void 0 && !ctx.terminal.persisted && (tenantId === void 0 || ctx.tenantId === tenantId);
|
|
7498
|
+
}
|
|
7499
|
+
/** Whether the run's worker promise has not yet reached its finally block. */
|
|
7500
|
+
hasActiveExecution(runId, tenantId) {
|
|
7501
|
+
const ctx = this.runs.get(runId);
|
|
7502
|
+
return ctx !== void 0 && !ctx.terminal.workerSettled && (tenantId === void 0 || ctx.tenantId === tenantId);
|
|
6965
7503
|
}
|
|
6966
7504
|
getEvalStore() {
|
|
6967
7505
|
return getStoreLattice9("default", "eval").store;
|
|
6968
7506
|
}
|
|
7507
|
+
releaseProjectLease(leaseKey, leaseToken) {
|
|
7508
|
+
if (this.projectLeases.get(leaseKey) === leaseToken) this.projectLeases.delete(leaseKey);
|
|
7509
|
+
}
|
|
6969
7510
|
};
|
|
6970
7511
|
var evalRunner = new EvalRunner();
|
|
6971
7512
|
|
|
@@ -6980,6 +7521,18 @@ function getTenantId11(request) {
|
|
|
6980
7521
|
function getEvalStore() {
|
|
6981
7522
|
return getStoreLattice10("default", "eval").store;
|
|
6982
7523
|
}
|
|
7524
|
+
async function getOwnedSuite(store, tenantId, projectId, suiteId) {
|
|
7525
|
+
const project = await store.getProjectById(tenantId, projectId);
|
|
7526
|
+
if (!project) return null;
|
|
7527
|
+
const suite = await store.getSuiteById(tenantId, suiteId);
|
|
7528
|
+
return suite?.projectId === projectId ? suite : null;
|
|
7529
|
+
}
|
|
7530
|
+
async function getOwnedCase(store, tenantId, projectId, suiteId, caseId) {
|
|
7531
|
+
const suite = await getOwnedSuite(store, tenantId, projectId, suiteId);
|
|
7532
|
+
if (!suite) return null;
|
|
7533
|
+
const evalCase = await store.getCaseById(tenantId, caseId);
|
|
7534
|
+
return evalCase?.suiteId === suiteId ? evalCase : null;
|
|
7535
|
+
}
|
|
6983
7536
|
async function createProject(request, reply) {
|
|
6984
7537
|
try {
|
|
6985
7538
|
const tenantId = getTenantId11(request);
|
|
@@ -7135,6 +7688,7 @@ async function updateProject(request, reply) {
|
|
|
7135
7688
|
}
|
|
7136
7689
|
}
|
|
7137
7690
|
async function deleteProject(request, reply) {
|
|
7691
|
+
let releaseDeletion;
|
|
7138
7692
|
try {
|
|
7139
7693
|
const tenantId = getTenantId11(request);
|
|
7140
7694
|
const store = getEvalStore();
|
|
@@ -7143,6 +7697,23 @@ async function deleteProject(request, reply) {
|
|
|
7143
7697
|
if (!existing) {
|
|
7144
7698
|
return reply.status(404).send({ success: false, message: "Project not found" });
|
|
7145
7699
|
}
|
|
7700
|
+
releaseDeletion = evalRunner.reserveProjectDeletion(tenantId, pid) ?? void 0;
|
|
7701
|
+
if (!releaseDeletion) {
|
|
7702
|
+
return reply.status(409).send({
|
|
7703
|
+
success: false,
|
|
7704
|
+
message: "Cannot delete an evaluation project with an active run"
|
|
7705
|
+
});
|
|
7706
|
+
}
|
|
7707
|
+
const runs = await store.getRunsByTenant(tenantId, { projectId: pid });
|
|
7708
|
+
const hasActiveRun = runs.some(
|
|
7709
|
+
(run) => run.status === "running" || evalRunner.hasActiveExecution(run.id, tenantId)
|
|
7710
|
+
);
|
|
7711
|
+
if (hasActiveRun) {
|
|
7712
|
+
return reply.status(409).send({
|
|
7713
|
+
success: false,
|
|
7714
|
+
message: "Cannot delete an evaluation project with an active run"
|
|
7715
|
+
});
|
|
7716
|
+
}
|
|
7146
7717
|
const deleted = await store.deleteProject(tenantId, pid);
|
|
7147
7718
|
if (!deleted) {
|
|
7148
7719
|
return reply.status(500).send({ success: false, message: "Failed to delete project" });
|
|
@@ -7151,6 +7722,8 @@ async function deleteProject(request, reply) {
|
|
|
7151
7722
|
} catch (err) {
|
|
7152
7723
|
const message = err instanceof Error ? err.message : "Failed to delete project";
|
|
7153
7724
|
return reply.status(500).send({ success: false, message });
|
|
7725
|
+
} finally {
|
|
7726
|
+
releaseDeletion?.();
|
|
7154
7727
|
}
|
|
7155
7728
|
}
|
|
7156
7729
|
async function createSuite(request, reply) {
|
|
@@ -7179,8 +7752,8 @@ async function updateSuite(request, reply) {
|
|
|
7179
7752
|
try {
|
|
7180
7753
|
const tenantId = getTenantId11(request);
|
|
7181
7754
|
const store = getEvalStore();
|
|
7182
|
-
const { sid } = request.params;
|
|
7183
|
-
const existing = await store
|
|
7755
|
+
const { pid, sid } = request.params;
|
|
7756
|
+
const existing = await getOwnedSuite(store, tenantId, pid, sid);
|
|
7184
7757
|
if (!existing) {
|
|
7185
7758
|
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
7186
7759
|
}
|
|
@@ -7199,8 +7772,8 @@ async function deleteSuite(request, reply) {
|
|
|
7199
7772
|
try {
|
|
7200
7773
|
const tenantId = getTenantId11(request);
|
|
7201
7774
|
const store = getEvalStore();
|
|
7202
|
-
const { sid } = request.params;
|
|
7203
|
-
const existing = await store
|
|
7775
|
+
const { pid, sid } = request.params;
|
|
7776
|
+
const existing = await getOwnedSuite(store, tenantId, pid, sid);
|
|
7204
7777
|
if (!existing) {
|
|
7205
7778
|
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
7206
7779
|
}
|
|
@@ -7218,8 +7791,8 @@ async function createCase(request, reply) {
|
|
|
7218
7791
|
try {
|
|
7219
7792
|
const tenantId = getTenantId11(request);
|
|
7220
7793
|
const store = getEvalStore();
|
|
7221
|
-
const { sid } = request.params;
|
|
7222
|
-
const suite = await store
|
|
7794
|
+
const { pid, sid } = request.params;
|
|
7795
|
+
const suite = await getOwnedSuite(store, tenantId, pid, sid);
|
|
7223
7796
|
if (!suite) {
|
|
7224
7797
|
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
7225
7798
|
}
|
|
@@ -7248,7 +7821,11 @@ async function listCasesForSuite(request, reply) {
|
|
|
7248
7821
|
try {
|
|
7249
7822
|
const tenantId = getTenantId11(request);
|
|
7250
7823
|
const store = getEvalStore();
|
|
7251
|
-
const { sid } = request.params;
|
|
7824
|
+
const { pid, sid } = request.params;
|
|
7825
|
+
const suite = await getOwnedSuite(store, tenantId, pid, sid);
|
|
7826
|
+
if (!suite) {
|
|
7827
|
+
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
7828
|
+
}
|
|
7252
7829
|
const cases = await store.getCasesBySuite(tenantId, sid);
|
|
7253
7830
|
return {
|
|
7254
7831
|
success: true,
|
|
@@ -7264,8 +7841,8 @@ async function updateCase(request, reply) {
|
|
|
7264
7841
|
try {
|
|
7265
7842
|
const tenantId = getTenantId11(request);
|
|
7266
7843
|
const store = getEvalStore();
|
|
7267
|
-
const { cid } = request.params;
|
|
7268
|
-
const existing = await store
|
|
7844
|
+
const { pid, sid, cid } = request.params;
|
|
7845
|
+
const existing = await getOwnedCase(store, tenantId, pid, sid, cid);
|
|
7269
7846
|
if (!existing) {
|
|
7270
7847
|
return reply.status(404).send({ success: false, message: "Case not found" });
|
|
7271
7848
|
}
|
|
@@ -7284,8 +7861,8 @@ async function deleteCase(request, reply) {
|
|
|
7284
7861
|
try {
|
|
7285
7862
|
const tenantId = getTenantId11(request);
|
|
7286
7863
|
const store = getEvalStore();
|
|
7287
|
-
const { cid } = request.params;
|
|
7288
|
-
const existing = await store
|
|
7864
|
+
const { pid, sid, cid } = request.params;
|
|
7865
|
+
const existing = await getOwnedCase(store, tenantId, pid, sid, cid);
|
|
7289
7866
|
if (!existing) {
|
|
7290
7867
|
return reply.status(404).send({ success: false, message: "Case not found" });
|
|
7291
7868
|
}
|
|
@@ -7354,6 +7931,11 @@ function registerEvalRoutes(app2) {
|
|
|
7354
7931
|
}
|
|
7355
7932
|
});
|
|
7356
7933
|
app2.get("/api/eval/runs/:rid/stream", async (request, reply) => {
|
|
7934
|
+
const tenantId = getTenantId11(request);
|
|
7935
|
+
const { rid } = request.params;
|
|
7936
|
+
const store = getEvalStore();
|
|
7937
|
+
const ownedRun = await store.getRunById(tenantId, rid);
|
|
7938
|
+
if (!ownedRun) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
7357
7939
|
reply.hijack();
|
|
7358
7940
|
reply.raw.writeHead(200, {
|
|
7359
7941
|
"Content-Type": "text/event-stream",
|
|
@@ -7361,41 +7943,75 @@ function registerEvalRoutes(app2) {
|
|
|
7361
7943
|
Connection: "keep-alive",
|
|
7362
7944
|
"Access-Control-Allow-Origin": "*"
|
|
7363
7945
|
});
|
|
7364
|
-
const { rid } = request.params;
|
|
7365
7946
|
const emitter = evalRunner.getEventEmitter();
|
|
7366
7947
|
const eventKey = `run:${rid}`;
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7948
|
+
let settled = false;
|
|
7949
|
+
const cleanup = () => {
|
|
7950
|
+
emitter.off(eventKey, handler);
|
|
7951
|
+
request.raw.off("close", disconnect);
|
|
7952
|
+
};
|
|
7953
|
+
const disconnect = () => {
|
|
7954
|
+
if (settled) return;
|
|
7955
|
+
settled = true;
|
|
7956
|
+
cleanup();
|
|
7957
|
+
};
|
|
7958
|
+
const finish = (type, data) => {
|
|
7959
|
+
if (settled) return;
|
|
7960
|
+
settled = true;
|
|
7961
|
+
cleanup();
|
|
7962
|
+
reply.raw.write(`event: ${type}
|
|
7963
|
+
data: ${JSON.stringify(data)}
|
|
7374
7964
|
|
|
7375
7965
|
`);
|
|
7376
|
-
}
|
|
7377
7966
|
reply.raw.end();
|
|
7378
|
-
|
|
7379
|
-
}
|
|
7967
|
+
};
|
|
7380
7968
|
const handler = (event) => {
|
|
7381
|
-
|
|
7969
|
+
if (event.type === "completed" || event.type === "error") {
|
|
7970
|
+
finish(event.type, event.data);
|
|
7971
|
+
return;
|
|
7972
|
+
}
|
|
7973
|
+
if (!settled) reply.raw.write(`event: ${event.type}
|
|
7382
7974
|
data: ${JSON.stringify(event.data)}
|
|
7383
7975
|
|
|
7384
7976
|
`);
|
|
7385
|
-
if (event.type === "completed" || event.type === "error") {
|
|
7386
|
-
emitter.off(eventKey, handler);
|
|
7387
|
-
reply.raw.end();
|
|
7388
|
-
}
|
|
7389
7977
|
};
|
|
7390
7978
|
emitter.on(eventKey, handler);
|
|
7391
|
-
request.raw.on("close",
|
|
7392
|
-
|
|
7393
|
-
|
|
7979
|
+
request.raw.on("close", disconnect);
|
|
7980
|
+
let reconciledRun;
|
|
7981
|
+
try {
|
|
7982
|
+
reconciledRun = await store.getRunById(tenantId, rid);
|
|
7983
|
+
} catch (error) {
|
|
7984
|
+
finish("error", { message: error.message });
|
|
7985
|
+
return;
|
|
7986
|
+
}
|
|
7987
|
+
if (settled) return;
|
|
7988
|
+
if (!reconciledRun) {
|
|
7989
|
+
cleanup();
|
|
7990
|
+
reply.raw.end();
|
|
7991
|
+
return;
|
|
7992
|
+
}
|
|
7993
|
+
if (reconciledRun.status === "completed") {
|
|
7994
|
+
finish("completed", {
|
|
7995
|
+
passed: reconciledRun.passedCases,
|
|
7996
|
+
failed: reconciledRun.failedCases,
|
|
7997
|
+
avgScore: reconciledRun.avgScore
|
|
7998
|
+
});
|
|
7999
|
+
} else if (reconciledRun.status === "failed") {
|
|
8000
|
+
finish("error", { message: reconciledRun.error ?? "Evaluation failed" });
|
|
8001
|
+
} else if (reconciledRun.status === "aborted") {
|
|
8002
|
+
finish("error", { message: "Run aborted" });
|
|
8003
|
+
} else if (!evalRunner.isRunning(rid, tenantId)) {
|
|
8004
|
+
cleanup();
|
|
8005
|
+
reply.raw.end();
|
|
8006
|
+
}
|
|
7394
8007
|
});
|
|
7395
8008
|
app2.post("/api/eval/runs/:rid/abort", async (request, reply) => {
|
|
7396
8009
|
try {
|
|
8010
|
+
const tenantId = getTenantId11(request);
|
|
7397
8011
|
const { rid } = request.params;
|
|
7398
|
-
const
|
|
8012
|
+
const run = await getEvalStore().getRunById(tenantId, rid);
|
|
8013
|
+
if (!run) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
8014
|
+
const aborted = await evalRunner.abortRun(rid, tenantId);
|
|
7399
8015
|
if (!aborted) return reply.status(404).send({ success: false, message: "Run not found or not running" });
|
|
7400
8016
|
reply.send({ success: true, message: "Run aborted" });
|
|
7401
8017
|
} catch (err) {
|
|
@@ -7406,7 +8022,13 @@ data: ${JSON.stringify(event.data)}
|
|
|
7406
8022
|
try {
|
|
7407
8023
|
const tenantId = getTenantId11(request);
|
|
7408
8024
|
const { rid } = request.params;
|
|
7409
|
-
const
|
|
8025
|
+
const store = getEvalStore();
|
|
8026
|
+
const run = await store.getRunById(tenantId, rid);
|
|
8027
|
+
if (!run) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
8028
|
+
if (run.status === "running" || evalRunner.hasActiveExecution(rid, tenantId)) {
|
|
8029
|
+
return reply.status(409).send({ success: false, message: "Cannot delete an active evaluation run" });
|
|
8030
|
+
}
|
|
8031
|
+
const deleted = await store.deleteRun(tenantId, rid);
|
|
7410
8032
|
if (!deleted) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
7411
8033
|
reply.send({ success: true, message: "Run deleted" });
|
|
7412
8034
|
} catch (err) {
|
|
@@ -7523,31 +8145,31 @@ var TenantsController = class {
|
|
|
7523
8145
|
async createTenant(request, reply) {
|
|
7524
8146
|
const data = request.body;
|
|
7525
8147
|
const id = uuidv46();
|
|
7526
|
-
const
|
|
7527
|
-
return reply.status(201).send({ success: true, data:
|
|
8148
|
+
const tenant2 = await this.tenantStore.createTenant(id, data);
|
|
8149
|
+
return reply.status(201).send({ success: true, data: tenant2 });
|
|
7528
8150
|
}
|
|
7529
8151
|
async getTenant(request, reply) {
|
|
7530
8152
|
const { tenantId } = request.params;
|
|
7531
|
-
const
|
|
7532
|
-
if (!
|
|
8153
|
+
const tenant2 = await this.tenantStore.getTenantById(tenantId);
|
|
8154
|
+
if (!tenant2) {
|
|
7533
8155
|
return reply.status(404).send({
|
|
7534
8156
|
success: false,
|
|
7535
8157
|
error: "Tenant not found"
|
|
7536
8158
|
});
|
|
7537
8159
|
}
|
|
7538
|
-
return { success: true, data:
|
|
8160
|
+
return { success: true, data: tenant2 };
|
|
7539
8161
|
}
|
|
7540
8162
|
async updateTenant(request, reply) {
|
|
7541
8163
|
const { tenantId } = request.params;
|
|
7542
8164
|
const updates = request.body;
|
|
7543
|
-
const
|
|
7544
|
-
if (!
|
|
8165
|
+
const tenant2 = await this.tenantStore.updateTenant(tenantId, updates);
|
|
8166
|
+
if (!tenant2) {
|
|
7545
8167
|
return reply.status(404).send({
|
|
7546
8168
|
success: false,
|
|
7547
8169
|
error: "Tenant not found"
|
|
7548
8170
|
});
|
|
7549
8171
|
}
|
|
7550
|
-
return { success: true, data:
|
|
8172
|
+
return { success: true, data: tenant2 };
|
|
7551
8173
|
}
|
|
7552
8174
|
async deleteTenant(request, reply) {
|
|
7553
8175
|
const { tenantId } = request.params;
|
|
@@ -7700,10 +8322,10 @@ var AuthController = class {
|
|
|
7700
8322
|
const links = await this.userTenantLinkStore.getTenantsByUser(userId);
|
|
7701
8323
|
const tenantsWithDetails = await Promise.all(
|
|
7702
8324
|
links.map(async (link) => {
|
|
7703
|
-
const
|
|
8325
|
+
const tenant2 = await this.tenantStore.getTenantById(link.tenantId);
|
|
7704
8326
|
return {
|
|
7705
8327
|
...link,
|
|
7706
|
-
tenant:
|
|
8328
|
+
tenant: tenant2 || null
|
|
7707
8329
|
};
|
|
7708
8330
|
})
|
|
7709
8331
|
);
|
|
@@ -7736,8 +8358,8 @@ var AuthController = class {
|
|
|
7736
8358
|
error: "You do not have access to this tenant"
|
|
7737
8359
|
});
|
|
7738
8360
|
}
|
|
7739
|
-
const
|
|
7740
|
-
if (!
|
|
8361
|
+
const tenant2 = await this.tenantStore.getTenantById(tenantId);
|
|
8362
|
+
if (!tenant2) {
|
|
7741
8363
|
return reply.status(404).send({
|
|
7742
8364
|
success: false,
|
|
7743
8365
|
error: "Tenant not found"
|
|
@@ -7761,7 +8383,7 @@ var AuthController = class {
|
|
|
7761
8383
|
return {
|
|
7762
8384
|
success: true,
|
|
7763
8385
|
data: {
|
|
7764
|
-
tenant,
|
|
8386
|
+
tenant: tenant2,
|
|
7765
8387
|
token,
|
|
7766
8388
|
personalAssistant
|
|
7767
8389
|
}
|
|
@@ -7831,8 +8453,8 @@ var AuthController = class {
|
|
|
7831
8453
|
error: "User not found"
|
|
7832
8454
|
});
|
|
7833
8455
|
}
|
|
7834
|
-
const
|
|
7835
|
-
if (!
|
|
8456
|
+
const tenant2 = await this.tenantStore.getTenantById(tenantId);
|
|
8457
|
+
if (!tenant2) {
|
|
7836
8458
|
return reply.status(404).send({
|
|
7837
8459
|
success: false,
|
|
7838
8460
|
error: "Tenant not found"
|
|
@@ -8002,7 +8624,7 @@ function registerConsoleAuthHook(app2, options) {
|
|
|
8002
8624
|
if (PUBLIC_ROUTES.some((route) => request.url === route)) return;
|
|
8003
8625
|
if (request.url.startsWith("/s/")) return;
|
|
8004
8626
|
const path4 = request.url.split("?")[0];
|
|
8005
|
-
if (
|
|
8627
|
+
if (request.method === "GET" && /^\/api\/workspaces\/[^/]+\/projects\/[^/]+\/viewfile$/.test(path4) || request.method === "GET" && /^\/api\/workspaces\/[^/]+\/projects\/[^/]+\/downloadfile$/.test(path4) || request.method === "POST" && /^\/api\/workspaces\/[^/]+\/projects\/[^/]+\/uploadfile$/.test(path4) || path4.startsWith("/f/") || path4 === "/f") {
|
|
8006
8628
|
return;
|
|
8007
8629
|
}
|
|
8008
8630
|
await reply.status(401).send({
|
|
@@ -9002,12 +9624,12 @@ async function handleBridgeProxy(request, reply) {
|
|
|
9002
9624
|
};
|
|
9003
9625
|
const timeout = 3e5;
|
|
9004
9626
|
try {
|
|
9005
|
-
const response = await new Promise((
|
|
9627
|
+
const response = await new Promise((resolve3, reject) => {
|
|
9006
9628
|
const timer = setTimeout(() => {
|
|
9007
9629
|
pending.delete(requestId);
|
|
9008
9630
|
reject(new Error(`Bridge request timeout after ${timeout}ms`));
|
|
9009
9631
|
}, timeout);
|
|
9010
|
-
pending.set(requestId, { resolve:
|
|
9632
|
+
pending.set(requestId, { resolve: resolve3, reject, timer, agentId });
|
|
9011
9633
|
try {
|
|
9012
9634
|
if (conn.ws.readyState !== conn.ws.OPEN) {
|
|
9013
9635
|
pending.delete(requestId);
|
|
@@ -10639,11 +11261,365 @@ function isAttachmentPayload(value) {
|
|
|
10639
11261
|
return typeof record.webAppId === "string" && typeof record.userDigest === "string" && /^[a-f0-9]{64}$/.test(record.userDigest) && typeof record.threadId === "string" && typeof record.projectId === "string" && typeof record.uri === "string" && record.uri.startsWith("/") && typeof record.name === "string" && record.name.length > 0 && typeof record.mimeType === "string" && record.mimeType.length > 0 && typeof record.size === "number" && Number.isSafeInteger(record.size) && record.size >= 0 && typeof record.contentDigest === "string" && /^[a-f0-9]{64}$/.test(record.contentDigest) && typeof record.exp === "number" && Number.isSafeInteger(record.exp);
|
|
10640
11262
|
}
|
|
10641
11263
|
|
|
11264
|
+
// src/controllers/capability-bundles.ts
|
|
11265
|
+
import { z as z6 } from "zod";
|
|
11266
|
+
|
|
11267
|
+
// src/services/capability/CapabilityBundleService.ts
|
|
11268
|
+
import { z as z5 } from "zod";
|
|
11269
|
+
import { CapabilityBundleMergeError, CapabilityBundleValidationError, mergeCapabilityBundles, validateCapabilityBundleInput } from "@axiom-lattice/core";
|
|
11270
|
+
var CapabilityBundleServiceError = class extends Error {
|
|
11271
|
+
constructor(code, message, statusCode) {
|
|
11272
|
+
super(message);
|
|
11273
|
+
this.code = code;
|
|
11274
|
+
this.statusCode = statusCode;
|
|
11275
|
+
this.name = "CapabilityBundleServiceError";
|
|
11276
|
+
}
|
|
11277
|
+
};
|
|
11278
|
+
var uuid = z5.string().uuid();
|
|
11279
|
+
function ownRecord(value) {
|
|
11280
|
+
if (value === void 0 || value === null) return {};
|
|
11281
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new CapabilityBundleServiceError("INVALID_BUNDLE_CONFIG", "Project config must be an object", 409);
|
|
11282
|
+
const result = {};
|
|
11283
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
11284
|
+
if (key !== "__proto__" && key !== "constructor" && key !== "prototype") result[key] = entry;
|
|
11285
|
+
}
|
|
11286
|
+
return result;
|
|
11287
|
+
}
|
|
11288
|
+
function isDuplicate(error) {
|
|
11289
|
+
return error instanceof Error && /already exists|unique|duplicate/i.test(error.message);
|
|
11290
|
+
}
|
|
11291
|
+
function rejectStableKey(input) {
|
|
11292
|
+
if (input !== null && typeof input === "object" && !Array.isArray(input) && Object.prototype.hasOwnProperty.call(input, "key")) {
|
|
11293
|
+
throw new CapabilityBundleServiceError("INVALID_INPUT", "Capability bundle key is generated and cannot be supplied or changed", 400);
|
|
11294
|
+
}
|
|
11295
|
+
}
|
|
11296
|
+
function slugify(name) {
|
|
11297
|
+
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
11298
|
+
return slug || "runtime-profile";
|
|
11299
|
+
}
|
|
11300
|
+
function projectBundleIds(config) {
|
|
11301
|
+
const record = ownRecord(config);
|
|
11302
|
+
if (!Object.prototype.hasOwnProperty.call(record, "capabilityBundleIds")) return [];
|
|
11303
|
+
const value = record.capabilityBundleIds;
|
|
11304
|
+
if (!Array.isArray(value) || value.some((id) => typeof id !== "string" || !uuid.safeParse(id).success) || new Set(value).size !== value.length) {
|
|
11305
|
+
throw new CapabilityBundleServiceError("INVALID_BUNDLE_CONFIG", "Project capabilityBundleIds must be a unique array of UUIDs", 409);
|
|
11306
|
+
}
|
|
11307
|
+
return [...value];
|
|
11308
|
+
}
|
|
11309
|
+
function issue(error) {
|
|
11310
|
+
if (error instanceof CapabilityBundleMergeError || error instanceof CapabilityBundleValidationError) return { code: error instanceof CapabilityBundleValidationError && error.code === "CAPABILITY_BUNDLE_INVALID_CONFIG" ? "INVALID_BUNDLE_CONFIG" : error.code, message: error.message, bundleId: error instanceof CapabilityBundleMergeError ? error.bundleId : void 0, capabilityType: error.capabilityType, fieldPath: error.fieldPath };
|
|
11311
|
+
return { code: "CAPABILITY_BUNDLE_CONFIGURATION_ERROR", message: error instanceof Error ? error.message : "Invalid capability bundle configuration" };
|
|
11312
|
+
}
|
|
11313
|
+
var CapabilityBundleService = class {
|
|
11314
|
+
constructor(deps) {
|
|
11315
|
+
this.deps = deps;
|
|
11316
|
+
}
|
|
11317
|
+
async list(tenantId) {
|
|
11318
|
+
const values = await this.deps.bundleStore.listByTenant(tenantId);
|
|
11319
|
+
const bundles = [];
|
|
11320
|
+
const errors = [];
|
|
11321
|
+
for (const value of values) {
|
|
11322
|
+
try {
|
|
11323
|
+
validateCapabilityBundleInput(value);
|
|
11324
|
+
bundles.push(value);
|
|
11325
|
+
} catch {
|
|
11326
|
+
errors.push({ bundleId: value.id, code: "INVALID_BUNDLE_CONFIG", message: "Capability bundle record is invalid" });
|
|
11327
|
+
}
|
|
11328
|
+
}
|
|
11329
|
+
return { bundles, errors };
|
|
11330
|
+
}
|
|
11331
|
+
async get(tenantId, id) {
|
|
11332
|
+
const value = await this.deps.bundleStore.getById(tenantId, id);
|
|
11333
|
+
if (!value) throw new CapabilityBundleServiceError("BUNDLE_NOT_FOUND", "Capability bundle not found", 404);
|
|
11334
|
+
try {
|
|
11335
|
+
validateCapabilityBundleInput(value);
|
|
11336
|
+
} catch {
|
|
11337
|
+
throw new CapabilityBundleServiceError("INVALID_BUNDLE_CONFIG", "Capability bundle record is invalid", 422);
|
|
11338
|
+
}
|
|
11339
|
+
return value;
|
|
11340
|
+
}
|
|
11341
|
+
async create(tenantId, input) {
|
|
11342
|
+
rejectStableKey(input);
|
|
11343
|
+
validateCapabilityBundleInput(input);
|
|
11344
|
+
const value = input;
|
|
11345
|
+
const name = value.name.trim();
|
|
11346
|
+
const base = slugify(name);
|
|
11347
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
11348
|
+
const existing = await this.deps.bundleStore.listByTenant(tenantId) ?? [];
|
|
11349
|
+
const keys = new Set(existing.map((bundle) => bundle.key));
|
|
11350
|
+
let key = base;
|
|
11351
|
+
let suffix = 2;
|
|
11352
|
+
while (keys.has(key)) key = `${base}-${suffix++}`;
|
|
11353
|
+
const normalized = { ...value, key, name };
|
|
11354
|
+
validateCapabilityBundleInput(normalized);
|
|
11355
|
+
try {
|
|
11356
|
+
const result = await this.deps.bundleStore.create(tenantId, normalized);
|
|
11357
|
+
validateCapabilityBundleInput(result);
|
|
11358
|
+
return result;
|
|
11359
|
+
} catch (error) {
|
|
11360
|
+
if (!isDuplicate(error)) throw error;
|
|
11361
|
+
if (attempt === 4) throw new CapabilityBundleServiceError("DUPLICATE_BUNDLE_KEY", "Capability bundle key already exists", 409);
|
|
11362
|
+
}
|
|
11363
|
+
}
|
|
11364
|
+
throw new CapabilityBundleServiceError("DUPLICATE_BUNDLE_KEY", "Capability bundle key already exists", 409);
|
|
11365
|
+
}
|
|
11366
|
+
async update(tenantId, id, input) {
|
|
11367
|
+
rejectStableKey(input);
|
|
11368
|
+
validateCapabilityBundleInput(input, { partial: true });
|
|
11369
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || typeof input.expectedUpdatedAt !== "string" || !input.expectedUpdatedAt.trim()) throw new CapabilityBundleServiceError("INVALID_BUNDLE_CONFIG", "expectedUpdatedAt is required for capability bundle updates", 400);
|
|
11370
|
+
const value = input;
|
|
11371
|
+
const normalized = { ...value, ...value.name === void 0 ? {} : { name: value.name.trim() }, ...Object.prototype.hasOwnProperty.call(value, "description") ? { description: value.description?.trim() || void 0 } : {} };
|
|
11372
|
+
validateCapabilityBundleInput(normalized, { partial: true });
|
|
11373
|
+
try {
|
|
11374
|
+
const result = await this.deps.bundleStore.update(tenantId, id, normalized);
|
|
11375
|
+
if (!result) throw new CapabilityBundleServiceError("BUNDLE_NOT_FOUND", "Capability bundle not found", 404);
|
|
11376
|
+
if ("status" in result && result.status === "conflict") throw new CapabilityBundleServiceError("BUNDLE_CONFLICT", "Capability bundle changed after it was loaded; reload before saving", 409);
|
|
11377
|
+
validateCapabilityBundleInput(result);
|
|
11378
|
+
return result;
|
|
11379
|
+
} catch (error) {
|
|
11380
|
+
if (error instanceof CapabilityBundleServiceError) throw error;
|
|
11381
|
+
if (isDuplicate(error)) throw new CapabilityBundleServiceError("DUPLICATE_BUNDLE_KEY", "Capability bundle key already exists", 409);
|
|
11382
|
+
throw error;
|
|
11383
|
+
}
|
|
11384
|
+
}
|
|
11385
|
+
async remove(tenantId, id) {
|
|
11386
|
+
const result = await this.deps.bundleStore.deleteIfUnreferenced(tenantId, id);
|
|
11387
|
+
if (result === "not_found") throw new CapabilityBundleServiceError("BUNDLE_NOT_FOUND", "Capability bundle not found", 404);
|
|
11388
|
+
if (result === "in_use") throw new CapabilityBundleServiceError("BUNDLE_IN_USE", "Capability bundle is in use", 409);
|
|
11389
|
+
}
|
|
11390
|
+
async getProjectBundles(tenantId, projectId) {
|
|
11391
|
+
const project = await this.deps.projectStore.getProjectById(tenantId, projectId);
|
|
11392
|
+
if (!project) throw new CapabilityBundleServiceError("PROJECT_NOT_FOUND", "Project not found", 404);
|
|
11393
|
+
const ids = projectBundleIds(project.config);
|
|
11394
|
+
const stored = await this.deps.bundleStore.getManyByIds(tenantId, ids) ?? [];
|
|
11395
|
+
return this.compose(project, ids, stored);
|
|
11396
|
+
}
|
|
11397
|
+
async previewProject(tenantId, projectId) {
|
|
11398
|
+
const project = await this.deps.projectStore.getProjectById(tenantId, projectId);
|
|
11399
|
+
if (!project) throw new CapabilityBundleServiceError("PROJECT_NOT_FOUND", "Project not found", 404);
|
|
11400
|
+
let ids;
|
|
11401
|
+
try {
|
|
11402
|
+
ids = projectBundleIds(project.config);
|
|
11403
|
+
} catch (error) {
|
|
11404
|
+
return { bundleIds: [], capabilities: [], overrides: [], sources: [], warnings: [], errors: [{ code: "INVALID_BUNDLE_CONFIG", message: error instanceof Error ? error.message : "Invalid project capability configuration" }], revision: "" };
|
|
11405
|
+
}
|
|
11406
|
+
const stored = await this.deps.bundleStore.getManyByIds(tenantId, ids) ?? [];
|
|
11407
|
+
const byId = new Map(stored.map((value) => [value.id, value]));
|
|
11408
|
+
const missing = ids.filter((id) => !byId.has(id));
|
|
11409
|
+
if (missing.length > 0) return { bundleIds: ids, capabilities: [], overrides: [], sources: [], warnings: [], errors: missing.map((id) => ({ code: "BUNDLE_NOT_FOUND", message: `Persisted capability bundle '${id}' is missing`, bundleId: id })), revision: "" };
|
|
11410
|
+
try {
|
|
11411
|
+
const ordered = ids.map((id) => byId.get(id));
|
|
11412
|
+
ordered.forEach((bundle) => validateCapabilityBundleInput(bundle));
|
|
11413
|
+
return mergeCapabilityBundles(ordered);
|
|
11414
|
+
} catch (error) {
|
|
11415
|
+
return { bundleIds: ids, capabilities: [], overrides: [], sources: [], warnings: [], errors: [issue(error)], revision: "" };
|
|
11416
|
+
}
|
|
11417
|
+
}
|
|
11418
|
+
async project(tenantId, projectId, ids, persist, expectedRevisions) {
|
|
11419
|
+
const current = await this.deps.projectStore.getProjectById(tenantId, projectId);
|
|
11420
|
+
if (!current) throw new CapabilityBundleServiceError("PROJECT_NOT_FOUND", "Project not found", 404);
|
|
11421
|
+
if (new Set(ids).size !== ids.length) throw new CapabilityBundleServiceError("DUPLICATE_PROJECT_BUNDLE", "Duplicate capability bundle ID", 400);
|
|
11422
|
+
if (persist) this.assertExpectedRevisions(ids, expectedRevisions);
|
|
11423
|
+
const ordered = await this.loadOrderedBundles(tenantId, ids);
|
|
11424
|
+
let resultProject = current;
|
|
11425
|
+
if (persist) {
|
|
11426
|
+
projectBundleIds(current.config);
|
|
11427
|
+
const update = await this.deps.projectStore.updateCapabilityBundleIds(tenantId, projectId, ids, expectedRevisions);
|
|
11428
|
+
if (update.status === "project_not_found") throw new CapabilityBundleServiceError("PROJECT_NOT_FOUND", "Project not found", 404);
|
|
11429
|
+
if (update.status === "bundle_not_found") throw new CapabilityBundleServiceError("BUNDLE_NOT_FOUND", "Capability bundle not found", 404);
|
|
11430
|
+
if (update.status === "bundle_conflict") throw new CapabilityBundleServiceError("BUNDLE_CONFLICT", "Capability bundle changed after preview; preview the current bundle contents before saving", 409);
|
|
11431
|
+
resultProject = update.project;
|
|
11432
|
+
}
|
|
11433
|
+
return this.compose(resultProject, ids, ordered);
|
|
11434
|
+
}
|
|
11435
|
+
assertExpectedRevisions(ids, revisions) {
|
|
11436
|
+
if (!revisions || Object.keys(revisions).length !== ids.length || ids.some((id) => typeof revisions[id] !== "string" || !revisions[id].trim()) || Object.keys(revisions).some((id) => !ids.includes(id))) {
|
|
11437
|
+
throw new CapabilityBundleServiceError("BUNDLE_CONFLICT", "Expected revisions must exactly match the selected capability bundles", 409);
|
|
11438
|
+
}
|
|
11439
|
+
}
|
|
11440
|
+
async loadOrderedBundles(tenantId, ids) {
|
|
11441
|
+
const stored = await this.deps.bundleStore.getManyByIds(tenantId, ids) ?? [];
|
|
11442
|
+
const byId = new Map(stored.map((value) => [value.id, value]));
|
|
11443
|
+
return ids.map((id) => {
|
|
11444
|
+
const value = byId.get(id);
|
|
11445
|
+
if (!value) throw new CapabilityBundleServiceError("BUNDLE_NOT_FOUND", "Capability bundle not found", 404);
|
|
11446
|
+
validateCapabilityBundleInput(value);
|
|
11447
|
+
return value;
|
|
11448
|
+
});
|
|
11449
|
+
}
|
|
11450
|
+
compose(project, ids, bundles) {
|
|
11451
|
+
const valid = [];
|
|
11452
|
+
const errors = [];
|
|
11453
|
+
const byId = new Map(bundles.map((bundle) => [bundle.id, bundle]));
|
|
11454
|
+
for (const id of ids) {
|
|
11455
|
+
const bundle = byId.get(id);
|
|
11456
|
+
if (!bundle) {
|
|
11457
|
+
errors.push({ code: "BUNDLE_NOT_FOUND", message: `Persisted capability bundle '${id}' is missing`, bundleId: id });
|
|
11458
|
+
continue;
|
|
11459
|
+
}
|
|
11460
|
+
try {
|
|
11461
|
+
validateCapabilityBundleInput(bundle);
|
|
11462
|
+
valid.push(bundle);
|
|
11463
|
+
} catch (error) {
|
|
11464
|
+
errors.push({ ...issue(error), bundleId: id });
|
|
11465
|
+
}
|
|
11466
|
+
}
|
|
11467
|
+
let preview;
|
|
11468
|
+
try {
|
|
11469
|
+
preview = mergeCapabilityBundles(valid);
|
|
11470
|
+
} catch (error) {
|
|
11471
|
+
preview = { bundleIds: ids, capabilities: [], overrides: [], sources: [], warnings: [], errors: [issue(error)], revision: "" };
|
|
11472
|
+
}
|
|
11473
|
+
preview = { ...preview, bundleIds: ids, errors: [...errors, ...preview.errors] };
|
|
11474
|
+
return { project, persistedBundleIds: ids, bundles: valid, preview };
|
|
11475
|
+
}
|
|
11476
|
+
};
|
|
11477
|
+
|
|
11478
|
+
// src/controllers/capability-bundles.ts
|
|
11479
|
+
var logger3 = new Logger({ serviceName: "lattice/gateway/capability-bundles" });
|
|
11480
|
+
var capability = z6.unknown();
|
|
11481
|
+
var createSchema = z6.object({ name: z6.string(), description: z6.string().optional(), capabilities: z6.array(capability) }).strict();
|
|
11482
|
+
var updateSchema = createSchema.partial().extend({ expectedUpdatedAt: z6.string().optional() });
|
|
11483
|
+
var bundleIdSchema = z6.string().uuid();
|
|
11484
|
+
var bundleParamsSchema = z6.object({ bundleId: bundleIdSchema });
|
|
11485
|
+
var projectSchema = z6.object({ bundleIds: z6.array(bundleIdSchema), expectedRevisions: z6.record(z6.string()).optional() }).strict();
|
|
11486
|
+
function tenant(request, authRequired2) {
|
|
11487
|
+
const authenticated = request.user?.tenantId;
|
|
11488
|
+
const header = request.headers["x-tenant-id"];
|
|
11489
|
+
const requested = Array.isArray(header) ? header[0] : header;
|
|
11490
|
+
if (authenticated) {
|
|
11491
|
+
if (requested && requested !== authenticated) throw new CapabilityBundleServiceError("INVALID_INPUT", "Forbidden", 403);
|
|
11492
|
+
return authenticated;
|
|
11493
|
+
}
|
|
11494
|
+
if (authRequired2) throw new CapabilityBundleServiceError("INVALID_INPUT", "Unauthorized", 401);
|
|
11495
|
+
return requested || "default";
|
|
11496
|
+
}
|
|
11497
|
+
function sendError(reply, error) {
|
|
11498
|
+
if (error instanceof CapabilityBundleServiceError) return reply.status(error.statusCode).send({ success: false, code: error.code, message: error.message });
|
|
11499
|
+
if (error instanceof Error && error.name === "CapabilityBundleValidationError") {
|
|
11500
|
+
const validationError = error;
|
|
11501
|
+
const code = validationError.code === "CAPABILITY_BUNDLE_INELIGIBLE_CAPABILITY" ? "BUNDLE_PLUGIN_NOT_ELIGIBLE" : validationError.code === "CAPABILITY_BUNDLE_CREDENTIALS_FORBIDDEN" || validationError.code === "CAPABILITY_BUNDLE_INVALID_CONFIG" ? "INVALID_BUNDLE_CONFIG" : validationError.code;
|
|
11502
|
+
return reply.status(400).send({ success: false, code, message: error.message, capabilityType: validationError.capabilityType, fieldPath: validationError.fieldPath });
|
|
11503
|
+
}
|
|
11504
|
+
logger3.error("Capability bundle controller error", { error: error instanceof Error ? error : new Error(String(error)) });
|
|
11505
|
+
return reply.status(500).send({ success: false, code: "INTERNAL_ERROR", message: "Internal server error" });
|
|
11506
|
+
}
|
|
11507
|
+
var CapabilityBundleController = class {
|
|
11508
|
+
constructor(deps, options = {}) {
|
|
11509
|
+
this.service = new CapabilityBundleService(deps);
|
|
11510
|
+
this.authRequired = options.authRequired ?? true;
|
|
11511
|
+
}
|
|
11512
|
+
async listBundles(request, reply) {
|
|
11513
|
+
try {
|
|
11514
|
+
const result = await this.service.list(tenant(request, this.authRequired));
|
|
11515
|
+
return reply.send({ success: true, data: result.bundles, errors: result.errors });
|
|
11516
|
+
} catch (error) {
|
|
11517
|
+
return sendError(reply, error);
|
|
11518
|
+
}
|
|
11519
|
+
}
|
|
11520
|
+
async getBundle(request, reply) {
|
|
11521
|
+
try {
|
|
11522
|
+
const tenantId = tenant(request, this.authRequired);
|
|
11523
|
+
const parsed = bundleParamsSchema.safeParse(request.params);
|
|
11524
|
+
if (!parsed.success) return reply.status(400).send({ success: false, code: "INVALID_INPUT", message: "Invalid capability bundle ID" });
|
|
11525
|
+
return reply.send({ success: true, data: await this.service.get(tenantId, parsed.data.bundleId) });
|
|
11526
|
+
} catch (error) {
|
|
11527
|
+
return sendError(reply, error);
|
|
11528
|
+
}
|
|
11529
|
+
}
|
|
11530
|
+
async createBundle(request, reply) {
|
|
11531
|
+
try {
|
|
11532
|
+
const tenantId = tenant(request, this.authRequired);
|
|
11533
|
+
const parsed = createSchema.safeParse(request.body);
|
|
11534
|
+
if (!parsed.success) return reply.status(400).send({ success: false, code: "INVALID_INPUT", message: "Invalid capability bundle body" });
|
|
11535
|
+
return reply.status(201).send({ success: true, data: await this.service.create(tenantId, parsed.data) });
|
|
11536
|
+
} catch (error) {
|
|
11537
|
+
return sendError(reply, error);
|
|
11538
|
+
}
|
|
11539
|
+
}
|
|
11540
|
+
async updateBundle(request, reply) {
|
|
11541
|
+
try {
|
|
11542
|
+
const tenantId = tenant(request, this.authRequired);
|
|
11543
|
+
const params = bundleParamsSchema.safeParse(request.params);
|
|
11544
|
+
const parsed = updateSchema.safeParse(request.body);
|
|
11545
|
+
if (!params.success || !parsed.success) return reply.status(400).send({ success: false, code: "INVALID_INPUT", message: "Invalid capability bundle request" });
|
|
11546
|
+
return reply.send({ success: true, data: await this.service.update(tenantId, params.data.bundleId, parsed.data) });
|
|
11547
|
+
} catch (error) {
|
|
11548
|
+
return sendError(reply, error);
|
|
11549
|
+
}
|
|
11550
|
+
}
|
|
11551
|
+
async deleteBundle(request, reply) {
|
|
11552
|
+
try {
|
|
11553
|
+
const tenantId = tenant(request, this.authRequired);
|
|
11554
|
+
const parsed = bundleParamsSchema.safeParse(request.params);
|
|
11555
|
+
if (!parsed.success) return reply.status(400).send({ success: false, code: "INVALID_INPUT", message: "Invalid capability bundle ID" });
|
|
11556
|
+
await this.service.remove(tenantId, parsed.data.bundleId);
|
|
11557
|
+
return reply.send({ success: true, data: null });
|
|
11558
|
+
} catch (error) {
|
|
11559
|
+
return sendError(reply, error);
|
|
11560
|
+
}
|
|
11561
|
+
}
|
|
11562
|
+
async updateProjectBundles(request, reply) {
|
|
11563
|
+
try {
|
|
11564
|
+
const tenantId = tenant(request, this.authRequired);
|
|
11565
|
+
const parsed = projectSchema.safeParse(request.body);
|
|
11566
|
+
if (!parsed.success) return reply.status(400).send({ success: false, code: "INVALID_INPUT", message: "Invalid project capability bundle body" });
|
|
11567
|
+
const result = await this.service.project(tenantId, request.params.projectId, parsed.data.bundleIds, true, parsed.data.expectedRevisions);
|
|
11568
|
+
return reply.send({ success: true, data: { project: result.project, persistedBundleIds: result.persistedBundleIds, bundles: result.bundles, preview: result.preview } });
|
|
11569
|
+
} catch (error) {
|
|
11570
|
+
return sendError(reply, error);
|
|
11571
|
+
}
|
|
11572
|
+
}
|
|
11573
|
+
async getProjectBundles(request, reply) {
|
|
11574
|
+
try {
|
|
11575
|
+
const result = await this.service.getProjectBundles(tenant(request, this.authRequired), request.params.projectId);
|
|
11576
|
+
if (result.preview.errors.some((issue2) => issue2.code === "CAPABILITY_BUNDLE_CREDENTIALS_FORBIDDEN")) return reply.status(400).send({ success: false, code: "INVALID_BUNDLE_CONFIG", message: "Capability bundle record is invalid" });
|
|
11577
|
+
return reply.send({ success: true, data: { project: result.project, persistedBundleIds: result.persistedBundleIds, bundles: result.bundles, preview: result.preview } });
|
|
11578
|
+
} catch (error) {
|
|
11579
|
+
return sendError(reply, error);
|
|
11580
|
+
}
|
|
11581
|
+
}
|
|
11582
|
+
async getProjectPreview(request, reply) {
|
|
11583
|
+
try {
|
|
11584
|
+
const preview = await this.service.previewProject(tenant(request, this.authRequired), request.params.projectId);
|
|
11585
|
+
return reply.send({ success: true, data: preview });
|
|
11586
|
+
} catch (error) {
|
|
11587
|
+
return sendError(reply, error);
|
|
11588
|
+
}
|
|
11589
|
+
}
|
|
11590
|
+
async postProjectPreview(request, reply) {
|
|
11591
|
+
try {
|
|
11592
|
+
const tenantId = tenant(request, this.authRequired);
|
|
11593
|
+
const parsed = projectSchema.safeParse(request.body);
|
|
11594
|
+
if (!parsed.success) return reply.status(400).send({ success: false, code: "INVALID_INPUT", message: "Invalid project capability bundle body" });
|
|
11595
|
+
const result = await this.service.project(tenantId, request.params.projectId, parsed.data.bundleIds, false);
|
|
11596
|
+
return reply.send({ success: true, data: result.preview });
|
|
11597
|
+
} catch (error) {
|
|
11598
|
+
return sendError(reply, error);
|
|
11599
|
+
}
|
|
11600
|
+
}
|
|
11601
|
+
};
|
|
11602
|
+
function registerCapabilityBundleRoutes(app2, controller) {
|
|
11603
|
+
app2.get("/api/capability-bundles", controller.listBundles.bind(controller));
|
|
11604
|
+
app2.post("/api/capability-bundles", controller.createBundle.bind(controller));
|
|
11605
|
+
app2.get("/api/capability-bundles/:bundleId", controller.getBundle.bind(controller));
|
|
11606
|
+
app2.put("/api/capability-bundles/:bundleId", controller.updateBundle.bind(controller));
|
|
11607
|
+
app2.delete("/api/capability-bundles/:bundleId", controller.deleteBundle.bind(controller));
|
|
11608
|
+
app2.get("/api/projects/:projectId/capability-bundles", controller.getProjectBundles.bind(controller));
|
|
11609
|
+
app2.put("/api/projects/:projectId/capability-bundles", controller.updateProjectBundles.bind(controller));
|
|
11610
|
+
app2.get("/api/projects/:projectId/capability-preview", controller.getProjectPreview.bind(controller));
|
|
11611
|
+
app2.post("/api/projects/:projectId/capability-preview", controller.postProjectPreview.bind(controller));
|
|
11612
|
+
}
|
|
11613
|
+
|
|
10642
11614
|
// src/routes/index.ts
|
|
10643
11615
|
var registerLatticeRoutes = (app2, channelDeps) => {
|
|
10644
11616
|
const webAppStore = getStoreLattice16("default", "agentWebApp").store;
|
|
10645
11617
|
const assistantStore = getStoreLattice16("default", "assistant").store;
|
|
10646
11618
|
const projectStore = getStoreLattice16("default", "project").store;
|
|
11619
|
+
registerCapabilityBundleRoutes(app2, new CapabilityBundleController({
|
|
11620
|
+
bundleStore: getStoreLattice16("default", "capabilityBundle").store,
|
|
11621
|
+
projectStore
|
|
11622
|
+
}, { authRequired: process.env.AUTH_REQUIRED === "true" }));
|
|
10647
11623
|
registerAgentWebAppRoutes(app2, {
|
|
10648
11624
|
service: new AgentWebAppService({
|
|
10649
11625
|
webAppStore,
|
|
@@ -12620,13 +13596,13 @@ function createRateLimitMiddleware(maxRequests = 10, windowMs = 60 * 1e3, maxEnt
|
|
|
12620
13596
|
}
|
|
12621
13597
|
|
|
12622
13598
|
// src/router/middlewares/auditLogger.ts
|
|
12623
|
-
var
|
|
13599
|
+
var logger4 = new Logger({ serviceName: "lattice/gateway/audit" });
|
|
12624
13600
|
function createAuditLoggerMiddleware() {
|
|
12625
13601
|
return async (ctx, next) => {
|
|
12626
13602
|
const start2 = Date.now();
|
|
12627
13603
|
try {
|
|
12628
13604
|
await next();
|
|
12629
|
-
|
|
13605
|
+
logger4.info("message routed", {
|
|
12630
13606
|
event: "message:routed",
|
|
12631
13607
|
channel: ctx.inboundMessage.channel,
|
|
12632
13608
|
senderId: ctx.inboundMessage.sender.id,
|
|
@@ -12636,7 +13612,7 @@ function createAuditLoggerMiddleware() {
|
|
|
12636
13612
|
status: "success"
|
|
12637
13613
|
});
|
|
12638
13614
|
} catch (error) {
|
|
12639
|
-
|
|
13615
|
+
logger4.error(
|
|
12640
13616
|
error instanceof Error ? error.message : String(error),
|
|
12641
13617
|
{
|
|
12642
13618
|
event: "message:error",
|
|
@@ -12794,7 +13770,7 @@ ${summary}`
|
|
|
12794
13770
|
console.log(
|
|
12795
13771
|
`\u5C06\u5728 ${delayMs}ms \u540E\u91CD\u8BD5\u4EFB\u52A1 (${nextRetryCount}/${maxRetries})`
|
|
12796
13772
|
);
|
|
12797
|
-
await new Promise((
|
|
13773
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
12798
13774
|
return handleAgentTask(taskRequest, nextRetryCount);
|
|
12799
13775
|
}
|
|
12800
13776
|
if (callback_event) {
|
|
@@ -12994,7 +13970,7 @@ var DEFAULT_LOGGER_CONFIG = {
|
|
|
12994
13970
|
loggerName: "lattice/gateway"
|
|
12995
13971
|
};
|
|
12996
13972
|
var loggerLattice = initializeLogger(DEFAULT_LOGGER_CONFIG);
|
|
12997
|
-
var
|
|
13973
|
+
var logger5 = loggerLattice.client;
|
|
12998
13974
|
function initializeLogger(config) {
|
|
12999
13975
|
if (loggerLatticeManager.hasLattice("default")) {
|
|
13000
13976
|
loggerLatticeManager.removeLattice("default");
|
|
@@ -13027,7 +14003,9 @@ var getHeaderValue = (header) => {
|
|
|
13027
14003
|
var authRequired = process.env.AUTH_REQUIRED === "true";
|
|
13028
14004
|
registerConsoleAuthHook(app, { authRequired });
|
|
13029
14005
|
app.addHook("onRequest", (request, reply, done) => {
|
|
13030
|
-
|
|
14006
|
+
const requestPath = request.url.split("?", 1)[0];
|
|
14007
|
+
const isAgentRunRequest = requestPath === "/api/runs" || requestPath === "/api/resume_stream" || requestPath.includes("/threads/") && requestPath.endsWith("/recover");
|
|
14008
|
+
if (!request.headers["x-project-id"] && !isAgentRunRequest) {
|
|
13031
14009
|
request.headers["x-project-id"] = "default";
|
|
13032
14010
|
}
|
|
13033
14011
|
done();
|
|
@@ -13077,7 +14055,7 @@ app.setErrorHandler((error, request, reply) => {
|
|
|
13077
14055
|
"x-request-id": getHeaderValue(request.headers["x-request-id"]),
|
|
13078
14056
|
"x-user-id": getHeaderValue(request.headers["x-user-id"])
|
|
13079
14057
|
};
|
|
13080
|
-
|
|
14058
|
+
logger5.error(
|
|
13081
14059
|
`\u8BF7\u6C42\u9519\u8BEF: ${request.method} ${request.url} error:${error.message}`,
|
|
13082
14060
|
{
|
|
13083
14061
|
...context,
|
|
@@ -13112,34 +14090,34 @@ async function restoreMcpConnections() {
|
|
|
13112
14090
|
const storeLattice = getStoreLattice27("default", "mcp");
|
|
13113
14091
|
const store = storeLattice.store;
|
|
13114
14092
|
if (!store) {
|
|
13115
|
-
|
|
14093
|
+
logger5.info("MCP store not configured, skipping connection restoration");
|
|
13116
14094
|
return;
|
|
13117
14095
|
}
|
|
13118
14096
|
const configs = await store.getAllConfigs("default");
|
|
13119
14097
|
const connectedConfigs = configs.filter((c2) => c2.status === "connected");
|
|
13120
14098
|
if (connectedConfigs.length === 0) return;
|
|
13121
|
-
|
|
14099
|
+
logger5.info(`Restoring ${connectedConfigs.length} MCP server connections...`);
|
|
13122
14100
|
const { convertToConnection } = await import("./mcp-configs-SHRMQHIL.mjs");
|
|
13123
14101
|
for (const config of connectedConfigs) {
|
|
13124
14102
|
try {
|
|
13125
14103
|
const connection = convertToConnection(config.config);
|
|
13126
14104
|
mcpManager.addServer(config.key, connection);
|
|
13127
14105
|
} catch (err) {
|
|
13128
|
-
|
|
14106
|
+
logger5.warn(`Failed to prepare MCP server "${config.key}" for restoration`, { error: err });
|
|
13129
14107
|
}
|
|
13130
14108
|
}
|
|
13131
14109
|
await mcpManager.connect();
|
|
13132
14110
|
for (const config of connectedConfigs) {
|
|
13133
14111
|
try {
|
|
13134
14112
|
await mcpManager.registerToolsToToolLattice(config.key, config.selectedTools);
|
|
13135
|
-
|
|
14113
|
+
logger5.info(`MCP server "${config.key}" restored with ${config.selectedTools.length} tools`);
|
|
13136
14114
|
} catch (err) {
|
|
13137
|
-
|
|
14115
|
+
logger5.warn(`Failed to register tools for MCP server "${config.key}"`, { error: err });
|
|
13138
14116
|
}
|
|
13139
14117
|
}
|
|
13140
|
-
|
|
14118
|
+
logger5.info(`MCP connection restoration complete: ${connectedConfigs.length} servers`);
|
|
13141
14119
|
} catch (err) {
|
|
13142
|
-
|
|
14120
|
+
logger5.error("MCP connection restoration failed", { error: err });
|
|
13143
14121
|
}
|
|
13144
14122
|
}
|
|
13145
14123
|
var start = async (config) => {
|
|
@@ -13152,7 +14130,7 @@ var start = async (config) => {
|
|
|
13152
14130
|
file: config.loggerConfig.file || DEFAULT_LOGGER_CONFIG.file
|
|
13153
14131
|
};
|
|
13154
14132
|
loggerLattice = initializeLogger(loggerConfig);
|
|
13155
|
-
|
|
14133
|
+
logger5 = loggerLattice.client;
|
|
13156
14134
|
}
|
|
13157
14135
|
app.decorate("loggerLattice", loggerLattice);
|
|
13158
14136
|
let channelDeps;
|
|
@@ -13177,7 +14155,7 @@ var start = async (config) => {
|
|
|
13177
14155
|
});
|
|
13178
14156
|
channelDeps = { router, installationStore };
|
|
13179
14157
|
} catch (err) {
|
|
13180
|
-
|
|
14158
|
+
logger5.warn("Channel infrastructure unavailable", {
|
|
13181
14159
|
error: err instanceof Error ? err.message : String(err)
|
|
13182
14160
|
});
|
|
13183
14161
|
}
|
|
@@ -13185,7 +14163,7 @@ var start = async (config) => {
|
|
|
13185
14163
|
try {
|
|
13186
14164
|
const menuStore = getStoreLattice27("default", "menu").store;
|
|
13187
14165
|
setMenuRegistry(menuStore);
|
|
13188
|
-
|
|
14166
|
+
logger5.info("Menu registry initialized");
|
|
13189
14167
|
} catch {
|
|
13190
14168
|
}
|
|
13191
14169
|
registerAllBuiltinEntities();
|
|
@@ -13218,15 +14196,15 @@ var start = async (config) => {
|
|
|
13218
14196
|
getProjectStore: () => projectStore
|
|
13219
14197
|
});
|
|
13220
14198
|
a2a.registerA2ARoutes(app);
|
|
13221
|
-
|
|
14199
|
+
logger5.info("A2A standard routes registered");
|
|
13222
14200
|
} catch (err) {
|
|
13223
|
-
|
|
14201
|
+
logger5.warn("A2A standard routes unavailable", {
|
|
13224
14202
|
error: err instanceof Error ? err.message : String(err)
|
|
13225
14203
|
});
|
|
13226
14204
|
}
|
|
13227
14205
|
if (!sandboxLatticeManager2.hasLattice("default")) {
|
|
13228
14206
|
sandboxLatticeManager2.registerLattice("default", getConfiguredSandboxProvider());
|
|
13229
|
-
|
|
14207
|
+
logger5.info("Registered sandbox manager from env configuration");
|
|
13230
14208
|
}
|
|
13231
14209
|
try {
|
|
13232
14210
|
const { ResourceController } = await import("./resources-VA7LSDKN.mjs");
|
|
@@ -13236,12 +14214,12 @@ var start = async (config) => {
|
|
|
13236
14214
|
store: sharedResourceStore,
|
|
13237
14215
|
sandboxManager: sandboxLatticeManager2,
|
|
13238
14216
|
cache,
|
|
13239
|
-
logger:
|
|
14217
|
+
logger: logger5
|
|
13240
14218
|
});
|
|
13241
14219
|
registerResourceRoutes(app, resourceController);
|
|
13242
|
-
|
|
14220
|
+
logger5.info("Resource share routes registered");
|
|
13243
14221
|
} catch (err) {
|
|
13244
|
-
|
|
14222
|
+
logger5.warn("Resource share infrastructure unavailable", {
|
|
13245
14223
|
error: err instanceof Error ? err.message : String(err)
|
|
13246
14224
|
});
|
|
13247
14225
|
}
|
|
@@ -13253,14 +14231,14 @@ var start = async (config) => {
|
|
|
13253
14231
|
{ deps: { router: channelDeps.router } }
|
|
13254
14232
|
);
|
|
13255
14233
|
} catch (err) {
|
|
13256
|
-
|
|
14234
|
+
logger5.error("Failed to start channel connections", {
|
|
13257
14235
|
error: err instanceof Error ? err.message : String(err)
|
|
13258
14236
|
});
|
|
13259
14237
|
}
|
|
13260
14238
|
}
|
|
13261
14239
|
const target_port = config?.port || Number(process.env.PORT) || 4001;
|
|
13262
14240
|
await app.listen({ port: target_port, host: "0.0.0.0" });
|
|
13263
|
-
|
|
14241
|
+
logger5.info(`Lattice Gateway is running on port: ${target_port}`);
|
|
13264
14242
|
const queueServiceConfig = config?.queueServiceConfig;
|
|
13265
14243
|
if (queueServiceConfig) {
|
|
13266
14244
|
setQueueServiceType(queueServiceConfig.type);
|
|
@@ -13271,16 +14249,16 @@ var start = async (config) => {
|
|
|
13271
14249
|
}
|
|
13272
14250
|
if (process.env.AXIOM_RESTORE_PENDING_ON_STARTUP === "true") {
|
|
13273
14251
|
agentInstanceManager9.restore().then((stats) => {
|
|
13274
|
-
|
|
14252
|
+
logger5.info(`Agent recovery complete: ${stats.restored} threads restored, ${stats.errors} errors`);
|
|
13275
14253
|
}).catch((error) => {
|
|
13276
|
-
|
|
14254
|
+
logger5.error("Agent recovery failed", { error });
|
|
13277
14255
|
});
|
|
13278
14256
|
}
|
|
13279
14257
|
restoreMcpConnections().catch((error) => {
|
|
13280
|
-
|
|
14258
|
+
logger5.error("MCP connection restoration failed", { error });
|
|
13281
14259
|
});
|
|
13282
14260
|
} catch (err) {
|
|
13283
|
-
|
|
14261
|
+
logger5.error("Server start failed", { error: err });
|
|
13284
14262
|
process.exit(1);
|
|
13285
14263
|
}
|
|
13286
14264
|
};
|