@agent-relay/sandbox 0.1.2 → 0.1.3
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/README.md +226 -5
- package/dist/agent37/client.d.ts +115 -0
- package/dist/agent37/client.d.ts.map +1 -0
- package/dist/agent37/client.js +270 -0
- package/dist/agent37/client.js.map +1 -0
- package/dist/agent37/runtime.d.ts +415 -0
- package/dist/agent37/runtime.d.ts.map +1 -0
- package/dist/agent37/runtime.js +855 -0
- package/dist/agent37/runtime.js.map +1 -0
- package/dist/agentcore/bench.d.ts +93 -0
- package/dist/agentcore/bench.d.ts.map +1 -0
- package/dist/agentcore/bench.js +125 -0
- package/dist/agentcore/bench.js.map +1 -0
- package/dist/agentcore/capabilities.d.ts +169 -0
- package/dist/agentcore/capabilities.d.ts.map +1 -0
- package/dist/agentcore/capabilities.js +153 -0
- package/dist/agentcore/capabilities.js.map +1 -0
- package/dist/agentcore/config.d.ts +156 -0
- package/dist/agentcore/config.d.ts.map +1 -0
- package/dist/agentcore/config.js +12 -0
- package/dist/agentcore/config.js.map +1 -0
- package/dist/agentcore/internal/sdk.d.ts +181 -0
- package/dist/agentcore/internal/sdk.d.ts.map +1 -0
- package/dist/agentcore/internal/sdk.js +223 -0
- package/dist/agentcore/internal/sdk.js.map +1 -0
- package/dist/agentcore/runtime.d.ts +177 -0
- package/dist/agentcore/runtime.d.ts.map +1 -0
- package/dist/agentcore/runtime.js +706 -0
- package/dist/agentcore/runtime.js.map +1 -0
- package/dist/daytona/runtime.d.ts +23 -0
- package/dist/daytona/runtime.d.ts.map +1 -1
- package/dist/daytona/runtime.js +167 -5
- package/dist/daytona/runtime.js.map +1 -1
- package/dist/daytona/wire-supplement.d.ts +25 -0
- package/dist/daytona/wire-supplement.d.ts.map +1 -0
- package/dist/daytona/wire-supplement.js +16 -0
- package/dist/daytona/wire-supplement.js.map +1 -0
- package/dist/e2b/runtime.d.ts +107 -72
- package/dist/e2b/runtime.d.ts.map +1 -1
- package/dist/e2b/runtime.js +675 -190
- package/dist/e2b/runtime.js.map +1 -1
- package/dist/freestyle/capabilities.d.ts +28 -0
- package/dist/freestyle/capabilities.d.ts.map +1 -0
- package/dist/freestyle/capabilities.js +42 -0
- package/dist/freestyle/capabilities.js.map +1 -0
- package/dist/freestyle/config.d.ts +36 -0
- package/dist/freestyle/config.d.ts.map +1 -0
- package/dist/freestyle/config.js +2 -0
- package/dist/freestyle/config.js.map +1 -0
- package/dist/freestyle/internal/sdk.d.ts +69 -0
- package/dist/freestyle/internal/sdk.d.ts.map +1 -0
- package/dist/freestyle/internal/sdk.js +19 -0
- package/dist/freestyle/internal/sdk.js.map +1 -0
- package/dist/freestyle/runtime.d.ts +163 -0
- package/dist/freestyle/runtime.d.ts.map +1 -0
- package/dist/freestyle/runtime.js +798 -0
- package/dist/freestyle/runtime.js.map +1 -0
- package/dist/index.d.ts +35 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/microsandbox/runtime.d.ts +793 -0
- package/dist/microsandbox/runtime.d.ts.map +1 -0
- package/dist/microsandbox/runtime.js +2303 -0
- package/dist/microsandbox/runtime.js.map +1 -0
- package/dist/modal/bench.d.ts +199 -0
- package/dist/modal/bench.d.ts.map +1 -0
- package/dist/modal/bench.js +450 -0
- package/dist/modal/bench.js.map +1 -0
- package/dist/modal/capabilities.d.ts +180 -0
- package/dist/modal/capabilities.d.ts.map +1 -0
- package/dist/modal/capabilities.js +159 -0
- package/dist/modal/capabilities.js.map +1 -0
- package/dist/modal/config.d.ts +192 -0
- package/dist/modal/config.d.ts.map +1 -0
- package/dist/modal/config.js +169 -0
- package/dist/modal/config.js.map +1 -0
- package/dist/modal/internal/sdk.d.ts +141 -0
- package/dist/modal/internal/sdk.d.ts.map +1 -0
- package/dist/modal/internal/sdk.js +51 -0
- package/dist/modal/internal/sdk.js.map +1 -0
- package/dist/modal/runtime.d.ts +271 -0
- package/dist/modal/runtime.d.ts.map +1 -0
- package/dist/modal/runtime.js +871 -0
- package/dist/modal/runtime.js.map +1 -0
- package/dist/mount-script.d.ts +16 -1
- package/dist/mount-script.d.ts.map +1 -1
- package/dist/mount-script.js +76 -15
- package/dist/mount-script.js.map +1 -1
- package/dist/orchestrator.d.ts +15 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +13 -1
- package/dist/orchestrator.js.map +1 -1
- package/dist/port.d.ts +144 -6
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +27 -5
- package/dist/port.js.map +1 -1
- package/dist/types.d.ts +50 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vercel/bench.d.ts +169 -0
- package/dist/vercel/bench.d.ts.map +1 -0
- package/dist/vercel/bench.js +337 -0
- package/dist/vercel/bench.js.map +1 -0
- package/dist/vercel/capabilities.d.ts +106 -0
- package/dist/vercel/capabilities.d.ts.map +1 -0
- package/dist/vercel/capabilities.js +128 -0
- package/dist/vercel/capabilities.js.map +1 -0
- package/dist/vercel/config.d.ts +119 -0
- package/dist/vercel/config.d.ts.map +1 -0
- package/dist/vercel/config.js +8 -0
- package/dist/vercel/config.js.map +1 -0
- package/dist/vercel/internal/sdk.d.ts +173 -0
- package/dist/vercel/internal/sdk.d.ts.map +1 -0
- package/dist/vercel/internal/sdk.js +117 -0
- package/dist/vercel/internal/sdk.js.map +1 -0
- package/dist/vercel/runtime.d.ts +305 -0
- package/dist/vercel/runtime.d.ts.map +1 -0
- package/dist/vercel/runtime.js +1169 -0
- package/dist/vercel/runtime.js.map +1 -0
- package/docs/freestyle.md +110 -0
- package/package.json +35 -4
package/dist/e2b/runtime.js
CHANGED
|
@@ -1,58 +1,106 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
1
3
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// E2B provider for
|
|
4
|
+
// E2B provider for both public runtime planes.
|
|
3
5
|
//
|
|
4
|
-
// The `e2b`
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// and unit tests do not hard-couple to the SDK type graph, and a fake factory
|
|
8
|
-
// can drive every path without a real E2B API key.
|
|
6
|
+
// The `e2b` package is imported lazily so a consumer using another provider
|
|
7
|
+
// does not eagerly load it. The structural surface below follows the installed
|
|
8
|
+
// e2b v2 contract while keeping tests independent from an API key.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
// Command-LIFETIME budget for a background run (NOT a request timeout).
|
|
11
|
-
//
|
|
12
|
-
// Callers commonly pass a short `options.timeoutMs` meaning "how long to wait
|
|
13
|
-
// for the START call to return". For E2B that value is the SDK *request* cap,
|
|
14
|
-
// not the command lifetime — mapping it onto the background command's
|
|
15
|
-
// `timeoutMs` would kill every long-running command when the start request
|
|
16
|
-
// deadline elapses. We instead set the command lifetime and the microVM
|
|
17
|
-
// timeout to this budget, and route the caller's short timeout to
|
|
18
|
-
// `requestTimeoutMs`.
|
|
19
10
|
const DEFAULT_RUN_BUDGET_MS = 30 * 60_000;
|
|
20
11
|
const DEFAULT_CREATE_TIMEOUT_MS = 120_000;
|
|
21
|
-
const
|
|
12
|
+
const DEFAULT_LOOKUP_TIMEOUT_MS = 10_000;
|
|
13
|
+
const ADMISSION_RECONCILIATION_TIMEOUT_MS = 1_000;
|
|
14
|
+
const ADMISSION_RECONCILIATION_INTERVAL_MS = 25;
|
|
15
|
+
const SCRIPT_LOG_READ_MAX_BYTES = 262_144;
|
|
16
|
+
const ASYNC_SESSION_ENV = "AGENTWORKFORCE_E2B_ASYNC_SESSION";
|
|
17
|
+
const ASYNC_REQUEST_ENV = "AGENTWORKFORCE_E2B_ASYNC_REQUEST";
|
|
18
|
+
/** Terminal status used when E2B confirms that an admitted process is gone
|
|
19
|
+
* before it can publish an exit sidecar. */
|
|
20
|
+
export const E2B_ASYNC_PROCESS_LOST_EXIT_CODE = 255;
|
|
22
21
|
export class E2BSandboxRuntime {
|
|
23
22
|
id = "e2b";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
capabilities = {
|
|
24
|
+
// The SDK has a PTY API, but this adapter does not expose one through the
|
|
25
|
+
// public runtime contract.
|
|
26
|
+
pty: false,
|
|
27
|
+
// Launches always use the caller-provided template/snapshot identifier.
|
|
28
|
+
snapshots: true,
|
|
29
|
+
isolation: "strong",
|
|
30
|
+
// Handles can be resolved by id in a later process/request.
|
|
31
|
+
persistentHandle: true,
|
|
32
|
+
// Polling returns bounded snapshots; it does not expose a streaming API.
|
|
33
|
+
streamingLogs: false,
|
|
34
|
+
};
|
|
35
|
+
// E2B v2 performs metadata-filtered server-side lookup, and stop/start map
|
|
36
|
+
// to pause/connect (connect resumes a paused sandbox).
|
|
37
|
+
declaredCapabilities = { warmLease: true, lifecycle: true };
|
|
30
38
|
apiKey;
|
|
31
39
|
template;
|
|
32
40
|
runBudgetMs;
|
|
41
|
+
syncRunBudgetMs;
|
|
42
|
+
sandboxLifetimeMs;
|
|
33
43
|
createTimeoutMs;
|
|
34
44
|
injectedStatics;
|
|
35
45
|
staticsPromise;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// a run outlive the request that started it.
|
|
39
|
-
sandboxes = new Map();
|
|
46
|
+
registrations = new Map();
|
|
47
|
+
asyncAdmissions = new Map();
|
|
40
48
|
constructor(options) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
const apiKey = options.apiKey.trim();
|
|
50
|
+
if (!apiKey) {
|
|
51
|
+
throw new Error("E2B API key is required");
|
|
52
|
+
}
|
|
53
|
+
const template = options.template.trim();
|
|
54
|
+
if (!template) {
|
|
55
|
+
throw new Error("E2B sandbox template is required");
|
|
56
|
+
}
|
|
57
|
+
this.apiKey = apiKey;
|
|
58
|
+
this.template = template;
|
|
59
|
+
this.runBudgetMs = positiveDuration(options.runBudgetMs, DEFAULT_RUN_BUDGET_MS);
|
|
60
|
+
this.syncRunBudgetMs = positiveDuration(options.syncRunBudgetMs, this.runBudgetMs);
|
|
61
|
+
this.sandboxLifetimeMs = positiveDuration(options.sandboxLifetimeMs, this.runBudgetMs);
|
|
62
|
+
this.createTimeoutMs = positiveDuration(options.createTimeoutMs, DEFAULT_CREATE_TIMEOUT_MS);
|
|
45
63
|
this.injectedStatics = options.sandbox;
|
|
46
64
|
}
|
|
47
65
|
async findByLabels(labels, options = {}) {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
66
|
+
const states = options.states === undefined ? ["STARTED"] : options.states;
|
|
67
|
+
const providerStates = toE2BStates(states);
|
|
68
|
+
if (providerStates?.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const excludedIds = new Set(options.excludeIds ?? []);
|
|
72
|
+
const deadline = lookupDeadline(options.timeoutMs);
|
|
73
|
+
const paginator = await this.listSandboxes(labels, providerStates, pageSize(options));
|
|
74
|
+
while (paginator.hasNext) {
|
|
75
|
+
const page = await awaitLookupOperation(paginator.nextItems(), deadline, "listing matching E2B sandboxes");
|
|
76
|
+
for (const info of page) {
|
|
77
|
+
if (!matchesState(info.state, states) || excludedIds.has(info.sandboxId)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
return this.registerInfo(info, options);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
51
84
|
}
|
|
52
85
|
async findAllByLabels(labels, options = {}) {
|
|
53
86
|
const states = options.states === undefined ? ["STARTED"] : options.states;
|
|
54
|
-
const
|
|
55
|
-
|
|
87
|
+
const providerStates = toE2BStates(states);
|
|
88
|
+
if (providerStates?.length === 0) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const deadline = lookupDeadline(options.timeoutMs);
|
|
92
|
+
const paginator = await this.listSandboxes(labels, providerStates, pageSize(options));
|
|
93
|
+
const handles = [];
|
|
94
|
+
const excludedIds = new Set(options.excludeIds ?? []);
|
|
95
|
+
while (paginator.hasNext) {
|
|
96
|
+
const page = await awaitLookupOperation(paginator.nextItems(), deadline, "listing matching E2B sandboxes");
|
|
97
|
+
for (const info of page) {
|
|
98
|
+
if (matchesState(info.state, states) && !excludedIds.has(info.sandboxId)) {
|
|
99
|
+
handles.push(this.registerInfo(info, options));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return handles;
|
|
56
104
|
}
|
|
57
105
|
async countByLabels(labels, options = {}) {
|
|
58
106
|
const maxCount = options.maxCount === undefined
|
|
@@ -62,100 +110,100 @@ export class E2BSandboxRuntime {
|
|
|
62
110
|
return 0;
|
|
63
111
|
}
|
|
64
112
|
const states = options.states === undefined ? ["STARTED"] : options.states;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Drain the E2B sandbox list (server-side metadata-filtered), keep the ones
|
|
71
|
-
* matching `states`, and stop once `cap` handles are collected. Returns `[]`
|
|
72
|
-
* when the provider build lacks server-side label search — an EXPLICIT
|
|
73
|
-
* warm-lease-degrades-to-cold gate, not a silent behavior change.
|
|
74
|
-
*/
|
|
75
|
-
async collectByLabels(labels, states, cap) {
|
|
76
|
-
const statics = await this.statics();
|
|
77
|
-
if (typeof statics.list !== "function") {
|
|
78
|
-
return [];
|
|
113
|
+
const providerStates = toE2BStates(states);
|
|
114
|
+
if (providerStates?.length === 0) {
|
|
115
|
+
return 0;
|
|
79
116
|
}
|
|
80
|
-
const
|
|
81
|
-
const paginator =
|
|
82
|
-
|
|
83
|
-
...(query ? { query: { metadata: query } } : {}),
|
|
84
|
-
});
|
|
85
|
-
const handles = [];
|
|
117
|
+
const deadline = lookupDeadline(options.timeoutMs);
|
|
118
|
+
const paginator = await this.listSandboxes(labels, providerStates, pageSize(options));
|
|
119
|
+
let count = 0;
|
|
86
120
|
while (paginator.hasNext) {
|
|
87
|
-
const page = await paginator.nextItems();
|
|
121
|
+
const page = await awaitLookupOperation(paginator.nextItems(), deadline, "counting matching E2B sandboxes");
|
|
88
122
|
for (const info of page) {
|
|
89
123
|
if (!matchesState(info.state, states)) {
|
|
90
124
|
continue;
|
|
91
125
|
}
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
return
|
|
126
|
+
count += 1;
|
|
127
|
+
if (count >= maxCount) {
|
|
128
|
+
return count;
|
|
95
129
|
}
|
|
96
130
|
}
|
|
97
131
|
}
|
|
98
|
-
return
|
|
132
|
+
return count;
|
|
99
133
|
}
|
|
100
134
|
async launch(options = {}) {
|
|
101
135
|
const statics = await this.statics();
|
|
136
|
+
const name = options.name?.trim() || options.label?.trim();
|
|
102
137
|
const metadata = {
|
|
103
138
|
...(options.labels ?? {}),
|
|
104
|
-
...(
|
|
139
|
+
...(name ? { name } : {}),
|
|
105
140
|
};
|
|
106
|
-
const
|
|
107
|
-
? options.createTimeoutSeconds * 1000
|
|
141
|
+
const requestTimeoutMs = options.createTimeoutSeconds && options.createTimeoutSeconds > 0
|
|
142
|
+
? Math.ceil(options.createTimeoutSeconds * 1000)
|
|
108
143
|
: this.createTimeoutMs;
|
|
109
144
|
const sandbox = await statics.create(this.template, {
|
|
110
145
|
apiKey: this.apiKey,
|
|
111
|
-
|
|
146
|
+
requestTimeoutMs,
|
|
147
|
+
timeoutMs: this.sandboxLifetimeMs,
|
|
148
|
+
...(hasEntries(metadata) ? { metadata } : {}),
|
|
112
149
|
...(hasEntries(options.env) ? { envs: options.env } : {}),
|
|
113
|
-
timeoutMs,
|
|
114
150
|
});
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// E2B sandboxes persist server-side the instant create() returns and are
|
|
120
|
-
// re-attachable by id via Sandbox.connect — so a "detached" launch is just
|
|
121
|
-
// launch. This is what lets a short-lived request context return before
|
|
122
|
-
// the run finishes and a later tick reconnect, instead of holding a
|
|
123
|
-
// request open until it hits a gateway timeout.
|
|
124
|
-
return this.launch(options);
|
|
151
|
+
return this.registerSandbox(sandbox, {
|
|
152
|
+
owned: true,
|
|
153
|
+
workdir: options.workdir,
|
|
154
|
+
});
|
|
125
155
|
}
|
|
126
156
|
async getById(id, options = {}) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
157
|
+
const statics = await this.statics();
|
|
158
|
+
let info;
|
|
159
|
+
try {
|
|
160
|
+
info = await statics.getInfo(id, { apiKey: this.apiKey });
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (isSandboxNotFound(error)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
130
167
|
}
|
|
131
|
-
// Sandbox.connect only succeeds against a running microVM; a paused/killed
|
|
132
|
-
// one throws NotFound (→ null above). So a resolved handle is "STARTED".
|
|
133
|
-
const handle = { id: sandbox.sandboxId, state: "STARTED" };
|
|
134
168
|
const states = options.states === undefined ? null : options.states;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
async uploadBundle(handle, options) {
|
|
138
|
-
const sandbox = await this.requireSandbox(handle);
|
|
139
|
-
for (const file of options.files) {
|
|
140
|
-
const data = typeof file.source === "string" ? file.source : toArrayBuffer(file.source);
|
|
141
|
-
await sandbox.files.write(file.destination, data);
|
|
169
|
+
if (!matchesState(info.state, states)) {
|
|
170
|
+
return null;
|
|
142
171
|
}
|
|
172
|
+
return this.registerInfo(info, options);
|
|
173
|
+
}
|
|
174
|
+
attachSandbox(sandbox, options = {}) {
|
|
175
|
+
return this.registerSandbox(sandbox, {
|
|
176
|
+
owned: options.owned ?? false,
|
|
177
|
+
homeDir: options.homeDir,
|
|
178
|
+
workdir: options.workdir,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async exec(handle, command, options = {}) {
|
|
182
|
+
const result = await this.runScript(handle, { command, ...options });
|
|
183
|
+
return {
|
|
184
|
+
output: result.output,
|
|
185
|
+
exitCode: result.exitCode ?? 0,
|
|
186
|
+
};
|
|
143
187
|
}
|
|
144
188
|
async runScript(handle, options) {
|
|
145
189
|
const sandbox = await this.requireSandbox(handle);
|
|
146
|
-
|
|
147
|
-
|
|
190
|
+
const explicitTimeoutMs = options.timeoutMs && options.timeoutMs > 0
|
|
191
|
+
? options.timeoutMs
|
|
192
|
+
: undefined;
|
|
148
193
|
try {
|
|
194
|
+
// Only an explicit caller timeout may push the sandbox past its
|
|
195
|
+
// configured lifetime; the implicit budget below must not.
|
|
196
|
+
await sandbox.setTimeout(Math.max(this.sandboxLifetimeMs, explicitTimeoutMs ?? 0));
|
|
149
197
|
const result = await sandbox.commands.run(options.command, {
|
|
150
|
-
...(options.
|
|
198
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
199
|
+
// Always send a timeout. E2B applies its own 60s command default when
|
|
200
|
+
// the field is omitted, which no other provider imposes and which the
|
|
201
|
+
// orchestrator does not expect: it omits `timeoutMs` on most execs, so
|
|
202
|
+
// an inherited 60s wall would kill long synchronous work on E2B alone.
|
|
203
|
+
timeoutMs: explicitTimeoutMs ?? this.syncRunBudgetMs,
|
|
151
204
|
...(hasEntries(options.env) ? { envs: options.env } : {}),
|
|
152
205
|
});
|
|
153
|
-
return
|
|
154
|
-
output: combineOutput(result.stdout, result.stderr),
|
|
155
|
-
...(result.stdout ? { stdout: result.stdout } : {}),
|
|
156
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
157
|
-
exitCode: typeof result.exitCode === "number" ? result.exitCode : null,
|
|
158
|
-
};
|
|
206
|
+
return commandResult(result);
|
|
159
207
|
}
|
|
160
208
|
catch (error) {
|
|
161
209
|
const normalized = normalizeCommandError(error);
|
|
@@ -168,128 +216,407 @@ export class E2BSandboxRuntime {
|
|
|
168
216
|
async startScript(handle, options) {
|
|
169
217
|
const sandbox = await this.requireSandbox(handle);
|
|
170
218
|
const sessionId = options.sessionId ?? `run-${handle.id}-${Date.now()}`;
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
219
|
+
const paths = scriptRunPaths(sessionId);
|
|
220
|
+
const fingerprint = asyncRequestFingerprint(options);
|
|
221
|
+
const admissionKey = `${handle.id}\0${paths.sessionToken}`;
|
|
222
|
+
const known = this.asyncAdmissions.get(admissionKey);
|
|
223
|
+
if (known) {
|
|
224
|
+
if (known.fingerprint !== fingerprint) {
|
|
225
|
+
throw asyncSessionConflict(sessionId);
|
|
226
|
+
}
|
|
227
|
+
await this.killDuplicateProcesses(sandbox, paths.sessionToken, fingerprint, known.pid);
|
|
228
|
+
return { sessionId, commandId: String(known.pid), reconciled: true };
|
|
229
|
+
}
|
|
230
|
+
// mkdir is the atomic admission claim. An existing directory is immutable:
|
|
231
|
+
// retries reconcile its durable request/process identity and never erase it
|
|
232
|
+
// or submit a second copy.
|
|
233
|
+
const claim = await this.runScript(handle, {
|
|
234
|
+
command: [
|
|
235
|
+
"# AGENTWORKFORCE_E2B_ASYNC_CLAIM",
|
|
236
|
+
`mkdir -p ${shellSingleQuote(paths.root)} || exit $?`,
|
|
237
|
+
`if mkdir ${shellSingleQuote(paths.dir)} 2>/dev/null; then`,
|
|
238
|
+
` printf '%s\\n' ${shellSingleQuote(fingerprint)} > ${shellSingleQuote(paths.requestTmp)} &&`,
|
|
239
|
+
` mv ${shellSingleQuote(paths.requestTmp)} ${shellSingleQuote(paths.request)} &&`,
|
|
240
|
+
" printf '%s\\n' created",
|
|
241
|
+
"else",
|
|
242
|
+
" printf '%s\\n' existing",
|
|
243
|
+
"fi",
|
|
244
|
+
].join("\n"),
|
|
245
|
+
cwd: options.cwd,
|
|
246
|
+
env: options.env,
|
|
247
|
+
timeoutMs: 30_000,
|
|
192
248
|
});
|
|
193
|
-
|
|
249
|
+
if (claim.exitCode !== 0) {
|
|
250
|
+
throw new Error(`Failed to claim E2B async session ${sessionId}: ${claim.output || "claim failed"}`);
|
|
251
|
+
}
|
|
252
|
+
const claimState = claim.stdout?.trim() ?? claim.output.trim();
|
|
253
|
+
if (claimState === "existing") {
|
|
254
|
+
const reconciled = await this.reconcileExistingAdmission(sandbox, sessionId, paths, fingerprint);
|
|
255
|
+
this.asyncAdmissions.set(admissionKey, reconciled);
|
|
256
|
+
return { sessionId, commandId: String(reconciled.pid), reconciled: true };
|
|
257
|
+
}
|
|
258
|
+
if (claimState !== "created") {
|
|
259
|
+
throw new Error(`E2B async session ${sessionId} returned an invalid claim result`);
|
|
260
|
+
}
|
|
261
|
+
const wrapped = [
|
|
262
|
+
`printf '%s\\n%s\\n' "$$" ${shellSingleQuote(fingerprint)} > ${shellSingleQuote(paths.admissionTmp)}`,
|
|
263
|
+
`mv ${shellSingleQuote(paths.admissionTmp)} ${shellSingleQuote(paths.admission)}`,
|
|
264
|
+
"{",
|
|
265
|
+
options.command,
|
|
266
|
+
`} > ${shellSingleQuote(paths.output)} 2>&1`,
|
|
267
|
+
"e2b_run_status=$?",
|
|
268
|
+
`printf '%s\\n' "$e2b_run_status" > ${shellSingleQuote(paths.exitTmp)}`,
|
|
269
|
+
`mv ${shellSingleQuote(paths.exitTmp)} ${shellSingleQuote(paths.exit)}`,
|
|
270
|
+
'exit "$e2b_run_status"',
|
|
271
|
+
].join("\n");
|
|
272
|
+
await sandbox.setTimeout(Math.max(this.sandboxLifetimeMs, this.runBudgetMs));
|
|
273
|
+
try {
|
|
274
|
+
const started = await sandbox.commands.run(wrapped, {
|
|
275
|
+
background: true,
|
|
276
|
+
timeoutMs: this.runBudgetMs,
|
|
277
|
+
...(options.timeoutMs && options.timeoutMs > 0
|
|
278
|
+
? { requestTimeoutMs: options.timeoutMs }
|
|
279
|
+
: {}),
|
|
280
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
281
|
+
envs: {
|
|
282
|
+
...(options.env ?? {}),
|
|
283
|
+
[ASYNC_SESSION_ENV]: paths.sessionToken,
|
|
284
|
+
[ASYNC_REQUEST_ENV]: fingerprint,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
if (!Number.isInteger(started.pid) || started.pid <= 0) {
|
|
288
|
+
throw new Error("E2B async command did not return a process id");
|
|
289
|
+
}
|
|
290
|
+
this.asyncAdmissions.set(admissionKey, { fingerprint, pid: started.pid });
|
|
291
|
+
return { sessionId, commandId: String(started.pid) };
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (isOutcomeUnknownAdmissionError(error)) {
|
|
295
|
+
const reconciled = await this.reconcileOutcomeUnknownAdmission(sandbox, paths, fingerprint);
|
|
296
|
+
if (reconciled) {
|
|
297
|
+
this.asyncAdmissions.set(admissionKey, reconciled);
|
|
298
|
+
return {
|
|
299
|
+
sessionId,
|
|
300
|
+
commandId: String(reconciled.pid),
|
|
301
|
+
reconciled: true,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
194
307
|
}
|
|
195
|
-
async getScriptStatus(handle, sessionId,
|
|
308
|
+
async getScriptStatus(handle, sessionId, commandId) {
|
|
196
309
|
const sandbox = await this.requireSandbox(handle);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
310
|
+
const paths = scriptRunPaths(sessionId);
|
|
311
|
+
const firstExit = parseShellExitCode(await this.readOptionalTextFile(sandbox, paths.exit) ?? "");
|
|
312
|
+
if (firstExit !== null) {
|
|
313
|
+
return { exitCode: firstExit };
|
|
314
|
+
}
|
|
315
|
+
const pid = parseProcessId(commandId);
|
|
316
|
+
if (pid === null) {
|
|
317
|
+
throw new Error(`Invalid E2B async command id "${commandId}"`);
|
|
318
|
+
}
|
|
319
|
+
const processes = await sandbox.commands.list();
|
|
320
|
+
if (processes.some((process) => isSessionProcess(process, paths.sessionToken, pid))) {
|
|
202
321
|
return { exitCode: null };
|
|
203
322
|
}
|
|
204
|
-
|
|
205
|
-
|
|
323
|
+
// Close the race where the process exits after list() but before its
|
|
324
|
+
// wrapper atomically publishes the exit sidecar.
|
|
325
|
+
const finalExit = parseShellExitCode(await this.readOptionalTextFile(sandbox, paths.exit) ?? "");
|
|
326
|
+
return {
|
|
327
|
+
exitCode: finalExit ?? E2B_ASYNC_PROCESS_LOST_EXIT_CODE,
|
|
328
|
+
};
|
|
206
329
|
}
|
|
207
330
|
async getScriptLogs(handle, sessionId, commandId) {
|
|
208
331
|
const sandbox = await this.requireSandbox(handle);
|
|
209
|
-
const
|
|
210
|
-
const output = await this.readFileBestEffort(sandbox, outPath, SCRIPT_LOG_READ_MAX_BYTES);
|
|
211
|
-
// exitCode:null — status is the source of truth for the exit code (matches
|
|
212
|
-
// Daytona/Local getScriptLogs).
|
|
332
|
+
const output = await this.readBoundedFile(sandbox, scriptRunPaths(sessionId).output, SCRIPT_LOG_READ_MAX_BYTES);
|
|
213
333
|
return { output, exitCode: null, cmdId: commandId };
|
|
214
334
|
}
|
|
335
|
+
startExec(handle, command, options = {}) {
|
|
336
|
+
return this.startScript(handle, {
|
|
337
|
+
command,
|
|
338
|
+
cwd: options.cwd,
|
|
339
|
+
env: options.env,
|
|
340
|
+
timeoutMs: options.timeoutMs,
|
|
341
|
+
sessionId: options.sessionId,
|
|
342
|
+
suppressInputEcho: true,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
getExecStatus(handle, sessionId, commandId) {
|
|
346
|
+
return this.getScriptStatus(handle, sessionId, commandId);
|
|
347
|
+
}
|
|
348
|
+
async getExecLogs(handle, sessionId, commandId) {
|
|
349
|
+
const status = await this.getScriptStatus(handle, sessionId, commandId);
|
|
350
|
+
if (status.exitCode === null) {
|
|
351
|
+
throw new Error(`E2B async command "${commandId}" is still running`);
|
|
352
|
+
}
|
|
353
|
+
const logs = await this.getScriptLogs(handle, sessionId, commandId);
|
|
354
|
+
return { output: logs.output, exitCode: status.exitCode };
|
|
355
|
+
}
|
|
356
|
+
async uploadFile(handle, source, destination) {
|
|
357
|
+
const sandbox = await this.requireSandbox(handle);
|
|
358
|
+
const bytes = typeof source === "string"
|
|
359
|
+
? await import("node:fs/promises").then((fs) => fs.readFile(source))
|
|
360
|
+
: source;
|
|
361
|
+
await sandbox.files.write(destination, toArrayBuffer(bytes));
|
|
362
|
+
}
|
|
363
|
+
async uploadBundle(handle, options) {
|
|
364
|
+
for (const file of options.files) {
|
|
365
|
+
await this.uploadFile(handle, file.source, file.destination);
|
|
366
|
+
}
|
|
367
|
+
const destinations = options.files.map((file) => file.destination);
|
|
368
|
+
if (options.manifest !== undefined) {
|
|
369
|
+
const manifestPath = options.manifestPath ?? "/workspace/manifest.json";
|
|
370
|
+
await this.uploadFile(handle, Buffer.from(JSON.stringify(options.manifest, null, 2), "utf8"), manifestPath);
|
|
371
|
+
destinations.push(manifestPath);
|
|
372
|
+
}
|
|
373
|
+
await this.verifyUploadedBundleFiles(handle, destinations);
|
|
374
|
+
}
|
|
375
|
+
async downloadFile(handle, source, destination) {
|
|
376
|
+
const sandbox = await this.requireSandbox(handle);
|
|
377
|
+
const bytes = await sandbox.files.read(source, { format: "bytes" });
|
|
378
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
379
|
+
if (destination) {
|
|
380
|
+
await import("node:fs/promises").then((fs) => fs.writeFile(destination, buffer));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
return buffer;
|
|
384
|
+
}
|
|
385
|
+
async getHomeDir(handle) {
|
|
386
|
+
if (handle.homeDir) {
|
|
387
|
+
return handle.homeDir;
|
|
388
|
+
}
|
|
389
|
+
const result = await this.runScript(handle, {
|
|
390
|
+
command: `printf '%s' "$HOME"`,
|
|
391
|
+
timeoutMs: 30_000,
|
|
392
|
+
});
|
|
393
|
+
const homeDir = result.exitCode === 0 ? result.stdout?.trim() ?? "" : "";
|
|
394
|
+
if (!homeDir) {
|
|
395
|
+
throw new Error(`Failed to resolve home directory for E2B sandbox "${handle.id}"`);
|
|
396
|
+
}
|
|
397
|
+
handle.homeDir = homeDir;
|
|
398
|
+
return homeDir;
|
|
399
|
+
}
|
|
400
|
+
async stop(handle) {
|
|
401
|
+
const entry = this.registrations.get(handle.id);
|
|
402
|
+
if (!entry || !entry.owned) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const statics = await this.statics();
|
|
406
|
+
await statics.pause(handle.id, { apiKey: this.apiKey });
|
|
407
|
+
entry.sandbox = undefined;
|
|
408
|
+
entry.state = "paused";
|
|
409
|
+
handle.state = "STOPPED";
|
|
410
|
+
}
|
|
215
411
|
async start(handle) {
|
|
216
|
-
|
|
217
|
-
|
|
412
|
+
const entry = this.registrations.get(handle.id);
|
|
413
|
+
if (!entry || !entry.owned) {
|
|
414
|
+
return handle;
|
|
415
|
+
}
|
|
416
|
+
const statics = await this.statics();
|
|
417
|
+
const sandbox = await statics.connect(handle.id, {
|
|
418
|
+
apiKey: this.apiKey,
|
|
419
|
+
timeoutMs: this.sandboxLifetimeMs,
|
|
420
|
+
});
|
|
421
|
+
entry.sandbox = sandbox;
|
|
422
|
+
entry.state = "running";
|
|
423
|
+
handle.state = "STARTED";
|
|
218
424
|
return handle;
|
|
219
425
|
}
|
|
220
|
-
async stop(_handle) {
|
|
221
|
-
// No-op (see start): pause/resume lifecycle deferred to warm-reuse follow-up.
|
|
222
|
-
}
|
|
223
426
|
async destroy(handle) {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
427
|
+
const entry = this.registrations.get(handle.id);
|
|
428
|
+
if (!entry) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!entry.owned) {
|
|
432
|
+
this.registrations.delete(handle.id);
|
|
227
433
|
return;
|
|
228
434
|
}
|
|
229
|
-
await
|
|
435
|
+
const statics = await this.statics();
|
|
436
|
+
await statics.kill(handle.id, { apiKey: this.apiKey });
|
|
437
|
+
this.registrations.delete(handle.id);
|
|
230
438
|
}
|
|
231
|
-
// --- internals ----------------------------------------------------------
|
|
232
439
|
async statics() {
|
|
233
440
|
if (this.injectedStatics) {
|
|
234
441
|
return this.injectedStatics;
|
|
235
442
|
}
|
|
236
443
|
if (!this.staticsPromise) {
|
|
237
|
-
// Lazy — keeps `e2b` out of every non-E2B bundle and off the eager-import
|
|
238
|
-
// ban list.
|
|
239
444
|
this.staticsPromise = import("e2b").then((mod) => mod.Sandbox);
|
|
240
445
|
}
|
|
241
446
|
return this.staticsPromise;
|
|
242
447
|
}
|
|
448
|
+
async listSandboxes(labels, states, limit) {
|
|
449
|
+
const statics = await this.statics();
|
|
450
|
+
const query = {
|
|
451
|
+
...(hasEntries(labels) ? { metadata: labels } : {}),
|
|
452
|
+
...(states ? { state: states } : {}),
|
|
453
|
+
};
|
|
454
|
+
return statics.list({
|
|
455
|
+
apiKey: this.apiKey,
|
|
456
|
+
limit,
|
|
457
|
+
...(hasEntries(query) ? { query } : {}),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
registerInfo(info, options) {
|
|
461
|
+
const existing = this.registrations.get(info.sandboxId);
|
|
462
|
+
this.registrations.set(info.sandboxId, {
|
|
463
|
+
...(existing?.sandbox ? { sandbox: existing.sandbox } : {}),
|
|
464
|
+
owned: options.owned ?? false,
|
|
465
|
+
state: info.state,
|
|
466
|
+
});
|
|
467
|
+
return {
|
|
468
|
+
...handleFromE2BInfo(info),
|
|
469
|
+
...(options.homeDir ? { homeDir: options.homeDir } : {}),
|
|
470
|
+
...(options.workdir ? { workdir: options.workdir } : {}),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
registerSandbox(sandbox, options) {
|
|
474
|
+
this.registrations.set(sandbox.sandboxId, {
|
|
475
|
+
sandbox,
|
|
476
|
+
owned: options.owned,
|
|
477
|
+
state: "running",
|
|
478
|
+
});
|
|
479
|
+
return {
|
|
480
|
+
id: sandbox.sandboxId,
|
|
481
|
+
state: "STARTED",
|
|
482
|
+
...(options.homeDir ? { homeDir: options.homeDir } : {}),
|
|
483
|
+
...(options.workdir ? { workdir: options.workdir } : {}),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
243
486
|
async requireSandbox(handle) {
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
throw new Error(`E2B sandbox "${handle.id}" is
|
|
487
|
+
const registered = this.registrations.get(handle.id);
|
|
488
|
+
if (registered?.state === "paused") {
|
|
489
|
+
throw new Error(`E2B sandbox "${handle.id}" is paused; call start() before using it`);
|
|
247
490
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
async connectSandbox(id) {
|
|
251
|
-
const cached = this.sandboxes.get(id);
|
|
252
|
-
if (cached) {
|
|
253
|
-
return cached;
|
|
491
|
+
if (registered?.sandbox) {
|
|
492
|
+
return registered.sandbox;
|
|
254
493
|
}
|
|
255
494
|
const statics = await this.statics();
|
|
256
495
|
try {
|
|
257
|
-
const sandbox = await statics.connect(id, {
|
|
258
|
-
|
|
496
|
+
const sandbox = await statics.connect(handle.id, {
|
|
497
|
+
apiKey: this.apiKey,
|
|
498
|
+
timeoutMs: this.sandboxLifetimeMs,
|
|
499
|
+
});
|
|
500
|
+
this.registrations.set(handle.id, {
|
|
501
|
+
sandbox,
|
|
502
|
+
owned: registered?.owned ?? false,
|
|
503
|
+
state: "running",
|
|
504
|
+
});
|
|
259
505
|
return sandbox;
|
|
260
506
|
}
|
|
261
507
|
catch (error) {
|
|
262
508
|
if (isSandboxNotFound(error)) {
|
|
263
|
-
|
|
509
|
+
throw new Error(`E2B sandbox "${handle.id}" is no longer available`, { cause: error });
|
|
264
510
|
}
|
|
265
511
|
throw error;
|
|
266
512
|
}
|
|
267
513
|
}
|
|
268
|
-
async
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
514
|
+
async verifyUploadedBundleFiles(handle, destinations) {
|
|
515
|
+
if (destinations.length === 0) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const result = await this.runScript(handle, {
|
|
519
|
+
command: destinations
|
|
520
|
+
.map((destination) => `test -f ${shellSingleQuote(destination)}`)
|
|
521
|
+
.join(" && "),
|
|
522
|
+
timeoutMs: 30_000,
|
|
523
|
+
});
|
|
524
|
+
if (result.exitCode !== 0) {
|
|
525
|
+
throw new Error(`Failed to verify uploaded E2B bundle files: ${result.output || "remote file check failed"}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async reconcileExistingAdmission(sandbox, sessionId, paths, fingerprint) {
|
|
529
|
+
const recordedRequest = (await this.readOptionalTextFile(sandbox, paths.request))?.trim();
|
|
530
|
+
if (!recordedRequest) {
|
|
531
|
+
throw new Error(`E2B async session ${sessionId} is already claimed but has no authoritative request record`);
|
|
532
|
+
}
|
|
533
|
+
if (recordedRequest !== fingerprint) {
|
|
534
|
+
throw asyncSessionConflict(sessionId);
|
|
535
|
+
}
|
|
536
|
+
const recordedAdmission = parseAdmissionRecord(await this.readOptionalTextFile(sandbox, paths.admission));
|
|
537
|
+
if (recordedAdmission && recordedAdmission.fingerprint !== fingerprint) {
|
|
538
|
+
throw new Error(`E2B async session ${sessionId} has a mismatched admission record`);
|
|
539
|
+
}
|
|
540
|
+
const processes = matchingSessionProcesses(await sandbox.commands.list(), paths.sessionToken, fingerprint);
|
|
541
|
+
if (recordedAdmission) {
|
|
542
|
+
await this.killProcessesExcept(sandbox, processes, recordedAdmission.pid);
|
|
543
|
+
return recordedAdmission;
|
|
544
|
+
}
|
|
545
|
+
if (processes.length === 1) {
|
|
546
|
+
const admission = { fingerprint, pid: processes[0].pid };
|
|
547
|
+
await sandbox.files.write(paths.admission, `${admission.pid}\n${admission.fingerprint}\n`);
|
|
548
|
+
return admission;
|
|
549
|
+
}
|
|
550
|
+
if (processes.length > 1) {
|
|
551
|
+
await this.killProcessesExcept(sandbox, processes, null);
|
|
552
|
+
}
|
|
553
|
+
throw new Error(`E2B async session ${sessionId} is already claimed but admission cannot be proven`);
|
|
554
|
+
}
|
|
555
|
+
async reconcileOutcomeUnknownAdmission(sandbox, paths, fingerprint) {
|
|
556
|
+
const deadline = Date.now() + ADMISSION_RECONCILIATION_TIMEOUT_MS;
|
|
557
|
+
do {
|
|
558
|
+
const recorded = parseAdmissionRecord(await this.readOptionalTextFile(sandbox, paths.admission));
|
|
559
|
+
const processes = matchingSessionProcesses(await sandbox.commands.list(), paths.sessionToken, fingerprint);
|
|
560
|
+
if (recorded?.fingerprint === fingerprint) {
|
|
561
|
+
await this.killProcessesExcept(sandbox, processes, recorded.pid);
|
|
562
|
+
return recorded;
|
|
563
|
+
}
|
|
564
|
+
if (processes.length === 1) {
|
|
565
|
+
const admission = { fingerprint, pid: processes[0].pid };
|
|
566
|
+
await sandbox.files.write(paths.admission, `${admission.pid}\n${admission.fingerprint}\n`);
|
|
567
|
+
return admission;
|
|
568
|
+
}
|
|
569
|
+
if (processes.length > 1) {
|
|
570
|
+
await this.killProcessesExcept(sandbox, processes, null);
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
if (Date.now() < deadline) {
|
|
574
|
+
await delay(ADMISSION_RECONCILIATION_INTERVAL_MS);
|
|
575
|
+
}
|
|
576
|
+
} while (Date.now() < deadline);
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
async killDuplicateProcesses(sandbox, sessionToken, fingerprint, admittedPid) {
|
|
580
|
+
const matches = matchingSessionProcesses(await sandbox.commands.list(), sessionToken, fingerprint);
|
|
581
|
+
await this.killProcessesExcept(sandbox, matches, admittedPid);
|
|
582
|
+
}
|
|
583
|
+
async killProcessesExcept(sandbox, processes, admittedPid) {
|
|
584
|
+
for (const process of processes) {
|
|
585
|
+
if (process.pid !== admittedPid) {
|
|
586
|
+
await sandbox.commands.kill(process.pid);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async readOptionalTextFile(sandbox, path, requestTimeoutMs) {
|
|
272
591
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
592
|
+
return await sandbox.files.read(path, {
|
|
593
|
+
format: "text",
|
|
594
|
+
...(requestTimeoutMs ? { requestTimeoutMs } : {}),
|
|
595
|
+
});
|
|
275
596
|
}
|
|
276
|
-
catch {
|
|
277
|
-
|
|
597
|
+
catch (error) {
|
|
598
|
+
if (isFileNotFound(error)) {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
throw error;
|
|
278
602
|
}
|
|
279
603
|
}
|
|
604
|
+
async readBoundedFile(sandbox, path, maxBytes, requestTimeoutMs) {
|
|
605
|
+
// Unlike `runScript`, this runs a fixed bounded `tail`, not caller work, so
|
|
606
|
+
// E2B's own command default is an appropriate wall when no cap is given.
|
|
607
|
+
const result = await sandbox.commands.run(`tail -c ${maxBytes} ${shellSingleQuote(path)} 2>/dev/null || true`, requestTimeoutMs
|
|
608
|
+
? { timeoutMs: requestTimeoutMs, requestTimeoutMs }
|
|
609
|
+
: undefined);
|
|
610
|
+
return result.stdout ?? "";
|
|
611
|
+
}
|
|
280
612
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const params = new URLSearchParams();
|
|
289
|
-
for (const [key, value] of entries) {
|
|
290
|
-
params.append(key, value);
|
|
291
|
-
}
|
|
292
|
-
return params.toString();
|
|
613
|
+
function commandResult(result) {
|
|
614
|
+
return {
|
|
615
|
+
output: combineOutput(result.stdout, result.stderr),
|
|
616
|
+
...(result.stdout ? { stdout: result.stdout } : {}),
|
|
617
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
618
|
+
exitCode: typeof result.exitCode === "number" ? result.exitCode : null,
|
|
619
|
+
};
|
|
293
620
|
}
|
|
294
621
|
function handleFromE2BInfo(info) {
|
|
295
622
|
return {
|
|
@@ -299,22 +626,49 @@ function handleFromE2BInfo(info) {
|
|
|
299
626
|
};
|
|
300
627
|
}
|
|
301
628
|
function normalizeE2BState(state) {
|
|
302
|
-
// Map to the normalized vocabulary the delivery path reasons about (Daytona
|
|
303
|
-
// STARTED/STOPPED). A paused E2B sandbox is not cold-reusable in this PR
|
|
304
|
-
// (warm resume is the follow-up) so it reads as STOPPED and is excluded by the
|
|
305
|
-
// default ["STARTED"] filter → cold provisioning.
|
|
306
629
|
return state === "running" ? "STARTED" : "STOPPED";
|
|
307
630
|
}
|
|
631
|
+
function normalizeRequestedState(state) {
|
|
632
|
+
const normalized = state.toUpperCase();
|
|
633
|
+
if (normalized === "RUNNING") {
|
|
634
|
+
return "STARTED";
|
|
635
|
+
}
|
|
636
|
+
if (normalized === "PAUSED") {
|
|
637
|
+
return "STOPPED";
|
|
638
|
+
}
|
|
639
|
+
return normalized;
|
|
640
|
+
}
|
|
308
641
|
function matchesState(state, states) {
|
|
309
642
|
if (states === null) {
|
|
310
643
|
return true;
|
|
311
644
|
}
|
|
312
|
-
|
|
313
|
-
|
|
645
|
+
if (!state) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
const actual = normalizeE2BState(state);
|
|
649
|
+
return states.some((candidate) => normalizeRequestedState(candidate) === actual);
|
|
650
|
+
}
|
|
651
|
+
function toE2BStates(states) {
|
|
652
|
+
if (states === null) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
const providerStates = new Set();
|
|
656
|
+
for (const state of states) {
|
|
657
|
+
const normalized = normalizeRequestedState(state);
|
|
658
|
+
if (normalized === "STARTED") {
|
|
659
|
+
providerStates.add("running");
|
|
660
|
+
}
|
|
661
|
+
else if (normalized === "STOPPED") {
|
|
662
|
+
providerStates.add("paused");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return [...providerStates];
|
|
314
666
|
}
|
|
315
667
|
function combineOutput(stdout, stderr) {
|
|
316
668
|
if (stdout && stderr) {
|
|
317
|
-
return stdout.endsWith("\n") || stderr.startsWith("\n")
|
|
669
|
+
return stdout.endsWith("\n") || stderr.startsWith("\n")
|
|
670
|
+
? `${stdout}${stderr}`
|
|
671
|
+
: `${stdout}\n${stderr}`;
|
|
318
672
|
}
|
|
319
673
|
return stdout || stderr || "";
|
|
320
674
|
}
|
|
@@ -336,15 +690,46 @@ function normalizeCommandError(error) {
|
|
|
336
690
|
exitCode: candidate.exitCode,
|
|
337
691
|
};
|
|
338
692
|
}
|
|
693
|
+
function isOutcomeUnknownAdmissionError(error) {
|
|
694
|
+
const record = error && typeof error === "object"
|
|
695
|
+
? error
|
|
696
|
+
: null;
|
|
697
|
+
const code = typeof record?.code === "string" ? record.code.toUpperCase() : "";
|
|
698
|
+
const name = typeof record?.name === "string" ? record.name : "";
|
|
699
|
+
const message = error instanceof Error
|
|
700
|
+
? error.message
|
|
701
|
+
: typeof record?.message === "string"
|
|
702
|
+
? record.message
|
|
703
|
+
: String(error);
|
|
704
|
+
return (code === "ECONNABORTED"
|
|
705
|
+
|| code === "ETIMEDOUT"
|
|
706
|
+
|| code === "ECONNRESET"
|
|
707
|
+
|| name === "TimeoutError"
|
|
708
|
+
|| /\btimed out\b/iu.test(message)
|
|
709
|
+
|| /\bnetwork connection lost\b/iu.test(message)
|
|
710
|
+
|| /\bsocket hang up\b/iu.test(message)
|
|
711
|
+
|| /\bfetch failed\b/iu.test(message));
|
|
712
|
+
}
|
|
339
713
|
function isSandboxNotFound(error) {
|
|
340
714
|
if (typeof error !== "object" || error === null) {
|
|
341
715
|
return false;
|
|
342
716
|
}
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
717
|
+
const record = error;
|
|
718
|
+
return (record.name === "NotFoundError"
|
|
719
|
+
|| record.name === "SandboxNotFoundError"
|
|
720
|
+
|| record.status === 404
|
|
721
|
+
|| record.statusCode === 404
|
|
722
|
+
|| (typeof record.message === "string" && /not\s*found/i.test(record.message)));
|
|
723
|
+
}
|
|
724
|
+
function isFileNotFound(error) {
|
|
725
|
+
if (typeof error !== "object" || error === null) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
const record = error;
|
|
729
|
+
return (record.name === "FileNotFoundError"
|
|
730
|
+
|| record.status === 404
|
|
731
|
+
|| record.statusCode === 404
|
|
732
|
+
|| (typeof record.message === "string" && /file.*not\s*found|no such file/iu.test(record.message)));
|
|
348
733
|
}
|
|
349
734
|
function toArrayBuffer(buffer) {
|
|
350
735
|
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
@@ -352,9 +737,109 @@ function toArrayBuffer(buffer) {
|
|
|
352
737
|
function hasEntries(record) {
|
|
353
738
|
return !!record && Object.keys(record).length > 0;
|
|
354
739
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
740
|
+
function positiveDuration(value, fallback) {
|
|
741
|
+
return Number.isFinite(value) && value !== undefined && value > 0
|
|
742
|
+
? Math.ceil(value)
|
|
743
|
+
: fallback;
|
|
744
|
+
}
|
|
745
|
+
function pageSize(options) {
|
|
746
|
+
const value = options.limit ?? options.pageSize ?? 10;
|
|
747
|
+
return Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : 10;
|
|
748
|
+
}
|
|
749
|
+
function lookupDeadline(timeoutMs) {
|
|
750
|
+
const normalized = positiveDuration(timeoutMs, DEFAULT_LOOKUP_TIMEOUT_MS);
|
|
751
|
+
return { endsAt: Date.now() + normalized, timeoutMs: normalized };
|
|
752
|
+
}
|
|
753
|
+
async function awaitLookupOperation(operation, deadline, description) {
|
|
754
|
+
const remainingMs = deadline.endsAt - Date.now();
|
|
755
|
+
if (remainingMs <= 0) {
|
|
756
|
+
throw new Error(`E2B sandbox lookup exceeded ${deadline.timeoutMs}ms while ${description}`);
|
|
757
|
+
}
|
|
758
|
+
let timer;
|
|
759
|
+
try {
|
|
760
|
+
return await Promise.race([
|
|
761
|
+
operation,
|
|
762
|
+
new Promise((_resolve, reject) => {
|
|
763
|
+
timer = setTimeout(() => {
|
|
764
|
+
reject(new Error(`E2B sandbox lookup exceeded ${deadline.timeoutMs}ms while ${description}`));
|
|
765
|
+
}, remainingMs);
|
|
766
|
+
}),
|
|
767
|
+
]);
|
|
768
|
+
}
|
|
769
|
+
finally {
|
|
770
|
+
if (timer !== undefined) {
|
|
771
|
+
clearTimeout(timer);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function parseShellExitCode(value) {
|
|
776
|
+
const normalized = value.trim();
|
|
777
|
+
if (!/^\d{1,3}$/u.test(normalized)) {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
const exitCode = Number(normalized);
|
|
781
|
+
return Number.isInteger(exitCode) && exitCode >= 0 && exitCode <= 255
|
|
782
|
+
? exitCode
|
|
783
|
+
: null;
|
|
784
|
+
}
|
|
785
|
+
function parseProcessId(value) {
|
|
786
|
+
const normalized = value.trim();
|
|
787
|
+
if (!/^\d+$/u.test(normalized)) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
const pid = Number(normalized);
|
|
791
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
792
|
+
}
|
|
793
|
+
function parseAdmissionRecord(value) {
|
|
794
|
+
if (value === null) {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
const [rawPid, fingerprint, ...extra] = value.trim().split(/\s+/u);
|
|
798
|
+
const pid = parseProcessId(rawPid ?? "");
|
|
799
|
+
if (pid === null || !fingerprint || extra.length > 0 || !/^[a-f\d]{64}$/u.test(fingerprint)) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
return { fingerprint, pid };
|
|
803
|
+
}
|
|
804
|
+
function asyncRequestFingerprint(options) {
|
|
805
|
+
const env = Object.entries(options.env ?? {}).sort(([left], [right]) => left.localeCompare(right));
|
|
806
|
+
return createHash("sha256").update(JSON.stringify({
|
|
807
|
+
command: options.command,
|
|
808
|
+
cwd: options.cwd ?? null,
|
|
809
|
+
env,
|
|
810
|
+
})).digest("hex");
|
|
811
|
+
}
|
|
812
|
+
function asyncSessionConflict(sessionId) {
|
|
813
|
+
return new Error(`E2B async session ${sessionId} is already claimed by a different request`);
|
|
814
|
+
}
|
|
815
|
+
function matchingSessionProcesses(processes, sessionToken, fingerprint) {
|
|
816
|
+
return processes.filter((process) => (process.envs?.[ASYNC_SESSION_ENV] === sessionToken
|
|
817
|
+
&& process.envs?.[ASYNC_REQUEST_ENV] === fingerprint));
|
|
818
|
+
}
|
|
819
|
+
function isSessionProcess(process, sessionToken, pid) {
|
|
820
|
+
return process.pid === pid && process.envs?.[ASYNC_SESSION_ENV] === sessionToken;
|
|
821
|
+
}
|
|
822
|
+
function delay(timeoutMs) {
|
|
823
|
+
return new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
824
|
+
}
|
|
825
|
+
function scriptRunPaths(sessionId) {
|
|
826
|
+
// Base64url is filesystem-safe and collision-free for the original UTF-8
|
|
827
|
+
// bytes, unlike replacing punctuation with underscores.
|
|
828
|
+
const safe = Buffer.from(sessionId, "utf8").toString("base64url") || "empty";
|
|
829
|
+
const root = "/tmp/e2b-run";
|
|
830
|
+
const dir = `${root}/${safe}`;
|
|
831
|
+
return {
|
|
832
|
+
root,
|
|
833
|
+
dir,
|
|
834
|
+
output: `${dir}/out`,
|
|
835
|
+
exit: `${dir}/exit`,
|
|
836
|
+
exitTmp: `${dir}/exit.tmp`,
|
|
837
|
+
admission: `${dir}/admission`,
|
|
838
|
+
admissionTmp: `${dir}/admission.tmp`,
|
|
839
|
+
request: `${dir}/request`,
|
|
840
|
+
requestTmp: `${dir}/request.tmp`,
|
|
841
|
+
sessionToken: safe,
|
|
842
|
+
};
|
|
358
843
|
}
|
|
359
844
|
function shellSingleQuote(value) {
|
|
360
845
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|