@mastra/cloudflare-sandbox 0.0.0 → 0.2.0-alpha.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 +11 -0
- package/LICENSE.md +30 -0
- package/dist/bridge-client.d.ts +59 -0
- package/dist/bridge-client.d.ts.map +1 -0
- package/dist/index.cjs +344 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +341 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox.d.ts +56 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/testing/fake-bridge.d.ts +44 -0
- package/dist/testing/fake-bridge.d.ts.map +1 -0
- package/package.json +21 -21
package/CHANGELOG.md
CHANGED
|
@@ -1 +1,12 @@
|
|
|
1
1
|
# @mastra/cloudflare-sandbox
|
|
2
|
+
|
|
3
|
+
## 0.2.0-alpha.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add a Cloudflare Sandbox provider that executes commands and writes workspace files through a deployed Sandbox Bridge Worker. ([#21596](https://github.com/mastra-ai/mastra/pull/21596))
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`6db7a5d`](https://github.com/mastra-ai/mastra/commit/6db7a5dd3dd2b6f7ef75dcd804fcffef5fa83963), [`0cdc5dc`](https://github.com/mastra-ai/mastra/commit/0cdc5dc69024957815da4f51acc4119eb4f447d7)]:
|
|
12
|
+
- @mastra/core@1.60.0-alpha.12
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Portions of this software are licensed as follows:
|
|
2
|
+
|
|
3
|
+
- All content that resides under any directory named "ee/" within this
|
|
4
|
+
repository, including but not limited to:
|
|
5
|
+
- `packages/core/src/auth/ee/`
|
|
6
|
+
- `packages/server/src/server/auth/ee/`
|
|
7
|
+
is licensed under the license defined in `ee/LICENSE`.
|
|
8
|
+
|
|
9
|
+
- All third-party components incorporated into the Mastra Software are
|
|
10
|
+
licensed under the original license provided by the owner of the
|
|
11
|
+
applicable component.
|
|
12
|
+
|
|
13
|
+
- Content outside of the above-mentioned directories or restrictions is
|
|
14
|
+
available under the "Apache License 2.0" as defined below.
|
|
15
|
+
|
|
16
|
+
# Apache License 2.0
|
|
17
|
+
|
|
18
|
+
Copyright (c) 2025 Kepler Software, Inc.
|
|
19
|
+
|
|
20
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
21
|
+
you may not use this file except in compliance with the License.
|
|
22
|
+
You may obtain a copy of the License at
|
|
23
|
+
|
|
24
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
25
|
+
|
|
26
|
+
Unless required by applicable law or agreed to in writing, software
|
|
27
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
28
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
29
|
+
See the License for the specific language governing permissions and
|
|
30
|
+
limitations under the License.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface CloudflareSandboxBridgeClientOptions {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
apiToken?: string;
|
|
4
|
+
fetch?: typeof globalThis.fetch;
|
|
5
|
+
}
|
|
6
|
+
/** Terminal and streaming events emitted by `POST /v1/sandbox/:id/exec`. */
|
|
7
|
+
export type CloudflareCommandEvent = {
|
|
8
|
+
type: 'stdout';
|
|
9
|
+
data: Uint8Array;
|
|
10
|
+
} | {
|
|
11
|
+
type: 'stderr';
|
|
12
|
+
data: Uint8Array;
|
|
13
|
+
} | {
|
|
14
|
+
type: 'exit';
|
|
15
|
+
exitCode: number;
|
|
16
|
+
} | {
|
|
17
|
+
type: 'error';
|
|
18
|
+
message: string;
|
|
19
|
+
code?: string;
|
|
20
|
+
};
|
|
21
|
+
export interface CloudflareExecRequest {
|
|
22
|
+
/** Command and arguments. The bridge applies ANSI-C quoting to each element. */
|
|
23
|
+
argv: string[];
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare class CloudflareSandboxBridgeError extends Error {
|
|
28
|
+
readonly status: number;
|
|
29
|
+
readonly body: string;
|
|
30
|
+
constructor(status: number, body: string);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Client for the Cloudflare Sandbox Bridge Worker.
|
|
34
|
+
*
|
|
35
|
+
* @see https://developers.cloudflare.com/sandbox/bridge/http-api/
|
|
36
|
+
*/
|
|
37
|
+
export declare class CloudflareSandboxBridgeClient {
|
|
38
|
+
readonly baseUrl: string;
|
|
39
|
+
private readonly apiToken?;
|
|
40
|
+
private readonly fetchImpl;
|
|
41
|
+
constructor(options: CloudflareSandboxBridgeClientOptions);
|
|
42
|
+
/** `POST /v1/sandbox` */
|
|
43
|
+
createSandbox(): Promise<string>;
|
|
44
|
+
/** `GET /v1/sandbox/:id/running` */
|
|
45
|
+
isRunning(id: string): Promise<boolean>;
|
|
46
|
+
/** `DELETE /v1/sandbox/:id` */
|
|
47
|
+
deleteSandbox(id: string): Promise<void>;
|
|
48
|
+
/** `PUT /v1/sandbox/:id/file/*` — one file per request, raw bytes as the body. */
|
|
49
|
+
writeFile(id: string, absolutePath: string, content: Uint8Array | string): Promise<void>;
|
|
50
|
+
/** `POST /v1/sandbox/:id/exec` — streams SSE events until `exit` or `error`. */
|
|
51
|
+
exec(id: string, request: CloudflareExecRequest, options: {
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
onEvent: (event: CloudflareCommandEvent) => void;
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
private emitBlock;
|
|
56
|
+
private headers;
|
|
57
|
+
private request;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=bridge-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-client.d.ts","sourceRoot":"","sources":["../src/bridge-client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oCAAoC;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED,4EAA4E;AAC5E,MAAM,MAAM,sBAAsB,GAC9B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,MAAM,WAAW,qBAAqB;IACpC,gFAAgF;IAChF,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAMzC;AAmBD;;;;GAIG;AACH,qBAAa,6BAA6B;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;gBAExC,OAAO,EAAE,oCAAoC;IAMzD,yBAAyB;IACnB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC;IAKtC,oCAAoC;IAC9B,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK7C,+BAA+B;IACzB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,kFAAkF;IAC5E,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY9F,gFAAgF;IAC1E,IAAI,CACR,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE;QACP,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,OAAO,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAC;KAClD,GACA,OAAO,CAAC,IAAI,CAAC;IAqChB,OAAO,CAAC,SAAS;IAkCjB,OAAO,CAAC,OAAO;YAID,OAAO;CAWtB"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let crypto = require("crypto");
|
|
3
|
+
let path = require("path");
|
|
4
|
+
let _mastra_core_workspace = require("@mastra/core/workspace");
|
|
5
|
+
//#region src/bridge-client.ts
|
|
6
|
+
var CloudflareSandboxBridgeError = class extends Error {
|
|
7
|
+
status;
|
|
8
|
+
body;
|
|
9
|
+
constructor(status, body) {
|
|
10
|
+
super(`Cloudflare Sandbox Bridge request failed (${status}): ${body || "empty response"}`);
|
|
11
|
+
this.name = "CloudflareSandboxBridgeError";
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.body = body;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function stripTrailingSlashes(url) {
|
|
17
|
+
let end = url.length;
|
|
18
|
+
while (end > 0 && url[end - 1] === "/") end--;
|
|
19
|
+
return url.slice(0, end);
|
|
20
|
+
}
|
|
21
|
+
/** Encodes an absolute sandbox path for the `/file/*` route, which omits the leading slash. */
|
|
22
|
+
function encodeFilePath(absolutePath) {
|
|
23
|
+
let start = 0;
|
|
24
|
+
while (start < absolutePath.length && absolutePath[start] === "/") start++;
|
|
25
|
+
return absolutePath.slice(start).split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Client for the Cloudflare Sandbox Bridge Worker.
|
|
29
|
+
*
|
|
30
|
+
* @see https://developers.cloudflare.com/sandbox/bridge/http-api/
|
|
31
|
+
*/
|
|
32
|
+
var CloudflareSandboxBridgeClient = class {
|
|
33
|
+
baseUrl;
|
|
34
|
+
apiToken;
|
|
35
|
+
fetchImpl;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.baseUrl = stripTrailingSlashes(options.baseUrl);
|
|
38
|
+
this.apiToken = options.apiToken;
|
|
39
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
40
|
+
}
|
|
41
|
+
/** `POST /v1/sandbox` */
|
|
42
|
+
async createSandbox() {
|
|
43
|
+
return (await this.request("/v1/sandbox", { method: "POST" })).id;
|
|
44
|
+
}
|
|
45
|
+
/** `GET /v1/sandbox/:id/running` */
|
|
46
|
+
async isRunning(id) {
|
|
47
|
+
return (await this.request(`/v1/sandbox/${encodeURIComponent(id)}/running`, {})).running === true;
|
|
48
|
+
}
|
|
49
|
+
/** `DELETE /v1/sandbox/:id` */
|
|
50
|
+
async deleteSandbox(id) {
|
|
51
|
+
await this.request(`/v1/sandbox/${encodeURIComponent(id)}`, { method: "DELETE" }, true);
|
|
52
|
+
}
|
|
53
|
+
/** `PUT /v1/sandbox/:id/file/*` — one file per request, raw bytes as the body. */
|
|
54
|
+
async writeFile(id, absolutePath, content) {
|
|
55
|
+
await this.request(`/v1/sandbox/${encodeURIComponent(id)}/file/${encodeFilePath(absolutePath)}`, {
|
|
56
|
+
method: "PUT",
|
|
57
|
+
body: content,
|
|
58
|
+
headers: { "content-type": "application/octet-stream" }
|
|
59
|
+
}, true);
|
|
60
|
+
}
|
|
61
|
+
/** `POST /v1/sandbox/:id/exec` — streams SSE events until `exit` or `error`. */
|
|
62
|
+
async exec(id, request, options) {
|
|
63
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/sandbox/${encodeURIComponent(id)}/exec`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
...this.headers(),
|
|
67
|
+
"content-type": "application/json",
|
|
68
|
+
accept: "text/event-stream"
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
argv: request.argv,
|
|
72
|
+
...request.timeoutMs === void 0 ? {} : { timeout_ms: request.timeoutMs },
|
|
73
|
+
...request.cwd === void 0 ? {} : { cwd: request.cwd }
|
|
74
|
+
}),
|
|
75
|
+
signal: options.signal
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) throw new CloudflareSandboxBridgeError(response.status, await response.text());
|
|
78
|
+
if (!response.body) throw new Error("Cloudflare Sandbox Bridge returned an empty command stream");
|
|
79
|
+
const reader = response.body.getReader();
|
|
80
|
+
const decoder = new TextDecoder();
|
|
81
|
+
let buffer = "";
|
|
82
|
+
while (true) {
|
|
83
|
+
const { done, value } = await reader.read();
|
|
84
|
+
buffer += decoder.decode(value, { stream: !done }).replace(/\r\n/g, "\n");
|
|
85
|
+
let boundary = buffer.indexOf("\n\n");
|
|
86
|
+
while (boundary !== -1) {
|
|
87
|
+
this.emitBlock(buffer.slice(0, boundary), options.onEvent);
|
|
88
|
+
buffer = buffer.slice(boundary + 2);
|
|
89
|
+
boundary = buffer.indexOf("\n\n");
|
|
90
|
+
}
|
|
91
|
+
if (done) break;
|
|
92
|
+
}
|
|
93
|
+
if (buffer.trim()) this.emitBlock(buffer, options.onEvent);
|
|
94
|
+
}
|
|
95
|
+
emitBlock(block, onEvent) {
|
|
96
|
+
let eventName;
|
|
97
|
+
const dataLines = [];
|
|
98
|
+
for (const line of block.split("\n")) if (line.startsWith("event:")) eventName = line.slice(6).trim();
|
|
99
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
100
|
+
const data = dataLines.join("\n");
|
|
101
|
+
if (!eventName || !data) return;
|
|
102
|
+
switch (eventName) {
|
|
103
|
+
case "stdout":
|
|
104
|
+
case "stderr":
|
|
105
|
+
onEvent({
|
|
106
|
+
type: eventName,
|
|
107
|
+
data: base64ToBytes(data)
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
case "exit": {
|
|
111
|
+
const parsed = safeJsonParse(data);
|
|
112
|
+
onEvent({
|
|
113
|
+
type: "exit",
|
|
114
|
+
exitCode: typeof parsed?.exit_code === "number" ? parsed.exit_code : 0
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
case "error": {
|
|
119
|
+
const parsed = safeJsonParse(data);
|
|
120
|
+
onEvent({
|
|
121
|
+
type: "error",
|
|
122
|
+
message: typeof parsed?.error === "string" ? parsed.error : data,
|
|
123
|
+
code: typeof parsed?.code === "string" ? parsed.code : void 0
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
default: return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
headers() {
|
|
131
|
+
return this.apiToken ? { authorization: `Bearer ${this.apiToken}` } : {};
|
|
132
|
+
}
|
|
133
|
+
async request(path, init, allowEmpty = false) {
|
|
134
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
135
|
+
...init,
|
|
136
|
+
headers: {
|
|
137
|
+
...this.headers(),
|
|
138
|
+
...init.headers
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok) throw new CloudflareSandboxBridgeError(response.status, await response.text());
|
|
142
|
+
if (allowEmpty || response.status === 204) return void 0;
|
|
143
|
+
return response.json();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
function base64ToBytes(value) {
|
|
147
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
148
|
+
}
|
|
149
|
+
function safeJsonParse(value) {
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(value);
|
|
152
|
+
} catch {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/sandbox.ts
|
|
158
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 3e5;
|
|
159
|
+
const WORKSPACE_ROOT = "/workspace";
|
|
160
|
+
/**
|
|
161
|
+
* Builds the argv array sent to the bridge. The bridge applies ANSI-C quoting to
|
|
162
|
+
* every element, so no local escaping is needed. Environment variables are applied
|
|
163
|
+
* with `env`, which keeps each assignment a separate argv element.
|
|
164
|
+
*/
|
|
165
|
+
function buildArgv(command, args, env) {
|
|
166
|
+
const assignments = Object.entries(env).map(([key, value]) => {
|
|
167
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: ${key}`);
|
|
168
|
+
return `${key}=${value}`;
|
|
169
|
+
});
|
|
170
|
+
const invocation = [command, ...args ?? []];
|
|
171
|
+
return assignments.length ? [
|
|
172
|
+
"env",
|
|
173
|
+
...assignments,
|
|
174
|
+
...invocation
|
|
175
|
+
] : invocation;
|
|
176
|
+
}
|
|
177
|
+
/** Resolves a path inside /workspace, rejecting anything that escapes the workspace root. */
|
|
178
|
+
function resolveWorkspacePath(path$1) {
|
|
179
|
+
const resolved = path.posix.resolve(WORKSPACE_ROOT, path$1);
|
|
180
|
+
if (resolved !== WORKSPACE_ROOT && !resolved.startsWith(`${WORKSPACE_ROOT}/`)) throw new Error(`Cloudflare Sandbox files must be written under ${WORKSPACE_ROOT}: ${path$1}`);
|
|
181
|
+
return resolved;
|
|
182
|
+
}
|
|
183
|
+
var CloudflareSandbox = class extends _mastra_core_workspace.MastraSandbox {
|
|
184
|
+
id;
|
|
185
|
+
name;
|
|
186
|
+
provider = "cloudflare-sandbox";
|
|
187
|
+
status = "pending";
|
|
188
|
+
client;
|
|
189
|
+
env;
|
|
190
|
+
workingDirectory;
|
|
191
|
+
commandTimeout;
|
|
192
|
+
instructions;
|
|
193
|
+
sandboxId;
|
|
194
|
+
createdAt = /* @__PURE__ */ new Date();
|
|
195
|
+
lastUsedAt;
|
|
196
|
+
constructor(options) {
|
|
197
|
+
const name = options.name ?? "Cloudflare Sandbox";
|
|
198
|
+
super({
|
|
199
|
+
...options,
|
|
200
|
+
name
|
|
201
|
+
});
|
|
202
|
+
this.id = options.id ?? `cloudflare-sandbox-${(0, crypto.randomUUID)()}`;
|
|
203
|
+
this.name = name;
|
|
204
|
+
this.sandboxId = options.sandboxId;
|
|
205
|
+
this.env = { ...options.env };
|
|
206
|
+
this.workingDirectory = options.workingDirectory;
|
|
207
|
+
this.commandTimeout = options.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
208
|
+
this.instructions = options.instructions;
|
|
209
|
+
this.client = options.client ?? new CloudflareSandboxBridgeClient({
|
|
210
|
+
baseUrl: options.baseUrl,
|
|
211
|
+
apiToken: options.apiToken,
|
|
212
|
+
fetch: options.fetch
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async start() {
|
|
216
|
+
if (this.sandboxId) {
|
|
217
|
+
if (!await this.client.isRunning(this.sandboxId)) this.logger?.debug(`Cloudflare sandbox ${this.sandboxId} is not running yet; it starts on first use`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.sandboxId = await this.client.createSandbox();
|
|
221
|
+
this.createdAt = /* @__PURE__ */ new Date();
|
|
222
|
+
}
|
|
223
|
+
async stop() {}
|
|
224
|
+
async destroy() {
|
|
225
|
+
if (!this.sandboxId) return;
|
|
226
|
+
await this.client.deleteSandbox(this.sandboxId);
|
|
227
|
+
this.sandboxId = void 0;
|
|
228
|
+
}
|
|
229
|
+
async executeCommand(command, args, options) {
|
|
230
|
+
const sandboxId = this.requireSandboxId();
|
|
231
|
+
const startedAt = Date.now();
|
|
232
|
+
const timeout = options?.timeout ?? this.commandTimeout;
|
|
233
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new RangeError("Command timeout must be positive");
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
let didTimeout = false;
|
|
236
|
+
const timer = setTimeout(() => {
|
|
237
|
+
didTimeout = true;
|
|
238
|
+
controller.abort();
|
|
239
|
+
}, timeout);
|
|
240
|
+
const signal = options?.abortSignal ? AbortSignal.any([controller.signal, options.abortSignal]) : controller.signal;
|
|
241
|
+
const stdoutDecoder = new TextDecoder();
|
|
242
|
+
const stderrDecoder = new TextDecoder();
|
|
243
|
+
let stdout = "";
|
|
244
|
+
let stderr = "";
|
|
245
|
+
let exitCode = 1;
|
|
246
|
+
const env = Object.fromEntries(Object.entries({
|
|
247
|
+
...this.env,
|
|
248
|
+
...options?.env
|
|
249
|
+
}).filter((entry) => entry[1] !== void 0));
|
|
250
|
+
try {
|
|
251
|
+
await this.client.exec(sandboxId, {
|
|
252
|
+
argv: buildArgv(command, args, env),
|
|
253
|
+
timeoutMs: timeout,
|
|
254
|
+
cwd: options?.cwd ?? this.workingDirectory
|
|
255
|
+
}, {
|
|
256
|
+
signal,
|
|
257
|
+
onEvent: (event) => {
|
|
258
|
+
switch (event.type) {
|
|
259
|
+
case "stdout": {
|
|
260
|
+
const chunk = stdoutDecoder.decode(event.data, { stream: true });
|
|
261
|
+
if (!chunk) return;
|
|
262
|
+
stdout += chunk;
|
|
263
|
+
options?.onStdout?.(chunk);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
case "stderr": {
|
|
267
|
+
const chunk = stderrDecoder.decode(event.data, { stream: true });
|
|
268
|
+
if (!chunk) return;
|
|
269
|
+
stderr += chunk;
|
|
270
|
+
options?.onStderr?.(chunk);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
case "exit":
|
|
274
|
+
exitCode = event.exitCode;
|
|
275
|
+
return;
|
|
276
|
+
case "error":
|
|
277
|
+
stderr += event.message;
|
|
278
|
+
options?.onStderr?.(event.message);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (!signal.aborted) throw error;
|
|
285
|
+
} finally {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
}
|
|
288
|
+
const stdoutTail = stdoutDecoder.decode();
|
|
289
|
+
if (stdoutTail) {
|
|
290
|
+
stdout += stdoutTail;
|
|
291
|
+
options?.onStdout?.(stdoutTail);
|
|
292
|
+
}
|
|
293
|
+
const stderrTail = stderrDecoder.decode();
|
|
294
|
+
if (stderrTail) {
|
|
295
|
+
stderr += stderrTail;
|
|
296
|
+
options?.onStderr?.(stderrTail);
|
|
297
|
+
}
|
|
298
|
+
this.lastUsedAt = /* @__PURE__ */ new Date();
|
|
299
|
+
return {
|
|
300
|
+
command,
|
|
301
|
+
args,
|
|
302
|
+
success: exitCode === 0 && !signal.aborted,
|
|
303
|
+
exitCode,
|
|
304
|
+
stdout,
|
|
305
|
+
stderr,
|
|
306
|
+
executionTimeMs: Date.now() - startedAt,
|
|
307
|
+
timedOut: didTimeout,
|
|
308
|
+
killed: signal.aborted && !didTimeout
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
async writeFiles(files) {
|
|
312
|
+
const sandboxId = this.requireSandboxId();
|
|
313
|
+
for (const file of files) await this.client.writeFile(sandboxId, resolveWorkspacePath(file.path), file.content);
|
|
314
|
+
this.lastUsedAt = /* @__PURE__ */ new Date();
|
|
315
|
+
}
|
|
316
|
+
getInfo() {
|
|
317
|
+
return {
|
|
318
|
+
id: this.id,
|
|
319
|
+
name: this.name,
|
|
320
|
+
provider: this.provider,
|
|
321
|
+
status: this.status,
|
|
322
|
+
createdAt: this.createdAt,
|
|
323
|
+
lastUsedAt: this.lastUsedAt,
|
|
324
|
+
metadata: {
|
|
325
|
+
sandboxId: this.sandboxId,
|
|
326
|
+
bridgeBaseUrl: this.client instanceof CloudflareSandboxBridgeClient ? this.client.baseUrl : void 0
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
getInstructions() {
|
|
331
|
+
const defaultInstructions = "Commands execute in a remote Cloudflare Sandbox. Read and write persistent project files under /workspace.";
|
|
332
|
+
return typeof this.instructions === "function" ? this.instructions({ defaultInstructions }) : this.instructions ?? defaultInstructions;
|
|
333
|
+
}
|
|
334
|
+
requireSandboxId() {
|
|
335
|
+
if (!this.sandboxId) throw new Error(`Cloudflare Sandbox ${this.id} has not been started`);
|
|
336
|
+
return this.sandboxId;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
//#endregion
|
|
340
|
+
exports.CloudflareSandbox = CloudflareSandbox;
|
|
341
|
+
exports.CloudflareSandboxBridgeClient = CloudflareSandboxBridgeClient;
|
|
342
|
+
exports.CloudflareSandboxBridgeError = CloudflareSandboxBridgeError;
|
|
343
|
+
|
|
344
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["posix","path","MastraSandbox"],"sources":["../src/bridge-client.ts","../src/sandbox.ts"],"sourcesContent":["export interface CloudflareSandboxBridgeClientOptions {\n baseUrl: string;\n apiToken?: string;\n fetch?: typeof globalThis.fetch;\n}\n\n/** Terminal and streaming events emitted by `POST /v1/sandbox/:id/exec`. */\nexport type CloudflareCommandEvent =\n | { type: 'stdout'; data: Uint8Array }\n | { type: 'stderr'; data: Uint8Array }\n | { type: 'exit'; exitCode: number }\n | { type: 'error'; message: string; code?: string };\n\nexport interface CloudflareExecRequest {\n /** Command and arguments. The bridge applies ANSI-C quoting to each element. */\n argv: string[];\n timeoutMs?: number;\n cwd?: string;\n}\n\nexport class CloudflareSandboxBridgeError extends Error {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string) {\n super(`Cloudflare Sandbox Bridge request failed (${status}): ${body || 'empty response'}`);\n this.name = 'CloudflareSandboxBridgeError';\n this.status = status;\n this.body = body;\n }\n}\n\nfunction stripTrailingSlashes(url: string): string {\n let end = url.length;\n while (end > 0 && url[end - 1] === '/') end--;\n return url.slice(0, end);\n}\n\n/** Encodes an absolute sandbox path for the `/file/*` route, which omits the leading slash. */\nfunction encodeFilePath(absolutePath: string): string {\n let start = 0;\n while (start < absolutePath.length && absolutePath[start] === '/') start++;\n return absolutePath\n .slice(start)\n .split('/')\n .map(segment => encodeURIComponent(segment))\n .join('/');\n}\n\n/**\n * Client for the Cloudflare Sandbox Bridge Worker.\n *\n * @see https://developers.cloudflare.com/sandbox/bridge/http-api/\n */\nexport class CloudflareSandboxBridgeClient {\n readonly baseUrl: string;\n private readonly apiToken?: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n\n constructor(options: CloudflareSandboxBridgeClientOptions) {\n this.baseUrl = stripTrailingSlashes(options.baseUrl);\n this.apiToken = options.apiToken;\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n }\n\n /** `POST /v1/sandbox` */\n async createSandbox(): Promise<string> {\n const created = await this.request<{ id: string }>('/v1/sandbox', { method: 'POST' });\n return created.id;\n }\n\n /** `GET /v1/sandbox/:id/running` */\n async isRunning(id: string): Promise<boolean> {\n const status = await this.request<{ running: boolean }>(`/v1/sandbox/${encodeURIComponent(id)}/running`, {});\n return status.running === true;\n }\n\n /** `DELETE /v1/sandbox/:id` */\n async deleteSandbox(id: string): Promise<void> {\n await this.request(`/v1/sandbox/${encodeURIComponent(id)}`, { method: 'DELETE' }, true);\n }\n\n /** `PUT /v1/sandbox/:id/file/*` — one file per request, raw bytes as the body. */\n async writeFile(id: string, absolutePath: string, content: Uint8Array | string): Promise<void> {\n await this.request(\n `/v1/sandbox/${encodeURIComponent(id)}/file/${encodeFilePath(absolutePath)}`,\n {\n method: 'PUT',\n body: content as RequestInit['body'],\n headers: { 'content-type': 'application/octet-stream' },\n },\n true,\n );\n }\n\n /** `POST /v1/sandbox/:id/exec` — streams SSE events until `exit` or `error`. */\n async exec(\n id: string,\n request: CloudflareExecRequest,\n options: {\n signal?: AbortSignal;\n onEvent: (event: CloudflareCommandEvent) => void;\n },\n ): Promise<void> {\n const response = await this.fetchImpl(`${this.baseUrl}/v1/sandbox/${encodeURIComponent(id)}/exec`, {\n method: 'POST',\n headers: { ...this.headers(), 'content-type': 'application/json', accept: 'text/event-stream' },\n body: JSON.stringify({\n argv: request.argv,\n ...(request.timeoutMs === undefined ? {} : { timeout_ms: request.timeoutMs }),\n ...(request.cwd === undefined ? {} : { cwd: request.cwd }),\n }),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new CloudflareSandboxBridgeError(response.status, await response.text());\n }\n if (!response.body) {\n throw new Error('Cloudflare Sandbox Bridge returned an empty command stream');\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n buffer += decoder.decode(value, { stream: !done }).replace(/\\r\\n/g, '\\n');\n let boundary = buffer.indexOf('\\n\\n');\n while (boundary !== -1) {\n this.emitBlock(buffer.slice(0, boundary), options.onEvent);\n buffer = buffer.slice(boundary + 2);\n boundary = buffer.indexOf('\\n\\n');\n }\n if (done) break;\n }\n if (buffer.trim()) this.emitBlock(buffer, options.onEvent);\n }\n\n private emitBlock(block: string, onEvent: (event: CloudflareCommandEvent) => void): void {\n let eventName: string | undefined;\n const dataLines: string[] = [];\n for (const line of block.split('\\n')) {\n if (line.startsWith('event:')) eventName = line.slice(6).trim();\n else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n const data = dataLines.join('\\n');\n if (!eventName || !data) return;\n\n switch (eventName) {\n case 'stdout':\n case 'stderr':\n onEvent({ type: eventName, data: base64ToBytes(data) });\n return;\n case 'exit': {\n const parsed = safeJsonParse(data);\n onEvent({ type: 'exit', exitCode: typeof parsed?.exit_code === 'number' ? parsed.exit_code : 0 });\n return;\n }\n case 'error': {\n const parsed = safeJsonParse(data);\n onEvent({\n type: 'error',\n message: typeof parsed?.error === 'string' ? parsed.error : data,\n code: typeof parsed?.code === 'string' ? parsed.code : undefined,\n });\n return;\n }\n default:\n return;\n }\n }\n\n private headers(): Record<string, string> {\n return this.apiToken ? { authorization: `Bearer ${this.apiToken}` } : {};\n }\n\n private async request<T>(path: string, init: RequestInit, allowEmpty = false): Promise<T> {\n const response = await this.fetchImpl(`${this.baseUrl}${path}`, {\n ...init,\n headers: { ...this.headers(), ...init.headers },\n });\n if (!response.ok) {\n throw new CloudflareSandboxBridgeError(response.status, await response.text());\n }\n if (allowEmpty || response.status === 204) return undefined as T;\n return response.json() as Promise<T>;\n }\n}\n\nfunction base64ToBytes(value: string): Uint8Array {\n return new Uint8Array(Buffer.from(value, 'base64'));\n}\n\nfunction safeJsonParse(value: string): Record<string, unknown> | undefined {\n try {\n return JSON.parse(value) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport { posix } from 'node:path';\nimport type {\n CommandResult,\n ExecuteCommandOptions,\n MastraSandboxOptions,\n ProviderStatus,\n SandboxFileInput,\n SandboxInfo,\n} from '@mastra/core/workspace';\nimport { MastraSandbox } from '@mastra/core/workspace';\nimport { CloudflareSandboxBridgeClient, type CloudflareSandboxBridgeClientOptions } from './bridge-client';\n\nconst DEFAULT_COMMAND_TIMEOUT_MS = 300_000;\nconst WORKSPACE_ROOT = '/workspace';\n\ntype InstructionsOption = string | ((options: { defaultInstructions: string }) => string);\ntype BridgeClient = Pick<\n CloudflareSandboxBridgeClient,\n 'createSandbox' | 'isRunning' | 'deleteSandbox' | 'writeFile' | 'exec'\n>;\n\nexport interface CloudflareSandboxOptions extends Omit<MastraSandboxOptions, 'processes'> {\n /** URL of a deployed Cloudflare Sandbox Bridge Worker. */\n baseUrl: string;\n /** Bearer token matching the Worker's `SANDBOX_API_KEY` secret, when authentication is enabled. */\n apiToken?: string;\n /** Stable Mastra identifier for this sandbox instance. */\n id?: string;\n /** Existing Cloudflare sandbox ID to reconnect to instead of creating a sandbox. */\n sandboxId?: string;\n /** Human-readable name shown in Mastra sandbox metadata. */\n name?: string;\n /** Environment variables applied to every command. */\n env?: Record<string, string>;\n /** Working directory applied to every command. Must be under /workspace. */\n workingDirectory?: string;\n /** Default command timeout in milliseconds. */\n commandTimeout?: number;\n /** Custom instructions returned by getInstructions(). */\n instructions?: InstructionsOption;\n /** Custom fetch implementation, primarily for advanced networking setup and tests. */\n fetch?: CloudflareSandboxBridgeClientOptions['fetch'];\n /** Preconfigured Bridge client, primarily for tests. */\n client?: BridgeClient;\n}\n\n/**\n * Builds the argv array sent to the bridge. The bridge applies ANSI-C quoting to\n * every element, so no local escaping is needed. Environment variables are applied\n * with `env`, which keeps each assignment a separate argv element.\n */\nfunction buildArgv(command: string, args: string[] | undefined, env: Record<string, string>): string[] {\n const assignments = Object.entries(env).map(([key, value]) => {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: ${key}`);\n return `${key}=${value}`;\n });\n const invocation = [command, ...(args ?? [])];\n return assignments.length ? ['env', ...assignments, ...invocation] : invocation;\n}\n\n/** Resolves a path inside /workspace, rejecting anything that escapes the workspace root. */\nfunction resolveWorkspacePath(path: string): string {\n const resolved = posix.resolve(WORKSPACE_ROOT, path);\n if (resolved !== WORKSPACE_ROOT && !resolved.startsWith(`${WORKSPACE_ROOT}/`)) {\n throw new Error(`Cloudflare Sandbox files must be written under ${WORKSPACE_ROOT}: ${path}`);\n }\n return resolved;\n}\n\nexport class CloudflareSandbox extends MastraSandbox {\n readonly id: string;\n readonly name: string;\n readonly provider = 'cloudflare-sandbox';\n status: ProviderStatus = 'pending';\n\n private readonly client: BridgeClient;\n private readonly env: Record<string, string>;\n private readonly workingDirectory?: string;\n private readonly commandTimeout: number;\n private readonly instructions?: InstructionsOption;\n private sandboxId?: string;\n private createdAt = new Date();\n private lastUsedAt?: Date;\n\n constructor(options: CloudflareSandboxOptions) {\n const name = options.name ?? 'Cloudflare Sandbox';\n super({ ...options, name });\n this.id = options.id ?? `cloudflare-sandbox-${randomUUID()}`;\n this.name = name;\n this.sandboxId = options.sandboxId;\n this.env = { ...options.env };\n this.workingDirectory = options.workingDirectory;\n this.commandTimeout = options.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT_MS;\n this.instructions = options.instructions;\n this.client =\n options.client ??\n new CloudflareSandboxBridgeClient({ baseUrl: options.baseUrl, apiToken: options.apiToken, fetch: options.fetch });\n }\n\n async start(): Promise<void> {\n if (this.sandboxId) {\n // The bridge boots the container on demand, so a stopped container is not fatal.\n const running = await this.client.isRunning(this.sandboxId);\n if (!running) {\n this.logger?.debug(`Cloudflare sandbox ${this.sandboxId} is not running yet; it starts on first use`);\n }\n return;\n }\n this.sandboxId = await this.client.createSandbox();\n this.createdAt = new Date();\n }\n\n async stop(): Promise<void> {\n // The bridge exposes create/delete but no suspend operation. Stop detaches this\n // Mastra lifecycle while preserving the remote sandbox for later reconnection.\n }\n\n async destroy(): Promise<void> {\n if (!this.sandboxId) return;\n await this.client.deleteSandbox(this.sandboxId);\n this.sandboxId = undefined;\n }\n\n async executeCommand(command: string, args?: string[], options?: ExecuteCommandOptions): Promise<CommandResult> {\n const sandboxId = this.requireSandboxId();\n\n const startedAt = Date.now();\n const timeout = options?.timeout ?? this.commandTimeout;\n if (!Number.isFinite(timeout) || timeout <= 0) throw new RangeError('Command timeout must be positive');\n\n const controller = new AbortController();\n let didTimeout = false;\n const timer = setTimeout(() => {\n didTimeout = true;\n controller.abort();\n }, timeout);\n const signal = options?.abortSignal ? AbortSignal.any([controller.signal, options.abortSignal]) : controller.signal;\n\n // stdout and stderr are separate byte streams, so each needs its own streaming decoder.\n const stdoutDecoder = new TextDecoder();\n const stderrDecoder = new TextDecoder();\n let stdout = '';\n let stderr = '';\n let exitCode = 1;\n\n const env = Object.fromEntries(\n Object.entries({ ...this.env, ...options?.env }).filter(\n (entry): entry is [string, string] => entry[1] !== undefined,\n ),\n );\n\n try {\n await this.client.exec(\n sandboxId,\n {\n argv: buildArgv(command, args, env),\n timeoutMs: timeout,\n cwd: options?.cwd ?? this.workingDirectory,\n },\n {\n signal,\n onEvent: event => {\n switch (event.type) {\n case 'stdout': {\n const chunk = stdoutDecoder.decode(event.data, { stream: true });\n if (!chunk) return;\n stdout += chunk;\n options?.onStdout?.(chunk);\n return;\n }\n case 'stderr': {\n const chunk = stderrDecoder.decode(event.data, { stream: true });\n if (!chunk) return;\n stderr += chunk;\n options?.onStderr?.(chunk);\n return;\n }\n case 'exit':\n exitCode = event.exitCode;\n return;\n case 'error':\n stderr += event.message;\n options?.onStderr?.(event.message);\n return;\n }\n },\n },\n );\n } catch (error) {\n if (!signal.aborted) throw error;\n } finally {\n clearTimeout(timer);\n }\n\n // Flush each decoder so a trailing truncated multi-byte sequence isn't dropped.\n const stdoutTail = stdoutDecoder.decode();\n if (stdoutTail) {\n stdout += stdoutTail;\n options?.onStdout?.(stdoutTail);\n }\n const stderrTail = stderrDecoder.decode();\n if (stderrTail) {\n stderr += stderrTail;\n options?.onStderr?.(stderrTail);\n }\n\n this.lastUsedAt = new Date();\n return {\n command,\n args,\n success: exitCode === 0 && !signal.aborted,\n exitCode,\n stdout,\n stderr,\n executionTimeMs: Date.now() - startedAt,\n timedOut: didTimeout,\n killed: signal.aborted && !didTimeout,\n };\n }\n\n async writeFiles(files: SandboxFileInput[]): Promise<void> {\n const sandboxId = this.requireSandboxId();\n // The bridge writes one file per request.\n for (const file of files) {\n await this.client.writeFile(sandboxId, resolveWorkspacePath(file.path), file.content);\n }\n this.lastUsedAt = new Date();\n }\n\n getInfo(): SandboxInfo {\n return {\n id: this.id,\n name: this.name,\n provider: this.provider,\n status: this.status,\n createdAt: this.createdAt,\n lastUsedAt: this.lastUsedAt,\n metadata: {\n sandboxId: this.sandboxId,\n bridgeBaseUrl: this.client instanceof CloudflareSandboxBridgeClient ? this.client.baseUrl : undefined,\n },\n };\n }\n\n getInstructions(): string {\n const defaultInstructions =\n 'Commands execute in a remote Cloudflare Sandbox. Read and write persistent project files under /workspace.';\n return typeof this.instructions === 'function'\n ? this.instructions({ defaultInstructions })\n : (this.instructions ?? defaultInstructions);\n }\n\n private requireSandboxId(): string {\n if (!this.sandboxId) throw new Error(`Cloudflare Sandbox ${this.id} has not been started`);\n return this.sandboxId;\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { CloudflareSandboxBridgeClient, CloudflareSandboxBridgeError, type CloudflareCommandEvent, type CloudflareExecRequest, type CloudflareSandboxBridgeClientOptions, } from './bridge-client.js';
|
|
2
|
+
export { CloudflareSandbox, type CloudflareSandboxOptions } from './sandbox.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,EACL,6BAA6B,EAC7B,4BAA4B,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oCAAoC,GAC1C,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { posix } from "path";
|
|
3
|
+
import { MastraSandbox } from "@mastra/core/workspace";
|
|
4
|
+
//#region src/bridge-client.ts
|
|
5
|
+
var CloudflareSandboxBridgeError = class extends Error {
|
|
6
|
+
status;
|
|
7
|
+
body;
|
|
8
|
+
constructor(status, body) {
|
|
9
|
+
super(`Cloudflare Sandbox Bridge request failed (${status}): ${body || "empty response"}`);
|
|
10
|
+
this.name = "CloudflareSandboxBridgeError";
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.body = body;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function stripTrailingSlashes(url) {
|
|
16
|
+
let end = url.length;
|
|
17
|
+
while (end > 0 && url[end - 1] === "/") end--;
|
|
18
|
+
return url.slice(0, end);
|
|
19
|
+
}
|
|
20
|
+
/** Encodes an absolute sandbox path for the `/file/*` route, which omits the leading slash. */
|
|
21
|
+
function encodeFilePath(absolutePath) {
|
|
22
|
+
let start = 0;
|
|
23
|
+
while (start < absolutePath.length && absolutePath[start] === "/") start++;
|
|
24
|
+
return absolutePath.slice(start).split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Client for the Cloudflare Sandbox Bridge Worker.
|
|
28
|
+
*
|
|
29
|
+
* @see https://developers.cloudflare.com/sandbox/bridge/http-api/
|
|
30
|
+
*/
|
|
31
|
+
var CloudflareSandboxBridgeClient = class {
|
|
32
|
+
baseUrl;
|
|
33
|
+
apiToken;
|
|
34
|
+
fetchImpl;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.baseUrl = stripTrailingSlashes(options.baseUrl);
|
|
37
|
+
this.apiToken = options.apiToken;
|
|
38
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
39
|
+
}
|
|
40
|
+
/** `POST /v1/sandbox` */
|
|
41
|
+
async createSandbox() {
|
|
42
|
+
return (await this.request("/v1/sandbox", { method: "POST" })).id;
|
|
43
|
+
}
|
|
44
|
+
/** `GET /v1/sandbox/:id/running` */
|
|
45
|
+
async isRunning(id) {
|
|
46
|
+
return (await this.request(`/v1/sandbox/${encodeURIComponent(id)}/running`, {})).running === true;
|
|
47
|
+
}
|
|
48
|
+
/** `DELETE /v1/sandbox/:id` */
|
|
49
|
+
async deleteSandbox(id) {
|
|
50
|
+
await this.request(`/v1/sandbox/${encodeURIComponent(id)}`, { method: "DELETE" }, true);
|
|
51
|
+
}
|
|
52
|
+
/** `PUT /v1/sandbox/:id/file/*` — one file per request, raw bytes as the body. */
|
|
53
|
+
async writeFile(id, absolutePath, content) {
|
|
54
|
+
await this.request(`/v1/sandbox/${encodeURIComponent(id)}/file/${encodeFilePath(absolutePath)}`, {
|
|
55
|
+
method: "PUT",
|
|
56
|
+
body: content,
|
|
57
|
+
headers: { "content-type": "application/octet-stream" }
|
|
58
|
+
}, true);
|
|
59
|
+
}
|
|
60
|
+
/** `POST /v1/sandbox/:id/exec` — streams SSE events until `exit` or `error`. */
|
|
61
|
+
async exec(id, request, options) {
|
|
62
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/sandbox/${encodeURIComponent(id)}/exec`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
...this.headers(),
|
|
66
|
+
"content-type": "application/json",
|
|
67
|
+
accept: "text/event-stream"
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
argv: request.argv,
|
|
71
|
+
...request.timeoutMs === void 0 ? {} : { timeout_ms: request.timeoutMs },
|
|
72
|
+
...request.cwd === void 0 ? {} : { cwd: request.cwd }
|
|
73
|
+
}),
|
|
74
|
+
signal: options.signal
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) throw new CloudflareSandboxBridgeError(response.status, await response.text());
|
|
77
|
+
if (!response.body) throw new Error("Cloudflare Sandbox Bridge returned an empty command stream");
|
|
78
|
+
const reader = response.body.getReader();
|
|
79
|
+
const decoder = new TextDecoder();
|
|
80
|
+
let buffer = "";
|
|
81
|
+
while (true) {
|
|
82
|
+
const { done, value } = await reader.read();
|
|
83
|
+
buffer += decoder.decode(value, { stream: !done }).replace(/\r\n/g, "\n");
|
|
84
|
+
let boundary = buffer.indexOf("\n\n");
|
|
85
|
+
while (boundary !== -1) {
|
|
86
|
+
this.emitBlock(buffer.slice(0, boundary), options.onEvent);
|
|
87
|
+
buffer = buffer.slice(boundary + 2);
|
|
88
|
+
boundary = buffer.indexOf("\n\n");
|
|
89
|
+
}
|
|
90
|
+
if (done) break;
|
|
91
|
+
}
|
|
92
|
+
if (buffer.trim()) this.emitBlock(buffer, options.onEvent);
|
|
93
|
+
}
|
|
94
|
+
emitBlock(block, onEvent) {
|
|
95
|
+
let eventName;
|
|
96
|
+
const dataLines = [];
|
|
97
|
+
for (const line of block.split("\n")) if (line.startsWith("event:")) eventName = line.slice(6).trim();
|
|
98
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
99
|
+
const data = dataLines.join("\n");
|
|
100
|
+
if (!eventName || !data) return;
|
|
101
|
+
switch (eventName) {
|
|
102
|
+
case "stdout":
|
|
103
|
+
case "stderr":
|
|
104
|
+
onEvent({
|
|
105
|
+
type: eventName,
|
|
106
|
+
data: base64ToBytes(data)
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
case "exit": {
|
|
110
|
+
const parsed = safeJsonParse(data);
|
|
111
|
+
onEvent({
|
|
112
|
+
type: "exit",
|
|
113
|
+
exitCode: typeof parsed?.exit_code === "number" ? parsed.exit_code : 0
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
case "error": {
|
|
118
|
+
const parsed = safeJsonParse(data);
|
|
119
|
+
onEvent({
|
|
120
|
+
type: "error",
|
|
121
|
+
message: typeof parsed?.error === "string" ? parsed.error : data,
|
|
122
|
+
code: typeof parsed?.code === "string" ? parsed.code : void 0
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
default: return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
headers() {
|
|
130
|
+
return this.apiToken ? { authorization: `Bearer ${this.apiToken}` } : {};
|
|
131
|
+
}
|
|
132
|
+
async request(path, init, allowEmpty = false) {
|
|
133
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
134
|
+
...init,
|
|
135
|
+
headers: {
|
|
136
|
+
...this.headers(),
|
|
137
|
+
...init.headers
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) throw new CloudflareSandboxBridgeError(response.status, await response.text());
|
|
141
|
+
if (allowEmpty || response.status === 204) return void 0;
|
|
142
|
+
return response.json();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
function base64ToBytes(value) {
|
|
146
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
147
|
+
}
|
|
148
|
+
function safeJsonParse(value) {
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(value);
|
|
151
|
+
} catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/sandbox.ts
|
|
157
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 3e5;
|
|
158
|
+
const WORKSPACE_ROOT = "/workspace";
|
|
159
|
+
/**
|
|
160
|
+
* Builds the argv array sent to the bridge. The bridge applies ANSI-C quoting to
|
|
161
|
+
* every element, so no local escaping is needed. Environment variables are applied
|
|
162
|
+
* with `env`, which keeps each assignment a separate argv element.
|
|
163
|
+
*/
|
|
164
|
+
function buildArgv(command, args, env) {
|
|
165
|
+
const assignments = Object.entries(env).map(([key, value]) => {
|
|
166
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: ${key}`);
|
|
167
|
+
return `${key}=${value}`;
|
|
168
|
+
});
|
|
169
|
+
const invocation = [command, ...args ?? []];
|
|
170
|
+
return assignments.length ? [
|
|
171
|
+
"env",
|
|
172
|
+
...assignments,
|
|
173
|
+
...invocation
|
|
174
|
+
] : invocation;
|
|
175
|
+
}
|
|
176
|
+
/** Resolves a path inside /workspace, rejecting anything that escapes the workspace root. */
|
|
177
|
+
function resolveWorkspacePath(path) {
|
|
178
|
+
const resolved = posix.resolve(WORKSPACE_ROOT, path);
|
|
179
|
+
if (resolved !== WORKSPACE_ROOT && !resolved.startsWith(`${WORKSPACE_ROOT}/`)) throw new Error(`Cloudflare Sandbox files must be written under ${WORKSPACE_ROOT}: ${path}`);
|
|
180
|
+
return resolved;
|
|
181
|
+
}
|
|
182
|
+
var CloudflareSandbox = class extends MastraSandbox {
|
|
183
|
+
id;
|
|
184
|
+
name;
|
|
185
|
+
provider = "cloudflare-sandbox";
|
|
186
|
+
status = "pending";
|
|
187
|
+
client;
|
|
188
|
+
env;
|
|
189
|
+
workingDirectory;
|
|
190
|
+
commandTimeout;
|
|
191
|
+
instructions;
|
|
192
|
+
sandboxId;
|
|
193
|
+
createdAt = /* @__PURE__ */ new Date();
|
|
194
|
+
lastUsedAt;
|
|
195
|
+
constructor(options) {
|
|
196
|
+
const name = options.name ?? "Cloudflare Sandbox";
|
|
197
|
+
super({
|
|
198
|
+
...options,
|
|
199
|
+
name
|
|
200
|
+
});
|
|
201
|
+
this.id = options.id ?? `cloudflare-sandbox-${randomUUID()}`;
|
|
202
|
+
this.name = name;
|
|
203
|
+
this.sandboxId = options.sandboxId;
|
|
204
|
+
this.env = { ...options.env };
|
|
205
|
+
this.workingDirectory = options.workingDirectory;
|
|
206
|
+
this.commandTimeout = options.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
207
|
+
this.instructions = options.instructions;
|
|
208
|
+
this.client = options.client ?? new CloudflareSandboxBridgeClient({
|
|
209
|
+
baseUrl: options.baseUrl,
|
|
210
|
+
apiToken: options.apiToken,
|
|
211
|
+
fetch: options.fetch
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async start() {
|
|
215
|
+
if (this.sandboxId) {
|
|
216
|
+
if (!await this.client.isRunning(this.sandboxId)) this.logger?.debug(`Cloudflare sandbox ${this.sandboxId} is not running yet; it starts on first use`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.sandboxId = await this.client.createSandbox();
|
|
220
|
+
this.createdAt = /* @__PURE__ */ new Date();
|
|
221
|
+
}
|
|
222
|
+
async stop() {}
|
|
223
|
+
async destroy() {
|
|
224
|
+
if (!this.sandboxId) return;
|
|
225
|
+
await this.client.deleteSandbox(this.sandboxId);
|
|
226
|
+
this.sandboxId = void 0;
|
|
227
|
+
}
|
|
228
|
+
async executeCommand(command, args, options) {
|
|
229
|
+
const sandboxId = this.requireSandboxId();
|
|
230
|
+
const startedAt = Date.now();
|
|
231
|
+
const timeout = options?.timeout ?? this.commandTimeout;
|
|
232
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new RangeError("Command timeout must be positive");
|
|
233
|
+
const controller = new AbortController();
|
|
234
|
+
let didTimeout = false;
|
|
235
|
+
const timer = setTimeout(() => {
|
|
236
|
+
didTimeout = true;
|
|
237
|
+
controller.abort();
|
|
238
|
+
}, timeout);
|
|
239
|
+
const signal = options?.abortSignal ? AbortSignal.any([controller.signal, options.abortSignal]) : controller.signal;
|
|
240
|
+
const stdoutDecoder = new TextDecoder();
|
|
241
|
+
const stderrDecoder = new TextDecoder();
|
|
242
|
+
let stdout = "";
|
|
243
|
+
let stderr = "";
|
|
244
|
+
let exitCode = 1;
|
|
245
|
+
const env = Object.fromEntries(Object.entries({
|
|
246
|
+
...this.env,
|
|
247
|
+
...options?.env
|
|
248
|
+
}).filter((entry) => entry[1] !== void 0));
|
|
249
|
+
try {
|
|
250
|
+
await this.client.exec(sandboxId, {
|
|
251
|
+
argv: buildArgv(command, args, env),
|
|
252
|
+
timeoutMs: timeout,
|
|
253
|
+
cwd: options?.cwd ?? this.workingDirectory
|
|
254
|
+
}, {
|
|
255
|
+
signal,
|
|
256
|
+
onEvent: (event) => {
|
|
257
|
+
switch (event.type) {
|
|
258
|
+
case "stdout": {
|
|
259
|
+
const chunk = stdoutDecoder.decode(event.data, { stream: true });
|
|
260
|
+
if (!chunk) return;
|
|
261
|
+
stdout += chunk;
|
|
262
|
+
options?.onStdout?.(chunk);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
case "stderr": {
|
|
266
|
+
const chunk = stderrDecoder.decode(event.data, { stream: true });
|
|
267
|
+
if (!chunk) return;
|
|
268
|
+
stderr += chunk;
|
|
269
|
+
options?.onStderr?.(chunk);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
case "exit":
|
|
273
|
+
exitCode = event.exitCode;
|
|
274
|
+
return;
|
|
275
|
+
case "error":
|
|
276
|
+
stderr += event.message;
|
|
277
|
+
options?.onStderr?.(event.message);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (!signal.aborted) throw error;
|
|
284
|
+
} finally {
|
|
285
|
+
clearTimeout(timer);
|
|
286
|
+
}
|
|
287
|
+
const stdoutTail = stdoutDecoder.decode();
|
|
288
|
+
if (stdoutTail) {
|
|
289
|
+
stdout += stdoutTail;
|
|
290
|
+
options?.onStdout?.(stdoutTail);
|
|
291
|
+
}
|
|
292
|
+
const stderrTail = stderrDecoder.decode();
|
|
293
|
+
if (stderrTail) {
|
|
294
|
+
stderr += stderrTail;
|
|
295
|
+
options?.onStderr?.(stderrTail);
|
|
296
|
+
}
|
|
297
|
+
this.lastUsedAt = /* @__PURE__ */ new Date();
|
|
298
|
+
return {
|
|
299
|
+
command,
|
|
300
|
+
args,
|
|
301
|
+
success: exitCode === 0 && !signal.aborted,
|
|
302
|
+
exitCode,
|
|
303
|
+
stdout,
|
|
304
|
+
stderr,
|
|
305
|
+
executionTimeMs: Date.now() - startedAt,
|
|
306
|
+
timedOut: didTimeout,
|
|
307
|
+
killed: signal.aborted && !didTimeout
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async writeFiles(files) {
|
|
311
|
+
const sandboxId = this.requireSandboxId();
|
|
312
|
+
for (const file of files) await this.client.writeFile(sandboxId, resolveWorkspacePath(file.path), file.content);
|
|
313
|
+
this.lastUsedAt = /* @__PURE__ */ new Date();
|
|
314
|
+
}
|
|
315
|
+
getInfo() {
|
|
316
|
+
return {
|
|
317
|
+
id: this.id,
|
|
318
|
+
name: this.name,
|
|
319
|
+
provider: this.provider,
|
|
320
|
+
status: this.status,
|
|
321
|
+
createdAt: this.createdAt,
|
|
322
|
+
lastUsedAt: this.lastUsedAt,
|
|
323
|
+
metadata: {
|
|
324
|
+
sandboxId: this.sandboxId,
|
|
325
|
+
bridgeBaseUrl: this.client instanceof CloudflareSandboxBridgeClient ? this.client.baseUrl : void 0
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
getInstructions() {
|
|
330
|
+
const defaultInstructions = "Commands execute in a remote Cloudflare Sandbox. Read and write persistent project files under /workspace.";
|
|
331
|
+
return typeof this.instructions === "function" ? this.instructions({ defaultInstructions }) : this.instructions ?? defaultInstructions;
|
|
332
|
+
}
|
|
333
|
+
requireSandboxId() {
|
|
334
|
+
if (!this.sandboxId) throw new Error(`Cloudflare Sandbox ${this.id} has not been started`);
|
|
335
|
+
return this.sandboxId;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
//#endregion
|
|
339
|
+
export { CloudflareSandbox, CloudflareSandboxBridgeClient, CloudflareSandboxBridgeError };
|
|
340
|
+
|
|
341
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/bridge-client.ts","../src/sandbox.ts"],"sourcesContent":["export interface CloudflareSandboxBridgeClientOptions {\n baseUrl: string;\n apiToken?: string;\n fetch?: typeof globalThis.fetch;\n}\n\n/** Terminal and streaming events emitted by `POST /v1/sandbox/:id/exec`. */\nexport type CloudflareCommandEvent =\n | { type: 'stdout'; data: Uint8Array }\n | { type: 'stderr'; data: Uint8Array }\n | { type: 'exit'; exitCode: number }\n | { type: 'error'; message: string; code?: string };\n\nexport interface CloudflareExecRequest {\n /** Command and arguments. The bridge applies ANSI-C quoting to each element. */\n argv: string[];\n timeoutMs?: number;\n cwd?: string;\n}\n\nexport class CloudflareSandboxBridgeError extends Error {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string) {\n super(`Cloudflare Sandbox Bridge request failed (${status}): ${body || 'empty response'}`);\n this.name = 'CloudflareSandboxBridgeError';\n this.status = status;\n this.body = body;\n }\n}\n\nfunction stripTrailingSlashes(url: string): string {\n let end = url.length;\n while (end > 0 && url[end - 1] === '/') end--;\n return url.slice(0, end);\n}\n\n/** Encodes an absolute sandbox path for the `/file/*` route, which omits the leading slash. */\nfunction encodeFilePath(absolutePath: string): string {\n let start = 0;\n while (start < absolutePath.length && absolutePath[start] === '/') start++;\n return absolutePath\n .slice(start)\n .split('/')\n .map(segment => encodeURIComponent(segment))\n .join('/');\n}\n\n/**\n * Client for the Cloudflare Sandbox Bridge Worker.\n *\n * @see https://developers.cloudflare.com/sandbox/bridge/http-api/\n */\nexport class CloudflareSandboxBridgeClient {\n readonly baseUrl: string;\n private readonly apiToken?: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n\n constructor(options: CloudflareSandboxBridgeClientOptions) {\n this.baseUrl = stripTrailingSlashes(options.baseUrl);\n this.apiToken = options.apiToken;\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n }\n\n /** `POST /v1/sandbox` */\n async createSandbox(): Promise<string> {\n const created = await this.request<{ id: string }>('/v1/sandbox', { method: 'POST' });\n return created.id;\n }\n\n /** `GET /v1/sandbox/:id/running` */\n async isRunning(id: string): Promise<boolean> {\n const status = await this.request<{ running: boolean }>(`/v1/sandbox/${encodeURIComponent(id)}/running`, {});\n return status.running === true;\n }\n\n /** `DELETE /v1/sandbox/:id` */\n async deleteSandbox(id: string): Promise<void> {\n await this.request(`/v1/sandbox/${encodeURIComponent(id)}`, { method: 'DELETE' }, true);\n }\n\n /** `PUT /v1/sandbox/:id/file/*` — one file per request, raw bytes as the body. */\n async writeFile(id: string, absolutePath: string, content: Uint8Array | string): Promise<void> {\n await this.request(\n `/v1/sandbox/${encodeURIComponent(id)}/file/${encodeFilePath(absolutePath)}`,\n {\n method: 'PUT',\n body: content as RequestInit['body'],\n headers: { 'content-type': 'application/octet-stream' },\n },\n true,\n );\n }\n\n /** `POST /v1/sandbox/:id/exec` — streams SSE events until `exit` or `error`. */\n async exec(\n id: string,\n request: CloudflareExecRequest,\n options: {\n signal?: AbortSignal;\n onEvent: (event: CloudflareCommandEvent) => void;\n },\n ): Promise<void> {\n const response = await this.fetchImpl(`${this.baseUrl}/v1/sandbox/${encodeURIComponent(id)}/exec`, {\n method: 'POST',\n headers: { ...this.headers(), 'content-type': 'application/json', accept: 'text/event-stream' },\n body: JSON.stringify({\n argv: request.argv,\n ...(request.timeoutMs === undefined ? {} : { timeout_ms: request.timeoutMs }),\n ...(request.cwd === undefined ? {} : { cwd: request.cwd }),\n }),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new CloudflareSandboxBridgeError(response.status, await response.text());\n }\n if (!response.body) {\n throw new Error('Cloudflare Sandbox Bridge returned an empty command stream');\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n buffer += decoder.decode(value, { stream: !done }).replace(/\\r\\n/g, '\\n');\n let boundary = buffer.indexOf('\\n\\n');\n while (boundary !== -1) {\n this.emitBlock(buffer.slice(0, boundary), options.onEvent);\n buffer = buffer.slice(boundary + 2);\n boundary = buffer.indexOf('\\n\\n');\n }\n if (done) break;\n }\n if (buffer.trim()) this.emitBlock(buffer, options.onEvent);\n }\n\n private emitBlock(block: string, onEvent: (event: CloudflareCommandEvent) => void): void {\n let eventName: string | undefined;\n const dataLines: string[] = [];\n for (const line of block.split('\\n')) {\n if (line.startsWith('event:')) eventName = line.slice(6).trim();\n else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n const data = dataLines.join('\\n');\n if (!eventName || !data) return;\n\n switch (eventName) {\n case 'stdout':\n case 'stderr':\n onEvent({ type: eventName, data: base64ToBytes(data) });\n return;\n case 'exit': {\n const parsed = safeJsonParse(data);\n onEvent({ type: 'exit', exitCode: typeof parsed?.exit_code === 'number' ? parsed.exit_code : 0 });\n return;\n }\n case 'error': {\n const parsed = safeJsonParse(data);\n onEvent({\n type: 'error',\n message: typeof parsed?.error === 'string' ? parsed.error : data,\n code: typeof parsed?.code === 'string' ? parsed.code : undefined,\n });\n return;\n }\n default:\n return;\n }\n }\n\n private headers(): Record<string, string> {\n return this.apiToken ? { authorization: `Bearer ${this.apiToken}` } : {};\n }\n\n private async request<T>(path: string, init: RequestInit, allowEmpty = false): Promise<T> {\n const response = await this.fetchImpl(`${this.baseUrl}${path}`, {\n ...init,\n headers: { ...this.headers(), ...init.headers },\n });\n if (!response.ok) {\n throw new CloudflareSandboxBridgeError(response.status, await response.text());\n }\n if (allowEmpty || response.status === 204) return undefined as T;\n return response.json() as Promise<T>;\n }\n}\n\nfunction base64ToBytes(value: string): Uint8Array {\n return new Uint8Array(Buffer.from(value, 'base64'));\n}\n\nfunction safeJsonParse(value: string): Record<string, unknown> | undefined {\n try {\n return JSON.parse(value) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport { posix } from 'node:path';\nimport type {\n CommandResult,\n ExecuteCommandOptions,\n MastraSandboxOptions,\n ProviderStatus,\n SandboxFileInput,\n SandboxInfo,\n} from '@mastra/core/workspace';\nimport { MastraSandbox } from '@mastra/core/workspace';\nimport { CloudflareSandboxBridgeClient, type CloudflareSandboxBridgeClientOptions } from './bridge-client';\n\nconst DEFAULT_COMMAND_TIMEOUT_MS = 300_000;\nconst WORKSPACE_ROOT = '/workspace';\n\ntype InstructionsOption = string | ((options: { defaultInstructions: string }) => string);\ntype BridgeClient = Pick<\n CloudflareSandboxBridgeClient,\n 'createSandbox' | 'isRunning' | 'deleteSandbox' | 'writeFile' | 'exec'\n>;\n\nexport interface CloudflareSandboxOptions extends Omit<MastraSandboxOptions, 'processes'> {\n /** URL of a deployed Cloudflare Sandbox Bridge Worker. */\n baseUrl: string;\n /** Bearer token matching the Worker's `SANDBOX_API_KEY` secret, when authentication is enabled. */\n apiToken?: string;\n /** Stable Mastra identifier for this sandbox instance. */\n id?: string;\n /** Existing Cloudflare sandbox ID to reconnect to instead of creating a sandbox. */\n sandboxId?: string;\n /** Human-readable name shown in Mastra sandbox metadata. */\n name?: string;\n /** Environment variables applied to every command. */\n env?: Record<string, string>;\n /** Working directory applied to every command. Must be under /workspace. */\n workingDirectory?: string;\n /** Default command timeout in milliseconds. */\n commandTimeout?: number;\n /** Custom instructions returned by getInstructions(). */\n instructions?: InstructionsOption;\n /** Custom fetch implementation, primarily for advanced networking setup and tests. */\n fetch?: CloudflareSandboxBridgeClientOptions['fetch'];\n /** Preconfigured Bridge client, primarily for tests. */\n client?: BridgeClient;\n}\n\n/**\n * Builds the argv array sent to the bridge. The bridge applies ANSI-C quoting to\n * every element, so no local escaping is needed. Environment variables are applied\n * with `env`, which keeps each assignment a separate argv element.\n */\nfunction buildArgv(command: string, args: string[] | undefined, env: Record<string, string>): string[] {\n const assignments = Object.entries(env).map(([key, value]) => {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: ${key}`);\n return `${key}=${value}`;\n });\n const invocation = [command, ...(args ?? [])];\n return assignments.length ? ['env', ...assignments, ...invocation] : invocation;\n}\n\n/** Resolves a path inside /workspace, rejecting anything that escapes the workspace root. */\nfunction resolveWorkspacePath(path: string): string {\n const resolved = posix.resolve(WORKSPACE_ROOT, path);\n if (resolved !== WORKSPACE_ROOT && !resolved.startsWith(`${WORKSPACE_ROOT}/`)) {\n throw new Error(`Cloudflare Sandbox files must be written under ${WORKSPACE_ROOT}: ${path}`);\n }\n return resolved;\n}\n\nexport class CloudflareSandbox extends MastraSandbox {\n readonly id: string;\n readonly name: string;\n readonly provider = 'cloudflare-sandbox';\n status: ProviderStatus = 'pending';\n\n private readonly client: BridgeClient;\n private readonly env: Record<string, string>;\n private readonly workingDirectory?: string;\n private readonly commandTimeout: number;\n private readonly instructions?: InstructionsOption;\n private sandboxId?: string;\n private createdAt = new Date();\n private lastUsedAt?: Date;\n\n constructor(options: CloudflareSandboxOptions) {\n const name = options.name ?? 'Cloudflare Sandbox';\n super({ ...options, name });\n this.id = options.id ?? `cloudflare-sandbox-${randomUUID()}`;\n this.name = name;\n this.sandboxId = options.sandboxId;\n this.env = { ...options.env };\n this.workingDirectory = options.workingDirectory;\n this.commandTimeout = options.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT_MS;\n this.instructions = options.instructions;\n this.client =\n options.client ??\n new CloudflareSandboxBridgeClient({ baseUrl: options.baseUrl, apiToken: options.apiToken, fetch: options.fetch });\n }\n\n async start(): Promise<void> {\n if (this.sandboxId) {\n // The bridge boots the container on demand, so a stopped container is not fatal.\n const running = await this.client.isRunning(this.sandboxId);\n if (!running) {\n this.logger?.debug(`Cloudflare sandbox ${this.sandboxId} is not running yet; it starts on first use`);\n }\n return;\n }\n this.sandboxId = await this.client.createSandbox();\n this.createdAt = new Date();\n }\n\n async stop(): Promise<void> {\n // The bridge exposes create/delete but no suspend operation. Stop detaches this\n // Mastra lifecycle while preserving the remote sandbox for later reconnection.\n }\n\n async destroy(): Promise<void> {\n if (!this.sandboxId) return;\n await this.client.deleteSandbox(this.sandboxId);\n this.sandboxId = undefined;\n }\n\n async executeCommand(command: string, args?: string[], options?: ExecuteCommandOptions): Promise<CommandResult> {\n const sandboxId = this.requireSandboxId();\n\n const startedAt = Date.now();\n const timeout = options?.timeout ?? this.commandTimeout;\n if (!Number.isFinite(timeout) || timeout <= 0) throw new RangeError('Command timeout must be positive');\n\n const controller = new AbortController();\n let didTimeout = false;\n const timer = setTimeout(() => {\n didTimeout = true;\n controller.abort();\n }, timeout);\n const signal = options?.abortSignal ? AbortSignal.any([controller.signal, options.abortSignal]) : controller.signal;\n\n // stdout and stderr are separate byte streams, so each needs its own streaming decoder.\n const stdoutDecoder = new TextDecoder();\n const stderrDecoder = new TextDecoder();\n let stdout = '';\n let stderr = '';\n let exitCode = 1;\n\n const env = Object.fromEntries(\n Object.entries({ ...this.env, ...options?.env }).filter(\n (entry): entry is [string, string] => entry[1] !== undefined,\n ),\n );\n\n try {\n await this.client.exec(\n sandboxId,\n {\n argv: buildArgv(command, args, env),\n timeoutMs: timeout,\n cwd: options?.cwd ?? this.workingDirectory,\n },\n {\n signal,\n onEvent: event => {\n switch (event.type) {\n case 'stdout': {\n const chunk = stdoutDecoder.decode(event.data, { stream: true });\n if (!chunk) return;\n stdout += chunk;\n options?.onStdout?.(chunk);\n return;\n }\n case 'stderr': {\n const chunk = stderrDecoder.decode(event.data, { stream: true });\n if (!chunk) return;\n stderr += chunk;\n options?.onStderr?.(chunk);\n return;\n }\n case 'exit':\n exitCode = event.exitCode;\n return;\n case 'error':\n stderr += event.message;\n options?.onStderr?.(event.message);\n return;\n }\n },\n },\n );\n } catch (error) {\n if (!signal.aborted) throw error;\n } finally {\n clearTimeout(timer);\n }\n\n // Flush each decoder so a trailing truncated multi-byte sequence isn't dropped.\n const stdoutTail = stdoutDecoder.decode();\n if (stdoutTail) {\n stdout += stdoutTail;\n options?.onStdout?.(stdoutTail);\n }\n const stderrTail = stderrDecoder.decode();\n if (stderrTail) {\n stderr += stderrTail;\n options?.onStderr?.(stderrTail);\n }\n\n this.lastUsedAt = new Date();\n return {\n command,\n args,\n success: exitCode === 0 && !signal.aborted,\n exitCode,\n stdout,\n stderr,\n executionTimeMs: Date.now() - startedAt,\n timedOut: didTimeout,\n killed: signal.aborted && !didTimeout,\n };\n }\n\n async writeFiles(files: SandboxFileInput[]): Promise<void> {\n const sandboxId = this.requireSandboxId();\n // The bridge writes one file per request.\n for (const file of files) {\n await this.client.writeFile(sandboxId, resolveWorkspacePath(file.path), file.content);\n }\n this.lastUsedAt = new Date();\n }\n\n getInfo(): SandboxInfo {\n return {\n id: this.id,\n name: this.name,\n provider: this.provider,\n status: this.status,\n createdAt: this.createdAt,\n lastUsedAt: this.lastUsedAt,\n metadata: {\n sandboxId: this.sandboxId,\n bridgeBaseUrl: this.client instanceof CloudflareSandboxBridgeClient ? this.client.baseUrl : undefined,\n },\n };\n }\n\n getInstructions(): string {\n const defaultInstructions =\n 'Commands execute in a remote Cloudflare Sandbox. Read and write persistent project files under /workspace.';\n return typeof this.instructions === 'function'\n ? this.instructions({ defaultInstructions })\n : (this.instructions ?? defaultInstructions);\n }\n\n private requireSandboxId(): string {\n if (!this.sandboxId) throw new Error(`Cloudflare Sandbox ${this.id} has not been started`);\n return this.sandboxId;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CommandResult, ExecuteCommandOptions, MastraSandboxOptions, ProviderStatus, SandboxFileInput, SandboxInfo } from '@mastra/core/workspace';
|
|
2
|
+
import { MastraSandbox } from '@mastra/core/workspace';
|
|
3
|
+
import { CloudflareSandboxBridgeClient, type CloudflareSandboxBridgeClientOptions } from './bridge-client.js';
|
|
4
|
+
type InstructionsOption = string | ((options: {
|
|
5
|
+
defaultInstructions: string;
|
|
6
|
+
}) => string);
|
|
7
|
+
type BridgeClient = Pick<CloudflareSandboxBridgeClient, 'createSandbox' | 'isRunning' | 'deleteSandbox' | 'writeFile' | 'exec'>;
|
|
8
|
+
export interface CloudflareSandboxOptions extends Omit<MastraSandboxOptions, 'processes'> {
|
|
9
|
+
/** URL of a deployed Cloudflare Sandbox Bridge Worker. */
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
/** Bearer token matching the Worker's `SANDBOX_API_KEY` secret, when authentication is enabled. */
|
|
12
|
+
apiToken?: string;
|
|
13
|
+
/** Stable Mastra identifier for this sandbox instance. */
|
|
14
|
+
id?: string;
|
|
15
|
+
/** Existing Cloudflare sandbox ID to reconnect to instead of creating a sandbox. */
|
|
16
|
+
sandboxId?: string;
|
|
17
|
+
/** Human-readable name shown in Mastra sandbox metadata. */
|
|
18
|
+
name?: string;
|
|
19
|
+
/** Environment variables applied to every command. */
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
/** Working directory applied to every command. Must be under /workspace. */
|
|
22
|
+
workingDirectory?: string;
|
|
23
|
+
/** Default command timeout in milliseconds. */
|
|
24
|
+
commandTimeout?: number;
|
|
25
|
+
/** Custom instructions returned by getInstructions(). */
|
|
26
|
+
instructions?: InstructionsOption;
|
|
27
|
+
/** Custom fetch implementation, primarily for advanced networking setup and tests. */
|
|
28
|
+
fetch?: CloudflareSandboxBridgeClientOptions['fetch'];
|
|
29
|
+
/** Preconfigured Bridge client, primarily for tests. */
|
|
30
|
+
client?: BridgeClient;
|
|
31
|
+
}
|
|
32
|
+
export declare class CloudflareSandbox extends MastraSandbox {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly provider = "cloudflare-sandbox";
|
|
36
|
+
status: ProviderStatus;
|
|
37
|
+
private readonly client;
|
|
38
|
+
private readonly env;
|
|
39
|
+
private readonly workingDirectory?;
|
|
40
|
+
private readonly commandTimeout;
|
|
41
|
+
private readonly instructions?;
|
|
42
|
+
private sandboxId?;
|
|
43
|
+
private createdAt;
|
|
44
|
+
private lastUsedAt?;
|
|
45
|
+
constructor(options: CloudflareSandboxOptions);
|
|
46
|
+
start(): Promise<void>;
|
|
47
|
+
stop(): Promise<void>;
|
|
48
|
+
destroy(): Promise<void>;
|
|
49
|
+
executeCommand(command: string, args?: string[], options?: ExecuteCommandOptions): Promise<CommandResult>;
|
|
50
|
+
writeFiles(files: SandboxFileInput[]): Promise<void>;
|
|
51
|
+
getInfo(): SandboxInfo;
|
|
52
|
+
getInstructions(): string;
|
|
53
|
+
private requireSandboxId;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,WAAW,EACZ,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,6BAA6B,EAAE,KAAK,oCAAoC,EAAE,MAAM,iBAAiB,CAAC;AAK3G,KAAK,kBAAkB,GAAG,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE;IAAE,mBAAmB,EAAE,MAAM,CAAA;CAAE,KAAK,MAAM,CAAC,CAAC;AAC1F,KAAK,YAAY,GAAG,IAAI,CACtB,6BAA6B,EAC7B,eAAe,GAAG,WAAW,GAAG,eAAe,GAAG,WAAW,GAAG,MAAM,CACvE,CAAC;AAEF,MAAM,WAAW,wBAAyB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC;IACvF,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+CAA+C;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,sFAAsF;IACtF,KAAK,CAAC,EAAE,oCAAoC,CAAC,OAAO,CAAC,CAAC;IACtD,wDAAwD;IACxD,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAyBD,qBAAa,iBAAkB,SAAQ,aAAa;IAClD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,wBAAwB;IACzC,MAAM,EAAE,cAAc,CAAa;IAEnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyB;IAC7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAqB;IACnD,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,UAAU,CAAC,CAAO;gBAEd,OAAO,EAAE,wBAAwB;IAevC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAatB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAMxB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC;IAiGzG,UAAU,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAS1D,OAAO,IAAI,WAAW;IAetB,eAAe,IAAI,MAAM;IAQzB,OAAO,CAAC,gBAAgB;CAIzB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory stand-in for a deployed Cloudflare Sandbox Bridge Worker.
|
|
3
|
+
*
|
|
4
|
+
* It implements the documented routes and SSE contract from
|
|
5
|
+
* https://developers.cloudflare.com/sandbox/bridge/http-api/ so unit tests drive
|
|
6
|
+
* the real `CloudflareSandboxBridgeClient` over `fetch` instead of a mock client.
|
|
7
|
+
*/
|
|
8
|
+
export interface FakeExecRequest {
|
|
9
|
+
argv: string[];
|
|
10
|
+
timeout_ms?: number;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface FakeExecResult {
|
|
14
|
+
stdout?: string;
|
|
15
|
+
stderr?: string;
|
|
16
|
+
exitCode?: number;
|
|
17
|
+
/** Emit an `error` event instead of `exit`. */
|
|
18
|
+
error?: {
|
|
19
|
+
error: string;
|
|
20
|
+
code?: string;
|
|
21
|
+
};
|
|
22
|
+
/** Split stdout into this many SSE frames to exercise chunked decoding. */
|
|
23
|
+
stdoutChunks?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface FakeBridgeRequest {
|
|
26
|
+
method: string;
|
|
27
|
+
url: string;
|
|
28
|
+
authorization?: string;
|
|
29
|
+
body?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface FakeBridge {
|
|
32
|
+
fetch: typeof globalThis.fetch;
|
|
33
|
+
requests: FakeBridgeRequest[];
|
|
34
|
+
execs: FakeExecRequest[];
|
|
35
|
+
files: Map<string, string>;
|
|
36
|
+
sandboxes: Set<string>;
|
|
37
|
+
/** Overrides the default `echo`-only behaviour. */
|
|
38
|
+
onExec?: (request: FakeExecRequest) => FakeExecResult;
|
|
39
|
+
}
|
|
40
|
+
export declare function createFakeBridge(options?: {
|
|
41
|
+
apiToken?: string;
|
|
42
|
+
baseUrl?: string;
|
|
43
|
+
}): FakeBridge;
|
|
44
|
+
//# sourceMappingURL=fake-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-bridge.d.ts","sourceRoot":"","sources":["../../src/testing/fake-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC/B,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,mDAAmD;IACnD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,cAAc,CAAC;CACvD;AAqBD,wBAAgB,gBAAgB,CAAC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,UAAU,CA+ElG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/cloudflare-sandbox",
|
|
3
|
-
"version": "0.0.0",
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
4
|
"description": "Cloudflare Sandbox provider for Mastra workspaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,31 +18,20 @@
|
|
|
18
18
|
},
|
|
19
19
|
"./package.json": "./package.json"
|
|
20
20
|
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "tsdown --silent --config tsdown.config.ts",
|
|
23
|
-
"build:lib": "pnpm build",
|
|
24
|
-
"build:watch": "pnpm build --watch",
|
|
25
|
-
"test:unit": "vitest run --exclude '**/*.integration.test.ts'",
|
|
26
|
-
"test:watch": "vitest watch",
|
|
27
|
-
"test": "vitest run ./src/**/*.integration.test.ts",
|
|
28
|
-
"test:cloud": "pnpm test",
|
|
29
|
-
"lint": "oxlint . && eslint .",
|
|
30
|
-
"lint:fix": "oxlint --fix . && eslint --fix ."
|
|
31
|
-
},
|
|
32
21
|
"license": "Apache-2.0",
|
|
33
22
|
"devDependencies": {
|
|
34
|
-
"@internal/lint": "workspace:*",
|
|
35
|
-
"@internal/types-builder": "workspace:*",
|
|
36
|
-
"@internal/workspace-test-utils": "workspace:*",
|
|
37
|
-
"@mastra/core": "workspace:*",
|
|
38
23
|
"@types/node": "22.19.15",
|
|
39
|
-
"@vitest/coverage-v8": "
|
|
40
|
-
"@vitest/ui": "
|
|
24
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
25
|
+
"@vitest/ui": "4.1.10",
|
|
41
26
|
"dotenv": "^17.4.2",
|
|
42
27
|
"eslint": "^10.7.0",
|
|
43
28
|
"tsdown": "0.22.9",
|
|
44
|
-
"typescript": "
|
|
45
|
-
"vitest": "
|
|
29
|
+
"typescript": "^6.0.3",
|
|
30
|
+
"vitest": "4.1.10",
|
|
31
|
+
"@internal/lint": "0.0.123",
|
|
32
|
+
"@internal/workspace-test-utils": "0.0.67",
|
|
33
|
+
"@internal/types-builder": "0.0.98",
|
|
34
|
+
"@mastra/core": "1.60.0-alpha.12"
|
|
46
35
|
},
|
|
47
36
|
"peerDependencies": {
|
|
48
37
|
"@mastra/core": ">=1.12.0-0 <2.0.0-0"
|
|
@@ -62,5 +51,16 @@
|
|
|
62
51
|
},
|
|
63
52
|
"engines": {
|
|
64
53
|
"node": ">=22.13.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsdown --silent --config tsdown.config.ts",
|
|
57
|
+
"build:lib": "pnpm build",
|
|
58
|
+
"build:watch": "pnpm build --watch",
|
|
59
|
+
"test:unit": "vitest run --exclude '**/*.integration.test.ts'",
|
|
60
|
+
"test:watch": "vitest watch",
|
|
61
|
+
"test": "vitest run ./src/**/*.integration.test.ts",
|
|
62
|
+
"test:cloud": "pnpm test",
|
|
63
|
+
"lint": "oxlint . && eslint .",
|
|
64
|
+
"lint:fix": "oxlint --fix . && eslint --fix ."
|
|
65
65
|
}
|
|
66
|
-
}
|
|
66
|
+
}
|