@mastra/factory 0.8.0-alpha.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +660 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +42 -4
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +8 -0
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/app-identity.d.ts +43 -0
- package/dist/integrations/github/app-identity.d.ts.map +1 -0
- package/dist/integrations/github/app-identity.js +62 -0
- package/dist/integrations/github/app-identity.js.map +1 -0
- package/dist/integrations/github/integration.d.ts +9 -0
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +9 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.d.ts +5 -0
- package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +7 -0
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +11 -6
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts +8 -0
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +30 -8
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +39 -6
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +48 -14
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/webhook.d.ts +39 -0
- package/dist/integrations/github/webhook.d.ts.map +1 -1
- package/dist/integrations/github/webhook.js +74 -13
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/linear/integration.d.ts.map +1 -1
- package/dist/integrations/linear/integration.js +1 -0
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.d.ts +10 -0
- package/dist/integrations/linear/routes.d.ts.map +1 -1
- package/dist/integrations/linear/routes.js +35 -1
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.d.ts.map +1 -1
- package/dist/integrations/linear/rules.js +2 -1
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.d.ts +11 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +64 -16
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts +12 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +47 -4
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
- package/dist/integrations/platform/linear/integration.js +1 -0
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +2 -1
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/routes/config.d.ts +12 -6
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +97 -56
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +2 -0
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.d.ts +15 -0
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +65 -2
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/knowledge.d.ts +152 -0
- package/dist/routes/knowledge.d.ts.map +1 -0
- package/dist/routes/knowledge.js +442 -0
- package/dist/routes/knowledge.js.map +1 -0
- package/dist/routes/projects.d.ts +9 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +8 -0
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/surface.d.ts +6 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -0
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +3 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.d.ts.map +1 -1
- package/dist/rules/binding-context.js +4 -1
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.d.ts.map +1 -1
- package/dist/rules/defaults.js +87 -4
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +6 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +118 -16
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +20 -7
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.d.ts +1 -0
- package/dist/rules/resolve.d.ts.map +1 -1
- package/dist/rules/resolve.js +3 -2
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +2 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +3 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/terminal-cleanup.d.ts +23 -0
- package/dist/rules/terminal-cleanup.d.ts.map +1 -0
- package/dist/rules/terminal-cleanup.js +43 -0
- package/dist/rules/terminal-cleanup.js.map +1 -0
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +24 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.d.ts +3 -0
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +10 -6
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +32 -3
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +2 -0
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.d.ts.map +1 -1
- package/dist/rules/validation.js +8 -3
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/base-checkpoint-triggers.d.ts +77 -0
- package/dist/sandbox/base-checkpoint-triggers.d.ts.map +1 -0
- package/dist/sandbox/base-checkpoint-triggers.js +137 -0
- package/dist/sandbox/base-checkpoint-triggers.js.map +1 -0
- package/dist/sandbox/base-checkpoint.d.ts +59 -0
- package/dist/sandbox/base-checkpoint.d.ts.map +1 -0
- package/dist/sandbox/base-checkpoint.js +145 -0
- package/dist/sandbox/base-checkpoint.js.map +1 -0
- package/dist/sandbox/fleet.d.ts +32 -0
- package/dist/sandbox/fleet.d.ts.map +1 -1
- package/dist/sandbox/fleet.js +39 -10
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/session/checkpoint-capture.d.ts +3 -2
- package/dist/session/checkpoint-capture.d.ts.map +1 -1
- package/dist/session/checkpoint-capture.js +3 -3
- package/dist/session/checkpoint-capture.js.map +1 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +2 -1
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +7 -0
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +41 -4
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/model-pack-hydration.d.ts +55 -0
- package/dist/session/model-pack-hydration.d.ts.map +1 -0
- package/dist/session/model-pack-hydration.js +56 -0
- package/dist/session/model-pack-hydration.js.map +1 -0
- package/dist/skills/service.d.ts +9 -0
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +10 -1
- package/dist/skills/service.js.map +1 -1
- package/dist/storage/domains/intake/base.d.ts +36 -0
- package/dist/storage/domains/intake/base.d.ts.map +1 -1
- package/dist/storage/domains/intake/base.js +91 -2
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/model-packs/base.d.ts +26 -0
- package/dist/storage/domains/model-packs/base.d.ts.map +1 -1
- package/dist/storage/domains/model-packs/base.js +102 -13
- package/dist/storage/domains/model-packs/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +47 -1
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +78 -19
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +11 -3
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +10 -1
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +67 -1
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +183 -12
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/workspace.d.ts +31 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +225 -54
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-plan/SKILL.md +1 -1
- package/factory-skills/factory-rereview/SKILL.md +3 -3
- package/factory-skills/factory-review/SKILL.md +3 -3
- package/factory-skills/factory-triage/SKILL.md +2 -0
- package/package.json +8 -8
|
@@ -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. Instead each project gets\n * its own isolated sandbox (provisioned by the fleet in `../sandbox/fleet`) and\n * the repo is cloned *inside* that sandbox. The agent's file tools and command\n * tools then operate entirely against the remote checkout.\n *\n * - `ensureProjectSandbox(row)` / `teardownProjectSandbox(row)` bind the fleet's\n * provision/reattach/teardown lifecycle to the per-(project,user) sandbox row.\n * - `materializeRepo(row, token)` runs `git clone` (first open) or `git pull`\n * (re-open) inside the sandbox, using a short-lived installation token that is\n * scrubbed from the git remote afterwards so it never persists in the VM.\n *\n * This module owns everything git/GitHub: clone/pull, commit/push, worktrees,\n * and `gh pr create`. Sandbox provisioning, budgets, and workdir layout live in\n * the fleet module.\n */\n\nimport { createHash } from 'node:crypto';\nimport { reportProgress } from '../../sandbox/fleet.js';\nimport type {\n MaterializationSandbox,\n ProgressFn,\n SandboxBindingStore,\n SandboxCommandResult,\n SandboxFleet,\n} from '../../sandbox/fleet.js';\nimport type {\n ProjectRepositorySandbox,\n SourceControlStorageHandle,\n} from '../../storage/domains/source-control/base.js';\nimport { timedPhase } from '../../timing.js';\n\ntype SourceControlSandboxStorage = SourceControlStorageHandle['sandboxes'];\ntype MaterializationStore = Pick<SourceControlSandboxStorage, 'markMaterialized'>;\n\ninterface RepoMaterializationBinding {\n id: string;\n sandboxWorkdir: string;\n materializedAt: Date | null;\n}\n\n/** Adapt a per-(project,user) sandbox binding row to the fleet's persistence seam. */\nfunction bindingStore(row: ProjectRepositorySandbox, storage: SourceControlSandboxStorage): SandboxBindingStore {\n return {\n sandboxId: row.sandboxId,\n setSandboxId: id =>\n id === null ? storage.clearBinding({ id: row.id }) : storage.setSandboxId({ id: row.id, sandboxId: id }),\n clear: () => storage.clearBinding({ id: row.id }),\n };\n}\n\n/**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n */\nexport async function ensureProjectSandbox(options: {\n fleet: SandboxFleet;\n row: ProjectRepositorySandbox;\n storage: SourceControlSandboxStorage;\n token: string;\n onProgress?: ProgressFn;\n}): Promise<MaterializationSandbox> {\n const { fleet, row, storage, token, onProgress } = options;\n return fleet.ensureSandbox(bindingStore(row, storage), { GH_TOKEN: token }, onProgress, {\n actingUserId: row.userId,\n });\n}\n\n/**\n * Tear down a user's sandbox for a project: stop the live VM (best-effort) and\n * clear the persisted `sandboxId`/`materializedAt` on the per-(project,user)\n * binding row so the next open re-provisions cleanly.\n *\n * @param row the per-(project,user) sandbox binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\nexport async function teardownProjectSandbox(options: {\n fleet: SandboxFleet;\n row: ProjectRepositorySandbox;\n storage: SourceControlSandboxStorage;\n sandbox?: MaterializationSandbox;\n}): Promise<void> {\n const { fleet, row, storage, sandbox } = options;\n return fleet.teardownSandbox(bindingStore(row, storage), sandbox);\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 */\nasync function sh(\n sandbox: MaterializationSandbox,\n script: string,\n options: ShOptions = {},\n): Promise<SandboxCommandResult> {\n // One budget for the command as a whole: each attempt only gets the time\n // remaining, so transport retries can never multiply the hang guard.\n const deadlineMs = Date.now() + (options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);\n for (let attempt = 0; ; attempt++) {\n try {\n return await shOnce(sandbox, script, { ...options, timeoutMs: Math.max(deadlineMs - Date.now(), 1) });\n } catch (error) {\n if (attempt >= SH_RETRIES || !isTransientTransportError(error)) throw error;\n const delayMs = SH_RETRY_DELAY_MS * (attempt + 1);\n if (deadlineMs - Date.now() <= delayMs) throw error;\n await new Promise(resolve => setTimeout(resolve, delayMs));\n }\n }\n}\n\n/** Single `sh -c` execution attempt, bounded by the hang guard. */\nasync function shOnce(\n sandbox: MaterializationSandbox,\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: MaterializationSandbox,\n script: string,\n options: ShOptions & { beforeRetry?: (attempt: number) => Promise<void> } = {},\n): Promise<SandboxCommandResult> {\n const { beforeRetry, ...shOptions } = options;\n const deadlineMs = Date.now() + (shOptions.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);\n for (let attempt = 0; ; attempt++) {\n const result = await sh(sandbox, script, {\n ...shOptions,\n timeoutMs: Math.max(deadlineMs - Date.now(), 1),\n });\n if (result.exitCode === 0 || attempt >= GIT_TRANSFER_RETRIES || !isTransientGitFailure(result)) return result;\n const delayMs = GIT_TRANSFER_RETRY_DELAY_MS * (attempt + 1);\n if (deadlineMs - Date.now() <= delayMs) return result;\n await new Promise(resolve => setTimeout(resolve, delayMs));\n await beforeRetry?.(attempt + 1);\n }\n}\n\n/** Error raised when the sandbox cannot materialize the repo (actionable). */\nexport class MaterializeError extends Error {\n constructor(\n message: string,\n readonly code:\n | 'git-missing'\n | 'egress-blocked'\n | 'clone-failed'\n | 'pull-failed'\n | 'push-failed'\n | 'commit-failed'\n | 'gh-missing'\n | 'pr-failed',\n ) {\n super(message);\n this.name = 'MaterializeError';\n }\n}\n\n/**\n * Build the token-auth clone/pull URL for a repo. The token lives only inside\n * this URL and is scrubbed from the remote after the operation.\n */\nfunction tokenUrl(repoFullName: string, token: string): string {\n return `https://x-access-token:${token}@github.com/${repoFullName}.git`;\n}\n\nfunction cleanUrl(repoFullName: string): string {\n return `https://github.com/${repoFullName}.git`;\n}\n\n/** Repo metadata needed to materialize, read from the org-owned project row. */\nexport interface RepoMaterializeInfo {\n repoFullName: string;\n defaultBranch: string;\n}\n\n/** Options for {@link materializeRepo}. */\nexport interface MaterializeRepoOptions {\n /** The per-(project,user) sandbox binding (provisioned via `ensureProjectSandbox`). */\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: MaterializationSandbox;\n /** A freshly minted, short-lived installation access token. */\n token: string;\n storage: MaterializationStore;\n onProgress?: ProgressFn;\n}\n\n/**\n * Materialize the repo inside the user's sandbox. Clones on first open, pulls on\n * re-open. Always scrubs the install token from the remote afterwards and sets\n * `materialized_at` on the per-user sandbox binding row.\n *\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, onProgress } = options;\n const workdir = sandboxRow.sandboxWorkdir;\n const repo = repoInfo.repoFullName;\n\n // 0. Defense in depth: never build a git command from values that aren't\n // strictly shaped, even if a malformed row reached the DB. Inputs are also\n // validated at the route boundary before storage.\n if (!/^[\\w.-]+\\/[\\w.-]+$/.test(repo)) {\n throw new MaterializeError(`Refusing to materialize: invalid repo full name '${repo}'.`, 'clone-failed');\n }\n if (!/^[A-Za-z0-9_./-]+$/.test(repoInfo.defaultBranch)) {\n throw new MaterializeError(\n `Refusing to materialize: invalid default branch '${repoInfo.defaultBranch}'.`,\n 'clone-failed',\n );\n }\n\n // 1. Preflight: git must be installed in the sandbox template.\n const gitVersion = await sh(sandbox, 'git --version');\n if (gitVersion.exitCode !== 0) {\n throw new MaterializeError(\n 'git is not installed in the sandbox. The sandbox template must include git.',\n 'git-missing',\n );\n }\n\n const authUrl = tokenUrl(repo, token);\n\n // The DB's `materializedAt` can drift from disk in both directions: a fresh\n // binding row over an already-populated workdir (local dev DB resets,\n // repaired rows, earlier flows) must pull instead of failing `git clone` on\n // the non-empty directory, and a stale `materializedAt` over an empty\n // sandbox (an expired/recreated VM whose disk was wiped) must re-clone\n // instead of running `git -C <workdir>` against a directory that no longer\n // exists. Disk is the source of truth: detect the checkout instead of\n // trusting the row.\n const alreadyMaterialized = await hasExistingCheckout(sandbox, workdir, repo);\n\n let tokenInRemote = false;\n try {\n if (!alreadyMaterialized) {\n // 2a. First open: shallow-clone the default branch into the workdir. A\n // shallow single-branch clone is dramatically faster for large repos; the\n // later re-open uses `git pull --ff-only`, which works on shallow clones.\n reportProgress(onProgress, {\n phase: 'cloning',\n message: `Cloning ${repo} (first open can take a minute)…`,\n });\n const clone = await gitTransfer(\n sandbox,\n `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`,\n {\n phase: 'repository clone',\n beforeRetry: async attempt => {\n reportProgress(onProgress, {\n phase: 'cloning',\n message: `Lost the connection to github.com — retrying the clone (attempt ${attempt + 1})…`,\n });\n // A clone that died partway leaves the destination non-empty, which\n // git refuses to clone into. Clear it so the retry starts clean.\n await sh(sandbox, `rm -rf ${shellQuote(workdir)}`);\n },\n },\n );\n if (clone.exitCode !== 0) {\n // git can fail after creating the checkout (\"Clone succeeded, but\n // checkout failed\") with the tokenized origin persisted — probe the\n // disk instead of assuming the failed clone left nothing behind.\n tokenInRemote = await hasGitDir(sandbox, workdir);\n throw classifyGitFailure(clone, 'clone-failed');\n }\n tokenInRemote = true;\n } else {\n // 2b. Re-open: refresh remote to the token URL and fast-forward pull.\n reportProgress(onProgress, { phase: 'pulling', message: `Updating ${repo} to the latest changes…` });\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, 'pull-failed');\n }\n tokenInRemote = true;\n const pull = await gitTransfer(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, {\n phase: 'repository pull',\n beforeRetry: async attempt =>\n reportProgress(onProgress, {\n phase: 'pulling',\n message: `Lost the connection to github.com — retrying the update (attempt ${attempt + 1})…`,\n }),\n });\n if (pull.exitCode !== 0) {\n if (!isBenignNonFastForward(pull)) {\n throw classifyGitFailure(pull, 'pull-failed');\n }\n // The workdir was left on a session's working branch that can't be\n // fast-forwarded (diverged from upstream, no upstream, or detached\n // HEAD), or its configured upstream ref was deleted after merge.\n // That checkout still holds usable work — never rebase or reset it\n // here. Leave it as-is and let the session reconcile with the remote\n // itself.\n reportProgress(onProgress, {\n phase: 'pulling',\n message: isDeletedUpstreamRef(pull)\n ? 'Workspace could not be updated from its remote — keeping the existing checkout as-is.'\n : 'Workspace has local changes that diverge from the remote — keeping them as-is.',\n });\n }\n }\n } catch (primary) {\n // 3a. The clone/pull failed — still scrub the token from the VM's git\n // config. The scrub must never hide the actionable failure, but once the\n // token reached the remote its own failure can't stay silent either:\n // report both, primary cause and classification first.\n throw await scrubbedFailure(sandbox, workdir, repo, tokenInRemote, primary, 'pull-failed');\n }\n\n // 3b. Success — the token is in the remote and the workdir has a `.git`, so\n // a failed scrub means the token may still be persisted: surface it.\n await scrubRemote(sandbox, workdir, repo, tokenInRemote);\n\n // 4. Mark materialized.\n reportProgress(onProgress, { phase: 'finalizing', message: 'Finalizing workspace…' });\n await storage.markMaterialized({ id: sandboxRow.id });\n}\n\n/**\n * Reset a pooled workdir that a new session just claimed: the previous\n * session's branch and dirty state must not leak into the new session, so\n * force-checkout the default branch and drop all local modifications. When\n * the claimed VM was reaped and re-provisioned there is no checkout yet and\n * this is a no-op (the clone path handles it). A wedged checkout falls back\n * to wiping the workdir so `materializeRepo` re-clones inside the same VM\n * instead of permanently failing the session.\n */\nexport async function recycleClaimedWorkdir(\n sandbox: MaterializationSandbox,\n workdir: string,\n defaultBranch: string,\n): Promise<void> {\n if (!/^[A-Za-z0-9_./-]+$/.test(defaultBranch)) {\n throw new MaterializeError(`Refusing to recycle: invalid default branch '${defaultBranch}'.`, 'clone-failed');\n }\n const w = shellQuote(workdir);\n const inspect = await sh(sandbox, `git -C ${w} rev-parse --is-inside-work-tree`);\n if (inspect.exitCode !== 0) return;\n const recycle = await sh(\n sandbox,\n // `-x` also drops gitignored files (.env, build caches) so no session\n // state survives into the next claim.\n `git -C ${w} checkout -f ${shellQuote(defaultBranch)} && git -C ${w} reset --hard && git -C ${w} clean -fdx`,\n { phase: 'claimed workdir recycle' },\n );\n if (recycle.exitCode === 0) return;\n const wipe = await sh(sandbox, `rm -rf ${w}`);\n if (wipe.exitCode !== 0) {\n throw new MaterializeError(`Failed to recycle claimed sandbox workdir: ${recycle.stderr}`, 'clone-failed');\n }\n}\n\n/** Check out a session's branch inside its isolated repository clone. */\nexport async function checkoutSessionBranch(\n sandbox: MaterializationSandbox,\n workdir: string,\n options: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<void> {\n return timedPhase('workspace.checkout', () => checkoutSessionBranchImpl(sandbox, workdir, options));\n}\n\nasync function checkoutSessionBranchImpl(\n sandbox: MaterializationSandbox,\n workdir: string,\n {\n branch,\n baseBranch,\n token,\n repoFullName,\n }: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<void> {\n if (!isValidGitRef(branch) || !isValidGitRef(baseBranch)) {\n throw new MaterializeError('Refusing to create a session from an invalid branch name.', 'clone-failed');\n }\n\n const current = await sh(sandbox, `git -C ${shellQuote(workdir)} branch --show-current`);\n if (current.exitCode === 0 && current.stdout.trim() === branch) return;\n\n const local = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} show-ref --verify --quiet refs/heads/${shellQuote(branch)}`,\n );\n if (local.exitCode === 0) {\n const checkout = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout ${shellQuote(branch)}`);\n if (checkout.exitCode !== 0) {\n // The session's agent may have switched branches itself (e.g. `gh pr\n // checkout`) and left uncommitted work in the tree. Git refuses to\n // switch back over those files — that work must win. The checkout is\n // intact and usable on its current branch; keep it as-is rather than\n // fail the workspace open, and never reset or stash to force the\n // switch through.\n if (isBlockedByLocalWork(checkout)) return;\n throw classifyGitFailure(checkout, 'clone-failed');\n }\n return;\n }\n\n const authUrl = tokenUrl(repoFullName, token);\n try {\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) throw classifyGitFailure(setUrl, 'pull-failed');\n const fetch = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} fetch origin ${shellQuote(baseBranch)} && git -C ${shellQuote(workdir)} checkout -b ${shellQuote(branch)} FETCH_HEAD`,\n { timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS, phase: 'branch checkout' },\n );\n if (fetch.exitCode !== 0) {\n // Same rule as above: uncommitted work in the tree blocks the switch\n // to the new branch. Leave the checkout on its current branch.\n if (isBlockedByLocalWork(fetch)) return;\n throw classifyGitFailure(fetch, 'clone-failed');\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 a failed `git checkout` just means uncommitted or untracked files\n * in the working tree would be clobbered by the branch switch. Those files are\n * a session's work in progress — the switch must yield to them, never the\n * other way around.\n */\nfunction isBlockedByLocalWork(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /Your local changes to the following files would be overwritten by checkout|untracked working tree files would be overwritten by checkout/i.test(\n output,\n );\n}\n\n/**\n * True when the workdir already holds a git checkout whose `origin` points at\n * this exact repo. Matches both the clean and token-auth URL forms; any other\n * remote (or no git dir at all) falls back to the clone path.\n */\nasync function hasExistingCheckout(\n sandbox: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n): Promise<boolean> {\n const result = await sh(sandbox, `git -C ${shellQuote(workdir)} remote get-url origin`);\n if (result.exitCode !== 0) return false;\n const url = result.stdout.trim().toLowerCase();\n const suffix = `github.com/${repoFullName.toLowerCase()}`;\n return url.endsWith(`${suffix}.git`) || url.endsWith(suffix);\n}\n\n/** Probed without `git -C` so a missing workdir returns false instead of throwing. */\nasync function hasGitDir(sandbox: MaterializationSandbox, 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: MaterializationSandbox,\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: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n tokenInRemote: boolean,\n primary: unknown,\n fallback: MaterializeError['code'],\n): Promise<unknown> {\n try {\n await scrubRemote(sandbox, workdir, repoFullName, tokenInRemote);\n return primary;\n } catch (scrubError) {\n const scrubMessage = scrubError instanceof Error ? scrubError.message : String(scrubError);\n if (!(primary instanceof Error)) {\n return new MaterializeError(`${String(primary)} — additionally: ${scrubMessage}`, fallback);\n }\n primary.message = `${primary.message} — additionally: ${scrubMessage}`;\n return primary;\n }\n}\n\n/**\n * True when a failed `git pull --ff-only` on re-open just means the current\n * branch can't be fast-forwarded — not that anything is broken. The shared\n * workdir is routinely left on a session's working branch, which may have\n * local commits diverging from upstream, no upstream at all (session branches\n * are created from `FETCH_HEAD`), or a detached HEAD. A checkout can also\n * hold uncommitted or untracked files (a build or script run left residue,\n * or an older session worked directly in the shared checkout), which makes\n * git refuse the merge outright. A checkout carrying `pull.rebase` in its git\n * config refuses for the same reason but says so in rebase's words instead of\n * merge's. After a PR merges with branch auto-delete, the configured upstream\n * ref may also be gone (`no such ref was fetched` / `couldn't find remote ref`);\n * there is nothing to pull and the checkout is still intact. In all of these\n * cases materialization must keep it as-is rather than fail the workspace open\n * — and must never discard the local state to force the pull through.\n */\nfunction isDeletedUpstreamRef(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /no such ref was fetched|couldn't find remote ref/i.test(output);\n}\n\nfunction isBenignNonFastForward(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return (\n isDeletedUpstreamRef(result) ||\n /Not possible to fast-forward|Diverging branches can't be fast-forwarded|no tracking information for the current branch|You are not currently on a branch|Your local changes to the following files would be overwritten by merge|untracked working tree files would be overwritten by merge|cannot pull with rebase|cannot rebase: You have unstaged changes|Your index contains uncommitted changes/i.test(\n output,\n )\n );\n}\n\n/**\n * Turn a failed git command into an actionable error, detecting the common\n * \"cannot reach github.com\" egress failure.\n */\nfunction classifyGitFailure(\n result: SandboxCommandResult,\n fallback: 'clone-failed' | 'pull-failed' | 'push-failed',\n): MaterializeError {\n const stderr = result.stderr || '';\n if (/could not resolve host|failed to connect|network is unreachable|Connection timed out/i.test(stderr)) {\n return new MaterializeError(\n 'The sandbox could not reach github.com. The sandbox network must allow outbound egress to github.com.',\n 'egress-blocked',\n );\n }\n const verb = fallback === 'clone-failed' ? 'clone' : fallback === 'pull-failed' ? 'pull' : 'push';\n return new MaterializeError(`git ${verb} failed: ${stderr}`, fallback);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 1 — git identity + token-scoped push primitive\n//\n// These helpers let the sandbox author and push commits safely. The install\n// token is short-lived, minted per-operation server-side, injected only into\n// the temporary remote URL inside the sandbox, and always scrubbed afterwards\n// so it never persists in `.git/config`.\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a git ref (branch) name. Server-side defense-in-depth: only allow a\n * conservative character set so a branch can never be built into a shell\n * command in a way that escapes quoting. Mirrors the route-layer check.\n */\nexport function isValidGitRef(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= 255 &&\n // Reject leading-dash refs (e.g. `--mirror`) so the value can never be\n // parsed as a git option when interpolated into a command.\n !value.startsWith('-') &&\n /^[A-Za-z0-9_./-]+$/.test(value)\n );\n}\n\n/** Identity used to author commits inside the sandbox. */\nexport interface GitIdentity {\n name?: string | null;\n email?: string | null;\n /** GitHub login, used to derive a stable noreply identity when name/email are absent. */\n login?: string | null;\n}\n\n/**\n * Resolve a concrete `{ name, email }` for git authorship from a possibly-sparse\n * identity. Falls back to a GitHub-style noreply identity so commits are never\n * authored with an empty or host-derived identity.\n */\nexport function resolveGitIdentity(identity: GitIdentity): { name: string; email: string } {\n const login = (identity.login || '').trim();\n const name = (identity.name || '').trim() || login || 'Mastra Code';\n const email =\n (identity.email || '').trim() ||\n (login ? `${login}@users.noreply.github.com` : 'mastra-code@users.noreply.github.com');\n return { name, email };\n}\n\n/**\n * Configure `user.name` / `user.email` for the given repo working tree inside\n * the sandbox so commits are authored correctly. Values are shell-quoted.\n */\nexport async function configureGitIdentity(\n sandbox: MaterializationSandbox,\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: MaterializationSandbox,\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: MaterializationSandbox,\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 worktree (or repo) path 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: MaterializationSandbox,\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 — worktree / branch lifecycle\n//\n// Each unit of work gets its own branch + working tree inside the same sandbox\n// as the base checkout. The worktree path is always computed server-side from a\n// sanitized branch name; client input never reaches a filesystem path.\n// ---------------------------------------------------------------------------\n\n/** Error raised when a worktree cannot be created/reused inside the sandbox. */\nexport class WorktreeError extends Error {\n constructor(\n message: string,\n readonly code: 'invalid-branch' | 'worktree-failed' | 'setup-failed' | 'teardown-failed',\n ) {\n super(message);\n this.name = 'WorktreeError';\n }\n}\n\n/**\n * Reduce a (already ref-validated) branch name to a filesystem-safe directory\n * segment for the worktree path: slashes/dots/unsafe chars collapsed to `-`.\n * This only affects the *directory name*, never the git branch itself.\n *\n * Sanitization is lossy (e.g. `feat/a` and `feat-a` both reduce to `feat-a`),\n * so an 8-char hash of the original branch is appended whenever the sanitized\n * form differs from the input. That keeps clean names (`main`) readable while\n * guaranteeing distinct branches never share a worktree directory.\n */\nexport function safeBranchDir(branch: string): string {\n const sanitized =\n branch\n .replace(/[^A-Za-z0-9._-]+/g, '-')\n .replace(/\\/+/g, '-')\n .replace(/^[-.]+|[-.]+$/g, '')\n .slice(0, 100) || 'work';\n if (sanitized === branch) return sanitized;\n const hash = createHash('sha256').update(branch).digest('hex').slice(0, 8);\n return `${sanitized}-${hash}`;\n}\n\n/**\n * Compute the absolute worktree path for a branch, server-side only. Worktrees\n * live alongside the repo checkout under a sibling `worktrees/` directory so the\n * repo's `.git` is shared. Never derived from client-supplied paths.\n */\nexport function computeWorktreePath(repoWorkdir: string, branch: string): string {\n const parent = repoWorkdir.replace(/\\/+$/, '').split('/').slice(0, -1).join('/') || '';\n return `${parent}/worktrees/${safeBranchDir(branch)}`;\n}\n\nexport interface EnsureWorktreeResult {\n worktreePath: string;\n branch: string;\n baseBranch: string;\n /** True when an existing worktree was reused rather than freshly created. */\n reused: boolean;\n}\n\n/**\n * Create (or reuse) a git worktree + branch inside the sandbox for a unit of\n * work. Idempotent: if a worktree already exists at the computed path it is\n * reused. The branch is created from the freshly fetched `origin/<baseBranch>`\n * — never the sandbox's possibly stale local ref — so new worktrees always\n * start from the latest remote state.\n *\n * @param sandbox live sandbox containing the base checkout\n * @param repoWorkdir the base repo checkout path inside the sandbox\n * @param branch the feature branch (ref-validated server-side)\n * @param baseBranch the branch to fork from (ref-validated; defaults to the repo's default branch)\n * @param token short-lived installation token used only for the base-branch fetch\n * @param repoFullName `owner/repo` used to build the tokenized remote URL\n */\nexport async function ensureWorktree(\n sandbox: MaterializationSandbox,\n repoWorkdir: string,\n {\n branch,\n baseBranch,\n token,\n repoFullName,\n }: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<EnsureWorktreeResult> {\n if (!isValidGitRef(branch)) {\n throw new WorktreeError(`Invalid branch name '${branch}'.`, 'invalid-branch');\n }\n if (!isValidGitRef(baseBranch)) {\n throw new WorktreeError(`Invalid base branch name '${baseBranch}'.`, 'invalid-branch');\n }\n\n const worktreePath = computeWorktreePath(repoWorkdir, branch);\n\n // Idempotent reuse: a worktree already checked out at this path has a `.git`\n // file (worktrees use a gitfile, not a directory). Reuse it as-is.\n const exists = await sh(sandbox, `test -e ${shellQuote(`${worktreePath}/.git`)}`);\n if (exists.exitCode === 0) {\n return { worktreePath, branch, baseBranch, reused: true };\n }\n\n // Fetch the latest base ref from origin before forking. The explicit refspec\n // updates `refs/remotes/origin/<base>` even when the checkout was created as\n // a single-branch clone. The fetch needs the install token (the resting\n // remote is tokenless), and a failure is a hard error — silently forking a\n // stale local ref is worse than failing the request.\n const baseRef = `origin/${baseBranch}`;\n await withInstallToken(sandbox, repoWorkdir, repoFullName, token, async () => {\n const fetch = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} fetch origin ${shellQuote(`+refs/heads/${baseBranch}:refs/remotes/${baseRef}`)}`,\n );\n if (fetch.exitCode !== 0) {\n throw classifyGitFailure(fetch, 'pull-failed');\n }\n });\n\n // Create the worktree. If the branch already exists, check it out into the\n // worktree; otherwise create it from the fetched base. `git worktree add -B`\n // creates-or-resets the branch to the base, which keeps this idempotent for a\n // fresh worktree while still working when the branch already exists remotely.\n // `--no-track` keeps the feature branch from tracking origin/<base>; pushes\n // set their own upstream via `push -u`.\n const add = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} worktree add --no-track -B ${shellQuote(branch)} ${shellQuote(worktreePath)} ${shellQuote(baseRef)}`,\n );\n if (add.exitCode !== 0) {\n throw new WorktreeError(`git worktree add failed: ${add.stderr.trim() || add.stdout.trim()}`, 'worktree-failed');\n }\n\n return { worktreePath, branch, baseBranch, reused: false };\n}\n\n/**\n * Run the project's setup command (e.g. `pnpm i && pnpm build`) inside a\n * freshly created worktree. Called before the worktree is handed to any agent\n * run so the checkout is ready to build/test. A non-zero exit is a hard error —\n * starting agent work in a half-set-up tree is worse than failing the 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 worktree\n * @param worktreePath the server-computed worktree path the command runs in\n * @param command the org-configured setup shell command\n */\nasync function runWorktreeLifecycleCommand(\n sandbox: MaterializationSandbox,\n worktreePath: string,\n command: string,\n options: { phase: 'setup' | 'teardown'; timeoutMs?: number },\n): Promise<void> {\n const result = await sh(sandbox, `cd ${shellQuote(worktreePath)} && { ${command}\\n}`, {\n phase: `worktree ${options.phase}`,\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 WorktreeError(\n `${label} command failed (exit ${result.exitCode}): ${detail}`,\n options.phase === 'setup' ? 'setup-failed' : 'teardown-failed',\n );\n }\n}\n\nexport async function runWorktreeSetup(\n sandbox: MaterializationSandbox,\n worktreePath: string,\n command: string,\n): Promise<void> {\n return runWorktreeLifecycleCommand(sandbox, worktreePath, 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 runWorktreeTeardown(\n sandbox: MaterializationSandbox,\n worktreePath: string,\n command: string,\n options: { timeoutMs?: number } = {},\n): Promise<void> {\n return runWorktreeLifecycleCommand(sandbox, worktreePath, command, {\n phase: 'teardown',\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n });\n}\n\n/**\n * Remove a worktree (and its local feature branch) from the sandbox. The\n * checkout is removed with `--force` — the caller owns confirming that any\n * uncommitted work in it can be discarded. Idempotent: a worktree whose\n * directory is already gone only has its metadata pruned.\n *\n * @param sandbox live sandbox containing the base checkout\n * @param repoWorkdir the base repo checkout path inside the sandbox\n * @param branch the worktree's feature branch (ref-validated)\n * @param worktreePath the persisted, server-computed worktree path\n */\nexport async function removeWorktree(\n sandbox: MaterializationSandbox,\n repoWorkdir: string,\n { branch, worktreePath }: { branch: string; worktreePath: string },\n): Promise<void> {\n if (!isValidGitRef(branch)) {\n throw new WorktreeError(`Invalid branch name '${branch}'.`, 'invalid-branch');\n }\n\n const remove = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} worktree remove --force ${shellQuote(worktreePath)}`,\n );\n if (remove.exitCode !== 0) {\n // Tolerate a checkout that's already gone (e.g. a fresh sandbox after\n // re-provisioning): prune stale metadata and only fail when the directory\n // still exists, meaning git genuinely refused to remove it.\n await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} worktree prune`);\n const exists = await sh(sandbox, `test -e ${shellQuote(worktreePath)}`);\n if (exists.exitCode === 0) {\n throw new WorktreeError(\n `git worktree remove failed: ${remove.stderr.trim() || remove.stdout.trim()}`,\n 'worktree-failed',\n );\n }\n }\n\n // Best-effort local branch cleanup; the branch may not exist locally anymore\n // or may still be pushed remotely — neither should fail the removal.\n await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} branch -D ${shellQuote(branch)}`);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 3 — `gh` CLI pull-request creation primitive\n//\n// PRs are opened from inside the sandbox with the GitHub CLI. `gh` must be\n// present in the sandbox template (preflighted only on the PR path so clone /\n// open still work when it is absent). The token is passed to `gh` via a\n// per-invocation `GH_TOKEN` env that is scoped to the single `gh` process and\n// never written to git config, a shell rc, or the VM's environment.\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: MaterializationSandbox): 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: MaterializationSandbox,\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":";;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAS,aAAa,KAA+B,SAA2D;CAC9G,OAAO;EACL,WAAW,IAAI;EACf,eAAc,OACZ,OAAO,OAAO,QAAQ,aAAa,EAAE,IAAI,IAAI,GAAG,CAAC,IAAI,QAAQ,aAAa;GAAE,IAAI,IAAI;GAAI,WAAW;EAAG,CAAC;EACzG,aAAa,QAAQ,aAAa,EAAE,IAAI,IAAI,GAAG,CAAC;CAClD;AACF;;;;;AAMA,eAAsB,qBAAqB,SAMP;CAClC,MAAM,EAAE,OAAO,KAAK,SAAS,OAAO,eAAe;CACnD,OAAO,MAAM,cAAc,aAAa,KAAK,OAAO,GAAG,EAAE,UAAU,MAAM,GAAG,YAAY,EACtF,cAAc,IAAI,OACpB,CAAC;AACH;;;;;;;;;AAUA,eAAsB,uBAAuB,SAK3B;CAChB,MAAM,EAAE,OAAO,KAAK,SAAS,YAAY;CACzC,OAAO,MAAM,gBAAgB,aAAa,KAAK,OAAO,GAAG,OAAO;AAClE;;;;;;;;AASA,SAAgB,WAAW,OAAuB;CAEhD,OAAO,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,OAAO,IAAI;AAChD;;;;;;;AAQA,MAAa,6BAA6B,KAAK;;AAE/C,MAAa,8BAA8B,IAAI;;;;;;;;;AAiB/C,SAAS,0BAA0B,OAAyB;CAC1D,MAAM,SAAU,OAAgC;CAChD,OAAO,OAAO,WAAW,YAAY,UAAU;AACjD;AAEA,MAAM,aAAa;AACnB,MAAM,oBAAoB;;;;;;;;AAS1B,eAAe,GACb,SACA,QACA,UAAqB,CAAC,GACS;CAG/B,MAAM,aAAa,KAAK,IAAI,KAAK,QAAQ,aAAA;CACzC,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;EACF,OAAO,MAAM,OAAO,SAAS,QAAQ;GAAE,GAAG;GAAS,WAAW,KAAK,IAAI,aAAa,KAAK,IAAI,GAAG,CAAC;EAAE,CAAC;CACtG,SAAS,OAAO;EACd,IAAI,WAAW,cAAc,CAAC,0BAA0B,KAAK,GAAG,MAAM;EACtE,MAAM,UAAU,qBAAqB,UAAU;EAC/C,IAAI,aAAa,KAAK,IAAI,KAAK,SAAS,MAAM;EAC9C,MAAM,IAAI,SAAQ,YAAW,WAAW,SAAS,OAAO,CAAC;CAC3D;AAEJ;;AAGA,eAAe,OACb,SACA,QACA,SAC+B;CAC/B,MAAM,YAAY,QAAQ,aAAA;CAC1B,IAAI;CACJ,MAAM,YAAY,IAAI,SAAgB,GAAG,WAAW;EAClD,QAAQ,iBAAiB;GACvB,MAAM,QAAQ,QAAQ,QAAQ,WAAW,QAAQ,UAAU;GAC3D,uBAAO,IAAI,MAAM,mCAAmC,KAAK,MAAM,YAAY,GAAI,EAAE,GAAG,MAAM,EAAE,CAAC;EAC/F,GAAG,SAAS;EACZ,MAAM,QAAQ;CAChB,CAAC;CACD,IAAI;EAGF,OAAO,MAAM,QAAQ,KAAK,CAAC,QAAQ,eAAe,MAAM,CAAC,MAAM,MAAM,GAAG,EAAE,SAAS,UAAU,CAAC,GAAG,SAAS,CAAC;CAC7G,UAAU;EACR,aAAa,KAAK;CACpB;AACF;AAEA,MAAM,uBAAuB;AAC7B,MAAM,8BAA8B;;;;;;;;;;;;;;;AAgBpC,SAAS,sBAAsB,QAAuC;CACpE,MAAM,SAAS,GAAG,OAAO,UAAU,GAAG,IAAI,OAAO,UAAU;CAC3D,OAAO,2RAA2R,KAChS,MACF;AACF;;;;;;;;;;AAWA,eAAe,YACb,SACA,QACA,UAA4E,CAAC,GAC9C;CAC/B,MAAM,EAAE,aAAa,GAAG,cAAc;CACtC,MAAM,aAAa,KAAK,IAAI,KAAK,UAAU,aAAA;CAC3C,KAAK,IAAI,UAAU,IAAK,WAAW;EACjC,MAAM,SAAS,MAAM,GAAG,SAAS,QAAQ;GACvC,GAAG;GACH,WAAW,KAAK,IAAI,aAAa,KAAK,IAAI,GAAG,CAAC;EAChD,CAAC;EACD,IAAI,OAAO,aAAa,KAAK,WAAW,wBAAwB,CAAC,sBAAsB,MAAM,GAAG,OAAO;EACvG,MAAM,UAAU,+BAA+B,UAAU;EACzD,IAAI,aAAa,KAAK,IAAI,KAAK,SAAS,OAAO;EAC/C,MAAM,IAAI,SAAQ,YAAW,WAAW,SAAS,OAAO,CAAC;EACzD,MAAM,cAAc,UAAU,CAAC;CACjC;AACF;;AAGA,IAAa,mBAAb,cAAsC,MAAM;CAG/B;CAFX,YACE,SACA,MASA;EACA,MAAM,OAAO;EAVJ,KAAA,OAAA;EAWT,KAAK,OAAO;CACd;AACF;;;;;AAMA,SAAS,SAAS,cAAsB,OAAuB;CAC7D,OAAO,0BAA0B,MAAM,cAAc,aAAa;AACpE;AAEA,SAAS,SAAS,cAA8B;CAC9C,OAAO,sBAAsB,aAAa;AAC5C;;;;;;;AA4BA,eAAsB,gBAAgB,SAAgD;CACpF,OAAO,WAAW,+BAA+B,oBAAoB,OAAO,CAAC;AAC/E;AAEA,eAAe,oBAAoB,SAAgD;CACjF,MAAM,EAAE,KAAK,YAAY,UAAU,SAAS,OAAO,SAAS,eAAe;CAC3E,MAAM,UAAU,WAAW;CAC3B,MAAM,OAAO,SAAS;CAKtB,IAAI,CAAC,qBAAqB,KAAK,IAAI,GACjC,MAAM,IAAI,iBAAiB,oDAAoD,KAAK,KAAK,cAAc;CAEzG,IAAI,CAAC,qBAAqB,KAAK,SAAS,aAAa,GACnD,MAAM,IAAI,iBACR,oDAAoD,SAAS,cAAc,KAC3E,cACF;CAKF,KAAI,MADqB,GAAG,SAAS,eAAe,EAAA,CACrC,aAAa,GAC1B,MAAM,IAAI,iBACR,+EACA,aACF;CAGF,MAAM,UAAU,SAAS,MAAM,KAAK;CAUpC,MAAM,sBAAsB,MAAM,oBAAoB,SAAS,SAAS,IAAI;CAE5E,IAAI,gBAAgB;CACpB,IAAI;EACF,IAAI,CAAC,qBAAqB;GAIxB,eAAe,YAAY;IACzB,OAAO;IACP,SAAS,WAAW,KAAK;GAC3B,CAAC;GACD,MAAM,QAAQ,MAAM,YAClB,SACA,gDAAgD,WAAW,SAAS,aAAa,EAAE,GAAG,WAAW,OAAO,EAAE,GAAG,WAAW,OAAO,KAC/H;IACE,OAAO;IACP,aAAa,OAAM,YAAW;KAC5B,eAAe,YAAY;MACzB,OAAO;MACP,SAAS,mEAAmE,UAAU,EAAE;KAC1F,CAAC;KAGD,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,GAAG;IACnD;GACF,CACF;GACA,IAAI,MAAM,aAAa,GAAG;IAIxB,gBAAgB,MAAM,UAAU,SAAS,OAAO;IAChD,MAAM,mBAAmB,OAAO,cAAc;GAChD;GACA,gBAAgB;EAClB,OAAO;GAEL,eAAe,YAAY;IAAE,OAAO;IAAW,SAAS,YAAY,KAAK;GAAyB,CAAC;GACnG,MAAM,SAAS,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,yBAAyB,WAAW,OAAO,GAAG;GAC7G,IAAI,OAAO,aAAa,GACtB,MAAM,IAAI,iBAAiB,6BAA6B,OAAO,UAAU,aAAa;GAExF,gBAAgB;GAChB,MAAM,OAAO,MAAM,YAAY,SAAS,UAAU,WAAW,OAAO,EAAE,kBAAkB;IACtF,OAAO;IACP,aAAa,OAAM,YACjB,eAAe,YAAY;KACzB,OAAO;KACP,SAAS,oEAAoE,UAAU,EAAE;IAC3F,CAAC;GACL,CAAC;GACD,IAAI,KAAK,aAAa,GAAG;IACvB,IAAI,CAAC,uBAAuB,IAAI,GAC9B,MAAM,mBAAmB,MAAM,aAAa;IAQ9C,eAAe,YAAY;KACzB,OAAO;KACP,SAAS,qBAAqB,IAAI,IAC9B,0FACA;IACN,CAAC;GACH;EACF;CACF,SAAS,SAAS;EAKhB,MAAM,MAAM,gBAAgB,SAAS,SAAS,MAAM,eAAe,SAAS,aAAa;CAC3F;CAIA,MAAM,YAAY,SAAS,SAAS,MAAM,aAAa;CAGvD,eAAe,YAAY;EAAE,OAAO;EAAc,SAAS;CAAwB,CAAC;CACpF,MAAM,QAAQ,iBAAiB,EAAE,IAAI,WAAW,GAAG,CAAC;AACtD;;;;;;;;;;AAWA,eAAsB,sBACpB,SACA,SACA,eACe;CACf,IAAI,CAAC,qBAAqB,KAAK,aAAa,GAC1C,MAAM,IAAI,iBAAiB,gDAAgD,cAAc,KAAK,cAAc;CAE9G,MAAM,IAAI,WAAW,OAAO;CAE5B,KAAI,MADkB,GAAG,SAAS,UAAU,EAAE,iCAAiC,EAAA,CACnE,aAAa,GAAG;CAC5B,MAAM,UAAU,MAAM,GACpB,SAGA,UAAU,EAAE,eAAe,WAAW,aAAa,EAAE,aAAa,EAAE,0BAA0B,EAAE,cAChG,EAAE,OAAO,0BAA0B,CACrC;CACA,IAAI,QAAQ,aAAa,GAAG;CAE5B,KAAI,MADe,GAAG,SAAS,UAAU,GAAG,EAAA,CACnC,aAAa,GACpB,MAAM,IAAI,iBAAiB,8CAA8C,QAAQ,UAAU,cAAc;AAE7G;;AAGA,eAAsB,sBACpB,SACA,SACA,SACe;CACf,OAAO,WAAW,4BAA4B,0BAA0B,SAAS,SAAS,OAAO,CAAC;AACpG;AAEA,eAAe,0BACb,SACA,SACA,EACE,QACA,YACA,OACA,gBAEa;CACf,IAAI,CAAC,cAAc,MAAM,KAAK,CAAC,cAAc,UAAU,GACrD,MAAM,IAAI,iBAAiB,6DAA6D,cAAc;CAGxG,MAAM,UAAU,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,uBAAuB;CACvF,IAAI,QAAQ,aAAa,KAAK,QAAQ,OAAO,KAAK,MAAM,QAAQ;CAMhE,KAAI,MAJgB,GAClB,SACA,UAAU,WAAW,OAAO,EAAE,wCAAwC,WAAW,MAAM,GACzF,EAAA,CACU,aAAa,GAAG;EACxB,MAAM,WAAW,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,YAAY,WAAW,MAAM,GAAG;EACjG,IAAI,SAAS,aAAa,GAAG;GAO3B,IAAI,qBAAqB,QAAQ,GAAG;GACpC,MAAM,mBAAmB,UAAU,cAAc;EACnD;EACA;CACF;CAEA,MAAM,UAAU,SAAS,cAAc,KAAK;CAC5C,IAAI;EACF,MAAM,SAAS,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,yBAAyB,WAAW,OAAO,GAAG;EAC7G,IAAI,OAAO,aAAa,GAAG,MAAM,mBAAmB,QAAQ,aAAa;EACzE,MAAM,QAAQ,MAAM,GAClB,SACA,UAAU,WAAW,OAAO,EAAE,gBAAgB,WAAW,UAAU,EAAE,aAAa,WAAW,OAAO,EAAE,eAAe,WAAW,MAAM,EAAE,cACxI;GAAE,WAAW;GAA6B,OAAO;EAAkB,CACrE;EACA,IAAI,MAAM,aAAa,GAAG;GAGxB,IAAI,qBAAqB,KAAK,GAAG;GACjC,MAAM,mBAAmB,OAAO,cAAc;EAChD;CACF,UAAU;EACR,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,yBAAyB,WAAW,SAAS,YAAY,CAAC,GAAG;CAC/G;AACF;;;;;;;AAQA,SAAS,qBAAqB,QAAuC;CACnE,MAAM,SAAS,GAAG,OAAO,UAAU,GAAG,IAAI,OAAO,UAAU;CAC3D,OAAO,4IAA4I,KACjJ,MACF;AACF;;;;;;AAOA,eAAe,oBACb,SACA,SACA,cACkB;CAClB,MAAM,SAAS,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,uBAAuB;CACtF,IAAI,OAAO,aAAa,GAAG,OAAO;CAClC,MAAM,MAAM,OAAO,OAAO,KAAK,CAAC,CAAC,YAAY;CAC7C,MAAM,SAAS,cAAc,aAAa,YAAY;CACtD,OAAO,IAAI,SAAS,GAAG,OAAO,KAAK,KAAK,IAAI,SAAS,MAAM;AAC7D;;AAGA,eAAe,UAAU,SAAiC,SAAmC;CAE3F,QAAO,MADa,GAAG,SAAS,WAAW,WAAW,GAAG,QAAQ,MAAM,GAAG,CAAC,CAAC,YAAY,IAAI,EAAA,EAC9E,aAAa;AAC7B;;;;;;;;;;AAWA,eAAe,YACb,SACA,SACA,cACA,eACe;CACf,MAAM,QAAQ,UAAU,WAAW,OAAO,EAAE,yBAAyB,WAAW,SAAS,YAAY,CAAC;CACtG,IAAI,CAAC,eAAe;EAClB,MAAM,GAAG,SAAS,KAAK,CAAC,CAAC,YAAY,KAAA,CAAS;EAC9C;CACF;CACA,IAAI;CACJ,IAAI;EACF,MAAM,SAAS,MAAM,GAAG,SAAS,KAAK;EACtC,IAAI,OAAO,aAAa,GAAG;EAC3B,UAAU,OAAO,OAAO,KAAK,KAAK,OAAO,OAAO,KAAK;CACvD,SAAS,OAAO;EACd,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;CACjE;CACA,MAAM,IAAI,iBAAiB,uDAAuD,WAAW,aAAa;AAC5G;;;;;;;AAQA,eAAe,gBACb,SACA,SACA,cACA,eACA,SACA,UACkB;CAClB,IAAI;EACF,MAAM,YAAY,SAAS,SAAS,cAAc,aAAa;EAC/D,OAAO;CACT,SAAS,YAAY;EACnB,MAAM,eAAe,sBAAsB,QAAQ,WAAW,UAAU,OAAO,UAAU;EACzF,IAAI,EAAE,mBAAmB,QACvB,OAAO,IAAI,iBAAiB,GAAG,OAAO,OAAO,EAAE,mBAAmB,gBAAgB,QAAQ;EAE5F,QAAQ,UAAU,GAAG,QAAQ,QAAQ,mBAAmB;EACxD,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;AAkBA,SAAS,qBAAqB,QAAuC;CACnE,MAAM,SAAS,GAAG,OAAO,UAAU,GAAG,IAAI,OAAO,UAAU;CAC3D,OAAO,oDAAoD,KAAK,MAAM;AACxE;AAEA,SAAS,uBAAuB,QAAuC;CACrE,MAAM,SAAS,GAAG,OAAO,UAAU,GAAG,IAAI,OAAO,UAAU;CAC3D,OACE,qBAAqB,MAAM,KAC3B,wYAAwY,KACtY,MACF;AAEJ;;;;;AAMA,SAAS,mBACP,QACA,UACkB;CAClB,MAAM,SAAS,OAAO,UAAU;CAChC,IAAI,wFAAwF,KAAK,MAAM,GACrG,OAAO,IAAI,iBACT,yGACA,gBACF;CAGF,OAAO,IAAI,iBAAiB,OADf,aAAa,iBAAiB,UAAU,aAAa,gBAAgB,SAAS,OACnD,WAAW,UAAU,QAAQ;AACvE;;;;;;AAgBA,SAAgB,cAAc,OAAiC;CAC7D,OACE,OAAO,UAAU,YACjB,MAAM,SAAS,KACf,MAAM,UAAU,OAGhB,CAAC,MAAM,WAAW,GAAG,KACrB,qBAAqB,KAAK,KAAK;AAEnC;;;;;;AAeA,SAAgB,mBAAmB,UAAwD;CACzF,MAAM,SAAS,SAAS,SAAS,GAAA,CAAI,KAAK;CAK1C,OAAO;EAAE,OAJK,SAAS,QAAQ,GAAA,CAAI,KAAK,KAAK,SAAS;EAIvC,QAFZ,SAAS,SAAS,GAAA,CAAI,KAAK,MAC3B,QAAQ,GAAG,MAAM,6BAA6B;CAC5B;AACvB;;;;;AAMA,eAAsB,qBACpB,SACA,SACA,UACe;CACf,MAAM,EAAE,MAAM,UAAU,mBAAmB,QAAQ;CACnD,MAAM,UAAU,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,oBAAoB,WAAW,IAAI,GAAG;CACtG,IAAI,QAAQ,aAAa,GACvB,MAAM,IAAI,iBAAiB,gCAAgC,QAAQ,OAAO,KAAK,KAAK,eAAe;CAErG,MAAM,WAAW,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,qBAAqB,WAAW,KAAK,GAAG;CACzG,IAAI,SAAS,aAAa,GACxB,MAAM,IAAI,iBAAiB,iCAAiC,SAAS,OAAO,KAAK,KAAK,eAAe;AAEzG;;;;;;;;;;;;;AAcA,eAAsB,iBACpB,SACA,SACA,cACA,OACA,IACY;CACZ,IAAI,CAAC,qBAAqB,KAAK,YAAY,GACzC,MAAM,IAAI,iBAAiB,6CAA6C,aAAa,KAAK,aAAa;CAGzG,MAAM,SAAS,MAAM,GACnB,SACA,UAAU,WAAW,OAAO,EAAE,yBAAyB,WAAW,SAAS,cAAc,KAAK,CAAC,GACjG;CACA,IAAI,OAAO,aAAa,GAAG;EAEzB,MAAM,YAAY,SAAS,SAAS,cAAc,KAAK;EACvD,MAAM,IAAI,iBAAiB,6BAA6B,OAAO,OAAO,KAAK,KAAK,aAAa;CAC/F;CAEA,IAAI;CACJ,IAAI;EACF,SAAS,MAAM,GAAG;CACpB,SAAS,SAAS;EAChB,MAAM,MAAM,gBAAgB,SAAS,SAAS,cAAc,MAAM,SAAS,aAAa;CAC1F;CAGA,MAAM,YAAY,SAAS,SAAS,cAAc,IAAI;CACtD,OAAO;AACT;;;;;;;AAQA,eAAsB,WACpB,SACA,SACA,QACA,OACA,cACe;CACf,IAAI,CAAC,cAAc,MAAM,GACvB,MAAM,IAAI,iBAAiB,0CAA0C,OAAO,KAAK,aAAa;CAGhG,MAAM,iBAAiB,SAAS,SAAS,cAAc,OAAO,YAAY;EACxE,MAAM,OAAO,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,kBAAkB,WAAW,MAAM,GAAG;EACnG,IAAI,KAAK,aAAa,GACpB,MAAM,mBAAmB,MAAM,aAAa;CAEhD,CAAC;AACH;;;;;;;;;;;;AAkBA,eAAsB,UACpB,SACA,SACA,SACA,UACuB;CACvB,MAAM,qBAAqB,SAAS,SAAS,QAAQ;CAErD,MAAM,MAAM,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,QAAQ;CACpE,IAAI,IAAI,aAAa,GACnB,MAAM,IAAI,iBAAiB,mBAAmB,IAAI,OAAO,KAAK,KAAK,IAAI,OAAO,KAAK,KAAK,eAAe;CAMzG,KAAI,MADiB,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,uBAAuB,EAAA,CAC3E,aAAa,GACtB,OAAO,EAAE,WAAW,MAAM;CAG5B,MAAM,SAAS,MAAM,GAAG,SAAS,UAAU,WAAW,OAAO,EAAE,aAAa,WAAW,OAAO,GAAG;CACjG,IAAI,OAAO,aAAa,GACtB,MAAM,IAAI,iBAAiB,sBAAsB,OAAO,OAAO,KAAK,KAAK,OAAO,OAAO,KAAK,KAAK,eAAe;CAGlH,OAAO,EAAE,WAAW,KAAK;AAC3B;;AAWA,IAAa,gBAAb,cAAmC,MAAM;CAG5B;CAFX,YACE,SACA,MACA;EACA,MAAM,OAAO;EAFJ,KAAA,OAAA;EAGT,KAAK,OAAO;CACd;AACF;;;;;;;;;;;AAYA,SAAgB,cAAc,QAAwB;CACpD,MAAM,YACJ,OACG,QAAQ,qBAAqB,GAAG,CAAC,CACjC,QAAQ,QAAQ,GAAG,CAAC,CACpB,QAAQ,kBAAkB,EAAE,CAAC,CAC7B,MAAM,GAAG,GAAG,KAAK;CACtB,IAAI,cAAc,QAAQ,OAAO;CAEjC,OAAO,GAAG,UAAU,GADP,WAAW,QAAQ,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAC9C;AAC5B;;;;;;AAOA,SAAgB,oBAAoB,aAAqB,QAAwB;CAE/E,OAAO,GADQ,YAAY,QAAQ,QAAQ,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,GAAG,KAAK,GACnE,aAAa,cAAc,MAAM;AACpD;;;;;;;;;;;;;;;AAwBA,eAAsB,eACpB,SACA,aACA,EACE,QACA,YACA,OACA,gBAE6B;CAC/B,IAAI,CAAC,cAAc,MAAM,GACvB,MAAM,IAAI,cAAc,wBAAwB,OAAO,KAAK,gBAAgB;CAE9E,IAAI,CAAC,cAAc,UAAU,GAC3B,MAAM,IAAI,cAAc,6BAA6B,WAAW,KAAK,gBAAgB;CAGvF,MAAM,eAAe,oBAAoB,aAAa,MAAM;CAK5D,KAAI,MADiB,GAAG,SAAS,WAAW,WAAW,GAAG,aAAa,MAAM,GAAG,EAAA,CACrE,aAAa,GACtB,OAAO;EAAE;EAAc;EAAQ;EAAY,QAAQ;CAAK;CAQ1D,MAAM,UAAU,UAAU;CAC1B,MAAM,iBAAiB,SAAS,aAAa,cAAc,OAAO,YAAY;EAC5E,MAAM,QAAQ,MAAM,GAClB,SACA,UAAU,WAAW,WAAW,EAAE,gBAAgB,WAAW,eAAe,WAAW,gBAAgB,SAAS,GAClH;EACA,IAAI,MAAM,aAAa,GACrB,MAAM,mBAAmB,OAAO,aAAa;CAEjD,CAAC;CAQD,MAAM,MAAM,MAAM,GAChB,SACA,UAAU,WAAW,WAAW,EAAE,8BAA8B,WAAW,MAAM,EAAE,GAAG,WAAW,YAAY,EAAE,GAAG,WAAW,OAAO,GACtI;CACA,IAAI,IAAI,aAAa,GACnB,MAAM,IAAI,cAAc,4BAA4B,IAAI,OAAO,KAAK,KAAK,IAAI,OAAO,KAAK,KAAK,iBAAiB;CAGjH,OAAO;EAAE;EAAc;EAAQ;EAAY,QAAQ;CAAM;AAC3D;;;;;;;;;;;;;;;;;;;;;AAsBA,eAAe,4BACb,SACA,cACA,SACA,SACe;CACf,MAAM,SAAS,MAAM,GAAG,SAAS,MAAM,WAAW,YAAY,EAAE,QAAQ,QAAQ,MAAM;EACpF,OAAO,YAAY,QAAQ;EAC3B,GAAI,QAAQ,cAAc,KAAA,IAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;CAC5E,CAAC;CACD,IAAI,OAAO,aAAa,GAAG;EACzB,MAAM,UAAU,OAAO,OAAO,KAAK,KAAK,OAAO,OAAO,KAAK,EAAA,CAAG,MAAM,KAAK;EAEzE,MAAM,IAAI,cACR,GAFY,QAAQ,UAAU,UAAU,UAAU,WAEzC,wBAAwB,OAAO,SAAS,KAAK,UACtD,QAAQ,UAAU,UAAU,iBAAiB,iBAC/C;CACF;AACF;AAEA,eAAsB,iBACpB,SACA,cACA,SACe;CACf,OAAO,4BAA4B,SAAS,cAAc,SAAS,EAAE,OAAO,QAAQ,CAAC;AACvF;;;;;;;AAQA,eAAsB,oBACpB,SACA,cACA,SACA,UAAkC,CAAC,GACpB;CACf,OAAO,4BAA4B,SAAS,cAAc,SAAS;EACjE,OAAO;EACP,GAAI,QAAQ,cAAc,KAAA,IAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;CAC5E,CAAC;AACH;;;;;;;;;;;;AAaA,eAAsB,eACpB,SACA,aACA,EAAE,QAAQ,gBACK;CACf,IAAI,CAAC,cAAc,MAAM,GACvB,MAAM,IAAI,cAAc,wBAAwB,OAAO,KAAK,gBAAgB;CAG9E,MAAM,SAAS,MAAM,GACnB,SACA,UAAU,WAAW,WAAW,EAAE,2BAA2B,WAAW,YAAY,GACtF;CACA,IAAI,OAAO,aAAa,GAAG;EAIzB,MAAM,GAAG,SAAS,UAAU,WAAW,WAAW,EAAE,gBAAgB;EAEpE,KAAI,MADiB,GAAG,SAAS,WAAW,WAAW,YAAY,GAAG,EAAA,CAC3D,aAAa,GACtB,MAAM,IAAI,cACR,+BAA+B,OAAO,OAAO,KAAK,KAAK,OAAO,OAAO,KAAK,KAC1E,iBACF;CAEJ;CAIA,MAAM,GAAG,SAAS,UAAU,WAAW,WAAW,EAAE,aAAa,WAAW,MAAM,GAAG;AACvF;;;;;;AAmCA,eAAe,kBAAkB,SAAgD;CAE/E,KAAI,MADkB,GAAG,SAAS,cAAc,EAAA,CACpC,aAAa,GACvB,MAAM,IAAI,iBACR,oHACA,YACF;AAEJ;;AAGA,SAAS,oBAAoB,QAAoC;CAE/D,OADc,OAAO,MAAM,0CAChB,CAAC,GAAG;AACjB;;;;;;;;;;AAWA,eAAsB,kBACpB,SACA,SACA,EAAE,OAAO,MAAM,MAAM,OAAO,QACM;CAClC,IAAI,CAAC,cAAc,IAAI,GACrB,MAAM,IAAI,iBAAiB,6CAA6C,KAAK,KAAK,WAAW;CAE/F,IAAI,CAAC,cAAc,IAAI,GACrB,MAAM,IAAI,iBAAiB,6CAA6C,KAAK,KAAK,WAAW;CAG/F,MAAM,kBAAkB,OAAO;CAK/B,MAAM,YAAY;EAChB,YAAY,WAAW,KAAK,EAAE;EAC9B,UAAU,WAAW,IAAI;EACzB,UAAU,WAAW,IAAI;EACzB,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,QAAQ,EAAE;CACjC,CAAC,CAAC,KAAK,GAAG;CAGV,MAAM,SAAS,MAAM,GAAG,SAAS,MAFZ,WAAW,OAAO,EAAE,MAAM,WAER;CACvC,IAAI,OAAO,aAAa,GAAG;EACzB,MAAM,aAAa,mBAAmB,QAAQ,aAAa;EAC3D,IAAI,WAAW,SAAS,kBACtB,MAAM;EAER,MAAM,IAAI,iBAAiB,wBAAwB,OAAO,OAAO,KAAK,KAAK,OAAO,OAAO,KAAK,KAAK,WAAW;CAChH;CAEA,MAAM,MAAM,oBAAoB,OAAO,MAAM;CAC7C,IAAI,CAAC,KACH,MAAM,IAAI,iBACR,iEAAiE,OAAO,OAAO,KAAK,KACpF,WACF;CAGF,OAAO,EAAE,IAAI;AACf"}
|
|
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. Instead each project gets\n * its own isolated sandbox (provisioned by the fleet in `../sandbox/fleet`) and\n * the repo is cloned *inside* that sandbox. The agent's file tools and command\n * tools then operate entirely against the remote checkout.\n *\n * - `ensureProjectSandbox(row)` / `teardownProjectSandbox(row)` bind the fleet's\n * provision/reattach/teardown lifecycle to the per-(project,user) sandbox row.\n * - `materializeRepo(row, token)` runs `git clone` (first open) or `git pull`\n * (re-open) inside the sandbox, using a short-lived installation token that is\n * scrubbed from the git remote afterwards so it never persists in the VM.\n *\n * This module owns everything git/GitHub: clone/pull, commit/push, worktrees,\n * and `gh pr create`. Sandbox provisioning, budgets, and workdir layout live in\n * the fleet module.\n */\n\nimport { createHash } from 'node:crypto';\nimport { reportProgress } from '../../sandbox/fleet.js';\nimport type {\n MaterializationSandbox,\n ProgressFn,\n SandboxBindingStore,\n SandboxCommandResult,\n SandboxFleet,\n} from '../../sandbox/fleet.js';\nimport type {\n ProjectRepositorySandbox,\n SourceControlStorageHandle,\n} from '../../storage/domains/source-control/base.js';\nimport { timedPhase } from '../../timing.js';\n\ntype SourceControlSandboxStorage = SourceControlStorageHandle['sandboxes'];\ntype MaterializationStore = Pick<SourceControlSandboxStorage, 'markMaterialized'>;\n\ninterface RepoMaterializationBinding {\n id: string;\n sandboxWorkdir: string;\n materializedAt: Date | null;\n}\n\n/** Adapt a per-(project,user) sandbox binding row to the fleet's persistence seam. */\nfunction bindingStore(\n row: ProjectRepositorySandbox,\n storage: SourceControlSandboxStorage,\n seedCheckpointName?: string,\n): SandboxBindingStore {\n return {\n sandboxId: row.sandboxId,\n // Boot-only fallback: fresh provisions seed from the repo base checkpoint\n // (when the builder has produced one) so materialization pulls instead of\n // cloning. Snapshots never write here.\n ...(seedCheckpointName ? { seedCheckpointName } : {}),\n setSandboxId: id =>\n id === null ? storage.clearBinding({ id: row.id }) : storage.setSandboxId({ id: row.id, sandboxId: id }),\n clear: () => storage.clearBinding({ id: row.id }),\n };\n}\n\n/**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n */\nexport async function ensureProjectSandbox(options: {\n fleet: SandboxFleet;\n row: ProjectRepositorySandbox;\n storage: SourceControlSandboxStorage;\n token: string;\n onProgress?: ProgressFn;\n /** Repo base checkpoint to seed a fresh provision from (boot-only). */\n seedCheckpointName?: string;\n}): Promise<MaterializationSandbox> {\n const { fleet, row, storage, token, onProgress, seedCheckpointName } = options;\n return fleet.ensureSandbox(bindingStore(row, storage, seedCheckpointName), { GH_TOKEN: token }, onProgress, {\n actingUserId: row.userId,\n });\n}\n\n/**\n * Tear down a user's sandbox for a project: stop the live VM (best-effort) and\n * clear the persisted `sandboxId`/`materializedAt` on the per-(project,user)\n * binding row so the next open re-provisions cleanly.\n *\n * @param row the per-(project,user) sandbox binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\nexport async function teardownProjectSandbox(options: {\n fleet: SandboxFleet;\n row: ProjectRepositorySandbox;\n storage: SourceControlSandboxStorage;\n sandbox?: MaterializationSandbox;\n}): Promise<void> {\n const { fleet, row, storage, sandbox } = options;\n return fleet.teardownSandbox(bindingStore(row, storage), sandbox);\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: MaterializationSandbox,\n script: string,\n options: ShOptions = {},\n): Promise<SandboxCommandResult> {\n // One budget for the command as a whole: each attempt only gets the time\n // remaining, so transport retries can never multiply the hang guard.\n const deadlineMs = Date.now() + (options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);\n for (let attempt = 0; ; attempt++) {\n try {\n return await shOnce(sandbox, script, { ...options, timeoutMs: Math.max(deadlineMs - Date.now(), 1) });\n } catch (error) {\n if (attempt >= SH_RETRIES || !isTransientTransportError(error)) throw error;\n const delayMs = SH_RETRY_DELAY_MS * (attempt + 1);\n if (deadlineMs - Date.now() <= delayMs) throw error;\n await new Promise(resolve => setTimeout(resolve, delayMs));\n }\n }\n}\n\n/** Single `sh -c` execution attempt, bounded by the hang guard. */\nasync function shOnce(\n sandbox: MaterializationSandbox,\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: MaterializationSandbox,\n script: string,\n options: ShOptions & { beforeRetry?: (attempt: number) => Promise<void> } = {},\n): Promise<SandboxCommandResult> {\n const { beforeRetry, ...shOptions } = options;\n const deadlineMs = Date.now() + (shOptions.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS);\n for (let attempt = 0; ; attempt++) {\n const result = await sh(sandbox, script, {\n ...shOptions,\n timeoutMs: Math.max(deadlineMs - Date.now(), 1),\n });\n if (result.exitCode === 0 || attempt >= GIT_TRANSFER_RETRIES || !isTransientGitFailure(result)) return result;\n const delayMs = GIT_TRANSFER_RETRY_DELAY_MS * (attempt + 1);\n if (deadlineMs - Date.now() <= delayMs) return result;\n await new Promise(resolve => setTimeout(resolve, delayMs));\n await beforeRetry?.(attempt + 1);\n }\n}\n\n/** Error raised when the sandbox cannot materialize the repo (actionable). */\nexport class MaterializeError extends Error {\n constructor(\n message: string,\n readonly code:\n | 'git-missing'\n | 'egress-blocked'\n | 'clone-failed'\n | 'pull-failed'\n | 'push-failed'\n | 'commit-failed'\n | 'gh-missing'\n | 'pr-failed',\n ) {\n super(message);\n this.name = 'MaterializeError';\n }\n}\n\n/**\n * Build the token-auth clone/pull URL for a repo. The token lives only inside\n * this URL and is scrubbed from the remote after the operation.\n */\nfunction tokenUrl(repoFullName: string, token: string): string {\n return `https://x-access-token:${token}@github.com/${repoFullName}.git`;\n}\n\nfunction cleanUrl(repoFullName: string): string {\n return `https://github.com/${repoFullName}.git`;\n}\n\n/** Repo metadata needed to materialize, read from the org-owned project row. */\nexport interface RepoMaterializeInfo {\n repoFullName: string;\n defaultBranch: string;\n}\n\n/** Options for {@link materializeRepo}. */\nexport interface MaterializeRepoOptions {\n /** The per-(project,user) sandbox binding (provisioned via `ensureProjectSandbox`). */\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: MaterializationSandbox;\n /** A freshly minted, short-lived installation access token. */\n token: string;\n storage: MaterializationStore;\n onProgress?: ProgressFn;\n /**\n * Skip the default-branch `git pull --ff-only` when the workdir already\n * holds a checkout. Set when the checkout was just seeded from a fresh base\n * checkpoint (rebuilt on every default-branch push), where the pull is\n * redundant network cost: session work happens on a branch that\n * `checkoutSessionBranch` fetches fresh regardless.\n */\n skipPullOnExistingCheckout?: boolean;\n}\n\n/**\n * Materialize the repo inside the user's sandbox. Clones on first open, pulls on\n * re-open. Always scrubs the install token from the remote afterwards and sets\n * `materialized_at` on the per-user sandbox binding row.\n *\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, onProgress, skipPullOnExistingCheckout } = options;\n const workdir = sandboxRow.sandboxWorkdir;\n const repo = repoInfo.repoFullName;\n\n // 0. Defense in depth: never build a git command from values that aren't\n // strictly shaped, even if a malformed row reached the DB. Inputs are also\n // validated at the route boundary before storage.\n if (!/^[\\w.-]+\\/[\\w.-]+$/.test(repo)) {\n throw new MaterializeError(`Refusing to materialize: invalid repo full name '${repo}'.`, 'clone-failed');\n }\n if (!/^[A-Za-z0-9_./-]+$/.test(repoInfo.defaultBranch)) {\n throw new MaterializeError(\n `Refusing to materialize: invalid default branch '${repoInfo.defaultBranch}'.`,\n 'clone-failed',\n );\n }\n\n // 1. Preflight: git must be installed in the sandbox template.\n const gitVersion = await sh(sandbox, 'git --version');\n if (gitVersion.exitCode !== 0) {\n throw new MaterializeError(\n 'git is not installed in the sandbox. The sandbox template must include git.',\n 'git-missing',\n );\n }\n\n const authUrl = tokenUrl(repo, token);\n\n // The DB's `materializedAt` can drift from disk in both directions: a fresh\n // binding row over an already-populated workdir (local dev DB resets,\n // repaired rows, earlier flows) must pull instead of failing `git clone` on\n // the non-empty directory, and a stale `materializedAt` over an empty\n // sandbox (an expired/recreated VM whose disk was wiped) must re-clone\n // instead of running `git -C <workdir>` against a directory that no longer\n // exists. Disk is the source of truth: detect the checkout instead of\n // trusting the row.\n const alreadyMaterialized = await hasExistingCheckout(sandbox, workdir, repo);\n\n let tokenInRemote = false;\n try {\n if (!alreadyMaterialized) {\n // 2a. First open: shallow-clone the default branch into the workdir. A\n // shallow single-branch clone is dramatically faster for large repos; the\n // later re-open uses `git pull --ff-only`, which works on shallow clones.\n reportProgress(onProgress, {\n phase: 'cloning',\n message: `Cloning ${repo} (first open can take a minute)…`,\n });\n // The workdir holds no usable checkout of this repo, but it may not be\n // empty: a checkpoint seed or a clone that died partway (a crashed or\n // OOM-killed server) leaves a partial tree behind, and `git clone`\n // refuses a non-empty destination with a non-retryable fatal. Nothing\n // here is recoverable — `hasExistingCheckout` already ruled out a\n // checkout of this repo — so clear its contents before cloning, exactly\n // as the retry path does. Keep the workdir itself because LocalSandbox\n // runs commands with this directory as the child process cwd.\n await sh(\n sandbox,\n `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,\n );\n const clone = await gitTransfer(\n sandbox,\n `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`,\n {\n phase: 'repository clone',\n beforeRetry: async attempt => {\n reportProgress(onProgress, {\n phase: 'cloning',\n message: `Lost the connection to github.com — retrying the clone (attempt ${attempt + 1})…`,\n });\n // A clone that died partway leaves the destination non-empty, which\n // git refuses to clone into. Clear its contents so the retry starts\n // clean without removing LocalSandbox's process cwd.\n await sh(\n sandbox,\n `mkdir -p ${shellQuote(workdir)} && find ${shellQuote(workdir)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +`,\n );\n },\n },\n );\n if (clone.exitCode !== 0) {\n // git can fail after creating the checkout (\"Clone succeeded, but\n // checkout failed\") with the tokenized origin persisted — probe the\n // disk instead of assuming the failed clone left nothing behind.\n tokenInRemote = await hasGitDir(sandbox, workdir);\n throw classifyGitFailure(clone, 'clone-failed');\n }\n tokenInRemote = true;\n } else if (skipPullOnExistingCheckout) {\n // 2b'. Checkpoint-seeded checkout: the base checkpoint is rebuilt on\n // every default-branch push, so the seeded default branch is already at\n // (or minutes behind) HEAD. Skip the network pull — the session branch\n // is fetched fresh by `checkoutSessionBranch` right after — but still\n // point origin at the token URL so that fetch can authenticate.\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, 'pull-failed');\n }\n tokenInRemote = true;\n } else {\n // 2b. Re-open: refresh remote to the token URL and fast-forward pull.\n reportProgress(onProgress, { phase: 'pulling', message: `Updating ${repo} to the latest changes…` });\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) {\n throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, 'pull-failed');\n }\n tokenInRemote = true;\n const pull = await gitTransfer(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, {\n phase: 'repository pull',\n beforeRetry: async attempt =>\n reportProgress(onProgress, {\n phase: 'pulling',\n message: `Lost the connection to github.com — retrying the update (attempt ${attempt + 1})…`,\n }),\n });\n if (pull.exitCode !== 0) {\n if (!isBenignNonFastForward(pull)) {\n throw classifyGitFailure(pull, 'pull-failed');\n }\n // The workdir was left on a session's working branch that can't be\n // fast-forwarded (diverged from upstream, no upstream, or detached\n // HEAD), or its configured upstream ref was deleted after merge.\n // That checkout still holds usable work — never rebase or reset it\n // here. Leave it as-is and let the session reconcile with the remote\n // itself.\n reportProgress(onProgress, {\n phase: 'pulling',\n message: isDeletedUpstreamRef(pull)\n ? 'Workspace could not be updated from its remote — keeping the existing checkout as-is.'\n : 'Workspace has local changes that diverge from the remote — keeping them as-is.',\n });\n }\n }\n } catch (primary) {\n // 3a. The clone/pull failed — still scrub the token from the VM's git\n // config. The scrub must never hide the actionable failure, but once the\n // token reached the remote its own failure can't stay silent either:\n // report both, primary cause and classification first.\n throw await scrubbedFailure(sandbox, workdir, repo, tokenInRemote, primary, 'pull-failed');\n }\n\n // 3b. Success — the token is in the remote and the workdir has a `.git`, so\n // a failed scrub means the token may still be persisted: surface it.\n await scrubRemote(sandbox, workdir, repo, tokenInRemote);\n\n // 4. Mark materialized.\n reportProgress(onProgress, { phase: 'finalizing', message: 'Finalizing workspace…' });\n await storage.markMaterialized({ id: sandboxRow.id });\n}\n\n/**\n * Reset a pooled workdir that a new session just claimed: the previous\n * session's branch and dirty state must not leak into the new session, so\n * force-checkout the default branch, drop all local modifications, and delete\n * every other local branch (a surviving branch would otherwise hand the next\n * session for the same branch the previous session's stale tip instead of a\n * fresh fork from the base branch). When the claimed VM was reaped and\n * re-provisioned there is no checkout yet and this is a no-op (the clone path\n * handles it). A wedged checkout falls back to wiping the workdir so\n * `materializeRepo` re-clones inside the same VM instead of permanently\n * failing the session.\n */\nexport async function recycleClaimedWorkdir(\n sandbox: MaterializationSandbox,\n workdir: string,\n defaultBranch: string,\n): Promise<void> {\n if (!/^[A-Za-z0-9_./-]+$/.test(defaultBranch)) {\n throw new MaterializeError(`Refusing to recycle: invalid default branch '${defaultBranch}'.`, 'clone-failed');\n }\n const w = shellQuote(workdir);\n const inspect = await sh(sandbox, `git -C ${w} rev-parse --is-inside-work-tree`);\n if (inspect.exitCode !== 0) return;\n const recycle = await sh(\n sandbox,\n // `-x` also drops gitignored files (.env, build caches) so no session\n // state survives into the next claim.\n `git -C ${w} checkout -f ${shellQuote(defaultBranch)} && git -C ${w} reset --hard && git -C ${w} clean -fdx`,\n { phase: 'claimed workdir recycle' },\n );\n let failure = recycle;\n if (recycle.exitCode === 0) {\n // Ref names cannot contain spaces or shell metacharacters (git rejects\n // them), so word-splitting the for-each-ref output is safe. `update-ref -d`\n // instead of `branch -D` so deletion never trips over \"not fully merged\"\n // checks; `--no-deref` so a symbolic ref pointing at the default branch\n // deletes the symref itself rather than following it and deleting the\n // default branch. Broken loose refs are skipped by for-each-ref and\n // handled by the collision fallback in `checkoutSessionBranch`.\n const sweep = await sh(\n sandbox,\n `set -e; for ref in $(git -C ${w} for-each-ref --format='%(refname)' refs/heads); do [ \"$ref\" = ${shellQuote(`refs/heads/${defaultBranch}`)} ] || git -C ${w} update-ref --no-deref -d \"$ref\"; done`,\n { phase: 'claimed workdir branch sweep' },\n );\n if (sweep.exitCode === 0) return;\n failure = sweep;\n }\n const wipe = await sh(sandbox, `rm -rf ${w}`);\n if (wipe.exitCode !== 0) {\n throw new MaterializeError(`Failed to recycle claimed sandbox workdir: ${failure.stderr}`, 'clone-failed');\n }\n}\n\n/** Check out a session's branch inside its isolated repository clone. */\nexport async function checkoutSessionBranch(\n sandbox: MaterializationSandbox,\n workdir: string,\n options: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<void> {\n return timedPhase('workspace.checkout', () => checkoutSessionBranchImpl(sandbox, workdir, options));\n}\n\nasync function checkoutSessionBranchImpl(\n sandbox: MaterializationSandbox,\n workdir: string,\n {\n branch,\n baseBranch,\n token,\n repoFullName,\n }: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<void> {\n if (!isValidGitRef(branch) || !isValidGitRef(baseBranch)) {\n throw new MaterializeError('Refusing to create a session from an invalid branch name.', 'clone-failed');\n }\n\n const current = await sh(sandbox, `git -C ${shellQuote(workdir)} branch --show-current`);\n if (current.exitCode === 0 && current.stdout.trim() === branch) return;\n\n const local = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} show-ref --verify --quiet refs/heads/${shellQuote(branch)}`,\n );\n if (local.exitCode === 0) {\n const checkout = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout ${shellQuote(branch)}`);\n if (checkout.exitCode !== 0) {\n // The session's agent may have switched branches itself (e.g. `gh pr\n // checkout`) and left uncommitted work in the tree. Git refuses to\n // switch back over those files — that work must win. The checkout is\n // intact and usable on its current branch; keep it as-is rather than\n // fail the workspace open, and never reset or stash to force the\n // switch through.\n if (isBlockedByLocalWork(checkout)) return;\n throw classifyGitFailure(checkout, 'clone-failed');\n }\n return;\n }\n\n const authUrl = tokenUrl(repoFullName, token);\n try {\n const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);\n if (setUrl.exitCode !== 0) throw classifyGitFailure(setUrl, 'pull-failed');\n const fetch = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} fetch origin ${shellQuote(baseBranch)} && git -C ${shellQuote(workdir)} checkout -b ${shellQuote(branch)} FETCH_HEAD`,\n { timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS, phase: 'branch checkout' },\n );\n if (fetch.exitCode !== 0) {\n // Same rule as above: uncommitted work in the tree blocks the switch\n // to the new branch. Leave the checkout on its current branch.\n if (isBlockedByLocalWork(fetch)) return;\n if (!isBranchCollision(fetch)) throw classifyGitFailure(fetch, 'clone-failed');\n // The branch exists even though the show-ref probe missed it: either a\n // concurrent materialization of this session created it between the\n // probe and `checkout -b` (adopt it), or a reused sandbox carries a\n // broken loose ref the probe cannot resolve (replace it and retry —\n // \"already exists\" means the fetch half succeeded, so FETCH_HEAD is\n // set).\n const adopt = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout ${shellQuote(branch)}`);\n if (adopt.exitCode === 0 || isBlockedByLocalWork(adopt)) return;\n const drop = await sh(\n sandbox,\n // `--no-deref` so a broken symref is deleted itself instead of git\n // following it to some other branch. `update-ref -d` can still refuse\n // a broken ref; fall back to removing the loose ref file (branch\n // passed isValidGitRef, so the interpolation inside the double quotes\n // is inert).\n `git -C ${shellQuote(workdir)} update-ref --no-deref -d refs/heads/${shellQuote(branch)} || rm -f -- \"$(git -C ${shellQuote(workdir)} rev-parse --absolute-git-dir)/refs/heads/${branch}\"`,\n );\n if (drop.exitCode !== 0) throw classifyGitFailure(fetch, 'clone-failed');\n const retry = await sh(sandbox, `git -C ${shellQuote(workdir)} checkout -b ${shellQuote(branch)} FETCH_HEAD`, {\n timeoutMs: CHECKOUT_COMMAND_TIMEOUT_MS,\n phase: 'branch checkout retry',\n });\n if (retry.exitCode !== 0) {\n if (isBlockedByLocalWork(retry)) return;\n throw classifyGitFailure(retry, 'clone-failed');\n }\n }\n } finally {\n await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`);\n }\n}\n\n/**\n * True when `git checkout -b` failed only because the branch ref already\n * exists — the collision Factory hits when a pooled sandbox carries a ref the\n * show-ref probe could not see (broken loose ref) or a concurrent\n * materialization created the branch after the probe ran.\n */\nfunction isBranchCollision(result: SandboxCommandResult): boolean {\n return /a branch named .* already exists/i.test(`${result.stderr || ''}\\n${result.stdout || ''}`);\n}\n\n/**\n * True when a failed `git checkout` just means uncommitted or untracked files\n * in the working tree would be clobbered by the branch switch. Those files are\n * a session's work in progress — the switch must yield to them, never the\n * other way around.\n */\nfunction isBlockedByLocalWork(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /Your local changes to the following files would be overwritten by checkout|untracked working tree files would be overwritten by checkout/i.test(\n output,\n );\n}\n\n/**\n * True when the workdir already holds a git checkout whose `origin` points at\n * this exact repo. Matches both the clean and token-auth URL forms; any other\n * remote (or no git dir at all) falls back to the clone path.\n */\nexport async function hasExistingCheckout(\n sandbox: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n): Promise<boolean> {\n const result = await sh(sandbox, `git -C ${shellQuote(workdir)} remote get-url origin`);\n if (result.exitCode !== 0) return false;\n const url = result.stdout.trim().toLowerCase();\n const suffix = `github.com/${repoFullName.toLowerCase()}`;\n return url.endsWith(`${suffix}.git`) || url.endsWith(suffix);\n}\n\n/** Probed without `git -C` so a missing workdir returns false instead of throwing. */\nasync function hasGitDir(sandbox: MaterializationSandbox, 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: MaterializationSandbox,\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: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n tokenInRemote: boolean,\n primary: unknown,\n fallback: MaterializeError['code'],\n): Promise<unknown> {\n try {\n await scrubRemote(sandbox, workdir, repoFullName, tokenInRemote);\n return primary;\n } catch (scrubError) {\n const scrubMessage = scrubError instanceof Error ? scrubError.message : String(scrubError);\n if (!(primary instanceof Error)) {\n return new MaterializeError(`${String(primary)} — additionally: ${scrubMessage}`, fallback);\n }\n primary.message = `${primary.message} — additionally: ${scrubMessage}`;\n return primary;\n }\n}\n\n/**\n * True when a failed `git pull --ff-only` on re-open just means the current\n * branch can't be fast-forwarded — not that anything is broken. The shared\n * workdir is routinely left on a session's working branch, which may have\n * local commits diverging from upstream, no upstream at all (session branches\n * are created from `FETCH_HEAD`), or a detached HEAD. A checkout can also\n * hold uncommitted or untracked files (a build or script run left residue,\n * or an older session worked directly in the shared checkout), which makes\n * git refuse the merge outright. A checkout carrying `pull.rebase` in its git\n * config refuses for the same reason but says so in rebase's words instead of\n * merge's. After a PR merges with branch auto-delete, the configured upstream\n * ref may also be gone (`no such ref was fetched` / `couldn't find remote ref`);\n * there is nothing to pull and the checkout is still intact. In all of these\n * cases materialization must keep it as-is rather than fail the workspace open\n * — and must never discard the local state to force the pull through.\n */\nfunction isDeletedUpstreamRef(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /no such ref was fetched|couldn't find remote ref/i.test(output);\n}\n\nfunction isBenignNonFastForward(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return (\n isDeletedUpstreamRef(result) ||\n /Not possible to fast-forward|Diverging branches can't be fast-forwarded|no tracking information for the current branch|You are not currently on a branch|Your local changes to the following files would be overwritten by merge|untracked working tree files would be overwritten by merge|cannot pull with rebase|cannot rebase: You have unstaged changes|Your index contains uncommitted changes/i.test(\n output,\n )\n );\n}\n\n/**\n * Turn a failed git command into an actionable error, detecting the common\n * \"cannot reach github.com\" egress failure.\n */\nfunction classifyGitFailure(\n result: SandboxCommandResult,\n fallback: 'clone-failed' | 'pull-failed' | 'push-failed',\n): MaterializeError {\n const stderr = result.stderr || '';\n if (/could not resolve host|failed to connect|network is unreachable|Connection timed out/i.test(stderr)) {\n return new MaterializeError(\n 'The sandbox could not reach github.com. The sandbox network must allow outbound egress to github.com.',\n 'egress-blocked',\n );\n }\n const verb = fallback === 'clone-failed' ? 'clone' : fallback === 'pull-failed' ? 'pull' : 'push';\n return new MaterializeError(`git ${verb} failed: ${stderr}`, fallback);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 1 — git identity + token-scoped push primitive\n//\n// These helpers let the sandbox author and push commits safely. The install\n// token is short-lived, minted per-operation server-side, injected only into\n// the temporary remote URL inside the sandbox, and always scrubbed afterwards\n// so it never persists in `.git/config`.\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a git ref (branch) name. Server-side defense-in-depth: only allow a\n * conservative character set so a branch can never be built into a shell\n * command in a way that escapes quoting. Mirrors the route-layer check.\n */\nexport function isValidGitRef(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= 255 &&\n // Reject leading-dash refs (e.g. `--mirror`) so the value can never be\n // parsed as a git option when interpolated into a command.\n !value.startsWith('-') &&\n /^[A-Za-z0-9_./-]+$/.test(value)\n );\n}\n\n/** Identity used to author commits inside the sandbox. */\nexport interface GitIdentity {\n name?: string | null;\n email?: string | null;\n /** GitHub login, used to derive a stable noreply identity when name/email are absent. */\n login?: string | null;\n}\n\n/**\n * Resolve a concrete `{ name, email }` for git authorship from a possibly-sparse\n * identity. Falls back to a GitHub-style noreply identity so commits are never\n * authored with an empty or host-derived identity.\n */\nexport function resolveGitIdentity(identity: GitIdentity): { name: string; email: string } {\n const login = (identity.login || '').trim();\n const name = (identity.name || '').trim() || login || 'Mastra Code';\n const email =\n (identity.email || '').trim() ||\n (login ? `${login}@users.noreply.github.com` : 'mastra-code@users.noreply.github.com');\n return { name, email };\n}\n\n/**\n * Configure `user.name` / `user.email` for the given repo working tree inside\n * the sandbox so commits are authored correctly. Values are shell-quoted.\n */\nexport async function configureGitIdentity(\n sandbox: MaterializationSandbox,\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: MaterializationSandbox,\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: MaterializationSandbox,\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 worktree (or repo) path 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: MaterializationSandbox,\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 — worktree / branch lifecycle\n//\n// Each unit of work gets its own branch + working tree inside the same sandbox\n// as the base checkout. The worktree path is always computed server-side from a\n// sanitized branch name; client input never reaches a filesystem path.\n// ---------------------------------------------------------------------------\n\n/** Error raised when a worktree cannot be created/reused inside the sandbox. */\nexport class WorktreeError extends Error {\n constructor(\n message: string,\n readonly code: 'invalid-branch' | 'worktree-failed' | 'setup-failed' | 'teardown-failed',\n ) {\n super(message);\n this.name = 'WorktreeError';\n }\n}\n\n/**\n * Reduce a (already ref-validated) branch name to a filesystem-safe directory\n * segment for the worktree path: slashes/dots/unsafe chars collapsed to `-`.\n * This only affects the *directory name*, never the git branch itself.\n *\n * Sanitization is lossy (e.g. `feat/a` and `feat-a` both reduce to `feat-a`),\n * so an 8-char hash of the original branch is appended whenever the sanitized\n * form differs from the input. That keeps clean names (`main`) readable while\n * guaranteeing distinct branches never share a worktree directory.\n */\nexport function safeBranchDir(branch: string): string {\n const sanitized =\n branch\n .replace(/[^A-Za-z0-9._-]+/g, '-')\n .replace(/\\/+/g, '-')\n .replace(/^[-.]+|[-.]+$/g, '')\n .slice(0, 100) || 'work';\n if (sanitized === branch) return sanitized;\n const hash = createHash('sha256').update(branch).digest('hex').slice(0, 8);\n return `${sanitized}-${hash}`;\n}\n\n/**\n * Compute the absolute worktree path for a branch, server-side only. Worktrees\n * live alongside the repo checkout under a sibling `worktrees/` directory so the\n * repo's `.git` is shared. Never derived from client-supplied paths.\n */\nexport function computeWorktreePath(repoWorkdir: string, branch: string): string {\n const parent = repoWorkdir.replace(/\\/+$/, '').split('/').slice(0, -1).join('/') || '';\n return `${parent}/worktrees/${safeBranchDir(branch)}`;\n}\n\nexport interface EnsureWorktreeResult {\n worktreePath: string;\n branch: string;\n baseBranch: string;\n /** True when an existing worktree was reused rather than freshly created. */\n reused: boolean;\n}\n\n/**\n * Create (or reuse) a git worktree + branch inside the sandbox for a unit of\n * work. Idempotent: if a worktree already exists at the computed path it is\n * reused. The branch is created from the freshly fetched `origin/<baseBranch>`\n * — never the sandbox's possibly stale local ref — so new worktrees always\n * start from the latest remote state.\n *\n * @param sandbox live sandbox containing the base checkout\n * @param repoWorkdir the base repo checkout path inside the sandbox\n * @param branch the feature branch (ref-validated server-side)\n * @param baseBranch the branch to fork from (ref-validated; defaults to the repo's default branch)\n * @param token short-lived installation token used only for the base-branch fetch\n * @param repoFullName `owner/repo` used to build the tokenized remote URL\n */\nexport async function ensureWorktree(\n sandbox: MaterializationSandbox,\n repoWorkdir: string,\n {\n branch,\n baseBranch,\n token,\n repoFullName,\n }: { branch: string; baseBranch: string; token: string; repoFullName: string },\n): Promise<EnsureWorktreeResult> {\n if (!isValidGitRef(branch)) {\n throw new WorktreeError(`Invalid branch name '${branch}'.`, 'invalid-branch');\n }\n if (!isValidGitRef(baseBranch)) {\n throw new WorktreeError(`Invalid base branch name '${baseBranch}'.`, 'invalid-branch');\n }\n\n const worktreePath = computeWorktreePath(repoWorkdir, branch);\n\n // Idempotent reuse: a worktree already checked out at this path has a `.git`\n // file (worktrees use a gitfile, not a directory). Reuse it as-is.\n const exists = await sh(sandbox, `test -e ${shellQuote(`${worktreePath}/.git`)}`);\n if (exists.exitCode === 0) {\n return { worktreePath, branch, baseBranch, reused: true };\n }\n\n // Fetch the latest base ref from origin before forking. The explicit refspec\n // updates `refs/remotes/origin/<base>` even when the checkout was created as\n // a single-branch clone. The fetch needs the install token (the resting\n // remote is tokenless), and a failure is a hard error — silently forking a\n // stale local ref is worse than failing the request.\n const baseRef = `origin/${baseBranch}`;\n await withInstallToken(sandbox, repoWorkdir, repoFullName, token, async () => {\n const fetch = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} fetch origin ${shellQuote(`+refs/heads/${baseBranch}:refs/remotes/${baseRef}`)}`,\n );\n if (fetch.exitCode !== 0) {\n throw classifyGitFailure(fetch, 'pull-failed');\n }\n });\n\n // Create the worktree. If the branch already exists, check it out into the\n // worktree; otherwise create it from the fetched base. `git worktree add -B`\n // creates-or-resets the branch to the base, which keeps this idempotent for a\n // fresh worktree while still working when the branch already exists remotely.\n // `--no-track` keeps the feature branch from tracking origin/<base>; pushes\n // set their own upstream via `push -u`.\n const add = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} worktree add --no-track -B ${shellQuote(branch)} ${shellQuote(worktreePath)} ${shellQuote(baseRef)}`,\n );\n if (add.exitCode !== 0) {\n throw new WorktreeError(`git worktree add failed: ${add.stderr.trim() || add.stdout.trim()}`, 'worktree-failed');\n }\n\n return { worktreePath, branch, baseBranch, reused: false };\n}\n\n/**\n * Run the project's setup command (e.g. `pnpm i && pnpm build`) inside a\n * freshly created worktree. Called before the worktree is handed to any agent\n * run so the checkout is ready to build/test. A non-zero exit is a hard error —\n * starting agent work in a half-set-up tree is worse than failing the 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 worktree\n * @param worktreePath the server-computed worktree path the command runs in\n * @param command the org-configured setup shell command\n */\nasync function runWorktreeLifecycleCommand(\n sandbox: MaterializationSandbox,\n worktreePath: string,\n command: string,\n options: { phase: 'setup' | 'teardown'; timeoutMs?: number },\n): Promise<void> {\n const result = await sh(sandbox, `cd ${shellQuote(worktreePath)} && { ${command}\\n}`, {\n phase: `worktree ${options.phase}`,\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 WorktreeError(\n `${label} command failed (exit ${result.exitCode}): ${detail}`,\n options.phase === 'setup' ? 'setup-failed' : 'teardown-failed',\n );\n }\n}\n\nexport async function runWorktreeSetup(\n sandbox: MaterializationSandbox,\n worktreePath: string,\n command: string,\n): Promise<void> {\n return runWorktreeLifecycleCommand(sandbox, worktreePath, 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 runWorktreeTeardown(\n sandbox: MaterializationSandbox,\n worktreePath: string,\n command: string,\n options: { timeoutMs?: number } = {},\n): Promise<void> {\n return runWorktreeLifecycleCommand(sandbox, worktreePath, command, {\n phase: 'teardown',\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n });\n}\n\n/**\n * Remove a worktree (and its local feature branch) from the sandbox. The\n * checkout is removed with `--force` — the caller owns confirming that any\n * uncommitted work in it can be discarded. Idempotent: a worktree whose\n * directory is already gone only has its metadata pruned.\n *\n * @param sandbox live sandbox containing the base checkout\n * @param repoWorkdir the base repo checkout path inside the sandbox\n * @param branch the worktree's feature branch (ref-validated)\n * @param worktreePath the persisted, server-computed worktree path\n */\nexport async function removeWorktree(\n sandbox: MaterializationSandbox,\n repoWorkdir: string,\n { branch, worktreePath }: { branch: string; worktreePath: string },\n): Promise<void> {\n if (!isValidGitRef(branch)) {\n throw new WorktreeError(`Invalid branch name '${branch}'.`, 'invalid-branch');\n }\n\n const remove = await sh(\n sandbox,\n `git -C ${shellQuote(repoWorkdir)} worktree remove --force ${shellQuote(worktreePath)}`,\n );\n if (remove.exitCode !== 0) {\n // Tolerate a checkout that's already gone (e.g. a fresh sandbox after\n // re-provisioning): prune stale metadata and only fail when the directory\n // still exists, meaning git genuinely refused to remove it.\n await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} worktree prune`);\n const exists = await sh(sandbox, `test -e ${shellQuote(worktreePath)}`);\n if (exists.exitCode === 0) {\n throw new WorktreeError(\n `git worktree remove failed: ${remove.stderr.trim() || remove.stdout.trim()}`,\n 'worktree-failed',\n );\n }\n }\n\n // Best-effort local branch cleanup; the branch may not exist locally anymore\n // or may still be pushed remotely — neither should fail the removal.\n await sh(sandbox, `git -C ${shellQuote(repoWorkdir)} branch -D ${shellQuote(branch)}`);\n}\n\n// ---------------------------------------------------------------------------\n// Phase 3 — `gh` CLI pull-request creation primitive\n//\n// PRs are opened from inside the sandbox with the GitHub CLI. `gh` must be\n// present in the sandbox template (preflighted only on the PR path so clone /\n// open still work when it is absent). The token is passed to `gh` via a\n// per-invocation `GH_TOKEN` env that is scoped to the single `gh` process and\n// never written to git config, a shell rc, or the VM's environment.\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: MaterializationSandbox): 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: MaterializationSandbox,\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,6 +1,7 @@
|
|
|
1
1
|
import type { MountedMastraCode } from '@mastra/code-sdk';
|
|
2
2
|
import type { NotificationPriority } from '@mastra/core/notifications';
|
|
3
3
|
import type { Context } from 'hono';
|
|
4
|
+
import { GithubAppIdentity } from './app-identity.js';
|
|
4
5
|
import type { GithubIntegration, GithubRepositoryPermission } from './integration.js';
|
|
5
6
|
import type { GithubSignalSubscriptionRow, GithubSubscriptionStorage, GithubWebhookPullRequestTarget } from './subscriptions.js';
|
|
6
7
|
export interface GithubWebhookHandlerOptions {
|
|
@@ -68,7 +69,20 @@ export type FactorySessionOwner = {
|
|
|
68
69
|
* much is common to both.
|
|
69
70
|
*/
|
|
70
71
|
export interface GithubWebhookDispatchIntegration {
|
|
72
|
+
/** App slug, used to recognize Factory's own bot identity. */
|
|
73
|
+
readonly slug?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Resolved identity of the App this integration posts as. Preferred over
|
|
76
|
+
* {@link slug}, which names the deployment's own self-hosted App and is unset
|
|
77
|
+
* on deployments that run against Platform's App.
|
|
78
|
+
*/
|
|
79
|
+
readonly identity?: GithubAppIdentity;
|
|
71
80
|
readonly integrationStorage: GithubSubscriptionStorage;
|
|
81
|
+
/**
|
|
82
|
+
* Extra bot logins this deployment authorizes to trigger author-gated
|
|
83
|
+
* notifications, merged over `DEFAULT_AUTHORIZED_BOTS`.
|
|
84
|
+
*/
|
|
85
|
+
readonly authorizedBots?: readonly string[];
|
|
72
86
|
readonly sourceControlStorage: {
|
|
73
87
|
sessions: {
|
|
74
88
|
getBySessionId(sessionId: string): Promise<FactorySessionOwner | null>;
|
|
@@ -90,13 +104,38 @@ export interface GithubWebhookDispatchDependencies {
|
|
|
90
104
|
}) => Promise<GithubSignalSubscriptionRow[]>;
|
|
91
105
|
retireSubscription?: (id: string, status: 'open' | 'closed' | 'merged') => Promise<void>;
|
|
92
106
|
isAuthorizedSender?: (notification: GithubWebhookNotification) => Promise<boolean>;
|
|
107
|
+
/** Called when the sender gate drops a notification, so the drop is observable. */
|
|
108
|
+
onSenderRejected?: (notification: GithubWebhookNotification) => void;
|
|
93
109
|
onTargetError?: (subscription: GithubSignalSubscriptionRow, error: unknown) => void;
|
|
110
|
+
/** Called when a subscription names a thread this deployment does not hold. */
|
|
111
|
+
onTargetSkipped?: (subscription: GithubSignalSubscriptionRow) => void;
|
|
94
112
|
}
|
|
95
113
|
export declare function normalizeGithubWebhookMetadata(parsed: ParsedGithubWebhook): GithubWebhookMetadata;
|
|
96
114
|
export declare function classifyGithubWebhook(parsed: ParsedGithubWebhook): GithubWebhookNotification | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* Reviewer bots authorized out of the box. Deployments extend — never replace —
|
|
117
|
+
* this set through the integration's `authorizedBots`.
|
|
118
|
+
*/
|
|
119
|
+
export declare const DEFAULT_AUTHORIZED_BOTS: readonly string[];
|
|
120
|
+
/**
|
|
121
|
+
* Parse a comma-separated `MASTRACODE_GITHUB_AUTHORIZED_BOTS` value into extra
|
|
122
|
+
* bot logins. Returns undefined when nothing usable was configured.
|
|
123
|
+
*/
|
|
124
|
+
export declare function parseAuthorizedBotsEnv(value: string | undefined): string[] | undefined;
|
|
125
|
+
/** Lowercased union of the default bot logins and any the deployment opted in. */
|
|
126
|
+
export declare function resolveAuthorizedBots(extra?: readonly string[]): Set<string>;
|
|
127
|
+
/**
|
|
128
|
+
* Recognizes Factory's own GitHub App identity. GitHub forbids an app from
|
|
129
|
+
* reviewing a pull request it authored, so `factory-review` falls back to
|
|
130
|
+
* posting its verdict as a comment under this login. Those comments have to
|
|
131
|
+
* clear the author gate for the review handoff to reach the authoring agent;
|
|
132
|
+
* the rules layer still decides which of them are worth acting on.
|
|
133
|
+
*/
|
|
134
|
+
export declare function isFactoryAppSender(sender: string | undefined, slug: string | undefined): boolean;
|
|
97
135
|
export declare function dispatchGithubWebhook(parsed: ParsedGithubWebhook, dependencies: GithubWebhookDispatchDependencies): Promise<{
|
|
98
136
|
delivered: number;
|
|
99
137
|
failed: number;
|
|
138
|
+
skipped: number;
|
|
100
139
|
ignored: boolean;
|
|
101
140
|
}>;
|
|
102
141
|
export declare function handleGithubWebhook(c: Context, options: GithubWebhookHandlerOptions & Partial<Omit<GithubWebhookDispatchDependencies, 'github'>>): Promise<GithubWebhookResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/webhook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAEvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAEtF,OAAO,KAAK,EACV,2BAA2B,EAC3B,yBAAyB,EACzB,8BAA8B,EAC/B,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,2BAA2B;IAC1C,0FAA0F;IAC1F,MAAM,EAAE,iBAAiB,CAAC;IAC1B,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/webhook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAEvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAEtF,OAAO,KAAK,EACV,2BAA2B,EAC3B,yBAAyB,EACzB,8BAA8B,EAC/B,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,2BAA2B;IAC1C,0FAA0F;IAC1F,MAAM,EAAE,iBAAiB,CAAC;IAC1B,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;AAaD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,IAAI,CAAA;KAAE,CAAA;CAAE,GACnD;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE;QAAE,KAAK,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAChE;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE;QAAE,KAAK,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAEtE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,qBAAqB,GAAG;QAAE,iBAAiB,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9G,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,oEAAoE;AACpE,MAAM,MAAM,mBAAmB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;GAIG;AACH,MAAM,WAAW,gCAAgC;IAC/C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IACtC,QAAQ,CAAC,kBAAkB,EAAE,yBAAyB,CAAC;IACvD;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,oBAAoB,EAAE;QAC7B,QAAQ,EAAE;YAAE,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAA;SAAE,CAAC;KACtF,CAAC;IACF,mCAAmC,CACjC,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,0BAA0B,GAAG,SAAS,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,iCAAiC;IAChD,UAAU,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC5C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,gCAAgC,CAAC;IAC1C,iBAAiB,CAAC,EAAE,CAClB,MAAM,EAAE,8BAA8B,EACtC,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,KACpC,OAAO,CAAC,2BAA2B,EAAE,CAAC,CAAC;IAC5C,kBAAkB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,kBAAkB,CAAC,EAAE,CAAC,YAAY,EAAE,yBAAyB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnF,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,CAAC,YAAY,EAAE,yBAAyB,KAAK,IAAI,CAAC;IACrE,aAAa,CAAC,EAAE,CAAC,YAAY,EAAE,2BAA2B,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACpF,+EAA+E;IAC/E,eAAe,CAAC,EAAE,CAAC,YAAY,EAAE,2BAA2B,KAAK,IAAI,CAAC;CACvE;AAuED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,mBAAmB,GAAG,qBAAqB,CAuBjG;AAiBD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,mBAAmB,GAAG,yBAAyB,GAAG,SAAS,CAgGxG;AA+DD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAAuD,CAAC;AAE7G;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,GAAG,SAAS,CAMtF;AAED,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAO5E;AAaD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGhG;AA+CD,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,mBAAmB,EAC3B,YAAY,EAAE,iCAAiC,GAC9C,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAkFnF;AAED,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,EACV,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC,iCAAiC,EAAE,QAAQ,CAAC,CAAC,GAChG,OAAO,CAAC,mBAAmB,CAAC,CAkC9B"}
|
|
@@ -7,7 +7,8 @@ const SUPPORTED_GITHUB_WEBHOOK_EVENTS = /* @__PURE__ */ new Set([
|
|
|
7
7
|
"issue_comment",
|
|
8
8
|
"pull_request",
|
|
9
9
|
"pull_request_review",
|
|
10
|
-
"pull_request_review_comment"
|
|
10
|
+
"pull_request_review_comment",
|
|
11
|
+
"push"
|
|
11
12
|
]);
|
|
12
13
|
function normalizeHeader(value) {
|
|
13
14
|
if (!value) return null;
|
|
@@ -206,8 +207,11 @@ function classifyGithubWebhook(parsed) {
|
|
|
206
207
|
async function resolveSubscriptionSession(controller, subscription, github) {
|
|
207
208
|
const { sessionId, resourceId, threadId } = subscription;
|
|
208
209
|
if (!sessionId || !resourceId || !threadId) throw new Error(`GitHub subscription ${subscription.id} is missing its session binding.`);
|
|
210
|
+
const thread = await controller.queryThreadById({ threadId });
|
|
211
|
+
if (!thread) return void 0;
|
|
212
|
+
const ownerResourceId = thread.resourceId || resourceId;
|
|
209
213
|
const scope = subscription.sessionScope || void 0;
|
|
210
|
-
let session = await controller.getSessionByResource(
|
|
214
|
+
let session = await controller.getSessionByResource(ownerResourceId, scope);
|
|
211
215
|
if (!session) {
|
|
212
216
|
const tags = {
|
|
213
217
|
factoryProjectId: resourceId,
|
|
@@ -224,7 +228,7 @@ async function resolveSubscriptionSession(controller, subscription, github) {
|
|
|
224
228
|
session = await controller.createSession({
|
|
225
229
|
id: sessionId,
|
|
226
230
|
ownerId: sessionRow.userId,
|
|
227
|
-
resourceId,
|
|
231
|
+
resourceId: ownerResourceId,
|
|
228
232
|
scope,
|
|
229
233
|
tags,
|
|
230
234
|
requestContext
|
|
@@ -237,7 +241,28 @@ async function resolveSubscriptionSession(controller, subscription, github) {
|
|
|
237
241
|
if (session.thread.getId() !== threadId) throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);
|
|
238
242
|
return session;
|
|
239
243
|
}
|
|
240
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Reviewer bots authorized out of the box. Deployments extend — never replace —
|
|
246
|
+
* this set through the integration's `authorizedBots`.
|
|
247
|
+
*/
|
|
248
|
+
const DEFAULT_AUTHORIZED_BOTS = ["coderabbitai[bot]", "devin-ai-integration[bot]"];
|
|
249
|
+
/**
|
|
250
|
+
* Parse a comma-separated `MASTRACODE_GITHUB_AUTHORIZED_BOTS` value into extra
|
|
251
|
+
* bot logins. Returns undefined when nothing usable was configured.
|
|
252
|
+
*/
|
|
253
|
+
function parseAuthorizedBotsEnv(value) {
|
|
254
|
+
const bots = (value ?? "").split(",").map((bot) => bot.trim()).filter(Boolean);
|
|
255
|
+
return bots.length > 0 ? bots : void 0;
|
|
256
|
+
}
|
|
257
|
+
/** Lowercased union of the default bot logins and any the deployment opted in. */
|
|
258
|
+
function resolveAuthorizedBots(extra) {
|
|
259
|
+
const bots = new Set(DEFAULT_AUTHORIZED_BOTS);
|
|
260
|
+
for (const bot of extra ?? []) {
|
|
261
|
+
const normalized = bot.trim().toLowerCase();
|
|
262
|
+
if (normalized) bots.add(normalized);
|
|
263
|
+
}
|
|
264
|
+
return bots;
|
|
265
|
+
}
|
|
241
266
|
const AUTHORIZED_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
242
267
|
"admin",
|
|
243
268
|
"maintain",
|
|
@@ -252,13 +277,26 @@ const AUTHOR_GATED_KINDS = /* @__PURE__ */ new Set([
|
|
|
252
277
|
"review-changes-requested",
|
|
253
278
|
"review-dismissed"
|
|
254
279
|
]);
|
|
280
|
+
/**
|
|
281
|
+
* Recognizes Factory's own GitHub App identity. GitHub forbids an app from
|
|
282
|
+
* reviewing a pull request it authored, so `factory-review` falls back to
|
|
283
|
+
* posting its verdict as a comment under this login. Those comments have to
|
|
284
|
+
* clear the author gate for the review handoff to reach the authoring agent;
|
|
285
|
+
* the rules layer still decides which of them are worth acting on.
|
|
286
|
+
*/
|
|
287
|
+
function isFactoryAppSender(sender, slug) {
|
|
288
|
+
if (!sender || !slug) return false;
|
|
289
|
+
return sender.toLowerCase() === `${slug.toLowerCase()}[bot]`;
|
|
290
|
+
}
|
|
255
291
|
async function isAuthorizedGithubSender(notification, github) {
|
|
256
292
|
if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;
|
|
257
293
|
const sender = notification.metadata.sender;
|
|
258
294
|
const repository = notification.metadata.repository;
|
|
259
295
|
if (!sender || !repository) return false;
|
|
296
|
+
if (github?.identity?.matches(sender)) return true;
|
|
297
|
+
if (isFactoryAppSender(sender, github?.slug)) return true;
|
|
260
298
|
const normalizedSender = sender.toLowerCase();
|
|
261
|
-
if (notification.metadata.senderType?.toLowerCase() === "bot" || normalizedSender.endsWith("[bot]")) return
|
|
299
|
+
if (notification.metadata.senderType?.toLowerCase() === "bot" || normalizedSender.endsWith("[bot]")) return resolveAuthorizedBots(github?.authorizedBots).has(normalizedSender);
|
|
262
300
|
if (!github) return false;
|
|
263
301
|
const abortController = new AbortController();
|
|
264
302
|
let timeout;
|
|
@@ -281,13 +319,18 @@ async function dispatchGithubWebhook(parsed, dependencies) {
|
|
|
281
319
|
if (!notification) return {
|
|
282
320
|
delivered: 0,
|
|
283
321
|
failed: 0,
|
|
322
|
+
skipped: 0,
|
|
284
323
|
ignored: true
|
|
285
324
|
};
|
|
286
|
-
if (!await (dependencies.isAuthorizedSender ?? ((n) => isAuthorizedGithubSender(n, dependencies.github)))(notification))
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
325
|
+
if (!await (dependencies.isAuthorizedSender ?? ((n) => isAuthorizedGithubSender(n, dependencies.github)))(notification)) {
|
|
326
|
+
dependencies.onSenderRejected?.(notification);
|
|
327
|
+
return {
|
|
328
|
+
delivered: 0,
|
|
329
|
+
failed: 0,
|
|
330
|
+
skipped: 0,
|
|
331
|
+
ignored: true
|
|
332
|
+
};
|
|
333
|
+
}
|
|
291
334
|
const target = {
|
|
292
335
|
installationExternalId: notification.metadata.installationId.toString(),
|
|
293
336
|
repositoryExternalId: notification.metadata.repositoryId.toString(),
|
|
@@ -304,8 +347,15 @@ async function dispatchGithubWebhook(parsed, dependencies) {
|
|
|
304
347
|
const subscriptions = await listSubscriptions(target, { includeTerminal: notification.action === "reopened" });
|
|
305
348
|
let delivered = 0;
|
|
306
349
|
let failed = 0;
|
|
350
|
+
let skipped = 0;
|
|
307
351
|
for (const subscription of subscriptions) try {
|
|
308
|
-
const
|
|
352
|
+
const session = await resolveSubscriptionSession(dependencies.controller, subscription, dependencies.github);
|
|
353
|
+
if (!session) {
|
|
354
|
+
skipped += 1;
|
|
355
|
+
dependencies.onTargetSkipped?.(subscription);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const result = await session.sendNotificationSignal({
|
|
309
359
|
source: "github",
|
|
310
360
|
kind: notification.kind,
|
|
311
361
|
summary: notification.summary,
|
|
@@ -335,6 +385,7 @@ async function dispatchGithubWebhook(parsed, dependencies) {
|
|
|
335
385
|
return {
|
|
336
386
|
delivered,
|
|
337
387
|
failed,
|
|
388
|
+
skipped,
|
|
338
389
|
ignored: false
|
|
339
390
|
};
|
|
340
391
|
}
|
|
@@ -355,7 +406,17 @@ async function handleGithubWebhook(c, options) {
|
|
|
355
406
|
status: 202,
|
|
356
407
|
body: { ok: true }
|
|
357
408
|
};
|
|
358
|
-
const result = await dispatchGithubWebhook(parsed,
|
|
409
|
+
const result = await dispatchGithubWebhook(parsed, {
|
|
410
|
+
onSenderRejected: (notification) => {
|
|
411
|
+
console.info("[GitHub Webhook] sender not authorized", {
|
|
412
|
+
deliveryId: parsed.deliveryId,
|
|
413
|
+
repository: notification.metadata.repository,
|
|
414
|
+
sender: notification.metadata.sender,
|
|
415
|
+
kind: notification.kind
|
|
416
|
+
});
|
|
417
|
+
},
|
|
418
|
+
...options
|
|
419
|
+
});
|
|
359
420
|
if (result.failed > 0) console.warn(`[GitHub Webhook] ${result.failed} subscribed target(s) failed for delivery ${parsed.deliveryId}.`);
|
|
360
421
|
return {
|
|
361
422
|
status: 202,
|
|
@@ -366,6 +427,6 @@ async function handleGithubWebhook(c, options) {
|
|
|
366
427
|
};
|
|
367
428
|
}
|
|
368
429
|
//#endregion
|
|
369
|
-
export { classifyGithubWebhook, dispatchGithubWebhook, handleGithubWebhook, normalizeGithubWebhookMetadata };
|
|
430
|
+
export { DEFAULT_AUTHORIZED_BOTS, classifyGithubWebhook, dispatchGithubWebhook, handleGithubWebhook, isFactoryAppSender, normalizeGithubWebhookMetadata, parseAuthorizedBotsEnv, resolveAuthorizedBots };
|
|
370
431
|
|
|
371
432
|
//# sourceMappingURL=webhook.js.map
|