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