@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.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/CHANGELOG.md +19 -0
  3. package/dist/index.js +1043 -526
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +977 -449
  6. package/dist/index.mjs.map +1 -1
  7. package/jest.config.js +5 -0
  8. package/package.json +6 -5
  9. package/src/__tests__/__mocks__/e2b.ts +1 -0
  10. package/src/__tests__/channel-installations.test.ts +199 -0
  11. package/src/__tests__/sandbox-provider-registration.test.ts +74 -0
  12. package/src/__tests__/workspace.test.ts +119 -8
  13. package/src/channels/__tests__/routes.test.ts +71 -0
  14. package/src/channels/lark/README.md +187 -0
  15. package/src/channels/lark/__tests__/aggregator.test.ts +23 -0
  16. package/src/channels/lark/__tests__/controller.test.ts +270 -0
  17. package/src/channels/lark/__tests__/mapping-service.test.ts +118 -0
  18. package/src/channels/lark/__tests__/parser.test.ts +72 -0
  19. package/src/channels/lark/__tests__/sender.test.ts +37 -0
  20. package/src/channels/lark/__tests__/verification.test.ts +157 -0
  21. package/src/channels/lark/aggregator.ts +16 -0
  22. package/src/channels/lark/config.ts +44 -0
  23. package/src/channels/lark/controller.ts +189 -0
  24. package/src/channels/lark/mapping-service.ts +138 -0
  25. package/src/channels/lark/parser.ts +68 -0
  26. package/src/channels/lark/routes.ts +121 -0
  27. package/src/channels/lark/runner.ts +37 -0
  28. package/src/channels/lark/sender.ts +58 -0
  29. package/src/channels/lark/types.ts +33 -0
  30. package/src/channels/lark/verification.ts +67 -0
  31. package/src/channels/routes.ts +25 -0
  32. package/src/controllers/channel-installations.ts +354 -0
  33. package/src/controllers/sandbox.ts +30 -80
  34. package/src/controllers/skills.ts +71 -321
  35. package/src/controllers/threads.ts +8 -6
  36. package/src/controllers/workspace.ts +64 -179
  37. package/src/index.ts +28 -5
  38. package/src/routes/channel-installations.ts +33 -0
  39. package/src/routes/index.ts +6 -0
  40. package/src/schemas/index.ts +2 -2
  41. package/src/services/sandbox_service.ts +21 -21
  42. 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 sandboxName = "global";
268
- const sandbox = await sandboxManager.createSandbox(sandboxName);
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.startsWith("/") ? filePath : `/${filePath}`;
332
+ const resolvedPath = filePath;
331
333
 
332
334
  if (workspace.storageType === "sandbox") {
333
335
  const sandboxManager = getSandBoxManager();
334
- const sandbox = await sandboxManager.createSandbox("global");
335
- const realPath = path.join("/home/gem/tenants", tenantId, "workspaces", workspaceId, projectId, resolvedPath);
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
- const downloadResult = await sandbox.file.downloadFile({
340
- path: realPath,
341
- });
342
-
343
- if (!downloadResult.ok) {
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.startsWith("/") ? filePath : `/${filePath}`;
394
+ const resolvedPath = filePath;
431
395
 
432
396
  if (workspace.storageType === "sandbox") {
433
397
  const sandboxManager = getSandBoxManager();
434
- const sandbox = await sandboxManager.createSandbox("global");
435
- const realPath = path.join("/home/gem/tenants", tenantId, "workspaces", workspaceId, projectId, resolvedPath);
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
- const downloadResult = await sandbox.file.downloadFile({
440
- path: realPath,
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
- if (typeof body?.stream === "function") {
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 stream detected, collecting for context injection`);
469
- // Collect stream content
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> (stream)`);
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> (stream)`);
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 (stream)`);
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(nodeStream);
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_./-]+$/.test(pathValue)) {
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 sandboxName = "global";
698
- const sandbox = await sandboxManager.createSandbox(sandboxName);
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(baseDir, pathValue, filename)
703
- : path.join(baseDir, filename);
704
-
705
- const uploadResult = await sandbox.file.uploadFile({
706
- file: buffer,
707
- path: realPath,
708
- }, { timeoutInSeconds: 300 });
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
- uploadResult.body?.data?.file_path?.replace(path.join("/home/gem/tenants", tenantId, "workspaces", workspaceId, projectId), "") ||
719
- (pathValue ? `/${pathValue}/${filename}` : `/${filename}`);
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
- const sandboxBaseURL = process.env.SANDBOX_BASE_URL || "http://localhost:8080";
244
- sandboxLatticeManager.registerLattice("default", {
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
+ }
@@ -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);
@@ -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
- isolatedLevel: {
832
+ vmIsolation: {
833
833
  type: "string",
834
- enum: ["agent", "thread", "global"],
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="isolatedLevel">-</span>
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('isolatedLevel').textContent = params.get('isolatedLevel') || '-';
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
- getFilesystemIsolatedLevel(tenantId: string, assistantId: string): "agent" | "thread" | "global" | null {
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?.isolatedLevel || null;
129
+ return config?.vmIsolation || null;
131
130
  }
132
131
 
133
132
  computeSandboxName(
134
133
  assistantId: string,
135
134
  threadId: string,
136
- isolatedLevel: string
135
+ vmIsolation: string,
136
+ tenantId?: string,
137
+ workspaceId?: string,
138
+ projectId?: string,
137
139
  ): string {
138
- let sandboxName: string;
139
-
140
- switch (isolatedLevel) {
140
+ switch (vmIsolation) {
141
141
  case "agent":
142
- sandboxName = assistantId;
143
- break;
144
- case "thread":
145
- sandboxName = threadId;
146
- break;
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
- sandboxName = "global";
150
- break;
149
+ return "global";
151
150
  }
151
+ }
152
152
 
153
- return normalizeSandboxName(sandboxName);
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
- const sandboxManager = getSandBoxManager()
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
- isolatedLevel: string,
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("{isolatedLevel}", isolatedLevel)
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
+ }