@axiom-lattice/gateway 2.1.22 → 2.1.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/gateway",
3
- "version": "2.1.22",
3
+ "version": "2.1.23",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -18,6 +18,7 @@
18
18
  "dependencies": {
19
19
  "@fastify/cors": "^11.0.0",
20
20
  "@fastify/http-proxy": "^9.5.0",
21
+ "@fastify/multipart": "^9.4.0",
21
22
  "@fastify/reply-from": "^12.5.0",
22
23
  "@fastify/sensible": "^6.0.3",
23
24
  "@fastify/swagger": "^9.5.1",
@@ -35,7 +36,7 @@
35
36
  "pino-roll": "^3.1.0",
36
37
  "redis": "^5.0.1",
37
38
  "uuid": "^9.0.1",
38
- "@axiom-lattice/core": "2.1.17",
39
+ "@axiom-lattice/core": "2.1.18",
39
40
  "@axiom-lattice/protocols": "2.1.11",
40
41
  "@axiom-lattice/queue-redis": "1.0.10"
41
42
  },
@@ -1,8 +1,37 @@
1
- import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
2
- // import { WebSocket, createWebSocketStream } from "ws";
3
- import { pipeline } from "stream/promises";
1
+ import { Readable } from "stream";
2
+ import { FastifyInstance } from "fastify";
4
3
  import { sandboxService } from "../services/sandbox_service";
5
- const SANDBOX_BASE_URL = process.env.SANDBOX_BASE_URL || "http://localhost:8080";
4
+ import { getSandBoxManager } from "@axiom-lattice/core";
5
+
6
+ /** Get filename from path (e.g. /home/gem/uploads/foo.pdf -> foo.pdf) */
7
+ function getFilenameFromPath(path: string): string {
8
+ const segments = path.replace(/\/+$/, "").split("/");
9
+ return segments[segments.length - 1] || "download";
10
+ }
11
+
12
+ /** Common extension -> MIME type for download response */
13
+ const EXT_TO_MIME: Record<string, string> = {
14
+ ".txt": "text/plain",
15
+ ".html": "text/html",
16
+ ".css": "text/css",
17
+ ".js": "application/javascript",
18
+ ".json": "application/json",
19
+ ".pdf": "application/pdf",
20
+ ".png": "image/png",
21
+ ".jpg": "image/jpeg",
22
+ ".jpeg": "image/jpeg",
23
+ ".gif": "image/gif",
24
+ ".webp": "image/webp",
25
+ ".svg": "image/svg+xml",
26
+ ".zip": "application/zip",
27
+ ".csv": "text/csv",
28
+ ".xml": "application/xml",
29
+ };
30
+
31
+ function getContentTypeFromFilename(filename: string): string {
32
+ const ext = filename.includes(".") ? filename.slice(filename.lastIndexOf(".")).toLowerCase() : "";
33
+ return EXT_TO_MIME[ext] ?? "application/octet-stream";
34
+ }
6
35
 
