@mastra/e2b 0.12.4-alpha.0 → 0.13.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
@@ -127,7 +127,7 @@ function validatePrefix(prefix) {
127
127
  * Arguments containing only safe characters are returned as-is.
128
128
  * All others are wrapped in single quotes with embedded single quotes escaped.
129
129
  */
130
- function shellQuote$1(arg) {
130
+ function shellQuote$2(arg) {
131
131
  if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
132
132
  return "'" + arg.replace(/'/g, "'\\''") + "'";
133
133
  }
@@ -180,12 +180,12 @@ Error details: ${installResult.stderr || installResult.stdout}`);
180
180
  };
181
181
  if (hasCredentials) {
182
182
  const credentialsContent = useSessionToken ? [
183
- `export AWSACCESSKEYID=${shellQuote$1(config.accessKeyId)}`,
184
- `export AWSSECRETACCESSKEY=${shellQuote$1(config.secretAccessKey)}`,
185
- `export AWSSESSIONTOKEN=${shellQuote$1(config.sessionToken)}`,
186
- `export AWS_ACCESS_KEY_ID=${shellQuote$1(config.accessKeyId)}`,
187
- `export AWS_SECRET_ACCESS_KEY=${shellQuote$1(config.secretAccessKey)}`,
188
- `export AWS_SESSION_TOKEN=${shellQuote$1(config.sessionToken)}`
183
+ `export AWSACCESSKEYID=${shellQuote$2(config.accessKeyId)}`,
184
+ `export AWSSECRETACCESSKEY=${shellQuote$2(config.secretAccessKey)}`,
185
+ `export AWSSESSIONTOKEN=${shellQuote$2(config.sessionToken)}`,
186
+ `export AWS_ACCESS_KEY_ID=${shellQuote$2(config.accessKeyId)}`,
187
+ `export AWS_SECRET_ACCESS_KEY=${shellQuote$2(config.secretAccessKey)}`,
188
+ `export AWS_SESSION_TOKEN=${shellQuote$2(config.sessionToken)}`
189
189
  ].join("\n") : `${config.accessKeyId}:${config.secretAccessKey}`;
190
190
  await sandbox.commands.run(`sudo rm -f ${credentialsPath}`);
191
191
  try {
@@ -213,8 +213,8 @@ Error details: ${installResult.stderr || installResult.stdout}`);
213
213
  mountOptions.push("ro");
214
214
  logger.debug(`${LOG_PREFIX} Mounting as read-only`);
215
215
  }
216
- const s3fsCmd = `s3fs ${shellQuote$1(bucketArg)} ${shellQuote$1(mountPath)} -o ${mountOptions.join(" -o ")}`;
217
- const mountCmd = useSessionToken ? `sudo sh -c ${shellQuote$1(`. ${credentialsPath} && exec ${s3fsCmd}`)}` : `sudo ${s3fsCmd}`;
216
+ const s3fsCmd = `s3fs ${shellQuote$2(bucketArg)} ${shellQuote$2(mountPath)} -o ${mountOptions.join(" -o ")}`;
217
+ const mountCmd = useSessionToken ? `sudo sh -c ${shellQuote$2(`. ${credentialsPath} && exec ${s3fsCmd}`)}` : `sudo ${s3fsCmd}`;
218
218
  logger.debug(`${LOG_PREFIX} Mounting S3:`, hasCredentials ? mountCmd.replace(credentialsPath, "***") : mountCmd);
219
219
  try {
220
220
  const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
@@ -237,7 +237,7 @@ Error details: ${installResult.stderr || installResult.stdout}`);
237
237
  } finally {
238
238
  if (useSessionToken) await removeSessionCredentials();
239
239
  }
240
- if ((await sandbox.commands.run(`mountpoint -q ${shellQuote$1(mountPath)}`)).exitCode !== 0) throw new Error(`s3fs returned exit 0 but ${mountPath} is not a mountpoint. The s3fs daemon likely failed during FUSE init (common causes: region mismatch, invalid credentials, or an S3-compatible endpoint that rejects the signature). Re-run inside the sandbox with '-f -o dbglevel=info' to see the underlying error.`);
240
+ if ((await sandbox.commands.run(`mountpoint -q ${shellQuote$2(mountPath)}`)).exitCode !== 0) throw new Error(`s3fs returned exit 0 but ${mountPath} is not a mountpoint. The s3fs daemon likely failed during FUSE init (common causes: region mismatch, invalid credentials, or an S3-compatible endpoint that rejects the signature). Re-run inside the sandbox with '-f -o dbglevel=info' to see the underlying error.`);
241
241
  }
242
242
  //#endregion
243
243
  //#region src/sandbox/mounts/gcs.ts
@@ -257,7 +257,7 @@ async function mountGCS(mountPath, config, ctx) {
257
257
  }
258
258
  const [uid, gid] = (await sandbox.commands.run("id -u && id -g")).stdout.trim().split("\n");
259
259
  const uidGidFlags = uid && gid ? `--uid=${uid} --gid=${gid}` : "";
260
- const onlyDirFlag = config.prefix ? ` --only-dir=${shellQuote$1(validatePrefix(config.prefix))}` : "";
260
+ const onlyDirFlag = config.prefix ? ` --only-dir=${shellQuote$2(validatePrefix(config.prefix))}` : "";
261
261
  const hasCredentials = !!config.serviceAccountKey;
262
262
  let mountCmd;
263
263
  if (hasCredentials) {
@@ -465,9 +465,9 @@ Error details: ${verifyResult.stderr || verifyResult.stdout || installResult?.st
465
465
  await sandbox.commands.run(`sudo rm -f ${configPath}`);
466
466
  await sandbox.files.write(configPath, yaml);
467
467
  await sandbox.commands.run(`sudo chown root:root ${configPath} && sudo chmod 600 ${configPath}`);
468
- await sandbox.commands.run(`sudo rm -rf ${shellQuote$1(cachePath)} && sudo mkdir -p ${shellQuote$1(cachePath)}`);
469
- const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote$1(prefix)}` : "";
470
- const mountCmd = `sudo blobfuse2 mount ${shellQuote$1(mountPath)} --config-file=${shellQuote$1(configPath)}${prefixFlags}`;
468
+ await sandbox.commands.run(`sudo rm -rf ${shellQuote$2(cachePath)} && sudo mkdir -p ${shellQuote$2(cachePath)}`);
469
+ const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote$2(prefix)}` : "";
470
+ const mountCmd = `sudo blobfuse2 mount ${shellQuote$2(mountPath)} --config-file=${shellQuote$2(configPath)}${prefixFlags}`;
471
471
  logger.debug(`${LOG_PREFIX} Mounting Azure Blob:`, mountCmd);
472
472
  try {
473
473
  const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
@@ -1594,6 +1594,8 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
1594
1594
  };
1595
1595
  //#endregion
1596
1596
  //#region ../../packages/_internals/workspace/dist/index.js
1597
+ /** Directory, relative to the template's build cwd, that holds every marker below. */
1598
+ const SETUP_MARKER_DIR = ".mastra-sandbox";
1597
1599
  /**
1598
1600
  * Setup completion marker shared by repo templates and their consumers.
1599
1601
  *
@@ -1603,8 +1605,29 @@ var E2BSandbox = class E2BSandbox extends _mastra_core_workspace.MastraSandbox {
1603
1605
  * booted from the image tell whether the setup it is about to run already
1604
1606
  * happened. Relative to the template's build cwd, which is also the runtime
1605
1607
  * working directory the repo was cloned into.
1608
+ *
1609
+ * This is the single-repository marker. Templates built from a repository
1610
+ * list write one marker per repository instead ({@link repoSetupMarkerPath}).
1606
1611
  */
1607
1612
  const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
1613
+ /**
1614
+ * Marker for the workspace-level setup of a multi-repository template: the
1615
+ * commands that ran at the working directory after every repository was set
1616
+ * up. Written even when there were no such commands, so a consumer can tell
1617
+ * "ran nothing" from "never built".
1618
+ */
1619
+ const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
1620
+ /**
1621
+ * One line per repository directory whose setup failed while the build was
1622
+ * told to continue. Absent when nothing failed. Consumers check this before
1623
+ * trusting a per-repository marker: that marker means every step ran, not
1624
+ * that every step succeeded.
1625
+ */
1626
+ const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
1627
+ /** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
1628
+ function repoSetupMarkerPath(repoDir) {
1629
+ return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
1630
+ }
1608
1631
  /** Blank entries never become build steps, so they never count toward the digest either. */
1609
1632
  function normalizeSetupCommands$1(setupCommand) {
1610
1633
  return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
@@ -1614,8 +1637,26 @@ function setupMarkerContent(setupCommand) {
1614
1637
  return `sha256:${(0, crypto.createHash)("sha256").update(normalizeSetupCommands$1(setupCommand).join("\n")).digest("hex")}`;
1615
1638
  }
1616
1639
  /** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
1617
- function setupMarkerCommand(content) {
1618
- return `mkdir -p "$(dirname "${SETUP_MARKER_PATH}")" && printf '%s' '${content}' > "${SETUP_MARKER_PATH}"`;
1640
+ function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
1641
+ return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
1642
+ }
1643
+ /** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
1644
+ const REPO_DIR_PATTERN = /^[\w.-]+$/;
1645
+ /**
1646
+ * Shell step that runs one setup command inside a repository clone.
1647
+ *
1648
+ * Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
1649
+ * the `cd` sits inside the parenthesized group so that the failure branch
1650
+ * runs back at the build cwd and the failure list lands at the workspace
1651
+ * level, never inside the repository.
1652
+ */
1653
+ function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
1654
+ if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
1655
+ if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
1656
+ return `( cd "${repoDir}" && sh -c ${shellQuote$1(command)} ) || { mkdir -p "${SETUP_MARKER_DIR}" && grep -qxF -- '${repoDir}' "${SETUP_FAILED_MARKER_PATH}" 2>/dev/null || printf '%s\\n' '${repoDir}' >> "${SETUP_FAILED_MARKER_PATH}"; }`;
1657
+ }
1658
+ function shellQuote$1(value) {
1659
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1619
1660
  }
1620
1661
  function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
1621
1662
  return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : ""}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
@@ -1707,8 +1748,16 @@ function isValidCloneUrl(cloneUrl) {
1707
1748
  */
1708
1749
  function repoTemplateRef(identity) {
1709
1750
  const name = repoTemplateName(identity);
1751
+ if (identity.repos) {
1752
+ const shas = identity.repos.map((repo) => repo.sha);
1753
+ return shas.every(Boolean) ? `${name}:${shaTag(listSha(shas))}` : `${name}:${CURRENT_TAG}`;
1754
+ }
1710
1755
  return identity.sha ? `${name}:${shaTag(identity.sha)}` : `${name}:${CURRENT_TAG}`;
1711
1756
  }
1757
+ /** One digest over every repository's pinned sha, in caller order. */
1758
+ function listSha(shas) {
1759
+ return (0, crypto.createHash)("sha256").update(shas.map((sha) => sha.toLowerCase()).join(",")).digest("hex");
1760
+ }
1712
1761
  function repoTemplateName(identity) {
1713
1762
  const cloneUrl = normalizeCloneUrl(identity.cloneUrl);
1714
1763
  const config = [
@@ -1718,7 +1767,13 @@ function repoTemplateName(identity) {
1718
1767
  identity.buildEnv ? Object.entries(identity.buildEnv).sort(([a], [b]) => a.localeCompare(b)) : null,
1719
1768
  identity.cpuCount ?? 2,
1720
1769
  identity.memoryMB ?? 1024,
1721
- ...identity.workingDirectory !== void 0 ? [identity.workingDirectory] : []
1770
+ ...identity.workingDirectory !== void 0 ? [identity.workingDirectory] : [],
1771
+ ...identity.repos ? [
1772
+ "repos",
1773
+ identity.repos.map((repo) => [normalizeCloneUrl(repo.cloneUrl), repo.setupCommand ?? null]),
1774
+ normalizeSetupCommands(identity.workspaceSetupCommand),
1775
+ identity.continueOnSetupFailure ?? false
1776
+ ] : []
1722
1777
  ];
1723
1778
  const hash = (0, crypto.createHash)("sha256").update(JSON.stringify(config)).digest("hex").slice(0, 8);
1724
1779
  const { owner, repo } = parseCloneUrl(cloneUrl);
@@ -1750,7 +1805,9 @@ function shaTag(sha) {
1750
1805
  * setup performs the full clone, so a broken build never wedges a session.
1751
1806
  */
1752
1807
  function createRepoTemplate(options) {
1753
- if (!options.getRepositoryAccess) return void 0;
1808
+ if (options.getRepositoryAccess && options.repos) throw new TypeError("createRepoTemplate: pass either getRepositoryAccess or repos, not both");
1809
+ if (!options.repos && (options.workspaceSetupCommand !== void 0 || options.continueOnSetupFailure !== void 0)) throw new TypeError("createRepoTemplate: workspaceSetupCommand and continueOnSetupFailure require repos");
1810
+ if (!options.getRepositoryAccess && !options.repos?.length) return void 0;
1754
1811
  return { resolveSpec: async () => (await resolveSpecAtHead(options)).spec };
1755
1812
  }
1756
1813
  /**
@@ -1762,24 +1819,58 @@ function createRepoTemplate(options) {
1762
1819
  * turns into its default-template fallback rather than a failed start.
1763
1820
  */
1764
1821
  async function resolveSpecAtHead(options) {
1765
- const access = options.getRepositoryAccess ? await options.getRepositoryAccess().catch(() => void 0) : void 0;
1766
- const cloneUrl = access?.cloneUrl;
1767
- if (!cloneUrl) throw new Error("Repo template has no clone URL: repository access returned none.");
1768
- assertCloneUrl(cloneUrl);
1769
- const token = access?.authorization?.token;
1822
+ const isList = options.repos !== void 0;
1823
+ const entries = options.repos ? options.repos : options.getRepositoryAccess ? [{
1824
+ getRepositoryAccess: options.getRepositoryAccess,
1825
+ setupCommand: options.setupCommand
1826
+ }] : [];
1827
+ if (entries.length === 0) throw new Error("Repo template has no clone URL: repository access returned none.");
1770
1828
  const buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv() : options.buildEnv;
1771
- const resolved = await resolveDefaultBranchHead(cloneUrl, token).catch(() => void 0);
1772
- const sha = resolved && SHA_PATTERN.test(resolved) ? resolved : void 0;
1773
- return {
1774
- spec: buildRepoTemplateSpec({
1829
+ const repos = [];
1830
+ const seenDirs = /* @__PURE__ */ new Map();
1831
+ for (const [index, entry] of entries.entries()) {
1832
+ const access = await entry.getRepositoryAccess().catch(() => void 0);
1833
+ const cloneUrl = access?.cloneUrl;
1834
+ if (!cloneUrl) throw new Error(`Repo template has no clone URL: repository access returned none${isList ? ` (repos[${index}])` : ""}.`);
1835
+ assertCloneUrl(cloneUrl);
1836
+ const repoDir = repoDirName(normalizeCloneUrl(cloneUrl));
1837
+ const other = seenDirs.get(repoDir);
1838
+ if (other) throw new Error(`Repo template: repositories ${other} and ${cloneUrl} would both clone into "${repoDir}"`);
1839
+ seenDirs.set(repoDir, cloneUrl);
1840
+ const token = access?.authorization?.token;
1841
+ const resolved = await resolveDefaultBranchHead(cloneUrl, token).catch(() => void 0);
1842
+ const sha = resolved && SHA_PATTERN.test(resolved) ? resolved : void 0;
1843
+ repos.push({
1775
1844
  cloneUrl,
1776
- ...sha ? { sha } : {},
1777
- ...normalizeSetupCommands(options.setupCommand).length > 0 ? { setupCommand: options.setupCommand } : {},
1778
- ...buildEnv ? { buildEnv } : {},
1779
- ...options.cpuCount !== void 0 ? { cpuCount: options.cpuCount } : {},
1780
- ...options.memoryMB !== void 0 ? { memoryMB: options.memoryMB } : {},
1781
- ...options.workingDirectory !== void 0 ? { workingDirectory: trimTrailingSlashes(assertWorkingDirectory(options.workingDirectory)) } : {}
1782
- }, token),
1845
+ repoDir,
1846
+ token,
1847
+ tokenEnv: token ? isList ? `${BUILD_TOKEN_ENV}_${index}` : BUILD_TOKEN_ENV : void 0,
1848
+ sha,
1849
+ setupCommand: entry.setupCommand
1850
+ });
1851
+ }
1852
+ const first = repos[0];
1853
+ const spec = buildRepoTemplateSpec({
1854
+ cloneUrl: first.cloneUrl,
1855
+ ...first.sha ? { sha: first.sha } : {},
1856
+ ...normalizeSetupCommands(first.setupCommand).length > 0 ? { setupCommand: first.setupCommand } : {},
1857
+ ...buildEnv ? { buildEnv } : {},
1858
+ ...options.cpuCount !== void 0 ? { cpuCount: options.cpuCount } : {},
1859
+ ...options.memoryMB !== void 0 ? { memoryMB: options.memoryMB } : {},
1860
+ ...options.workingDirectory !== void 0 ? { workingDirectory: trimTrailingSlashes(assertWorkingDirectory(options.workingDirectory)) } : {},
1861
+ ...isList ? {
1862
+ repos: repos.map((repo) => ({
1863
+ cloneUrl: repo.cloneUrl,
1864
+ ...repo.sha ? { sha: repo.sha } : {},
1865
+ ...normalizeSetupCommands(repo.setupCommand).length > 0 ? { setupCommand: repo.setupCommand } : {}
1866
+ })),
1867
+ ...options.workspaceSetupCommand !== void 0 ? { workspaceSetupCommand: options.workspaceSetupCommand } : {},
1868
+ ...options.continueOnSetupFailure !== void 0 ? { continueOnSetupFailure: options.continueOnSetupFailure } : {}
1869
+ } : {}
1870
+ }, repos, options.continueOnSetupFailure ?? false);
1871
+ const sha = isList ? repos.every((repo) => repo.sha) ? listSha(repos.map((repo) => repo.sha)) : void 0 : first.sha;
1872
+ return {
1873
+ spec,
1783
1874
  ...sha ? { sha } : {}
1784
1875
  };
1785
1876
  }
@@ -1829,31 +1920,45 @@ function assertCloneUrl(cloneUrl) {
1829
1920
  * REFERENCE — the token value never appears in the command string, and no
1830
1921
  * credential is written to the build filesystem.
1831
1922
  */
1832
- function gitAuthFlag() {
1833
- return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${BUILD_TOKEN_ENV}" | base64 -w0)"`;
1923
+ function gitAuthFlag(tokenEnv) {
1924
+ return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
1834
1925
  }
1835
- function buildRepoTemplateSpec(identity, token) {
1836
- const { sha, setupCommand, buildEnv, workingDirectory } = identity;
1837
- const cloneUrl = normalizeCloneUrl(identity.cloneUrl);
1838
- const repoDir = repoDirName(cloneUrl);
1839
- const auth = token ? `${gitAuthFlag()} ` : "";
1926
+ function buildRepoTemplateSpec(identity, repos, continueOnFailure) {
1927
+ const { buildEnv, workingDirectory } = identity;
1928
+ const isList = identity.repos !== void 0;
1840
1929
  let template = createDefaultMountableTemplate().template;
1841
1930
  const env = { ...buildEnv };
1842
- if (token) env[BUILD_TOKEN_ENV] = token;
1931
+ for (const repo of repos) if (repo.tokenEnv && repo.token) env[repo.tokenEnv] = repo.token;
1843
1932
  if (Object.keys(env).length > 0) template = template.setEnvs(env);
1844
1933
  if (workingDirectory) {
1845
1934
  const dir = trimTrailingSlashes(workingDirectory);
1846
1935
  template = template.runCmd(`mkdir -p "${dir}"`).setWorkdir(dir);
1847
1936
  }
1848
- template = template.runCmd(repoCloneCommand({
1849
- cloneUrl,
1850
- destination: repoDir,
1851
- ...token ? { tokenEnv: BUILD_TOKEN_ENV } : {}
1852
- }));
1853
- if (sha) template = template.runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
1854
- const setupCommands = normalizeSetupCommands(setupCommand);
1855
- for (const command of setupCommands) template = template.runCmd(`cd "${repoDir}" && ${command}`);
1856
- template = template.runCmd(setupMarkerCommand(setupMarkerContent(setupCommands)));
1937
+ const ordered = [...repos.filter((repo) => !repo.token), ...repos.filter((repo) => repo.token)];
1938
+ for (const repo of ordered) {
1939
+ const { repoDir, sha, tokenEnv } = repo;
1940
+ const cloneUrl = normalizeCloneUrl(repo.cloneUrl);
1941
+ const auth = tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : "";
1942
+ const setupCommands = normalizeSetupCommands(repo.setupCommand);
1943
+ template = template.runCmd(repoCloneCommand({
1944
+ cloneUrl,
1945
+ destination: repoDir,
1946
+ ...tokenEnv ? { tokenEnv } : {}
1947
+ }));
1948
+ if (sha) template = template.runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
1949
+ for (const command of setupCommands) template = template.runCmd(guardedSetupCommand({
1950
+ repoDir,
1951
+ command,
1952
+ continueOnFailure
1953
+ }));
1954
+ const content = setupMarkerContent(setupCommands);
1955
+ template = template.runCmd(isList ? setupMarkerCommand(content, repoSetupMarkerPath(repoDir)) : setupMarkerCommand(content));
1956
+ }
1957
+ if (isList) {
1958
+ const workspaceCommands = normalizeSetupCommands(identity.workspaceSetupCommand);
1959
+ for (const command of workspaceCommands) template = template.runCmd(command);
1960
+ template = template.runCmd(setupMarkerCommand(setupMarkerContent(workspaceCommands), WORKSPACE_SETUP_MARKER_PATH));
1961
+ }
1857
1962
  return {
1858
1963
  ref: repoTemplateRef(identity),
1859
1964
  template,