@mastra/docker 0.8.0 → 0.9.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -28,6 +28,10 @@ let dockerode = require("dockerode");
28
28
  dockerode = __toESM(dockerode, 1);
29
29
  let tar_stream = require("tar-stream");
30
30
  let crypto = require("crypto");
31
+ let path_posix = require("path/posix");
32
+ path_posix = __toESM(path_posix, 1);
33
+ let _grpc_grpc_js = require("@grpc/grpc-js");
34
+ let child_process = require("child_process");
31
35
  //#region src/sandbox/process-manager.ts
32
36
  /**
33
37
  * Docker Process Manager
@@ -451,7 +455,10 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
451
455
  _container = null;
452
456
  /** Configuration */
453
457
  _containerName;
458
+ /** Image the container boots from; rewritten by a template resolution on `start()`. */
454
459
  _image;
460
+ _templateSpec;
461
+ _workingDirectoryWasSet;
455
462
  _command;
456
463
  _env;
457
464
  _volumes;
@@ -492,7 +499,10 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
492
499
  });
493
500
  this.id = options.id ?? this._generateId();
494
501
  this._containerName = sanitizeContainerName(options.name ?? this.id);
502
+ if (options.image !== void 0 && options.template !== void 0) throw new TypeError("DockerSandbox: `image` and `template` are mutually exclusive");
503
+ this._templateSpec = options.template;
495
504
  this._image = options.image ?? "node:22-slim";
505
+ this._workingDirectoryWasSet = options.workingDirectory !== void 0 || options.workingDir !== void 0;
496
506
  this._command = options.command ?? ["sleep", "infinity"];
497
507
  this._env = options.env ?? {};
498
508
  this._volumes = options.volumes ?? {};
@@ -568,11 +578,14 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
568
578
  this.logger.debug(`${LOG_PREFIX} Container exists but not running (${actualState}), starting...`);
569
579
  await this._container.start();
570
580
  }
581
+ const reconnectedWorkingDir = info.Config?.WorkingDir;
582
+ if (!this._workingDirectoryWasSet && reconnectedWorkingDir) this.setWorkingDirectory(reconnectedWorkingDir);
571
583
  this.processes.setContainer(this._container);
572
584
  this.logger.debug(`${LOG_PREFIX} Reconnected to container ${existing.Id}`);
573
585
  return;
574
586
  }
575
587
  this._warnOnPrivilegedHardeningConflict(this._privileged);
588
+ await this._resolveTemplate();
576
589
  await this._ensureImage();
577
590
  const envArray = Object.entries(this._env).map(([k, v]) => `${k}=${v}`);
578
591
  const binds = Object.entries(this._volumes).map(([host, container]) => `${host}:${container}`);
@@ -827,6 +840,23 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
827
840
  }
828
841
  }
829
842
  /**
843
+ * Resolve the `template` option (if any) into the image to boot from,
844
+ * building it when no cached image exists. Runs on every `start()` that
845
+ * creates a container, so a resolver-form template re-resolves each time.
846
+ * Adopts the template's workdir unless the sandbox was given one explicitly.
847
+ */
848
+ async _resolveTemplate() {
849
+ if (!this._templateSpec) return;
850
+ const template = typeof this._templateSpec === "function" ? await this._templateSpec() : this._templateSpec;
851
+ const result = await template.build({ docker: this._docker });
852
+ if (result.status !== "ready") throw new _mastra_core_workspace.SandboxError(`Docker template build failed: ${result.error ?? "unknown error"}`, "START_FAILED", {
853
+ templateId: result.templateId,
854
+ reason: "template_build_failed"
855
+ });
856
+ this._image = result.templateId;
857
+ if (!this._workingDirectoryWasSet && template.workdir !== void 0) this.setWorkingDirectory(template.workdir);
858
+ }
859
+ /**
830
860
  * Ensure the Docker image is available locally. Pulls if needed.
831
861
  */