7
36
  interface SandboxParams {
8
37
  assistantId: string;
@@ -17,21 +46,17 @@ interface ResourceParams extends SandboxParams {
17
46
  resourcePath: string;
18
47
  }
19
48
 
20
- export async function registerSandboxProxyRoutes(app: FastifyInstance): Promise<void> {
21
- app.get<{ Params: SandboxParams }>(
22
- "/api/assistants/:assistantId/threads/:threadId/sandbox",
49
+ export function registerSandboxProxyRoutes(app: FastifyInstance): void {
50
+ // Register uploadfile route FIRST before wildcard routes to ensure it matches
51
+ app.post<{ Params: SandboxParams }>(
52
+ "/api/assistants/:assistantId/threads/:threadId/sandbox/uploadfile",
23
53
  async (request, reply) => {
54
+ console.log("[Sandbox Upload] Route matched:", request.url);
24
55
  const { assistantId, threadId } = request.params;
25
56
 
26
57
  const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
27
58
  if (!sandboxConfig) {
28
- const errorHtml = sandboxService.generateErrorHtml(
29
- assistantId,
30
- threadId,
31
- "unknown",
32
- `Assistant ${assistantId} not found`
33
- );
34
- return reply.status(404).type("text/html").send(errorHtml);
59
+ return reply.status(500).send({ error: "Assistant sandbox config not found" });
35
60
  }
36
61
 
37
62
  const { isolatedLevel } = sandboxConfig;
@@ -41,22 +66,181 @@ export async function registerSandboxProxyRoutes(app: FastifyInstance): Promise<
41
66
  isolatedLevel
42
67
  );
43
68
 
69
+ const sandboxManager = getSandBoxManager("default")
70
+ const sandbox = await sandboxManager.createSandbox(sandboxName)
71
+
44
72
  try {
45
- const html = await sandboxService.getVncHtml(sandboxName);
46
- const rewrittenHtml = sandboxService.rewriteHtml(html, assistantId, threadId);
47
- return reply.type("text/html").send(rewrittenHtml);
48
- } catch (error: any) {
49
- const errorHtml = sandboxService.generateErrorHtml(
50
- assistantId,
51
- threadId,
52
- isolatedLevel,
53
- error.message || "Failed to connect to sandbox"
54
- );
55
- return reply.status(502).type("text/html").send(errorHtml);
73
+ const data = await request.file();
74
+ if (!data) {
75
+ return reply.status(400).send({ error: "No file in request" });
76
+ }
77
+
78
+ const buffer = await data.toBuffer();
79
+ const pathEntry = data.fields?.path;
80
+ const pathValue =
81
+ pathEntry && typeof pathEntry === "object" && "value" in pathEntry
82
+ ? String((pathEntry as { value: unknown }).value)
83
+ : typeof pathEntry === "string"
84
+ ? pathEntry
85
+ : undefined;
86
+
87
+ const formData = new FormData();
88
+ formData.append("file", new Blob([buffer]), data.filename ?? "file");
89
+
90
+ const path = `/home/gem/uploads/${pathValue ? pathValue : ""}${data.filename}`;
91
+ const uploadResult = await sandbox.file.uploadFile({
92
+ file: buffer,
93
+ path: path,
94
+ })
95
+
96
+ if (!uploadResult.ok) {
97
+ return reply.status(502).send({ error: `Upload error: ${uploadResult.error}` });
98
+ }
99
+
100
+ const relativePath = uploadResult.body?.data?.file_path.replace(`/home/gem`, "");
101
+ const result = { id: relativePath, name: data.filename, size: buffer.length };
102
+
103
+ return reply.status(200).send({ message: "File uploaded successfully", ...result });
104
+ } catch (error: unknown) {
105
+ const message = error instanceof Error ? error.message : String(error);
106
+ return reply.status(502).send({ error: `Upload proxy error: ${message}` });
56
107
  }
57
108
  }
58
109
  );
59
110
 
111
+ // Download file from sandbox: GET with query param path (file path in sandbox, e.g. /home/gem/uploads/foo.txt)
112
+ app.get<{
113
+ Params: SandboxParams;
114
+ Querystring: { path: string };
115
+ }>(
116
+ "/api/assistants/:assistantId/threads/:threadId/sandbox/downloadfile",
117
+ async (request, reply) => {
118
+ const { assistantId, threadId } = request.params;
119
+ const { path: filePath } = request.query;
120
+
121
+ if (!filePath || typeof filePath !== "string") {
122
+ return reply.status(400).send({ error: "Query parameter 'path' is required" });
123
+ }
124
+
125
+ const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
126
+ if (!sandboxConfig) {
127
+ return reply.status(404).send({ error: "Assistant sandbox config not found" });
128
+ }
129
+
130
+ const { isolatedLevel } = sandboxConfig;
131
+ const sandboxName = sandboxService.computeSandboxName(
132
+ assistantId,
133
+ threadId,
134
+ isolatedLevel
135
+ );
136
+
137
+ const sandboxManager = getSandBoxManager("default");
138
+ const sandbox = await sandboxManager.createSandbox(sandboxName);
139
+
140
+ try {
141
+ // Resolve path: if relative (no leading /), prefix with /home/gem
142
+ const resolvedPath = filePath.startsWith("/home/gem") ? filePath : `/home/gem/${filePath.replace(/^\//, "")}`;
143
+ const filename = getFilenameFromPath(resolvedPath);
144
+ const inferredContentType = getContentTypeFromFilename(filename);
145
+
146
+ const downloadResult = await sandbox.file.downloadFile({
147
+ path: resolvedPath,
148
+ });
149
+
150
+ if (!downloadResult.ok) {
151
+ return reply.status(502).send({
152
+ error: `Download error: ${JSON.stringify(downloadResult.error)}`,
153
+ });
154
+ }
155
+
156
+ // Use body.stream() to get ReadableStream and pipe directly (no buffering)
157
+ const body = downloadResult.body as unknown as {
158
+ stream?: () => ReadableStream<Uint8Array>;
159
+ contentType?: string;
160
+ contentDisposition?: string;
161
+ };
162
+ if (typeof body?.stream === "function") {
163
+ const webStream = body.stream();
164
+ const nodeStream = Readable.fromWeb(webStream);
165
+ const contentType = body.contentType ?? inferredContentType;
166
+ const contentDisposition =
167
+ body.contentDisposition ?? `inline; filename="${filename.replace(/"/g, '\\"')}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
168
+ reply = reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(nodeStream);
169
+ return reply;
170
+ }
171
+
172
+ // Fallback: buffer body when stream is not available
173
+ const bodyUnknown = downloadResult.body as unknown;
174
+ let buf: Buffer;
175
+ let contentType = inferredContentType;
176
+ let contentDisposition = `inline; filename="${filename.replace(/"/g, '\\"')}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
177
+
178
+ if (bodyUnknown instanceof ArrayBuffer) {
179
+ buf = Buffer.from(bodyUnknown);
180
+ } else if (bodyUnknown instanceof Buffer) {
181
+ buf = bodyUnknown;
182
+ } else if (
183
+ bodyUnknown &&
184
+ typeof (bodyUnknown as { arrayBuffer?: () => Promise<ArrayBuffer> }).arrayBuffer === "function"
185
+ ) {
186
+ const res = bodyUnknown as { arrayBuffer: () => Promise<ArrayBuffer>; headers?: Headers };
187
+ buf = Buffer.from(await res.arrayBuffer());
188
+ if (res.headers?.get("content-type")) contentType = res.headers.get("content-type")!;
189
+ if (res.headers?.get("content-disposition")) contentDisposition = res.headers.get("content-disposition")!;
190
+ } else if (bodyUnknown && typeof (bodyUnknown as { blob?: () => Promise<Blob> }).blob === "function") {
191
+ const blob = await (bodyUnknown as { blob: () => Promise<Blob> }).blob();
192
+ buf = Buffer.from(await blob.arrayBuffer());
193
+ } else {
194
+ return reply.status(502).send({ error: "Unexpected download response format" });
195
+ }
196
+
197
+ reply = reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(buf);
198
+ return reply;
199
+ } catch (error: unknown) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ return reply.status(502).send({ error: `Download proxy error: ${message}` });
202
+ }
203
+ }
204
+ );
205
+
206
+ // app.get<{ Params: SandboxParams }>(
207
+ // "/api/assistants/:assistantId/threads/:threadId/sandbox/browser",
208
+ // async (request, reply) => {
209
+ // const { assistantId, threadId } = request.params;
210
+
211
+ // const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
212
+ // if (!sandboxConfig) {
213
+ // const errorHtml = sandboxService.generateErrorHtml(
214
+ // assistantId,
215
+ // threadId,
216
+ // "unknown",
217
+ // `Assistant ${assistantId} not found`
218
+ // );
219
+ // return reply.status(404).type("text/html").send(errorHtml);
220
+ // }
221
+
222
+ // const { isolatedLevel } = sandboxConfig;
223
+ // const sandboxName = sandboxService.computeSandboxName(
224
+ // assistantId,
225
+ // threadId,
226
+ // isolatedLevel
227
+ // );
228
+
229
+ // try {
230
+ // const html = await sandboxService.getVncHtml(sandboxName);
231
+ // const rewrittenHtml = sandboxService.rewriteHtml(html, assistantId, threadId);
232
+ // return reply.type("text/html").send(rewrittenHtml);
233
+ // } catch (error: any) {
234
+ // const errorHtml = sandboxService.generateErrorHtml(
235
+ // assistantId,
236
+ // threadId,
237
+ // isolatedLevel,
238
+ // error.message || "Failed to connect to sandbox"
239
+ // );
240
+ // return reply.status(502).type("text/html").send(errorHtml);
241
+ // }
242
+ // }
243
+ // );
60
244
 
61
245
 
62
246
  // app.get<{ Params: SandboxParams }>(
@@ -114,37 +298,37 @@ export async function registerSandboxProxyRoutes(app: FastifyInstance): Promise<
114
298
  // }
115
299
  // );
116
300
 
117
- app.get<{ Params: ProxyParams }>(
118
- "/api/assistants/:assistantId/threads/:threadId/sandbox/vnc/*",
119
- async (request, reply) => {
120
- const { assistantId, threadId, "*": restPath } = request.params;
301
+ // app.get<{ Params: ProxyParams }>(
302
+ // "/api/assistants/:assistantId/threads/:threadId/sandbox/browser/vnc/*",
303
+ // async (request, reply) => {
304
+ // const { assistantId, threadId, "*": restPath } = request.params;
121
305
 
122
- const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
123
- if (!sandboxConfig) {
124
- return reply.status(404).send("Assistant not found");
125
- }
306
+ // const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
307
+ // if (!sandboxConfig) {
308
+ // return reply.status(404).send("Assistant not found");
309
+ // }
126
310
 
127
- const { isolatedLevel } = sandboxConfig;
128
- const sandboxName = sandboxService.computeSandboxName(
129
- assistantId,
130
- threadId,
131
- isolatedLevel
132
- );
311
+ // const { isolatedLevel } = sandboxConfig;
312
+ // const sandboxName = sandboxService.computeSandboxName(
313
+ // assistantId,
314
+ // threadId,
315
+ // isolatedLevel
316
+ // );
133
317
 
134
- const targetPath = restPath ? `/vnc/${restPath}` : "/vnc/";
135
- const targetUrl = `${sandboxService.getTargetUrl(sandboxName)}${targetPath}`;
318
+ // const targetPath = restPath ? `/vnc/${restPath}` : "/vnc/";
319
+ // const targetUrl = `${sandboxService.getTargetUrl(sandboxName)}${targetPath}`;
136
320
 
137
- try {
138
- const response = await fetch(targetUrl);
139
- const contentType = response.headers.get("content-type") || "application/octet-stream";
321
+ // try {
322
+ // const response = await fetch(targetUrl);
323
+ // const contentType = response.headers.get("content-type") || "application/octet-stream";
140
324
 
141
- const body = await response.arrayBuffer();
142
- reply.status(response.status).type(contentType).send(Buffer.from(body));
143
- } catch (error: any) {
144
- reply.status(502).send(`Proxy error: ${error.message}`);
145
- }
146
- }
147
- );
325
+ // const body = await response.arrayBuffer();
326
+ // reply.status(response.status).type(contentType).send(Buffer.from(body));
327
+ // } catch (error: any) {
328
+ // reply.status(502).send(`Proxy error: ${error.message}`);
329
+ // }
330
+ // }
331
+ // );
148
332
 
149
333
 
150
334
  }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import fastify from "fastify";
