@openai/agents-extensions 0.8.5 → 0.9.1
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/dist/metadata.js +2 -2
- package/dist/metadata.mjs +2 -2
- package/dist/sandbox/blaxel/index.d.ts +2 -0
- package/dist/sandbox/blaxel/index.js +19 -0
- package/dist/sandbox/blaxel/index.js.map +1 -0
- package/dist/sandbox/blaxel/index.mjs +3 -0
- package/dist/sandbox/blaxel/index.mjs.map +1 -0
- package/dist/sandbox/blaxel/mounts.d.ts +101 -0
- package/dist/sandbox/blaxel/mounts.js +542 -0
- package/dist/sandbox/blaxel/mounts.js.map +1 -0
- package/dist/sandbox/blaxel/mounts.mjs +529 -0
- package/dist/sandbox/blaxel/mounts.mjs.map +1 -0
- package/dist/sandbox/blaxel/sandbox.d.ts +155 -0
- package/dist/sandbox/blaxel/sandbox.js +1110 -0
- package/dist/sandbox/blaxel/sandbox.js.map +1 -0
- package/dist/sandbox/blaxel/sandbox.mjs +1072 -0
- package/dist/sandbox/blaxel/sandbox.mjs.map +1 -0
- package/dist/sandbox/cloudflare/index.d.ts +2 -0
- package/dist/sandbox/cloudflare/index.js +19 -0
- package/dist/sandbox/cloudflare/index.js.map +1 -0
- package/dist/sandbox/cloudflare/index.mjs +3 -0
- package/dist/sandbox/cloudflare/index.mjs.map +1 -0
- package/dist/sandbox/cloudflare/mounts.d.ts +28 -0
- package/dist/sandbox/cloudflare/mounts.js +150 -0
- package/dist/sandbox/cloudflare/mounts.js.map +1 -0
- package/dist/sandbox/cloudflare/mounts.mjs +143 -0
- package/dist/sandbox/cloudflare/mounts.mjs.map +1 -0
- package/dist/sandbox/cloudflare/sandbox.d.ts +91 -0
- package/dist/sandbox/cloudflare/sandbox.js +1163 -0
- package/dist/sandbox/cloudflare/sandbox.js.map +1 -0
- package/dist/sandbox/cloudflare/sandbox.mjs +1125 -0
- package/dist/sandbox/cloudflare/sandbox.mjs.map +1 -0
- package/dist/sandbox/daytona/index.d.ts +2 -0
- package/dist/sandbox/daytona/index.js +19 -0
- package/dist/sandbox/daytona/index.js.map +1 -0
- package/dist/sandbox/daytona/index.mjs +3 -0
- package/dist/sandbox/daytona/index.mjs.map +1 -0
- package/dist/sandbox/daytona/mounts.d.ts +11 -0
- package/dist/sandbox/daytona/mounts.js +17 -0
- package/dist/sandbox/daytona/mounts.js.map +1 -0
- package/dist/sandbox/daytona/mounts.mjs +12 -0
- package/dist/sandbox/daytona/mounts.mjs.map +1 -0
- package/dist/sandbox/daytona/sandbox.d.ts +160 -0
- package/dist/sandbox/daytona/sandbox.js +938 -0
- package/dist/sandbox/daytona/sandbox.js.map +1 -0
- package/dist/sandbox/daytona/sandbox.mjs +900 -0
- package/dist/sandbox/daytona/sandbox.mjs.map +1 -0
- package/dist/sandbox/e2b/index.d.ts +2 -0
- package/dist/sandbox/e2b/index.js +19 -0
- package/dist/sandbox/e2b/index.js.map +1 -0
- package/dist/sandbox/e2b/index.mjs +3 -0
- package/dist/sandbox/e2b/index.mjs.map +1 -0
- package/dist/sandbox/e2b/mounts.d.ts +11 -0
- package/dist/sandbox/e2b/mounts.js +17 -0
- package/dist/sandbox/e2b/mounts.js.map +1 -0
- package/dist/sandbox/e2b/mounts.mjs +12 -0
- package/dist/sandbox/e2b/mounts.mjs.map +1 -0
- package/dist/sandbox/e2b/sandbox.d.ts +169 -0
- package/dist/sandbox/e2b/sandbox.js +829 -0
- package/dist/sandbox/e2b/sandbox.js.map +1 -0
- package/dist/sandbox/e2b/sandbox.mjs +791 -0
- package/dist/sandbox/e2b/sandbox.mjs.map +1 -0
- package/dist/sandbox/modal/index.d.ts +2 -0
- package/dist/sandbox/modal/index.js +19 -0
- package/dist/sandbox/modal/index.js.map +1 -0
- package/dist/sandbox/modal/index.mjs +3 -0
- package/dist/sandbox/modal/index.mjs.map +1 -0
- package/dist/sandbox/modal/mounts.d.ts +24 -0
- package/dist/sandbox/modal/mounts.js +194 -0
- package/dist/sandbox/modal/mounts.js.map +1 -0
- package/dist/sandbox/modal/mounts.mjs +188 -0
- package/dist/sandbox/modal/mounts.mjs.map +1 -0
- package/dist/sandbox/modal/sandbox.d.ts +220 -0
- package/dist/sandbox/modal/sandbox.js +1621 -0
- package/dist/sandbox/modal/sandbox.js.map +1 -0
- package/dist/sandbox/modal/sandbox.mjs +1581 -0
- package/dist/sandbox/modal/sandbox.mjs.map +1 -0
- package/dist/sandbox/runloop/index.d.ts +2 -0
- package/dist/sandbox/runloop/index.js +19 -0
- package/dist/sandbox/runloop/index.js.map +1 -0
- package/dist/sandbox/runloop/index.mjs +3 -0
- package/dist/sandbox/runloop/index.mjs.map +1 -0
- package/dist/sandbox/runloop/mounts.d.ts +11 -0
- package/dist/sandbox/runloop/mounts.js +17 -0
- package/dist/sandbox/runloop/mounts.js.map +1 -0
- package/dist/sandbox/runloop/mounts.mjs +12 -0
- package/dist/sandbox/runloop/mounts.mjs.map +1 -0
- package/dist/sandbox/runloop/sandbox.d.ts +266 -0
- package/dist/sandbox/runloop/sandbox.js +1390 -0
- package/dist/sandbox/runloop/sandbox.js.map +1 -0
- package/dist/sandbox/runloop/sandbox.mjs +1346 -0
- package/dist/sandbox/runloop/sandbox.mjs.map +1 -0
- package/dist/sandbox/shared/archive.d.ts +34 -0
- package/dist/sandbox/shared/archive.js +397 -0
- package/dist/sandbox/shared/archive.js.map +1 -0
- package/dist/sandbox/shared/archive.mjs +389 -0
- package/dist/sandbox/shared/archive.mjs.map +1 -0
- package/dist/sandbox/shared/editor.d.ts +17 -0
- package/dist/sandbox/shared/editor.js +65 -0
- package/dist/sandbox/shared/editor.js.map +1 -0
- package/dist/sandbox/shared/editor.mjs +61 -0
- package/dist/sandbox/shared/editor.mjs.map +1 -0
- package/dist/sandbox/shared/environment.d.ts +1 -0
- package/dist/sandbox/shared/environment.js +13 -0
- package/dist/sandbox/shared/environment.js.map +1 -0
- package/dist/sandbox/shared/environment.mjs +2 -0
- package/dist/sandbox/shared/environment.mjs.map +1 -0
- package/dist/sandbox/shared/git.d.ts +4 -0
- package/dist/sandbox/shared/git.js +20 -0
- package/dist/sandbox/shared/git.js.map +1 -0
- package/dist/sandbox/shared/git.mjs +17 -0
- package/dist/sandbox/shared/git.mjs.map +1 -0
- package/dist/sandbox/shared/inContainerMounts.d.ts +40 -0
- package/dist/sandbox/shared/inContainerMounts.js +670 -0
- package/dist/sandbox/shared/inContainerMounts.js.map +1 -0
- package/dist/sandbox/shared/inContainerMounts.mjs +662 -0
- package/dist/sandbox/shared/inContainerMounts.mjs.map +1 -0
- package/dist/sandbox/shared/index.d.ts +23 -0
- package/dist/sandbox/shared/index.js +112 -0
- package/dist/sandbox/shared/index.js.map +1 -0
- package/dist/sandbox/shared/index.mjs +18 -0
- package/dist/sandbox/shared/index.mjs.map +1 -0
- package/dist/sandbox/shared/localSources.d.ts +8 -0
- package/dist/sandbox/shared/localSources.js +378 -0
- package/dist/sandbox/shared/localSources.js.map +1 -0
- package/dist/sandbox/shared/localSources.mjs +372 -0
- package/dist/sandbox/shared/localSources.mjs.map +1 -0
- package/dist/sandbox/shared/manifest.d.ts +38 -0
- package/dist/sandbox/shared/manifest.js +254 -0
- package/dist/sandbox/shared/manifest.js.map +1 -0
- package/dist/sandbox/shared/manifest.mjs +233 -0
- package/dist/sandbox/shared/manifest.mjs.map +1 -0
- package/dist/sandbox/shared/media.d.ts +1 -0
- package/dist/sandbox/shared/media.js +10 -0
- package/dist/sandbox/shared/media.js.map +1 -0
- package/dist/sandbox/shared/media.mjs +2 -0
- package/dist/sandbox/shared/media.mjs.map +1 -0
- package/dist/sandbox/shared/metadata.d.ts +7 -0
- package/dist/sandbox/shared/metadata.js +87 -0
- package/dist/sandbox/shared/metadata.js.map +1 -0
- package/dist/sandbox/shared/metadata.mjs +81 -0
- package/dist/sandbox/shared/metadata.mjs.map +1 -0
- package/dist/sandbox/shared/output.d.ts +1 -0
- package/dist/sandbox/shared/output.js +8 -0
- package/dist/sandbox/shared/output.js.map +1 -0
- package/dist/sandbox/shared/output.mjs +2 -0
- package/dist/sandbox/shared/output.mjs.map +1 -0
- package/dist/sandbox/shared/paths.d.ts +27 -0
- package/dist/sandbox/shared/paths.js +245 -0
- package/dist/sandbox/shared/paths.js.map +1 -0
- package/dist/sandbox/shared/paths.mjs +236 -0
- package/dist/sandbox/shared/paths.mjs.map +1 -0
- package/dist/sandbox/shared/ports.d.ts +14 -0
- package/dist/sandbox/shared/ports.js +75 -0
- package/dist/sandbox/shared/ports.js.map +1 -0
- package/dist/sandbox/shared/ports.mjs +69 -0
- package/dist/sandbox/shared/ports.mjs.map +1 -0
- package/dist/sandbox/shared/process.d.ts +16 -0
- package/dist/sandbox/shared/process.js +90 -0
- package/dist/sandbox/shared/process.js.map +1 -0
- package/dist/sandbox/shared/process.mjs +86 -0
- package/dist/sandbox/shared/process.mjs.map +1 -0
- package/dist/sandbox/shared/pty.d.ts +81 -0
- package/dist/sandbox/shared/pty.js +382 -0
- package/dist/sandbox/shared/pty.js.map +1 -0
- package/dist/sandbox/shared/pty.mjs +335 -0
- package/dist/sandbox/shared/pty.mjs.map +1 -0
- package/dist/sandbox/shared/runAs.d.ts +46 -0
- package/dist/sandbox/shared/runAs.js +149 -0
- package/dist/sandbox/shared/runAs.js.map +1 -0
- package/dist/sandbox/shared/runAs.mjs +141 -0
- package/dist/sandbox/shared/runAs.mjs.map +1 -0
- package/dist/sandbox/shared/session.d.ts +16 -0
- package/dist/sandbox/shared/session.js +138 -0
- package/dist/sandbox/shared/session.js.map +1 -0
- package/dist/sandbox/shared/session.mjs +128 -0
- package/dist/sandbox/shared/session.mjs.map +1 -0
- package/dist/sandbox/shared/sessionBase.d.ts +90 -0
- package/dist/sandbox/shared/sessionBase.js +446 -0
- package/dist/sandbox/shared/sessionBase.js.map +1 -0
- package/dist/sandbox/shared/sessionBase.mjs +442 -0
- package/dist/sandbox/shared/sessionBase.mjs.map +1 -0
- package/dist/sandbox/shared/sessionState.d.ts +7 -0
- package/dist/sandbox/shared/sessionState.js +35 -0
- package/dist/sandbox/shared/sessionState.js.map +1 -0
- package/dist/sandbox/shared/sessionState.mjs +31 -0
- package/dist/sandbox/shared/sessionState.mjs.map +1 -0
- package/dist/sandbox/shared/snapshots.d.ts +9 -0
- package/dist/sandbox/shared/snapshots.js +62 -0
- package/dist/sandbox/shared/snapshots.js.map +1 -0
- package/dist/sandbox/shared/snapshots.mjs +57 -0
- package/dist/sandbox/shared/snapshots.mjs.map +1 -0
- package/dist/sandbox/shared/typeGuards.d.ts +10 -0
- package/dist/sandbox/shared/typeGuards.js +50 -0
- package/dist/sandbox/shared/typeGuards.js.map +1 -0
- package/dist/sandbox/shared/typeGuards.mjs +38 -0
- package/dist/sandbox/shared/typeGuards.mjs.map +1 -0
- package/dist/sandbox/shared/types.d.ts +24 -0
- package/dist/sandbox/shared/types.js +3 -0
- package/dist/sandbox/shared/types.js.map +1 -0
- package/dist/sandbox/shared/types.mjs +2 -0
- package/dist/sandbox/shared/types.mjs.map +1 -0
- package/dist/sandbox/vercel/index.d.ts +1 -0
- package/dist/sandbox/vercel/index.js +18 -0
- package/dist/sandbox/vercel/index.js.map +1 -0
- package/dist/sandbox/vercel/index.mjs +2 -0
- package/dist/sandbox/vercel/index.mjs.map +1 -0
- package/dist/sandbox/vercel/sandbox.d.ts +135 -0
- package/dist/sandbox/vercel/sandbox.js +1001 -0
- package/dist/sandbox/vercel/sandbox.js.map +1 -0
- package/dist/sandbox/vercel/sandbox.mjs +963 -0
- package/dist/sandbox/vercel/sandbox.mjs.map +1 -0
- package/package.json +108 -4
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
import { UserError } from '@openai/agents-core';
|
|
2
|
+
import { loadEnv } from '@openai/agents-core/_shims';
|
|
3
|
+
import { SandboxArchiveError, SandboxMountError, SandboxProviderError, SandboxUnsupportedFeatureError, SandboxWorkspaceReadNotFoundError, normalizeSandboxClientCreateArgs, } from '@openai/agents-core/sandbox';
|
|
4
|
+
import { assertTarWorkspacePersistence, toWorkspaceArchiveBytes, validateWorkspaceTarArchive, } from "../shared/archive.mjs";
|
|
5
|
+
import { RemoteSandboxEditor } from "../shared/editor.mjs";
|
|
6
|
+
import { assertShellEnvironmentName, materializeEnvironment, } from "../shared/environment.mjs";
|
|
7
|
+
import { applyInlineManifestEntryToState, applyInlineManifestToState, entryContainsLocalSource, manifestContainsLocalSource, } from "../shared/manifest.mjs";
|
|
8
|
+
import { imageOutputFromBytes } from "../shared/media.mjs";
|
|
9
|
+
import { assertSandboxEntryMetadataSupported, assertSandboxManifestMetadataSupported, MOUNT_MANIFEST_METADATA_SUPPORT, } from "../shared/metadata.mjs";
|
|
10
|
+
import { elapsedSeconds, formatExecResponse, truncateOutput, } from "../shared/output.mjs";
|
|
11
|
+
import { assertConfiguredExposedPort } from "../shared/ports.mjs";
|
|
12
|
+
import { addPtyWebSocketListener, appendPtyOutput, createPtyProcessEntry, formatPtyExecUpdate, markPtyDone, openPtyWebSocket, PtyProcessRegistry, shellCommandForPty, writePtyStdin, } from "../shared/pty.mjs";
|
|
13
|
+
import { posixDirname, resolveSandboxWorkdir, shellQuote, validateRemoteSandboxPathForManifest, } from "../shared/paths.mjs";
|
|
14
|
+
import { assertCoreSnapshotUnsupported, closeRemoteSessionOnManifestError, withProviderError, withSandboxSpan, } from "../shared/session.mjs";
|
|
15
|
+
import { createRunAsRemoteEditor, manifestMaterializationOptionsWithRunAs, readRunAsRemoteFile, runAsRemotePathExists, sandboxUserShellCommand, } from "../shared/runAs.mjs";
|
|
16
|
+
import { deserializeRemoteSandboxSessionStateValues, serializeRemoteSandboxSessionState, } from "../shared/sessionState.mjs";
|
|
17
|
+
import { isRecord, readOptionalNumber, readOptionalNumberArray, readOptionalRecordArray, readString, } from "../shared/typeGuards.mjs";
|
|
18
|
+
import { buildCloudflareBucketMountConfig, cloudflareBucketMountRequestOptions, isCloudflareBucketMountEntry, } from "./mounts.mjs";
|
|
19
|
+
export class CloudflareSandboxSession {
|
|
20
|
+
state;
|
|
21
|
+
apiKey;
|
|
22
|
+
ptyProcesses = new PtyProcessRegistry();
|
|
23
|
+
concurrencyLimits;
|
|
24
|
+
remotePathResolver = async (path, options) => await this.resolveRemotePath(path, options);
|
|
25
|
+
constructor(args) {
|
|
26
|
+
this.state = {
|
|
27
|
+
...args.state,
|
|
28
|
+
sandboxId: normalizeCloudflarePersistedSandboxId(args.state.sandboxId),
|
|
29
|
+
};
|
|
30
|
+
this.apiKey = args.apiKey;
|
|
31
|
+
this.concurrencyLimits = args.concurrencyLimits;
|
|
32
|
+
}
|
|
33
|
+
createEditor(runAs) {
|
|
34
|
+
if (runAs) {
|
|
35
|
+
return createRunAsRemoteEditor({
|
|
36
|
+
providerName: 'CloudflareSandboxClient',
|
|
37
|
+
providerId: 'cloudflare',
|
|
38
|
+
runAs,
|
|
39
|
+
resolvePath: this.remotePathResolver,
|
|
40
|
+
runCommand: this.runAsCommandRunner.bind(this),
|
|
41
|
+
writer: this.writer(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return new RemoteSandboxEditor({
|
|
45
|
+
resolvePath: this.remotePathResolver,
|
|
46
|
+
pathExists: async (path) => await this.pathExists(path),
|
|
47
|
+
mkdir: async (path) => {
|
|
48
|
+
await this.mkdirPath(path, 'create directory');
|
|
49
|
+
},
|
|
50
|
+
readText: async (path) => new TextDecoder().decode(await this.readFileBytes(path)),
|
|
51
|
+
writeText: async (path, content) => {
|
|
52
|
+
await this.ensureParentDir(path);
|
|
53
|
+
await this.writeFile(path, new TextEncoder().encode(content));
|
|
54
|
+
},
|
|
55
|
+
deletePath: async (path) => {
|
|
56
|
+
const result = await this.execShell(`rm -f -- ${shellQuote(path)}`);
|
|
57
|
+
if (result.exitCode !== 0) {
|
|
58
|
+
throw new SandboxProviderError('CloudflareSandboxClient failed to delete path.', {
|
|
59
|
+
provider: 'cloudflare',
|
|
60
|
+
operation: 'delete path',
|
|
61
|
+
sandboxId: this.state.sandboxId,
|
|
62
|
+
path,
|
|
63
|
+
exitCode: result.exitCode,
|
|
64
|
+
output: result.output,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
supportsPty() {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
async execCommand(args) {
|
|
74
|
+
if (args.tty) {
|
|
75
|
+
return await this.execPtyCommand(args);
|
|
76
|
+
}
|
|
77
|
+
const start = Date.now();
|
|
78
|
+
const wrapped = sandboxUserShellCommand(buildShellCommand(args.cmd, this.state.environment, resolveSandboxWorkdir(this.state.manifest.root, args.workdir)), args.runAs);
|
|
79
|
+
const result = await this.execShell(wrapped);
|
|
80
|
+
const output = truncateOutput(result.output, args.maxOutputTokens);
|
|
81
|
+
return formatExecResponse({
|
|
82
|
+
output: output.text,
|
|
83
|
+
wallTimeSeconds: elapsedSeconds(start),
|
|
84
|
+
exitCode: result.exitCode,
|
|
85
|
+
originalTokenCount: output.originalTokenCount,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async writeStdin(args) {
|
|
89
|
+
return await writePtyStdin({
|
|
90
|
+
providerName: 'CloudflareSandboxClient',
|
|
91
|
+
registry: this.ptyProcesses,
|
|
92
|
+
sessionId: args.sessionId,
|
|
93
|
+
chars: args.chars,
|
|
94
|
+
yieldTimeMs: args.yieldTimeMs,
|
|
95
|
+
maxOutputTokens: args.maxOutputTokens,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async execPtyCommand(args) {
|
|
99
|
+
const start = Date.now();
|
|
100
|
+
const entry = createPtyProcessEntry({ tty: true });
|
|
101
|
+
let readyPromise = Promise.resolve();
|
|
102
|
+
let removeMessageListener = () => { };
|
|
103
|
+
const socket = await openPtyWebSocket({
|
|
104
|
+
url: buildCloudflarePtyWebSocketUrl(this.state),
|
|
105
|
+
providerName: 'CloudflareSandboxClient',
|
|
106
|
+
headers: buildCloudflarePtyWebSocketHeaders(this.apiKey),
|
|
107
|
+
headersUnsupportedUrl: buildCloudflarePtyWebSocketUrl(this.state, this.apiKey),
|
|
108
|
+
configure: (pendingSocket) => {
|
|
109
|
+
removeMessageListener = addPtyWebSocketListener(pendingSocket, 'message', (event) => handleCloudflarePtyMessage(entry, event));
|
|
110
|
+
readyPromise = waitForCloudflarePtyReady(pendingSocket);
|
|
111
|
+
readyPromise.catch(() => { });
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
const removeCloseListener = addPtyWebSocketListener(socket, 'close', () => {
|
|
115
|
+
if (!entry.done) {
|
|
116
|
+
markPtyDone(entry);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
const removeErrorListener = addPtyWebSocketListener(socket, 'error', () => {
|
|
120
|
+
if (!entry.done) {
|
|
121
|
+
markPtyDone(entry, 1);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
try {
|
|
125
|
+
await readyPromise;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
socket.close();
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
entry.sendInput = async (chars) => {
|
|
132
|
+
socket.send(new TextEncoder().encode(chars));
|
|
133
|
+
};
|
|
134
|
+
entry.terminate = async () => {
|
|
135
|
+
removeMessageListener();
|
|
136
|
+
removeCloseListener();
|
|
137
|
+
removeErrorListener();
|
|
138
|
+
socket.close();
|
|
139
|
+
};
|
|
140
|
+
let registered = false;
|
|
141
|
+
let sessionId;
|
|
142
|
+
try {
|
|
143
|
+
const command = shellCommandForPty({
|
|
144
|
+
...args,
|
|
145
|
+
cmd: sandboxUserShellCommand(buildShellCommand(args.cmd, this.state.environment, resolveSandboxWorkdir(this.state.manifest.root, args.workdir)), args.runAs),
|
|
146
|
+
});
|
|
147
|
+
await entry.sendInput(`${command}\n`);
|
|
148
|
+
const registeredProcess = this.ptyProcesses.register(entry);
|
|
149
|
+
registered = true;
|
|
150
|
+
sessionId = registeredProcess.sessionId;
|
|
151
|
+
if (registeredProcess.pruned) {
|
|
152
|
+
await registeredProcess.pruned.terminate?.().catch(() => { });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (!registered) {
|
|
157
|
+
await entry.terminate?.().catch(() => { });
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
return await formatPtyExecUpdate({
|
|
162
|
+
registry: this.ptyProcesses,
|
|
163
|
+
sessionId,
|
|
164
|
+
entry,
|
|
165
|
+
startTime: start,
|
|
166
|
+
yieldTimeMs: args.yieldTimeMs,
|
|
167
|
+
maxOutputTokens: args.maxOutputTokens,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async viewImage(args) {
|
|
171
|
+
const absolutePath = await this.resolveRemotePath(args.path);
|
|
172
|
+
const bytes = args.runAs
|
|
173
|
+
? await readRunAsRemoteFile({
|
|
174
|
+
providerName: 'CloudflareSandboxClient',
|
|
175
|
+
providerId: 'cloudflare',
|
|
176
|
+
path: absolutePath,
|
|
177
|
+
runAs: args.runAs,
|
|
178
|
+
runCommand: this.runAsCommandRunner.bind(this),
|
|
179
|
+
})
|
|
180
|
+
: await this.readFileBytes(absolutePath);
|
|
181
|
+
return imageOutputFromBytes(args.path, bytes);
|
|
182
|
+
}
|
|
183
|
+
async readFile(args) {
|
|
184
|
+
const absolutePath = await this.resolveRemotePath(args.path);
|
|
185
|
+
const bytes = args.runAs
|
|
186
|
+
? await readRunAsRemoteFile({
|
|
187
|
+
providerName: 'CloudflareSandboxClient',
|
|
188
|
+
providerId: 'cloudflare',
|
|
189
|
+
path: absolutePath,
|
|
190
|
+
runAs: args.runAs,
|
|
191
|
+
runCommand: this.runAsCommandRunner.bind(this),
|
|
192
|
+
})
|
|
193
|
+
: await this.readFileBytes(absolutePath);
|
|
194
|
+
if (typeof args.maxBytes === 'number' && bytes.byteLength > args.maxBytes) {
|
|
195
|
+
return bytes.subarray(0, args.maxBytes);
|
|
196
|
+
}
|
|
197
|
+
return bytes;
|
|
198
|
+
}
|
|
199
|
+
async pathExists(path, runAs) {
|
|
200
|
+
const absolutePath = await this.resolveRemotePath(path);
|
|
201
|
+
if (!runAs) {
|
|
202
|
+
const result = await this.execShell(`test -e ${shellQuote(absolutePath)}`);
|
|
203
|
+
return result.exitCode === 0;
|
|
204
|
+
}
|
|
205
|
+
return await runAsRemotePathExists(absolutePath, runAs, this.runAsCommandRunner.bind(this));
|
|
206
|
+
}
|
|
207
|
+
async running() {
|
|
208
|
+
const response = await this.fetch(`/v1/sandbox/${this.state.sandboxId}/running`, {
|
|
209
|
+
method: 'GET',
|
|
210
|
+
});
|
|
211
|
+
if (response.status === 404) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
throw cloudflareProviderHttpError('check running state', response, {
|
|
216
|
+
sandboxId: this.state.sandboxId,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const payload = (await response.json().catch(() => ({})));
|
|
220
|
+
return Boolean(payload.running);
|
|
221
|
+
}
|
|
222
|
+
async resolveExposedPort(port) {
|
|
223
|
+
const requestedPort = assertConfiguredExposedPort({
|
|
224
|
+
providerName: 'CloudflareSandboxClient',
|
|
225
|
+
port,
|
|
226
|
+
configuredPorts: this.state.configuredExposedPorts,
|
|
227
|
+
});
|
|
228
|
+
throw new SandboxUnsupportedFeatureError('CloudflareSandboxClient does not support exposed port resolution until the worker deployment exposes a port-resolution endpoint.', {
|
|
229
|
+
provider: 'cloudflare',
|
|
230
|
+
port: requestedPort,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
async materializeEntry(args) {
|
|
234
|
+
assertSandboxEntryMetadataSupported('CloudflareSandboxClient', args.path, args.entry, MOUNT_MANIFEST_METADATA_SUPPORT);
|
|
235
|
+
if (entryContainsLocalSource(args.entry)) {
|
|
236
|
+
const { applyLocalSourceManifestEntryToState } = await import("../shared/localSources.mjs");
|
|
237
|
+
await applyLocalSourceManifestEntryToState(this.state, args.path, args.entry, 'cloudflare', this.writer(), this.remotePathResolver, this.manifestMaterializationOptions(args.runAs));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
await applyInlineManifestEntryToState(this.state, args.path, args.entry, 'cloudflare', this.writer(), this.remotePathResolver, this.manifestMaterializationOptions(args.runAs));
|
|
241
|
+
}
|
|
242
|
+
async applyManifest(manifest, runAs) {
|
|
243
|
+
assertCloudflareManifestRoot(manifest);
|
|
244
|
+
assertSandboxManifestMetadataSupported('CloudflareSandboxClient', manifest, MOUNT_MANIFEST_METADATA_SUPPORT);
|
|
245
|
+
assertCloudflareManifestMountsSupported(manifest);
|
|
246
|
+
if (manifestContainsLocalSource(manifest)) {
|
|
247
|
+
const { applyLocalSourceManifestToState } = await import("../shared/localSources.mjs");
|
|
248
|
+
await applyLocalSourceManifestToState(this.state, manifest, 'cloudflare', this.writer(), this.remotePathResolver, this.manifestMaterializationOptions(runAs));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
await applyInlineManifestToState(this.state, manifest, 'cloudflare', this.writer(), this.remotePathResolver, this.manifestMaterializationOptions(runAs));
|
|
252
|
+
}
|
|
253
|
+
async mountBucket(args) {
|
|
254
|
+
const mountPath = await this.resolveRemotePath(args.mountPath, {
|
|
255
|
+
forWrite: true,
|
|
256
|
+
});
|
|
257
|
+
const response = await this.fetch(`/v1/sandbox/${this.state.sandboxId}/mount`, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: {
|
|
260
|
+
'Content-Type': 'application/json',
|
|
261
|
+
},
|
|
262
|
+
body: JSON.stringify({
|
|
263
|
+
bucket: args.bucket,
|
|
264
|
+
mountPath,
|
|
265
|
+
options: args.options,
|
|
266
|
+
}),
|
|
267
|
+
});
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
throw await cloudflareMountError({
|
|
270
|
+
response,
|
|
271
|
+
message: 'Cloudflare bucket mount failed.',
|
|
272
|
+
mountPath,
|
|
273
|
+
bucket: args.bucket,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async unmountBucket(mountPath) {
|
|
278
|
+
const resolvedMountPath = await this.resolveRemotePath(mountPath, {
|
|
279
|
+
forWrite: true,
|
|
280
|
+
});
|
|
281
|
+
const response = await this.fetch(`/v1/sandbox/${this.state.sandboxId}/unmount`, {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: {
|
|
284
|
+
'Content-Type': 'application/json',
|
|
285
|
+
},
|
|
286
|
+
body: JSON.stringify({
|
|
287
|
+
mountPath: resolvedMountPath,
|
|
288
|
+
}),
|
|
289
|
+
});
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
throw await cloudflareMountError({
|
|
292
|
+
response,
|
|
293
|
+
message: 'Cloudflare bucket unmount failed.',
|
|
294
|
+
mountPath: resolvedMountPath,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async persistWorkspace() {
|
|
299
|
+
assertTarWorkspacePersistence('CloudflareSandboxClient', 'tar');
|
|
300
|
+
const excludes = [...this.state.manifest.ephemeralPersistencePaths()]
|
|
301
|
+
.filter((path) => path.length > 0)
|
|
302
|
+
.sort((left, right) => left.localeCompare(right))
|
|
303
|
+
.join(',');
|
|
304
|
+
const response = await this.fetch(`/v1/sandbox/${this.state.sandboxId}/persist${excludes ? `?excludes=${encodeURIComponent(excludes)}` : ''}`, {
|
|
305
|
+
method: 'POST',
|
|
306
|
+
});
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
throw new SandboxArchiveError(`Cloudflare sandbox persist failed with HTTP ${response.status}.`, {
|
|
309
|
+
provider: 'cloudflare',
|
|
310
|
+
sandboxId: this.state.sandboxId,
|
|
311
|
+
status: response.status,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const archive = decodeStreamedPayload(new Uint8Array(await response.arrayBuffer()), response.headers);
|
|
315
|
+
validateWorkspaceTarArchive(archive);
|
|
316
|
+
return archive;
|
|
317
|
+
}
|
|
318
|
+
async hydrateWorkspace(data) {
|
|
319
|
+
assertTarWorkspacePersistence('CloudflareSandboxClient', 'tar');
|
|
320
|
+
const archive = toWorkspaceArchiveBytes(data);
|
|
321
|
+
validateWorkspaceTarArchive(archive);
|
|
322
|
+
const response = await this.fetch(`/v1/sandbox/${this.state.sandboxId}/hydrate`, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: {
|
|
325
|
+
'Content-Type': 'application/octet-stream',
|
|
326
|
+
},
|
|
327
|
+
body: toArrayBuffer(archive),
|
|
328
|
+
});
|
|
329
|
+
if (!response.ok) {
|
|
330
|
+
throw new SandboxArchiveError(`Cloudflare sandbox hydrate failed with HTTP ${response.status}.`, {
|
|
331
|
+
provider: 'cloudflare',
|
|
332
|
+
sandboxId: this.state.sandboxId,
|
|
333
|
+
status: response.status,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async close() {
|
|
338
|
+
await this.ptyProcesses.terminateAll();
|
|
339
|
+
await withSandboxSpan('sandbox.stop', {
|
|
340
|
+
backend_id: 'cloudflare',
|
|
341
|
+
sandbox_id: this.state.sandboxId,
|
|
342
|
+
}, async () => {
|
|
343
|
+
const response = await this.fetch(`/v1/sandbox/${this.state.sandboxId}`, {
|
|
344
|
+
method: 'DELETE',
|
|
345
|
+
});
|
|
346
|
+
if (!response.ok && response.status !== 404) {
|
|
347
|
+
throw cloudflareProviderHttpError('delete sandbox', response, {
|
|
348
|
+
sandboxId: this.state.sandboxId,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async shutdown() {
|
|
354
|
+
await this.close();
|
|
355
|
+
}
|
|
356
|
+
async delete() {
|
|
357
|
+
await this.close();
|
|
358
|
+
}
|
|
359
|
+
writer() {
|
|
360
|
+
return {
|
|
361
|
+
mkdir: async (path) => {
|
|
362
|
+
await this.mkdirPath(path, 'create directory');
|
|
363
|
+
},
|
|
364
|
+
writeFile: async (path, content) => {
|
|
365
|
+
await this.ensureParentDir(path);
|
|
366
|
+
await this.writeFile(path, typeof content === 'string'
|
|
367
|
+
? new TextEncoder().encode(content)
|
|
368
|
+
: content);
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
manifestMaterializationOptions(runAs) {
|
|
373
|
+
return manifestMaterializationOptionsWithRunAs({
|
|
374
|
+
providerName: 'CloudflareSandboxClient',
|
|
375
|
+
providerId: 'cloudflare',
|
|
376
|
+
runAs,
|
|
377
|
+
runCommand: this.runAsCommandRunner.bind(this),
|
|
378
|
+
options: {
|
|
379
|
+
materializeMount: this.materializeMountEntry.bind(this),
|
|
380
|
+
concurrencyLimits: this.concurrencyLimits,
|
|
381
|
+
},
|
|
382
|
+
support: MOUNT_MANIFEST_METADATA_SUPPORT,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async materializeMountEntry(absolutePath, entry) {
|
|
386
|
+
if (!isCloudflareBucketMountEntry(entry)) {
|
|
387
|
+
throw new SandboxUnsupportedFeatureError('CloudflareSandboxClient only supports CloudflareBucketMountStrategy mount entries.', {
|
|
388
|
+
provider: 'cloudflare',
|
|
389
|
+
feature: 'entry.mountStrategy',
|
|
390
|
+
path: absolutePath,
|
|
391
|
+
mountType: entry.type,
|
|
392
|
+
strategyType: entry.mountStrategy?.type,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const config = buildCloudflareBucketMountConfig(entry);
|
|
396
|
+
await this.mountBucket({
|
|
397
|
+
bucket: config.bucketName,
|
|
398
|
+
mountPath: entry.mountPath ?? absolutePath,
|
|
399
|
+
options: cloudflareBucketMountRequestOptions(config),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
async execShell(shellCommand) {
|
|
403
|
+
const response = await this.fetch(`/v1/sandbox/${this.state.sandboxId}/exec`, {
|
|
404
|
+
method: 'POST',
|
|
405
|
+
headers: {
|
|
406
|
+
'Content-Type': 'application/json',
|
|
407
|
+
},
|
|
408
|
+
body: JSON.stringify({
|
|
409
|
+
argv: ['/bin/sh', '-lc', shellCommand],
|
|
410
|
+
...(typeof this.state.timeouts?.execTimeoutMs === 'number' ||
|
|
411
|
+
typeof this.state.timeoutMs === 'number'
|
|
412
|
+
? {
|
|
413
|
+
timeout_ms: this.state.timeouts?.execTimeoutMs ?? this.state.timeoutMs,
|
|
414
|
+
}
|
|
415
|
+
: {}),
|
|
416
|
+
}),
|
|
417
|
+
});
|
|
418
|
+
if (!response.ok) {
|
|
419
|
+
throw cloudflareProviderHttpError('execute command', response, {
|
|
420
|
+
sandboxId: this.state.sandboxId,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (!response.body) {
|
|
424
|
+
throw new SandboxProviderError('CloudflareSandboxClient failed to execute command.', {
|
|
425
|
+
provider: 'cloudflare',
|
|
426
|
+
operation: 'execute command',
|
|
427
|
+
sandboxId: this.state.sandboxId,
|
|
428
|
+
cause: 'missing response body',
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const decoder = new TextDecoder();
|
|
432
|
+
const reader = response.body.getReader();
|
|
433
|
+
let buffer = '';
|
|
434
|
+
let stdout = '';
|
|
435
|
+
let stderr = '';
|
|
436
|
+
let exitCode;
|
|
437
|
+
const processEvent = (event) => {
|
|
438
|
+
if (event.event === 'stdout') {
|
|
439
|
+
stdout += decodeBase64Text(event.data);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (event.event === 'stderr') {
|
|
443
|
+
stderr += decodeBase64Text(event.data);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (event.event === 'exit') {
|
|
447
|
+
try {
|
|
448
|
+
const payload = JSON.parse(event.data);
|
|
449
|
+
exitCode =
|
|
450
|
+
typeof payload.exit_code === 'number' &&
|
|
451
|
+
Number.isFinite(payload.exit_code)
|
|
452
|
+
? Math.trunc(payload.exit_code)
|
|
453
|
+
: 1;
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
exitCode = 1;
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (event.event === 'error') {
|
|
461
|
+
throw new SandboxProviderError('CloudflareSandboxClient failed to execute command.', {
|
|
462
|
+
provider: 'cloudflare',
|
|
463
|
+
operation: 'execute command',
|
|
464
|
+
sandboxId: this.state.sandboxId,
|
|
465
|
+
cause: event.data || 'unknown error',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
while (true) {
|
|
470
|
+
const chunk = await reader.read();
|
|
471
|
+
if (chunk.done) {
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
475
|
+
const { events, remaining } = splitCompleteSseEvents(buffer);
|
|
476
|
+
buffer = remaining;
|
|
477
|
+
for (const part of events) {
|
|
478
|
+
const event = parseSseEvent(part);
|
|
479
|
+
if (!event) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
processEvent(event);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
buffer += decoder.decode();
|
|
486
|
+
for (const part of collectSseEvents(buffer)) {
|
|
487
|
+
const event = parseSseEvent(part);
|
|
488
|
+
if (event) {
|
|
489
|
+
processEvent(event);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
exitCode: exitCode ?? 1,
|
|
494
|
+
output: [stdout.trimEnd(), stderr.trimEnd()]
|
|
495
|
+
.filter((value) => value.length > 0)
|
|
496
|
+
.join('\n'),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
async runAsCommandRunner(command, options = {}) {
|
|
500
|
+
const result = await this.execShell(sandboxUserShellCommand(command, options.runAs));
|
|
501
|
+
return {
|
|
502
|
+
status: result.exitCode,
|
|
503
|
+
stdout: result.output,
|
|
504
|
+
stderr: '',
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async readFileBytes(path) {
|
|
508
|
+
const response = await this.fetch(`/v1/sandbox/${this.state.sandboxId}/file/${encodeSandboxPath(path)}`, {
|
|
509
|
+
method: 'GET',
|
|
510
|
+
});
|
|
511
|
+
if (response.status === 404) {
|
|
512
|
+
throw new SandboxWorkspaceReadNotFoundError(`Cloudflare sandbox path not found: ${path}.`, {
|
|
513
|
+
provider: 'cloudflare',
|
|
514
|
+
sandboxId: this.state.sandboxId,
|
|
515
|
+
path,
|
|
516
|
+
status: response.status,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (!response.ok) {
|
|
520
|
+
throw cloudflareProviderHttpError('read file', response, {
|
|
521
|
+
sandboxId: this.state.sandboxId,
|
|
522
|
+
path,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
526
|
+
return decodeStreamedPayload(bytes, response.headers);
|
|
527
|
+
}
|
|
528
|
+
async writeFile(path, content) {
|
|
529
|
+
const response = await this.fetch(`/v1/sandbox/${this.state.sandboxId}/file/${encodeSandboxPath(path)}`, {
|
|
530
|
+
method: 'PUT',
|
|
531
|
+
headers: {
|
|
532
|
+
'Content-Type': 'application/octet-stream',
|
|
533
|
+
},
|
|
534
|
+
body: toArrayBuffer(content),
|
|
535
|
+
});
|
|
536
|
+
if (!response.ok) {
|
|
537
|
+
throw cloudflareProviderHttpError('write file', response, {
|
|
538
|
+
sandboxId: this.state.sandboxId,
|
|
539
|
+
path,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async ensureParentDir(path) {
|
|
544
|
+
const parent = posixDirname(path);
|
|
545
|
+
if (parent !== '.' && parent !== '/') {
|
|
546
|
+
await this.mkdirPath(parent, 'create parent directory');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async mkdirPath(path, operation) {
|
|
550
|
+
const result = await this.execShell(`mkdir -p -- ${shellQuote(path)}`);
|
|
551
|
+
if (result.exitCode !== 0) {
|
|
552
|
+
throw new SandboxProviderError('CloudflareSandboxClient failed to create directory.', {
|
|
553
|
+
provider: 'cloudflare',
|
|
554
|
+
operation,
|
|
555
|
+
sandboxId: this.state.sandboxId,
|
|
556
|
+
path,
|
|
557
|
+
exitCode: result.exitCode,
|
|
558
|
+
output: result.output,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async fetch(path, init, timeoutMs) {
|
|
563
|
+
const headers = new Headers(init.headers ?? {});
|
|
564
|
+
const apiKey = this.apiKey ?? loadEnv().CLOUDFLARE_SANDBOX_API_KEY;
|
|
565
|
+
if (apiKey) {
|
|
566
|
+
headers.set('Authorization', `Bearer ${apiKey}`);
|
|
567
|
+
}
|
|
568
|
+
const requestTimeoutMs = timeoutMs ??
|
|
569
|
+
this.state.timeouts?.requestTimeoutMs ??
|
|
570
|
+
this.state.requestTimeoutMs;
|
|
571
|
+
let timeoutId;
|
|
572
|
+
let controller;
|
|
573
|
+
if (typeof requestTimeoutMs === 'number' && !init.signal) {
|
|
574
|
+
controller = new AbortController();
|
|
575
|
+
timeoutId = setTimeout(() => {
|
|
576
|
+
controller?.abort();
|
|
577
|
+
}, requestTimeoutMs);
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
return await withProviderError('CloudflareSandboxClient', 'cloudflare', 'request worker', async () => await fetch(`${this.state.workerUrl}${path}`, {
|
|
581
|
+
...init,
|
|
582
|
+
headers,
|
|
583
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
584
|
+
}), {
|
|
585
|
+
sandboxId: this.state.sandboxId,
|
|
586
|
+
path,
|
|
587
|
+
method: init.method ?? 'GET',
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
if (timeoutId) {
|
|
592
|
+
clearTimeout(timeoutId);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async resolveRemotePath(path, options = {}) {
|
|
597
|
+
return await validateRemoteSandboxPathForManifest({
|
|
598
|
+
manifest: this.state.manifest,
|
|
599
|
+
path,
|
|
600
|
+
options,
|
|
601
|
+
runCommand: async (command) => {
|
|
602
|
+
const result = await this.execShell(command);
|
|
603
|
+
return {
|
|
604
|
+
status: result.exitCode,
|
|
605
|
+
stdout: result.output,
|
|
606
|
+
stderr: '',
|
|
607
|
+
};
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* @see {@link https://developers.cloudflare.com/sandbox/ | Sandbox SDK overview}.
|
|
614
|
+
* @see {@link https://developers.cloudflare.com/sandbox/get-started/ | Getting started}.
|
|
615
|
+
* @see {@link https://developers.cloudflare.com/sandbox/concepts/architecture/ | Sandbox architecture}.
|
|
616
|
+
*/
|
|
617
|
+
export class CloudflareSandboxClient {
|
|
618
|
+
backendId = 'cloudflare';
|
|
619
|
+
options;
|
|
620
|
+
constructor(options = {}) {
|
|
621
|
+
this.options = options;
|
|
622
|
+
}
|
|
623
|
+
async create(args, manifestOptions) {
|
|
624
|
+
const createArgs = normalizeSandboxClientCreateArgs(args, manifestOptions);
|
|
625
|
+
assertCoreSnapshotUnsupported('CloudflareSandboxClient', createArgs.snapshot);
|
|
626
|
+
const manifest = createArgs.manifest;
|
|
627
|
+
const resolvedOptions = {
|
|
628
|
+
...this.options,
|
|
629
|
+
...createArgs.options,
|
|
630
|
+
};
|
|
631
|
+
const timeouts = resolveCloudflareTimeouts(resolvedOptions);
|
|
632
|
+
const workerUrl = resolvedOptions.workerUrl;
|
|
633
|
+
if (!workerUrl) {
|
|
634
|
+
throw new UserError('CloudflareSandboxClient requires options.workerUrl.');
|
|
635
|
+
}
|
|
636
|
+
assertCloudflareManifestRoot(manifest);
|
|
637
|
+
assertSandboxManifestMetadataSupported('CloudflareSandboxClient', manifest, MOUNT_MANIFEST_METADATA_SUPPORT);
|
|
638
|
+
assertCloudflareManifestMountsSupported(manifest);
|
|
639
|
+
return await withSandboxSpan('sandbox.start', {
|
|
640
|
+
backend_id: this.backendId,
|
|
641
|
+
}, async () => {
|
|
642
|
+
const environment = await materializeEnvironment(manifest);
|
|
643
|
+
const headers = new Headers();
|
|
644
|
+
headers.set('Content-Type', 'application/json');
|
|
645
|
+
const apiKey = resolvedOptions.apiKey ?? loadEnv().CLOUDFLARE_SANDBOX_API_KEY;
|
|
646
|
+
if (apiKey) {
|
|
647
|
+
headers.set('Authorization', `Bearer ${apiKey}`);
|
|
648
|
+
}
|
|
649
|
+
const normalizedWorkerUrl = normalizeCloudflareWorkerUrl(workerUrl, 'options.workerUrl');
|
|
650
|
+
const response = await withProviderError('CloudflareSandboxClient', 'cloudflare', 'create sandbox', async () => await fetchWithOptionalTimeout(`${normalizedWorkerUrl}/v1/sandbox`, {
|
|
651
|
+
method: 'POST',
|
|
652
|
+
headers,
|
|
653
|
+
body: JSON.stringify({
|
|
654
|
+
...(resolvedOptions.exposedPorts
|
|
655
|
+
? { exposedPorts: resolvedOptions.exposedPorts }
|
|
656
|
+
: {}),
|
|
657
|
+
...(typeof timeouts.execTimeoutMs === 'number'
|
|
658
|
+
? { timeoutMs: timeouts.execTimeoutMs }
|
|
659
|
+
: {}),
|
|
660
|
+
...(typeof timeouts.createTimeoutMs === 'number'
|
|
661
|
+
? { createTimeoutMs: timeouts.createTimeoutMs }
|
|
662
|
+
: {}),
|
|
663
|
+
...(resolvedOptions.mounts
|
|
664
|
+
? { mounts: resolvedOptions.mounts }
|
|
665
|
+
: {}),
|
|
666
|
+
}),
|
|
667
|
+
}, timeouts.createTimeoutMs ?? timeouts.requestTimeoutMs), { workerUrl: normalizedWorkerUrl });
|
|
668
|
+
if (!response.ok) {
|
|
669
|
+
throw cloudflareProviderHttpError('create sandbox', response, {
|
|
670
|
+
workerUrl: normalizedWorkerUrl,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
const payload = await withProviderError('CloudflareSandboxClient', 'cloudflare', 'parse create response', async () => (await response.json()), { workerUrl: normalizedWorkerUrl });
|
|
674
|
+
const sandboxId = normalizeCloudflareCreatedSandboxId(payload.id, normalizedWorkerUrl);
|
|
675
|
+
const session = new CloudflareSandboxSession({
|
|
676
|
+
apiKey,
|
|
677
|
+
concurrencyLimits: createArgs.concurrencyLimits,
|
|
678
|
+
state: {
|
|
679
|
+
manifest,
|
|
680
|
+
workerUrl: normalizedWorkerUrl,
|
|
681
|
+
sandboxId,
|
|
682
|
+
configuredExposedPorts: resolvedOptions.exposedPorts,
|
|
683
|
+
timeoutMs: resolvedOptions.timeoutMs,
|
|
684
|
+
createTimeoutMs: timeouts.createTimeoutMs,
|
|
685
|
+
requestTimeoutMs: timeouts.requestTimeoutMs,
|
|
686
|
+
timeouts,
|
|
687
|
+
mounts: resolvedOptions.mounts,
|
|
688
|
+
environment,
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
try {
|
|
692
|
+
await session.applyManifest(manifest);
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
await closeRemoteSessionOnManifestError('Cloudflare', session, error);
|
|
696
|
+
}
|
|
697
|
+
return session;
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
async serializeSessionState(state) {
|
|
701
|
+
return serializeRemoteSandboxSessionState(state);
|
|
702
|
+
}
|
|
703
|
+
canPersistOwnedSessionState() {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
async deserializeSessionState(state) {
|
|
707
|
+
const baseState = deserializeRemoteSandboxSessionStateValues(state);
|
|
708
|
+
return {
|
|
709
|
+
...state,
|
|
710
|
+
...baseState,
|
|
711
|
+
workerUrl: readString(state, 'workerUrl'),
|
|
712
|
+
sandboxId: readString(state, 'sandboxId'),
|
|
713
|
+
configuredExposedPorts: readOptionalNumberArray(state.configuredExposedPorts),
|
|
714
|
+
timeoutMs: readOptionalNumber(state, 'timeoutMs'),
|
|
715
|
+
createTimeoutMs: readOptionalNumber(state, 'createTimeoutMs'),
|
|
716
|
+
requestTimeoutMs: readOptionalNumber(state, 'requestTimeoutMs'),
|
|
717
|
+
timeouts: resolveCloudflareTimeouts({
|
|
718
|
+
timeoutMs: readOptionalNumber(state, 'timeoutMs'),
|
|
719
|
+
createTimeoutMs: readOptionalNumber(state, 'createTimeoutMs'),
|
|
720
|
+
requestTimeoutMs: readOptionalNumber(state, 'requestTimeoutMs'),
|
|
721
|
+
timeouts: isRecord(state.timeouts)
|
|
722
|
+
? state.timeouts
|
|
723
|
+
: undefined,
|
|
724
|
+
}),
|
|
725
|
+
mounts: readOptionalRecordArray(state.mounts),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
async resume(state) {
|
|
729
|
+
const sandboxId = normalizeCloudflarePersistedSandboxId(state.sandboxId);
|
|
730
|
+
const workerUrl = normalizeCloudflareWorkerUrl(state.workerUrl, 'persisted workerUrl');
|
|
731
|
+
assertCloudflareManifestRoot(state.manifest);
|
|
732
|
+
assertSandboxManifestMetadataSupported('CloudflareSandboxClient', state.manifest, MOUNT_MANIFEST_METADATA_SUPPORT);
|
|
733
|
+
assertCloudflareManifestMountsSupported(state.manifest);
|
|
734
|
+
const session = new CloudflareSandboxSession({
|
|
735
|
+
state: {
|
|
736
|
+
...state,
|
|
737
|
+
sandboxId,
|
|
738
|
+
workerUrl,
|
|
739
|
+
},
|
|
740
|
+
apiKey: this.options.apiKey ?? loadEnv().CLOUDFLARE_SANDBOX_API_KEY,
|
|
741
|
+
});
|
|
742
|
+
if (!(await session.running())) {
|
|
743
|
+
throw new UserError(`Cloudflare sandbox ${sandboxId} is no longer running.`);
|
|
744
|
+
}
|
|
745
|
+
return session;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function buildShellCommand(command, environment, cwd) {
|
|
749
|
+
const exports = Object.entries(environment).map(([key, value]) => {
|
|
750
|
+
assertShellEnvironmentName(key);
|
|
751
|
+
return `export ${key}=${shellQuote(value)}`;
|
|
752
|
+
});
|
|
753
|
+
return [`cd ${shellQuote(cwd)}`, ...exports, command].join(' && ');
|
|
754
|
+
}
|
|
755
|
+
function encodeSandboxPath(path) {
|
|
756
|
+
return path.replace(/^\//, '').split('/').map(encodeURIComponent).join('/');
|
|
757
|
+
}
|
|
758
|
+
function resolveCloudflareTimeouts(options) {
|
|
759
|
+
return {
|
|
760
|
+
...options.timeouts,
|
|
761
|
+
execTimeoutMs: options.timeouts?.execTimeoutMs ?? options.timeoutMs,
|
|
762
|
+
createTimeoutMs: options.timeouts?.createTimeoutMs ?? options.createTimeoutMs,
|
|
763
|
+
requestTimeoutMs: options.timeouts?.requestTimeoutMs ?? options.requestTimeoutMs,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function normalizeCloudflareWorkerUrl(workerUrl, source) {
|
|
767
|
+
let url;
|
|
768
|
+
try {
|
|
769
|
+
url = new URL(workerUrl);
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
throw new UserError(`Cloudflare sandbox ${source} must be an absolute http(s) URL.`);
|
|
773
|
+
}
|
|
774
|
+
if ((url.protocol !== 'https:' && url.protocol !== 'http:') ||
|
|
775
|
+
url.search ||
|
|
776
|
+
url.hash) {
|
|
777
|
+
throw new UserError(`Cloudflare sandbox ${source} must be an absolute http(s) URL.`);
|
|
778
|
+
}
|
|
779
|
+
return url.href.replace(/\/$/u, '');
|
|
780
|
+
}
|
|
781
|
+
const CLOUDFLARE_SANDBOX_ID_PATTERN = /^[A-Za-z0-9_-]+$/u;
|
|
782
|
+
function normalizeCloudflareCreatedSandboxId(sandboxId, workerUrl) {
|
|
783
|
+
if (isValidCloudflareSandboxId(sandboxId)) {
|
|
784
|
+
return sandboxId;
|
|
785
|
+
}
|
|
786
|
+
throw new SandboxProviderError('CloudflareSandboxClient failed to create sandbox.', {
|
|
787
|
+
provider: 'cloudflare',
|
|
788
|
+
operation: 'create sandbox',
|
|
789
|
+
workerUrl,
|
|
790
|
+
cause: 'invalid sandbox id',
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
function normalizeCloudflarePersistedSandboxId(sandboxId) {
|
|
794
|
+
if (sandboxId === undefined || sandboxId === null || sandboxId === '') {
|
|
795
|
+
throw new UserError('Cloudflare sandbox resume requires a persisted sandboxId.');
|
|
796
|
+
}
|
|
797
|
+
if (isValidCloudflareSandboxId(sandboxId)) {
|
|
798
|
+
return sandboxId;
|
|
799
|
+
}
|
|
800
|
+
throw new UserError('Cloudflare sandbox persisted sandboxId must be a safe path segment.');
|
|
801
|
+
}
|
|
802
|
+
function isValidCloudflareSandboxId(sandboxId) {
|
|
803
|
+
return (typeof sandboxId === 'string' &&
|
|
804
|
+
CLOUDFLARE_SANDBOX_ID_PATTERN.test(sandboxId));
|
|
805
|
+
}
|
|
806
|
+
async function fetchWithOptionalTimeout(input, init, timeoutMs) {
|
|
807
|
+
let timeoutId;
|
|
808
|
+
let controller;
|
|
809
|
+
if (typeof timeoutMs === 'number' && !init.signal) {
|
|
810
|
+
controller = new AbortController();
|
|
811
|
+
timeoutId = setTimeout(() => {
|
|
812
|
+
controller?.abort();
|
|
813
|
+
}, timeoutMs);
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
return await fetch(input, {
|
|
817
|
+
...init,
|
|
818
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
finally {
|
|
822
|
+
if (timeoutId) {
|
|
823
|
+
clearTimeout(timeoutId);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function buildCloudflarePtyWebSocketUrl(state, apiKey) {
|
|
828
|
+
const base = state.workerUrl.replace(/\/$/u, '');
|
|
829
|
+
const wsBase = base.replace(/^https:/u, 'wss:').replace(/^http:/u, 'ws:');
|
|
830
|
+
const url = new URL(`${wsBase}/v1/sandbox/${state.sandboxId}/pty`);
|
|
831
|
+
url.searchParams.set('cols', '80');
|
|
832
|
+
url.searchParams.set('rows', '24');
|
|
833
|
+
if (apiKey) {
|
|
834
|
+
url.searchParams.set('authorization', `Bearer ${apiKey}`);
|
|
835
|
+
}
|
|
836
|
+
return url.toString();
|
|
837
|
+
}
|
|
838
|
+
function buildCloudflarePtyWebSocketHeaders(apiKey) {
|
|
839
|
+
if (apiKey) {
|
|
840
|
+
return {
|
|
841
|
+
Authorization: `Bearer ${apiKey}`,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
function assertCloudflareManifestRoot(manifest) {
|
|
847
|
+
if (manifest.root !== '/workspace') {
|
|
848
|
+
throw new UserError('Cloudflare sandboxes currently require manifest.root="/workspace".');
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function assertCloudflareManifestMountsSupported(manifest) {
|
|
852
|
+
for (const { entry, mountPath } of manifest.mountTargets()) {
|
|
853
|
+
if (!isCloudflareBucketMountEntry(entry)) {
|
|
854
|
+
throw new SandboxUnsupportedFeatureError('CloudflareSandboxClient only supports CloudflareBucketMountStrategy mount entries.', {
|
|
855
|
+
provider: 'cloudflare',
|
|
856
|
+
feature: 'entry.mountStrategy',
|
|
857
|
+
path: mountPath,
|
|
858
|
+
mountType: entry.type,
|
|
859
|
+
strategyType: entry.mountStrategy?.type,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
buildCloudflareBucketMountConfig(entry);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function cloudflareMountError(args) {
|
|
866
|
+
const payload = (await args.response.json().catch(() => ({})));
|
|
867
|
+
return new SandboxMountError(args.message, {
|
|
868
|
+
provider: 'cloudflare',
|
|
869
|
+
mountPath: args.mountPath,
|
|
870
|
+
...(args.bucket ? { bucket: args.bucket } : {}),
|
|
871
|
+
status: args.response.status,
|
|
872
|
+
reason: typeof payload.error === 'string'
|
|
873
|
+
? payload.error
|
|
874
|
+
: `HTTP ${args.response.status}`,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
function cloudflareProviderHttpError(operation, response, context = {}) {
|
|
878
|
+
return new SandboxProviderError(`CloudflareSandboxClient failed to ${operation}.`, {
|
|
879
|
+
provider: 'cloudflare',
|
|
880
|
+
operation,
|
|
881
|
+
status: response.status,
|
|
882
|
+
...context,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
function waitForCloudflarePtyReady(socket) {
|
|
886
|
+
return new Promise((resolve, reject) => {
|
|
887
|
+
let removeMessage = () => { };
|
|
888
|
+
let removeClose = () => { };
|
|
889
|
+
let removeError = () => { };
|
|
890
|
+
const timer = setTimeout(() => {
|
|
891
|
+
cleanup();
|
|
892
|
+
reject(new UserError('CloudflareSandboxClient PTY ready timed out.'));
|
|
893
|
+
}, 30_000);
|
|
894
|
+
const cleanup = () => {
|
|
895
|
+
clearTimeout(timer);
|
|
896
|
+
removeMessage();
|
|
897
|
+
removeClose();
|
|
898
|
+
removeError();
|
|
899
|
+
};
|
|
900
|
+
removeMessage = addPtyWebSocketListener(socket, 'message', (event) => {
|
|
901
|
+
const payload = parseCloudflarePtyControlPayload(event);
|
|
902
|
+
if (isRecord(payload)) {
|
|
903
|
+
if (payload.type === 'ready') {
|
|
904
|
+
cleanup();
|
|
905
|
+
resolve();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (payload.type === 'error') {
|
|
909
|
+
cleanup();
|
|
910
|
+
reject(new UserError(cloudflarePtyReadyErrorMessage(payload)));
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
removeClose = addPtyWebSocketListener(socket, 'close', () => {
|
|
915
|
+
cleanup();
|
|
916
|
+
reject(new UserError('CloudflareSandboxClient PTY WebSocket closed before ready.'));
|
|
917
|
+
});
|
|
918
|
+
removeError = addPtyWebSocketListener(socket, 'error', () => {
|
|
919
|
+
cleanup();
|
|
920
|
+
reject(new UserError('CloudflareSandboxClient PTY WebSocket failed.'));
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
function cloudflarePtyReadyErrorMessage(payload) {
|
|
925
|
+
const message = payload.message;
|
|
926
|
+
return typeof message === 'string' && message.length > 0
|
|
927
|
+
? `CloudflareSandboxClient PTY failed before ready: ${message}`
|
|
928
|
+
: 'CloudflareSandboxClient PTY failed before ready.';
|
|
929
|
+
}
|
|
930
|
+
function handleCloudflarePtyMessage(entry, event) {
|
|
931
|
+
const payload = parseCloudflarePtyControlPayload(event);
|
|
932
|
+
if (isRecord(payload)) {
|
|
933
|
+
if (payload.type === 'ready') {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (payload.type === 'exit') {
|
|
937
|
+
markPtyDone(entry, typeof payload.code === 'number' ? payload.code : null);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (payload.type === 'error') {
|
|
941
|
+
const message = payload.message;
|
|
942
|
+
if (typeof message === 'string') {
|
|
943
|
+
appendPtyOutput(entry, message);
|
|
944
|
+
}
|
|
945
|
+
if (!entry.done) {
|
|
946
|
+
markPtyDone(entry, 1);
|
|
947
|
+
}
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const data = webSocketEventData(event);
|
|
952
|
+
if (typeof data === 'string') {
|
|
953
|
+
appendPtyOutput(entry, data);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (data instanceof ArrayBuffer) {
|
|
957
|
+
appendPtyOutput(entry, data);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (ArrayBuffer.isView(data)) {
|
|
961
|
+
appendPtyOutput(entry, new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function parseCloudflarePtyControlPayload(event) {
|
|
965
|
+
const text = webSocketEventText(event);
|
|
966
|
+
if (!text) {
|
|
967
|
+
return undefined;
|
|
968
|
+
}
|
|
969
|
+
try {
|
|
970
|
+
return JSON.parse(text);
|
|
971
|
+
}
|
|
972
|
+
catch {
|
|
973
|
+
return undefined;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function parseSseEvent(chunk) {
|
|
977
|
+
const lines = chunk
|
|
978
|
+
.split(/\r?\n/)
|
|
979
|
+
.map((line) => line.trimEnd())
|
|
980
|
+
.filter((line) => line.length > 0);
|
|
981
|
+
if (lines.length === 0) {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
let event = 'message';
|
|
985
|
+
const data = [];
|
|
986
|
+
for (const line of lines) {
|
|
987
|
+
if (line.startsWith('event:')) {
|
|
988
|
+
event = line.slice('event:'.length).trim();
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (line.startsWith('data:')) {
|
|
992
|
+
data.push(line.slice('data:'.length).trim());
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
event,
|
|
997
|
+
data: data.join('\n'),
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
function splitCompleteSseEvents(buffer) {
|
|
1001
|
+
const events = [];
|
|
1002
|
+
const delimiterPattern = /\r\n\r\n|\n\n|\r\r/gu;
|
|
1003
|
+
let start = 0;
|
|
1004
|
+
let match;
|
|
1005
|
+
while ((match = delimiterPattern.exec(buffer))) {
|
|
1006
|
+
events.push(buffer.slice(start, match.index));
|
|
1007
|
+
start = match.index + match[0].length;
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
events,
|
|
1011
|
+
remaining: buffer.slice(start),
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
function collectSseEvents(text) {
|
|
1015
|
+
const { events, remaining } = splitCompleteSseEvents(text);
|
|
1016
|
+
if (remaining.trim().length > 0) {
|
|
1017
|
+
events.push(remaining);
|
|
1018
|
+
}
|
|
1019
|
+
return events;
|
|
1020
|
+
}
|
|
1021
|
+
function decodeBase64Text(value) {
|
|
1022
|
+
return new TextDecoder().decode(decodeBase64Bytes(value));
|
|
1023
|
+
}
|
|
1024
|
+
const STREAMED_PAYLOAD_SSE_SNIFF_BYTES = 256;
|
|
1025
|
+
function decodeStreamedPayload(body, headers) {
|
|
1026
|
+
if (!shouldParseStreamedPayloadAsSse(body, headers)) {
|
|
1027
|
+
return body;
|
|
1028
|
+
}
|
|
1029
|
+
const text = new TextDecoder().decode(body);
|
|
1030
|
+
if (!startsWithSseFrame(text)) {
|
|
1031
|
+
return body;
|
|
1032
|
+
}
|
|
1033
|
+
const events = collectSseEvents(text);
|
|
1034
|
+
const chunkData = [];
|
|
1035
|
+
let isBinary = false;
|
|
1036
|
+
for (const rawEvent of events) {
|
|
1037
|
+
const event = parseSseEvent(rawEvent);
|
|
1038
|
+
if (!event) {
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (event.event === 'metadata') {
|
|
1042
|
+
try {
|
|
1043
|
+
const payload = JSON.parse(event.data);
|
|
1044
|
+
isBinary = Boolean(payload.isBinary);
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
// Ignore malformed metadata and fall back to treating the payload as text.
|
|
1048
|
+
}
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
if (event.event === 'chunk') {
|
|
1052
|
+
chunkData.push(event.data);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (!isBinary || chunkData.length === 0) {
|
|
1056
|
+
return body;
|
|
1057
|
+
}
|
|
1058
|
+
let chunks;
|
|
1059
|
+
try {
|
|
1060
|
+
chunks = chunkData.map((data) => decodeBase64Bytes(data));
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
throw new SandboxProviderError('CloudflareSandboxClient received an invalid binary stream payload.', {
|
|
1064
|
+
provider: 'cloudflare',
|
|
1065
|
+
operation: 'decode streamed payload',
|
|
1066
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
const size = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
1070
|
+
const merged = new Uint8Array(size);
|
|
1071
|
+
let offset = 0;
|
|
1072
|
+
for (const chunk of chunks) {
|
|
1073
|
+
merged.set(chunk, offset);
|
|
1074
|
+
offset += chunk.byteLength;
|
|
1075
|
+
}
|
|
1076
|
+
return merged;
|
|
1077
|
+
}
|
|
1078
|
+
function shouldParseStreamedPayloadAsSse(body, headers) {
|
|
1079
|
+
const contentType = headers?.get('Content-Type')?.toLowerCase() ?? '';
|
|
1080
|
+
if (contentType.split(';', 1)[0]?.trim() === 'text/event-stream') {
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
const prefix = body.subarray(0, Math.min(body.byteLength, STREAMED_PAYLOAD_SSE_SNIFF_BYTES));
|
|
1084
|
+
return startsWithSseFrame(new TextDecoder().decode(prefix));
|
|
1085
|
+
}
|
|
1086
|
+
function startsWithSseFrame(text) {
|
|
1087
|
+
const trimmed = text.trimStart();
|
|
1088
|
+
return (trimmed.startsWith(':') ||
|
|
1089
|
+
trimmed.startsWith('data:') ||
|
|
1090
|
+
trimmed.startsWith('event:') ||
|
|
1091
|
+
trimmed.startsWith('id:') ||
|
|
1092
|
+
trimmed.startsWith('retry:'));
|
|
1093
|
+
}
|
|
1094
|
+
function decodeBase64Bytes(value) {
|
|
1095
|
+
const binary = atob(value.replace(/\s+/gu, ''));
|
|
1096
|
+
const bytes = new Uint8Array(binary.length);
|
|
1097
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
1098
|
+
bytes[index] = binary.charCodeAt(index);
|
|
1099
|
+
}
|
|
1100
|
+
return bytes;
|
|
1101
|
+
}
|
|
1102
|
+
function toArrayBuffer(bytes) {
|
|
1103
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
1104
|
+
copy.set(bytes);
|
|
1105
|
+
return copy.buffer;
|
|
1106
|
+
}
|
|
1107
|
+
function webSocketEventData(event) {
|
|
1108
|
+
return event && typeof event === 'object' && 'data' in event
|
|
1109
|
+
? event.data
|
|
1110
|
+
: event;
|
|
1111
|
+
}
|
|
1112
|
+
function webSocketEventText(event) {
|
|
1113
|
+
const data = webSocketEventData(event);
|
|
1114
|
+
if (typeof data === 'string') {
|
|
1115
|
+
return data;
|
|
1116
|
+
}
|
|
1117
|
+
if (data instanceof ArrayBuffer) {
|
|
1118
|
+
return new TextDecoder().decode(data);
|
|
1119
|
+
}
|
|
1120
|
+
if (ArrayBuffer.isView(data)) {
|
|
1121
|
+
return new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
1122
|
+
}
|
|
1123
|
+
return undefined;
|
|
1124
|
+
}
|
|
1125
|
+
//# sourceMappingURL=sandbox.mjs.map
|