@mastra/factory 0.12.0-alpha.13 → 0.12.0-alpha.15

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.
Files changed (39) hide show
  1. package/dist/factory.d.ts +8 -0
  2. package/dist/factory.d.ts.map +1 -1
  3. package/dist/factory.js +4 -3
  4. package/dist/factory.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/integrations/github/acceptance-labels.d.ts +17 -0
  8. package/dist/integrations/github/acceptance-labels.d.ts.map +1 -0
  9. package/dist/integrations/github/acceptance-labels.js +55 -0
  10. package/dist/integrations/github/acceptance-labels.js.map +1 -0
  11. package/dist/integrations/github/sandbox.d.ts +10 -13
  12. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  13. package/dist/integrations/github/sandbox.js +74 -65
  14. package/dist/integrations/github/sandbox.js.map +1 -1
  15. package/dist/packages/_internals/workspace/dist/index.js +11 -1
  16. package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
  17. package/dist/routes/work-items.d.ts.map +1 -1
  18. package/dist/routes/work-items.js +7 -0
  19. package/dist/routes/work-items.js.map +1 -1
  20. package/dist/rules/transition-service.d.ts +12 -1
  21. package/dist/rules/transition-service.d.ts.map +1 -1
  22. package/dist/rules/transition-service.js +28 -2
  23. package/dist/rules/transition-service.js.map +1 -1
  24. package/dist/sandbox/session-sandbox.d.ts.map +1 -1
  25. package/dist/sandbox/session-sandbox.js +4 -1
  26. package/dist/sandbox/session-sandbox.js.map +1 -1
  27. package/dist/storage/domains/work-items/base.d.ts +8 -0
  28. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  29. package/dist/storage/domains/work-items/base.js +10 -1
  30. package/dist/storage/domains/work-items/base.js.map +1 -1
  31. package/dist/workspace.d.ts +8 -0
  32. package/dist/workspace.d.ts.map +1 -1
  33. package/dist/workspace.js +16 -2
  34. package/dist/workspace.js.map +1 -1
  35. package/package.json +8 -8
  36. package/dist/session/checkpoint-capture.d.ts +0 -18
  37. package/dist/session/checkpoint-capture.d.ts.map +0 -1
  38. package/dist/session/checkpoint-capture.js +0 -27
  39. package/dist/session/checkpoint-capture.js.map +0 -1
@@ -1,6 +1,23 @@
1
1
  import { timedPhase } from "../../timing.js";
2
+ import { repoCloneCommand } from "../../packages/_internals/workspace/dist/index.js";
2
3
  //#region src/integrations/github/sandbox.ts
3
4
  /**
5
+ * Repo materialization for GitHub-backed repositories.
6
+ *
7
+ * A GitHub repo is never cloned onto the server host. The repo is cloned
8
+ * *inside* the session's sandbox, so the agent's file tools and command tools
9
+ * operate entirely against the remote checkout.
10
+ *
11
+ * - `materializeRepo(row, token)` clones the repo inside the sandbox when no
12
+ * checkout exists yet (a base-image boot, a wiped disk), using a short-lived
13
+ * installation token that is scrubbed from the git remote afterwards so it
14
+ * never persists in the VM. A checkout that is already there, from a repo
15
+ * template image or an earlier start, is left exactly as it is.
16
+ *
17
+ * This module owns everything git/GitHub: clone, commit/push, setup/teardown commands,
18
+ * and `gh pr create`. Workdir layout lives in `../sandbox/workdir`.
19
+ */
20
+ /**
4
21
  * Single-quote a string for safe POSIX shell interpolation. Wraps the value in
5
22
  * single quotes and escapes any embedded single quote using the canonical
6
23
  * close-quote / escaped-quote / reopen-quote sequence (`'\''`). This is the
@@ -42,16 +59,22 @@ const SH_RETRY_DELAY_MS = 2e3;
42
59
  */
43
60
  async function sh(sandbox, script, options = {}) {
44
61
  const deadlineMs = Date.now() + (options.timeoutMs ?? 9e5);
45
- for (let attempt = 0;; attempt++) try {
46
- return await shOnce(sandbox, script, {
47
- ...options,
48
- timeoutMs: Math.max(deadlineMs - Date.now(), 1)
49
- });
50
- } catch (error) {
51
- if (attempt >= SH_RETRIES || !isTransientTransportError(error)) throw error;
52
- const delayMs = SH_RETRY_DELAY_MS * (attempt + 1);
53
- if (deadlineMs - Date.now() <= delayMs) throw error;
54
- await new Promise((resolve) => setTimeout(resolve, delayMs));
62
+ for (let attempt = 0;; attempt++) {
63
+ const started = performance.now();
64
+ try {
65
+ const result = await shOnce(sandbox, script, {
66
+ ...options,
67
+ timeoutMs: Math.max(deadlineMs - Date.now(), 1)
68
+ });
69
+ if (options.phase) process.stderr.write(`[factory:timing] ${options.phase} attempt=${attempt + 1} exit=${result.exitCode} ${Math.round(performance.now() - started)}ms\n`);
70
+ return result;
71
+ } catch (error) {
72
+ if (options.phase) process.stderr.write(`[factory:timing] ${options.phase} attempt=${attempt + 1} threw after ${Math.round(performance.now() - started)}ms: ${error instanceof Error ? error.message : String(error)}\n`);
73
+ if (attempt >= SH_RETRIES || !isTransientTransportError(error)) throw error;
74
+ const delayMs = SH_RETRY_DELAY_MS * (attempt + 1);
75
+ if (deadlineMs - Date.now() <= delayMs) throw error;
76
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
77
+ }
55
78
  }
56
79
  }
57
80
  /** Single `sh -c` execution attempt, bounded by the hang guard. */
@@ -109,6 +132,7 @@ async function gitTransfer(sandbox, script, options = {}) {
109
132
  timeoutMs: Math.max(deadlineMs - Date.now(), 1)
110
133
  });
111
134
  if (result.exitCode === 0 || attempt >= GIT_TRANSFER_RETRIES || !isTransientGitFailure(result)) return result;
135
+ process.stderr.write(`[factory:timing] git ${shOptions.phase ?? "transfer"} retrying after attempt ${attempt + 1}\n`);
112
136
  const delayMs = GIT_TRANSFER_RETRY_DELAY_MS * (attempt + 1);
113
137
  if (deadlineMs - Date.now() <= delayMs) return result;
114
138
  await new Promise((resolve) => setTimeout(resolve, delayMs));
@@ -135,9 +159,10 @@ function cleanUrl(repoFullName) {
135
159
  return `https://github.com/${repoFullName}.git`;
136
160
  }
137
161
  /**
138
- * Materialize the repo inside the user's sandbox. Clones on first open, pulls on
139
- * re-open. Always scrubs the install token from the remote afterwards and sets
140
- * `materialized_at` on the per-user sandbox binding row.
162
+ * Materialize the repo inside the user's sandbox: clone when no checkout of
163
+ * this repo exists, otherwise nothing. Scrubs the install token from the
164
+ * remote after a clone and sets `materialized_at` on the per-user sandbox
165
+ * binding row.
141
166
  */