2
2
  import cors from "@fastify/cors";
3
+ import multipart from "@fastify/multipart";
3
4
  import sensible from "@fastify/sensible";
4
5
  import websocket from "@fastify/websocket";
5
6
  import { registerLatticeRoutes } from "./routes";
@@ -114,11 +115,18 @@ app.register(cors, {
114
115
  "X-Requested-With",
115
116
  "x-tenant-id",
116
117
  "x-request-id",
118
+ "x-assistant-id",
119
+ "x-thread-id",
117
120
  ],
118
121
  exposedHeaders: ["Content-Type"],
119
122
  credentials: true,
120
123
  });
121
124
  app.register(sensible);
125
+ app.register(multipart, {
126
+ limits: {
127
+ fileSize: Number(process.env.BODY_LIMIT) || 50 * 1024 * 1024,
128
+ },
129
+ });
122
130
  app.register(websocket);
123
131
 
124
132
  // Error handler
@@ -76,6 +76,7 @@ export async function agent_invoke({
76
76
  "x-tenant-id": tenant_id,
77
77
  "x-request-id": run_id,
78
78
  "x-thread-id": thread_id,
79
+ "x-assistant-id": assistant_id,
79
80
  runConfig, // Inject runConfig for tools to access
80
81
  },
81
82
  recursionLimit: 200,
@@ -146,6 +147,7 @@ export async function agent_stream({
146
147
  "x-tenant-id": tenant_id,
147
148
  "x-request-id": run_id,
148
149
  "x-thread-id": thread_id,
150
+ "x-assistant-id": assistant_id,
149
151
  runConfig, // Inject runConfig for tools to access
150
152
  },
151
153
  streamMode: ["updates", "messages"],
@@ -1,7 +1,6 @@
1
- import { getAgentConfig, getAgentLattice, normalizeSandboxName } from "@axiom-lattice/core";
1
+ import { getAgentConfig, getAgentLattice, getSandBoxManager, normalizeSandboxName, sandboxLatticeManager } from "@axiom-lattice/core";
2
2
  import { ConnectedSandboxConfig } from "@axiom-lattice/protocols";
3
3
 
4
- const SANDBOX_BASE_URL = process.env.SANDBOX_BASE_URL || "http://localhost:8080";
5
4
 
6
5
  const ERROR_HTML = `<!DOCTYPE html>
7
6
  <html lang="zh-CN">
@@ -114,11 +113,6 @@ const ERROR_HTML = `<!DOCTYPE html>
114
113
  </html>`;
115
114
 
116
115
  export class SandboxService {
117
- private baseUrl: string;
118
-
119
- constructor(baseUrl?: string) {
120
- this.baseUrl = baseUrl || SANDBOX_BASE_URL;
121
- }
122
116
 
123
117
  getSandboxConfig(assistantId: string): ConnectedSandboxConfig | null {
124
118
  const agentConfig = getAgentConfig(assistantId);
@@ -154,7 +148,8 @@ export class SandboxService {
154
148
  }
155
149
 
156
150
  getTargetUrl(sandboxName: string): string {
157
- return `${this.baseUrl}/sandbox/${sandboxName}`;
151
+ const sandboxManager = getSandBoxManager("default")
152
+ return `${sandboxManager.getBaseURL()}/sandbox/${sandboxName}`;
158
153
  }
159
154
 
160
155
  async getVncHtml(sandboxName: string): Promise<string> {