@axiom-lattice/opensandbox-gateway 0.2.9 → 0.2.11
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 +6 -6
- package/CHANGELOG.md +17 -0
- package/README.md +4 -4
- package/deploy/OFFLINE_DEPLOYMENT.md +1 -1
- package/deploy/README.md +2 -2
- package/dist/{chunk-M6FKTT7J.mjs → chunk-QC7CI5UY.mjs} +263 -9
- package/dist/chunk-QC7CI5UY.mjs.map +1 -0
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +73 -10
- package/dist/index.mjs +1 -1
- package/jest.config.js +11 -0
- package/package.json +7 -2
- package/scripts/resident-service-e2e.ts +139 -0
- package/src/__tests__/OpenSandboxRuntimeService.test.ts +152 -0
- package/src/__tests__/app.test.ts +151 -0
- package/src/__tests__/schemas.test.ts +31 -0
- package/src/app.ts +3 -1
- package/src/controllers/sandbox.ts +57 -0
- package/src/lib/proxy.ts +152 -0
- package/src/routes/sandbox.ts +32 -0
- package/src/schemas/sandbox.ts +12 -0
- package/src/services/OpenSandboxRuntimeService.ts +128 -10
- package/src/types/runtime-service.ts +5 -0
- package/tsconfig.json +1 -1
- package/dist/chunk-M6FKTT7J.mjs.map +0 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @axiom-lattice/opensandbox-gateway@0.2.
|
|
2
|
+
> @axiom-lattice/opensandbox-gateway@0.2.11 build /home/runner/work/agentic/agentic/packages/opensandbox-gateway
|
|
3
3
|
> tsup src/index.ts src/cli.ts --format esm --dts --clean --sourcemap
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
[34mESM[39m Build start
|
|
11
11
|
[32mESM[39m [1mdist/cli.mjs [22m[32m245.00 B[39m
|
|
12
12
|
[32mESM[39m [1mdist/index.mjs [22m[32m196.00 B[39m
|
|
13
|
-
[32mESM[39m [1mdist/chunk-
|
|
13
|
+
[32mESM[39m [1mdist/chunk-QC7CI5UY.mjs [22m[32m47.41 KB[39m
|
|
14
14
|
[32mESM[39m [1mdist/cli.mjs.map [22m[32m387.00 B[39m
|
|
15
15
|
[32mESM[39m [1mdist/index.mjs.map [22m[32m71.00 B[39m
|
|
16
|
-
[32mESM[39m [1mdist/chunk-
|
|
17
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
+
[32mESM[39m [1mdist/chunk-QC7CI5UY.mjs.map [22m[32m99.42 KB[39m
|
|
17
|
+
[32mESM[39m ⚡️ Build success in 166ms
|
|
18
18
|
[34mDTS[39m Build start
|
|
19
|
-
[32mDTS[39m ⚡️ Build success in
|
|
19
|
+
[32mDTS[39m ⚡️ Build success in 18414ms
|
|
20
20
|
[32mDTS[39m [1mdist/cli.d.mts [22m[32m20.00 B[39m
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m13.25 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @axiom-lattice/opensandbox-gateway
|
|
2
2
|
|
|
3
|
+
## 0.2.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a19e0aa: Unify the sandbox service base-URL env and treat missing sandbox files as a structured not-found (ADR-174 / ADR-175).
|
|
8
|
+
|
|
9
|
+
The resident plugin deployment runtime now reads `MICROSANDBOX_SERVICE_BASE_URL` only (dropping `MICROSANDBOX_SERVICE_URL`), so a provider-only config also enables `manage_plugin` deploy/undeploy/status; `sandbox-console` reads `VITE_MICROSANDBOX_SERVICE_BASE_URL`. `MICROSANDBOX_SERVICE_URL` no longer has any effect.
|
|
10
|
+
|
|
11
|
+
`opensandbox-gateway` now preserves the sandbox's upstream HTTP status (and surfaces execd's `{ code, message }`) instead of collapsing every failure to `500 "Download failed"`, and `MicrosandboxServiceClient` attaches the HTTP status to request errors; `microsandbox-service` `readFile`/`downloadFile` map missing files to `404 FILE_NOT_FOUND` (matching `deleteFile`). A missing sandbox file (e.g. the plugin-skill manifest) is therefore classified as not-found rather than a hard failure, fixing `Error loading skill content: ... Download failed`.
|
|
12
|
+
|
|
13
|
+
## 0.2.10
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 26fecff: task refactor
|
|
18
|
+
- 910bd08: enhance tasks
|
|
19
|
+
|
|
3
20
|
## 0.2.9
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -193,10 +193,10 @@ lattice-opensandbox-gateway
|
|
|
193
193
|
|
|
194
194
|
## Connecting from Core
|
|
195
195
|
|
|
196
|
-
Set the same `
|
|
196
|
+
Set the same `MICROSANDBOX_SERVICE_BASE_URL` used for microsandbox-service:
|
|
197
197
|
|
|
198
198
|
```bash
|
|
199
|
-
|
|
199
|
+
MICROSANDBOX_SERVICE_BASE_URL=http://localhost:4002
|
|
200
200
|
```
|
|
201
201
|
|
|
202
202
|
The `MicrosandboxRemoteProvider` → `MicrosandboxServiceClient` → HTTP → gateway chain works identically.
|
|
@@ -254,7 +254,7 @@ Deploys the full sandbox stack (`opensandbox-server` + `opensandbox-gateway`) wi
|
|
|
254
254
|
|
|
255
255
|
```
|
|
256
256
|
Core (agent) Docker host
|
|
257
|
-
│
|
|
257
|
+
│ MICROSANDBOX_SERVICE_BASE_URL │
|
|
258
258
|
▼ │
|
|
259
259
|
opensandbox-gateway ──HTTP──▶ opensandbox-server ──docker.sock──▶ sandbox containers
|
|
260
260
|
(Node Fastify, :4002) (Python FastAPI, :8080)
|
|
@@ -299,7 +299,7 @@ curl -H "Authorization: Bearer $SANDBOX_API_KEY" http://localhost:4002/api/sandb
|
|
|
299
299
|
On the agent process, set:
|
|
300
300
|
|
|
301
301
|
```bash
|
|
302
|
-
|
|
302
|
+
MICROSANDBOX_SERVICE_BASE_URL=http://localhost:4002
|
|
303
303
|
MICROSANDBOX_API_KEY=<same as SANDBOX_API_KEY>
|
|
304
304
|
MICROSANDBOX_IMAGE=kioko12520/sandbox:0.1.0 # optional, dynamic sandbox image
|
|
305
305
|
```
|
|
@@ -69,7 +69,7 @@ curl -X POST "http://localhost:4002/api/files/read" \
|
|
|
69
69
|
## Connect the agent (Core)
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
|
|
72
|
+
MICROSANDBOX_SERVICE_BASE_URL=http://localhost:4002
|
|
73
73
|
MICROSANDBOX_API_KEY=<same as SANDBOX_API_KEY>
|
|
74
74
|
MICROSANDBOX_IMAGE=kioko12520/sandbox:0.1.0
|
|
75
75
|
```
|
package/deploy/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Self-contained Docker Compose deployment for the Axiom Lattice OpenSandbox sandb
|
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
Core (agent) Docker host
|
|
17
|
-
│
|
|
17
|
+
│ MICROSANDBOX_SERVICE_BASE_URL │
|
|
18
18
|
▼ ▼
|
|
19
19
|
opensandbox-gateway ──HTTP──▶ opensandbox-server ──docker.sock──▶ sandbox containers
|
|
20
20
|
(Node Fastify, :4002) (Python FastAPI, :8080)
|
|
@@ -40,7 +40,7 @@ curl http://localhost:4002/health # gateway
|
|
|
40
40
|
Then on the agent process:
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
|
|
43
|
+
MICROSANDBOX_SERVICE_BASE_URL=http://localhost:4002
|
|
44
44
|
MICROSANDBOX_API_KEY=<same as SANDBOX_API_KEY>
|
|
45
45
|
MICROSANDBOX_IMAGE=kioko12520/sandbox:0.1.0 # optional; dynamic business sandbox image
|
|
46
46
|
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/services/OpenSandboxRuntimeService.ts
|
|
2
2
|
import { ConnectionConfig, Sandbox } from "@alibaba-group/opensandbox";
|
|
3
3
|
import { SandboxManager } from "@alibaba-group/opensandbox";
|
|
4
|
-
import { SandboxException } from "@alibaba-group/opensandbox";
|
|
4
|
+
import { SandboxApiException, SandboxException } from "@alibaba-group/opensandbox";
|
|
5
5
|
|
|
6
6
|
// src/lib/errors.ts
|
|
7
7
|
var HttpError = class extends Error {
|
|
@@ -57,6 +57,37 @@ function getEnvNumber(key, fallback) {
|
|
|
57
57
|
const v = process.env[key];
|
|
58
58
|
return v ? Number(v) : fallback;
|
|
59
59
|
}
|
|
60
|
+
function isHttpStatus(value) {
|
|
61
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 400 && value <= 599;
|
|
62
|
+
}
|
|
63
|
+
function mapSandboxErrorCodeToStatus(code) {
|
|
64
|
+
switch (code) {
|
|
65
|
+
case "NOT_FOUND":
|
|
66
|
+
case "FILE_NOT_FOUND":
|
|
67
|
+
return 404;
|
|
68
|
+
case "FORBIDDEN":
|
|
69
|
+
return 403;
|
|
70
|
+
case "CONFLICT":
|
|
71
|
+
return 409;
|
|
72
|
+
case "INVALID_REQUEST":
|
|
73
|
+
case "INVALID_ARGUMENT":
|
|
74
|
+
return 400;
|
|
75
|
+
default:
|
|
76
|
+
return 500;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function parseUpstreamError(rawBody) {
|
|
80
|
+
if (typeof rawBody !== "string" || rawBody.length === 0) return {};
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(rawBody);
|
|
83
|
+
return {
|
|
84
|
+
code: typeof parsed.code === "string" ? parsed.code : void 0,
|
|
85
|
+
message: typeof parsed.message === "string" ? parsed.message : void 0
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
60
91
|
var OpenSandboxRuntimeService = class {
|
|
61
92
|
constructor() {
|
|
62
93
|
this.cache = /* @__PURE__ */ new Map();
|
|
@@ -166,7 +197,10 @@ var OpenSandboxRuntimeService = class {
|
|
|
166
197
|
const sandbox = await Sandbox.create({
|
|
167
198
|
connectionConfig: this.connectionConfig,
|
|
168
199
|
image: input.image ?? this.defaultImage,
|
|
169
|
-
|
|
200
|
+
// `null` means no expiry; `undefined` falls back to the service default.
|
|
201
|
+
// Parity with microsandbox: lifecycle is governed by the server TTL here
|
|
202
|
+
// (there is no local idle sweeper), so `null` already means "never expire".
|
|
203
|
+
timeoutSeconds: input.timeoutSeconds === null ? null : input.timeoutSeconds ?? this.defaultTimeout,
|
|
170
204
|
readyTimeoutSeconds: 1800,
|
|
171
205
|
resource: {
|
|
172
206
|
cpu: String(input.cpus ?? this.defaultCpus),
|
|
@@ -175,7 +209,7 @@ var OpenSandboxRuntimeService = class {
|
|
|
175
209
|
env: input.env ?? {},
|
|
176
210
|
metadata: { [NAME_METADATA_KEY]: name },
|
|
177
211
|
volumes: this.mapVolumes(input.volumes),
|
|
178
|
-
entrypoint: ["tail", "-f", "/dev/null"]
|
|
212
|
+
entrypoint: input.entrypoint ?? ["tail", "-f", "/dev/null"]
|
|
179
213
|
});
|
|
180
214
|
const info = await sandbox.getInfo();
|
|
181
215
|
const entry = {
|
|
@@ -190,10 +224,12 @@ var OpenSandboxRuntimeService = class {
|
|
|
190
224
|
wrapError(err, code, message) {
|
|
191
225
|
if (err instanceof HttpError) throw err;
|
|
192
226
|
if (err instanceof SandboxException) {
|
|
227
|
+
const upstream = err instanceof SandboxApiException ? parseUpstreamError(err.rawBody) : {};
|
|
228
|
+
const status = err instanceof SandboxApiException && isHttpStatus(err.statusCode) ? err.statusCode : mapSandboxErrorCodeToStatus(upstream.code ?? err.error.code);
|
|
193
229
|
throw new HttpError(
|
|
194
|
-
|
|
195
|
-
err.error.code,
|
|
196
|
-
err.error.message ?? message
|
|
230
|
+
status,
|
|
231
|
+
upstream.code ?? err.error.code,
|
|
232
|
+
upstream.message ?? err.error.message ?? message
|
|
197
233
|
);
|
|
198
234
|
}
|
|
199
235
|
throw new HttpError(500, code, message);
|
|
@@ -268,6 +304,64 @@ var OpenSandboxRuntimeService = class {
|
|
|
268
304
|
}
|
|
269
305
|
return { name, status: "unknown" };
|
|
270
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Resolve the relative proxy path for a guest port.
|
|
309
|
+
*
|
|
310
|
+
* The returned path is content-addressed to this service (never the sandbox
|
|
311
|
+
* host) so callers compose `${MICROSANDBOX_SERVICE_BASE_URL}${endpoint}` exactly
|
|
312
|
+
* like the microsandbox-service contract. The sandbox must exist, but the
|
|
313
|
+
* guest port is resolved on demand by {@link resolveProxyTarget}.
|
|
314
|
+
*
|
|
315
|
+
* @param name - Sandbox name
|
|
316
|
+
* @param port - Guest port
|
|
317
|
+
* @returns The relative proxy path
|
|
318
|
+
* @throws {HttpError} 404 `SANDBOX_NOT_FOUND` when the sandbox does not exist
|
|
319
|
+
*/
|
|
320
|
+
async getEndpoint(name, port) {
|
|
321
|
+
await this.resolveEntry(name);
|
|
322
|
+
return { endpoint: `/api/sandboxes/${encodeURIComponent(name)}/proxy/${port}` };
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Resolve the absolute upstream URL the sandbox proxy dials for a guest port.
|
|
326
|
+
*
|
|
327
|
+
* The SDK endpoint is scheme-less (e.g. `localhost:44772`); a missing scheme
|
|
328
|
+
* is prefixed with `http://`, while an already-absolute endpoint is preserved.
|
|
329
|
+
* Endpoint-specific headers returned by the SDK are surfaced so the proxy can
|
|
330
|
+
* forward them upstream.
|
|
331
|
+
*
|
|
332
|
+
* @param name - Sandbox name
|
|
333
|
+
* @param port - Guest port
|
|
334
|
+
* @returns The absolute upstream target and any required headers
|
|
335
|
+
* @throws {HttpError} 404 `SANDBOX_NOT_FOUND` when the sandbox does not exist
|
|
336
|
+
*/
|
|
337
|
+
async resolveProxyTarget(name, port) {
|
|
338
|
+
const entry = await this.resolveEntry(name);
|
|
339
|
+
try {
|
|
340
|
+
const endpoint = await entry.sandbox.getEndpoint(port);
|
|
341
|
+
const target = endpoint.endpoint.includes("://") ? endpoint.endpoint : `http://${endpoint.endpoint}`;
|
|
342
|
+
return { target, headers: endpoint.headers };
|
|
343
|
+
} catch (err) {
|
|
344
|
+
throw this.wrapError(
|
|
345
|
+
err,
|
|
346
|
+
"ENDPOINT_NOT_FOUND",
|
|
347
|
+
`Endpoint for port ${port} not found on '${name}'`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Resolve a cached sandbox entry, recovering it from server metadata when the
|
|
353
|
+
* local cache is cold.
|
|
354
|
+
*
|
|
355
|
+
* @param name - Sandbox name
|
|
356
|
+
* @throws {HttpError} 404 `SANDBOX_NOT_FOUND` when no sandbox matches the name
|
|
357
|
+
*/
|
|
358
|
+
async resolveEntry(name) {
|
|
359
|
+
const entry = this.cache.get(name) ?? await this.findByNameViaMetadata(name);
|
|
360
|
+
if (!entry) {
|
|
361
|
+
throw new HttpError(404, "SANDBOX_NOT_FOUND", `Sandbox '${name}' not found`);
|
|
362
|
+
}
|
|
363
|
+
return entry;
|
|
364
|
+
}
|
|
271
365
|
resolvePath(path3) {
|
|
272
366
|
if (path3 === "~" || path3 === "~/") return "/";
|
|
273
367
|
if (path3.startsWith("~/")) return `/${path3.slice(2)}`;
|
|
@@ -575,6 +669,9 @@ function registerImageRoutes(app, imageService) {
|
|
|
575
669
|
app.delete("/api/images/detail", controller.deleteImage);
|
|
576
670
|
}
|
|
577
671
|
|
|
672
|
+
// src/routes/sandbox.ts
|
|
673
|
+
import { Transform } from "stream";
|
|
674
|
+
|
|
578
675
|
// src/schemas/sandbox.ts
|
|
579
676
|
import z2 from "zod";
|
|
580
677
|
var bindMountSchema = z2.object({
|
|
@@ -601,7 +698,17 @@ var ensureSandboxSchema = z2.object({
|
|
|
601
698
|
cpus: z2.number().int().positive().optional(),
|
|
602
699
|
memoryMib: z2.number().int().positive().optional(),
|
|
603
700
|
env: z2.record(z2.string()).optional(),
|
|
604
|
-
volumes: z2.record(volumeSchema).optional()
|
|
701
|
+
volumes: z2.record(volumeSchema).optional(),
|
|
702
|
+
/** Main process command; replaces the default `tail -f /dev/null`. */
|
|
703
|
+
entrypoint: z2.array(z2.string().min(1)).nonempty().optional(),
|
|
704
|
+
/** Absolute lifetime in seconds; `null` means no expiry. */
|
|
705
|
+
timeoutSeconds: z2.number().int().positive().nullable().optional(),
|
|
706
|
+
/** Guest ports to publish (resolved on demand by the server proxy). */
|
|
707
|
+
exposePorts: z2.array(z2.number().int().positive().max(65535)).optional()
|
|
708
|
+
});
|
|
709
|
+
var sandboxEndpointParamsSchema = z2.object({
|
|
710
|
+
name: z2.string().min(1),
|
|
711
|
+
port: z2.coerce.number().int().positive().max(65535)
|
|
605
712
|
});
|
|
606
713
|
var sandboxNameParamsSchema = z2.object({
|
|
607
714
|
name: z2.string().min(1)
|
|
@@ -652,6 +759,90 @@ var sandboxLogsSchema = z2.object({
|
|
|
652
759
|
sources: z2.array(z2.string()).optional()
|
|
653
760
|
});
|
|
654
761
|
|
|
762
|
+
// src/lib/proxy.ts
|
|
763
|
+
import http from "http";
|
|
764
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
765
|
+
"connection",
|
|
766
|
+
"keep-alive",
|
|
767
|
+
"proxy-authenticate",
|
|
768
|
+
"proxy-authorization",
|
|
769
|
+
"te",
|
|
770
|
+
"trailer",
|
|
771
|
+
"transfer-encoding",
|
|
772
|
+
"upgrade"
|
|
773
|
+
]);
|
|
774
|
+
function isHopByHop(header) {
|
|
775
|
+
return HOP_BY_HOP_HEADERS.has(header) || header.startsWith("proxy-");
|
|
776
|
+
}
|
|
777
|
+
function buildProxyHeaders(incoming, requestPath) {
|
|
778
|
+
void requestPath;
|
|
779
|
+
const headers = {};
|
|
780
|
+
for (const [rawKey, rawValue] of Object.entries(incoming)) {
|
|
781
|
+
if (rawValue === void 0) continue;
|
|
782
|
+
const key = rawKey.toLowerCase();
|
|
783
|
+
if (isHopByHop(key)) continue;
|
|
784
|
+
if (key === "host" || key === "content-length") continue;
|
|
785
|
+
if (key === "authorization") continue;
|
|
786
|
+
headers[key] = Array.isArray(rawValue) ? rawValue.join(", ") : rawValue;
|
|
787
|
+
}
|
|
788
|
+
return headers;
|
|
789
|
+
}
|
|
790
|
+
function parseTarget(target) {
|
|
791
|
+
const url = new URL(target.includes("://") ? target : `http://${target}`);
|
|
792
|
+
const basePath = url.pathname === "/" ? "" : url.pathname.replace(/\/$/, "");
|
|
793
|
+
return {
|
|
794
|
+
hostname: url.hostname,
|
|
795
|
+
port: url.port.length > 0 ? Number(url.port) : url.protocol === "https:" ? 443 : 80,
|
|
796
|
+
basePath
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function toProxyError(error) {
|
|
800
|
+
if (error instanceof HttpError) return error;
|
|
801
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
802
|
+
return new HttpError(502, "PROXY_ERROR", message);
|
|
803
|
+
}
|
|
804
|
+
function filterResponseHeaders(headers) {
|
|
805
|
+
const filtered = {};
|
|
806
|
+
for (const [rawKey, rawValue] of Object.entries(headers)) {
|
|
807
|
+
if (rawValue === void 0) continue;
|
|
808
|
+
const key = rawKey.toLowerCase();
|
|
809
|
+
if (isHopByHop(key)) continue;
|
|
810
|
+
filtered[key] = rawValue;
|
|
811
|
+
}
|
|
812
|
+
return filtered;
|
|
813
|
+
}
|
|
814
|
+
function pipeRequestUpstream(options) {
|
|
815
|
+
const { target, method, path: path3, headers, body, reply } = options;
|
|
816
|
+
const { hostname, port, basePath } = parseTarget(target);
|
|
817
|
+
return new Promise((resolve, reject) => {
|
|
818
|
+
const upstream = http.request(
|
|
819
|
+
{ hostname, port, method, path: `${basePath}${path3}`, headers },
|
|
820
|
+
(upstreamResponse) => {
|
|
821
|
+
try {
|
|
822
|
+
reply.code(upstreamResponse.statusCode ?? 502);
|
|
823
|
+
reply.headers(filterResponseHeaders(upstreamResponse.headers));
|
|
824
|
+
reply.send(upstreamResponse);
|
|
825
|
+
resolve();
|
|
826
|
+
} catch (error) {
|
|
827
|
+
upstreamResponse.destroy();
|
|
828
|
+
reject(toProxyError(error));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
);
|
|
832
|
+
upstream.on("error", (error) => reject(toProxyError(error)));
|
|
833
|
+
body.on("error", (error) => {
|
|
834
|
+
upstream.destroy();
|
|
835
|
+
reject(toProxyError(error));
|
|
836
|
+
});
|
|
837
|
+
const source = body;
|
|
838
|
+
if (source.readableEnded === true || source.destroyed === true) {
|
|
839
|
+
upstream.end();
|
|
840
|
+
} else {
|
|
841
|
+
body.pipe(upstream);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
655
846
|
// src/controllers/sandbox.ts
|
|
656
847
|
function createSandboxController(runtimeService) {
|
|
657
848
|
return {
|
|
@@ -687,6 +878,48 @@ function createSandboxController(runtimeService) {
|
|
|
687
878
|
getStatus: async (request, reply) => {
|
|
688
879
|
return reply.send(ok(await runtimeService.getStatus(request.params.name)));
|
|
689
880
|
},
|
|
881
|
+
getEndpoint: async (request, reply) => {
|
|
882
|
+
const parsed = sandboxEndpointParamsSchema.safeParse(request.params ?? {});
|
|
883
|
+
if (!parsed.success) {
|
|
884
|
+
throw new HttpError(400, "INVALID_REQUEST", parsed.error.message);
|
|
885
|
+
}
|
|
886
|
+
return reply.send(ok(await runtimeService.getEndpoint(parsed.data.name, parsed.data.port)));
|
|
887
|
+
},
|
|
888
|
+
/**
|
|
889
|
+
* Stream a request through to a sandbox's guest port.
|
|
890
|
+
*
|
|
891
|
+
* The caller authenticates to this service with its `Authorization` header;
|
|
892
|
+
* {@link buildProxyHeaders} strips that service key so it is not forwarded
|
|
893
|
+
* to the (unauthenticated) sandbox host. Endpoint headers returned by the
|
|
894
|
+
* SDK are merged on top so routing/credential headers reach the sandbox.
|
|
895
|
+
* The live request stream (body-limited by the scoped parser, or the raw
|
|
896
|
+
* request for bodyless methods) is piped upstream without buffering.
|
|
897
|
+
*
|
|
898
|
+
* @throws {HttpError} 404 `SANDBOX_NOT_FOUND` when the sandbox does not exist,
|
|
899
|
+
* or 413 `PAYLOAD_TOO_LARGE` when the streaming body exceeds `bodyLimit`.
|
|
900
|
+
*/
|
|
901
|
+
proxyRequest: async (request, reply) => {
|
|
902
|
+
const { name, port } = request.params;
|
|
903
|
+
const rest = request.params["*"] ?? "";
|
|
904
|
+
const resolved = await runtimeService.resolveProxyTarget(name, Number(port));
|
|
905
|
+
const path3 = `/${rest}`;
|
|
906
|
+
const headers = {
|
|
907
|
+
...buildProxyHeaders(
|
|
908
|
+
request.headers,
|
|
909
|
+
path3
|
|
910
|
+
),
|
|
911
|
+
...resolved.headers ?? {}
|
|
912
|
+
};
|
|
913
|
+
const body = request.body ?? request.raw;
|
|
914
|
+
await pipeRequestUpstream({
|
|
915
|
+
target: resolved.target,
|
|
916
|
+
method: request.method,
|
|
917
|
+
path: path3,
|
|
918
|
+
headers,
|
|
919
|
+
body,
|
|
920
|
+
reply
|
|
921
|
+
});
|
|
922
|
+
},
|
|
690
923
|
getSandboxLogs: async (request, reply) => {
|
|
691
924
|
const body = sandboxLogsSchema.parse(request.body ?? {});
|
|
692
925
|
return reply.send(ok(await runtimeService.getSandboxLogs(request.params.name, body)));
|
|
@@ -750,6 +983,7 @@ function registerSandboxRoutes(app, runtimeService) {
|
|
|
750
983
|
app.post("/api/sandboxes/:name/kill", controller.killSandbox);
|
|
751
984
|
app.delete("/api/sandboxes/:name", controller.deleteSandbox);
|
|
752
985
|
app.get("/api/sandboxes/:name/status", controller.getStatus);
|
|
986
|
+
app.get("/api/sandboxes/:name/endpoints/:port", controller.getEndpoint);
|
|
753
987
|
app.post("/api/sandboxes/:name/logs", controller.getSandboxLogs);
|
|
754
988
|
app.post("/api/files/read", controller.readFile);
|
|
755
989
|
app.post("/api/files/write", controller.writeFile);
|
|
@@ -760,6 +994,26 @@ function registerSandboxRoutes(app, runtimeService) {
|
|
|
760
994
|
app.post("/api/files/upload", controller.uploadFile);
|
|
761
995
|
app.get("/api/files/download", controller.downloadFile);
|
|
762
996
|
app.post("/api/shell/exec", controller.execCommand);
|
|
997
|
+
app.register(async (instance) => {
|
|
998
|
+
const bodyLimit = instance.initialConfig.bodyLimit ?? 100 * 1024 * 1024;
|
|
999
|
+
instance.removeAllContentTypeParsers();
|
|
1000
|
+
instance.addContentTypeParser("*", (_request, payload, done) => {
|
|
1001
|
+
let received = 0;
|
|
1002
|
+
const limiter = new Transform({
|
|
1003
|
+
transform(chunk, _encoding, callback) {
|
|
1004
|
+
received += chunk.length;
|
|
1005
|
+
if (received > bodyLimit) {
|
|
1006
|
+
callback(new HttpError(413, "PAYLOAD_TOO_LARGE", "Request body is too large"));
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
callback(null, chunk);
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
payload.pipe(limiter);
|
|
1013
|
+
done(null, limiter);
|
|
1014
|
+
});
|
|
1015
|
+
instance.all("/api/sandboxes/:name/proxy/:port/*", controller.proxyRequest);
|
|
1016
|
+
});
|
|
763
1017
|
}
|
|
764
1018
|
|
|
765
1019
|
// src/controllers/volume-fs.ts
|
|
@@ -986,9 +1240,9 @@ async function configureSwagger(app) {
|
|
|
986
1240
|
|
|
987
1241
|
// src/app.ts
|
|
988
1242
|
async function buildApp({
|
|
1243
|
+
runtimeService = new OpenSandboxRuntimeService(),
|
|
989
1244
|
apiKey
|
|
990
1245
|
} = {}) {
|
|
991
|
-
const runtimeService = new OpenSandboxRuntimeService();
|
|
992
1246
|
const imageService = new ImageService();
|
|
993
1247
|
const app = fastify({
|
|
994
1248
|
logger: false,
|
|
@@ -1123,4 +1377,4 @@ export {
|
|
|
1123
1377
|
buildApp,
|
|
1124
1378
|
startServer
|
|
1125
1379
|
};
|
|
1126
|
-
//# sourceMappingURL=chunk-
|
|
1380
|
+
//# sourceMappingURL=chunk-QC7CI5UY.mjs.map
|