@axiom-lattice/gateway 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +608 -88
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +605 -85
- 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));
|
|
@@ -3712,7 +3712,7 @@ var ExportImportService = class {
|
|
|
3712
3712
|
const files = /* @__PURE__ */ new Map();
|
|
3713
3713
|
let bundle;
|
|
3714
3714
|
const gunzip = zlib.createGunzip();
|
|
3715
|
-
await new Promise((
|
|
3715
|
+
await new Promise((resolve3, reject) => {
|
|
3716
3716
|
let settled = false;
|
|
3717
3717
|
let entryCount = 0;
|
|
3718
3718
|
let declaredTotalBytes = 0;
|
|
@@ -3832,7 +3832,7 @@ var ExportImportService = class {
|
|
|
3832
3832
|
parser.on("end", () => {
|
|
3833
3833
|
if (settled) return;
|
|
3834
3834
|
settled = true;
|
|
3835
|
-
|
|
3835
|
+
resolve3();
|
|
3836
3836
|
});
|
|
3837
3837
|
gunzip.pipe(parser);
|
|
3838
3838
|
gunzip.end(buffer);
|
|
@@ -4517,6 +4517,20 @@ var dataQuerySchema = {
|
|
|
4517
4517
|
};
|
|
4518
4518
|
|
|
4519
4519
|
// src/schemas/index.ts
|
|
4520
|
+
var downloadFolderSchema = {
|
|
4521
|
+
description: "Download a workspace project folder as a ZIP archive",
|
|
4522
|
+
tags: ["Workspace"],
|
|
4523
|
+
querystring: {
|
|
4524
|
+
type: "object",
|
|
4525
|
+
properties: {
|
|
4526
|
+
path: { type: "string", minLength: 1 },
|
|
4527
|
+
assistantId: { type: "string", minLength: 1 },
|
|
4528
|
+
tenantId: { type: "string", minLength: 1 }
|
|
4529
|
+
},
|
|
4530
|
+
required: ["path"],
|
|
4531
|
+
additionalProperties: false
|
|
4532
|
+
}
|
|
4533
|
+
};
|
|
4520
4534
|
var getAllMemoryItemsSchema = {
|
|
4521
4535
|
description: "Get all memory items for an assistant thread",
|
|
4522
4536
|
tags: ["Memory"],
|
|
@@ -5114,11 +5128,89 @@ function registerSandboxProxyRoutes(app2) {
|
|
|
5114
5128
|
// src/controllers/workspace.ts
|
|
5115
5129
|
import * as fs2 from "fs/promises";
|
|
5116
5130
|
import * as path2 from "path";
|
|
5131
|
+
import { constants as fsConstants } from "fs";
|
|
5117
5132
|
import * as os2 from "os";
|
|
5118
5133
|
import { getStoreLattice as getStoreLattice6 } from "@axiom-lattice/core";
|
|
5119
5134
|
import { SandboxFilesystem, FilesystemBackend } from "@axiom-lattice/core";
|
|
5120
5135
|
import { getSandBoxManager as getSandBoxManager3 } from "@axiom-lattice/core";
|
|
5121
5136
|
import { v4 as uuidv42 } from "uuid";
|
|
5137
|
+
var MAX_ZIP_ENTRIES = 1e4;
|
|
5138
|
+
var MAX_ZIP_FILE_SIZE = 100 * 1024 * 1024;
|
|
5139
|
+
var MAX_ZIP_TOTAL_SIZE = 512 * 1024 * 1024;
|
|
5140
|
+
var FolderDownloadError = class extends Error {
|
|
5141
|
+
constructor(statusCode, code, message) {
|
|
5142
|
+
super(message);
|
|
5143
|
+
this.statusCode = statusCode;
|
|
5144
|
+
this.code = code;
|
|
5145
|
+
this.name = "FolderDownloadError";
|
|
5146
|
+
}
|
|
5147
|
+
};
|
|
5148
|
+
function zipCrc32(data) {
|
|
5149
|
+
let crc = 4294967295;
|
|
5150
|
+
for (const byte of data) {
|
|
5151
|
+
crc ^= byte;
|
|
5152
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
5153
|
+
crc = crc >>> 1 ^ 3988292384 & -(crc & 1);
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
return (crc ^ 4294967295) >>> 0;
|
|
5157
|
+
}
|
|
5158
|
+
function createZip(entries) {
|
|
5159
|
+
const localParts = [];
|
|
5160
|
+
const centralParts = [];
|
|
5161
|
+
let offset = 0;
|
|
5162
|
+
for (const entry of entries) {
|
|
5163
|
+
const name = Buffer.from(entry.name, "utf8");
|
|
5164
|
+
const data = entry.data;
|
|
5165
|
+
const crc = zipCrc32(data);
|
|
5166
|
+
const local = Buffer.alloc(30 + name.length + data.length);
|
|
5167
|
+
local.writeUInt32LE(67324752, 0);
|
|
5168
|
+
local.writeUInt16LE(20, 4);
|
|
5169
|
+
local.writeUInt16LE(0, 6);
|
|
5170
|
+
local.writeUInt16LE(2048, 8);
|
|
5171
|
+
local.writeUInt16LE(0, 10);
|
|
5172
|
+
local.writeUInt16LE(0, 12);
|
|
5173
|
+
local.writeUInt32LE(crc, 14);
|
|
5174
|
+
local.writeUInt32LE(data.length, 18);
|
|
5175
|
+
local.writeUInt32LE(data.length, 22);
|
|
5176
|
+
local.writeUInt16LE(name.length, 26);
|
|
5177
|
+
local.writeUInt16LE(0, 28);
|
|
5178
|
+
name.copy(local, 30);
|
|
5179
|
+
data.copy(local, 30 + name.length);
|
|
5180
|
+
localParts.push(local);
|
|
5181
|
+
const central = Buffer.alloc(46 + name.length);
|
|
5182
|
+
central.writeUInt32LE(33639248, 0);
|
|
5183
|
+
central.writeUInt16LE(20, 4);
|
|
5184
|
+
central.writeUInt16LE(20, 6);
|
|
5185
|
+
central.writeUInt16LE(0, 8);
|
|
5186
|
+
central.writeUInt16LE(2048, 10);
|
|
5187
|
+
central.writeUInt16LE(0, 12);
|
|
5188
|
+
central.writeUInt16LE(0, 14);
|
|
5189
|
+
central.writeUInt32LE(crc, 16);
|
|
5190
|
+
central.writeUInt32LE(data.length, 20);
|
|
5191
|
+
central.writeUInt32LE(data.length, 24);
|
|
5192
|
+
central.writeUInt16LE(name.length, 28);
|
|
5193
|
+
central.writeUInt16LE(0, 30);
|
|
5194
|
+
central.writeUInt16LE(0, 32);
|
|
5195
|
+
central.writeUInt16LE(0, 34);
|
|
5196
|
+
central.writeUInt16LE(entry.directory ? 16 : 0, 36);
|
|
5197
|
+
central.writeUInt32LE(entry.directory ? 16 : 0, 38);
|
|
5198
|
+
central.writeUInt32LE(offset, 42);
|
|
5199
|
+
name.copy(central, 46);
|
|
5200
|
+
centralParts.push(central);
|
|
5201
|
+
offset += local.length;
|
|
5202
|
+
}
|
|
5203
|
+
const centralDirectory = Buffer.concat(centralParts);
|
|
5204
|
+
const end = Buffer.alloc(22);
|
|
5205
|
+
end.writeUInt32LE(101010256, 0);
|
|
5206
|
+
end.writeUInt16LE(0, 4);
|
|
5207
|
+
end.writeUInt16LE(0, 6);
|
|
5208
|
+
end.writeUInt16LE(entries.length, 8);
|
|
5209
|
+
end.writeUInt16LE(entries.length, 10);
|
|
5210
|
+
end.writeUInt32LE(centralDirectory.length, 12);
|
|
5211
|
+
end.writeUInt32LE(offset, 16);
|
|
5212
|
+
return Buffer.concat([...localParts, centralDirectory, end]);
|
|
5213
|
+
}
|
|
5122
5214
|
var WorkspaceController = class {
|
|
5123
5215
|
constructor() {
|
|
5124
5216
|
this.workspaceStore = getStoreLattice6("default", "workspace").store;
|
|
@@ -5384,6 +5476,259 @@ var WorkspaceController = class {
|
|
|
5384
5476
|
return reply.status(502).send({ success: false, error: `Download proxy error: ${message}` });
|
|
5385
5477
|
}
|
|
5386
5478
|
}
|
|
5479
|
+
async downloadFolder(request, reply) {
|
|
5480
|
+
const tenantId = this.getTenantId(request);
|
|
5481
|
+
const { workspaceId, projectId } = request.params;
|
|
5482
|
+
const folderPath = request.query.path;
|
|
5483
|
+
const assistantId = request.query.assistantId;
|
|
5484
|
+
if (!folderPath || folderPath.includes("\\") || folderPath.split("/").includes("..")) {
|
|
5485
|
+
return reply.status(400).send({
|
|
5486
|
+
success: false,
|
|
5487
|
+
error: "Invalid folder path",
|
|
5488
|
+
code: "INVALID_PATH"
|
|
5489
|
+
});
|
|
5490
|
+
}
|
|
5491
|
+
try {
|
|
5492
|
+
const storedWorkspace = await this.workspaceStore.getWorkspaceById(tenantId, workspaceId);
|
|
5493
|
+
if (!storedWorkspace) {
|
|
5494
|
+
throw new FolderDownloadError(404, "WORKSPACE_NOT_FOUND", "Workspace not found");
|
|
5495
|
+
}
|
|
5496
|
+
const project = await this.projectStore.getProjectById(tenantId, projectId);
|
|
5497
|
+
if (!project || project.workspaceId !== workspaceId) {
|
|
5498
|
+
throw new FolderDownloadError(404, "PROJECT_NOT_FOUND", "Project not found");
|
|
5499
|
+
}
|
|
5500
|
+
const { backend, workspace } = await this.getBackend(
|
|
5501
|
+
tenantId,
|
|
5502
|
+
workspaceId,
|
|
5503
|
+
projectId,
|
|
5504
|
+
assistantId,
|
|
5505
|
+
folderPath
|
|
5506
|
+
);
|
|
5507
|
+
if (!backend.lsInfo || workspace.storageType === "sandbox" && !backend.readBinary) {
|
|
5508
|
+
throw new FolderDownloadError(502, "BACKEND_UNSUPPORTED", "Backend does not support folder downloads");
|
|
5509
|
+
}
|
|
5510
|
+
let listEntries2;
|
|
5511
|
+
if (workspace.storageType !== "sandbox") {
|
|
5512
|
+
const root = filesystemRootDirectory(tenantId, workspaceId, projectId);
|
|
5513
|
+
const target = path2.resolve(root, `.${folderPath.startsWith("/") ? folderPath : `/${folderPath}`}`);
|
|
5514
|
+
const contained = await resolvePathWithinRoot(root, target).catch((error) => {
|
|
5515
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5516
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5517
|
+
}
|
|
5518
|
+
throw error;
|
|
5519
|
+
});
|
|
5520
|
+
let stat2;
|
|
5521
|
+
try {
|
|
5522
|
+
stat2 = await fs2.stat(contained.targetPath);
|
|
5523
|
+
} catch (error) {
|
|
5524
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5525
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5526
|
+
}
|
|
5527
|
+
throw error;
|
|
5528
|
+
}
|
|
5529
|
+
if (!stat2.isDirectory()) {
|
|
5530
|
+
throw new FolderDownloadError(400, "NOT_A_FOLDER", "Path is not a folder");
|
|
5531
|
+
}
|
|
5532
|
+
} else if (folderPath) {
|
|
5533
|
+
const sandboxManager = getSandBoxManager3();
|
|
5534
|
+
const volumeConfig = {
|
|
5535
|
+
assistant_id: assistantId || "",
|
|
5536
|
+
thread_id: "",
|
|
5537
|
+
tenantId,
|
|
5538
|
+
workspaceId,
|
|
5539
|
+
projectId
|
|
5540
|
+
};
|
|
5541
|
+
const volume = await sandboxManager.getVolumeBackendForPath(volumeConfig, folderPath);
|
|
5542
|
+
if (!volume) {
|
|
5543
|
+
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
5544
|
+
try {
|
|
5545
|
+
await sandbox.file.listPath(folderPath, { recursive: false });
|
|
5546
|
+
} catch (error) {
|
|
5547
|
+
const code = error instanceof Error ? error.code : void 0;
|
|
5548
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5549
|
+
if (code === "ENOENT" || /not found|no such file|enoent/i.test(message)) {
|
|
5550
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5551
|
+
}
|
|
5552
|
+
throw error;
|
|
5553
|
+
}
|
|
5554
|
+
listEntries2 = async (directoryPath) => {
|
|
5555
|
+
const result = await sandbox.file.listPath(directoryPath, { recursive: false });
|
|
5556
|
+
return result.files;
|
|
5557
|
+
};
|
|
5558
|
+
} else {
|
|
5559
|
+
listEntries2 = async (directoryPath) => backend.lsInfo(directoryPath);
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
if (!listEntries2) {
|
|
5563
|
+
listEntries2 = async (directoryPath) => backend.lsInfo(directoryPath);
|
|
5564
|
+
}
|
|
5565
|
+
const folderName = path2.basename(folderPath.replace(/\/$/, "")) || "folder";
|
|
5566
|
+
const prefix = `${folderName}/`;
|
|
5567
|
+
const entries = [{ name: prefix, data: Buffer.alloc(0), directory: true }];
|
|
5568
|
+
let totalSize = 0;
|
|
5569
|
+
const filesystemRoot = workspace.storageType === "sandbox" ? void 0 : filesystemRootDirectory(tenantId, workspaceId, projectId);
|
|
5570
|
+
const addFile = (archivePath, data) => {
|
|
5571
|
+
if (data.length > MAX_ZIP_FILE_SIZE || totalSize + data.length > MAX_ZIP_TOTAL_SIZE) {
|
|
5572
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
5573
|
+
}
|
|
5574
|
+
totalSize += data.length;
|
|
5575
|
+
entries.push({ name: archivePath, data, directory: false });
|
|
5576
|
+
};
|
|
5577
|
+
const collectFilesystem = async (directoryPath, archivePath) => {
|
|
5578
|
+
let children;
|
|
5579
|
+
try {
|
|
5580
|
+
children = await fs2.readdir(directoryPath, { withFileTypes: true });
|
|
5581
|
+
} catch (error) {
|
|
5582
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5583
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5584
|
+
}
|
|
5585
|
+
throw error;
|
|
5586
|
+
}
|
|
5587
|
+
for (const child of children) {
|
|
5588
|
+
if (entries.length >= MAX_ZIP_ENTRIES) {
|
|
5589
|
+
throw new FolderDownloadError(400, "ENTRY_LIMIT", "Folder contains too many entries");
|
|
5590
|
+
}
|
|
5591
|
+
const childPath = path2.join(directoryPath, child.name);
|
|
5592
|
+
const archiveChildPath = `${archivePath}${child.name}`;
|
|
5593
|
+
let resolvedChild;
|
|
5594
|
+
try {
|
|
5595
|
+
resolvedChild = await resolvePathWithinRoot(filesystemRoot, childPath);
|
|
5596
|
+
} catch (error) {
|
|
5597
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5598
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5599
|
+
}
|
|
5600
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains a path outside the project");
|
|
5601
|
+
}
|
|
5602
|
+
let childStat;
|
|
5603
|
+
try {
|
|
5604
|
+
childStat = await fs2.lstat(resolvedChild.targetPath);
|
|
5605
|
+
} catch (error) {
|
|
5606
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5607
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5608
|
+
}
|
|
5609
|
+
throw error;
|
|
5610
|
+
}
|
|
5611
|
+
if (childStat.isSymbolicLink() || child.isSymbolicLink()) {
|
|
5612
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Symbolic links are not allowed in folder downloads");
|
|
5613
|
+
}
|
|
5614
|
+
if (child.isDirectory()) {
|
|
5615
|
+
entries.push({ name: `${archiveChildPath}/`, data: Buffer.alloc(0), directory: true });
|
|
5616
|
+
await collectFilesystem(resolvedChild.targetPath, `${archiveChildPath}/`);
|
|
5617
|
+
} else if (child.isFile()) {
|
|
5618
|
+
if (childStat.size > MAX_ZIP_FILE_SIZE || totalSize + childStat.size > MAX_ZIP_TOTAL_SIZE) {
|
|
5619
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
5620
|
+
}
|
|
5621
|
+
const flags = fsConstants.O_NOFOLLOW === void 0 ? fsConstants.O_RDONLY : fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW;
|
|
5622
|
+
let handle3;
|
|
5623
|
+
try {
|
|
5624
|
+
handle3 = await fs2.open(resolvedChild.targetPath, flags);
|
|
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
|
+
try {
|
|
5632
|
+
const openedStat = await handle3.stat();
|
|
5633
|
+
if (!openedStat.isFile() || openedStat.size > MAX_ZIP_FILE_SIZE || totalSize + openedStat.size > MAX_ZIP_TOTAL_SIZE) {
|
|
5634
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
5635
|
+
}
|
|
5636
|
+
const readLimit = MAX_ZIP_FILE_SIZE + 1;
|
|
5637
|
+
const dataBuffer = Buffer.alloc(Math.min(openedStat.size + 1, readLimit));
|
|
5638
|
+
let bytesRead = 0;
|
|
5639
|
+
while (bytesRead < dataBuffer.length) {
|
|
5640
|
+
const result = await handle3.read(
|
|
5641
|
+
dataBuffer,
|
|
5642
|
+
bytesRead,
|
|
5643
|
+
dataBuffer.length - bytesRead,
|
|
5644
|
+
bytesRead
|
|
5645
|
+
);
|
|
5646
|
+
if (result.bytesRead === 0) break;
|
|
5647
|
+
bytesRead += result.bytesRead;
|
|
5648
|
+
}
|
|
5649
|
+
const data = dataBuffer.subarray(0, bytesRead);
|
|
5650
|
+
const finalStat = await handle3.stat();
|
|
5651
|
+
if (finalStat.size > MAX_ZIP_FILE_SIZE || totalSize + finalStat.size > MAX_ZIP_TOTAL_SIZE || finalStat.size > data.length) {
|
|
5652
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
5653
|
+
}
|
|
5654
|
+
addFile(archiveChildPath, data);
|
|
5655
|
+
} finally {
|
|
5656
|
+
await handle3.close();
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
};
|
|
5661
|
+
const collect = async (currentPath, archivePath) => {
|
|
5662
|
+
let children;
|
|
5663
|
+
try {
|
|
5664
|
+
children = await listEntries2(currentPath);
|
|
5665
|
+
} catch (error) {
|
|
5666
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5667
|
+
if (/not found|enoent|no such file/i.test(message)) {
|
|
5668
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5669
|
+
}
|
|
5670
|
+
throw error;
|
|
5671
|
+
}
|
|
5672
|
+
for (const child of children) {
|
|
5673
|
+
if (entries.length >= MAX_ZIP_ENTRIES) {
|
|
5674
|
+
throw new FolderDownloadError(400, "ENTRY_LIMIT", "Folder contains too many entries");
|
|
5675
|
+
}
|
|
5676
|
+
const childPath = child.path;
|
|
5677
|
+
if (childPath.includes("\\") || childPath.split("/").includes("..") || !childPath.startsWith(folderPath.endsWith("/") ? folderPath : `${folderPath}/`)) {
|
|
5678
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains an unsafe child path");
|
|
5679
|
+
}
|
|
5680
|
+
if (workspace.storageType !== "sandbox") {
|
|
5681
|
+
const root = filesystemRootDirectory(tenantId, workspaceId, projectId);
|
|
5682
|
+
const virtualChildPath = childPath.startsWith("/") ? childPath : `/${childPath}`;
|
|
5683
|
+
const target = path2.resolve(root, `.${virtualChildPath}`);
|
|
5684
|
+
try {
|
|
5685
|
+
await resolvePathWithinRoot(root, target);
|
|
5686
|
+
const childStat = await fs2.lstat(target);
|
|
5687
|
+
if (childStat.isSymbolicLink()) {
|
|
5688
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Symbolic links are not allowed in folder downloads");
|
|
5689
|
+
}
|
|
5690
|
+
} catch (error) {
|
|
5691
|
+
if (error instanceof FolderDownloadError) throw error;
|
|
5692
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
5693
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
5694
|
+
}
|
|
5695
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains a path outside the project");
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
const childName = path2.basename(childPath.replace(/\/$/, ""));
|
|
5699
|
+
const childArchivePath = `${archivePath}${childName}`;
|
|
5700
|
+
if (child.is_dir) {
|
|
5701
|
+
entries.push({ name: `${childArchivePath}/`, data: Buffer.alloc(0), directory: true });
|
|
5702
|
+
await collect(childPath, `${childArchivePath}/`);
|
|
5703
|
+
} else {
|
|
5704
|
+
const size = child.size ?? 0;
|
|
5705
|
+
if (size > MAX_ZIP_FILE_SIZE || totalSize + size > MAX_ZIP_TOTAL_SIZE) {
|
|
5706
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
5707
|
+
}
|
|
5708
|
+
const data = await backend.readBinary(childPath);
|
|
5709
|
+
addFile(childArchivePath, data);
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
};
|
|
5713
|
+
if (workspace.storageType === "sandbox") {
|
|
5714
|
+
await collect(folderPath, prefix);
|
|
5715
|
+
} else {
|
|
5716
|
+
const root = filesystemRoot;
|
|
5717
|
+
const target = path2.resolve(root, `.${folderPath.startsWith("/") ? folderPath : `/${folderPath}`}`);
|
|
5718
|
+
await collectFilesystem(target, prefix);
|
|
5719
|
+
}
|
|
5720
|
+
const zip = createZip(entries);
|
|
5721
|
+
const downloadName = `${folderName}.zip`;
|
|
5722
|
+
return reply.status(200).type("application/zip").header("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(downloadName)}`).send(zip);
|
|
5723
|
+
} catch (error) {
|
|
5724
|
+
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));
|
|
5725
|
+
return reply.status(folderError.statusCode).send({
|
|
5726
|
+
success: false,
|
|
5727
|
+
error: folderError.message,
|
|
5728
|
+
code: folderError.code
|
|
5729
|
+
});
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5387
5732
|
/**
|
|
5388
5733
|
* View a file inline in the browser (not download).
|
|
5389
5734
|
* Returns file content with inline Content-Disposition for preview.
|
|
@@ -5676,6 +6021,11 @@ function registerWorkspaceRoutes(app2) {
|
|
|
5676
6021
|
"/api/workspaces/:workspaceId/projects/:projectId/downloadfile",
|
|
5677
6022
|
controller.downloadFile.bind(controller)
|
|
5678
6023
|
);
|
|
6024
|
+
app2.get(
|
|
6025
|
+
"/api/workspaces/:workspaceId/projects/:projectId/downloadfolder",
|
|
6026
|
+
{ schema: downloadFolderSchema },
|
|
6027
|
+
controller.downloadFolder.bind(controller)
|
|
6028
|
+
);
|
|
5679
6029
|
app2.get(
|
|
5680
6030
|
"/api/workspaces/:workspaceId/projects/:projectId/viewfile",
|
|
5681
6031
|
controller.viewFile.bind(controller)
|
|
@@ -5903,7 +6253,7 @@ async function testDatabaseConnection(request, reply) {
|
|
|
5903
6253
|
await db.connect();
|
|
5904
6254
|
await db.listTables();
|
|
5905
6255
|
const latency = Date.now() - startTime;
|
|
5906
|
-
await new Promise((
|
|
6256
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
5907
6257
|
await db.disconnect();
|
|
5908
6258
|
await sqlDatabaseManager.removeDatabase(tenantId, testKey);
|
|
5909
6259
|
return {
|
|
@@ -6725,6 +7075,7 @@ import {
|
|
|
6725
7075
|
LatticeEvalProject
|
|
6726
7076
|
} from "@axiom-lattice/core";
|
|
6727
7077
|
import { v4 as uuidv43 } from "uuid";
|
|
7078
|
+
var TERMINAL_PERSIST_ATTEMPTS = 2;
|
|
6728
7079
|
function mapLogs(logs) {
|
|
6729
7080
|
return logs.map((l) => ({
|
|
6730
7081
|
timestamp: l.ts,
|
|
@@ -6736,18 +7087,42 @@ function mapLogs(logs) {
|
|
|
6736
7087
|
var EvalRunner = class {
|
|
6737
7088
|
constructor() {
|
|
6738
7089
|
this.runs = /* @__PURE__ */ new Map();
|
|
7090
|
+
this.projectLeases = /* @__PURE__ */ new Map();
|
|
6739
7091
|
this.eventEmitter = new EventEmitter();
|
|
6740
7092
|
this.processStartedAt = /* @__PURE__ */ new Date();
|
|
6741
7093
|
}
|
|
6742
7094
|
getEventEmitter() {
|
|
6743
7095
|
return this.eventEmitter;
|
|
6744
7096
|
}
|
|
7097
|
+
reserveProjectDeletion(tenantId, projectId) {
|
|
7098
|
+
const leaseKey = `${tenantId}:${projectId}`;
|
|
7099
|
+
if (this.projectLeases.has(leaseKey) || [...this.runs.values()].some(
|
|
7100
|
+
(ctx) => ctx.tenantId === tenantId && ctx.projectId === projectId && !ctx.terminal.workerSettled
|
|
7101
|
+
)) return null;
|
|
7102
|
+
const leaseToken = Symbol(`delete:${leaseKey}`);
|
|
7103
|
+
this.projectLeases.set(leaseKey, leaseToken);
|
|
7104
|
+
return () => this.releaseProjectLease(leaseKey, leaseToken);
|
|
7105
|
+
}
|
|
6745
7106
|
async startRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId) {
|
|
7107
|
+
const leaseKey = `${tenantId}:${projectId}`;
|
|
7108
|
+
if (this.projectLeases.has(leaseKey)) {
|
|
7109
|
+
throw new Error("A run is already in progress for this project");
|
|
7110
|
+
}
|
|
7111
|
+
const leaseToken = Symbol(leaseKey);
|
|
7112
|
+
this.projectLeases.set(leaseKey, leaseToken);
|
|
7113
|
+
try {
|
|
7114
|
+
return await this.startLeasedRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId, leaseKey, leaseToken);
|
|
7115
|
+
} catch (error) {
|
|
7116
|
+
this.releaseProjectLease(leaseKey, leaseToken);
|
|
7117
|
+
throw error;
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
7120
|
+
async startLeasedRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId, leaseKey, leaseToken) {
|
|
6746
7121
|
const store = this.getEvalStore();
|
|
6747
7122
|
const project = await store.getProjectById(tenantId, projectId);
|
|
6748
7123
|
if (!project) throw new Error("Project not found");
|
|
6749
7124
|
for (const ctx of this.runs.values()) {
|
|
6750
|
-
if (ctx.projectId === projectId) {
|
|
7125
|
+
if (ctx.projectId === projectId && !ctx.terminal.persisted) {
|
|
6751
7126
|
throw new Error("A run is already in progress for this project");
|
|
6752
7127
|
}
|
|
6753
7128
|
}
|
|
@@ -6851,14 +7226,35 @@ var EvalRunner = class {
|
|
|
6851
7226
|
};
|
|
6852
7227
|
const abortController = new AbortController();
|
|
6853
7228
|
const stats = { passed: 0, failed: 0, interrupted: 0, totalScore: 0 };
|
|
7229
|
+
const terminal = { claiming: false, persisted: false, workerSettled: false };
|
|
7230
|
+
const terminalUnavailable = () => terminal.claiming || terminal.persisted;
|
|
7231
|
+
const persistTerminal = async (update, event) => {
|
|
7232
|
+
if (terminalUnavailable()) return false;
|
|
7233
|
+
terminal.claiming = true;
|
|
7234
|
+
try {
|
|
7235
|
+
let lastError;
|
|
7236
|
+
for (let attempt = 0; attempt < TERMINAL_PERSIST_ATTEMPTS; attempt++) {
|
|
7237
|
+
try {
|
|
7238
|
+
const persistedRun = await store.updateRunStatus(tenantId, runId, update);
|
|
7239
|
+
if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
|
|
7240
|
+
lastError = void 0;
|
|
7241
|
+
break;
|
|
7242
|
+
} catch (error) {
|
|
7243
|
+
lastError = error;
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
if (lastError) throw lastError;
|
|
7247
|
+
terminal.persisted = true;
|
|
7248
|
+
this.eventEmitter.emit(`run:${runId}`, event);
|
|
7249
|
+
return true;
|
|
7250
|
+
} finally {
|
|
7251
|
+
terminal.claiming = false;
|
|
7252
|
+
}
|
|
7253
|
+
};
|
|
6854
7254
|
const onCaseComplete = async (result, suiteName) => {
|
|
7255
|
+
if (terminalUnavailable()) return;
|
|
6855
7256
|
const passed = result.result?.pass ?? false;
|
|
6856
7257
|
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
7258
|
await store.createRunResult(tenantId, runId, uuidv43(), {
|
|
6863
7259
|
suiteName,
|
|
6864
7260
|
caseId: result.caseId,
|
|
@@ -6876,13 +7272,22 @@ var EvalRunner = class {
|
|
|
6876
7272
|
error: result.error,
|
|
6877
7273
|
interrupted: result.interrupted
|
|
6878
7274
|
});
|
|
7275
|
+
if (terminalUnavailable()) return;
|
|
7276
|
+
if (passed) stats.passed++;
|
|
7277
|
+
else stats.failed++;
|
|
7278
|
+
stats.totalScore += score;
|
|
7279
|
+
if (result.interrupted) stats.interrupted++;
|
|
7280
|
+
const completedCount = stats.passed + stats.failed;
|
|
7281
|
+
if (terminalUnavailable()) return;
|
|
6879
7282
|
const avgScore = completedCount > 0 ? stats.totalScore / completedCount : 0;
|
|
6880
|
-
await store.updateRunStatus(tenantId, runId, {
|
|
7283
|
+
const persistedRun = await store.updateRunStatus(tenantId, runId, {
|
|
6881
7284
|
passedCases: stats.passed,
|
|
6882
7285
|
failedCases: stats.failed,
|
|
6883
7286
|
interruptedCases: stats.interrupted,
|
|
6884
7287
|
avgScore
|
|
6885
7288
|
});
|
|
7289
|
+
if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
|
|
7290
|
+
if (terminalUnavailable()) return;
|
|
6886
7291
|
this.eventEmitter.emit(`run:${runId}`, {
|
|
6887
7292
|
type: "progress",
|
|
6888
7293
|
runId,
|
|
@@ -6894,65 +7299,86 @@ var EvalRunner = class {
|
|
|
6894
7299
|
const evalProject = new LatticeEvalProject(projectConfig, onCaseComplete);
|
|
6895
7300
|
const calibration = await evalProject.calibrateJudge();
|
|
6896
7301
|
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
7302
|
throw new Error(`Judge calibration failed: ${calibration.reason}`);
|
|
6903
7303
|
}
|
|
6904
7304
|
if (calibration.bypassed) {
|
|
6905
7305
|
console.warn(`[eval-runner] Judge calibration bypassed \u2014 judge may be unreliable: ${calibration.reason}`);
|
|
6906
7306
|
}
|
|
6907
7307
|
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
|
-
}
|
|
7308
|
+
const completedCount = stats.passed + stats.failed;
|
|
7309
|
+
await persistTerminal({
|
|
7310
|
+
status: "completed",
|
|
7311
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
7312
|
+
passedCases: stats.passed,
|
|
7313
|
+
failedCases: stats.failed,
|
|
7314
|
+
interruptedCases: stats.interrupted,
|
|
7315
|
+
avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0
|
|
7316
|
+
}, {
|
|
7317
|
+
type: "completed",
|
|
7318
|
+
runId,
|
|
7319
|
+
data: { passed: stats.passed, failed: stats.failed, avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0 }
|
|
7320
|
+
});
|
|
6924
7321
|
return { report };
|
|
6925
7322
|
} catch (err) {
|
|
6926
7323
|
const errorMsg = err.message;
|
|
6927
|
-
|
|
6928
|
-
await
|
|
7324
|
+
try {
|
|
7325
|
+
await persistTerminal({
|
|
6929
7326
|
status: "failed",
|
|
6930
7327
|
error: errorMsg,
|
|
6931
7328
|
completedAt: /* @__PURE__ */ new Date()
|
|
6932
|
-
}
|
|
6933
|
-
this.eventEmitter.emit(`run:${runId}`, {
|
|
7329
|
+
}, {
|
|
6934
7330
|
type: "error",
|
|
6935
7331
|
runId,
|
|
6936
7332
|
data: { message: errorMsg }
|
|
6937
7333
|
});
|
|
7334
|
+
} catch {
|
|
6938
7335
|
}
|
|
6939
7336
|
throw err;
|
|
6940
7337
|
} finally {
|
|
6941
|
-
|
|
7338
|
+
terminal.workerSettled = true;
|
|
7339
|
+
if (terminal.persisted) {
|
|
7340
|
+
this.runs.delete(runId);
|
|
7341
|
+
this.releaseProjectLease(leaseKey, leaseToken);
|
|
7342
|
+
}
|
|
6942
7343
|
}
|
|
6943
7344
|
})();
|
|
6944
|
-
this.runs.set(runId, { runId, projectId, tenantId, abortController, promise: runPromise, taskId });
|
|
7345
|
+
this.runs.set(runId, { runId, projectId, tenantId, abortController, promise: runPromise, terminal, leaseKey, leaseToken, taskId });
|
|
6945
7346
|
runPromise.catch(() => {
|
|
6946
7347
|
});
|
|
6947
7348
|
return runId;
|
|
6948
7349
|
}
|
|
6949
|
-
async abortRun(runId) {
|
|
7350
|
+
async abortRun(runId, tenantId) {
|
|
6950
7351
|
const ctx = this.runs.get(runId);
|
|
6951
|
-
if (!ctx) return false;
|
|
7352
|
+
if (!ctx || tenantId !== void 0 && ctx.tenantId !== tenantId) return false;
|
|
7353
|
+
if (ctx.terminal.claiming || ctx.terminal.persisted) return false;
|
|
6952
7354
|
ctx.abortController.abort();
|
|
6953
7355
|
const store = this.getEvalStore();
|
|
6954
|
-
|
|
6955
|
-
|
|
7356
|
+
ctx.terminal.claiming = true;
|
|
7357
|
+
try {
|
|
7358
|
+
let lastError;
|
|
7359
|
+
for (let attempt = 0; attempt < TERMINAL_PERSIST_ATTEMPTS; attempt++) {
|
|
7360
|
+
try {
|
|
7361
|
+
const persistedRun = await store.updateRunStatus(ctx.tenantId, runId, { status: "aborted", completedAt: /* @__PURE__ */ new Date() });
|
|
7362
|
+
if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
|
|
7363
|
+
lastError = void 0;
|
|
7364
|
+
break;
|
|
7365
|
+
} catch (error) {
|
|
7366
|
+
lastError = error;
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
if (lastError) throw lastError;
|
|
7370
|
+
ctx.terminal.persisted = true;
|
|
7371
|
+
this.eventEmitter.emit(`run:${runId}`, {
|
|
7372
|
+
type: "error",
|
|
7373
|
+
runId,
|
|
7374
|
+
data: { message: "Run aborted" }
|
|
7375
|
+
});
|
|
7376
|
+
this.releaseProjectLease(ctx.leaseKey, ctx.leaseToken);
|
|
7377
|
+
if (ctx.terminal.workerSettled) this.runs.delete(runId);
|
|
7378
|
+
return true;
|
|
7379
|
+
} finally {
|
|
7380
|
+
ctx.terminal.claiming = false;
|
|
7381
|
+
}
|
|
6956
7382
|
}
|
|
6957
7383
|
async waitForRun(runId) {
|
|
6958
7384
|
const ctx = this.runs.get(runId);
|
|
@@ -6960,12 +7386,25 @@ var EvalRunner = class {
|
|
|
6960
7386
|
const { report } = await ctx.promise;
|
|
6961
7387
|
return report;
|
|
6962
7388
|
}
|
|
6963
|
-
|
|
6964
|
-
|
|
7389
|
+
/**
|
|
7390
|
+
* Whether the run can still be controlled or observed as a non-terminal run.
|
|
7391
|
+
* Acknowledged aborts return false even while their worker is unwinding.
|
|
7392
|
+
*/
|
|
7393
|
+
isRunning(runId, tenantId) {
|
|
7394
|
+
const ctx = this.runs.get(runId);
|
|
7395
|
+
return ctx !== void 0 && !ctx.terminal.persisted && (tenantId === void 0 || ctx.tenantId === tenantId);
|
|
7396
|
+
}
|
|
7397
|
+
/** Whether the run's worker promise has not yet reached its finally block. */
|
|
7398
|
+
hasActiveExecution(runId, tenantId) {
|
|
7399
|
+
const ctx = this.runs.get(runId);
|
|
7400
|
+
return ctx !== void 0 && !ctx.terminal.workerSettled && (tenantId === void 0 || ctx.tenantId === tenantId);
|
|
6965
7401
|
}
|
|
6966
7402
|
getEvalStore() {
|
|
6967
7403
|
return getStoreLattice9("default", "eval").store;
|
|
6968
7404
|
}
|
|
7405
|
+
releaseProjectLease(leaseKey, leaseToken) {
|
|
7406
|
+
if (this.projectLeases.get(leaseKey) === leaseToken) this.projectLeases.delete(leaseKey);
|
|
7407
|
+
}
|
|
6969
7408
|
};
|
|
6970
7409
|
var evalRunner = new EvalRunner();
|
|
6971
7410
|
|
|
@@ -6980,6 +7419,18 @@ function getTenantId11(request) {
|
|
|
6980
7419
|
function getEvalStore() {
|
|
6981
7420
|
return getStoreLattice10("default", "eval").store;
|
|
6982
7421
|
}
|
|
7422
|
+
async function getOwnedSuite(store, tenantId, projectId, suiteId) {
|
|
7423
|
+
const project = await store.getProjectById(tenantId, projectId);
|
|
7424
|
+
if (!project) return null;
|
|
7425
|
+
const suite = await store.getSuiteById(tenantId, suiteId);
|
|
7426
|
+
return suite?.projectId === projectId ? suite : null;
|
|
7427
|
+
}
|
|
7428
|
+
async function getOwnedCase(store, tenantId, projectId, suiteId, caseId) {
|
|
7429
|
+
const suite = await getOwnedSuite(store, tenantId, projectId, suiteId);
|
|
7430
|
+
if (!suite) return null;
|
|
7431
|
+
const evalCase = await store.getCaseById(tenantId, caseId);
|
|
7432
|
+
return evalCase?.suiteId === suiteId ? evalCase : null;
|
|
7433
|
+
}
|
|
6983
7434
|
async function createProject(request, reply) {
|
|
6984
7435
|
try {
|
|
6985
7436
|
const tenantId = getTenantId11(request);
|
|
@@ -7135,6 +7586,7 @@ async function updateProject(request, reply) {
|
|
|
7135
7586
|
}
|
|
7136
7587
|
}
|
|
7137
7588
|
async function deleteProject(request, reply) {
|
|
7589
|
+
let releaseDeletion;
|
|
7138
7590
|
try {
|
|
7139
7591
|
const tenantId = getTenantId11(request);
|
|
7140
7592
|
const store = getEvalStore();
|
|
@@ -7143,6 +7595,23 @@ async function deleteProject(request, reply) {
|
|
|
7143
7595
|
if (!existing) {
|
|
7144
7596
|
return reply.status(404).send({ success: false, message: "Project not found" });
|
|
7145
7597
|
}
|
|
7598
|
+
releaseDeletion = evalRunner.reserveProjectDeletion(tenantId, pid) ?? void 0;
|
|
7599
|
+
if (!releaseDeletion) {
|
|
7600
|
+
return reply.status(409).send({
|
|
7601
|
+
success: false,
|
|
7602
|
+
message: "Cannot delete an evaluation project with an active run"
|
|
7603
|
+
});
|
|
7604
|
+
}
|
|
7605
|
+
const runs = await store.getRunsByTenant(tenantId, { projectId: pid });
|
|
7606
|
+
const hasActiveRun = runs.some(
|
|
7607
|
+
(run) => run.status === "running" || evalRunner.hasActiveExecution(run.id, tenantId)
|
|
7608
|
+
);
|
|
7609
|
+
if (hasActiveRun) {
|
|
7610
|
+
return reply.status(409).send({
|
|
7611
|
+
success: false,
|
|
7612
|
+
message: "Cannot delete an evaluation project with an active run"
|
|
7613
|
+
});
|
|
7614
|
+
}
|
|
7146
7615
|
const deleted = await store.deleteProject(tenantId, pid);
|
|
7147
7616
|
if (!deleted) {
|
|
7148
7617
|
return reply.status(500).send({ success: false, message: "Failed to delete project" });
|
|
@@ -7151,6 +7620,8 @@ async function deleteProject(request, reply) {
|
|
|
7151
7620
|
} catch (err) {
|
|
7152
7621
|
const message = err instanceof Error ? err.message : "Failed to delete project";
|
|
7153
7622
|
return reply.status(500).send({ success: false, message });
|
|
7623
|
+
} finally {
|
|
7624
|
+
releaseDeletion?.();
|
|
7154
7625
|
}
|
|
7155
7626
|
}
|
|
7156
7627
|
async function createSuite(request, reply) {
|
|
@@ -7179,8 +7650,8 @@ async function updateSuite(request, reply) {
|
|
|
7179
7650
|
try {
|
|
7180
7651
|
const tenantId = getTenantId11(request);
|
|
7181
7652
|
const store = getEvalStore();
|
|
7182
|
-
const { sid } = request.params;
|
|
7183
|
-
const existing = await store
|
|
7653
|
+
const { pid, sid } = request.params;
|
|
7654
|
+
const existing = await getOwnedSuite(store, tenantId, pid, sid);
|
|
7184
7655
|
if (!existing) {
|
|
7185
7656
|
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
7186
7657
|
}
|
|
@@ -7199,8 +7670,8 @@ async function deleteSuite(request, reply) {
|
|
|
7199
7670
|
try {
|
|
7200
7671
|
const tenantId = getTenantId11(request);
|
|
7201
7672
|
const store = getEvalStore();
|
|
7202
|
-
const { sid } = request.params;
|
|
7203
|
-
const existing = await store
|
|
7673
|
+
const { pid, sid } = request.params;
|
|
7674
|
+
const existing = await getOwnedSuite(store, tenantId, pid, sid);
|
|
7204
7675
|
if (!existing) {
|
|
7205
7676
|
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
7206
7677
|
}
|
|
@@ -7218,8 +7689,8 @@ async function createCase(request, reply) {
|
|
|
7218
7689
|
try {
|
|
7219
7690
|
const tenantId = getTenantId11(request);
|
|
7220
7691
|
const store = getEvalStore();
|
|
7221
|
-
const { sid } = request.params;
|
|
7222
|
-
const suite = await store
|
|
7692
|
+
const { pid, sid } = request.params;
|
|
7693
|
+
const suite = await getOwnedSuite(store, tenantId, pid, sid);
|
|
7223
7694
|
if (!suite) {
|
|
7224
7695
|
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
7225
7696
|
}
|
|
@@ -7248,7 +7719,11 @@ async function listCasesForSuite(request, reply) {
|
|
|
7248
7719
|
try {
|
|
7249
7720
|
const tenantId = getTenantId11(request);
|
|
7250
7721
|
const store = getEvalStore();
|
|
7251
|
-
const { sid } = request.params;
|
|
7722
|
+
const { pid, sid } = request.params;
|
|
7723
|
+
const suite = await getOwnedSuite(store, tenantId, pid, sid);
|
|
7724
|
+
if (!suite) {
|
|
7725
|
+
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
7726
|
+
}
|
|
7252
7727
|
const cases = await store.getCasesBySuite(tenantId, sid);
|
|
7253
7728
|
return {
|
|
7254
7729
|
success: true,
|
|
@@ -7264,8 +7739,8 @@ async function updateCase(request, reply) {
|
|
|
7264
7739
|
try {
|
|
7265
7740
|
const tenantId = getTenantId11(request);
|
|
7266
7741
|
const store = getEvalStore();
|
|
7267
|
-
const { cid } = request.params;
|
|
7268
|
-
const existing = await store
|
|
7742
|
+
const { pid, sid, cid } = request.params;
|
|
7743
|
+
const existing = await getOwnedCase(store, tenantId, pid, sid, cid);
|
|
7269
7744
|
if (!existing) {
|
|
7270
7745
|
return reply.status(404).send({ success: false, message: "Case not found" });
|
|
7271
7746
|
}
|
|
@@ -7284,8 +7759,8 @@ async function deleteCase(request, reply) {
|
|
|
7284
7759
|
try {
|
|
7285
7760
|
const tenantId = getTenantId11(request);
|
|
7286
7761
|
const store = getEvalStore();
|
|
7287
|
-
const { cid } = request.params;
|
|
7288
|
-
const existing = await store
|
|
7762
|
+
const { pid, sid, cid } = request.params;
|
|
7763
|
+
const existing = await getOwnedCase(store, tenantId, pid, sid, cid);
|
|
7289
7764
|
if (!existing) {
|
|
7290
7765
|
return reply.status(404).send({ success: false, message: "Case not found" });
|
|
7291
7766
|
}
|
|
@@ -7354,6 +7829,11 @@ function registerEvalRoutes(app2) {
|
|
|
7354
7829
|
}
|
|
7355
7830
|
});
|
|
7356
7831
|
app2.get("/api/eval/runs/:rid/stream", async (request, reply) => {
|
|
7832
|
+
const tenantId = getTenantId11(request);
|
|
7833
|
+
const { rid } = request.params;
|
|
7834
|
+
const store = getEvalStore();
|
|
7835
|
+
const ownedRun = await store.getRunById(tenantId, rid);
|
|
7836
|
+
if (!ownedRun) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
7357
7837
|
reply.hijack();
|
|
7358
7838
|
reply.raw.writeHead(200, {
|
|
7359
7839
|
"Content-Type": "text/event-stream",
|
|
@@ -7361,41 +7841,75 @@ function registerEvalRoutes(app2) {
|
|
|
7361
7841
|
Connection: "keep-alive",
|
|
7362
7842
|
"Access-Control-Allow-Origin": "*"
|
|
7363
7843
|
});
|
|
7364
|
-
const { rid } = request.params;
|
|
7365
7844
|
const emitter = evalRunner.getEventEmitter();
|
|
7366
7845
|
const eventKey = `run:${rid}`;
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7846
|
+
let settled = false;
|
|
7847
|
+
const cleanup = () => {
|
|
7848
|
+
emitter.off(eventKey, handler);
|
|
7849
|
+
request.raw.off("close", disconnect);
|
|
7850
|
+
};
|
|
7851
|
+
const disconnect = () => {
|
|
7852
|
+
if (settled) return;
|
|
7853
|
+
settled = true;
|
|
7854
|
+
cleanup();
|
|
7855
|
+
};
|
|
7856
|
+
const finish = (type, data) => {
|
|
7857
|
+
if (settled) return;
|
|
7858
|
+
settled = true;
|
|
7859
|
+
cleanup();
|
|
7860
|
+
reply.raw.write(`event: ${type}
|
|
7861
|
+
data: ${JSON.stringify(data)}
|
|
7374
7862
|
|
|
7375
7863
|
`);
|
|
7376
|
-
}
|
|
7377
7864
|
reply.raw.end();
|
|
7378
|
-
|
|
7379
|
-
}
|
|
7865
|
+
};
|
|
7380
7866
|
const handler = (event) => {
|
|
7381
|
-
|
|
7867
|
+
if (event.type === "completed" || event.type === "error") {
|
|
7868
|
+
finish(event.type, event.data);
|
|
7869
|
+
return;
|
|
7870
|
+
}
|
|
7871
|
+
if (!settled) reply.raw.write(`event: ${event.type}
|
|
7382
7872
|
data: ${JSON.stringify(event.data)}
|
|
7383
7873
|
|
|
7384
7874
|
`);
|
|
7385
|
-
if (event.type === "completed" || event.type === "error") {
|
|
7386
|
-
emitter.off(eventKey, handler);
|
|
7387
|
-
reply.raw.end();
|
|
7388
|
-
}
|
|
7389
7875
|
};
|
|
7390
7876
|
emitter.on(eventKey, handler);
|
|
7391
|
-
request.raw.on("close",
|
|
7392
|
-
|
|
7393
|
-
|
|
7877
|
+
request.raw.on("close", disconnect);
|
|
7878
|
+
let reconciledRun;
|
|
7879
|
+
try {
|
|
7880
|
+
reconciledRun = await store.getRunById(tenantId, rid);
|
|
7881
|
+
} catch (error) {
|
|
7882
|
+
finish("error", { message: error.message });
|
|
7883
|
+
return;
|
|
7884
|
+
}
|
|
7885
|
+
if (settled) return;
|
|
7886
|
+
if (!reconciledRun) {
|
|
7887
|
+
cleanup();
|
|
7888
|
+
reply.raw.end();
|
|
7889
|
+
return;
|
|
7890
|
+
}
|
|
7891
|
+
if (reconciledRun.status === "completed") {
|
|
7892
|
+
finish("completed", {
|
|
7893
|
+
passed: reconciledRun.passedCases,
|
|
7894
|
+
failed: reconciledRun.failedCases,
|
|
7895
|
+
avgScore: reconciledRun.avgScore
|
|
7896
|
+
});
|
|
7897
|
+
} else if (reconciledRun.status === "failed") {
|
|
7898
|
+
finish("error", { message: reconciledRun.error ?? "Evaluation failed" });
|
|
7899
|
+
} else if (reconciledRun.status === "aborted") {
|
|
7900
|
+
finish("error", { message: "Run aborted" });
|
|
7901
|
+
} else if (!evalRunner.isRunning(rid, tenantId)) {
|
|
7902
|
+
cleanup();
|
|
7903
|
+
reply.raw.end();
|
|
7904
|
+
}
|
|
7394
7905
|
});
|
|
7395
7906
|
app2.post("/api/eval/runs/:rid/abort", async (request, reply) => {
|
|
7396
7907
|
try {
|
|
7908
|
+
const tenantId = getTenantId11(request);
|
|
7397
7909
|
const { rid } = request.params;
|
|
7398
|
-
const
|
|
7910
|
+
const run = await getEvalStore().getRunById(tenantId, rid);
|
|
7911
|
+
if (!run) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
7912
|
+
const aborted = await evalRunner.abortRun(rid, tenantId);
|
|
7399
7913
|
if (!aborted) return reply.status(404).send({ success: false, message: "Run not found or not running" });
|
|
7400
7914
|
reply.send({ success: true, message: "Run aborted" });
|
|
7401
7915
|
} catch (err) {
|
|
@@ -7406,7 +7920,13 @@ data: ${JSON.stringify(event.data)}
|
|
|
7406
7920
|
try {
|
|
7407
7921
|
const tenantId = getTenantId11(request);
|
|
7408
7922
|
const { rid } = request.params;
|
|
7409
|
-
const
|
|
7923
|
+
const store = getEvalStore();
|
|
7924
|
+
const run = await store.getRunById(tenantId, rid);
|
|
7925
|
+
if (!run) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
7926
|
+
if (run.status === "running" || evalRunner.hasActiveExecution(rid, tenantId)) {
|
|
7927
|
+
return reply.status(409).send({ success: false, message: "Cannot delete an active evaluation run" });
|
|
7928
|
+
}
|
|
7929
|
+
const deleted = await store.deleteRun(tenantId, rid);
|
|
7410
7930
|
if (!deleted) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
7411
7931
|
reply.send({ success: true, message: "Run deleted" });
|
|
7412
7932
|
} catch (err) {
|
|
@@ -8002,7 +8522,7 @@ function registerConsoleAuthHook(app2, options) {
|
|
|
8002
8522
|
if (PUBLIC_ROUTES.some((route) => request.url === route)) return;
|
|
8003
8523
|
if (request.url.startsWith("/s/")) return;
|
|
8004
8524
|
const path4 = request.url.split("?")[0];
|
|
8005
|
-
if (
|
|
8525
|
+
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
8526
|
return;
|
|
8007
8527
|
}
|
|
8008
8528
|
await reply.status(401).send({
|
|
@@ -9002,12 +9522,12 @@ async function handleBridgeProxy(request, reply) {
|
|
|
9002
9522
|
};
|
|
9003
9523
|
const timeout = 3e5;
|
|
9004
9524
|
try {
|
|
9005
|
-
const response = await new Promise((
|
|
9525
|
+
const response = await new Promise((resolve3, reject) => {
|
|
9006
9526
|
const timer = setTimeout(() => {
|
|
9007
9527
|
pending.delete(requestId);
|
|
9008
9528
|
reject(new Error(`Bridge request timeout after ${timeout}ms`));
|
|
9009
9529
|
}, timeout);
|
|
9010
|
-
pending.set(requestId, { resolve:
|
|
9530
|
+
pending.set(requestId, { resolve: resolve3, reject, timer, agentId });
|
|
9011
9531
|
try {
|
|
9012
9532
|
if (conn.ws.readyState !== conn.ws.OPEN) {
|
|
9013
9533
|
pending.delete(requestId);
|
|
@@ -12794,7 +13314,7 @@ ${summary}`
|
|
|
12794
13314
|
console.log(
|
|
12795
13315
|
`\u5C06\u5728 ${delayMs}ms \u540E\u91CD\u8BD5\u4EFB\u52A1 (${nextRetryCount}/${maxRetries})`
|
|
12796
13316
|
);
|
|
12797
|
-
await new Promise((
|
|
13317
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
12798
13318
|
return handleAgentTask(taskRequest, nextRetryCount);
|
|
12799
13319
|
}
|
|
12800
13320
|
if (callback_event) {
|