@mastra/factory 0.1.0-alpha.3 → 0.1.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/factory.d.ts.map +1 -1
  3. package/dist/factory.js +772 -206
  4. package/dist/factory.js.map +1 -1
  5. package/dist/index.js +772 -206
  6. package/dist/index.js.map +1 -1
  7. package/dist/integrations/github/integration.d.ts +2 -2
  8. package/dist/integrations/github/integration.js +43 -11
  9. package/dist/integrations/github/integration.js.map +1 -1
  10. package/dist/integrations/github/provenance.js.map +1 -1
  11. package/dist/integrations/github/routes.js +10 -9
  12. package/dist/integrations/github/routes.js.map +1 -1
  13. package/dist/integrations/github/sandbox.d.ts +1 -0
  14. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  15. package/dist/integrations/github/sandbox.js +2 -2
  16. package/dist/integrations/github/sandbox.js.map +1 -1
  17. package/dist/integrations/github/session-subscriptions.d.ts +3 -0
  18. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
  19. package/dist/integrations/github/session-subscriptions.js +30 -0
  20. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  21. package/dist/integrations/github/token-refresh.d.ts +6 -0
  22. package/dist/integrations/github/token-refresh.d.ts.map +1 -0
  23. package/dist/integrations/github/token-refresh.js +17 -0
  24. package/dist/integrations/github/token-refresh.js.map +1 -0
  25. package/dist/integrations/platform/github/integration.js +41 -9
  26. package/dist/integrations/platform/github/integration.js.map +1 -1
  27. package/dist/routes/config.d.ts +16 -11
  28. package/dist/routes/config.d.ts.map +1 -1
  29. package/dist/routes/config.js +222 -109
  30. package/dist/routes/config.js.map +1 -1
  31. package/dist/routes/custom-provider-source.d.ts +52 -0
  32. package/dist/routes/custom-provider-source.d.ts.map +1 -0
  33. package/dist/routes/custom-provider-source.js +107 -0
  34. package/dist/routes/custom-provider-source.js.map +1 -0
  35. package/dist/routes/oauth.js +33 -1
  36. package/dist/routes/oauth.js.map +1 -1
  37. package/dist/routes/surface.d.ts +4 -0
  38. package/dist/routes/surface.d.ts.map +1 -1
  39. package/dist/routes/surface.js +266 -110
  40. package/dist/routes/surface.js.map +1 -1
  41. package/dist/sandbox/fleet.d.ts +3 -0
  42. package/dist/sandbox/fleet.d.ts.map +1 -1
  43. package/dist/sandbox/fleet.js +10 -3
  44. package/dist/sandbox/fleet.js.map +1 -1
  45. package/dist/spa-static.d.ts +4 -5
  46. package/dist/spa-static.d.ts.map +1 -1
  47. package/dist/spa-static.js +5 -2
  48. package/dist/spa-static.js.map +1 -1
  49. package/dist/storage/domains/custom-providers/base.d.ts +57 -0
  50. package/dist/storage/domains/custom-providers/base.d.ts.map +1 -0
  51. package/dist/storage/domains/custom-providers/base.js +150 -0
  52. package/dist/storage/domains/custom-providers/base.js.map +1 -0
  53. package/dist/storage/domains/memory-settings/base.d.ts +62 -0
  54. package/dist/storage/domains/memory-settings/base.d.ts.map +1 -0
  55. package/dist/storage/domains/memory-settings/base.js +111 -0
  56. package/dist/storage/domains/memory-settings/base.js.map +1 -0
  57. package/dist/workspace.d.ts.map +1 -1
  58. package/dist/workspace.js +17 -0
  59. package/dist/workspace.js.map +1 -1
  60. package/package.json +7 -6
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/integrations/github/session-subscriptions.ts","../../../src/integrations/github/provenance.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\nimport type {\n ProjectRepository,\n ProjectSourceControlConnection,\n SourceControlInstallation,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { subscribeToPullRequest, unsubscribeFromPullRequest } from './subscriptions.js';\n\ntype RepositorySessionState = { factoryProjectId?: string; projectRepositoryId?: string };\n\n/**\n * Minimal shape of the host-authenticated user placed on the request context\n * under the `user` key. Mirrors the host's auth user without importing it:\n * `workosId` (stable external id) wins over the row `id`, and `organizationId`\n * scopes org tenancy.\n */\ninterface SessionAuthUser {\n workosId?: string;\n id?: string;\n organizationId?: string;\n}\n\nfunction sessionUserId(user: SessionAuthUser | undefined): string | undefined {\n return user?.workosId ?? user?.id;\n}\n\nfunction sessionOrgId(user: SessionAuthUser | undefined): string | undefined {\n return user?.organizationId;\n}\n\nconst pullRequestInputSchema = z.object({\n pullRequest: z.union([z.number().int().positive(), z.string().min(1)]),\n});\n\ninterface SessionTarget {\n context: AgentControllerRequestContext<RepositorySessionState>;\n projectRepository: ProjectRepository;\n connection: ProjectSourceControlConnection;\n installation: SourceControlInstallation;\n repository: SourceControlRepository;\n orgId: string;\n userId: string;\n}\n\nfunction parsePullRequest(value: number | string, expectedRepo: string): number {\n if (typeof value === 'number') return value;\n if (/^\\d+$/.test(value)) return Number(value);\n const match = value.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match || match[1]!.toLowerCase() !== expectedRepo.toLowerCase()) {\n throw new Error(`Pull request must belong to ${expectedRepo}.`);\n }\n return Number(match[2]);\n}\n\n/**\n * Whether the current request comes from a session that GitHub subscriptions\n * can ever apply to: an authenticated org user on a GitHub-project session\n * with an active thread. Mirrors the gate in `resolveSessionTarget` without\n * throwing, for passive callers that should no-op instead of erroring.\n */\nfunction isGithubProjectSession(requestContext: RequestContext): boolean {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n return Boolean(\n context?.threadId && context.getState().projectRepositoryId && sessionOrgId(user) && sessionUserId(user),\n );\n}\n\nasync function resolveSessionTarget(requestContext: RequestContext, github: GithubIntegration): Promise<SessionTarget> {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n const orgId = sessionOrgId(user);\n const userId = sessionUserId(user);\n const projectRepositoryId = context?.getState().projectRepositoryId;\n if (!context || !context.threadId || !projectRepositoryId || !orgId || !userId) {\n throw new Error('GitHub subscriptions require an authenticated repository session with an active thread.');\n }\n\n const projectRepository = await github.sourceControlStorage.projectRepositories.get({\n orgId,\n id: projectRepositoryId,\n });\n if (!projectRepository) throw new Error('Project repository not found for this organization.');\n const connection = await github.sourceControlStorage.connections.get({ orgId, id: projectRepository.connectionId });\n if (!connection) throw new Error('Source-control connection not found for this organization.');\n const repository = await github.sourceControlStorage.repositories.get({ orgId, id: projectRepository.repositoryId });\n if (!repository) throw new Error('Repository not found for this organization.');\n const installation = await github.sourceControlStorage.installations.get({ orgId, id: connection.installationId });\n if (!installation) throw new Error('Source-control installation not found for this organization.');\n return { context, projectRepository, connection, installation, repository, orgId, userId };\n}\n\nasync function verifyPullRequest(target: SessionTarget, pullRequest: number, github: GithubIntegration) {\n const [owner, repo] = target.repository.slug.split('/');\n if (!owner || !repo) throw new Error('GitHub repository is invalid.');\n const octokit = github.getInstallationOctokit(Number(target.installation.externalId));\n const { data } = await octokit.pulls.get({ owner, repo, pull_number: pullRequest });\n if (String(data.base.repo.id) !== target.repository.externalId)\n throw new Error('Pull request repository does not match the active project repository.');\n}\n\nasync function subscriptionInput(target: SessionTarget, pullRequestNumber: number) {\n return {\n orgId: target.orgId,\n installationExternalId: target.installation.externalId,\n projectRepositoryId: target.projectRepository.id,\n repositoryExternalId: target.repository.externalId,\n repositorySlug: target.repository.slug,\n changeRequestId: String(pullRequestNumber),\n sessionId: target.context.session.id,\n ownerId: target.context.session.ownerId,\n resourceId: target.connection.factoryProjectId,\n threadId: target.context.threadId!,\n sessionScope: target.context.scope,\n source: 'explicit-tool' as const,\n subscribedByUserId: target.userId,\n };\n}\n\nexport async function subscribeCurrentSessionToPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n source: 'auto-gh-pr-create' | 'explicit-tool',\n github: GithubIntegration,\n) {\n // The auto path observes every successful `gh pr create` in every session,\n // including local and non-GitHub-project sessions where subscriptions can\n // never apply. Skip silently there; only the explicit tool should surface\n // \"this session cannot subscribe\" as an error.\n if (source === 'auto-gh-pr-create' && !isGithubProjectSession(requestContext)) return undefined;\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await verifyPullRequest(target, number, github);\n await subscribeToPullRequest({ ...(await subscriptionInput(target, number)), source }, github.integrationStorage);\n return number;\n}\n\nexport async function unsubscribeCurrentSessionFromPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);\n return number;\n}\n\nexport function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration) {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n if (!context?.getState().projectRepositoryId || !sessionOrgId(user) || !sessionUserId(user)) return {};\n\n return {\n github_subscribe_pr: createTool({\n id: 'github_subscribe_pr',\n description:\n 'Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, 'explicit-tool', github);\n return { subscribed: true, pullRequestNumber: number };\n },\n }),\n github_unsubscribe_pr: createTool({\n id: 'github_unsubscribe_pr',\n description:\n 'Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);\n return { subscribed: false, pullRequestNumber: number };\n },\n }),\n };\n}\n\nexport function stripHeredocBodies(command: string): string {\n const lines = command.split('\\n');\n const executableLines: string[] = [];\n let delimiter: string | undefined;\n\n for (const line of lines) {\n if (delimiter) {\n if (line.trim() === delimiter) delimiter = undefined;\n continue;\n }\n executableLines.push(line);\n const heredoc = line.match(/<<-?\\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\\1/);\n delimiter = heredoc?.[2];\n }\n\n return executableLines.join('\\n');\n}\n\nexport function parseCreatedPullRequest(context: {\n toolName: string;\n input: unknown;\n output?: unknown;\n error?: unknown;\n}) {\n if (context.toolName !== 'execute_command' || context.error) return undefined;\n const command = (context.input as { command?: unknown } | undefined)?.command;\n if (\n typeof command !== 'string' ||\n !/(?:^|\\n|;|&&|\\|\\|)\\s*gh\\s+pr\\s+create(?:\\s|$)/.test(stripHeredocBodies(command))\n ) {\n return undefined;\n }\n const output = context.output as { stdout?: unknown; result?: unknown } | undefined;\n const stdout = typeof context.output === 'string' ? context.output : (output?.stdout ?? output?.result);\n if (typeof stdout !== 'string') return undefined;\n const urls = stdout.match(/https:\\/\\/github\\.com\\/[^\\s/]+\\/[^\\s/]+\\/pull\\/\\d+/g) ?? [];\n return urls.length === 1 ? urls[0] : undefined;\n}\n","import type { FactoryRuleJsonValue } from '../../rules/types.js';\nimport type { IntegrationStorageHandle } from '../../storage/domains/integrations/base.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { FactoryRunBindingRecord, WorkItemRow } from '../../storage/domains/work-items/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { parseCreatedPullRequest } from './session-subscriptions.js';\n\nexport interface RecordFactoryPullRequestProvenanceInput {\n binding: FactoryRunBindingRecord;\n item: WorkItemRow;\n assistantMessageId: string;\n toolCallId: string;\n toolName: string;\n toolInput: FactoryRuleJsonValue;\n toolResult: FactoryRuleJsonValue;\n status: 'success' | 'error';\n}\n\nexport interface FactoryPullRequestProvenanceData {\n kind: 'factory-pr-provenance';\n bindingId: string;\n workItemId: string;\n repositoryId: number;\n pullRequestNumber: number;\n pullRequestUrl: string;\n assistantMessageId: string;\n toolCallId: string;\n}\n\nexport async function recordFactoryPullRequestProvenance(\n github: GithubIntegration,\n sourceControl: SourceControlStorageHandle,\n integrationStorage: IntegrationStorageHandle<\n Record<string, unknown>,\n Record<string, unknown>,\n FactoryPullRequestProvenanceData\n >,\n input: RecordFactoryPullRequestProvenanceInput,\n): Promise<void> {\n if (input.status !== 'success' || input.item.externalSource?.type === 'pull-request') return;\n const url = parseCreatedPullRequest({\n toolName: input.toolName,\n input: input.toolInput,\n output: input.toolResult,\n });\n if (!url) return;\n\n try {\n const match = url.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match) return;\n const repositorySlug = match[1]!;\n let repositoryId: number | undefined;\n let installationId: number | undefined;\n for (const connection of await sourceControl.connections.list({\n orgId: input.binding.orgId,\n factoryProjectId: input.binding.factoryProjectId,\n })) {\n const installation = await sourceControl.installations.get({\n orgId: input.binding.orgId,\n id: connection.installationId,\n });\n if (!installation) continue;\n for (const link of await sourceControl.projectRepositories.list({\n orgId: input.binding.orgId,\n connectionId: connection.id,\n })) {\n const repository = await sourceControl.repositories.get({ orgId: input.binding.orgId, id: link.repositoryId });\n if (!repository || repository.slug.toLowerCase() !== repositorySlug.toLowerCase()) continue;\n repositoryId = Number(repository.externalId);\n installationId = Number(installation.externalId);\n break;\n }\n if (repositoryId !== undefined) break;\n }\n const pullRequestNumber = Number(match[2]);\n const [owner, repo] = repositorySlug.split('/');\n if (\n !owner ||\n !repo ||\n repositoryId === undefined ||\n installationId === undefined ||\n !Number.isInteger(repositoryId) ||\n !Number.isInteger(installationId) ||\n !Number.isInteger(pullRequestNumber) ||\n pullRequestNumber < 1\n )\n return;\n const targetKey = `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;\n if (\n (await integrationStorage.subscriptions.listByTarget(targetKey)).some(row => row.orgId === input.binding.orgId)\n ) {\n return;\n }\n\n const { data } = await github\n .getInstallationOctokit(installationId)\n .pulls.get({ owner, repo, pull_number: pullRequestNumber });\n if (data.base.repo.id !== repositoryId || data.number !== pullRequestNumber || data.html_url !== url) return;\n\n await integrationStorage.subscriptions.create({\n orgId: input.binding.orgId,\n targetKey,\n threadId: input.binding.threadId,\n status: 'active',\n data: {\n kind: 'factory-pr-provenance',\n bindingId: input.binding.id,\n workItemId: input.item.id,\n repositoryId,\n pullRequestNumber,\n pullRequestUrl: url,\n assistantMessageId: input.assistantMessageId,\n toolCallId: input.toolCallId,\n },\n });\n } catch {\n return;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/integrations/github/session-subscriptions.ts","../../../src/integrations/github/provenance.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\nimport type {\n ProjectRepository,\n ProjectSourceControlConnection,\n SourceControlInstallation,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { subscribeToPullRequest, unsubscribeFromPullRequest } from './subscriptions.js';\nimport { injectGithubToken } from './token-refresh.js';\n\ntype RepositorySessionState = { factoryProjectId?: string; projectRepositoryId?: string };\n\n/**\n * Minimal shape of the host-authenticated user placed on the request context\n * under the `user` key. Mirrors the host's auth user without importing it:\n * `workosId` (stable external id) wins over the row `id`, and `organizationId`\n * scopes org tenancy.\n */\ninterface SessionAuthUser {\n workosId?: string;\n id?: string;\n organizationId?: string;\n}\n\nfunction sessionUserId(user: SessionAuthUser | undefined): string | undefined {\n return user?.workosId ?? user?.id;\n}\n\nfunction sessionOrgId(user: SessionAuthUser | undefined): string | undefined {\n return user?.organizationId;\n}\n\nconst pullRequestInputSchema = z.object({\n pullRequest: z.union([z.number().int().positive(), z.string().min(1)]),\n});\n\ninterface SessionTarget {\n context: AgentControllerRequestContext<RepositorySessionState>;\n projectRepository: ProjectRepository;\n connection: ProjectSourceControlConnection;\n installation: SourceControlInstallation;\n repository: SourceControlRepository;\n orgId: string;\n userId: string;\n}\n\nfunction parsePullRequest(value: number | string, expectedRepo: string): number {\n if (typeof value === 'number') return value;\n if (/^\\d+$/.test(value)) return Number(value);\n const match = value.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match || match[1]!.toLowerCase() !== expectedRepo.toLowerCase()) {\n throw new Error(`Pull request must belong to ${expectedRepo}.`);\n }\n return Number(match[2]);\n}\n\n/**\n * Whether the current request comes from a session that GitHub subscriptions\n * can ever apply to: an authenticated org user on a GitHub-project session\n * with an active thread. Mirrors the gate in `resolveSessionTarget` without\n * throwing, for passive callers that should no-op instead of erroring.\n */\nfunction isGithubProjectSession(requestContext: RequestContext): boolean {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n return Boolean(\n context?.threadId && context.getState().projectRepositoryId && sessionOrgId(user) && sessionUserId(user),\n );\n}\n\nasync function resolveSessionTarget(requestContext: RequestContext, github: GithubIntegration): Promise<SessionTarget> {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n const orgId = sessionOrgId(user);\n const userId = sessionUserId(user);\n const projectRepositoryId = context?.getState().projectRepositoryId;\n if (!context || !context.threadId || !projectRepositoryId || !orgId || !userId) {\n throw new Error('GitHub subscriptions require an authenticated repository session with an active thread.');\n }\n\n const projectRepository = await github.sourceControlStorage.projectRepositories.get({\n orgId,\n id: projectRepositoryId,\n });\n if (!projectRepository) throw new Error('Project repository not found for this organization.');\n const connection = await github.sourceControlStorage.connections.get({ orgId, id: projectRepository.connectionId });\n if (!connection) throw new Error('Source-control connection not found for this organization.');\n const repository = await github.sourceControlStorage.repositories.get({ orgId, id: projectRepository.repositoryId });\n if (!repository) throw new Error('Repository not found for this organization.');\n const installation = await github.sourceControlStorage.installations.get({ orgId, id: connection.installationId });\n if (!installation) throw new Error('Source-control installation not found for this organization.');\n return { context, projectRepository, connection, installation, repository, orgId, userId };\n}\n\nasync function verifyPullRequest(target: SessionTarget, pullRequest: number, github: GithubIntegration) {\n const [owner, repo] = target.repository.slug.split('/');\n if (!owner || !repo) throw new Error('GitHub repository is invalid.');\n const octokit = github.getInstallationOctokit(Number(target.installation.externalId));\n const { data } = await octokit.pulls.get({ owner, repo, pull_number: pullRequest });\n if (String(data.base.repo.id) !== target.repository.externalId)\n throw new Error('Pull request repository does not match the active project repository.');\n}\n\nasync function subscriptionInput(target: SessionTarget, pullRequestNumber: number) {\n return {\n orgId: target.orgId,\n installationExternalId: target.installation.externalId,\n projectRepositoryId: target.projectRepository.id,\n repositoryExternalId: target.repository.externalId,\n repositorySlug: target.repository.slug,\n changeRequestId: String(pullRequestNumber),\n sessionId: target.context.session.id,\n ownerId: target.context.session.ownerId,\n resourceId: target.connection.factoryProjectId,\n threadId: target.context.threadId!,\n sessionScope: target.context.scope,\n source: 'explicit-tool' as const,\n subscribedByUserId: target.userId,\n };\n}\n\nexport async function subscribeCurrentSessionToPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n source: 'auto-gh-pr-create' | 'explicit-tool',\n github: GithubIntegration,\n) {\n // The auto path observes every successful `gh pr create` in every session,\n // including local and non-GitHub-project sessions where subscriptions can\n // never apply. Skip silently there; only the explicit tool should surface\n // \"this session cannot subscribe\" as an error.\n if (source === 'auto-gh-pr-create' && !isGithubProjectSession(requestContext)) return undefined;\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await verifyPullRequest(target, number, github);\n await subscribeToPullRequest({ ...(await subscriptionInput(target, number)), source }, github.integrationStorage);\n return number;\n}\n\nexport async function unsubscribeCurrentSessionFromPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);\n return number;\n}\n\nexport async function refreshGithubToken(requestContext: RequestContext, github: GithubIntegration): Promise<void> {\n const target = await resolveSessionTarget(requestContext, github);\n const access = await github.versionControl.getRepositoryAccess({\n orgId: target.orgId,\n repositoryId: target.repository.id,\n });\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session.');\n injectGithubToken(requestContext, token);\n}\n\nexport function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration) {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n if (!context?.getState().projectRepositoryId || !sessionOrgId(user) || !sessionUserId(user)) return {};\n\n return {\n github_refresh_token: createTool({\n id: 'github_refresh_token',\n description:\n 'Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.',\n inputSchema: z.object({}),\n execute: async () => {\n await refreshGithubToken(requestContext, github);\n return { refreshed: true };\n },\n }),\n github_subscribe_pr: createTool({\n id: 'github_subscribe_pr',\n description:\n 'Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, 'explicit-tool', github);\n return { subscribed: true, pullRequestNumber: number };\n },\n }),\n github_unsubscribe_pr: createTool({\n id: 'github_unsubscribe_pr',\n description:\n 'Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);\n return { subscribed: false, pullRequestNumber: number };\n },\n }),\n };\n}\n\nexport function stripHeredocBodies(command: string): string {\n const lines = command.split('\\n');\n const executableLines: string[] = [];\n let delimiter: string | undefined;\n\n for (const line of lines) {\n if (delimiter) {\n if (line.trim() === delimiter) delimiter = undefined;\n continue;\n }\n executableLines.push(line);\n const heredoc = line.match(/<<-?\\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\\1/);\n delimiter = heredoc?.[2];\n }\n\n return executableLines.join('\\n');\n}\n\nexport function parseCreatedPullRequest(context: {\n toolName: string;\n input: unknown;\n output?: unknown;\n error?: unknown;\n}) {\n if (context.toolName !== 'execute_command' || context.error) return undefined;\n const command = (context.input as { command?: unknown } | undefined)?.command;\n if (\n typeof command !== 'string' ||\n !/(?:^|\\n|;|&&|\\|\\|)\\s*gh\\s+pr\\s+create(?:\\s|$)/.test(stripHeredocBodies(command))\n ) {\n return undefined;\n }\n const output = context.output as { stdout?: unknown; result?: unknown } | undefined;\n const stdout = typeof context.output === 'string' ? context.output : (output?.stdout ?? output?.result);\n if (typeof stdout !== 'string') return undefined;\n const urls = stdout.match(/https:\\/\\/github\\.com\\/[^\\s/]+\\/[^\\s/]+\\/pull\\/\\d+/g) ?? [];\n return urls.length === 1 ? urls[0] : undefined;\n}\n","import type { FactoryRuleJsonValue } from '../../rules/types.js';\nimport type { IntegrationStorageHandle } from '../../storage/domains/integrations/base.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { FactoryRunBindingRecord, WorkItemRow } from '../../storage/domains/work-items/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { parseCreatedPullRequest } from './session-subscriptions.js';\n\nexport interface RecordFactoryPullRequestProvenanceInput {\n binding: FactoryRunBindingRecord;\n item: WorkItemRow;\n assistantMessageId: string;\n toolCallId: string;\n toolName: string;\n toolInput: FactoryRuleJsonValue;\n toolResult: FactoryRuleJsonValue;\n status: 'success' | 'error';\n}\n\nexport interface FactoryPullRequestProvenanceData {\n kind: 'factory-pr-provenance';\n bindingId: string;\n workItemId: string;\n repositoryId: number;\n pullRequestNumber: number;\n pullRequestUrl: string;\n assistantMessageId: string;\n toolCallId: string;\n}\n\nexport async function recordFactoryPullRequestProvenance(\n github: GithubIntegration,\n sourceControl: SourceControlStorageHandle,\n integrationStorage: IntegrationStorageHandle<\n Record<string, unknown>,\n Record<string, unknown>,\n FactoryPullRequestProvenanceData\n >,\n input: RecordFactoryPullRequestProvenanceInput,\n): Promise<void> {\n if (input.status !== 'success' || input.item.externalSource?.type === 'pull-request') return;\n const url = parseCreatedPullRequest({\n toolName: input.toolName,\n input: input.toolInput,\n output: input.toolResult,\n });\n if (!url) return;\n\n try {\n const match = url.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match) return;\n const repositorySlug = match[1]!;\n let repositoryId: number | undefined;\n let installationId: number | undefined;\n for (const connection of await sourceControl.connections.list({\n orgId: input.binding.orgId,\n factoryProjectId: input.binding.factoryProjectId,\n })) {\n const installation = await sourceControl.installations.get({\n orgId: input.binding.orgId,\n id: connection.installationId,\n });\n if (!installation) continue;\n for (const link of await sourceControl.projectRepositories.list({\n orgId: input.binding.orgId,\n connectionId: connection.id,\n })) {\n const repository = await sourceControl.repositories.get({ orgId: input.binding.orgId, id: link.repositoryId });\n if (!repository || repository.slug.toLowerCase() !== repositorySlug.toLowerCase()) continue;\n repositoryId = Number(repository.externalId);\n installationId = Number(installation.externalId);\n break;\n }\n if (repositoryId !== undefined) break;\n }\n const pullRequestNumber = Number(match[2]);\n const [owner, repo] = repositorySlug.split('/');\n if (\n !owner ||\n !repo ||\n repositoryId === undefined ||\n installationId === undefined ||\n !Number.isInteger(repositoryId) ||\n !Number.isInteger(installationId) ||\n !Number.isInteger(pullRequestNumber) ||\n pullRequestNumber < 1\n )\n return;\n const targetKey = `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;\n if (\n (await integrationStorage.subscriptions.listByTarget(targetKey)).some(row => row.orgId === input.binding.orgId)\n ) {\n return;\n }\n\n const { data } = await github\n .getInstallationOctokit(installationId)\n .pulls.get({ owner, repo, pull_number: pullRequestNumber });\n if (data.base.repo.id !== repositoryId || data.number !== pullRequestNumber || data.html_url !== url) return;\n\n await integrationStorage.subscriptions.create({\n orgId: input.binding.orgId,\n targetKey,\n threadId: input.binding.threadId,\n status: 'active',\n data: {\n kind: 'factory-pr-provenance',\n bindingId: input.binding.id,\n workItemId: input.item.id,\n repositoryId,\n pullRequestNumber,\n pullRequestUrl: url,\n assistantMessageId: input.assistantMessageId,\n toolCallId: input.toolCallId,\n },\n });\n } catch {\n return;\n }\n}\n"],"mappings":"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","names":[]}
@@ -114,8 +114,8 @@ function bindingStore(row, storage) {
114
114
  };
115
115
  }
