@sapiom/sandbox 0.2.0
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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/sandbox.d.ts +80 -0
- package/dist/cjs/sandbox.d.ts.map +1 -0
- package/dist/cjs/sandbox.js +345 -0
- package/dist/cjs/sandbox.js.map +1 -0
- package/dist/cjs/types.d.ts +148 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/sandbox.d.ts +80 -0
- package/dist/esm/sandbox.d.ts.map +1 -0
- package/dist/esm/sandbox.js +341 -0
- package/dist/esm/sandbox.js.map +1 -0
- package/dist/esm/types.d.ts +148 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/package.json +56 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Sapiom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# @sapiom/sandbox
|
|
2
|
+
|
|
3
|
+
Sandbox environment management for the Sapiom SDK. Create isolated execution environments, manage files, run commands, and stream output in real time.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @sapiom/sandbox
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @sapiom/sandbox
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { SapiomSandbox } from "@sapiom/sandbox";
|
|
17
|
+
|
|
18
|
+
// Create a sandbox (uses SAPIOM_API_KEY env var by default)
|
|
19
|
+
const sandbox = await SapiomSandbox.create({ name: "my-sandbox" });
|
|
20
|
+
|
|
21
|
+
// Write a file
|
|
22
|
+
await sandbox.writeFile("hello.py", 'print("Hello from the sandbox!")');
|
|
23
|
+
|
|
24
|
+
// Execute a command and wait for completion
|
|
25
|
+
const result = await sandbox.exec("python hello.py");
|
|
26
|
+
console.log(result.stdout); // "Hello from the sandbox!"
|
|
27
|
+
|
|
28
|
+
// Stream output from a long-running process
|
|
29
|
+
const proc = await sandbox.execStream("node runner.js");
|
|
30
|
+
for await (const line of proc.output) {
|
|
31
|
+
console.log(`[${line.stream}] ${line.data}`);
|
|
32
|
+
}
|
|
33
|
+
console.log("exit code:", proc.exitCode);
|
|
34
|
+
|
|
35
|
+
// Clean up
|
|
36
|
+
await sandbox.destroy();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `SapiomSandbox.create(opts)`
|
|
42
|
+
|
|
43
|
+
Creates a new sandbox and returns a handle for interacting with it.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const sandbox = await SapiomSandbox.create({
|
|
47
|
+
name: "my-sandbox",
|
|
48
|
+
tier: "m",
|
|
49
|
+
ttl: "1h",
|
|
50
|
+
image: "python:3.12",
|
|
51
|
+
envs: { NODE_ENV: "production" },
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Options:**
|
|
56
|
+
|
|
57
|
+
| Option | Type | Required | Description |
|
|
58
|
+
|-----------|--------------------------|----------|-------------------------------------------------------|
|
|
59
|
+
| `name` | `string` | Yes | Sandbox name (lowercase alphanumeric + hyphens, 2-63 chars) |
|
|
60
|
+
| `apiKey` | `string` | No | Sapiom API key. Falls back to `SAPIOM_API_KEY` env var |
|
|
61
|
+
| `baseUrl` | `string` | No | Override the sandbox service URL |
|
|
62
|
+
| `fetch` | `typeof fetch` | No | Pre-configured fetch function (overrides `apiKey`) |
|
|
63
|
+
| `tier` | `SandboxTier` | No | Memory tier: `'xs'`, `'s'`, `'m'`, `'l'`, `'xl'` (default `'s'`) |
|
|
64
|
+
| `ttl` | `string` | No | Time-to-live (e.g. `'1h'`, `'24h'`, `'7d'`) |
|
|
65
|
+
| `envs` | `Record<string, string>` | No | Environment variables |
|
|
66
|
+
| `port` | `number` | No | Single port to expose (mutually exclusive with `ports`) |
|
|
67
|
+
| `ports` | `PortSpec[]` | No | Array of port specs to expose (mutually exclusive with `port`) |
|
|
68
|
+
| `image` | `string` | No | Pre-built Docker image for instant creation |
|
|
69
|
+
|
|
70
|
+
### `sandbox.writeFile(path, content)`
|
|
71
|
+
|
|
72
|
+
Writes a file relative to the sandbox's workspace root.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
await sandbox.writeFile("src/index.ts", 'console.log("hi")');
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `sandbox.readFile(path)`
|
|
79
|
+
|
|
80
|
+
Reads a file relative to the sandbox's workspace root and returns its content as a string.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const content = await sandbox.readFile("src/index.ts");
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `sandbox.exec(command, opts?)`
|
|
87
|
+
|
|
88
|
+
Executes a shell command inside the sandbox. By default waits for the process to finish.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// Wait for completion (default)
|
|
92
|
+
const result = await sandbox.exec("npm install");
|
|
93
|
+
console.log(result.exitCode); // 0
|
|
94
|
+
console.log(result.stdout);
|
|
95
|
+
console.log(result.stderr);
|
|
96
|
+
|
|
97
|
+
// Fire-and-forget
|
|
98
|
+
const bg = await sandbox.exec("npm start", { waitForCompletion: false });
|
|
99
|
+
console.log(bg.pid);
|
|
100
|
+
|
|
101
|
+
// Check on it later
|
|
102
|
+
const status = await sandbox.getProcess(bg.pid);
|
|
103
|
+
console.log(status.completed, status.exitCode);
|
|
104
|
+
|
|
105
|
+
// Or wait for it to finish
|
|
106
|
+
const final = await sandbox.waitForProcess(bg.pid);
|
|
107
|
+
console.log(final.exitCode);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Options:**
|
|
111
|
+
|
|
112
|
+
| Option | Type | Default | Description |
|
|
113
|
+
|----------------------|--------------------------|---------|--------------------------------------------------|
|
|
114
|
+
| `cwd` | `string` | — | Working directory (resolved relative to workspaceRoot) |
|
|
115
|
+
| `env` | `Record<string, string>` | — | Environment variables for the process |
|
|
116
|
+
| `waitForCompletion` | `boolean` | `true` | Wait for the process to finish |
|
|
117
|
+
| `pollInterval` | `number` | `1000` | Polling interval in ms |
|
|
118
|
+
| `timeout` | `number` | `60000` | Timeout in ms when waiting |
|
|
119
|
+
| `signal` | `AbortSignal` | — | Signal to cancel the operation |
|
|
120
|
+
|
|
121
|
+
### `sandbox.execStream(command, opts?)`
|
|
122
|
+
|
|
123
|
+
Executes a command and streams output in real time via an async iterable. Ideal for long-running processes like AI agent runs.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const proc = await sandbox.execStream("node agent.js");
|
|
127
|
+
for await (const line of proc.output) {
|
|
128
|
+
// line.stream is 'stdout' or 'stderr'
|
|
129
|
+
process.stdout.write(line.data);
|
|
130
|
+
}
|
|
131
|
+
console.log("exit code:", proc.exitCode);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Supports cancellation via `AbortSignal`:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
const proc = await sandbox.execStream("long-task", {
|
|
139
|
+
signal: controller.signal,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
setTimeout(() => controller.abort(), 30_000);
|
|
143
|
+
|
|
144
|
+
for await (const line of proc.output) {
|
|
145
|
+
console.log(line.data);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Options:**
|
|
150
|
+
|
|
151
|
+
| Option | Type | Default | Description |
|
|
152
|
+
|-----------|--------------------------|---------|--------------------------------------------------|
|
|
153
|
+
| `cwd` | `string` | — | Working directory (resolved relative to workspaceRoot) |
|
|
154
|
+
| `env` | `Record<string, string>` | — | Environment variables for the process |
|
|
155
|
+
| `signal` | `AbortSignal` | — | Signal to cancel the operation |
|
|
156
|
+
|
|
157
|
+
### `sandbox.getProcess(pid)`
|
|
158
|
+
|
|
159
|
+
Gets the current status of a process by PID.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const status = await sandbox.getProcess(pid);
|
|
163
|
+
console.log(status.completed, status.exitCode);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `sandbox.waitForProcess(pid, opts?)`
|
|
167
|
+
|
|
168
|
+
Waits for a process to complete by polling its status. Returns the same `ExecResult` as `exec()`.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const result = await sandbox.waitForProcess(pid, { timeout: 120_000 });
|
|
172
|
+
console.log(result.exitCode, result.stdout);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `sandbox.destroy()`
|
|
176
|
+
|
|
177
|
+
Destroys the sandbox and releases all associated resources.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
await sandbox.destroy();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Properties
|
|
184
|
+
|
|
185
|
+
| Property | Type | Description |
|
|
186
|
+
|-------------------------|----------|----------------------------------------------|
|
|
187
|
+
| `sandbox.name` | `string` | Sandbox identifier |
|
|
188
|
+
| `sandbox.workspaceRoot` | `string` | Absolute workspace root path in the sandbox |
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { SapiomSandbox } from "./sandbox.js";
|
|
2
|
+
export type { SandboxCreateOptions, SandboxCreateResponse, SandboxTier, PortSpec, ExecOptions, ExecStreamOptions, ExecResult, StreamingExecResult, OutputLine, ProcessCreateResponse, ProcessStatusResponse, } from "./types.js";
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EACV,oBAAoB,EACpB,qBAAqB,EACrB,WAAW,EACX,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,UAAU,EACV,mBAAmB,EACnB,UAAU,EACV,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SapiomSandbox = void 0;
|
|
4
|
+
var sandbox_js_1 = require("./sandbox.js");
|
|
5
|
+
Object.defineProperty(exports, "SapiomSandbox", { enumerable: true, get: function () { return sandbox_js_1.SapiomSandbox; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,2CAA6C;AAApC,2GAAA,aAAa,OAAA"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { SandboxCreateOptions, ExecOptions, ExecResult, ExecStreamOptions, StreamingExecResult, ProcessStatusResponse } from "./types.js";
|
|
2
|
+
export declare class SapiomSandbox {
|
|
3
|
+
/** Name / identifier of the sandbox. */
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** Absolute workspace root path inside the sandbox. */
|
|
6
|
+
readonly workspaceRoot: string;
|
|
7
|
+
private readonly _fetch;
|
|
8
|
+
private readonly _baseUrl;
|
|
9
|
+
private constructor();
|
|
10
|
+
/**
|
|
11
|
+
* Create a new sandbox and return a handle for interacting with it.
|
|
12
|
+
*/
|
|
13
|
+
static create(opts: SandboxCreateOptions): Promise<SapiomSandbox>;
|
|
14
|
+
/**
|
|
15
|
+
* Write a file inside the sandbox.
|
|
16
|
+
*
|
|
17
|
+
* @param path - File path relative to workspaceRoot.
|
|
18
|
+
* @param content - File content as a string.
|
|
19
|
+
*/
|
|
20
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Read a file from the sandbox.
|
|
23
|
+
*
|
|
24
|
+
* @param path - File path relative to workspaceRoot.
|
|
25
|
+
* @returns The file content as a string.
|
|
26
|
+
*/
|
|
27
|
+
readFile(path: string): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Execute a command in the sandbox.
|
|
30
|
+
*
|
|
31
|
+
* By default waits for the process to finish (polls process status).
|
|
32
|
+
* Set `opts.waitForCompletion = false` for fire-and-forget execution.
|
|
33
|
+
* Use {@link execStream} for real-time streaming output.
|
|
34
|
+
*
|
|
35
|
+
* @param command - The shell command to run.
|
|
36
|
+
* @param opts - Execution options.
|
|
37
|
+
*/
|
|
38
|
+
exec(command: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Execute a command and stream its output in real time.
|
|
41
|
+
*
|
|
42
|
+
* Returns a {@link StreamingExecResult} whose `output` property is an
|
|
43
|
+
* async iterable of {@link OutputLine} objects. `exitCode` is populated
|
|
44
|
+
* after the iterable is fully consumed.
|
|
45
|
+
*
|
|
46
|
+
* @param command - The shell command to run.
|
|
47
|
+
* @param opts - Stream execution options.
|
|
48
|
+
*/
|
|
49
|
+
execStream(command: string, opts?: ExecStreamOptions): Promise<StreamingExecResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Get the current status of a process by PID.
|
|
52
|
+
*
|
|
53
|
+
* Useful for checking on processes started with
|
|
54
|
+
* `exec(cmd, { waitForCompletion: false })`.
|
|
55
|
+
*
|
|
56
|
+
* @param pid - The process ID returned from exec.
|
|
57
|
+
*/
|
|
58
|
+
getProcess(pid: string): Promise<ProcessStatusResponse>;
|
|
59
|
+
/**
|
|
60
|
+
* Wait for a process to complete by polling its status.
|
|
61
|
+
*
|
|
62
|
+
* Useful for processes started with
|
|
63
|
+
* `exec(cmd, { waitForCompletion: false })`.
|
|
64
|
+
*
|
|
65
|
+
* @param pid - The process ID returned from exec.
|
|
66
|
+
* @param opts - Polling options.
|
|
67
|
+
*/
|
|
68
|
+
waitForProcess(pid: string, opts?: {
|
|
69
|
+
pollInterval?: number;
|
|
70
|
+
timeout?: number;
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
}): Promise<ExecResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Destroy the sandbox and release all resources.
|
|
75
|
+
*/
|
|
76
|
+
destroy(): Promise<void>;
|
|
77
|
+
private _createProcess;
|
|
78
|
+
private _pollProcess;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EAEpB,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,mBAAmB,EAGnB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AA0DpB,qBAAa,aAAa;IACxB,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,uDAAuD;IACvD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO;IAYP;;OAEG;WACU,MAAM,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;IAiCvE;;;;;OAKG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB7D;;;;;OAKG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiB7C;;;;;;;;;OASG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IA0BpE;;;;;;;;;OASG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,mBAAmB,CAAC;IA4H/B;;;;;;;OAOG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAe7D;;;;;;;;OAQG;IACG,cAAc,CAClB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACvE,OAAO,CAAC,UAAU,CAAC;IAItB;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAkBhB,cAAc;YA8Bd,YAAY;CAqC3B"}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SapiomSandbox = void 0;
|
|
4
|
+
const fetch_1 = require("@sapiom/fetch");
|
|
5
|
+
const DEFAULT_BASE_URL = "https://blaxel.services.sapiom.ai";
|
|
6
|
+
const DEFAULT_POLL_INTERVAL = 1000;
|
|
7
|
+
const DEFAULT_EXEC_TIMEOUT = 60000;
|
|
8
|
+
function assertRelativePath(path) {
|
|
9
|
+
const segments = path.split("/");
|
|
10
|
+
for (const seg of segments) {
|
|
11
|
+
if (seg === "..") {
|
|
12
|
+
throw new Error(`Path must not contain '..' segments: ${path}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function resolvePath(workspaceRoot, relativePath) {
|
|
17
|
+
assertRelativePath(relativePath);
|
|
18
|
+
const base = workspaceRoot.endsWith("/")
|
|
19
|
+
? workspaceRoot.slice(0, -1)
|
|
20
|
+
: workspaceRoot;
|
|
21
|
+
const rel = relativePath.startsWith("/")
|
|
22
|
+
? relativePath.slice(1)
|
|
23
|
+
: relativePath;
|
|
24
|
+
return `${base}/${rel}`;
|
|
25
|
+
}
|
|
26
|
+
function encodePathSegments(path) {
|
|
27
|
+
return path
|
|
28
|
+
.split("/")
|
|
29
|
+
.map((seg) => encodeURIComponent(seg))
|
|
30
|
+
.join("/");
|
|
31
|
+
}
|
|
32
|
+
function fileUrl(baseUrl, sandboxName, absolutePath) {
|
|
33
|
+
// Strip leading slash so the URL path is well-formed, then encode each segment
|
|
34
|
+
const cleanPath = absolutePath.startsWith("/")
|
|
35
|
+
? absolutePath.slice(1)
|
|
36
|
+
: absolutePath;
|
|
37
|
+
return `${baseUrl}/v1/sandboxes/${encodeURIComponent(sandboxName)}/filesystem/${encodePathSegments(cleanPath)}`;
|
|
38
|
+
}
|
|
39
|
+
function parseOutputLine(line) {
|
|
40
|
+
if (line.startsWith("stdout:")) {
|
|
41
|
+
return { stream: "stdout", data: line.slice(7) };
|
|
42
|
+
}
|
|
43
|
+
if (line.startsWith("stderr:")) {
|
|
44
|
+
return { stream: "stderr", data: line.slice(7) };
|
|
45
|
+
}
|
|
46
|
+
// Unrecognized framing — treat as stdout rather than dropping
|
|
47
|
+
return { stream: "stdout", data: line };
|
|
48
|
+
}
|
|
49
|
+
class SapiomSandbox {
|
|
50
|
+
constructor(name, workspaceRoot, fetchFn, baseUrl) {
|
|
51
|
+
this.name = name;
|
|
52
|
+
this.workspaceRoot = workspaceRoot;
|
|
53
|
+
this._fetch = fetchFn;
|
|
54
|
+
this._baseUrl = baseUrl;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create a new sandbox and return a handle for interacting with it.
|
|
58
|
+
*/
|
|
59
|
+
static async create(opts) {
|
|
60
|
+
const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
61
|
+
const fetchFn = opts.fetch ?? (0, fetch_1.createFetch)({ apiKey: opts.apiKey });
|
|
62
|
+
if (opts.port !== undefined && opts.ports !== undefined) {
|
|
63
|
+
throw new Error("Cannot specify both 'port' and 'ports'");
|
|
64
|
+
}
|
|
65
|
+
const body = { name: opts.name };
|
|
66
|
+
if (opts.tier !== undefined)
|
|
67
|
+
body.tier = opts.tier;
|
|
68
|
+
if (opts.ttl !== undefined)
|
|
69
|
+
body.ttl = opts.ttl;
|
|
70
|
+
if (opts.envs !== undefined)
|
|
71
|
+
body.envs = opts.envs;
|
|
72
|
+
if (opts.port !== undefined)
|
|
73
|
+
body.port = opts.port;
|
|
74
|
+
if (opts.ports !== undefined)
|
|
75
|
+
body.ports = opts.ports;
|
|
76
|
+
if (opts.image !== undefined)
|
|
77
|
+
body.image = opts.image;
|
|
78
|
+
const response = await fetchFn(`${baseUrl}/v1/sandboxes`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "Content-Type": "application/json" },
|
|
81
|
+
body: JSON.stringify(body),
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const text = await response.text();
|
|
85
|
+
throw new Error(`Failed to create sandbox: ${response.status} ${text}`);
|
|
86
|
+
}
|
|
87
|
+
const data = (await response.json());
|
|
88
|
+
return new SapiomSandbox(data.name, data.workspaceRoot, fetchFn, baseUrl);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Write a file inside the sandbox.
|
|
92
|
+
*
|
|
93
|
+
* @param path - File path relative to workspaceRoot.
|
|
94
|
+
* @param content - File content as a string.
|
|
95
|
+
*/
|
|
96
|
+
async writeFile(path, content) {
|
|
97
|
+
assertRelativePath(path);
|
|
98
|
+
const url = fileUrl(this._baseUrl, this.name, path);
|
|
99
|
+
const response = await this._fetch(url, {
|
|
100
|
+
method: "PUT",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify({ content }),
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
const text = await response.text();
|
|
106
|
+
throw new Error(`Failed to write file '${path}': ${response.status} ${text}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Read a file from the sandbox.
|
|
111
|
+
*
|
|
112
|
+
* @param path - File path relative to workspaceRoot.
|
|
113
|
+
* @returns The file content as a string.
|
|
114
|
+
*/
|
|
115
|
+
async readFile(path) {
|
|
116
|
+
assertRelativePath(path);
|
|
117
|
+
const url = fileUrl(this._baseUrl, this.name, path);
|
|
118
|
+
const response = await this._fetch(url);
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const text = await response.text();
|
|
121
|
+
throw new Error(`Failed to read file '${path}': ${response.status} ${text}`);
|
|
122
|
+
}
|
|
123
|
+
const data = (await response.json());
|
|
124
|
+
return data.content;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Execute a command in the sandbox.
|
|
128
|
+
*
|
|
129
|
+
* By default waits for the process to finish (polls process status).
|
|
130
|
+
* Set `opts.waitForCompletion = false` for fire-and-forget execution.
|
|
131
|
+
* Use {@link execStream} for real-time streaming output.
|
|
132
|
+
*
|
|
133
|
+
* @param command - The shell command to run.
|
|
134
|
+
* @param opts - Execution options.
|
|
135
|
+
*/
|
|
136
|
+
async exec(command, opts) {
|
|
137
|
+
const proc = await this._createProcess(command, opts);
|
|
138
|
+
// If the process already completed synchronously, return immediately
|
|
139
|
+
if (proc.status === "completed") {
|
|
140
|
+
return {
|
|
141
|
+
pid: proc.pid,
|
|
142
|
+
exitCode: proc.exitCode ?? 0,
|
|
143
|
+
stdout: proc.stdout ?? "",
|
|
144
|
+
stderr: proc.stderr ?? "",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const waitForCompletion = opts?.waitForCompletion ?? true;
|
|
148
|
+
if (!waitForCompletion) {
|
|
149
|
+
return {
|
|
150
|
+
pid: proc.pid,
|
|
151
|
+
exitCode: -1,
|
|
152
|
+
stdout: proc.stdout ?? "",
|
|
153
|
+
stderr: proc.stderr ?? "",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return this._pollProcess(proc.pid, opts);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Execute a command and stream its output in real time.
|
|
160
|
+
*
|
|
161
|
+
* Returns a {@link StreamingExecResult} whose `output` property is an
|
|
162
|
+
* async iterable of {@link OutputLine} objects. `exitCode` is populated
|
|
163
|
+
* after the iterable is fully consumed.
|
|
164
|
+
*
|
|
165
|
+
* @param command - The shell command to run.
|
|
166
|
+
* @param opts - Stream execution options.
|
|
167
|
+
*/
|
|
168
|
+
async execStream(command, opts) {
|
|
169
|
+
const proc = await this._createProcess(command, opts);
|
|
170
|
+
// If the process already completed synchronously, return without
|
|
171
|
+
// opening the log stream — yield the stdout/stderr from the create
|
|
172
|
+
// response directly.
|
|
173
|
+
if (proc.status === "completed") {
|
|
174
|
+
const code = proc.exitCode ?? 0;
|
|
175
|
+
const stdout = proc.stdout ?? "";
|
|
176
|
+
const stderr = proc.stderr ?? "";
|
|
177
|
+
const output = (async function* () {
|
|
178
|
+
if (stdout)
|
|
179
|
+
yield { stream: "stdout", data: stdout };
|
|
180
|
+
if (stderr)
|
|
181
|
+
yield { stream: "stderr", data: stderr };
|
|
182
|
+
})();
|
|
183
|
+
return {
|
|
184
|
+
pid: proc.pid,
|
|
185
|
+
get exitCode() {
|
|
186
|
+
return code;
|
|
187
|
+
},
|
|
188
|
+
output,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
let finalExitCode = -1;
|
|
192
|
+
// Capture references for the generator closure
|
|
193
|
+
const fetchFn = this._fetch;
|
|
194
|
+
const baseUrl = this._baseUrl;
|
|
195
|
+
const sandboxName = this.name;
|
|
196
|
+
const signal = opts?.signal;
|
|
197
|
+
async function* streamOutput() {
|
|
198
|
+
const response = await fetchFn(`${baseUrl}/v1/sandboxes/${encodeURIComponent(sandboxName)}/process/${proc.pid}/logs/stream`, { signal });
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
const text = await response.text();
|
|
201
|
+
throw new Error(`Failed to stream process ${proc.pid}: ${response.status} ${text}`);
|
|
202
|
+
}
|
|
203
|
+
if (!response.body) {
|
|
204
|
+
throw new Error(`No response body for process ${proc.pid} log stream`);
|
|
205
|
+
}
|
|
206
|
+
const reader = response.body.getReader();
|
|
207
|
+
const decoder = new TextDecoder();
|
|
208
|
+
let buffer = "";
|
|
209
|
+
try {
|
|
210
|
+
for (;;) {
|
|
211
|
+
const { done, value } = await reader.read();
|
|
212
|
+
if (done)
|
|
213
|
+
break;
|
|
214
|
+
buffer += decoder.decode(value, { stream: true });
|
|
215
|
+
const lines = buffer.split("\n");
|
|
216
|
+
buffer = lines.pop();
|
|
217
|
+
for (const line of lines) {
|
|
218
|
+
if (!line)
|
|
219
|
+
continue;
|
|
220
|
+
yield parseOutputLine(line);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Flush remaining multibyte bytes from the decoder
|
|
224
|
+
buffer += decoder.decode();
|
|
225
|
+
if (buffer) {
|
|
226
|
+
yield parseOutputLine(buffer);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
reader.releaseLock();
|
|
231
|
+
}
|
|
232
|
+
// Fetch final process status for the exit code
|
|
233
|
+
const statusResponse = await fetchFn(`${baseUrl}/v1/sandboxes/${encodeURIComponent(sandboxName)}/process/${proc.pid}`, { signal });
|
|
234
|
+
if (!statusResponse.ok) {
|
|
235
|
+
const text = await statusResponse.text();
|
|
236
|
+
throw new Error(`Failed to get final status for process ${proc.pid}: ${statusResponse.status} ${text}`);
|
|
237
|
+
}
|
|
238
|
+
let status = (await statusResponse.json());
|
|
239
|
+
// If the process hasn't completed yet (e.g. stream disconnected
|
|
240
|
+
// before the process finished), poll until it does.
|
|
241
|
+
while (status.status !== "completed") {
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
243
|
+
const retry = await fetchFn(`${baseUrl}/v1/sandboxes/${encodeURIComponent(sandboxName)}/process/${proc.pid}`, { signal });
|
|
244
|
+
if (!retry.ok) {
|
|
245
|
+
const text = await retry.text();
|
|
246
|
+
throw new Error(`Failed to get final status for process ${proc.pid}: ${retry.status} ${text}`);
|
|
247
|
+
}
|
|
248
|
+
status = (await retry.json());
|
|
249
|
+
}
|
|
250
|
+
finalExitCode = status.exitCode ?? 0;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
pid: proc.pid,
|
|
254
|
+
get exitCode() {
|
|
255
|
+
return finalExitCode;
|
|
256
|
+
},
|
|
257
|
+
output: streamOutput(),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get the current status of a process by PID.
|
|
262
|
+
*
|
|
263
|
+
* Useful for checking on processes started with
|
|
264
|
+
* `exec(cmd, { waitForCompletion: false })`.
|
|
265
|
+
*
|
|
266
|
+
* @param pid - The process ID returned from exec.
|
|
267
|
+
*/
|
|
268
|
+
async getProcess(pid) {
|
|
269
|
+
const response = await this._fetch(`${this._baseUrl}/v1/sandboxes/${encodeURIComponent(this.name)}/process/${pid}`);
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
const text = await response.text();
|
|
272
|
+
throw new Error(`Failed to get process ${pid}: ${response.status} ${text}`);
|
|
273
|
+
}
|
|
274
|
+
return (await response.json());
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Wait for a process to complete by polling its status.
|
|
278
|
+
*
|
|
279
|
+
* Useful for processes started with
|
|
280
|
+
* `exec(cmd, { waitForCompletion: false })`.
|
|
281
|
+
*
|
|
282
|
+
* @param pid - The process ID returned from exec.
|
|
283
|
+
* @param opts - Polling options.
|
|
284
|
+
*/
|
|
285
|
+
async waitForProcess(pid, opts) {
|
|
286
|
+
return this._pollProcess(pid, opts);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Destroy the sandbox and release all resources.
|
|
290
|
+
*/
|
|
291
|
+
async destroy() {
|
|
292
|
+
const response = await this._fetch(`${this._baseUrl}/v1/sandboxes/${encodeURIComponent(this.name)}`, { method: "DELETE" });
|
|
293
|
+
if (!response.ok) {
|
|
294
|
+
const text = await response.text();
|
|
295
|
+
throw new Error(`Failed to destroy sandbox: ${response.status} ${text}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Private helpers
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
async _createProcess(command, opts) {
|
|
302
|
+
const body = { command };
|
|
303
|
+
if (opts?.cwd !== undefined) {
|
|
304
|
+
body.cwd = resolvePath(this.workspaceRoot, opts.cwd);
|
|
305
|
+
}
|
|
306
|
+
if (opts?.env !== undefined)
|
|
307
|
+
body.env = opts.env;
|
|
308
|
+
const response = await this._fetch(`${this._baseUrl}/v1/sandboxes/${encodeURIComponent(this.name)}/process`, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: { "Content-Type": "application/json" },
|
|
311
|
+
body: JSON.stringify(body),
|
|
312
|
+
signal: opts?.signal,
|
|
313
|
+
});
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
const text = await response.text();
|
|
316
|
+
throw new Error(`Failed to execute command: ${response.status} ${text}`);
|
|
317
|
+
}
|
|
318
|
+
return (await response.json());
|
|
319
|
+
}
|
|
320
|
+
async _pollProcess(pid, opts) {
|
|
321
|
+
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
|
|
322
|
+
const timeout = opts?.timeout ?? DEFAULT_EXEC_TIMEOUT;
|
|
323
|
+
const deadline = Date.now() + timeout;
|
|
324
|
+
while (Date.now() < deadline) {
|
|
325
|
+
const response = await this._fetch(`${this._baseUrl}/v1/sandboxes/${encodeURIComponent(this.name)}/process/${pid}`, { signal: opts?.signal });
|
|
326
|
+
if (!response.ok) {
|
|
327
|
+
const text = await response.text();
|
|
328
|
+
throw new Error(`Failed to poll process ${pid}: ${response.status} ${text}`);
|
|
329
|
+
}
|
|
330
|
+
const status = (await response.json());
|
|
331
|
+
if (status.status === "completed") {
|
|
332
|
+
return {
|
|
333
|
+
pid,
|
|
334
|
+
exitCode: status.exitCode ?? 0,
|
|
335
|
+
stdout: status.stdout ?? "",
|
|
336
|
+
stderr: status.stderr ?? "",
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
340
|
+
}
|
|
341
|
+
throw new Error(`Process ${pid} timed out after ${timeout}ms`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
exports.SapiomSandbox = SapiomSandbox;
|
|
345
|
+
//# sourceMappingURL=sandbox.js.map
|