832
862
  async _ensureImage() {
@@ -834,7 +864,7 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
834
864
  await this._docker.getImage(this._image).inspect();
835
865
  this.logger.debug(`${LOG_PREFIX} Image ${this._image} available locally`);
836
866
  } catch (error) {
837
- if (!isImageNotFoundError(error)) throw error;
867
+ if (!isImageNotFoundError$1(error)) throw error;
838
868
  this.logger.debug(`${LOG_PREFIX} Pulling image ${this._image}...`);
839
869
  try {
840
870
  const stream = await this._docker.pull(this._image);
@@ -870,7 +900,7 @@ function isContainerNotFoundError(error) {
870
900
  }
871
901
  return false;
872
902
  }
873
- function isImageNotFoundError(error) {
903
+ function isImageNotFoundError$1(error) {
874
904
  if (error instanceof Error) return error.message.toLowerCase().includes("no such image");
875
905
  return false;
876
906
  }
@@ -1170,8 +1200,898 @@ const dockerSandboxProvider = {
1170
1200
  createSandbox: (config) => new DockerSandbox(config)
1171
1201
  };
1172
1202
  //#endregion
1203
+ //#region src/template/build-session.ts
1204
+ /**
1205
+ * BuildKit session that serves build secrets to the daemon.
1206
+ *
1207
+ * `docker build` with BuildKit (`version=2`) can attach a long-lived gRPC
1208
+ * "session" that the daemon calls back into for things it needs mid-build.
1209
+ * `RUN --mount=type=secret,id=X` is resolved by calling
1210
+ * `/moby.buildkit.secrets.v1.Secrets/GetSecret` on that session, so the value
1211
+ * exists only in the tmpfs mount for the duration of that one RUN — never as a
1212
+ * build arg, layer, history entry, or cache metadata.
1213
+ *
1214
+ * dockerode's own session helper only registers the registry-auth service and
1215
+ * overwrites any `session` id passed to `buildImage`, so this module dials the
1216
+ * hijacked `/session` endpoint itself and registers both services.
1217
+ */
1218
+ const SECRETS_GET_METHOD = "/moby.buildkit.secrets.v1.Secrets/GetSecret";
1219
+ const AUTH_CREDENTIALS_METHOD = "/moby.filesync.v1.Auth/Credentials";
1220
+ /**
1221
+ * Open a session that answers `GetSecret` from `secrets`. The returned id must
1222
+ * be sent as the `session` query parameter of the build request.
1223
+ */
1224
+ function openBuildSession(docker, secrets) {
1225
+ const id = (0, crypto.randomUUID)();
1226
+ return new Promise((resolve, reject) => {
1227
+ docker.modem.dial({
1228
+ method: "POST",
1229
+ path: "/session",
1230
+ hijack: true,
1231
+ headers: {
1232
+ Upgrade: "h2c",
1233
+ "X-Docker-Expose-Session-Uuid": id,
1234
+ "X-Docker-Expose-Session-Name": "mastra-docker-template",
1235
+ "X-Docker-Expose-Session-Grpc-Method": [SECRETS_GET_METHOD, AUTH_CREDENTIALS_METHOD]
1236
+ },
1237
+ statusCodes: {
1238
+ 200: true,
1239
+ 500: "server error"
1240
+ }
1241
+ }, (err, socket) => {
1242
+ if (err) {
1243
+ reject(err);
1244
+ return;
1245
+ }
1246
+ const server = new _grpc_grpc_js.Server();
1247
+ server.createConnectionInjector(_grpc_grpc_js.ServerCredentials.createInsecure()).injectConnection(socket);
1248
+ server.addService(secretsService, { GetSecret(call, callback) {
1249
+ const value = Object.hasOwn(secrets, call.request.id) ? secrets[call.request.id] : void 0;
1250
+ if (value === void 0) {
1251
+ callback(Object.assign(/* @__PURE__ */ new Error(`no build secret registered with id '${call.request.id}'`), { code: 5 }));
1252
+ return;
1253
+ }
1254
+ callback(null, { data: Buffer.from(value, "utf8") });
1255
+ } });
1256
+ server.addService(authService, { Credentials(_call, callback) {
1257
+ callback(null, {});
1258
+ } });
1259
+ resolve({
1260
+ id,
1261
+ close() {
1262
+ server.forceShutdown();
1263
+ socket.end();
1264
+ }
1265
+ });
1266
+ });
1267
+ });
1268
+ }
1269
+ /** @internal exported for tests */
1270
+ function decodeGetSecretRequest(buffer) {
1271
+ let offset = 0;
1272
+ let id = "";
1273
+ const readVarint = () => {
1274
+ let result = 0;
1275
+ let shift = 0;
1276
+ for (;;) {
1277
+ if (offset >= buffer.length) throw new Error("truncated varint");
1278
+ const byte = buffer[offset++];
1279
+ result += (byte & 127) * 2 ** shift;
1280
+ if ((byte & 128) === 0) return result;
1281
+ shift += 7;
1282
+ }
1283
+ };
1284
+ while (offset < buffer.length) {
1285
+ const key = readVarint();
1286
+ const field = Math.floor(key / 8);
1287
+ const wireType = key % 8;
1288
+ switch (wireType) {
1289
+ case 0:
1290
+ readVarint();
1291
+ break;
1292
+ case 1:
1293
+ offset += 8;
1294
+ break;
1295
+ case 5:
1296
+ offset += 4;
1297
+ break;
1298
+ case 2: {
1299
+ const length = readVarint();
1300
+ const value = buffer.subarray(offset, offset + length);
1301
+ offset += length;
1302
+ if (field === 1) id = value.toString("utf8");
1303
+ break;
1304
+ }
1305
+ default: throw new Error(`unsupported protobuf wire type ${wireType}`);
1306
+ }
1307
+ }
1308
+ return { id };
1309
+ }
1310
+ /** @internal exported for tests */
1311
+ function encodeGetSecretResponse(response) {
1312
+ return Buffer.concat([
1313
+ Buffer.from([10]),
1314
+ encodeVarint(response.data.length),
1315
+ response.data
1316
+ ]);
1317
+ }
1318
+ function encodeVarint(value) {
1319
+ const bytes = [];
1320
+ let remaining = value;
1321
+ while (remaining >= 128) {
1322
+ bytes.push(remaining % 128 | 128);
1323
+ remaining = Math.floor(remaining / 128);
1324
+ }
1325
+ bytes.push(remaining);
1326
+ return Buffer.from(bytes);
1327
+ }
1328
+ const secretsService = { GetSecret: {
1329
+ path: SECRETS_GET_METHOD,
1330
+ requestStream: false,
1331
+ responseStream: false,
1332
+ requestSerialize: () => {
1333
+ throw new Error("server does not serialize requests");
1334
+ },
1335
+ requestDeserialize: decodeGetSecretRequest,
1336
+ responseSerialize: encodeGetSecretResponse,
1337
+ responseDeserialize: () => {
1338
+ throw new Error("server does not deserialize responses");
1339
+ }
1340
+ } };
1341
+ const authService = { Credentials: {
1342
+ path: AUTH_CREDENTIALS_METHOD,
1343
+ requestStream: false,
1344
+ responseStream: false,
1345
+ requestSerialize: () => {
1346
+ throw new Error("server does not serialize requests");
1347
+ },
1348
+ requestDeserialize: () => ({}),
1349
+ responseSerialize: () => Buffer.alloc(0),
1350
+ responseDeserialize: () => {
1351
+ throw new Error("server does not deserialize responses");
1352
+ }
1353
+ } };
1354
+ //#endregion
1355
+ //#region src/template/dockerfile.ts
1356
+ /**
1357
+ * Pure Dockerfile synthesis and content-addressed identity for DockerTemplate.
1358
+ *
1359
+ * A `DockerTemplate` records an ordered list of operations (setWorkdir, setEnvs,
1360
+ * runCmd, runWithSecrets, aptInstall, pipInstall, npmInstall) over a base image. This module
1361
+ * turns that ordered list into a deterministic Dockerfile string and a stable
1362
+ * content hash. It performs no I/O, so it is fully unit-testable without a
1363
+ * Docker daemon.
1364
+ */
1365
+ function toCommandList(command) {
1366
+ return Array.isArray(command) ? command : [command];
1367
+ }
1368
+ function sortedEntries(record) {
1369
+ return Object.entries(record).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1370
+ }
1371
+ function renderEnvLine(envs) {
1372
+ const pairs = sortedEntries(envs);
1373
+ if (pairs.length === 0) return void 0;
1374
+ return `ENV ${pairs.map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(" ")}`;
1375
+ }
1376
+ function renderAptInstall(packages, options) {
1377
+ const flags = [];
1378
+ if (options?.noInstallRecommends) flags.push("--no-install-recommends");
1379
+ if (options?.fixMissing) flags.push("--fix-missing");
1380
+ return `RUN apt-get update && apt-get install -y ${flags.length > 0 ? `${flags.join(" ")} ` : ""}${toCommandList(packages).join(" ")} && rm -rf /var/lib/apt/lists/*`;
1381
+ }
1382
+ function renderPipInstall(packages, options) {
1383
+ return `RUN pip install${options?.g === false ? " --user" : ""} ${packages === void 0 ? "." : toCommandList(packages).join(" ")}`;
1384
+ }
1385
+ function renderNpmInstall(packages, options) {
1386
+ const flags = [];
1387
+ if (options?.g) flags.push("-g");
1388
+ if (packages === void 0) {
1389
+ if (options?.dev) flags.push("--include=dev");
1390
+ return `RUN npm install${flags.length > 0 ? ` ${flags.join(" ")}` : ""}`;
1391
+ }
1392
+ return `RUN npm install${flags.length > 0 ? ` ${flags.join(" ")}` : ""} ${toCommandList(packages).join(" ")}`;
1393
+ }
1394
+ function secretStageName(index) {
1395
+ return `mastra-secret-${index}`;
1396
+ }
1397
+ function mainStageName(index) {
1398
+ return `mastra-main-${index}`;
1399
+ }
1400
+ /**
1401
+ * Render a deterministic Dockerfile from a template definition.
1402
+ *
1403
+ * Operations accumulate in a chain of "main" stages. Every `runWithSecrets`
1404
+ * operation forks a throwaway stage from the main stage as it stands at that
1405
+ * point (so it sees earlier WORKDIR/ENV/installs) and runs the command with
1406
+ * each secret exposed through a BuildKit secret mount, exported into the
1407
+ * command's environment for that single RUN. The next main stage then starts
1408
+ * from the same pre-secret snapshot and `COPY --from`s only the declared
1409
+ * output path. Secret mounts are tmpfs-backed and never part of a layer, its
1410
+ * history, or the build cache; the throwaway stage additionally keeps anything
1411
+ * else the command wrote (caches, logs) out of the template image.
1412
+ */
1413
+ /**
1414
+ * `RUN --mount=type=secret,id=X,mode=0444 ... export X="$(cat /run/secrets/X)" && <command>`.
1415
+ * Reading the file into a shell variable keeps the value visible only to this
1416
+ * RUN's process tree, without depending on the newer `env=` mount option.
1417
+ * `mode=0444` because BuildKit's default is `0400 root`, which a base image
1418
+ * with a non-root `USER` cannot read; world-readable within the RUN is no
1419
+ * wider than the exported variable already is.
1420
+ */
1421
+ function renderSecretRun(command, secrets) {
1422
+ const names = [...secrets].sort();
1423
+ const mounts = names.map((name) => `--mount=type=secret,id=${name},mode=0444`);
1424
+ const exports = names.map((name) => `${name}="$(cat /run/secrets/${name})"`);
1425
+ const prefix = names.length > 0 ? [`export ${exports.join(" ")}`] : [];
1426
+ return `RUN ${[...mounts, ""].join(" ")}${[...prefix, ...toCommandList(command)].join(" && ")}`;
1427
+ }
1428
+ function synthesizeDockerfile(definition) {
1429
+ const lines = [];
1430
+ let mainIndex = 0;
1431
+ const openMain = (from) => {
1432
+ lines.push(`FROM ${from} AS ${mainStageName(mainIndex)}`);
1433
+ };
1434
+ openMain(definition.baseImage);
1435
+ definition.operations.forEach((operation, index) => {
1436
+ switch (operation.method) {
1437
+ case "setWorkdir":
1438
+ lines.push(`WORKDIR ${operation.args[0]}`);
1439
+ break;
1440
+ case "setEnvs": {
1441
+ const line = renderEnvLine(operation.args[0]);
1442
+ if (line) lines.push(line);
1443
+ break;
1444
+ }
1445
+ case "runCmd":
1446
+ lines.push(`RUN ${toCommandList(operation.args[0]).join(" && ")}`);
1447
+ break;
1448
+ case "runWithSecrets": {
1449
+ const [command, { secrets, output }] = operation.args;
1450
+ const snapshot = mainStageName(mainIndex);
1451
+ const stage = secretStageName(index);
1452
+ lines.push(`FROM ${snapshot} AS ${stage}`, renderSecretRun(command, secrets));
1453
+ mainIndex += 1;
1454
+ openMain(snapshot);
1455
+ lines.push(`COPY --from=${stage} ${output} ${output}`);
1456
+ break;
1457
+ }
1458
+ case "aptInstall":
1459
+ lines.push(renderAptInstall(operation.args[0], operation.args[1]));
1460
+ break;
1461
+ case "pipInstall":
1462
+ lines.push(renderPipInstall(operation.args[0], operation.args[1]));
1463
+ break;
1464
+ case "npmInstall":
1465
+ lines.push(renderNpmInstall(operation.args[0], operation.args[1]));
1466
+ break;
1467
+ }
1468
+ });
1469
+ return `${lines.join("\n")}\n`;
1470
+ }
1471
+ /** Names of every secret referenced by `runWithSecrets` operations, deduplicated and sorted. */
1472
+ function secretNames(definition) {
1473
+ const names = /* @__PURE__ */ new Set();
1474
+ for (const operation of definition.operations) if (operation.method === "runWithSecrets") for (const name of operation.args[1].secrets) names.add(name);
1475
+ return [...names].sort();
1476
+ }
1477
+ /** Prefix for template image tags built locally. */
1478
+ const TEMPLATE_IMAGE_REPO = "mastra-template";
1479
+ function canonicalOperation(operation) {
1480
+ switch (operation.method) {
1481
+ case "setEnvs": return {
1482
+ method: "setEnvs",
1483
+ args: [sortedEntries(operation.args[0])]
1484
+ };
1485
+ case "runWithSecrets": {
1486
+ const [command, { secrets, output }] = operation.args;
1487
+ return {
1488
+ method: "runWithSecrets",
1489
+ args: [command, {
1490
+ secrets: [...secrets].sort(),
1491
+ output
1492
+ }]
1493
+ };
1494
+ }
1495
+ default: return operation;
1496
+ }
1497
+ }
1498
+ /**
1499
+ * Stable content hash over the base image and ordered operations. Env records
1500
+ * and secret name lists are canonicalized so insertion order does not change
1501
+ * the identity. Secret *values* never enter the definition, so the same
1502
+ * definition built with different credentials resolves to the same image tag.
1503
+ */
1504
+ function templateIdentity(definition) {
1505
+ const canonical = JSON.stringify({
1506
+ schemaVersion: 1,
1507
+ baseImage: definition.baseImage,
1508
+ operations: definition.operations.map(canonicalOperation)
1509
+ });
1510
+ return (0, crypto.createHash)("sha256").update(canonical).digest("hex").slice(0, 24);
1511
+ }
1512
+ /** Full `mastra-template:<hash>` tag for a definition. */
1513
+ function templateImageTag(definition) {
1514
+ return `${TEMPLATE_IMAGE_REPO}:${templateIdentity(definition)}`;
1515
+ }
1516
+ //#endregion
1517
+ //#region src/template/template.ts
1518
+ /**
1519
+ * DockerTemplate — a reusable prepared baseline for the local Docker sandbox.
1520
+ *
1521
+ * Prepare an environment once (base image + ordered setup commands + env +
1522
+ * package installs), `build()` it into a content-addressed local image, then
1523
+ * spawn multiple disposable `DockerSandbox`es from that image. Each sandbox is a
1524
+ * fresh container with its own writable layer over the shared read-only image,
1525
+ * so their filesystems are independent. The built image's lifecycle is
1526
+ * controlled by `dispose()`, independent of any sandbox's `destroy()`.
1527
+ *
1528
+ * Unlike `docker commit` (which cannot capture mounted volumes and skips layer
1529
+ * caching), the baseline is produced by synthesizing a Dockerfile and running
1530
+ * `docker build`, so repo/setup content is baked into reproducible, cached
1531
+ * image layers.
1532
+ *
1533
+ * @example Prepare once, spawn many
1534
+ * ```typescript
1535
+ * import { DockerTemplate } from '@mastra/docker';
1536
+ *
1537
+ * const template = new DockerTemplate({ baseImage: 'node:22-slim' })
1538
+ * .runCmd('git clone --depth=1 https://example.com/repo /workspace/app')
1539
+ * .setWorkdir('/workspace/app')
1540
+ * .runCmd('npm ci');
1541
+ *
1542
+ * const result = await template.build();
1543
+ * if (result.status !== 'ready') throw new Error(result.error);
1544
+ *
1545
+ * const a = await template.createSandbox();
1546
+ * const b = await template.createSandbox(); // independent writable filesystem
1547
+ * ```
1548
+ */
1549
+ const MAX_OPERATIONS = 256;
1550
+ const MAX_STRING_LENGTH = 32 * 1024;
1551
+ const MAX_COLLECTION_ITEMS = 512;
1552
+ /**
1553
+ * Immutable, chainable builder + build/lifecycle for a local Docker template.
1554
+ * Operation methods return a new instance (like the platform `Template()`
1555
+ * builder); `build`/`createSandbox`/`dispose` operate against the daemon.
1556
+ */
1557
+ var DockerTemplate = class DockerTemplate {
1558
+ #baseImage;
1559
+ #operations;
1560
+ #dockerOptions;
1561
+ #secrets;
1562
+ #docker;
1563
+ #built = false;
1564
+ #inFlight;
1565
+ constructor(options = {}, state) {
1566
+ this.#baseImage = state ? state.baseImage : validateString(options.baseImage ?? "node:22-slim", "baseImage");
1567
+ this.#operations = state?.operations ?? [];
1568
+ this.#dockerOptions = options.dockerOptions;
1569
+ this.#secrets = options.secrets;
1570
+ }
1571
+ #clone(next) {
1572
+ return new DockerTemplate({
1573
+ dockerOptions: this.#dockerOptions,
1574
+ secrets: this.#secrets
1575
+ }, {
1576
+ baseImage: next.baseImage ?? this.#baseImage,
1577
+ operations: next.operations ?? this.#operations
1578
+ });
1579
+ }
1580
+ #append(operation) {
1581
+ if (this.#operations.length >= MAX_OPERATIONS) throw new RangeError(`Docker template cannot contain more than ${MAX_OPERATIONS} operations`);
1582
+ return this.#clone({ operations: [...this.#operations, operation] });
1583
+ }
1584
+ /** Set the base image. */
1585
+ from(image) {
1586
+ return this.#clone({ baseImage: validateString(image, "image") });
1587
+ }
1588
+ /** Set the working directory for subsequent steps and the runtime container. */
1589
+ setWorkdir(path) {
1590
+ return this.#append({
1591
+ method: "setWorkdir",
1592
+ args: [validateString(path, "path")]
1593
+ });
1594
+ }
1595
+ /**
1596
+ * Set environment variables. They are baked into the image via `ENV` and
1597
+ * participate in the template identity, so never put secrets here — use
1598
+ * {@link runWithSecrets} for anything that must not persist in the image.
1599
+ */
1600
+ setEnvs(envs) {
1601
+ return this.#append({
1602
+ method: "setEnvs",
1603
+ args: [validateStringRecord(envs, "envs")]
1604
+ });
1605
+ }
1606
+ /** Run a command (or `&&`-joined list of commands) as a build step. */
1607
+ runCmd(command) {
1608
+ return this.#append({
1609
+ method: "runCmd",
1610
+ args: [validateStringOrStrings(command, "command")]
1611
+ });
1612
+ }
1613
+ /**
1614
+ * Run a command that needs build-time secrets, without persisting them.
1615
+ *
1616
+ * The command runs in a throwaway build stage forked from the template as it
1617
+ * stands at that point, so earlier WORKDIR/ENV/installs apply and later
1618
+ * steps see the copied `output`. The named secrets are resolved when
1619
+ * `build()` runs — from `build({ secrets })`, then the template's `secrets`
1620
+ * option, then `process.env` — and exposed to the command as environment
1621
+ * variables. Values reach the daemon through a BuildKit secret mount
1622
+ * (`RUN --mount=type=secret`), which is tmpfs-backed and scoped to that one
1623
+ * RUN — never a build arg, layer, history entry, or cache metadata. Only
1624
+ * `output` is copied into the template image. Builds that use secrets
1625
+ * require a BuildKit-capable daemon (Docker 20.10+).
1626
+ */
1627
+ runWithSecrets(command, options) {
1628
+ if (!Array.isArray(options.secrets)) throw new TypeError("secrets must be an array of strings");
1629
+ if (options.secrets.length > MAX_COLLECTION_ITEMS) throw new RangeError(`secrets cannot contain more than ${MAX_COLLECTION_ITEMS} items`);
1630
+ const secrets = options.secrets.map((name) => {
1631
+ if (typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new TypeError(`secrets must be environment variable names, got ${JSON.stringify(name)}`);
1632
+ return name;
1633
+ });
1634
+ const output = validateString(options.output, "output");
1635
+ if (!output.startsWith("/")) throw new TypeError("output must be an absolute path");
1636
+ return this.#append({
1637
+ method: "runWithSecrets",
1638
+ args: [validateStringOrStrings(command, "command"), {
1639
+ secrets,
1640
+ output
1641
+ }]
1642
+ });
1643
+ }
1644
+ /** Install apt packages. */
1645
+ aptInstall(packages, options) {
1646
+ return this.#append({
1647
+ method: "aptInstall",
1648
+ args: [validateStringOrStrings(packages, "packages"), options]
1649
+ });
1650
+ }
1651
+ /** Install pip packages (or `pip install .` for the current workdir when omitted). */
1652
+ pipInstall(packages, options) {
1653
+ const validated = packages === void 0 ? void 0 : validateStringOrStrings(packages, "packages");
1654
+ return this.#append({
1655
+ method: "pipInstall",
1656
+ args: [validated, options]
1657
+ });
1658
+ }
1659
+ /** Install npm packages (or run `npm install` for the current workdir when omitted). */
1660
+ npmInstall(packages, options) {
1661
+ const validated = packages === void 0 ? void 0 : validateStringOrStrings(packages, "packages");
1662
+ return this.#append({
1663
+ method: "npmInstall",
1664
+ args: [validated, options]
1665
+ });
1666
+ }
1667
+ /** The resolved definition (base image + ordered operations). */
1668
+ get definition() {
1669
+ return {
1670
+ baseImage: this.#baseImage,
1671
+ operations: this.#operations
1672
+ };
1673
+ }
1674
+ /** The synthesized Dockerfile for this template. */
1675
+ get dockerfile() {
1676
+ return synthesizeDockerfile(this.definition);
1677
+ }
1678
+ /** The content-addressed local image tag this template resolves to. */
1679
+ get templateId() {
1680
+ return templateImageTag(this.definition);
1681
+ }
1682
+ /**
1683
+ * The working directory the built image ends up with, i.e. the last
1684
+ * `setWorkdir` in the chain (resolved against earlier ones when relative).
1685
+ * `undefined` when the template never sets one, in which case the base
1686
+ * image's `WORKDIR` (or the sandbox default) applies.
1687
+ */
1688
+ get workdir() {
1689
+ let current;
1690
+ for (const op of this.#operations) {
1691
+ if (op.method !== "setWorkdir") continue;
1692
+ const next = op.args[0];
1693
+ current = next.startsWith("/") || current === void 0 ? next : path_posix.default.join(current, next);
1694
+ }
1695
+ return current;
1696
+ }
1697
+ #getDocker() {
1698
+ if (!this.#docker) this.#docker = new dockerode.default(this.#dockerOptions);
1699
+ return this.#docker;
1700
+ }
1701
+ /**
1702
+ * Build (or reuse) the template's image. Idempotent: if an image with the
1703
+ * computed tag already exists locally and `force` is not set, returns
1704
+ * `ready` without rebuilding. Otherwise synthesizes a Dockerfile, runs
1705
+ * `docker build`, and surfaces any build-step failure as `status: 'failed'`.
1706
+ *
1707
+ * @throws if a secret named by `runWithSecrets` cannot be resolved from
1708
+ * `options.secrets`, the template's `secrets` option, or `process.env`.
1709
+ */
1710
+ async build(options = {}) {
1711
+ if (!options.force && options.secrets === void 0 && options.docker === void 0 && this.#inFlight) return this.#inFlight;
1712
+ const run = (this.#inFlight?.catch(() => void 0) ?? Promise.resolve()).then(() => this.#build(options));
1713
+ this.#inFlight = run;
1714
+ run.finally(() => {
1715
+ if (this.#inFlight === run) this.#inFlight = void 0;
1716
+ }).catch(() => void 0);
1717
+ return run;
1718
+ }
1719
+ async #build(options) {
1720
+ const docker = options.docker ?? this.#getDocker();
1721
+ const tag = this.templateId;
1722
+ if (!options.force) try {
1723
+ await docker.getImage(tag).inspect();
1724
+ this.#built = true;
1725
+ return {
1726
+ status: "ready",
1727
+ templateId: tag
1728
+ };
1729
+ } catch (error) {
1730
+ if (!isImageNotFoundError(error)) throw error;
1731
+ }
1732
+ const secrets = await this.#resolveSecrets(options.secrets);
1733
+ const context = (0, tar_stream.pack)();
1734
+ context.entry({ name: "Dockerfile" }, this.dockerfile);
1735
+ context.finalize();
1736
+ try {
1737
+ const nocache = options.force === true;
1738
+ if (secrets) {
1739
+ const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache);
1740
+ try {
1741
+ await this.#followBuild(docker, stream);
1742
+ } finally {
1743
+ session.close();
1744
+ }
1745
+ } else await this.#followBuild(docker, await docker.buildImage(context, {
1746
+ t: tag,
1747
+ nocache
1748
+ }));
1749
+ } catch (error) {
1750
+ this.#built = false;
1751
+ return {
1752
+ status: "failed",
1753
+ templateId: tag,
1754
+ error: error instanceof Error ? error.message : String(error)
1755
+ };
1756
+ }
1757
+ this.#built = true;
1758
+ return {
1759
+ status: "ready",
1760
+ templateId: tag
1761
+ };
1762
+ }
1763
+ async #resolveSecrets(override) {
1764
+ const names = secretNames(this.definition);
1765
+ if (names.length === 0) return void 0;
1766
+ const fromBuild = await readSecrets(override);
1767
+ const fromTemplate = await readSecrets(this.#secrets);
1768
+ const resolved = {};
1769
+ for (const name of names) {
1770
+ const value = fromBuild[name] ?? fromTemplate[name] ?? process.env[name];
1771
+ if (value === void 0) throw new Error(`Docker template secret ${name} was not provided (secrets option) and is not set in the environment`);
1772
+ resolved[name] = value;
1773
+ }
1774
+ return resolved;
1775
+ }
1776
+ /**
1777
+ * BuildKit build with a session serving the secret mounts. Dials `/build`
1778
+ * directly because `docker.buildImage` replaces any session id with its own
1779
+ * auth-only session when `version` is `'2'`.
1780
+ */
1781
+ async #buildWithSecrets(docker, context, tag, secrets, nocache) {
1782
+ const session = await openBuildSession(docker, secrets);
1783
+ let stream;
1784
+ try {
1785
+ stream = await new Promise((resolve, reject) => {
1786
+ docker.modem.dial({
1787
+ path: "/build?",
1788
+ method: "POST",
1789
+ file: context,
1790
+ options: {
1791
+ t: tag,
1792
+ version: "2",
1793
+ session: session.id,
1794
+ nocache
1795
+ },
1796
+ isStream: true,
1797
+ statusCodes: {
1798
+ 200: true,
1799
+ 500: "server error"
1800
+ }
1801
+ }, (err, data) => err ? reject(err) : resolve(data));
1802
+ });
1803
+ } catch (error) {
1804
+ session.close();
1805
+ throw error;
1806
+ }
1807
+ return {
1808
+ stream,
1809
+ session
1810
+ };
1811
+ }
1812
+ #followBuild(docker, stream) {
1813
+ return new Promise((resolve, reject) => {
1814
+ docker.modem.followProgress(stream, (err, output) => {
1815
+ if (err) {
1816
+ reject(err);
1817
+ return;
1818
+ }
1819
+ const failure = output?.find((entry) => entry && (entry.error !== void 0 || entry.errorDetail !== void 0));
1820
+ if (failure) {
1821
+ const detail = failure.errorDetail;
1822
+ reject(new Error(String(detail?.message ?? failure.error ?? "docker build failed")));
1823
+ return;
1824
+ }
1825
+ resolve();
1826
+ });
1827
+ });
1828
+ }
1829
+ /**
1830
+ * Create a `DockerSandbox` bound to the built image. Lazily builds the
1831
+ * template if it has not been built yet. Each call returns a fresh sandbox
1832
+ * with an independent writable layer. Invocation-specific `env`/config passed
1833
+ * via `options` reaches only the container and is never baked into the image.
1834
+ *
1835
+ * The sandbox's working directory follows the template's last `setWorkdir`
1836
+ * unless `options.workingDirectory` overrides it, so relative paths resolve
1837
+ * against the same directory the image was prepared in.
1838
+ *
1839
+ * @throws if the (lazy) build fails.
1840
+ */
1841
+ async createSandbox(options = {}) {
1842
+ if (!this.#built) {
1843
+ const result = await this.build();
1844
+ if (result.status !== "ready") throw new Error(`Docker template build failed: ${result.error ?? "unknown error"}`);
1845
+ }
1846
+ const workingDirectory = options.workingDirectory ?? options.workingDir ?? this.workdir;
1847
+ return new DockerSandbox({
1848
+ ...options,
1849
+ ...workingDirectory !== void 0 && { workingDirectory },
1850
+ template: this,
1851
+ dockerOptions: options.dockerOptions ?? this.#dockerOptions
1852
+ });
1853
+ }
1854
+ /**
1855
+ * Remove the built image (`docker rmi`). Tolerant of an already-removed
1856
+ * image. Independent of any sandbox created from this template, but the
1857
+ * daemon refuses to remove an image that a container (running or stopped)
1858
+ * still references, so destroy those sandboxes first.
1859
+ */
1860
+ async dispose() {
1861
+ const docker = this.#getDocker();
1862
+ try {
1863
+ await docker.getImage(this.templateId).remove();
1864
+ } catch (error) {
1865
+ if (!isImageNotFoundError(error)) throw error;
1866
+ }
1867
+ this.#built = false;
1868
+ }
1869
+ };
1870
+ function validateString(value, name) {
1871
+ if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
1872
+ if (value.length === 0) throw new TypeError(`${name} must not be empty`);
1873
+ if (value.length > MAX_STRING_LENGTH) throw new RangeError(`${name} cannot exceed ${MAX_STRING_LENGTH} characters`);
1874
+ return value;
1875
+ }
1876
+ function validateStringOrStrings(value, name) {
1877
+ if (typeof value === "string") return validateString(value, name);
1878
+ if (!Array.isArray(value)) throw new TypeError(`${name} must be a string or an array of strings`);
1879
+ if (value.length === 0) throw new TypeError(`${name} must not be empty`);
1880
+ if (value.length > MAX_COLLECTION_ITEMS) throw new RangeError(`${name} cannot contain more than ${MAX_COLLECTION_ITEMS} items`);
1881
+ return value.map((item, index) => validateString(item, `${name}[${index}]`));
1882
+ }
1883
+ function validateStringRecord(value, name) {
1884
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${name} must be a plain object`);
1885
+ const entries = Object.entries(value);
1886
+ if (entries.length > MAX_COLLECTION_ITEMS) throw new RangeError(`${name} cannot contain more than ${MAX_COLLECTION_ITEMS} items`);
1887
+ return Object.fromEntries(entries.map(([key, item]) => {
1888
+ if (typeof item !== "string") throw new TypeError(`${name}.${key} must be a string`);
1889
+ return [validateString(key, `${name} key`), item];
1890
+ }));
1891
+ }
1892
+ async function readSecrets(source) {
1893
+ if (!source) return {};
1894
+ return typeof source === "function" ? await source() : source;
1895
+ }
1896
+ function isImageNotFoundError(error) {
1897
+ if (error instanceof Error) {
1898
+ const msg = error.message.toLowerCase();
1899
+ return msg.includes("no such image") || msg.includes("404");
1900
+ }
1901
+ return false;
1902
+ }
1903
+ //#endregion
1904
+ //#region ../../packages/_internals/workspace/dist/index.js
1905
+ /**
1906
+ * Setup completion marker shared by repo templates and their consumers.
1907
+ *
1908
+ * A repo template writes this file beside the checkout as its last build
1909
+ * step, so it exists only in images where every setup command succeeded. Its
1910
+ * content is a digest of the setup commands the image ran, letting a sandbox
1911
+ * booted from the image tell whether the setup it is about to run already
1912
+ * happened. Relative to the template's build cwd, which is also the runtime
1913
+ * working directory the repo was cloned into.
1914
+ */
1915
+ const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
1916
+ /** Blank entries never become build steps, so they never count toward the digest either. */
1917
+ function normalizeSetupCommands(setupCommand) {
1918
+ return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
1919
+ }
1920
+ /** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */
1921
+ function setupMarkerContent(setupCommand) {
1922
+ return `sha256:${(0, crypto.createHash)("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
1923
+ }
1924
+ /** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
1925
+ function setupMarkerCommand(content) {
1926
+ return `mkdir -p "$(dirname "${SETUP_MARKER_PATH}")" && printf '%s' '${content}' > "${SETUP_MARKER_PATH}"`;
1927
+ }
1928
+ //#endregion
1929
+ //#region src/template/repo-template.ts
1930
+ /**
1931
+ * createDockerRepoTemplate — a repo checkout plus setup commands as a
1932
+ * reusable, content-addressed local image, with the same contract as the
1933
+ * E2B and platform repo templates (`getRepositoryAccess`, `setupCommand`,
1934
+ * `buildEnv`, `workingDirectory`).
1935
+ *
1936
+ * Returns a template RESOLVER for `DockerSandbox`'s `template` option rather
1937
+ * than a fixed template: each resolution calls `getRepositoryAccess`, looks up
1938
+ * the current head of `ref` (`git ls-remote`, no clone) and pins that sha into
1939
+ * the template identity. A moved branch therefore yields a fresh image on the
1940
+ * next new sandbox, and an unmoved one reuses the cached image. When the head
1941
+ * cannot be resolved the resolver rejects: an unpinned clone cached under a
1942
+ * stable tag would otherwise serve stale repository state forever.
1943
+ *
1944
+ * The clone runs in a throwaway build stage; the credential is passed by value
1945
+ * to that stage only and never enters the template identity or the image.
1946
+ *
1947
+ * @example
1948
+ * ```typescript
1949
+ * const sandbox = new DockerSandbox({
1950
+ * template: createDockerRepoTemplate({
1951
+ * getRepositoryAccess: async () => ({ cloneUrl: 'https://github.com/acme/app.git' }),
1952
+ * setupCommand: ['npm ci', 'npm run build'],
1953
+ * }),
1954
+ * });
1955
+ * ```
1956
+ */
1957
+ const execFileAsync = (0, util.promisify)(child_process.execFile);
1958
+ /** Env var the build's clone reads the credential from (see `cloneFull`). */
1959
+ const BUILD_TOKEN_ENV = "GH_TOKEN";
1960
+ const DEFAULT_BASE_IMAGE = "node:22-slim";
1961
+ const DEFAULT_WORKING_DIRECTORY = "/workspace";
1962
+ const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/i;
1963
+ const SHA_PATTERN = /^[0-9a-f]{40}$/i;
1964
+ const CLONE_URL_ALLOWED_CHARS = /^[a-z0-9:/._-]+$/i;
1965
+ const CLONE_URL_HOST_PATTERN = /^[a-z0-9.-]+$/i;
1966
+ const CLONE_URL_SEGMENT_PATTERN = /^[\w.-]+$/;
1967
+ /** Refs interpolate into shell too; git ref names are already restricted, so allowlist tightly. */
1968
+ const REF_PATTERN = /^[\w./-]+$/;
1969
+ function createDockerRepoTemplate(options) {
1970
+ if (!options.getRepositoryAccess) return void 0;
1971
+ if (options.ref !== void 0 && !REF_PATTERN.test(options.ref)) throw new Error(`Invalid ref '${options.ref}': expected a git ref name`);
1972
+ const workingDirectory = trimTrailingSlashes(options.workingDirectory ?? DEFAULT_WORKING_DIRECTORY);
1973
+ if (!workingDirectory.startsWith("/")) throw new Error(`workingDirectory must be an absolute path, got '${options.workingDirectory}'`);
1974
+ return () => resolveRepoTemplate(options, workingDirectory);
1975
+ }
1976
+ async function resolveRepoTemplate(options, workingDirectory) {
1977
+ const access = await options.getRepositoryAccess();
1978
+ const cloneUrl = access?.cloneUrl;
1979
+ if (!cloneUrl) throw new Error("Repo template has no clone URL: repository access returned none.");
1980
+ assertCloneUrl(cloneUrl);
1981
+ const token = access?.authorization?.token;
1982
+ const buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv() : options.buildEnv;
1983
+ const sha = await resolveHead(cloneUrl, options.ref, token);
1984
+ 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`);
1985
+ return buildRepoTemplate({
1986
+ cloneUrl,
1987
+ sha,
1988
+ token,
1989
+ buildEnv,
1990
+ setupCommand: options.setupCommand,
1991
+ workingDirectory,
1992
+ baseImage: options.baseImage,
1993
+ dockerOptions: options.dockerOptions
1994
+ });
1995
+ }
1996
+ /**
1997
+ * Pure assembly of the template from already-resolved inputs. Exported for
1998
+ * tests so the Dockerfile can be asserted without a network head lookup.
1999
+ * @internal
2000
+ */
2001
+ function buildRepoTemplate(inputs) {
2002
+ const { cloneUrl, sha, token, buildEnv } = inputs;
2003
+ const destination = `${trimTrailingSlashes(inputs.workingDirectory)}/${repoDirName(cloneUrl)}`;
2004
+ let template = new DockerTemplate({
2005
+ baseImage: inputs.baseImage ?? DEFAULT_BASE_IMAGE,
2006
+ dockerOptions: inputs.dockerOptions,
2007
+ ...token ? { secrets: { [BUILD_TOKEN_ENV]: token } } : {}
2008
+ });
2009
+ if (inputs.baseImage === void 0) template = template.aptInstall(["git", "ca-certificates"]);
2010
+ if (buildEnv && Object.keys(buildEnv).length > 0) template = template.setEnvs(buildEnv);
2011
+ const tokenEnv = token ? BUILD_TOKEN_ENV : void 0;
2012
+ const clone = [cloneFull({
2013
+ cloneUrl,
2014
+ destination,
2015
+ tokenEnv
2016
+ }), `git -C ${shellQuote(destination)} checkout --detach ${shellQuote(sha)}`];
2017
+ template = template.runWithSecrets(clone, {
2018
+ secrets: tokenEnv ? [tokenEnv] : [],
2019
+ output: destination
2020
+ }).setWorkdir(destination);
2021
+ const setupCommands = normalizeSetupCommands(inputs.setupCommand);
2022
+ for (const command of setupCommands) template = template.runCmd(command);
2023
+ if (setupCommands.length > 0) template = template.runCmd(setupMarkerCommand(setupMarkerContent(setupCommands)));
2024
+ return template;
2025
+ }
2026
+ /**
2027
+ * Resolve `ref` (or the default branch) to a commit sha with `git ls-remote`
2028
+ * on the host, without cloning. A full sha is returned as is. Any failure
2029
+ * yields undefined; the caller decides how to surface it.
2030
+ * @internal exported for tests.
2031
+ */
2032
+ async function resolveHead(cloneUrl, ref, token) {
2033
+ if (ref && FULL_SHA_PATTERN.test(ref)) return ref.toLowerCase();
2034
+ try {
2035
+ const authEnv = token ? {
2036
+ GIT_CONFIG_COUNT: "1",
2037
+ GIT_CONFIG_KEY_0: "http.extraheader",
2038
+ GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`
2039
+ } : {};
2040
+ const { stdout } = await execFileAsync("git", [
2041
+ "ls-remote",
2042
+ "--",
2043
+ cloneUrl,
2044
+ ref ?? "HEAD"
2045
+ ], {
2046
+ timeout: 1e4,
2047
+ env: {
2048
+ ...process.env,
2049
+ ...authEnv,
2050
+ GIT_TERMINAL_PROMPT: "0"
2051
+ }
2052
+ });
2053
+ const lines = stdout.split("\n").filter(Boolean).map((line) => line.split(" "));
2054
+ const sha = (lines.find(([, name]) => name?.endsWith("^{}")) ?? lines[0])?.[0]?.trim();
2055
+ return sha && SHA_PATTERN.test(sha) ? sha.toLowerCase() : void 0;
2056
+ } catch {
2057
+ return;
2058
+ }
2059
+ }
2060
+ function cloneFull({ cloneUrl, destination, tokenEnv }) {
2061
+ return `git ${tokenEnv ? `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)" ` : ""}clone ${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
2062
+ }
2063
+ function assertCloneUrl(cloneUrl) {
2064
+ if (cloneUrl.length > 2048 || !CLONE_URL_ALLOWED_CHARS.test(cloneUrl)) throw new Error(`Invalid cloneUrl '${cloneUrl}': expected an https URL with a plain host and path`);
2065
+ let url;
2066
+ try {
2067
+ url = new URL(cloneUrl);
2068
+ } catch {
2069
+ throw new Error(`Invalid cloneUrl '${cloneUrl}': not a URL`);
2070
+ }
2071
+ const segments = url.pathname.split("/").slice(1);
2072
+ if (!(url.protocol === "https:" && !url.username && !url.password && !url.search && !url.hash && CLONE_URL_HOST_PATTERN.test(url.hostname) && segments.length > 0 && segments.every((segment) => CLONE_URL_SEGMENT_PATTERN.test(segment)))) throw new Error(`Invalid cloneUrl '${cloneUrl}': expected an https URL such as https://host/owner/repo.git`);
2073
+ }
2074
+ function repoDirName(cloneUrl) {
2075
+ return (trimTrailingSlashes(cloneUrl).split("/").at(-1) ?? "").replace(/\.git$/i, "").replace(/[^\w.-]/g, "-").replace(/^\.+/, "") || "repo";
2076
+ }
2077
+ function trimTrailingSlashes(path) {
2078
+ let end = path.length;
2079
+ while (end > 1 && path[end - 1] === "/") end--;
2080
+ return path.slice(0, end);
2081
+ }
2082
+ function shellQuote(value) {
2083
+ return `'${value.replace(/'/g, `'\\''`)}'`;
2084
+ }
2085
+ //#endregion
1173
2086
  exports.DockerProcessManager = DockerProcessManager;
1174
2087
  exports.DockerSandbox = DockerSandbox;
2088
+ exports.DockerTemplate = DockerTemplate;
2089
+ exports.TEMPLATE_IMAGE_REPO = TEMPLATE_IMAGE_REPO;
2090
+ exports.createDockerRepoTemplate = createDockerRepoTemplate;
1175
2091
  exports.dockerSandboxProvider = dockerSandboxProvider;
2092
+ exports.secretNames = secretNames;
2093
+ exports.synthesizeDockerfile = synthesizeDockerfile;
2094
+ exports.templateIdentity = templateIdentity;
2095
+ exports.templateImageTag = templateImageTag;
1176
2096
 
1177
2097
  //# sourceMappingURL=index.cjs.map