@mastra/docker 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # @mastra/docker
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Docker sandbox containers now support resource limits and security hardening through Docker HostConfig options. Configure memory, CPU quota, process IDs, capabilities, security options, read-only root filesystems, and tmpfs mounts. ([#16577](https://github.com/mastra-ai/mastra/pull/16577))
8
+
9
+ ```typescript
10
+ const sandbox = new DockerSandbox({
11
+ memory: 512 * 1024 * 1024,
12
+ memorySwap: 512 * 1024 * 1024,
13
+ cpuQuota: 100_000,
14
+ pidsLimit: 256,
15
+ readonlyRootfs: true,
16
+ capDrop: ['ALL'],
17
+ securityOpt: ['no-new-privileges:true'],
18
+ });
19
+ ```
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [[`20787de`](https://github.com/mastra-ai/mastra/commit/20787de5965234a1af28fe35f49437c537dbfa0d), [`784ad98`](https://github.com/mastra-ai/mastra/commit/784ad989549de91dc5d33ab8ef36caa6f7dcd34e), [`fceae1f`](https://github.com/mastra-ai/mastra/commit/fceae1f5f5db4722cb078a663c6eb4bd22944123), [`090a647`](https://github.com/mastra-ai/mastra/commit/090a647ba5a66d36f203f9f49457e03a1ff4e6fb), [`bf02acb`](https://github.com/mastra-ai/mastra/commit/bf02acbb8a6110f638ac844e89f1ebf04cb7fe74), [`090a647`](https://github.com/mastra-ai/mastra/commit/090a647ba5a66d36f203f9f49457e03a1ff4e6fb), [`bdb4cbf`](https://github.com/mastra-ai/mastra/commit/bdb4cbf8ba4b685d7481f28bb9dc3de6c79c9ed2), [`0fd3fbe`](https://github.com/mastra-ai/mastra/commit/0fd3fbe40fb63657aedd72f6e7b38c8e8ee6940d), [`f84447d`](https://github.com/mastra-ai/mastra/commit/f84447d6c80f3471836a9b300d246b331fb47e0d), [`a1a5b3e`](https://github.com/mastra-ai/mastra/commit/a1a5b3e42ab2ca5161ea21db59ebf28442680fa7), [`af84f57`](https://github.com/mastra-ai/mastra/commit/af84f571ed762e92e8e61c5f9a72363520914274), [`8b3c6f9`](https://github.com/mastra-ai/mastra/commit/8b3c6f90f7879833ba7d1bc70937e1d8f69d0804), [`fed0475`](https://github.com/mastra-ai/mastra/commit/fed0475ccfea31e4fc251469ac05640d0742c1f0), [`0d53730`](https://github.com/mastra-ai/mastra/commit/0d53730c1ed87ef80c87caa5701c4170ea8028e6), [`522f44d`](https://github.com/mastra-ai/mastra/commit/522f44d947214bfc06cff50599bae1ef3494880d)]:
24
+ - @mastra/core@1.34.0
25
+
26
+ ## 0.2.0-alpha.0
27
+
28
+ ### Minor Changes
29
+
30
+ - Docker sandbox containers now support resource limits and security hardening through Docker HostConfig options. Configure memory, CPU quota, process IDs, capabilities, security options, read-only root filesystems, and tmpfs mounts. ([#16577](https://github.com/mastra-ai/mastra/pull/16577))
31
+
32
+ ```typescript
33
+ const sandbox = new DockerSandbox({
34
+ memory: 512 * 1024 * 1024,
35
+ memorySwap: 512 * 1024 * 1024,
36
+ cpuQuota: 100_000,
37
+ pidsLimit: 256,
38
+ readonlyRootfs: true,
39
+ capDrop: ['ALL'],
40
+ securityOpt: ['no-new-privileges:true'],
41
+ });
42
+ ```
43
+
44
+ ### Patch Changes
45
+
46
+ - Updated dependencies [[`fceae1f`](https://github.com/mastra-ai/mastra/commit/fceae1f5f5db4722cb078a663c6eb4bd22944123), [`bf02acb`](https://github.com/mastra-ai/mastra/commit/bf02acbb8a6110f638ac844e89f1ebf04cb7fe74), [`0fd3fbe`](https://github.com/mastra-ai/mastra/commit/0fd3fbe40fb63657aedd72f6e7b38c8e8ee6940d), [`fed0475`](https://github.com/mastra-ai/mastra/commit/fed0475ccfea31e4fc251469ac05640d0742c1f0), [`522f44d`](https://github.com/mastra-ai/mastra/commit/522f44d947214bfc06cff50599bae1ef3494880d)]:
47
+ - @mastra/core@1.34.0-alpha.1
48
+
3
49
  ## 0.1.0
4
50
 
5
51
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var util = require('util');
3
4
  var workspace = require('@mastra/core/workspace');
4
5
  var Docker = require('dockerode');
5
6
 
@@ -275,6 +276,19 @@ var DockerSandbox = class extends workspace.MastraSandbox {
275
276
  _volumes;
276
277
  _network;
277
278
  _privileged;
279
+ _privilegedWasSet;
280
+ _memory;
281
+ _memorySwap;
282
+ _cpuShares;
283
+ _cpuQuota;
284
+ _cpuPeriod;
285
+ _pidsLimit;
286
+ _readonlyRootfs;
287
+ _capDrop;
288
+ _capAdd;
289
+ _securityOpt;
290
+ _ulimits;
291
+ _tmpfs;
278
292
  _workingDir;
279
293
  _labels;
280
294
  _instructionsOverride;
@@ -295,6 +309,19 @@ var DockerSandbox = class extends workspace.MastraSandbox {
295
309
  this._volumes = options.volumes ?? {};
296
310
  this._network = options.network;
297
311
  this._privileged = options.privileged ?? false;
312
+ this._privilegedWasSet = options.privileged !== void 0;
313
+ this._memory = options.memory;
314
+ this._memorySwap = options.memorySwap;
315
+ this._cpuShares = options.cpuShares;
316
+ this._cpuQuota = options.cpuQuota;
317
+ this._cpuPeriod = options.cpuPeriod;
318
+ this._pidsLimit = options.pidsLimit;
319
+ this._readonlyRootfs = options.readonlyRootfs;
320
+ this._capDrop = options.capDrop;
321
+ this._capAdd = options.capAdd;
322
+ this._securityOpt = options.securityOpt;
323
+ this._ulimits = options.ulimits;
324
+ this._tmpfs = options.tmpfs;
298
325
  this._workingDir = options.workingDir ?? "/workspace";
299
326
  this._labels = {
300
327
  ...options.labels,
@@ -324,6 +351,8 @@ var DockerSandbox = class extends workspace.MastraSandbox {
324
351
  this.logger.debug(`${LOG_PREFIX} Found existing container ${existing.Id}`);
325
352
  this._container = this._docker.getContainer(existing.Id);
326
353
  const info = await this._container.inspect();
354
+ this._warnOnPrivilegedHardeningConflict(info.HostConfig?.Privileged ?? this._privileged);
355
+ this._warnOnReconnectedHostConfigMismatch(existing.Id, info.HostConfig);
327
356
  const actualState = info.State?.Running ? "running" : "stopped";
328
357
  if (actualState !== "running") {
329
358
  this.logger.debug(`${LOG_PREFIX} Container exists but not running (${actualState}), starting...`);
@@ -333,6 +362,7 @@ var DockerSandbox = class extends workspace.MastraSandbox {
333
362
  this.logger.debug(`${LOG_PREFIX} Reconnected to container ${existing.Id}`);
334
363
  return;
335
364
  }
365
+ this._warnOnPrivilegedHardeningConflict(this._privileged);
336
366
  await this._ensureImage();
337
367
  const envArray = Object.entries(this._env).map(([k, v]) => `${k}=${v}`);
338
368
  const binds = Object.entries(this._volumes).map(([host, container]) => `${host}:${container}`);
@@ -346,7 +376,19 @@ var DockerSandbox = class extends workspace.MastraSandbox {
346
376
  HostConfig: {
347
377
  Binds: binds.length > 0 ? binds : void 0,
348
378
  NetworkMode: this._network,
349
- Privileged: this._privileged
379
+ Privileged: this._privileged,
380
+ Memory: this._memory,
381
+ MemorySwap: this._memorySwap,
382
+ CpuShares: this._cpuShares,
383
+ CpuQuota: this._cpuQuota,
384
+ CpuPeriod: this._cpuPeriod,
385
+ PidsLimit: this._pidsLimit,
386
+ ReadonlyRootfs: this._readonlyRootfs,
387
+ CapDrop: this._capDrop,
388
+ CapAdd: this._capAdd,
389
+ SecurityOpt: this._securityOpt,
390
+ Ulimits: this._ulimits?.map(toDockerUlimit),
391
+ Tmpfs: this._tmpfs
350
392
  },
351
393
  // Keep stdin open for interactive use
352
394
  OpenStdin: true,
@@ -356,6 +398,64 @@ var DockerSandbox = class extends workspace.MastraSandbox {
356
398
  this.processes.setContainer(this._container);
357
399
  this.logger.debug(`${LOG_PREFIX} Container started: ${this._container.id}`);
358
400
  }
401
+ _warnOnPrivilegedHardeningConflict(effectivePrivileged) {
402
+ if (!effectivePrivileged) return;
403
+ const conflictedHostConfigFields = [
404
+ this._capDrop && this._capDrop.length > 0 ? "CapDrop" : void 0,
405
+ this._capAdd && this._capAdd.length > 0 ? "CapAdd" : void 0,
406
+ this._securityOpt && this._securityOpt.length > 0 ? "SecurityOpt" : void 0
407
+ ].filter((field) => field !== void 0);
408
+ if (conflictedHostConfigFields.length === 0) return;
409
+ const optionNames = conflictedHostConfigFields.map(toDockerSandboxOptionName);
410
+ this.logger.warn(
411
+ `${LOG_PREFIX} Privileged containers can bypass some requested hardening controls: ${optionNames.join(", ")}`,
412
+ { fields: optionNames, hostConfigFields: conflictedHostConfigFields }
413
+ );
414
+ }
415
+ _warnOnReconnectedHostConfigMismatch(containerId, hostConfig) {
416
+ if (!hostConfig) return;
417
+ const mismatchedHostConfigFields = this._requestedHardeningHostConfigEntries(hostConfig).filter(([field, requestedValue]) => !isHostConfigValueEqual(field, hostConfig[field], requestedValue)).map(([field]) => field);
418
+ if (mismatchedHostConfigFields.length === 0) return;
419
+ if (!this._privilegedWasSet && hostConfig.Privileged === true && mismatchedHostConfigFields.includes("Privileged")) {
420
+ this.logger.warn(
421
+ `${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.`,
422
+ { containerId, fields: ["privileged"], hostConfigFields: ["Privileged"] }
423
+ );
424
+ }
425
+ const remainingMismatchedHostConfigFields = mismatchedHostConfigFields.filter(
426
+ (field) => field !== "Privileged" || this._privilegedWasSet
427
+ );
428
+ if (remainingMismatchedHostConfigFields.length === 0) return;
429
+ const mismatchedOptions = remainingMismatchedHostConfigFields.map(toDockerSandboxOptionName);
430
+ this.logger.warn(
431
+ `${LOG_PREFIX} Reconnected to existing container ${containerId}; requested Docker option(s) ${mismatchedOptions.join(
432
+ ", "
433
+ )} differ from inspected HostConfig field(s) ${remainingMismatchedHostConfigFields.join(
434
+ ", "
435
+ )} and cannot be applied to the existing container. Destroy and recreate the sandbox to apply them.`,
436
+ { containerId, fields: mismatchedOptions, hostConfigFields: remainingMismatchedHostConfigFields }
437
+ );
438
+ }
439
+ _requestedHardeningHostConfigEntries(hostConfig) {
440
+ const entries = [
441
+ ["Memory", this._memory],
442
+ ["MemorySwap", this._memorySwap],
443
+ ["CpuShares", this._cpuShares],
444
+ ["CpuQuota", this._cpuQuota],
445
+ ["CpuPeriod", this._cpuPeriod],
446
+ ["PidsLimit", this._pidsLimit],
447
+ ["ReadonlyRootfs", this._readonlyRootfs],
448
+ ["CapDrop", this._capDrop],
449
+ ["CapAdd", this._capAdd],
450
+ ["SecurityOpt", this._securityOpt],
451
+ ["Ulimits", this._ulimits],
452
+ ["Tmpfs", this._tmpfs]
453
+ ];
454
+ if (this._privilegedWasSet || hostConfig?.Privileged === true) {
455
+ entries.unshift(["Privileged", this._privileged]);
456
+ }
457
+ return entries.filter((entry) => isPresentHostConfigValue(entry[1]));
458
+ }
359
459
  async stop() {
360
460
  const container = await this._resolveContainer();
361
461
  if (!container) return;
@@ -517,6 +617,92 @@ function isImageNotFoundError(error) {
517
617
  }
518
618
  return false;
519
619
  }
620
+ function isHostConfigValueEqual(field, actual, expected) {
621
+ return util.isDeepStrictEqual(normalizeHostConfigValue(field, actual), normalizeHostConfigValue(field, expected));
622
+ }
623
+ function normalizeHostConfigValue(field, value) {
624
+ if (value == null) return void 0;
625
+ if ((field === "CapAdd" || field === "CapDrop") && Array.isArray(value)) {
626
+ if (value.length === 0) return void 0;
627
+ return value.map(normalizeCapability).sort();
628
+ }
629
+ if (field === "SecurityOpt" && Array.isArray(value)) {
630
+ if (value.length === 0) return void 0;
631
+ return value.map(normalizeSecurityOpt).sort();
632
+ }
633
+ if (field === "Tmpfs" && value && typeof value === "object" && !Array.isArray(value)) {
634
+ if (Object.keys(value).length === 0) return void 0;
635
+ return Object.fromEntries(
636
+ Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([path, options]) => [path, typeof options === "string" ? normalizeTmpfsOptions(options) : options])
637
+ );
638
+ }
639
+ if (Array.isArray(value)) {
640
+ if (field === "Ulimits" && value.length === 0) return void 0;
641
+ return value.map((nestedValue) => normalizeHostConfigValue(field, nestedValue)).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
642
+ }
643
+ if (field === "Ulimits" && value && typeof value === "object") {
644
+ return normalizeUlimit(value);
645
+ }
646
+ if (value && typeof value === "object") {
647
+ return Object.fromEntries(
648
+ Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, nestedValue]) => [key, normalizeHostConfigValue(field, nestedValue)])
649
+ );
650
+ }
651
+ return value;
652
+ }
653
+ function isPresentHostConfigValue(value) {
654
+ if (value == null) return false;
655
+ if (Array.isArray(value)) return value.length > 0;
656
+ if (value && typeof value === "object") return Object.keys(value).length > 0;
657
+ return true;
658
+ }
659
+ function normalizeCapability(capability) {
660
+ return typeof capability === "string" ? capability.toUpperCase().replace(/^CAP_/, "") : capability;
661
+ }
662
+ function normalizeTmpfsOptions(options) {
663
+ return options.split(",").map((option) => option.trim()).filter(Boolean).sort().join(",");
664
+ }
665
+ function normalizeSecurityOpt(option) {
666
+ if (typeof option !== "string") return option;
667
+ const noNewPrivileges = option.match(/^no-new-privileges[:=](.+)$/i);
668
+ if (noNewPrivileges) {
669
+ return `no-new-privileges=${noNewPrivileges[1]}`;
670
+ }
671
+ return option;
672
+ }
673
+ function normalizeUlimit(value) {
674
+ const record = value;
675
+ return {
676
+ name: record.name ?? record.Name,
677
+ soft: record.soft ?? record.Soft,
678
+ hard: record.hard ?? record.Hard
679
+ };
680
+ }
681
+ function toDockerUlimit(ulimit) {
682
+ return {
683
+ Name: ulimit.name,
684
+ Soft: ulimit.soft,
685
+ Hard: ulimit.hard
686
+ };
687
+ }
688
+ function toDockerSandboxOptionName(field) {
689
+ const optionNames = {
690
+ Privileged: "privileged",
691
+ Memory: "memory",
692
+ MemorySwap: "memorySwap",
693
+ CpuShares: "cpuShares",
694
+ CpuQuota: "cpuQuota",
695
+ CpuPeriod: "cpuPeriod",
696
+ PidsLimit: "pidsLimit",
697
+ ReadonlyRootfs: "readonlyRootfs",
698
+ CapDrop: "capDrop",
699
+ CapAdd: "capAdd",
700
+ SecurityOpt: "securityOpt",
701
+ Ulimits: "ulimits",
702
+ Tmpfs: "tmpfs"
703
+ };
704
+ return optionNames[field] ?? String(field);
705
+ }
520
706
 
521
707
  // src/provider.ts
522
708
  var dockerSandboxProvider = {
@@ -559,6 +745,68 @@ var dockerSandboxProvider = {
559
745
  type: "boolean",
560
746
  description: "Run in privileged mode",
561
747
  default: false
748
+ },
749
+ memory: {
750
+ type: "number",
751
+ description: "Memory limit in bytes"
752
+ },
753
+ memorySwap: {
754
+ type: "number",
755
+ description: "Total memory plus swap in bytes"
756
+ },
757
+ cpuShares: {
758
+ type: "number",
759
+ description: "CPU shares relative weight"
760
+ },
761
+ cpuQuota: {
762
+ type: "number",
763
+ description: "CPU quota in microseconds per period"
764
+ },
765
+ cpuPeriod: {
766
+ type: "number",
767
+ description: "CPU period in microseconds"
768
+ },
769
+ pidsLimit: {
770
+ type: "number",
771
+ description: "Maximum number of PIDs in the container"
772
+ },
773
+ readonlyRootfs: {
774
+ type: "boolean",
775
+ description: "Mount the container root filesystem as read-only"
776
+ },
777
+ capDrop: {
778
+ type: "array",
779
+ description: "Linux capabilities to drop",
780
+ items: { type: "string" }
781
+ },
782
+ capAdd: {
783
+ type: "array",
784
+ description: "Linux capabilities to add",
785
+ items: { type: "string" }
786
+ },
787
+ securityOpt: {
788
+ type: "array",
789
+ description: "Docker security options",
790
+ items: { type: "string" }
791
+ },
792
+ ulimits: {
793
+ type: "array",
794
+ description: "Ulimit entries for Docker HostConfig.Ulimits",
795
+ items: {
796
+ type: "object",
797
+ required: ["name", "soft", "hard"],
798
+ additionalProperties: false,
799
+ properties: {
800
+ name: { type: "string" },
801
+ soft: { type: "number" },
802
+ hard: { type: "number" }
803
+ }
804
+ }
805
+ },
806
+ tmpfs: {
807
+ type: "object",
808
+ description: "tmpfs mount paths with options",
809
+ additionalProperties: { type: "string" }
562
810
  }
563
811
  }
564
812
  },