@mastra/apple-container 0.3.0 → 0.4.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.js CHANGED
@@ -1,944 +1,861 @@
1
- import { spawn } from 'child_process';
2
- import { createHash } from 'crypto';
3
- import { StringDecoder } from 'string_decoder';
4
- import { MastraSandbox, SandboxExecutionError, ProcessHandle } from '@mastra/core/workspace';
5
-
6
- // src/sandbox/index.ts
7
- var DEFAULT_COMMAND_TIMEOUT_MS = 3e5;
8
- var DEFAULT_IMAGE = "node:22-slim";
9
- var DEFAULT_COMMAND = ["sleep", "infinity"];
10
- var DEFAULT_WORKING_DIR = "/workspace";
11
- var APPLE_CONTAINER_CLI_GRACE_TIMEOUT_MS = 1e4;
12
- var APPLE_CONTAINER_READY_TIMEOUT_MS = 1e4;
13
- var APPLE_CONTAINER_READY_EXEC_TIMEOUT_MS = 5e3;
14
- var APPLE_CONTAINER_TIMEOUT_EXIT_CODE = 124;
15
- var APPLE_CONTAINER_TIMEOUT_MARKER = "__MASTRA_APPLE_CONTAINER_TIMEOUT__";
1
+ import { spawn } from "child_process";
2
+ import { createHash } from "crypto";
3
+ import { StringDecoder } from "string_decoder";
4
+ import { MastraSandbox, ProcessHandle, SandboxExecutionError, UnsupportedStdinCloseError } from "@mastra/core/workspace";
5
+ //#region src/sandbox/index.ts
6
+ /**
7
+ * Apple container CLI sandbox provider.
8
+ *
9
+ * This provider maps Mastra's WorkspaceSandbox command execution contract to
10
+ * Apple's `container` CLI. It starts a long-lived OCI Linux container and uses
11
+ * `container exec` for commands.
12
+ *
13
+ * @see https://github.com/apple/container
14
+ */
15
+ const DEFAULT_COMMAND_TIMEOUT_MS = 3e5;
16
+ const DEFAULT_IMAGE = "node:22-slim";
17
+ const DEFAULT_COMMAND = ["sleep", "infinity"];
18
+ const DEFAULT_WORKING_DIR = "/workspace";
19
+ const APPLE_CONTAINER_CLI_GRACE_TIMEOUT_MS = 1e4;
20
+ const APPLE_CONTAINER_READY_TIMEOUT_MS = 1e4;
21
+ const APPLE_CONTAINER_READY_EXEC_TIMEOUT_MS = 5e3;
22
+ const APPLE_CONTAINER_TIMEOUT_EXIT_CODE = 124;
23
+ const APPLE_CONTAINER_TIMEOUT_MARKER = "__MASTRA_APPLE_CONTAINER_TIMEOUT__";
16
24
  var DefaultAppleContainerCommandRunner = class {
17
- constructor(binary = "container") {
18
- this.binary = binary;
19
- }
20
- binary;
21
- run(args, options = {}) {
22
- return runAppleContainerCli(this.binary, args, options);
23
- }
25
+ binary;
26
+ constructor(binary = "container") {
27
+ this.binary = binary;
28
+ }
29
+ run(args, options = {}) {
30
+ return runAppleContainerCli(this.binary, args, options);
31
+ }
24
32
  };
