@mastra/docker 0.8.0-alpha.1 → 0.8.0-alpha.2
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/README.md +33 -0
- package/dist/index.cjs +248 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +248 -28
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +2 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/sandbox/index.d.ts +80 -0
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/process-manager.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3,8 +3,109 @@ import { isDeepStrictEqual } from "util";
|
|
|
3
3
|
import { MastraSandbox, ProcessHandle, SandboxAbortError, SandboxError, SandboxNotReadyError, SandboxProcessManager, validateSandboxFileMode } from "@mastra/core/workspace";
|
|
4
4
|
import Docker from "dockerode";
|
|
5
5
|
import { pack } from "tar-stream";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
6
7
|
//#region src/sandbox/process-manager.ts
|
|
7
8
|
/**
|
|
9
|
+
* Docker Process Manager
|
|
10
|
+
*
|
|
11
|
+
* Implements SandboxProcessManager for Docker containers.
|
|
12
|
+
* Uses `container.exec()` to run commands inside a long-lived container.
|
|
13
|
+
* Each spawned process gets a dedicated exec instance with separate
|
|
14
|
+
* stdout/stderr streams.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Directory (inside the container) where each spawned process records the PGID
|
|
18
|
+
* of its process group. Created with mode 700 so only the (root) exec user can
|
|
19
|
+
* write the PGID files.
|
|
20
|
+
*/
|
|
21
|
+
const PROC_DIR = "/tmp/.mastra-proc";
|
|
22
|
+
/**
|
|
23
|
+
* Wrapper (run as the exec command) that places the user command in its own
|
|
24
|
+
* process group and records the group's PGID so kill() can signal the whole
|
|
25
|
+
* group later.
|
|
26
|
+
*
|
|
27
|
+
* Docker's exec-inspect `Pid` is a host/daemon-namespace PID and cannot be used
|
|
28
|
+
* with an in-container `kill`, so we need a container-namespace identity. We use
|
|
29
|
+
* a *kernel-enforced* process group as that identity:
|
|
30
|
+
*
|
|
31
|
+
* 1. `setsid -w` re-execs the command as a new session/process-group leader,
|
|
32
|
+
* so its PID == PGID. Every descendant inherits that PGID (unless it calls
|
|
33
|
+
* `setsid` itself) and stays reachable even if it re-parents to PID 1.
|
|
34
|
+
* `-w` keeps the wrapper (and thus the exec) alive for the whole lifetime
|
|
35
|
+
* and propagates the child's exit status — without it `setsid` forks and
|
|
36
|
+
* returns immediately, so the exec would appear to finish while the real
|
|
37
|
+
* work keeps running.
|
|
38
|
+
* 2. The leader writes its own PID (`$$`) — the PGID — to a private file that
|
|
39
|
+
* only this process wrote, so the identity is kernel-owned and cannot be
|
|
40
|
+
* forged by another container process.
|
|
41
|
+
*
|
|
42
|
+
* If `setsid -w` is unavailable in the image (e.g. BusyBox), we degrade
|
|
43
|
+
* gracefully: the command runs directly and we record its PID so kill() can
|
|
44
|
+
* still signal it (descendant coverage is then best-effort). The probe
|
|
45
|
+
* `setsid -w true` also covers images without setsid at all.
|
|
46
|
+
*
|
|
47
|
+
* Positional args: $1 = pgid file path, $2 = user command. The script text is a
|
|
48
|
+
* static constant; runtime values travel only as argv, never interpolated into
|
|
49
|
+
* the command string.
|
|
50
|
+
*/
|
|
51
|
+
const SPAWN_WRAPPER = `
|
|
52
|
+
umask 077
|
|
53
|
+
d="\${1%/*}"
|
|
54
|
+
mkdir -p "$d" 2>/dev/null
|
|
55
|
+
chmod 700 "$d" 2>/dev/null
|
|
56
|
+
if setsid -w true >/dev/null 2>&1; then
|
|
57
|
+
exec setsid -w sh -c 'echo $$ > "$1" || exit 126; sh -c "$2"; ret=$?; rm -f "$1" 2>/dev/null; exit $ret' sh "$1" "$2"
|
|
58
|
+
fi
|
|
59
|
+
echo $$ > "$1" || exit 126
|
|
60
|
+
sh -c "$2"
|
|
61
|
+
ret=$?
|
|
62
|
+
rm -f "$1" 2>/dev/null
|
|
63
|
+
exit $ret
|
|
64
|
+
`;
|
|
65
|
+
/**
|
|
66
|
+
* Kill script: read the recorded PGID and SIGKILL the whole process group.
|
|
67
|
+
* A negative PID targets the kernel-owned process group, so descendants that
|
|
68
|
+
* re-parented to PID 1 are still caught. We SIGSTOP the
|
|
69
|
+
* group first to freeze fork races, then SIGKILL. The file may not exist yet if
|
|
70
|
+
* kill races the leader's first write, so we briefly wait for it.
|
|
71
|
+
*
|
|
72
|
+
* Positional arg: $1 = pgid file path (static script; no interpolation).
|
|
73
|
+
*/
|
|
74
|
+
const KILL_SCRIPT = `
|
|
75
|
+
f="$1"
|
|
76
|
+
i=0
|
|
77
|
+
while [ ! -r "$f" ] && [ "$i" -lt 40 ]; do sleep 0.05; i=$((i + 1)); done
|
|
78
|
+
# If the PGID was never recorded (file absent/unreadable after the wait, or
|
|
79
|
+
# empty), we have no group to signal or verify — report failure rather than
|
|
80
|
+
# falsely claiming the tree was terminated.
|
|
81
|
+
[ -r "$f" ] || exit 1
|
|
82
|
+
pgid=$(cat "$f" 2>/dev/null)
|
|
83
|
+
rm -f "$f" 2>/dev/null
|
|
84
|
+
[ -n "$pgid" ] || exit 1
|
|
85
|
+
kill -STOP -"$pgid" 2>/dev/null
|
|
86
|
+
kill -KILL -"$pgid" 2>/dev/null
|
|
87
|
+
# Fallback for images without setsid: the leader is not a group leader, so also
|
|
88
|
+
# signal it directly.
|
|
89
|
+
kill -KILL "$pgid" 2>/dev/null
|
|
90
|
+
# Verify the group is actually gone before reporting success. kill -0 probes
|
|
91
|
+
# for the group's existence without sending a signal, and we poll while it
|
|
92
|
+
# still reports the group alive. When the probe finally fails we must inspect
|
|
93
|
+
# why: ESRCH ("no such process") means every member was reaped, so report
|
|
94
|
+
# success; EPERM or any other error means termination is unconfirmed (e.g. a
|
|
95
|
+
# member dropped privileges and became unsignalable), so exit nonzero and let
|
|
96
|
+
# kill() report failure instead of falsely claiming the tree was terminated.
|
|
97
|
+
j=0
|
|
98
|
+
while err=$(kill -0 -"$pgid" 2>&1); do
|
|
99
|
+
j=$((j + 1))
|
|
100
|
+
[ "$j" -ge 40 ] && exit 1
|
|
101
|
+
sleep 0.05
|
|
102
|
+
done
|
|
103
|
+
case "$err" in
|
|
104
|
+
*[Ss]uch\\ process*) exit 0 ;;
|
|
105
|
+
*) exit 1 ;;
|
|
106
|
+
esac
|
|
107
|
+
`;
|
|
108
|
+
/**
|
|
8
109
|
* Wraps a Docker exec instance to conform to Mastra's ProcessHandle.
|
|
9
110
|
* Not exported — internal to this module.
|
|
10
111
|
*
|
|
@@ -24,13 +125,16 @@ var DockerProcessHandle = class extends ProcessHandle {
|
|
|
24
125
|
_waitPromise = null;
|
|
25
126
|
_stdinStream = null;
|
|
26
127
|
_execStream = null;
|
|
27
|
-
|
|
128
|
+
/** @internal Container path of the file holding this process group's PGID. */
|
|
129
|
+
_pgidFile;
|
|
130
|
+
constructor(exec, container, startTime, stdinStream, pgidFile, options) {
|
|
28
131
|
super(options);
|
|
29
132
|
this.pid = exec.id;
|
|
30
133
|
this._exec = exec;
|
|
31
134
|
this._container = container;
|
|
32
135
|
this._startTime = startTime;
|
|
33
136
|
this._stdinStream = stdinStream;
|
|
137
|
+
this._pgidFile = pgidFile;
|
|
34
138
|
}
|
|
35
139
|
get exitCode() {
|
|
36
140
|
return this._exitCode;
|
|
@@ -61,37 +165,28 @@ var DockerProcessHandle = class extends ProcessHandle {
|
|
|
61
165
|
async kill() {
|
|
62
166
|
if (this._exitCode !== void 0) return false;
|
|
63
167
|
try {
|
|
64
|
-
|
|
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
|
-
await (await this._container.exec({
|
|
168
|
+
const killExec = await this._container.exec({
|
|
81
169
|
Cmd: [
|
|
82
170
|
"sh",
|
|
83
171
|
"-c",
|
|
84
|
-
|
|
172
|
+
KILL_SCRIPT,
|
|
173
|
+
"sh",
|
|
174
|
+
this._pgidFile
|
|
85
175
|
],
|
|
86
176
|
AttachStdout: false,
|
|
87
177
|
AttachStderr: false
|
|
88
|
-
})
|
|
178
|
+
});
|
|
179
|
+
await killExec.start({});
|
|
180
|
+
let killInfo = await killExec.inspect();
|
|
181
|
+
while (killInfo.Running) {
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
183
|
+
killInfo = await killExec.inspect();
|
|
184
|
+
}
|
|
185
|
+
if (killInfo.ExitCode !== 0) throw new Error(`kill helper exited with code ${killInfo.ExitCode}`);
|
|
89
186
|
this._killed = true;
|
|
90
187
|
this._destroyStream();
|
|
91
188
|
return true;
|
|
92
189
|
} catch (error) {
|
|
93
|
-
this._killed = true;
|
|
94
|
-
this._destroyStream();
|
|
95
190
|
const msg = error instanceof Error ? error.message.toLowerCase() : "";
|
|
96
191
|
if (!msg.includes("no such process") && !msg.includes("esrch")) console.warn(`[DockerProcessManager] kill(${this.pid}) failed unexpectedly:`, error);
|
|
97
192
|
return false;
|
|
@@ -145,11 +240,15 @@ var DockerProcessManager = class extends SandboxProcessManager {
|
|
|
145
240
|
}
|
|
146
241
|
async spawn(command, options = {}) {
|
|
147
242
|
const container = this.container;
|
|
243
|
+
const pgidFile = `${PROC_DIR}/${randomUUID()}`;
|
|
148
244
|
const envArray = Object.entries({ ...options.env }).filter((entry) => entry[1] !== void 0).map(([k, v]) => `${k}=${v}`);
|
|
149
245
|
const exec = await container.exec({
|
|
150
246
|
Cmd: [
|
|
151
247
|
"sh",
|
|
152
248
|
"-c",
|
|
249
|
+
SPAWN_WRAPPER,
|
|
250
|
+
"sh",
|
|
251
|
+
pgidFile,
|
|
153
252
|
command
|
|
154
253
|
],
|
|
155
254
|
AttachStdout: true,
|
|
@@ -164,7 +263,7 @@ var DockerProcessManager = class extends SandboxProcessManager {
|
|
|
164
263
|
stdin: true
|
|
165
264
|
});
|
|
166
265
|
const startTime = Date.now();
|
|
167
|
-
const handle = new DockerProcessHandle(exec, container, startTime, stream, options);
|
|
266
|
+
const handle = new DockerProcessHandle(exec, container, startTime, stream, pgidFile, options);
|
|
168
267
|
handle._setExecStream(stream);
|
|
169
268
|
const waitPromise = new Promise((resolve) => {
|
|
170
269
|
const buffer = [];
|
|
@@ -240,8 +339,15 @@ var DockerProcessManager = class extends SandboxProcessManager {
|
|
|
240
339
|
if (handle.exitCode === void 0) {
|
|
241
340
|
handle._killed = true;
|
|
242
341
|
handle._timedOut = true;
|
|
243
|
-
|
|
244
|
-
|
|
342
|
+
const forceClose = () => {
|
|
343
|
+
if (handle.exitCode === void 0) {
|
|
344
|
+
handle._killed = true;
|
|
345
|
+
handle._destroyStream();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
handle.kill().then((killed) => {
|
|
349
|
+
if (!killed) forceClose();
|
|
350
|
+
}).catch(forceClose);
|
|
245
351
|
}
|
|
246
352
|
}, resolvedTimeout);
|
|
247
353
|
waitPromise.then(() => clearTimeout(timer));
|
|
@@ -334,12 +440,14 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
|
|
|
334
440
|
_cpuQuota;
|
|
335
441
|
_cpuPeriod;
|
|
336
442
|
_pidsLimit;
|
|
443
|
+
_init;
|
|
337
444
|
_readonlyRootfs;
|
|
338
445
|
_capDrop;
|
|
339
446
|
_capAdd;
|
|
340
447
|
_securityOpt;
|
|
341
448
|
_ulimits;
|
|
342
449
|
_tmpfs;
|
|
450
|
+
_mounts;
|
|
343
451
|
_labels;
|
|
344
452
|
_instructionsOverride;
|
|
345
453
|
_constructorOptions;
|
|
@@ -373,12 +481,14 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
|
|
|
373
481
|
this._cpuQuota = options.cpuQuota;
|
|
374
482
|
this._cpuPeriod = options.cpuPeriod;
|
|
375
483
|
this._pidsLimit = options.pidsLimit;
|
|
484
|
+
this._init = options.init ?? true;
|
|
376
485
|
this._readonlyRootfs = options.readonlyRootfs;
|
|
377
486
|
this._capDrop = options.capDrop;
|
|
378
487
|
this._capAdd = options.capAdd;
|
|
379
488
|
this._securityOpt = options.securityOpt;
|
|
380
489
|
this._ulimits = options.ulimits;
|
|
381
490
|
this._tmpfs = options.tmpfs;
|
|
491
|
+
this._mounts = options.mounts;
|
|
382
492
|
this.setWorkingDirectory(options.workingDirectory ?? options.workingDir ?? "/workspace");
|
|
383
493
|
this._labels = {
|
|
384
494
|
...options.labels,
|
|
@@ -460,12 +570,14 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
|
|
|
460
570
|
CpuQuota: this._cpuQuota,
|
|
461
571
|
CpuPeriod: this._cpuPeriod,
|
|
462
572
|
PidsLimit: this._pidsLimit,
|
|
573
|
+
Init: this._init,
|
|
463
574
|
ReadonlyRootfs: this._readonlyRootfs,
|
|
464
575
|
CapDrop: this._capDrop,
|
|
465
576
|
CapAdd: this._capAdd,
|
|
466
577
|
SecurityOpt: this._securityOpt,
|
|
467
578
|
Ulimits: this._ulimits?.map(toDockerUlimit),
|
|
468
|
-
Tmpfs: this._tmpfs
|
|
579
|
+
Tmpfs: this._tmpfs,
|
|
580
|
+
Mounts: this._mounts?.map(toDockerMount)
|
|
469
581
|
},
|
|
470
582
|
OpenStdin: true,
|
|
471
583
|
Tty: false
|
|
@@ -519,7 +631,8 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
|
|
|
519
631
|
["CapAdd", this._capAdd],
|
|
520
632
|
["SecurityOpt", this._securityOpt],
|
|
521
633
|
["Ulimits", this._ulimits],
|
|
522
|
-
["Tmpfs", this._tmpfs]
|
|
634
|
+
["Tmpfs", this._tmpfs],
|
|
635
|
+
["Mounts", this._mounts?.map(toDockerMount)]
|
|
523
636
|
];
|
|
524
637
|
if (this._privilegedWasSet || hostConfig?.Privileged === true) entries.unshift(["Privileged", this._privileged]);
|
|
525
638
|
return entries.filter((entry) => isPresentHostConfigValue(entry[1]));
|
|
@@ -795,6 +908,31 @@ function toDockerUlimit(ulimit) {
|
|
|
795
908
|
Hard: ulimit.hard
|
|
796
909
|
};
|
|
797
910
|
}
|
|
911
|
+
function toDockerMount(mount) {
|
|
912
|
+
const settings = {
|
|
913
|
+
Type: mount.type,
|
|
914
|
+
Target: mount.target,
|
|
915
|
+
Source: mount.type === "tmpfs" ? "" : mount.source
|
|
916
|
+
};
|
|
917
|
+
if (mount.readOnly !== void 0) settings.ReadOnly = mount.readOnly;
|
|
918
|
+
if (mount.type === "volume" && mount.volumeOptions) {
|
|
919
|
+
const { subpath, noCopy, labels } = mount.volumeOptions;
|
|
920
|
+
const volumeOptions = {};
|
|
921
|
+
if (subpath !== void 0) volumeOptions.Subpath = subpath;
|
|
922
|
+
if (noCopy !== void 0) volumeOptions.NoCopy = noCopy;
|
|
923
|
+
if (labels !== void 0) volumeOptions.Labels = labels;
|
|
924
|
+
if (Object.keys(volumeOptions).length > 0) settings.VolumeOptions = volumeOptions;
|
|
925
|
+
}
|
|
926
|
+
if (mount.type === "bind" && mount.bindOptions?.propagation !== void 0) settings.BindOptions = { Propagation: mount.bindOptions.propagation };
|
|
927
|
+
if (mount.type === "tmpfs" && mount.tmpfsOptions) {
|
|
928
|
+
const { sizeBytes, mode } = mount.tmpfsOptions;
|
|
929
|
+
const tmpfsOptions = {};
|
|
930
|
+
if (sizeBytes !== void 0) tmpfsOptions.SizeBytes = sizeBytes;
|
|
931
|
+
if (mode !== void 0) tmpfsOptions.Mode = mode;
|
|
932
|
+
if (Object.keys(tmpfsOptions).length > 0) settings.TmpfsOptions = tmpfsOptions;
|
|
933
|
+
}
|
|
934
|
+
return settings;
|
|
935
|
+
}
|
|
798
936
|
function toDockerSandboxOptionName(field) {
|
|
799
937
|
return {
|
|
800
938
|
Privileged: "privileged",
|
|
@@ -809,7 +947,8 @@ function toDockerSandboxOptionName(field) {
|
|
|
809
947
|
CapAdd: "capAdd",
|
|
810
948
|
SecurityOpt: "securityOpt",
|
|
811
949
|
Ulimits: "ulimits",
|
|
812
|
-
Tmpfs: "tmpfs"
|
|
950
|
+
Tmpfs: "tmpfs",
|
|
951
|
+
Mounts: "mounts"
|
|
813
952
|
}[field] ?? String(field);
|
|
814
953
|
}
|
|
815
954
|
//#endregion
|
|
@@ -920,6 +1059,87 @@ const dockerSandboxProvider = {
|
|
|
920
1059
|
type: "object",
|
|
921
1060
|
description: "tmpfs mount paths with options",
|
|
922
1061
|
additionalProperties: { type: "string" }
|
|
1062
|
+
},
|
|
1063
|
+
mounts: {
|
|
1064
|
+
type: "array",
|
|
1065
|
+
description: "Mounts mapped 1:1 onto Docker HostConfig.Mounts (supports volume subpath)",
|
|
1066
|
+
items: { oneOf: [
|
|
1067
|
+
{
|
|
1068
|
+
type: "object",
|
|
1069
|
+
required: [
|
|
1070
|
+
"type",
|
|
1071
|
+
"target",
|
|
1072
|
+
"source"
|
|
1073
|
+
],
|
|
1074
|
+
additionalProperties: false,
|
|
1075
|
+
properties: {
|
|
1076
|
+
type: { const: "volume" },
|
|
1077
|
+
target: { type: "string" },
|
|
1078
|
+
source: { type: "string" },
|
|
1079
|
+
readOnly: { type: "boolean" },
|
|
1080
|
+
volumeOptions: {
|
|
1081
|
+
type: "object",
|
|
1082
|
+
additionalProperties: false,
|
|
1083
|
+
properties: {
|
|
1084
|
+
subpath: { type: "string" },
|
|
1085
|
+
noCopy: { type: "boolean" },
|
|
1086
|
+
labels: {
|
|
1087
|
+
type: "object",
|
|
1088
|
+
additionalProperties: { type: "string" }
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
type: "object",
|
|
1096
|
+
required: [
|
|
1097
|
+
"type",
|
|
1098
|
+
"target",
|
|
1099
|
+
"source"
|
|
1100
|
+
],
|
|
1101
|
+
additionalProperties: false,
|
|
1102
|
+
properties: {
|
|
1103
|
+
type: { const: "bind" },
|
|
1104
|
+
target: { type: "string" },
|
|
1105
|
+
source: { type: "string" },
|
|
1106
|
+
readOnly: { type: "boolean" },
|
|
1107
|
+
bindOptions: {
|
|
1108
|
+
type: "object",
|
|
1109
|
+
additionalProperties: false,
|
|
1110
|
+
properties: { propagation: {
|
|
1111
|
+
type: "string",
|
|
1112
|
+
enum: [
|
|
1113
|
+
"private",
|
|
1114
|
+
"rprivate",
|
|
1115
|
+
"shared",
|
|
1116
|
+
"rshared",
|
|
1117
|
+
"slave",
|
|
1118
|
+
"rslave"
|
|
1119
|
+
]
|
|
1120
|
+
} }
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
type: "object",
|
|
1126
|
+
required: ["type", "target"],
|
|
1127
|
+
additionalProperties: false,
|
|
1128
|
+
properties: {
|
|
1129
|
+
type: { const: "tmpfs" },
|
|
1130
|
+
target: { type: "string" },
|
|
1131
|
+
readOnly: { type: "boolean" },
|
|
1132
|
+
tmpfsOptions: {
|
|
1133
|
+
type: "object",
|
|
1134
|
+
additionalProperties: false,
|
|
1135
|
+
properties: {
|
|
1136
|
+
sizeBytes: { type: "number" },
|
|
1137
|
+
mode: { type: "number" }
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
] }
|
|
923
1143
|
}
|
|
924
1144
|
}
|
|
925
1145
|
},
|