@axiom-lattice/gateway 2.1.37 → 2.1.39

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.
@@ -52,6 +52,19 @@ export class WorkspaceController {
52
52
  }
53
53
 
54
54
  private getTenantId(request: FastifyRequest): string {
55
+ // First try to get from authenticated user context
56
+ const userTenantId = (request as any).user?.tenantId;
57
+ if (userTenantId) {
58
+ return userTenantId;
59
+ }
60
+
61
+ // Try to get from query parameters (for direct URL access like file downloads)
62
+ const queryTenantId = (request.query as any)?.tenantId;
63
+ if (queryTenantId) {
64
+ return String(queryTenantId);
65
+ }
66
+
67
+ // Fallback to request headers for backward compatibility
55
68
  return (request.headers["x-tenant-id"] as string) || "default";
56
69
  }
57
70
 
@@ -239,8 +252,7 @@ export class WorkspaceController {
239
252
 
240
253
  // ==================== File Operations ====================
241
254
 
242
- private async getBackend(workspaceId: string, projectId: string) {
243
- const tenantId = "default";
255
+ private async getBackend(tenantId: string, workspaceId: string, projectId: string) {
244
256
  const workspace = await this.workspaceStore.getWorkspaceById(
245
257
  tenantId,
246
258
  workspaceId
@@ -251,18 +263,22 @@ export class WorkspaceController {
251
263
  }
252
264
 
253
265
  if (workspace.storageType === "sandbox") {
254
- const sandboxManager = getSandBoxManager("default");
266
+ const sandboxManager = getSandBoxManager();
255
267
  const sandboxName = "global";
256
268
  const sandbox = await sandboxManager.createSandbox(sandboxName);
257
- return { backend: new SandboxFilesystem({
258
- sandboxInstance: sandbox,
259
- workingDirectory: `/workspaces/${workspaceId}/${projectId}`,
260
- }), workspace };
269
+ return {
270
+ backend: new SandboxFilesystem({
271
+ sandboxInstance: sandbox,
272
+ workingDirectory: `/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`,
273
+ }), workspace
274
+ };
261
275
  } else {
262
- return { backend: new FilesystemBackend({
263
- rootDir: `/lattice_store/workspaces/${workspaceId}/${projectId}`,
264
- virtualMode: true,
265
- }), workspace };
276
+ return {
277
+ backend: new FilesystemBackend({
278
+ rootDir: `/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`,
279
+ virtualMode: true,
280
+ }), workspace
281
+ };
266
282
  }
267
283
  }
268
284
 
@@ -301,6 +317,7 @@ export class WorkspaceController {
301
317
  }>,
302
318
  reply: FastifyReply
303
319
  ) {
320
+ const tenantId = this.getTenantId(request);
304
321
  const { workspaceId, projectId } = request.params;
305
322
  const filePath = request.query.path;
306
323
 
@@ -309,13 +326,13 @@ export class WorkspaceController {
309
326
  }
310
327
 
311
328
  try {
312
- const { workspace } = await this.getBackend(workspaceId, projectId);
329
+ const { workspace } = await this.getBackend(tenantId, workspaceId, projectId);
313
330
  const resolvedPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
314
331
 
315
332
  if (workspace.storageType === "sandbox") {
316
- const sandboxManager = getSandBoxManager("default");
333
+ const sandboxManager = getSandBoxManager();
317
334
  const sandbox = await sandboxManager.createSandbox("global");
318
- const realPath = path.join("/home/gem/workspaces", workspaceId, projectId, resolvedPath);
335
+ const realPath = path.join("/home/gem/tenants", tenantId, "workspaces", workspaceId, projectId, resolvedPath);
319
336
  const filename = this.getFilenameFromPath(resolvedPath);
320
337
  const inferredContentType = this.getMimeType(filename);
321
338
 
@@ -341,14 +358,14 @@ export class WorkspaceController {
341
358
  const nodeStream = Readable.fromWeb(webStream);
342
359
  const contentType = body.contentType ?? inferredContentType;
343
360
  const contentDisposition =
344
- body.contentDisposition ?? `inline; filename*=UTF-8''${encodeURIComponent(filename)}`;
361
+ body.contentDisposition ?? `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`;
345
362
  return reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(nodeStream);
346
363
  }
347
364
 
348
365
  const bodyUnknown = downloadResult.body as unknown;
349
366
  let buf: Buffer;
350
367
  let contentType = inferredContentType;
351
- let contentDisposition = `inline; filename*=UTF-8''${encodeURIComponent(filename)}`;
368
+ let contentDisposition = `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`;
352
369
 
353
370
  if (bodyUnknown instanceof ArrayBuffer) {
354
371
  buf = Buffer.from(bodyUnknown);
@@ -372,7 +389,7 @@ export class WorkspaceController {
372
389
  return reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(buf);
373
390
  }
374
391
 
375
- const { backend } = await this.getBackend(workspaceId, projectId);
392
+ const { backend } = await this.getBackend(tenantId, workspaceId, projectId);
376
393
  const content = await backend.read(resolvedPath, 0, Infinity);
377
394
  const filename = this.getFilenameFromPath(resolvedPath);
378
395
  const mimeType = this.getMimeType(filename);
@@ -381,7 +398,7 @@ export class WorkspaceController {
381
398
  return reply
382
399
  .status(200)
383
400
  .type(mimeType)
384
- .header("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(filename)}`)
401
+ .header("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`)
385
402
  .send(buffer);
386
403
  } catch (error: unknown) {
387
404
  const message = error instanceof Error ? error.message : String(error);
@@ -389,16 +406,122 @@ export class WorkspaceController {
389
406
  }
390
407
  }
391
408
 
409
+ /**
410
+ * View a file inline in the browser (not download).
411
+ * Returns file content with inline Content-Disposition for preview.
412
+ */
413
+ async viewFile(
414
+ request: FastifyRequest<{
415
+ Params: ProjectParams;
416
+ Querystring: { path: string };
417
+ }>,
418
+ reply: FastifyReply
419
+ ) {
420
+ const tenantId = this.getTenantId(request);
421
+ const { workspaceId, projectId } = request.params;
422
+ const filePath = request.query.path;
423
+
424
+ if (!filePath) {
425
+ return reply.status(400).send({ success: false, error: "Path is required" });
426
+ }
427
+
428
+ try {
429
+ const { workspace } = await this.getBackend(tenantId, workspaceId, projectId);
430
+ const resolvedPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
431
+
432
+ if (workspace.storageType === "sandbox") {
433
+ const sandboxManager = getSandBoxManager();
434
+ const sandbox = await sandboxManager.createSandbox("global");
435
+ const realPath = path.join("/home/gem/tenants", tenantId, "workspaces", workspaceId, projectId, resolvedPath);
436
+ const filename = this.getFilenameFromPath(resolvedPath);
437
+ const inferredContentType = this.getMimeType(filename);
438
+
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
+ };
455
+
456
+ if (typeof body?.stream === "function") {
457
+ const webStream = body.stream();
458
+ const nodeStream = Readable.fromWeb(webStream);
459
+ const contentType = body.contentType ?? inferredContentType;
460
+ // inline for viewing, not attachment for download
461
+ return reply
462
+ .status(200)
463
+ .type(contentType)
464
+ .header("Content-Disposition", "inline")
465
+ .send(nodeStream);
466
+ }
467
+
468
+ const bodyUnknown = downloadResult.body as unknown;
469
+ let buf: Buffer;
470
+ let contentType = inferredContentType;
471
+
472
+ if (bodyUnknown instanceof ArrayBuffer) {
473
+ buf = Buffer.from(bodyUnknown);
474
+ } else if (bodyUnknown instanceof Buffer) {
475
+ buf = bodyUnknown;
476
+ } else if (
477
+ bodyUnknown &&
478
+ typeof (bodyUnknown as { arrayBuffer?: () => Promise<ArrayBuffer> }).arrayBuffer === "function"
479
+ ) {
480
+ const res = bodyUnknown as { arrayBuffer: () => Promise<ArrayBuffer>; headers?: Headers };
481
+ buf = Buffer.from(await res.arrayBuffer());
482
+ if (res.headers?.get("content-type")) contentType = res.headers.get("content-type")!;
483
+ } else if (bodyUnknown && typeof (bodyUnknown as { blob?: () => Promise<Blob> }).blob === "function") {
484
+ const blob = await (bodyUnknown as { blob: () => Promise<Blob> }).blob();
485
+ buf = Buffer.from(await blob.arrayBuffer());
486
+ } else {
487
+ return reply.status(502).send({ success: false, error: "Unexpected view response format" });
488
+ }
489
+
490
+ return reply
491
+ .status(200)
492
+ .type(contentType)
493
+ .header("Content-Disposition", "inline")
494
+ .send(buf);
495
+ }
496
+
497
+ const { backend } = await this.getBackend(tenantId, workspaceId, projectId);
498
+ const content = await backend.read(resolvedPath, 0, Infinity);
499
+ const filename = this.getFilenameFromPath(resolvedPath);
500
+ const mimeType = this.getMimeType(filename);
501
+ const buffer = Buffer.from(content, "utf-8");
502
+
503
+ return reply
504
+ .status(200)
505
+ .type(mimeType)
506
+ .header("Content-Disposition", "inline")
507
+ .send(buffer);
508
+ } catch (error: unknown) {
509
+ const message = error instanceof Error ? error.message : String(error);
510
+ return reply.status(502).send({ success: false, error: `View proxy error: ${message}` });
511
+ }
512
+ }
513
+
392
514
  async listPath(
393
515
  request: FastifyRequest<{
394
516
  Params: ProjectParams;
395
517
  Querystring: ListPathQuery;
396
518
  }>
397
519
  ) {
520
+ const tenantId = this.getTenantId(request);
398
521
  const { workspaceId, projectId } = request.params;
399
522
  const path = (request.query.path as string) || "/";
400
523
 
401
- const { backend } = await this.getBackend(workspaceId, projectId);
524
+ const { backend } = await this.getBackend(tenantId, workspaceId, projectId);
402
525
  const files = await backend.lsInfo(path);
403
526
 
404
527
  return { success: true, data: files };
@@ -410,10 +533,11 @@ export class WorkspaceController {
410
533
  Querystring: ReadFileQuery;
411
534
  }>
412
535
  ) {
536
+ const tenantId = this.getTenantId(request);
413
537
  const { workspaceId, projectId } = request.params;
414
538
  const { path, offset = 0, limit = 1000 } = request.query;
415
539
 
416
- const { backend } = await this.getBackend(workspaceId, projectId);
540
+ const { backend } = await this.getBackend(tenantId, workspaceId, projectId);
417
541
  const content = await backend.read(path, Number(offset), Number(limit));
418
542
 
419
543
  return { success: true, data: { content, offset, limit } };
@@ -475,11 +599,11 @@ export class WorkspaceController {
475
599
  }
476
600
 
477
601
  if (workspace.storageType === "sandbox") {
478
- const sandboxManager = getSandBoxManager("default");
602
+ const sandboxManager = getSandBoxManager();
479
603
  const sandboxName = "global";
480
604
  const sandbox = await sandboxManager.createSandbox(sandboxName);
481
605
 
482
- const baseDir = path.join("/home/gem/workspaces", workspaceId, projectId);
606
+ const baseDir = path.join("/home/gem/tenants", tenantId, "workspaces", workspaceId, projectId);
483
607
  const realPath = pathValue
484
608
  ? path.join(baseDir, pathValue, filename)
485
609
  : path.join(baseDir, filename);
@@ -497,7 +621,7 @@ export class WorkspaceController {
497
621
  }
498
622
 
499
623
  const relativePath =
500
- uploadResult.body?.data?.file_path?.replace(path.join("/home/gem/workspaces", workspaceId, projectId), "") ||
624
+ uploadResult.body?.data?.file_path?.replace(path.join("/home/gem/tenants", tenantId, "workspaces", workspaceId, projectId), "") ||
501
625
  (pathValue ? `/${pathValue}/${filename}` : `/${filename}`);
502
626
 
503
627
  const result = {
@@ -511,8 +635,8 @@ export class WorkspaceController {
511
635
  return reply.status(200).send({ success: true, data: result });
512
636
  }
513
637
 
514
- // Filesystem storage: write under /lattice_store/workspaces/{workspaceId}/{projectId} and expose as virtual path "/<filename>"
515
- const rootDir = `/lattice_store/workspaces/${workspaceId}/${projectId}`;
638
+ // Filesystem storage: write under /lattice_store/tenants/{tenantId}/workspaces/{workspaceId}/{projectId} and expose as virtual path "/<filename>"
639
+ const rootDir = `/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`;
516
640
  const targetDir = pathValue ? path.join(rootDir, pathValue) : rootDir;
517
641
  const targetPath = path.join(targetDir, filename);
518
642
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
@@ -595,4 +719,8 @@ export function registerWorkspaceRoutes(app: FastifyInstance) {
595
719
  "/api/workspaces/:workspaceId/projects/:projectId/downloadfile",
596
720
  controller.downloadFile.bind(controller)
597
721
  );
722
+ app.get(
723
+ "/api/workspaces/:workspaceId/projects/:projectId/viewfile",
724
+ controller.viewFile.bind(controller)
725
+ );
598
726
  }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  registerLoggerLattice,
15
15
  getLoggerLattice,
16
16
  loggerLatticeManager,
17
+ sandboxLatticeManager,
17
18
  } from "@axiom-lattice/core";
18
19
  import {
19
20
  LoggerType,
@@ -196,6 +197,15 @@ const start = async (config?: LatticeGatewayConfig) => {
196
197
  // Access via: request.server.loggerLattice or app.loggerLattice
197
198
  app.decorate("loggerLattice", loggerLattice);
198
199
 
200
+ // Register sandbox manager if not already registered
201
+ if (!sandboxLatticeManager.hasLattice("default")) {
202
+ const sandboxBaseURL = process.env.SANDBOX_BASE_URL || "http://localhost:8080";
203
+ sandboxLatticeManager.registerLattice("default", {
204
+ baseURL: sandboxBaseURL,
205
+ });
206
+ logger.info(`Registered sandbox manager with baseURL: ${sandboxBaseURL}`);
207
+ }
208
+
199
209
  const target_port = config?.port || Number(process.env.PORT) || 4001;
200
210
 
201
211
  await app.listen({ port: target_port, host: "0.0.0.0" });
@@ -11,13 +11,28 @@ import { Command, CommandParams } from "@langchain/langgraph";
11
11
  import { v4 } from "uuid";
12
12
  import {
13
13
  getAgentClient,
14
- getAgentLattice,
14
+ agentLatticeManager,
15
15
  InMemoryChunkBuffer,
16
16
  registerChunkBuffer,
17
17
  getChunkBuffer,
18
18
  hasChunkBuffer,
19
19
  } from "@axiom-lattice/core";
20
20
 
21
+ /**
22
+ * Check if an agent exists for the given tenant and assistant ID
23
+ */
24
+ export async function checkAgentExists(
25
+ tenant_id: string,
26
+ assistant_id: string
27
+ ): Promise<boolean> {
28
+ try {
29
+ const agentLattice = agentLatticeManager.getAgentLatticeWithTenant(tenant_id, assistant_id);
30
+ return agentLattice !== undefined;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
21
36
  /**
22
37
  * Get or create the global ChunkBuffer instance
23
38
  */
@@ -53,7 +68,7 @@ export async function agent_invoke({
53
68
  command?: CommandParams<any>;
54
69
  custom_run_config?: Record<string, any>;
55
70
  }) {
56
- const agentLattice = getAgentLattice(assistant_id);
71
+ const agentLattice = agentLatticeManager.getAgentLatticeWithTenant(tenant_id, assistant_id);
57
72
  const runnable_agent = agentLattice?.client;
58
73
 
59
74
  const { message, ...rest } = input;
@@ -66,6 +81,7 @@ export async function agent_invoke({
66
81
  // Get runConfig from agent config and merge with custom_run_config
67
82
  const runConfig = {
68
83
  ...agentLattice?.config?.runConfig || {},
84
+ tenantId: tenant_id,
69
85
  workspaceId: workspace_id,
70
86
  projectId: project_id,
71
87
  ...custom_run_config || {},
@@ -124,8 +140,8 @@ export async function agent_stream({
124
140
  run_id?: string;
125
141
  custom_run_config?: Record<string, any>;
126
142
  }) {
127
- const runnable_agent = getAgentClient(assistant_id) as any;
128
- const agentLattice = getAgentLattice(assistant_id);
143
+ const runnable_agent = await getAgentClient(tenant_id, assistant_id);
144
+ const agentLattice = agentLatticeManager.getAgentLatticeWithTenant(tenant_id, assistant_id);
129
145
  const { message, ...rest } = input;
130
146
  let messages: BaseMessage[] = [];
131
147
  if (!command) {
@@ -139,6 +155,7 @@ export async function agent_stream({
139
155
  // Get runConfig from agent config and merge with custom_run_config
140
156
  const runConfig = {
141
157
  ...agentLattice?.config?.runConfig || {},
158
+ tenantId: tenant_id,
142
159
  workspaceId: workspace_id,
143
160
  projectId: project_id,
144
161
  ...custom_run_config || {},
@@ -150,7 +167,7 @@ export async function agent_stream({
150
167
  if (!runnable_agent) {
151
168
  throw new Error(`Agent ${assistant_id} not found`);
152
169
  }
153
- const agentStream = await runnable_agent.stream(
170
+ const agentStream = await (runnable_agent as any).stream(
154
171
  command
155
172
  ? new Command(command)
156
173
  : {
@@ -235,13 +252,15 @@ export async function agent_stream({
235
252
  }
236
253
 
237
254
  export async function agent_state({
238
- thread_id,
239
255
  assistant_id,
256
+ thread_id,
257
+ tenant_id,
240
258
  }: {
241
259
  assistant_id: string;
242
260
  thread_id: string;
261
+ tenant_id: string;
243
262
  }) {
244
- const runnable_agent = getAgentClient(assistant_id);
263
+ const runnable_agent = await getAgentClient(tenant_id, assistant_id);
245
264
  if (!runnable_agent) {
246
265
  throw new Error(`Agent ${assistant_id} not found`);
247
266
  }
@@ -260,7 +279,7 @@ export async function agent_messages({
260
279
  thread_id: string;
261
280
  tenant_id: string;
262
281
  }) {
263
- const runnable_agent = getAgentClient(assistant_id);
282
+ const runnable_agent = await getAgentClient(tenant_id, assistant_id);
264
283
  if (!runnable_agent) {
265
284
  throw new Error(`Agent ${assistant_id} not found`);
266
285
  }
@@ -298,8 +317,8 @@ export async function agent_messages({
298
317
  return new_messages;
299
318
  }
300
319
 
301
- export async function draw_graph(assistant_id: string) {
302
- const runnable_agent = getAgentClient(assistant_id);
320
+ export async function draw_graph(assistant_id: string, tenant_id: string) {
321
+ const runnable_agent = await getAgentClient(tenant_id, assistant_id);
303
322
  if (!runnable_agent) {
304
323
  throw new Error(`Agent ${assistant_id} not found`);
305
324
  }
@@ -120,7 +120,7 @@ const handleAgentTask = async (
120
120
 
121
121
  // Stream has ended successfully
122
122
  if (callback_event) {
123
- const state = await agent_state({ assistant_id, thread_id });
123
+ const state = await agent_state({ assistant_id, thread_id, tenant_id });
124
124
  eventBus.publish(callback_event, {
125
125
  success: true,
126
126
  state: state,
@@ -137,7 +137,7 @@ const handleAgentTask = async (
137
137
  await response.text(); // Consume the response
138
138
 
139
139
  if (callback_event) {
140
- const state = await agent_state({ assistant_id, thread_id });
140
+ const state = await agent_state({ assistant_id, thread_id, tenant_id });
141
141
  eventBus.publish(callback_event, {
142
142
  success: true,
143
143
  state,
@@ -1,5 +1,5 @@
1
- import { getAgentConfig, getAgentLattice, getSandBoxManager, normalizeSandboxName, sandboxLatticeManager } from "@axiom-lattice/core";
2
- import { ConnectedSandboxConfig, SandboxMiddlewareConfig } from "@axiom-lattice/protocols";
1
+ import { agentLatticeManager, getSandBoxManager, normalizeSandboxName, sandboxLatticeManager } from "@axiom-lattice/core";
2
+ import { SandboxMiddlewareConfig } from "@axiom-lattice/protocols";
3
3
 
4
4
 
5
5
  const ERROR_HTML = `<!DOCTYPE html>
@@ -114,19 +114,20 @@ const ERROR_HTML = `<!DOCTYPE html>
114
114
 
115
115
  export class SandboxService {
116
116
 
117
- getFilesystemIsolatedLevel(assistantId: string): "agent" | "thread" | "global" | null {
118
- const agentConfig = getAgentConfig(assistantId);
119
- if (!agentConfig) {
117
+ getFilesystemIsolatedLevel(tenantId: string, assistantId: string): "agent" | "thread" | "global" | null {
118
+ const agentLattice = agentLatticeManager.getAgentLatticeWithTenant(tenantId, assistantId);
119
+ if (!agentLattice) {
120
120
  return null;
121
121
  }
122
122
 
123
- const agentLattice = getAgentLattice(assistantId);
124
- const filesystemConfig = agentLattice?.config?.middleware?.find(m => m.type === "filesystem");
123
+ const filesystemConfig = agentLattice?.config?.middleware?.find((m: any) => m.type === "filesystem");
125
124
  if (!filesystemConfig) {
126
125
  return null;
127
126
  }
128
127
 
129
- return filesystemConfig.config?.isolatedLevel || null;
128
+ // Type guard for SandboxMiddlewareConfig which has isolatedLevel
129
+ const config = filesystemConfig.config as SandboxMiddlewareConfig;
130
+ return config?.isolatedLevel || null;
130
131
  }
131
132
 
132
133
  computeSandboxName(
@@ -153,7 +154,7 @@ export class SandboxService {
153
154
  }
154
155
 
155
156
  getTargetUrl(sandboxName: string): string {
156
- const sandboxManager = getSandBoxManager("default")
157
+ const sandboxManager = getSandBoxManager()
157
158
  return `${sandboxManager.getBaseURL()}/sandbox/${sandboxName}`;
158
159
  }
159
160