@mastra/docker 0.9.0-alpha.0 → 0.9.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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { DockerSandbox, type DockerSandboxOptions, type DockerTemplateSpec } from './sandbox/index.js';
1
+ export { DockerSandbox, type DockerSandboxOptions, type DockerSandboxStartOptions, type DockerTemplateSpec, } from './sandbox/index.js';
2
2
  export { DockerProcessManager } from './sandbox/process-manager.js';
3
3
  export { dockerSandboxProvider } from './provider.js';
4
4
  export * from './template/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC9F,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,cAAc,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -7,6 +7,38 @@ import { createHash, randomUUID } from "crypto";
7
7
  import posixPath from "path/posix";
8
8
  import { Server, ServerCredentials } from "@grpc/grpc-js";
9
9
  import { execFile } from "child_process";
10
+ //#region src/abort.ts
11
+ function createAbortError(signal, operation) {
12
+ return new SandboxAbortError(operation, signal.reason);
13
+ }
14
+ function throwIfAborted(signal, operation) {
15
+ if (signal?.aborted) throw createAbortError(signal, operation);
16
+ }
17
+ function isAbortError(error) {
18
+ return error instanceof SandboxAbortError || error instanceof Error && (error.name === "AbortError" || "code" in error && error.code === "ABORT_ERR");
19
+ }
20
+ function normalizeAbortError(error, operation) {
21
+ return error instanceof SandboxAbortError ? error : isAbortError(error) ? new SandboxAbortError(operation, error) : error;
22
+ }
23
+ function waitForAbortable(promise, signal, operation, onAbort) {
24
+ throwIfAborted(signal, operation);
25
+ if (!signal) return promise;
26
+ return new Promise((resolve, reject) => {
27
+ const abort = () => {
28
+ onAbort?.();
29
+ reject(new SandboxAbortError(operation, signal.reason));
30
+ };
31
+ signal.addEventListener("abort", abort, { once: true });
32
+ promise.then((value) => {
33
+ signal.removeEventListener("abort", abort);
34
+ resolve(value);
35
+ }, (error) => {
36
+ signal.removeEventListener("abort", abort);
37
+ reject(normalizeAbortError(error, operation));
38
+ });
39
+ });
40
+ }
41
+ //#endregion
10
42
  //#region src/sandbox/process-manager.ts
11
43
  /**
12
44
  * Docker Process Manager
@@ -179,16 +211,20 @@ var DockerProcessHandle = class extends ProcessHandle {
179
211
  AttachStdout: false,
180
212
  AttachStderr: false
181
213
  });
182
- await killExec.start({});
183
- let killInfo = await killExec.inspect();
184
- while (killInfo.Running) {
185
- await new Promise((resolve) => setTimeout(resolve, 10));
186
- killInfo = await killExec.inspect();
214
+ const killStream = await killExec.start({});
215
+ try {
216
+ let killInfo = await killExec.inspect();
217
+ while (killInfo.Running) {
218
+ await new Promise((resolve) => setTimeout(resolve, 10));
219
+ killInfo = await killExec.inspect();
220
+ }
221
+ if (killInfo.ExitCode !== 0) throw new Error(`kill helper exited with code ${killInfo.ExitCode}`);
222
+ this._killed = true;
223
+ this._destroyStream();
224
+ return true;
225
+ } finally {
226
+ killStream.destroy();
187
227
  }
188
- if (killInfo.ExitCode !== 0) throw new Error(`kill helper exited with code ${killInfo.ExitCode}`);
189
- this._killed = true;
190
- this._destroyStream();
191
- return true;
192
228
  } catch (error) {
193
229
  const msg = error instanceof Error ? error.message.toLowerCase() : "";
194
230
  if (!msg.includes("no such process") && !msg.includes("esrch")) console.warn(`[DockerProcessManager] kill(${this.pid}) failed unexpectedly:`, error);
@@ -245,6 +281,7 @@ var DockerProcessManager = class extends SandboxProcessManager {
245
281
  const container = this.container;
246
282
  const pgidFile = `${PROC_DIR}/${randomUUID()}`;
247
283
  const envArray = Object.entries({ ...options.env }).filter((entry) => entry[1] !== void 0).map(([k, v]) => `${k}=${v}`);
284
+ const attachStdin = options.stdinMode !== "ignore";
248
285
  const exec = await container.exec({
249
286
  Cmd: [
250
287
  "sh",
@@ -256,17 +293,17 @@ var DockerProcessManager = class extends SandboxProcessManager {
256
293
  ],
257
294
  AttachStdout: true,
258
295
  AttachStderr: true,
259
- AttachStdin: true,
296
+ AttachStdin: attachStdin,
260
297
  Tty: false,
261
298
  Env: envArray.length > 0 ? envArray : void 0,
262
299
  WorkingDir: options.cwd
263
300
  });
264
301
  const stream = await exec.start({
265
302
  hijack: true,
266
- stdin: true
303
+ stdin: attachStdin
267
304
  });
268
305
  const startTime = Date.now();
269
- const handle = new DockerProcessHandle(exec, container, startTime, stream, pgidFile, options);
306
+ const handle = new DockerProcessHandle(exec, container, startTime, attachStdin ? stream : null, pgidFile, options);
270
307
  handle._setExecStream(stream);
271
308
  const waitPromise = new Promise((resolve) => {
272
309
  const buffer = [];
@@ -539,7 +576,9 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
539
576
  if (!this._container) throw new SandboxNotReadyError(this.id);
540
577
  return this._container;
541
578
  }
542
- async start() {
579
+ async start(options = {}) {
580
+ const { abortSignal } = options;
581
+ throwIfAborted(abortSignal, "start Docker sandbox");
543
582
  this.logger.debug(`${LOG_PREFIX} Starting sandbox ${this.id}...`);
544
583
  const existing = await this._findExistingContainer();
545
584
  if (existing) {
@@ -560,7 +599,7 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
560
599
  return;
561
600
  }
562
601
  this._warnOnPrivilegedHardeningConflict(this._privileged);
563
- await this._resolveTemplate();
602
+ await this._resolveTemplate(abortSignal);
564
603
  await this._ensureImage();
565
604
  const envArray = Object.entries(this._env).map(([k, v]) => `${k}=${v}`);
566
605
  const binds = Object.entries(this._volumes).map(([host, container]) => `${host}:${container}`);
@@ -820,10 +859,20 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
820
859
  * creates a container, so a resolver-form template re-resolves each time.
821
860
  * Adopts the template's workdir unless the sandbox was given one explicitly.
822
861
  */