116
116
  async function ensureProjectSandbox(options) {
117
- const { fleet, row, storage, onProgress } = options;
118
- return fleet.ensureSandbox(bindingStore(row, storage), onProgress);
117
+ const { fleet, row, storage, token, onProgress } = options;
118
+ return fleet.ensureSandbox(bindingStore(row, storage), { GH_TOKEN: token }, onProgress);
119
119
  }
120
120
  async function teardownProjectSandbox(options) {
121
121
  const { fleet, row, storage, sandbox } = options;
@@ -1388,13 +1388,6 @@ async function loadOrCreateSandboxRow(github, project, userId) {
1388
1388
  async function prepareProject(options) {
1389
1389
  const { github, fleet, project, userId, onProgress } = options;
1390
1390
  const sandboxRow = await loadOrCreateSandboxRow(github, project, userId);
1391
- const sandbox = await ensureProjectSandbox({
1392
- fleet,
1393
- row: sandboxRow,
1394
- storage: github.sourceControlStorage.sandboxes,
1395
- onProgress
1396
- });
1397
- const fresh = await github.sourceControlStorage.sandboxes.getById({ id: sandboxRow.id });
1398
1391
  const access = await github.versionControl.getRepositoryAccess({
1399
1392
  orgId: project.installation.orgId,
1400
1393
  repositoryId: project.repository.id
@@ -1402,6 +1395,14 @@ async function prepareProject(options) {
1402
1395
  if (!access.authorization) {
1403
1396
  throw new MaterializeError("Repository access did not include a bearer token.", "clone-failed");
1404
1397
  }
1398
+ const sandbox = await ensureProjectSandbox({
1399
+ fleet,
1400
+ row: sandboxRow,
1401
+ storage: github.sourceControlStorage.sandboxes,
1402
+ token: access.authorization.token,
1403
+ onProgress
1404
+ });
1405
+ const fresh = await github.sourceControlStorage.sandboxes.getById({ id: sandboxRow.id });
1405
1406
  const finalRow = fresh ?? sandboxRow;
1406
1407
  await materializeRepo({
1407
1408
  row: finalRow,