@mastra/factory 0.10.2-alpha.0 → 0.10.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/LICENSE.md +6 -4
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +4 -1
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/webhook.d.ts.map +1 -1
- package/dist/integrations/github/webhook.js +9 -0
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +5 -5
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +2 -0
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/memory-settings-hydration.d.ts +21 -8
- package/dist/session/memory-settings-hydration.d.ts.map +1 -1
- package/dist/session/memory-settings-hydration.js +27 -9
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/org-seed.d.ts +67 -0
- package/dist/session/org-seed.d.ts.map +1 -0
- package/dist/session/org-seed.js +53 -0
- package/dist/session/org-seed.js.map +1 -0
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.js","names":[],"sources":["../../../src/integrations/github/webhook.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { MountedMastraCode } from '@mastra/code-sdk';\nimport type { NotificationPriority } from '@mastra/core/notifications';\nimport { RequestContext } from '@mastra/core/request-context';\nimport type { Context } from 'hono';\nimport { GithubAppIdentity } from './app-identity.js';\nimport type { GithubIntegration, GithubRepositoryPermission } from './integration.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from './subscriptions.js';\nimport type {\n GithubSignalSubscriptionRow,\n GithubSubscriptionStorage,\n GithubWebhookPullRequestTarget,\n} from './subscriptions.js';\n\nexport interface GithubWebhookHandlerOptions {\n /** Integration providing webhook-secret verification + collaborator permission checks. */\n github: GithubIntegration;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n}\n\nconst SUPPORTED_GITHUB_WEBHOOK_EVENTS = new Set([\n 'issues',\n 'issue_comment',\n 'pull_request',\n 'pull_request_review',\n 'pull_request_review_comment',\n // Direct pushes to the default branch drive base-checkpoint rebuilds. The\n // rules engine and subscription dispatcher both ignore push events.\n 'push',\n]);\n\nexport interface GithubWebhookMetadata {\n event: string;\n action?: string;\n deliveryId: string;\n repository?: string;\n repositoryId?: number;\n issueNumber?: number;\n pullRequestNumber?: number;\n sender?: string;\n senderType?: string;\n installationId?: number;\n}\n\nexport interface ParsedGithubWebhook {\n event: string;\n deliveryId: string;\n payload: Record<string, unknown>;\n}\n\nexport type GithubWebhookResult =\n | { status: 202; body: { ok: true; ignored?: true } }\n | { status: 400; body: { error: 'bad_request'; message: string } }\n | { status: 401; body: { error: 'unauthorized'; message: string } };\n\nexport interface GithubWebhookNotification {\n action: string;\n kind: string;\n priority: NotificationPriority;\n summary: string;\n terminal: boolean;\n metadata: GithubWebhookMetadata & { pullRequestNumber: number; repositoryId: number; installationId: number };\n payload: Record<string, unknown>;\n}\n\n/** The Factory session row fields a woken session has to run as. */\nexport type FactorySessionOwner = { userId: string; orgId: string };\n\n/**\n * The integration surface this dispatch uses. Narrow on purpose: the GitHub App\n * integration and the platform-backed one are unrelated classes, and only this\n * much is common to both.\n */\nexport interface GithubWebhookDispatchIntegration {\n /** App slug, used to recognize Factory's own bot identity. */\n readonly slug?: string;\n /**\n * Resolved identity of the App this integration posts as. Preferred over\n * {@link slug}, which names the deployment's own self-hosted App and is unset\n * on deployments that run against Platform's App.\n */\n readonly identity?: GithubAppIdentity;\n readonly integrationStorage: GithubSubscriptionStorage;\n /**\n * Extra bot logins this deployment authorizes to trigger author-gated\n * notifications, merged over `DEFAULT_AUTHORIZED_BOTS`.\n */\n readonly authorizedBots?: readonly string[];\n readonly sourceControlStorage: {\n sessions: { getBySessionId(sessionId: string): Promise<FactorySessionOwner | null> };\n };\n getRepositoryCollaboratorPermission(\n installationId: number,\n repoFullName: string,\n username: string,\n signal?: AbortSignal,\n ): Promise<GithubRepositoryPermission | undefined>;\n}\n\nexport interface GithubWebhookDispatchDependencies {\n controller: MountedMastraCode['controller'];\n /**\n * Integration used by the default sender-authorization check (collaborator\n * permission lookup) and to resolve the owner of a session being recreated.\n * Author-gated notifications fail closed when neither this nor an\n * `isAuthorizedSender` override is supplied.\n */\n github?: GithubWebhookDispatchIntegration;\n listSubscriptions?: (\n target: GithubWebhookPullRequestTarget,\n options?: { includeTerminal?: boolean },\n ) => Promise<GithubSignalSubscriptionRow[]>;\n retireSubscription?: (id: string, status: 'open' | 'closed' | 'merged') => Promise<void>;\n isAuthorizedSender?: (notification: GithubWebhookNotification) => Promise<boolean>;\n /** Called when the sender gate drops a notification, so the drop is observable. */\n onSenderRejected?: (notification: GithubWebhookNotification) => void;\n onTargetError?: (subscription: GithubSignalSubscriptionRow, error: unknown) => void;\n /** Called when a subscription names a thread this deployment does not hold. */\n onTargetSkipped?: (subscription: GithubSignalSubscriptionRow) => void;\n}\n\nfunction normalizeHeader(value: string | undefined | null): string | null {\n if (!value) return null;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\nfunction verifySignature(rawBody: string, signature: string, secret: string): boolean {\n if (!signature.startsWith('sha256=')) return false;\n const signatureHex = signature.slice('sha256='.length);\n if (!/^[a-fA-F0-9]{64}$/.test(signatureHex)) return false;\n\n const expectedHex = createHmac('sha256', secret).update(rawBody).digest('hex');\n const received = Buffer.from(signatureHex, 'hex');\n const expected = Buffer.from(expectedHex, 'hex');\n return received.length === expected.length && timingSafeEqual(received, expected);\n}\n\nasync function parseGithubWebhook(\n c: Context,\n secret: string | undefined,\n): Promise<ParsedGithubWebhook | GithubWebhookResult> {\n if (!secret) {\n return { status: 401, body: { error: 'unauthorized', message: 'GitHub webhook secret is not configured' } };\n }\n\n const event = normalizeHeader(c.req.header('x-github-event'));\n const deliveryId = normalizeHeader(c.req.header('x-github-delivery'));\n const signature = normalizeHeader(c.req.header('x-hub-signature-256'));\n\n if (!event) return { status: 400, body: { error: 'bad_request', message: 'Missing x-github-event header' } };\n if (!deliveryId) return { status: 400, body: { error: 'bad_request', message: 'Missing x-github-delivery header' } };\n if (!signature)\n return { status: 401, body: { error: 'unauthorized', message: 'Missing x-hub-signature-256 header' } };\n\n const rawBody = await c.req.text();\n if (!verifySignature(rawBody, signature, secret)) {\n return { status: 401, body: { error: 'unauthorized', message: 'Invalid GitHub webhook signature' } };\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(rawBody);\n } catch {\n return { status: 400, body: { error: 'bad_request', message: 'Malformed JSON payload' } };\n }\n\n if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\n return { status: 400, body: { error: 'bad_request', message: 'Payload must be a JSON object' } };\n }\n\n return { event, deliveryId, payload: payload as Record<string, unknown> };\n}\n\nfunction getObject(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction getString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction getNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction getBoolean(value: unknown): boolean | undefined {\n return typeof value === 'boolean' ? value : undefined;\n}\n\nexport function normalizeGithubWebhookMetadata(parsed: ParsedGithubWebhook): GithubWebhookMetadata {\n const { event, deliveryId, payload } = parsed;\n const repository = getObject(payload.repository);\n const issue = getObject(payload.issue);\n const pullRequest = getObject(payload.pull_request);\n const sender = getObject(payload.sender);\n const installation = getObject(payload.installation);\n const issuePullRequest = getObject(issue?.pull_request);\n\n return {\n event,\n action: getString(payload.action),\n deliveryId,\n repository: getString(repository?.full_name),\n repositoryId: getNumber(repository?.id),\n issueNumber: getNumber(issue?.number),\n pullRequestNumber:\n getNumber(pullRequest?.number) ??\n (event === 'issue_comment' && issuePullRequest ? getNumber(issue?.number) : undefined),\n sender: getString(sender?.login),\n senderType: getString(sender?.type),\n installationId: getNumber(installation?.id),\n };\n}\n\nfunction notificationSummary(metadata: GithubWebhookMetadata, label: string): string {\n const actor = metadata.sender ? `${metadata.sender} ` : '';\n return `${actor}${label} on ${metadata.repository}#${metadata.pullRequestNumber}`;\n}\n\nfunction notificationTargetUrl(event: string, payload: Record<string, unknown>): string | undefined {\n if (event === 'issue_comment' || event === 'pull_request_review_comment') {\n return getString(getObject(payload.comment)?.html_url);\n }\n if (event === 'pull_request_review') {\n return getString(getObject(payload.review)?.html_url);\n }\n return getString(getObject(payload.pull_request)?.html_url);\n}\n\nexport function classifyGithubWebhook(parsed: ParsedGithubWebhook): GithubWebhookNotification | undefined {\n const metadata = normalizeGithubWebhookMetadata(parsed);\n const { event, payload } = parsed;\n const action = metadata.action;\n if (\n !action ||\n !metadata.repositoryId ||\n !metadata.installationId ||\n !metadata.pullRequestNumber ||\n !metadata.repository\n ) {\n return undefined;\n }\n\n let priority: NotificationPriority;\n let kind: string;\n let label: string;\n let terminal = false;\n\n if (event === 'pull_request_review' && action === 'submitted') {\n const state = getString(getObject(payload.review)?.state)?.toLowerCase().replaceAll('_', '-');\n priority = state === 'approved' || state === 'changes-requested' ? 'urgent' : 'high';\n kind =\n state === 'approved'\n ? 'review-approved'\n : state === 'changes-requested'\n ? 'review-changes-requested'\n : 'review-submitted';\n label =\n state === 'approved'\n ? 'approved the pull request'\n : state === 'changes-requested'\n ? 'requested changes'\n : 'submitted a review';\n } else if (event === 'pull_request' && action === 'closed') {\n const merged = getBoolean(getObject(payload.pull_request)?.merged) === true;\n priority = 'urgent';\n kind = merged ? 'pull-request-merged' : 'pull-request-closed';\n label = merged ? 'merged the pull request' : 'closed the pull request';\n terminal = true;\n } else if (event === 'issue_comment' && action === 'created') {\n priority = 'high';\n kind = 'issue-comment-created';\n label = 'commented';\n } else if (event === 'pull_request_review_comment' && action === 'created') {\n priority = 'high';\n kind = 'review-comment-created';\n label = 'left a review comment';\n } else if (event === 'pull_request' && action === 'reopened') {\n priority = 'high';\n kind = 'pull-request-reopened';\n label = 'reopened the pull request';\n } else if (event === 'pull_request_review' && action === 'dismissed') {\n priority = 'high';\n kind = 'review-dismissed';\n label = 'dismissed a review';\n } else if (\n event === 'pull_request' &&\n [\n 'synchronize',\n 'ready_for_review',\n 'converted_to_draft',\n 'assigned',\n 'unassigned',\n 'review_requested',\n 'review_request_removed',\n ].includes(action)\n ) {\n priority = 'medium';\n kind = `pull-request-${action.replaceAll('_', '-')}`;\n label = action.replaceAll('_', ' ');\n } else if (\n event === 'pull_request' &&\n ['edited', 'labeled', 'unlabeled', 'milestoned', 'demilestoned'].includes(action)\n ) {\n priority = 'low';\n kind = `pull-request-${action.replaceAll('_', '-')}`;\n label = action.replaceAll('_', ' ');\n } else {\n return undefined;\n }\n\n return {\n action,\n kind,\n priority,\n summary: notificationSummary(metadata, label),\n terminal,\n metadata: {\n ...metadata,\n pullRequestNumber: metadata.pullRequestNumber,\n repositoryId: metadata.repositoryId,\n installationId: metadata.installationId,\n },\n payload,\n };\n}\n\nasync function resolveSubscriptionSession(\n controller: MountedMastraCode['controller'],\n subscription: GithubSignalSubscriptionRow,\n github?: GithubWebhookDispatchIntegration,\n) {\n const { sessionId, resourceId, threadId } = subscription;\n if (!sessionId || !resourceId || !threadId) {\n throw new Error(`GitHub subscription ${subscription.id} is missing its session binding.`);\n }\n // Read the thread straight from storage before touching sessions. This answers\n // two questions at once, and `queryThreadById` does it without constructing a\n // session (so no workspace or sandbox is provisioned just to make the check).\n //\n // First: do we even have this thread? A pull request's events can reach a\n // deployment that never owned the subscribed thread, and delivery must not\n // fabricate a session for a thread that lives somewhere else.\n //\n // Second: which resource owns it? The subscription records the Factory project\n // as its `resourceId`, but an unscoped session is registered under its own id,\n // so the stored value routinely names a resource that does not own the thread.\n // The thread row is the authoritative answer; the stored id is only a fallback.\n const thread = await controller.queryThreadById({ threadId });\n if (!thread) return undefined;\n const ownerResourceId = thread.resourceId || resourceId;\n const scope = subscription.sessionScope || undefined;\n let session = await controller.getSessionByResource(ownerResourceId, scope);\n if (!session) {\n const tags = {\n factoryProjectId: resourceId,\n projectRepositoryId: subscription.data.projectRepositoryId,\n ...(scope ? { worktreePath: scope } : {}),\n };\n // Creating the session resolves its workspace, which authorizes the caller\n // against the Factory session row — no signed-in user, so run as its owner.\n // The session is created under the resource that owns the thread, so the\n // thread switch below resolves; the persisted Factory session is keyed by\n // the subscription's session ID.\n const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);\n if (!sessionRow) {\n throw new Error(`GitHub subscription ${subscription.id} has no Factory session ${sessionId} to run as.`);\n }\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: sessionRow.userId, organizationId: sessionRow.orgId });\n session = await controller.createSession({\n id: sessionId,\n ownerId: sessionRow.userId,\n resourceId: ownerResourceId,\n scope,\n tags,\n requestContext,\n });\n }\n if (session.thread.getId() !== threadId) {\n await session.thread.switch({ threadId, emitEvent: false });\n }\n if (session.thread.getId() !== threadId) {\n throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);\n }\n return session;\n}\n\n/**\n * Reviewer bots authorized out of the box. Deployments extend — never replace —\n * this set through the integration's `authorizedBots`.\n */\nexport const DEFAULT_AUTHORIZED_BOTS: readonly string[] = ['coderabbitai[bot]', 'devin-ai-integration[bot]'];\n\n/**\n * Parse a comma-separated `MASTRACODE_GITHUB_AUTHORIZED_BOTS` value into extra\n * bot logins. Returns undefined when nothing usable was configured.\n */\nexport function parseAuthorizedBotsEnv(value: string | undefined): string[] | undefined {\n const bots = (value ?? '')\n .split(',')\n .map(bot => bot.trim())\n .filter(Boolean);\n return bots.length > 0 ? bots : undefined;\n}\n\n/** Lowercased union of the default bot logins and any the deployment opted in. */\nexport function resolveAuthorizedBots(extra?: readonly string[]): Set<string> {\n const bots = new Set(DEFAULT_AUTHORIZED_BOTS);\n for (const bot of extra ?? []) {\n const normalized = bot.trim().toLowerCase();\n if (normalized) bots.add(normalized);\n }\n return bots;\n}\n\nconst AUTHORIZED_PERMISSIONS = new Set(['admin', 'maintain', 'write']);\nconst PERMISSION_CHECK_TIMEOUT_MS = 5_000;\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment-created',\n 'review-comment-created',\n 'review-submitted',\n 'review-approved',\n 'review-changes-requested',\n 'review-dismissed',\n]);\n\n/**\n * Recognizes Factory's own GitHub App identity. GitHub forbids an app from\n * reviewing a pull request it authored, so `factory-review` falls back to\n * posting its verdict as a comment under this login. Those comments have to\n * clear the author gate for the review handoff to reach the authoring agent;\n * the rules layer still decides which of them are worth acting on.\n */\nexport function isFactoryAppSender(sender: string | undefined, slug: string | undefined): boolean {\n if (!sender || !slug) return false;\n return sender.toLowerCase() === `${slug.toLowerCase()}[bot]`;\n}\n\nasync function isAuthorizedGithubSender(\n notification: GithubWebhookNotification,\n github:\n | Pick<\n GithubWebhookDispatchIntegration,\n 'getRepositoryCollaboratorPermission' | 'slug' | 'identity' | 'authorizedBots'\n >\n | undefined,\n): 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 (github?.identity?.matches(sender)) return true;\n if (isFactoryAppSender(sender, github?.slug)) return true;\n const normalizedSender = sender.toLowerCase();\n if (notification.metadata.senderType?.toLowerCase() === 'bot' || normalizedSender.endsWith('[bot]')) {\n return resolveAuthorizedBots(github?.authorizedBots).has(normalizedSender);\n }\n if (!github) return false;\n const abortController = new AbortController();\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n const permission = await Promise.race([\n github.getRepositoryCollaboratorPermission(\n notification.metadata.installationId,\n repository,\n sender,\n abortController.signal,\n ),\n new Promise<undefined>(resolve => {\n timeout = setTimeout(() => {\n abortController.abort();\n resolve(undefined);\n }, PERMISSION_CHECK_TIMEOUT_MS);\n }),\n ]);\n return permission !== undefined && AUTHORIZED_PERMISSIONS.has(permission);\n } catch {\n return false;\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nexport async function dispatchGithubWebhook(\n parsed: ParsedGithubWebhook,\n dependencies: GithubWebhookDispatchDependencies,\n): Promise<{ delivered: number; failed: number; skipped: number; ignored: boolean }> {\n const notification = classifyGithubWebhook(parsed);\n if (!notification) return { delivered: 0, failed: 0, skipped: 0, ignored: true };\n const isAuthorizedSender =\n dependencies.isAuthorizedSender ??\n ((n: GithubWebhookNotification) => isAuthorizedGithubSender(n, dependencies.github));\n if (!(await isAuthorizedSender(notification))) {\n dependencies.onSenderRejected?.(notification);\n return { delivered: 0, failed: 0, skipped: 0, ignored: true };\n }\n\n const target = {\n installationExternalId: notification.metadata.installationId.toString(),\n repositoryExternalId: notification.metadata.repositoryId.toString(),\n changeRequestId: notification.metadata.pullRequestNumber.toString(),\n };\n const listSubscriptions =\n dependencies.listSubscriptions ??\n ((subscriptionTarget: GithubWebhookPullRequestTarget, options?: { includeTerminal?: boolean }) => {\n if (!dependencies.github) throw new Error('GitHub integration is required to load webhook subscriptions.');\n return listPullRequestSubscriptionsForWebhook(\n subscriptionTarget,\n options,\n dependencies.github.integrationStorage,\n );\n });\n const retireSubscription =\n dependencies.retireSubscription ??\n ((id: string, status: 'open' | 'closed' | 'merged') => {\n if (!dependencies.github) throw new Error('GitHub integration is required to retire webhook subscriptions.');\n return retirePullRequestSubscription(id, status, dependencies.github.integrationStorage);\n });\n const subscriptions = await listSubscriptions(target, { includeTerminal: notification.action === 'reopened' });\n let delivered = 0;\n let failed = 0;\n let skipped = 0;\n\n for (const subscription of subscriptions) {\n try {\n const session = await resolveSubscriptionSession(dependencies.controller, subscription, dependencies.github);\n // No session means this deployment does not hold the subscribed thread.\n // That is not a delivery failure, so it must not be retried or counted as\n // one; the subscription is left untouched because the thread may exist\n // wherever the subscription was created.\n if (!session) {\n skipped += 1;\n dependencies.onTargetSkipped?.(subscription);\n continue;\n }\n const result = await session.sendNotificationSignal({\n source: 'github',\n kind: notification.kind,\n summary: notification.summary,\n priority: notification.priority,\n payload: notification.payload,\n sourceId: parsed.deliveryId,\n dedupeKey: `${parsed.deliveryId}:${subscription.sessionId}:${subscription.threadId}`,\n coalesceKey: `github:${subscription.data.repositoryExternalId}:pull-request:${subscription.data.changeRequestId}`,\n metadata: {\n event: notification.metadata.event,\n action: notification.action,\n repository: notification.metadata.repository,\n issueNumber: notification.metadata.issueNumber,\n pullRequestNumber: notification.metadata.pullRequestNumber,\n targetUrl: notificationTargetUrl(parsed.event, parsed.payload),\n deliveryId: parsed.deliveryId,\n },\n });\n await Promise.all([result.persisted, result.accepted].filter(Boolean));\n if (notification.terminal) {\n await retireSubscription(subscription.id, notification.kind === 'pull-request-merged' ? 'merged' : 'closed');\n } else if (notification.action === 'reopened') {\n await retireSubscription(subscription.id, 'open');\n }\n delivered += 1;\n } catch (error) {\n failed += 1;\n dependencies.onTargetError?.(subscription, error);\n }\n }\n\n return { delivered, failed, skipped, ignored: false };\n}\n\nexport async function handleGithubWebhook(\n c: Context,\n options: GithubWebhookHandlerOptions & Partial<Omit<GithubWebhookDispatchDependencies, 'github'>>,\n): Promise<GithubWebhookResult> {\n const parsed = await parseGithubWebhook(c, options.github.webhookSecret);\n if ('status' in parsed) return parsed;\n\n if (!SUPPORTED_GITHUB_WEBHOOK_EVENTS.has(parsed.event)) {\n return { status: 202, body: { ok: true, ignored: true } };\n }\n\n const metadata = normalizeGithubWebhookMetadata(parsed);\n console.info('[GitHub Webhook]', metadata);\n\n if (options.ingestFactoryEvent) {\n await options.ingestFactoryEvent(parsed);\n }\n\n if (!options.controller) {\n return { status: 202, body: { ok: true } };\n }\n\n const result = await dispatchGithubWebhook(parsed, {\n onSenderRejected: notification => {\n console.info('[GitHub Webhook] sender not authorized', {\n deliveryId: parsed.deliveryId,\n repository: notification.metadata.repository,\n sender: notification.metadata.sender,\n kind: notification.kind,\n });\n },\n ...(options as GithubWebhookDispatchDependencies),\n });\n if (result.failed > 0) {\n console.warn(`[GitHub Webhook] ${result.failed} subscribed target(s) failed for delivery ${parsed.deliveryId}.`);\n }\n return { status: 202, body: { ok: true, ...(result.ignored ? { ignored: true as const } : {}) } };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"webhook.js","names":[],"sources":["../../../src/integrations/github/webhook.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { MountedMastraCode } from '@mastra/code-sdk';\nimport type { NotificationPriority } from '@mastra/core/notifications';\nimport { RequestContext } from '@mastra/core/request-context';\nimport type { Context } from 'hono';\nimport { hasResolvedOrg, seedSessionOrg } from '../../session/org-seed.js';\nimport { GithubAppIdentity } from './app-identity.js';\nimport type { GithubIntegration, GithubRepositoryPermission } from './integration.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from './subscriptions.js';\nimport type {\n GithubSignalSubscriptionRow,\n GithubSubscriptionStorage,\n GithubWebhookPullRequestTarget,\n} from './subscriptions.js';\n\nexport interface GithubWebhookHandlerOptions {\n /** Integration providing webhook-secret verification + collaborator permission checks. */\n github: GithubIntegration;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n}\n\nconst SUPPORTED_GITHUB_WEBHOOK_EVENTS = new Set([\n 'issues',\n 'issue_comment',\n 'pull_request',\n 'pull_request_review',\n 'pull_request_review_comment',\n // Direct pushes to the default branch drive base-checkpoint rebuilds. The\n // rules engine and subscription dispatcher both ignore push events.\n 'push',\n]);\n\nexport interface GithubWebhookMetadata {\n event: string;\n action?: string;\n deliveryId: string;\n repository?: string;\n repositoryId?: number;\n issueNumber?: number;\n pullRequestNumber?: number;\n sender?: string;\n senderType?: string;\n installationId?: number;\n}\n\nexport interface ParsedGithubWebhook {\n event: string;\n deliveryId: string;\n payload: Record<string, unknown>;\n}\n\nexport type GithubWebhookResult =\n | { status: 202; body: { ok: true; ignored?: true } }\n | { status: 400; body: { error: 'bad_request'; message: string } }\n | { status: 401; body: { error: 'unauthorized'; message: string } };\n\nexport interface GithubWebhookNotification {\n action: string;\n kind: string;\n priority: NotificationPriority;\n summary: string;\n terminal: boolean;\n metadata: GithubWebhookMetadata & { pullRequestNumber: number; repositoryId: number; installationId: number };\n payload: Record<string, unknown>;\n}\n\n/** The Factory session row fields a woken session has to run as. */\nexport type FactorySessionOwner = { userId: string; orgId: string };\n\n/**\n * The integration surface this dispatch uses. Narrow on purpose: the GitHub App\n * integration and the platform-backed one are unrelated classes, and only this\n * much is common to both.\n */\nexport interface GithubWebhookDispatchIntegration {\n /** App slug, used to recognize Factory's own bot identity. */\n readonly slug?: string;\n /**\n * Resolved identity of the App this integration posts as. Preferred over\n * {@link slug}, which names the deployment's own self-hosted App and is unset\n * on deployments that run against Platform's App.\n */\n readonly identity?: GithubAppIdentity;\n readonly integrationStorage: GithubSubscriptionStorage;\n /**\n * Extra bot logins this deployment authorizes to trigger author-gated\n * notifications, merged over `DEFAULT_AUTHORIZED_BOTS`.\n */\n readonly authorizedBots?: readonly string[];\n readonly sourceControlStorage: {\n sessions: { getBySessionId(sessionId: string): Promise<FactorySessionOwner | null> };\n };\n getRepositoryCollaboratorPermission(\n installationId: number,\n repoFullName: string,\n username: string,\n signal?: AbortSignal,\n ): Promise<GithubRepositoryPermission | undefined>;\n}\n\nexport interface GithubWebhookDispatchDependencies {\n controller: MountedMastraCode['controller'];\n /**\n * Integration used by the default sender-authorization check (collaborator\n * permission lookup) and to resolve the owner of a session being recreated.\n * Author-gated notifications fail closed when neither this nor an\n * `isAuthorizedSender` override is supplied.\n */\n github?: GithubWebhookDispatchIntegration;\n listSubscriptions?: (\n target: GithubWebhookPullRequestTarget,\n options?: { includeTerminal?: boolean },\n ) => Promise<GithubSignalSubscriptionRow[]>;\n retireSubscription?: (id: string, status: 'open' | 'closed' | 'merged') => Promise<void>;\n isAuthorizedSender?: (notification: GithubWebhookNotification) => Promise<boolean>;\n /** Called when the sender gate drops a notification, so the drop is observable. */\n onSenderRejected?: (notification: GithubWebhookNotification) => void;\n onTargetError?: (subscription: GithubSignalSubscriptionRow, error: unknown) => void;\n /** Called when a subscription names a thread this deployment does not hold. */\n onTargetSkipped?: (subscription: GithubSignalSubscriptionRow) => void;\n}\n\nfunction normalizeHeader(value: string | undefined | null): string | null {\n if (!value) return null;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\nfunction verifySignature(rawBody: string, signature: string, secret: string): boolean {\n if (!signature.startsWith('sha256=')) return false;\n const signatureHex = signature.slice('sha256='.length);\n if (!/^[a-fA-F0-9]{64}$/.test(signatureHex)) return false;\n\n const expectedHex = createHmac('sha256', secret).update(rawBody).digest('hex');\n const received = Buffer.from(signatureHex, 'hex');\n const expected = Buffer.from(expectedHex, 'hex');\n return received.length === expected.length && timingSafeEqual(received, expected);\n}\n\nasync function parseGithubWebhook(\n c: Context,\n secret: string | undefined,\n): Promise<ParsedGithubWebhook | GithubWebhookResult> {\n if (!secret) {\n return { status: 401, body: { error: 'unauthorized', message: 'GitHub webhook secret is not configured' } };\n }\n\n const event = normalizeHeader(c.req.header('x-github-event'));\n const deliveryId = normalizeHeader(c.req.header('x-github-delivery'));\n const signature = normalizeHeader(c.req.header('x-hub-signature-256'));\n\n if (!event) return { status: 400, body: { error: 'bad_request', message: 'Missing x-github-event header' } };\n if (!deliveryId) return { status: 400, body: { error: 'bad_request', message: 'Missing x-github-delivery header' } };\n if (!signature)\n return { status: 401, body: { error: 'unauthorized', message: 'Missing x-hub-signature-256 header' } };\n\n const rawBody = await c.req.text();\n if (!verifySignature(rawBody, signature, secret)) {\n return { status: 401, body: { error: 'unauthorized', message: 'Invalid GitHub webhook signature' } };\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(rawBody);\n } catch {\n return { status: 400, body: { error: 'bad_request', message: 'Malformed JSON payload' } };\n }\n\n if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\n return { status: 400, body: { error: 'bad_request', message: 'Payload must be a JSON object' } };\n }\n\n return { event, deliveryId, payload: payload as Record<string, unknown> };\n}\n\nfunction getObject(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction getString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction getNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction getBoolean(value: unknown): boolean | undefined {\n return typeof value === 'boolean' ? value : undefined;\n}\n\nexport function normalizeGithubWebhookMetadata(parsed: ParsedGithubWebhook): GithubWebhookMetadata {\n const { event, deliveryId, payload } = parsed;\n const repository = getObject(payload.repository);\n const issue = getObject(payload.issue);\n const pullRequest = getObject(payload.pull_request);\n const sender = getObject(payload.sender);\n const installation = getObject(payload.installation);\n const issuePullRequest = getObject(issue?.pull_request);\n\n return {\n event,\n action: getString(payload.action),\n deliveryId,\n repository: getString(repository?.full_name),\n repositoryId: getNumber(repository?.id),\n issueNumber: getNumber(issue?.number),\n pullRequestNumber:\n getNumber(pullRequest?.number) ??\n (event === 'issue_comment' && issuePullRequest ? getNumber(issue?.number) : undefined),\n sender: getString(sender?.login),\n senderType: getString(sender?.type),\n installationId: getNumber(installation?.id),\n };\n}\n\nfunction notificationSummary(metadata: GithubWebhookMetadata, label: string): string {\n const actor = metadata.sender ? `${metadata.sender} ` : '';\n return `${actor}${label} on ${metadata.repository}#${metadata.pullRequestNumber}`;\n}\n\nfunction notificationTargetUrl(event: string, payload: Record<string, unknown>): string | undefined {\n if (event === 'issue_comment' || event === 'pull_request_review_comment') {\n return getString(getObject(payload.comment)?.html_url);\n }\n if (event === 'pull_request_review') {\n return getString(getObject(payload.review)?.html_url);\n }\n return getString(getObject(payload.pull_request)?.html_url);\n}\n\nexport function classifyGithubWebhook(parsed: ParsedGithubWebhook): GithubWebhookNotification | undefined {\n const metadata = normalizeGithubWebhookMetadata(parsed);\n const { event, payload } = parsed;\n const action = metadata.action;\n if (\n !action ||\n !metadata.repositoryId ||\n !metadata.installationId ||\n !metadata.pullRequestNumber ||\n !metadata.repository\n ) {\n return undefined;\n }\n\n let priority: NotificationPriority;\n let kind: string;\n let label: string;\n let terminal = false;\n\n if (event === 'pull_request_review' && action === 'submitted') {\n const state = getString(getObject(payload.review)?.state)?.toLowerCase().replaceAll('_', '-');\n priority = state === 'approved' || state === 'changes-requested' ? 'urgent' : 'high';\n kind =\n state === 'approved'\n ? 'review-approved'\n : state === 'changes-requested'\n ? 'review-changes-requested'\n : 'review-submitted';\n label =\n state === 'approved'\n ? 'approved the pull request'\n : state === 'changes-requested'\n ? 'requested changes'\n : 'submitted a review';\n } else if (event === 'pull_request' && action === 'closed') {\n const merged = getBoolean(getObject(payload.pull_request)?.merged) === true;\n priority = 'urgent';\n kind = merged ? 'pull-request-merged' : 'pull-request-closed';\n label = merged ? 'merged the pull request' : 'closed the pull request';\n terminal = true;\n } else if (event === 'issue_comment' && action === 'created') {\n priority = 'high';\n kind = 'issue-comment-created';\n label = 'commented';\n } else if (event === 'pull_request_review_comment' && action === 'created') {\n priority = 'high';\n kind = 'review-comment-created';\n label = 'left a review comment';\n } else if (event === 'pull_request' && action === 'reopened') {\n priority = 'high';\n kind = 'pull-request-reopened';\n label = 'reopened the pull request';\n } else if (event === 'pull_request_review' && action === 'dismissed') {\n priority = 'high';\n kind = 'review-dismissed';\n label = 'dismissed a review';\n } else if (\n event === 'pull_request' &&\n [\n 'synchronize',\n 'ready_for_review',\n 'converted_to_draft',\n 'assigned',\n 'unassigned',\n 'review_requested',\n 'review_request_removed',\n ].includes(action)\n ) {\n priority = 'medium';\n kind = `pull-request-${action.replaceAll('_', '-')}`;\n label = action.replaceAll('_', ' ');\n } else if (\n event === 'pull_request' &&\n ['edited', 'labeled', 'unlabeled', 'milestoned', 'demilestoned'].includes(action)\n ) {\n priority = 'low';\n kind = `pull-request-${action.replaceAll('_', '-')}`;\n label = action.replaceAll('_', ' ');\n } else {\n return undefined;\n }\n\n return {\n action,\n kind,\n priority,\n summary: notificationSummary(metadata, label),\n terminal,\n metadata: {\n ...metadata,\n pullRequestNumber: metadata.pullRequestNumber,\n repositoryId: metadata.repositoryId,\n installationId: metadata.installationId,\n },\n payload,\n };\n}\n\nasync function resolveSubscriptionSession(\n controller: MountedMastraCode['controller'],\n subscription: GithubSignalSubscriptionRow,\n github?: GithubWebhookDispatchIntegration,\n) {\n const { sessionId, resourceId, threadId } = subscription;\n if (!sessionId || !resourceId || !threadId) {\n throw new Error(`GitHub subscription ${subscription.id} is missing its session binding.`);\n }\n // Read the thread straight from storage before touching sessions. This answers\n // two questions at once, and `queryThreadById` does it without constructing a\n // session (so no workspace or sandbox is provisioned just to make the check).\n //\n // First: do we even have this thread? A pull request's events can reach a\n // deployment that never owned the subscribed thread, and delivery must not\n // fabricate a session for a thread that lives somewhere else.\n //\n // Second: which resource owns it? The subscription records the Factory project\n // as its `resourceId`, but an unscoped session is registered under its own id,\n // so the stored value routinely names a resource that does not own the thread.\n // The thread row is the authoritative answer; the stored id is only a fallback.\n const thread = await controller.queryThreadById({ threadId });\n if (!thread) return undefined;\n const ownerResourceId = thread.resourceId || resourceId;\n const scope = subscription.sessionScope || undefined;\n let session = await controller.getSessionByResource(ownerResourceId, scope);\n if (!session) {\n const tags = {\n factoryProjectId: resourceId,\n projectRepositoryId: subscription.data.projectRepositoryId,\n ...(scope ? { worktreePath: scope } : {}),\n };\n // Creating the session resolves its workspace, which authorizes the caller\n // against the Factory session row — no signed-in user, so run as its owner.\n // The session is created under the resource that owns the thread, so the\n // thread switch below resolves; the persisted Factory session is keyed by\n // the subscription's session ID.\n const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);\n if (!sessionRow) {\n throw new Error(`GitHub subscription ${subscription.id} has no Factory session ${sessionId} to run as.`);\n }\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: sessionRow.userId, organizationId: sessionRow.orgId });\n session = await controller.createSession({\n id: sessionId,\n ownerId: sessionRow.userId,\n resourceId: ownerResourceId,\n scope,\n tags,\n requestContext,\n });\n await seedSessionOrg(session, sessionRow.orgId);\n } else if (!hasResolvedOrg(session.state?.get()?.factoryOrgId)) {\n // A session created before the org seed existed carries the project tag and\n // no org, so capture would refuse for the rest of its life even though the\n // org is recoverable. Heal it — but only here, and only when it is missing:\n // hoisting the row fetch above would make an existing-session delivery throw\n // on a missing row where it previously succeeded, and fetching it every time\n // would add a storage read to every delivery. A row that is missing or a\n // lookup that throws leaves the session marked unresolved, and delivery\n // continues either way.\n try {\n const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);\n await seedSessionOrg(session, sessionRow?.orgId);\n } catch (error) {\n console.warn('[GitHub webhook] Unable to resolve the session organization.', error);\n await seedSessionOrg(session, undefined);\n }\n } else if (session.state?.get()?.factoryOrgUnresolved) {\n // The org is present, so an earlier failed resolution left a stale marker\n // behind. Clear it without a storage read — nothing else re-seeds a session\n // once the start hook has run, so the marker would otherwise outlive its\n // cause.\n await seedSessionOrg(session, session.state.get()?.factoryOrgId);\n }\n if (session.thread.getId() !== threadId) {\n await session.thread.switch({ threadId, emitEvent: false });\n }\n if (session.thread.getId() !== threadId) {\n throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);\n }\n return session;\n}\n\n/**\n * Reviewer bots authorized out of the box. Deployments extend — never replace —\n * this set through the integration's `authorizedBots`.\n */\nexport const DEFAULT_AUTHORIZED_BOTS: readonly string[] = ['coderabbitai[bot]', 'devin-ai-integration[bot]'];\n\n/**\n * Parse a comma-separated `MASTRACODE_GITHUB_AUTHORIZED_BOTS` value into extra\n * bot logins. Returns undefined when nothing usable was configured.\n */\nexport function parseAuthorizedBotsEnv(value: string | undefined): string[] | undefined {\n const bots = (value ?? '')\n .split(',')\n .map(bot => bot.trim())\n .filter(Boolean);\n return bots.length > 0 ? bots : undefined;\n}\n\n/** Lowercased union of the default bot logins and any the deployment opted in. */\nexport function resolveAuthorizedBots(extra?: readonly string[]): Set<string> {\n const bots = new Set(DEFAULT_AUTHORIZED_BOTS);\n for (const bot of extra ?? []) {\n const normalized = bot.trim().toLowerCase();\n if (normalized) bots.add(normalized);\n }\n return bots;\n}\n\nconst AUTHORIZED_PERMISSIONS = new Set(['admin', 'maintain', 'write']);\nconst PERMISSION_CHECK_TIMEOUT_MS = 5_000;\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment-created',\n 'review-comment-created',\n 'review-submitted',\n 'review-approved',\n 'review-changes-requested',\n 'review-dismissed',\n]);\n\n/**\n * Recognizes Factory's own GitHub App identity. GitHub forbids an app from\n * reviewing a pull request it authored, so `factory-review` falls back to\n * posting its verdict as a comment under this login. Those comments have to\n * clear the author gate for the review handoff to reach the authoring agent;\n * the rules layer still decides which of them are worth acting on.\n */\nexport function isFactoryAppSender(sender: string | undefined, slug: string | undefined): boolean {\n if (!sender || !slug) return false;\n return sender.toLowerCase() === `${slug.toLowerCase()}[bot]`;\n}\n\nasync function isAuthorizedGithubSender(\n notification: GithubWebhookNotification,\n github:\n | Pick<\n GithubWebhookDispatchIntegration,\n 'getRepositoryCollaboratorPermission' | 'slug' | 'identity' | 'authorizedBots'\n >\n | undefined,\n): 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 (github?.identity?.matches(sender)) return true;\n if (isFactoryAppSender(sender, github?.slug)) return true;\n const normalizedSender = sender.toLowerCase();\n if (notification.metadata.senderType?.toLowerCase() === 'bot' || normalizedSender.endsWith('[bot]')) {\n return resolveAuthorizedBots(github?.authorizedBots).has(normalizedSender);\n }\n if (!github) return false;\n const abortController = new AbortController();\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n const permission = await Promise.race([\n github.getRepositoryCollaboratorPermission(\n notification.metadata.installationId,\n repository,\n sender,\n abortController.signal,\n ),\n new Promise<undefined>(resolve => {\n timeout = setTimeout(() => {\n abortController.abort();\n resolve(undefined);\n }, PERMISSION_CHECK_TIMEOUT_MS);\n }),\n ]);\n return permission !== undefined && AUTHORIZED_PERMISSIONS.has(permission);\n } catch {\n return false;\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nexport async function dispatchGithubWebhook(\n parsed: ParsedGithubWebhook,\n dependencies: GithubWebhookDispatchDependencies,\n): Promise<{ delivered: number; failed: number; skipped: number; ignored: boolean }> {\n const notification = classifyGithubWebhook(parsed);\n if (!notification) return { delivered: 0, failed: 0, skipped: 0, ignored: true };\n const isAuthorizedSender =\n dependencies.isAuthorizedSender ??\n ((n: GithubWebhookNotification) => isAuthorizedGithubSender(n, dependencies.github));\n if (!(await isAuthorizedSender(notification))) {\n dependencies.onSenderRejected?.(notification);\n return { delivered: 0, failed: 0, skipped: 0, ignored: true };\n }\n\n const target = {\n installationExternalId: notification.metadata.installationId.toString(),\n repositoryExternalId: notification.metadata.repositoryId.toString(),\n changeRequestId: notification.metadata.pullRequestNumber.toString(),\n };\n const listSubscriptions =\n dependencies.listSubscriptions ??\n ((subscriptionTarget: GithubWebhookPullRequestTarget, options?: { includeTerminal?: boolean }) => {\n if (!dependencies.github) throw new Error('GitHub integration is required to load webhook subscriptions.');\n return listPullRequestSubscriptionsForWebhook(\n subscriptionTarget,\n options,\n dependencies.github.integrationStorage,\n );\n });\n const retireSubscription =\n dependencies.retireSubscription ??\n ((id: string, status: 'open' | 'closed' | 'merged') => {\n if (!dependencies.github) throw new Error('GitHub integration is required to retire webhook subscriptions.');\n return retirePullRequestSubscription(id, status, dependencies.github.integrationStorage);\n });\n const subscriptions = await listSubscriptions(target, { includeTerminal: notification.action === 'reopened' });\n let delivered = 0;\n let failed = 0;\n let skipped = 0;\n\n for (const subscription of subscriptions) {\n try {\n const session = await resolveSubscriptionSession(dependencies.controller, subscription, dependencies.github);\n // No session means this deployment does not hold the subscribed thread.\n // That is not a delivery failure, so it must not be retried or counted as\n // one; the subscription is left untouched because the thread may exist\n // wherever the subscription was created.\n if (!session) {\n skipped += 1;\n dependencies.onTargetSkipped?.(subscription);\n continue;\n }\n const result = await session.sendNotificationSignal({\n source: 'github',\n kind: notification.kind,\n summary: notification.summary,\n priority: notification.priority,\n payload: notification.payload,\n sourceId: parsed.deliveryId,\n dedupeKey: `${parsed.deliveryId}:${subscription.sessionId}:${subscription.threadId}`,\n coalesceKey: `github:${subscription.data.repositoryExternalId}:pull-request:${subscription.data.changeRequestId}`,\n metadata: {\n event: notification.metadata.event,\n action: notification.action,\n repository: notification.metadata.repository,\n issueNumber: notification.metadata.issueNumber,\n pullRequestNumber: notification.metadata.pullRequestNumber,\n targetUrl: notificationTargetUrl(parsed.event, parsed.payload),\n deliveryId: parsed.deliveryId,\n },\n });\n await Promise.all([result.persisted, result.accepted].filter(Boolean));\n if (notification.terminal) {\n await retireSubscription(subscription.id, notification.kind === 'pull-request-merged' ? 'merged' : 'closed');\n } else if (notification.action === 'reopened') {\n await retireSubscription(subscription.id, 'open');\n }\n delivered += 1;\n } catch (error) {\n failed += 1;\n dependencies.onTargetError?.(subscription, error);\n }\n }\n\n return { delivered, failed, skipped, ignored: false };\n}\n\nexport async function handleGithubWebhook(\n c: Context,\n options: GithubWebhookHandlerOptions & Partial<Omit<GithubWebhookDispatchDependencies, 'github'>>,\n): Promise<GithubWebhookResult> {\n const parsed = await parseGithubWebhook(c, options.github.webhookSecret);\n if ('status' in parsed) return parsed;\n\n if (!SUPPORTED_GITHUB_WEBHOOK_EVENTS.has(parsed.event)) {\n return { status: 202, body: { ok: true, ignored: true } };\n }\n\n const metadata = normalizeGithubWebhookMetadata(parsed);\n console.info('[GitHub Webhook]', metadata);\n\n if (options.ingestFactoryEvent) {\n await options.ingestFactoryEvent(parsed);\n }\n\n if (!options.controller) {\n return { status: 202, body: { ok: true } };\n }\n\n const result = await dispatchGithubWebhook(parsed, {\n onSenderRejected: notification => {\n console.info('[GitHub Webhook] sender not authorized', {\n deliveryId: parsed.deliveryId,\n repository: notification.metadata.repository,\n sender: notification.metadata.sender,\n kind: notification.kind,\n });\n },\n ...(options as GithubWebhookDispatchDependencies),\n });\n if (result.failed > 0) {\n console.warn(`[GitHub Webhook] ${result.failed} subscribed target(s) failed for delivery ${parsed.deliveryId}.`);\n }\n return { status: 202, body: { ok: true, ...(result.ignored ? { ignored: true as const } : {}) } };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/slack.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EAEd,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EAChB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/slack.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readRequestContextOrgId, seedSessionOrg } from "../../session/org-seed.js";
|
|
1
2
|
import { hydrateFactorySession, resolveFactoryDefaultModelId, resolveFactoryProjectForSession, resolveFactorySourceRepository } from "../../session/factory-session.js";
|
|
2
3
|
import { randomUUID } from "crypto";
|
|
3
4
|
import { createSlackAdapter } from "@mastra/slack";
|
|
@@ -230,7 +231,8 @@ const resolveChannelThreadId = ({ resourceId, defaultThreadId }) => resourceId.s
|
|
|
230
231
|
*/
|
|
231
232
|
function createChannelSessionStartHook(deps) {
|
|
232
233
|
const { projects, sourceControl, memorySettings } = deps;
|
|
233
|
-
return async ({ session, thread }) => {
|
|
234
|
+
return async ({ session, thread, requestContext }) => {
|
|
235
|
+
await seedSessionOrg(session, readRequestContextOrgId(requestContext));
|
|
234
236
|
if (!projects || !sourceControl) return;
|
|
235
237
|
if (thread.resourceId.startsWith("channel:")) return;
|
|
236
238
|
const owner = await resolveFactoryProjectForSession({
|
|
@@ -238,10 +240,8 @@ function createChannelSessionStartHook(deps) {
|
|
|
238
240
|
sessionId: thread.resourceId
|
|
239
241
|
});
|
|
240
242
|
if (!owner) return;
|
|
241
|
-
await session.state.set({
|
|
242
|
-
|
|
243
|
-
factoryOrgId: owner.orgId
|
|
244
|
-
});
|
|
243
|
+
await session.state.set({ factoryProjectId: owner.factoryProjectId });
|
|
244
|
+
await seedSessionOrg(session, owner.orgId);
|
|
245
245
|
const modeModelKey = `modeModelId_${session.mode.get()}`;
|
|
246
246
|
if (await session.thread.getSetting({ key: modeModelKey })) return;
|
|
247
247
|
const defaultModelId = await resolveFactoryDefaultModelId(projects, owner.factoryProjectId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack.js","names":[],"sources":["../../../src/integrations/slack/slack.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type {\n ChannelHandler,\n ChannelHandlerContext,\n ChannelHandlers,\n ChannelSessionStart,\n ResolveResourceId,\n ResolveThreadId,\n} from '@mastra/core/channels';\nimport type { Mastra } from '@mastra/core/mastra';\nimport { createSlackAdapter } from '@mastra/slack';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\nimport { Card, CardText, Actions, LinkButton } from 'chat';\n\nimport {\n hydrateFactorySession,\n resolveFactoryDefaultModelId,\n resolveFactoryProjectForSession,\n resolveFactorySourceRepository,\n} from '../../session/factory-session.js';\nimport type {\n ChannelAccountLink,\n ChannelAccountLinkKey,\n ChannelIdentityStorage,\n} from '../../storage/domains/channel-identity/base.js';\nimport type { MemorySettingsStorage } from '../../storage/domains/memory-settings/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { FactoryChannelsConfig } from '../base.js';\n\n// Derive the thread/message types from the core handler signature rather than\n// importing them from `chat` directly: mc-web can resolve a different `chat`\n// version than @mastra/core, and the two `Thread`/`Message` declarations are\n// structurally incompatible (private fields). Using the handler's own types\n// keeps everything on one version.\ntype HandlerThread = Parameters<ChannelHandler>[0];\ntype HandlerMessage = Parameters<ChannelHandler>[1];\n\n/** Dependencies the Slack channel handlers close over, injected from the web entry. */\ninterface SlackChannelDeps {\n /**\n * The factory's reverse-index store mapping a Slack sender to a Mastra\n * tenant. When provided, inbound messages from an unlinked sender are not\n * dispatched — the run only proceeds (with the sender's tenant stamped on\n * the request context) once they've linked their account. Unlinked senders\n * get an ephemeral \"connect your account\" card instead.\n */\n accountLinks?: ChannelIdentityStorage;\n /**\n * Factory projects domain. When provided (alongside `accountLinks`), a\n * linked sender's run must also resolve to a Factory project before it\n * dispatches: their link's default factory, else their tenant's only\n * factory (stamped back onto the link), else an ephemeral \"pick a default\n * factory\" card and no run. Unset → no factory routing (runs dispatch as\n * before).\n */\n projects?: FactoryProjectsStorage;\n /**\n * Storage handle of the integration that owns source control\n * (`IntegrationContext.storage.sourceControlOwner`). Used to make new Slack\n * threads repo-backed: when the sender is linked and their factory has a\n * repository, the thread's resourceId becomes a Factory user-session id (repo\n * cloned on a `slack/{threadTs}` branch) instead of the chat-only\n * `channel:...` id. It also lets a started session read back the project it\n * belongs to. Nothing here is provider-specific — the connection is matched\n * by the handle's own `integrationId`. Absent (no source-control integration\n * registered) → chat-only sessions as before.\n */\n sourceControl?: SourceControlStorageHandle;\n /**\n * Observational-memory settings domain. When provided, a repo-backed session\n * adopts its factory project's shared memory settings on start, matching the\n * web kickoff.\n */\n memorySettings?: MemorySettingsStorage;\n /**\n * Factory work-items domain. When provided, a dispatched new-session thread\n * (DM or mention) upserts a Work-board card in Building (`execute`) carrying\n * the Slack thread as its external source and binding the repo-backed\n * session. Best-effort — a failure never blocks the run. Unset → no card.\n */\n workItems?: WorkItemsStorage;\n /** Overrides applied to the Slack channel adapter entry. */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\n/**\n * Read the Slack team id off a raw platform payload (Events API envelope or\n * slash-command body — both carry `team_id`), duck-typed to build the\n * workspace-scoped account-link key.\n */\nfunction rawTeamId(rawPayload: unknown): string | undefined {\n if (!rawPayload || typeof rawPayload !== 'object') return undefined;\n const raw = rawPayload as { team_id?: unknown; team?: unknown };\n if (typeof raw.team_id === 'string' && raw.team_id) return raw.team_id;\n if (typeof raw.team === 'string' && raw.team) return raw.team;\n if (raw.team && typeof raw.team === 'object') {\n const id = (raw.team as { id?: unknown }).id;\n if (typeof id === 'string' && id) return id;\n }\n return undefined;\n}\n\n/**\n * The Slack team id survives onto a normalized chat Message only on\n * `message.raw` (the Slack Events API envelope).\n */\nfunction slackTeamId(message: HandlerMessage): string | undefined {\n return rawTeamId(message.raw);\n}\n\n/**\n * Resolve the web-UI origin for links humans open in a browser (Connect card,\n * session deep links). Prefers `MASTRACODE_PUBLIC_URL` — the origin auth\n * cookies and OAuth redirect allow-lists are registered against — over the\n * channels tunnel, which only Slack's servers need to reach.\n */\nfunction webPublicUrl(): string | undefined {\n return process.env.MASTRACODE_PUBLIC_URL ?? process.env.MASTRACODE_CHANNELS_PUBLIC_URL;\n}\n\n/** Outcome of the sender-link gate for one inbound message. */\ntype LinkedSenderResult =\n /** Gating not configured — dispatch as before account linking existed. */\n | { status: 'ungated' }\n /** Sender unlinked — Connect card posted (when possible), do not dispatch. */\n | { status: 'blocked' }\n /** Sender linked — their tenant plus the sender key the link lives under. */\n | { status: 'linked'; link: ChannelAccountLink; key: ChannelAccountLinkKey };\n\n/**\n * Resolve the sender's account link, posting an ephemeral \"connect your\n * account\" card (visible only to the sender) linking into the web UI's\n * Slack-connect flow when they're unlinked.\n */\nexport async function resolveLinkedSender({\n thread,\n message,\n accountLinks,\n}: {\n thread: HandlerThread;\n message: HandlerMessage;\n accountLinks?: ChannelIdentityStorage;\n}): Promise<LinkedSenderResult> {\n if (!accountLinks) return { status: 'ungated' };\n const platform = thread.adapter.name;\n const externalUserId = message.author.userId;\n const externalTeamId = slackTeamId(message);\n // Without a team id we can't identify the workspace-scoped link; treat as\n // unlinked so a run never proceeds tenant-less.\n const key = externalTeamId ? { platform, externalTeamId, externalUserId } : undefined;\n const link = key ? await accountLinks.getAccountLink(key) : null;\n if (link && key) return { status: 'linked', link, key };\n\n const publicUrl = webPublicUrl();\n // A public origin is all the card needs. The link carries no identity: the\n // web app authenticates the visitor, then Slack's OIDC flow proves which\n // Slack account they control. Without an origin, still block, just no card.\n if (publicUrl) {\n await thread.postEphemeral(message.author, buildConnectCard(publicUrl), { fallbackToDM: true });\n }\n return { status: 'blocked' };\n}\n\n/**\n * The \"connect your account\" card. The link is deliberately identity-free —\n * `/connect/slack` sends the visitor to Connections, where \"Connect Slack\"\n * runs the OIDC flow and Slack itself asserts the (team, user) pair.\n */\nfunction buildConnectCard(publicUrl: string) {\n return Card({\n title: 'Connect your account',\n children: [\n CardText('Connect your account to use this agent.'),\n Actions([\n LinkButton({\n url: `${publicUrl}/connect/slack`,\n label: 'Connect account',\n }),\n ]),\n ],\n });\n}\n\n/** Outcome of factory routing for one linked sender's inbound message. */\ntype FactoryRouteResult =\n /** Factory routing not configured — dispatch without a factory. */\n | { status: 'ungated' }\n /** No factory resolved — prompt card posted (when possible), do not dispatch. */\n | { status: 'blocked' }\n /** The Factory project this sender's runs route to. */\n | { status: 'resolved'; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n\n/**\n * Decide which Factory project a linked sender's run belongs to:\n *\n * 1. The link's `defaultFactoryProjectId`, when it still exists (a stale id —\n * deleted factory — falls through as if unset).\n * 2. Else, the tenant's only factory, stamped back onto the link so it shows\n * up (and stays editable) in Connected Accounts settings.\n * 3. Else — zero or several factories — an ephemeral \"pick a default factory\"\n * card deep-linking to settings, and the run is blocked.\n */\nexport async function resolveFactoryForLink({\n thread,\n message,\n link,\n key,\n accountLinks,\n projects,\n}: {\n thread: HandlerThread;\n message: HandlerMessage;\n link: ChannelAccountLink;\n key: ChannelAccountLinkKey;\n accountLinks: ChannelIdentityStorage;\n projects?: FactoryProjectsStorage;\n}): Promise<FactoryRouteResult> {\n if (!projects) return { status: 'ungated' };\n // Factories are org-scoped; a personal account (no org) has none and lands\n // on the prompt below.\n const orgId = link.orgId ?? '';\n\n if (link.defaultFactoryProjectId) {\n const existing = await projects.get({ orgId, id: link.defaultFactoryProjectId });\n if (existing) {\n return {\n status: 'resolved',\n factoryProjectId: existing.id,\n slackWorkItemsEnabled: existing.slackWorkItemsEnabled,\n };\n }\n }\n\n const factories = orgId ? await projects.list({ orgId }) : [];\n if (factories.length === 1) {\n const only = factories[0]!;\n await accountLinks.setDefaultFactory({ ...key, userId: link.userId, factoryProjectId: only.id });\n return {\n status: 'resolved',\n factoryProjectId: only.id,\n slackWorkItemsEnabled: only.slackWorkItemsEnabled,\n };\n }\n\n const publicUrl = webPublicUrl();\n if (publicUrl) {\n await thread.postEphemeral(\n message.author,\n Card({\n title: 'Pick a default factory',\n children: [\n CardText(\n factories.length === 0\n ? 'Your account has no factory yet. Create one in the web app, then message me again.'\n : 'Your account has several factories. Pick which one Slack sessions should go to, then message me again.',\n ),\n Actions([\n LinkButton({\n url: `${publicUrl}/settings/connections`,\n label: 'Open settings',\n }),\n ]),\n ],\n }),\n { fallbackToDM: true },\n );\n }\n return { status: 'blocked' };\n}\n\n/**\n * Deterministic per-thread branch name: `slack/{threadTs}` with characters\n * outside the sandbox git-ref allow-list (`[A-Za-z0-9_./-]`, and `.` for\n * readability) mapped to `-`. `thread.id` is `{channelId}:{threadTs}`\n * (platform-prefixed on handler threads) — the trailing segment is the ts.\n *\n * Top-level DM and channel conversations use the empty-threadTs thread form,\n * so the trailing segment can be empty; a bare `slack/` is not a valid git\n * ref. Fall back to the last non-empty segment (the channel id): one\n * deterministic branch per top-level conversation.\n */\nfunction threadBranch(threadId: string): string {\n const segments = threadId.split(':');\n const tail = segments.findLast(segment => segment.length > 0) ?? threadId;\n return `slack/${tail.replace(/[^A-Za-z0-9_/-]/g, '-')}`;\n}\n\n/**\n * Resolve the resourceId for a NEW Slack channel thread. A linked sender whose\n * factory has a repository gets a Factory user-session id — the controller\n * session then materializes the repo sandbox via the factory's dynamic\n * workspace (clone + PAT), the session shows up in the web Sessions list, and\n * View Session deep-links land on the normal workspace route. Everything else\n * (unlinked, unrouted, repo-less, or no source control) keeps the chat-only\n * `defaultResourceId`.\n *\n * Pure lookups only — cards for unlinked/unrouted senders are the dispatch\n * gate's job; this hook must never post.\n */\nexport function createChannelResourceIdResolver(deps: SlackChannelDeps): ResolveResourceId {\n const { accountLinks, projects, sourceControl } = deps;\n return async ({ platform, thread, message }) => {\n // NOT the hook's `defaultResourceId`: configuring a custom resolver\n // bypasses AgentControllerChannels' own `channel:{thread.id}` derivation\n // (agent-controller-channels.ts `resolveChannelResourceId`), and the base\n // default is the per-USER memory key. Chat-only fallbacks must stay\n // per-thread, so reproduce the controller default here.\n const chatOnlyResourceId = `channel:${thread.id}`;\n if (!accountLinks || !projects || !sourceControl) return chatOnlyResourceId;\n try {\n const externalTeamId = rawTeamId(message.raw);\n if (!externalTeamId) return chatOnlyResourceId;\n const link = await accountLinks.getAccountLink({\n platform,\n externalTeamId,\n externalUserId: message.author.userId,\n });\n if (!link) return chatOnlyResourceId;\n\n // Same chain as `resolveFactoryForLink`, minus prompts/stamping: the\n // dispatch gate has already run (and stamped a lone factory) by the\n // time a new thread is created, so this is a read-only re-resolve.\n const orgId = link.orgId ?? '';\n let factoryProjectId: string | undefined;\n if (link.defaultFactoryProjectId && (await projects.get({ orgId, id: link.defaultFactoryProjectId }))) {\n factoryProjectId = link.defaultFactoryProjectId;\n } else if (orgId) {\n const factories = await projects.list({ orgId });\n if (factories.length === 1) factoryProjectId = factories[0]!.id;\n }\n if (!factoryProjectId) return chatOnlyResourceId;\n\n const repo = await resolveFactorySourceRepository({ sourceControl, orgId, factoryProjectId });\n if (!repo.found) return chatOnlyResourceId;\n\n const branch = threadBranch(thread.id);\n // Attributed to the Slack sender, not to whoever connected the repository:\n // unlike an autonomous rule run, a Slack thread has a real interactive user.\n const existing = await sourceControl.sessions.getForBranch({\n projectRepositoryId: repo.projectRepositoryId,\n userId: link.userId,\n branch,\n });\n if (existing) return existing.sessionId;\n const session = await sourceControl.sessions.create({\n sessionId: randomUUID(),\n projectRepositoryId: repo.projectRepositoryId,\n orgId,\n userId: link.userId,\n branch,\n baseBranch: repo.baseBranch,\n // DMs are the only private origin; channel threads are org-visible.\n visibility: thread.isDM ? 'private' : 'org',\n });\n return session.sessionId;\n } catch (error) {\n // Fall back to a chat-only session rather than dropping the message.\n console.warn('[slack] repo-backed session resolution failed for thread', thread.id, error);\n return chatOnlyResourceId;\n }\n };\n}\n\n/**\n * Thread id for a NEW Slack channel thread. Repo-backed threads take the\n * user-session id AS their thread id, matching the web convention\n * (FactoryStartCoordinator seeds threads with threadId = sessionId) so\n * `/workspaces/{sessionId}/threads/{sessionId}` resolves Slack-created\n * sessions exactly like web-created ones — no `?resourceId=` override needed.\n * Chat-only threads keep the default random id: their `channel:...`\n * resourceId is a memory key, not a unique thread id.\n */\nexport const resolveChannelThreadId: ResolveThreadId = ({ resourceId, defaultThreadId }) =>\n resourceId.startsWith('channel:') ? defaultThreadId : resourceId;\n\n/**\n * Apply the factory's configuration to a Slack-created session the first time\n * its thread reaches the controller.\n *\n * Without this a Slack session runs on the SDK's built-in mode default\n * (`openai/gpt-5.5`), so a factory configured for any other provider fails every\n * message with a missing-credentials error. The web kickoff has always applied\n * the factory default; this brings Slack to the same footing.\n *\n * Only repo-backed threads are configured. Their resourceId IS the Factory\n * session id, which the source-control rows turn back into a project — a\n * chat-only `channel:...` id names no project, so there is nothing to read.\n *\n * Skips a session whose mode already has a model persisted on the thread. That\n * is the durable record of a deliberate choice — either an earlier start or a\n * user's own switch — and re-applying the factory default over it would undo\n * the user's selection every time the process restarts.\n */\nexport function createChannelSessionStartHook(deps: SlackChannelDeps): ChannelSessionStart {\n const { projects, sourceControl, memorySettings } = deps;\n return async ({ session, thread }) => {\n if (!projects || !sourceControl) return;\n if (thread.resourceId.startsWith('channel:')) return;\n\n const owner = await resolveFactoryProjectForSession({ sourceControl, sessionId: thread.resourceId });\n if (!owner) return;\n\n // Repo-backed Slack sessions are factory sessions: stamp the owning\n // project onto controller state so downstream reads (org-first credential\n // resolution, authority gates) recognize them, same as board runs.\n await session.state.set({ factoryProjectId: owner.factoryProjectId, factoryOrgId: owner.orgId });\n\n const modeModelKey = `modeModelId_${session.mode.get()}`;\n if (await session.thread.getSetting({ key: modeModelKey })) return;\n\n const defaultModelId = await resolveFactoryDefaultModelId(projects, owner.factoryProjectId);\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n defaultModelId,\n memorySettings,\n });\n };\n}\n\n/**\n * The internal Mastra thread the framework created for a channel conversation.\n * The handler's `thread.id` is the platform thread id (e.g. `slack:C123:ts`),\n * NOT the internal UUID — the mapping lives in the stored thread's channel\n * metadata.\n */\nasync function findInternalThread(mastra: Mastra | undefined, thread: HandlerThread) {\n const store = await mastra?.getStorage()?.getStore('memory');\n const { threads } = (await store?.listThreads({\n filter: {\n metadata: {\n channel_platform: thread.adapter.name,\n channel_externalThreadId: thread.id,\n channel_externalChannelId: thread.channelId,\n },\n },\n perPage: 1,\n })) ?? { threads: [] };\n return threads[0];\n}\n\n/**\n * Build the \"new session\" handler for mention / direct-message events. A mention or\n * DM on a not-yet-subscribed thread starts a NEW session; once subscribed, later\n * events are follow-ups and don't re-announce.\n */\n/**\n * Run the account-link + factory-routing gates for one inbound message.\n * Returns `null` when the run must not dispatch (a prompt card was posted\n * where possible); otherwise the dispatch context — with `routed` present\n * only when a linked sender resolved to a factory.\n */\nasync function gateDispatch(\n thread: HandlerThread,\n message: HandlerMessage,\n { accountLinks, projects }: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n): Promise<{\n routed?: { link: ChannelAccountLink; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n} | null> {\n const sender = await resolveLinkedSender({ thread, message, accountLinks });\n if (sender.status === 'blocked') return null;\n // Linked senders must also route to a Factory project before a run starts.\n if (sender.status === 'linked' && accountLinks) {\n // Stamp the tenant on the run's request context — the single seam\n // `resolveCredentialStore` reads to load this sender's model credentials.\n // This belongs to the link, not to the routing: a linked sender whose\n // factory routing comes back `ungated` still exits below and dispatches, so\n // stamping only in the routed branch would silently run them on default\n // credentials.\n ctx.requestContext.set('user', { id: sender.link.userId, organizationId: sender.link.orgId });\n\n const route = await resolveFactoryForLink({ thread, message, ...sender, accountLinks, projects });\n if (route.status === 'blocked') return null;\n if (route.status === 'resolved') {\n return {\n routed: {\n link: sender.link,\n factoryProjectId: route.factoryProjectId,\n slackWorkItemsEnabled: route.slackWorkItemsEnabled,\n },\n };\n }\n }\n return {};\n}\n\n/**\n * Upsert the Work-board card for a dispatched Slack-thread run. Keyed on the\n * thread via `externalSource` — the work-items domain's unique\n * `(factory_project_id, source_key)` index makes repeat messages reuse the\n * same card, and `reuseMode: 'preserve'` keeps a card a human already dragged\n * across stages untouched. The card lands in Building (`execute`) for every\n * dispatched thread (DM or mention) — there is deliberately no per-origin\n * stage split; smart routing is a follow-up.\n *\n * The session id / branch / threadId and the workspace deep-link are resolved\n * by the caller (which already looked up the internal thread), so this helper\n * just shapes and writes. Best-effort: the run is already dispatched, so a\n * failure logs instead of throwing — work-item creation must never abort a Slack run.\n */\nexport async function upsertThreadWorkItem({\n workItems,\n thread,\n message,\n link,\n factoryProjectId,\n session,\n url,\n}: {\n workItems: WorkItemsStorage;\n thread: HandlerThread;\n message: HandlerMessage;\n link: ChannelAccountLink;\n factoryProjectId: string;\n /**\n * The repo-backed Factory session to bind under the `chat` role, or\n * `undefined` for a chat-only thread (no Factory session to bind).\n */\n session?: { sessionId: string; branch: string; threadId: string };\n /** Workspace deep-link to the running session; omitted when no public URL. */\n url?: string;\n}): Promise<void> {\n try {\n const title = message.text.length > 80 ? `${message.text.slice(0, 79)}…` : message.text;\n\n await workItems.upsert({\n orgId: link.orgId ?? '',\n userId: link.userId,\n factoryProjectId,\n reuseMode: 'preserve',\n input: {\n title: title || 'Slack thread',\n // `integrationId` is the platform ('slack'); `type` is a single\n // constant (no DM/mention distinction); `externalId` is the stable\n // platform thread id — together they form the idempotency key.\n externalSource: {\n integrationId: thread.adapter.name,\n type: 'slack-thread',\n externalId: thread.id,\n ...(url ? { url } : {}),\n },\n stages: ['execute'],\n ...(session ? { sessions: { chat: session } } : {}),\n },\n });\n } catch (error) {\n console.warn('[slack] work-item creation failed for thread', thread.id, error);\n }\n}\n\nfunction createNewSessionChatHandler(deps: SlackChannelDeps): ChannelHandler {\n const { workItems } = deps;\n return async (thread, message, defaultHandler, ctx) => {\n // Gate on the sender having linked their Slack account to a Mastra tenant.\n // Unlinked → post the ephemeral Connect card and stop; no session/run is\n // created (which would otherwise be tenant-less and fail credential\n // resolution). This handler is the only gate — core dispatches whatever\n // reaches it — so every slot that can start a run must call it.\n const gate = await gateDispatch(thread, message, deps, ctx);\n if (!gate) return;\n\n // A mention on a not-yet-subscribed thread is a NEW session. The\n // default handler auto-subscribes, so once subscribed this is a\n // follow-up mention — don't re-announce.\n const isNewSession = !(await thread.isSubscribed());\n\n // Run the framework handler first so the internal Mastra thread and\n // controller session are created before we build the deep link.\n await defaultHandler(thread, message);\n\n if (!isNewSession) return;\n\n // The internal-thread lookup and deep-link are needed by BOTH the\n // announcement card AND work-item creation, so they run BEFORE the\n // card-only `MASTRACODE_PUBLIC_URL` gate — a deployment without a public\n // origin should still create board cards, just without a clickable link.\n const internalThread = await findInternalThread(ctx.mastra, thread);\n if (!internalThread) {\n console.warn('[onMention] no internal thread found for', thread.id);\n return;\n }\n\n // When the sender routed to a factory we know exactly which workspace the\n // session belongs to — deep-link straight into it. A repo-backed thread's\n // resourceId IS the Factory user-session id, so the link lands on the same\n // route a web-started run navigates to; chat-only threads keep the literal\n // `channel` segment (the real resource rides the `?resourceId=` override).\n // Unrouted senders fall back to the factory-agnostic /threads/ redirect.\n // One predicate drives both the path segment and the query param, so the\n // two can never disagree about what the URL already carries.\n const isChatOnly = internalThread.resourceId.startsWith('channel:');\n const workspaceSegment = isChatOnly ? 'channel' : encodeURIComponent(internalThread.resourceId);\n const threadPath = gate.routed\n ? `/factories/${encodeURIComponent(gate.routed.factoryProjectId)}/workspaces/${workspaceSegment}/threads/${encodeURIComponent(internalThread.id)}`\n : `/threads/${internalThread.id}`;\n\n // The param is an override for a URL that can't otherwise name its\n // resource. A routed repo-backed thread already spells the resourceId out\n // as its workspace segment, so appending it again is duplication the app\n // ignores. Chat-only threads need it (their segment is the literal string\n // `channel`), and so does the unrouted fallback, which has no workspace\n // segment at all — `ChannelThreadRedirect` forwards the search through.\n //\n // One shared deep-link: the card's button and the work-item `url` read the\n // SAME value so they can never drift. Undefined without a public origin —\n // the card is then skipped, but the work item is still created (url omitted).\n const needsResourceParam = isChatOnly || !gate.routed;\n const deepLink = process.env.MASTRACODE_PUBLIC_URL\n ? needsResourceParam\n ? `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}?resourceId=${encodeURIComponent(internalThread.resourceId)}`\n : `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}`\n : undefined;\n\n // A dispatched, routed new-session thread becomes a Work-board card in\n // Building. Only routed senders (linked → factory) have the org/user/factory\n // a work item needs. Bind the repo-backed Factory session under the `chat`\n // role; a chat-only `channel:` resourceId is NOT a session id, so bind\n // nothing rather than a bad id. Best-effort (the helper swallows failures).\n if (workItems && gate.routed?.slackWorkItemsEnabled) {\n const session = isChatOnly\n ? undefined\n : { sessionId: internalThread.resourceId, branch: threadBranch(thread.id), threadId: internalThread.id };\n await upsertThreadWorkItem({\n workItems,\n thread,\n message,\n link: gate.routed.link,\n factoryProjectId: gate.routed.factoryProjectId,\n session,\n url: deepLink,\n });\n }\n\n // The announcement card is only useful with a public origin to deep-link\n // to — otherwise the link would be `undefined/threads/...`. Without one the\n // session (and now the work item) still exist; we just skip the broken card.\n if (!deepLink) return;\n\n await thread.post(\n Card({\n title: 'New session started',\n children: [Actions([LinkButton({ url: deepLink, label: 'View session' })])],\n }),\n );\n };\n}\nexport const createHandlers = (deps: SlackChannelDeps): ChannelHandlers => {\n const newSessionChatHandler = createNewSessionChatHandler(deps);\n\n return {\n onSubscribedMessage: async (thread, message, defaultHandler, ctx) => {\n // `aside` as its own leading word lets humans talk in a subscribed\n // thread without the bot replying. Word boundary so messages that\n // merely start with \"aside...\" (e.g. \"asides can wait\") still route.\n if (/^aside\\b/i.test(message.text)) return;\n // A subscribed follow-up from an unlinked sender must not run either\n // (e.g. the link was removed mid-conversation), and it must still\n // resolve a factory (e.g. the default was cleared or its factory\n // deleted mid-conversation).\n const gate = await gateDispatch(thread, message, deps, ctx);\n if (!gate) return;\n await defaultHandler(thread, message);\n },\n onMention: newSessionChatHandler,\n onDirectMessage: newSessionChatHandler,\n };\n};\n\n/** Slack app credentials, passed in explicitly rather than read from env here. */\ninterface SlackCredentials {\n clientId?: string;\n clientSecret?: string;\n signingSecret: string;\n botToken?: string;\n}\n\nexport function createSlackChannelsConfig(deps: SlackChannelDeps & { slack: SlackCredentials }): FactoryChannelsConfig {\n const adapter = createSlackAdapter(deps.slack);\n const slack =\n deps.adapterOptions?.streaming === false\n ? { adapter, ...deps.adapterOptions }\n : { adapter, ...deps.adapterOptions, streaming: deps.adapterOptions?.streaming ?? true };\n\n return {\n adapters: { slack },\n handlers: createHandlers(deps),\n // New linked+repo-backed threads own a Factory user-session id as their\n // resourceId, which is what makes the controller session repo-backed.\n resolveResourceId: createChannelResourceIdResolver(deps),\n resolveThreadId: resolveChannelThreadId,\n // Those sessions are created by the channel machinery, not the web kickoff,\n // so this is where they pick up the factory's configuration.\n onSessionStart: createChannelSessionStartHook(deps),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"slack.js","names":[],"sources":["../../../src/integrations/slack/slack.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type {\n ChannelHandler,\n ChannelHandlerContext,\n ChannelHandlers,\n ChannelSessionStart,\n ResolveResourceId,\n ResolveThreadId,\n} from '@mastra/core/channels';\nimport type { Mastra } from '@mastra/core/mastra';\nimport { createSlackAdapter } from '@mastra/slack';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\nimport { Card, CardText, Actions, LinkButton } from 'chat';\n\nimport {\n hydrateFactorySession,\n resolveFactoryDefaultModelId,\n resolveFactoryProjectForSession,\n resolveFactorySourceRepository,\n} from '../../session/factory-session.js';\nimport { readRequestContextOrgId, seedSessionOrg } from '../../session/org-seed.js';\nimport type {\n ChannelAccountLink,\n ChannelAccountLinkKey,\n ChannelIdentityStorage,\n} from '../../storage/domains/channel-identity/base.js';\nimport type { MemorySettingsStorage } from '../../storage/domains/memory-settings/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { FactoryChannelsConfig } from '../base.js';\n\n// Derive the thread/message types from the core handler signature rather than\n// importing them from `chat` directly: mc-web can resolve a different `chat`\n// version than @mastra/core, and the two `Thread`/`Message` declarations are\n// structurally incompatible (private fields). Using the handler's own types\n// keeps everything on one version.\ntype HandlerThread = Parameters<ChannelHandler>[0];\ntype HandlerMessage = Parameters<ChannelHandler>[1];\n\n/** Dependencies the Slack channel handlers close over, injected from the web entry. */\ninterface SlackChannelDeps {\n /**\n * The factory's reverse-index store mapping a Slack sender to a Mastra\n * tenant. When provided, inbound messages from an unlinked sender are not\n * dispatched — the run only proceeds (with the sender's tenant stamped on\n * the request context) once they've linked their account. Unlinked senders\n * get an ephemeral \"connect your account\" card instead.\n */\n accountLinks?: ChannelIdentityStorage;\n /**\n * Factory projects domain. When provided (alongside `accountLinks`), a\n * linked sender's run must also resolve to a Factory project before it\n * dispatches: their link's default factory, else their tenant's only\n * factory (stamped back onto the link), else an ephemeral \"pick a default\n * factory\" card and no run. Unset → no factory routing (runs dispatch as\n * before).\n */\n projects?: FactoryProjectsStorage;\n /**\n * Storage handle of the integration that owns source control\n * (`IntegrationContext.storage.sourceControlOwner`). Used to make new Slack\n * threads repo-backed: when the sender is linked and their factory has a\n * repository, the thread's resourceId becomes a Factory user-session id (repo\n * cloned on a `slack/{threadTs}` branch) instead of the chat-only\n * `channel:...` id. It also lets a started session read back the project it\n * belongs to. Nothing here is provider-specific — the connection is matched\n * by the handle's own `integrationId`. Absent (no source-control integration\n * registered) → chat-only sessions as before.\n */\n sourceControl?: SourceControlStorageHandle;\n /**\n * Observational-memory settings domain. When provided, a repo-backed session\n * adopts its factory project's shared memory settings on start, matching the\n * web kickoff.\n */\n memorySettings?: MemorySettingsStorage;\n /**\n * Factory work-items domain. When provided, a dispatched new-session thread\n * (DM or mention) upserts a Work-board card in Building (`execute`) carrying\n * the Slack thread as its external source and binding the repo-backed\n * session. Best-effort — a failure never blocks the run. Unset → no card.\n */\n workItems?: WorkItemsStorage;\n /** Overrides applied to the Slack channel adapter entry. */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\n/**\n * Read the Slack team id off a raw platform payload (Events API envelope or\n * slash-command body — both carry `team_id`), duck-typed to build the\n * workspace-scoped account-link key.\n */\nfunction rawTeamId(rawPayload: unknown): string | undefined {\n if (!rawPayload || typeof rawPayload !== 'object') return undefined;\n const raw = rawPayload as { team_id?: unknown; team?: unknown };\n if (typeof raw.team_id === 'string' && raw.team_id) return raw.team_id;\n if (typeof raw.team === 'string' && raw.team) return raw.team;\n if (raw.team && typeof raw.team === 'object') {\n const id = (raw.team as { id?: unknown }).id;\n if (typeof id === 'string' && id) return id;\n }\n return undefined;\n}\n\n/**\n * The Slack team id survives onto a normalized chat Message only on\n * `message.raw` (the Slack Events API envelope).\n */\nfunction slackTeamId(message: HandlerMessage): string | undefined {\n return rawTeamId(message.raw);\n}\n\n/**\n * Resolve the web-UI origin for links humans open in a browser (Connect card,\n * session deep links). Prefers `MASTRACODE_PUBLIC_URL` — the origin auth\n * cookies and OAuth redirect allow-lists are registered against — over the\n * channels tunnel, which only Slack's servers need to reach.\n */\nfunction webPublicUrl(): string | undefined {\n return process.env.MASTRACODE_PUBLIC_URL ?? process.env.MASTRACODE_CHANNELS_PUBLIC_URL;\n}\n\n/** Outcome of the sender-link gate for one inbound message. */\ntype LinkedSenderResult =\n /** Gating not configured — dispatch as before account linking existed. */\n | { status: 'ungated' }\n /** Sender unlinked — Connect card posted (when possible), do not dispatch. */\n | { status: 'blocked' }\n /** Sender linked — their tenant plus the sender key the link lives under. */\n | { status: 'linked'; link: ChannelAccountLink; key: ChannelAccountLinkKey };\n\n/**\n * Resolve the sender's account link, posting an ephemeral \"connect your\n * account\" card (visible only to the sender) linking into the web UI's\n * Slack-connect flow when they're unlinked.\n */\nexport async function resolveLinkedSender({\n thread,\n message,\n accountLinks,\n}: {\n thread: HandlerThread;\n message: HandlerMessage;\n accountLinks?: ChannelIdentityStorage;\n}): Promise<LinkedSenderResult> {\n if (!accountLinks) return { status: 'ungated' };\n const platform = thread.adapter.name;\n const externalUserId = message.author.userId;\n const externalTeamId = slackTeamId(message);\n // Without a team id we can't identify the workspace-scoped link; treat as\n // unlinked so a run never proceeds tenant-less.\n const key = externalTeamId ? { platform, externalTeamId, externalUserId } : undefined;\n const link = key ? await accountLinks.getAccountLink(key) : null;\n if (link && key) return { status: 'linked', link, key };\n\n const publicUrl = webPublicUrl();\n // A public origin is all the card needs. The link carries no identity: the\n // web app authenticates the visitor, then Slack's OIDC flow proves which\n // Slack account they control. Without an origin, still block, just no card.\n if (publicUrl) {\n await thread.postEphemeral(message.author, buildConnectCard(publicUrl), { fallbackToDM: true });\n }\n return { status: 'blocked' };\n}\n\n/**\n * The \"connect your account\" card. The link is deliberately identity-free —\n * `/connect/slack` sends the visitor to Connections, where \"Connect Slack\"\n * runs the OIDC flow and Slack itself asserts the (team, user) pair.\n */\nfunction buildConnectCard(publicUrl: string) {\n return Card({\n title: 'Connect your account',\n children: [\n CardText('Connect your account to use this agent.'),\n Actions([\n LinkButton({\n url: `${publicUrl}/connect/slack`,\n label: 'Connect account',\n }),\n ]),\n ],\n });\n}\n\n/** Outcome of factory routing for one linked sender's inbound message. */\ntype FactoryRouteResult =\n /** Factory routing not configured — dispatch without a factory. */\n | { status: 'ungated' }\n /** No factory resolved — prompt card posted (when possible), do not dispatch. */\n | { status: 'blocked' }\n /** The Factory project this sender's runs route to. */\n | { status: 'resolved'; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n\n/**\n * Decide which Factory project a linked sender's run belongs to:\n *\n * 1. The link's `defaultFactoryProjectId`, when it still exists (a stale id —\n * deleted factory — falls through as if unset).\n * 2. Else, the tenant's only factory, stamped back onto the link so it shows\n * up (and stays editable) in Connected Accounts settings.\n * 3. Else — zero or several factories — an ephemeral \"pick a default factory\"\n * card deep-linking to settings, and the run is blocked.\n */\nexport async function resolveFactoryForLink({\n thread,\n message,\n link,\n key,\n accountLinks,\n projects,\n}: {\n thread: HandlerThread;\n message: HandlerMessage;\n link: ChannelAccountLink;\n key: ChannelAccountLinkKey;\n accountLinks: ChannelIdentityStorage;\n projects?: FactoryProjectsStorage;\n}): Promise<FactoryRouteResult> {\n if (!projects) return { status: 'ungated' };\n // Factories are org-scoped; a personal account (no org) has none and lands\n // on the prompt below.\n const orgId = link.orgId ?? '';\n\n if (link.defaultFactoryProjectId) {\n const existing = await projects.get({ orgId, id: link.defaultFactoryProjectId });\n if (existing) {\n return {\n status: 'resolved',\n factoryProjectId: existing.id,\n slackWorkItemsEnabled: existing.slackWorkItemsEnabled,\n };\n }\n }\n\n const factories = orgId ? await projects.list({ orgId }) : [];\n if (factories.length === 1) {\n const only = factories[0]!;\n await accountLinks.setDefaultFactory({ ...key, userId: link.userId, factoryProjectId: only.id });\n return {\n status: 'resolved',\n factoryProjectId: only.id,\n slackWorkItemsEnabled: only.slackWorkItemsEnabled,\n };\n }\n\n const publicUrl = webPublicUrl();\n if (publicUrl) {\n await thread.postEphemeral(\n message.author,\n Card({\n title: 'Pick a default factory',\n children: [\n CardText(\n factories.length === 0\n ? 'Your account has no factory yet. Create one in the web app, then message me again.'\n : 'Your account has several factories. Pick which one Slack sessions should go to, then message me again.',\n ),\n Actions([\n LinkButton({\n url: `${publicUrl}/settings/connections`,\n label: 'Open settings',\n }),\n ]),\n ],\n }),\n { fallbackToDM: true },\n );\n }\n return { status: 'blocked' };\n}\n\n/**\n * Deterministic per-thread branch name: `slack/{threadTs}` with characters\n * outside the sandbox git-ref allow-list (`[A-Za-z0-9_./-]`, and `.` for\n * readability) mapped to `-`. `thread.id` is `{channelId}:{threadTs}`\n * (platform-prefixed on handler threads) — the trailing segment is the ts.\n *\n * Top-level DM and channel conversations use the empty-threadTs thread form,\n * so the trailing segment can be empty; a bare `slack/` is not a valid git\n * ref. Fall back to the last non-empty segment (the channel id): one\n * deterministic branch per top-level conversation.\n */\nfunction threadBranch(threadId: string): string {\n const segments = threadId.split(':');\n const tail = segments.findLast(segment => segment.length > 0) ?? threadId;\n return `slack/${tail.replace(/[^A-Za-z0-9_/-]/g, '-')}`;\n}\n\n/**\n * Resolve the resourceId for a NEW Slack channel thread. A linked sender whose\n * factory has a repository gets a Factory user-session id — the controller\n * session then materializes the repo sandbox via the factory's dynamic\n * workspace (clone + PAT), the session shows up in the web Sessions list, and\n * View Session deep-links land on the normal workspace route. Everything else\n * (unlinked, unrouted, repo-less, or no source control) keeps the chat-only\n * `defaultResourceId`.\n *\n * Pure lookups only — cards for unlinked/unrouted senders are the dispatch\n * gate's job; this hook must never post.\n */\nexport function createChannelResourceIdResolver(deps: SlackChannelDeps): ResolveResourceId {\n const { accountLinks, projects, sourceControl } = deps;\n return async ({ platform, thread, message }) => {\n // NOT the hook's `defaultResourceId`: configuring a custom resolver\n // bypasses AgentControllerChannels' own `channel:{thread.id}` derivation\n // (agent-controller-channels.ts `resolveChannelResourceId`), and the base\n // default is the per-USER memory key. Chat-only fallbacks must stay\n // per-thread, so reproduce the controller default here.\n const chatOnlyResourceId = `channel:${thread.id}`;\n if (!accountLinks || !projects || !sourceControl) return chatOnlyResourceId;\n try {\n const externalTeamId = rawTeamId(message.raw);\n if (!externalTeamId) return chatOnlyResourceId;\n const link = await accountLinks.getAccountLink({\n platform,\n externalTeamId,\n externalUserId: message.author.userId,\n });\n if (!link) return chatOnlyResourceId;\n\n // Same chain as `resolveFactoryForLink`, minus prompts/stamping: the\n // dispatch gate has already run (and stamped a lone factory) by the\n // time a new thread is created, so this is a read-only re-resolve.\n const orgId = link.orgId ?? '';\n let factoryProjectId: string | undefined;\n if (link.defaultFactoryProjectId && (await projects.get({ orgId, id: link.defaultFactoryProjectId }))) {\n factoryProjectId = link.defaultFactoryProjectId;\n } else if (orgId) {\n const factories = await projects.list({ orgId });\n if (factories.length === 1) factoryProjectId = factories[0]!.id;\n }\n if (!factoryProjectId) return chatOnlyResourceId;\n\n const repo = await resolveFactorySourceRepository({ sourceControl, orgId, factoryProjectId });\n if (!repo.found) return chatOnlyResourceId;\n\n const branch = threadBranch(thread.id);\n // Attributed to the Slack sender, not to whoever connected the repository:\n // unlike an autonomous rule run, a Slack thread has a real interactive user.\n const existing = await sourceControl.sessions.getForBranch({\n projectRepositoryId: repo.projectRepositoryId,\n userId: link.userId,\n branch,\n });\n if (existing) return existing.sessionId;\n const session = await sourceControl.sessions.create({\n sessionId: randomUUID(),\n projectRepositoryId: repo.projectRepositoryId,\n orgId,\n userId: link.userId,\n branch,\n baseBranch: repo.baseBranch,\n // DMs are the only private origin; channel threads are org-visible.\n visibility: thread.isDM ? 'private' : 'org',\n });\n return session.sessionId;\n } catch (error) {\n // Fall back to a chat-only session rather than dropping the message.\n console.warn('[slack] repo-backed session resolution failed for thread', thread.id, error);\n return chatOnlyResourceId;\n }\n };\n}\n\n/**\n * Thread id for a NEW Slack channel thread. Repo-backed threads take the\n * user-session id AS their thread id, matching the web convention\n * (FactoryStartCoordinator seeds threads with threadId = sessionId) so\n * `/workspaces/{sessionId}/threads/{sessionId}` resolves Slack-created\n * sessions exactly like web-created ones — no `?resourceId=` override needed.\n * Chat-only threads keep the default random id: their `channel:...`\n * resourceId is a memory key, not a unique thread id.\n */\nexport const resolveChannelThreadId: ResolveThreadId = ({ resourceId, defaultThreadId }) =>\n resourceId.startsWith('channel:') ? defaultThreadId : resourceId;\n\n/**\n * Apply the factory's configuration to a Slack-created session the first time\n * its thread reaches the controller.\n *\n * Without this a Slack session runs on the SDK's built-in mode default\n * (`openai/gpt-5.5`), so a factory configured for any other provider fails every\n * message with a missing-credentials error. The web kickoff has always applied\n * the factory default; this brings Slack to the same footing.\n *\n * Only repo-backed threads are configured. Their resourceId IS the Factory\n * session id, which the source-control rows turn back into a project — a\n * chat-only `channel:...` id names no project, so there is nothing to read.\n *\n * Skips a session whose mode already has a model persisted on the thread. That\n * is the durable record of a deliberate choice — either an earlier start or a\n * user's own switch — and re-applying the factory default over it would undo\n * the user's selection every time the process restarts.\n */\nexport function createChannelSessionStartHook(deps: SlackChannelDeps): ChannelSessionStart {\n const { projects, sourceControl, memorySettings } = deps;\n return async ({ session, thread, requestContext }) => {\n // Seed the tenant org above every guard below. `gateDispatch` stamps it on\n // the message's request context before the session exists, so this needs no\n // storage read — which matters, because the guards below deliberately skip\n // storage on a restarted session. A channel-only thread and a thread whose\n // dispatch was ungated both land here with no org and are marked unresolved\n // rather than being left to look like a local session.\n await seedSessionOrg(session, readRequestContextOrgId(requestContext));\n\n if (!projects || !sourceControl) return;\n if (thread.resourceId.startsWith('channel:')) return;\n\n const owner = await resolveFactoryProjectForSession({ sourceControl, sessionId: thread.resourceId });\n if (!owner) return;\n\n // Repo-backed Slack sessions are factory sessions: stamp the owning\n // project onto controller state so downstream reads (org-first credential\n // resolution, authority gates) recognize them, same as board runs.\n await session.state.set({ factoryProjectId: owner.factoryProjectId });\n // Route the org through the seed helper rather than stamping it directly:\n // an ungated dispatch marked this session unresolved above, and owner\n // recovery is the resolution that has to clear that marker with it.\n await seedSessionOrg(session, owner.orgId);\n\n const modeModelKey = `modeModelId_${session.mode.get()}`;\n if (await session.thread.getSetting({ key: modeModelKey })) return;\n\n const defaultModelId = await resolveFactoryDefaultModelId(projects, owner.factoryProjectId);\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n defaultModelId,\n memorySettings,\n });\n };\n}\n\n/**\n * The internal Mastra thread the framework created for a channel conversation.\n * The handler's `thread.id` is the platform thread id (e.g. `slack:C123:ts`),\n * NOT the internal UUID — the mapping lives in the stored thread's channel\n * metadata.\n */\nasync function findInternalThread(mastra: Mastra | undefined, thread: HandlerThread) {\n const store = await mastra?.getStorage()?.getStore('memory');\n const { threads } = (await store?.listThreads({\n filter: {\n metadata: {\n channel_platform: thread.adapter.name,\n channel_externalThreadId: thread.id,\n channel_externalChannelId: thread.channelId,\n },\n },\n perPage: 1,\n })) ?? { threads: [] };\n return threads[0];\n}\n\n/**\n * Build the \"new session\" handler for mention / direct-message events. A mention or\n * DM on a not-yet-subscribed thread starts a NEW session; once subscribed, later\n * events are follow-ups and don't re-announce.\n */\n/**\n * Run the account-link + factory-routing gates for one inbound message.\n * Returns `null` when the run must not dispatch (a prompt card was posted\n * where possible); otherwise the dispatch context — with `routed` present\n * only when a linked sender resolved to a factory.\n */\nasync function gateDispatch(\n thread: HandlerThread,\n message: HandlerMessage,\n { accountLinks, projects }: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n): Promise<{\n routed?: { link: ChannelAccountLink; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n} | null> {\n const sender = await resolveLinkedSender({ thread, message, accountLinks });\n if (sender.status === 'blocked') return null;\n // Linked senders must also route to a Factory project before a run starts.\n if (sender.status === 'linked' && accountLinks) {\n // Stamp the tenant on the run's request context — the single seam\n // `resolveCredentialStore` reads to load this sender's model credentials.\n // This belongs to the link, not to the routing: a linked sender whose\n // factory routing comes back `ungated` still exits below and dispatches, so\n // stamping only in the routed branch would silently run them on default\n // credentials.\n ctx.requestContext.set('user', { id: sender.link.userId, organizationId: sender.link.orgId });\n\n const route = await resolveFactoryForLink({ thread, message, ...sender, accountLinks, projects });\n if (route.status === 'blocked') return null;\n if (route.status === 'resolved') {\n return {\n routed: {\n link: sender.link,\n factoryProjectId: route.factoryProjectId,\n slackWorkItemsEnabled: route.slackWorkItemsEnabled,\n },\n };\n }\n }\n return {};\n}\n\n/**\n * Upsert the Work-board card for a dispatched Slack-thread run. Keyed on the\n * thread via `externalSource` — the work-items domain's unique\n * `(factory_project_id, source_key)` index makes repeat messages reuse the\n * same card, and `reuseMode: 'preserve'` keeps a card a human already dragged\n * across stages untouched. The card lands in Building (`execute`) for every\n * dispatched thread (DM or mention) — there is deliberately no per-origin\n * stage split; smart routing is a follow-up.\n *\n * The session id / branch / threadId and the workspace deep-link are resolved\n * by the caller (which already looked up the internal thread), so this helper\n * just shapes and writes. Best-effort: the run is already dispatched, so a\n * failure logs instead of throwing — work-item creation must never abort a Slack run.\n */\nexport async function upsertThreadWorkItem({\n workItems,\n thread,\n message,\n link,\n factoryProjectId,\n session,\n url,\n}: {\n workItems: WorkItemsStorage;\n thread: HandlerThread;\n message: HandlerMessage;\n link: ChannelAccountLink;\n factoryProjectId: string;\n /**\n * The repo-backed Factory session to bind under the `chat` role, or\n * `undefined` for a chat-only thread (no Factory session to bind).\n */\n session?: { sessionId: string; branch: string; threadId: string };\n /** Workspace deep-link to the running session; omitted when no public URL. */\n url?: string;\n}): Promise<void> {\n try {\n const title = message.text.length > 80 ? `${message.text.slice(0, 79)}…` : message.text;\n\n await workItems.upsert({\n orgId: link.orgId ?? '',\n userId: link.userId,\n factoryProjectId,\n reuseMode: 'preserve',\n input: {\n title: title || 'Slack thread',\n // `integrationId` is the platform ('slack'); `type` is a single\n // constant (no DM/mention distinction); `externalId` is the stable\n // platform thread id — together they form the idempotency key.\n externalSource: {\n integrationId: thread.adapter.name,\n type: 'slack-thread',\n externalId: thread.id,\n ...(url ? { url } : {}),\n },\n stages: ['execute'],\n ...(session ? { sessions: { chat: session } } : {}),\n },\n });\n } catch (error) {\n console.warn('[slack] work-item creation failed for thread', thread.id, error);\n }\n}\n\nfunction createNewSessionChatHandler(deps: SlackChannelDeps): ChannelHandler {\n const { workItems } = deps;\n return async (thread, message, defaultHandler, ctx) => {\n // Gate on the sender having linked their Slack account to a Mastra tenant.\n // Unlinked → post the ephemeral Connect card and stop; no session/run is\n // created (which would otherwise be tenant-less and fail credential\n // resolution). This handler is the only gate — core dispatches whatever\n // reaches it — so every slot that can start a run must call it.\n const gate = await gateDispatch(thread, message, deps, ctx);\n if (!gate) return;\n\n // A mention on a not-yet-subscribed thread is a NEW session. The\n // default handler auto-subscribes, so once subscribed this is a\n // follow-up mention — don't re-announce.\n const isNewSession = !(await thread.isSubscribed());\n\n // Run the framework handler first so the internal Mastra thread and\n // controller session are created before we build the deep link.\n await defaultHandler(thread, message);\n\n if (!isNewSession) return;\n\n // The internal-thread lookup and deep-link are needed by BOTH the\n // announcement card AND work-item creation, so they run BEFORE the\n // card-only `MASTRACODE_PUBLIC_URL` gate — a deployment without a public\n // origin should still create board cards, just without a clickable link.\n const internalThread = await findInternalThread(ctx.mastra, thread);\n if (!internalThread) {\n console.warn('[onMention] no internal thread found for', thread.id);\n return;\n }\n\n // When the sender routed to a factory we know exactly which workspace the\n // session belongs to — deep-link straight into it. A repo-backed thread's\n // resourceId IS the Factory user-session id, so the link lands on the same\n // route a web-started run navigates to; chat-only threads keep the literal\n // `channel` segment (the real resource rides the `?resourceId=` override).\n // Unrouted senders fall back to the factory-agnostic /threads/ redirect.\n // One predicate drives both the path segment and the query param, so the\n // two can never disagree about what the URL already carries.\n const isChatOnly = internalThread.resourceId.startsWith('channel:');\n const workspaceSegment = isChatOnly ? 'channel' : encodeURIComponent(internalThread.resourceId);\n const threadPath = gate.routed\n ? `/factories/${encodeURIComponent(gate.routed.factoryProjectId)}/workspaces/${workspaceSegment}/threads/${encodeURIComponent(internalThread.id)}`\n : `/threads/${internalThread.id}`;\n\n // The param is an override for a URL that can't otherwise name its\n // resource. A routed repo-backed thread already spells the resourceId out\n // as its workspace segment, so appending it again is duplication the app\n // ignores. Chat-only threads need it (their segment is the literal string\n // `channel`), and so does the unrouted fallback, which has no workspace\n // segment at all — `ChannelThreadRedirect` forwards the search through.\n //\n // One shared deep-link: the card's button and the work-item `url` read the\n // SAME value so they can never drift. Undefined without a public origin —\n // the card is then skipped, but the work item is still created (url omitted).\n const needsResourceParam = isChatOnly || !gate.routed;\n const deepLink = process.env.MASTRACODE_PUBLIC_URL\n ? needsResourceParam\n ? `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}?resourceId=${encodeURIComponent(internalThread.resourceId)}`\n : `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}`\n : undefined;\n\n // A dispatched, routed new-session thread becomes a Work-board card in\n // Building. Only routed senders (linked → factory) have the org/user/factory\n // a work item needs. Bind the repo-backed Factory session under the `chat`\n // role; a chat-only `channel:` resourceId is NOT a session id, so bind\n // nothing rather than a bad id. Best-effort (the helper swallows failures).\n if (workItems && gate.routed?.slackWorkItemsEnabled) {\n const session = isChatOnly\n ? undefined\n : { sessionId: internalThread.resourceId, branch: threadBranch(thread.id), threadId: internalThread.id };\n await upsertThreadWorkItem({\n workItems,\n thread,\n message,\n link: gate.routed.link,\n factoryProjectId: gate.routed.factoryProjectId,\n session,\n url: deepLink,\n });\n }\n\n // The announcement card is only useful with a public origin to deep-link\n // to — otherwise the link would be `undefined/threads/...`. Without one the\n // session (and now the work item) still exist; we just skip the broken card.\n if (!deepLink) return;\n\n await thread.post(\n Card({\n title: 'New session started',\n children: [Actions([LinkButton({ url: deepLink, label: 'View session' })])],\n }),\n );\n };\n}\nexport const createHandlers = (deps: SlackChannelDeps): ChannelHandlers => {\n const newSessionChatHandler = createNewSessionChatHandler(deps);\n\n return {\n onSubscribedMessage: async (thread, message, defaultHandler, ctx) => {\n // `aside` as its own leading word lets humans talk in a subscribed\n // thread without the bot replying. Word boundary so messages that\n // merely start with \"aside...\" (e.g. \"asides can wait\") still route.\n if (/^aside\\b/i.test(message.text)) return;\n // A subscribed follow-up from an unlinked sender must not run either\n // (e.g. the link was removed mid-conversation), and it must still\n // resolve a factory (e.g. the default was cleared or its factory\n // deleted mid-conversation).\n const gate = await gateDispatch(thread, message, deps, ctx);\n if (!gate) return;\n await defaultHandler(thread, message);\n },\n onMention: newSessionChatHandler,\n onDirectMessage: newSessionChatHandler,\n };\n};\n\n/** Slack app credentials, passed in explicitly rather than read from env here. */\ninterface SlackCredentials {\n clientId?: string;\n clientSecret?: string;\n signingSecret: string;\n botToken?: string;\n}\n\nexport function createSlackChannelsConfig(deps: SlackChannelDeps & { slack: SlackCredentials }): FactoryChannelsConfig {\n const adapter = createSlackAdapter(deps.slack);\n const slack =\n deps.adapterOptions?.streaming === false\n ? { adapter, ...deps.adapterOptions }\n : { adapter, ...deps.adapterOptions, streaming: deps.adapterOptions?.streaming ?? true };\n\n return {\n adapters: { slack },\n handlers: createHandlers(deps),\n // New linked+repo-backed threads own a Factory user-session id as their\n // resourceId, which is what makes the controller session repo-backed.\n resolveResourceId: createChannelResourceIdResolver(deps),\n resolveThreadId: resolveChannelThreadId,\n // Those sessions are created by the channel machinery, not the web kickoff,\n // so this is where they pick up the factory's configuration.\n onSessionStart: createChannelSessionStartHook(deps),\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory-session.d.ts","sourceRoot":"","sources":["../../src/session/factory-session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAGrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACxF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"factory-session.d.ts","sourceRoot":"","sources":["../../src/session/factory-session.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { seedSessionOrg } from "./org-seed.js";
|
|
1
2
|
import { factoryMemorySettingsUserId } from "../storage/domains/memory-settings/base.js";
|
|
2
3
|
import { applyStoredMemorySettings } from "./memory-settings-hydration.js";
|
|
3
4
|
import { randomUUID } from "crypto";
|
|
@@ -150,6 +151,7 @@ async function ensureFactorySourceSession(args) {
|
|
|
150
151
|
* default it was created with, and the reason is logged.
|
|
151
152
|
*/
|
|
152
153
|
async function hydrateFactorySession(session, args) {
|
|
154
|
+
await seedSessionOrg(session, args.orgId);
|
|
153
155
|
try {
|
|
154
156
|
const record = args.memorySettings && args.factoryProjectId ? await args.memorySettings.get({
|
|
155
157
|
orgId: args.orgId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory-session.js","names":[],"sources":["../../src/session/factory-session.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { resolveProviderOMDefault } from '@mastra/code-sdk/onboarding/packs';\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\n\nimport { factoryMemorySettingsUserId } from '../storage/domains/memory-settings/base.js';\nimport type { MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { applyStoredMemorySettings } from './memory-settings-hydration.js';\n\ntype FactorySession = Awaited<ReturnType<AgentController<MastraCodeState>['createSession']>>;\n\n/**\n * Read the factory project's default model. Best-effort: a missing project or an\n * uninitialized storage domain means \"no default\", never a failed run.\n */\nexport async function resolveFactoryDefaultModelId(\n projects: FactoryProjectsStorage | undefined,\n factoryProjectId: string | undefined,\n): Promise<string | undefined> {\n if (!projects || !factoryProjectId) return undefined;\n try {\n const project = await projects.getById({ id: factoryProjectId });\n return project?.defaultModelId ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nexport interface EnsureFactorySourceSessionArgs {\n /**\n * Storage handle of the integration that owns source control. Nothing here is\n * provider-specific: the connection is matched by the handle's own\n * `integrationId`, so GitHub, Slack-on-behalf-of-GitHub, or any future owner\n * all resolve through the same traversal.\n */\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n branch: string;\n /** Pick a specific linked repository by slug. Defaults to the first linked repository. */\n repositorySlug?: string;\n /**\n * Attribute the run to this user instead of the repo connector. Set when the\n * run has an interactive user — e.g. the person who approved a proposed run.\n */\n attributeToUserId?: string;\n}\n\nexport interface EnsuredFactorySourceSession {\n sessionId: string;\n userId: string;\n projectRepositoryId: string;\n branch: string;\n baseBranch: string;\n}\n\nexport class FactorySourceSessionResolutionError extends Error {\n constructor(readonly reason: 'connection' | 'repository') {\n super(\n reason === 'connection'\n ? 'Factory source-control connection not found.'\n : 'Factory source-control repository not found.',\n );\n this.name = 'FactorySourceSessionResolutionError';\n }\n}\n\nexport interface ResolvedFactorySourceRepository {\n projectRepositoryId: string;\n /** The repository's pinned branch, else its default branch. */\n baseBranch: string;\n /** Who connected the repository. The attribution for runs with no interactive user. */\n connectedByUserId: string;\n}\n\n/**\n * Outcome of {@link resolveFactorySourceRepository}. A miss carries which step\n * failed: callers differ on whether that is an error (an autonomous run cannot\n * proceed) or a routine fallback (a chat integration drops to a chat-only\n * session), and the two steps fail for different reasons worth reporting apart.\n */\nexport type FactorySourceRepositoryResult =\n | ({ found: true } & ResolvedFactorySourceRepository)\n | { found: false; reason: 'connection' | 'repository' };\n\n/**\n * Resolve which repository a factory project's source-control runs act on: the\n * owner's connection on the project, then one of its linked repositories.\n *\n * The owner is whichever integration owns source control, matched by the\n * handle's own `integrationId` — nothing here is provider-specific.\n */\nexport async function resolveFactorySourceRepository(args: {\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n /** Pick a specific linked repository by slug. Defaults to the first linked repository. */\n repositorySlug?: string;\n}): Promise<FactorySourceRepositoryResult> {\n const { sourceControl, orgId, factoryProjectId, repositorySlug } = args;\n\n const connections = await sourceControl.connections.list({ orgId, factoryProjectId });\n const candidates = connections.filter(candidate => candidate.integrationId === sourceControl.integrationId);\n if (candidates.length === 0) return { found: false, reason: 'connection' };\n\n // A project can carry stale connections: a provider-app reinstall leaves the\n // old connection pointing at an installation that no longer exists, and that\n // row can sit ahead of the healthy one. Try every candidate and skip the ones\n // that no longer resolve rather than failing on the first.\n for (const connection of candidates) {\n let resolved;\n try {\n const projectRepositories = await sourceControl.projectRepositories.list({ orgId, connectionId: connection.id });\n const resolvedRepositories = await Promise.all(\n projectRepositories.map(async projectRepository => ({\n projectRepository,\n repository: await sourceControl.repositories.get({ orgId, id: projectRepository.repositoryId }),\n })),\n );\n resolved = resolvedRepositories.find(\n candidate => candidate.repository && (!repositorySlug || candidate.repository.slug === repositorySlug),\n );\n } catch {\n // The connection no longer resolves (e.g. its installation was deleted).\n continue;\n }\n if (!resolved?.repository) continue;\n\n return {\n found: true,\n projectRepositoryId: resolved.projectRepository.id,\n baseBranch: resolved.projectRepository.branch ?? resolved.repository.defaultBranch,\n connectedByUserId: connection.createdByUserId,\n };\n }\n\n return { found: false, reason: 'repository' };\n}\n\n/**\n * Walk a Factory user-session id back to the project it belongs to.\n *\n * Repo-backed channel threads are keyed by their Factory session id, which is\n * the only handle a session-start hook gets. This turns that id back into the\n * project whose configuration the session should adopt. Durable by\n * construction — it reads the same rows the session was created from, so it\n * survives restarts without any in-memory mapping.\n */\nexport async function resolveFactoryProjectForSession(args: {\n sourceControl: SourceControlStorageHandle;\n sessionId: string;\n}): Promise<{ factoryProjectId: string; orgId: string; userId: string } | null> {\n const { sourceControl, sessionId } = args;\n\n const session = await sourceControl.sessions.getBySessionId(sessionId);\n if (!session) return null;\n const projectRepository = await sourceControl.projectRepositories.get({\n orgId: session.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) return null;\n const connection = await sourceControl.connections.get({ orgId: session.orgId, id: projectRepository.connectionId });\n if (!connection) return null;\n\n return { factoryProjectId: connection.factoryProjectId, orgId: session.orgId, userId: session.userId };\n}\n\n/**\n * Create the source-control session a repo-backed factory run needs.\n *\n * `FactoryStartCoordinator.prepare` requires this record to already exist —\n * `resolveSourceSession` throws `Factory session not found` otherwise — so every\n * autonomous entry point has to produce one before it can start a run. This is\n * that step, in one place: the owner's connection on the factory project, one of\n * its linked repositories, and a session on the requested branch with the\n * repository's pinned or default branch as the base.\n *\n * The run is attributed to `attributeToUserId` when the caller has an\n * interactive user (e.g. the approver of a proposed run), and otherwise falls\n * back to whoever connected the repository (`connection.createdByUserId`),\n * because a genuinely autonomous run has no interactive user of its own.\n */\nexport async function ensureFactorySourceSession(\n args: EnsureFactorySourceSessionArgs,\n): Promise<EnsuredFactorySourceSession> {\n const { sourceControl, orgId, factoryProjectId, branch, repositorySlug } = args;\n\n const resolved = await resolveFactorySourceRepository({ sourceControl, orgId, factoryProjectId, repositorySlug });\n if (!resolved.found) throw new FactorySourceSessionResolutionError(resolved.reason);\n\n const userId = args.attributeToUserId ?? resolved.connectedByUserId;\n const session = await sourceControl.sessions.create({\n sessionId: randomUUID(),\n projectRepositoryId: resolved.projectRepositoryId,\n orgId,\n userId,\n branch,\n baseBranch: resolved.baseBranch,\n visibility: 'org',\n });\n return {\n sessionId: session.sessionId,\n userId,\n projectRepositoryId: resolved.projectRepositoryId,\n branch: session.branch,\n baseBranch: resolved.baseBranch,\n };\n}\n\nexport interface HydrateFactorySessionArgs {\n orgId: string;\n /**\n * The factory project whose shared memory settings apply. Factory sessions\n * never read an individual user's personal memory settings — the project's\n * own row (or the built-in defaults) is what they run with.\n */\n factoryProjectId?: string;\n /** The factory project's default model. Without it the session keeps the SDK's built-in mode default. */\n defaultModelId?: string;\n /**\n * When provided, the factory project's stored memory-settings row is\n * applied. When omitted (or no row exists) the session is reset to the\n * built-in memory defaults.\n */\n memorySettings?: MemorySettingsStorage;\n}\n\n/**\n * Apply a factory project's configuration to a freshly created session:\n * observational-memory settings, then the project's default model.\n *\n * Both steps are best-effort. A retired model id or an unreachable settings row\n * must not sink a run that is otherwise ready — the session simply keeps the\n * default it was created with, and the reason is logged.\n */\nexport async function hydrateFactorySession(session: FactorySession, args: HydrateFactorySessionArgs): Promise<void> {\n try {\n const record =\n args.memorySettings && args.factoryProjectId\n ? await args.memorySettings.get({\n orgId: args.orgId,\n userId: factoryMemorySettingsUserId(args.factoryProjectId),\n })\n : null;\n // Without a stored row, fall back to the low-cost OM model of the factory\n // default model's provider — a factory connected only to Anthropic should\n // not observe with the (uncredentialed) built-in Google default.\n const provider = args.defaultModelId?.split('/')[0];\n const fallbackOmModelId = provider ? resolveProviderOMDefault(provider, args.defaultModelId).modelId : undefined;\n await applyStoredMemorySettings(session, record, fallbackOmModelId);\n } catch (error) {\n console.warn('[Factory Start] Failed to apply observational-memory settings', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n if (args.defaultModelId) {\n try {\n await session.model.switch({ modelId: args.defaultModelId });\n } catch (error) {\n console.warn('[Factory Start] Failed to apply factory default model', {\n modelId: args.defaultModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n}\n"],"mappings":";;;;;;;;;AAkBA,eAAsB,6BACpB,UACA,kBAC6B;CAC7B,IAAI,CAAC,YAAY,CAAC,kBAAkB,OAAO,KAAA;CAC3C,IAAI;EAEF,QAAO,MADe,SAAS,QAAQ,EAAE,IAAI,iBAAiB,CAAC,EAAA,EAC/C,kBAAkB,KAAA;CACpC,QAAQ;EACN;CACF;AACF;AA8BA,IAAa,sCAAb,cAAyD,MAAM;CACxC;CAArB,YAAY,QAA8C;EACxD,MACE,WAAW,eACP,iDACA,8CACN;EALmB,KAAA,SAAA;EAMnB,KAAK,OAAO;CACd;AACF;;;;;;;;AA2BA,eAAsB,+BAA+B,MAMV;CACzC,MAAM,EAAE,eAAe,OAAO,kBAAkB,mBAAmB;CAGnE,MAAM,cAAa,MADO,cAAc,YAAY,KAAK;EAAE;EAAO;CAAiB,CAAC,EAAA,CACrD,QAAO,cAAa,UAAU,kBAAkB,cAAc,aAAa;CAC1G,IAAI,WAAW,WAAW,GAAG,OAAO;EAAE,OAAO;EAAO,QAAQ;CAAa;CAMzE,KAAK,MAAM,cAAc,YAAY;EACnC,IAAI;EACJ,IAAI;GACF,MAAM,sBAAsB,MAAM,cAAc,oBAAoB,KAAK;IAAE;IAAO,cAAc,WAAW;GAAG,CAAC;GAO/G,YAAW,MANwB,QAAQ,IACzC,oBAAoB,IAAI,OAAM,uBAAsB;IAClD;IACA,YAAY,MAAM,cAAc,aAAa,IAAI;KAAE;KAAO,IAAI,kBAAkB;IAAa,CAAC;GAChG,EAAE,CACJ,EAAA,CACgC,MAC9B,cAAa,UAAU,eAAe,CAAC,kBAAkB,UAAU,WAAW,SAAS,eACzF;EACF,QAAQ;GAEN;EACF;EACA,IAAI,CAAC,UAAU,YAAY;EAE3B,OAAO;GACL,OAAO;GACP,qBAAqB,SAAS,kBAAkB;GAChD,YAAY,SAAS,kBAAkB,UAAU,SAAS,WAAW;GACrE,mBAAmB,WAAW;EAChC;CACF;CAEA,OAAO;EAAE,OAAO;EAAO,QAAQ;CAAa;AAC9C;;;;;;;;;;AAWA,eAAsB,gCAAgC,MAG0B;CAC9E,MAAM,EAAE,eAAe,cAAc;CAErC,MAAM,UAAU,MAAM,cAAc,SAAS,eAAe,SAAS;CACrE,IAAI,CAAC,SAAS,OAAO;CACrB,MAAM,oBAAoB,MAAM,cAAc,oBAAoB,IAAI;EACpE,OAAO,QAAQ;EACf,IAAI,QAAQ;CACd,CAAC;CACD,IAAI,CAAC,mBAAmB,OAAO;CAC/B,MAAM,aAAa,MAAM,cAAc,YAAY,IAAI;EAAE,OAAO,QAAQ;EAAO,IAAI,kBAAkB;CAAa,CAAC;CACnH,IAAI,CAAC,YAAY,OAAO;CAExB,OAAO;EAAE,kBAAkB,WAAW;EAAkB,OAAO,QAAQ;EAAO,QAAQ,QAAQ;CAAO;AACvG;;;;;;;;;;;;;;;;AAiBA,eAAsB,2BACpB,MACsC;CACtC,MAAM,EAAE,eAAe,OAAO,kBAAkB,QAAQ,mBAAmB;CAE3E,MAAM,WAAW,MAAM,+BAA+B;EAAE;EAAe;EAAO;EAAkB;CAAe,CAAC;CAChH,IAAI,CAAC,SAAS,OAAO,MAAM,IAAI,oCAAoC,SAAS,MAAM;CAElF,MAAM,SAAS,KAAK,qBAAqB,SAAS;CAClD,MAAM,UAAU,MAAM,cAAc,SAAS,OAAO;EAClD,WAAW,WAAW;EACtB,qBAAqB,SAAS;EAC9B;EACA;EACA;EACA,YAAY,SAAS;EACrB,YAAY;CACd,CAAC;CACD,OAAO;EACL,WAAW,QAAQ;EACnB;EACA,qBAAqB,SAAS;EAC9B,QAAQ,QAAQ;EAChB,YAAY,SAAS;CACvB;AACF;;;;;;;;;AA4BA,eAAsB,sBAAsB,SAAyB,MAAgD;CACnH,IAAI;EACF,MAAM,SACJ,KAAK,kBAAkB,KAAK,mBACxB,MAAM,KAAK,eAAe,IAAI;GAC5B,OAAO,KAAK;GACZ,QAAQ,4BAA4B,KAAK,gBAAgB;EAC3D,CAAC,IACD;EAIN,MAAM,WAAW,KAAK,gBAAgB,MAAM,GAAG,CAAC,CAAC;EAEjD,MAAM,0BAA0B,SAAS,QADf,WAAW,yBAAyB,UAAU,KAAK,cAAc,CAAC,CAAC,UAAU,KAAA,CACrC;CACpE,SAAS,OAAO;EACd,QAAQ,KAAK,iEAAiE,EAC5E,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D,CAAC;CACH;CACA,IAAI,KAAK,gBACP,IAAI;EACF,MAAM,QAAQ,MAAM,OAAO,EAAE,SAAS,KAAK,eAAe,CAAC;CAC7D,SAAS,OAAO;EACd,QAAQ,KAAK,yDAAyD;GACpE,SAAS,KAAK;GACd,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAC9D,CAAC;CACH;AAEJ"}
|
|
1
|
+
{"version":3,"file":"factory-session.js","names":[],"sources":["../../src/session/factory-session.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { resolveProviderOMDefault } from '@mastra/code-sdk/onboarding/packs';\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\n\nimport { factoryMemorySettingsUserId } from '../storage/domains/memory-settings/base.js';\nimport type { MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { applyStoredMemorySettings } from './memory-settings-hydration.js';\nimport { seedSessionOrg } from './org-seed.js';\n\ntype FactorySession = Awaited<ReturnType<AgentController<MastraCodeState>['createSession']>>;\n\n/**\n * Read the factory project's default model. Best-effort: a missing project or an\n * uninitialized storage domain means \"no default\", never a failed run.\n */\nexport async function resolveFactoryDefaultModelId(\n projects: FactoryProjectsStorage | undefined,\n factoryProjectId: string | undefined,\n): Promise<string | undefined> {\n if (!projects || !factoryProjectId) return undefined;\n try {\n const project = await projects.getById({ id: factoryProjectId });\n return project?.defaultModelId ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nexport interface EnsureFactorySourceSessionArgs {\n /**\n * Storage handle of the integration that owns source control. Nothing here is\n * provider-specific: the connection is matched by the handle's own\n * `integrationId`, so GitHub, Slack-on-behalf-of-GitHub, or any future owner\n * all resolve through the same traversal.\n */\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n branch: string;\n /** Pick a specific linked repository by slug. Defaults to the first linked repository. */\n repositorySlug?: string;\n /**\n * Attribute the run to this user instead of the repo connector. Set when the\n * run has an interactive user — e.g. the person who approved a proposed run.\n */\n attributeToUserId?: string;\n}\n\nexport interface EnsuredFactorySourceSession {\n sessionId: string;\n userId: string;\n projectRepositoryId: string;\n branch: string;\n baseBranch: string;\n}\n\nexport class FactorySourceSessionResolutionError extends Error {\n constructor(readonly reason: 'connection' | 'repository') {\n super(\n reason === 'connection'\n ? 'Factory source-control connection not found.'\n : 'Factory source-control repository not found.',\n );\n this.name = 'FactorySourceSessionResolutionError';\n }\n}\n\nexport interface ResolvedFactorySourceRepository {\n projectRepositoryId: string;\n /** The repository's pinned branch, else its default branch. */\n baseBranch: string;\n /** Who connected the repository. The attribution for runs with no interactive user. */\n connectedByUserId: string;\n}\n\n/**\n * Outcome of {@link resolveFactorySourceRepository}. A miss carries which step\n * failed: callers differ on whether that is an error (an autonomous run cannot\n * proceed) or a routine fallback (a chat integration drops to a chat-only\n * session), and the two steps fail for different reasons worth reporting apart.\n */\nexport type FactorySourceRepositoryResult =\n | ({ found: true } & ResolvedFactorySourceRepository)\n | { found: false; reason: 'connection' | 'repository' };\n\n/**\n * Resolve which repository a factory project's source-control runs act on: the\n * owner's connection on the project, then one of its linked repositories.\n *\n * The owner is whichever integration owns source control, matched by the\n * handle's own `integrationId` — nothing here is provider-specific.\n */\nexport async function resolveFactorySourceRepository(args: {\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n /** Pick a specific linked repository by slug. Defaults to the first linked repository. */\n repositorySlug?: string;\n}): Promise<FactorySourceRepositoryResult> {\n const { sourceControl, orgId, factoryProjectId, repositorySlug } = args;\n\n const connections = await sourceControl.connections.list({ orgId, factoryProjectId });\n const candidates = connections.filter(candidate => candidate.integrationId === sourceControl.integrationId);\n if (candidates.length === 0) return { found: false, reason: 'connection' };\n\n // A project can carry stale connections: a provider-app reinstall leaves the\n // old connection pointing at an installation that no longer exists, and that\n // row can sit ahead of the healthy one. Try every candidate and skip the ones\n // that no longer resolve rather than failing on the first.\n for (const connection of candidates) {\n let resolved;\n try {\n const projectRepositories = await sourceControl.projectRepositories.list({ orgId, connectionId: connection.id });\n const resolvedRepositories = await Promise.all(\n projectRepositories.map(async projectRepository => ({\n projectRepository,\n repository: await sourceControl.repositories.get({ orgId, id: projectRepository.repositoryId }),\n })),\n );\n resolved = resolvedRepositories.find(\n candidate => candidate.repository && (!repositorySlug || candidate.repository.slug === repositorySlug),\n );\n } catch {\n // The connection no longer resolves (e.g. its installation was deleted).\n continue;\n }\n if (!resolved?.repository) continue;\n\n return {\n found: true,\n projectRepositoryId: resolved.projectRepository.id,\n baseBranch: resolved.projectRepository.branch ?? resolved.repository.defaultBranch,\n connectedByUserId: connection.createdByUserId,\n };\n }\n\n return { found: false, reason: 'repository' };\n}\n\n/**\n * Walk a Factory user-session id back to the project it belongs to.\n *\n * Repo-backed channel threads are keyed by their Factory session id, which is\n * the only handle a session-start hook gets. This turns that id back into the\n * project whose configuration the session should adopt. Durable by\n * construction — it reads the same rows the session was created from, so it\n * survives restarts without any in-memory mapping.\n */\nexport async function resolveFactoryProjectForSession(args: {\n sourceControl: SourceControlStorageHandle;\n sessionId: string;\n}): Promise<{ factoryProjectId: string; orgId: string; userId: string } | null> {\n const { sourceControl, sessionId } = args;\n\n const session = await sourceControl.sessions.getBySessionId(sessionId);\n if (!session) return null;\n const projectRepository = await sourceControl.projectRepositories.get({\n orgId: session.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) return null;\n const connection = await sourceControl.connections.get({ orgId: session.orgId, id: projectRepository.connectionId });\n if (!connection) return null;\n\n return { factoryProjectId: connection.factoryProjectId, orgId: session.orgId, userId: session.userId };\n}\n\n/**\n * Create the source-control session a repo-backed factory run needs.\n *\n * `FactoryStartCoordinator.prepare` requires this record to already exist —\n * `resolveSourceSession` throws `Factory session not found` otherwise — so every\n * autonomous entry point has to produce one before it can start a run. This is\n * that step, in one place: the owner's connection on the factory project, one of\n * its linked repositories, and a session on the requested branch with the\n * repository's pinned or default branch as the base.\n *\n * The run is attributed to `attributeToUserId` when the caller has an\n * interactive user (e.g. the approver of a proposed run), and otherwise falls\n * back to whoever connected the repository (`connection.createdByUserId`),\n * because a genuinely autonomous run has no interactive user of its own.\n */\nexport async function ensureFactorySourceSession(\n args: EnsureFactorySourceSessionArgs,\n): Promise<EnsuredFactorySourceSession> {\n const { sourceControl, orgId, factoryProjectId, branch, repositorySlug } = args;\n\n const resolved = await resolveFactorySourceRepository({ sourceControl, orgId, factoryProjectId, repositorySlug });\n if (!resolved.found) throw new FactorySourceSessionResolutionError(resolved.reason);\n\n const userId = args.attributeToUserId ?? resolved.connectedByUserId;\n const session = await sourceControl.sessions.create({\n sessionId: randomUUID(),\n projectRepositoryId: resolved.projectRepositoryId,\n orgId,\n userId,\n branch,\n baseBranch: resolved.baseBranch,\n visibility: 'org',\n });\n return {\n sessionId: session.sessionId,\n userId,\n projectRepositoryId: resolved.projectRepositoryId,\n branch: session.branch,\n baseBranch: resolved.baseBranch,\n };\n}\n\nexport interface HydrateFactorySessionArgs {\n orgId: string;\n /**\n * The factory project whose shared memory settings apply. Factory sessions\n * never read an individual user's personal memory settings — the project's\n * own row (or the built-in defaults) is what they run with.\n */\n factoryProjectId?: string;\n /** The factory project's default model. Without it the session keeps the SDK's built-in mode default. */\n defaultModelId?: string;\n /**\n * When provided, the factory project's stored memory-settings row is\n * applied. When omitted (or no row exists) the session is reset to the\n * built-in memory defaults.\n */\n memorySettings?: MemorySettingsStorage;\n}\n\n/**\n * Apply a factory project's configuration to a freshly created session:\n * observational-memory settings, then the project's default model.\n *\n * Both steps are best-effort. A retired model id or an unreachable settings row\n * must not sink a run that is otherwise ready — the session simply keeps the\n * default it was created with, and the reason is logged.\n */\nexport async function hydrateFactorySession(session: FactorySession, args: HydrateFactorySessionArgs): Promise<void> {\n // The org rung knowledge capture scopes on. Seeded first so it lands even if\n // a later best-effort step fails; an empty org marks the session unresolved.\n await seedSessionOrg(session, args.orgId);\n try {\n const record =\n args.memorySettings && args.factoryProjectId\n ? await args.memorySettings.get({\n orgId: args.orgId,\n userId: factoryMemorySettingsUserId(args.factoryProjectId),\n })\n : null;\n // Without a stored row, fall back to the low-cost OM model of the factory\n // default model's provider — a factory connected only to Anthropic should\n // not observe with the (uncredentialed) built-in Google default.\n const provider = args.defaultModelId?.split('/')[0];\n const fallbackOmModelId = provider ? resolveProviderOMDefault(provider, args.defaultModelId).modelId : undefined;\n await applyStoredMemorySettings(session, record, fallbackOmModelId);\n } catch (error) {\n console.warn('[Factory Start] Failed to apply observational-memory settings', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n if (args.defaultModelId) {\n try {\n await session.model.switch({ modelId: args.defaultModelId });\n } catch (error) {\n console.warn('[Factory Start] Failed to apply factory default model', {\n modelId: args.defaultModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -21,6 +21,7 @@ interface OMStateWrites {
|
|
|
21
21
|
observationThreshold?: number;
|
|
22
22
|
reflectionThreshold?: number;
|
|
23
23
|
observeAttachments?: 'auto' | boolean;
|
|
24
|
+
factoryOrgId?: string;
|
|
24
25
|
}
|
|
25
26
|
/** The slice of a session needed to apply stored observational-memory settings. */
|
|
26
27
|
export interface OMConfigurableSession {
|
|
@@ -56,15 +57,27 @@ export interface MemorySettingsHydrationDependencies {
|
|
|
56
57
|
memorySettings: Pick<MemorySettingsStorage, 'get'>;
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
|
-
* Seed a freshly created controller session's
|
|
60
|
-
* from the owner's
|
|
61
|
-
* session-created listener so the seed lands before the caller can
|
|
60
|
+
* Seed a freshly created controller session's tenant org and its
|
|
61
|
+
* observational-memory settings from the owner's source-control row. Registered
|
|
62
|
+
* as a blocking session-created listener so the seed lands before the caller can
|
|
63
|
+
* start a run.
|
|
62
64
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* The org seed matters beyond settings. Subconscious knowledge capture scopes
|
|
66
|
+
* every node and record on `factoryOrgId`; before the SDK refusal guard,
|
|
67
|
+
* missing it made capture substitute the session owner id. For web chat sessions
|
|
68
|
+
* that is the agent controller's own id rather than a tenant, so captured
|
|
69
|
+
* knowledge landed under an org rung no reader ever queries. Same rule as the
|
|
70
|
+
* start coordinator: the org
|
|
71
|
+
* comes from the row the session was created from, never improvised from an
|
|
72
|
+
* owner id.
|
|
73
|
+
*
|
|
74
|
+
* Memory settings for sessions tagged `factoryProjectId` (work/review runs) are
|
|
75
|
+
* owned by the start coordinator, and sessions without a GitHub source-control
|
|
76
|
+
* row (e.g. chat-only channel sessions) hydrate through `hydrateFactorySession`
|
|
77
|
+
* with their own resolved tenant; both are skipped here. The org seed is not
|
|
78
|
+
* skipped on the tag alone: a web chat session persists `factoryProjectId` from
|
|
79
|
+
* its browser seed, so on resume it carries the tag without ever having been
|
|
80
|
+
* through the coordinator. Best-effort: failures are logged, never thrown.
|
|
68
81
|
*/
|
|
69
82
|
export declare function hydrateSessionMemorySettings(session: MemorySettingsHydrationSession, { sourceControl, memorySettings }: MemorySettingsHydrationDependencies): Promise<void>;
|
|
70
83
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-settings-hydration.d.ts","sourceRoot":"","sources":["../../src/session/memory-settings-hydration.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAC9G,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"memory-settings-hydration.d.ts","sourceRoot":"","sources":["../../src/session/memory-settings-hydration.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAC9G,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAG5F,yDAAyD;AACzD,eAAO,MAAM,6BAA6B,QAAS,CAAC;AACpD,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,2DAA2D;AAC3D,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,WAAW,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D;AAED;;;;GAIG;AACH,UAAU,aAAa;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,CAAC,GAAG,EAAE,mBAAmB,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAAC;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,mFAAmF;AACnF,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE;QAAE,QAAQ,EAAE,WAAW,CAAC;QAAC,SAAS,EAAE,WAAW,CAAA;KAAE,CAAC;IACtD,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;QAC/C,GAAG,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KACvD,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,oBAAoB,GAAG,IAAI,EACnC,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED,MAAM,WAAW,8BAA+B,SAAQ,qBAAqB;IAC3E,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;CAChD;AAED,MAAM,WAAW,mCAAmC;IAClD,4FAA4F;IAC5F,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;KAC1E,CAAC;IACF,cAAc,EAAE,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,8BAA8B,EACvC,EAAE,aAAa,EAAE,cAAc,EAAE,EAAE,mCAAmC,GACrE,OAAO,CAAC,IAAI,CAAC,CAsBf"}
|