@mastra/factory 0.2.2 → 0.3.0-alpha.2
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 +54 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +54 -6
- package/dist/auth.js.map +1 -1
- package/dist/factory.d.ts +10 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +35 -4
- package/dist/factory.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/integrations/base.d.ts +25 -0
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +23 -8
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/sandbox-release.d.ts +38 -0
- package/dist/integrations/github/sandbox-release.d.ts.map +1 -0
- package/dist/integrations/github/sandbox-release.js +74 -0
- package/dist/integrations/github/sandbox-release.js.map +1 -0
- package/dist/integrations/github/sandbox.d.ts +10 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +18 -1
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +1 -1
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts +4 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +52 -17
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/routes/surface.d.ts +3 -1
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +2 -1
- package/dist/routes/surface.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +2 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +43 -12
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/start-coordinator.js +1 -1
- package/dist/rules/transition-service.d.ts +17 -0
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +29 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/sandbox/fleet.d.ts +12 -1
- package/dist/sandbox/fleet.d.ts.map +1 -1
- package/dist/sandbox/fleet.js +35 -1
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/spa-static.js +2 -1
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.d.ts +8 -1
- package/dist/state-signing.d.ts.map +1 -1
- package/dist/state-signing.js +5 -2
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/channel-identity/base.d.ts +96 -0
- package/dist/storage/domains/channel-identity/base.d.ts.map +1 -0
- package/dist/storage/domains/channel-identity/base.js +149 -0
- package/dist/storage/domains/channel-identity/base.js.map +1 -0
- package/dist/storage/domains/source-control/base.d.ts +51 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +91 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +13 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +35 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +19 -3
- package/dist/workspace.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-release.js","names":[],"sources":["../../../src/integrations/github/sandbox-release.ts"],"sourcesContent":["import type { SandboxFleet } from '../../sandbox/fleet.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport { recycleClaimedWorkdir } from './sandbox.js';\n\n/**\n * Scrub a sandbox that is about to enter the reuse pool: force-checkout the\n * default branch and drop the released session's local state, so idle pooled\n * VMs don't sit on stale branches, dirty worktrees, or abandoned work while\n * they wait for the next claim. Best-effort — the VM may already be reaped\n * (the pooled claim then falls back to fresh provisioning) and the claim\n * path recycles the workdir again before reuse, so a failed scrub never\n * blocks the release.\n */\nexport async function cleanReleasedSandbox(options: {\n fleet: Pick<SandboxFleet, 'reattachSandbox'>;\n sourceControl: Pick<SourceControlStorageHandle, 'projectRepositories' | 'repositories'>;\n orgId: string;\n projectRepositoryId: string;\n sandboxId: string;\n sandboxWorkdir: string;\n}): Promise<void> {\n try {\n const projectRepository = await options.sourceControl.projectRepositories.get({\n orgId: options.orgId,\n id: options.projectRepositoryId,\n });\n if (!projectRepository) return;\n const repository = await options.sourceControl.repositories.get({\n orgId: options.orgId,\n id: projectRepository.repositoryId,\n });\n if (!repository) return;\n const sandbox = await options.fleet.reattachSandbox(options.sandboxId);\n await recycleClaimedWorkdir(sandbox, options.sandboxWorkdir, repository.defaultBranch);\n } catch {\n // Reaped, unreachable, or wedged — the claim-side recycle is the\n // correctness guarantee; this scrub is hygiene for idle VMs.\n }\n}\n\n/**\n * Release the sandboxes held by a work item's sessions back to the reuse pool.\n *\n * Called when the item commits into a terminal stage (`done` / `canceled`):\n * its branch sessions stop receiving runs, so their VMs — which would\n * otherwise idle until the provider reaps them — can serve the next session\n * for the same repository instead of a fresh provision. Each session\n * keeps its row (a reopened branch simply claims a pooled VM or provisions\n * fresh on next use); it only loses its sandbox binding.\n */\nexport async function releaseWorkItemSandboxes(options: {\n workItems: Pick<WorkItemsStorage, 'get'>;\n sourceControl: Pick<SourceControlStorageHandle, 'sessions' | 'sandboxPool' | 'projectRepositories' | 'repositories'>;\n fleet: Pick<SandboxFleet, 'reattachSandbox'>;\n orgId: string;\n workItemId: string;\n}): Promise<void> {\n const { workItems, sourceControl } = options;\n const item = await workItems.get({ orgId: options.orgId, id: options.workItemId });\n if (!item) return;\n const sessionIds = [...new Set(Object.values(item.sessions).map(session => session.sessionId))];\n for (const sessionId of sessionIds) {\n const session = await sourceControl.sessions.getBySessionId(sessionId);\n if (!session || session.orgId !== options.orgId) continue;\n if (!session.sandboxId || !session.sandboxWorkdir) continue;\n await cleanReleasedSandbox({\n fleet: options.fleet,\n sourceControl,\n orgId: session.orgId,\n projectRepositoryId: session.projectRepositoryId,\n sandboxId: session.sandboxId,\n sandboxWorkdir: session.sandboxWorkdir,\n });\n await sourceControl.sandboxPool.release({\n orgId: session.orgId,\n projectRepositoryId: session.projectRepositoryId,\n userId: session.userId,\n sandboxId: session.sandboxId,\n sandboxWorkdir: session.sandboxWorkdir,\n });\n await sourceControl.sessions.setSandbox({\n id: session.id,\n sandboxId: null,\n sandboxWorkdir: session.sandboxWorkdir,\n });\n }\n}\n"],"mappings":";;;;;;;;;;;AAcA,eAAsB,qBAAqB,SAOzB;CAChB,IAAI;EACF,MAAM,oBAAoB,MAAM,QAAQ,cAAc,oBAAoB,IAAI;GAC5E,OAAO,QAAQ;GACf,IAAI,QAAQ;EACd,CAAC;EACD,IAAI,CAAC,mBAAmB;EACxB,MAAM,aAAa,MAAM,QAAQ,cAAc,aAAa,IAAI;GAC9D,OAAO,QAAQ;GACf,IAAI,kBAAkB;EACxB,CAAC;EACD,IAAI,CAAC,YAAY;EAEjB,MAAM,sBAAsB,MADN,QAAQ,MAAM,gBAAgB,QAAQ,SAAS,GAChC,QAAQ,gBAAgB,WAAW,aAAa;CACvF,QAAQ,CAGR;AACF;;;;;;;;;;;AAYA,eAAsB,yBAAyB,SAM7B;CAChB,MAAM,EAAE,WAAW,kBAAkB;CACrC,MAAM,OAAO,MAAM,UAAU,IAAI;EAAE,OAAO,QAAQ;EAAO,IAAI,QAAQ;CAAW,CAAC;CACjF,IAAI,CAAC,MAAM;CACX,MAAM,aAAa,CAAC,GAAG,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,KAAI,YAAW,QAAQ,SAAS,CAAC,CAAC;CAC9F,KAAK,MAAM,aAAa,YAAY;EAClC,MAAM,UAAU,MAAM,cAAc,SAAS,eAAe,SAAS;EACrE,IAAI,CAAC,WAAW,QAAQ,UAAU,QAAQ,OAAO;EACjD,IAAI,CAAC,QAAQ,aAAa,CAAC,QAAQ,gBAAgB;EACnD,MAAM,qBAAqB;GACzB,OAAO,QAAQ;GACf;GACA,OAAO,QAAQ;GACf,qBAAqB,QAAQ;GAC7B,WAAW,QAAQ;GACnB,gBAAgB,QAAQ;EAC1B,CAAC;EACD,MAAM,cAAc,YAAY,QAAQ;GACtC,OAAO,QAAQ;GACf,qBAAqB,QAAQ;GAC7B,QAAQ,QAAQ;GAChB,WAAW,QAAQ;GACnB,gBAAgB,QAAQ;EAC1B,CAAC;EACD,MAAM,cAAc,SAAS,WAAW;GACtC,IAAI,QAAQ;GACZ,WAAW;GACX,gBAAgB,QAAQ;EAC1B,CAAC;CACH;AACF"}
|
|
@@ -88,6 +88,16 @@ export declare function materializeRepo(options: {
|
|
|
88
88
|
storage: MaterializationStore;
|
|
89
89
|
onProgress?: ProgressFn;
|
|
90
90
|
}): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Reset a pooled workdir that a new session just claimed: the previous
|
|
93
|
+
* session's branch and dirty state must not leak into the new session, so
|
|
94
|
+
* force-checkout the default branch and drop all local modifications. When
|
|
95
|
+
* the claimed VM was reaped and re-provisioned there is no checkout yet and
|
|
96
|
+
* this is a no-op (the clone path handles it). A wedged checkout falls back
|
|
97
|
+
* to wiping the workdir so `materializeRepo` re-clones inside the same VM
|
|
98
|
+
* instead of permanently failing the session.
|
|
99
|
+
*/
|
|
100
|
+
export declare function recycleClaimedWorkdir(sandbox: MaterializationSandbox, workdir: string, defaultBranch: string): Promise<void>;
|
|
91
101
|
/** Check out a session's branch inside its isolated repository clone. */
|
|
92
102
|
export declare function checkoutSessionBranch(sandbox: MaterializationSandbox, workdir: string, { branch, baseBranch, token, repoFullName, }: {
|
|
93
103
|
branch: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/sandbox.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -190,6 +190,23 @@ async function materializeRepo(options) {
|
|
|
190
190
|
});
|
|
191
191
|
await storage.markMaterialized({ id: sandboxRow.id });
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Reset a pooled workdir that a new session just claimed: the previous
|
|
195
|
+
* session's branch and dirty state must not leak into the new session, so
|
|
196
|
+
* force-checkout the default branch and drop all local modifications. When
|
|
197
|
+
* the claimed VM was reaped and re-provisioned there is no checkout yet and
|
|
198
|
+
* this is a no-op (the clone path handles it). A wedged checkout falls back
|
|
199
|
+
* to wiping the workdir so `materializeRepo` re-clones inside the same VM
|
|
200
|
+
* instead of permanently failing the session.
|
|
201
|
+
*/
|
|
202
|
+
async function recycleClaimedWorkdir(sandbox, workdir, defaultBranch) {
|
|
203
|
+
if (!/^[A-Za-z0-9_./-]+$/.test(defaultBranch)) throw new MaterializeError(`Refusing to recycle: invalid default branch '${defaultBranch}'.`, "clone-failed");
|
|
204
|
+
const w = shellQuote(workdir);
|
|
205
|
+
if ((await sh(sandbox, `git -C ${w} rev-parse --is-inside-work-tree`)).exitCode !== 0) return;
|
|
206
|
+
const recycle = await sh(sandbox, `git -C ${w} checkout -f ${shellQuote(defaultBranch)} && git -C ${w} reset --hard && git -C ${w} clean -fdx`, { phase: "claimed workdir recycle" });
|
|
207
|
+
if (recycle.exitCode === 0) return;
|
|
208
|
+
if ((await sh(sandbox, `rm -rf ${w}`)).exitCode !== 0) throw new MaterializeError(`Failed to recycle claimed sandbox workdir: ${recycle.stderr}`, "clone-failed");
|
|
209
|
+
}
|
|
193
210
|
/** Check out a session's branch inside its isolated repository clone. */
|
|
194
211
|
async function checkoutSessionBranch(sandbox, workdir, { branch, baseBranch, token, repoFullName }) {
|
|
195
212
|
if (!isValidGitRef(branch) || !isValidGitRef(baseBranch)) throw new MaterializeError("Refusing to create a session from an invalid branch name.", "clone-failed");
|
|
@@ -505,6 +522,6 @@ async function createPullRequest(sandbox, workdir, { token, base, head, title, b
|
|
|
505
522
|
return { url };
|
|
506
523
|
}
|
|
507
524
|
//#endregion
|
|
508
|
-
export { CHECKOUT_COMMAND_TIMEOUT_MS, MaterializeError, WorktreeError, checkoutSessionBranch, commitAll, computeWorktreePath, configureGitIdentity, createPullRequest, ensureProjectSandbox, ensureWorktree, isValidGitRef, materializeRepo, pushBranch, removeWorktree, resolveGitIdentity, runWorktreeSetup, safeBranchDir, shellQuote, teardownProjectSandbox, withInstallToken };
|
|
525
|
+
export { CHECKOUT_COMMAND_TIMEOUT_MS, MaterializeError, WorktreeError, checkoutSessionBranch, commitAll, computeWorktreePath, configureGitIdentity, createPullRequest, ensureProjectSandbox, ensureWorktree, isValidGitRef, materializeRepo, pushBranch, recycleClaimedWorkdir, removeWorktree, resolveGitIdentity, runWorktreeSetup, safeBranchDir, shellQuote, teardownProjectSandbox, withInstallToken };
|
|
509
526
|
|
|
510
527
|
//# sourceMappingURL=sandbox.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.js","names":[],"sources":["../../../src/integrations/github/sandbox.ts"],"sourcesContent":["/**\n * Repo materialization for GitHub-backed repositories.\n *\n * A GitHub repo is never cloned onto the server host. 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';\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}\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 */\nconst 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\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/**\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: {\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}): 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 succeeded = 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 sh(\n sandbox,\n `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`,\n { phase: 'repository clone' },\n );\n if (clone.exitCode !== 0) {\n throw classifyGitFailure(clone, 'clone-failed');\n }\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 const pull = await sh(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, { phase: 'repository pull' });\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). That branch holds the session's local work — never rebase or\n // reset it here. The checkout is still perfectly usable; leave it\n // as-is and let the session reconcile with the remote itself.\n reportProgress(onProgress, {\n phase: 'pulling',\n message: 'Workspace has local changes that diverge from the remote — keeping them as-is.',\n });\n }\n }\n succeeded = true;\n } finally {\n // 3. Always scrub the token from the remote so it isn't left in the VM's\n // git config, even when the clone/pull above failed partway through. This\n // is best-effort on the failure path (the workdir may not even exist, and\n // a scrub error must never mask the primary failure); on the success path\n // the scrub must succeed or we surface it.\n await scrubRemote(sandbox, workdir, repo, succeeded);\n }\n\n // 4. Mark materialized.\n reportProgress(onProgress, { phase: 'finalizing', message: 'Finalizing workspace…' });\n await storage.markMaterialized({ id: sandboxRow.id });\n}\n\n/** Check out a session's branch inside its isolated repository clone. */\nexport async function checkoutSessionBranch(\n sandbox: 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) throw classifyGitFailure(checkout, 'clone-failed');\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) throw classifyGitFailure(fetch, 'clone-failed');\n } finally {\n await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`);\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/**\n * Reset the git remote back to the tokenless URL. On a successful clone/pull the\n * workdir always has a `.git`, so a non-zero exit code here means the token may\n * still be persisted — surface it. On the failure path the workdir may not exist\n * (e.g. a failed clone), so a non-zero exit is tolerated — and never masks the\n * primary failure being thrown through the `finally`.\n */\nasync function scrubRemote(\n sandbox: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n expectGitDir: boolean,\n): Promise<void> {\n const result = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`,\n );\n if (result.exitCode !== 0 && expectGitDir) {\n throw new MaterializeError(\n `Failed to scrub installation token from git remote: ${result.stderr.trim() || result.stdout.trim()}`,\n 'pull-failed',\n );\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. In all of those cases\n * the checkout is intact and holds the session's work; materialization must\n * keep it rather than fail the workspace open.\n */\nfunction isBenignNonFastForward(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /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/i.test(\n output,\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 in a\n// `finally` 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 in a `finally`. The\n * token 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 * On the success path the scrub must succeed (a leaked token is a hard error);\n * if it fails we surface it. On the failure path the scrub is best-effort but\n * still attempted, and the original operation error is rethrown.\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 try {\n return await fn();\n } finally {\n // Always restore the tokenless remote. The workdir has a `.git` (we just\n // rewrote its remote) so a scrub failure means the token may still persist\n // — surface it.\n await scrubRemote(sandbox, workdir, repoFullName, true);\n }\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',\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 */\nexport async function runWorktreeSetup(\n sandbox: MaterializationSandbox,\n worktreePath: string,\n command: string,\n): Promise<void> {\n const result = await sh(sandbox, `cd ${shellQuote(worktreePath)} && { ${command}\\n}`, { phase: 'worktree setup' });\n if (result.exitCode !== 0) {\n const detail = (result.stderr.trim() || result.stdout.trim()).slice(-2000);\n throw new WorktreeError(`Setup command failed (exit ${result.exitCode}): ${detail}`, 'setup-failed');\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
|
+
{"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';\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}\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 */\nconst 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\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/**\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: {\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}): 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 succeeded = 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 sh(\n sandbox,\n `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`,\n { phase: 'repository clone' },\n );\n if (clone.exitCode !== 0) {\n throw classifyGitFailure(clone, 'clone-failed');\n }\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 const pull = await sh(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, { phase: 'repository pull' });\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). That branch holds the session's local work — never rebase or\n // reset it here. The checkout is still perfectly usable; leave it\n // as-is and let the session reconcile with the remote itself.\n reportProgress(onProgress, {\n phase: 'pulling',\n message: 'Workspace has local changes that diverge from the remote — keeping them as-is.',\n });\n }\n }\n succeeded = true;\n } finally {\n // 3. Always scrub the token from the remote so it isn't left in the VM's\n // git config, even when the clone/pull above failed partway through. This\n // is best-effort on the failure path (the workdir may not even exist, and\n // a scrub error must never mask the primary failure); on the success path\n // the scrub must succeed or we surface it.\n await scrubRemote(sandbox, workdir, repo, succeeded);\n }\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 {\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) throw classifyGitFailure(checkout, 'clone-failed');\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) throw classifyGitFailure(fetch, 'clone-failed');\n } finally {\n await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`);\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/**\n * Reset the git remote back to the tokenless URL. On a successful clone/pull the\n * workdir always has a `.git`, so a non-zero exit code here means the token may\n * still be persisted — surface it. On the failure path the workdir may not exist\n * (e.g. a failed clone), so a non-zero exit is tolerated — and never masks the\n * primary failure being thrown through the `finally`.\n */\nasync function scrubRemote(\n sandbox: MaterializationSandbox,\n workdir: string,\n repoFullName: string,\n expectGitDir: boolean,\n): Promise<void> {\n const result = await sh(\n sandbox,\n `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`,\n );\n if (result.exitCode !== 0 && expectGitDir) {\n throw new MaterializeError(\n `Failed to scrub installation token from git remote: ${result.stderr.trim() || result.stdout.trim()}`,\n 'pull-failed',\n );\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. In all of those cases\n * the checkout is intact and holds the session's work; materialization must\n * keep it rather than fail the workspace open.\n */\nfunction isBenignNonFastForward(result: SandboxCommandResult): boolean {\n const output = `${result.stderr || ''}\\n${result.stdout || ''}`;\n return /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/i.test(\n output,\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 in a\n// `finally` 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 in a `finally`. The\n * token 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 * On the success path the scrub must succeed (a leaked token is a hard error);\n * if it fails we surface it. On the failure path the scrub is best-effort but\n * still attempted, and the original operation error is rethrown.\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 try {\n return await fn();\n } finally {\n // Always restore the tokenless remote. The workdir has a `.git` (we just\n // rewrote its remote) so a scrub failure means the token may still persist\n // — surface it.\n await scrubRemote(sandbox, workdir, repoFullName, true);\n }\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',\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 */\nexport async function runWorktreeSetup(\n sandbox: MaterializationSandbox,\n worktreePath: string,\n command: string,\n): Promise<void> {\n const result = await sh(sandbox, `cd ${shellQuote(worktreePath)} && { ${command}\\n}`, { phase: 'worktree setup' });\n if (result.exitCode !== 0) {\n const detail = (result.stderr.trim() || result.stdout.trim()).slice(-2000);\n throw new WorktreeError(`Setup command failed (exit ${result.exitCode}): ${detail}`, 'setup-failed');\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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"}
|
|
@@ -250,7 +250,7 @@ var PlatformGithubEventWorker = class extends MastraWorker {
|
|
|
250
250
|
else query.set("afterTimestamp", String(cursor.afterTimestamp));
|
|
251
251
|
const pollStartedAt = performance.now();
|
|
252
252
|
const page = await this.#client.request("GET", `${API_PREFIX}/repositories/${repositoryId}/events?${query}`);
|
|
253
|
-
this.deps?.logger.
|
|
253
|
+
this.deps?.logger.debug("Platform GitHub repository event poll completed", {
|
|
254
254
|
repositoryId,
|
|
255
255
|
eventCount: page.events.length,
|
|
256
256
|
latencyMs: Math.round(performance.now() - pollStartedAt)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-worker.js","names":["#client","#controller","#github","#storage","#ingestFactoryEvent","#reconcileFactoryState","#pollEventsEnabled","#reconcileIntervalMs","#intervalMs","#now","#dispatch","#leaseOwner","#leaseTtlMs","#leaseProvider","#running","#startedAt","#settings","#schedule","#timer","#stopLeaseRenewal","#inFlight","#hasLease","#leaseKey","#tick","#ensureLease","#poll","#startLeaseRenewal","#leaseRenewalTimer","#discoverRepositories","#pollRepository","#maybeReconcile","#lastReconcileAt","#saveSettings","#isAuthorizedSender"],"sources":["../../../../src/integrations/platform/github/event-worker.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MountedMastraCode } from '@mastra/code-sdk';\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type { IntegrationStorageHandle } from '../../../storage/domains/integrations/base.js';\nimport type { GithubRepositoryPermission } from '../../github/integration.js';\nimport type { GithubPullRequestReconciler, ReconcileRepository } from '../../github/rules.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from '../../github/subscriptions.js';\nimport type { GithubSubscriptionStorage } from '../../github/subscriptions.js';\nimport { dispatchGithubWebhook } from '../../github/webhook.js';\nimport type { GithubWebhookNotification, ParsedGithubWebhook } from '../../github/webhook.js';\nimport type { PlatformApiClient } from '../api-client.js';\nimport { PlatformApiError } from '../api-client.js';\n\nconst API_PREFIX = '/v1/server/github-app';\nconst DEFAULT_POLL_INTERVAL_MS = 20_000;\nconst DEFAULT_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst EVENT_PAGE_SIZE = 500;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst CURSOR_ORG_ID = '__platform_github_event_worker__';\nconst CURSOR_USER_ID = 'worker';\nconst SUPPORTED_EVENTS = new Set([\n 'issues',\n 'issue_comment',\n 'pull_request',\n 'pull_request_review',\n 'pull_request_review_comment',\n]);\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment',\n 'pull-request-comment',\n 'pull-request-review',\n 'pull-request-review-comment',\n]);\nconst AUTHORIZED_BOTS = new Set(['coderabbitai[bot]', 'devin-ai-integration[bot]']);\nconst AUTHORIZED_PERMISSIONS = new Set<GithubRepositoryPermission>(['admin', 'maintain', 'write']);\nconst PERMISSION_CHECK_TIMEOUT_MS = 5_000;\n\ntype EventCursor = { afterEventId: string } | { afterTimestamp: number };\ntype PlatformGithubEventWorkerSettings = {\n version: 1;\n repositories: Record<string, EventCursor>;\n};\n\nexport type PlatformGithubEventStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n PlatformGithubEventWorkerSettings,\n Record<string, unknown>\n>;\n\ntype EventLogEntry = {\n id: string;\n deliveryId: string;\n event: string;\n payload: unknown;\n};\n\ntype Repository = { id: number; fullName?: string; installationId: number };\n\nexport interface PlatformGithubEventDispatchIntegration {\n readonly integrationStorage: GithubSubscriptionStorage;\n getRepositoryCollaboratorPermission(\n installationId: number,\n repoFullName: string,\n username: string,\n signal?: AbortSignal,\n ): Promise<GithubRepositoryPermission | undefined>;\n}\n\nexport interface PlatformGithubEventWorkerConfig {\n client: PlatformApiClient;\n controller: MountedMastraCode['controller'];\n github: PlatformGithubEventDispatchIntegration;\n storage: PlatformGithubEventStorage;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n reconcileFactoryState?: GithubPullRequestReconciler;\n /** When false the worker skips event tailing and only runs the reconcile sweep. */\n pollEventsEnabled?: boolean;\n intervalMs?: number;\n reconcileIntervalMs?: number;\n now?: () => number;\n dispatch?: typeof dispatchGithubWebhook;\n}\n\nexport class PlatformGithubEventWorker extends MastraWorker {\n readonly name = 'platform-github-events';\n\n readonly #client: PlatformApiClient;\n readonly #controller: MountedMastraCode['controller'];\n readonly #github: PlatformGithubEventDispatchIntegration;\n readonly #storage: PlatformGithubEventStorage;\n readonly #ingestFactoryEvent: ((event: ParsedGithubWebhook) => Promise<unknown>) | undefined;\n readonly #reconcileFactoryState: GithubPullRequestReconciler | undefined;\n readonly #pollEventsEnabled: boolean;\n readonly #reconcileIntervalMs: number;\n readonly #intervalMs: number;\n readonly #now: () => number;\n readonly #dispatch: typeof dispatchGithubWebhook;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #leaseRenewalTimer: ReturnType<typeof setInterval> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n #leaseTtlMs: number;\n #hasLease = false;\n #startedAt = 0;\n #lastReconcileAt = 0;\n #settings: PlatformGithubEventWorkerSettings = { version: 1, repositories: {} };\n\n constructor(config: PlatformGithubEventWorkerConfig) {\n super();\n this.#client = config.client;\n this.#controller = config.controller;\n this.#github = config.github;\n this.#storage = config.storage;\n this.#ingestFactoryEvent = config.ingestFactoryEvent;\n this.#reconcileFactoryState = config.reconcileFactoryState;\n this.#pollEventsEnabled = config.pollEventsEnabled ?? true;\n this.#reconcileIntervalMs = config.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS;\n this.#intervalMs = config.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('Platform GitHub event polling interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n this.#now = config.now ?? Date.now;\n this.#dispatch = config.dispatch ?? dispatchGithubWebhook;\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('PlatformGithubEventWorker: call init() before start()');\n\n this.#startedAt = this.#now() - 1;\n this.#settings = normalizeSettings(await this.#storage.settings.get(CURSOR_ORG_ID, CURSOR_USER_ID));\n this.#running = true;\n this.deps.logger.info('Platform GitHub event polling started', {\n intervalMs: this.#intervalMs,\n leaseTtlMs: this.#leaseTtlMs,\n });\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n this.#stopLeaseRenewal();\n await this.#inFlight;\n if (this.#hasLease) {\n await this.#leaseProvider.releaseLease(this.#leaseKey(), this.#leaseOwner).catch(() => undefined);\n this.#hasLease = false;\n }\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick();\n this.#inFlight = run;\n void run.finally(() => {\n if (this.#inFlight === run) this.#inFlight = undefined;\n });\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n let nextDelay = this.#intervalMs;\n try {\n if (!(await this.#ensureLease())) return;\n nextDelay = await this.#poll();\n } catch (error) {\n nextDelay = retryDelay(error, this.#intervalMs);\n this.deps?.logger.error('Platform GitHub event polling cycle failed', {\n error: error instanceof Error ? error.message : String(error),\n retryInMs: nextDelay,\n });\n } finally {\n this.#schedule(nextDelay);\n }\n }\n\n async #ensureLease(): Promise<boolean> {\n if (this.#hasLease) return true;\n const result = await this.#leaseProvider.acquireLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs);\n this.#hasLease = result.acquired;\n if (this.#hasLease) this.#startLeaseRenewal();\n return this.#hasLease;\n }\n\n #startLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) return;\n this.#leaseRenewalTimer = setInterval(\n () => {\n void this.#leaseProvider\n .renewLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs)\n .then(renewed => {\n if (!renewed) {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n }\n })\n .catch(error => {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n this.deps?.logger.warn('Platform GitHub event polling lease renewal failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n });\n },\n Math.floor(this.#leaseTtlMs / 3),\n );\n this.#leaseRenewalTimer.unref?.();\n }\n\n #stopLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) clearInterval(this.#leaseRenewalTimer);\n this.#leaseRenewalTimer = undefined;\n }\n\n async #poll(): Promise<number> {\n const repositories = await this.#discoverRepositories();\n // Reconcile-only mode has no event tail to keep fresh, so tick on the\n // slower reconcile cadence instead of the polling interval.\n let retryInMs = this.#pollEventsEnabled ? this.#intervalMs : this.#reconcileIntervalMs;\n\n if (this.#pollEventsEnabled) {\n for (const repository of repositories) {\n if (!this.#running || !this.#hasLease) break;\n try {\n await this.#pollRepository(repository.id);\n } catch (error) {\n const delay = retryDelay(error, this.#intervalMs);\n retryInMs = Math.max(retryInMs, delay);\n this.deps?.logger.error('Platform GitHub repository event polling failed', {\n repositoryId: repository.id,\n error: error instanceof Error ? error.message : String(error),\n retryInMs: delay,\n });\n if (error instanceof PlatformApiError && error.status === 429) break;\n }\n }\n }\n\n await this.#maybeReconcile(repositories);\n\n return retryInMs;\n }\n\n /**\n * State-based safety net: event tailing can miss merges (cursor gaps,\n * downtime, terminally failed decisions), so on a slower cadence we compare\n * still-open PR cards against actual GitHub state and replay missed merges\n * through the normal ingress, which dedupes against the event path.\n */\n async #maybeReconcile(repositories: Repository[]): Promise<void> {\n if (!this.#reconcileFactoryState || !this.#running || !this.#hasLease) return;\n const now = this.#now();\n if (now - this.#lastReconcileAt < this.#reconcileIntervalMs) return;\n // Advance the clock before sweeping so a persistently failing sweep stays\n // on cadence instead of retrying every poll tick.\n this.#lastReconcileAt = now;\n const targets: ReconcileRepository[] = repositories.flatMap(repository =>\n repository.fullName\n ? [{ id: repository.id, fullName: repository.fullName, installationId: repository.installationId }]\n : [],\n );\n if (targets.length === 0) {\n this.deps?.logger.debug('Platform GitHub pull request reconcile skipped: no named repositories');\n return;\n }\n this.deps?.logger.debug('Platform GitHub pull request reconcile sweep starting', {\n candidateRepositories: targets.length,\n });\n const startedAt = Date.now();\n try {\n // `counts.repositories` is the factory-configured subset actually swept;\n // `candidateRepositories` is everything the installations exposed.\n const { errors, ...counts } = await this.#reconcileFactoryState(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform GitHub pull request reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.merged > 0 || counts.closed > 0) {\n this.deps?.logger.info('Platform GitHub pull request reconcile replayed missed merges/closes', context);\n } else {\n this.deps?.logger.info('Platform GitHub pull request reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform GitHub pull request reconcile failed', {\n repositories: targets.length,\n durationMs: Date.now() - startedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n async #discoverRepositories(): Promise<Repository[]> {\n const result = await this.#client.request<{\n installations: Array<{\n installationId: number;\n usable: boolean;\n suspendedAt: string | null;\n }>;\n }>('GET', `${API_PREFIX}/installations`);\n const repositories = new Map<number, Repository>();\n\n for (const installation of result.installations) {\n if (!installation.usable || installation.suspendedAt) continue;\n const page = await this.#client.request<{ repositories: Array<{ id: number; fullName?: string }> }>(\n 'GET',\n `${API_PREFIX}/installations/${installation.installationId}/repositories`,\n );\n for (const repository of page.repositories) {\n repositories.set(repository.id, { ...repository, installationId: installation.installationId });\n }\n }\n\n return [...repositories.values()];\n }\n\n async #pollRepository(repositoryId: number): Promise<void> {\n const key = String(repositoryId);\n if (!this.#settings.repositories[key]) {\n this.#settings.repositories[key] = { afterTimestamp: this.#startedAt };\n await this.#saveSettings();\n }\n\n while (this.#running && this.#hasLease) {\n const cursor: EventCursor = this.#settings.repositories[key]!;\n const query = new URLSearchParams({ limit: String(EVENT_PAGE_SIZE) });\n if ('afterEventId' in cursor) query.set('afterEventId', cursor.afterEventId);\n else query.set('afterTimestamp', String(cursor.afterTimestamp));\n\n const pollStartedAt = performance.now();\n const page = await this.#client.request<{ events: EventLogEntry[]; nextCursor: string | null }>(\n 'GET',\n `${API_PREFIX}/repositories/${repositoryId}/events?${query}`,\n );\n this.deps?.logger.info('Platform GitHub repository event poll completed', {\n repositoryId,\n eventCount: page.events.length,\n latencyMs: Math.round(performance.now() - pollStartedAt),\n });\n if (page.events.length === 0 || !page.nextCursor) return;\n\n for (const event of page.events) {\n if (!this.#running || !this.#hasLease) return;\n const parsed = parseEvent(event);\n if (!parsed) {\n this.deps?.logger.warn('Platform GitHub event log returned a malformed event', {\n repositoryId,\n eventId: event.id,\n });\n continue;\n }\n if (isFactoryClosureEvent(parsed)) {\n await this.#ingestFactoryEvent?.(parsed);\n }\n const result = await this.#dispatch(parsed, {\n controller: this.#controller,\n listSubscriptions: (target, options) =>\n listPullRequestSubscriptionsForWebhook(target, options, this.#github.integrationStorage),\n retireSubscription: (id, status) =>\n retirePullRequestSubscription(id, status, this.#github.integrationStorage),\n isAuthorizedSender: notification => this.#isAuthorizedSender(notification),\n onTargetError: (subscription, error) => {\n this.deps?.logger.error('Platform GitHub event delivery failed for a subscription', {\n subscriptionId: subscription.id,\n resourceId: subscription.resourceId,\n threadId: subscription.threadId,\n error: error instanceof Error ? error.message : String(error),\n });\n },\n });\n if (result.failed > 0) {\n throw new Error(\n `Platform GitHub event ${event.deliveryId} failed for ${result.failed} subscribed target(s).`,\n );\n }\n }\n\n if (page.nextCursor === ('afterEventId' in cursor ? cursor.afterEventId : undefined)) return;\n this.#settings.repositories[key] = { afterEventId: page.nextCursor };\n await this.#saveSettings();\n }\n }\n\n async #isAuthorizedSender(notification: GithubWebhookNotification): Promise<boolean> {\n if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;\n const sender = notification.metadata.sender;\n const repository = notification.metadata.repository;\n if (!sender || !repository) return false;\n if (AUTHORIZED_BOTS.has(sender)) return true;\n\n const abortController = new AbortController();\n const timeout = setTimeout(() => abortController.abort(), PERMISSION_CHECK_TIMEOUT_MS);\n try {\n const permission = await this.#github.getRepositoryCollaboratorPermission(\n notification.metadata.installationId,\n repository,\n sender,\n abortController.signal,\n );\n return permission !== undefined && AUTHORIZED_PERMISSIONS.has(permission);\n } catch {\n return false;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n async #saveSettings(): Promise<void> {\n await this.#storage.settings.save(CURSOR_ORG_ID, CURSOR_USER_ID, this.#settings);\n }\n\n #leaseKey(): string {\n return `${this.name}:${this.#storage.integrationId}`;\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;\n}\n\nfunction normalizeSettings(value: PlatformGithubEventWorkerSettings | null): PlatformGithubEventWorkerSettings {\n if (!value || value.version !== 1 || !value.repositories || typeof value.repositories !== 'object') {\n return { version: 1, repositories: {} };\n }\n return { version: 1, repositories: { ...value.repositories } };\n}\n\nfunction isFactoryClosureEvent(event: ParsedGithubWebhook): boolean {\n return (event.event === 'issues' || event.event === 'pull_request') && event.payload.action === 'closed';\n}\n\nfunction parseEvent(event: EventLogEntry): ParsedGithubWebhook | null {\n if (\n !event.id ||\n !event.deliveryId ||\n !SUPPORTED_EVENTS.has(event.event) ||\n !event.payload ||\n typeof event.payload !== 'object' ||\n Array.isArray(event.payload)\n ) {\n return null;\n }\n return {\n event: event.event,\n deliveryId: event.deliveryId,\n payload: event.payload as Record<string, unknown>,\n };\n}\n\nfunction retryDelay(error: unknown, fallbackMs: number): number {\n if (error instanceof PlatformApiError && error.status === 429 && error.retryAfterSeconds !== null) {\n return Math.max(fallbackMs, error.retryAfterSeconds * 1_000);\n }\n return fallbackMs;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"event-worker.js","names":["#client","#controller","#github","#storage","#ingestFactoryEvent","#reconcileFactoryState","#pollEventsEnabled","#reconcileIntervalMs","#intervalMs","#now","#dispatch","#leaseOwner","#leaseTtlMs","#leaseProvider","#running","#startedAt","#settings","#schedule","#timer","#stopLeaseRenewal","#inFlight","#hasLease","#leaseKey","#tick","#ensureLease","#poll","#startLeaseRenewal","#leaseRenewalTimer","#discoverRepositories","#pollRepository","#maybeReconcile","#lastReconcileAt","#saveSettings","#isAuthorizedSender"],"sources":["../../../../src/integrations/platform/github/event-worker.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MountedMastraCode } from '@mastra/code-sdk';\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type { IntegrationStorageHandle } from '../../../storage/domains/integrations/base.js';\nimport type { GithubRepositoryPermission } from '../../github/integration.js';\nimport type { GithubPullRequestReconciler, ReconcileRepository } from '../../github/rules.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from '../../github/subscriptions.js';\nimport type { GithubSubscriptionStorage } from '../../github/subscriptions.js';\nimport { dispatchGithubWebhook } from '../../github/webhook.js';\nimport type { GithubWebhookNotification, ParsedGithubWebhook } from '../../github/webhook.js';\nimport type { PlatformApiClient } from '../api-client.js';\nimport { PlatformApiError } from '../api-client.js';\n\nconst API_PREFIX = '/v1/server/github-app';\nconst DEFAULT_POLL_INTERVAL_MS = 20_000;\nconst DEFAULT_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst EVENT_PAGE_SIZE = 500;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst CURSOR_ORG_ID = '__platform_github_event_worker__';\nconst CURSOR_USER_ID = 'worker';\nconst SUPPORTED_EVENTS = new Set([\n 'issues',\n 'issue_comment',\n 'pull_request',\n 'pull_request_review',\n 'pull_request_review_comment',\n]);\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment',\n 'pull-request-comment',\n 'pull-request-review',\n 'pull-request-review-comment',\n]);\nconst AUTHORIZED_BOTS = new Set(['coderabbitai[bot]', 'devin-ai-integration[bot]']);\nconst AUTHORIZED_PERMISSIONS = new Set<GithubRepositoryPermission>(['admin', 'maintain', 'write']);\nconst PERMISSION_CHECK_TIMEOUT_MS = 5_000;\n\ntype EventCursor = { afterEventId: string } | { afterTimestamp: number };\ntype PlatformGithubEventWorkerSettings = {\n version: 1;\n repositories: Record<string, EventCursor>;\n};\n\nexport type PlatformGithubEventStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n PlatformGithubEventWorkerSettings,\n Record<string, unknown>\n>;\n\ntype EventLogEntry = {\n id: string;\n deliveryId: string;\n event: string;\n payload: unknown;\n};\n\ntype Repository = { id: number; fullName?: string; installationId: number };\n\nexport interface PlatformGithubEventDispatchIntegration {\n readonly integrationStorage: GithubSubscriptionStorage;\n getRepositoryCollaboratorPermission(\n installationId: number,\n repoFullName: string,\n username: string,\n signal?: AbortSignal,\n ): Promise<GithubRepositoryPermission | undefined>;\n}\n\nexport interface PlatformGithubEventWorkerConfig {\n client: PlatformApiClient;\n controller: MountedMastraCode['controller'];\n github: PlatformGithubEventDispatchIntegration;\n storage: PlatformGithubEventStorage;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n reconcileFactoryState?: GithubPullRequestReconciler;\n /** When false the worker skips event tailing and only runs the reconcile sweep. */\n pollEventsEnabled?: boolean;\n intervalMs?: number;\n reconcileIntervalMs?: number;\n now?: () => number;\n dispatch?: typeof dispatchGithubWebhook;\n}\n\nexport class PlatformGithubEventWorker extends MastraWorker {\n readonly name = 'platform-github-events';\n\n readonly #client: PlatformApiClient;\n readonly #controller: MountedMastraCode['controller'];\n readonly #github: PlatformGithubEventDispatchIntegration;\n readonly #storage: PlatformGithubEventStorage;\n readonly #ingestFactoryEvent: ((event: ParsedGithubWebhook) => Promise<unknown>) | undefined;\n readonly #reconcileFactoryState: GithubPullRequestReconciler | undefined;\n readonly #pollEventsEnabled: boolean;\n readonly #reconcileIntervalMs: number;\n readonly #intervalMs: number;\n readonly #now: () => number;\n readonly #dispatch: typeof dispatchGithubWebhook;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #leaseRenewalTimer: ReturnType<typeof setInterval> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n #leaseTtlMs: number;\n #hasLease = false;\n #startedAt = 0;\n #lastReconcileAt = 0;\n #settings: PlatformGithubEventWorkerSettings = { version: 1, repositories: {} };\n\n constructor(config: PlatformGithubEventWorkerConfig) {\n super();\n this.#client = config.client;\n this.#controller = config.controller;\n this.#github = config.github;\n this.#storage = config.storage;\n this.#ingestFactoryEvent = config.ingestFactoryEvent;\n this.#reconcileFactoryState = config.reconcileFactoryState;\n this.#pollEventsEnabled = config.pollEventsEnabled ?? true;\n this.#reconcileIntervalMs = config.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS;\n this.#intervalMs = config.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('Platform GitHub event polling interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n this.#now = config.now ?? Date.now;\n this.#dispatch = config.dispatch ?? dispatchGithubWebhook;\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('PlatformGithubEventWorker: call init() before start()');\n\n this.#startedAt = this.#now() - 1;\n this.#settings = normalizeSettings(await this.#storage.settings.get(CURSOR_ORG_ID, CURSOR_USER_ID));\n this.#running = true;\n this.deps.logger.info('Platform GitHub event polling started', {\n intervalMs: this.#intervalMs,\n leaseTtlMs: this.#leaseTtlMs,\n });\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n this.#stopLeaseRenewal();\n await this.#inFlight;\n if (this.#hasLease) {\n await this.#leaseProvider.releaseLease(this.#leaseKey(), this.#leaseOwner).catch(() => undefined);\n this.#hasLease = false;\n }\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick();\n this.#inFlight = run;\n void run.finally(() => {\n if (this.#inFlight === run) this.#inFlight = undefined;\n });\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n let nextDelay = this.#intervalMs;\n try {\n if (!(await this.#ensureLease())) return;\n nextDelay = await this.#poll();\n } catch (error) {\n nextDelay = retryDelay(error, this.#intervalMs);\n this.deps?.logger.error('Platform GitHub event polling cycle failed', {\n error: error instanceof Error ? error.message : String(error),\n retryInMs: nextDelay,\n });\n } finally {\n this.#schedule(nextDelay);\n }\n }\n\n async #ensureLease(): Promise<boolean> {\n if (this.#hasLease) return true;\n const result = await this.#leaseProvider.acquireLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs);\n this.#hasLease = result.acquired;\n if (this.#hasLease) this.#startLeaseRenewal();\n return this.#hasLease;\n }\n\n #startLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) return;\n this.#leaseRenewalTimer = setInterval(\n () => {\n void this.#leaseProvider\n .renewLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs)\n .then(renewed => {\n if (!renewed) {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n }\n })\n .catch(error => {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n this.deps?.logger.warn('Platform GitHub event polling lease renewal failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n });\n },\n Math.floor(this.#leaseTtlMs / 3),\n );\n this.#leaseRenewalTimer.unref?.();\n }\n\n #stopLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) clearInterval(this.#leaseRenewalTimer);\n this.#leaseRenewalTimer = undefined;\n }\n\n async #poll(): Promise<number> {\n const repositories = await this.#discoverRepositories();\n // Reconcile-only mode has no event tail to keep fresh, so tick on the\n // slower reconcile cadence instead of the polling interval.\n let retryInMs = this.#pollEventsEnabled ? this.#intervalMs : this.#reconcileIntervalMs;\n\n if (this.#pollEventsEnabled) {\n for (const repository of repositories) {\n if (!this.#running || !this.#hasLease) break;\n try {\n await this.#pollRepository(repository.id);\n } catch (error) {\n const delay = retryDelay(error, this.#intervalMs);\n retryInMs = Math.max(retryInMs, delay);\n this.deps?.logger.error('Platform GitHub repository event polling failed', {\n repositoryId: repository.id,\n error: error instanceof Error ? error.message : String(error),\n retryInMs: delay,\n });\n if (error instanceof PlatformApiError && error.status === 429) break;\n }\n }\n }\n\n await this.#maybeReconcile(repositories);\n\n return retryInMs;\n }\n\n /**\n * State-based safety net: event tailing can miss merges (cursor gaps,\n * downtime, terminally failed decisions), so on a slower cadence we compare\n * still-open PR cards against actual GitHub state and replay missed merges\n * through the normal ingress, which dedupes against the event path.\n */\n async #maybeReconcile(repositories: Repository[]): Promise<void> {\n if (!this.#reconcileFactoryState || !this.#running || !this.#hasLease) return;\n const now = this.#now();\n if (now - this.#lastReconcileAt < this.#reconcileIntervalMs) return;\n // Advance the clock before sweeping so a persistently failing sweep stays\n // on cadence instead of retrying every poll tick.\n this.#lastReconcileAt = now;\n const targets: ReconcileRepository[] = repositories.flatMap(repository =>\n repository.fullName\n ? [{ id: repository.id, fullName: repository.fullName, installationId: repository.installationId }]\n : [],\n );\n if (targets.length === 0) {\n this.deps?.logger.debug('Platform GitHub pull request reconcile skipped: no named repositories');\n return;\n }\n this.deps?.logger.debug('Platform GitHub pull request reconcile sweep starting', {\n candidateRepositories: targets.length,\n });\n const startedAt = Date.now();\n try {\n // `counts.repositories` is the factory-configured subset actually swept;\n // `candidateRepositories` is everything the installations exposed.\n const { errors, ...counts } = await this.#reconcileFactoryState(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform GitHub pull request reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.merged > 0 || counts.closed > 0) {\n this.deps?.logger.info('Platform GitHub pull request reconcile replayed missed merges/closes', context);\n } else {\n this.deps?.logger.info('Platform GitHub pull request reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform GitHub pull request reconcile failed', {\n repositories: targets.length,\n durationMs: Date.now() - startedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n async #discoverRepositories(): Promise<Repository[]> {\n const result = await this.#client.request<{\n installations: Array<{\n installationId: number;\n usable: boolean;\n suspendedAt: string | null;\n }>;\n }>('GET', `${API_PREFIX}/installations`);\n const repositories = new Map<number, Repository>();\n\n for (const installation of result.installations) {\n if (!installation.usable || installation.suspendedAt) continue;\n const page = await this.#client.request<{ repositories: Array<{ id: number; fullName?: string }> }>(\n 'GET',\n `${API_PREFIX}/installations/${installation.installationId}/repositories`,\n );\n for (const repository of page.repositories) {\n repositories.set(repository.id, { ...repository, installationId: installation.installationId });\n }\n }\n\n return [...repositories.values()];\n }\n\n async #pollRepository(repositoryId: number): Promise<void> {\n const key = String(repositoryId);\n if (!this.#settings.repositories[key]) {\n this.#settings.repositories[key] = { afterTimestamp: this.#startedAt };\n await this.#saveSettings();\n }\n\n while (this.#running && this.#hasLease) {\n const cursor: EventCursor = this.#settings.repositories[key]!;\n const query = new URLSearchParams({ limit: String(EVENT_PAGE_SIZE) });\n if ('afterEventId' in cursor) query.set('afterEventId', cursor.afterEventId);\n else query.set('afterTimestamp', String(cursor.afterTimestamp));\n\n const pollStartedAt = performance.now();\n const page = await this.#client.request<{ events: EventLogEntry[]; nextCursor: string | null }>(\n 'GET',\n `${API_PREFIX}/repositories/${repositoryId}/events?${query}`,\n );\n this.deps?.logger.debug('Platform GitHub repository event poll completed', {\n repositoryId,\n eventCount: page.events.length,\n latencyMs: Math.round(performance.now() - pollStartedAt),\n });\n if (page.events.length === 0 || !page.nextCursor) return;\n\n for (const event of page.events) {\n if (!this.#running || !this.#hasLease) return;\n const parsed = parseEvent(event);\n if (!parsed) {\n this.deps?.logger.warn('Platform GitHub event log returned a malformed event', {\n repositoryId,\n eventId: event.id,\n });\n continue;\n }\n if (isFactoryClosureEvent(parsed)) {\n await this.#ingestFactoryEvent?.(parsed);\n }\n const result = await this.#dispatch(parsed, {\n controller: this.#controller,\n listSubscriptions: (target, options) =>\n listPullRequestSubscriptionsForWebhook(target, options, this.#github.integrationStorage),\n retireSubscription: (id, status) =>\n retirePullRequestSubscription(id, status, this.#github.integrationStorage),\n isAuthorizedSender: notification => this.#isAuthorizedSender(notification),\n onTargetError: (subscription, error) => {\n this.deps?.logger.error('Platform GitHub event delivery failed for a subscription', {\n subscriptionId: subscription.id,\n resourceId: subscription.resourceId,\n threadId: subscription.threadId,\n error: error instanceof Error ? error.message : String(error),\n });\n },\n });\n if (result.failed > 0) {\n throw new Error(\n `Platform GitHub event ${event.deliveryId} failed for ${result.failed} subscribed target(s).`,\n );\n }\n }\n\n if (page.nextCursor === ('afterEventId' in cursor ? cursor.afterEventId : undefined)) return;\n this.#settings.repositories[key] = { afterEventId: page.nextCursor };\n await this.#saveSettings();\n }\n }\n\n async #isAuthorizedSender(notification: GithubWebhookNotification): Promise<boolean> {\n if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;\n const sender = notification.metadata.sender;\n const repository = notification.metadata.repository;\n if (!sender || !repository) return false;\n if (AUTHORIZED_BOTS.has(sender)) return true;\n\n const abortController = new AbortController();\n const timeout = setTimeout(() => abortController.abort(), PERMISSION_CHECK_TIMEOUT_MS);\n try {\n const permission = await this.#github.getRepositoryCollaboratorPermission(\n notification.metadata.installationId,\n repository,\n sender,\n abortController.signal,\n );\n return permission !== undefined && AUTHORIZED_PERMISSIONS.has(permission);\n } catch {\n return false;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n async #saveSettings(): Promise<void> {\n await this.#storage.settings.save(CURSOR_ORG_ID, CURSOR_USER_ID, this.#settings);\n }\n\n #leaseKey(): string {\n return `${this.name}:${this.#storage.integrationId}`;\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;\n}\n\nfunction normalizeSettings(value: PlatformGithubEventWorkerSettings | null): PlatformGithubEventWorkerSettings {\n if (!value || value.version !== 1 || !value.repositories || typeof value.repositories !== 'object') {\n return { version: 1, repositories: {} };\n }\n return { version: 1, repositories: { ...value.repositories } };\n}\n\nfunction isFactoryClosureEvent(event: ParsedGithubWebhook): boolean {\n return (event.event === 'issues' || event.event === 'pull_request') && event.payload.action === 'closed';\n}\n\nfunction parseEvent(event: EventLogEntry): ParsedGithubWebhook | null {\n if (\n !event.id ||\n !event.deliveryId ||\n !SUPPORTED_EVENTS.has(event.event) ||\n !event.payload ||\n typeof event.payload !== 'object' ||\n Array.isArray(event.payload)\n ) {\n return null;\n }\n return {\n event: event.event,\n deliveryId: event.deliveryId,\n payload: event.payload as Record<string, unknown>,\n };\n}\n\nfunction retryDelay(error: unknown, fallbackMs: number): number {\n if (error instanceof PlatformApiError && error.status === 429 && error.retryAfterSeconds !== null) {\n return Math.max(fallbackMs, error.retryAfterSeconds * 1_000);\n }\n return fallbackMs;\n}\n"],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ import type { IntegrationStorageHandle } from '../../../storage/domains/integrat
|
|
|
6
6
|
import type { SourceControlStorageHandle } from '../../../storage/domains/source-control/base.js';
|
|
7
7
|
import type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../../base.js';
|
|
8
8
|
import type { GithubIntegration, GithubRepositoryPermission, RepoSummary } from '../../github/integration.js';
|
|
9
|
+
import type { GithubIssueTriageInput, GithubIssueTriageResult } from '../../github/issue-triage.js';
|
|
9
10
|
import type { ReconcilePullRequestState } from '../../github/rules.js';
|
|
10
11
|
import type { GithubSubscriptionStorage } from '../../github/subscriptions.js';
|
|
11
12
|
import { PlatformGithubEventWorker } from './event-worker.js';
|
|
@@ -14,7 +15,9 @@ export declare class PlatformGithubIntegration implements FactoryIntegration {
|
|
|
14
15
|
readonly id = "github";
|
|
15
16
|
readonly intake: Intake;
|
|
16
17
|
readonly versionControl: VersionControl;
|
|
17
|
-
constructor(
|
|
18
|
+
constructor(options?: {
|
|
19
|
+
runIssueTriage?: (input: GithubIssueTriageInput) => Promise<GithubIssueTriageResult>;
|
|
20
|
+
});
|
|
18
21
|
get storage(): SourceControlStorageHandle;
|
|
19
22
|
get sourceControlStorage(): SourceControlStorageHandle;
|
|
20
23
|
get integrationStorage(): GithubSubscriptionStorage;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/github/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAKpD,OAAO,KAAK,EAEV,MAAM,EAIP,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAwBV,cAAc,EACf,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+CAA+C,CAAC;AAC9F,OAAO,KAAK,EAEV,0BAA0B,EAC3B,MAAM,iDAAiD,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC9F,OAAO,KAAK,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/github/integration.ts"],"names":[],"mappings":"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"}
|