@mastra/factory 0.14.0 → 0.14.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)` 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\nexport interface SessionBranchOptions {\n branch: string;\n baseBranch: string;\n token: string;\n repoFullName: string;\n /** A pull-request card's session starts on the PR head instead of the base tip. */\n pullRequestNumber?: number;\n}\n\n/**\n * Past file contents of a blob-less history load on demand; git asks gh, which\n * answers from the session's `GH_TOKEN`, so no credential is ever written.\n */\nconst GH_CREDENTIAL_HELPER = '!gh auth git-credential';\n\n/**\n * A pull-request session first fetches the base's whole commit history without\n * file contents, so `git log` and `git blame` work in the review, then the PR\n * head. Every other session starts from the shallow base tip as it is.\n */\nfunction startPointFetchCommands(\n workdir: string,\n { baseBranch, pullRequestNumber }: Pick<SessionBranchOptions, 'baseBranch' | 'pullRequestNumber'>,\n shallowClone: boolean,\n): string {\n const git = `git -C ${shellQuote(workdir)}`;\n if (pullRequestNumber === undefined) return `${git} fetch origin ${shellQuote(baseBranch)}`;\n const unshallow = shallowClone ? '--unshallow ' : '';\n return `${git} fetch ${unshallow}--filter=blob:none origin ${shellQuote(baseBranch)} && ${git} fetch --filter=blob:none origin refs/pull/${pullRequestNumber}/head`;\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: SessionBranchOptions,\n): Promise<void> {\n return timedPhase('workspace.checkout', () => checkoutSessionBranchImpl(sandbox, workdir, options));\n}\n\nasync function checkoutSessionBranchImpl(\n sandbox: ExecutableSandbox,\n workdir: string,\n options: SessionBranchOptions,\n): Promise<void> {\n const { branch, baseBranch, token, repoFullName, pullRequestNumber } = options;\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 pullRequestSession = pullRequestNumber !== undefined;\n if (pullRequestSession) {\n await sh(sandbox, `git -C ${shellQuote(workdir)} config credential.helper ${shellQuote(GH_CREDENTIAL_HELPER)}`);\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 shallowClone =\n pullRequestSession &&\n (await sh(sandbox, `git -C ${shellQuote(workdir)} rev-parse --is-shallow-repository`)).stdout.trim() === 'true';\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 `${startPointFetchCommands(workdir, options, shallowClone)} && 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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"}
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\nexport interface SessionBranchOptions {\n branch: string;\n baseBranch: string;\n token: string;\n repoFullName: string;\n /** A pull-request card's session starts on the PR head instead of the base tip. */\n pullRequestNumber?: number;\n}\n\n/**\n * Past file contents of a blob-less history load on demand; git asks gh, which\n * answers from the session's `GH_TOKEN`, so no credential is ever written.\n */\nconst GH_CREDENTIAL_HELPER = '!gh auth git-credential';\n\n/**\n * A pull-request session first fetches the base's whole commit history without\n * file contents, so `git log` and `git blame` work in the review, then the PR\n * head. Every other session starts from the shallow base tip as it is.\n */\nfunction startPointFetchCommands(\n workdir: string,\n { baseBranch, pullRequestNumber }: Pick<SessionBranchOptions, 'baseBranch' | 'pullRequestNumber'>,\n shallowClone: boolean,\n): string {\n const git = `git -C ${shellQuote(workdir)}`;\n if (pullRequestNumber === undefined) return `${git} fetch origin ${shellQuote(baseBranch)}`;\n const unshallow = shallowClone ? '--unshallow ' : '';\n return `${git} fetch ${unshallow}--filter=blob:none origin ${shellQuote(baseBranch)} && ${git} fetch --filter=blob:none origin refs/pull/${pullRequestNumber}/head`;\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: SessionBranchOptions,\n): Promise<void> {\n return timedPhase('workspace.checkout', () => checkoutSessionBranchImpl(sandbox, workdir, options));\n}\n\nasync function checkoutSessionBranchImpl(\n sandbox: ExecutableSandbox,\n workdir: string,\n options: SessionBranchOptions,\n): Promise<void> {\n const { branch, baseBranch, token, repoFullName, pullRequestNumber } = options;\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 pullRequestSession = pullRequestNumber !== undefined;\n // Every session pushes its branch over plain HTTPS; git authenticates through\n // gh's GH_TOKEN via this helper. Install it before the already-on-branch early\n // return so non-PR (issue/Linear/manual) sessions get it too — the helper\n // writes no credential, it just delegates to gh.\n await sh(sandbox, `git -C ${shellQuote(workdir)} config credential.helper ${shellQuote(GH_CREDENTIAL_HELPER)}`);\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 shallowClone =\n pullRequestSession &&\n (await sh(sandbox, `git -C ${shellQuote(workdir)} rev-parse --is-shallow-repository`)).stdout.trim() === 'true';\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 `${startPointFetchCommands(workdir, options, shallowClone)} && 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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"}
@@ -1,4 +1,5 @@
1
- import type { MemorySettingsRecord, MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';
1
+ import { type MemorySettingsRecord, type MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';
2
+ import type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';
2
3
  import type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';
3
4
  /** Default thresholds mirror the TUI `/om` fallbacks. */
4
5
  export declare const DEFAULT_OBSERVATION_THRESHOLD = 30000;
@@ -54,6 +55,7 @@ export interface MemorySettingsHydrationDependencies {
54
55
  sourceControl: {
55
56
  sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;
56
57
  };
58
+ projects: Pick<FactoryProjectsStorage, 'get'>;
57
59
  memorySettings: Pick<MemorySettingsStorage, 'get'>;
58
60
  }
59
61
  /**
@@ -71,14 +73,14 @@ export interface MemorySettingsHydrationDependencies {
71
73
  * comes from the row the session was created from, never improvised from an
72
74
  * owner id.
73
75
  *
74
- * Memory settings for sessions tagged `factoryProjectId` (work/review runs) are
75
- * owned by the start coordinator, and sessions without a GitHub source-control
76
- * row (e.g. chat-only channel sessions) hydrate through `hydrateFactorySession`
77
- * with their own resolved tenant; both are skipped here. The org seed is not
78
- * skipped on the tag alone: a web chat session persists `factoryProjectId` from
79
- * its browser seed, so on resume it carries the tag without ever having been
80
- * through the coordinator. Best-effort: failures are logged, never thrown.
76
+ * Tagged sessions may be coordinator-owned runs or web sessions that only carry
77
+ * browser-seeded Factory state. This path resolves their source-control row and
78
+ * reapplies a stored project settings row when one exists. If no project row
79
+ * exists, it preserves the coordinator's provider-aware fallback.
80
+ * Sessions without a GitHub source-control row (e.g. chat-only channel sessions)
81
+ * hydrate through `hydrateFactorySession` with their own resolved tenant.
82
+ * Best-effort: failures are logged, never thrown.
81
83
  */
82
- export declare function hydrateSessionMemorySettings(session: MemorySettingsHydrationSession, { sourceControl, memorySettings }: MemorySettingsHydrationDependencies): Promise<void>;
84
+ export declare function hydrateSessionMemorySettings(session: MemorySettingsHydrationSession, { sourceControl, projects, memorySettings }: MemorySettingsHydrationDependencies): Promise<void>;
83
85
  export {};
84
86
  //# sourceMappingURL=memory-settings-hydration.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory-settings-hydration.d.ts","sourceRoot":"","sources":["../../src/session/memory-settings-hydration.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAC9G,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAG5F,yDAAyD;AACzD,eAAO,MAAM,6BAA6B,QAAS,CAAC;AACpD,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,2DAA2D;AAC3D,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,WAAW,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D;AAED;;;;GAIG;AACH,UAAU,aAAa;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,CAAC,GAAG,EAAE,mBAAmB,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAAC;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,mFAAmF;AACnF,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE;QAAE,QAAQ,EAAE,WAAW,CAAC;QAAC,SAAS,EAAE,WAAW,CAAA;KAAE,CAAC;IACtD,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;QAC/C,GAAG,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KACvD,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,oBAAoB,GAAG,IAAI,EACnC,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED,MAAM,WAAW,8BAA+B,SAAQ,qBAAqB;IAC3E,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;CAChD;AAED,MAAM,WAAW,mCAAmC;IAClD,4FAA4F;IAC5F,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;KAC1E,CAAC;IACF,cAAc,EAAE,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,8BAA8B,EACvC,EAAE,aAAa,EAAE,cAAc,EAAE,EAAE,mCAAmC,GACrE,OAAO,CAAC,IAAI,CAAC,CAsBf"}
1
+ {"version":3,"file":"memory-settings-hydration.d.ts","sourceRoot":"","sources":["../../src/session/memory-settings-hydration.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC3B,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAG5F,yDAAyD;AACzD,eAAO,MAAM,6BAA6B,QAAS,CAAC;AACpD,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,2DAA2D;AAC3D,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,WAAW,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D;AAED;;;;GAIG;AACH,UAAU,aAAa;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,CAAC,GAAG,EAAE,mBAAmB,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAAC;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,mFAAmF;AACnF,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE;QAAE,QAAQ,EAAE,WAAW,CAAC;QAAC,SAAS,EAAE,WAAW,CAAA;KAAE,CAAC;IACtD,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;QAC/C,GAAG,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KACvD,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,oBAAoB,GAAG,IAAI,EACnC,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED,MAAM,WAAW,8BAA+B,SAAQ,qBAAqB;IAC3E,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;CAChD;AAED,MAAM,WAAW,mCAAmC;IAClD,4FAA4F;IAC5F,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;KAC1E,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,cAAc,EAAE,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,8BAA8B,EACvC,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,mCAAmC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAyCf"}
@@ -1,4 +1,6 @@
1
- import { hasResolvedOrg, seedSessionOrg } from "./org-seed.js";
1
+ import { seedSessionOrg } from "./org-seed.js";
2
+ import { factoryMemorySettingsUserId } from "../storage/domains/memory-settings/base.js";
3
+ import { resolveProviderOMDefault } from "@mastra/code-sdk/onboarding/packs";
2
4
  import { DEFAULT_OM_MODEL_ID } from "@mastra/code-sdk/constants";
3
5
  //#region src/session/memory-settings-hydration.ts
4
6
  /** Default thresholds mirror the TUI `/om` fallbacks. */
@@ -43,27 +45,38 @@ async function applyStoredMemorySettings(session, record, fallbackOmModelId) {
43
45
  * comes from the row the session was created from, never improvised from an
44
46
  * owner id.
45
47
  *
46
- * Memory settings for sessions tagged `factoryProjectId` (work/review runs) are
47
- * owned by the start coordinator, and sessions without a GitHub source-control
48
- * row (e.g. chat-only channel sessions) hydrate through `hydrateFactorySession`
49
- * with their own resolved tenant; both are skipped here. The org seed is not
50
- * skipped on the tag alone: a web chat session persists `factoryProjectId` from
51
- * its browser seed, so on resume it carries the tag without ever having been
52
- * through the coordinator. Best-effort: failures are logged, never thrown.
48
+ * Tagged sessions may be coordinator-owned runs or web sessions that only carry
49
+ * browser-seeded Factory state. This path resolves their source-control row and
50
+ * reapplies a stored project settings row when one exists. If no project row
51
+ * exists, it preserves the coordinator's provider-aware fallback.
52
+ * Sessions without a GitHub source-control row (e.g. chat-only channel sessions)
53
+ * hydrate through `hydrateFactorySession` with their own resolved tenant.
54
+ * Best-effort: failures are logged, never thrown.
53
55
  */
54
- async function hydrateSessionMemorySettings(session, { sourceControl, memorySettings }) {
56
+ async function hydrateSessionMemorySettings(session, { sourceControl, projects, memorySettings }) {
55
57
  const state = session.state.get() ?? {};
56
58
  const isFactoryRun = Boolean(state.factoryProjectId);
57
- if (isFactoryRun && hasResolvedOrg(state.factoryOrgId)) return;
58
59
  try {
59
60
  const record = await sourceControl.sessions.getBySessionId(session.identity.getResourceId());
60
61
  await seedSessionOrg(session, record?.orgId);
61
62
  if (!record) return;
62
- if (isFactoryRun) return;
63
- await applyStoredMemorySettings(session, await memorySettings.get({
63
+ const factoryProjectId = isFactoryRun ? String(state.factoryProjectId) : void 0;
64
+ const settings = await memorySettings.get({
64
65
  orgId: record.orgId,
65
- userId: record.userId
66
- }));
66
+ userId: factoryProjectId ? factoryMemorySettingsUserId(factoryProjectId) : record.userId
67
+ });
68
+ if (factoryProjectId && !settings) return;
69
+ const project = factoryProjectId ? await projects.get({
70
+ orgId: record.orgId,
71
+ id: factoryProjectId
72
+ }) : null;
73
+ const provider = project?.defaultModelId?.split("/")[0];
74
+ const fallbackOmModelId = provider ? resolveProviderOMDefault(provider, project.defaultModelId ?? void 0).modelId : void 0;
75
+ await applyStoredMemorySettings(session, factoryProjectId && settings && !fallbackOmModelId ? {
76
+ ...settings,
77
+ observerModelId: settings?.observerModelId ?? session.om.observer.modelId() ?? null,
78
+ reflectorModelId: settings?.reflectorModelId ?? session.om.reflector.modelId() ?? null
79
+ } : settings, fallbackOmModelId);
67
80
  } catch (error) {
68
81
  console.warn("[Factory memory-settings hydration] Unable to apply stored memory settings.", error);
69
82
  if (!session.state.get()?.factoryOrgId) await seedSessionOrg(session, void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"memory-settings-hydration.js","names":[],"sources":["../../src/session/memory-settings-hydration.ts"],"sourcesContent":["import { DEFAULT_OM_MODEL_ID } from '@mastra/code-sdk/constants';\n\nimport type { MemorySettingsRecord, MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { hasResolvedOrg, seedSessionOrg } from './org-seed.js';\n\n/** Default thresholds mirror the TUI `/om` fallbacks. */\nexport const DEFAULT_OBSERVATION_THRESHOLD = 30_000;\nexport const DEFAULT_REFLECTION_THRESHOLD = 40_000;\n\n/** One observational-memory role's read/switch surface. */\ninterface OMRoleSlice {\n modelId: () => string | undefined;\n switchModel: (args: { modelId: string }) => Promise<unknown>;\n}\n\n/**\n * Session-state fields memory-settings hydration writes. The index signatures\n * mirror `MastraCodeState` so the concrete `Session.state.set(Partial<MastraCodeState>)`\n * stays assignable to this minimal surface (contravariant parameter check).\n */\ninterface OMStateWrites {\n [key: string]: unknown;\n [key: `subagentModelId_${string}`]: string | undefined;\n observationThreshold?: number;\n reflectionThreshold?: number;\n observeAttachments?: 'auto' | boolean;\n factoryOrgId?: string;\n}\n\n/** The slice of a session needed to apply stored observational-memory settings. */\nexport interface OMConfigurableSession {\n om: { observer: OMRoleSlice; reflector: OMRoleSlice };\n state: {\n get: () => Record<string, unknown> | undefined;\n set: (updates: OMStateWrites) => Promise<void> | void;\n };\n}\n\n/**\n * Apply a stored memory-settings row onto a session, so the DB — not whatever\n * happens to sit in persisted session state (e.g. a stale boot-time seed from\n * before memory settings moved to the DB) — is what the web surface reads and\n * what the session's OM actually runs with. The row is authoritative: knobs\n * without a stored value reset to the built-in defaults. This is the single\n * application path shared by the settings routes, coordinator hydration, and\n * the web session boot seed.\n */\nexport async function applyStoredMemorySettings(\n session: OMConfigurableSession,\n record: MemorySettingsRecord | null,\n fallbackOmModelId?: string,\n): Promise<void> {\n for (const role of ['observer', 'reflector'] as const) {\n const stored = role === 'observer' ? record?.observerModelId : record?.reflectorModelId;\n const target = stored ?? fallbackOmModelId ?? DEFAULT_OM_MODEL_ID;\n if (session.om[role].modelId() !== target) {\n await session.om[role].switchModel({ modelId: target });\n }\n }\n const state = session.state.get() ?? {};\n const updates: OMStateWrites = {};\n const observationThreshold = record?.observationThreshold ?? DEFAULT_OBSERVATION_THRESHOLD;\n if (state.observationThreshold !== observationThreshold) {\n updates.observationThreshold = observationThreshold;\n }\n const reflectionThreshold = record?.reflectionThreshold ?? DEFAULT_REFLECTION_THRESHOLD;\n if (state.reflectionThreshold !== reflectionThreshold) {\n updates.reflectionThreshold = reflectionThreshold;\n }\n const observeAttachments = record?.observeAttachments ?? 'auto';\n if ((state.observeAttachments ?? 'auto') !== observeAttachments) {\n updates.observeAttachments = observeAttachments;\n }\n if (Object.keys(updates).length > 0) await session.state.set(updates);\n}\n\nexport interface MemorySettingsHydrationSession extends OMConfigurableSession {\n readonly identity: { getResourceId(): string };\n}\n\nexport interface MemorySettingsHydrationDependencies {\n /** GitHub-integration source-control rows — the only creator of web user sessions today. */\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n memorySettings: Pick<MemorySettingsStorage, 'get'>;\n}\n\n/**\n * Seed a freshly created controller session's tenant org and its\n * observational-memory settings from the owner's source-control row. Registered\n * as a blocking session-created listener so the seed lands before the caller can\n * start a run.\n *\n * The org seed matters beyond settings. Subconscious knowledge curation scopes\n * every node and record on `factoryOrgId`; before the SDK refusal guard,\n * missing it made curation substitute the session owner id. For web chat sessions\n * that is the agent controller's own id rather than a tenant, so curated\n * knowledge landed under an org rung no reader ever queries. Same rule as the\n * start coordinator: the org\n * comes from the row the session was created from, never improvised from an\n * owner id.\n *\n * Memory settings for sessions tagged `factoryProjectId` (work/review runs) are\n * owned by the start coordinator, and sessions without a GitHub source-control\n * row (e.g. chat-only channel sessions) hydrate through `hydrateFactorySession`\n * with their own resolved tenant; both are skipped here. The org seed is not\n * skipped on the tag alone: a web chat session persists `factoryProjectId` from\n * its browser seed, so on resume it carries the tag without ever having been\n * through the coordinator. Best-effort: failures are logged, never thrown.\n */\nexport async function hydrateSessionMemorySettings(\n session: MemorySettingsHydrationSession,\n { sourceControl, memorySettings }: MemorySettingsHydrationDependencies,\n): Promise<void> {\n const state = session.state.get() ?? {};\n const isFactoryRun = Boolean(state.factoryProjectId);\n // A coordinator-hydrated run already carries both halves. Nothing to add.\n if (isFactoryRun && hasResolvedOrg(state.factoryOrgId)) return;\n try {\n const record = await sourceControl.sessions.getBySessionId(session.identity.getResourceId());\n // No row, or a row whose org is blank, leaves the session with no tenant.\n // Mark it rather than returning silently: an unmarked projectless factory\n // session is indistinguishable from a local one, and curation would file it\n // under the local scope — the same bug wearing a different rung.\n await seedSessionOrg(session, record?.orgId);\n if (!record) return;\n if (isFactoryRun) return;\n const settings = await memorySettings.get({ orgId: record.orgId, userId: record.userId });\n await applyStoredMemorySettings(session, settings);\n } catch (error) {\n console.warn('[Factory memory-settings hydration] Unable to apply stored memory settings.', error);\n // A failed lookup is an unresolved org, not an absent one — unless the seed\n // already landed and a later step is what threw.\n if (!session.state.get()?.factoryOrgId) await seedSessionOrg(session, undefined);\n }\n}\n"],"mappings":";;;;AAOA,MAAa,gCAAgC;AAC7C,MAAa,+BAA+B;;;;;;;;;;AAwC5C,eAAsB,0BACpB,SACA,QACA,mBACe;CACf,KAAK,MAAM,QAAQ,CAAC,YAAY,WAAW,GAAY;EAErD,MAAM,UADS,SAAS,aAAa,QAAQ,kBAAkB,QAAQ,qBAC9C,qBAAqB;EAC9C,IAAI,QAAQ,GAAG,KAAK,CAAC,QAAQ,MAAM,QACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,EAAE,SAAS,OAAO,CAAC;CAE1D;CACA,MAAM,QAAQ,QAAQ,MAAM,IAAI,KAAK,CAAC;CACtC,MAAM,UAAyB,CAAC;CAChC,MAAM,uBAAuB,QAAQ,wBAAA;CACrC,IAAI,MAAM,yBAAyB,sBACjC,QAAQ,uBAAuB;CAEjC,MAAM,sBAAsB,QAAQ,uBAAA;CACpC,IAAI,MAAM,wBAAwB,qBAChC,QAAQ,sBAAsB;CAEhC,MAAM,qBAAqB,QAAQ,sBAAsB;CACzD,KAAK,MAAM,sBAAsB,YAAY,oBAC3C,QAAQ,qBAAqB;CAE/B,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAAG,MAAM,QAAQ,MAAM,IAAI,OAAO;AACtE;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,eAAsB,6BACpB,SACA,EAAE,eAAe,kBACF;CACf,MAAM,QAAQ,QAAQ,MAAM,IAAI,KAAK,CAAC;CACtC,MAAM,eAAe,QAAQ,MAAM,gBAAgB;CAEnD,IAAI,gBAAgB,eAAe,MAAM,YAAY,GAAG;CACxD,IAAI;EACF,MAAM,SAAS,MAAM,cAAc,SAAS,eAAe,QAAQ,SAAS,cAAc,CAAC;EAK3F,MAAM,eAAe,SAAS,QAAQ,KAAK;EAC3C,IAAI,CAAC,QAAQ;EACb,IAAI,cAAc;EAElB,MAAM,0BAA0B,SAAS,MADlB,eAAe,IAAI;GAAE,OAAO,OAAO;GAAO,QAAQ,OAAO;EAAO,CAAC,CACvC;CACnD,SAAS,OAAO;EACd,QAAQ,KAAK,+EAA+E,KAAK;EAGjG,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,EAAE,cAAc,MAAM,eAAe,SAAS,KAAA,CAAS;CACjF;AACF"}
1
+ {"version":3,"file":"memory-settings-hydration.js","names":[],"sources":["../../src/session/memory-settings-hydration.ts"],"sourcesContent":["import { DEFAULT_OM_MODEL_ID } from '@mastra/code-sdk/constants';\nimport { resolveProviderOMDefault } from '@mastra/code-sdk/onboarding/packs';\n\nimport {\n factoryMemorySettingsUserId,\n type MemorySettingsRecord,\n type MemorySettingsStorage,\n} from '../storage/domains/memory-settings/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { seedSessionOrg } from './org-seed.js';\n\n/** Default thresholds mirror the TUI `/om` fallbacks. */\nexport const DEFAULT_OBSERVATION_THRESHOLD = 30_000;\nexport const DEFAULT_REFLECTION_THRESHOLD = 40_000;\n\n/** One observational-memory role's read/switch surface. */\ninterface OMRoleSlice {\n modelId: () => string | undefined;\n switchModel: (args: { modelId: string }) => Promise<unknown>;\n}\n\n/**\n * Session-state fields memory-settings hydration writes. The index signatures\n * mirror `MastraCodeState` so the concrete `Session.state.set(Partial<MastraCodeState>)`\n * stays assignable to this minimal surface (contravariant parameter check).\n */\ninterface OMStateWrites {\n [key: string]: unknown;\n [key: `subagentModelId_${string}`]: string | undefined;\n observationThreshold?: number;\n reflectionThreshold?: number;\n observeAttachments?: 'auto' | boolean;\n factoryOrgId?: string;\n}\n\n/** The slice of a session needed to apply stored observational-memory settings. */\nexport interface OMConfigurableSession {\n om: { observer: OMRoleSlice; reflector: OMRoleSlice };\n state: {\n get: () => Record<string, unknown> | undefined;\n set: (updates: OMStateWrites) => Promise<void> | void;\n };\n}\n\n/**\n * Apply a stored memory-settings row onto a session, so the DB — not whatever\n * happens to sit in persisted session state (e.g. a stale boot-time seed from\n * before memory settings moved to the DB) — is what the web surface reads and\n * what the session's OM actually runs with. The row is authoritative: knobs\n * without a stored value reset to the built-in defaults. This is the single\n * application path shared by the settings routes, coordinator hydration, and\n * the web session boot seed.\n */\nexport async function applyStoredMemorySettings(\n session: OMConfigurableSession,\n record: MemorySettingsRecord | null,\n fallbackOmModelId?: string,\n): Promise<void> {\n for (const role of ['observer', 'reflector'] as const) {\n const stored = role === 'observer' ? record?.observerModelId : record?.reflectorModelId;\n const target = stored ?? fallbackOmModelId ?? DEFAULT_OM_MODEL_ID;\n if (session.om[role].modelId() !== target) {\n await session.om[role].switchModel({ modelId: target });\n }\n }\n const state = session.state.get() ?? {};\n const updates: OMStateWrites = {};\n const observationThreshold = record?.observationThreshold ?? DEFAULT_OBSERVATION_THRESHOLD;\n if (state.observationThreshold !== observationThreshold) {\n updates.observationThreshold = observationThreshold;\n }\n const reflectionThreshold = record?.reflectionThreshold ?? DEFAULT_REFLECTION_THRESHOLD;\n if (state.reflectionThreshold !== reflectionThreshold) {\n updates.reflectionThreshold = reflectionThreshold;\n }\n const observeAttachments = record?.observeAttachments ?? 'auto';\n if ((state.observeAttachments ?? 'auto') !== observeAttachments) {\n updates.observeAttachments = observeAttachments;\n }\n if (Object.keys(updates).length > 0) await session.state.set(updates);\n}\n\nexport interface MemorySettingsHydrationSession extends OMConfigurableSession {\n readonly identity: { getResourceId(): string };\n}\n\nexport interface MemorySettingsHydrationDependencies {\n /** GitHub-integration source-control rows — the only creator of web user sessions today. */\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n projects: Pick<FactoryProjectsStorage, 'get'>;\n memorySettings: Pick<MemorySettingsStorage, 'get'>;\n}\n\n/**\n * Seed a freshly created controller session's tenant org and its\n * observational-memory settings from the owner's source-control row. Registered\n * as a blocking session-created listener so the seed lands before the caller can\n * start a run.\n *\n * The org seed matters beyond settings. Subconscious knowledge curation scopes\n * every node and record on `factoryOrgId`; before the SDK refusal guard,\n * missing it made curation substitute the session owner id. For web chat sessions\n * that is the agent controller's own id rather than a tenant, so curated\n * knowledge landed under an org rung no reader ever queries. Same rule as the\n * start coordinator: the org\n * comes from the row the session was created from, never improvised from an\n * owner id.\n *\n * Tagged sessions may be coordinator-owned runs or web sessions that only carry\n * browser-seeded Factory state. This path resolves their source-control row and\n * reapplies a stored project settings row when one exists. If no project row\n * exists, it preserves the coordinator's provider-aware fallback.\n * Sessions without a GitHub source-control row (e.g. chat-only channel sessions)\n * hydrate through `hydrateFactorySession` with their own resolved tenant.\n * Best-effort: failures are logged, never thrown.\n */\nexport async function hydrateSessionMemorySettings(\n session: MemorySettingsHydrationSession,\n { sourceControl, projects, memorySettings }: MemorySettingsHydrationDependencies,\n): Promise<void> {\n const state = session.state.get() ?? {};\n const isFactoryRun = Boolean(state.factoryProjectId);\n try {\n const record = await sourceControl.sessions.getBySessionId(session.identity.getResourceId());\n // No row, or a row whose org is blank, leaves the session with no tenant.\n // Mark it rather than returning silently: an unmarked projectless factory\n // session is indistinguishable from a local one, and curation would file it\n // under the local scope — the same bug wearing a different rung.\n await seedSessionOrg(session, record?.orgId);\n if (!record) return;\n const factoryProjectId = isFactoryRun ? String(state.factoryProjectId) : undefined;\n const settings = await memorySettings.get({\n orgId: record.orgId,\n userId: factoryProjectId ? factoryMemorySettingsUserId(factoryProjectId) : record.userId,\n });\n // Coordinator hydration applies a provider-aware fallback when no project row\n // exists. Do not replace that fallback with the generic OM default here.\n if (factoryProjectId && !settings) return;\n const project = factoryProjectId ? await projects.get({ orgId: record.orgId, id: factoryProjectId }) : null;\n const provider = project?.defaultModelId?.split('/')[0];\n const fallbackOmModelId = provider\n ? resolveProviderOMDefault(provider, project.defaultModelId ?? undefined).modelId\n : undefined;\n await applyStoredMemorySettings(\n session,\n factoryProjectId && settings && !fallbackOmModelId\n ? {\n ...settings,\n observerModelId: settings?.observerModelId ?? session.om.observer.modelId() ?? null,\n reflectorModelId: settings?.reflectorModelId ?? session.om.reflector.modelId() ?? null,\n }\n : settings,\n fallbackOmModelId,\n );\n } catch (error) {\n console.warn('[Factory memory-settings hydration] Unable to apply stored memory settings.', error);\n // A failed lookup is an unresolved org, not an absent one — unless the seed\n // already landed and a later step is what threw.\n if (!session.state.get()?.factoryOrgId) await seedSessionOrg(session, undefined);\n }\n}\n"],"mappings":";;;;;;AAaA,MAAa,gCAAgC;AAC7C,MAAa,+BAA+B;;;;;;;;;;AAwC5C,eAAsB,0BACpB,SACA,QACA,mBACe;CACf,KAAK,MAAM,QAAQ,CAAC,YAAY,WAAW,GAAY;EAErD,MAAM,UADS,SAAS,aAAa,QAAQ,kBAAkB,QAAQ,qBAC9C,qBAAqB;EAC9C,IAAI,QAAQ,GAAG,KAAK,CAAC,QAAQ,MAAM,QACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,EAAE,SAAS,OAAO,CAAC;CAE1D;CACA,MAAM,QAAQ,QAAQ,MAAM,IAAI,KAAK,CAAC;CACtC,MAAM,UAAyB,CAAC;CAChC,MAAM,uBAAuB,QAAQ,wBAAA;CACrC,IAAI,MAAM,yBAAyB,sBACjC,QAAQ,uBAAuB;CAEjC,MAAM,sBAAsB,QAAQ,uBAAA;CACpC,IAAI,MAAM,wBAAwB,qBAChC,QAAQ,sBAAsB;CAEhC,MAAM,qBAAqB,QAAQ,sBAAsB;CACzD,KAAK,MAAM,sBAAsB,YAAY,oBAC3C,QAAQ,qBAAqB;CAE/B,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAAG,MAAM,QAAQ,MAAM,IAAI,OAAO;AACtE;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,eAAsB,6BACpB,SACA,EAAE,eAAe,UAAU,kBACZ;CACf,MAAM,QAAQ,QAAQ,MAAM,IAAI,KAAK,CAAC;CACtC,MAAM,eAAe,QAAQ,MAAM,gBAAgB;CACnD,IAAI;EACF,MAAM,SAAS,MAAM,cAAc,SAAS,eAAe,QAAQ,SAAS,cAAc,CAAC;EAK3F,MAAM,eAAe,SAAS,QAAQ,KAAK;EAC3C,IAAI,CAAC,QAAQ;EACb,MAAM,mBAAmB,eAAe,OAAO,MAAM,gBAAgB,IAAI,KAAA;EACzE,MAAM,WAAW,MAAM,eAAe,IAAI;GACxC,OAAO,OAAO;GACd,QAAQ,mBAAmB,4BAA4B,gBAAgB,IAAI,OAAO;EACpF,CAAC;EAGD,IAAI,oBAAoB,CAAC,UAAU;EACnC,MAAM,UAAU,mBAAmB,MAAM,SAAS,IAAI;GAAE,OAAO,OAAO;GAAO,IAAI;EAAiB,CAAC,IAAI;EACvG,MAAM,WAAW,SAAS,gBAAgB,MAAM,GAAG,CAAC,CAAC;EACrD,MAAM,oBAAoB,WACtB,yBAAyB,UAAU,QAAQ,kBAAkB,KAAA,CAAS,CAAC,CAAC,UACxE,KAAA;EACJ,MAAM,0BACJ,SACA,oBAAoB,YAAY,CAAC,oBAC7B;GACE,GAAG;GACH,iBAAiB,UAAU,mBAAmB,QAAQ,GAAG,SAAS,QAAQ,KAAK;GAC/E,kBAAkB,UAAU,oBAAoB,QAAQ,GAAG,UAAU,QAAQ,KAAK;EACpF,IACA,UACJ,iBACF;CACF,SAAS,OAAO;EACd,QAAQ,KAAK,+EAA+E,KAAK;EAGjG,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,EAAE,cAAc,MAAM,eAAe,SAAS,KAAA,CAAS;CACjF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAE/E,OAAO,KAAK,EAAE,mBAAmB,EAA2B,MAAM,mCAAmC,CAAC;AAItG,OAAO,EAAoB,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,KAAK,EAEV,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAwB9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAQ7E,eAAO,MAAM,2BAA2B,QAQQ,CAAC;AAEjD;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,SAAS,CAS7F;AAED,eAAO,MAAM,mBAAmB,aAO9B,CAAC;AAEH,qBAAa,kBAAmB,YAAW,WAAW;;IAMlD,QAAQ,CAAC,QAAQ,EAAE,WAAW;gBAArB,QAAQ,EAAE,WAAW,EAC9B,kBAAkB,EAAE,MAAM,EAAE,EAC5B,eAAe,GAAE,MAAM,GAAG,SAA2C;IAqBjE,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAO3C,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAMjD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;IAMrD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAyB7D,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAI7C;AAuDD,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzE;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEnD,MAAM,WAAW,6BAA6B;IAC5C,iEAAiE;IACjE,OAAO,CAAC,EAAE,0BAA0B,CAAC;IACrC,4BAA4B;IAC5B,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,gFAAgF;IAChF,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;iEAE6D;IAC7D,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;IAC9D,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC/C,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;CAC9C;AAED,KAAK,mBAAmB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEtD,gFAAgF;AAChF,qBAAa,wBAAwB;;IAInC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAI/B,QAAQ,CACZ,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,mBAAmB,GAC9B,OAAO,CAAC,OAAO,CAAC;IAWb,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS1D;AAED,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,6BAAkC,IAuBlE,4CAA4C,uBAAuB,2MAkdlF"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAE/E,OAAO,KAAK,EAAE,mBAAmB,EAA2B,MAAM,mCAAmC,CAAC;AAItG,OAAO,EAAoB,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,KAAK,EAEV,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAwB9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAQ7E,eAAO,MAAM,2BAA2B,QAQQ,CAAC;AAEjD;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,SAAS,CAS7F;AAED,eAAO,MAAM,mBAAmB,aAO9B,CAAC;AAEH,qBAAa,kBAAmB,YAAW,WAAW;;IAMlD,QAAQ,CAAC,QAAQ,EAAE,WAAW;gBAArB,QAAQ,EAAE,WAAW,EAC9B,kBAAkB,EAAE,MAAM,EAAE,EAC5B,eAAe,GAAE,MAAM,GAAG,SAA2C;IAqBjE,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAO3C,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAMjD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;IAMrD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAyB7D,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAI7C;AAuDD,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzE;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEnD,MAAM,WAAW,6BAA6B;IAC5C,iEAAiE;IACjE,OAAO,CAAC,EAAE,0BAA0B,CAAC;IACrC,4BAA4B;IAC5B,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,gFAAgF;IAChF,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;iEAE6D;IAC7D,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;IAC9D,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC/C,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;CAC9C;AAED,KAAK,mBAAmB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEtD,gFAAgF;AAChF,qBAAa,wBAAwB;;IAInC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAI/B,QAAQ,CACZ,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,mBAAmB,GAC9B,OAAO,CAAC,OAAO,CAAC;IAWb,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS1D;AAED,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,6BAAkC,IAuBlE,4CAA4C,uBAAuB,2MA8elF"}
package/dist/workspace.js CHANGED
@@ -227,17 +227,34 @@ function createWorkspaceFactory(options = {}) {
227
227
  await guardedSetup(args);
228
228
  if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) throw retiredError();
229
229
  const target = requireExec(args.sandbox);
230
+ const publishStartSideEffects = () => {
231
+ storage.sessions.setSandbox({
232
+ id: session.id,
233
+ sandboxId: target.id,
234
+ sandboxWorkdir: sessionEntry.workdir ?? ""
235
+ }).catch(() => {});
236
+ constructedWorkspaces.get(workspaceId)?.skills?.refresh().catch(() => {});
237
+ };
238
+ const existingRegistration = githubTokenInjectors.get(workspaceId);
239
+ if (existingRegistration) {
240
+ existingRegistration.inject = (freshToken) => {
241
+ if (!target.setEnv) throw new Error("The active sandbox provider does not support runtime GitHub token refresh.");
242
+ target.setEnv((env) => ({
243
+ ...env,
244
+ GH_TOKEN: freshToken
245
+ }));
246
+ existingRegistration.ghToken = freshToken;
247
+ };
248
+ existingRegistration.inject(existingRegistration.ghToken);
249
+ publishStartSideEffects();
250
+ return;
251
+ }
230
252
  const patKind = await resolveGithubPatKind("default");
231
253
  const ghCliToken = await getGithubPat(() => github.integrationStorage, session.orgId, patKind) ?? await getRepositoryToken();
232
254
  target.setEnv?.((env) => ({
233
255
  ...env,
234
256
  GH_TOKEN: ghCliToken
235
257
  }));
236
- storage.sessions.setSandbox({
237
- id: session.id,
238
- sandboxId: target.id,
239
- sandboxWorkdir: sessionEntry.workdir ?? ""
240
- }).catch(() => {});
241
258
  const tokenRegistration = {
242
259
  inject: (freshToken) => {
243
260
  if (!target.setEnv) throw new Error("The active sandbox provider does not support runtime GitHub token refresh.");
@@ -254,7 +271,7 @@ function createWorkspaceFactory(options = {}) {
254
271
  };
255
272
  githubTokenInjectors.set(workspaceId, tokenRegistration);
256
273
  registerGithubTokenContext(tokenRegistration);
257
- constructedWorkspaces.get(workspaceId)?.skills?.refresh().catch(() => {});
274
+ publishStartSideEffects();
258
275
  };
259
276
  const constructSessionEntry = () => getSessionSandbox(session.id, repoFullName, () => {
260
277
  const sandbox = createSessionSandboxInstance({