142
167
  async function materializeRepo(options) {
143
168
  return timedPhase("workspace.materialize", () => materializeRepoImpl(options));
@@ -149,13 +174,18 @@ async function materializeRepoImpl(options) {
149
174
  if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) throw new MaterializeError(`Refusing to materialize: invalid repo full name '${repo}'.`, "clone-failed");
150
175
  if (!/^[A-Za-z0-9_./-]+$/.test(repoInfo.defaultBranch)) throw new MaterializeError(`Refusing to materialize: invalid default branch '${repoInfo.defaultBranch}'.`, "clone-failed");
151
176
  if ((await sh(sandbox, "git --version")).exitCode !== 0) throw new MaterializeError("git is not installed in the sandbox. The sandbox template must include git.", "git-missing");
152
- const authUrl = tokenUrl(repo, token);
153
- const alreadyMaterialized = await hasExistingCheckout(sandbox, workdir, repo);
154
- let tokenInRemote = false;
155
- try {
156
- if (!alreadyMaterialized) {
157
- await sh(sandbox, `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`);
158
- const clone = await gitTransfer(sandbox, `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`, {
177
+ const existing = await existingCheckoutRemote(sandbox, workdir, repo);
178
+ if (existing !== null) {
179
+ if (/\/\/[^/]*@/.test(existing)) await scrubRemote(sandbox, workdir, repo, true);
180
+ } else {
181
+ await sh(sandbox, `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`);
182
+ let tokenInRemote = false;
183
+ try {
184
+ const clone = await gitTransfer(sandbox, repoCloneCommand({
185
+ cloneUrl: tokenUrl(repo, token),
186
+ destination: workdir,
187
+ branch: repoInfo.defaultBranch
188
+ }), {
159
189
  phase: "repository clone",
160
190
  beforeRetry: async () => {
161
191
  await sh(sandbox, `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`);
@@ -166,19 +196,11 @@ async function materializeRepoImpl(options) {
166
196
  throw classifyGitFailure(clone, "clone-failed");
167
197
  }
168
198
  tokenInRemote = true;
169
- } else {
170
- const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);
171
- if (setUrl.exitCode !== 0) throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, "pull-failed");
172
- tokenInRemote = true;
173
- const pull = await gitTransfer(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, { phase: "repository pull" });
174
- if (pull.exitCode !== 0) {
175
- if (!isBenignNonFastForward(pull)) throw classifyGitFailure(pull, "pull-failed");
176
- }
199
+ } catch (primary) {
200
+ throw await scrubbedFailure(sandbox, workdir, repo, tokenInRemote, primary, "clone-failed");
177
201
  }
178
- } catch (primary) {
179
- throw await scrubbedFailure(sandbox, workdir, repo, tokenInRemote, primary, "pull-failed");
202
+ await scrubRemote(sandbox, workdir, repo, tokenInRemote);
180
203
  }
181
- await scrubRemote(sandbox, workdir, repo, tokenInRemote);
182
204
  await storage.markMaterialized({ id: sandboxRow.id });
183
205
  }
184
206
  /** Check out a session's branch inside its isolated repository clone. */
@@ -199,7 +221,7 @@ async function checkoutSessionBranchImpl(sandbox, workdir, { branch, baseBranch,
199
221
  }
200
222
  const authUrl = tokenUrl(repoFullName, token);
201
223
  try {
202
- const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);
224
+ const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`, { phase: "branch checkout remote" });
203
225
  if (setUrl.exitCode !== 0) throw classifyGitFailure(setUrl, "pull-failed");
204
226
  const fetch = await sh(sandbox, `git -C ${shellQuote(workdir)} fetch origin ${shellQuote(baseBranch)} && git -C ${shellQuote(workdir)} checkout -b ${shellQuote(branch)} FETCH_HEAD`, {
205
227
  timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS,
@@ -244,16 +266,27 @@ function isBlockedByLocalWork(result) {
244
266
  return /Your local changes to the following files would be overwritten by checkout|untracked working tree files would be overwritten by checkout/i.test(output);
245
267
  }
246
268
  /**
247
- * True when the workdir already holds a git checkout whose `origin` points at
248
- * this exact repo. Matches both the clean and token-auth URL forms; any other
249
- * remote (or no git dir at all) falls back to the clone path.
269
+ * The `origin` URL of a checkout of this exact repo in the workdir, or null
270
+ * when there is none. Matches both the clean and token-auth URL forms; any
271
+ * other remote (or no git dir at all) sends materialize down the clone path.
250
272
  */
251
- async function hasExistingCheckout(sandbox, workdir, repoFullName) {
273
+ async function existingCheckoutRemote(sandbox, workdir, repoFullName) {
252
274
  const result = await sh(sandbox, `git -C ${shellQuote(workdir)} remote get-url origin`);
253
- if (result.exitCode !== 0) return false;
254
- const url = result.stdout.trim().toLowerCase();
255
- const suffix = `github.com/${repoFullName.toLowerCase()}`;
256
- return url.endsWith(`${suffix}.git`) || url.endsWith(suffix);
275
+ if (result.exitCode !== 0) return null;
276
+ const url = result.stdout.trim();
277
+ return isRemoteForRepo(url, repoFullName) ? url : null;
278
+ }
279
+ /** True only for `https://github.com/<repo>[.git]`, with or without embedded credentials. */
280
+ function isRemoteForRepo(url, repoFullName) {
281
+ let parsed;
282
+ try {
283
+ parsed = new URL(url);
284
+ } catch {
285
+ return false;
286
+ }
287
+ if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "github.com") return false;
288
+ if (parsed.port !== "" || parsed.search !== "" || parsed.hash !== "") return false;
289
+ return parsed.pathname.replace(/\.git$/, "").toLowerCase() === `/${repoFullName.toLowerCase()}`;
257
290
  }
258
291
  /** Probed without `git -C` so a missing workdir returns false instead of throwing. */
259
292
  async function hasGitDir(sandbox, workdir) {
@@ -302,30 +335,6 @@ async function scrubbedFailure(sandbox, workdir, repoFullName, tokenInRemote, pr
302
335
  }
303
336
  }
304
337
  /**
305
- * True when a failed `git pull --ff-only` on re-open just means the current
306
- * branch can't be fast-forwarded — not that anything is broken. The shared
307
- * workdir is routinely left on a session's working branch, which may have
308
- * local commits diverging from upstream, no upstream at all (session branches
309
- * are created from `FETCH_HEAD`), or a detached HEAD. A checkout can also
310
- * hold uncommitted or untracked files (a build or script run left residue,
311
- * or an older session worked directly in the shared checkout), which makes
312
- * git refuse the merge outright. A checkout carrying `pull.rebase` in its git
313
- * config refuses for the same reason but says so in rebase's words instead of
314
- * merge's. After a PR merges with branch auto-delete, the configured upstream
315
- * ref may also be gone (`no such ref was fetched` / `couldn't find remote ref`);
316
- * there is nothing to pull and the checkout is still intact. In all of these
317
- * cases materialization must keep it as-is rather than fail the workspace open
318
- * — and must never discard the local state to force the pull through.
319
- */
320
- function isDeletedUpstreamRef(result) {
321
- const output = `${result.stderr || ""}\n${result.stdout || ""}`;
322
- return /no such ref was fetched|couldn't find remote ref/i.test(output);
323
- }
324
- function isBenignNonFastForward(result) {
325
- const output = `${result.stderr || ""}\n${result.stdout || ""}`;
326
- return isDeletedUpstreamRef(result) || /Not possible to fast-forward|Diverging branches can't be fast-forwarded|no tracking information for the current branch|You are not currently on a branch|Your local changes to the following files would be overwritten by merge|untracked working tree files would be overwritten by merge|cannot pull with rebase|cannot rebase: You have unstaged changes|Your index contains uncommitted changes/i.test(output);
327
- }
328
- /**
329
338
  * Turn a failed git command into an actionable error, detecting the common
330
339
  * "cannot reach github.com" egress failure.
331
340
  */
@@ -524,6 +533,6 @@ async function createPullRequest(sandbox, workdir, { token, base, head, title, b
524
533
  return { url };
525
534
  }
526
535
  //#endregion
527
- export { CHECKOUT_COMMAND_TIMEOUT_MS, DEFAULT_COMMAND_TIMEOUT_MS, MaterializeError, SetupCommandError, checkoutSessionBranch, commitAll, configureGitIdentity, createPullRequest, hasExistingCheckout, isValidGitRef, materializeRepo, pushBranch, resolveGitIdentity, runSetupCommand, runTeardownCommand, sh, shellQuote, withInstallToken };
536
+ export { CHECKOUT_COMMAND_TIMEOUT_MS, DEFAULT_COMMAND_TIMEOUT_MS, MaterializeError, SetupCommandError, checkoutSessionBranch, commitAll, configureGitIdentity, createPullRequest, isValidGitRef, materializeRepo, pushBranch, resolveGitIdentity, runSetupCommand, runTeardownCommand, sh, shellQuote, withInstallToken };
528
537
 
529
538
  //# sourceMappingURL=sandbox.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.js","names":[],"sources":["../../../src/integrations/github/sandbox.ts"],"sourcesContent":["/**\n * Repo materialization for GitHub-backed repositories.\n *\n * A GitHub repo is never cloned onto the server host. The repo is cloned\n * *inside* the session's sandbox, so the agent's file tools and command tools\n * operate entirely against the remote checkout.\n *\n * - `materializeRepo(row, token)` runs `git clone` (first open) or `git pull`\n * (re-open) inside the sandbox, using a short-lived installation token that is\n * scrubbed from the git remote afterwards so it never persists in the VM.\n *\n * This module owns everything git/GitHub: clone/pull, commit/push, setup/teardown commands,\n * and `gh pr create`. Workdir layout lives in `../sandbox/workdir`.\n */\n\nimport type { ExecutableSandbox, SandboxCommandResult } from '../../sandbox/materialization.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport { timedPhase } from '../../timing.js';\n\ntype MaterializationStore = Pick<SourceControlStorageHandle['sessions'], 'markMaterialized'>;\n\ninterface RepoMaterializationBinding {\n id: string;\n sandboxWorkdir: string;\n materializedAt: Date | null;\n}\n\n/**\n * Single-quote a string for safe POSIX shell interpolation. Wraps the value in\n * single quotes and escapes any embedded single quote using the canonical\n * close-quote / escaped-quote / reopen-quote sequence (`'\\''`). This is the\n * standard POSIX-safe construction and prevents the quoted string from being\n * terminated early.\n */\nexport function shellQuote(value: string): string {\n // Replace each ' with the four-character sequence: ' \\ ' '\n return `'` + value.split(`'`).join(`'\\\\''`) + `'`;\n}\n\n/**\n * Default hang guard for sandbox shell commands. Generous by design — large\n * clones and dependency installs legitimately take minutes; the guard exists\n * so a wedged sandbox surfaces a failure instead of hanging the request that\n * triggered materialization forever.\n */\nexport const DEFAULT_COMMAND_TIMEOUT_MS = 15 * 60_000;\n/** Branch checkout only fetches one ref — a much tighter budget applies. */\nexport const CHECKOUT_COMMAND_TIMEOUT_MS = 5 * 60_000;\n\ninterface ShOptions {\n /** Override the hang-guard budget for this command. */\n timeoutMs?: number;\n /** Human-readable phase name included in the timeout error. */\n phase?: string;\n}\n\n/**\n * A thrown transport-level failure that is worth retrying: remote sandbox\n * providers (e.g. the platform workspace proxy) surface transient 5xx errors\n * as exceptions carrying an HTTP `status` — typically while a freshly\n * provisioned VM is still coming up. Command failures are NOT exceptions\n * (they resolve with a non-zero exit code), so retrying here never re-runs a\n * command that the sandbox already executed and rejected.\n */\nfunction isTransientTransportError(error: unknown): boolean {\n const status = (error as { status?: unknown })?.status;\n return typeof status === 'number' && status >= 500;\n}\n\nconst SH_RETRIES = 2;\nconst SH_RETRY_DELAY_MS = 2000;\n\n/**\n * Run a shell script in the sandbox via `sh -c`, bounded by a hang guard.\n * Transient transport-level 5xx failures (proxy hiccups while the VM boots)\n * are retried with a short backoff; every script routed through here is safe\n * to re-run. Hang-guard timeouts are NOT retried — the budget applies to the\n * command as a whole.\n */\nexport async function sh(\n sandbox: ExecutableSandbox,\n script: string,\n options: ShOptions = {},\n): Promise<SandboxCommandResult> {\n // One budget for the command as a whole: each attempt only gets the time\n // remaining, so transport retries can never multiply the hang guard.\n const deadlineMs = Date.now() + (options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);\n for (let attempt = 0; ; attempt++) {\n try {\n return await shOnce(sandbox, script, { ...options, timeoutMs: Math.max(deadlineMs - Date.now(), 1) });\n } catch (error) {\n if (attempt >= SH_RETRIES || !isTransientTransportError(error)) throw error;\n const delayMs = SH_RETRY_DELAY_MS * (attempt + 1);\n if (deadlineMs - Date.now() <= delayMs) throw error;\n await new Promise(resolve => setTimeout(resolve, delayMs));\n }\n }\n}\n\n/** Single `sh -c` execution attempt, bounded by the hang guard. */\nasync function shOnce(\n sandbox: ExecutableSandbox,\n script: string,\n options: ShOptions,\n): Promise<SandboxCommandResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const hangGuard = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n const phase = options.phase ? ` during ${options.phase}` : '';\n reject(new Error(`Sandbox command timed out after ${Math.round(timeoutMs / 1000)}s${phase}.`));\n }, timeoutMs);\n timer.unref?.();\n });\n try {\n // Forward the budget to the provider too so it can terminate the wedged\n // process; the race stays as the outer guard for providers that ignore it.\n return await Promise.race([sandbox.executeCommand('sh', ['-c', script], { timeout: timeoutMs }), hangGuard]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nconst GIT_TRANSFER_RETRIES = 2;\nconst GIT_TRANSFER_RETRY_DELAY_MS = 2000;\n\n/**\n * True when a git transfer died mid-flight rather than being refused.\n *\n * `sh` already retries transport errors the sandbox provider *throws*, but a\n * git command that reaches the network and then loses it exits non-zero\n * instead — so a single HTTP/2 framing glitch or dropped connection to\n * github.com would otherwise permanently fail opening a workspace. These\n * patterns all mean \"the bytes stopped arriving\", which says nothing about\n * whether the operation would succeed if attempted again.\n *\n * Deliberately narrow: a refusal (bad credentials, missing repo, blocked\n * egress) is terminal and must surface immediately rather than be retried into\n * a slow failure.\n */\nfunction isTransientGitFailure(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /HTTP2 framing layer|RPC failed; curl|RPC failed; HTTP 5\\d\\d|the remote end hung up unexpectedly|early EOF|unexpected disconnect|connection reset by peer|Recv failure|Send failure|GnuTLS recv error|TLS connection was non-properly terminated|502 Bad Gateway|503 Service Unavailable/i.test(\n output,\n );\n}\n\n/**\n * Run a git command that only *reads* from the remote, retrying it when the\n * transfer dies mid-flight. Restricted to read-only transfers on purpose:\n * re-running a clone or a fetch is free, whereas re-running a push could\n * duplicate work already accepted by the remote before the connection dropped.\n *\n * `beforeRetry` lets a call site clear whatever the aborted attempt left\n * behind — a half-written clone directory blocks the next `git clone` outright.\n */\nasync function gitTransfer(\n sandbox: ExecutableSandbox,\n script: string,\n options: ShOptions & { beforeRetry?: (attempt: number) => Promise<void> } = {},\n): Promise<SandboxCommandResult> {\n const { beforeRetry, ...shOptions } = options;\n const deadlineMs = Date.now() + (shOptions.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);\n for (let attempt = 0; ; attempt++) {\n const result = await sh(sandbox, script, {\n ...shOptions,\n timeoutMs: Math.max(deadlineMs - Date.now(), 1),\n });\n if (result.exitCode === 0 || attempt >= GIT_TRANSFER_RETRIES || !isTransientGitFailure(result)) return result;\n const delayMs = GIT_TRANSFER_RETRY_DELAY_MS * (attempt + 1);\n if (deadlineMs - Date.now() <= delayMs) return result;\n await new Promise(resolve => setTimeout(resolve, delayMs));\n await beforeRetry?.(attempt + 1);\n }\n}\n\n/** Error raised when the sandbox cannot materialize the repo (actionable). */\nexport class MaterializeError extends Error {\n constructor(\n message: string,\n readonly code:\n | 'git-missing'\n | 'egress-blocked'\n | 'clone-failed'\n | 'pull-failed'\n | 'push-failed'\n | 'commit-failed'\n | 'gh-missing'\n | 'pr-failed',\n ) {\n super(message);\n this.name = 'MaterializeError';\n }\n}\n\n/**\n * Build the token-auth clone/pull URL for a repo. The token lives only inside\n * this URL and is scrubbed from the remote after the operation.\n */\nfunction tokenUrl(repoFullName: string, token: string): string {\n return `https://x-access-token:${token}@github.com/${repoFullName}.git`;\n}\n\nfunction cleanUrl(repoFullName: string): string {\n return `https://github.com/${repoFullName}.git`;\n}\n\n/** Repo metadata needed to materialize, read from the org-owned project row. */\nexport interface RepoMaterializeInfo {\n repoFullName: string;\n defaultBranch: string;\n}\n\n/** Options for {@link materializeRepo}. */\nexport interface MaterializeRepoOptions {\n /** The per-(project,user) sandbox binding whose workdir this materializes into. */\n row: RepoMaterializationBinding;\n /** Repo metadata from the org-owned project row. */\n repoInfo: RepoMaterializeInfo;\n /** The live sandbox to run git inside. */\n sandbox: ExecutableSandbox;\n /** A freshly minted, short-lived installation access token. */\n token: string;\n storage: MaterializationStore;\n}\n\n/**\n * Materialize the repo inside the user's sandbox. Clones on first open, pulls on\n * re-open. Always scrubs the install token from the remote afterwards and sets\n * `materialized_at` on the per-user sandbox binding row.\n */\nexport async function materializeRepo(options: MaterializeRepoOptions): Promise<void> {\n return timedPhase('workspace.materialize', () => materializeRepoImpl(options));\n}\n\nasync function materializeRepoImpl(options: MaterializeRepoOptions): Promise<void> {\n const { row: sandboxRow, repoInfo, sandbox, token, storage } = options;\n const workdir = sandboxRow.sandboxWorkdir;\n const repo = repoInfo.repoFullName;\n\n // 0. Defense in depth: never build a git command from values that aren't\n // strictly shaped, even if a malformed row reached the DB. Inputs are also\n // validated at the route boundary before storage.\n if (!/^[\\w.-]+\\/[\\w.-]+$/.test(repo)) {\n throw new MaterializeError(`Refusing to materialize: invalid repo full name '${repo}'.`, 'clone-failed');\n }\n if (!/^[A-Za-z0-9_./-]+$/.test(repoInfo.defaultBranch)) {\n throw new MaterializeError(\n `Refusing to materialize: invalid default branch '${repoInfo.defaultBranch}'.`,\n 'clone-failed',\n );\n }\n\n // 1. Preflight: git must be installed in the sandbox template.\n const gitVersion = await sh(sandbox, 'git --version');\n if (gitVersion.exitCode !== 0) {\n throw new MaterializeError(\n 'git is not installed in the sandbox. The sandbox template must include git.',\n 'git-missing',\n );\n }\n\n const authUrl = tokenUrl(repo, token);\n\n // The DB's `materializedAt` can drift from disk in both directions: a fresh\n // binding row over an already-populated workdir (local dev DB resets,\n // repaired rows, earlier flows) must pull instead of failing `git clone` on\n // the non-empty directory, and a stale `materializedAt` over an empty\n // sandbox (an expired/recreated VM whose disk was wiped) must re-clone\n // instead of running `git -C <workdir>` against a directory that no longer\n // exists. Disk is the source of truth: detect the checkout instead of\n // trusting the row.\n const alreadyMaterialized = await hasExistingCheckout(sandbox, workdir, repo);\n\n let tokenInRemote = false;\n try {\n if (!alreadyMaterialized) {\n // 2a. First open: shallow-clone the default branch into the workdir. A\n // shallow single-branch clone is dramatically faster for large repos; the\n // later re-open uses `git pull --ff-only`, which works on shallow clones.\n // The workdir holds no usable checkout of this repo, but it may not be\n // empty: a checkpoint seed or a clone that died partway (a crashed or\n // OOM-killed server) leaves a partial tree behind, and `git clone`\n // refuses a non-empty destination with a non-retryable fatal. Nothing\n // here is recoverable — `hasExistingCheckout` already ruled out a\n // checkout of this repo — so clear its contents before cloning, exactly\n // as the retry path does. Keep the workdir itself because LocalSandbox\n // runs commands with this directory as the child process cwd.\n await sh(\n sandbox,\n `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,\n );\n const clone = await gitTransfer(\n sandbox,\n `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`,\n {\n phase: 'repository clone',\n beforeRetry: async () => {\n // A clone that died partway leaves the destination non-empty, which\n // git refuses to clone into. Clear its contents so the retry starts\n // clean without removing LocalSandbox's process cwd.\n await sh(\n sandbox,\n `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,\n );\n },\n },\n );\n if (clone.exitCode !== 0) {\n // git can fail after creating the checkout (\"Clone succeeded, but\n // checkout failed\") with the tokenized origin persisted — probe the\n // disk instead of assuming the failed clone left nothing behind.\n tokenInRemote = await hasGitDir(sandbox, workdir);\n throw classifyGitFailure(clone, 'clone-failed');\n }\n tokenInRemote = true;\n } else {\n // 2b. Re-open: refresh remote to the token URL and fast-forward pull.\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, 'pull-failed');\n }\n tokenInRemote = true;\n const pull = await gitTransfer(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, {\n phase: 'repository pull',\n });\n if (pull.exitCode !== 0) {\n if (!isBenignNonFastForward(pull)) {\n throw classifyGitFailure(pull, 'pull-failed');\n }\n // The workdir was left on a session's working branch that can't be\n // fast-forwarded (diverged from upstream, no upstream, or detached\n // HEAD), or its configured upstream ref was deleted after merge.\n // That checkout still holds usable work — never rebase or reset it\n // here. Leave it as-is and let the session reconcile with the remote\n // itself.\n }\n }\n } catch (primary) {\n // 3a. The clone/pull failed — still scrub the token from the VM's git\n // config. The scrub must never hide the actionable failure, but once the\n // token reached the remote its own failure can't stay silent either:\n // report both, primary cause and classification first.\n throw await scrubbedFailure(sandbox, workdir, repo, tokenInRemote, primary, 'pull-failed');\n }\n\n // 3b. Success — the token is in the remote and the workdir has a `.git`, so\n // a failed scrub means the token may still be persisted: surface it.\n await scrubRemote(sandbox, workdir, repo, tokenInRemote);\n\n // 4. Mark materialized.\n await storage.markMaterialized({ id: sandboxRow.id });\n}\n\n/** Check out a session's branch inside its isolated repository clone. */\nexport async function checkoutSessionBranch(\n sandbox: ExecutableSandbox,\n workdir: string,\n options: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<void> {\n return timedPhase('workspace.checkout', () => checkoutSessionBranchImpl(sandbox, workdir, options));\n}\n\nasync function checkoutSessionBranchImpl(\n sandbox: ExecutableSandbox,\n workdir: string,\n {\n branch,\n baseBranch,\n token,\n repoFullName,\n }: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<void> {\n if (!isValidGitRef(branch) || !isValidGitRef(baseBranch)) {\n throw new MaterializeError('Refusing to create a session from an invalid branch name.', 'clone-failed');\n }\n\n const current = await sh(sandbox, `git -C ${shellQuote(workdir)} branch --show-current`);\n if (current.exitCode === 0 && current.stdout.trim() === branch) return;\n\n const local = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} show-ref --verify --quiet refs/heads/${shellQuote(branch)}`,\n );\n if (local.exitCode === 0) {\n const checkout = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout ${shellQuote(branch)}`);\n if (checkout.exitCode !== 0) {\n // The session's agent may have switched branches itself (e.g. `gh pr\n // checkout`) and left uncommitted work in the tree. Git refuses to\n // switch back over those files — that work must win. The checkout is\n // intact and usable on its current branch; keep it as-is rather than\n // fail the workspace open, and never reset or stash to force the\n // switch through.\n if (isBlockedByLocalWork(checkout)) return;\n throw classifyGitFailure(checkout, 'clone-failed');\n }\n return;\n }\n\n const authUrl = tokenUrl(repoFullName, token);\n try {\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) throw classifyGitFailure(setUrl, 'pull-failed');\n const fetch = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} fetch origin ${shellQuote(baseBranch)} && git -C ${shellQuote(workdir)} checkout -b ${shellQuote(branch)} FETCH_HEAD`,\n { timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS, phase: 'branch checkout' },\n );\n if (fetch.exitCode !== 0) {\n // Same rule as above: uncommitted work in the tree blocks the switch\n // to the new branch. Leave the checkout on its current branch.\n if (isBlockedByLocalWork(fetch)) return;\n if (!isBranchCollision(fetch)) throw classifyGitFailure(fetch, 'clone-failed');\n // The branch exists even though the show-ref probe missed it: either a\n // concurrent materialization of this session created it between the\n // probe and `checkout -b` (adopt it), or a reused sandbox carries a\n // broken loose ref the probe cannot resolve (replace it and retry —\n // \"already exists\" means the fetch half succeeded, so FETCH_HEAD is\n // set).\n const adopt = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout ${shellQuote(branch)}`);\n if (adopt.exitCode === 0 || isBlockedByLocalWork(adopt)) return;\n const drop = await sh(\n sandbox,\n // `--no-deref` so a broken symref is deleted itself instead of git\n // following it to some other branch. `update-ref -d` can still refuse\n // a broken ref; fall back to removing the loose ref file (branch\n // passed isValidGitRef, so the interpolation inside the double quotes\n // is inert).\n `git -C ${shellQuote(workdir)} update-ref --no-deref -d refs/heads/${shellQuote(branch)} || rm -f -- \"$(git -C ${shellQuote(workdir)} rev-parse --absolute-git-dir)/refs/heads/${branch}\"`,\n );\n if (drop.exitCode !== 0) throw classifyGitFailure(fetch, 'clone-failed');\n const retry = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout -b ${shellQuote(branch)} FETCH_HEAD`, {\n timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS,\n phase: 'branch checkout retry',\n });\n if (retry.exitCode !== 0) {\n if (isBlockedByLocalWork(retry)) return;\n throw classifyGitFailure(retry, 'clone-failed');\n }\n }\n } finally {\n await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`);\n }\n}\n\n/**\n * True when `git checkout -b` failed only because the branch ref already\n * exists — the collision Factory hits when a pooled sandbox carries a ref the\n * show-ref probe could not see (broken loose ref) or a concurrent\n * materialization created the branch after the probe ran.\n */\nfunction isBranchCollision(result: SandboxCommandResult): boolean {\n return /a branch named .* already exists/i.test(`${result.stderr || ''}\\n${result.stdout || ''}`);\n}\n\n/**\n * True when a failed `git checkout` just means uncommitted or untracked files\n * in the working tree would be clobbered by the branch switch. Those files are\n * a session's work in progress — the switch must yield to them, never the\n * other way around.\n */\nfunction isBlockedByLocalWork(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /Your local changes to the following files would be overwritten by checkout|untracked working tree files would be overwritten by checkout/i.test(\n output,\n );\n}\n\n/**\n * True when the workdir already holds a git checkout whose `origin` points at\n * this exact repo. Matches both the clean and token-auth URL forms; any other\n * remote (or no git dir at all) falls back to the clone path.\n */\nexport async function hasExistingCheckout(\n sandbox: ExecutableSandbox,\n workdir: string,\n repoFullName: string,\n): Promise<boolean> {\n const result = await sh(sandbox, `git -C ${shellQuote(workdir)} remote get-url origin`);\n if (result.exitCode !== 0) return false;\n const url = result.stdout.trim().toLowerCase();\n const suffix = `github.com/${repoFullName.toLowerCase()}`;\n return url.endsWith(`${suffix}.git`) || url.endsWith(suffix);\n}\n\n/** Probed without `git -C` so a missing workdir returns false instead of throwing. */\nasync function hasGitDir(sandbox: ExecutableSandbox, workdir: string): Promise<boolean> {\n const probe = await sh(sandbox, `test -d ${shellQuote(`${workdir}/.git`)}`).catch(() => null);\n return probe?.exitCode === 0;\n}\n\n/**\n * Reset the git remote back to the tokenless URL. Strict when the token\n * reached the remote: any failure — a non-zero exit or a provider throw —\n * means the token may still be persisted, so it is thrown for the caller to\n * surface. Best-effort when it never did: the workdir may not exist (e.g. a\n * failed clone), which makes providers that spawn with `cwd` throw rather\n * than return a non-zero exit code; both outcomes are tolerated so neither\n * masks the primary failure.\n */\nasync function scrubRemote(\n sandbox: ExecutableSandbox,\n workdir: string,\n repoFullName: string,\n tokenInRemote: boolean,\n): Promise<void> {\n const scrub = `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`;\n if (!tokenInRemote) {\n await sh(sandbox, scrub).catch(() => undefined);\n return;\n }\n let failure: string;\n try {\n const result = await sh(sandbox, scrub);\n if (result.exitCode === 0) return;\n failure = result.stderr.trim() || result.stdout.trim();\n } catch (error) {\n failure = error instanceof Error ? error.message : String(error);\n }\n throw new MaterializeError(`Failed to scrub installation token from git remote: ${failure}`, 'pull-failed');\n}\n\n/**\n * Scrub after `primary` already failed and return the error to throw. A failed\n * scrub is appended to `primary` rather than replacing it, so the caller gets\n * the error it would have had without the scrub — same class, same `code` —\n * carrying the leaked-token warning in its message.\n */\nasync function scrubbedFailure(\n sandbox: ExecutableSandbox,\n workdir: string,\n repoFullName: string,\n tokenInRemote: boolean,\n primary: unknown,\n fallback: MaterializeError['code'],\n): Promise<unknown> {\n try {\n await scrubRemote(sandbox, workdir, repoFullName, tokenInRemote);\n return primary;\n } catch (scrubError) {\n const scrubMessage = scrubError instanceof Error ? scrubError.message : String(scrubError);\n if (!(primary instanceof Error)) {\n return new MaterializeError(`${String(primary)} — additionally: ${scrubMessage}`, fallback);\n }\n primary.message = `${primary.message} — additionally: ${scrubMessage}`;\n return primary;\n }\n}\n\n/**\n * True when a failed `git pull --ff-only` on re-open just means the current\n * branch can't be fast-forwarded — not that anything is broken. The shared\n * workdir is routinely left on a session's working branch, which may have\n * local commits diverging from upstream, no upstream at all (session branches\n * are created from `FETCH_HEAD`), or a detached HEAD. A checkout can also\n * hold uncommitted or untracked files (a build or script run left residue,\n * or an older session worked directly in the shared checkout), which makes\n * git refuse the merge outright. A checkout carrying `pull.rebase` in its git\n * config refuses for the same reason but says so in rebase's words instead of\n * merge's. After a PR merges with branch auto-delete, the configured upstream\n * ref may also be gone (`no such ref was fetched` / `couldn't find remote ref`);\n * there is nothing to pull and the checkout is still intact. In all of these\n * cases materialization must keep it as-is rather than fail the workspace open\n * — and must never discard the local state to force the pull through.\n */\nfunction isDeletedUpstreamRef(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /no such ref was fetched|couldn't find remote ref/i.test(output);\n}\n\nfunction isBenignNonFastForward(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return (\n isDeletedUpstreamRef(result) ||\n /Not possible to fast-forward|Diverging branches can't be fast-forwarded|no tracking information for the current branch|You are not currently on a branch|Your local changes to the following files would be overwritten by merge|untracked working tree files would be overwritten by merge|cannot pull with rebase|cannot rebase: You have unstaged changes|Your index contains uncommitted changes/i.test(\n output,\n )\n );\n}\n\n/**\n * Turn a failed git command into an actionable error, detecting the common\n * \"cannot reach github.com\" egress failure.\n */\nfunction classifyGitFailure(\n result: SandboxCommandResult,\n fallback: 'clone-failed' | 'pull-failed' | 'push-failed',\n): MaterializeError {\n const stderr = result.stderr || '';\n if (/could not resolve host|failed to connect|network is unreachable|Connection timed out/i.test(stderr)) {\n return new MaterializeError(\n 'The sandbox could not reach github.com. The sandbox network must allow outbound egress to github.com.',\n 'egress-blocked',\n );\n }\n const verb = fallback === 'clone-failed' ? 'clone' : fallback === 'pull-failed' ? 'pull' : 'push';\n return new MaterializeError(`git ${verb} failed: ${stderr}`, fallback);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 1 — git identity + token-scoped push primitive\n//\n// These helpers let the sandbox author and push commits safely. The install\n// token is short-lived, minted per-operation server-side, injected only into\n// the temporary remote URL inside the sandbox, and always scrubbed afterwards\n// so it never persists in `.git/config`.\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a git ref (branch) name. Server-side defense-in-depth: only allow a\n * conservative character set so a branch can never be built into a shell\n * command in a way that escapes quoting. Mirrors the route-layer check.\n */\nexport function isValidGitRef(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= 255 &&\n // Reject leading-dash refs (e.g. `--mirror`) so the value can never be\n // parsed as a git option when interpolated into a command.\n !value.startsWith('-') &&\n /^[A-Za-z0-9_./-]+$/.test(value)\n );\n}\n\n/** Identity used to author commits inside the sandbox. */\nexport interface GitIdentity {\n name?: string | null;\n email?: string | null;\n /** GitHub login, used to derive a stable noreply identity when name/email are absent. */\n login?: string | null;\n}\n\n/**\n * Resolve a concrete `{ name, email }` for git authorship from a possibly-sparse\n * identity. Falls back to a GitHub-style noreply identity so commits are never\n * authored with an empty or host-derived identity.\n */\nexport function resolveGitIdentity(identity: GitIdentity): { name: string; email: string } {\n const login = (identity.login || '').trim();\n const name = (identity.name || '').trim() || login || 'Mastra Code';\n const email =\n (identity.email || '').trim() ||\n (login ? `${login}@users.noreply.github.com` : 'mastra-code@users.noreply.github.com');\n return { name, email };\n}\n\n/**\n * Configure `user.name` / `user.email` for the given repo working tree inside\n * the sandbox so commits are authored correctly. Values are shell-quoted.\n */\nexport async function configureGitIdentity(\n sandbox: ExecutableSandbox,\n workdir: string,\n identity: GitIdentity,\n): Promise<void> {\n const { name, email } = resolveGitIdentity(identity);\n const setName = await sh(sandbox, `git -C ${shellQuote(workdir)} config user.name ${shellQuote(name)}`);\n if (setName.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git user.name: ${setName.stderr.trim()}`, 'commit-failed');\n }\n const setEmail = await sh(sandbox, `git -C ${shellQuote(workdir)} config user.email ${shellQuote(email)}`);\n if (setEmail.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git user.email: ${setEmail.stderr.trim()}`, 'commit-failed');\n }\n}\n\n/**\n * Temporarily rewrite `origin` to a tokenized URL, run `fn` (e.g. a push), and\n * **always** scrub the remote back to the tokenless URL afterwards. The token\n * therefore only ever lives in the remote URL for the duration of the\n * operation and is never left in the VM's git config.\n *\n * Once the tokenized URL is installed a failed scrub may leave the token\n * persisted, so it is always surfaced: on its own after a successful `fn`,\n * appended to `fn`'s own error otherwise — `fn`'s error is never replaced.\n * Only a failed set-url (the token never reached the remote) downgrades the\n * scrub to best-effort.\n */\nexport async function withInstallToken<T>(\n sandbox: ExecutableSandbox,\n workdir: string,\n repoFullName: string,\n token: string,\n fn: () => Promise<T>,\n): Promise<T> {\n if (!/^[\\w.-]+\\/[\\w.-]+$/.test(repoFullName)) {\n throw new MaterializeError(`Refusing to push: invalid repo full name '${repoFullName}'.`, 'push-failed');\n }\n\n const setUrl = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(tokenUrl(repoFullName, token))}`,\n );\n if (setUrl.exitCode !== 0) {\n // Best-effort scrub even though set-url failed, then surface the failure.\n await scrubRemote(sandbox, workdir, repoFullName, false);\n throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr.trim()}`, 'push-failed');\n }\n\n let result: T;\n try {\n result = await fn();\n } catch (primary) {\n throw await scrubbedFailure(sandbox, workdir, repoFullName, true, primary, 'push-failed');\n }\n // Restore the tokenless remote. The workdir has a `.git` (we just rewrote\n // its remote) so a scrub failure means the token may still persist — surface it.\n await scrubRemote(sandbox, workdir, repoFullName, true);\n return result;\n}\n\n/**\n * Push a branch back to GitHub from inside the sandbox using a short-lived\n * installation token. The branch is ref-validated, the token is injected only\n * into the remote URL via `withInstallToken`, and egress failures are\n * classified into actionable errors.\n */\nexport async function pushBranch(\n sandbox: ExecutableSandbox,\n workdir: string,\n branch: string,\n token: string,\n repoFullName: string,\n): Promise<void> {\n if (!isValidGitRef(branch)) {\n throw new MaterializeError(`Refusing to push: invalid branch name '${branch}'.`, 'push-failed');\n }\n\n await withInstallToken(sandbox, workdir, repoFullName, token, async () => {\n const push = await sh(sandbox, `git -C ${shellQuote(workdir)} push -u origin ${shellQuote(branch)}`);\n if (push.exitCode !== 0) {\n throw classifyGitFailure(push, 'push-failed');\n }\n });\n}\n\nexport interface CommitResult {\n /** True when a commit was created; false when there was nothing to commit. */\n committed: boolean;\n}\n\n/**\n * Stage every change in the working tree and create a commit inside the\n * sandbox. The git identity is configured first so authorship is correct. When\n * there is nothing to commit this is a no-op (`committed: false`) rather than an\n * error, so callers can safely commit-then-push without first diffing.\n *\n * @param sandbox the live sandbox containing the checkout\n * @param workdir the session workdir to commit in\n * @param message the commit message (quoted; arbitrary text is safe)\n * @param identity authorship identity for the commit\n */\nexport async function commitAll(\n sandbox: ExecutableSandbox,\n workdir: string,\n message: string,\n identity: GitIdentity,\n): Promise<CommitResult> {\n await configureGitIdentity(sandbox, workdir, identity);\n\n const add = await sh(sandbox, `git -C ${shellQuote(workdir)} add -A`);\n if (add.exitCode !== 0) {\n throw new MaterializeError(`git add failed: ${add.stderr.trim() || add.stdout.trim()}`, 'commit-failed');\n }\n\n // Nothing staged → nothing to commit. `git diff --cached --quiet` exits 1 when\n // there are staged changes, 0 when the index is clean.\n const staged = await sh(sandbox, `git -C ${shellQuote(workdir)} diff --cached --quiet`);\n if (staged.exitCode === 0) {\n return { committed: false };\n }\n\n const commit = await sh(sandbox, `git -C ${shellQuote(workdir)} commit -m ${shellQuote(message)}`);\n if (commit.exitCode !== 0) {\n throw new MaterializeError(`git commit failed: ${commit.stderr.trim() || commit.stdout.trim()}`, 'commit-failed');\n }\n\n return { committed: true };\n}\n\n// ---------------------------------------------------------------------------\n// Phase 2 — setup / teardown lifecycle commands\n//\n// The org-configured setup and teardown shell commands run in the session's\n// materialized workdir. The workdir is always resolved server-side from the\n// live sandbox; client input never reaches a filesystem path.\n// ---------------------------------------------------------------------------\n\n/** Error raised when the org's setup or teardown command fails in the sandbox. */\nexport class SetupCommandError extends Error {\n constructor(\n message: string,\n readonly code: 'setup-failed' | 'teardown-failed',\n ) {\n super(message);\n this.name = 'SetupCommandError';\n }\n}\n\n/**\n * Run the project's setup command (e.g. `pnpm i && pnpm build`) inside the\n * freshly materialized session workdir. Called before the checkout is handed\n * to any agent run so it is ready to build/test. A non-zero exit is a hard\n * error — starting agent work in a half-set-up tree is worse than failing the\n * request.\n *\n * Security model: the command is intentionally arbitrary shell — that is the\n * feature (install deps, build, seed fixtures). It is only configurable by\n * authenticated org members (the settings route is gated by\n * `resolveOrgTenant` + org-scoped project lookup, with length and\n * control-character validation), and it executes exclusively inside the\n * project's isolated sandbox — the same environment where org members already\n * run arbitrary shell via the agent's command tool. It never runs on the web\n * server host, so it grants no privilege beyond what sandbox access already\n * provides.\n *\n * @param sandbox live sandbox containing the checkout\n * @param workdir the server-resolved session workdir the command runs in\n * @param command the org-configured setup shell command\n */\nasync function runLifecycleCommand(\n sandbox: ExecutableSandbox,\n workdir: string,\n command: string,\n options: { phase: 'setup' | 'teardown'; timeoutMs?: number },\n): Promise<void> {\n const result = await sh(sandbox, `cd ${shellQuote(workdir)} && { ${command}\\n}`, {\n phase: `${options.phase} command`,\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n });\n if (result.exitCode !== 0) {\n const detail = (result.stderr.trim() || result.stdout.trim()).slice(-1800);\n const label = options.phase === 'setup' ? 'Setup' : 'Teardown';\n throw new SetupCommandError(\n `${label} command failed (exit ${result.exitCode}): ${detail}`,\n options.phase === 'setup' ? 'setup-failed' : 'teardown-failed',\n );\n }\n}\n\nexport async function runSetupCommand(\n sandbox: ExecutableSandbox,\n workdir: string,\n command: string,\n): Promise<void> {\n return runLifecycleCommand(sandbox, workdir, command, { phase: 'setup' });\n}\n\n/**\n * Run the repository's best-effort teardown command from the materialized\n * session workdir. Callers own lifecycle policy: this helper reports failures\n * so the retirement coordinator can log them while still continuing with\n * scrub, pooling/destruction, cache invalidation, and row deletion.\n */\nexport async function runTeardownCommand(\n sandbox: ExecutableSandbox,\n workdir: string,\n command: string,\n options: { timeoutMs?: number } = {},\n): Promise<void> {\n return runLifecycleCommand(sandbox, workdir, command, {\n phase: 'teardown',\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n });\n}\n\nexport interface CreatePullRequestArgs {\n /** Short-lived installation token, injected only into the `gh` process env. */\n token: string;\n /** Base branch the PR merges into. Ref-validated. */\n base: string;\n /** Head branch the PR is opened from. Ref-validated. */\n head: string;\n /** PR title. */\n title: string;\n /** PR body (optional). */\n body?: string;\n}\n\nexport interface CreatePullRequestResult {\n /** The PR URL parsed from `gh pr create` stdout. */\n url: string;\n}\n\n/**\n * Preflight that `gh` is installed in the sandbox. Only called on the PR path so\n * a missing `gh` never blocks clone/open. Surfaces an actionable error naming\n * the sandbox template requirement.\n */\nasync function assertGhAvailable(sandbox: ExecutableSandbox): Promise<void> {\n const version = await sh(sandbox, 'gh --version');\n if (version.exitCode !== 0) {\n throw new MaterializeError(\n 'The GitHub CLI (gh) is not installed in the sandbox. The sandbox template must include gh to open pull requests.',\n 'gh-missing',\n );\n }\n}\n\n/** Match the first GitHub PR URL in `gh pr create` output. */\nfunction parsePullRequestUrl(stdout: string): string | undefined {\n const match = stdout.match(/https:\\/\\/github\\.com\\/[^\\s]+\\/pull\\/\\d+/);\n return match?.[0];\n}\n\n/**\n * Open a pull request from inside the sandbox via `gh pr create`. The token is\n * passed only through a per-invocation `GH_TOKEN` env scoped to the single `gh`\n * process (never persisted), all arguments are shell-quoted, and the resulting\n * PR URL is parsed from stdout.\n *\n * @param sandbox live sandbox containing the checkout\n * @param workdir the worktree (or repo) path the PR head branch is checked out in\n */\nexport async function createPullRequest(\n sandbox: ExecutableSandbox,\n workdir: string,\n { token, base, head, title, body }: CreatePullRequestArgs,\n): Promise<CreatePullRequestResult> {\n if (!isValidGitRef(base)) {\n throw new MaterializeError(`Refusing to open PR: invalid base branch '${base}'.`, 'pr-failed');\n }\n if (!isValidGitRef(head)) {\n throw new MaterializeError(`Refusing to open PR: invalid head branch '${head}'.`, 'pr-failed');\n }\n\n await assertGhAvailable(sandbox);\n\n // GH_TOKEN is prefixed inline so it is exported only to the single `gh`\n // process and never to the wider shell session, git config, or VM env. `gh`\n // is run from inside the checkout so it targets the correct repo/head branch.\n const ghCommand = [\n `GH_TOKEN=${shellQuote(token)} gh pr create`,\n `--base ${shellQuote(base)}`,\n `--head ${shellQuote(head)}`,\n `--title ${shellQuote(title)}`,\n `--body ${shellQuote(body ?? '')}`,\n ].join(' ');\n const script = `cd ${shellQuote(workdir)} && ${ghCommand}`;\n\n const result = await sh(sandbox, script);\n if (result.exitCode !== 0) {\n const classified = classifyGitFailure(result, 'push-failed');\n if (classified.code === 'egress-blocked') {\n throw classified;\n }\n throw new MaterializeError(`gh pr create failed: ${result.stderr.trim() || result.stdout.trim()}`, 'pr-failed');\n }\n\n const url = parsePullRequestUrl(result.stdout);\n if (!url) {\n throw new MaterializeError(\n `gh pr create succeeded but no PR URL was found in its output: ${result.stdout.trim()}`,\n 'pr-failed',\n );\n }\n\n return { url };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sandbox.js","names":[],"sources":["../../../src/integrations/github/sandbox.ts"],"sourcesContent":["/**\n * Repo materialization for GitHub-backed repositories.\n *\n * A GitHub repo is never cloned onto the server host. The repo is cloned\n * *inside* the session's sandbox, so the agent's file tools and command tools\n * operate entirely against the remote checkout.\n *\n * - `materializeRepo(row, token)` clones the repo inside the sandbox when no\n * checkout exists yet (a base-image boot, a wiped disk), using a short-lived\n * installation token that is scrubbed from the git remote afterwards so it\n * never persists in the VM. A checkout that is already there, from a repo\n * template image or an earlier start, is left exactly as it is.\n *\n * This module owns everything git/GitHub: clone, commit/push, setup/teardown commands,\n * and `gh pr create`. Workdir layout lives in `../sandbox/workdir`.\n */\n\nimport { repoCloneCommand } from '@internal/workspace';\nimport type { ExecutableSandbox, SandboxCommandResult } from '../../sandbox/materialization.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport { timedPhase } from '../../timing.js';\n\ntype MaterializationStore = Pick<SourceControlStorageHandle['sessions'], 'markMaterialized'>;\n\ninterface RepoMaterializationBinding {\n id: string;\n sandboxWorkdir: string;\n materializedAt: Date | null;\n}\n\n/**\n * Single-quote a string for safe POSIX shell interpolation. Wraps the value in\n * single quotes and escapes any embedded single quote using the canonical\n * close-quote / escaped-quote / reopen-quote sequence (`'\\''`). This is the\n * standard POSIX-safe construction and prevents the quoted string from being\n * terminated early.\n */\nexport function shellQuote(value: string): string {\n // Replace each ' with the four-character sequence: ' \\ ' '\n return `'` + value.split(`'`).join(`'\\\\''`) + `'`;\n}\n\n/**\n * Default hang guard for sandbox shell commands. Generous by design — large\n * clones and dependency installs legitimately take minutes; the guard exists\n * so a wedged sandbox surfaces a failure instead of hanging the request that\n * triggered materialization forever.\n */\nexport const DEFAULT_COMMAND_TIMEOUT_MS = 15 * 60_000;\n/** Branch checkout only fetches one ref — a much tighter budget applies. */\nexport const CHECKOUT_COMMAND_TIMEOUT_MS = 5 * 60_000;\n\ninterface ShOptions {\n /** Override the hang-guard budget for this command. */\n timeoutMs?: number;\n /** Human-readable phase name included in the timeout error. */\n phase?: string;\n}\n\n/**\n * A thrown transport-level failure that is worth retrying: remote sandbox\n * providers (e.g. the platform workspace proxy) surface transient 5xx errors\n * as exceptions carrying an HTTP `status` — typically while a freshly\n * provisioned VM is still coming up. Command failures are NOT exceptions\n * (they resolve with a non-zero exit code), so retrying here never re-runs a\n * command that the sandbox already executed and rejected.\n */\nfunction isTransientTransportError(error: unknown): boolean {\n const status = (error as { status?: unknown })?.status;\n return typeof status === 'number' && status >= 500;\n}\n\nconst SH_RETRIES = 2;\nconst SH_RETRY_DELAY_MS = 2000;\n\n/**\n * Run a shell script in the sandbox via `sh -c`, bounded by a hang guard.\n * Transient transport-level 5xx failures (proxy hiccups while the VM boots)\n * are retried with a short backoff; every script routed through here is safe\n * to re-run. Hang-guard timeouts are NOT retried — the budget applies to the\n * command as a whole.\n */\nexport async function sh(\n sandbox: ExecutableSandbox,\n script: string,\n options: ShOptions = {},\n): Promise<SandboxCommandResult> {\n // One budget for the command as a whole: each attempt only gets the time\n // remaining, so transport retries can never multiply the hang guard.\n const deadlineMs = Date.now() + (options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);\n for (let attempt = 0; ; attempt++) {\n const started = performance.now();\n try {\n const result = await shOnce(sandbox, script, { ...options, timeoutMs: Math.max(deadlineMs - Date.now(), 1) });\n // Phased commands are the session start path; report each so a slow\n // start names the command that took the time rather than the phase.\n if (options.phase) {\n process.stderr.write(\n `[factory:timing] ${options.phase} attempt=${attempt + 1} exit=${result.exitCode} ${Math.round(performance.now() - started)}ms\\n`,\n );\n }\n return result;\n } catch (error) {\n if (options.phase) {\n process.stderr.write(\n `[factory:timing] ${options.phase} attempt=${attempt + 1} threw after ${Math.round(performance.now() - started)}ms: ${error instanceof Error ? error.message : String(error)}\\n`,\n );\n }\n if (attempt >= SH_RETRIES || !isTransientTransportError(error)) throw error;\n const delayMs = SH_RETRY_DELAY_MS * (attempt + 1);\n if (deadlineMs - Date.now() <= delayMs) throw error;\n await new Promise(resolve => setTimeout(resolve, delayMs));\n }\n }\n}\n\n/** Single `sh -c` execution attempt, bounded by the hang guard. */\nasync function shOnce(\n sandbox: ExecutableSandbox,\n script: string,\n options: ShOptions,\n): Promise<SandboxCommandResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const hangGuard = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n const phase = options.phase ? ` during ${options.phase}` : '';\n reject(new Error(`Sandbox command timed out after ${Math.round(timeoutMs / 1000)}s${phase}.`));\n }, timeoutMs);\n timer.unref?.();\n });\n try {\n // Forward the budget to the provider too so it can terminate the wedged\n // process; the race stays as the outer guard for providers that ignore it.\n return await Promise.race([sandbox.executeCommand('sh', ['-c', script], { timeout: timeoutMs }), hangGuard]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nconst GIT_TRANSFER_RETRIES = 2;\nconst GIT_TRANSFER_RETRY_DELAY_MS = 2000;\n\n/**\n * True when a git transfer died mid-flight rather than being refused.\n *\n * `sh` already retries transport errors the sandbox provider *throws*, but a\n * git command that reaches the network and then loses it exits non-zero\n * instead — so a single HTTP/2 framing glitch or dropped connection to\n * github.com would otherwise permanently fail opening a workspace. These\n * patterns all mean \"the bytes stopped arriving\", which says nothing about\n * whether the operation would succeed if attempted again.\n *\n * Deliberately narrow: a refusal (bad credentials, missing repo, blocked\n * egress) is terminal and must surface immediately rather than be retried into\n * a slow failure.\n */\nfunction isTransientGitFailure(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /HTTP2 framing layer|RPC failed; curl|RPC failed; HTTP 5\\d\\d|the remote end hung up unexpectedly|early EOF|unexpected disconnect|connection reset by peer|Recv failure|Send failure|GnuTLS recv error|TLS connection was non-properly terminated|502 Bad Gateway|503 Service Unavailable/i.test(\n output,\n );\n}\n\n/**\n * Run a git command that only *reads* from the remote, retrying it when the\n * transfer dies mid-flight. Restricted to read-only transfers on purpose:\n * re-running a clone or a fetch is free, whereas re-running a push could\n * duplicate work already accepted by the remote before the connection dropped.\n *\n * `beforeRetry` lets a call site clear whatever the aborted attempt left\n * behind — a half-written clone directory blocks the next `git clone` outright.\n */\nasync function gitTransfer(\n sandbox: ExecutableSandbox,\n script: string,\n options: ShOptions & { beforeRetry?: (attempt: number) => Promise<void> } = {},\n): Promise<SandboxCommandResult> {\n const { beforeRetry, ...shOptions } = options;\n const deadlineMs = Date.now() + (shOptions.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);\n for (let attempt = 0; ; attempt++) {\n const result = await sh(sandbox, script, {\n ...shOptions,\n timeoutMs: Math.max(deadlineMs - Date.now(), 1),\n });\n if (result.exitCode === 0 || attempt >= GIT_TRANSFER_RETRIES || !isTransientGitFailure(result)) return result;\n process.stderr.write(`[factory:timing] git ${shOptions.phase ?? 'transfer'} retrying after attempt ${attempt + 1}\\n`);\n const delayMs = GIT_TRANSFER_RETRY_DELAY_MS * (attempt + 1);\n if (deadlineMs - Date.now() <= delayMs) return result;\n await new Promise(resolve => setTimeout(resolve, delayMs));\n await beforeRetry?.(attempt + 1);\n }\n}\n\n/** Error raised when the sandbox cannot materialize the repo (actionable). */\nexport class MaterializeError extends Error {\n constructor(\n message: string,\n readonly code:\n | 'git-missing'\n | 'egress-blocked'\n | 'clone-failed'\n | 'pull-failed'\n | 'push-failed'\n | 'commit-failed'\n | 'gh-missing'\n | 'pr-failed',\n ) {\n super(message);\n this.name = 'MaterializeError';\n }\n}\n\n/**\n * Build the token-auth clone/pull URL for a repo. The token lives only inside\n * this URL and is scrubbed from the remote after the operation.\n */\nfunction tokenUrl(repoFullName: string, token: string): string {\n return `https://x-access-token:${token}@github.com/${repoFullName}.git`;\n}\n\nfunction cleanUrl(repoFullName: string): string {\n return `https://github.com/${repoFullName}.git`;\n}\n\n/** Repo metadata needed to materialize, read from the org-owned project row. */\nexport interface RepoMaterializeInfo {\n repoFullName: string;\n defaultBranch: string;\n}\n\n/** Options for {@link materializeRepo}. */\nexport interface MaterializeRepoOptions {\n /** The per-(project,user) sandbox binding whose workdir this materializes into. */\n row: RepoMaterializationBinding;\n /** Repo metadata from the org-owned project row. */\n repoInfo: RepoMaterializeInfo;\n /** The live sandbox to run git inside. */\n sandbox: ExecutableSandbox;\n /** A freshly minted, short-lived installation access token. */\n token: string;\n storage: MaterializationStore;\n}\n\n/**\n * Materialize the repo inside the user's sandbox: clone when no checkout of\n * this repo exists, otherwise nothing. Scrubs the install token from the\n * remote after a clone and sets `materialized_at` on the per-user sandbox\n * binding row.\n */\nexport async function materializeRepo(options: MaterializeRepoOptions): Promise<void> {\n return timedPhase('workspace.materialize', () => materializeRepoImpl(options));\n}\n\nasync function materializeRepoImpl(options: MaterializeRepoOptions): Promise<void> {\n const { row: sandboxRow, repoInfo, sandbox, token, storage } = options;\n const workdir = sandboxRow.sandboxWorkdir;\n const repo = repoInfo.repoFullName;\n\n // 0. Defense in depth: never build a git command from values that aren't\n // strictly shaped, even if a malformed row reached the DB. Inputs are also\n // validated at the route boundary before storage.\n if (!/^[\\w.-]+\\/[\\w.-]+$/.test(repo)) {\n throw new MaterializeError(`Refusing to materialize: invalid repo full name '${repo}'.`, 'clone-failed');\n }\n if (!/^[A-Za-z0-9_./-]+$/.test(repoInfo.defaultBranch)) {\n throw new MaterializeError(\n `Refusing to materialize: invalid default branch '${repoInfo.defaultBranch}'.`,\n 'clone-failed',\n );\n }\n\n // 1. Preflight: git must be installed in the sandbox template.\n const gitVersion = await sh(sandbox, 'git --version');\n if (gitVersion.exitCode !== 0) {\n throw new MaterializeError(\n 'git is not installed in the sandbox. The sandbox template must include git.',\n 'git-missing',\n );\n }\n\n // The DB's `materializedAt` can drift from disk in both directions: a fresh\n // binding row over an already-populated workdir (a repo template image,\n // local dev DB resets, repaired rows) must not fail `git clone` on the\n // non-empty directory, and a stale `materializedAt` over an empty sandbox\n // (an expired/recreated VM whose disk was wiped) must re-clone instead of\n // running `git -C <workdir>` against a directory that no longer exists.\n // Disk is the source of truth: detect the checkout instead of trusting the\n // row. An existing checkout is left as it is, whatever it is on: a template\n // image sits detached at its pinned commit, a resumed session on its\n // branch. Syncing with the remote is the session's business; the branch\n // checkout that follows fetches the base branch it needs.\n const existing = await existingCheckoutRemote(sandbox, workdir, repo);\n if (existing !== null) {\n // A token an earlier start failed to scrub must not outlive it; the\n // remote already carries the plain URL otherwise, so this costs nothing\n // on the common path.\n if (/\\/\\/[^/]*@/.test(existing)) await scrubRemote(sandbox, workdir, repo, true);\n } else {\n // 2. First open: shallow-clone the default branch into the workdir, the\n // same clone a repo template bakes into its image. The workdir holds no\n // usable checkout of this repo, but it may not be empty: a checkpoint\n // seed or a clone that died partway (a crashed or OOM-killed server)\n // leaves a partial tree behind, and `git clone` refuses a non-empty\n // destination with a non-retryable fatal. Nothing here is recoverable,\n // the probe above already ruled out a checkout of this repo, so\n // clear its contents before cloning, exactly as the retry path does. Keep\n // the workdir itself because LocalSandbox runs commands with this\n // directory as the child process cwd.\n await sh(\n sandbox,\n `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,\n );\n let tokenInRemote = false;\n try {\n const clone = await gitTransfer(\n sandbox,\n repoCloneCommand({ cloneUrl: tokenUrl(repo, token), destination: workdir, branch: repoInfo.defaultBranch }),\n {\n phase: 'repository clone',\n beforeRetry: async () => {\n // A clone that died partway leaves the destination non-empty, which\n // git refuses to clone into. Clear its contents so the retry starts\n // clean without removing LocalSandbox's process cwd.\n await sh(\n sandbox,\n `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,\n );\n },\n },\n );\n if (clone.exitCode !== 0) {\n // git can fail after creating the checkout (\"Clone succeeded, but\n // checkout failed\") with the tokenized origin persisted: probe the\n // disk instead of assuming the failed clone left nothing behind.\n tokenInRemote = await hasGitDir(sandbox, workdir);\n throw classifyGitFailure(clone, 'clone-failed');\n }\n tokenInRemote = true;\n } catch (primary) {\n // 3a. The clone failed: still scrub the token from the VM's git config.\n // The scrub must never hide the actionable failure, but once the token\n // reached the remote its own failure can't stay silent either: report\n // both, primary cause and classification first.\n throw await scrubbedFailure(sandbox, workdir, repo, tokenInRemote, primary, 'clone-failed');\n }\n\n // 3b. Success: the token is in the remote and the workdir has a `.git`, so\n // a failed scrub means the token may still be persisted: surface it.\n await scrubRemote(sandbox, workdir, repo, tokenInRemote);\n }\n\n // 4. Mark materialized.\n await storage.markMaterialized({ id: sandboxRow.id });\n}\n\n/** Check out a session's branch inside its isolated repository clone. */\nexport async function checkoutSessionBranch(\n sandbox: ExecutableSandbox,\n workdir: string,\n options: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<void> {\n return timedPhase('workspace.checkout', () => checkoutSessionBranchImpl(sandbox, workdir, options));\n}\n\nasync function checkoutSessionBranchImpl(\n sandbox: ExecutableSandbox,\n workdir: string,\n {\n branch,\n baseBranch,\n token,\n repoFullName,\n }: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<void> {\n if (!isValidGitRef(branch) || !isValidGitRef(baseBranch)) {\n throw new MaterializeError('Refusing to create a session from an invalid branch name.', 'clone-failed');\n }\n\n const current = await sh(sandbox, `git -C ${shellQuote(workdir)} branch --show-current`);\n if (current.exitCode === 0 && current.stdout.trim() === branch) return;\n\n const local = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} show-ref --verify --quiet refs/heads/${shellQuote(branch)}`,\n );\n if (local.exitCode === 0) {\n const checkout = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout ${shellQuote(branch)}`);\n if (checkout.exitCode !== 0) {\n // The session's agent may have switched branches itself (e.g. `gh pr\n // checkout`) and left uncommitted work in the tree. Git refuses to\n // switch back over those files — that work must win. The checkout is\n // intact and usable on its current branch; keep it as-is rather than\n // fail the workspace open, and never reset or stash to force the\n // switch through.\n if (isBlockedByLocalWork(checkout)) return;\n throw classifyGitFailure(checkout, 'clone-failed');\n }\n return;\n }\n\n const authUrl = tokenUrl(repoFullName, token);\n try {\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`, {\n phase: 'branch checkout remote',\n });\n if (setUrl.exitCode !== 0) throw classifyGitFailure(setUrl, 'pull-failed');\n const fetch = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} fetch origin ${shellQuote(baseBranch)} && git -C ${shellQuote(workdir)} checkout -b ${shellQuote(branch)} FETCH_HEAD`,\n { timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS, phase: 'branch checkout' },\n );\n if (fetch.exitCode !== 0) {\n // Same rule as above: uncommitted work in the tree blocks the switch\n // to the new branch. Leave the checkout on its current branch.\n if (isBlockedByLocalWork(fetch)) return;\n if (!isBranchCollision(fetch)) throw classifyGitFailure(fetch, 'clone-failed');\n // The branch exists even though the show-ref probe missed it: either a\n // concurrent materialization of this session created it between the\n // probe and `checkout -b` (adopt it), or a reused sandbox carries a\n // broken loose ref the probe cannot resolve (replace it and retry —\n // \"already exists\" means the fetch half succeeded, so FETCH_HEAD is\n // set).\n const adopt = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout ${shellQuote(branch)}`);\n if (adopt.exitCode === 0 || isBlockedByLocalWork(adopt)) return;\n const drop = await sh(\n sandbox,\n // `--no-deref` so a broken symref is deleted itself instead of git\n // following it to some other branch. `update-ref -d` can still refuse\n // a broken ref; fall back to removing the loose ref file (branch\n // passed isValidGitRef, so the interpolation inside the double quotes\n // is inert).\n `git -C ${shellQuote(workdir)} update-ref --no-deref -d refs/heads/${shellQuote(branch)} || rm -f -- \"$(git -C ${shellQuote(workdir)} rev-parse --absolute-git-dir)/refs/heads/${branch}\"`,\n );\n if (drop.exitCode !== 0) throw classifyGitFailure(fetch, 'clone-failed');\n const retry = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout -b ${shellQuote(branch)} FETCH_HEAD`, {\n timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS,\n phase: 'branch checkout retry',\n });\n if (retry.exitCode !== 0) {\n if (isBlockedByLocalWork(retry)) return;\n throw classifyGitFailure(retry, 'clone-failed');\n }\n }\n } finally {\n await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`);\n }\n}\n\n/**\n * True when `git checkout -b` failed only because the branch ref already\n * exists — the collision Factory hits when a pooled sandbox carries a ref the\n * show-ref probe could not see (broken loose ref) or a concurrent\n * materialization created the branch after the probe ran.\n */\nfunction isBranchCollision(result: SandboxCommandResult): boolean {\n return /a branch named .* already exists/i.test(`${result.stderr || ''}\\n${result.stdout || ''}`);\n}\n\n/**\n * True when a failed `git checkout` just means uncommitted or untracked files\n * in the working tree would be clobbered by the branch switch. Those files are\n * a session's work in progress — the switch must yield to them, never the\n * other way around.\n */\nfunction isBlockedByLocalWork(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /Your local changes to the following files would be overwritten by checkout|untracked working tree files would be overwritten by checkout/i.test(\n output,\n );\n}\n\n/**\n * The `origin` URL of a checkout of this exact repo in the workdir, or null\n * when there is none. Matches both the clean and token-auth URL forms; any\n * other remote (or no git dir at all) sends materialize down the clone path.\n */\nasync function existingCheckoutRemote(\n sandbox: ExecutableSandbox,\n workdir: string,\n repoFullName: string,\n): Promise<string | null> {\n const result = await sh(sandbox, `git -C ${shellQuote(workdir)} remote get-url origin`);\n if (result.exitCode !== 0) return null;\n const url = result.stdout.trim();\n return isRemoteForRepo(url, repoFullName) ? url : null;\n}\n\n/** True only for `https://github.com/<repo>[.git]`, with or without embedded credentials. */\nfunction isRemoteForRepo(url: string, repoFullName: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return false;\n }\n if (parsed.protocol !== 'https:' || parsed.hostname.toLowerCase() !== 'github.com') return false;\n if (parsed.port !== '' || parsed.search !== '' || parsed.hash !== '') return false;\n return parsed.pathname.replace(/\\.git$/, '').toLowerCase() === `/${repoFullName.toLowerCase()}`;\n}\n\n/** Probed without `git -C` so a missing workdir returns false instead of throwing. */\nasync function hasGitDir(sandbox: ExecutableSandbox, workdir: string): Promise<boolean> {\n const probe = await sh(sandbox, `test -d ${shellQuote(`${workdir}/.git`)}`).catch(() => null);\n return probe?.exitCode === 0;\n}\n\n/**\n * Reset the git remote back to the tokenless URL. Strict when the token\n * reached the remote: any failure — a non-zero exit or a provider throw —\n * means the token may still be persisted, so it is thrown for the caller to\n * surface. Best-effort when it never did: the workdir may not exist (e.g. a\n * failed clone), which makes providers that spawn with `cwd` throw rather\n * than return a non-zero exit code; both outcomes are tolerated so neither\n * masks the primary failure.\n */\nasync function scrubRemote(\n sandbox: ExecutableSandbox,\n workdir: string,\n repoFullName: string,\n tokenInRemote: boolean,\n): Promise<void> {\n const scrub = `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`;\n if (!tokenInRemote) {\n await sh(sandbox, scrub).catch(() => undefined);\n return;\n }\n let failure: string;\n try {\n const result = await sh(sandbox, scrub);\n if (result.exitCode === 0) return;\n failure = result.stderr.trim() || result.stdout.trim();\n } catch (error) {\n failure = error instanceof Error ? error.message : String(error);\n }\n throw new MaterializeError(`Failed to scrub installation token from git remote: ${failure}`, 'pull-failed');\n}\n\n/**\n * Scrub after `primary` already failed and return the error to throw. A failed\n * scrub is appended to `primary` rather than replacing it, so the caller gets\n * the error it would have had without the scrub — same class, same `code` —\n * carrying the leaked-token warning in its message.\n */\nasync function scrubbedFailure(\n sandbox: ExecutableSandbox,\n workdir: string,\n repoFullName: string,\n tokenInRemote: boolean,\n primary: unknown,\n fallback: MaterializeError['code'],\n): Promise<unknown> {\n try {\n await scrubRemote(sandbox, workdir, repoFullName, tokenInRemote);\n return primary;\n } catch (scrubError) {\n const scrubMessage = scrubError instanceof Error ? scrubError.message : String(scrubError);\n if (!(primary instanceof Error)) {\n return new MaterializeError(`${String(primary)} — additionally: ${scrubMessage}`, fallback);\n }\n primary.message = `${primary.message} — additionally: ${scrubMessage}`;\n return primary;\n }\n}\n\n/**\n * Turn a failed git command into an actionable error, detecting the common\n * \"cannot reach github.com\" egress failure.\n */\nfunction classifyGitFailure(\n result: SandboxCommandResult,\n fallback: 'clone-failed' | 'pull-failed' | 'push-failed',\n): MaterializeError {\n const stderr = result.stderr || '';\n if (/could not resolve host|failed to connect|network is unreachable|Connection timed out/i.test(stderr)) {\n return new MaterializeError(\n 'The sandbox could not reach github.com. The sandbox network must allow outbound egress to github.com.',\n 'egress-blocked',\n );\n }\n const verb = fallback === 'clone-failed' ? 'clone' : fallback === 'pull-failed' ? 'pull' : 'push';\n return new MaterializeError(`git ${verb} failed: ${stderr}`, fallback);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 1 — git identity + token-scoped push primitive\n//\n// These helpers let the sandbox author and push commits safely. The install\n// token is short-lived, minted per-operation server-side, injected only into\n// the temporary remote URL inside the sandbox, and always scrubbed afterwards\n// so it never persists in `.git/config`.\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a git ref (branch) name. Server-side defense-in-depth: only allow a\n * conservative character set so a branch can never be built into a shell\n * command in a way that escapes quoting. Mirrors the route-layer check.\n */\nexport function isValidGitRef(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= 255 &&\n // Reject leading-dash refs (e.g. `--mirror`) so the value can never be\n // parsed as a git option when interpolated into a command.\n !value.startsWith('-') &&\n /^[A-Za-z0-9_./-]+$/.test(value)\n );\n}\n\n/** Identity used to author commits inside the sandbox. */\nexport interface GitIdentity {\n name?: string | null;\n email?: string | null;\n /** GitHub login, used to derive a stable noreply identity when name/email are absent. */\n login?: string | null;\n}\n\n/**\n * Resolve a concrete `{ name, email }` for git authorship from a possibly-sparse\n * identity. Falls back to a GitHub-style noreply identity so commits are never\n * authored with an empty or host-derived identity.\n */\nexport function resolveGitIdentity(identity: GitIdentity): { name: string; email: string } {\n const login = (identity.login || '').trim();\n const name = (identity.name || '').trim() || login || 'Mastra Code';\n const email =\n (identity.email || '').trim() ||\n (login ? `${login}@users.noreply.github.com` : 'mastra-code@users.noreply.github.com');\n return { name, email };\n}\n\n/**\n * Configure `user.name` / `user.email` for the given repo working tree inside\n * the sandbox so commits are authored correctly. Values are shell-quoted.\n */\nexport async function configureGitIdentity(\n sandbox: ExecutableSandbox,\n workdir: string,\n identity: GitIdentity,\n): Promise<void> {\n const { name, email } = resolveGitIdentity(identity);\n const setName = await sh(sandbox, `git -C ${shellQuote(workdir)} config user.name ${shellQuote(name)}`);\n if (setName.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git user.name: ${setName.stderr.trim()}`, 'commit-failed');\n }\n const setEmail = await sh(sandbox, `git -C ${shellQuote(workdir)} config user.email ${shellQuote(email)}`);\n if (setEmail.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git user.email: ${setEmail.stderr.trim()}`, 'commit-failed');\n }\n}\n\n/**\n * Temporarily rewrite `origin` to a tokenized URL, run `fn` (e.g. a push), and\n * **always** scrub the remote back to the tokenless URL afterwards. The token\n * therefore only ever lives in the remote URL for the duration of the\n * operation and is never left in the VM's git config.\n *\n * Once the tokenized URL is installed a failed scrub may leave the token\n * persisted, so it is always surfaced: on its own after a successful `fn`,\n * appended to `fn`'s own error otherwise — `fn`'s error is never replaced.\n * Only a failed set-url (the token never reached the remote) downgrades the\n * scrub to best-effort.\n */\nexport async function withInstallToken<T>(\n sandbox: ExecutableSandbox,\n workdir: string,\n repoFullName: string,\n token: string,\n fn: () => Promise<T>,\n): Promise<T> {\n if (!/^[\\w.-]+\\/[\\w.-]+$/.test(repoFullName)) {\n throw new MaterializeError(`Refusing to push: invalid repo full name '${repoFullName}'.`, 'push-failed');\n }\n\n const setUrl = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(tokenUrl(repoFullName, token))}`,\n );\n if (setUrl.exitCode !== 0) {\n // Best-effort scrub even though set-url failed, then surface the failure.\n await scrubRemote(sandbox, workdir, repoFullName, false);\n throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr.trim()}`, 'push-failed');\n }\n\n let result: T;\n try {\n result = await fn();\n } catch (primary) {\n throw await scrubbedFailure(sandbox, workdir, repoFullName, true, primary, 'push-failed');\n }\n // Restore the tokenless remote. The workdir has a `.git` (we just rewrote\n // its remote) so a scrub failure means the token may still persist — surface it.\n await scrubRemote(sandbox, workdir, repoFullName, true);\n return result;\n}\n\n/**\n * Push a branch back to GitHub from inside the sandbox using a short-lived\n * installation token. The branch is ref-validated, the token is injected only\n * into the remote URL via `withInstallToken`, and egress failures are\n * classified into actionable errors.\n */\nexport async function pushBranch(\n sandbox: ExecutableSandbox,\n workdir: string,\n branch: string,\n token: string,\n repoFullName: string,\n): Promise<void> {\n if (!isValidGitRef(branch)) {\n throw new MaterializeError(`Refusing to push: invalid branch name '${branch}'.`, 'push-failed');\n }\n\n await withInstallToken(sandbox, workdir, repoFullName, token, async () => {\n const push = await sh(sandbox, `git -C ${shellQuote(workdir)} push -u origin ${shellQuote(branch)}`);\n if (push.exitCode !== 0) {\n throw classifyGitFailure(push, 'push-failed');\n }\n });\n}\n\nexport interface CommitResult {\n /** True when a commit was created; false when there was nothing to commit. */\n committed: boolean;\n}\n\n/**\n * Stage every change in the working tree and create a commit inside the\n * sandbox. The git identity is configured first so authorship is correct. When\n * there is nothing to commit this is a no-op (`committed: false`) rather than an\n * error, so callers can safely commit-then-push without first diffing.\n *\n * @param sandbox the live sandbox containing the checkout\n * @param workdir the session workdir to commit in\n * @param message the commit message (quoted; arbitrary text is safe)\n * @param identity authorship identity for the commit\n */\nexport async function commitAll(\n sandbox: ExecutableSandbox,\n workdir: string,\n message: string,\n identity: GitIdentity,\n): Promise<CommitResult> {\n await configureGitIdentity(sandbox, workdir, identity);\n\n const add = await sh(sandbox, `git -C ${shellQuote(workdir)} add -A`);\n if (add.exitCode !== 0) {\n throw new MaterializeError(`git add failed: ${add.stderr.trim() || add.stdout.trim()}`, 'commit-failed');\n }\n\n // Nothing staged → nothing to commit. `git diff --cached --quiet` exits 1 when\n // there are staged changes, 0 when the index is clean.\n const staged = await sh(sandbox, `git -C ${shellQuote(workdir)} diff --cached --quiet`);\n if (staged.exitCode === 0) {\n return { committed: false };\n }\n\n const commit = await sh(sandbox, `git -C ${shellQuote(workdir)} commit -m ${shellQuote(message)}`);\n if (commit.exitCode !== 0) {\n throw new MaterializeError(`git commit failed: ${commit.stderr.trim() || commit.stdout.trim()}`, 'commit-failed');\n }\n\n return { committed: true };\n}\n\n// ---------------------------------------------------------------------------\n// Phase 2 — setup / teardown lifecycle commands\n//\n// The org-configured setup and teardown shell commands run in the session's\n// materialized workdir. The workdir is always resolved server-side from the\n// live sandbox; client input never reaches a filesystem path.\n// ---------------------------------------------------------------------------\n\n/** Error raised when the org's setup or teardown command fails in the sandbox. */\nexport class SetupCommandError extends Error {\n constructor(\n message: string,\n readonly code: 'setup-failed' | 'teardown-failed',\n ) {\n super(message);\n this.name = 'SetupCommandError';\n }\n}\n\n/**\n * Run the project's setup command (e.g. `pnpm i && pnpm build`) inside the\n * freshly materialized session workdir. Called before the checkout is handed\n * to any agent run so it is ready to build/test. A non-zero exit is a hard\n * error — starting agent work in a half-set-up tree is worse than failing the\n * request.\n *\n * Security model: the command is intentionally arbitrary shell — that is the\n * feature (install deps, build, seed fixtures). It is only configurable by\n * authenticated org members (the settings route is gated by\n * `resolveOrgTenant` + org-scoped project lookup, with length and\n * control-character validation), and it executes exclusively inside the\n * project's isolated sandbox — the same environment where org members already\n * run arbitrary shell via the agent's command tool. It never runs on the web\n * server host, so it grants no privilege beyond what sandbox access already\n * provides.\n *\n * @param sandbox live sandbox containing the checkout\n * @param workdir the server-resolved session workdir the command runs in\n * @param command the org-configured setup shell command\n */\nasync function runLifecycleCommand(\n sandbox: ExecutableSandbox,\n workdir: string,\n command: string,\n options: { phase: 'setup' | 'teardown'; timeoutMs?: number },\n): Promise<void> {\n const result = await sh(sandbox, `cd ${shellQuote(workdir)} && { ${command}\\n}`, {\n phase: `${options.phase} command`,\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n });\n if (result.exitCode !== 0) {\n const detail = (result.stderr.trim() || result.stdout.trim()).slice(-1800);\n const label = options.phase === 'setup' ? 'Setup' : 'Teardown';\n throw new SetupCommandError(\n `${label} command failed (exit ${result.exitCode}): ${detail}`,\n options.phase === 'setup' ? 'setup-failed' : 'teardown-failed',\n );\n }\n}\n\nexport async function runSetupCommand(\n sandbox: ExecutableSandbox,\n workdir: string,\n command: string,\n): Promise<void> {\n return runLifecycleCommand(sandbox, workdir, command, { phase: 'setup' });\n}\n\n/**\n * Run the repository's best-effort teardown command from the materialized\n * session workdir. Callers own lifecycle policy: this helper reports failures\n * so the retirement coordinator can log them while still continuing with\n * scrub, pooling/destruction, cache invalidation, and row deletion.\n */\nexport async function runTeardownCommand(\n sandbox: ExecutableSandbox,\n workdir: string,\n command: string,\n options: { timeoutMs?: number } = {},\n): Promise<void> {\n return runLifecycleCommand(sandbox, workdir, command, {\n phase: 'teardown',\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n });\n}\n\nexport interface CreatePullRequestArgs {\n /** Short-lived installation token, injected only into the `gh` process env. */\n token: string;\n /** Base branch the PR merges into. Ref-validated. */\n base: string;\n /** Head branch the PR is opened from. Ref-validated. */\n head: string;\n /** PR title. */\n title: string;\n /** PR body (optional). */\n body?: string;\n}\n\nexport interface CreatePullRequestResult {\n /** The PR URL parsed from `gh pr create` stdout. */\n url: string;\n}\n\n/**\n * Preflight that `gh` is installed in the sandbox. Only called on the PR path so\n * a missing `gh` never blocks clone/open. Surfaces an actionable error naming\n * the sandbox template requirement.\n */\nasync function assertGhAvailable(sandbox: ExecutableSandbox): Promise<void> {\n const version = await sh(sandbox, 'gh --version');\n if (version.exitCode !== 0) {\n throw new MaterializeError(\n 'The GitHub CLI (gh) is not installed in the sandbox. The sandbox template must include gh to open pull requests.',\n 'gh-missing',\n );\n }\n}\n\n/** Match the first GitHub PR URL in `gh pr create` output. */\nfunction parsePullRequestUrl(stdout: string): string | undefined {\n const match = stdout.match(/https:\\/\\/github\\.com\\/[^\\s]+\\/pull\\/\\d+/);\n return match?.[0];\n}\n\n/**\n * Open a pull request from inside the sandbox via `gh pr create`. The token is\n * passed only through a per-invocation `GH_TOKEN` env scoped to the single `gh`\n * process (never persisted), all arguments are shell-quoted, and the resulting\n * PR URL is parsed from stdout.\n *\n * @param sandbox live sandbox containing the checkout\n * @param workdir the worktree (or repo) path the PR head branch is checked out in\n */\nexport async function createPullRequest(\n sandbox: ExecutableSandbox,\n workdir: string,\n { token, base, head, title, body }: CreatePullRequestArgs,\n): Promise<CreatePullRequestResult> {\n if (!isValidGitRef(base)) {\n throw new MaterializeError(`Refusing to open PR: invalid base branch '${base}'.`, 'pr-failed');\n }\n if (!isValidGitRef(head)) {\n throw new MaterializeError(`Refusing to open PR: invalid head branch '${head}'.`, 'pr-failed');\n }\n\n await assertGhAvailable(sandbox);\n\n // GH_TOKEN is prefixed inline so it is exported only to the single `gh`\n // process and never to the wider shell session, git config, or VM env. `gh`\n // is run from inside the checkout so it targets the correct repo/head branch.\n const ghCommand = [\n `GH_TOKEN=${shellQuote(token)} gh pr create`,\n `--base ${shellQuote(base)}`,\n `--head ${shellQuote(head)}`,\n `--title ${shellQuote(title)}`,\n `--body ${shellQuote(body ?? '')}`,\n ].join(' ');\n const script = `cd ${shellQuote(workdir)} && ${ghCommand}`;\n\n const result = await sh(sandbox, script);\n if (result.exitCode !== 0) {\n const classified = classifyGitFailure(result, 'push-failed');\n if (classified.code === 'egress-blocked') {\n throw classified;\n }\n throw new MaterializeError(`gh pr create failed: ${result.stderr.trim() || result.stdout.trim()}`, 'pr-failed');\n }\n\n const url = parsePullRequestUrl(result.stdout);\n if (!url) {\n throw new MaterializeError(\n `gh pr create succeeded but no PR URL was found in its output: ${result.stdout.trim()}`,\n 'pr-failed',\n );\n }\n\n return { url };\n}\n"],"mappings":"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"}
@@ -19,7 +19,17 @@ function normalizeSetupCommands(setupCommand) {
19
19
  function setupMarkerContent(setupCommand) {
20
20
  return `sha256:${createHash("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
21
21
  }
22
+ function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
23
+ return `git ${tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : ""}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
24
+ }
25
+ /** Per-invocation auth header; `-c` config never reaches `.git/config`. */
26
+ function gitAuthFlag(tokenEnv) {
27
+ return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
28
+ }
29
+ function shellQuote(value) {
30
+ return `'${value.replace(/'/g, `'\\''`)}'`;
31
+ }
22
32
  //#endregion
23
- export { SETUP_MARKER_PATH, normalizeSetupCommands, setupMarkerContent };
33
+ export { SETUP_MARKER_PATH, normalizeSetupCommands, repoCloneCommand, setupMarkerContent };
24
34
 
25
35
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../../../../packages/_internals/workspace/dist/index.js"],"sourcesContent":["import { createHash } from \"crypto\";\n//#region src/setup-marker.ts\n/**\n* Setup completion marker shared by repo templates and their consumers.\n*\n* A repo template writes this file beside the checkout as its last build\n* step, so it exists only in images where every setup command succeeded. Its\n* content is a digest of the setup commands the image ran, letting a sandbox\n* booted from the image tell whether the setup it is about to run already\n* happened. Relative to the template's build cwd, which is also the runtime\n* working directory the repo was cloned into.\n*/\nconst SETUP_MARKER_PATH = \".mastra-sandbox/setup\";\n/** Blank entries never become build steps, so they never count toward the digest either. */\nfunction normalizeSetupCommands(setupCommand) {\n\treturn (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== \"\");\n}\n/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */\nfunction setupMarkerContent(setupCommand) {\n\treturn `sha256:${createHash(\"sha256\").update(normalizeSetupCommands(setupCommand).join(\"\\n\")).digest(\"hex\")}`;\n}\n/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */\nfunction setupMarkerCommand(content) {\n\treturn `mkdir -p \"$(dirname \"${SETUP_MARKER_PATH}\")\" && printf '%s' '${content}' > \"${SETUP_MARKER_PATH}\"`;\n}\n//#endregion\nexport { SETUP_MARKER_PATH, normalizeSetupCommands, setupMarkerCommand, setupMarkerContent };\n\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;;;;;;;;AAYA,MAAM,oBAAoB;;AAE1B,SAAS,uBAAuB,cAAc;CAC7C,QAAQ,iBAAiB,KAAK,IAAI,CAAC,IAAI,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,YAAY,EAAA,CAAG,QAAQ,YAAY,QAAQ,KAAK,MAAM,EAAE;AAC9I;;AAEA,SAAS,mBAAmB,cAAc;CACzC,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,uBAAuB,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK;AAC3G"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../../../../packages/_internals/workspace/dist/index.js"],"sourcesContent":["import { createHash } from \"crypto\";\n//#region src/setup-marker.ts\n/**\n* Setup completion marker shared by repo templates and their consumers.\n*\n* A repo template writes this file beside the checkout as its last build\n* step, so it exists only in images where every setup command succeeded. Its\n* content is a digest of the setup commands the image ran, letting a sandbox\n* booted from the image tell whether the setup it is about to run already\n* happened. Relative to the template's build cwd, which is also the runtime\n* working directory the repo was cloned into.\n*/\nconst SETUP_MARKER_PATH = \".mastra-sandbox/setup\";\n/** Blank entries never become build steps, so they never count toward the digest either. */\nfunction normalizeSetupCommands(setupCommand) {\n\treturn (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== \"\");\n}\n/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */\nfunction setupMarkerContent(setupCommand) {\n\treturn `sha256:${createHash(\"sha256\").update(normalizeSetupCommands(setupCommand).join(\"\\n\")).digest(\"hex\")}`;\n}\n/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */\nfunction setupMarkerCommand(content) {\n\treturn `mkdir -p \"$(dirname \"${SETUP_MARKER_PATH}\")\" && printf '%s' '${content}' > \"${SETUP_MARKER_PATH}\"`;\n}\n//#endregion\n//#region src/repo-clone.ts\nfunction repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {\n\treturn `git ${tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : \"\"}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : \"\"}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;\n}\n/** Per-invocation auth header; `-c` config never reaches `.git/config`. */\nfunction gitAuthFlag(tokenEnv) {\n\treturn `-c http.extraheader=\"AUTHORIZATION: basic $(printf 'x-access-token:%s' \"$${tokenEnv}\" | base64 -w0)\"`;\n}\nfunction shellQuote(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\nexport { SETUP_MARKER_PATH, normalizeSetupCommands, repoCloneCommand, setupMarkerCommand, setupMarkerContent };\n\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;;;;;;;;AAYA,MAAM,oBAAoB;;AAE1B,SAAS,uBAAuB,cAAc;CAC7C,QAAQ,iBAAiB,KAAK,IAAI,CAAC,IAAI,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,YAAY,EAAA,CAAG,QAAQ,YAAY,QAAQ,KAAK,MAAM,EAAE;AAC9I;;AAEA,SAAS,mBAAmB,cAAc;CACzC,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,uBAAuB,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK;AAC3G;AAOA,SAAS,iBAAiB,EAAE,UAAU,aAAa,QAAQ,YAAY;CACtE,OAAO,OAAO,WAAW,GAAG,YAAY,QAAQ,EAAE,KAAK,GAAG,kCAAkC,SAAS,YAAY,WAAW,MAAM,EAAE,KAAK,KAAK,WAAW,QAAQ,EAAE,GAAG,WAAW,WAAW;AAC7L;;AAEA,SAAS,YAAY,UAAU;CAC9B,OAAO,4EAA4E,SAAS;AAC7F;AACA,SAAS,WAAW,OAAO;CAC1B,OAAO,IAAI,MAAM,QAAQ,MAAM,OAAO,EAAE;AACzC"}
@@ -1 +1 @@
1
- {"version":3,"file":"work-items.d.ts","sourceRoot":"","sources":["../../src/routes/work-items.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAKpD,OAAO,KAAK,EACV,uBAAuB,EAGxB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,KAAK,EAA4B,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAGzG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAElF,OAAO,KAAK,EACV,mBAAmB,EAInB,mBAAmB,EAKnB,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAQ/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,KAAK,EAAE,YAAY,CAAC;IACpB,yFAAyF;IACzF,QAAQ,EAAE,sBAAsB,CAAC;IACjC,kDAAkD;IAClD,SAAS,EAAE,gBAAgB,CAAC;IAC5B,8DAA8D;IAC9D,QAAQ,EAAE,uBAAuB,CAAC;IAClC,iDAAiD;IACjD,WAAW,EAAE,kBAAkB,CAAC;IAChC,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,GAAG,gBAAgB,CAAC,CAAC;IACpF,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC;IAC5D,wFAAwF;IACxF,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;CAC/C;AAsGD,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CA2B7E;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAgC7E;AAsMD,qBAAa,cAAe,SAAQ,KAAK,CAAC,kBAAkB,CAAC;;IAyJ3D,gEAAgE;IAChE,MAAM,IAAI,QAAQ,EAAE;CAgYrB"}
1
+ {"version":3,"file":"work-items.d.ts","sourceRoot":"","sources":["../../src/routes/work-items.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAKpD,OAAO,KAAK,EACV,uBAAuB,EAGxB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,KAAK,EAA4B,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAGzG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAElF,OAAO,KAAK,EACV,mBAAmB,EAInB,mBAAmB,EAKnB,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAQ/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,KAAK,EAAE,YAAY,CAAC;IACpB,yFAAyF;IACzF,QAAQ,EAAE,sBAAsB,CAAC;IACjC,kDAAkD;IAClD,SAAS,EAAE,gBAAgB,CAAC;IAC5B,8DAA8D;IAC9D,QAAQ,EAAE,uBAAuB,CAAC;IAClC,iDAAiD;IACjD,WAAW,EAAE,kBAAkB,CAAC;IAChC,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,GAAG,gBAAgB,CAAC,CAAC;IACpF,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC;IAC5D,wFAAwF;IACxF,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;CAC/C;AAsGD,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CA2B7E;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAgC7E;AAgND,qBAAa,cAAe,SAAQ,KAAK,CAAC,kBAAkB,CAAC;;IAyJ3D,gEAAgE;IAChE,MAAM,IAAI,QAAQ,EAAE;CAgYrB"}
@@ -274,6 +274,12 @@ function summaryRole(decision) {
274
274
  if (board !== "work" && board !== "review" || !isFactoryRuleStage(stage)) return null;
275
275
  return roleForStage(board, stage);
276
276
  }
277
+ /** A linked-card decision names where the card is synced from, so the UI can say "GitHub" rather than "a linked card". */
278
+ function summarySource(decision) {
279
+ if (decision.type !== "upsertLinkedWorkItem") return null;
280
+ const source = decision.source;
281
+ return source === "github-issue" || source === "github-pr" || source === "linear-issue" || source === "manual" ? source : null;
282
+ }
277
283
  function decisionSummary(decision) {
278
284
  return {
279
285
  id: decision.id,
@@ -281,6 +287,7 @@ function decisionSummary(decision) {
281
287
  workItemId: decision.workItemId,
282
288
  type: factoryDecisionType(decision),
283
289
  role: summaryRole(decision.decision),
290
+ source: summarySource(decision.decision),
284
291
  status: decision.status,
285
292
  attempts: decision.attempts,
286
293
  failureOccurrence: decision.failureOccurrence,