@mastra/docker 0.8.0 → 0.9.0-alpha.1

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