@axiom-lattice/gateway 2.1.53 → 2.1.55
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +19 -0
- package/dist/index.js +1043 -526
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +977 -449
- package/dist/index.mjs.map +1 -1
- package/jest.config.js +5 -0
- package/package.json +6 -5
- package/src/__tests__/__mocks__/e2b.ts +1 -0
- package/src/__tests__/channel-installations.test.ts +199 -0
- package/src/__tests__/sandbox-provider-registration.test.ts +74 -0
- package/src/__tests__/workspace.test.ts +119 -8
- package/src/channels/__tests__/routes.test.ts +71 -0
- package/src/channels/lark/README.md +187 -0
- package/src/channels/lark/__tests__/aggregator.test.ts +23 -0
- package/src/channels/lark/__tests__/controller.test.ts +270 -0
- package/src/channels/lark/__tests__/mapping-service.test.ts +118 -0
- package/src/channels/lark/__tests__/parser.test.ts +72 -0
- package/src/channels/lark/__tests__/sender.test.ts +37 -0
- package/src/channels/lark/__tests__/verification.test.ts +157 -0
- package/src/channels/lark/aggregator.ts +16 -0
- package/src/channels/lark/config.ts +44 -0
- package/src/channels/lark/controller.ts +189 -0
- package/src/channels/lark/mapping-service.ts +138 -0
- package/src/channels/lark/parser.ts +68 -0
- package/src/channels/lark/routes.ts +121 -0
- package/src/channels/lark/runner.ts +37 -0
- package/src/channels/lark/sender.ts +58 -0
- package/src/channels/lark/types.ts +33 -0
- package/src/channels/lark/verification.ts +67 -0
- package/src/channels/routes.ts +25 -0
- package/src/controllers/channel-installations.ts +354 -0
- package/src/controllers/sandbox.ts +30 -80
- package/src/controllers/skills.ts +71 -321
- package/src/controllers/threads.ts +8 -6
- package/src/controllers/workspace.ts +64 -179
- package/src/index.ts +28 -5
- package/src/routes/channel-installations.ts +33 -0
- package/src/routes/index.ts +6 -0
- package/src/schemas/index.ts +2 -2
- package/src/services/sandbox_service.ts +21 -21
- package/src/services/skill_service.ts +97 -0
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
|
9
9
|
import * as fs from "fs/promises";
|
|
10
10
|
import * as path from "path";
|
|
11
|
-
import { Readable } from "stream";
|
|
12
11
|
import { getStoreLattice } from "@axiom-lattice/core";
|
|
13
12
|
import { SandboxFilesystem, FilesystemBackend } from "@axiom-lattice/core";
|
|
14
13
|
import { getSandBoxManager } from "@axiom-lattice/core";
|
|
15
|
-
import { sandboxService } from "../services/sandbox_service";
|
|
16
14
|
import { v4 as uuidv4 } from "uuid";
|
|
17
15
|
import type {
|
|
18
16
|
CreateWorkspaceRequest,
|
|
@@ -264,12 +262,16 @@ export class WorkspaceController {
|
|
|
264
262
|
|
|
265
263
|
if (workspace.storageType === "sandbox") {
|
|
266
264
|
const sandboxManager = getSandBoxManager();
|
|
267
|
-
const
|
|
268
|
-
|
|
265
|
+
const sandbox = await sandboxManager.getSandboxFromConfig({
|
|
266
|
+
assistant_id: "",
|
|
267
|
+
thread_id: "",
|
|
268
|
+
tenantId,
|
|
269
|
+
workspaceId,
|
|
270
|
+
projectId,
|
|
271
|
+
});
|
|
269
272
|
return {
|
|
270
273
|
backend: new SandboxFilesystem({
|
|
271
274
|
sandboxInstance: sandbox,
|
|
272
|
-
workingDirectory: `/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`,
|
|
273
275
|
}), workspace
|
|
274
276
|
};
|
|
275
277
|
} else {
|
|
@@ -327,66 +329,28 @@ export class WorkspaceController {
|
|
|
327
329
|
|
|
328
330
|
try {
|
|
329
331
|
const { workspace } = await this.getBackend(tenantId, workspaceId, projectId);
|
|
330
|
-
const resolvedPath = filePath
|
|
332
|
+
const resolvedPath = filePath;
|
|
331
333
|
|
|
332
334
|
if (workspace.storageType === "sandbox") {
|
|
333
335
|
const sandboxManager = getSandBoxManager();
|
|
334
|
-
const sandbox = await sandboxManager.
|
|
335
|
-
|
|
336
|
+
const sandbox = await sandboxManager.getSandboxFromConfig({
|
|
337
|
+
assistant_id: "",
|
|
338
|
+
thread_id: "",
|
|
339
|
+
tenantId,
|
|
340
|
+
workspaceId,
|
|
341
|
+
projectId,
|
|
342
|
+
});
|
|
343
|
+
const realPath = resolvedPath;
|
|
336
344
|
const filename = this.getFilenameFromPath(resolvedPath);
|
|
337
345
|
const inferredContentType = this.getMimeType(filename);
|
|
338
346
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return reply.status(502).send({
|
|
345
|
-
success: false,
|
|
346
|
-
error: `Download error: ${JSON.stringify(downloadResult.error)}`,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const body = downloadResult.body as unknown as {
|
|
351
|
-
stream?: () => ReadableStream<Uint8Array>;
|
|
352
|
-
contentType?: string;
|
|
353
|
-
contentDisposition?: string;
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
if (typeof body?.stream === "function") {
|
|
357
|
-
const webStream = body.stream();
|
|
358
|
-
const nodeStream = Readable.fromWeb(webStream);
|
|
359
|
-
const contentType = body.contentType ?? inferredContentType;
|
|
360
|
-
const contentDisposition =
|
|
361
|
-
body.contentDisposition ?? `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
362
|
-
return reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(nodeStream);
|
|
347
|
+
try {
|
|
348
|
+
const buf = await sandbox.file.downloadFile({ file: realPath });
|
|
349
|
+
const contentDisposition = `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
350
|
+
return reply.status(200).type(inferredContentType).header("Content-Disposition", contentDisposition).send(buf);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
return reply.status(502).send({ success: false, error: String(err) });
|
|
363
353
|
}
|
|
364
|
-
|
|
365
|
-
const bodyUnknown = downloadResult.body as unknown;
|
|
366
|
-
let buf: Buffer;
|
|
367
|
-
let contentType = inferredContentType;
|
|
368
|
-
let contentDisposition = `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
369
|
-
|
|
370
|
-
if (bodyUnknown instanceof ArrayBuffer) {
|
|
371
|
-
buf = Buffer.from(bodyUnknown);
|
|
372
|
-
} else if (bodyUnknown instanceof Buffer) {
|
|
373
|
-
buf = bodyUnknown;
|
|
374
|
-
} else if (
|
|
375
|
-
bodyUnknown &&
|
|
376
|
-
typeof (bodyUnknown as { arrayBuffer?: () => Promise<ArrayBuffer> }).arrayBuffer === "function"
|
|
377
|
-
) {
|
|
378
|
-
const res = bodyUnknown as { arrayBuffer: () => Promise<ArrayBuffer>; headers?: Headers };
|
|
379
|
-
buf = Buffer.from(await res.arrayBuffer());
|
|
380
|
-
if (res.headers?.get("content-type")) contentType = res.headers.get("content-type")!;
|
|
381
|
-
if (res.headers?.get("content-disposition")) contentDisposition = res.headers.get("content-disposition")!;
|
|
382
|
-
} else if (bodyUnknown && typeof (bodyUnknown as { blob?: () => Promise<Blob> }).blob === "function") {
|
|
383
|
-
const blob = await (bodyUnknown as { blob: () => Promise<Blob> }).blob();
|
|
384
|
-
buf = Buffer.from(await blob.arrayBuffer());
|
|
385
|
-
} else {
|
|
386
|
-
return reply.status(502).send({ success: false, error: "Unexpected download response format" });
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(buf);
|
|
390
354
|
}
|
|
391
355
|
|
|
392
356
|
const { backend } = await this.getBackend(tenantId, workspaceId, projectId);
|
|
@@ -427,51 +391,33 @@ export class WorkspaceController {
|
|
|
427
391
|
|
|
428
392
|
try {
|
|
429
393
|
const { workspace } = await this.getBackend(tenantId, workspaceId, projectId);
|
|
430
|
-
const resolvedPath = filePath
|
|
394
|
+
const resolvedPath = filePath;
|
|
431
395
|
|
|
432
396
|
if (workspace.storageType === "sandbox") {
|
|
433
397
|
const sandboxManager = getSandBoxManager();
|
|
434
|
-
const sandbox = await sandboxManager.
|
|
435
|
-
|
|
398
|
+
const sandbox = await sandboxManager.getSandboxFromConfig({
|
|
399
|
+
assistant_id: "",
|
|
400
|
+
thread_id: "",
|
|
401
|
+
tenantId,
|
|
402
|
+
workspaceId,
|
|
403
|
+
projectId,
|
|
404
|
+
});
|
|
405
|
+
const realPath = resolvedPath;
|
|
436
406
|
const filename = this.getFilenameFromPath(resolvedPath);
|
|
437
407
|
const inferredContentType = this.getMimeType(filename);
|
|
438
408
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (!downloadResult.ok) {
|
|
444
|
-
return reply.status(502).send({
|
|
445
|
-
success: false,
|
|
446
|
-
error: `View error: ${JSON.stringify(downloadResult.error)}`,
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const body = downloadResult.body as unknown as {
|
|
451
|
-
stream?: () => ReadableStream<Uint8Array>;
|
|
452
|
-
contentType?: string;
|
|
453
|
-
contentDisposition?: string;
|
|
454
|
-
};
|
|
409
|
+
try {
|
|
410
|
+
const buf = await sandbox.file.downloadFile({ file: realPath });
|
|
411
|
+
let contentType = inferredContentType;
|
|
455
412
|
|
|
456
|
-
|
|
457
|
-
const webStream = body.stream();
|
|
458
|
-
const nodeStream = Readable.fromWeb(webStream);
|
|
459
|
-
const contentType = body.contentType ?? inferredContentType;
|
|
460
|
-
console.log(`[viewFile] Sandbox returned stream, contentType: ${contentType}, filename: ${filename}`);
|
|
461
|
-
|
|
462
|
-
// Check if it's HTML and needs context injection
|
|
413
|
+
// Inject AI2APP context script for HTML files (sandbox storage)
|
|
463
414
|
const isHtml = contentType?.toLowerCase().includes("text/html") ||
|
|
464
415
|
filename.toLowerCase().endsWith(".html") ||
|
|
465
416
|
filename.toLowerCase().endsWith(".htm");
|
|
466
|
-
|
|
417
|
+
let outputBuf = buf;
|
|
467
418
|
if (isHtml) {
|
|
468
|
-
console.log(`[viewFile] HTML
|
|
469
|
-
|
|
470
|
-
const chunks: Buffer[] = [];
|
|
471
|
-
for await (const chunk of nodeStream) {
|
|
472
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
473
|
-
}
|
|
474
|
-
let content = Buffer.concat(chunks).toString("utf-8");
|
|
419
|
+
console.log(`[viewFile] Injecting AI2APP context for sandbox HTML file: ${filename}, tenantId: ${tenantId}, contentType: ${contentType}`);
|
|
420
|
+
let content = buf.toString("utf-8");
|
|
475
421
|
const contextScript = `<script>window.__AI2APP_CONTEXT__=${JSON.stringify({
|
|
476
422
|
tenantId,
|
|
477
423
|
workspaceId,
|
|
@@ -481,84 +427,25 @@ export class WorkspaceController {
|
|
|
481
427
|
|
|
482
428
|
if (content.toLowerCase().includes("</head>")) {
|
|
483
429
|
content = content.replace(/<\/head>/i, `${contextScript}</head>`);
|
|
484
|
-
console.log(`[viewFile] Context script injected before </head
|
|
430
|
+
console.log(`[viewFile] Context script injected before </head>`);
|
|
485
431
|
} else if (content.toLowerCase().includes("<html>")) {
|
|
486
432
|
content = content.replace(/<html>/i, `<html>${contextScript}`);
|
|
487
|
-
console.log(`[viewFile] Context script injected after <html
|
|
433
|
+
console.log(`[viewFile] Context script injected after <html>`);
|
|
488
434
|
} else {
|
|
489
435
|
content = contextScript + content;
|
|
490
|
-
console.log(`[viewFile] Context script prepended to content
|
|
436
|
+
console.log(`[viewFile] Context script prepended to content`);
|
|
491
437
|
}
|
|
492
|
-
|
|
493
|
-
return reply
|
|
494
|
-
.status(200)
|
|
495
|
-
.type(contentType)
|
|
496
|
-
.header("Content-Disposition", "inline")
|
|
497
|
-
.send(Buffer.from(content, "utf-8"));
|
|
438
|
+
outputBuf = Buffer.from(content, "utf-8");
|
|
498
439
|
}
|
|
499
|
-
|
|
500
|
-
// inline for viewing, not attachment for download
|
|
440
|
+
|
|
501
441
|
return reply
|
|
502
442
|
.status(200)
|
|
503
443
|
.type(contentType)
|
|
504
444
|
.header("Content-Disposition", "inline")
|
|
505
|
-
.send(
|
|
445
|
+
.send(outputBuf);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
return reply.status(502).send({ success: false, error: String(err) });
|
|
506
448
|
}
|
|
507
|
-
|
|
508
|
-
const bodyUnknown = downloadResult.body as unknown;
|
|
509
|
-
let buf: Buffer;
|
|
510
|
-
let contentType = inferredContentType;
|
|
511
|
-
|
|
512
|
-
if (bodyUnknown instanceof ArrayBuffer) {
|
|
513
|
-
buf = Buffer.from(bodyUnknown);
|
|
514
|
-
} else if (bodyUnknown instanceof Buffer) {
|
|
515
|
-
buf = bodyUnknown;
|
|
516
|
-
} else if (
|
|
517
|
-
bodyUnknown &&
|
|
518
|
-
typeof (bodyUnknown as { arrayBuffer?: () => Promise<ArrayBuffer> }).arrayBuffer === "function"
|
|
519
|
-
) {
|
|
520
|
-
const res = bodyUnknown as { arrayBuffer: () => Promise<ArrayBuffer>; headers?: Headers };
|
|
521
|
-
buf = Buffer.from(await res.arrayBuffer());
|
|
522
|
-
if (res.headers?.get("content-type")) contentType = res.headers.get("content-type")!;
|
|
523
|
-
} else if (bodyUnknown && typeof (bodyUnknown as { blob?: () => Promise<Blob> }).blob === "function") {
|
|
524
|
-
const blob = await (bodyUnknown as { blob: () => Promise<Blob> }).blob();
|
|
525
|
-
buf = Buffer.from(await blob.arrayBuffer());
|
|
526
|
-
} else {
|
|
527
|
-
return reply.status(502).send({ success: false, error: "Unexpected view response format" });
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Inject AI2APP context script for HTML files (sandbox storage)
|
|
531
|
-
const isHtml = contentType?.toLowerCase().includes("text/html") ||
|
|
532
|
-
filename.toLowerCase().endsWith(".html") ||
|
|
533
|
-
filename.toLowerCase().endsWith(".htm");
|
|
534
|
-
if (isHtml) {
|
|
535
|
-
console.log(`[viewFile] Injecting AI2APP context for sandbox HTML file: ${filename}, tenantId: ${tenantId}, contentType: ${contentType}`);
|
|
536
|
-
let content = buf.toString("utf-8");
|
|
537
|
-
const contextScript = `<script>window.__AI2APP_CONTEXT__=${JSON.stringify({
|
|
538
|
-
tenantId,
|
|
539
|
-
workspaceId,
|
|
540
|
-
projectId,
|
|
541
|
-
timestamp: Date.now()
|
|
542
|
-
})};</script>`;
|
|
543
|
-
|
|
544
|
-
if (content.toLowerCase().includes("</head>")) {
|
|
545
|
-
content = content.replace(/<\/head>/i, `${contextScript}</head>`);
|
|
546
|
-
console.log(`[viewFile] Context script injected before </head>`);
|
|
547
|
-
} else if (content.toLowerCase().includes("<html>")) {
|
|
548
|
-
content = content.replace(/<html>/i, `<html>${contextScript}`);
|
|
549
|
-
console.log(`[viewFile] Context script injected after <html>`);
|
|
550
|
-
} else {
|
|
551
|
-
content = contextScript + content;
|
|
552
|
-
console.log(`[viewFile] Context script prepended to content`);
|
|
553
|
-
}
|
|
554
|
-
buf = Buffer.from(content, "utf-8");
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
return reply
|
|
558
|
-
.status(200)
|
|
559
|
-
.type(contentType)
|
|
560
|
-
.header("Content-Disposition", "inline")
|
|
561
|
-
.send(buf);
|
|
562
449
|
}
|
|
563
450
|
|
|
564
451
|
const { backend } = await this.getBackend(tenantId, workspaceId, projectId);
|
|
@@ -686,7 +573,7 @@ export class WorkspaceController {
|
|
|
686
573
|
? pathEntry
|
|
687
574
|
: undefined;
|
|
688
575
|
|
|
689
|
-
if (pathValue && !/^[a-zA-Z0-9_
|
|
576
|
+
if (pathValue && !/^[a-zA-Z0-9_./~\-]+$/.test(pathValue)) {
|
|
690
577
|
return reply
|
|
691
578
|
.status(400)
|
|
692
579
|
.send({ success: false, error: "Invalid path parameter" });
|
|
@@ -694,29 +581,27 @@ export class WorkspaceController {
|
|
|
694
581
|
|
|
695
582
|
if (workspace.storageType === "sandbox") {
|
|
696
583
|
const sandboxManager = getSandBoxManager();
|
|
697
|
-
const
|
|
698
|
-
|
|
584
|
+
const sandbox = await sandboxManager.getSandboxFromConfig({
|
|
585
|
+
assistant_id: "",
|
|
586
|
+
thread_id: "",
|
|
587
|
+
tenantId,
|
|
588
|
+
workspaceId,
|
|
589
|
+
projectId,
|
|
590
|
+
});
|
|
699
591
|
|
|
700
|
-
const baseDir = path.join("/home/gem/tenants", tenantId, "workspaces", workspaceId, projectId);
|
|
701
592
|
const realPath = pathValue
|
|
702
|
-
? path.join(
|
|
703
|
-
:
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
file: buffer
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (!uploadResult.ok) {
|
|
711
|
-
return reply.status(502).send({
|
|
712
|
-
success: false,
|
|
713
|
-
error: `Upload error: ${JSON.stringify(uploadResult.error)}`,
|
|
714
|
-
});
|
|
593
|
+
? path.posix.join(pathValue, filename)
|
|
594
|
+
: filename;
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
await sandbox.file.uploadFile({ file: realPath, data: buffer });
|
|
598
|
+
} catch (err) {
|
|
599
|
+
return reply.status(500).send({ success: false, error: String(err) });
|
|
715
600
|
}
|
|
716
601
|
|
|
717
|
-
const relativePath =
|
|
718
|
-
|
|
719
|
-
|
|
602
|
+
const relativePath = pathValue
|
|
603
|
+
? path.posix.join(pathValue, filename)
|
|
604
|
+
: `/${filename}`;
|
|
720
605
|
|
|
721
606
|
const result = {
|
|
722
607
|
path: relativePath,
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
sqlDatabaseManager,
|
|
21
21
|
getStoreLattice,
|
|
22
22
|
agentInstanceManager,
|
|
23
|
+
createSandboxProvider,
|
|
24
|
+
type CreateSandboxProviderConfig,
|
|
23
25
|
} from "@axiom-lattice/core";
|
|
24
26
|
import type { DatabaseConfigStore } from "@axiom-lattice/protocols";
|
|
25
27
|
import {
|
|
@@ -206,6 +208,30 @@ export interface LatticeGatewayConfig {
|
|
|
206
208
|
loggerConfig?: Partial<GatewayLoggerConfig>; // Optional logger configuration to override defaults
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
function getConfiguredSandboxProvider(): ReturnType<typeof createSandboxProvider> {
|
|
212
|
+
const sandboxProviderType =
|
|
213
|
+
(process.env.SANDBOX_PROVIDER_TYPE as CreateSandboxProviderConfig["type"] | undefined) ||
|
|
214
|
+
"microsandbox";
|
|
215
|
+
|
|
216
|
+
return createSandboxProvider({
|
|
217
|
+
type: sandboxProviderType,
|
|
218
|
+
remoteBaseURL: process.env.SANDBOX_BASE_URL,
|
|
219
|
+
microsandboxServiceBaseURL: process.env.MICROSANDBOX_SERVICE_BASE_URL,
|
|
220
|
+
e2bApiKey: process.env.E2B_API_KEY,
|
|
221
|
+
e2bTemplate: process.env.E2B_TEMPLATE,
|
|
222
|
+
e2bTimeoutMs: process.env.E2B_TIMEOUT_MS
|
|
223
|
+
? parseInt(process.env.E2B_TIMEOUT_MS, 10)
|
|
224
|
+
: undefined,
|
|
225
|
+
daytonaApiKey: process.env.DAYTONA_API_KEY,
|
|
226
|
+
daytonaApiUrl: process.env.DAYTONA_API_URL,
|
|
227
|
+
daytonaTarget: process.env.DAYTONA_TARGET,
|
|
228
|
+
daytonaTimeout: process.env.DAYTONA_TIMEOUT
|
|
229
|
+
? parseInt(process.env.DAYTONA_TIMEOUT, 10)
|
|
230
|
+
: undefined,
|
|
231
|
+
daytonaVolumeName: process.env.DAYTONA_VOLUME_NAME,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
209
235
|
// Start server
|
|
210
236
|
const start = async (config?: LatticeGatewayConfig) => {
|
|
211
237
|
try {
|
|
@@ -240,11 +266,8 @@ const start = async (config?: LatticeGatewayConfig) => {
|
|
|
240
266
|
|
|
241
267
|
// Register sandbox manager if not already registered
|
|
242
268
|
if (!sandboxLatticeManager.hasLattice("default")) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
baseURL: sandboxBaseURL,
|
|
246
|
-
});
|
|
247
|
-
logger.info(`Registered sandbox manager with baseURL: ${sandboxBaseURL}`);
|
|
269
|
+
sandboxLatticeManager.registerLattice("default", getConfiguredSandboxProvider());
|
|
270
|
+
logger.info("Registered sandbox manager from env configuration");
|
|
248
271
|
}
|
|
249
272
|
|
|
250
273
|
const target_port = config?.port || Number(process.env.PORT) || 4001;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { FastifyInstance } from "fastify";
|
|
2
|
+
import * as channelInstallationController from "../controllers/channel-installations";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Register channel installation management routes
|
|
6
|
+
*/
|
|
7
|
+
export function registerChannelInstallationRoutes(app: FastifyInstance): void {
|
|
8
|
+
// List all installations for current tenant
|
|
9
|
+
app.get<{
|
|
10
|
+
Querystring: { channel?: "lark" };
|
|
11
|
+
}>("/api/channel-installations", channelInstallationController.getChannelInstallationList);
|
|
12
|
+
|
|
13
|
+
// Get single installation by ID
|
|
14
|
+
app.get<{
|
|
15
|
+
Params: { installationId: string };
|
|
16
|
+
}>("/api/channel-installations/:installationId", channelInstallationController.getChannelInstallation);
|
|
17
|
+
|
|
18
|
+
// Create new installation
|
|
19
|
+
app.post<{
|
|
20
|
+
Body: any;
|
|
21
|
+
}>("/api/channel-installations", channelInstallationController.createChannelInstallation);
|
|
22
|
+
|
|
23
|
+
// Update installation
|
|
24
|
+
app.put<{
|
|
25
|
+
Params: { installationId: string };
|
|
26
|
+
Body: any;
|
|
27
|
+
}>("/api/channel-installations/:installationId", channelInstallationController.updateChannelInstallation);
|
|
28
|
+
|
|
29
|
+
// Delete installation
|
|
30
|
+
app.delete<{
|
|
31
|
+
Params: { installationId: string };
|
|
32
|
+
}>("/api/channel-installations/:installationId", channelInstallationController.deleteChannelInstallation);
|
|
33
|
+
}
|
package/src/routes/index.ts
CHANGED
|
@@ -39,6 +39,8 @@ import { registerMcpServerConfigRoutes } from "../controllers/mcp-configs";
|
|
|
39
39
|
import { registerUserRoutes } from "../controllers/users";
|
|
40
40
|
import { registerTenantRoutes } from "../controllers/tenants";
|
|
41
41
|
import { registerAuthRoutes } from "../controllers/auth";
|
|
42
|
+
import { registerChannelRoutes } from "../channels/routes";
|
|
43
|
+
import { registerChannelInstallationRoutes } from "./channel-installations";
|
|
42
44
|
// import {
|
|
43
45
|
// getThreadStatusHandler,
|
|
44
46
|
// getAgentThreadsHandler,
|
|
@@ -343,6 +345,10 @@ export const registerLatticeRoutes = (app: FastifyInstance): void => {
|
|
|
343
345
|
allowTenantRegistration: process.env.ALLOW_TENANT_REGISTRATION !== "false",
|
|
344
346
|
});
|
|
345
347
|
|
|
348
|
+
registerChannelRoutes(app);
|
|
349
|
+
|
|
350
|
+
registerChannelInstallationRoutes(app);
|
|
351
|
+
|
|
346
352
|
// // Thread 状态查询路由
|
|
347
353
|
// app.get("/api/threads/:thread_id/status", getThreadStatusHandler);
|
|
348
354
|
// app.get("/api/assistants/:assistant_id/threads/status", getAgentThreadsHandler);
|
package/src/schemas/index.ts
CHANGED
|
@@ -829,9 +829,9 @@ export const getSandboxUrlSchema: FastifySchema = {
|
|
|
829
829
|
properties: {
|
|
830
830
|
sandboxUrl: { type: "string", description: "Sandbox URL" },
|
|
831
831
|
sandboxName: { type: "string", description: "Sandbox name" },
|
|
832
|
-
|
|
832
|
+
vmIsolation: {
|
|
833
833
|
type: "string",
|
|
834
|
-
enum: ["agent", "
|
|
834
|
+
enum: ["agent", "project", "global"],
|
|
835
835
|
description: "Sandbox isolation level",
|
|
836
836
|
},
|
|
837
837
|
},
|
|
@@ -93,7 +93,7 @@ const ERROR_HTML = `<!DOCTYPE html>
|
|
|
93
93
|
</div>
|
|
94
94
|
<div class="info-item">
|
|
95
95
|
<span class="label">隔离级别</span>
|
|
96
|
-
<span class="value" id="
|
|
96
|
+
<span class="value" id="vmIsolation">-</span>
|
|
97
97
|
</div>
|
|
98
98
|
<div class="info-item">
|
|
99
99
|
<span class="label">错误信息</span>
|
|
@@ -106,7 +106,7 @@ const ERROR_HTML = `<!DOCTYPE html>
|
|
|
106
106
|
const params = new URLSearchParams(window.location.search);
|
|
107
107
|
document.getElementById('assistantId').textContent = params.get('assistantId') || '-';
|
|
108
108
|
document.getElementById('threadId').textContent = params.get('threadId') || '-';
|
|
109
|
-
document.getElementById('
|
|
109
|
+
document.getElementById('vmIsolation').textContent = params.get('vmIsolation') || '-';
|
|
110
110
|
document.getElementById('errorMsg').textContent = params.get('error') || '未知错误';
|
|
111
111
|
</script>
|
|
112
112
|
</body>
|
|
@@ -114,7 +114,7 @@ const ERROR_HTML = `<!DOCTYPE html>
|
|
|
114
114
|
|
|
115
115
|
export class SandboxService {
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
getFilesystemVmIsolation(tenantId: string, assistantId: string): "agent" | "project" | "global" | null {
|
|
118
118
|
const agentLattice = agentLatticeManager.getAgentLatticeWithTenant(tenantId, assistantId);
|
|
119
119
|
if (!agentLattice) {
|
|
120
120
|
return null;
|
|
@@ -125,37 +125,37 @@ export class SandboxService {
|
|
|
125
125
|
return null;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
// Type guard for SandboxMiddlewareConfig which has isolatedLevel
|
|
129
128
|
const config = filesystemConfig.config as SandboxMiddlewareConfig;
|
|
130
|
-
return config?.
|
|
129
|
+
return config?.vmIsolation || null;
|
|
131
130
|
}
|
|
132
131
|
|
|
133
132
|
computeSandboxName(
|
|
134
133
|
assistantId: string,
|
|
135
134
|
threadId: string,
|
|
136
|
-
|
|
135
|
+
vmIsolation: string,
|
|
136
|
+
tenantId?: string,
|
|
137
|
+
workspaceId?: string,
|
|
138
|
+
projectId?: string,
|
|
137
139
|
): string {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
switch (isolatedLevel) {
|
|
140
|
+
switch (vmIsolation) {
|
|
141
141
|
case "agent":
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
return normalizeSandboxName(`${tenantId ?? "default"}-${assistantId}`);
|
|
143
|
+
case "project":
|
|
144
|
+
return normalizeSandboxName(
|
|
145
|
+
`${tenantId ?? "default"}-${workspaceId ?? "default"}-${projectId ?? "default"}`
|
|
146
|
+
);
|
|
147
147
|
case "global":
|
|
148
148
|
default:
|
|
149
|
-
|
|
150
|
-
break;
|
|
149
|
+
return "global";
|
|
151
150
|
}
|
|
151
|
+
}
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
private getBrowserSandboxBaseURL(): string {
|
|
154
|
+
return process.env.AGENT_INFRA_SANDBOX_BASE_URL || "http://localhost:8080";
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
getTargetUrl(sandboxName: string): string {
|
|
157
|
-
|
|
158
|
-
return `${sandboxManager.getBaseURL()}/sandbox/${sandboxName}`;
|
|
158
|
+
return `${this.getBrowserSandboxBaseURL()}/sandbox/${sandboxName}`;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
async getVncHtml(sandboxName: string): Promise<string> {
|
|
@@ -208,14 +208,14 @@ export class SandboxService {
|
|
|
208
208
|
generateErrorHtml(
|
|
209
209
|
assistantId: string,
|
|
210
210
|
threadId: string,
|
|
211
|
-
|
|
211
|
+
vmIsolation: string,
|
|
212
212
|
errorMessage: string
|
|
213
213
|
): string {
|
|
214
214
|
const encodedError = encodeURIComponent(errorMessage);
|
|
215
215
|
return ERROR_HTML
|
|
216
216
|
.replace("{assistantId}", assistantId)
|
|
217
217
|
.replace("{threadId}", threadId)
|
|
218
|
-
.replace("{
|
|
218
|
+
.replace("{vmIsolation}", vmIsolation)
|
|
219
219
|
.replace("{errorMessage}", errorMessage);
|
|
220
220
|
}
|
|
221
221
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillService
|
|
3
|
+
*
|
|
4
|
+
* Business logic layer for skill management.
|
|
5
|
+
* Wraps SandboxSkillStore from @axiom-lattice/core.
|
|
6
|
+
* Controllers should consume this service, not directly manipulate storage.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { SandboxSkillStore } from "@axiom-lattice/core";
|
|
10
|
+
import type {
|
|
11
|
+
Skill,
|
|
12
|
+
SkillStoreContext,
|
|
13
|
+
CreateSkillRequest,
|
|
14
|
+
} from "@axiom-lattice/protocols";
|
|
15
|
+
import { FastifyRequest } from "fastify";
|
|
16
|
+
|
|
17
|
+
function getTenantId(request: FastifyRequest): string {
|
|
18
|
+
return (request as any).user?.tenantId || (request.headers["x-tenant-id"] as string) || "default";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getWorkspaceId(request: FastifyRequest): string | undefined {
|
|
22
|
+
return request.headers["x-workspace-id"] as string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getProjectId(request: FastifyRequest): string | undefined {
|
|
26
|
+
return request.headers["x-project-id"] as string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildContext(request: FastifyRequest): SkillStoreContext {
|
|
30
|
+
return {
|
|
31
|
+
workspaceId: getWorkspaceId(request),
|
|
32
|
+
projectId: getProjectId(request),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class SkillService {
|
|
37
|
+
private store: SandboxSkillStore;
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
this.store = new SandboxSkillStore();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getAllSkills(request: FastifyRequest): Promise<Skill[]> {
|
|
44
|
+
const tenantId = getTenantId(request);
|
|
45
|
+
return this.store.getAllSkills(tenantId, buildContext(request));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getSkillById(request: FastifyRequest, id: string): Promise<Skill | null> {
|
|
49
|
+
const tenantId = getTenantId(request);
|
|
50
|
+
return this.store.getSkillById(tenantId, id, buildContext(request));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async createSkill(
|
|
54
|
+
request: FastifyRequest,
|
|
55
|
+
id: string,
|
|
56
|
+
data: CreateSkillRequest
|
|
57
|
+
): Promise<Skill> {
|
|
58
|
+
const tenantId = getTenantId(request);
|
|
59
|
+
return this.store.createSkill(tenantId, id, data, buildContext(request));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async updateSkill(
|
|
63
|
+
request: FastifyRequest,
|
|
64
|
+
id: string,
|
|
65
|
+
updates: Partial<CreateSkillRequest>
|
|
66
|
+
): Promise<Skill | null> {
|
|
67
|
+
const tenantId = getTenantId(request);
|
|
68
|
+
return this.store.updateSkill(tenantId, id, updates, buildContext(request));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async deleteSkill(request: FastifyRequest, id: string): Promise<boolean> {
|
|
72
|
+
const tenantId = getTenantId(request);
|
|
73
|
+
return this.store.deleteSkill(tenantId, id, buildContext(request));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async searchByMetadata(
|
|
77
|
+
request: FastifyRequest,
|
|
78
|
+
key: string,
|
|
79
|
+
value: string
|
|
80
|
+
): Promise<Skill[]> {
|
|
81
|
+
const tenantId = getTenantId(request);
|
|
82
|
+
return this.store.searchByMetadata(tenantId, key, value, buildContext(request));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async filterByCompatibility(
|
|
86
|
+
request: FastifyRequest,
|
|
87
|
+
compatibility: string
|
|
88
|
+
): Promise<Skill[]> {
|
|
89
|
+
const tenantId = getTenantId(request);
|
|
90
|
+
return this.store.filterByCompatibility(tenantId, compatibility, buildContext(request));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async filterByLicense(request: FastifyRequest, license: string): Promise<Skill[]> {
|
|
94
|
+
const tenantId = getTenantId(request);
|
|
95
|
+
return this.store.filterByLicense(tenantId, license, buildContext(request));
|
|
96
|
+
}
|
|
97
|
+
}
|