25
- var AppleContainerSandbox = class _AppleContainerSandbox extends MastraSandbox {
26
- id;
27
- name = "AppleContainerSandbox";
28
- provider = "apple-container";
29
- status = "pending";
30
- _containerName;
31
- _configuredName;
32
- _image;
33
- _command;
34
- _env;
35
- _volumes;
36
- _mounts;
37
- _network;
38
- _publishedPorts;
39
- _publishedSockets;
40
- _cpus;
41
- _memory;
42
- _platform;
43
- _arch;
44
- _os;
45
- _rosetta;
46
- _readonlyRootfs;
47
- _ssh;
48
- _init;
49
- _virtualization;
50
- _capAdd;
51
- _capDrop;
52
- _tmpfs;
53
- _dns;
54
- _dnsSearch;
55
- _noDns;
56
- _userLabels;
57
- _labels;
58
- _configHash;
59
- _workingDir;
60
- _timeout;
61
- _deleteOnDestroy;
62
- _runner;
63
- _instructionsOverride;
64
- _createdAt;
65
- _constructorOptions;
66
- _containerId;
67
- constructor(options = {}) {
68
- super({
69
- ...options,
70
- name: "AppleContainerSandbox"
71
- });
72
- this.id = options.id ?? generateId();
73
- this._configuredName = options.name;
74
- this._containerName = sanitizeContainerName(options.name ?? this.id);
75
- this._image = options.image ?? DEFAULT_IMAGE;
76
- this._command = options.command ?? DEFAULT_COMMAND;
77
- this._env = options.env ?? {};
78
- this._volumes = options.volumes ?? {};
79
- this._mounts = options.mounts ?? [];
80
- this._network = options.network;
81
- this._publishedPorts = options.publishedPorts ?? [];
82
- this._publishedSockets = options.publishedSockets ?? [];
83
- this._cpus = options.cpus;
84
- this._memory = options.memory;
85
- this._platform = options.platform;
86
- this._arch = options.arch;
87
- this._os = options.os;
88
- this._rosetta = options.rosetta ?? false;
89
- this._readonlyRootfs = options.readonlyRootfs ?? false;
90
- this._ssh = options.ssh ?? false;
91
- this._init = options.init ?? true;
92
- this._virtualization = options.virtualization ?? false;
93
- this._capAdd = options.capAdd ?? [];
94
- this._capDrop = options.capDrop ?? [];
95
- this._tmpfs = options.tmpfs ?? [];
96
- validateTmpfsPaths(this._tmpfs);
97
- this._dns = options.dns ?? [];
98
- this._dnsSearch = options.dnsSearch ?? [];
99
- this._noDns = options.noDns ?? false;
100
- this._workingDir = options.workingDir ?? DEFAULT_WORKING_DIR;
101
- this._userLabels = options.labels ?? {};
102
- this._configHash = hashConfig(this._runtimeConfigForHash());
103
- this._labels = {
104
- ...this._userLabels,
105
- "mastra.sandbox": "true",
106
- "mastra.sandbox.id": this.id,
107
- "mastra.sandbox.config-hash": this._configHash
108
- };
109
- this._timeout = options.timeout ?? DEFAULT_COMMAND_TIMEOUT_MS;
110
- this._deleteOnDestroy = options.deleteOnDestroy ?? true;
111
- this._runner = options.runner ?? new DefaultAppleContainerCommandRunner(options.containerBinary);
112
- this._instructionsOverride = options.instructions;
113
- this._createdAt = /* @__PURE__ */ new Date();
114
- this._constructorOptions = { ...options };
115
- }
116
- /**
117
- * Construct a sibling `AppleContainerSandbox` that inherits this sandbox's
118
- * configuration (image, resources, network, security options, labels) with
119
- * per-instance overrides.
120
- *
121
- * Performs no I/O — the sandbox clone provisions (or reconnects to an
122
- * existing container labelled with the same logical `id`) on its own
123
- * `start()`. Use it when one configured sandbox acts as the template for a
124
- * fleet of independent sandboxes (e.g. one per project).
125
- *
126
- * `options.idleTimeoutMinutes` is ignored (Apple containers have no
127
- * provider-side idle teardown; `timeout` here is a command timeout), and
128
- * `options.sandboxId` is ignored because reconnection is by logical `id`.
129
- */
130
- clone(options = {}) {
131
- const { id: _id, name: _name, ...base } = this._constructorOptions;
132
- return new _AppleContainerSandbox({
133
- ...base,
134
- ...options.id !== void 0 && { id: options.id },
135
- ...options.env !== void 0 && { env: options.env }
136
- });
137
- }
138
- get containerId() {
139
- return this._containerId ?? this._containerName;
140
- }
141
- async start() {
142
- await this._runPlainLifecycle("starting", "running", () => this._startContainer());
143
- }
144
- async stop() {
145
- await this._runPlainLifecycle("stopping", "stopped", () => this._stopContainer());
146
- }
147
- async destroy() {
148
- await this._runPlainLifecycle("destroying", "destroyed", () => this._destroyContainer());
149
- }
150
- async _startContainer() {
151
- const existing = await this._inspectContainer();
152
- if (existing) {
153
- this._assertMastraOwned(existing);
154
- this._assertCompatibleConfig(existing);
155
- this._containerId = existing.configuration?.id ?? this._containerName;
156
- if (!isRunning(existing)) {
157
- const result2 = await this._runCli(["start", this.containerId]);
158
- this._assertSuccess(result2, `start Apple container ${this.containerId}`);
159
- await this._waitUntilContainerReady(`start Apple container ${this.containerId}`);
160
- }
161
- return;
162
- }
163
- const env = envFlags(this._env);
164
- const result = await this._runCli(this._buildRunArgs(env.args), { env: env.env });
165
- this._assertSuccess(result, `create Apple container ${this._containerName}`);
166
- this._containerId = this._containerName;
167
- try {
168
- await this._waitUntilContainerReady(`create Apple container ${this._containerName}`);
169
- } catch (error) {
170
- if (this._deleteOnDestroy) {
171
- await this._deleteContainerIgnoringMissing();
172
- }
173
- throw error;
174
- }
175
- }
176
- async _stopContainer() {
177
- const existing = await this._inspectContainer();
178
- if (!existing) {
179
- return;
180
- }
181
- this._assertMastraOwned(existing);
182
- if (!isRunning(existing)) {
183
- return;
184
- }
185
- const result = await this._runCli(["stop", this.containerId]);
186
- if (!result.success && !isMissingContainerMessage(result.stderr)) {
187
- this._assertSuccess(result, `stop Apple container ${this.containerId}`);
188
- }
189
- }
190
- async _destroyContainer() {
191
- if (!this._deleteOnDestroy) {
192
- await this._stopContainer();
193
- return;
194
- }
195
- const existing = await this._inspectContainer();
196
- if (!existing) {
197
- return;
198
- }
199
- this._assertMastraOwned(existing);
200
- await this._deleteContainerIgnoringMissing();
201
- }
202
- async executeCommand(command, args = [], options = {}) {
203
- await this.ensureRunning();
204
- const commandTimeout = options.timeout ?? this._timeout;
205
- const hasCommandTimeout = Number.isFinite(commandTimeout) && commandTimeout > 0;
206
- const fullCommand = buildShellCommand(command, args);
207
- const shellCommand = hasCommandTimeout ? buildTimeoutShellCommand(fullCommand, commandTimeout) : fullCommand;
208
- const env = envFlags({ ...this._env, ...options.env });
209
- const cliArgs = [
210
- "exec",
211
- ...env.args,
212
- "--workdir",
213
- options.cwd ?? this._workingDir,
214
- this.containerId,
215
- "sh",
216
- "-lc",
217
- shellCommand
218
- ];
219
- const result = await this._runner.run(cliArgs, {
220
- timeout: hasCommandTimeout ? commandTimeout + APPLE_CONTAINER_CLI_GRACE_TIMEOUT_MS : void 0,
221
- env: env.env,
222
- abortSignal: options.abortSignal,
223
- onStdout: options.onStdout,
224
- onStderr: options.onStderr,
225
- maxRetainedBytes: options.maxRetainedBytes
226
- });
227
- const timedOut = result.exitCode === APPLE_CONTAINER_TIMEOUT_EXIT_CODE && result.stderr.includes(APPLE_CONTAINER_TIMEOUT_MARKER);
228
- const stderr = timedOut ? stripTimeoutMarker(result.stderr) : result.stderr;
229
- return {
230
- ...result,
231
- stderr,
232
- ...timedOut && { timedOut: true, killed: true },
233
- command: fullCommand,
234
- args
235
- };
236
- }
237
- async getInfo() {
238
- const inspect = this._containerId ? await this._inspectContainer() : void 0;
239
- const resources = inspect?.configuration?.resources;
240
- return {
241
- id: this.id,
242
- name: this.name,
243
- provider: this.provider,
244
- status: this.status,
245
- createdAt: this._createdAt,
246
- resources: resources ? {
247
- cpuCores: resources.cpus,
248
- memoryMB: resources.memoryInBytes ? Math.round(resources.memoryInBytes / 1024 / 1024) : void 0
249
- } : void 0,
250
- metadata: {
251
- ...this._serializableConfig()
252
- }
253
- };
254
- }
255
- getInstructions(opts) {
256
- const defaultInstructions = this._buildDefaultInstructions();
257
- if (typeof this._instructionsOverride === "string") {
258
- return this._instructionsOverride;
259
- }
260
- if (typeof this._instructionsOverride === "function") {
261
- return this._instructionsOverride({ defaultInstructions, requestContext: opts?.requestContext });
262
- }
263
- return defaultInstructions;
264
- }
265
- _buildDefaultInstructions() {
266
- const parts = [
267
- `Apple container sandbox: commands run inside a local OCI Linux container from image ${this._image}.`,
268
- `Working directory: ${this._workingDir}.`
269
- ];
270
- const volumeCount = Object.keys(this._volumes).length + this._mounts.length;
271
- if (volumeCount > 0) {
272
- parts.push(`${volumeCount} host mount(s) are configured.`);
273
- }
274
- if (this._timeout > 0) {
275
- parts.push(`Default command timeout: ${Math.ceil(this._timeout / 1e3)}s.`);
276
- }
277
- return parts.join(" ");
278
- }
279
- async _runPlainLifecycle(activeStatus, completeStatus, operation) {
280
- const managedByBaseWrapper = this.status === activeStatus;
281
- if (!managedByBaseWrapper) {
282
- this.status = activeStatus;
283
- }
284
- try {
285
- await operation();
286
- if (!managedByBaseWrapper) {
287
- this.status = completeStatus;
288
- }
289
- } catch (error) {
290
- if (!managedByBaseWrapper) {
291
- this.status = "error";
292
- }
293
- throw error;
294
- }
295
- }
296
- async _inspectContainer() {
297
- const result = await this._runCli(["inspect", this.containerId]);
298
- if (!result.success) {
299
- if (isMissingContainerMessage(result.stderr)) return void 0;
300
- this._assertSuccess(result, `inspect Apple container ${this.containerId}`);
301
- return void 0;
302
- }
303
- try {
304
- const parsed = JSON.parse(result.stdout);
305
- return Array.isArray(parsed) ? parsed[0] : parsed;
306
- } catch (error) {
307
- throw new SandboxExecutionError(
308
- `Failed to parse Apple container inspect output for ${this.containerId}: ${error instanceof Error ? error.message : String(error)}`,
309
- 1,
310
- result.stdout,
311
- result.stderr
312
- );
313
- }
314
- }
315
- _buildRunArgs(envArgs) {
316
- const args = ["run", "-d", "--name", this._containerName, "--workdir", this._workingDir];
317
- args.push(...envArgs);
318
- for (const [hostPath, containerPath] of Object.entries(this._volumes)) {
319
- args.push("--volume", `${hostPath}:${containerPath}`);
320
- }
321
- for (const mount of this._mounts) args.push("--mount", mount);
322
- for (const [key, value] of Object.entries(this._labels)) args.push("--label", `${key}=${value}`);
323
- for (const port of this._publishedPorts) args.push("--publish", port);
324
- for (const socket of this._publishedSockets) args.push("--publish-socket", socket);
325
- for (const cap of this._capAdd) args.push("--cap-add", cap);
326
- for (const cap of this._capDrop) args.push("--cap-drop", cap);
327
- for (const tmpfs of this._tmpfs) args.push("--tmpfs", tmpfs);
328
- for (const dns of this._dns) args.push("--dns", dns);
329
- for (const domain of this._dnsSearch) args.push("--dns-search", domain);
330
- if (this._network) args.push("--network", this._network);
331
- if (this._cpus !== void 0) args.push("--cpus", String(this._cpus));
332
- if (this._memory !== void 0) args.push("--memory", this._memory);
333
- if (this._platform) args.push("--platform", this._platform);
334
- if (this._arch) args.push("--arch", this._arch);
335
- if (this._os) args.push("--os", this._os);
336
- if (this._rosetta) args.push("--rosetta");
337
- if (this._readonlyRootfs) args.push("--read-only");
338
- if (this._ssh) args.push("--ssh");
339
- if (this._init) args.push("--init");
340
- if (this._virtualization) args.push("--virtualization");
341
- if (this._noDns) args.push("--no-dns");
342
- args.push(this._image, ...this._command);
343
- return args;
344
- }
345
- async _waitUntilContainerReady(action) {
346
- const deadline = Date.now() + APPLE_CONTAINER_READY_TIMEOUT_MS;
347
- let lastResult;
348
- while (Date.now() < deadline) {
349
- const inspect = await this._inspectContainer();
350
- if (!inspect) {
351
- throw new SandboxExecutionError(
352
- `${action} failed because Apple container ${this.containerId} disappeared`,
353
- 1,
354
- "",
355
- ""
356
- );
357
- }
358
- this._assertMastraOwned(inspect);
359
- this._assertCompatibleConfig(inspect);
360
- if (!isRunning(inspect)) {
361
- const state = getContainerState(inspect) ?? "not running";
362
- throw new SandboxExecutionError(
363
- `${action} failed because Apple container ${this.containerId} is ${state}`,
364
- 1,
365
- "",
366
- ""
367
- );
368
- }
369
- lastResult = await this._runCli(["exec", this.containerId, "sh", "-lc", "true"], {
370
- timeout: APPLE_CONTAINER_READY_EXEC_TIMEOUT_MS
371
- });
372
- if (lastResult.success) return;
373
- if (!isMissingContainerMessage(lastResult.stderr) && !/not running|not yet running/i.test(lastResult.stderr)) {
374
- break;
375
- }
376
- await delay(100);
377
- }
378
- throw new SandboxExecutionError(
379
- `${action} failed because Apple container ${this.containerId} did not become ready for exec`,
380
- lastResult?.exitCode ?? 1,
381
- lastResult?.stdout ?? "",
382
- lastResult?.stderr ?? ""
383
- );
384
- }
385
- async _deleteContainerIgnoringMissing() {
386
- const result = await this._runCli(["delete", "--force", this.containerId]);
387
- if (!result.success && !isMissingContainerMessage(result.stderr)) {
388
- this._assertSuccess(result, `delete Apple container ${this.containerId}`);
389
- }
390
- }
391
- _runtimeConfigForHash() {
392
- return {
393
- image: this._image,
394
- command: this._command,
395
- env: this._env,
396
- volumes: this._volumes,
397
- mounts: this._mounts,
398
- network: this._network,
399
- publishedPorts: this._publishedPorts,
400
- publishedSockets: this._publishedSockets,
401
- cpus: this._cpus,
402
- memory: this._memory,
403
- platform: this._platform,
404
- arch: this._arch,
405
- os: this._os,
406
- rosetta: this._rosetta,
407
- readonlyRootfs: this._readonlyRootfs,
408
- ssh: this._ssh,
409
- init: this._init,
410
- virtualization: this._virtualization,
411
- capAdd: this._capAdd,
412
- capDrop: this._capDrop,
413
- tmpfs: this._tmpfs,
414
- dns: this._dns,
415
- dnsSearch: this._dnsSearch,
416
- noDns: this._noDns,
417
- labels: this._userLabels,
418
- workingDir: this._workingDir
419
- };
420
- }
421
- _serializableConfig() {
422
- return compactConfig({
423
- id: this.id,
424
- name: this._configuredName,
425
- image: this._image,
426
- command: this._command,
427
- env: this._env,
428
- volumes: this._volumes,
429
- mounts: this._mounts,
430
- network: this._network,
431
- publishedPorts: this._publishedPorts,
432
- publishedSockets: this._publishedSockets,
433
- cpus: this._cpus,
434
- memory: this._memory,
435
- platform: this._platform,
436
- arch: this._arch,
437
- os: this._os,
438
- rosetta: this._rosetta,
439
- readonlyRootfs: this._readonlyRootfs,
440
- ssh: this._ssh,
441
- init: this._init,
442
- virtualization: this._virtualization,
443
- capAdd: this._capAdd,
444
- capDrop: this._capDrop,
445
- tmpfs: this._tmpfs,
446
- dns: this._dns,
447
- dnsSearch: this._dnsSearch,
448
- noDns: this._noDns,
449
- labels: this._userLabels,
450
- workingDir: this._workingDir,
451
- timeout: this._timeout,
452
- deleteOnDestroy: this._deleteOnDestroy
453
- });
454
- }
455
- _assertSuccess(result, action) {
456
- if (result.success) return;
457
- throw new SandboxExecutionError(
458
- `${action} failed with exit code ${result.exitCode}: ${result.stderr}`,
459
- result.exitCode,
460
- result.stdout,
461
- result.stderr
462
- );
463
- }
464
- _assertMastraOwned(inspect) {
465
- if (isMastraOwned(inspect, this.id)) return;
466
- throw new SandboxExecutionError(
467
- `Refusing to manage Apple container ${this.containerId} because it is not labeled as Mastra sandbox ${this.id}`,
468
- 1,
469
- "",
470
- ""
471
- );
472
- }
473
- _assertCompatibleConfig(inspect) {
474
- const existingHash = inspect.configuration?.labels?.["mastra.sandbox.config-hash"];
475
- if (!existingHash || existingHash === this._configHash) return;
476
- throw new SandboxExecutionError(
477
- `Refusing to manage Apple container ${this.containerId} because its immutable configuration does not match sandbox ${this.id}`,
478
- 1,
479
- "",
480
- ""
481
- );
482
- }
483
- _runCli(args, options = {}) {
484
- return this._runner.run(args, {
485
- timeout: this._timeout,
486
- ...options
487
- });
488
- }
33
+ var AppleContainerSandbox = class AppleContainerSandbox extends MastraSandbox {
34
+ id;
35
+ name = "AppleContainerSandbox";
36
+ provider = "apple-container";
37
+ status = "pending";
38
+ _containerName;
39
+ _configuredName;
40
+ _image;
41
+ _command;
42
+ _env;
43
+ _volumes;
44
+ _mounts;
45
+ _network;
46
+ _publishedPorts;
47
+ _publishedSockets;
48
+ _cpus;
49
+ _memory;
50
+ _platform;
51
+ _arch;
52
+ _os;
53
+ _rosetta;
54
+ _readonlyRootfs;
55
+ _ssh;
56
+ _init;
57
+ _virtualization;
58
+ _capAdd;
59
+ _capDrop;
60
+ _tmpfs;
61
+ _dns;
62
+ _dnsSearch;
63
+ _noDns;
64
+ _userLabels;
65
+ _labels;
66
+ _configHash;
67
+ _workingDir;
68
+ _timeout;
69
+ _deleteOnDestroy;
70
+ _runner;
71
+ _instructionsOverride;
72
+ _createdAt;
73
+ _constructorOptions;
74
+ _containerId;
75
+ constructor(options = {}) {
76
+ super({
77
+ ...options,
78
+ name: "AppleContainerSandbox"
79
+ });
80
+ this.id = options.id ?? generateId();
81
+ this._configuredName = options.name;
82
+ this._containerName = sanitizeContainerName(options.name ?? this.id);
83
+ this._image = options.image ?? DEFAULT_IMAGE;
84
+ this._command = options.command ?? DEFAULT_COMMAND;
85
+ this._env = options.env ?? {};
86
+ this._volumes = options.volumes ?? {};
87
+ this._mounts = options.mounts ?? [];
88
+ this._network = options.network;
89
+ this._publishedPorts = options.publishedPorts ?? [];
90
+ this._publishedSockets = options.publishedSockets ?? [];
91
+ this._cpus = options.cpus;
92
+ this._memory = options.memory;
93
+ this._platform = options.platform;
94
+ this._arch = options.arch;
95
+ this._os = options.os;
96
+ this._rosetta = options.rosetta ?? false;
97
+ this._readonlyRootfs = options.readonlyRootfs ?? false;
98
+ this._ssh = options.ssh ?? false;
99
+ this._init = options.init ?? true;
100
+ this._virtualization = options.virtualization ?? false;
101
+ this._capAdd = options.capAdd ?? [];
102
+ this._capDrop = options.capDrop ?? [];
103
+ this._tmpfs = options.tmpfs ?? [];
104
+ validateTmpfsPaths(this._tmpfs);
105
+ this._dns = options.dns ?? [];
106
+ this._dnsSearch = options.dnsSearch ?? [];
107
+ this._noDns = options.noDns ?? false;
108
+ this._workingDir = options.workingDir ?? DEFAULT_WORKING_DIR;
109
+ this._userLabels = options.labels ?? {};
110
+ this._configHash = hashConfig(this._runtimeConfigForHash());
111
+ this._labels = {
112
+ ...this._userLabels,
113
+ "mastra.sandbox": "true",
114
+ "mastra.sandbox.id": this.id,
115
+ "mastra.sandbox.config-hash": this._configHash
116
+ };
117
+ this._timeout = options.timeout ?? DEFAULT_COMMAND_TIMEOUT_MS;
118
+ this._deleteOnDestroy = options.deleteOnDestroy ?? true;
119
+ this._runner = options.runner ?? new DefaultAppleContainerCommandRunner(options.containerBinary);
120
+ this._instructionsOverride = options.instructions;
121
+ this._createdAt = /* @__PURE__ */ new Date();
122
+ this._constructorOptions = { ...options };
123
+ }
124
+ /**
125
+ * Construct a sibling `AppleContainerSandbox` that inherits this sandbox's
126
+ * configuration (image, resources, network, security options, labels) with
127
+ * per-instance overrides.
128
+ *
129
+ * Performs no I/O — the sandbox clone provisions (or reconnects to an
130
+ * existing container labelled with the same logical `id`) on its own
131
+ * `start()`. Use it when one configured sandbox acts as the template for a
132
+ * fleet of independent sandboxes (e.g. one per project).
133
+ *
134
+ * `options.idleTimeoutMinutes` is ignored (Apple containers have no
135
+ * provider-side idle teardown; `timeout` here is a command timeout), and
136
+ * `options.sandboxId` is ignored because reconnection is by logical `id`.
137
+ */
138
+ clone(options = {}) {
139
+ const { id: _id, name: _name, ...base } = this._constructorOptions;
140
+ return new AppleContainerSandbox({
141
+ ...base,
142
+ ...options.id !== void 0 && { id: options.id },
143
+ ...options.env !== void 0 && { env: options.env }
144
+ });
145
+ }
146
+ get containerId() {
147
+ return this._containerId ?? this._containerName;
148
+ }
149
+ async start() {
150
+ await this._runPlainLifecycle("starting", "running", () => this._startContainer());
151
+ }
152
+ async stop() {
153
+ await this._runPlainLifecycle("stopping", "stopped", () => this._stopContainer());
154
+ }
155
+ async destroy() {
156
+ await this._runPlainLifecycle("destroying", "destroyed", () => this._destroyContainer());
157
+ }
158
+ async _startContainer() {
159
+ const existing = await this._inspectContainer();
160
+ if (existing) {
161
+ this._assertMastraOwned(existing);
162
+ this._assertCompatibleConfig(existing);
163
+ this._containerId = existing.configuration?.id ?? this._containerName;
164
+ if (!isRunning(existing)) {
165
+ const result = await this._runCli(["start", this.containerId]);
166
+ this._assertSuccess(result, `start Apple container ${this.containerId}`);
167
+ await this._waitUntilContainerReady(`start Apple container ${this.containerId}`);
168
+ }
169
+ return;
170
+ }
171
+ const env = envFlags(this._env);
172
+ const result = await this._runCli(this._buildRunArgs(env.args), { env: env.env });
173
+ this._assertSuccess(result, `create Apple container ${this._containerName}`);
174
+ this._containerId = this._containerName;
175
+ try {
176
+ await this._waitUntilContainerReady(`create Apple container ${this._containerName}`);
177
+ } catch (error) {
178
+ if (this._deleteOnDestroy) await this._deleteContainerIgnoringMissing();
179
+ throw error;
180
+ }
181
+ }
182
+ async _stopContainer() {
183
+ const existing = await this._inspectContainer();
184
+ if (!existing) return;
185
+ this._assertMastraOwned(existing);
186
+ if (!isRunning(existing)) return;
187
+ const result = await this._runCli(["stop", this.containerId]);
188
+ if (!result.success && !isMissingContainerMessage(result.stderr)) this._assertSuccess(result, `stop Apple container ${this.containerId}`);
189
+ }
190
+ async _destroyContainer() {
191
+ if (!this._deleteOnDestroy) {
192
+ await this._stopContainer();
193
+ return;
194
+ }
195
+ const existing = await this._inspectContainer();
196
+ if (!existing) return;
197
+ this._assertMastraOwned(existing);
198
+ await this._deleteContainerIgnoringMissing();
199
+ }
200
+ async executeCommand(command, args = [], options = {}) {
201
+ await this.ensureRunning();
202
+ const commandTimeout = options.timeout ?? this._timeout;
203
+ const hasCommandTimeout = Number.isFinite(commandTimeout) && commandTimeout > 0;
204
+ const fullCommand = buildShellCommand(command, args);
205
+ const shellCommand = hasCommandTimeout ? buildTimeoutShellCommand(fullCommand, commandTimeout) : fullCommand;
206
+ const env = envFlags({
207
+ ...this._env,
208
+ ...options.env
209
+ });
210
+ const cliArgs = [
211
+ "exec",
212
+ ...env.args,
213
+ "--workdir",
214
+ options.cwd ?? this._workingDir,
215
+ this.containerId,
216
+ "sh",
217
+ "-lc",
218
+ shellCommand
219
+ ];
220
+ const result = await this._runner.run(cliArgs, {
221
+ timeout: hasCommandTimeout ? commandTimeout + APPLE_CONTAINER_CLI_GRACE_TIMEOUT_MS : void 0,
222
+ env: env.env,
223
+ abortSignal: options.abortSignal,
224
+ onStdout: options.onStdout,
225
+ onStderr: options.onStderr,
226
+ maxRetainedBytes: options.maxRetainedBytes
227
+ });
228
+ const timedOut = result.exitCode === APPLE_CONTAINER_TIMEOUT_EXIT_CODE && result.stderr.includes(APPLE_CONTAINER_TIMEOUT_MARKER);
229
+ const stderr = timedOut ? stripTimeoutMarker(result.stderr) : result.stderr;
230
+ return {
231
+ ...result,
232
+ stderr,
233
+ ...timedOut && {
234
+ timedOut: true,
235
+ killed: true
236
+ },
237
+ command: fullCommand,
238
+ args
239
+ };
240
+ }
241
+ async getInfo() {
242
+ const resources = (this._containerId ? await this._inspectContainer() : void 0)?.configuration?.resources;
243
+ return {
244
+ id: this.id,
245
+ name: this.name,
246
+ provider: this.provider,
247
+ status: this.status,
248
+ createdAt: this._createdAt,
249
+ resources: resources ? {
250
+ cpuCores: resources.cpus,
251
+ memoryMB: resources.memoryInBytes ? Math.round(resources.memoryInBytes / 1024 / 1024) : void 0
252
+ } : void 0,
253
+ metadata: { ...this._serializableConfig() }
254
+ };
255
+ }
256
+ getInstructions(opts) {
257
+ const defaultInstructions = this._buildDefaultInstructions();
258
+ if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
259
+ if (typeof this._instructionsOverride === "function") return this._instructionsOverride({
260
+ defaultInstructions,
261
+ requestContext: opts?.requestContext
262
+ });
263
+ return defaultInstructions;
264
+ }
265
+ _buildDefaultInstructions() {
266
+ const parts = [`Apple container sandbox: commands run inside a local OCI Linux container from image ${this._image}.`, `Working directory: ${this._workingDir}.`];
267
+ const volumeCount = Object.keys(this._volumes).length + this._mounts.length;
268
+ if (volumeCount > 0) parts.push(`${volumeCount} host mount(s) are configured.`);
269
+ if (this._timeout > 0) parts.push(`Default command timeout: ${Math.ceil(this._timeout / 1e3)}s.`);
270
+ return parts.join(" ");
271
+ }
272
+ async _runPlainLifecycle(activeStatus, completeStatus, operation) {
273
+ const managedByBaseWrapper = this.status === activeStatus;
274
+ if (!managedByBaseWrapper) this.status = activeStatus;
275
+ try {
276
+ await operation();
277
+ if (!managedByBaseWrapper) this.status = completeStatus;
278
+ } catch (error) {
279
+ if (!managedByBaseWrapper) this.status = "error";
280
+ throw error;
281
+ }
282
+ }
283
+ async _inspectContainer() {
284
+ const result = await this._runCli(["inspect", this.containerId]);
285
+ if (!result.success) {
286
+ if (isMissingContainerMessage(result.stderr)) return void 0;
287
+ this._assertSuccess(result, `inspect Apple container ${this.containerId}`);
288
+ return;
289
+ }
290
+ try {
291
+ const parsed = JSON.parse(result.stdout);
292
+ return Array.isArray(parsed) ? parsed[0] : parsed;
293
+ } catch (error) {
294
+ throw new SandboxExecutionError(`Failed to parse Apple container inspect output for ${this.containerId}: ${error instanceof Error ? error.message : String(error)}`, 1, result.stdout, result.stderr);
295
+ }
296
+ }
297
+ _buildRunArgs(envArgs) {
298
+ const args = [
299
+ "run",
300
+ "-d",
301
+ "--name",
302
+ this._containerName,
303
+ "--workdir",
304
+ this._workingDir
305
+ ];
306
+ args.push(...envArgs);
307
+ for (const [hostPath, containerPath] of Object.entries(this._volumes)) args.push("--volume", `${hostPath}:${containerPath}`);
308
+ for (const mount of this._mounts) args.push("--mount", mount);
309
+ for (const [key, value] of Object.entries(this._labels)) args.push("--label", `${key}=${value}`);
310
+ for (const port of this._publishedPorts) args.push("--publish", port);
311
+ for (const socket of this._publishedSockets) args.push("--publish-socket", socket);
312
+ for (const cap of this._capAdd) args.push("--cap-add", cap);
313
+ for (const cap of this._capDrop) args.push("--cap-drop", cap);
314
+ for (const tmpfs of this._tmpfs) args.push("--tmpfs", tmpfs);
315
+ for (const dns of this._dns) args.push("--dns", dns);
316
+ for (const domain of this._dnsSearch) args.push("--dns-search", domain);
317
+ if (this._network) args.push("--network", this._network);
318
+ if (this._cpus !== void 0) args.push("--cpus", String(this._cpus));
319
+ if (this._memory !== void 0) args.push("--memory", this._memory);
320
+ if (this._platform) args.push("--platform", this._platform);
321
+ if (this._arch) args.push("--arch", this._arch);
322
+ if (this._os) args.push("--os", this._os);
323
+ if (this._rosetta) args.push("--rosetta");
324
+ if (this._readonlyRootfs) args.push("--read-only");
325
+ if (this._ssh) args.push("--ssh");
326
+ if (this._init) args.push("--init");
327
+ if (this._virtualization) args.push("--virtualization");
328
+ if (this._noDns) args.push("--no-dns");
329
+ args.push(this._image, ...this._command);
330
+ return args;
331
+ }
332
+ async _waitUntilContainerReady(action) {
333
+ const deadline = Date.now() + APPLE_CONTAINER_READY_TIMEOUT_MS;
334
+ let lastResult;
335
+ while (Date.now() < deadline) {
336
+ const inspect = await this._inspectContainer();
337
+ if (!inspect) throw new SandboxExecutionError(`${action} failed because Apple container ${this.containerId} disappeared`, 1, "", "");
338
+ this._assertMastraOwned(inspect);
339
+ this._assertCompatibleConfig(inspect);
340
+ if (!isRunning(inspect)) {
341
+ const state = getContainerState(inspect) ?? "not running";
342
+ throw new SandboxExecutionError(`${action} failed because Apple container ${this.containerId} is ${state}`, 1, "", "");
343
+ }
344
+ lastResult = await this._runCli([
345
+ "exec",
346
+ this.containerId,
347
+ "sh",
348
+ "-lc",
349
+ "true"
350
+ ], { timeout: APPLE_CONTAINER_READY_EXEC_TIMEOUT_MS });
351
+ if (lastResult.success) return;
352
+ if (!isMissingContainerMessage(lastResult.stderr) && !/not running|not yet running/i.test(lastResult.stderr)) break;
353
+ await delay(100);
354
+ }
355
+ throw new SandboxExecutionError(`${action} failed because Apple container ${this.containerId} did not become ready for exec`, lastResult?.exitCode ?? 1, lastResult?.stdout ?? "", lastResult?.stderr ?? "");
356
+ }
357
+ async _deleteContainerIgnoringMissing() {
358
+ const result = await this._runCli([
359
+ "delete",
360
+ "--force",
361
+ this.containerId
362
+ ]);
363
+ if (!result.success && !isMissingContainerMessage(result.stderr)) this._assertSuccess(result, `delete Apple container ${this.containerId}`);
364
+ }
365
+ _runtimeConfigForHash() {
366
+ return {
367
+ image: this._image,
368
+ command: this._command,
369
+ env: this._env,
370
+ volumes: this._volumes,
371
+ mounts: this._mounts,
372
+ network: this._network,
373
+ publishedPorts: this._publishedPorts,
374
+ publishedSockets: this._publishedSockets,
375
+ cpus: this._cpus,
376
+ memory: this._memory,
377
+ platform: this._platform,
378
+ arch: this._arch,
379
+ os: this._os,
380
+ rosetta: this._rosetta,
381
+ readonlyRootfs: this._readonlyRootfs,
382
+ ssh: this._ssh,
383
+ init: this._init,
384
+ virtualization: this._virtualization,
385
+ capAdd: this._capAdd,
386
+ capDrop: this._capDrop,
387
+ tmpfs: this._tmpfs,
388
+ dns: this._dns,
389
+ dnsSearch: this._dnsSearch,
390
+ noDns: this._noDns,
391
+ labels: this._userLabels,
392
+ workingDir: this._workingDir
393
+ };
394
+ }
395
+ _serializableConfig() {
396
+ return compactConfig({
397
+ id: this.id,
398
+ name: this._configuredName,
399
+ image: this._image,
400
+ command: this._command,
401
+ env: this._env,
402
+ volumes: this._volumes,
403
+ mounts: this._mounts,
404
+ network: this._network,
405
+ publishedPorts: this._publishedPorts,
406
+ publishedSockets: this._publishedSockets,
407
+ cpus: this._cpus,
408
+ memory: this._memory,
409
+ platform: this._platform,
410
+ arch: this._arch,
411
+ os: this._os,
412
+ rosetta: this._rosetta,
413
+ readonlyRootfs: this._readonlyRootfs,
414
+ ssh: this._ssh,
415
+ init: this._init,
416
+ virtualization: this._virtualization,
417
+ capAdd: this._capAdd,
418
+ capDrop: this._capDrop,
419
+ tmpfs: this._tmpfs,
420
+ dns: this._dns,
421
+ dnsSearch: this._dnsSearch,
422
+ noDns: this._noDns,
423
+ labels: this._userLabels,
424
+ workingDir: this._workingDir,
425
+ timeout: this._timeout,
426
+ deleteOnDestroy: this._deleteOnDestroy
427
+ });
428
+ }
429
+ _assertSuccess(result, action) {
430
+ if (result.success) return;
431
+ throw new SandboxExecutionError(`${action} failed with exit code ${result.exitCode}: ${result.stderr}`, result.exitCode, result.stdout, result.stderr);
432
+ }
433
+ _assertMastraOwned(inspect) {
434
+ if (isMastraOwned(inspect, this.id)) return;
435
+ throw new SandboxExecutionError(`Refusing to manage Apple container ${this.containerId} because it is not labeled as Mastra sandbox ${this.id}`, 1, "", "");
436
+ }
437
+ _assertCompatibleConfig(inspect) {
438
+ const existingHash = inspect.configuration?.labels?.["mastra.sandbox.config-hash"];
439
+ if (!existingHash || existingHash === this._configHash) return;
440
+ throw new SandboxExecutionError(`Refusing to manage Apple container ${this.containerId} because its immutable configuration does not match sandbox ${this.id}`, 1, "", "");
441
+ }
442
+ _runCli(args, options = {}) {
443
+ return this._runner.run(args, {
444
+ timeout: this._timeout,
445
+ ...options
446
+ });
447
+ }
489
448
  };
490
449
  function runAppleContainerCli(binary, args, options = {}) {
491
- const handle = new AppleContainerCliProcess(binary, args, options);
492
- return handle.wait();
450
+ return new AppleContainerCliProcess(binary, args, options).wait();
493
451
  }
494
452
  var AppleContainerCliProcess = class extends ProcessHandle {
495
- pid;
496
- exitCode;
497
- child;
498
- waitPromise;
499
- startedAt = Date.now();
500
- killed = false;
501
- timedOut = false;
502
- forceKillTimeout;
503
- constructor(binary, args, options = {}) {
504
- super({
505
- maxRetainedBytes: options.maxRetainedBytes,
506
- onStdout: options.onStdout,
507
- onStderr: options.onStderr
508
- });
509
- this.child = spawn(binary, args, {
510
- stdio: ["ignore", "pipe", "pipe"],
511
- env: options.env ? { ...process.env, ...options.env } : process.env
512
- });
513
- this.pid = this.child.pid ? String(this.child.pid) : `${binary}:${args.join(" ")}`;
514
- let settled = false;
515
- const stdoutDecoder = new StringDecoder();
516
- const stderrDecoder = new StringDecoder();
517
- let stdoutDecoderEnded = false;
518
- let stderrDecoderEnded = false;
519
- let timeout;
520
- const onAbort = () => {
521
- void this.kill();
522
- };
523
- const flushStdout = () => {
524
- if (stdoutDecoderEnded) return;
525
- stdoutDecoderEnded = true;
526
- const data = stdoutDecoder.end();
527
- if (data) this.emitStdout(data);
528
- };
529
- const flushStderr = () => {
530
- if (stderrDecoderEnded) return;
531
- stderrDecoderEnded = true;
532
- const data = stderrDecoder.end();
533
- if (data) this.emitStderr(data);
534
- };
535
- const finish = (exitCode) => {
536
- this.exitCode = exitCode;
537
- return {
538
- success: exitCode === 0,
539
- exitCode,
540
- stdout: this.stdout,
541
- stderr: this.stderr,
542
- executionTimeMs: Date.now() - this.startedAt,
543
- killed: this.killed,
544
- timedOut: this.timedOut
545
- };
546
- };
547
- const cleanup = () => {
548
- if (timeout) clearTimeout(timeout);
549
- if (this.forceKillTimeout) clearTimeout(this.forceKillTimeout);
550
- options.abortSignal?.removeEventListener("abort", onAbort);
551
- };
552
- this.waitPromise = new Promise((resolve, reject) => {
553
- const settle = (callback) => {
554
- if (settled) return;
555
- settled = true;
556
- cleanup();
557
- callback();
558
- };
559
- this.child.stdout.on("data", (chunk) => {
560
- const data = stdoutDecoder.write(chunk);
561
- if (data) this.emitStdout(data);
562
- });
563
- this.child.stderr.on("data", (chunk) => {
564
- const data = stderrDecoder.write(chunk);
565
- if (data) this.emitStderr(data);
566
- });
567
- this.child.stdout.on("end", flushStdout);
568
- this.child.stderr.on("end", flushStderr);
569
- this.child.on("error", (error) => {
570
- settle(() => {
571
- flushStdout();
572
- flushStderr();
573
- reject(
574
- error instanceof Error && "code" in error && error.code === "ENOENT" ? new SandboxExecutionError(`Apple container CLI not found: ${binary}`, 127, this.stdout, error.message) : error
575
- );
576
- });
577
- });
578
- this.child.on("close", (code) => {
579
- settle(() => {
580
- flushStdout();
581
- flushStderr();
582
- resolve(finish(code ?? (this.killed ? 137 : 1)));
583
- });
584
- });
585
- });
586
- timeout = options.timeout && options.timeout > 0 ? setTimeout(() => {
587
- this.timedOut = true;
588
- void this.kill();
589
- }, options.timeout) : void 0;
590
- if (options.abortSignal) {
591
- if (options.abortSignal.aborted) {
592
- void this.kill();
593
- } else {
594
- options.abortSignal.addEventListener("abort", onAbort, { once: true });
595
- }
596
- }
597
- }
598
- async wait() {
599
- return this.waitPromise;
600
- }
601
- async kill() {
602
- if (this.exitCode !== void 0 || this.child.killed) return false;
603
- this.killed = true;
604
- this.child.kill("SIGTERM");
605
- this.forceKillTimeout = setTimeout(() => {
606
- if (this.exitCode === void 0 && this.child.exitCode === null && this.child.signalCode === null) {
607
- this.child.kill("SIGKILL");
608
- }
609
- }, 1e3);
610
- this.forceKillTimeout.unref();
611
- return true;
612
- }
613
- async sendStdin() {
614
- throw new Error("Apple container CLI runner does not support stdin");
615
- }
453
+ pid;
454
+ exitCode;
455
+ child;
456
+ waitPromise;
457
+ startedAt = Date.now();
458
+ killed = false;
459
+ timedOut = false;
460
+ forceKillTimeout;
461
+ constructor(binary, args, options = {}) {
462
+ super({
463
+ maxRetainedBytes: options.maxRetainedBytes,
464
+ onStdout: options.onStdout,
465
+ onStderr: options.onStderr
466
+ });
467
+ this.child = spawn(binary, args, {
468
+ stdio: [
469
+ "ignore",
470
+ "pipe",
471
+ "pipe"
472
+ ],
473
+ env: options.env ? {
474
+ ...process.env,
475
+ ...options.env
476
+ } : process.env
477
+ });
478
+ this.pid = this.child.pid ? String(this.child.pid) : `${binary}:${args.join(" ")}`;
479
+ let settled = false;
480
+ const stdoutDecoder = new StringDecoder();
481
+ const stderrDecoder = new StringDecoder();
482
+ let stdoutDecoderEnded = false;
483
+ let stderrDecoderEnded = false;
484
+ let timeout;
485
+ const onAbort = () => {
486
+ this.kill();
487
+ };
488
+ const flushStdout = () => {
489
+ if (stdoutDecoderEnded) return;
490
+ stdoutDecoderEnded = true;
491
+ const data = stdoutDecoder.end();
492
+ if (data) this.emitStdout(data);
493
+ };
494
+ const flushStderr = () => {
495
+ if (stderrDecoderEnded) return;
496
+ stderrDecoderEnded = true;
497
+ const data = stderrDecoder.end();
498
+ if (data) this.emitStderr(data);
499
+ };
500
+ const finish = (exitCode) => {
501
+ this.exitCode = exitCode;
502
+ return {
503
+ success: exitCode === 0,
504
+ exitCode,
505
+ stdout: this.stdout,
506
+ stderr: this.stderr,
507
+ executionTimeMs: Date.now() - this.startedAt,
508
+ killed: this.killed,
509
+ timedOut: this.timedOut
510
+ };
511
+ };
512
+ const cleanup = () => {
513
+ if (timeout) clearTimeout(timeout);
514
+ if (this.forceKillTimeout) clearTimeout(this.forceKillTimeout);
515
+ options.abortSignal?.removeEventListener("abort", onAbort);
516
+ };
517
+ this.waitPromise = new Promise((resolve, reject) => {
518
+ const settle = (callback) => {
519
+ if (settled) return;
520
+ settled = true;
521
+ cleanup();
522
+ callback();
523
+ };
524
+ this.child.stdout.on("data", (chunk) => {
525
+ const data = stdoutDecoder.write(chunk);
526
+ if (data) this.emitStdout(data);
527
+ });
528
+ this.child.stderr.on("data", (chunk) => {
529
+ const data = stderrDecoder.write(chunk);
530
+ if (data) this.emitStderr(data);
531
+ });
532
+ this.child.stdout.on("end", flushStdout);
533
+ this.child.stderr.on("end", flushStderr);
534
+ this.child.on("error", (error) => {
535
+ settle(() => {
536
+ flushStdout();
537
+ flushStderr();
538
+ reject(error instanceof Error && "code" in error && error.code === "ENOENT" ? new SandboxExecutionError(`Apple container CLI not found: ${binary}`, 127, this.stdout, error.message) : error);
539
+ });
540
+ });
541
+ this.child.on("close", (code) => {
542
+ settle(() => {
543
+ flushStdout();
544
+ flushStderr();
545
+ resolve(finish(code ?? (this.killed ? 137 : 1)));
546
+ });
547
+ });
548
+ });
549
+ timeout = options.timeout && options.timeout > 0 ? setTimeout(() => {
550
+ this.timedOut = true;
551
+ this.kill();
552
+ }, options.timeout) : void 0;
553
+ if (options.abortSignal) if (options.abortSignal.aborted) this.kill();
554
+ else options.abortSignal.addEventListener("abort", onAbort, { once: true });
555
+ }
556
+ async wait() {
557
+ return this.waitPromise;
558
+ }
559
+ async kill() {
560
+ if (this.exitCode !== void 0 || this.child.killed) return false;
561
+ this.killed = true;
562
+ this.child.kill("SIGTERM");
563
+ this.forceKillTimeout = setTimeout(() => {
564
+ if (this.exitCode === void 0 && this.child.exitCode === null && this.child.signalCode === null) this.child.kill("SIGKILL");
565
+ }, 1e3);
566
+ this.forceKillTimeout.unref();
567
+ return true;
568
+ }
569
+ async sendStdin() {
570
+ throw new Error("Apple container CLI runner does not support stdin");
571
+ }
572
+ async closeStdin() {
573
+ throw new UnsupportedStdinCloseError("Apple container CLI runner does not support closing stdin");
574
+ }
616
575
  };
617
576
  function generateId() {
618
- return `apple-container-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
577
+ return `apple-container-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
619
578
  }
620
579
  function sanitizeContainerName(name) {
621
- const sanitized = name.replace(/[^a-zA-Z0-9_.-]/g, "-");
622
- return /^[a-zA-Z0-9]/.test(sanitized) ? sanitized : `c-${sanitized}`;
580
+ const sanitized = name.replace(/[^a-zA-Z0-9_.-]/g, "-");
581
+ return /^[a-zA-Z0-9]/.test(sanitized) ? sanitized : `c-${sanitized}`;
623
582
  }
624
583
  function buildShellCommand(command, args) {
625
- return args.length > 0 ? [command, ...args].map(shellQuote).join(" ") : command;
584
+ return args.length > 0 ? [command, ...args].map(shellQuote).join(" ") : command;
626
585
  }
627
586
  function buildTimeoutShellCommand(command, timeoutMs) {
628
- const timeoutSeconds = formatTimeoutSeconds(timeoutMs);
629
- const innerScript = [
630
- `sh -lc ${shellQuote(command)} & child=$!`,
631
- `trap 'kill -TERM "$child" 2>/dev/null; wait "$child" 2>/dev/null; exit ${APPLE_CONTAINER_TIMEOUT_EXIT_CODE}' TERM INT`,
632
- 'wait "$child"; code=$?',
633
- "trap - TERM INT",
634
- 'printf "%s" "$code" > "$MASTRA_TIMEOUT_RESULT_FILE"',
635
- 'exit "$code"'
636
- ].join("; ");
637
- return [
638
- 'result_file="/tmp/.mastra-apple-container-exit-$$"',
639
- 'rm -f "$result_file"',
640
- `MASTRA_TIMEOUT_RESULT_FILE="$result_file" timeout ${timeoutSeconds}s sh -lc ${shellQuote(innerScript)}`,
641
- "timeout_code=$?",
642
- 'if [ -f "$result_file" ]; then code="$(cat "$result_file")"; rm -f "$result_file"; exit "$code"; fi',
643
- 'rm -f "$result_file"',
644
- `case "$timeout_code" in 124|137|143) printf '%s\\n' ${shellQuote(APPLE_CONTAINER_TIMEOUT_MARKER)} >&2; exit ${APPLE_CONTAINER_TIMEOUT_EXIT_CODE};; *) exit "$timeout_code";; esac`
645
- ].join("; ");
587
+ return [
588
+ "result_file=\"/tmp/.mastra-apple-container-exit-$$\"",
589
+ "rm -f \"$result_file\"",
590
+ `MASTRA_TIMEOUT_RESULT_FILE="$result_file" timeout ${formatTimeoutSeconds(timeoutMs)}s sh -lc ${shellQuote([
591
+ `sh -lc ${shellQuote(command)} & child=$!`,
592
+ `trap 'kill -TERM "$child" 2>/dev/null; wait "$child" 2>/dev/null; exit ${APPLE_CONTAINER_TIMEOUT_EXIT_CODE}' TERM INT`,
593
+ "wait \"$child\"; code=$?",
594
+ "trap - TERM INT",
595
+ "printf \"%s\" \"$code\" > \"$MASTRA_TIMEOUT_RESULT_FILE\"",
596
+ "exit \"$code\""
597
+ ].join("; "))}`,
598
+ "timeout_code=$?",
599
+ "if [ -f \"$result_file\" ]; then code=\"$(cat \"$result_file\")\"; rm -f \"$result_file\"; exit \"$code\"; fi",
600
+ "rm -f \"$result_file\"",
601
+ `case "$timeout_code" in 124|137|143) printf '%s\\n' ${shellQuote(APPLE_CONTAINER_TIMEOUT_MARKER)} >&2; exit ${APPLE_CONTAINER_TIMEOUT_EXIT_CODE};; *) exit "$timeout_code";; esac`
602
+ ].join("; ");
646
603
  }
647
604
  function formatTimeoutSeconds(timeoutMs) {
648
- return Math.max(timeoutMs / 1e3, 1e-3).toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
605
+ return Math.max(timeoutMs / 1e3, .001).toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
649
606
  }
650
607
  function shellQuote(arg) {
651
- if (/^[a-zA-Z0-9._\-\/=:@]+$/.test(arg)) return arg;
652
- return `'${arg.replace(/'/g, "'\\''")}'`;
608
+ if (/^[a-zA-Z0-9._\-\/=:@]+$/.test(arg)) return arg;
609
+ return `'${arg.replace(/'/g, "'\\''")}'`;
653
610
  }
654
611
  function stripTimeoutMarker(stderr) {
655
- return stderr.split("\n").filter((line) => line.trim() !== APPLE_CONTAINER_TIMEOUT_MARKER).join("\n").replace(/^\n+|\n+$/g, "");
612
+ return stderr.split("\n").filter((line) => line.trim() !== APPLE_CONTAINER_TIMEOUT_MARKER).join("\n").replace(/^\n+|\n+$/g, "");
656
613
  }
657
614
  function envFlags(env) {
658
- const args = [];
659
- const childEnv = {};
660
- for (const [key, value] of Object.entries(env)) {
661
- if (value === void 0) continue;
662
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
663
- throw new Error(`Invalid environment variable name for Apple container command: ${key}`);
664
- }
665
- args.push("--env", key);
666
- childEnv[key] = value;
667
- }
668
- return { args, env: childEnv };
615
+ const args = [];
616
+ const childEnv = {};
617
+ for (const [key, value] of Object.entries(env)) {
618
+ if (value === void 0) continue;
619
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name for Apple container command: ${key}`);
620
+ args.push("--env", key);
621
+ childEnv[key] = value;
622
+ }
623
+ return {
624
+ args,
625
+ env: childEnv
626
+ };
669
627
  }
670
628
  function isRunning(inspect) {
671
- return getContainerState(inspect) === "running";
629
+ return getContainerState(inspect) === "running";
672
630
  }
673
631
  function getContainerState(inspect) {
674
- return typeof inspect.status === "string" ? inspect.status : inspect.status?.state;
632
+ return typeof inspect.status === "string" ? inspect.status : inspect.status?.state;
675
633
  }
676
634
  function isMastraOwned(inspect, sandboxId) {
677
- const labels = inspect.configuration?.labels;
678
- return labels?.["mastra.sandbox"] === "true" && labels["mastra.sandbox.id"] === sandboxId;
635
+ const labels = inspect.configuration?.labels;
636
+ return labels?.["mastra.sandbox"] === "true" && labels["mastra.sandbox.id"] === sandboxId;
679
637
  }
680
638
  function isMissingContainerMessage(message) {
681
- return /not found|no such|does not exist|unknown container/i.test(message);
639
+ return /not found|no such|does not exist|unknown container/i.test(message);
682
640
  }
683
641
  function validateTmpfsPaths(tmpfs) {
684
- for (const entry of tmpfs) {
685
- if (!entry.startsWith("/") || /[:,]/.test(entry)) {
686
- throw new Error(
687
- `Invalid Apple container tmpfs path "${entry}". Apple container --tmpfs accepts container paths only, for example "/tmp".`
688
- );
689
- }
690
- }
642
+ for (const entry of tmpfs) if (!entry.startsWith("/") || /[:,]/.test(entry)) throw new Error(`Invalid Apple container tmpfs path "${entry}". Apple container --tmpfs accepts container paths only, for example "/tmp".`);
691
643
  }
692
644
  function compactConfig(config) {
693
- const result = {};
694
- for (const [key, value] of Object.entries(config)) {
695
- if (value === void 0) continue;
696
- if (Array.isArray(value) && value.length === 0) continue;
697
- if (isPlainRecord(value) && Object.keys(value).length === 0) continue;
698
- result[key] = value;
699
- }
700
- return result;
645
+ const result = {};
646
+ for (const [key, value] of Object.entries(config)) {
647
+ if (value === void 0) continue;
648
+ if (Array.isArray(value) && value.length === 0) continue;
649
+ if (isPlainRecord(value) && Object.keys(value).length === 0) continue;
650
+ result[key] = value;
651
+ }
652
+ return result;
701
653
  }
702
654
  function hashConfig(config) {
703
- return createHash("sha256").update(stableStringify(compactConfig(config))).digest("hex").slice(0, 16);
655
+ return createHash("sha256").update(stableStringify(compactConfig(config))).digest("hex").slice(0, 16);
704
656
  }
705
657
  function stableStringify(value) {
706
- if (Array.isArray(value)) {
707
- return `[${value.map(stableStringify).join(",")}]`;
708
- }
709
- if (isPlainRecord(value)) {
710
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
711
- }
712
- return JSON.stringify(value);
658
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
659
+ if (isPlainRecord(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
660
+ return JSON.stringify(value);
713
661
  }
714
662
  function isPlainRecord(value) {
715
- return typeof value === "object" && value !== null && !Array.isArray(value);
663
+ return typeof value === "object" && value !== null && !Array.isArray(value);
716
664
  }
717
665
  function delay(ms) {
718
- return new Promise((resolve) => setTimeout(resolve, ms));
666
+ return new Promise((resolve) => setTimeout(resolve, ms));
719
667
  }
720
-
721
- // src/provider.ts
722
- var appleContainerSandboxProvider = {
723
- id: "apple-container",
724
- name: "Apple Container Sandbox",
725
- description: "Local OCI Linux container sandbox powered by Apple container",
726
- configSchema: {
727
- type: "object",
728
- additionalProperties: false,
729
- properties: {
730
- id: {
731
- type: "string",
732
- description: "Stable sandbox ID used for reconnecting to the same Apple container."
733
- },
734
- image: {
735
- type: "string",
736
- description: "OCI image to use",
737
- default: "node:22-slim"
738
- },
739
- name: {
740
- type: "string",
741
- description: "Apple container name. Defaults to the sandbox ID."
742
- },
743
- command: {
744
- type: "array",
745
- description: "Container init command. Must keep the container alive for exec-based command execution.",
746
- items: { type: "string" }
747
- },
748
- env: {
749
- type: "object",
750
- description: "Environment variables",
751
- additionalProperties: { type: "string" }
752
- },
753
- volumes: {
754
- type: "object",
755
- description: "Host-to-container bind mounts (host path -> container path)",
756
- additionalProperties: { type: "string" }
757
- },
758
- mounts: {
759
- type: "array",
760
- description: "Raw Apple container --mount specs",
761
- items: { type: "string" }
762
- },
763
- network: {
764
- type: "string",
765
- description: "Apple container network attachment spec"
766
- },
767
- publishedPorts: {
768
- type: "array",
769
- description: "Port publish specs",
770
- items: { type: "string" }
771
- },
772
- publishedSockets: {
773
- type: "array",
774
- description: "Socket publish specs",
775
- items: { type: "string" }
776
- },
777
- cpus: {
778
- anyOf: [{ type: "number" }, { type: "string" }],
779
- description: "Number of CPUs to allocate"
780
- },
781
- memory: {
782
- type: "string",
783
- description: "Memory allocation, for example 1G"
784
- },
785
- platform: {
786
- type: "string",
787
- description: "OCI platform, for example linux/arm64"
788
- },
789
- arch: {
790
- type: "string",
791
- description: "Image architecture for multi-arch images"
792
- },
793
- os: {
794
- type: "string",
795
- description: "Operating system for multi-platform images"
796
- },
797
- rosetta: {
798
- type: "boolean",
799
- description: "Enable Rosetta in the container",
800
- default: false
801
- },
802
- readonlyRootfs: {
803
- type: "boolean",
804
- description: "Mount the container root filesystem as read-only",
805
- default: false
806
- },
807
- ssh: {
808
- type: "boolean",
809
- description: "Forward the host SSH agent socket",
810
- default: false
811
- },
812
- init: {
813
- type: "boolean",
814
- description: "Enable Apple's init process in the container",
815
- default: true
816
- },
817
- virtualization: {
818
- type: "boolean",
819
- description: "Expose virtualization capabilities to the container",
820
- default: false
821
- },
822
- capAdd: {
823
- type: "array",
824
- description: "Linux capabilities to add",
825
- items: { type: "string" }
826
- },
827
- capDrop: {
828
- type: "array",
829
- description: "Linux capabilities to drop",
830
- items: { type: "string" }
831
- },
832
- tmpfs: {
833
- type: "array",
834
- description: "tmpfs destination paths",
835
- items: { type: "string" }
836
- },
837
- dns: {
838
- type: "array",
839
- description: "DNS nameserver IPs",
840
- items: { type: "string" }
841
- },
842
- dnsSearch: {
843
- type: "array",
844
- description: "DNS search domains",
845
- items: { type: "string" }
846
- },
847
- noDns: {
848
- type: "boolean",
849
- description: "Do not configure DNS in the container",
850
- default: false
851
- },
852
- labels: {
853
- type: "object",
854
- description: "Container labels",
855
- additionalProperties: { type: "string" }
856
- },
857
- workingDir: {
858
- type: "string",
859
- description: "Working directory inside the container",
860
- default: "/workspace"
861
- },
862
- timeout: {
863
- type: "number",
864
- description: "Default command timeout in milliseconds",
865
- default: 3e5
866
- },
867
- deleteOnDestroy: {
868
- type: "boolean",
869
- description: "Delete the Apple container on destroy",
870
- default: true
871
- }
872
- }
873
- },
874
- createSandbox: (config) => {
875
- const {
876
- id,
877
- image,
878
- name,
879
- command,
880
- env,
881
- volumes,
882
- mounts,
883
- network,
884
- publishedPorts,
885
- publishedSockets,
886
- cpus,
887
- memory,
888
- platform,
889
- arch,
890
- os,
891
- rosetta,
892
- readonlyRootfs,
893
- ssh,
894
- init,
895
- virtualization,
896
- capAdd,
897
- capDrop,
898
- tmpfs,
899
- dns,
900
- dnsSearch,
901
- noDns,
902
- labels,
903
- workingDir,
904
- timeout,
905
- deleteOnDestroy
906
- } = config;
907
- return new AppleContainerSandbox({
908
- id,
909
- image,
910
- name,
911
- command,
912
- env,
913
- volumes,
914
- mounts,
915
- network,
916
- publishedPorts,
917
- publishedSockets,
918
- cpus,
919
- memory,
920
- platform,
921
- arch,
922
- os,
923
- rosetta,
924
- readonlyRootfs,
925
- ssh,
926
- init,
927
- virtualization,
928
- capAdd,
929
- capDrop,
930
- tmpfs,
931
- dns,
932
- dnsSearch,
933
- noDns,
934
- labels,
935
- workingDir,
936
- timeout,
937
- deleteOnDestroy
938
- });
939
- }
668
+ //#endregion
669
+ //#region src/provider.ts
670
+ const appleContainerSandboxProvider = {
671
+ id: "apple-container",
672
+ name: "Apple Container Sandbox",
673
+ description: "Local OCI Linux container sandbox powered by Apple container",
674
+ configSchema: {
675
+ type: "object",
676
+ additionalProperties: false,
677
+ properties: {
678
+ id: {
679
+ type: "string",
680
+ description: "Stable sandbox ID used for reconnecting to the same Apple container."
681
+ },
682
+ image: {
683
+ type: "string",
684
+ description: "OCI image to use",
685
+ default: "node:22-slim"
686
+ },
687
+ name: {
688
+ type: "string",
689
+ description: "Apple container name. Defaults to the sandbox ID."
690
+ },
691
+ command: {
692
+ type: "array",
693
+ description: "Container init command. Must keep the container alive for exec-based command execution.",
694
+ items: { type: "string" }
695
+ },
696
+ env: {
697
+ type: "object",
698
+ description: "Environment variables",
699
+ additionalProperties: { type: "string" }
700
+ },
701
+ volumes: {
702
+ type: "object",
703
+ description: "Host-to-container bind mounts (host path -> container path)",
704
+ additionalProperties: { type: "string" }
705
+ },
706
+ mounts: {
707
+ type: "array",
708
+ description: "Raw Apple container --mount specs",
709
+ items: { type: "string" }
710
+ },
711
+ network: {
712
+ type: "string",
713
+ description: "Apple container network attachment spec"
714
+ },
715
+ publishedPorts: {
716
+ type: "array",
717
+ description: "Port publish specs",
718
+ items: { type: "string" }
719
+ },
720
+ publishedSockets: {
721
+ type: "array",
722
+ description: "Socket publish specs",
723
+ items: { type: "string" }
724
+ },
725
+ cpus: {
726
+ anyOf: [{ type: "number" }, { type: "string" }],
727
+ description: "Number of CPUs to allocate"
728
+ },
729
+ memory: {
730
+ type: "string",
731
+ description: "Memory allocation, for example 1G"
732
+ },
733
+ platform: {
734
+ type: "string",
735
+ description: "OCI platform, for example linux/arm64"
736
+ },
737
+ arch: {
738
+ type: "string",
739
+ description: "Image architecture for multi-arch images"
740
+ },
741
+ os: {
742
+ type: "string",
743
+ description: "Operating system for multi-platform images"
744
+ },
745
+ rosetta: {
746
+ type: "boolean",
747
+ description: "Enable Rosetta in the container",
748
+ default: false
749
+ },
750
+ readonlyRootfs: {
751
+ type: "boolean",
752
+ description: "Mount the container root filesystem as read-only",
753
+ default: false
754
+ },
755
+ ssh: {
756
+ type: "boolean",
757
+ description: "Forward the host SSH agent socket",
758
+ default: false
759
+ },
760
+ init: {
761
+ type: "boolean",
762
+ description: "Enable Apple's init process in the container",
763
+ default: true
764
+ },
765
+ virtualization: {
766
+ type: "boolean",
767
+ description: "Expose virtualization capabilities to the container",
768
+ default: false
769
+ },
770
+ capAdd: {
771
+ type: "array",
772
+ description: "Linux capabilities to add",
773
+ items: { type: "string" }
774
+ },
775
+ capDrop: {
776
+ type: "array",
777
+ description: "Linux capabilities to drop",
778
+ items: { type: "string" }
779
+ },
780
+ tmpfs: {
781
+ type: "array",
782
+ description: "tmpfs destination paths",
783
+ items: { type: "string" }
784
+ },
785
+ dns: {
786
+ type: "array",
787
+ description: "DNS nameserver IPs",
788
+ items: { type: "string" }
789
+ },
790
+ dnsSearch: {
791
+ type: "array",
792
+ description: "DNS search domains",
793
+ items: { type: "string" }
794
+ },
795
+ noDns: {
796
+ type: "boolean",
797
+ description: "Do not configure DNS in the container",
798
+ default: false
799
+ },
800
+ labels: {
801
+ type: "object",
802
+ description: "Container labels",
803
+ additionalProperties: { type: "string" }
804
+ },
805
+ workingDir: {
806
+ type: "string",
807
+ description: "Working directory inside the container",
808
+ default: "/workspace"
809
+ },
810
+ timeout: {
811
+ type: "number",
812
+ description: "Default command timeout in milliseconds",
813
+ default: 3e5
814
+ },
815
+ deleteOnDestroy: {
816
+ type: "boolean",
817
+ description: "Delete the Apple container on destroy",
818
+ default: true
819
+ }
820
+ }
821
+ },
822
+ createSandbox: (config) => {
823
+ const { id, image, name, command, env, volumes, mounts, network, publishedPorts, publishedSockets, cpus, memory, platform, arch, os, rosetta, readonlyRootfs, ssh, init, virtualization, capAdd, capDrop, tmpfs, dns, dnsSearch, noDns, labels, workingDir, timeout, deleteOnDestroy } = config;
824
+ return new AppleContainerSandbox({
825
+ id,
826
+ image,
827
+ name,
828
+ command,
829
+ env,
830
+ volumes,
831
+ mounts,
832
+ network,
833
+ publishedPorts,
834
+ publishedSockets,
835
+ cpus,
836
+ memory,
837
+ platform,
838
+ arch,
839
+ os,
840
+ rosetta,
841
+ readonlyRootfs,
842
+ ssh,
843
+ init,
844
+ virtualization,
845
+ capAdd,
846
+ capDrop,
847
+ tmpfs,
848
+ dns,
849
+ dnsSearch,
850
+ noDns,
851
+ labels,
852
+ workingDir,
853
+ timeout,
854
+ deleteOnDestroy
855
+ });
856
+ }
940
857
  };
941
-
858
+ //#endregion
942
859
  export { AppleContainerSandbox, DefaultAppleContainerCommandRunner, appleContainerSandboxProvider };
943
- //# sourceMappingURL=index.js.map
860
+
944
861
  //# sourceMappingURL=index.js.map