@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.js
CHANGED
|
@@ -195,9 +195,9 @@ async function saveProjectFile(params) {
|
|
|
195
195
|
}
|
|
196
196
|
const targetPath = path3.join(resolvedTargetDir, filename);
|
|
197
197
|
await fs3.writeFile(targetPath, buffer);
|
|
198
|
-
const
|
|
198
|
+
const stat3 = await fs3.stat(targetPath);
|
|
199
199
|
const uri = path3.posix.join("/", relativeDirectory, filename);
|
|
200
|
-
return { uri, name: filename, size:
|
|
200
|
+
return { uri, name: filename, size: stat3.size };
|
|
201
201
|
}
|
|
202
202
|
var fs3, path3, import_core23, BINARY_EXTENSIONS, ProjectFileValidationError;
|
|
203
203
|
var init_workspace_file_service = __esm({
|
|
@@ -1212,7 +1212,7 @@ var init_WechatChannelAdapter = __esm({
|
|
|
1212
1212
|
installationId,
|
|
1213
1213
|
error: err instanceof Error ? err.message : String(err)
|
|
1214
1214
|
});
|
|
1215
|
-
await new Promise((
|
|
1215
|
+
await new Promise((resolve4) => setTimeout(resolve4, reconnectDelay));
|
|
1216
1216
|
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
|
|
1217
1217
|
}
|
|
1218
1218
|
scheduleNextPoll();
|
|
@@ -3350,8 +3350,8 @@ function resolveLocalA2AWorkingDirectory(options) {
|
|
|
3350
3350
|
}
|
|
3351
3351
|
function validateWorkingDirectory(cwd) {
|
|
3352
3352
|
try {
|
|
3353
|
-
const
|
|
3354
|
-
if (!
|
|
3353
|
+
const stat3 = (0, import_node_fs.statSync)(cwd);
|
|
3354
|
+
if (!stat3.isDirectory()) {
|
|
3355
3355
|
return `Working directory is not a directory: ${cwd}`;
|
|
3356
3356
|
}
|
|
3357
3357
|
return null;
|
|
@@ -4125,8 +4125,8 @@ async function pipeAsyncIterableToSse(raw, stream, options = {}) {
|
|
|
4125
4125
|
let closed = raw.destroyed || raw.writableEnded;
|
|
4126
4126
|
let returned = false;
|
|
4127
4127
|
let resolveClosed;
|
|
4128
|
-
const closedPromise = new Promise((
|
|
4129
|
-
resolveClosed =
|
|
4128
|
+
const closedPromise = new Promise((resolve4) => {
|
|
4129
|
+
resolveClosed = resolve4;
|
|
4130
4130
|
});
|
|
4131
4131
|
const returnConsumer = () => {
|
|
4132
4132
|
if (returned) return;
|
|
@@ -4176,14 +4176,14 @@ async function pipeAsyncIterableToSse(raw, stream, options = {}) {
|
|
|
4176
4176
|
async function writeSse(raw, value, closed) {
|
|
4177
4177
|
if (raw.destroyed || raw.writableEnded) return false;
|
|
4178
4178
|
if (raw.write(value)) return true;
|
|
4179
|
-
return new Promise((
|
|
4179
|
+
return new Promise((resolve4) => {
|
|
4180
4180
|
let settled = false;
|
|
4181
4181
|
const onDrain = () => finish(!raw.destroyed && !raw.writableEnded);
|
|
4182
4182
|
const finish = (writable) => {
|
|
4183
4183
|
if (settled) return;
|
|
4184
4184
|
settled = true;
|
|
4185
4185
|
raw.removeListener("drain", onDrain);
|
|
4186
|
-
|
|
4186
|
+
resolve4(writable);
|
|
4187
4187
|
};
|
|
4188
4188
|
raw.once("drain", onDrain);
|
|
4189
4189
|
void closed.then(() => finish(false));
|
|
@@ -6977,7 +6977,7 @@ var ExportImportService = class {
|
|
|
6977
6977
|
const files = /* @__PURE__ */ new Map();
|
|
6978
6978
|
let bundle;
|
|
6979
6979
|
const gunzip = zlib.createGunzip();
|
|
6980
|
-
await new Promise((
|
|
6980
|
+
await new Promise((resolve4, reject) => {
|
|
6981
6981
|
let settled = false;
|
|
6982
6982
|
let entryCount = 0;
|
|
6983
6983
|
let declaredTotalBytes = 0;
|
|
@@ -7097,7 +7097,7 @@ var ExportImportService = class {
|
|
|
7097
7097
|
parser.on("end", () => {
|
|
7098
7098
|
if (settled) return;
|
|
7099
7099
|
settled = true;
|
|
7100
|
-
|
|
7100
|
+
resolve4();
|
|
7101
7101
|
});
|
|
7102
7102
|
gunzip.pipe(parser);
|
|
7103
7103
|
gunzip.end(buffer);
|
|
@@ -7782,6 +7782,20 @@ var dataQuerySchema = {
|
|
|
7782
7782
|
};
|
|
7783
7783
|
|
|
7784
7784
|
// src/schemas/index.ts
|
|
7785
|
+
var downloadFolderSchema = {
|
|
7786
|
+
description: "Download a workspace project folder as a ZIP archive",
|
|
7787
|
+
tags: ["Workspace"],
|
|
7788
|
+
querystring: {
|
|
7789
|
+
type: "object",
|
|
7790
|
+
properties: {
|
|
7791
|
+
path: { type: "string", minLength: 1 },
|
|
7792
|
+
assistantId: { type: "string", minLength: 1 },
|
|
7793
|
+
tenantId: { type: "string", minLength: 1 }
|
|
7794
|
+
},
|
|
7795
|
+
required: ["path"],
|
|
7796
|
+
additionalProperties: false
|
|
7797
|
+
}
|
|
7798
|
+
};
|
|
7785
7799
|
var getAllMemoryItemsSchema = {
|
|
7786
7800
|
description: "Get all memory items for an assistant thread",
|
|
7787
7801
|
tags: ["Memory"],
|
|
@@ -8380,6 +8394,7 @@ function registerSandboxProxyRoutes(app2) {
|
|
|
8380
8394
|
// src/controllers/workspace.ts
|
|
8381
8395
|
var fs4 = __toESM(require("fs/promises"));
|
|
8382
8396
|
var path4 = __toESM(require("path"));
|
|
8397
|
+
var import_fs = require("fs");
|
|
8383
8398
|
var os2 = __toESM(require("os"));
|
|
8384
8399
|
var import_core24 = require("@axiom-lattice/core");
|
|
8385
8400
|
var import_core25 = require("@axiom-lattice/core");
|
|
@@ -8388,6 +8403,83 @@ var import_uuid3 = require("uuid");
|
|
|
8388
8403
|
init_mime();
|
|
8389
8404
|
init_workspace_file_service();
|
|
8390
8405
|
init_path_containment();
|
|
8406
|
+
var MAX_ZIP_ENTRIES = 1e4;
|
|
8407
|
+
var MAX_ZIP_FILE_SIZE = 100 * 1024 * 1024;
|
|
8408
|
+
var MAX_ZIP_TOTAL_SIZE = 512 * 1024 * 1024;
|
|
8409
|
+
var FolderDownloadError = class extends Error {
|
|
8410
|
+
constructor(statusCode, code, message) {
|
|
8411
|
+
super(message);
|
|
8412
|
+
this.statusCode = statusCode;
|
|
8413
|
+
this.code = code;
|
|
8414
|
+
this.name = "FolderDownloadError";
|
|
8415
|
+
}
|
|
8416
|
+
};
|
|
8417
|
+
function zipCrc32(data) {
|
|
8418
|
+
let crc = 4294967295;
|
|
8419
|
+
for (const byte of data) {
|
|
8420
|
+
crc ^= byte;
|
|
8421
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
8422
|
+
crc = crc >>> 1 ^ 3988292384 & -(crc & 1);
|
|
8423
|
+
}
|
|
8424
|
+
}
|
|
8425
|
+
return (crc ^ 4294967295) >>> 0;
|
|
8426
|
+
}
|
|
8427
|
+
function createZip(entries) {
|
|
8428
|
+
const localParts = [];
|
|
8429
|
+
const centralParts = [];
|
|
8430
|
+
let offset = 0;
|
|
8431
|
+
for (const entry of entries) {
|
|
8432
|
+
const name = Buffer.from(entry.name, "utf8");
|
|
8433
|
+
const data = entry.data;
|
|
8434
|
+
const crc = zipCrc32(data);
|
|
8435
|
+
const local = Buffer.alloc(30 + name.length + data.length);
|
|
8436
|
+
local.writeUInt32LE(67324752, 0);
|
|
8437
|
+
local.writeUInt16LE(20, 4);
|
|
8438
|
+
local.writeUInt16LE(0, 6);
|
|
8439
|
+
local.writeUInt16LE(2048, 8);
|
|
8440
|
+
local.writeUInt16LE(0, 10);
|
|
8441
|
+
local.writeUInt16LE(0, 12);
|
|
8442
|
+
local.writeUInt32LE(crc, 14);
|
|
8443
|
+
local.writeUInt32LE(data.length, 18);
|
|
8444
|
+
local.writeUInt32LE(data.length, 22);
|
|
8445
|
+
local.writeUInt16LE(name.length, 26);
|
|
8446
|
+
local.writeUInt16LE(0, 28);
|
|
8447
|
+
name.copy(local, 30);
|
|
8448
|
+
data.copy(local, 30 + name.length);
|
|
8449
|
+
localParts.push(local);
|
|
8450
|
+
const central = Buffer.alloc(46 + name.length);
|
|
8451
|
+
central.writeUInt32LE(33639248, 0);
|
|
8452
|
+
central.writeUInt16LE(20, 4);
|
|
8453
|
+
central.writeUInt16LE(20, 6);
|
|
8454
|
+
central.writeUInt16LE(0, 8);
|
|
8455
|
+
central.writeUInt16LE(2048, 10);
|
|
8456
|
+
central.writeUInt16LE(0, 12);
|
|
8457
|
+
central.writeUInt16LE(0, 14);
|
|
8458
|
+
central.writeUInt32LE(crc, 16);
|
|
8459
|
+
central.writeUInt32LE(data.length, 20);
|
|
8460
|
+
central.writeUInt32LE(data.length, 24);
|
|
8461
|
+
central.writeUInt16LE(name.length, 28);
|
|
8462
|
+
central.writeUInt16LE(0, 30);
|
|
8463
|
+
central.writeUInt16LE(0, 32);
|
|
8464
|
+
central.writeUInt16LE(0, 34);
|
|
8465
|
+
central.writeUInt16LE(entry.directory ? 16 : 0, 36);
|
|
8466
|
+
central.writeUInt32LE(entry.directory ? 16 : 0, 38);
|
|
8467
|
+
central.writeUInt32LE(offset, 42);
|
|
8468
|
+
name.copy(central, 46);
|
|
8469
|
+
centralParts.push(central);
|
|
8470
|
+
offset += local.length;
|
|
8471
|
+
}
|
|
8472
|
+
const centralDirectory = Buffer.concat(centralParts);
|
|
8473
|
+
const end = Buffer.alloc(22);
|
|
8474
|
+
end.writeUInt32LE(101010256, 0);
|
|
8475
|
+
end.writeUInt16LE(0, 4);
|
|
8476
|
+
end.writeUInt16LE(0, 6);
|
|
8477
|
+
end.writeUInt16LE(entries.length, 8);
|
|
8478
|
+
end.writeUInt16LE(entries.length, 10);
|
|
8479
|
+
end.writeUInt32LE(centralDirectory.length, 12);
|
|
8480
|
+
end.writeUInt32LE(offset, 16);
|
|
8481
|
+
return Buffer.concat([...localParts, centralDirectory, end]);
|
|
8482
|
+
}
|
|
8391
8483
|
var WorkspaceController = class {
|
|
8392
8484
|
constructor() {
|
|
8393
8485
|
this.workspaceStore = (0, import_core24.getStoreLattice)("default", "workspace").store;
|
|
@@ -8653,6 +8745,259 @@ var WorkspaceController = class {
|
|
|
8653
8745
|
return reply.status(502).send({ success: false, error: `Download proxy error: ${message}` });
|
|
8654
8746
|
}
|
|
8655
8747
|
}
|
|
8748
|
+
async downloadFolder(request, reply) {
|
|
8749
|
+
const tenantId = this.getTenantId(request);
|
|
8750
|
+
const { workspaceId, projectId } = request.params;
|
|
8751
|
+
const folderPath = request.query.path;
|
|
8752
|
+
const assistantId = request.query.assistantId;
|
|
8753
|
+
if (!folderPath || folderPath.includes("\\") || folderPath.split("/").includes("..")) {
|
|
8754
|
+
return reply.status(400).send({
|
|
8755
|
+
success: false,
|
|
8756
|
+
error: "Invalid folder path",
|
|
8757
|
+
code: "INVALID_PATH"
|
|
8758
|
+
});
|
|
8759
|
+
}
|
|
8760
|
+
try {
|
|
8761
|
+
const storedWorkspace = await this.workspaceStore.getWorkspaceById(tenantId, workspaceId);
|
|
8762
|
+
if (!storedWorkspace) {
|
|
8763
|
+
throw new FolderDownloadError(404, "WORKSPACE_NOT_FOUND", "Workspace not found");
|
|
8764
|
+
}
|
|
8765
|
+
const project = await this.projectStore.getProjectById(tenantId, projectId);
|
|
8766
|
+
if (!project || project.workspaceId !== workspaceId) {
|
|
8767
|
+
throw new FolderDownloadError(404, "PROJECT_NOT_FOUND", "Project not found");
|
|
8768
|
+
}
|
|
8769
|
+
const { backend, workspace } = await this.getBackend(
|
|
8770
|
+
tenantId,
|
|
8771
|
+
workspaceId,
|
|
8772
|
+
projectId,
|
|
8773
|
+
assistantId,
|
|
8774
|
+
folderPath
|
|
8775
|
+
);
|
|
8776
|
+
if (!backend.lsInfo || workspace.storageType === "sandbox" && !backend.readBinary) {
|
|
8777
|
+
throw new FolderDownloadError(502, "BACKEND_UNSUPPORTED", "Backend does not support folder downloads");
|
|
8778
|
+
}
|
|
8779
|
+
let listEntries2;
|
|
8780
|
+
if (workspace.storageType !== "sandbox") {
|
|
8781
|
+
const root = filesystemRootDirectory(tenantId, workspaceId, projectId);
|
|
8782
|
+
const target = path4.resolve(root, `.${folderPath.startsWith("/") ? folderPath : `/${folderPath}`}`);
|
|
8783
|
+
const contained = await resolvePathWithinRoot(root, target).catch((error) => {
|
|
8784
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
8785
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
8786
|
+
}
|
|
8787
|
+
throw error;
|
|
8788
|
+
});
|
|
8789
|
+
let stat3;
|
|
8790
|
+
try {
|
|
8791
|
+
stat3 = await fs4.stat(contained.targetPath);
|
|
8792
|
+
} catch (error) {
|
|
8793
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
8794
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
8795
|
+
}
|
|
8796
|
+
throw error;
|
|
8797
|
+
}
|
|
8798
|
+
if (!stat3.isDirectory()) {
|
|
8799
|
+
throw new FolderDownloadError(400, "NOT_A_FOLDER", "Path is not a folder");
|
|
8800
|
+
}
|
|
8801
|
+
} else if (folderPath) {
|
|
8802
|
+
const sandboxManager = (0, import_core26.getSandBoxManager)();
|
|
8803
|
+
const volumeConfig = {
|
|
8804
|
+
assistant_id: assistantId || "",
|
|
8805
|
+
thread_id: "",
|
|
8806
|
+
tenantId,
|
|
8807
|
+
workspaceId,
|
|
8808
|
+
projectId
|
|
8809
|
+
};
|
|
8810
|
+
const volume = await sandboxManager.getVolumeBackendForPath(volumeConfig, folderPath);
|
|
8811
|
+
if (!volume) {
|
|
8812
|
+
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
8813
|
+
try {
|
|
8814
|
+
await sandbox.file.listPath(folderPath, { recursive: false });
|
|
8815
|
+
} catch (error) {
|
|
8816
|
+
const code = error instanceof Error ? error.code : void 0;
|
|
8817
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8818
|
+
if (code === "ENOENT" || /not found|no such file|enoent/i.test(message)) {
|
|
8819
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
8820
|
+
}
|
|
8821
|
+
throw error;
|
|
8822
|
+
}
|
|
8823
|
+
listEntries2 = async (directoryPath) => {
|
|
8824
|
+
const result = await sandbox.file.listPath(directoryPath, { recursive: false });
|
|
8825
|
+
return result.files;
|
|
8826
|
+
};
|
|
8827
|
+
} else {
|
|
8828
|
+
listEntries2 = async (directoryPath) => backend.lsInfo(directoryPath);
|
|
8829
|
+
}
|
|
8830
|
+
}
|
|
8831
|
+
if (!listEntries2) {
|
|
8832
|
+
listEntries2 = async (directoryPath) => backend.lsInfo(directoryPath);
|
|
8833
|
+
}
|
|
8834
|
+
const folderName = path4.basename(folderPath.replace(/\/$/, "")) || "folder";
|
|
8835
|
+
const prefix = `${folderName}/`;
|
|
8836
|
+
const entries = [{ name: prefix, data: Buffer.alloc(0), directory: true }];
|
|
8837
|
+
let totalSize = 0;
|
|
8838
|
+
const filesystemRoot = workspace.storageType === "sandbox" ? void 0 : filesystemRootDirectory(tenantId, workspaceId, projectId);
|
|
8839
|
+
const addFile = (archivePath, data) => {
|
|
8840
|
+
if (data.length > MAX_ZIP_FILE_SIZE || totalSize + data.length > MAX_ZIP_TOTAL_SIZE) {
|
|
8841
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
8842
|
+
}
|
|
8843
|
+
totalSize += data.length;
|
|
8844
|
+
entries.push({ name: archivePath, data, directory: false });
|
|
8845
|
+
};
|
|
8846
|
+
const collectFilesystem = async (directoryPath, archivePath) => {
|
|
8847
|
+
let children;
|
|
8848
|
+
try {
|
|
8849
|
+
children = await fs4.readdir(directoryPath, { withFileTypes: true });
|
|
8850
|
+
} catch (error) {
|
|
8851
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
8852
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
8853
|
+
}
|
|
8854
|
+
throw error;
|
|
8855
|
+
}
|
|
8856
|
+
for (const child of children) {
|
|
8857
|
+
if (entries.length >= MAX_ZIP_ENTRIES) {
|
|
8858
|
+
throw new FolderDownloadError(400, "ENTRY_LIMIT", "Folder contains too many entries");
|
|
8859
|
+
}
|
|
8860
|
+
const childPath = path4.join(directoryPath, child.name);
|
|
8861
|
+
const archiveChildPath = `${archivePath}${child.name}`;
|
|
8862
|
+
let resolvedChild;
|
|
8863
|
+
try {
|
|
8864
|
+
resolvedChild = await resolvePathWithinRoot(filesystemRoot, childPath);
|
|
8865
|
+
} catch (error) {
|
|
8866
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
8867
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
8868
|
+
}
|
|
8869
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains a path outside the project");
|
|
8870
|
+
}
|
|
8871
|
+
let childStat;
|
|
8872
|
+
try {
|
|
8873
|
+
childStat = await fs4.lstat(resolvedChild.targetPath);
|
|
8874
|
+
} catch (error) {
|
|
8875
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
8876
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
8877
|
+
}
|
|
8878
|
+
throw error;
|
|
8879
|
+
}
|
|
8880
|
+
if (childStat.isSymbolicLink() || child.isSymbolicLink()) {
|
|
8881
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Symbolic links are not allowed in folder downloads");
|
|
8882
|
+
}
|
|
8883
|
+
if (child.isDirectory()) {
|
|
8884
|
+
entries.push({ name: `${archiveChildPath}/`, data: Buffer.alloc(0), directory: true });
|
|
8885
|
+
await collectFilesystem(resolvedChild.targetPath, `${archiveChildPath}/`);
|
|
8886
|
+
} else if (child.isFile()) {
|
|
8887
|
+
if (childStat.size > MAX_ZIP_FILE_SIZE || totalSize + childStat.size > MAX_ZIP_TOTAL_SIZE) {
|
|
8888
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
8889
|
+
}
|
|
8890
|
+
const flags = import_fs.constants.O_NOFOLLOW === void 0 ? import_fs.constants.O_RDONLY : import_fs.constants.O_RDONLY | import_fs.constants.O_NOFOLLOW;
|
|
8891
|
+
let handle3;
|
|
8892
|
+
try {
|
|
8893
|
+
handle3 = await fs4.open(resolvedChild.targetPath, flags);
|
|
8894
|
+
} catch (error) {
|
|
8895
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
8896
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
8897
|
+
}
|
|
8898
|
+
throw error;
|
|
8899
|
+
}
|
|
8900
|
+
try {
|
|
8901
|
+
const openedStat = await handle3.stat();
|
|
8902
|
+
if (!openedStat.isFile() || openedStat.size > MAX_ZIP_FILE_SIZE || totalSize + openedStat.size > MAX_ZIP_TOTAL_SIZE) {
|
|
8903
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
8904
|
+
}
|
|
8905
|
+
const readLimit = MAX_ZIP_FILE_SIZE + 1;
|
|
8906
|
+
const dataBuffer = Buffer.alloc(Math.min(openedStat.size + 1, readLimit));
|
|
8907
|
+
let bytesRead = 0;
|
|
8908
|
+
while (bytesRead < dataBuffer.length) {
|
|
8909
|
+
const result = await handle3.read(
|
|
8910
|
+
dataBuffer,
|
|
8911
|
+
bytesRead,
|
|
8912
|
+
dataBuffer.length - bytesRead,
|
|
8913
|
+
bytesRead
|
|
8914
|
+
);
|
|
8915
|
+
if (result.bytesRead === 0) break;
|
|
8916
|
+
bytesRead += result.bytesRead;
|
|
8917
|
+
}
|
|
8918
|
+
const data = dataBuffer.subarray(0, bytesRead);
|
|
8919
|
+
const finalStat = await handle3.stat();
|
|
8920
|
+
if (finalStat.size > MAX_ZIP_FILE_SIZE || totalSize + finalStat.size > MAX_ZIP_TOTAL_SIZE || finalStat.size > data.length) {
|
|
8921
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
8922
|
+
}
|
|
8923
|
+
addFile(archiveChildPath, data);
|
|
8924
|
+
} finally {
|
|
8925
|
+
await handle3.close();
|
|
8926
|
+
}
|
|
8927
|
+
}
|
|
8928
|
+
}
|
|
8929
|
+
};
|
|
8930
|
+
const collect = async (currentPath, archivePath) => {
|
|
8931
|
+
let children;
|
|
8932
|
+
try {
|
|
8933
|
+
children = await listEntries2(currentPath);
|
|
8934
|
+
} catch (error) {
|
|
8935
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8936
|
+
if (/not found|enoent|no such file/i.test(message)) {
|
|
8937
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
8938
|
+
}
|
|
8939
|
+
throw error;
|
|
8940
|
+
}
|
|
8941
|
+
for (const child of children) {
|
|
8942
|
+
if (entries.length >= MAX_ZIP_ENTRIES) {
|
|
8943
|
+
throw new FolderDownloadError(400, "ENTRY_LIMIT", "Folder contains too many entries");
|
|
8944
|
+
}
|
|
8945
|
+
const childPath = child.path;
|
|
8946
|
+
if (childPath.includes("\\") || childPath.split("/").includes("..") || !childPath.startsWith(folderPath.endsWith("/") ? folderPath : `${folderPath}/`)) {
|
|
8947
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains an unsafe child path");
|
|
8948
|
+
}
|
|
8949
|
+
if (workspace.storageType !== "sandbox") {
|
|
8950
|
+
const root = filesystemRootDirectory(tenantId, workspaceId, projectId);
|
|
8951
|
+
const virtualChildPath = childPath.startsWith("/") ? childPath : `/${childPath}`;
|
|
8952
|
+
const target = path4.resolve(root, `.${virtualChildPath}`);
|
|
8953
|
+
try {
|
|
8954
|
+
await resolvePathWithinRoot(root, target);
|
|
8955
|
+
const childStat = await fs4.lstat(target);
|
|
8956
|
+
if (childStat.isSymbolicLink()) {
|
|
8957
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Symbolic links are not allowed in folder downloads");
|
|
8958
|
+
}
|
|
8959
|
+
} catch (error) {
|
|
8960
|
+
if (error instanceof FolderDownloadError) throw error;
|
|
8961
|
+
if (error instanceof Error && error.code === "ENOENT") {
|
|
8962
|
+
throw new FolderDownloadError(404, "FOLDER_NOT_FOUND", "Folder not found");
|
|
8963
|
+
}
|
|
8964
|
+
throw new FolderDownloadError(400, "INVALID_PATH", "Folder contains a path outside the project");
|
|
8965
|
+
}
|
|
8966
|
+
}
|
|
8967
|
+
const childName = path4.basename(childPath.replace(/\/$/, ""));
|
|
8968
|
+
const childArchivePath = `${archivePath}${childName}`;
|
|
8969
|
+
if (child.is_dir) {
|
|
8970
|
+
entries.push({ name: `${childArchivePath}/`, data: Buffer.alloc(0), directory: true });
|
|
8971
|
+
await collect(childPath, `${childArchivePath}/`);
|
|
8972
|
+
} else {
|
|
8973
|
+
const size = child.size ?? 0;
|
|
8974
|
+
if (size > MAX_ZIP_FILE_SIZE || totalSize + size > MAX_ZIP_TOTAL_SIZE) {
|
|
8975
|
+
throw new FolderDownloadError(400, "SIZE_LIMIT", "Folder exceeds download size limits");
|
|
8976
|
+
}
|
|
8977
|
+
const data = await backend.readBinary(childPath);
|
|
8978
|
+
addFile(childArchivePath, data);
|
|
8979
|
+
}
|
|
8980
|
+
}
|
|
8981
|
+
};
|
|
8982
|
+
if (workspace.storageType === "sandbox") {
|
|
8983
|
+
await collect(folderPath, prefix);
|
|
8984
|
+
} else {
|
|
8985
|
+
const root = filesystemRoot;
|
|
8986
|
+
const target = path4.resolve(root, `.${folderPath.startsWith("/") ? folderPath : `/${folderPath}`}`);
|
|
8987
|
+
await collectFilesystem(target, prefix);
|
|
8988
|
+
}
|
|
8989
|
+
const zip = createZip(entries);
|
|
8990
|
+
const downloadName = `${folderName}.zip`;
|
|
8991
|
+
return reply.status(200).type("application/zip").header("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(downloadName)}`).send(zip);
|
|
8992
|
+
} catch (error) {
|
|
8993
|
+
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));
|
|
8994
|
+
return reply.status(folderError.statusCode).send({
|
|
8995
|
+
success: false,
|
|
8996
|
+
error: folderError.message,
|
|
8997
|
+
code: folderError.code
|
|
8998
|
+
});
|
|
8999
|
+
}
|
|
9000
|
+
}
|
|
8656
9001
|
/**
|
|
8657
9002
|
* View a file inline in the browser (not download).
|
|
8658
9003
|
* Returns file content with inline Content-Disposition for preview.
|
|
@@ -8945,6 +9290,11 @@ function registerWorkspaceRoutes(app2) {
|
|
|
8945
9290
|
"/api/workspaces/:workspaceId/projects/:projectId/downloadfile",
|
|
8946
9291
|
controller.downloadFile.bind(controller)
|
|
8947
9292
|
);
|
|
9293
|
+
app2.get(
|
|
9294
|
+
"/api/workspaces/:workspaceId/projects/:projectId/downloadfolder",
|
|
9295
|
+
{ schema: downloadFolderSchema },
|
|
9296
|
+
controller.downloadFolder.bind(controller)
|
|
9297
|
+
);
|
|
8948
9298
|
app2.get(
|
|
8949
9299
|
"/api/workspaces/:workspaceId/projects/:projectId/viewfile",
|
|
8950
9300
|
controller.viewFile.bind(controller)
|
|
@@ -9169,7 +9519,7 @@ async function testDatabaseConnection(request, reply) {
|
|
|
9169
9519
|
await db.connect();
|
|
9170
9520
|
await db.listTables();
|
|
9171
9521
|
const latency = Date.now() - startTime;
|
|
9172
|
-
await new Promise((
|
|
9522
|
+
await new Promise((resolve4) => setTimeout(resolve4, 100));
|
|
9173
9523
|
await db.disconnect();
|
|
9174
9524
|
await import_core27.sqlDatabaseManager.removeDatabase(tenantId, testKey);
|
|
9175
9525
|
return {
|
|
@@ -9986,6 +10336,7 @@ var import_uuid5 = require("uuid");
|
|
|
9986
10336
|
var import_events = require("events");
|
|
9987
10337
|
var import_core31 = require("@axiom-lattice/core");
|
|
9988
10338
|
var import_uuid4 = require("uuid");
|
|
10339
|
+
var TERMINAL_PERSIST_ATTEMPTS = 2;
|
|
9989
10340
|
function mapLogs(logs) {
|
|
9990
10341
|
return logs.map((l) => ({
|
|
9991
10342
|
timestamp: l.ts,
|
|
@@ -9997,18 +10348,42 @@ function mapLogs(logs) {
|
|
|
9997
10348
|
var EvalRunner = class {
|
|
9998
10349
|
constructor() {
|
|
9999
10350
|
this.runs = /* @__PURE__ */ new Map();
|
|
10351
|
+
this.projectLeases = /* @__PURE__ */ new Map();
|
|
10000
10352
|
this.eventEmitter = new import_events.EventEmitter();
|
|
10001
10353
|
this.processStartedAt = /* @__PURE__ */ new Date();
|
|
10002
10354
|
}
|
|
10003
10355
|
getEventEmitter() {
|
|
10004
10356
|
return this.eventEmitter;
|
|
10005
10357
|
}
|
|
10358
|
+
reserveProjectDeletion(tenantId, projectId) {
|
|
10359
|
+
const leaseKey = `${tenantId}:${projectId}`;
|
|
10360
|
+
if (this.projectLeases.has(leaseKey) || [...this.runs.values()].some(
|
|
10361
|
+
(ctx) => ctx.tenantId === tenantId && ctx.projectId === projectId && !ctx.terminal.workerSettled
|
|
10362
|
+
)) return null;
|
|
10363
|
+
const leaseToken = Symbol(`delete:${leaseKey}`);
|
|
10364
|
+
this.projectLeases.set(leaseKey, leaseToken);
|
|
10365
|
+
return () => this.releaseProjectLease(leaseKey, leaseToken);
|
|
10366
|
+
}
|
|
10006
10367
|
async startRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId) {
|
|
10368
|
+
const leaseKey = `${tenantId}:${projectId}`;
|
|
10369
|
+
if (this.projectLeases.has(leaseKey)) {
|
|
10370
|
+
throw new Error("A run is already in progress for this project");
|
|
10371
|
+
}
|
|
10372
|
+
const leaseToken = Symbol(leaseKey);
|
|
10373
|
+
this.projectLeases.set(leaseKey, leaseToken);
|
|
10374
|
+
try {
|
|
10375
|
+
return await this.startLeasedRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId, leaseKey, leaseToken);
|
|
10376
|
+
} catch (error) {
|
|
10377
|
+
this.releaseProjectLease(leaseKey, leaseToken);
|
|
10378
|
+
throw error;
|
|
10379
|
+
}
|
|
10380
|
+
}
|
|
10381
|
+
async startLeasedRun(tenantId, projectId, suiteIds, caseIds, runConfig, taskId, leaseKey, leaseToken) {
|
|
10007
10382
|
const store2 = this.getEvalStore();
|
|
10008
10383
|
const project = await store2.getProjectById(tenantId, projectId);
|
|
10009
10384
|
if (!project) throw new Error("Project not found");
|
|
10010
10385
|
for (const ctx of this.runs.values()) {
|
|
10011
|
-
if (ctx.projectId === projectId) {
|
|
10386
|
+
if (ctx.projectId === projectId && !ctx.terminal.persisted) {
|
|
10012
10387
|
throw new Error("A run is already in progress for this project");
|
|
10013
10388
|
}
|
|
10014
10389
|
}
|
|
@@ -10112,14 +10487,35 @@ var EvalRunner = class {
|
|
|
10112
10487
|
};
|
|
10113
10488
|
const abortController = new AbortController();
|
|
10114
10489
|
const stats = { passed: 0, failed: 0, interrupted: 0, totalScore: 0 };
|
|
10490
|
+
const terminal = { claiming: false, persisted: false, workerSettled: false };
|
|
10491
|
+
const terminalUnavailable = () => terminal.claiming || terminal.persisted;
|
|
10492
|
+
const persistTerminal = async (update, event) => {
|
|
10493
|
+
if (terminalUnavailable()) return false;
|
|
10494
|
+
terminal.claiming = true;
|
|
10495
|
+
try {
|
|
10496
|
+
let lastError;
|
|
10497
|
+
for (let attempt = 0; attempt < TERMINAL_PERSIST_ATTEMPTS; attempt++) {
|
|
10498
|
+
try {
|
|
10499
|
+
const persistedRun = await store2.updateRunStatus(tenantId, runId, update);
|
|
10500
|
+
if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
|
|
10501
|
+
lastError = void 0;
|
|
10502
|
+
break;
|
|
10503
|
+
} catch (error) {
|
|
10504
|
+
lastError = error;
|
|
10505
|
+
}
|
|
10506
|
+
}
|
|
10507
|
+
if (lastError) throw lastError;
|
|
10508
|
+
terminal.persisted = true;
|
|
10509
|
+
this.eventEmitter.emit(`run:${runId}`, event);
|
|
10510
|
+
return true;
|
|
10511
|
+
} finally {
|
|
10512
|
+
terminal.claiming = false;
|
|
10513
|
+
}
|
|
10514
|
+
};
|
|
10115
10515
|
const onCaseComplete = async (result, suiteName) => {
|
|
10516
|
+
if (terminalUnavailable()) return;
|
|
10116
10517
|
const passed = result.result?.pass ?? false;
|
|
10117
10518
|
const score = result.result?.final_score ?? 0;
|
|
10118
|
-
if (passed) stats.passed++;
|
|
10119
|
-
else stats.failed++;
|
|
10120
|
-
stats.totalScore += score;
|
|
10121
|
-
if (result.interrupted) stats.interrupted++;
|
|
10122
|
-
const completedCount = stats.passed + stats.failed;
|
|
10123
10519
|
await store2.createRunResult(tenantId, runId, (0, import_uuid4.v4)(), {
|
|
10124
10520
|
suiteName,
|
|
10125
10521
|
caseId: result.caseId,
|
|
@@ -10137,13 +10533,22 @@ var EvalRunner = class {
|
|
|
10137
10533
|
error: result.error,
|
|
10138
10534
|
interrupted: result.interrupted
|
|
10139
10535
|
});
|
|
10536
|
+
if (terminalUnavailable()) return;
|
|
10537
|
+
if (passed) stats.passed++;
|
|
10538
|
+
else stats.failed++;
|
|
10539
|
+
stats.totalScore += score;
|
|
10540
|
+
if (result.interrupted) stats.interrupted++;
|
|
10541
|
+
const completedCount = stats.passed + stats.failed;
|
|
10542
|
+
if (terminalUnavailable()) return;
|
|
10140
10543
|
const avgScore = completedCount > 0 ? stats.totalScore / completedCount : 0;
|
|
10141
|
-
await store2.updateRunStatus(tenantId, runId, {
|
|
10544
|
+
const persistedRun = await store2.updateRunStatus(tenantId, runId, {
|
|
10142
10545
|
passedCases: stats.passed,
|
|
10143
10546
|
failedCases: stats.failed,
|
|
10144
10547
|
interruptedCases: stats.interrupted,
|
|
10145
10548
|
avgScore
|
|
10146
10549
|
});
|
|
10550
|
+
if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
|
|
10551
|
+
if (terminalUnavailable()) return;
|
|
10147
10552
|
this.eventEmitter.emit(`run:${runId}`, {
|
|
10148
10553
|
type: "progress",
|
|
10149
10554
|
runId,
|
|
@@ -10155,65 +10560,86 @@ var EvalRunner = class {
|
|
|
10155
10560
|
const evalProject = new import_core31.LatticeEvalProject(projectConfig, onCaseComplete);
|
|
10156
10561
|
const calibration = await evalProject.calibrateJudge();
|
|
10157
10562
|
if (!calibration.ok && !calibration.bypassed) {
|
|
10158
|
-
await store2.updateRunStatus(tenantId, runId, {
|
|
10159
|
-
status: "failed",
|
|
10160
|
-
error: `Judge calibration failed: ${calibration.reason}`,
|
|
10161
|
-
completedAt: /* @__PURE__ */ new Date()
|
|
10162
|
-
});
|
|
10163
10563
|
throw new Error(`Judge calibration failed: ${calibration.reason}`);
|
|
10164
10564
|
}
|
|
10165
10565
|
if (calibration.bypassed) {
|
|
10166
10566
|
console.warn(`[eval-runner] Judge calibration bypassed \u2014 judge may be unreliable: ${calibration.reason}`);
|
|
10167
10567
|
}
|
|
10168
10568
|
const { report } = await evalProject.runAllSuitesBatch(concurrency, abortController.signal);
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
data: { passed: stats.passed, failed: stats.failed, avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0 }
|
|
10183
|
-
});
|
|
10184
|
-
}
|
|
10569
|
+
const completedCount = stats.passed + stats.failed;
|
|
10570
|
+
await persistTerminal({
|
|
10571
|
+
status: "completed",
|
|
10572
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
10573
|
+
passedCases: stats.passed,
|
|
10574
|
+
failedCases: stats.failed,
|
|
10575
|
+
interruptedCases: stats.interrupted,
|
|
10576
|
+
avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0
|
|
10577
|
+
}, {
|
|
10578
|
+
type: "completed",
|
|
10579
|
+
runId,
|
|
10580
|
+
data: { passed: stats.passed, failed: stats.failed, avgScore: completedCount > 0 ? stats.totalScore / completedCount : 0 }
|
|
10581
|
+
});
|
|
10185
10582
|
return { report };
|
|
10186
10583
|
} catch (err) {
|
|
10187
10584
|
const errorMsg = err.message;
|
|
10188
|
-
|
|
10189
|
-
await
|
|
10585
|
+
try {
|
|
10586
|
+
await persistTerminal({
|
|
10190
10587
|
status: "failed",
|
|
10191
10588
|
error: errorMsg,
|
|
10192
10589
|
completedAt: /* @__PURE__ */ new Date()
|
|
10193
|
-
}
|
|
10194
|
-
this.eventEmitter.emit(`run:${runId}`, {
|
|
10590
|
+
}, {
|
|
10195
10591
|
type: "error",
|
|
10196
10592
|
runId,
|
|
10197
10593
|
data: { message: errorMsg }
|
|
10198
10594
|
});
|
|
10595
|
+
} catch {
|
|
10199
10596
|
}
|
|
10200
10597
|
throw err;
|
|
10201
10598
|
} finally {
|
|
10202
|
-
|
|
10599
|
+
terminal.workerSettled = true;
|
|
10600
|
+
if (terminal.persisted) {
|
|
10601
|
+
this.runs.delete(runId);
|
|
10602
|
+
this.releaseProjectLease(leaseKey, leaseToken);
|
|
10603
|
+
}
|
|
10203
10604
|
}
|
|
10204
10605
|
})();
|
|
10205
|
-
this.runs.set(runId, { runId, projectId, tenantId, abortController, promise: runPromise, taskId });
|
|
10606
|
+
this.runs.set(runId, { runId, projectId, tenantId, abortController, promise: runPromise, terminal, leaseKey, leaseToken, taskId });
|
|
10206
10607
|
runPromise.catch(() => {
|
|
10207
10608
|
});
|
|
10208
10609
|
return runId;
|
|
10209
10610
|
}
|
|
10210
|
-
async abortRun(runId) {
|
|
10611
|
+
async abortRun(runId, tenantId) {
|
|
10211
10612
|
const ctx = this.runs.get(runId);
|
|
10212
|
-
if (!ctx) return false;
|
|
10613
|
+
if (!ctx || tenantId !== void 0 && ctx.tenantId !== tenantId) return false;
|
|
10614
|
+
if (ctx.terminal.claiming || ctx.terminal.persisted) return false;
|
|
10213
10615
|
ctx.abortController.abort();
|
|
10214
10616
|
const store2 = this.getEvalStore();
|
|
10215
|
-
|
|
10216
|
-
|
|
10617
|
+
ctx.terminal.claiming = true;
|
|
10618
|
+
try {
|
|
10619
|
+
let lastError;
|
|
10620
|
+
for (let attempt = 0; attempt < TERMINAL_PERSIST_ATTEMPTS; attempt++) {
|
|
10621
|
+
try {
|
|
10622
|
+
const persistedRun = await store2.updateRunStatus(ctx.tenantId, runId, { status: "aborted", completedAt: /* @__PURE__ */ new Date() });
|
|
10623
|
+
if (!persistedRun) throw new Error(`Evaluation run ${runId} no longer exists`);
|
|
10624
|
+
lastError = void 0;
|
|
10625
|
+
break;
|
|
10626
|
+
} catch (error) {
|
|
10627
|
+
lastError = error;
|
|
10628
|
+
}
|
|
10629
|
+
}
|
|
10630
|
+
if (lastError) throw lastError;
|
|
10631
|
+
ctx.terminal.persisted = true;
|
|
10632
|
+
this.eventEmitter.emit(`run:${runId}`, {
|
|
10633
|
+
type: "error",
|
|
10634
|
+
runId,
|
|
10635
|
+
data: { message: "Run aborted" }
|
|
10636
|
+
});
|
|
10637
|
+
this.releaseProjectLease(ctx.leaseKey, ctx.leaseToken);
|
|
10638
|
+
if (ctx.terminal.workerSettled) this.runs.delete(runId);
|
|
10639
|
+
return true;
|
|
10640
|
+
} finally {
|
|
10641
|
+
ctx.terminal.claiming = false;
|
|
10642
|
+
}
|
|
10217
10643
|
}
|
|
10218
10644
|
async waitForRun(runId) {
|
|
10219
10645
|
const ctx = this.runs.get(runId);
|
|
@@ -10221,12 +10647,25 @@ var EvalRunner = class {
|
|
|
10221
10647
|
const { report } = await ctx.promise;
|
|
10222
10648
|
return report;
|
|
10223
10649
|
}
|
|
10224
|
-
|
|
10225
|
-
|
|
10650
|
+
/**
|
|
10651
|
+
* Whether the run can still be controlled or observed as a non-terminal run.
|
|
10652
|
+
* Acknowledged aborts return false even while their worker is unwinding.
|
|
10653
|
+
*/
|
|
10654
|
+
isRunning(runId, tenantId) {
|
|
10655
|
+
const ctx = this.runs.get(runId);
|
|
10656
|
+
return ctx !== void 0 && !ctx.terminal.persisted && (tenantId === void 0 || ctx.tenantId === tenantId);
|
|
10657
|
+
}
|
|
10658
|
+
/** Whether the run's worker promise has not yet reached its finally block. */
|
|
10659
|
+
hasActiveExecution(runId, tenantId) {
|
|
10660
|
+
const ctx = this.runs.get(runId);
|
|
10661
|
+
return ctx !== void 0 && !ctx.terminal.workerSettled && (tenantId === void 0 || ctx.tenantId === tenantId);
|
|
10226
10662
|
}
|
|
10227
10663
|
getEvalStore() {
|
|
10228
10664
|
return (0, import_core31.getStoreLattice)("default", "eval").store;
|
|
10229
10665
|
}
|
|
10666
|
+
releaseProjectLease(leaseKey, leaseToken) {
|
|
10667
|
+
if (this.projectLeases.get(leaseKey) === leaseToken) this.projectLeases.delete(leaseKey);
|
|
10668
|
+
}
|
|
10230
10669
|
};
|
|
10231
10670
|
var evalRunner = new EvalRunner();
|
|
10232
10671
|
|
|
@@ -10241,6 +10680,18 @@ function getTenantId12(request) {
|
|
|
10241
10680
|
function getEvalStore() {
|
|
10242
10681
|
return (0, import_core32.getStoreLattice)("default", "eval").store;
|
|
10243
10682
|
}
|
|
10683
|
+
async function getOwnedSuite(store2, tenantId, projectId, suiteId) {
|
|
10684
|
+
const project = await store2.getProjectById(tenantId, projectId);
|
|
10685
|
+
if (!project) return null;
|
|
10686
|
+
const suite = await store2.getSuiteById(tenantId, suiteId);
|
|
10687
|
+
return suite?.projectId === projectId ? suite : null;
|
|
10688
|
+
}
|
|
10689
|
+
async function getOwnedCase(store2, tenantId, projectId, suiteId, caseId) {
|
|
10690
|
+
const suite = await getOwnedSuite(store2, tenantId, projectId, suiteId);
|
|
10691
|
+
if (!suite) return null;
|
|
10692
|
+
const evalCase = await store2.getCaseById(tenantId, caseId);
|
|
10693
|
+
return evalCase?.suiteId === suiteId ? evalCase : null;
|
|
10694
|
+
}
|
|
10244
10695
|
async function createProject(request, reply) {
|
|
10245
10696
|
try {
|
|
10246
10697
|
const tenantId = getTenantId12(request);
|
|
@@ -10396,6 +10847,7 @@ async function updateProject(request, reply) {
|
|
|
10396
10847
|
}
|
|
10397
10848
|
}
|
|
10398
10849
|
async function deleteProject(request, reply) {
|
|
10850
|
+
let releaseDeletion;
|
|
10399
10851
|
try {
|
|
10400
10852
|
const tenantId = getTenantId12(request);
|
|
10401
10853
|
const store2 = getEvalStore();
|
|
@@ -10404,6 +10856,23 @@ async function deleteProject(request, reply) {
|
|
|
10404
10856
|
if (!existing) {
|
|
10405
10857
|
return reply.status(404).send({ success: false, message: "Project not found" });
|
|
10406
10858
|
}
|
|
10859
|
+
releaseDeletion = evalRunner.reserveProjectDeletion(tenantId, pid) ?? void 0;
|
|
10860
|
+
if (!releaseDeletion) {
|
|
10861
|
+
return reply.status(409).send({
|
|
10862
|
+
success: false,
|
|
10863
|
+
message: "Cannot delete an evaluation project with an active run"
|
|
10864
|
+
});
|
|
10865
|
+
}
|
|
10866
|
+
const runs = await store2.getRunsByTenant(tenantId, { projectId: pid });
|
|
10867
|
+
const hasActiveRun = runs.some(
|
|
10868
|
+
(run) => run.status === "running" || evalRunner.hasActiveExecution(run.id, tenantId)
|
|
10869
|
+
);
|
|
10870
|
+
if (hasActiveRun) {
|
|
10871
|
+
return reply.status(409).send({
|
|
10872
|
+
success: false,
|
|
10873
|
+
message: "Cannot delete an evaluation project with an active run"
|
|
10874
|
+
});
|
|
10875
|
+
}
|
|
10407
10876
|
const deleted = await store2.deleteProject(tenantId, pid);
|
|
10408
10877
|
if (!deleted) {
|
|
10409
10878
|
return reply.status(500).send({ success: false, message: "Failed to delete project" });
|
|
@@ -10412,6 +10881,8 @@ async function deleteProject(request, reply) {
|
|
|
10412
10881
|
} catch (err) {
|
|
10413
10882
|
const message = err instanceof Error ? err.message : "Failed to delete project";
|
|
10414
10883
|
return reply.status(500).send({ success: false, message });
|
|
10884
|
+
} finally {
|
|
10885
|
+
releaseDeletion?.();
|
|
10415
10886
|
}
|
|
10416
10887
|
}
|
|
10417
10888
|
async function createSuite(request, reply) {
|
|
@@ -10440,8 +10911,8 @@ async function updateSuite(request, reply) {
|
|
|
10440
10911
|
try {
|
|
10441
10912
|
const tenantId = getTenantId12(request);
|
|
10442
10913
|
const store2 = getEvalStore();
|
|
10443
|
-
const { sid } = request.params;
|
|
10444
|
-
const existing = await store2
|
|
10914
|
+
const { pid, sid } = request.params;
|
|
10915
|
+
const existing = await getOwnedSuite(store2, tenantId, pid, sid);
|
|
10445
10916
|
if (!existing) {
|
|
10446
10917
|
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
10447
10918
|
}
|
|
@@ -10460,8 +10931,8 @@ async function deleteSuite(request, reply) {
|
|
|
10460
10931
|
try {
|
|
10461
10932
|
const tenantId = getTenantId12(request);
|
|
10462
10933
|
const store2 = getEvalStore();
|
|
10463
|
-
const { sid } = request.params;
|
|
10464
|
-
const existing = await store2
|
|
10934
|
+
const { pid, sid } = request.params;
|
|
10935
|
+
const existing = await getOwnedSuite(store2, tenantId, pid, sid);
|
|
10465
10936
|
if (!existing) {
|
|
10466
10937
|
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
10467
10938
|
}
|
|
@@ -10479,8 +10950,8 @@ async function createCase(request, reply) {
|
|
|
10479
10950
|
try {
|
|
10480
10951
|
const tenantId = getTenantId12(request);
|
|
10481
10952
|
const store2 = getEvalStore();
|
|
10482
|
-
const { sid } = request.params;
|
|
10483
|
-
const suite = await store2
|
|
10953
|
+
const { pid, sid } = request.params;
|
|
10954
|
+
const suite = await getOwnedSuite(store2, tenantId, pid, sid);
|
|
10484
10955
|
if (!suite) {
|
|
10485
10956
|
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
10486
10957
|
}
|
|
@@ -10509,7 +10980,11 @@ async function listCasesForSuite(request, reply) {
|
|
|
10509
10980
|
try {
|
|
10510
10981
|
const tenantId = getTenantId12(request);
|
|
10511
10982
|
const store2 = getEvalStore();
|
|
10512
|
-
const { sid } = request.params;
|
|
10983
|
+
const { pid, sid } = request.params;
|
|
10984
|
+
const suite = await getOwnedSuite(store2, tenantId, pid, sid);
|
|
10985
|
+
if (!suite) {
|
|
10986
|
+
return reply.status(404).send({ success: false, message: "Suite not found" });
|
|
10987
|
+
}
|
|
10513
10988
|
const cases = await store2.getCasesBySuite(tenantId, sid);
|
|
10514
10989
|
return {
|
|
10515
10990
|
success: true,
|
|
@@ -10525,8 +11000,8 @@ async function updateCase(request, reply) {
|
|
|
10525
11000
|
try {
|
|
10526
11001
|
const tenantId = getTenantId12(request);
|
|
10527
11002
|
const store2 = getEvalStore();
|
|
10528
|
-
const { cid } = request.params;
|
|
10529
|
-
const existing = await store2
|
|
11003
|
+
const { pid, sid, cid } = request.params;
|
|
11004
|
+
const existing = await getOwnedCase(store2, tenantId, pid, sid, cid);
|
|
10530
11005
|
if (!existing) {
|
|
10531
11006
|
return reply.status(404).send({ success: false, message: "Case not found" });
|
|
10532
11007
|
}
|
|
@@ -10545,8 +11020,8 @@ async function deleteCase(request, reply) {
|
|
|
10545
11020
|
try {
|
|
10546
11021
|
const tenantId = getTenantId12(request);
|
|
10547
11022
|
const store2 = getEvalStore();
|
|
10548
|
-
const { cid } = request.params;
|
|
10549
|
-
const existing = await store2
|
|
11023
|
+
const { pid, sid, cid } = request.params;
|
|
11024
|
+
const existing = await getOwnedCase(store2, tenantId, pid, sid, cid);
|
|
10550
11025
|
if (!existing) {
|
|
10551
11026
|
return reply.status(404).send({ success: false, message: "Case not found" });
|
|
10552
11027
|
}
|
|
@@ -10615,6 +11090,11 @@ function registerEvalRoutes(app2) {
|
|
|
10615
11090
|
}
|
|
10616
11091
|
});
|
|
10617
11092
|
app2.get("/api/eval/runs/:rid/stream", async (request, reply) => {
|
|
11093
|
+
const tenantId = getTenantId12(request);
|
|
11094
|
+
const { rid } = request.params;
|
|
11095
|
+
const store2 = getEvalStore();
|
|
11096
|
+
const ownedRun = await store2.getRunById(tenantId, rid);
|
|
11097
|
+
if (!ownedRun) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
10618
11098
|
reply.hijack();
|
|
10619
11099
|
reply.raw.writeHead(200, {
|
|
10620
11100
|
"Content-Type": "text/event-stream",
|
|
@@ -10622,41 +11102,75 @@ function registerEvalRoutes(app2) {
|
|
|
10622
11102
|
Connection: "keep-alive",
|
|
10623
11103
|
"Access-Control-Allow-Origin": "*"
|
|
10624
11104
|
});
|
|
10625
|
-
const { rid } = request.params;
|
|
10626
11105
|
const emitter = evalRunner.getEventEmitter();
|
|
10627
11106
|
const eventKey = `run:${rid}`;
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
|
|
11107
|
+
let settled = false;
|
|
11108
|
+
const cleanup = () => {
|
|
11109
|
+
emitter.off(eventKey, handler);
|
|
11110
|
+
request.raw.off("close", disconnect);
|
|
11111
|
+
};
|
|
11112
|
+
const disconnect = () => {
|
|
11113
|
+
if (settled) return;
|
|
11114
|
+
settled = true;
|
|
11115
|
+
cleanup();
|
|
11116
|
+
};
|
|
11117
|
+
const finish = (type, data) => {
|
|
11118
|
+
if (settled) return;
|
|
11119
|
+
settled = true;
|
|
11120
|
+
cleanup();
|
|
11121
|
+
reply.raw.write(`event: ${type}
|
|
11122
|
+
data: ${JSON.stringify(data)}
|
|
10635
11123
|
|
|
10636
11124
|
`);
|
|
10637
|
-
}
|
|
10638
11125
|
reply.raw.end();
|
|
10639
|
-
|
|
10640
|
-
}
|
|
11126
|
+
};
|
|
10641
11127
|
const handler = (event) => {
|
|
10642
|
-
|
|
11128
|
+
if (event.type === "completed" || event.type === "error") {
|
|
11129
|
+
finish(event.type, event.data);
|
|
11130
|
+
return;
|
|
11131
|
+
}
|
|
11132
|
+
if (!settled) reply.raw.write(`event: ${event.type}
|
|
10643
11133
|
data: ${JSON.stringify(event.data)}
|
|
10644
11134
|
|
|
10645
11135
|
`);
|
|
10646
|
-
if (event.type === "completed" || event.type === "error") {
|
|
10647
|
-
emitter.off(eventKey, handler);
|
|
10648
|
-
reply.raw.end();
|
|
10649
|
-
}
|
|
10650
11136
|
};
|
|
10651
11137
|
emitter.on(eventKey, handler);
|
|
10652
|
-
request.raw.on("close",
|
|
10653
|
-
|
|
10654
|
-
|
|
11138
|
+
request.raw.on("close", disconnect);
|
|
11139
|
+
let reconciledRun;
|
|
11140
|
+
try {
|
|
11141
|
+
reconciledRun = await store2.getRunById(tenantId, rid);
|
|
11142
|
+
} catch (error) {
|
|
11143
|
+
finish("error", { message: error.message });
|
|
11144
|
+
return;
|
|
11145
|
+
}
|
|
11146
|
+
if (settled) return;
|
|
11147
|
+
if (!reconciledRun) {
|
|
11148
|
+
cleanup();
|
|
11149
|
+
reply.raw.end();
|
|
11150
|
+
return;
|
|
11151
|
+
}
|
|
11152
|
+
if (reconciledRun.status === "completed") {
|
|
11153
|
+
finish("completed", {
|
|
11154
|
+
passed: reconciledRun.passedCases,
|
|
11155
|
+
failed: reconciledRun.failedCases,
|
|
11156
|
+
avgScore: reconciledRun.avgScore
|
|
11157
|
+
});
|
|
11158
|
+
} else if (reconciledRun.status === "failed") {
|
|
11159
|
+
finish("error", { message: reconciledRun.error ?? "Evaluation failed" });
|
|
11160
|
+
} else if (reconciledRun.status === "aborted") {
|
|
11161
|
+
finish("error", { message: "Run aborted" });
|
|
11162
|
+
} else if (!evalRunner.isRunning(rid, tenantId)) {
|
|
11163
|
+
cleanup();
|
|
11164
|
+
reply.raw.end();
|
|
11165
|
+
}
|
|
10655
11166
|
});
|
|
10656
11167
|
app2.post("/api/eval/runs/:rid/abort", async (request, reply) => {
|
|
10657
11168
|
try {
|
|
11169
|
+
const tenantId = getTenantId12(request);
|
|
10658
11170
|
const { rid } = request.params;
|
|
10659
|
-
const
|
|
11171
|
+
const run = await getEvalStore().getRunById(tenantId, rid);
|
|
11172
|
+
if (!run) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
11173
|
+
const aborted = await evalRunner.abortRun(rid, tenantId);
|
|
10660
11174
|
if (!aborted) return reply.status(404).send({ success: false, message: "Run not found or not running" });
|
|
10661
11175
|
reply.send({ success: true, message: "Run aborted" });
|
|
10662
11176
|
} catch (err) {
|
|
@@ -10667,7 +11181,13 @@ data: ${JSON.stringify(event.data)}
|
|
|
10667
11181
|
try {
|
|
10668
11182
|
const tenantId = getTenantId12(request);
|
|
10669
11183
|
const { rid } = request.params;
|
|
10670
|
-
const
|
|
11184
|
+
const store2 = getEvalStore();
|
|
11185
|
+
const run = await store2.getRunById(tenantId, rid);
|
|
11186
|
+
if (!run) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
11187
|
+
if (run.status === "running" || evalRunner.hasActiveExecution(rid, tenantId)) {
|
|
11188
|
+
return reply.status(409).send({ success: false, message: "Cannot delete an active evaluation run" });
|
|
11189
|
+
}
|
|
11190
|
+
const deleted = await store2.deleteRun(tenantId, rid);
|
|
10671
11191
|
if (!deleted) return reply.status(404).send({ success: false, message: "Run not found" });
|
|
10672
11192
|
reply.send({ success: true, message: "Run deleted" });
|
|
10673
11193
|
} catch (err) {
|
|
@@ -11263,7 +11783,7 @@ function registerConsoleAuthHook(app2, options) {
|
|
|
11263
11783
|
if (PUBLIC_ROUTES.some((route) => request.url === route)) return;
|
|
11264
11784
|
if (request.url.startsWith("/s/")) return;
|
|
11265
11785
|
const path6 = request.url.split("?")[0];
|
|
11266
|
-
if (
|
|
11786
|
+
if (request.method === "GET" && /^\/api\/workspaces\/[^/]+\/projects\/[^/]+\/viewfile$/.test(path6) || request.method === "GET" && /^\/api\/workspaces\/[^/]+\/projects\/[^/]+\/downloadfile$/.test(path6) || request.method === "POST" && /^\/api\/workspaces\/[^/]+\/projects\/[^/]+\/uploadfile$/.test(path6) || path6.startsWith("/f/") || path6 === "/f") {
|
|
11267
11787
|
return;
|
|
11268
11788
|
}
|
|
11269
11789
|
await reply.status(401).send({
|
|
@@ -12266,12 +12786,12 @@ async function handleBridgeProxy(request, reply) {
|
|
|
12266
12786
|
};
|
|
12267
12787
|
const timeout = 3e5;
|
|
12268
12788
|
try {
|
|
12269
|
-
const response = await new Promise((
|
|
12789
|
+
const response = await new Promise((resolve4, reject) => {
|
|
12270
12790
|
const timer = setTimeout(() => {
|
|
12271
12791
|
pending.delete(requestId);
|
|
12272
12792
|
reject(new Error(`Bridge request timeout after ${timeout}ms`));
|
|
12273
12793
|
}, timeout);
|
|
12274
|
-
pending.set(requestId, { resolve:
|
|
12794
|
+
pending.set(requestId, { resolve: resolve4, reject, timer, agentId });
|
|
12275
12795
|
try {
|
|
12276
12796
|
if (conn.ws.readyState !== conn.ws.OPEN) {
|
|
12277
12797
|
pending.delete(requestId);
|
|
@@ -16049,7 +16569,7 @@ ${summary}`
|
|
|
16049
16569
|
console.log(
|
|
16050
16570
|
`\u5C06\u5728 ${delayMs}ms \u540E\u91CD\u8BD5\u4EFB\u52A1 (${nextRetryCount}/${maxRetries})`
|
|
16051
16571
|
);
|
|
16052
|
-
await new Promise((
|
|
16572
|
+
await new Promise((resolve4) => setTimeout(resolve4, delayMs));
|
|
16053
16573
|
return handleAgentTask(taskRequest, nextRetryCount);
|
|
16054
16574
|
}
|
|
16055
16575
|
if (callback_event) {
|