@mastra/docker 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/index.cjs +851 -823
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +825 -815
- package/dist/index.js.map +1 -1
- package/dist/sandbox/process-manager.d.ts.map +1 -1
- package/package.json +9 -8
package/dist/index.js
CHANGED
|
@@ -1,849 +1,859 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from
|
|
2
|
-
import {
|
|
3
|
-
import Docker from
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { isDeepStrictEqual } from "util";
|
|
2
|
+
import { MastraSandbox, ProcessHandle, SandboxError, SandboxNotReadyError, SandboxProcessManager } from "@mastra/core/workspace";
|
|
3
|
+
import Docker from "dockerode";
|
|
4
|
+
//#region src/sandbox/process-manager.ts
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a Docker exec instance to conform to Mastra's ProcessHandle.
|
|
7
|
+
* Not exported — internal to this module.
|
|
8
|
+
*
|
|
9
|
+
* Listener dispatch is handled by the base class. The manager's spawn()
|
|
10
|
+
* method wires Docker stream callbacks to handle.emitStdout/emitStderr.
|
|
11
|
+
*/
|
|
6
12
|
var DockerProcessHandle = class extends ProcessHandle {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
13
|
+
pid;
|
|
14
|
+
_exec;
|
|
15
|
+
_container;
|
|
16
|
+
_startTime;
|
|
17
|
+
_exitCode;
|
|
18
|
+
/** @internal Set by kill() and timeout to distinguish forced termination from natural exit */
|
|
19
|
+
_killed = false;
|
|
20
|
+
/** @internal Set by the timeout path to distinguish timeout kills from explicit kills */
|
|
21
|
+
_timedOut = false;
|
|
22
|
+
_waitPromise = null;
|
|
23
|
+
_stdinStream = null;
|
|
24
|
+
_execStream = null;
|
|
25
|
+
constructor(exec, container, startTime, stdinStream, options) {
|
|
26
|
+
super(options);
|
|
27
|
+
this.pid = exec.id;
|
|
28
|
+
this._exec = exec;
|
|
29
|
+
this._container = container;
|
|
30
|
+
this._startTime = startTime;
|
|
31
|
+
this._stdinStream = stdinStream;
|
|
32
|
+
}
|
|
33
|
+
get exitCode() {
|
|
34
|
+
return this._exitCode;
|
|
35
|
+
}
|
|
36
|
+
/** @internal Set exit code when stream closes */
|
|
37
|
+
_setExitCode(code) {
|
|
38
|
+
this._exitCode = code;
|
|
39
|
+
}
|
|
40
|
+
/** @internal Set the wait promise from spawn */
|
|
41
|
+
_setWaitPromise(p) {
|
|
42
|
+
this._waitPromise = p;
|
|
43
|
+
}
|
|
44
|
+
/** @internal Set the exec stream so kill() can destroy it */
|
|
45
|
+
_setExecStream(stream) {
|
|
46
|
+
this._execStream = stream;
|
|
47
|
+
}
|
|
48
|
+
async wait() {
|
|
49
|
+
if (this._waitPromise) return this._waitPromise;
|
|
50
|
+
const info = await this._inspectExec();
|
|
51
|
+
return {
|
|
52
|
+
success: (info.ExitCode ?? 1) === 0,
|
|
53
|
+
exitCode: info.ExitCode ?? 1,
|
|
54
|
+
stdout: this.stdout,
|
|
55
|
+
stderr: this.stderr,
|
|
56
|
+
executionTimeMs: Date.now() - this._startTime
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async kill() {
|
|
60
|
+
if (this._exitCode !== void 0) return false;
|
|
61
|
+
try {
|
|
62
|
+
let info = await this._inspectExec();
|
|
63
|
+
if (!info.Running || !info.Pid) {
|
|
64
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
65
|
+
info = await this._inspectExec();
|
|
66
|
+
}
|
|
67
|
+
if (!info.Running) {
|
|
68
|
+
this._killed = true;
|
|
69
|
+
this._destroyStream();
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const pid = info.Pid;
|
|
73
|
+
if (!pid) {
|
|
74
|
+
this._killed = true;
|
|
75
|
+
this._destroyStream();
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
await (await this._container.exec({
|
|
79
|
+
Cmd: [
|
|
80
|
+
"sh",
|
|
81
|
+
"-c",
|
|
82
|
+
`kill -9 -${pid} 2>/dev/null || kill -9 ${pid}`
|
|
83
|
+
],
|
|
84
|
+
AttachStdout: false,
|
|
85
|
+
AttachStderr: false
|
|
86
|
+
})).start({});
|
|
87
|
+
this._killed = true;
|
|
88
|
+
this._destroyStream();
|
|
89
|
+
return true;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this._killed = true;
|
|
92
|
+
this._destroyStream();
|
|
93
|
+
const msg = error instanceof Error ? error.message.toLowerCase() : "";
|
|
94
|
+
if (!msg.includes("no such process") && !msg.includes("esrch")) console.warn(`[DockerProcessManager] kill(${this.pid}) failed unexpectedly:`, error);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async sendStdin(data) {
|
|
99
|
+
if (this._exitCode !== void 0) throw new Error(`Process ${this.pid} has already exited with code ${this._exitCode}`);
|
|
100
|
+
if (!this._stdinStream) throw new Error(`Process ${this.pid} was not started with stdin support`);
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
this._stdinStream.write(data, (error) => error ? reject(error) : resolve());
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async closeStdin() {
|
|
106
|
+
if (this._exitCode !== void 0) throw new Error(`Process ${this.pid} has already exited with code ${this._exitCode}`);
|
|
107
|
+
if (!this._stdinStream) throw new Error(`Process ${this.pid} was not started with stdin support`);
|
|
108
|
+
const stream = this._stdinStream;
|
|
109
|
+
if (stream.writableEnded) return;
|
|
110
|
+
await new Promise((resolve) => stream.end(resolve));
|
|
111
|
+
}
|
|
112
|
+
/** @internal Force-close the exec stream to unblock wait(). */
|
|
113
|
+
_destroyStream() {
|
|
114
|
+
const stream = this._execStream;
|
|
115
|
+
if (stream && typeof stream.destroy === "function") {
|
|
116
|
+
stream.destroy();
|
|
117
|
+
this._execStream = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async _inspectExec() {
|
|
121
|
+
return this._exec.inspect();
|
|
122
|
+
}
|
|
113
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* Docker implementation of SandboxProcessManager.
|
|
126
|
+
* Uses `container.exec()` with stream-based I/O.
|
|
127
|
+
*/
|
|
114
128
|
var DockerProcessManager = class extends SandboxProcessManager {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return results;
|
|
257
|
-
}
|
|
129
|
+
_container = null;
|
|
130
|
+
_defaultTimeout;
|
|
131
|
+
constructor(options) {
|
|
132
|
+
super(options);
|
|
133
|
+
this._defaultTimeout = options.defaultTimeout ?? 0;
|
|
134
|
+
}
|
|
135
|
+
/** @internal Called by DockerSandbox after container is ready */
|
|
136
|
+
setContainer(container) {
|
|
137
|
+
this._container = container;
|
|
138
|
+
}
|
|
139
|
+
/** Get the container, throwing if not set */
|
|
140
|
+
get container() {
|
|
141
|
+
if (!this._container) throw new Error("Docker container not available. Has the sandbox been started?");
|
|
142
|
+
return this._container;
|
|
143
|
+
}
|
|
144
|
+
async spawn(command, options = {}) {
|
|
145
|
+
const container = this.container;
|
|
146
|
+
const mergedEnv = {
|
|
147
|
+
...this.env,
|
|
148
|
+
...options.env
|
|
149
|
+
};
|
|
150
|
+
const envArray = Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0).map(([k, v]) => `${k}=${v}`);
|
|
151
|
+
const exec = await container.exec({
|
|
152
|
+
Cmd: [
|
|
153
|
+
"sh",
|
|
154
|
+
"-c",
|
|
155
|
+
command
|
|
156
|
+
],
|
|
157
|
+
AttachStdout: true,
|
|
158
|
+
AttachStderr: true,
|
|
159
|
+
AttachStdin: true,
|
|
160
|
+
Tty: false,
|
|
161
|
+
Env: envArray.length > 0 ? envArray : void 0,
|
|
162
|
+
WorkingDir: options.cwd
|
|
163
|
+
});
|
|
164
|
+
const stream = await exec.start({
|
|
165
|
+
hijack: true,
|
|
166
|
+
stdin: true
|
|
167
|
+
});
|
|
168
|
+
const startTime = Date.now();
|
|
169
|
+
const handle = new DockerProcessHandle(exec, container, startTime, stream, options);
|
|
170
|
+
handle._setExecStream(stream);
|
|
171
|
+
const waitPromise = new Promise((resolve) => {
|
|
172
|
+
const buffer = [];
|
|
173
|
+
stream.on("data", (chunk) => {
|
|
174
|
+
buffer.push(chunk);
|
|
175
|
+
let combined = Buffer.concat(buffer);
|
|
176
|
+
buffer.length = 0;
|
|
177
|
+
while (combined.length >= 8) {
|
|
178
|
+
const type = combined[0];
|
|
179
|
+
const size = combined.readUInt32BE(4);
|
|
180
|
+
if (combined.length < 8 + size) {
|
|
181
|
+
buffer.push(combined);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
const payload = combined.subarray(8, 8 + size).toString("utf-8");
|
|
185
|
+
if (type === 1) handle.emitStdout(payload);
|
|
186
|
+
else if (type === 2) handle.emitStderr(payload);
|
|
187
|
+
combined = combined.subarray(8 + size);
|
|
188
|
+
}
|
|
189
|
+
if (combined.length > 0 && buffer.length === 0) buffer.push(combined);
|
|
190
|
+
});
|
|
191
|
+
stream.on("end", async () => {
|
|
192
|
+
try {
|
|
193
|
+
const exitCode = (await exec.inspect()).ExitCode ?? 1;
|
|
194
|
+
handle._setExitCode(exitCode);
|
|
195
|
+
resolve({
|
|
196
|
+
success: exitCode === 0,
|
|
197
|
+
exitCode,
|
|
198
|
+
stdout: handle.stdout,
|
|
199
|
+
stderr: handle.stderr,
|
|
200
|
+
executionTimeMs: Date.now() - startTime
|
|
201
|
+
});
|
|
202
|
+
} catch {
|
|
203
|
+
handle._setExitCode(1);
|
|
204
|
+
resolve({
|
|
205
|
+
success: false,
|
|
206
|
+
exitCode: 1,
|
|
207
|
+
stdout: handle.stdout,
|
|
208
|
+
stderr: handle.stderr,
|
|
209
|
+
executionTimeMs: Date.now() - startTime
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
stream.on("close", () => {
|
|
214
|
+
if (handle.exitCode !== void 0) return;
|
|
215
|
+
if (!handle._killed) return;
|
|
216
|
+
handle._setExitCode(137);
|
|
217
|
+
resolve({
|
|
218
|
+
success: false,
|
|
219
|
+
exitCode: 137,
|
|
220
|
+
stdout: handle.stdout,
|
|
221
|
+
stderr: handle.stderr,
|
|
222
|
+
executionTimeMs: Date.now() - startTime,
|
|
223
|
+
killed: true,
|
|
224
|
+
timedOut: handle._timedOut
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
stream.on("error", () => {
|
|
228
|
+
if (handle.exitCode !== void 0) return;
|
|
229
|
+
handle._setExitCode(1);
|
|
230
|
+
resolve({
|
|
231
|
+
success: false,
|
|
232
|
+
exitCode: 1,
|
|
233
|
+
stdout: handle.stdout,
|
|
234
|
+
stderr: handle.stderr || "Stream error",
|
|
235
|
+
executionTimeMs: Date.now() - startTime
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
const resolvedTimeout = options.timeout ?? this._defaultTimeout;
|
|
240
|
+
if (resolvedTimeout > 0) {
|
|
241
|
+
const timer = setTimeout(() => {
|
|
242
|
+
if (handle.exitCode === void 0) {
|
|
243
|
+
handle._killed = true;
|
|
244
|
+
handle._timedOut = true;
|
|
245
|
+
handle.kill().catch(() => {});
|
|
246
|
+
handle._destroyStream();
|
|
247
|
+
}
|
|
248
|
+
}, resolvedTimeout);
|
|
249
|
+
waitPromise.then(() => clearTimeout(timer));
|
|
250
|
+
}
|
|
251
|
+
handle._setWaitPromise(waitPromise);
|
|
252
|
+
this._tracked.set(handle.pid, handle);
|
|
253
|
+
return handle;
|
|
254
|
+
}
|
|
255
|
+
/** Clear all tracked process handles and release the container reference (e.g., after container stop/destroy) */
|
|
256
|
+
reset() {
|
|
257
|
+
this._tracked.clear();
|
|
258
|
+
this._container = null;
|
|
259
|
+
}
|
|
260
|
+
async list() {
|
|
261
|
+
const results = [];
|
|
262
|
+
for (const [pid, handle] of this._tracked) results.push({
|
|
263
|
+
pid,
|
|
264
|
+
command: handle.command,
|
|
265
|
+
running: handle.exitCode === void 0,
|
|
266
|
+
exitCode: handle.exitCode
|
|
267
|
+
});
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
258
270
|
};
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/sandbox/index.ts
|
|
273
|
+
/**
|
|
274
|
+
* Docker Sandbox Provider
|
|
275
|
+
*
|
|
276
|
+
* A Docker-based sandbox implementation that uses long-lived containers
|
|
277
|
+
* with `docker exec` for command execution. Targets local development,
|
|
278
|
+
* CI/CD, air-gapped deployments, and cost-sensitive scenarios where
|
|
279
|
+
* cloud sandboxes are overkill.
|
|
280
|
+
*
|
|
281
|
+
* @see https://docs.docker.com/engine/api/
|
|
282
|
+
*/
|
|
283
|
+
const LOG_PREFIX = "[DockerSandbox]";
|
|
284
|
+
/**
|
|
285
|
+
* Docker sandbox implementation using long-lived containers.
|
|
286
|
+
*
|
|
287
|
+
* Features:
|
|
288
|
+
* - Long-lived container with `docker exec` for commands
|
|
289
|
+
* - Bind mount support via Docker volumes
|
|
290
|
+
* - Reconnection to existing containers by ID/name
|
|
291
|
+
* - Container label tracking for discovery
|
|
292
|
+
*
|
|
293
|
+
* @example Basic usage
|
|
294
|
+
* ```typescript
|
|
295
|
+
* import { Workspace } from '@mastra/core/workspace';
|
|
296
|
+
* import { DockerSandbox } from '@mastra/docker';
|
|
297
|
+
*
|
|
298
|
+
* const sandbox = new DockerSandbox({
|
|
299
|
+
* image: 'node:22-slim',
|
|
300
|
+
* timeout: 60000,
|
|
301
|
+
* });
|
|
302
|
+
*
|
|
303
|
+
* const workspace = new Workspace({ sandbox });
|
|
304
|
+
* const result = await workspace.executeCode('console.log("Hello!")');
|
|
305
|
+
* ```
|
|
306
|
+
*
|
|
307
|
+
* @example With bind mounts
|
|
308
|
+
* ```typescript
|
|
309
|
+
* const sandbox = new DockerSandbox({
|
|
310
|
+
* image: 'node:22-slim',
|
|
311
|
+
* volumes: { '/my/project': '/workspace/project' },
|
|
312
|
+
* });
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
var DockerSandbox = class DockerSandbox extends MastraSandbox {
|
|
316
|
+
id;
|
|
317
|
+
name = "DockerSandbox";
|
|
318
|
+
provider = "docker";
|
|
319
|
+
status = "pending";
|
|
320
|
+
/** Underlying Docker client */
|
|
321
|
+
_docker;
|
|
322
|
+
/** Container reference (set after start) */
|
|
323
|
+
_container = null;
|
|
324
|
+
/** Configuration */
|
|
325
|
+
_containerName;
|
|
326
|
+
_image;
|
|
327
|
+
_command;
|
|
328
|
+
_env;
|
|
329
|
+
_volumes;
|
|
330
|
+
_network;
|
|
331
|
+
_privileged;
|
|
332
|
+
_privilegedWasSet;
|
|
333
|
+
_memory;
|
|
334
|
+
_memorySwap;
|
|
335
|
+
_cpuShares;
|
|
336
|
+
_cpuQuota;
|
|
337
|
+
_cpuPeriod;
|
|
338
|
+
_pidsLimit;
|
|
339
|
+
_readonlyRootfs;
|
|
340
|
+
_capDrop;
|
|
341
|
+
_capAdd;
|
|
342
|
+
_securityOpt;
|
|
343
|
+
_ulimits;
|
|
344
|
+
_tmpfs;
|
|
345
|
+
_workingDir;
|
|
346
|
+
_labels;
|
|
347
|
+
_instructionsOverride;
|
|
348
|
+
_constructorOptions;
|
|
349
|
+
constructor(options = {}) {
|
|
350
|
+
const processManager = new DockerProcessManager({
|
|
351
|
+
env: options.env ?? {},
|
|
352
|
+
defaultTimeout: options.timeout ?? 3e5
|
|
353
|
+
});
|
|
354
|
+
super({
|
|
355
|
+
...options,
|
|
356
|
+
name: "DockerSandbox",
|
|
357
|
+
processes: processManager
|
|
358
|
+
});
|
|
359
|
+
this.id = options.id ?? this._generateId();
|
|
360
|
+
this._containerName = sanitizeContainerName(options.name ?? this.id);
|
|
361
|
+
this._image = options.image ?? "node:22-slim";
|
|
362
|
+
this._command = options.command ?? ["sleep", "infinity"];
|
|
363
|
+
this._env = options.env ?? {};
|
|
364
|
+
this._volumes = options.volumes ?? {};
|
|
365
|
+
this._network = options.network;
|
|
366
|
+
this._privileged = options.privileged ?? false;
|
|
367
|
+
this._privilegedWasSet = options.privileged !== void 0;
|
|
368
|
+
this._memory = options.memory;
|
|
369
|
+
this._memorySwap = options.memorySwap;
|
|
370
|
+
this._cpuShares = options.cpuShares;
|
|
371
|
+
this._cpuQuota = options.cpuQuota;
|
|
372
|
+
this._cpuPeriod = options.cpuPeriod;
|
|
373
|
+
this._pidsLimit = options.pidsLimit;
|
|
374
|
+
this._readonlyRootfs = options.readonlyRootfs;
|
|
375
|
+
this._capDrop = options.capDrop;
|
|
376
|
+
this._capAdd = options.capAdd;
|
|
377
|
+
this._securityOpt = options.securityOpt;
|
|
378
|
+
this._ulimits = options.ulimits;
|
|
379
|
+
this._tmpfs = options.tmpfs;
|
|
380
|
+
this._workingDir = options.workingDir ?? "/workspace";
|
|
381
|
+
this._labels = {
|
|
382
|
+
...options.labels,
|
|
383
|
+
"mastra.sandbox": "true",
|
|
384
|
+
"mastra.sandbox.id": this.id
|
|
385
|
+
};
|
|
386
|
+
this._instructionsOverride = options.instructions;
|
|
387
|
+
this._docker = new Docker(options.dockerOptions);
|
|
388
|
+
this._constructorOptions = { ...options };
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Construct a sibling `DockerSandbox` that inherits this sandbox's
|
|
392
|
+
* configuration (image, resource limits, security options, labels,
|
|
393
|
+
* connection options) with per-instance overrides.
|
|
394
|
+
*
|
|
395
|
+
* Performs no I/O — the sandbox clone provisions (or reconnects to an
|
|
396
|
+
* existing container labelled with the same logical `id`) on its own
|
|
397
|
+
* `start()`. Use it when one configured sandbox acts as the template for a
|
|
398
|
+
* fleet of independent sandboxes (e.g. one per project).
|
|
399
|
+
*
|
|
400
|
+
* `options.idleTimeoutMinutes` is ignored (Docker containers have no
|
|
401
|
+
* provider-side idle teardown; `timeout` here is a command timeout), and
|
|
402
|
+
* `options.sandboxId` is ignored because reconnection is by logical `id`.
|
|
403
|
+
*/
|
|
404
|
+
clone(options = {}) {
|
|
405
|
+
const { id: _id, name: _name, ...base } = this._constructorOptions;
|
|
406
|
+
return new DockerSandbox({
|
|
407
|
+
...base,
|
|
408
|
+
...options.id !== void 0 && { id: options.id },
|
|
409
|
+
...options.env !== void 0 && { env: options.env }
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get the underlying Docker container for direct access.
|
|
414
|
+
* @throws {SandboxNotReadyError} If the sandbox has not been started.
|
|
415
|
+
*/
|
|
416
|
+
get container() {
|
|
417
|
+
if (!this._container) throw new SandboxNotReadyError(this.id);
|
|
418
|
+
return this._container;
|
|
419
|
+
}
|
|
420
|
+
async start() {
|
|
421
|
+
this.logger.debug(`${LOG_PREFIX} Starting sandbox ${this.id}...`);
|
|
422
|
+
const existing = await this._findExistingContainer();
|
|
423
|
+
if (existing) {
|
|
424
|
+
this.logger.debug(`${LOG_PREFIX} Found existing container ${existing.Id}`);
|
|
425
|
+
this._container = this._docker.getContainer(existing.Id);
|
|
426
|
+
const info = await this._container.inspect();
|
|
427
|
+
this._warnOnPrivilegedHardeningConflict(info.HostConfig?.Privileged ?? this._privileged);
|
|
428
|
+
this._warnOnReconnectedHostConfigMismatch(existing.Id, info.HostConfig);
|
|
429
|
+
const actualState = info.State?.Running ? "running" : "stopped";
|
|
430
|
+
if (actualState !== "running") {
|
|
431
|
+
this.logger.debug(`${LOG_PREFIX} Container exists but not running (${actualState}), starting...`);
|
|
432
|
+
await this._container.start();
|
|
433
|
+
}
|
|
434
|
+
this.processes.setContainer(this._container);
|
|
435
|
+
this.logger.debug(`${LOG_PREFIX} Reconnected to container ${existing.Id}`);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this._warnOnPrivilegedHardeningConflict(this._privileged);
|
|
439
|
+
await this._ensureImage();
|
|
440
|
+
const envArray = Object.entries(this._env).map(([k, v]) => `${k}=${v}`);
|
|
441
|
+
const binds = Object.entries(this._volumes).map(([host, container]) => `${host}:${container}`);
|
|
442
|
+
this.logger.debug(`${LOG_PREFIX} Creating container with image ${this._image}...`);
|
|
443
|
+
this._container = await this._docker.createContainer({
|
|
444
|
+
name: this._containerName,
|
|
445
|
+
Image: this._image,
|
|
446
|
+
Cmd: this._command,
|
|
447
|
+
Env: envArray,
|
|
448
|
+
WorkingDir: this._workingDir,
|
|
449
|
+
Labels: this._labels,
|
|
450
|
+
HostConfig: {
|
|
451
|
+
Binds: binds.length > 0 ? binds : void 0,
|
|
452
|
+
NetworkMode: this._network,
|
|
453
|
+
Privileged: this._privileged,
|
|
454
|
+
Memory: this._memory,
|
|
455
|
+
MemorySwap: this._memorySwap,
|
|
456
|
+
CpuShares: this._cpuShares,
|
|
457
|
+
CpuQuota: this._cpuQuota,
|
|
458
|
+
CpuPeriod: this._cpuPeriod,
|
|
459
|
+
PidsLimit: this._pidsLimit,
|
|
460
|
+
ReadonlyRootfs: this._readonlyRootfs,
|
|
461
|
+
CapDrop: this._capDrop,
|
|
462
|
+
CapAdd: this._capAdd,
|
|
463
|
+
SecurityOpt: this._securityOpt,
|
|
464
|
+
Ulimits: this._ulimits?.map(toDockerUlimit),
|
|
465
|
+
Tmpfs: this._tmpfs
|
|
466
|
+
},
|
|
467
|
+
OpenStdin: true,
|
|
468
|
+
Tty: false
|
|
469
|
+
});
|
|
470
|
+
await this._container.start();
|
|
471
|
+
this.processes.setContainer(this._container);
|
|
472
|
+
this.logger.debug(`${LOG_PREFIX} Container started: ${this._container.id}`);
|
|
473
|
+
}
|
|
474
|
+
_warnOnPrivilegedHardeningConflict(effectivePrivileged) {
|
|
475
|
+
if (!effectivePrivileged) return;
|
|
476
|
+
const conflictedHostConfigFields = [
|
|
477
|
+
this._capDrop && this._capDrop.length > 0 ? "CapDrop" : void 0,
|
|
478
|
+
this._capAdd && this._capAdd.length > 0 ? "CapAdd" : void 0,
|
|
479
|
+
this._securityOpt && this._securityOpt.length > 0 ? "SecurityOpt" : void 0
|
|
480
|
+
].filter((field) => field !== void 0);
|
|
481
|
+
if (conflictedHostConfigFields.length === 0) return;
|
|
482
|
+
const optionNames = conflictedHostConfigFields.map(toDockerSandboxOptionName);
|
|
483
|
+
this.logger.warn(`${LOG_PREFIX} Privileged containers can bypass some requested hardening controls: ${optionNames.join(", ")}`, {
|
|
484
|
+
fields: optionNames,
|
|
485
|
+
hostConfigFields: conflictedHostConfigFields
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
_warnOnReconnectedHostConfigMismatch(containerId, hostConfig) {
|
|
489
|
+
if (!hostConfig) return;
|
|
490
|
+
const mismatchedHostConfigFields = this._requestedHardeningHostConfigEntries(hostConfig).filter(([field, requestedValue]) => !isHostConfigValueEqual(field, hostConfig[field], requestedValue)).map(([field]) => field);
|
|
491
|
+
if (mismatchedHostConfigFields.length === 0) return;
|
|
492
|
+
if (!this._privilegedWasSet && hostConfig.Privileged === true && mismatchedHostConfigFields.includes("Privileged")) this.logger.warn(`${LOG_PREFIX} Reconnected to existing container ${containerId}; the existing container is privileged, but this DockerSandbox did not request privileged mode. Destroy and recreate the sandbox to apply the default non-privileged mode.`, {
|
|
493
|
+
containerId,
|
|
494
|
+
fields: ["privileged"],
|
|
495
|
+
hostConfigFields: ["Privileged"]
|
|
496
|
+
});
|
|
497
|
+
const remainingMismatchedHostConfigFields = mismatchedHostConfigFields.filter((field) => field !== "Privileged" || this._privilegedWasSet);
|
|
498
|
+
if (remainingMismatchedHostConfigFields.length === 0) return;
|
|
499
|
+
const mismatchedOptions = remainingMismatchedHostConfigFields.map(toDockerSandboxOptionName);
|
|
500
|
+
this.logger.warn(`${LOG_PREFIX} Reconnected to existing container ${containerId}; requested Docker option(s) ${mismatchedOptions.join(", ")} differ from inspected HostConfig field(s) ${remainingMismatchedHostConfigFields.join(", ")} and cannot be applied to the existing container. Destroy and recreate the sandbox to apply them.`, {
|
|
501
|
+
containerId,
|
|
502
|
+
fields: mismatchedOptions,
|
|
503
|
+
hostConfigFields: remainingMismatchedHostConfigFields
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
_requestedHardeningHostConfigEntries(hostConfig) {
|
|
507
|
+
const entries = [
|
|
508
|
+
["Memory", this._memory],
|
|
509
|
+
["MemorySwap", this._memorySwap],
|
|
510
|
+
["CpuShares", this._cpuShares],
|
|
511
|
+
["CpuQuota", this._cpuQuota],
|
|
512
|
+
["CpuPeriod", this._cpuPeriod],
|
|
513
|
+
["PidsLimit", this._pidsLimit],
|
|
514
|
+
["ReadonlyRootfs", this._readonlyRootfs],
|
|
515
|
+
["CapDrop", this._capDrop],
|
|
516
|
+
["CapAdd", this._capAdd],
|
|
517
|
+
["SecurityOpt", this._securityOpt],
|
|
518
|
+
["Ulimits", this._ulimits],
|
|
519
|
+
["Tmpfs", this._tmpfs]
|
|
520
|
+
];
|
|
521
|
+
if (this._privilegedWasSet || hostConfig?.Privileged === true) entries.unshift(["Privileged", this._privileged]);
|
|
522
|
+
return entries.filter((entry) => isPresentHostConfigValue(entry[1]));
|
|
523
|
+
}
|
|
524
|
+
async stop() {
|
|
525
|
+
const container = await this._resolveContainer();
|
|
526
|
+
if (!container) return;
|
|
527
|
+
this.logger.debug(`${LOG_PREFIX} Stopping container ${container.id}...`);
|
|
528
|
+
try {
|
|
529
|
+
await container.stop({ t: 10 });
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (!isContainerNotRunningError(error)) throw error;
|
|
532
|
+
}
|
|
533
|
+
this.processes.reset();
|
|
534
|
+
this.logger.debug(`${LOG_PREFIX} Container stopped`);
|
|
535
|
+
}
|
|
536
|
+
async destroy() {
|
|
537
|
+
const container = await this._resolveContainer();
|
|
538
|
+
if (!container) return;
|
|
539
|
+
this.logger.debug(`${LOG_PREFIX} Destroying container ${container.id}...`);
|
|
540
|
+
try {
|
|
541
|
+
await container.remove({
|
|
542
|
+
force: true,
|
|
543
|
+
v: true
|
|
544
|
+
});
|
|
545
|
+
} catch (error) {
|
|
546
|
+
if (!isContainerNotFoundError(error)) throw error;
|
|
547
|
+
}
|
|
548
|
+
this.processes.reset();
|
|
549
|
+
this._container = null;
|
|
550
|
+
this.logger.debug(`${LOG_PREFIX} Container destroyed`);
|
|
551
|
+
}
|
|
552
|
+
getInstructions(opts) {
|
|
553
|
+
const defaultInstructions = [
|
|
554
|
+
`You are working inside a Docker container (image: ${this._image}).`,
|
|
555
|
+
`The working directory is ${this._workingDir}.`,
|
|
556
|
+
"You can execute shell commands using executeCommand().",
|
|
557
|
+
"You can spawn background processes using processes.spawn()."
|
|
558
|
+
].join("\n");
|
|
559
|
+
if (this._instructionsOverride === void 0) return defaultInstructions;
|
|
560
|
+
if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
|
|
561
|
+
return this._instructionsOverride({
|
|
562
|
+
defaultInstructions,
|
|
563
|
+
requestContext: opts?.requestContext
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
async getInfo() {
|
|
567
|
+
const info = {
|
|
568
|
+
id: this.id,
|
|
569
|
+
name: this.name,
|
|
570
|
+
provider: this.provider,
|
|
571
|
+
status: this.status,
|
|
572
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
573
|
+
metadata: {
|
|
574
|
+
image: this._image,
|
|
575
|
+
workingDir: this._workingDir,
|
|
576
|
+
labels: this._labels
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
if (this._container) try {
|
|
580
|
+
const inspect = await this._container.inspect();
|
|
581
|
+
info.createdAt = new Date(inspect.Created);
|
|
582
|
+
info.metadata = {
|
|
583
|
+
...info.metadata,
|
|
584
|
+
containerId: inspect.Id,
|
|
585
|
+
containerName: inspect.Name,
|
|
586
|
+
state: inspect.State.Status
|
|
587
|
+
};
|
|
588
|
+
} catch {}
|
|
589
|
+
return info;
|
|
590
|
+
}
|
|
591
|
+
_generateId() {
|
|
592
|
+
return `docker-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Resolve the container reference, looking up by label if `_container` is unset.
|
|
596
|
+
* This ensures `stop()` and `destroy()` work even when the instance was created
|
|
597
|
+
* with an existing container's ID but `start()` was never called.
|
|
598
|
+
*/
|
|
599
|
+
async _resolveContainer() {
|
|
600
|
+
if (this._container) return this._container;
|
|
601
|
+
const existing = await this._findExistingContainer();
|
|
602
|
+
if (!existing) return null;
|
|
603
|
+
this._container = this._docker.getContainer(existing.Id);
|
|
604
|
+
return this._container;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Find an existing container matching this sandbox's ID via labels.
|
|
608
|
+
*/
|
|
609
|
+
async _findExistingContainer() {
|
|
610
|
+
try {
|
|
611
|
+
return (await this._docker.listContainers({
|
|
612
|
+
all: true,
|
|
613
|
+
filters: { label: [`mastra.sandbox.id=${this.id}`] }
|
|
614
|
+
}))[0] ?? null;
|
|
615
|
+
} catch (error) {
|
|
616
|
+
this.logger.debug(`${LOG_PREFIX} Failed to list containers: ${error instanceof Error ? error.message : String(error)}`);
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Ensure the Docker image is available locally. Pulls if needed.
|
|
622
|
+
*/
|
|
623
|
+
async _ensureImage() {
|
|
624
|
+
try {
|
|
625
|
+
await this._docker.getImage(this._image).inspect();
|
|
626
|
+
this.logger.debug(`${LOG_PREFIX} Image ${this._image} available locally`);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (!isImageNotFoundError(error)) throw error;
|
|
629
|
+
this.logger.debug(`${LOG_PREFIX} Pulling image ${this._image}...`);
|
|
630
|
+
try {
|
|
631
|
+
const stream = await this._docker.pull(this._image);
|
|
632
|
+
await new Promise((resolve, reject) => {
|
|
633
|
+
this._docker.modem.followProgress(stream, (err) => {
|
|
634
|
+
if (err) reject(err);
|
|
635
|
+
else resolve();
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
this.logger.debug(`${LOG_PREFIX} Image ${this._image} pulled successfully`);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
throw new SandboxError(`Failed to pull Docker image '${this._image}': ${error instanceof Error ? error.message : String(error)}`, "NOT_READY", {
|
|
641
|
+
image: this._image,
|
|
642
|
+
reason: "image_pull_failed"
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
626
647
|
};
|
|
627
648
|
function sanitizeContainerName(value) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
649
|
+
const replaced = value.replace(/[^a-zA-Z0-9_.-]/g, "-");
|
|
650
|
+
const withLeading = /^[a-zA-Z0-9]/.test(replaced) ? replaced : `s-${replaced}`;
|
|
651
|
+
return withLeading.length >= 2 ? withLeading : `${withLeading}-sandbox`;
|
|
631
652
|
}
|
|
632
653
|
function isContainerNotRunningError(error) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
return false;
|
|
654
|
+
if (error instanceof Error) return error.message.includes("is not running") || error.message.includes("container already stopped");
|
|
655
|
+
return false;
|
|
637
656
|
}
|
|
638
657
|
function isContainerNotFoundError(error) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
658
|
+
if (error instanceof Error) {
|
|
659
|
+
const msg = error.message.toLowerCase();
|
|
660
|
+
return msg.includes("no such container") || msg.includes("removal") && msg.includes("is already in progress");
|
|
661
|
+
}
|
|
662
|
+
return false;
|
|
644
663
|
}
|
|
645
664
|
function isImageNotFoundError(error) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
return false;
|
|
665
|
+
if (error instanceof Error) return error.message.toLowerCase().includes("no such image");
|
|
666
|
+
return false;
|
|
650
667
|
}
|
|
651
668
|
function isHostConfigValueEqual(field, actual, expected) {
|
|
652
|
-
|
|
669
|
+
return isDeepStrictEqual(normalizeHostConfigValue(field, actual), normalizeHostConfigValue(field, expected));
|
|
653
670
|
}
|
|
654
671
|
function normalizeHostConfigValue(field, value) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
return normalizeUlimit(value);
|
|
676
|
-
}
|
|
677
|
-
if (value && typeof value === "object") {
|
|
678
|
-
return Object.fromEntries(
|
|
679
|
-
Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, nestedValue]) => [key, normalizeHostConfigValue(field, nestedValue)])
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
return value;
|
|
672
|
+
if (value == null) return void 0;
|
|
673
|
+
if ((field === "CapAdd" || field === "CapDrop") && Array.isArray(value)) {
|
|
674
|
+
if (value.length === 0) return void 0;
|
|
675
|
+
return value.map(normalizeCapability).sort();
|
|
676
|
+
}
|
|
677
|
+
if (field === "SecurityOpt" && Array.isArray(value)) {
|
|
678
|
+
if (value.length === 0) return void 0;
|
|
679
|
+
return value.map(normalizeSecurityOpt).sort();
|
|
680
|
+
}
|
|
681
|
+
if (field === "Tmpfs" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
682
|
+
if (Object.keys(value).length === 0) return void 0;
|
|
683
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([path, options]) => [path, typeof options === "string" ? normalizeTmpfsOptions(options) : options]));
|
|
684
|
+
}
|
|
685
|
+
if (Array.isArray(value)) {
|
|
686
|
+
if (field === "Ulimits" && value.length === 0) return void 0;
|
|
687
|
+
return value.map((nestedValue) => normalizeHostConfigValue(field, nestedValue)).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
688
|
+
}
|
|
689
|
+
if (field === "Ulimits" && value && typeof value === "object") return normalizeUlimit(value);
|
|
690
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, nestedValue]) => [key, normalizeHostConfigValue(field, nestedValue)]));
|
|
691
|
+
return value;
|
|
683
692
|
}
|
|
684
693
|
function isPresentHostConfigValue(value) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
694
|
+
if (value == null) return false;
|
|
695
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
696
|
+
if (value && typeof value === "object") return Object.keys(value).length > 0;
|
|
697
|
+
return true;
|
|
689
698
|
}
|
|
690
699
|
function normalizeCapability(capability) {
|
|
691
|
-
|
|
700
|
+
return typeof capability === "string" ? capability.toUpperCase().replace(/^CAP_/, "") : capability;
|
|
692
701
|
}
|
|
693
702
|
function normalizeTmpfsOptions(options) {
|
|
694
|
-
|
|
703
|
+
return options.split(",").map((option) => option.trim()).filter(Boolean).sort().join(",");
|
|
695
704
|
}
|
|
696
705
|
function normalizeSecurityOpt(option) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
return option;
|
|
706
|
+
if (typeof option !== "string") return option;
|
|
707
|
+
const noNewPrivileges = option.match(/^no-new-privileges[:=](.+)$/i);
|
|
708
|
+
if (noNewPrivileges) return `no-new-privileges=${noNewPrivileges[1]}`;
|
|
709
|
+
return option;
|
|
703
710
|
}
|
|
704
711
|
function normalizeUlimit(value) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
712
|
+
const record = value;
|
|
713
|
+
return {
|
|
714
|
+
name: record.name ?? record.Name,
|
|
715
|
+
soft: record.soft ?? record.Soft,
|
|
716
|
+
hard: record.hard ?? record.Hard
|
|
717
|
+
};
|
|
711
718
|
}
|
|
712
719
|
function toDockerUlimit(ulimit) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
720
|
+
return {
|
|
721
|
+
Name: ulimit.name,
|
|
722
|
+
Soft: ulimit.soft,
|
|
723
|
+
Hard: ulimit.hard
|
|
724
|
+
};
|
|
718
725
|
}
|
|
719
726
|
function toDockerSandboxOptionName(field) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
return optionNames[field] ?? String(field);
|
|
727
|
+
return {
|
|
728
|
+
Privileged: "privileged",
|
|
729
|
+
Memory: "memory",
|
|
730
|
+
MemorySwap: "memorySwap",
|
|
731
|
+
CpuShares: "cpuShares",
|
|
732
|
+
CpuQuota: "cpuQuota",
|
|
733
|
+
CpuPeriod: "cpuPeriod",
|
|
734
|
+
PidsLimit: "pidsLimit",
|
|
735
|
+
ReadonlyRootfs: "readonlyRootfs",
|
|
736
|
+
CapDrop: "capDrop",
|
|
737
|
+
CapAdd: "capAdd",
|
|
738
|
+
SecurityOpt: "securityOpt",
|
|
739
|
+
Ulimits: "ulimits",
|
|
740
|
+
Tmpfs: "tmpfs"
|
|
741
|
+
}[field] ?? String(field);
|
|
736
742
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region src/provider.ts
|
|
745
|
+
const dockerSandboxProvider = {
|
|
746
|
+
id: "docker",
|
|
747
|
+
name: "Docker Sandbox",
|
|
748
|
+
description: "Local container sandbox powered by Docker",
|
|
749
|
+
configSchema: {
|
|
750
|
+
type: "object",
|
|
751
|
+
properties: {
|
|
752
|
+
image: {
|
|
753
|
+
type: "string",
|
|
754
|
+
description: "Docker image to use",
|
|
755
|
+
default: "node:22-slim"
|
|
756
|
+
},
|
|
757
|
+
timeout: {
|
|
758
|
+
type: "number",
|
|
759
|
+
description: "Default command timeout in milliseconds",
|
|
760
|
+
default: 3e5
|
|
761
|
+
},
|
|
762
|
+
env: {
|
|
763
|
+
type: "object",
|
|
764
|
+
description: "Environment variables",
|
|
765
|
+
additionalProperties: { type: "string" }
|
|
766
|
+
},
|
|
767
|
+
volumes: {
|
|
768
|
+
type: "object",
|
|
769
|
+
description: "Host-to-container bind mounts (host path → container path)",
|
|
770
|
+
additionalProperties: { type: "string" }
|
|
771
|
+
},
|
|
772
|
+
network: {
|
|
773
|
+
type: "string",
|
|
774
|
+
description: "Docker network to join"
|
|
775
|
+
},
|
|
776
|
+
workingDir: {
|
|
777
|
+
type: "string",
|
|
778
|
+
description: "Working directory inside the container",
|
|
779
|
+
default: "/workspace"
|
|
780
|
+
},
|
|
781
|
+
privileged: {
|
|
782
|
+
type: "boolean",
|
|
783
|
+
description: "Run in privileged mode",
|
|
784
|
+
default: false
|
|
785
|
+
},
|
|
786
|
+
memory: {
|
|
787
|
+
type: "number",
|
|
788
|
+
description: "Memory limit in bytes"
|
|
789
|
+
},
|
|
790
|
+
memorySwap: {
|
|
791
|
+
type: "number",
|
|
792
|
+
description: "Total memory plus swap in bytes"
|
|
793
|
+
},
|
|
794
|
+
cpuShares: {
|
|
795
|
+
type: "number",
|
|
796
|
+
description: "CPU shares relative weight"
|
|
797
|
+
},
|
|
798
|
+
cpuQuota: {
|
|
799
|
+
type: "number",
|
|
800
|
+
description: "CPU quota in microseconds per period"
|
|
801
|
+
},
|
|
802
|
+
cpuPeriod: {
|
|
803
|
+
type: "number",
|
|
804
|
+
description: "CPU period in microseconds"
|
|
805
|
+
},
|
|
806
|
+
pidsLimit: {
|
|
807
|
+
type: "number",
|
|
808
|
+
description: "Maximum number of PIDs in the container"
|
|
809
|
+
},
|
|
810
|
+
readonlyRootfs: {
|
|
811
|
+
type: "boolean",
|
|
812
|
+
description: "Mount the container root filesystem as read-only"
|
|
813
|
+
},
|
|
814
|
+
capDrop: {
|
|
815
|
+
type: "array",
|
|
816
|
+
description: "Linux capabilities to drop",
|
|
817
|
+
items: { type: "string" }
|
|
818
|
+
},
|
|
819
|
+
capAdd: {
|
|
820
|
+
type: "array",
|
|
821
|
+
description: "Linux capabilities to add",
|
|
822
|
+
items: { type: "string" }
|
|
823
|
+
},
|
|
824
|
+
securityOpt: {
|
|
825
|
+
type: "array",
|
|
826
|
+
description: "Docker security options",
|
|
827
|
+
items: { type: "string" }
|
|
828
|
+
},
|
|
829
|
+
ulimits: {
|
|
830
|
+
type: "array",
|
|
831
|
+
description: "Ulimit entries for Docker HostConfig.Ulimits",
|
|
832
|
+
items: {
|
|
833
|
+
type: "object",
|
|
834
|
+
required: [
|
|
835
|
+
"name",
|
|
836
|
+
"soft",
|
|
837
|
+
"hard"
|
|
838
|
+
],
|
|
839
|
+
additionalProperties: false,
|
|
840
|
+
properties: {
|
|
841
|
+
name: { type: "string" },
|
|
842
|
+
soft: { type: "number" },
|
|
843
|
+
hard: { type: "number" }
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
tmpfs: {
|
|
848
|
+
type: "object",
|
|
849
|
+
description: "tmpfs mount paths with options",
|
|
850
|
+
additionalProperties: { type: "string" }
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
createSandbox: (config) => new DockerSandbox(config)
|
|
845
855
|
};
|
|
846
|
-
|
|
856
|
+
//#endregion
|
|
847
857
|
export { DockerProcessManager, DockerSandbox, dockerSandboxProvider };
|
|
848
|
-
|
|
858
|
+
|
|
849
859
|
//# sourceMappingURL=index.js.map
|