823
- async _resolveTemplate() {
862
+ async _resolveTemplate(abortSignal) {
824
863
  if (!this._templateSpec) return;
825
- const template = typeof this._templateSpec === "function" ? await this._templateSpec() : this._templateSpec;
826
- const result = await template.build({ docker: this._docker });
864
+ throwIfAborted(abortSignal, "start Docker sandbox");
865
+ let template;
866
+ try {
867
+ template = typeof this._templateSpec === "function" ? await this._templateSpec({ abortSignal }) : this._templateSpec;
868
+ } catch (error) {
869
+ throw normalizeAbortError(error, "start Docker sandbox");
870
+ }
871
+ throwIfAborted(abortSignal, "start Docker sandbox");
872
+ const result = await template.build({
873
+ docker: this._docker,
874
+ abortSignal
875
+ });
827
876
  if (result.status !== "ready") throw new SandboxError(`Docker template build failed: ${result.error ?? "unknown error"}`, "START_FAILED", {
828
877
  templateId: result.templateId,
829
878
  reason: "template_build_failed"
@@ -1196,9 +1245,17 @@ const AUTH_CREDENTIALS_METHOD = "/moby.filesync.v1.Auth/Credentials";
1196
1245
  * Open a session that answers `GetSecret` from `secrets`. The returned id must
1197
1246
  * be sent as the `session` query parameter of the build request.
1198
1247
  */
1199
- function openBuildSession(docker, secrets) {
1248
+ function openBuildSession(docker, secrets, abortSignal) {
1249
+ throwIfAborted(abortSignal, "build Docker template");
1200
1250
  const id = randomUUID();
1201
1251
  return new Promise((resolve, reject) => {
1252
+ let settled = false;
1253
+ const abort = () => {
1254
+ if (settled || !abortSignal) return;
1255
+ settled = true;
1256
+ reject(createAbortError(abortSignal, "build Docker template"));
1257
+ };
1258
+ abortSignal?.addEventListener("abort", abort, { once: true });
1202
1259
  docker.modem.dial({
1203
1260
  method: "POST",
1204
1261
  path: "/session",
@@ -1214,8 +1271,14 @@ function openBuildSession(docker, secrets) {
1214
1271
  500: "server error"
1215
1272
  }
1216
1273
  }, (err, socket) => {
1274
+ if (settled) {
1275
+ socket?.end?.();
1276
+ return;
1277
+ }
1278
+ settled = true;
1279
+ abortSignal?.removeEventListener("abort", abort);
1217
1280
  if (err) {
1218
- reject(err);
1281
+ reject(abortSignal?.aborted ? createAbortError(abortSignal, "build Docker template") : err);
1219
1282
  return;
1220
1283
  }
1221
1284
  const server = new Server();
@@ -1536,7 +1599,8 @@ var DockerTemplate = class DockerTemplate {
1536
1599
  #secrets;
1537
1600
  #docker;
1538
1601
  #built = false;
1539
- #inFlight;
1602
+ #queueTail;
1603
+ #plainBuild;
1540
1604
  constructor(options = {}, state) {
1541
1605
  this.#baseImage = state ? state.baseImage : validateString(options.baseImage ?? "node:22-slim", "baseImage");
1542
1606
  this.#operations = state?.operations ?? [];
@@ -1683,19 +1747,49 @@ var DockerTemplate = class DockerTemplate {
1683
1747
  * `options.secrets`, the template's `secrets` option, or `process.env`.
1684
1748
  */
1685
1749
  async build(options = {}) {
1686
- if (!options.force && options.secrets === void 0 && options.docker === void 0 && this.#inFlight) return this.#inFlight;
1687
- const run = (this.#inFlight?.catch(() => void 0) ?? Promise.resolve()).then(() => this.#build(options));
1688
- this.#inFlight = run;
1689
- run.finally(() => {
1690
- if (this.#inFlight === run) this.#inFlight = void 0;
1750
+ throwIfAborted(options.abortSignal, "build Docker template");
1751
+ const isPlain = !options.force && options.secrets === void 0 && options.docker === void 0;
1752
+ if (isPlain && this.#plainBuild) return this.#waitForBuild(this.#plainBuild, options.abortSignal);
1753
+ const previous = this.#queueTail?.promise.catch(() => void 0) ?? Promise.resolve();
1754
+ const controller = new AbortController();
1755
+ const run = {
1756
+ controller,
1757
+ waiters: 0,
1758
+ promise: previous.then(() => {
1759
+ throwIfAborted(controller.signal, "build Docker template");
1760
+ return this.#build({
1761
+ ...options,
1762
+ abortSignal: controller.signal
1763
+ });
1764
+ })
1765
+ };
1766
+ this.#queueTail = run;
1767
+ if (isPlain) this.#plainBuild = run;
1768
+ run.promise.finally(() => {
1769
+ if (this.#queueTail === run) this.#queueTail = void 0;
1770
+ if (this.#plainBuild === run) this.#plainBuild = void 0;
1691
1771
  }).catch(() => void 0);
1692
- return run;
1772
+ return this.#waitForBuild(run, options.abortSignal);
1773
+ }
1774
+ #waitForBuild(run, abortSignal) {
1775
+ run.waiters += 1;
1776
+ let settled = false;
1777
+ const release = (aborted) => {
1778
+ if (settled) return;
1779
+ settled = true;
1780
+ run.waiters -= 1;
1781
+ if (aborted && run.waiters === 0) run.controller.abort();
1782
+ };
1783
+ return waitForAbortable(run.promise, abortSignal, "build Docker template", () => release(true)).finally(() => release(false));
1693
1784
  }
1694
1785
  async #build(options) {
1695
1786
  const docker = options.docker ?? this.#getDocker();
1696
1787
  const tag = this.templateId;
1788
+ const { abortSignal } = options;
1789
+ throwIfAborted(abortSignal, "build Docker template");
1697
1790
  if (!options.force) try {
1698
1791
  await docker.getImage(tag).inspect();
1792
+ throwIfAborted(abortSignal, "build Docker template");
1699
1793
  this.#built = true;
1700
1794
  return {
1701
1795
  status: "ready",
@@ -1705,24 +1799,37 @@ var DockerTemplate = class DockerTemplate {
1705
1799
  if (!isImageNotFoundError(error)) throw error;
1706
1800
  }
1707
1801
  const secrets = await this.#resolveSecrets(options.secrets);
1802
+ throwIfAborted(abortSignal, "build Docker template");
1708
1803
  const context = pack();
1709
1804
  context.entry({ name: "Dockerfile" }, this.dockerfile);
1710
1805
  context.finalize();
1711
1806
  try {
1712
1807
  const nocache = options.force === true;
1713
1808
  if (secrets) {
1714
- const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache);
1809
+ const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache, abortSignal);
1715
1810
  try {
1716
- await this.#followBuild(docker, stream);
1811
+ await this.#followBuild(docker, stream, abortSignal);
1717
1812
  } finally {
1718
1813
  session.close();
1719
1814
  }
1720
- } else await this.#followBuild(docker, await docker.buildImage(context, {
1721
- t: tag,
1722
- nocache
1723
- }));
1815
+ } else {
1816
+ const stream = await waitForAbortable(docker.buildImage(context, {
1817
+ t: tag,
1818
+ nocache
1819
+ }).then((stream) => {
1820
+ if (abortSignal?.aborted) {
1821
+ stream.destroy?.();
1822
+ throw createAbortError(abortSignal, "build Docker template");
1823
+ }
1824
+ return stream;
1825
+ }), abortSignal, "build Docker template", () => {
1826
+ context.destroy?.();
1827
+ });
1828
+ await this.#followBuild(docker, stream, abortSignal);
1829
+ }
1724
1830
  } catch (error) {
1725
1831
  this.#built = false;
1832
+ if (abortSignal?.aborted) throw normalizeAbortError(error, "build Docker template");
1726
1833
  return {
1727
1834
  status: "failed",
1728
1835
  templateId: tag,
@@ -1753,12 +1860,35 @@ var DockerTemplate = class DockerTemplate {
1753
1860
  * directly because `docker.buildImage` replaces any session id with its own
1754
1861
  * auth-only session when `version` is `'2'`.
1755
1862
  */
1756
- async #buildWithSecrets(docker, context, tag, secrets, nocache) {
1757
- const session = await openBuildSession(docker, secrets);
1863
+ async #buildWithSecrets(docker, context, tag, secrets, nocache, abortSignal) {
1864
+ throwIfAborted(abortSignal, "build Docker template");
1865
+ const session = await openBuildSession(docker, secrets, abortSignal);
1758
1866
  let stream;
1759
1867
  try {
1760
1868
  stream = await new Promise((resolve, reject) => {
1761
- docker.modem.dial({
1869
+ let settled = false;
1870
+ let request;
1871
+ const finish = (error, data) => {
1872
+ if (settled) {
1873
+ if (data) data.destroy?.();
1874
+ return;
1875
+ }
1876
+ settled = true;
1877
+ abortSignal?.removeEventListener("abort", abort);
1878
+ error === void 0 ? resolve(data) : reject(error);
1879
+ };
1880
+ const abort = () => {
1881
+ context.destroy?.();
1882
+ request?.destroy?.();
1883
+ session.close();
1884
+ if (abortSignal) finish(createAbortError(abortSignal, "build Docker template"));
1885
+ };
1886
+ abortSignal?.addEventListener("abort", abort, { once: true });
1887
+ if (abortSignal?.aborted) {
1888
+ abort();
1889
+ return;
1890
+ }
1891
+ request = docker.modem.dial({
1762
1892
  path: "/build?",
1763
1893
  method: "POST",
1764
1894
  file: context,
@@ -1773,7 +1903,7 @@ var DockerTemplate = class DockerTemplate {
1773
1903
  200: true,
1774
1904
  500: "server error"
1775
1905
  }
1776
- }, (err, data) => err ? reject(err) : resolve(data));
1906
+ }, (err, data) => finish(err ?? void 0, data));
1777
1907
  });
1778
1908
  } catch (error) {
1779
1909
  session.close();
@@ -1784,20 +1914,36 @@ var DockerTemplate = class DockerTemplate {
1784
1914
  session
1785
1915
  };
1786
1916
  }
1787
- #followBuild(docker, stream) {
1917
+ #followBuild(docker, stream, abortSignal) {
1788
1918
  return new Promise((resolve, reject) => {
1919
+ let settled = false;
1920
+ const finish = (error) => {
1921
+ if (settled) return;
1922
+ settled = true;
1923
+ abortSignal?.removeEventListener("abort", abort);
1924
+ error === void 0 ? resolve() : reject(error);
1925
+ };
1926
+ const abort = () => {
1927
+ stream.destroy?.();
1928
+ if (abortSignal) finish(createAbortError(abortSignal, "build Docker template"));
1929
+ };
1930
+ abortSignal?.addEventListener("abort", abort, { once: true });
1931
+ if (abortSignal?.aborted) {
1932
+ abort();
1933
+ return;
1934
+ }
1789
1935
  docker.modem.followProgress(stream, (err, output) => {
1790
1936
  if (err) {
1791
- reject(err);
1937
+ finish(err);
1792
1938
  return;
1793
1939
  }
1794
1940
  const failure = output?.find((entry) => entry && (entry.error !== void 0 || entry.errorDetail !== void 0));
1795
1941
  if (failure) {
1796
1942
  const detail = failure.errorDetail;
1797
- reject(new Error(String(detail?.message ?? failure.error ?? "docker build failed")));
1943
+ finish(new Error(String(detail?.message ?? failure.error ?? "docker build failed")));
1798
1944
  return;
1799
1945
  }
1800
- resolve();
1946
+ finish();
1801
1947
  });
1802
1948
  });
1803
1949
  }
@@ -1946,16 +2092,31 @@ function createDockerRepoTemplate(options) {
1946
2092
  if (options.ref !== void 0 && !REF_PATTERN.test(options.ref)) throw new Error(`Invalid ref '${options.ref}': expected a git ref name`);
1947
2093
  const workingDirectory = trimTrailingSlashes(options.workingDirectory ?? DEFAULT_WORKING_DIRECTORY);
1948
2094
  if (!workingDirectory.startsWith("/")) throw new Error(`workingDirectory must be an absolute path, got '${options.workingDirectory}'`);
1949
- return () => resolveRepoTemplate(options, workingDirectory);
2095
+ return (resolveOptions) => resolveRepoTemplate(options, workingDirectory, resolveOptions);
1950
2096
  }
1951
- async function resolveRepoTemplate(options, workingDirectory) {
1952
- const access = await options.getRepositoryAccess();
2097
+ async function resolveRepoTemplate(options, workingDirectory, resolveOptions = {}) {
2098
+ const { abortSignal } = resolveOptions;
2099
+ throwIfAborted(abortSignal, "resolve Docker repository template");
2100
+ let access;
2101
+ try {
2102
+ access = await options.getRepositoryAccess(resolveOptions);
2103
+ } catch (error) {
2104
+ throw normalizeAbortError(error, "resolve Docker repository template");
2105
+ }
2106
+ throwIfAborted(abortSignal, "resolve Docker repository template");
1953
2107
  const cloneUrl = access?.cloneUrl;
1954
2108
  if (!cloneUrl) throw new Error("Repo template has no clone URL: repository access returned none.");
1955
2109
  assertCloneUrl(cloneUrl);
1956
2110
  const token = access?.authorization?.token;
1957
- const buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv() : options.buildEnv;
1958
- const sha = await resolveHead(cloneUrl, options.ref, token);
2111
+ let buildEnv;
2112
+ try {
2113
+ buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv(resolveOptions) : options.buildEnv;
2114
+ } catch (error) {
2115
+ throw normalizeAbortError(error, "resolve Docker repository template");
2116
+ }
2117
+ throwIfAborted(abortSignal, "resolve Docker repository template");
2118
+ const sha = await resolveHead(cloneUrl, options.ref, token, abortSignal);
2119
+ throwIfAborted(abortSignal, "resolve Docker repository template");
1959
2120
  if (!sha) throw new Error(`Could not resolve ${options.ref ?? "HEAD"} of ${cloneUrl} with git ls-remote; check the ref, the credential and network access`);
1960
2121
  return buildRepoTemplate({
1961
2122
  cloneUrl,
@@ -2004,7 +2165,8 @@ function buildRepoTemplate(inputs) {
2004
2165
  * yields undefined; the caller decides how to surface it.
2005
2166
  * @internal exported for tests.
2006
2167
  */
2007
- async function resolveHead(cloneUrl, ref, token) {
2168
+ async function resolveHead(cloneUrl, ref, token, abortSignal) {
2169
+ throwIfAborted(abortSignal, "resolve Docker repository template");
2008
2170
  if (ref && FULL_SHA_PATTERN.test(ref)) return ref.toLowerCase();
2009
2171
  try {
2010
2172
  const authEnv = token ? {
@@ -2019,6 +2181,7 @@ async function resolveHead(cloneUrl, ref, token) {
2019
2181
  ref ?? "HEAD"
2020
2182
  ], {
2021
2183
  timeout: 1e4,
2184
+ signal: abortSignal,
2022
2185
  env: {
2023
2186
  ...process.env,
2024
2187
  ...authEnv,
@@ -2028,7 +2191,8 @@ async function resolveHead(cloneUrl, ref, token) {
2028
2191
  const lines = stdout.split("\n").filter(Boolean).map((line) => line.split(" "));
2029
2192
  const sha = (lines.find(([, name]) => name?.endsWith("^{}")) ?? lines[0])?.[0]?.trim();
2030
2193
  return sha && SHA_PATTERN.test(sha) ? sha.toLowerCase() : void 0;
2031
- } catch {
2194
+ } catch (error) {
2195
+ if (abortSignal?.aborted) throw normalizeAbortError(error, "resolve Docker repository template");
2032
2196
  return;
2033
2197
  }
2034
2198
  }