@openai/agents-extensions 0.8.5 → 0.9.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/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,1581 @@
|
|
|
1
|
+
import { UserError } from '@openai/agents-core';
|
|
2
|
+
import { SandboxProviderError, SandboxUnsupportedFeatureError, normalizeSandboxClientCreateArgs, isMount, } from '@openai/agents-core/sandbox';
|
|
3
|
+
import { normalizePosixPath, relativePosixPathWithinRoot, } from '@openai/agents-core/sandbox/internal';
|
|
4
|
+
import { posix as pathPosix } from 'node:path';
|
|
5
|
+
import { assertCoreSnapshotUnsupported, imageOutputFromBytes, RemoteSandboxEditor, assertTarWorkspacePersistence, assertSandboxEntryMetadataSupported, assertSandboxManifestMetadataSupported, MOUNT_MANIFEST_METADATA_SUPPORT, closeRemoteSessionOnManifestError, createRunAsRemoteEditor, decodeNativeSnapshotRef, deserializeRemoteSandboxSessionStateValues, elapsedSeconds, encodeNativeSnapshotRef, formatExecResponse, hydrateRemoteWorkspaceTar, manifestMaterializationOptionsWithRunAs, materializeEnvironment, persistRemoteWorkspaceTar, assertConfiguredExposedPort, getCachedExposedPortEndpoint, recordResolvedExposedPortEndpoint, resolveSandboxAbsolutePath, resolveSandboxRelativePath, resolveSandboxWorkdir, cloneManifestWithoutMountEntries, readRunAsRemoteFile, runAsRemotePathExists, sandboxUserShellCommand, serializeRemoteSandboxSessionState, truncateOutput, validateRemoteSandboxPathForManifest, readOptionalNumber, readOptionalNumberArray, readOptionalString, readString, withProviderError, withSandboxSpan, } from "../shared/index.mjs";
|
|
6
|
+
import { applyLocalSourceManifestEntryToState, applyLocalSourceManifestToState, } from "../shared/localSources.mjs";
|
|
7
|
+
import { buildModalCloudBucketMountConfig, isModalCloudBucketMountEntry, } from "./mounts.mjs";
|
|
8
|
+
const DEFAULT_MODAL_IMAGE_TAG = 'debian:bookworm-slim';
|
|
9
|
+
const DEFAULT_EXEC_YIELD_TIME_MS = 10_000;
|
|
10
|
+
const DEFAULT_WRITE_STDIN_YIELD_TIME_MS = 250;
|
|
11
|
+
const MAX_ACTIVE_PROCESS_OUTPUT_CHARS = 1024 * 1024;
|
|
12
|
+
const COMPLETED_ACTIVE_PROCESS_RETENTION_MS = 60_000;
|
|
13
|
+
export class ModalImageSelector {
|
|
14
|
+
kind;
|
|
15
|
+
value;
|
|
16
|
+
constructor(kind, value) {
|
|
17
|
+
this.kind = kind;
|
|
18
|
+
this.value = value;
|
|
19
|
+
}
|
|
20
|
+
static fromImage(image) {
|
|
21
|
+
return new ModalImageSelector('image', image);
|
|
22
|
+
}
|
|
23
|
+
static fromId(imageId) {
|
|
24
|
+
return new ModalImageSelector('id', imageId);
|
|
25
|
+
}
|
|
26
|
+
static fromTag(imageTag) {
|
|
27
|
+
return new ModalImageSelector('tag', imageTag);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ModalSandboxSelector {
|
|
31
|
+
kind;
|
|
32
|
+
value;
|
|
33
|
+
constructor(kind, value) {
|
|
34
|
+
this.kind = kind;
|
|
35
|
+
this.value = value;
|
|
36
|
+
}
|
|
37
|
+
static fromSandbox(sandbox) {
|
|
38
|
+
return new ModalSandboxSelector('sandbox', sandbox);
|
|
39
|
+
}
|
|
40
|
+
static fromId(sandboxId) {
|
|
41
|
+
return new ModalSandboxSelector('id', sandboxId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class ModalSandboxSession {
|
|
45
|
+
state;
|
|
46
|
+
modal;
|
|
47
|
+
app;
|
|
48
|
+
cloudBucketMountsProvider;
|
|
49
|
+
cloudBucketMounts;
|
|
50
|
+
cloudBucketMountsResolved = false;
|
|
51
|
+
sandbox;
|
|
52
|
+
ownsSandbox;
|
|
53
|
+
closed = false;
|
|
54
|
+
closePromise;
|
|
55
|
+
activeProcesses = new Map();
|
|
56
|
+
remotePathResolver = async (path, options) => await this.resolveRemotePath(path, options);
|
|
57
|
+
concurrencyLimits;
|
|
58
|
+
nextProcessId = 1;
|
|
59
|
+
constructor(args) {
|
|
60
|
+
this.state = args.state;
|
|
61
|
+
this.modal = args.modal;
|
|
62
|
+
this.app = args.app;
|
|
63
|
+
this.sandbox = args.sandbox;
|
|
64
|
+
this.ownsSandbox = args.ownsSandbox ?? args.state.ownsSandbox ?? true;
|
|
65
|
+
this.state.ownsSandbox = this.ownsSandbox;
|
|
66
|
+
this.cloudBucketMounts = args.cloudBucketMounts;
|
|
67
|
+
this.cloudBucketMountsProvider = args.cloudBucketMountsProvider;
|
|
68
|
+
this.concurrencyLimits = args.concurrencyLimits;
|
|
69
|
+
this.cloudBucketMountsResolved =
|
|
70
|
+
args.cloudBucketMounts !== undefined || !args.cloudBucketMountsProvider;
|
|
71
|
+
}
|
|
72
|
+
createEditor(runAs) {
|
|
73
|
+
if (runAs) {
|
|
74
|
+
return createRunAsRemoteEditor({
|
|
75
|
+
providerName: 'ModalSandboxClient',
|
|
76
|
+
providerId: 'modal',
|
|
77
|
+
runAs,
|
|
78
|
+
resolvePath: this.remotePathResolver,
|
|
79
|
+
runCommand: this.runAsCommandRunner.bind(this),
|
|
80
|
+
writer: this.writer(),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return new RemoteSandboxEditor({
|
|
84
|
+
resolvePath: this.remotePathResolver,
|
|
85
|
+
pathExists: async (path) => await this.pathExists(path),
|
|
86
|
+
readText: async (path) => new TextDecoder().decode(await this.readSandboxFile(path)),
|
|
87
|
+
writeText: async (path, content) => {
|
|
88
|
+
await this.writeSandboxFile(path, content);
|
|
89
|
+
},
|
|
90
|
+
deletePath: async (path) => {
|
|
91
|
+
await this.removeSandboxPath(path);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
supportsPty() {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
async execCommand(args) {
|
|
99
|
+
const start = Date.now();
|
|
100
|
+
const command = buildShellCommand({
|
|
101
|
+
...args,
|
|
102
|
+
cmd: sandboxUserShellCommand(args.cmd, args.runAs),
|
|
103
|
+
});
|
|
104
|
+
const process = await this.sandbox.exec(command, {
|
|
105
|
+
mode: 'text',
|
|
106
|
+
workdir: this.resolveWorkdir(args.workdir),
|
|
107
|
+
env: this.state.environment,
|
|
108
|
+
pty: args.tty ?? false,
|
|
109
|
+
stdout: 'pipe',
|
|
110
|
+
stderr: 'pipe',
|
|
111
|
+
});
|
|
112
|
+
const activeProcess = createActiveProcess(process);
|
|
113
|
+
await waitForProcessOrTimeout(activeProcess, args.yieldTimeMs ?? DEFAULT_EXEC_YIELD_TIME_MS);
|
|
114
|
+
if (activeProcess.done) {
|
|
115
|
+
const output = truncateOutput(consumeCommandOutput(activeProcess), args.maxOutputTokens);
|
|
116
|
+
return formatExecResponse({
|
|
117
|
+
output: output.text,
|
|
118
|
+
wallTimeSeconds: elapsedSeconds(start),
|
|
119
|
+
exitCode: activeProcess.exitCode ?? 1,
|
|
120
|
+
originalTokenCount: output.originalTokenCount,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const sessionId = this.nextProcessId++;
|
|
124
|
+
this.registerActiveProcess(sessionId, activeProcess);
|
|
125
|
+
const output = truncateOutput(consumeCommandOutput(activeProcess), args.maxOutputTokens);
|
|
126
|
+
return formatExecResponse({
|
|
127
|
+
output: output.text,
|
|
128
|
+
wallTimeSeconds: elapsedSeconds(start),
|
|
129
|
+
sessionId,
|
|
130
|
+
originalTokenCount: output.originalTokenCount,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async writeStdin(args) {
|
|
134
|
+
const activeProcess = this.activeProcesses.get(args.sessionId);
|
|
135
|
+
if (!activeProcess) {
|
|
136
|
+
return formatExecResponse({
|
|
137
|
+
output: `write_stdin failed: session not found: ${args.sessionId}`,
|
|
138
|
+
wallTimeSeconds: 0,
|
|
139
|
+
exitCode: 1,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const start = Date.now();
|
|
143
|
+
const chars = args.chars ?? '';
|
|
144
|
+
if (chars.length > 0) {
|
|
145
|
+
if (!activeProcess.process.stdin?.writeText) {
|
|
146
|
+
throw new UserError('Modal sandbox process does not expose stdin for write_stdin().');
|
|
147
|
+
}
|
|
148
|
+
await activeProcess.process.stdin.writeText(chars);
|
|
149
|
+
}
|
|
150
|
+
await waitForProcessOrTimeout(activeProcess, args.yieldTimeMs ?? DEFAULT_WRITE_STDIN_YIELD_TIME_MS);
|
|
151
|
+
const output = truncateOutput(consumeCommandOutput(activeProcess), args.maxOutputTokens);
|
|
152
|
+
if (activeProcess.done) {
|
|
153
|
+
this.deleteActiveProcess(args.sessionId, activeProcess);
|
|
154
|
+
return formatExecResponse({
|
|
155
|
+
output: output.text,
|
|
156
|
+
wallTimeSeconds: elapsedSeconds(start),
|
|
157
|
+
exitCode: activeProcess.exitCode ?? 1,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return formatExecResponse({
|
|
161
|
+
output: output.text,
|
|
162
|
+
wallTimeSeconds: elapsedSeconds(start),
|
|
163
|
+
sessionId: args.sessionId,
|
|
164
|
+
originalTokenCount: output.originalTokenCount,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async viewImage(args) {
|
|
168
|
+
const absolutePath = await this.resolveRemotePath(args.path);
|
|
169
|
+
const bytes = args.runAs
|
|
170
|
+
? await readRunAsRemoteFile({
|
|
171
|
+
providerName: 'ModalSandboxClient',
|
|
172
|
+
providerId: 'modal',
|
|
173
|
+
path: absolutePath,
|
|
174
|
+
runAs: args.runAs,
|
|
175
|
+
runCommand: this.runAsCommandRunner.bind(this),
|
|
176
|
+
})
|
|
177
|
+
: await this.readSandboxFile(absolutePath);
|
|
178
|
+
return imageOutputFromBytes(args.path, bytes);
|
|
179
|
+
}
|
|
180
|
+
async readFile(args) {
|
|
181
|
+
const absolutePath = await this.resolveRemotePath(args.path);
|
|
182
|
+
const bytes = args.runAs
|
|
183
|
+
? await readRunAsRemoteFile({
|
|
184
|
+
providerName: 'ModalSandboxClient',
|
|
185
|
+
providerId: 'modal',
|
|
186
|
+
path: absolutePath,
|
|
187
|
+
runAs: args.runAs,
|
|
188
|
+
runCommand: this.runAsCommandRunner.bind(this),
|
|
189
|
+
})
|
|
190
|
+
: await this.readSandboxFile(absolutePath);
|
|
191
|
+
if (typeof args.maxBytes === 'number' && bytes.byteLength > args.maxBytes) {
|
|
192
|
+
return bytes.subarray(0, args.maxBytes);
|
|
193
|
+
}
|
|
194
|
+
return bytes;
|
|
195
|
+
}
|
|
196
|
+
async pathExists(path, runAs) {
|
|
197
|
+
const absolutePath = await this.resolveRemotePath(path);
|
|
198
|
+
if (!runAs) {
|
|
199
|
+
const process = await this.sandbox.exec(['test', '-e', absolutePath], {
|
|
200
|
+
mode: 'text',
|
|
201
|
+
workdir: this.state.manifest.root,
|
|
202
|
+
stdout: 'ignore',
|
|
203
|
+
stderr: 'pipe',
|
|
204
|
+
});
|
|
205
|
+
return (await process.wait()) === 0;
|
|
206
|
+
}
|
|
207
|
+
return await runAsRemotePathExists(absolutePath, runAs, this.runAsCommandRunner.bind(this));
|
|
208
|
+
}
|
|
209
|
+
async running() {
|
|
210
|
+
try {
|
|
211
|
+
return (await this.sandbox.poll()) === null;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async resolveExposedPort(port) {
|
|
218
|
+
const requestedPort = assertConfiguredExposedPort({
|
|
219
|
+
providerName: 'ModalSandboxClient',
|
|
220
|
+
port,
|
|
221
|
+
configuredPorts: this.state.configuredExposedPorts,
|
|
222
|
+
});
|
|
223
|
+
const cached = getCachedExposedPortEndpoint(this.state, requestedPort);
|
|
224
|
+
if (cached) {
|
|
225
|
+
return cached;
|
|
226
|
+
}
|
|
227
|
+
if (!this.sandbox.tunnels) {
|
|
228
|
+
throw new SandboxProviderError('ModalSandboxClient exposed port resolution requires Modal sandbox tunnels support.', {
|
|
229
|
+
provider: 'modal',
|
|
230
|
+
port: requestedPort,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
let tunnels;
|
|
234
|
+
try {
|
|
235
|
+
tunnels = await this.sandbox.tunnels(10_000);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
throw new SandboxProviderError(`ModalSandboxClient failed to look up tunnels for exposed port ${requestedPort}.`, {
|
|
239
|
+
provider: 'modal',
|
|
240
|
+
port: requestedPort,
|
|
241
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const tunnel = tunnels[requestedPort];
|
|
245
|
+
if (typeof tunnel?.host !== 'string' ||
|
|
246
|
+
tunnel.host.length === 0 ||
|
|
247
|
+
typeof tunnel.port !== 'number') {
|
|
248
|
+
throw new SandboxProviderError(`ModalSandboxClient did not expose port ${requestedPort}.`, {
|
|
249
|
+
provider: 'modal',
|
|
250
|
+
port: requestedPort,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return recordResolvedExposedPortEndpoint(this.state, requestedPort, {
|
|
254
|
+
host: tunnel.host,
|
|
255
|
+
port: tunnel.port,
|
|
256
|
+
tls: true,
|
|
257
|
+
query: '',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
async materializeEntry(args) {
|
|
261
|
+
assertSandboxEntryMetadataSupported('ModalSandboxClient', args.path, args.entry, MOUNT_MANIFEST_METADATA_SUPPORT);
|
|
262
|
+
assertModalLiveEntryMountsUnsupported(args.entry, args.path, this.state.manifest);
|
|
263
|
+
await applyLocalSourceManifestEntryToState(this.state, args.path, args.entry, 'modal', this.writer(), this.remotePathResolver, this.manifestMaterializationOptions(args.runAs));
|
|
264
|
+
this.invalidateCloudBucketMounts();
|
|
265
|
+
}
|
|
266
|
+
async applyManifest(manifest, runAs) {
|
|
267
|
+
assertSandboxManifestMetadataSupported('ModalSandboxClient', manifest, MOUNT_MANIFEST_METADATA_SUPPORT);
|
|
268
|
+
assertModalLiveManifestMountsUnsupported(manifest, this.state.manifest);
|
|
269
|
+
await applyLocalSourceManifestToState(this.state, manifest, 'modal', this.writer(), this.remotePathResolver, this.manifestMaterializationOptions(runAs));
|
|
270
|
+
this.invalidateCloudBucketMounts();
|
|
271
|
+
}
|
|
272
|
+
async persistWorkspace() {
|
|
273
|
+
if (this.state.workspacePersistence === 'snapshot_filesystem') {
|
|
274
|
+
const archive = await this.persistSnapshotFilesystem();
|
|
275
|
+
if (archive) {
|
|
276
|
+
return archive;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else if (this.state.workspacePersistence === 'snapshot_directory') {
|
|
280
|
+
const archive = await this.persistSnapshotDirectory();
|
|
281
|
+
if (archive) {
|
|
282
|
+
return archive;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
assertTarWorkspacePersistence('ModalSandboxClient', this.state.workspacePersistence);
|
|
287
|
+
}
|
|
288
|
+
return await persistRemoteWorkspaceTar({
|
|
289
|
+
providerName: 'ModalSandboxClient',
|
|
290
|
+
manifest: this.state.manifest,
|
|
291
|
+
io: this.archiveIo(),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async hydrateWorkspace(data) {
|
|
295
|
+
const snapshotRef = decodeNativeSnapshotRef(data);
|
|
296
|
+
if (snapshotRef?.provider === 'modal_snapshot_filesystem') {
|
|
297
|
+
this.assertExpectedSnapshotPersistence(snapshotRef.workspacePersistence ?? 'snapshot_filesystem', 'snapshot_filesystem');
|
|
298
|
+
await this.restoreSnapshotFilesystem(snapshotRef.snapshotId);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (snapshotRef?.provider === 'modal_snapshot_directory') {
|
|
302
|
+
this.assertExpectedSnapshotPersistence(snapshotRef.workspacePersistence ?? 'snapshot_directory', 'snapshot_directory');
|
|
303
|
+
await this.restoreSnapshotDirectory(snapshotRef.snapshotId);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (this.state.workspacePersistence === 'tar') {
|
|
307
|
+
assertTarWorkspacePersistence('ModalSandboxClient', this.state.workspacePersistence);
|
|
308
|
+
}
|
|
309
|
+
await hydrateRemoteWorkspaceTar({
|
|
310
|
+
providerName: 'ModalSandboxClient',
|
|
311
|
+
manifest: this.state.manifest,
|
|
312
|
+
io: this.archiveIo(),
|
|
313
|
+
data,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async persistSnapshotFilesystem() {
|
|
317
|
+
if (this.nativeSnapshotRequiresTarFallback()) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
if (!this.sandbox.snapshotFilesystem) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
const snapshotId = snapshotIdFromModalResult(await withProviderError('ModalSandboxClient', 'modal', 'capture snapshot_filesystem', async () => await this.sandbox.snapshotFilesystem(this.state.snapshotFilesystemTimeoutMs), { sandboxId: this.state.sandboxId }));
|
|
324
|
+
if (!snapshotId) {
|
|
325
|
+
throw new SandboxProviderError('Modal snapshot_filesystem persistence did not return a snapshot id.', {
|
|
326
|
+
provider: 'modal',
|
|
327
|
+
sandboxId: this.state.sandboxId,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return encodeNativeSnapshotRef({
|
|
331
|
+
provider: 'modal_snapshot_filesystem',
|
|
332
|
+
snapshotId,
|
|
333
|
+
workspacePersistence: 'snapshot_filesystem',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async persistSnapshotDirectory() {
|
|
337
|
+
if (this.nativeSnapshotRequiresTarFallback()) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
if (!this.sandbox.snapshotDirectory) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
const snapshotId = snapshotIdFromModalResult(await withProviderError('ModalSandboxClient', 'modal', 'capture snapshot_directory', async () => await withOptionalTimeoutMs(this.sandbox.snapshotDirectory(this.state.manifest.root), this.state.snapshotFilesystemTimeoutMs, 'Modal snapshot_directory persistence timed out.', async (snapshot) => {
|
|
344
|
+
await this.deleteSnapshotDirectoryImage(snapshot);
|
|
345
|
+
}), { sandboxId: this.state.sandboxId }));
|
|
346
|
+
if (!snapshotId) {
|
|
347
|
+
throw new SandboxProviderError('Modal snapshot_directory persistence did not return a snapshot id.', {
|
|
348
|
+
provider: 'modal',
|
|
349
|
+
sandboxId: this.state.sandboxId,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return encodeNativeSnapshotRef({
|
|
353
|
+
provider: 'modal_snapshot_directory',
|
|
354
|
+
snapshotId,
|
|
355
|
+
workspacePersistence: 'snapshot_directory',
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
nativeSnapshotRequiresTarFallback() {
|
|
359
|
+
return this.state.manifest.ephemeralPersistencePaths().size > 0;
|
|
360
|
+
}
|
|
361
|
+
assertExpectedSnapshotPersistence(actual, expected) {
|
|
362
|
+
if (actual !== expected || this.state.workspacePersistence !== expected) {
|
|
363
|
+
throw new SandboxProviderError(`Modal snapshot reference uses ${actual}, but this session expects ${this.state.workspacePersistence}.`, {
|
|
364
|
+
provider: 'modal',
|
|
365
|
+
workspacePersistence: this.state.workspacePersistence,
|
|
366
|
+
snapshotWorkspacePersistence: actual,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async restoreSnapshotFilesystem(snapshotId) {
|
|
371
|
+
const previousSandbox = this.sandbox;
|
|
372
|
+
const previousSandboxId = this.state.sandboxId;
|
|
373
|
+
const previousSandboxOwned = this.ownsSandbox;
|
|
374
|
+
const sandbox = await withProviderError('ModalSandboxClient', 'modal', 'restore snapshot_filesystem', async () => await withOptionalTimeoutMs((async () => {
|
|
375
|
+
const image = maybeSetModalSandboxCmd(await this.modalImageFromId(snapshotId), this.state.useSleepCmd);
|
|
376
|
+
return await this.modal.sandboxes.create(this.app, image, await this.sandboxCreateParams());
|
|
377
|
+
})(), this.state.snapshotFilesystemRestoreTimeoutMs, 'Modal snapshot_filesystem restore timed out.', terminateModalSandboxAfterTimeout), { sandboxId: previousSandboxId, snapshotId });
|
|
378
|
+
if (previousSandboxOwned) {
|
|
379
|
+
try {
|
|
380
|
+
await previousSandbox.terminate();
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
let replacementTerminateCause;
|
|
384
|
+
try {
|
|
385
|
+
await sandbox.terminate();
|
|
386
|
+
}
|
|
387
|
+
catch (replacementTerminateError) {
|
|
388
|
+
replacementTerminateCause = errorMessage(replacementTerminateError);
|
|
389
|
+
}
|
|
390
|
+
throw new SandboxProviderError('Modal snapshot_filesystem restore created a replacement sandbox, but terminating the previous sandbox failed.', {
|
|
391
|
+
provider: 'modal',
|
|
392
|
+
sandboxId: previousSandboxId,
|
|
393
|
+
replacementSandboxId: sandbox.sandboxId,
|
|
394
|
+
cause: errorMessage(error),
|
|
395
|
+
...(replacementTerminateCause ? { replacementTerminateCause } : {}),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
await this.resetActiveProcesses();
|
|
400
|
+
this.sandbox = sandbox;
|
|
401
|
+
this.ownsSandbox = true;
|
|
402
|
+
this.state.sandboxId = sandbox.sandboxId;
|
|
403
|
+
this.state.ownsSandbox = true;
|
|
404
|
+
delete this.state.exposedPorts;
|
|
405
|
+
}
|
|
406
|
+
async restoreSnapshotDirectory(snapshotId) {
|
|
407
|
+
if (!this.sandbox.mountImage) {
|
|
408
|
+
throw new SandboxProviderError('Modal snapshot_directory restore requires sandbox mountImage(path, image) support.', {
|
|
409
|
+
provider: 'modal',
|
|
410
|
+
sandboxId: this.state.sandboxId,
|
|
411
|
+
snapshotId,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
const sandbox = this.sandbox;
|
|
415
|
+
const ownsSandbox = this.ownsSandbox;
|
|
416
|
+
await withProviderError('ModalSandboxClient', 'modal', 'restore snapshot_directory', async () => {
|
|
417
|
+
if (!ownsSandbox) {
|
|
418
|
+
const image = await withOptionalTimeoutMs(this.modalImageFromId(snapshotId), this.state.snapshotFilesystemRestoreTimeoutMs, 'Modal snapshot_directory restore timed out.');
|
|
419
|
+
await sandbox.mountImage(this.state.manifest.root, image);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await withOptionalTimeoutMs((async () => {
|
|
423
|
+
const image = await this.modalImageFromId(snapshotId);
|
|
424
|
+
await sandbox.mountImage(this.state.manifest.root, image);
|
|
425
|
+
})(), this.state.snapshotFilesystemRestoreTimeoutMs, 'Modal snapshot_directory restore timed out.', async () => {
|
|
426
|
+
await terminateModalSandboxAfterTimeout(sandbox);
|
|
427
|
+
});
|
|
428
|
+
}, { sandboxId: this.state.sandboxId, snapshotId });
|
|
429
|
+
}
|
|
430
|
+
async deleteSnapshotDirectoryImage(snapshot) {
|
|
431
|
+
const snapshotId = snapshotIdFromModalResult(snapshot);
|
|
432
|
+
if (!snapshotId || !this.modal.images.delete) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
await this.modal.images.delete(snapshotId).catch(() => { });
|
|
436
|
+
}
|
|
437
|
+
async modalImageFromId(snapshotId) {
|
|
438
|
+
return this.modal.images.fromId
|
|
439
|
+
? await withProviderError('ModalSandboxClient', 'modal', 'look up image', async () => await this.modal.images.fromId(snapshotId), { imageId: snapshotId })
|
|
440
|
+
: { imageId: snapshotId };
|
|
441
|
+
}
|
|
442
|
+
async sandboxCreateParams() {
|
|
443
|
+
const cloudBucketMounts = await this.resolveCloudBucketMounts();
|
|
444
|
+
return {
|
|
445
|
+
workdir: this.state.manifest.root,
|
|
446
|
+
env: this.state.environment,
|
|
447
|
+
...(typeof this.state.timeoutMs === 'number'
|
|
448
|
+
? { timeoutMs: this.state.timeoutMs }
|
|
449
|
+
: {}),
|
|
450
|
+
...(typeof this.state.idleTimeoutMs === 'number'
|
|
451
|
+
? { idleTimeoutMs: this.state.idleTimeoutMs }
|
|
452
|
+
: {}),
|
|
453
|
+
...(this.state.gpu ? { gpu: this.state.gpu } : {}),
|
|
454
|
+
...(this.state.configuredExposedPorts
|
|
455
|
+
? { encryptedPorts: this.state.configuredExposedPorts }
|
|
456
|
+
: {}),
|
|
457
|
+
...(cloudBucketMounts ? { cloudBucketMounts } : {}),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
async resolveCloudBucketMounts() {
|
|
461
|
+
if (this.cloudBucketMountsResolved) {
|
|
462
|
+
return this.cloudBucketMounts;
|
|
463
|
+
}
|
|
464
|
+
this.cloudBucketMounts = await this.cloudBucketMountsProvider?.();
|
|
465
|
+
this.cloudBucketMountsResolved = true;
|
|
466
|
+
return this.cloudBucketMounts;
|
|
467
|
+
}
|
|
468
|
+
async close() {
|
|
469
|
+
if (this.closed) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (this.closePromise) {
|
|
473
|
+
await this.closePromise;
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
this.closePromise = this.closeOnce();
|
|
477
|
+
try {
|
|
478
|
+
await this.closePromise;
|
|
479
|
+
this.closed = true;
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
if (!this.closed) {
|
|
483
|
+
this.closePromise = undefined;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async closeOnce() {
|
|
488
|
+
const processes = [...this.activeProcesses.values()];
|
|
489
|
+
const activeProcesses = processes.filter((activeProcess) => !activeProcess.done);
|
|
490
|
+
await Promise.all(activeProcesses.map(async (activeProcess) => {
|
|
491
|
+
if (activeProcess.process.stdin?.close) {
|
|
492
|
+
await activeProcess.process.stdin.close().catch(() => { });
|
|
493
|
+
}
|
|
494
|
+
}));
|
|
495
|
+
if (this.ownsSandbox) {
|
|
496
|
+
await this.sandbox.terminate();
|
|
497
|
+
await Promise.all(activeProcesses.map(async (activeProcess) => {
|
|
498
|
+
await activeProcess.donePromise.catch(() => { });
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
await Promise.all(activeProcesses.map(async (activeProcess) => {
|
|
503
|
+
await activeProcess.stopOutputPumps?.().catch(() => { });
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
506
|
+
for (const process of processes) {
|
|
507
|
+
clearActiveProcessCleanup(process);
|
|
508
|
+
}
|
|
509
|
+
this.activeProcesses.clear();
|
|
510
|
+
}
|
|
511
|
+
async resetActiveProcesses() {
|
|
512
|
+
const processes = [...this.activeProcesses.values()];
|
|
513
|
+
this.activeProcesses.clear();
|
|
514
|
+
await Promise.all(processes.map(async (activeProcess) => {
|
|
515
|
+
clearActiveProcessCleanup(activeProcess);
|
|
516
|
+
if (!activeProcess.done && activeProcess.process.stdin?.close) {
|
|
517
|
+
await activeProcess.process.stdin.close().catch(() => { });
|
|
518
|
+
}
|
|
519
|
+
if (!activeProcess.done) {
|
|
520
|
+
await activeProcess.stopOutputPumps?.().catch(() => { });
|
|
521
|
+
}
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
manifestMaterializationOptions(runAs) {
|
|
525
|
+
return manifestMaterializationOptionsWithRunAs({
|
|
526
|
+
providerName: 'ModalSandboxClient',
|
|
527
|
+
providerId: 'modal',
|
|
528
|
+
runAs,
|
|
529
|
+
runCommand: this.runAsCommandRunner.bind(this),
|
|
530
|
+
options: {
|
|
531
|
+
materializeMount: async (absolutePath, _entry) => {
|
|
532
|
+
throwModalLiveMountsUnsupported([absolutePath]);
|
|
533
|
+
},
|
|
534
|
+
concurrencyLimits: this.concurrencyLimits,
|
|
535
|
+
},
|
|
536
|
+
support: MOUNT_MANIFEST_METADATA_SUPPORT,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
invalidateCloudBucketMounts() {
|
|
540
|
+
if (!this.cloudBucketMountsProvider) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
this.cloudBucketMounts = undefined;
|
|
544
|
+
this.cloudBucketMountsResolved = false;
|
|
545
|
+
}
|
|
546
|
+
async shutdown() {
|
|
547
|
+
await this.close();
|
|
548
|
+
}
|
|
549
|
+
async delete() {
|
|
550
|
+
await this.close();
|
|
551
|
+
}
|
|
552
|
+
registerActiveProcess(sessionId, activeProcess) {
|
|
553
|
+
this.activeProcesses.set(sessionId, activeProcess);
|
|
554
|
+
activeProcess.donePromise
|
|
555
|
+
.finally(() => {
|
|
556
|
+
activeProcess.cleanupTimer = setTimeout(() => {
|
|
557
|
+
if (this.activeProcesses.get(sessionId) === activeProcess) {
|
|
558
|
+
this.activeProcesses.delete(sessionId);
|
|
559
|
+
}
|
|
560
|
+
}, COMPLETED_ACTIVE_PROCESS_RETENTION_MS);
|
|
561
|
+
unrefTimer(activeProcess.cleanupTimer);
|
|
562
|
+
})
|
|
563
|
+
.catch(() => { });
|
|
564
|
+
}
|
|
565
|
+
deleteActiveProcess(sessionId, activeProcess) {
|
|
566
|
+
clearActiveProcessCleanup(activeProcess);
|
|
567
|
+
if (this.activeProcesses.get(sessionId) === activeProcess) {
|
|
568
|
+
this.activeProcesses.delete(sessionId);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
resolveAbsolutePath(path) {
|
|
572
|
+
return resolveSandboxAbsolutePath(this.state.manifest.root, path);
|
|
573
|
+
}
|
|
574
|
+
async resolveRemotePath(path, options = {}) {
|
|
575
|
+
return await validateRemoteSandboxPathForManifest({
|
|
576
|
+
manifest: this.state.manifest,
|
|
577
|
+
path,
|
|
578
|
+
options,
|
|
579
|
+
runCommand: async (command) => {
|
|
580
|
+
const process = await this.sandbox.exec(['/bin/sh', '-c', command], {
|
|
581
|
+
mode: 'text',
|
|
582
|
+
workdir: this.state.manifest.root,
|
|
583
|
+
stdout: 'pipe',
|
|
584
|
+
stderr: 'pipe',
|
|
585
|
+
});
|
|
586
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
587
|
+
readStreamText(process.stdout),
|
|
588
|
+
readStreamText(process.stderr),
|
|
589
|
+
process.wait(),
|
|
590
|
+
]);
|
|
591
|
+
return { status: exitCode, stdout, stderr };
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
resolveWorkdir(path) {
|
|
596
|
+
return resolveSandboxWorkdir(this.state.manifest.root, path);
|
|
597
|
+
}
|
|
598
|
+
async ensureDirectory(path) {
|
|
599
|
+
await this.runDirectCommand(['mkdir', '-p', '--', path]);
|
|
600
|
+
}
|
|
601
|
+
async writeSandboxFile(path, content) {
|
|
602
|
+
await this.ensureDirectory(pathPosix.dirname(path));
|
|
603
|
+
const handle = await this.sandbox.open(path, 'w');
|
|
604
|
+
try {
|
|
605
|
+
await handle.write(typeof content === 'string'
|
|
606
|
+
? new TextEncoder().encode(content)
|
|
607
|
+
: content);
|
|
608
|
+
if (handle.flush) {
|
|
609
|
+
await handle.flush();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
await handle.close();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async readSandboxFile(path) {
|
|
617
|
+
const handle = await this.sandbox.open(path, 'r');
|
|
618
|
+
try {
|
|
619
|
+
return Uint8Array.from(await handle.read());
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
await handle.close();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async removeSandboxPath(path) {
|
|
626
|
+
await this.runDirectCommand(['rm', '-f', '--', path]);
|
|
627
|
+
}
|
|
628
|
+
async runAsCommandRunner(command, options = {}) {
|
|
629
|
+
const process = await this.sandbox.exec(['/bin/sh', '-lc', sandboxUserShellCommand(command, options.runAs)], {
|
|
630
|
+
mode: 'text',
|
|
631
|
+
workdir: this.state.manifest.root,
|
|
632
|
+
env: this.state.environment,
|
|
633
|
+
stdout: 'pipe',
|
|
634
|
+
stderr: 'pipe',
|
|
635
|
+
});
|
|
636
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
637
|
+
readStreamText(process.stdout),
|
|
638
|
+
readStreamText(process.stderr),
|
|
639
|
+
process.wait(),
|
|
640
|
+
]);
|
|
641
|
+
return {
|
|
642
|
+
status: exitCode,
|
|
643
|
+
stdout,
|
|
644
|
+
stderr,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
writer() {
|
|
648
|
+
return {
|
|
649
|
+
mkdir: async (path) => {
|
|
650
|
+
await this.ensureDirectory(path);
|
|
651
|
+
},
|
|
652
|
+
writeFile: async (path, content) => {
|
|
653
|
+
await this.writeSandboxFile(path, content);
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
archiveIo() {
|
|
658
|
+
return {
|
|
659
|
+
runCommand: async (command) => {
|
|
660
|
+
const process = await this.sandbox.exec(['/bin/sh', '-c', command], {
|
|
661
|
+
mode: 'text',
|
|
662
|
+
workdir: this.state.manifest.root,
|
|
663
|
+
env: this.state.environment,
|
|
664
|
+
stdout: 'pipe',
|
|
665
|
+
stderr: 'pipe',
|
|
666
|
+
});
|
|
667
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
668
|
+
readStreamText(process.stdout),
|
|
669
|
+
readStreamText(process.stderr),
|
|
670
|
+
process.wait(),
|
|
671
|
+
]);
|
|
672
|
+
return { status: exitCode, stdout, stderr };
|
|
673
|
+
},
|
|
674
|
+
readFile: async (path) => await this.readSandboxFile(path),
|
|
675
|
+
writeFile: async (path, content) => {
|
|
676
|
+
await this.writeSandboxFile(path, content);
|
|
677
|
+
},
|
|
678
|
+
mkdir: async (path) => {
|
|
679
|
+
await this.ensureDirectory(path);
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async runDirectCommand(command) {
|
|
684
|
+
const process = await this.sandbox.exec(command, {
|
|
685
|
+
mode: 'text',
|
|
686
|
+
workdir: this.state.manifest.root,
|
|
687
|
+
stdout: 'ignore',
|
|
688
|
+
stderr: 'pipe',
|
|
689
|
+
});
|
|
690
|
+
const stderr = await readStreamText(process.stderr);
|
|
691
|
+
const exitCode = await process.wait();
|
|
692
|
+
if (exitCode !== 0) {
|
|
693
|
+
throw new UserError(`Modal sandbox command failed (${command.join(' ')}): ${stderr || `exit ${exitCode}`}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* @see {@link https://modal.com/docs/guide/sandboxes | Sandboxes guide}.
|
|
699
|
+
* @see {@link https://modal.com/docs/guide/sdk-javascript-go | JavaScript and Go SDK guide}.
|
|
700
|
+
* @see {@link https://modal.com/docs/reference/modal.Sandbox | Sandbox reference}.
|
|
701
|
+
*/
|
|
702
|
+
export class ModalSandboxClient {
|
|
703
|
+
backendId = 'modal';
|
|
704
|
+
options;
|
|
705
|
+
constructor(options = {}) {
|
|
706
|
+
this.options = options;
|
|
707
|
+
}
|
|
708
|
+
async create(args, manifestOptions) {
|
|
709
|
+
const createArgs = normalizeSandboxClientCreateArgs(args, manifestOptions);
|
|
710
|
+
assertCoreSnapshotUnsupported('ModalSandboxClient', createArgs.snapshot);
|
|
711
|
+
const manifest = createArgs.manifest;
|
|
712
|
+
const resolvedOptions = resolveOptions(this.options, createArgs.options);
|
|
713
|
+
validateOptions(resolvedOptions);
|
|
714
|
+
const useSleepCmd = resolvedOptions.useSleepCmd ?? true;
|
|
715
|
+
assertSandboxManifestMetadataSupported('ModalSandboxClient', manifest, MOUNT_MANIFEST_METADATA_SUPPORT);
|
|
716
|
+
assertModalManifestMountsSupported(manifest, resolvedOptions.nativeCloudBucketSecretName);
|
|
717
|
+
assertModalReusableSandboxMountsSupported(manifest, resolvedOptions.sandbox);
|
|
718
|
+
return await withSandboxSpan('sandbox.start', {
|
|
719
|
+
backend_id: this.backendId,
|
|
720
|
+
}, async () => {
|
|
721
|
+
const modalModule = await loadModalModule();
|
|
722
|
+
const modal = createModalClientFromModule(modalModule, resolvedOptions);
|
|
723
|
+
const workspacePersistence = resolvedOptions.workspacePersistence ?? 'tar';
|
|
724
|
+
const app = shouldResolveModalApp({
|
|
725
|
+
ownsSandbox: !resolvedOptions.sandbox,
|
|
726
|
+
workspacePersistence,
|
|
727
|
+
})
|
|
728
|
+
? await lookupModalApp({
|
|
729
|
+
modal,
|
|
730
|
+
appName: resolvedOptions.appName,
|
|
731
|
+
environment: resolvedOptions.environment,
|
|
732
|
+
createIfMissing: !resolvedOptions.sandbox,
|
|
733
|
+
})
|
|
734
|
+
: {};
|
|
735
|
+
const environment = await materializeEnvironment(manifest, resolvedOptions.env);
|
|
736
|
+
const imageState = modalImageStateFromOptions(resolvedOptions);
|
|
737
|
+
let cloudBucketMounts;
|
|
738
|
+
let sandbox;
|
|
739
|
+
if (resolvedOptions.sandbox) {
|
|
740
|
+
sandbox = await withProviderError('ModalSandboxClient', 'modal', 'resolve sandbox selector', async () => await resolveSelectedModalSandbox(modal, resolvedOptions.sandbox));
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
const { image, imageId, imageTag } = await resolveModalImage(modal, resolvedOptions);
|
|
744
|
+
imageState.imageId = imageId;
|
|
745
|
+
imageState.imageTag = imageTag;
|
|
746
|
+
const sandboxImage = maybeSetModalSandboxCmd(image, useSleepCmd);
|
|
747
|
+
cloudBucketMounts = await modalCloudBucketMountsForManifest({
|
|
748
|
+
modalModule,
|
|
749
|
+
manifest,
|
|
750
|
+
defaultSecretName: resolvedOptions.nativeCloudBucketSecretName,
|
|
751
|
+
});
|
|
752
|
+
const createParams = {
|
|
753
|
+
workdir: manifest.root,
|
|
754
|
+
env: environment,
|
|
755
|
+
...(typeof resolvedOptions.timeoutMs === 'number'
|
|
756
|
+
? { timeoutMs: resolvedOptions.timeoutMs }
|
|
757
|
+
: {}),
|
|
758
|
+
...(typeof resolvedOptions.idleTimeoutMs === 'number'
|
|
759
|
+
? { idleTimeoutMs: resolvedOptions.idleTimeoutMs }
|
|
760
|
+
: {}),
|
|
761
|
+
...(resolvedOptions.gpu ? { gpu: resolvedOptions.gpu } : {}),
|
|
762
|
+
...(resolvedOptions.exposedPorts
|
|
763
|
+
? { encryptedPorts: resolvedOptions.exposedPorts }
|
|
764
|
+
: {}),
|
|
765
|
+
...(cloudBucketMounts ? { cloudBucketMounts } : {}),
|
|
766
|
+
};
|
|
767
|
+
sandbox = await withOptionalTimeout(withProviderError('ModalSandboxClient', 'modal', 'create sandbox', async () => await modal.sandboxes.create(app, sandboxImage, createParams), { appName: resolvedOptions.appName }), resolvedOptions.sandboxCreateTimeoutS, 'Modal sandbox creation timed out.', terminateModalSandboxAfterTimeout);
|
|
768
|
+
}
|
|
769
|
+
const sessionState = {
|
|
770
|
+
manifest,
|
|
771
|
+
sandboxId: sandbox.sandboxId,
|
|
772
|
+
ownsSandbox: !resolvedOptions.sandbox,
|
|
773
|
+
appName: resolvedOptions.appName,
|
|
774
|
+
imageId: imageState.imageId,
|
|
775
|
+
imageTag: imageState.imageTag,
|
|
776
|
+
sandboxCreateTimeoutS: resolvedOptions.sandboxCreateTimeoutS,
|
|
777
|
+
workspacePersistence,
|
|
778
|
+
snapshotFilesystemTimeoutMs: resolvedOptions.snapshotFilesystemTimeoutMs,
|
|
779
|
+
snapshotFilesystemRestoreTimeoutMs: resolvedOptions.snapshotFilesystemRestoreTimeoutMs,
|
|
780
|
+
environment,
|
|
781
|
+
timeoutMs: resolvedOptions.timeoutMs,
|
|
782
|
+
idleTimeoutMs: resolvedOptions.idleTimeoutMs,
|
|
783
|
+
gpu: resolvedOptions.gpu,
|
|
784
|
+
configuredExposedPorts: resolvedOptions.exposedPorts,
|
|
785
|
+
modalEnvironment: resolvedOptions.environment,
|
|
786
|
+
endpoint: resolvedOptions.endpoint,
|
|
787
|
+
imageBuilderVersion: resolvedOptions.imageBuilderVersion,
|
|
788
|
+
nativeCloudBucketSecretName: resolvedOptions.nativeCloudBucketSecretName,
|
|
789
|
+
useSleepCmd,
|
|
790
|
+
};
|
|
791
|
+
const session = new ModalSandboxSession({
|
|
792
|
+
modal,
|
|
793
|
+
app,
|
|
794
|
+
sandbox,
|
|
795
|
+
ownsSandbox: !resolvedOptions.sandbox,
|
|
796
|
+
state: sessionState,
|
|
797
|
+
cloudBucketMounts,
|
|
798
|
+
concurrencyLimits: createArgs.concurrencyLimits,
|
|
799
|
+
cloudBucketMountsProvider: async () => await modalCloudBucketMountsForManifest({
|
|
800
|
+
modalModule,
|
|
801
|
+
manifest: sessionState.manifest,
|
|
802
|
+
defaultSecretName: resolvedOptions.nativeCloudBucketSecretName,
|
|
803
|
+
}),
|
|
804
|
+
});
|
|
805
|
+
try {
|
|
806
|
+
await session.applyManifest(cloneManifestWithoutMountEntries(manifest));
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
if (resolvedOptions.sandbox) {
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
await closeRemoteSessionOnManifestError('Modal', session, error);
|
|
813
|
+
}
|
|
814
|
+
return session;
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
async serializeSessionState(state) {
|
|
818
|
+
return serializeRemoteSandboxSessionState(state);
|
|
819
|
+
}
|
|
820
|
+
canPersistOwnedSessionState() {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
async deserializeSessionState(state) {
|
|
824
|
+
const baseState = deserializeRemoteSandboxSessionStateValues(state, this.options.env);
|
|
825
|
+
return {
|
|
826
|
+
...state,
|
|
827
|
+
...baseState,
|
|
828
|
+
ownsSandbox: state.ownsSandbox === false ? false : true,
|
|
829
|
+
appName: readString(state, 'appName'),
|
|
830
|
+
imageId: readOptionalString(state, 'imageId'),
|
|
831
|
+
imageTag: readString(state, 'imageTag', DEFAULT_MODAL_IMAGE_TAG),
|
|
832
|
+
workspacePersistence: state.workspacePersistence ??
|
|
833
|
+
'tar',
|
|
834
|
+
snapshotFilesystemTimeoutMs: readOptionalNumber(state, 'snapshotFilesystemTimeoutMs'),
|
|
835
|
+
snapshotFilesystemRestoreTimeoutMs: readOptionalNumber(state, 'snapshotFilesystemRestoreTimeoutMs'),
|
|
836
|
+
timeoutMs: readOptionalNumber(state, 'timeoutMs'),
|
|
837
|
+
idleTimeoutMs: readOptionalNumber(state, 'idleTimeoutMs'),
|
|
838
|
+
gpu: readOptionalString(state, 'gpu'),
|
|
839
|
+
configuredExposedPorts: readOptionalNumberArray(state.configuredExposedPorts),
|
|
840
|
+
modalEnvironment: readOptionalString(state, 'modalEnvironment'),
|
|
841
|
+
endpoint: readOptionalString(state, 'endpoint'),
|
|
842
|
+
nativeCloudBucketSecretName: readOptionalString(state, 'nativeCloudBucketSecretName'),
|
|
843
|
+
imageBuilderVersion: readOptionalString(state, 'imageBuilderVersion'),
|
|
844
|
+
useSleepCmd: typeof state.useSleepCmd === 'boolean' ? state.useSleepCmd : true,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
async resume(state) {
|
|
848
|
+
if (!state.sandboxId) {
|
|
849
|
+
throw new UserError('Modal sandbox resume requires a persisted sandboxId.');
|
|
850
|
+
}
|
|
851
|
+
const sandboxId = state.sandboxId;
|
|
852
|
+
const modalModule = await loadModalModule();
|
|
853
|
+
const modal = createModalClientFromModule(modalModule, {
|
|
854
|
+
...this.options,
|
|
855
|
+
appName: state.appName,
|
|
856
|
+
image: state.imageId
|
|
857
|
+
? ModalImageSelector.fromId(state.imageId)
|
|
858
|
+
: undefined,
|
|
859
|
+
imageTag: state.imageTag,
|
|
860
|
+
sandboxCreateTimeoutS: state.sandboxCreateTimeoutS,
|
|
861
|
+
workspacePersistence: state.workspacePersistence,
|
|
862
|
+
snapshotFilesystemTimeoutMs: state.snapshotFilesystemTimeoutMs,
|
|
863
|
+
snapshotFilesystemRestoreTimeoutMs: state.snapshotFilesystemRestoreTimeoutMs,
|
|
864
|
+
timeoutMs: state.timeoutMs,
|
|
865
|
+
idleTimeoutMs: state.idleTimeoutMs,
|
|
866
|
+
gpu: state.gpu,
|
|
867
|
+
exposedPorts: state.configuredExposedPorts,
|
|
868
|
+
environment: state.modalEnvironment ?? this.options.environment,
|
|
869
|
+
endpoint: state.endpoint ?? this.options.endpoint,
|
|
870
|
+
imageBuilderVersion: state.imageBuilderVersion ?? this.options.imageBuilderVersion,
|
|
871
|
+
useSleepCmd: state.useSleepCmd,
|
|
872
|
+
});
|
|
873
|
+
const sandbox = await withProviderError('ModalSandboxClient', 'modal', 'resume sandbox', async () => await modal.sandboxes.fromId(sandboxId), { sandboxId });
|
|
874
|
+
const ownsSandbox = state.ownsSandbox !== false;
|
|
875
|
+
const app = shouldResolveModalApp({
|
|
876
|
+
ownsSandbox,
|
|
877
|
+
workspacePersistence: state.workspacePersistence,
|
|
878
|
+
})
|
|
879
|
+
? await lookupModalApp({
|
|
880
|
+
modal,
|
|
881
|
+
appName: state.appName,
|
|
882
|
+
environment: state.modalEnvironment,
|
|
883
|
+
createIfMissing: ownsSandbox,
|
|
884
|
+
})
|
|
885
|
+
: {};
|
|
886
|
+
const exitCode = await withProviderError('ModalSandboxClient', 'modal', 'poll sandbox', async () => await sandbox.poll(), { sandboxId });
|
|
887
|
+
if (exitCode !== null) {
|
|
888
|
+
throw new UserError(`Modal sandbox ${state.sandboxId} is no longer running.`);
|
|
889
|
+
}
|
|
890
|
+
return new ModalSandboxSession({
|
|
891
|
+
state,
|
|
892
|
+
modal,
|
|
893
|
+
app,
|
|
894
|
+
sandbox,
|
|
895
|
+
ownsSandbox,
|
|
896
|
+
cloudBucketMountsProvider: async () => await modalCloudBucketMountsForManifest({
|
|
897
|
+
modalModule,
|
|
898
|
+
manifest: state.manifest,
|
|
899
|
+
defaultSecretName: state.nativeCloudBucketSecretName ??
|
|
900
|
+
this.options.nativeCloudBucketSecretName,
|
|
901
|
+
}),
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async function loadModalModule() {
|
|
906
|
+
try {
|
|
907
|
+
return adaptModalModule(await import('modal'));
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
throw new UserError(`Modal sandbox support requires the optional \`modal\` package. Install it before using Modal-backed sandbox examples. ${error.message}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
async function lookupModalApp(args) {
|
|
914
|
+
return await withProviderError('ModalSandboxClient', 'modal', 'look up app', async () => await args.modal.apps.fromName(args.appName, {
|
|
915
|
+
createIfMissing: args.createIfMissing,
|
|
916
|
+
...(args.environment ? { environment: args.environment } : {}),
|
|
917
|
+
}), { appName: args.appName });
|
|
918
|
+
}
|
|
919
|
+
function shouldResolveModalApp(args) {
|
|
920
|
+
return (args.ownsSandbox || args.workspacePersistence === 'snapshot_filesystem');
|
|
921
|
+
}
|
|
922
|
+
function adaptModalModule(modal) {
|
|
923
|
+
const secretApi = modal.Secret;
|
|
924
|
+
const usesClassSecretApi = typeof secretApi === 'function';
|
|
925
|
+
const cloudBucketMountClass = typeof modal.CloudBucketMount === 'function'
|
|
926
|
+
? modal.CloudBucketMount
|
|
927
|
+
: undefined;
|
|
928
|
+
return {
|
|
929
|
+
ModalClient: modal.ModalClient,
|
|
930
|
+
CloudBucketMount: usesClassSecretApi && cloudBucketMountClass
|
|
931
|
+
? adaptClassModalCloudBucketMount(cloudBucketMountClass)
|
|
932
|
+
: modal.CloudBucketMount,
|
|
933
|
+
Secret: {
|
|
934
|
+
fromName: async (name, params) => {
|
|
935
|
+
if (usesClassSecretApi) {
|
|
936
|
+
return await modal.Secret.fromName(name, {
|
|
937
|
+
...(params?.environment ? { environment: params.environment } : {}),
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
if (isModalSecretModule(secretApi)) {
|
|
941
|
+
return await secretApi.fromName(name, params);
|
|
942
|
+
}
|
|
943
|
+
throw new Error('Missing Secret export from modal.');
|
|
944
|
+
},
|
|
945
|
+
fromObject: async (entries, params) => {
|
|
946
|
+
if (usesClassSecretApi) {
|
|
947
|
+
const environment = readOptionalString(params, 'environment');
|
|
948
|
+
return await modal.Secret.fromObject(entries, {
|
|
949
|
+
...(environment ? { environment } : {}),
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
if (isModalSecretModule(secretApi)) {
|
|
953
|
+
return await secretApi.fromObject(entries, params);
|
|
954
|
+
}
|
|
955
|
+
throw new Error('Missing Secret export from modal.');
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function adaptClassModalCloudBucketMount(CloudBucketMount) {
|
|
961
|
+
return class {
|
|
962
|
+
mount;
|
|
963
|
+
constructor(bucketName, params) {
|
|
964
|
+
const bucketEndpointUrl = readOptionalString(params, 'bucketEndpointUrl');
|
|
965
|
+
const keyPrefix = readOptionalString(params, 'keyPrefix');
|
|
966
|
+
this.mount = new CloudBucketMount(bucketName, {
|
|
967
|
+
...(bucketEndpointUrl ? { bucketEndpointUrl } : {}),
|
|
968
|
+
...(keyPrefix ? { keyPrefix } : {}),
|
|
969
|
+
...(typeof params?.readOnly === 'boolean'
|
|
970
|
+
? { readOnly: params.readOnly }
|
|
971
|
+
: {}),
|
|
972
|
+
...(params?.secret ? { secret: params.secret } : {}),
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
toProto(mountPath) {
|
|
976
|
+
if (!this.mount.toProto) {
|
|
977
|
+
throw new SandboxProviderError('Modal CloudBucketMount is missing toProto support.', {
|
|
978
|
+
provider: 'modal',
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
return this.mount.toProto(mountPath);
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function isModalSecretModule(value) {
|
|
986
|
+
return (typeof value === 'object' &&
|
|
987
|
+
value !== null &&
|
|
988
|
+
'fromName' in value &&
|
|
989
|
+
'fromObject' in value);
|
|
990
|
+
}
|
|
991
|
+
function createModalClientFromModule(modalModule, options) {
|
|
992
|
+
const { ModalClient } = modalModule;
|
|
993
|
+
const client = new ModalClient({
|
|
994
|
+
...(options.tokenId ? { tokenId: options.tokenId } : {}),
|
|
995
|
+
...(options.tokenSecret ? { tokenSecret: options.tokenSecret } : {}),
|
|
996
|
+
...(options.environment ? { environment: options.environment } : {}),
|
|
997
|
+
...(options.endpoint ? { endpoint: options.endpoint } : {}),
|
|
998
|
+
});
|
|
999
|
+
if (options.imageBuilderVersion !== undefined && client.imageBuilderVersion) {
|
|
1000
|
+
const originalImageBuilderVersion = client.imageBuilderVersion.bind(client);
|
|
1001
|
+
client.imageBuilderVersion = (version) => originalImageBuilderVersion(version ?? options.imageBuilderVersion);
|
|
1002
|
+
}
|
|
1003
|
+
return client;
|
|
1004
|
+
}
|
|
1005
|
+
async function resolveModalImage(modal, options) {
|
|
1006
|
+
const selector = modalImageSelectorFromOptions(options);
|
|
1007
|
+
if (selector.kind === 'image') {
|
|
1008
|
+
const image = selector.value;
|
|
1009
|
+
return {
|
|
1010
|
+
image,
|
|
1011
|
+
imageId: modalImageId(image),
|
|
1012
|
+
imageTag: options.imageTag ?? DEFAULT_MODAL_IMAGE_TAG,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
if (selector.kind === 'id') {
|
|
1016
|
+
const imageId = selector.value;
|
|
1017
|
+
const image = modal.images.fromId
|
|
1018
|
+
? await withProviderError('ModalSandboxClient', 'modal', 'look up image', async () => await modal.images.fromId(imageId), { imageId })
|
|
1019
|
+
: { imageId };
|
|
1020
|
+
return {
|
|
1021
|
+
image,
|
|
1022
|
+
imageId,
|
|
1023
|
+
imageTag: options.imageTag ?? DEFAULT_MODAL_IMAGE_TAG,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
const imageTag = selector.value;
|
|
1027
|
+
const image = await withProviderError('ModalSandboxClient', 'modal', 'resolve image', async () => modal.images.fromRegistry(imageTag), { imageTag });
|
|
1028
|
+
return {
|
|
1029
|
+
image,
|
|
1030
|
+
imageId: modalImageId(image),
|
|
1031
|
+
imageTag,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function modalImageStateFromOptions(options) {
|
|
1035
|
+
const selector = modalImageSelectorFromOptions(options);
|
|
1036
|
+
if (selector.kind === 'image') {
|
|
1037
|
+
return {
|
|
1038
|
+
imageId: modalImageId(selector.value),
|
|
1039
|
+
imageTag: options.imageTag ?? DEFAULT_MODAL_IMAGE_TAG,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
if (selector.kind === 'id') {
|
|
1043
|
+
return {
|
|
1044
|
+
imageId: selector.value,
|
|
1045
|
+
imageTag: options.imageTag ?? DEFAULT_MODAL_IMAGE_TAG,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
imageTag: selector.value,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function modalImageSelectorFromOptions(options) {
|
|
1053
|
+
return (options.image ??
|
|
1054
|
+
ModalImageSelector.fromTag(options.imageTag ?? DEFAULT_MODAL_IMAGE_TAG));
|
|
1055
|
+
}
|
|
1056
|
+
async function resolveSelectedModalSandbox(modal, selector) {
|
|
1057
|
+
if (selector.kind === 'sandbox') {
|
|
1058
|
+
return selector.value;
|
|
1059
|
+
}
|
|
1060
|
+
return await withProviderError('ModalSandboxClient', 'modal', 'resolve sandbox', async () => await modal.sandboxes.fromId(selector.value), { sandboxId: selector.value });
|
|
1061
|
+
}
|
|
1062
|
+
function modalImageId(image) {
|
|
1063
|
+
return image.imageId ?? image.objectId;
|
|
1064
|
+
}
|
|
1065
|
+
function maybeSetModalSandboxCmd(image, useSleepCmd) {
|
|
1066
|
+
if (!useSleepCmd || typeof image.cmd !== 'function') {
|
|
1067
|
+
return image;
|
|
1068
|
+
}
|
|
1069
|
+
return image.cmd(['sleep', 'infinity']);
|
|
1070
|
+
}
|
|
1071
|
+
function errorMessage(error) {
|
|
1072
|
+
return error instanceof Error ? error.message : String(error);
|
|
1073
|
+
}
|
|
1074
|
+
async function modalCloudBucketMountsForManifest(args) {
|
|
1075
|
+
const mountEntries = args.manifest
|
|
1076
|
+
.mountTargets()
|
|
1077
|
+
.filter(({ entry }) => isModalCloudBucketMountEntry(entry));
|
|
1078
|
+
if (mountEntries.length === 0) {
|
|
1079
|
+
return undefined;
|
|
1080
|
+
}
|
|
1081
|
+
if (!args.modalModule.CloudBucketMount) {
|
|
1082
|
+
throw new SandboxProviderError('Modal cloud bucket mounts require modal.CloudBucketMount support.', {
|
|
1083
|
+
provider: 'modal',
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
const cloudBucketMounts = {};
|
|
1087
|
+
for (const { entry, mountPath } of mountEntries) {
|
|
1088
|
+
const config = buildModalCloudBucketMountConfig(entry, {
|
|
1089
|
+
secretName: readOptionalString(entry.mountStrategy, 'secretName') ??
|
|
1090
|
+
args.defaultSecretName,
|
|
1091
|
+
secretEnvironmentName: readOptionalString(entry.mountStrategy, 'secretEnvironmentName'),
|
|
1092
|
+
});
|
|
1093
|
+
cloudBucketMounts[mountPath] = new args.modalModule.CloudBucketMount(config.bucketName, {
|
|
1094
|
+
...(config.bucketEndpointUrl
|
|
1095
|
+
? { bucketEndpointUrl: config.bucketEndpointUrl }
|
|
1096
|
+
: {}),
|
|
1097
|
+
...(config.keyPrefix ? { keyPrefix: config.keyPrefix } : {}),
|
|
1098
|
+
...(typeof config.readOnly === 'boolean'
|
|
1099
|
+
? { readOnly: config.readOnly }
|
|
1100
|
+
: {}),
|
|
1101
|
+
...(await modalCloudBucketMountSecret(args.modalModule, config)),
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
return cloudBucketMounts;
|
|
1105
|
+
}
|
|
1106
|
+
async function modalCloudBucketMountSecret(modalModule, config) {
|
|
1107
|
+
if (!config.secretName && !config.credentials) {
|
|
1108
|
+
return {};
|
|
1109
|
+
}
|
|
1110
|
+
const Secret = modalModule.Secret;
|
|
1111
|
+
if (!Secret) {
|
|
1112
|
+
throw new SandboxProviderError('Modal cloud bucket mounts require modal.Secret support when credentials are configured.', {
|
|
1113
|
+
provider: 'modal',
|
|
1114
|
+
bucketName: config.bucketName,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
const secretName = config.secretName;
|
|
1118
|
+
if (secretName) {
|
|
1119
|
+
return {
|
|
1120
|
+
secret: await withProviderError('ModalSandboxClient', 'modal', 'resolve cloud bucket secret', async () => await Secret.fromName(secretName, {
|
|
1121
|
+
...(config.secretEnvironmentName
|
|
1122
|
+
? { environment: config.secretEnvironmentName }
|
|
1123
|
+
: {}),
|
|
1124
|
+
}), {
|
|
1125
|
+
bucketName: config.bucketName,
|
|
1126
|
+
secretName,
|
|
1127
|
+
}),
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
secret: await withProviderError('ModalSandboxClient', 'modal', 'create cloud bucket secret', async () => await Secret.fromObject(config.credentials ?? {}), { bucketName: config.bucketName }),
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function assertModalManifestMountsSupported(manifest, defaultSecretName) {
|
|
1135
|
+
for (const { entry, mountPath } of manifest.mountTargets()) {
|
|
1136
|
+
assertModalMountEntrySupported(entry, mountPath, defaultSecretName);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function assertModalMountEntrySupported(entry, mountPath, defaultSecretName) {
|
|
1140
|
+
if (!isModalCloudBucketMountEntry(entry)) {
|
|
1141
|
+
throw new SandboxUnsupportedFeatureError('ModalSandboxClient only supports ModalCloudBucketMountStrategy mount entries.', {
|
|
1142
|
+
provider: 'modal',
|
|
1143
|
+
feature: 'entry.mountStrategy',
|
|
1144
|
+
path: mountPath,
|
|
1145
|
+
mountType: entry.type,
|
|
1146
|
+
strategyType: entry.mountStrategy?.type,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
buildModalCloudBucketMountConfig(entry, {
|
|
1150
|
+
secretName: readOptionalString(entry.mountStrategy, 'secretName') ??
|
|
1151
|
+
defaultSecretName,
|
|
1152
|
+
secretEnvironmentName: readOptionalString(entry.mountStrategy, 'secretEnvironmentName'),
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
function assertModalLiveManifestMountsUnsupported(manifest, currentManifest) {
|
|
1156
|
+
const mountPaths = uniqueStrings([
|
|
1157
|
+
...manifest.mountTargets().map(({ mountPath }) => mountPath),
|
|
1158
|
+
...collectExistingModalMountPathsOverlappedByPaths(currentManifest, collectModalManifestEntryPaths(manifest)),
|
|
1159
|
+
]);
|
|
1160
|
+
if (mountPaths.length === 0) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
throwModalLiveMountsUnsupported(mountPaths);
|
|
1164
|
+
}
|
|
1165
|
+
function assertModalLiveEntryMountsUnsupported(entry, path, currentManifest) {
|
|
1166
|
+
const mountPaths = uniqueStrings([
|
|
1167
|
+
...collectModalEntryMountPaths(entry, path),
|
|
1168
|
+
...collectExistingModalMountPathsOverlappedByPaths(currentManifest, collectModalEntryPaths(entry, path, currentManifest.root)),
|
|
1169
|
+
]);
|
|
1170
|
+
if (mountPaths.length === 0) {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
throwModalLiveMountsUnsupported(mountPaths);
|
|
1174
|
+
}
|
|
1175
|
+
function collectModalManifestEntryPaths(manifest) {
|
|
1176
|
+
return [...manifest.iterEntries()].map(({ logicalPath }) => logicalPath);
|
|
1177
|
+
}
|
|
1178
|
+
function collectModalEntryPaths(entry, path, root) {
|
|
1179
|
+
const logicalPath = resolveSandboxRelativePath(root, path);
|
|
1180
|
+
return collectModalEntryPathsFromRelative(entry, logicalPath);
|
|
1181
|
+
}
|
|
1182
|
+
function collectModalEntryPathsFromRelative(entry, path) {
|
|
1183
|
+
if (entry.type !== 'dir' || !entry.children) {
|
|
1184
|
+
return [normalizeManifestRelativePath(path)];
|
|
1185
|
+
}
|
|
1186
|
+
const paths = [normalizeManifestRelativePath(path)];
|
|
1187
|
+
for (const [childPath, childEntry] of Object.entries(entry.children)) {
|
|
1188
|
+
paths.push(...collectModalEntryPathsFromRelative(childEntry, pathPosix.join(path, childPath)));
|
|
1189
|
+
}
|
|
1190
|
+
return paths;
|
|
1191
|
+
}
|
|
1192
|
+
function collectModalEntryMountPaths(entry, path) {
|
|
1193
|
+
if (isMount(entry)) {
|
|
1194
|
+
return [entry.mountPath ?? path];
|
|
1195
|
+
}
|
|
1196
|
+
if (entry.type !== 'dir' || !entry.children) {
|
|
1197
|
+
return [];
|
|
1198
|
+
}
|
|
1199
|
+
const mountPaths = [];
|
|
1200
|
+
for (const [childPath, childEntry] of Object.entries(entry.children)) {
|
|
1201
|
+
mountPaths.push(...collectModalEntryMountPaths(childEntry, pathPosix.join(path, childPath)));
|
|
1202
|
+
}
|
|
1203
|
+
return mountPaths;
|
|
1204
|
+
}
|
|
1205
|
+
function collectExistingModalMountPathsOverlappedByPaths(currentManifest, paths) {
|
|
1206
|
+
if (paths.length === 0) {
|
|
1207
|
+
return [];
|
|
1208
|
+
}
|
|
1209
|
+
const normalizedPaths = paths.map(normalizeManifestRelativePath);
|
|
1210
|
+
const mountPaths = [];
|
|
1211
|
+
for (const target of currentManifest.mountTargets()) {
|
|
1212
|
+
const mountedPaths = [
|
|
1213
|
+
target.logicalPath,
|
|
1214
|
+
existingMountPathRelativeToManifestRoot(currentManifest, target.mountPath),
|
|
1215
|
+
].filter((path) => path !== null);
|
|
1216
|
+
if (mountedPaths.some((mountedPath) => normalizedPaths.some((path) => manifestRelativePathsOverlap(path, mountedPath)))) {
|
|
1217
|
+
mountPaths.push(target.mountPath);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return mountPaths;
|
|
1221
|
+
}
|
|
1222
|
+
function existingMountPathRelativeToManifestRoot(manifest, mountPath) {
|
|
1223
|
+
return relativePosixPathWithinRoot(normalizePosixPath(manifest.root), normalizePosixPath(mountPath));
|
|
1224
|
+
}
|
|
1225
|
+
function manifestRelativePathsOverlap(left, right) {
|
|
1226
|
+
const normalizedLeft = normalizeManifestRelativePath(left);
|
|
1227
|
+
const normalizedRight = normalizeManifestRelativePath(right);
|
|
1228
|
+
if (normalizedLeft === '' || normalizedRight === '') {
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
return (normalizedLeft === normalizedRight ||
|
|
1232
|
+
normalizedLeft.startsWith(`${normalizedRight}/`) ||
|
|
1233
|
+
normalizedRight.startsWith(`${normalizedLeft}/`));
|
|
1234
|
+
}
|
|
1235
|
+
function normalizeManifestRelativePath(path) {
|
|
1236
|
+
const normalized = normalizePosixPath(path);
|
|
1237
|
+
return normalized === '.' ? '' : normalized;
|
|
1238
|
+
}
|
|
1239
|
+
function uniqueStrings(values) {
|
|
1240
|
+
return [...new Set(values)];
|
|
1241
|
+
}
|
|
1242
|
+
function throwModalLiveMountsUnsupported(mountPaths) {
|
|
1243
|
+
throw new SandboxUnsupportedFeatureError('ModalSandboxClient cannot apply mount entries to a running sandbox.', {
|
|
1244
|
+
provider: 'modal',
|
|
1245
|
+
feature: 'manifest.mounts',
|
|
1246
|
+
mountPaths,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
function assertModalReusableSandboxMountsSupported(manifest, sandbox) {
|
|
1250
|
+
if (!sandbox) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const mountTargets = manifest.mountTargets();
|
|
1254
|
+
if (mountTargets.length === 0) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
throw new SandboxUnsupportedFeatureError('ModalSandboxClient cannot apply mount entries when reusing an existing sandbox.', {
|
|
1258
|
+
provider: 'modal',
|
|
1259
|
+
feature: 'manifest.mounts',
|
|
1260
|
+
mountPaths: mountTargets.map(({ mountPath }) => mountPath),
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
function resolveOptions(defaults, overrides) {
|
|
1264
|
+
return {
|
|
1265
|
+
appName: overrides?.appName ?? defaults.appName ?? '',
|
|
1266
|
+
image: overrides?.image ?? defaults.image,
|
|
1267
|
+
sandbox: overrides?.sandbox ?? defaults.sandbox,
|
|
1268
|
+
imageTag: overrides?.imageTag ?? defaults.imageTag,
|
|
1269
|
+
sandboxCreateTimeoutS: overrides?.sandboxCreateTimeoutS ?? defaults.sandboxCreateTimeoutS,
|
|
1270
|
+
workspacePersistence: overrides?.workspacePersistence ?? defaults.workspacePersistence ?? 'tar',
|
|
1271
|
+
snapshotFilesystemTimeoutMs: overrides?.snapshotFilesystemTimeoutMs ??
|
|
1272
|
+
defaults.snapshotFilesystemTimeoutMs,
|
|
1273
|
+
snapshotFilesystemRestoreTimeoutMs: overrides?.snapshotFilesystemRestoreTimeoutMs ??
|
|
1274
|
+
defaults.snapshotFilesystemRestoreTimeoutMs,
|
|
1275
|
+
env: {
|
|
1276
|
+
...(defaults.env ?? {}),
|
|
1277
|
+
...(overrides?.env ?? {}),
|
|
1278
|
+
},
|
|
1279
|
+
timeoutMs: overrides?.timeoutMs ?? defaults.timeoutMs,
|
|
1280
|
+
idleTimeoutMs: overrides?.idleTimeoutMs ?? defaults.idleTimeoutMs,
|
|
1281
|
+
gpu: overrides?.gpu ?? defaults.gpu,
|
|
1282
|
+
exposedPorts: overrides?.exposedPorts ?? defaults.exposedPorts,
|
|
1283
|
+
environment: overrides?.environment ?? defaults.environment,
|
|
1284
|
+
endpoint: overrides?.endpoint ?? defaults.endpoint,
|
|
1285
|
+
tokenId: overrides?.tokenId ?? defaults.tokenId,
|
|
1286
|
+
tokenSecret: overrides?.tokenSecret ?? defaults.tokenSecret,
|
|
1287
|
+
imageBuilderVersion: overrides?.imageBuilderVersion ?? defaults.imageBuilderVersion,
|
|
1288
|
+
nativeCloudBucketSecretName: overrides?.nativeCloudBucketSecretName ??
|
|
1289
|
+
defaults.nativeCloudBucketSecretName,
|
|
1290
|
+
useSleepCmd: overrides?.useSleepCmd ?? defaults.useSleepCmd ?? true,
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
function validateOptions(options) {
|
|
1294
|
+
if (!options.appName || options.appName.trim().length === 0) {
|
|
1295
|
+
throw new UserError('ModalSandboxClient requires a non-empty appName.');
|
|
1296
|
+
}
|
|
1297
|
+
const workspacePersistence = options.workspacePersistence ?? 'tar';
|
|
1298
|
+
if (!['tar', 'snapshot_filesystem', 'snapshot_directory'].includes(workspacePersistence)) {
|
|
1299
|
+
throw new SandboxUnsupportedFeatureError('ModalSandboxClient workspacePersistence must be "tar", "snapshot_filesystem", or "snapshot_directory".', {
|
|
1300
|
+
provider: 'modal',
|
|
1301
|
+
feature: 'workspacePersistence',
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
if (options.nativeCloudBucketSecretName !== undefined &&
|
|
1305
|
+
options.nativeCloudBucketSecretName.trim() === '') {
|
|
1306
|
+
throw new SandboxUnsupportedFeatureError('Modal nativeCloudBucketSecretName must be a non-empty string.', {
|
|
1307
|
+
provider: 'modal',
|
|
1308
|
+
feature: 'nativeCloudBucketSecretName',
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
if (options.image) {
|
|
1312
|
+
validateModalImageSelector(options.image);
|
|
1313
|
+
}
|
|
1314
|
+
if (options.sandbox) {
|
|
1315
|
+
validateModalSandboxSelector(options.sandbox);
|
|
1316
|
+
}
|
|
1317
|
+
for (const [name, value] of [
|
|
1318
|
+
['sandboxCreateTimeoutS', options.sandboxCreateTimeoutS],
|
|
1319
|
+
['snapshotFilesystemTimeoutMs', options.snapshotFilesystemTimeoutMs],
|
|
1320
|
+
[
|
|
1321
|
+
'snapshotFilesystemRestoreTimeoutMs',
|
|
1322
|
+
options.snapshotFilesystemRestoreTimeoutMs,
|
|
1323
|
+
],
|
|
1324
|
+
['timeoutMs', options.timeoutMs],
|
|
1325
|
+
['idleTimeoutMs', options.idleTimeoutMs],
|
|
1326
|
+
]) {
|
|
1327
|
+
if (value !== undefined &&
|
|
1328
|
+
(typeof value !== 'number' || !Number.isFinite(value) || value <= 0)) {
|
|
1329
|
+
throw new UserError(`ModalSandboxClient ${name} must be a positive number.`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function validateModalImageSelector(selector) {
|
|
1334
|
+
if (selector.kind === 'image') {
|
|
1335
|
+
if (!selector.value || typeof selector.value !== 'object') {
|
|
1336
|
+
throw new UserError('ModalSandboxClient image selector requires a Modal image object.');
|
|
1337
|
+
}
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if ((selector.kind === 'id' || selector.kind === 'tag') &&
|
|
1341
|
+
(typeof selector.value !== 'string' || selector.value.trim().length === 0)) {
|
|
1342
|
+
throw new UserError('ModalSandboxClient image selector requires a non-empty string value.');
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
function validateModalSandboxSelector(selector) {
|
|
1346
|
+
if (selector.kind === 'sandbox') {
|
|
1347
|
+
if (!selector.value || typeof selector.value !== 'object') {
|
|
1348
|
+
throw new UserError('ModalSandboxClient sandbox selector requires a Modal sandbox object.');
|
|
1349
|
+
}
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (selector.kind === 'id' &&
|
|
1353
|
+
(typeof selector.value !== 'string' || selector.value.trim().length === 0)) {
|
|
1354
|
+
throw new UserError('ModalSandboxClient sandbox selector requires a non-empty sandbox id.');
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function snapshotIdFromModalResult(snapshot) {
|
|
1358
|
+
if (typeof snapshot === 'string') {
|
|
1359
|
+
return snapshot || undefined;
|
|
1360
|
+
}
|
|
1361
|
+
return snapshot.objectId ?? snapshot.imageId ?? snapshot.id;
|
|
1362
|
+
}
|
|
1363
|
+
function buildShellCommand(args) {
|
|
1364
|
+
const shellPath = args.shell ?? '/bin/sh';
|
|
1365
|
+
const login = args.shell ? (args.login ?? true) : false;
|
|
1366
|
+
const flag = login ? '-lc' : '-c';
|
|
1367
|
+
return [shellPath, flag, args.cmd];
|
|
1368
|
+
}
|
|
1369
|
+
function createActiveProcess(process) {
|
|
1370
|
+
const activeProcess = {
|
|
1371
|
+
process,
|
|
1372
|
+
stdout: '',
|
|
1373
|
+
stderr: '',
|
|
1374
|
+
droppedStdoutChars: 0,
|
|
1375
|
+
droppedStderrChars: 0,
|
|
1376
|
+
done: false,
|
|
1377
|
+
exitCode: null,
|
|
1378
|
+
donePromise: Promise.resolve(),
|
|
1379
|
+
};
|
|
1380
|
+
const stdoutPump = startTextStreamPump(process.stdout, (chunk) => {
|
|
1381
|
+
appendActiveProcessOutput(activeProcess, chunk, 'stdout');
|
|
1382
|
+
});
|
|
1383
|
+
const stderrPump = startTextStreamPump(process.stderr, (chunk) => {
|
|
1384
|
+
appendActiveProcessOutput(activeProcess, chunk, 'stderr');
|
|
1385
|
+
});
|
|
1386
|
+
activeProcess.stopOutputPumps = async () => {
|
|
1387
|
+
await Promise.allSettled([stdoutPump.cancel(), stderrPump.cancel()]);
|
|
1388
|
+
};
|
|
1389
|
+
activeProcess.donePromise = (async () => {
|
|
1390
|
+
try {
|
|
1391
|
+
activeProcess.exitCode = await process.wait();
|
|
1392
|
+
await Promise.allSettled([stdoutPump.promise, stderrPump.promise]);
|
|
1393
|
+
}
|
|
1394
|
+
catch (error) {
|
|
1395
|
+
activeProcess.waitError = error;
|
|
1396
|
+
await Promise.allSettled([stdoutPump.promise, stderrPump.promise]);
|
|
1397
|
+
}
|
|
1398
|
+
finally {
|
|
1399
|
+
activeProcess.done = true;
|
|
1400
|
+
}
|
|
1401
|
+
})();
|
|
1402
|
+
return activeProcess;
|
|
1403
|
+
}
|
|
1404
|
+
function startTextStreamPump(stream, onChunk) {
|
|
1405
|
+
const reader = stream.getReader();
|
|
1406
|
+
const promise = (async () => {
|
|
1407
|
+
try {
|
|
1408
|
+
while (true) {
|
|
1409
|
+
const { done, value } = await reader.read();
|
|
1410
|
+
if (done) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (typeof value === 'string') {
|
|
1414
|
+
onChunk(value);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
finally {
|
|
1419
|
+
reader.releaseLock();
|
|
1420
|
+
}
|
|
1421
|
+
})();
|
|
1422
|
+
return {
|
|
1423
|
+
promise,
|
|
1424
|
+
cancel: async () => {
|
|
1425
|
+
await reader.cancel().catch(() => { });
|
|
1426
|
+
await promise.catch(() => { });
|
|
1427
|
+
},
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
function clearActiveProcessCleanup(activeProcess) {
|
|
1431
|
+
if (activeProcess.cleanupTimer) {
|
|
1432
|
+
clearTimeout(activeProcess.cleanupTimer);
|
|
1433
|
+
activeProcess.cleanupTimer = undefined;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function unrefTimer(timer) {
|
|
1437
|
+
const maybeTimer = timer;
|
|
1438
|
+
maybeTimer.unref?.();
|
|
1439
|
+
}
|
|
1440
|
+
async function pumpTextStream(stream, onChunk) {
|
|
1441
|
+
await startTextStreamPump(stream, onChunk).promise;
|
|
1442
|
+
}
|
|
1443
|
+
async function readStreamText(stream) {
|
|
1444
|
+
let text = '';
|
|
1445
|
+
await pumpTextStream(stream, (chunk) => {
|
|
1446
|
+
text += chunk;
|
|
1447
|
+
});
|
|
1448
|
+
return text;
|
|
1449
|
+
}
|
|
1450
|
+
async function waitForProcessOrTimeout(activeProcess, timeoutMs) {
|
|
1451
|
+
let timeout;
|
|
1452
|
+
try {
|
|
1453
|
+
await Promise.race([
|
|
1454
|
+
activeProcess.donePromise,
|
|
1455
|
+
new Promise((resolve) => {
|
|
1456
|
+
timeout = setTimeout(resolve, timeoutMs);
|
|
1457
|
+
unrefTimer(timeout);
|
|
1458
|
+
}),
|
|
1459
|
+
]);
|
|
1460
|
+
}
|
|
1461
|
+
finally {
|
|
1462
|
+
if (timeout) {
|
|
1463
|
+
clearTimeout(timeout);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (activeProcess.waitError) {
|
|
1467
|
+
await activeProcess.stopOutputPumps?.();
|
|
1468
|
+
throw new SandboxProviderError('ModalSandboxClient failed to wait for process completion.', {
|
|
1469
|
+
provider: 'modal',
|
|
1470
|
+
operation: 'wait process',
|
|
1471
|
+
cause: modalErrorMessage(activeProcess.waitError),
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
function modalErrorMessage(error) {
|
|
1476
|
+
return error instanceof Error ? error.message : String(error);
|
|
1477
|
+
}
|
|
1478
|
+
function joinCommandOutput(activeProcess) {
|
|
1479
|
+
return [
|
|
1480
|
+
withDroppedOutputPrefix(activeProcess.stdout, activeProcess.droppedStdoutChars),
|
|
1481
|
+
withDroppedOutputPrefix(activeProcess.stderr, activeProcess.droppedStderrChars),
|
|
1482
|
+
]
|
|
1483
|
+
.filter((value) => value.trim().length > 0)
|
|
1484
|
+
.map((value) => value.trimEnd())
|
|
1485
|
+
.join('\n');
|
|
1486
|
+
}
|
|
1487
|
+
function consumeCommandOutput(activeProcess) {
|
|
1488
|
+
const output = joinCommandOutput(activeProcess);
|
|
1489
|
+
activeProcess.stdout = '';
|
|
1490
|
+
activeProcess.stderr = '';
|
|
1491
|
+
activeProcess.droppedStdoutChars = 0;
|
|
1492
|
+
activeProcess.droppedStderrChars = 0;
|
|
1493
|
+
return output;
|
|
1494
|
+
}
|
|
1495
|
+
function appendActiveProcessOutput(activeProcess, chunk, stream) {
|
|
1496
|
+
if (chunk.length === 0) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (stream === 'stdout') {
|
|
1500
|
+
const stdout = appendBoundedOutput(activeProcess.stdout, activeProcess.droppedStdoutChars, chunk);
|
|
1501
|
+
activeProcess.stdout = stdout.output;
|
|
1502
|
+
activeProcess.droppedStdoutChars = stdout.droppedChars;
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
const stderr = appendBoundedOutput(activeProcess.stderr, activeProcess.droppedStderrChars, chunk);
|
|
1506
|
+
activeProcess.stderr = stderr.output;
|
|
1507
|
+
activeProcess.droppedStderrChars = stderr.droppedChars;
|
|
1508
|
+
}
|
|
1509
|
+
function appendBoundedOutput(output, droppedChars, chunk) {
|
|
1510
|
+
const nextLength = output.length + chunk.length;
|
|
1511
|
+
if (nextLength <= MAX_ACTIVE_PROCESS_OUTPUT_CHARS) {
|
|
1512
|
+
return {
|
|
1513
|
+
output: output + chunk,
|
|
1514
|
+
droppedChars,
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
const nextDroppedChars = droppedChars + nextLength - MAX_ACTIVE_PROCESS_OUTPUT_CHARS;
|
|
1518
|
+
if (chunk.length >= MAX_ACTIVE_PROCESS_OUTPUT_CHARS) {
|
|
1519
|
+
return {
|
|
1520
|
+
output: chunk.slice(-MAX_ACTIVE_PROCESS_OUTPUT_CHARS),
|
|
1521
|
+
droppedChars: nextDroppedChars,
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
const existingCharsToKeep = MAX_ACTIVE_PROCESS_OUTPUT_CHARS - chunk.length;
|
|
1525
|
+
return {
|
|
1526
|
+
output: output.slice(-existingCharsToKeep) + chunk,
|
|
1527
|
+
droppedChars: nextDroppedChars,
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
function withDroppedOutputPrefix(output, droppedChars) {
|
|
1531
|
+
if (droppedChars === 0) {
|
|
1532
|
+
return output;
|
|
1533
|
+
}
|
|
1534
|
+
return `[...${droppedChars} characters truncated from process output...]\n${output}`;
|
|
1535
|
+
}
|
|
1536
|
+
async function withOptionalTimeout(promise, timeoutSeconds, message, onLateResolve) {
|
|
1537
|
+
if (typeof timeoutSeconds !== 'number') {
|
|
1538
|
+
return await promise;
|
|
1539
|
+
}
|
|
1540
|
+
return await withOptionalTimeoutMs(promise, timeoutSeconds * 1000, message, onLateResolve);
|
|
1541
|
+
}
|
|
1542
|
+
async function withOptionalTimeoutMs(promise, timeoutMs, message, onLateResolve) {
|
|
1543
|
+
if (typeof timeoutMs !== 'number') {
|
|
1544
|
+
return await promise;
|
|
1545
|
+
}
|
|
1546
|
+
let settled = false;
|
|
1547
|
+
let timeoutId;
|
|
1548
|
+
try {
|
|
1549
|
+
return await Promise.race([
|
|
1550
|
+
promise.then((value) => {
|
|
1551
|
+
settled = true;
|
|
1552
|
+
return value;
|
|
1553
|
+
}, (error) => {
|
|
1554
|
+
settled = true;
|
|
1555
|
+
throw error;
|
|
1556
|
+
}),
|
|
1557
|
+
new Promise((_, reject) => {
|
|
1558
|
+
timeoutId = setTimeout(() => {
|
|
1559
|
+
if (settled) {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
promise
|
|
1563
|
+
.then(async (value) => {
|
|
1564
|
+
await onLateResolve?.(value);
|
|
1565
|
+
})
|
|
1566
|
+
.catch(() => { });
|
|
1567
|
+
reject(new UserError(message));
|
|
1568
|
+
}, timeoutMs);
|
|
1569
|
+
}),
|
|
1570
|
+
]);
|
|
1571
|
+
}
|
|
1572
|
+
finally {
|
|
1573
|
+
if (timeoutId) {
|
|
1574
|
+
clearTimeout(timeoutId);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
async function terminateModalSandboxAfterTimeout(sandbox) {
|
|
1579
|
+
await sandbox.terminate().catch(() => { });
|
|
1580
|
+
}
|
|
1581
|
+
//# sourceMappingURL=sandbox.mjs.map
|