@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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +8 -0
- package/dist/index.js +134 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +124 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/controllers/sandbox.ts +235 -51
- package/src/index.ts +8 -0
- package/src/services/agent_service.ts +2 -0
- package/src/services/sandbox_service.ts +3 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/gateway",
|
|
3
|
-
"version": "2.1.
|
|
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.
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
306
|
+
// const sandboxConfig = sandboxService.getSandboxConfig(assistantId);
|
|
307
|
+
// if (!sandboxConfig) {
|
|
308
|
+
// return reply.status(404).send("Assistant not found");
|
|
309
|
+
// }
|
|
126
310
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
311
|
+
// const { isolatedLevel } = sandboxConfig;
|
|
312
|
+
// const sandboxName = sandboxService.computeSandboxName(
|
|
313
|
+
// assistantId,
|
|
314
|
+
// threadId,
|
|
315
|
+
// isolatedLevel
|
|
316
|
+
// );
|
|
133
317
|
|
|
134
|
-
|
|
135
|
-
|
|
318
|
+
// const targetPath = restPath ? `/vnc/${restPath}` : "/vnc/";
|
|
319
|
+
// const targetUrl = `${sandboxService.getTargetUrl(sandboxName)}${targetPath}`;
|
|
136
320
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
321
|
+
// try {
|
|
322
|
+
// const response = await fetch(targetUrl);
|
|
323
|
+
// const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
140
324
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
151
|
+
const sandboxManager = getSandBoxManager("default")
|
|
152
|
+
return `${sandboxManager.getBaseURL()}/sandbox/${sandboxName}`;
|
|
158
153
|
}
|
|
159
154
|
|
|
160
155
|
async getVncHtml(sandboxName: string): Promise<string> {
|