@sandboxrouter/sdk 0.1.0 → 0.3.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/README.md +271 -62
- package/dist/bench.js +4 -4
- package/dist/catalog.d.ts +232 -3
- package/dist/catalog.js +569 -14
- package/dist/check.d.ts +28 -0
- package/dist/check.js +185 -0
- package/dist/cli.js +149 -12
- package/dist/computers.d.ts +55 -13
- package/dist/computers.js +132 -6
- package/dist/conformance.d.ts +46 -0
- package/dist/conformance.js +65 -0
- package/dist/cost.d.ts +67 -0
- package/dist/cost.js +153 -0
- package/dist/gc.d.ts +29 -0
- package/dist/gc.js +54 -0
- package/dist/index.d.ts +34 -2
- package/dist/index.js +34 -2
- package/dist/leaderboard.d.ts +6 -0
- package/dist/leaderboard.js +5 -4
- package/dist/ledger.d.ts +38 -0
- package/dist/ledger.js +89 -0
- package/dist/providers/agent37.d.ts +40 -0
- package/dist/providers/agent37.js +224 -0
- package/dist/providers/agentcore.d.ts +71 -0
- package/dist/providers/agentcore.js +446 -0
- package/dist/providers/alibaba.d.ts +18 -0
- package/dist/providers/alibaba.js +106 -0
- package/dist/providers/azure.d.ts +43 -0
- package/dist/providers/azure.js +330 -0
- package/dist/providers/beam.d.ts +11 -0
- package/dist/providers/beam.js +179 -0
- package/dist/providers/blaxel.d.ts +3 -1
- package/dist/providers/blaxel.js +32 -22
- package/dist/providers/boat.d.ts +25 -0
- package/dist/providers/boat.js +263 -0
- package/dist/providers/cloudflare.d.ts +30 -0
- package/dist/providers/cloudflare.js +152 -0
- package/dist/providers/codesandbox.d.ts +59 -0
- package/dist/providers/codesandbox.js +287 -0
- package/dist/providers/daytona.d.ts +25 -4
- package/dist/providers/daytona.js +449 -28
- package/dist/providers/dedalus.d.ts +12 -0
- package/dist/providers/dedalus.js +244 -0
- package/dist/providers/deno.d.ts +50 -0
- package/dist/providers/deno.js +170 -0
- package/dist/providers/docker.d.ts +17 -0
- package/dist/providers/docker.js +154 -0
- package/dist/providers/e2b.d.ts +70 -3
- package/dist/providers/e2b.js +376 -8
- package/dist/providers/exedev.d.ts +23 -0
- package/dist/providers/exedev.js +290 -0
- package/dist/providers/freestyle.d.ts +49 -0
- package/dist/providers/freestyle.js +261 -0
- package/dist/providers/hopx.d.ts +15 -0
- package/dist/providers/hopx.js +177 -0
- package/dist/providers/instabox.d.ts +76 -0
- package/dist/providers/instabox.js +360 -0
- package/dist/providers/koyeb.d.ts +71 -0
- package/dist/providers/koyeb.js +379 -0
- package/dist/providers/lambda.d.ts +51 -0
- package/dist/providers/lambda.js +467 -0
- package/dist/providers/langsmith.d.ts +16 -0
- package/dist/providers/langsmith.js +149 -0
- package/dist/providers/limrun.d.ts +77 -0
- package/dist/providers/limrun.js +626 -0
- package/dist/providers/maritime.d.ts +5 -1
- package/dist/providers/maritime.js +39 -8
- package/dist/providers/mobilerun.d.ts +44 -0
- package/dist/providers/mobilerun.js +417 -0
- package/dist/providers/modal.js +12 -4
- package/dist/providers/morph.js +12 -4
- package/dist/providers/northflank.d.ts +21 -0
- package/dist/providers/northflank.js +259 -0
- package/dist/providers/novita.d.ts +12 -0
- package/dist/providers/novita.js +81 -0
- package/dist/providers/openscale.d.ts +17 -0
- package/dist/providers/openscale.js +87 -0
- package/dist/providers/orgo.d.ts +45 -0
- package/dist/providers/orgo.js +414 -0
- package/dist/providers/sail.d.ts +32 -0
- package/dist/providers/sail.js +372 -0
- package/dist/providers/superserve.d.ts +25 -0
- package/dist/providers/superserve.js +337 -0
- package/dist/providers/tensorlake.js +17 -14
- package/dist/providers/upstash.d.ts +30 -0
- package/dist/providers/upstash.js +258 -0
- package/dist/providers/vercel.js +14 -3
- package/dist/providers/workspaces.d.ts +60 -0
- package/dist/providers/workspaces.js +662 -0
- package/dist/router.d.ts +36 -5
- package/dist/router.js +174 -25
- package/dist/server.d.ts +1 -1
- package/dist/server.js +22 -3
- package/dist/types.d.ts +18 -6
- package/dist/util.d.ts +28 -1
- package/dist/util.js +56 -7
- package/package.json +106 -17
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import * as zlib from "node:zlib";
|
|
3
|
+
import { ProviderError } from "../types.js";
|
|
4
|
+
import { dirname, killedByTimeout, optionalImport, shq, timeoutError, toBytes, wrapCommand } from "../util.js";
|
|
5
|
+
/**
|
|
6
|
+
* Amazon Bedrock AgentCore Code Interpreter: AWS-managed sandboxes (2 vCPU / 8 GB, 10 GB disk) with a
|
|
7
|
+
* shell, called over the AgentCore data plane with SigV4 (service "bedrock-agentcore").
|
|
8
|
+
*
|
|
9
|
+
* PUT /code-interpreters/{id}/sessions/start {"name", "sessionTimeoutSeconds"} -> {"sessionId", ...}
|
|
10
|
+
* GET /code-interpreters/{id}/sessions/get?sessionId=S -> {"status": "READY" | "TERMINATED", ...}
|
|
11
|
+
* POST /code-interpreters/{id}/tools/invoke header x-amzn-code-interpreter-session-id: S
|
|
12
|
+
* {"name": "executeCommand", "arguments": {"command": "..."}}
|
|
13
|
+
* -> event stream of {"result": {"content": [...], "structuredContent": {"stdout", "stderr", "exitCode"}, "isError"}}
|
|
14
|
+
* PUT /code-interpreters/{id}/sessions/stop?sessionId=S {}
|
|
15
|
+
* Host: https://bedrock-agentcore.{region}.amazonaws.com. The built-in interpreter id is aws.codeinterpreter.v1.
|
|
16
|
+
*
|
|
17
|
+
* Docs:
|
|
18
|
+
* https://docs.aws.amazon.com/bedrock-agentcore/latest/APIReference/API_StartCodeInterpreterSession.html
|
|
19
|
+
* https://docs.aws.amazon.com/bedrock-agentcore/latest/APIReference/API_InvokeCodeInterpreter.html
|
|
20
|
+
* https://docs.aws.amazon.com/bedrock-agentcore/latest/APIReference/API_GetCodeInterpreterSession.html
|
|
21
|
+
* https://docs.aws.amazon.com/bedrock-agentcore/latest/APIReference/API_StopCodeInterpreterSession.html
|
|
22
|
+
* https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/code-interpreter-api-reference-examples.html
|
|
23
|
+
* https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/bedrock-agentcore-limits.html (2vCPU/8GB, 15 min per request, 100 MB payload)
|
|
24
|
+
* https://docs.aws.amazon.com/general/latest/gr/bedrock_agentcore.html (endpoints)
|
|
25
|
+
* https://aws.amazon.com/bedrock/agentcore/pricing/
|
|
26
|
+
*
|
|
27
|
+
* Mapping:
|
|
28
|
+
* - Size is fixed at 2 vCPU / 8 GB; vcpu/memoryGb are ignored and the catalog caps limits there.
|
|
29
|
+
* - timeoutMinutes -> sessionTimeoutSeconds (1..28800; default 30 min).
|
|
30
|
+
* - No env API: spec.env and opts.env are exported in front of each command.
|
|
31
|
+
* - Files move as base64 through executeCommand (paths are plain shell paths, so absolute paths work
|
|
32
|
+
* the same as in exec). writeFile sends 48 KB chunks to stay under the kernel's 128 KB single-argument
|
|
33
|
+
* limit; readFile is one command.
|
|
34
|
+
* - Streaming: each result event the service sends is passed to onStdout/onStderr as it arrives.
|
|
35
|
+
*
|
|
36
|
+
* ## Setup (once)
|
|
37
|
+
*
|
|
38
|
+
* Nothing to create for the built-in interpreter. The caller's IAM identity needs
|
|
39
|
+
* bedrock-agentcore:StartCodeInterpreterSession, InvokeCodeInterpreter, GetCodeInterpreterSession and
|
|
40
|
+
* StopCodeInterpreterSession on arn:aws:bedrock-agentcore:{region}:aws:code-interpreter/aws.codeinterpreter.v1
|
|
41
|
+
* (or on your own interpreter, if you created one with CreateCodeInterpreter for VPC/network settings and
|
|
42
|
+
* set AGENTCORE_CODE_INTERPRETER_ID). Set AWS_REGION (or AGENTCORE_REGION) to a region AgentCore runs in,
|
|
43
|
+
* plus credentials the default AWS chain can find. Needs the optional peers @smithy/signature-v4,
|
|
44
|
+
* @aws-crypto/sha256-js and @aws-sdk/credential-provider-node.
|
|
45
|
+
*/
|
|
46
|
+
const ID = "agentcore";
|
|
47
|
+
const DEFAULT_INTERPRETER = "aws.codeinterpreter.v1";
|
|
48
|
+
const SESSION_ID = /^[0-9a-zA-Z]{1,40}$/;
|
|
49
|
+
/** Raw bytes per writeFile command: base64 makes it 64 KB, under Linux's 128 KB per-argument cap. */
|
|
50
|
+
const WRITE_CHUNK = 48 * 1024;
|
|
51
|
+
/** True when the default AWS chain plausibly has credentials (env keys, a profile, or a container/web identity). */
|
|
52
|
+
export function awsCredentialsHinted(env = process.env) {
|
|
53
|
+
return !!((env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY) ||
|
|
54
|
+
env.AWS_PROFILE ||
|
|
55
|
+
env.AWS_WEB_IDENTITY_TOKEN_FILE ||
|
|
56
|
+
env.AWS_CONTAINER_CREDENTIALS_FULL_URI ||
|
|
57
|
+
env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI);
|
|
58
|
+
}
|
|
59
|
+
/** SigV4-signs and sends JSON requests for one AWS service in one region. */
|
|
60
|
+
export class AwsClient {
|
|
61
|
+
provider;
|
|
62
|
+
service;
|
|
63
|
+
region;
|
|
64
|
+
credentials;
|
|
65
|
+
signer;
|
|
66
|
+
constructor(provider, service, region, credentials) {
|
|
67
|
+
this.provider = provider;
|
|
68
|
+
this.service = service;
|
|
69
|
+
this.region = region;
|
|
70
|
+
this.credentials = credentials;
|
|
71
|
+
}
|
|
72
|
+
getSigner() {
|
|
73
|
+
this.signer ??= (async () => {
|
|
74
|
+
const { SignatureV4 } = await optionalImport("@smithy/signature-v4", this.provider);
|
|
75
|
+
const { Sha256 } = await optionalImport("@aws-crypto/sha256-js", this.provider);
|
|
76
|
+
let credentials = this.credentials;
|
|
77
|
+
if (!credentials) {
|
|
78
|
+
const { defaultProvider } = await optionalImport("@aws-sdk/credential-provider-node", this.provider);
|
|
79
|
+
credentials = defaultProvider();
|
|
80
|
+
}
|
|
81
|
+
return new SignatureV4({ service: this.service, region: this.region, credentials, sha256: Sha256, applyChecksum: false });
|
|
82
|
+
})();
|
|
83
|
+
this.signer.catch(() => (this.signer = undefined));
|
|
84
|
+
return this.signer;
|
|
85
|
+
}
|
|
86
|
+
/** Signs and sends; returns the response as-is (callers check status). */
|
|
87
|
+
async send(method, url, opts = {}) {
|
|
88
|
+
const body = opts.json === undefined ? undefined : JSON.stringify(opts.json);
|
|
89
|
+
const headers = { host: url.host, ...opts.headers };
|
|
90
|
+
if (body !== undefined)
|
|
91
|
+
headers["content-type"] = "application/json";
|
|
92
|
+
const query = {};
|
|
93
|
+
url.searchParams.forEach((v, k) => (query[k] = v));
|
|
94
|
+
const signed = await (await this.getSigner()).sign({
|
|
95
|
+
method,
|
|
96
|
+
protocol: url.protocol,
|
|
97
|
+
hostname: url.hostname,
|
|
98
|
+
port: url.port ? Number(url.port) : undefined,
|
|
99
|
+
path: url.pathname,
|
|
100
|
+
query,
|
|
101
|
+
headers,
|
|
102
|
+
body,
|
|
103
|
+
});
|
|
104
|
+
const out = { ...signed.headers };
|
|
105
|
+
for (const k of Object.keys(out))
|
|
106
|
+
if (k.toLowerCase() === "host")
|
|
107
|
+
delete out[k]; // fetch sets it from the URL
|
|
108
|
+
try {
|
|
109
|
+
return await fetch(url, { method, headers: out, body, signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined });
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
throw new ProviderError(this.provider, `${method} ${url.pathname}: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** send() + parse JSON, throwing ProviderError (with the AWS error name) on non-2xx. */
|
|
116
|
+
async json(method, url, opts = {}) {
|
|
117
|
+
const res = await this.send(method, url, opts);
|
|
118
|
+
if (!res.ok)
|
|
119
|
+
await awsFail(this.provider, res, `${method} ${url.pathname}`);
|
|
120
|
+
const text = await res.text();
|
|
121
|
+
return (text ? JSON.parse(text) : {});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export async function awsFail(provider, res, what) {
|
|
125
|
+
const text = (await res.text().catch(() => "")).slice(0, 500);
|
|
126
|
+
let detail = text;
|
|
127
|
+
let name = res.headers.get("x-amzn-errortype")?.split(":")[0] ?? "";
|
|
128
|
+
try {
|
|
129
|
+
const j = JSON.parse(text);
|
|
130
|
+
detail = j?.message ?? j?.Message ?? text;
|
|
131
|
+
name ||= String(j?.__type ?? "").split("#").pop() ?? "";
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* not JSON */
|
|
135
|
+
}
|
|
136
|
+
throw new ProviderError(provider, `${what} -> ${res.status} ${name ? `${name}: ` : ""}${detail}`.trim(), res.status);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Decodes the AWS event stream binary framing: [total len u32][headers len u32][prelude crc u32]
|
|
140
|
+
* [headers][payload][message crc u32]. Header value types per the spec (7 = string).
|
|
141
|
+
*/
|
|
142
|
+
export async function* readEventStream(res) {
|
|
143
|
+
if (!res.body)
|
|
144
|
+
return;
|
|
145
|
+
const reader = res.body.getReader();
|
|
146
|
+
let buf = Buffer.alloc(0);
|
|
147
|
+
const crc32 = zlib.crc32;
|
|
148
|
+
for (;;) {
|
|
149
|
+
const { done, value } = await reader.read();
|
|
150
|
+
if (value)
|
|
151
|
+
buf = Buffer.concat([buf, Buffer.from(value)]);
|
|
152
|
+
while (buf.length >= 12) {
|
|
153
|
+
const total = buf.readUInt32BE(0);
|
|
154
|
+
if (total < 16)
|
|
155
|
+
throw new Error(`bad event stream frame length ${total}`);
|
|
156
|
+
if (buf.length < total)
|
|
157
|
+
break;
|
|
158
|
+
const frame = buf.subarray(0, total);
|
|
159
|
+
buf = buf.subarray(total);
|
|
160
|
+
if (crc32) {
|
|
161
|
+
if (crc32(frame.subarray(0, 8)) !== frame.readUInt32BE(8))
|
|
162
|
+
throw new Error("event stream prelude CRC mismatch");
|
|
163
|
+
if (crc32(frame.subarray(0, total - 4)) !== frame.readUInt32BE(total - 4))
|
|
164
|
+
throw new Error("event stream message CRC mismatch");
|
|
165
|
+
}
|
|
166
|
+
const hlen = frame.readUInt32BE(4);
|
|
167
|
+
yield { headers: decodeHeaders(frame.subarray(12, 12 + hlen)), payload: new Uint8Array(frame.subarray(12 + hlen, total - 4)) };
|
|
168
|
+
}
|
|
169
|
+
if (done) {
|
|
170
|
+
if (buf.length)
|
|
171
|
+
throw new Error("event stream ended mid-frame");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function decodeHeaders(b) {
|
|
177
|
+
const out = {};
|
|
178
|
+
let i = 0;
|
|
179
|
+
while (i < b.length) {
|
|
180
|
+
const nlen = b.readUInt8(i);
|
|
181
|
+
const name = b.subarray(i + 1, i + 1 + nlen).toString("utf8");
|
|
182
|
+
i += 1 + nlen;
|
|
183
|
+
const type = b.readUInt8(i++);
|
|
184
|
+
switch (type) {
|
|
185
|
+
case 0:
|
|
186
|
+
out[name] = true;
|
|
187
|
+
break;
|
|
188
|
+
case 1:
|
|
189
|
+
out[name] = false;
|
|
190
|
+
break;
|
|
191
|
+
case 2:
|
|
192
|
+
out[name] = b.readInt8(i);
|
|
193
|
+
i += 1;
|
|
194
|
+
break;
|
|
195
|
+
case 3:
|
|
196
|
+
out[name] = b.readInt16BE(i);
|
|
197
|
+
i += 2;
|
|
198
|
+
break;
|
|
199
|
+
case 4:
|
|
200
|
+
out[name] = b.readInt32BE(i);
|
|
201
|
+
i += 4;
|
|
202
|
+
break;
|
|
203
|
+
case 5:
|
|
204
|
+
case 8:
|
|
205
|
+
out[name] = Number(b.readBigInt64BE(i));
|
|
206
|
+
i += 8;
|
|
207
|
+
break;
|
|
208
|
+
case 6:
|
|
209
|
+
case 7: {
|
|
210
|
+
const len = b.readUInt16BE(i);
|
|
211
|
+
const v = b.subarray(i + 2, i + 2 + len);
|
|
212
|
+
out[name] = type === 7 ? v.toString("utf8") : new Uint8Array(v);
|
|
213
|
+
i += 2 + len;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 9:
|
|
217
|
+
out[name] = new Uint8Array(b.subarray(i, i + 16));
|
|
218
|
+
i += 16;
|
|
219
|
+
break;
|
|
220
|
+
default: throw new Error(`unknown event stream header type ${type}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
/** Encodes one event stream frame (string headers only). Used by tests' mocks. */
|
|
226
|
+
export function encodeEvent(headers, payload) {
|
|
227
|
+
const hparts = [];
|
|
228
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
229
|
+
const name = Buffer.from(k);
|
|
230
|
+
const val = Buffer.from(v);
|
|
231
|
+
const h = Buffer.alloc(1 + name.length + 1 + 2 + val.length);
|
|
232
|
+
h.writeUInt8(name.length, 0);
|
|
233
|
+
name.copy(h, 1);
|
|
234
|
+
h.writeUInt8(7, 1 + name.length);
|
|
235
|
+
h.writeUInt16BE(val.length, 2 + name.length);
|
|
236
|
+
val.copy(h, 4 + name.length);
|
|
237
|
+
hparts.push(h);
|
|
238
|
+
}
|
|
239
|
+
const hb = Buffer.concat(hparts);
|
|
240
|
+
const pb = Buffer.from(payload);
|
|
241
|
+
const total = 12 + hb.length + pb.length + 4;
|
|
242
|
+
const frame = Buffer.alloc(total);
|
|
243
|
+
frame.writeUInt32BE(total, 0);
|
|
244
|
+
frame.writeUInt32BE(hb.length, 4);
|
|
245
|
+
const crc32 = zlib.crc32 ?? (() => 0);
|
|
246
|
+
frame.writeUInt32BE(crc32(frame.subarray(0, 8)) >>> 0, 8);
|
|
247
|
+
hb.copy(frame, 12);
|
|
248
|
+
pb.copy(frame, 12 + hb.length);
|
|
249
|
+
frame.writeUInt32BE(crc32(frame.subarray(0, total - 4)) >>> 0, total - 4);
|
|
250
|
+
return frame;
|
|
251
|
+
}
|
|
252
|
+
export class AgentCoreProvider {
|
|
253
|
+
id = ID;
|
|
254
|
+
region;
|
|
255
|
+
interpreter;
|
|
256
|
+
opts;
|
|
257
|
+
client;
|
|
258
|
+
constructor(opts = {}) {
|
|
259
|
+
this.opts = opts;
|
|
260
|
+
this.region = opts.region ?? process.env.AGENTCORE_REGION ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
|
|
261
|
+
this.interpreter = opts.codeInterpreterId ?? process.env.AGENTCORE_CODE_INTERPRETER_ID ?? DEFAULT_INTERPRETER;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Opt-in: AWS credentials alone are common, so the router only uses AgentCore when you name an
|
|
265
|
+
* interpreter (AGENTCORE_CODE_INTERPRETER_ID=aws.codeinterpreter.v1 for the managed one) or
|
|
266
|
+
* construct the provider yourself.
|
|
267
|
+
*/
|
|
268
|
+
configured() {
|
|
269
|
+
const optedIn = Object.keys(this.opts).length > 0 || !!process.env.AGENTCORE_CODE_INTERPRETER_ID;
|
|
270
|
+
return optedIn && !!this.region && (!!this.opts.credentials || awsCredentialsHinted());
|
|
271
|
+
}
|
|
272
|
+
/** @internal */
|
|
273
|
+
aws() {
|
|
274
|
+
if (!this.region)
|
|
275
|
+
throw new ProviderError(ID, "set AWS_REGION (or AGENTCORE_REGION) to a region AgentCore Code Interpreter runs in");
|
|
276
|
+
this.client ??= new AwsClient(ID, "bedrock-agentcore", this.region, this.opts.credentials);
|
|
277
|
+
return this.client;
|
|
278
|
+
}
|
|
279
|
+
/** @internal */
|
|
280
|
+
url(path, query = {}) {
|
|
281
|
+
const base = this.opts.endpoint ?? `https://bedrock-agentcore.${this.region}.amazonaws.com`;
|
|
282
|
+
const u = new URL(`${base.replace(/\/+$/, "")}/code-interpreters/${encodeURIComponent(this.interpreter)}/${path}`);
|
|
283
|
+
for (const [k, v] of Object.entries(query))
|
|
284
|
+
u.searchParams.set(k, v);
|
|
285
|
+
return u;
|
|
286
|
+
}
|
|
287
|
+
async create(spec) {
|
|
288
|
+
const timeout = Math.min(28_800, Math.max(60, Math.round((spec.timeoutMinutes ?? 30) * 60)));
|
|
289
|
+
const name = (spec.metadata?.name ?? `sandboxrouter-${randomBytes(4).toString("hex")}`).slice(0, 100);
|
|
290
|
+
const r = await this.aws().json("PUT", this.url("sessions/start"), {
|
|
291
|
+
json: { name, sessionTimeoutSeconds: timeout, clientToken: randomBytes(24).toString("hex") },
|
|
292
|
+
});
|
|
293
|
+
if (!r.sessionId)
|
|
294
|
+
throw new ProviderError(ID, "StartCodeInterpreterSession returned no sessionId");
|
|
295
|
+
return new AgentCoreSandbox(this, r.sessionId, spec.env);
|
|
296
|
+
}
|
|
297
|
+
async connect(nativeId) {
|
|
298
|
+
if (!SESSION_ID.test(nativeId))
|
|
299
|
+
throw new ProviderError(ID, `invalid session id: ${nativeId}`);
|
|
300
|
+
const s = await this.aws().json("GET", this.url("sessions/get", { sessionId: nativeId }));
|
|
301
|
+
if (s.status !== "READY")
|
|
302
|
+
throw new ProviderError(ID, `session ${nativeId} is ${s.status ?? "unknown"}`, 404);
|
|
303
|
+
return new AgentCoreSandbox(this, nativeId);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
class AgentCoreSandbox {
|
|
307
|
+
p;
|
|
308
|
+
nativeId;
|
|
309
|
+
env;
|
|
310
|
+
constructor(p, nativeId, env) {
|
|
311
|
+
this.p = p;
|
|
312
|
+
this.nativeId = nativeId;
|
|
313
|
+
this.env = env;
|
|
314
|
+
}
|
|
315
|
+
/** InvokeCodeInterpreter; calls onResult for each result event. */
|
|
316
|
+
async invoke(name, args, onResult, timeoutMs = 16 * 60_000) {
|
|
317
|
+
const res = await this.p.aws().send("POST", this.p.url("tools/invoke"), {
|
|
318
|
+
json: { name, arguments: args },
|
|
319
|
+
headers: { "x-amzn-code-interpreter-session-id": this.nativeId, accept: "application/vnd.amazon.eventstream, application/json" },
|
|
320
|
+
timeoutMs,
|
|
321
|
+
});
|
|
322
|
+
if (!res.ok)
|
|
323
|
+
await awsFail(ID, res, `InvokeCodeInterpreter ${name}`);
|
|
324
|
+
const type = res.headers.get("content-type") ?? "";
|
|
325
|
+
if (!type.includes("eventstream")) {
|
|
326
|
+
// Plain JSON body: either {"result": {...}} or one of the exception members.
|
|
327
|
+
const j = (await res.json());
|
|
328
|
+
const exc = Object.keys(j).find((k) => k.endsWith("Exception"));
|
|
329
|
+
if (exc)
|
|
330
|
+
throw new ProviderError(ID, `${name}: ${exc}: ${j[exc]?.message ?? ""}`.trim(), exceptionStatus(exc));
|
|
331
|
+
onResult((j.result ?? j));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
for await (const msg of readEventStream(res)) {
|
|
335
|
+
const mtype = msg.headers[":message-type"];
|
|
336
|
+
const text = new TextDecoder().decode(msg.payload);
|
|
337
|
+
if (mtype === "exception" || mtype === "error") {
|
|
338
|
+
const exc = String(msg.headers[":exception-type"] ?? msg.headers[":error-code"] ?? "error");
|
|
339
|
+
let detail = String(msg.headers[":error-message"] ?? "");
|
|
340
|
+
try {
|
|
341
|
+
detail ||= JSON.parse(text)?.message ?? "";
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
detail ||= text;
|
|
345
|
+
}
|
|
346
|
+
throw new ProviderError(ID, `${name}: ${exc}: ${detail}`.trim(), exceptionStatus(exc));
|
|
347
|
+
}
|
|
348
|
+
if (!text)
|
|
349
|
+
continue;
|
|
350
|
+
const j = JSON.parse(text);
|
|
351
|
+
onResult((msg.headers[":event-type"] === "result" ? j : (j.result ?? j)));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/** executeCommand, collecting stdout/stderr/exit code across result events. */
|
|
355
|
+
async command(command, opts = {}) {
|
|
356
|
+
let stdout = "";
|
|
357
|
+
let stderr = "";
|
|
358
|
+
let exitCode;
|
|
359
|
+
let errorText = "";
|
|
360
|
+
await this.invoke("executeCommand", { command }, (r) => {
|
|
361
|
+
const sc = r.structuredContent;
|
|
362
|
+
if (sc) {
|
|
363
|
+
if (sc.stdout)
|
|
364
|
+
(stdout += sc.stdout), opts.onStdout?.(sc.stdout);
|
|
365
|
+
if (sc.stderr)
|
|
366
|
+
(stderr += sc.stderr), opts.onStderr?.(sc.stderr);
|
|
367
|
+
if (typeof sc.exitCode === "number")
|
|
368
|
+
exitCode = sc.exitCode;
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
const t = (r.content ?? []).filter((c) => c.type === "text" && c.text).map((c) => c.text).join("");
|
|
372
|
+
if (r.isError)
|
|
373
|
+
errorText += t;
|
|
374
|
+
else if (t)
|
|
375
|
+
(stdout += t), opts.onStdout?.(t);
|
|
376
|
+
}
|
|
377
|
+
if (r.isError && exitCode === undefined && !sc)
|
|
378
|
+
exitCode = 1;
|
|
379
|
+
}, opts.timeoutMs);
|
|
380
|
+
if (exitCode === undefined && errorText)
|
|
381
|
+
throw new ProviderError(ID, `executeCommand: ${errorText.trim()}`);
|
|
382
|
+
return { stdout, stderr, exitCode: exitCode ?? 0 };
|
|
383
|
+
}
|
|
384
|
+
async exec(cmd, opts = {}) {
|
|
385
|
+
const t0 = Date.now();
|
|
386
|
+
const timeout = Math.max(1, Math.round(opts.timeoutSeconds ?? 60));
|
|
387
|
+
const full = wrapCommand(cmd, { cwd: opts.cwd, env: { ...this.env, ...opts.env } });
|
|
388
|
+
const b64 = Buffer.from(full).toString("base64");
|
|
389
|
+
// base64 keeps the user's command intact through whatever quoting the service applies.
|
|
390
|
+
const command = `timeout -k 5 ${timeout} bash -c "$(echo ${b64} | base64 -d)"`;
|
|
391
|
+
const r = await this.command(command, { onStdout: opts.onStdout, onStderr: opts.onStderr, timeoutMs: (timeout + 60) * 1000 });
|
|
392
|
+
const durationMs = Date.now() - t0;
|
|
393
|
+
// `timeout` leaves exit 124 (or 137 after SIGKILL) when it had to stop the command.
|
|
394
|
+
if (killedByTimeout(r.exitCode, durationMs, timeout))
|
|
395
|
+
throw timeoutError(ID, timeout);
|
|
396
|
+
return { ...r, durationMs };
|
|
397
|
+
}
|
|
398
|
+
async writeFile(path, content) {
|
|
399
|
+
const bytes = toBytes(content);
|
|
400
|
+
const tmp = `${path}.sbr-${randomBytes(4).toString("hex")}`;
|
|
401
|
+
for (let i = 0, first = true; first || i < bytes.length; i += WRITE_CHUNK, first = false) {
|
|
402
|
+
const b64 = Buffer.from(bytes.subarray(i, i + WRITE_CHUNK)).toString("base64");
|
|
403
|
+
const prefix = first ? `mkdir -p ${shq(dirname(path))} && ` : "";
|
|
404
|
+
const r = await this.command(`${prefix}echo ${b64 || "''"} | base64 -d ${first ? ">" : ">>"} ${shq(tmp)}`);
|
|
405
|
+
if (r.exitCode !== 0)
|
|
406
|
+
throw new ProviderError(ID, `writeFile ${path}: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
|
|
407
|
+
}
|
|
408
|
+
const r = await this.command(`mv -f ${shq(tmp)} ${shq(path)}`);
|
|
409
|
+
if (r.exitCode !== 0)
|
|
410
|
+
throw new ProviderError(ID, `writeFile ${path}: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
|
|
411
|
+
}
|
|
412
|
+
async readFile(path) {
|
|
413
|
+
const r = await this.command(`base64 -w0 < ${shq(path)}`);
|
|
414
|
+
if (r.exitCode !== 0)
|
|
415
|
+
throw new ProviderError(ID, `readFile ${path}: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
|
|
416
|
+
return new Uint8Array(Buffer.from(r.stdout.trim(), "base64"));
|
|
417
|
+
}
|
|
418
|
+
async kill() {
|
|
419
|
+
try {
|
|
420
|
+
await this.p.aws().json("PUT", this.p.url("sessions/stop", { sessionId: this.nativeId }), { json: {} });
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
if (err instanceof ProviderError && err.status === 404)
|
|
424
|
+
return;
|
|
425
|
+
throw err;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function exceptionStatus(name) {
|
|
430
|
+
const n = name.toLowerCase();
|
|
431
|
+
if (n.startsWith("accessdenied"))
|
|
432
|
+
return 403;
|
|
433
|
+
if (n.startsWith("resourcenotfound"))
|
|
434
|
+
return 404;
|
|
435
|
+
if (n.startsWith("validation"))
|
|
436
|
+
return 400;
|
|
437
|
+
if (n.startsWith("conflict"))
|
|
438
|
+
return 409;
|
|
439
|
+
if (n.startsWith("throttling"))
|
|
440
|
+
return 429;
|
|
441
|
+
if (n.startsWith("servicequota"))
|
|
442
|
+
return 402;
|
|
443
|
+
if (n.startsWith("internalserver"))
|
|
444
|
+
return 500;
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
|
|
2
|
+
export declare const ALIBABA_REGIONS: readonly ["cn-beijing", "cn-shanghai", "cn-hangzhou", "cn-shenzhen", "cn-hongkong", "ap-southeast-1", "us-east-1", "us-west-1"];
|
|
3
|
+
export declare class AlibabaProvider implements Provider {
|
|
4
|
+
private apiKey;
|
|
5
|
+
private region;
|
|
6
|
+
readonly id: "alibaba";
|
|
7
|
+
constructor(apiKey?: string | undefined, region?: string);
|
|
8
|
+
configured(): boolean;
|
|
9
|
+
/** @internal Connection options for the e2b SDK. */
|
|
10
|
+
conn(): {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
domain: string;
|
|
13
|
+
apiUrl: string;
|
|
14
|
+
};
|
|
15
|
+
create(spec: SandboxSpec): Promise<ProviderSandbox>;
|
|
16
|
+
/** Reattach by sandbox id (a paused sandbox is resumed on connect, per Alibaba's lifecycle docs). */
|
|
17
|
+
connect(nativeId: string): Promise<ProviderSandbox>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ProviderError } from "../types.js";
|
|
2
|
+
import { optionalImport } from "../util.js";
|
|
3
|
+
import { E2BCompatBox } from "./e2b.js";
|
|
4
|
+
/**
|
|
5
|
+
* Alibaba Cloud Agent Sandbox (Flash Edition) through its E2B-compatible API, driven with the
|
|
6
|
+
* official `e2b` SDK pointed at Alibaba's regional endpoints:
|
|
7
|
+
* API URL https://api.<region>.sandbox.aliyuncs.com
|
|
8
|
+
* domain <region>.sandbox.aliyuncs.com (data plane: <port>-<sandboxId>.<domain>, the SDK's default)
|
|
9
|
+
* Documented regions: cn-beijing, cn-shanghai, cn-hangzhou, cn-shenzhen, cn-hongkong,
|
|
10
|
+
* ap-southeast-1, us-east-1, us-west-1. The API key, region, template and sandbox must all belong
|
|
11
|
+
* to the same account and region. Default region here: ap-southeast-1 (Singapore).
|
|
12
|
+
*
|
|
13
|
+
* Auth: ALIBABA_SANDBOX_API_KEY, created in the Agent Sandbox console (Function Compute and
|
|
14
|
+
* Agent Sandbox Flash Edition must be activated first). ALIBABA_SANDBOX_REGION picks the region.
|
|
15
|
+
* apiUrl is passed explicitly (it equals the SDK's https://api.<domain> default) so a stray
|
|
16
|
+
* E2B_API_URL in the environment can't redirect calls; E2B_SANDBOX_URL would still override the
|
|
17
|
+
* data plane, so don't set it alongside this adapter.
|
|
18
|
+
*
|
|
19
|
+
* Size: vCPU and memory come from the template, as with E2B. image.alibaba = template name or ID;
|
|
20
|
+
* default "code-interpreter-v1", the only official template the docs name (2 vCPU / 2048 MiB in
|
|
21
|
+
* the CLI docs' sample listing, which is labelled as format-only).
|
|
22
|
+
* Lifetime: timeoutMs, max 86 400 s (24 h) in the active state.
|
|
23
|
+
* Pause/resume exists but "is currently available only to allowlisted accounts", so this adapter
|
|
24
|
+
* does not offer pause.
|
|
25
|
+
* Not supported by Alibaba's E2B layer: snapshots, volumes, team/API-key management, E2B MCP gateway.
|
|
26
|
+
*
|
|
27
|
+
* Docs: https://www.alibabacloud.com/help/en/agent-sandbox/e2b-sdk-integration-parameter-description
|
|
28
|
+
* https://www.alibabacloud.com/help/en/agent-sandbox/e2b-sdk-compatible-api-list
|
|
29
|
+
* https://www.alibabacloud.com/help/en/agent-sandbox/using-the-cloud-sandbox-via-the-sdk
|
|
30
|
+
* https://www.alibabacloud.com/help/en/agent-sandbox/create-a-sandbox
|
|
31
|
+
* https://www.alibabacloud.com/help/en/agent-sandbox/lifecycle
|
|
32
|
+
* https://www.alibabacloud.com/help/en/agent-sandbox/create-api-key
|
|
33
|
+
* https://www.alibabacloud.com/help/en/agent-sandbox/usage-constraints-of-fc-agent-sandbox
|
|
34
|
+
* https://www.alibabacloud.com/help/en/agent-sandbox/product-overview/billing-overview
|
|
35
|
+
*/
|
|
36
|
+
const ID = "alibaba";
|
|
37
|
+
export const ALIBABA_REGIONS = [
|
|
38
|
+
"cn-beijing",
|
|
39
|
+
"cn-shanghai",
|
|
40
|
+
"cn-hangzhou",
|
|
41
|
+
"cn-shenzhen",
|
|
42
|
+
"cn-hongkong",
|
|
43
|
+
"ap-southeast-1",
|
|
44
|
+
"us-east-1",
|
|
45
|
+
"us-west-1",
|
|
46
|
+
];
|
|
47
|
+
const DEFAULT_REGION = "ap-southeast-1";
|
|
48
|
+
const DEFAULT_TEMPLATE = "code-interpreter-v1";
|
|
49
|
+
/** Longest single active period Flash Edition allows. */
|
|
50
|
+
const MAX_TIMEOUT_MS = 86_400_000;
|
|
51
|
+
export class AlibabaProvider {
|
|
52
|
+
apiKey;
|
|
53
|
+
region;
|
|
54
|
+
id = ID;
|
|
55
|
+
constructor(apiKey = process.env.ALIBABA_SANDBOX_API_KEY, region = process.env.ALIBABA_SANDBOX_REGION || DEFAULT_REGION) {
|
|
56
|
+
this.apiKey = apiKey;
|
|
57
|
+
this.region = region;
|
|
58
|
+
}
|
|
59
|
+
configured() {
|
|
60
|
+
return !!this.apiKey;
|
|
61
|
+
}
|
|
62
|
+
/** @internal Connection options for the e2b SDK. */
|
|
63
|
+
conn() {
|
|
64
|
+
if (!this.apiKey)
|
|
65
|
+
throw new ProviderError(ID, "ALIBABA_SANDBOX_API_KEY is not set");
|
|
66
|
+
// Unlisted regions are allowed (new ones get added), but the value becomes a hostname.
|
|
67
|
+
if (!/^[a-z]{2}(-[a-z0-9]+)+$/.test(this.region))
|
|
68
|
+
throw new ProviderError(ID, `invalid region: ${this.region}`);
|
|
69
|
+
const domain = `${this.region}.sandbox.aliyuncs.com`;
|
|
70
|
+
return { apiKey: this.apiKey, domain, apiUrl: `https://api.${domain}` };
|
|
71
|
+
}
|
|
72
|
+
async create(spec) {
|
|
73
|
+
const conn = this.conn();
|
|
74
|
+
const { Sandbox } = await optionalImport("e2b", "alibaba");
|
|
75
|
+
const timeoutMs = Math.min(MAX_TIMEOUT_MS, Math.max(1, spec.timeoutMinutes ?? 30) * 60_000);
|
|
76
|
+
try {
|
|
77
|
+
const sbx = await Sandbox.create(spec.image?.alibaba ?? DEFAULT_TEMPLATE, {
|
|
78
|
+
...conn,
|
|
79
|
+
timeoutMs,
|
|
80
|
+
envs: spec.env,
|
|
81
|
+
metadata: spec.metadata,
|
|
82
|
+
});
|
|
83
|
+
return new E2BCompatBox(sbx, ID);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
throw wrap(err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Reattach by sandbox id (a paused sandbox is resumed on connect, per Alibaba's lifecycle docs). */
|
|
90
|
+
async connect(nativeId) {
|
|
91
|
+
const conn = this.conn();
|
|
92
|
+
const { Sandbox } = await optionalImport("e2b", "alibaba");
|
|
93
|
+
try {
|
|
94
|
+
return new E2BCompatBox((await Sandbox.connect(nativeId, conn)), ID);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
throw wrap(err);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function wrap(err) {
|
|
102
|
+
if (err instanceof ProviderError)
|
|
103
|
+
return err;
|
|
104
|
+
const status = err?.name === "SandboxNotFoundError" || err?.name === "NotFoundError" ? 404 : err?.name === "AuthenticationError" ? 401 : undefined;
|
|
105
|
+
return new ProviderError(ID, err?.message ?? String(err), status);
|
|
106
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
|
|
2
|
+
export interface AzureSessionsOptions {
|
|
3
|
+
/** Pool management endpoint, e.g. https://westus2.dynamicsessions.io/subscriptions/.../sessionPools/my-pool */
|
|
4
|
+
endpoint?: string;
|
|
5
|
+
/** A ready Entra access token for https://dynamicsessions.io. */
|
|
6
|
+
token?: string;
|
|
7
|
+
tenantId?: string;
|
|
8
|
+
clientId?: string;
|
|
9
|
+
clientSecret?: string;
|
|
10
|
+
/** Entra authority host (default https://login.microsoftonline.com). */
|
|
11
|
+
authorityHost?: string;
|
|
12
|
+
/** Data-plane api-version (default 2025-10-02-preview, the first with Shell pools). */
|
|
13
|
+
apiVersion?: string;
|
|
14
|
+
/** Where the files API puts uploads inside a session (default /mnt/data). */
|
|
15
|
+
filesDir?: string;
|
|
16
|
+
/** outputStreamsMaxLength sent with each execution (default 4096, the documented default). */
|
|
17
|
+
outputCap?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class AzureSessionsProvider implements Provider {
|
|
20
|
+
readonly id: "azure";
|
|
21
|
+
/** @internal */ readonly endpoint?: string;
|
|
22
|
+
/** @internal */ readonly apiVersion: string;
|
|
23
|
+
/** @internal */ readonly filesDir: string;
|
|
24
|
+
/** @internal */ readonly outputCap: number;
|
|
25
|
+
private readonly staticToken?;
|
|
26
|
+
private readonly tenantId?;
|
|
27
|
+
private readonly clientId?;
|
|
28
|
+
private readonly clientSecret?;
|
|
29
|
+
private readonly authorityHost;
|
|
30
|
+
private cached?;
|
|
31
|
+
constructor(opts?: AzureSessionsOptions);
|
|
32
|
+
configured(): boolean;
|
|
33
|
+
/** @internal Bearer token: the static one, or a cached client-credentials token. */
|
|
34
|
+
token(): Promise<string>;
|
|
35
|
+
/** @internal One data-plane call. Throws ProviderError on non-2xx. */
|
|
36
|
+
call(method: string, path: string, identifier: string, init?: {
|
|
37
|
+
json?: unknown;
|
|
38
|
+
form?: FormData;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}): Promise<Response>;
|
|
41
|
+
create(spec: SandboxSpec): Promise<ProviderSandbox>;
|
|
42
|
+
connect(nativeId: string): Promise<ProviderSandbox>;
|
|
43
|
+
}
|