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