@mastra/factory 0.19.0-alpha.1 → 0.19.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/integrations/github/session-subscriptions.d.ts +1 -7
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +19 -18
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.d.ts +13 -1
- package/dist/integrations/github/token-refresh.d.ts.map +1 -1
- package/dist/integrations/github/token-refresh.js +21 -3
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +2 -0
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/sandbox/materialization.d.ts +2 -1
- package/dist/sandbox/materialization.d.ts.map +1 -1
- package/dist/sandbox/materialization.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +35 -10
- package/dist/workspace.js.map +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -468,6 +468,12 @@ Reconciliation re-applies label-derived placement. The issue sweep replays an op
|
|
|
468
468
|
|
|
469
469
|
A delivery that concerns two cards is evaluated once per card, each under its own ingress identity: every decision is committed against one card, at that card's revision. A merged pull request is the standard case — its Review card closes and the Work item that wrote the code assesses whether it is finished. An opening pull request is evaluated the same way. Its own Review card is filed by the arrival, the evaluation flagged `pullRequestIntake`, which is committed against the Work item that authored the pull request when provenance or a matching session branch names one; that binding is what links the new card to its item. The authoring item is then answered in a second evaluation of its own (`pullRequestIntake` unset), which is where a handler places the item that is now out for review. The built-in `pullRequestOpened` files the card only on the arrival and returns nothing for the authoring item.
|
|
470
470
|
|
|
471
|
+
### GitHub CLI authentication in Factory sessions
|
|
472
|
+
|
|
473
|
+
Factory offers `github_refresh_token` to sessions backed by an authorized GitHub repository, including Slack sessions that have no repository ID in controller state. The tool is visible before the sandbox starts, but it only works once the sandbox is running. Chat-only and GitLab-backed sessions do not get the tool.
|
|
474
|
+
|
|
475
|
+
If a sandbox `gh` command fails authentication, run `github_refresh_token` and retry the failed command. It reloads the organization's stored GitHub credential into that sandbox (or obtains repository access when no personal access token is configured); it does not rotate or renew an expired or revoked personal access token. Replace an invalid token in Factory's GitHub integration settings before retrying. The tool does not refresh GitLab credentials or return the GitHub token.
|
|
476
|
+
|
|
471
477
|
### GitLab intake and source control
|
|
472
478
|
|
|
473
479
|
Direct deployments can use either a GitLab Personal Access Token or Group Access Token. Both authenticate the GitLab API and Git-over-HTTPS in the same way; the difference is reach: a personal token follows the user's accessible projects, while a group token is limited to its group and subgroups. Configure the token with `api` and `write_repository` scopes so Factory can manage issues and merge requests, clone repositories, and push session branches.
|
|
@@ -7,13 +7,7 @@ export declare function upsertFactoryTriageComment(requestContext: RequestContex
|
|
|
7
7
|
body: string;
|
|
8
8
|
}, github: GithubIntegration): Promise<import("./integration.js").GithubTriageCommentUpsertResult>;
|
|
9
9
|
export declare function refreshGithubToken(requestContext: RequestContext, github: GithubIntegration): Promise<void>;
|
|
10
|
-
export declare function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration): {
|
|
11
|
-
github_refresh_token?: undefined;
|
|
12
|
-
github_upsert_factory_triage_comment?: undefined;
|
|
13
|
-
github_subscribe_pr?: undefined;
|
|
14
|
-
github_unsubscribe_pr?: undefined;
|
|
15
|
-
} | {
|
|
16
|
-
github_refresh_token: import("@mastra/core/tools").Tool<Record<string, never>, unknown, unknown, unknown, import("@mastra/core/tools").ToolExecutionContext<unknown, unknown, unknown>, "github_refresh_token", unknown>;
|
|
10
|
+
export declare function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration): Record<string, import("@mastra/code-sdk/agents/tools").ToolLike | undefined> | {
|
|
17
11
|
github_upsert_factory_triage_comment: import("@mastra/core/tools").Tool<{
|
|
18
12
|
issueNumber: number;
|
|
19
13
|
body: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-subscriptions.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/session-subscriptions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"session-subscriptions.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/session-subscriptions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAYnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAyI1D,wBAAsB,oCAAoC,CACxD,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,MAAM,GAAG,MAAM,EAC5B,MAAM,EAAE,mBAAmB,GAAG,eAAe,EAC7C,MAAM,EAAE,iBAAiB,+BAY1B;AAED,wBAAsB,wCAAwC,CAC5D,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,MAAM,GAAG,MAAM,EAC5B,MAAM,EAAE,iBAAiB,mBAM1B;AAED,wBAAsB,0BAA0B,CAC9C,cAAc,EAAE,cAAc,EAC9B,KAAK,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC5C,MAAM,EAAE,iBAAiB,uEAa1B;AAED,wBAAsB,kBAAkB,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBjH;AAED,wBAAgB,6BAA6B,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB;;;;;;;;;;;EA8CtG;AAID,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,sBAgBA"}
|
|
@@ -2,7 +2,7 @@ import { getFactoryAuthOrgId, getFactoryAuthUserFromContext, getFactoryAuthUserI
|
|
|
2
2
|
import { runsPullRequestCreate } from "../../session/shell-commands.js";
|
|
3
3
|
import { getGithubPat } from "./pat.js";
|
|
4
4
|
import { subscribeToPullRequest, unsubscribeFromPullRequest } from "./subscriptions.js";
|
|
5
|
-
import { getRegisteredGithubPatKind,
|
|
5
|
+
import { getGithubRefreshTarget, getRegisteredGithubPatKind, requireGithubTokenInjector } from "./token-refresh.js";
|
|
6
6
|
import { createTool } from "@mastra/core/tools";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
//#region src/integrations/github/session-subscriptions.ts
|
|
@@ -150,31 +150,32 @@ async function upsertFactoryTriageComment(requestContext, input, github) {
|
|
|
150
150
|
}));
|
|
151
151
|
}
|
|
152
152
|
async function refreshGithubToken(requestContext, github) {
|
|
153
|
-
const
|
|
153
|
+
const inject = requireGithubTokenInjector(requestContext);
|
|
154
|
+
const target = getGithubRefreshTarget(requestContext);
|
|
155
|
+
if (!target) throw new Error("The active session is not backed by a GitHub workspace.");
|
|
154
156
|
const pat = await getGithubPat(() => github.integrationStorage, target.orgId, getRegisteredGithubPatKind(requestContext));
|
|
155
157
|
if (pat) {
|
|
156
|
-
|
|
158
|
+
inject(pat);
|
|
157
159
|
return;
|
|
158
160
|
}
|
|
159
|
-
const token = (await github.versionControl.getRepositoryAccess(
|
|
160
|
-
orgId: target.orgId,
|
|
161
|
-
repositoryId: target.repository.id
|
|
162
|
-
})).authorization?.token;
|
|
161
|
+
const token = (await github.versionControl.getRepositoryAccess(target)).authorization?.token;
|
|
163
162
|
if (!token) throw new Error("Repository access did not include a bearer token for the Factory session.");
|
|
164
|
-
|
|
163
|
+
inject(token);
|
|
165
164
|
}
|
|
166
165
|
function createGithubSubscriptionTools(requestContext, github) {
|
|
167
|
-
|
|
166
|
+
const tools = {};
|
|
167
|
+
if (getGithubRefreshTarget(requestContext)) tools.github_refresh_token = createTool({
|
|
168
|
+
id: "github_refresh_token",
|
|
169
|
+
description: "Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.",
|
|
170
|
+
inputSchema: z.object({}),
|
|
171
|
+
execute: async () => {
|
|
172
|
+
await refreshGithubToken(requestContext, github);
|
|
173
|
+
return { refreshed: true };
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
if (!isGithubProjectSession(requestContext)) return tools;
|
|
168
177
|
return {
|
|
169
|
-
|
|
170
|
-
id: "github_refresh_token",
|
|
171
|
-
description: "Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.",
|
|
172
|
-
inputSchema: z.object({}),
|
|
173
|
-
execute: async () => {
|
|
174
|
-
await refreshGithubToken(requestContext, github);
|
|
175
|
-
return { refreshed: true };
|
|
176
|
-
}
|
|
177
|
-
}),
|
|
178
|
+
...tools,
|
|
178
179
|
github_upsert_factory_triage_comment: createTool({
|
|
179
180
|
id: "github_upsert_factory_triage_comment",
|
|
180
181
|
description: "Create or update this Factory App’s canonical triage handoff comment on an issue in the active repository. Use this for every marked pending or final Factory triage handoff; never use gh to create or edit that handoff.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-subscriptions.js","names":[],"sources":["../../../src/integrations/github/session-subscriptions.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\nimport { getFactoryAuthOrgId, getFactoryAuthUserFromContext, getFactoryAuthUserId } from '../../auth.js';\nimport { runsPullRequestCreate } from '../../session/shell-commands.js';\nimport type {\n ProjectRepository,\n ProjectSourceControlConnection,\n SourceControlInstallation,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { getGithubPat } from './pat.js';\nimport { subscribeToPullRequest, unsubscribeFromPullRequest } from './subscriptions.js';\nimport { getRegisteredGithubPatKind, injectGithubToken } from './token-refresh.js';\n\ntype RepositorySessionState = { factoryProjectId?: string; projectRepositoryId?: string };\n\n/**\n * The host-authenticated user placed on the request context under the `user`\n * key, read through the host's own normalizer. A local mirror of that shape\n * used to live here; it silently missed the provider shapes the normalizer\n * knows about, which turned every subscription tool into a no-op for those\n * users instead of an error anybody could see.\n */\nfunction sessionUserId(requestContext: RequestContext): string | undefined {\n return getFactoryAuthUserId(getFactoryAuthUserFromContext(requestContext));\n}\n\nfunction sessionOrgId(requestContext: RequestContext): string | undefined {\n return getFactoryAuthOrgId(getFactoryAuthUserFromContext(requestContext));\n}\n\nconst pullRequestInputSchema = z.object({\n pullRequest: z.union([z.number().int().positive(), z.string().min(1)]),\n});\n\nconst TRIAGE_COMMENT_MARKER = '<!-- mastra-factory-triage -->';\nconst triageCommentInputSchema = z.object({\n issueNumber: z.number().int().positive(),\n body: z.string().startsWith(TRIAGE_COMMENT_MARKER),\n});\n\nconst triageCommentLocks = new Map<string, Promise<void>>();\n\nasync function serializeTriageComment<T>(key: string, operation: () => Promise<T>): Promise<T> {\n const previous = triageCommentLocks.get(key) ?? Promise.resolve();\n let release: (() => void) | undefined;\n const current = new Promise<void>(resolve => {\n release = resolve;\n });\n const queued = previous.then(() => current);\n triageCommentLocks.set(key, queued);\n await previous;\n try {\n return await operation();\n } finally {\n release!();\n if (triageCommentLocks.get(key) === queued) triageCommentLocks.delete(key);\n }\n}\n\ninterface SessionTarget {\n context: AgentControllerRequestContext<RepositorySessionState>;\n projectRepository: ProjectRepository;\n connection: ProjectSourceControlConnection;\n installation: SourceControlInstallation;\n repository: SourceControlRepository;\n orgId: string;\n userId: string;\n}\n\nfunction parsePullRequest(value: number | string, expectedRepo: string): number {\n if (typeof value === 'number') return value;\n if (/^\\d+$/.test(value)) return Number(value);\n const match = value.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match || match[1]!.toLowerCase() !== expectedRepo.toLowerCase()) {\n throw new Error(`Pull request must belong to ${expectedRepo}.`);\n }\n return Number(match[2]);\n}\n\n/**\n * Whether the current request comes from a session that GitHub subscriptions\n * can ever apply to: an authenticated org user on a GitHub-project session\n * with an active thread. Mirrors the gate in `resolveSessionTarget` without\n * throwing, for passive callers that should no-op instead of erroring.\n */\nfunction isGithubProjectSession(requestContext: RequestContext): boolean {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n return Boolean(\n context?.threadId &&\n context.getState().projectRepositoryId &&\n sessionOrgId(requestContext) &&\n sessionUserId(requestContext),\n );\n}\n\nasync function resolveSessionTarget(requestContext: RequestContext, github: GithubIntegration): Promise<SessionTarget> {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const orgId = sessionOrgId(requestContext);\n const userId = sessionUserId(requestContext);\n const projectRepositoryId = context?.getState().projectRepositoryId;\n if (!context || !context.threadId || !projectRepositoryId || !orgId || !userId) {\n throw new Error('GitHub subscriptions require an authenticated repository session with an active thread.');\n }\n\n const projectRepository = await github.sourceControlStorage.projectRepositories.get({\n orgId,\n id: projectRepositoryId,\n });\n if (!projectRepository) throw new Error('Project repository not found for this organization.');\n const connection = await github.sourceControlStorage.connections.get({ orgId, id: projectRepository.connectionId });\n if (!connection) throw new Error('Source-control connection not found for this organization.');\n const repository = await github.sourceControlStorage.repositories.get({ orgId, id: projectRepository.repositoryId });\n if (!repository) throw new Error('Repository not found for this organization.');\n const installation = await github.sourceControlStorage.installations.get({ orgId, id: connection.installationId });\n if (!installation) throw new Error('Source-control installation not found for this organization.');\n return { context, projectRepository, connection, installation, repository, orgId, userId };\n}\n\nasync function verifyPullRequest(target: SessionTarget, pullRequest: number, github: GithubIntegration) {\n const [owner, repo] = target.repository.slug.split('/');\n if (!owner || !repo) throw new Error('GitHub repository is invalid.');\n const octokit = github.getInstallationOctokit(Number(target.installation.externalId));\n const { data } = await octokit.pulls.get({ owner, repo, pull_number: pullRequest });\n if (String(data.base.repo.id) !== target.repository.externalId)\n throw new Error('Pull request repository does not match the active project repository.');\n}\n\nasync function subscriptionInput(target: SessionTarget, pullRequestNumber: number) {\n return {\n orgId: target.orgId,\n installationExternalId: target.installation.externalId,\n projectRepositoryId: target.projectRepository.id,\n repositoryExternalId: target.repository.externalId,\n repositorySlug: target.repository.slug,\n changeRequestId: String(pullRequestNumber),\n sessionId: target.context.session.id,\n ownerId: target.context.session.ownerId,\n resourceId: target.connection.factoryProjectId,\n threadId: target.context.threadId!,\n sessionScope: target.context.scope,\n source: 'explicit-tool' as const,\n subscribedByUserId: target.userId,\n };\n}\n\nexport async function subscribeCurrentSessionToPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n source: 'auto-gh-pr-create' | 'explicit-tool',\n github: GithubIntegration,\n) {\n // The auto path observes every successful `gh pr create` in every session,\n // including local and non-GitHub-project sessions where subscriptions can\n // never apply. Skip silently there; only the explicit tool should surface\n // \"this session cannot subscribe\" as an error.\n if (source === 'auto-gh-pr-create' && !isGithubProjectSession(requestContext)) return undefined;\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await verifyPullRequest(target, number, github);\n await subscribeToPullRequest({ ...(await subscriptionInput(target, number)), source }, github.integrationStorage);\n return number;\n}\n\nexport async function unsubscribeCurrentSessionFromPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);\n return number;\n}\n\nexport async function upsertFactoryTriageComment(\n requestContext: RequestContext,\n input: { issueNumber: number; body: string },\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const installationId = Number(target.installation.externalId);\n if (!Number.isSafeInteger(installationId) || installationId <= 0) throw new Error('GitHub installation is invalid.');\n return serializeTriageComment(`${installationId}:${target.repository.externalId}:${input.issueNumber}`, () =>\n github.upsertFactoryTriageComment({\n installationId,\n repository: target.repository.slug,\n issueNumber: input.issueNumber,\n body: input.body,\n }),\n );\n}\n\nexport async function refreshGithubToken(requestContext: RequestContext, github: GithubIntegration): Promise<void> {\n const target = await resolveSessionTarget(requestContext, github);\n // `GH_TOKEN` feeds the `gh` CLI, so a configured org PAT wins over a minted\n // installation token (which 403s on integration-restricted endpoints). The\n // workspace records which PAT kind the sandbox was provisioned with, so a\n // review-board sandbox keeps its reviewer token on refresh.\n const pat = await getGithubPat(\n () => github.integrationStorage,\n target.orgId,\n getRegisteredGithubPatKind(requestContext),\n );\n if (pat) {\n injectGithubToken(requestContext, pat);\n return;\n }\n const access = await github.versionControl.getRepositoryAccess({\n orgId: target.orgId,\n repositoryId: target.repository.id,\n });\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session.');\n injectGithubToken(requestContext, token);\n}\n\nexport function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration) {\n if (!isGithubProjectSession(requestContext)) return {};\n\n return {\n github_refresh_token: createTool({\n id: 'github_refresh_token',\n description:\n 'Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.',\n inputSchema: z.object({}),\n execute: async () => {\n await refreshGithubToken(requestContext, github);\n return { refreshed: true };\n },\n }),\n github_upsert_factory_triage_comment: createTool({\n id: 'github_upsert_factory_triage_comment',\n description:\n 'Create or update this Factory App’s canonical triage handoff comment on an issue in the active repository. Use this for every marked pending or final Factory triage handoff; never use gh to create or edit that handoff.',\n inputSchema: triageCommentInputSchema,\n execute: async input => upsertFactoryTriageComment(requestContext, input, github),\n }),\n github_subscribe_pr: createTool({\n id: 'github_subscribe_pr',\n description:\n 'Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, 'explicit-tool', github);\n return { subscribed: true, pullRequestNumber: number };\n },\n }),\n github_unsubscribe_pr: createTool({\n id: 'github_unsubscribe_pr',\n description:\n 'Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);\n return { subscribed: false, pullRequestNumber: number };\n },\n }),\n };\n}\n\nconst GITHUB_PULL_REQUEST_URL = /^https:\\/\\/github\\.com\\/[^\\s/]+\\/[^\\s/]+\\/pull\\/\\d+\\/?$/;\n\nexport function parseCreatedPullRequest(context: {\n toolName: string;\n input: unknown;\n output?: unknown;\n error?: unknown;\n}) {\n if (context.error) return undefined;\n // The provider-neutral change-request tool reports the created PR directly;\n // subscribe on it exactly as on a successful `gh pr create`.\n if (context.toolName === 'source_control_create_change_request') {\n const url = (context.output as { url?: unknown } | undefined)?.url;\n return typeof url === 'string' && GITHUB_PULL_REQUEST_URL.test(url) ? url.replace(/\\/$/, '') : undefined;\n }\n if (context.toolName !== 'execute_command') return undefined;\n const command = (context.input as { command?: unknown } | undefined)?.command;\n if (typeof command !== 'string' || !runsPullRequestCreate(command)) return undefined;\n const output = context.output as { stdout?: unknown; result?: unknown } | undefined;\n const stdout = typeof context.output === 'string' ? context.output : (output?.stdout ?? output?.result);\n if (typeof stdout !== 'string') return undefined;\n const urls = stdout.match(/https:\\/\\/github\\.com\\/[^\\s/]+\\/[^\\s/]+\\/pull\\/\\d+/g) ?? [];\n return urls.length === 1 ? urls[0] : undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-subscriptions.js","names":[],"sources":["../../../src/integrations/github/session-subscriptions.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\nimport { getFactoryAuthOrgId, getFactoryAuthUserFromContext, getFactoryAuthUserId } from '../../auth.js';\nimport { runsPullRequestCreate } from '../../session/shell-commands.js';\nimport type {\n ProjectRepository,\n ProjectSourceControlConnection,\n SourceControlInstallation,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { IntegrationTools } from '../base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { getGithubPat } from './pat.js';\nimport { subscribeToPullRequest, unsubscribeFromPullRequest } from './subscriptions.js';\nimport { getGithubRefreshTarget, getRegisteredGithubPatKind, requireGithubTokenInjector } from './token-refresh.js';\n\ntype RepositorySessionState = { factoryProjectId?: string; projectRepositoryId?: string };\n\n/**\n * The host-authenticated user placed on the request context under the `user`\n * key, read through the host's own normalizer. A local mirror of that shape\n * used to live here; it silently missed the provider shapes the normalizer\n * knows about, which turned every subscription tool into a no-op for those\n * users instead of an error anybody could see.\n */\nfunction sessionUserId(requestContext: RequestContext): string | undefined {\n return getFactoryAuthUserId(getFactoryAuthUserFromContext(requestContext));\n}\n\nfunction sessionOrgId(requestContext: RequestContext): string | undefined {\n return getFactoryAuthOrgId(getFactoryAuthUserFromContext(requestContext));\n}\n\nconst pullRequestInputSchema = z.object({\n pullRequest: z.union([z.number().int().positive(), z.string().min(1)]),\n});\n\nconst TRIAGE_COMMENT_MARKER = '<!-- mastra-factory-triage -->';\nconst triageCommentInputSchema = z.object({\n issueNumber: z.number().int().positive(),\n body: z.string().startsWith(TRIAGE_COMMENT_MARKER),\n});\n\nconst triageCommentLocks = new Map<string, Promise<void>>();\n\nasync function serializeTriageComment<T>(key: string, operation: () => Promise<T>): Promise<T> {\n const previous = triageCommentLocks.get(key) ?? Promise.resolve();\n let release: (() => void) | undefined;\n const current = new Promise<void>(resolve => {\n release = resolve;\n });\n const queued = previous.then(() => current);\n triageCommentLocks.set(key, queued);\n await previous;\n try {\n return await operation();\n } finally {\n release!();\n if (triageCommentLocks.get(key) === queued) triageCommentLocks.delete(key);\n }\n}\n\ninterface SessionTarget {\n context: AgentControllerRequestContext<RepositorySessionState>;\n projectRepository: ProjectRepository;\n connection: ProjectSourceControlConnection;\n installation: SourceControlInstallation;\n repository: SourceControlRepository;\n orgId: string;\n userId: string;\n}\n\nfunction parsePullRequest(value: number | string, expectedRepo: string): number {\n if (typeof value === 'number') return value;\n if (/^\\d+$/.test(value)) return Number(value);\n const match = value.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match || match[1]!.toLowerCase() !== expectedRepo.toLowerCase()) {\n throw new Error(`Pull request must belong to ${expectedRepo}.`);\n }\n return Number(match[2]);\n}\n\n/**\n * Whether the current request comes from a session that GitHub subscriptions\n * can ever apply to: an authenticated org user on a GitHub-project session\n * with an active thread. Mirrors the gate in `resolveSessionTarget` without\n * throwing, for passive callers that should no-op instead of erroring.\n */\nfunction isGithubProjectSession(requestContext: RequestContext): boolean {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n return Boolean(\n context?.threadId &&\n context.getState().projectRepositoryId &&\n sessionOrgId(requestContext) &&\n sessionUserId(requestContext),\n );\n}\n\nasync function resolveSessionTarget(requestContext: RequestContext, github: GithubIntegration): Promise<SessionTarget> {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const orgId = sessionOrgId(requestContext);\n const userId = sessionUserId(requestContext);\n const projectRepositoryId = context?.getState().projectRepositoryId;\n if (!context || !context.threadId || !projectRepositoryId || !orgId || !userId) {\n throw new Error('GitHub subscriptions require an authenticated repository session with an active thread.');\n }\n\n const projectRepository = await github.sourceControlStorage.projectRepositories.get({\n orgId,\n id: projectRepositoryId,\n });\n if (!projectRepository) throw new Error('Project repository not found for this organization.');\n const connection = await github.sourceControlStorage.connections.get({ orgId, id: projectRepository.connectionId });\n if (!connection) throw new Error('Source-control connection not found for this organization.');\n const repository = await github.sourceControlStorage.repositories.get({ orgId, id: projectRepository.repositoryId });\n if (!repository) throw new Error('Repository not found for this organization.');\n const installation = await github.sourceControlStorage.installations.get({ orgId, id: connection.installationId });\n if (!installation) throw new Error('Source-control installation not found for this organization.');\n return { context, projectRepository, connection, installation, repository, orgId, userId };\n}\n\nasync function verifyPullRequest(target: SessionTarget, pullRequest: number, github: GithubIntegration) {\n const [owner, repo] = target.repository.slug.split('/');\n if (!owner || !repo) throw new Error('GitHub repository is invalid.');\n const octokit = github.getInstallationOctokit(Number(target.installation.externalId));\n const { data } = await octokit.pulls.get({ owner, repo, pull_number: pullRequest });\n if (String(data.base.repo.id) !== target.repository.externalId)\n throw new Error('Pull request repository does not match the active project repository.');\n}\n\nasync function subscriptionInput(target: SessionTarget, pullRequestNumber: number) {\n return {\n orgId: target.orgId,\n installationExternalId: target.installation.externalId,\n projectRepositoryId: target.projectRepository.id,\n repositoryExternalId: target.repository.externalId,\n repositorySlug: target.repository.slug,\n changeRequestId: String(pullRequestNumber),\n sessionId: target.context.session.id,\n ownerId: target.context.session.ownerId,\n resourceId: target.connection.factoryProjectId,\n threadId: target.context.threadId!,\n sessionScope: target.context.scope,\n source: 'explicit-tool' as const,\n subscribedByUserId: target.userId,\n };\n}\n\nexport async function subscribeCurrentSessionToPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n source: 'auto-gh-pr-create' | 'explicit-tool',\n github: GithubIntegration,\n) {\n // The auto path observes every successful `gh pr create` in every session,\n // including local and non-GitHub-project sessions where subscriptions can\n // never apply. Skip silently there; only the explicit tool should surface\n // \"this session cannot subscribe\" as an error.\n if (source === 'auto-gh-pr-create' && !isGithubProjectSession(requestContext)) return undefined;\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await verifyPullRequest(target, number, github);\n await subscribeToPullRequest({ ...(await subscriptionInput(target, number)), source }, github.integrationStorage);\n return number;\n}\n\nexport async function unsubscribeCurrentSessionFromPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);\n return number;\n}\n\nexport async function upsertFactoryTriageComment(\n requestContext: RequestContext,\n input: { issueNumber: number; body: string },\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const installationId = Number(target.installation.externalId);\n if (!Number.isSafeInteger(installationId) || installationId <= 0) throw new Error('GitHub installation is invalid.');\n return serializeTriageComment(`${installationId}:${target.repository.externalId}:${input.issueNumber}`, () =>\n github.upsertFactoryTriageComment({\n installationId,\n repository: target.repository.slug,\n issueNumber: input.issueNumber,\n body: input.body,\n }),\n );\n}\n\nexport async function refreshGithubToken(requestContext: RequestContext, github: GithubIntegration): Promise<void> {\n const inject = requireGithubTokenInjector(requestContext);\n // The workspace resolver records the target only after authorizing the\n // caller against the GitHub-backed session that owns this sandbox.\n const target = getGithubRefreshTarget(requestContext);\n if (!target) throw new Error('The active session is not backed by a GitHub workspace.');\n // `GH_TOKEN` feeds the `gh` CLI, so a configured org PAT wins over a minted\n // installation token (which 403s on integration-restricted endpoints). The\n // workspace records which PAT kind the sandbox was provisioned with, so a\n // review-board sandbox keeps its reviewer token on refresh.\n const pat = await getGithubPat(\n () => github.integrationStorage,\n target.orgId,\n getRegisteredGithubPatKind(requestContext),\n );\n if (pat) {\n inject(pat);\n return;\n }\n const access = await github.versionControl.getRepositoryAccess(target);\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session.');\n inject(token);\n}\n\nexport function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration) {\n const tools: IntegrationTools = {};\n if (getGithubRefreshTarget(requestContext)) {\n tools.github_refresh_token = createTool({\n id: 'github_refresh_token',\n description:\n 'Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.',\n inputSchema: z.object({}),\n execute: async () => {\n await refreshGithubToken(requestContext, github);\n return { refreshed: true };\n },\n });\n }\n if (!isGithubProjectSession(requestContext)) return tools;\n\n return {\n ...tools,\n github_upsert_factory_triage_comment: createTool({\n id: 'github_upsert_factory_triage_comment',\n description:\n 'Create or update this Factory App’s canonical triage handoff comment on an issue in the active repository. Use this for every marked pending or final Factory triage handoff; never use gh to create or edit that handoff.',\n inputSchema: triageCommentInputSchema,\n execute: async input => upsertFactoryTriageComment(requestContext, input, github),\n }),\n github_subscribe_pr: createTool({\n id: 'github_subscribe_pr',\n description:\n 'Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, 'explicit-tool', github);\n return { subscribed: true, pullRequestNumber: number };\n },\n }),\n github_unsubscribe_pr: createTool({\n id: 'github_unsubscribe_pr',\n description:\n 'Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);\n return { subscribed: false, pullRequestNumber: number };\n },\n }),\n };\n}\n\nconst GITHUB_PULL_REQUEST_URL = /^https:\\/\\/github\\.com\\/[^\\s/]+\\/[^\\s/]+\\/pull\\/\\d+\\/?$/;\n\nexport function parseCreatedPullRequest(context: {\n toolName: string;\n input: unknown;\n output?: unknown;\n error?: unknown;\n}) {\n if (context.error) return undefined;\n // The provider-neutral change-request tool reports the created PR directly;\n // subscribe on it exactly as on a successful `gh pr create`.\n if (context.toolName === 'source_control_create_change_request') {\n const url = (context.output as { url?: unknown } | undefined)?.url;\n return typeof url === 'string' && GITHUB_PULL_REQUEST_URL.test(url) ? url.replace(/\\/$/, '') : undefined;\n }\n if (context.toolName !== 'execute_command') return undefined;\n const command = (context.input as { command?: unknown } | undefined)?.command;\n if (typeof command !== 'string' || !runsPullRequestCreate(command)) return undefined;\n const output = context.output as { stdout?: unknown; result?: unknown } | undefined;\n const stdout = typeof context.output === 'string' ? context.output : (output?.stdout ?? output?.result);\n if (typeof stdout !== 'string') return undefined;\n const urls = stdout.match(/https:\\/\\/github\\.com\\/[^\\s/]+\\/[^\\s/]+\\/pull\\/\\d+/g) ?? [];\n return urls.length === 1 ? urls[0] : undefined;\n}\n"],"mappings":"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"}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import type { RequestContext } from '@mastra/core/request-context';
|
|
2
2
|
import type { GithubPatKind } from './pat.js';
|
|
3
3
|
type GithubTokenInjector = (token: string) => void;
|
|
4
|
+
type GithubTokenInjectorResolver = () => GithubTokenInjector;
|
|
5
|
+
/** The authorized GitHub repository behind the request's Factory session. */
|
|
6
|
+
export interface GithubRefreshTarget {
|
|
7
|
+
orgId: string;
|
|
8
|
+
repositoryId: string;
|
|
9
|
+
}
|
|
10
|
+
/** Recorded by the workspace resolver once it has authorized a GitHub-backed
|
|
11
|
+
* session, before the sandbox starts, so the refresh tool is offered only to
|
|
12
|
+
* sessions it can serve. */
|
|
13
|
+
export declare function registerGithubRefreshTarget(requestContext: RequestContext, target: GithubRefreshTarget): void;
|
|
14
|
+
export declare function getGithubRefreshTarget(requestContext: RequestContext): GithubRefreshTarget | undefined;
|
|
4
15
|
export declare function registerGithubTokenInjector(requestContext: RequestContext, injector: GithubTokenInjector): void;
|
|
16
|
+
export declare function registerGithubTokenInjectorResolver(requestContext: RequestContext, resolver: GithubTokenInjectorResolver): void;
|
|
5
17
|
/** Record which PAT kind the active sandbox was provisioned with, so token
|
|
6
18
|
* refresh re-injects the same credential (review-board sandboxes keep the
|
|
7
19
|
* reviewer token instead of being clobbered with the worker token). */
|
|
8
20
|
export declare function registerGithubPatKind(requestContext: RequestContext, kind: GithubPatKind): void;
|
|
9
21
|
export declare function getRegisteredGithubPatKind(requestContext: RequestContext): GithubPatKind;
|
|
10
|
-
export declare function
|
|
22
|
+
export declare function requireGithubTokenInjector(requestContext: RequestContext): GithubTokenInjector;
|
|
11
23
|
export {};
|
|
12
24
|
//# sourceMappingURL=token-refresh.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-refresh.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/token-refresh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"token-refresh.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/token-refresh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAO9C,KAAK,mBAAmB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AACnD,KAAK,2BAA2B,GAAG,MAAM,mBAAmB,CAAC;AAE7D,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;4BAE4B;AAC5B,wBAAgB,2BAA2B,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAE7G;AAED,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,cAAc,GAAG,mBAAmB,GAAG,SAAS,CAEtG;AAED,wBAAgB,2BAA2B,CAAC,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAG/G;AAED,wBAAgB,mCAAmC,CACjD,cAAc,EAAE,cAAc,EAC9B,QAAQ,EAAE,2BAA2B,GACpC,IAAI,CAGN;AAED;;uEAEuE;AACvE,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAE/F;AAED,wBAAgB,0BAA0B,CAAC,cAAc,EAAE,cAAc,GAAG,aAAa,CAGxF;AAED,wBAAgB,0BAA0B,CAAC,cAAc,EAAE,cAAc,GAAG,mBAAmB,CAU9F"}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
//#region src/integrations/github/token-refresh.ts
|
|
2
2
|
const GITHUB_TOKEN_INJECTOR_CONTEXT_KEY = "factoryGithubTokenInjector";
|
|
3
|
+
const GITHUB_TOKEN_INJECTOR_RESOLVER_CONTEXT_KEY = "factoryGithubTokenInjectorResolver";
|
|
3
4
|
const GITHUB_PAT_KIND_CONTEXT_KEY = "factoryGithubPatKind";
|
|
5
|
+
const GITHUB_REFRESH_TARGET_CONTEXT_KEY = "factoryGithubRefreshTarget";
|
|
6
|
+
/** Recorded by the workspace resolver once it has authorized a GitHub-backed
|
|
7
|
+
* session, before the sandbox starts, so the refresh tool is offered only to
|
|
8
|
+
* sessions it can serve. */
|
|
9
|
+
function registerGithubRefreshTarget(requestContext, target) {
|
|
10
|
+
requestContext.set(GITHUB_REFRESH_TARGET_CONTEXT_KEY, target);
|
|
11
|
+
}
|
|
12
|
+
function getGithubRefreshTarget(requestContext) {
|
|
13
|
+
return requestContext.get(GITHUB_REFRESH_TARGET_CONTEXT_KEY);
|
|
14
|
+
}
|
|
4
15
|
function registerGithubTokenInjector(requestContext, injector) {
|
|
16
|
+
requestContext.set(GITHUB_TOKEN_INJECTOR_RESOLVER_CONTEXT_KEY, void 0);
|
|
5
17
|
requestContext.set(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY, injector);
|
|
6
18
|
}
|
|
19
|
+
function registerGithubTokenInjectorResolver(requestContext, resolver) {
|
|
20
|
+
requestContext.set(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY, void 0);
|
|
21
|
+
requestContext.set(GITHUB_TOKEN_INJECTOR_RESOLVER_CONTEXT_KEY, resolver);
|
|
22
|
+
}
|
|
7
23
|
/** Record which PAT kind the active sandbox was provisioned with, so token
|
|
8
24
|
* refresh re-injects the same credential (review-board sandboxes keep the
|
|
9
25
|
* reviewer token instead of being clobbered with the worker token). */
|
|
@@ -13,12 +29,14 @@ function registerGithubPatKind(requestContext, kind) {
|
|
|
13
29
|
function getRegisteredGithubPatKind(requestContext) {
|
|
14
30
|
return requestContext.get(GITHUB_PAT_KIND_CONTEXT_KEY) === "reviewer" ? "reviewer" : "default";
|
|
15
31
|
}
|
|
16
|
-
function
|
|
32
|
+
function requireGithubTokenInjector(requestContext) {
|
|
33
|
+
const resolver = requestContext.get(GITHUB_TOKEN_INJECTOR_RESOLVER_CONTEXT_KEY);
|
|
34
|
+
if (resolver) return resolver();
|
|
17
35
|
const injector = requestContext.get(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY);
|
|
18
36
|
if (!injector) throw new Error("GitHub token refresh requires an active Factory sandbox workspace.");
|
|
19
|
-
injector
|
|
37
|
+
return injector;
|
|
20
38
|
}
|
|
21
39
|
//#endregion
|
|
22
|
-
export {
|
|
40
|
+
export { getGithubRefreshTarget, getRegisteredGithubPatKind, registerGithubPatKind, registerGithubRefreshTarget, registerGithubTokenInjector, registerGithubTokenInjectorResolver, requireGithubTokenInjector };
|
|
23
41
|
|
|
24
42
|
//# sourceMappingURL=token-refresh.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-refresh.js","names":[],"sources":["../../../src/integrations/github/token-refresh.ts"],"sourcesContent":["import type { RequestContext } from '@mastra/core/request-context';\n\nimport type { GithubPatKind } from './pat.js';\n\nconst GITHUB_TOKEN_INJECTOR_CONTEXT_KEY = 'factoryGithubTokenInjector';\nconst GITHUB_PAT_KIND_CONTEXT_KEY = 'factoryGithubPatKind';\n\ntype GithubTokenInjector = (token: string) => void;\n\nexport function registerGithubTokenInjector(requestContext: RequestContext, injector: GithubTokenInjector): void {\n requestContext.set(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY, injector);\n}\n\n/** Record which PAT kind the active sandbox was provisioned with, so token\n * refresh re-injects the same credential (review-board sandboxes keep the\n * reviewer token instead of being clobbered with the worker token). */\nexport function registerGithubPatKind(requestContext: RequestContext, kind: GithubPatKind): void {\n requestContext.set(GITHUB_PAT_KIND_CONTEXT_KEY, kind);\n}\n\nexport function getRegisteredGithubPatKind(requestContext: RequestContext): GithubPatKind {\n const kind = requestContext.get(GITHUB_PAT_KIND_CONTEXT_KEY);\n return kind === 'reviewer' ? 'reviewer' : 'default';\n}\n\nexport function
|
|
1
|
+
{"version":3,"file":"token-refresh.js","names":[],"sources":["../../../src/integrations/github/token-refresh.ts"],"sourcesContent":["import type { RequestContext } from '@mastra/core/request-context';\n\nimport type { GithubPatKind } from './pat.js';\n\nconst GITHUB_TOKEN_INJECTOR_CONTEXT_KEY = 'factoryGithubTokenInjector';\nconst GITHUB_TOKEN_INJECTOR_RESOLVER_CONTEXT_KEY = 'factoryGithubTokenInjectorResolver';\nconst GITHUB_PAT_KIND_CONTEXT_KEY = 'factoryGithubPatKind';\nconst GITHUB_REFRESH_TARGET_CONTEXT_KEY = 'factoryGithubRefreshTarget';\n\ntype GithubTokenInjector = (token: string) => void;\ntype GithubTokenInjectorResolver = () => GithubTokenInjector;\n\n/** The authorized GitHub repository behind the request's Factory session. */\nexport interface GithubRefreshTarget {\n orgId: string;\n repositoryId: string;\n}\n\n/** Recorded by the workspace resolver once it has authorized a GitHub-backed\n * session, before the sandbox starts, so the refresh tool is offered only to\n * sessions it can serve. */\nexport function registerGithubRefreshTarget(requestContext: RequestContext, target: GithubRefreshTarget): void {\n requestContext.set(GITHUB_REFRESH_TARGET_CONTEXT_KEY, target);\n}\n\nexport function getGithubRefreshTarget(requestContext: RequestContext): GithubRefreshTarget | undefined {\n return requestContext.get(GITHUB_REFRESH_TARGET_CONTEXT_KEY) as GithubRefreshTarget | undefined;\n}\n\nexport function registerGithubTokenInjector(requestContext: RequestContext, injector: GithubTokenInjector): void {\n requestContext.set(GITHUB_TOKEN_INJECTOR_RESOLVER_CONTEXT_KEY, undefined);\n requestContext.set(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY, injector);\n}\n\nexport function registerGithubTokenInjectorResolver(\n requestContext: RequestContext,\n resolver: GithubTokenInjectorResolver,\n): void {\n requestContext.set(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY, undefined);\n requestContext.set(GITHUB_TOKEN_INJECTOR_RESOLVER_CONTEXT_KEY, resolver);\n}\n\n/** Record which PAT kind the active sandbox was provisioned with, so token\n * refresh re-injects the same credential (review-board sandboxes keep the\n * reviewer token instead of being clobbered with the worker token). */\nexport function registerGithubPatKind(requestContext: RequestContext, kind: GithubPatKind): void {\n requestContext.set(GITHUB_PAT_KIND_CONTEXT_KEY, kind);\n}\n\nexport function getRegisteredGithubPatKind(requestContext: RequestContext): GithubPatKind {\n const kind = requestContext.get(GITHUB_PAT_KIND_CONTEXT_KEY);\n return kind === 'reviewer' ? 'reviewer' : 'default';\n}\n\nexport function requireGithubTokenInjector(requestContext: RequestContext): GithubTokenInjector {\n const resolver = requestContext.get(GITHUB_TOKEN_INJECTOR_RESOLVER_CONTEXT_KEY) as\n GithubTokenInjectorResolver | undefined;\n if (resolver) return resolver();\n\n const injector = requestContext.get(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY) as GithubTokenInjector | undefined;\n if (!injector) {\n throw new Error('GitHub token refresh requires an active Factory sandbox workspace.');\n }\n return injector;\n}\n"],"mappings":";AAIA,MAAM,oCAAoC;AAC1C,MAAM,6CAA6C;AACnD,MAAM,8BAA8B;AACpC,MAAM,oCAAoC;;;;AAc1C,SAAgB,4BAA4B,gBAAgC,QAAmC;CAC7G,eAAe,IAAI,mCAAmC,MAAM;AAC9D;AAEA,SAAgB,uBAAuB,gBAAiE;CACtG,OAAO,eAAe,IAAI,iCAAiC;AAC7D;AAEA,SAAgB,4BAA4B,gBAAgC,UAAqC;CAC/G,eAAe,IAAI,4CAA4C,KAAA,CAAS;CACxE,eAAe,IAAI,mCAAmC,QAAQ;AAChE;AAEA,SAAgB,oCACd,gBACA,UACM;CACN,eAAe,IAAI,mCAAmC,KAAA,CAAS;CAC/D,eAAe,IAAI,4CAA4C,QAAQ;AACzE;;;;AAKA,SAAgB,sBAAsB,gBAAgC,MAA2B;CAC/F,eAAe,IAAI,6BAA6B,IAAI;AACtD;AAEA,SAAgB,2BAA2B,gBAA+C;CAExF,OADa,eAAe,IAAI,2BACtB,MAAM,aAAa,aAAa;AAC5C;AAEA,SAAgB,2BAA2B,gBAAqD;CAC9F,MAAM,WAAW,eAAe,IAAI,0CAA0C;CAE9E,IAAI,UAAU,OAAO,SAAS;CAE9B,MAAM,WAAW,eAAe,IAAI,iCAAiC;CACrE,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,oEAAoE;CAEtF,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/slack.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EAEd,eAAe,EACf,qBAAqB,EACrB,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":"AACA,OAAO,KAAK,EACV,cAAc,EAEd,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EAChB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAe/D,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,gDAAgD,CAAC;AAGxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AAC3F,OAAO,KAAK,EAAyB,iBAAiB,EAAE,MAAM,2CAA2C,CAAC;AAC1G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8CAA8C,CAAC;AAC/F,OAAO,KAAK,EAAE,sBAAsB,EAAe,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AACtH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAWxD,KAAK,aAAa,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;AACnD,KAAK,cAAc,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;AAoBpD,uFAAuF;AACvF,UAAU,gBAAgB;IACxB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,sBAAsB,CAAC;IACtC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,0BAA0B,CAAC;IAC3C,uGAAuG;IACvG,cAAc,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACvD;;;;;OAKG;IACH,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;IAC7C,4DAA4D;IAC5D,cAAc,CAAC,EAAE,yBAAyB,CAAC;CAC5C;AAyCD,+DAA+D;AAC/D,KAAK,kBAAkB;AACrB,0EAA0E;AACxE;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE;AACvB,8EAA8E;GAC5E;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE;AACvB,6EAA6E;GAC3E;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,qBAAqB,CAAA;CAAE,CAAC;AAE/E;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,MAAM,EACN,OAAO,EACP,YAAY,GACb,EAAE;IACD,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,cAAc,CAAC;IACxB,YAAY,CAAC,EAAE,sBAAsB,CAAC;CACvC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAmB9B;AAwBD,0EAA0E;AAC1E,KAAK,kBAAkB;AACrB,mEAAmE;AACjE;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE;AACvB,iFAAiF;GAC/E;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE;AACvB,uDAAuD;GACrD;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,qBAAqB,EAAE,OAAO,CAAA;CAAE,CAAC;AAErF;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,MAAM,EACN,IAAI,EACJ,GAAG,EACH,YAAY,EACZ,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,kBAAkB,CAAC;IACzB,GAAG,EAAE,qBAAqB,CAAC;IAC3B,YAAY,EAAE,sBAAsB,CAAC;IACrC,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CACnC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkD9B;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,gBAAgB,GAAG,iBAAiB,CAiHzF;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,EAAE,eAC6B,CAAC;AAEnE,gGAAgG;AAChG,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,gBAAgB,GAAG,qBAAqB,CAiB1F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,gBAAgB,GAAG,mBAAmB,CAsIzF;AAsKD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,sBAAsB,CAOhG;AAaD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,SAAS,EACT,MAAM,EACN,OAAO,EACP,IAAI,EACJ,gBAAgB,EAChB,OAAO,EACP,GAAG,GACJ,EAAE;IACD,SAAS,EAAE,gBAAgB,CAAC;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,cAAc,CAAC;IACxB,IAAI,EAAE,kBAAkB,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAClE,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBhB;AAiND,eAAO,MAAM,cAAc,GAAI,MAAM,gBAAgB,KAAG,eAyBvD,CAAC;AAEF,kFAAkF;AAClF,UAAU,gBAAgB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,gBAAgB,GAAG;IAAE,KAAK,EAAE,gBAAgB,CAAA;CAAE,GAAG,qBAAqB,CAqBrH"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { actorFromChannelAuthor } from "../../storage/domains/comments/actor.js";
|
|
2
|
+
import { primeTenantCredentialsForRequestContext } from "../../routes/tenant-credentials.js";
|
|
2
3
|
import { readRequestContextOrgId, seedSessionOrg } from "../../session/org-seed.js";
|
|
3
4
|
import { prepareSessionRunContext } from "../subscription-session.js";
|
|
4
5
|
import { applyPersonalMemorySettings } from "../../session/memory-settings-hydration.js";
|
|
@@ -518,6 +519,7 @@ async function gateDispatch(thread, message, deps, ctx, options = { requireInter
|
|
|
518
519
|
await thread.post("This thread belongs to a Factory session in another organization.");
|
|
519
520
|
return null;
|
|
520
521
|
}
|
|
522
|
+
await primeTenantCredentialsForRequestContext(ctx.requestContext);
|
|
521
523
|
const route = await resolveFactoryForLink({
|
|
522
524
|
thread,
|
|
523
525
|
...sender,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack.js","names":[],"sources":["../../../src/integrations/slack/slack.ts"],"sourcesContent":["import { ChannelSessionRejectedError } from '@mastra/core/channels';\nimport type {\n ChannelHandler,\n ChannelHandlerContext,\n ChannelHandlers,\n ChannelSessionResolve,\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 createSourceControlSessionLookup,\n FactorySourceControlConflictError,\n hydrateFactorySession,\n resolveFactoryDefaultModelId,\n resolveFactoryProjectForSession,\n resolveFactorySourceControl,\n resolveFactorySourceRepository,\n} from '../../session/factory-session.js';\nimport { applyPersonalMemorySettings } from '../../session/memory-settings-hydration.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 { FactoryActorExternalIdentity } from '../../storage/domains/comments/actor.js';\nimport { actorFromChannelAuthor } from '../../storage/domains/comments/actor.js';\nimport type { CommentsDomain } from '../../storage/domains/comments/domain.js';\nimport type { MemorySettingsStorage } from '../../storage/domains/memory-settings/base.js';\nimport type { ActiveModelPackRecord, ModelPacksStorage } from '../../storage/domains/model-packs/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { ExternalWorkItemSource, WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { FactoryChannelsConfig } from '../base.js';\nimport { prepareSessionRunContext } from '../subscription-session.js';\n\nimport { resolveEmojiShortcodes } from './emoji.js';\nimport { slackCommentSource } from './feed-publisher.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\nconst SLACK_REQUEST_TIMEOUT_MS = 15_000;\nconst MAX_WORK_ITEM_TITLE_CHARS = 80;\nconst graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n\nclass SlackSessionStartError extends Error {}\n\n/**\n * A card is titled with what the sender wrote. Cutting counts graphemes, not\n * code points: `👍🏼` is a base plus a skin-tone modifier, and a cut between\n * them leaves the wrong emoji.\n */\nfunction workItemTitleFrom(messageText: string): string {\n const resolved = resolveEmojiShortcodes(messageText);\n const characters = [...graphemes.segment(resolved)].map(({ segment }) => segment);\n if (characters.length <= MAX_WORK_ITEM_TITLE_CHARS) return resolved;\n return `${characters.slice(0, MAX_WORK_ITEM_TITLE_CHARS - 1).join('')}…`;\n}\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 a \"pick a default factory\"\n * card posted in the thread and no run. Unset → no factory routing (runs\n * dispatch as 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 /** Registered source-control partitions used to select the provider linked to each Factory project. */\n sourceControls?: readonly 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 — and the linked sender's own settings win over them for every\n * knob the sender has saved.\n */\n memorySettings?: MemorySettingsStorage;\n /**\n * Model-packs domain. When provided, a new repo-backed session starts on the\n * linked sender's active pack build model — the model that user picked for\n * themselves — before the factory's shared default. Read once, when the\n * thread's build model is first chosen; the choice is persisted on the thread\n * and never re-resolved. Unset → the factory default, as before.\n */\n modelPacks?: ModelPacksStorage;\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 /**\n * Work-item feed. With it (and `workItems`), an `aside` lands as a comment on\n * the card the thread created. Unset → asides stay ignored, as before.\n */\n feed?: Pick<CommentsDomain, 'createComment'>;\n /** Overrides applied to the Slack channel adapter entry. */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\nfunction configuredSourceControls(deps: SlackChannelDeps): readonly SourceControlStorageHandle[] {\n return deps.sourceControls ?? (deps.sourceControl ? [deps.sourceControl] : []);\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, thread), { fallbackToDM: true });\n }\n return { status: 'blocked' };\n}\n\nconst retryHint = (thread: HandlerThread) => (thread.isDM ? 'message me again' : 'mention me again');\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, thread: HandlerThread) {\n return Card({\n title: 'Connect your account',\n children: [\n CardText(`Connect your account to use this agent, then ${retryHint(thread)}.`),\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 — a \"pick a default factory\" card\n * posted publicly in the thread so Slack notifies the sender, and the run is\n * blocked.\n */\nexport async function resolveFactoryForLink({\n thread,\n link,\n key,\n accountLinks,\n projects,\n}: {\n thread: HandlerThread;\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.post(\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 ${retryHint(thread)}.`\n : `Your account has several factories. Pick which one Slack sessions should go to, then ${retryHint(thread)}.`,\n ),\n Actions([\n LinkButton({\n url: `${publicUrl}/settings/connections`,\n label: 'Open settings',\n }),\n ]),\n ],\n }),\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.\n *\n * A gated deployment refuses a thread it cannot place, rather than falling back\n * to a chat-only session: a repo-backed thread is the whole point of the\n * integration, and answering in a chat-only one would run the sender's request\n * in no project, on the SDK's built-in defaults. A linked sender whose project\n * cannot start a session gets an actionable error in Slack; the dispatch gate\n * handles unlinked or unrouted senders before this hook runs.\n * The hook only runs when a NEW thread is actually created (`getOrCreateThread`\n * resolves the owner lazily), so an established conversation is never touched:\n * it keeps its session, its model, and its history.\n *\n * The chat-only `channel:...` id survives for two cases where refusing would be\n * wrong: a deployment that cannot produce a repo-backed thread at all (no\n * account linking, no projects, or no source-control integration registered),\n * and a message carrying no sender id. In both, a `channel:` thread is the only\n * shape a session can take.\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 } = deps;\n const sourceControls = configuredSourceControls(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 // A deployment with no account linking, no projects, or no source-control\n // integration cannot produce a repo-backed thread at all, so a `channel:`\n // thread is the only shape it has. Every refusal below is a different thing\n // entirely: a sender the bot cannot place, in a deployment that can.\n if (!accountLinks || !projects || sourceControls.length === 0) return chatOnlyResourceId;\n // No sender id at all — a malformed message, not a sender the host turned\n // away. There is nothing to place, so give it the per-thread memory key.\n if (!message.author.userId) return chatOnlyResourceId;\n\n const externalTeamId = rawTeamId(message.raw);\n if (!externalTeamId) {\n throw new ChannelSessionRejectedError('No Slack workspace id on the message — the sender cannot be identified');\n }\n try {\n const link = await accountLinks.getAccountLink({\n platform,\n externalTeamId,\n externalUserId: message.author.userId,\n });\n if (!link) {\n throw new ChannelSessionRejectedError(\n `Slack sender ${message.author.userId} is not linked to a Factory account`,\n );\n }\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) {\n throw new ChannelSessionRejectedError(`Linked Slack sender ${message.author.userId} has no Factory project`);\n }\n\n // Linked and routed, so a repo-backed session is not a preference: with no\n // repository to work in there is no thread to start, and starting one\n // anyway would answer in a project the sender never picked, on the SDK's\n // built-in defaults.\n const sourceControl = await resolveFactorySourceControl({ sourceControls, orgId, factoryProjectId });\n if (!sourceControl) {\n const connections = await Promise.all(\n sourceControls.map(sourceControl => sourceControl.connections.list({ orgId, factoryProjectId })),\n );\n throw new SlackSessionStartError(\n connections.some(rows => rows.length > 0)\n ? 'Could not start a session: link a repository to this Factory project.'\n : 'Could not start a session: connect source control to this Factory project.',\n );\n }\n const repo = await resolveFactorySourceRepository({\n sourceControl,\n orgId,\n factoryProjectId,\n firstLinkedRepository: true,\n });\n if (!repo.found) {\n throw new SlackSessionStartError(\n repo.reason === 'connection'\n ? 'Could not start a session: connect source control to this Factory project.'\n : 'Could not start a session: link a repository to this Factory project.',\n );\n }\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: globalThis.crypto.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 if (error instanceof ChannelSessionRejectedError || error instanceof SlackSessionStartError) throw error;\n if (error instanceof FactorySourceControlConflictError) {\n throw new SlackSessionStartError(\n 'Could not start a session: this Factory project has repositories from more than one source-control provider.',\n { cause: error },\n );\n }\n console.error('[slack] failed to start repo-backed session for thread', thread.id, error);\n throw new SlackSessionStartError('Could not start a session right now. Please try again later.', {\n cause: error,\n });\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/** Create channel sessions with their Factory ownership present in initial controller state. */\nexport function createChannelSessionResolver(deps: SlackChannelDeps): ChannelSessionResolve {\n const sourceControlSessions = createSourceControlSessionLookup(configuredSourceControls(deps));\n return async ({ controller, thread, requestContext }) => {\n const sourceControl = thread.resourceId.startsWith('channel:')\n ? null\n : await sourceControlSessions.getSourceControlBySessionId(thread.resourceId);\n const owner = sourceControl\n ? await resolveFactoryProjectForSession({ sourceControl, sessionId: thread.resourceId })\n : null;\n return controller.createSession({\n id: thread.resourceId,\n ownerId: controller.id,\n resourceId: thread.resourceId,\n requestContext,\n ...(owner ? { tags: { factoryProjectId: owner.factoryProjectId } } : {}),\n });\n };\n}\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 * The model this session starts on is picked here, once, in the order of who\n * chose it: the linked sender's active model pack, else the factory project's\n * default, else the SDK's built-in mode default. The choice is persisted on the\n * thread as `modeModelId_<mode>`, so it outlives the process that made it.\n *\n * Observational memory is configured here too, in the same order of who chose\n * it: the project's shared settings first, then the linked sender's own row,\n * which wins for every knob they have saved. The pair is re-applied on every\n * start rather than once — memory settings are stored preference, not a choice\n * made on this thread, and a restarted process re-resolves the project's row\n * before this hook runs.\n *\n * The model resolution is skipped on a session whose mode already has a model\n * persisted on the thread. That is the durable record of a deliberate choice —\n * either an earlier start or a user's own switch — and re-applying a preference\n * over it would undo the user's selection every time the process restarts.\n * Memory settings have no such per-thread record, so they are re-applied on\n * every start.\n */\nexport function createChannelSessionStartHook(deps: SlackChannelDeps): ChannelSessionStart {\n const { projects, memorySettings, modelPacks } = deps;\n const sourceControlSessions = createSourceControlSessionLookup(configuredSourceControls(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 model lookups below deliberately\n // skip storage on a restarted session. A channel-only thread and a thread\n // whose dispatch was ungated both land here with no org and are marked\n // unresolved rather than being left to look like a local session.\n await seedSessionOrg(session, readRequestContextOrgId(requestContext));\n\n if (!projects) return;\n if (thread.resourceId.startsWith('channel:')) return;\n\n const sourceControl = await sourceControlSessions.getSourceControlBySessionId(thread.resourceId);\n if (!sourceControl) return;\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 const persistedModelId = await session.thread.getSetting({ key: modeModelKey });\n if (typeof persistedModelId === 'string') {\n // A restarted session restores its generation model from the thread, but\n // still needs the project memory row and a provider-compatible fallback.\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n observationalMemoryModelId: persistedModelId,\n memorySettings,\n });\n // Subagent models live in session state only, so restore the ones this\n // thread pinned at its first start instead of re-resolving them.\n for (const agentType of SUBAGENT_TYPES) {\n const modelId = await session.thread.getSetting({ key: pinnedSubagentModelKey(agentType) });\n if (typeof modelId === 'string') await applySubagentModel(session, agentType, modelId);\n }\n } else {\n const factoryModelId = await resolveFactoryDefaultModelId(projects, owner.factoryProjectId);\n const userPackModels = await resolveActivePackModels(modelPacks, owner);\n const userModelId = userPackModels?.build || undefined;\n const selectedModelId = userModelId ?? factoryModelId;\n\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n defaultModelId: factoryModelId,\n // Slack runs with the linked sender's credentials. Derive OM's fallback\n // from that sender's selected model rather than the factory model, which\n // may belong to a provider the sender cannot access.\n observationalMemoryModelId: selectedModelId,\n memorySettings,\n });\n\n if (selectedModelId && selectedModelId !== factoryModelId) {\n // The sender's own choice beats the factory's. `switch` applies the model\n // and persists it as this mode's model on the thread in one step — which\n // is what makes the choice outlive this process.\n try {\n await session.model.switch({ modelId: selectedModelId });\n } catch (error) {\n console.warn(\"[slack] Failed to apply the sender's model pack model\", {\n modelId: selectedModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n const currentModelId = session.model.get();\n // The message continues on the factory/SDK model. Realign the OM\n // fallback with that model while preserving explicit project settings.\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n observationalMemoryModelId: currentModelId,\n memorySettings,\n });\n if (currentModelId && !factoryModelId) {\n try {\n await session.model.saveForMode({ modeId: session.mode.get(), modelId: currentModelId });\n } catch (saveError) {\n console.warn(\"[slack] Failed to persist the sender's model pack model\", {\n modelId: currentModelId,\n error: saveError instanceof Error ? saveError.message : String(saveError),\n });\n }\n }\n }\n } else if (!selectedModelId) {\n // Neither preference exists, so the SDK's built-in mode default is this\n // thread's model of record. Pin it too: a later SDK upgrade that moves\n // that default must not silently retarget a thread that already started.\n const currentModelId = session.model.get();\n if (currentModelId) {\n await session.model.saveForMode({ modeId: session.mode.get(), modelId: currentModelId });\n }\n }\n\n // Subagents follow the sender's pack like the TUI does (explore→fast,\n // plan→plan, execute→build); roles the pack leaves empty use the factory\n // default. Pinned on the thread like the main model, so a restart\n // restores these rather than whatever pack or default exists by then.\n const packSubagentModels = {\n explore: userPackModels?.fast,\n plan: userPackModels?.plan,\n execute: userPackModels?.build,\n };\n for (const agentType of SUBAGENT_TYPES) {\n const modelId = packSubagentModels[agentType] || factoryModelId;\n if (!modelId) continue;\n if (await applySubagentModel(session, agentType, modelId)) {\n await session.thread.setSetting({ key: pinnedSubagentModelKey(agentType), value: modelId });\n }\n }\n }\n\n // The sender's own observational-memory settings, applied last so they beat\n // the project's — and on EVERY start, not just the first. Unlike the model,\n // this is stored preference rather than a choice made on this thread: a\n // restarted process re-resolves the project row before this hook runs, so\n // skipping it here would quietly put a thread back on the project's OM\n // configuration. Chat-only threads never reach this point.\n await applyPersonalMemorySettings(session, {\n memorySettings,\n orgId: owner.orgId,\n userId: owner.userId,\n });\n };\n}\n\nconst SUBAGENT_TYPES = ['explore', 'plan', 'execute'] as const;\nconst pinnedSubagentModelKey = (agentType: string) => `slackSubagentModelId_${agentType}`;\n\n/** Best-effort: a subagent model that can't be applied must not fail the message. */\nasync function applySubagentModel(\n session: Parameters<ChannelSessionStart>[0]['session'],\n agentType: (typeof SUBAGENT_TYPES)[number],\n modelId: string,\n): Promise<boolean> {\n try {\n await session.subagents.model.set({ modelId, agentType });\n return true;\n } catch (error) {\n console.warn('[slack] Failed to apply the subagent model', {\n agentType,\n modelId,\n error: error instanceof Error ? error.message : String(error),\n });\n return false;\n }\n}\n\n/**\n * The models of the sender's active model pack — the models that user chose\n * for themselves — or `undefined` when they have no pack.\n *\n * Best-effort by design: an uninitialized model-packs domain, a read failure, or\n * a pack saved without a build model all mean \"no personal preference\", which\n * falls through to the factory default rather than failing the dispatch.\n */\nasync function resolveActivePackModels(\n modelPacks: ModelPacksStorage | undefined,\n owner: { orgId: string; userId: string },\n): Promise<ActiveModelPackRecord['models'] | undefined> {\n if (!modelPacks) return undefined;\n try {\n const active = await modelPacks.getActive({ orgId: owner.orgId, userId: owner.userId });\n return active?.models;\n } catch (error) {\n console.warn('[slack] model pack lookup failed for a new session', {\n orgId: owner.orgId,\n userId: owner.userId,\n error: error instanceof Error ? error.message : String(error),\n });\n return undefined;\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\nasync function prepareExistingSessionOwnerContext(\n thread: HandlerThread,\n deps: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n options: { expectedOrgId?: string; requireInternalThread: boolean },\n): Promise<'ready' | 'organization-mismatch'> {\n const sourceControls = configuredSourceControls(deps);\n if (sourceControls.length === 0 && !options.requireInternalThread) return 'ready';\n\n const internalThread = await findInternalThread(ctx.mastra, thread);\n if (!internalThread) {\n if (options.requireInternalThread) {\n throw new Error(`Could not resolve the internal Slack thread for ${thread.id}.`);\n }\n return 'ready';\n }\n if (sourceControls.length === 0 || internalThread.resourceId.startsWith('channel:')) return 'ready';\n if (!options.expectedOrgId) {\n throw new Error(`Could not authorize the owner of Slack Factory session ${internalThread.resourceId}.`);\n }\n\n const preparation = await prepareSessionRunContext(\n ctx.requestContext,\n internalThread.resourceId,\n { sessions: createSourceControlSessionLookup(sourceControls) },\n { expectedOrgId: options.expectedOrgId },\n );\n if (preparation === 'organization-mismatch') return preparation;\n if (preparation === 'unavailable') {\n throw new Error(`Could not authorize the owner of Slack Factory session ${internalThread.resourceId}.`);\n }\n return 'ready';\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 deps: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n options: { requireInternalThread: boolean } = { requireInternalThread: false },\n): Promise<{\n routed?: { link: ChannelAccountLink; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n} | null> {\n const { accountLinks, projects } = deps;\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 const ownerContext = await prepareExistingSessionOwnerContext(thread, deps, ctx, {\n expectedOrgId: sender.link.orgId,\n requireInternalThread: options.requireInternalThread,\n });\n if (ownerContext === 'organization-mismatch') {\n await thread.post('This thread belongs to a Factory session in another organization.');\n return null;\n }\n\n const route = await resolveFactoryForLink({ thread, ...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 * A channel id and a `ts` name a thread only inside the workspace that issued\n * them, so the team scopes the card's key — without it two workspaces could\n * hold one key and an aside would land on another tenant's card.\n */\nexport function slackThreadSource(thread: HandlerThread, teamId?: string): ExternalWorkItemSource {\n return {\n integrationId: thread.adapter.name,\n type: 'slack-thread',\n ...(teamId ? { workspaceId: teamId } : {}),\n externalId: thread.id,\n };\n}\n\n/** Cards written before the key carried a team are still keyed bare. */\nasync function findThreadWorkItem(\n workItems: WorkItemsStorage,\n thread: HandlerThread,\n teamId?: string,\n): Promise<WorkItemRow | null> {\n const scoped = await workItems.getBySource(slackThreadSource(thread, teamId));\n if (scoped || !teamId) return scoped;\n return workItems.getBySource(slackThreadSource(thread));\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 = workItemTitleFrom(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 externalSource: { ...slackThreadSource(thread, slackTeamId(message)), ...(url ? { url } : {}) },\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 preDispatchErrorDetails(error: unknown): { message: string; cause?: { message: string } } {\n const readMessage = (value: unknown) => {\n const message =\n typeof value === 'string'\n ? value\n : value && typeof value === 'object' && 'message' in value\n ? value.message\n : undefined;\n return typeof message === 'string' && message.length > 0 ? message : undefined;\n };\n try {\n const message = readMessage(error) ?? 'Unknown error';\n const cause = error && typeof error === 'object' && 'cause' in error ? readMessage(error.cause) : undefined;\n return { message, ...(cause ? { cause: { message: cause } } : {}) };\n } catch {\n return { message: 'Error details unavailable' };\n }\n}\n\nasync function reportPreDispatchError(\n thread: HandlerThread,\n message: HandlerMessage,\n ctx: ChannelHandlerContext,\n error: unknown,\n): Promise<void> {\n try {\n if (error instanceof ChannelSessionRejectedError) return;\n } catch {\n // A thrown proxy can fail even the refusal check; still report the failure.\n }\n const correlation = {\n platform: thread.adapter.name,\n threadId: thread.id,\n messageId: message.id,\n authorId: message.author.userId,\n };\n const logger = typeof ctx.mastra?.getLogger === 'function' ? ctx.mastra.getLogger() : undefined;\n const logError = (line: string) => (logger ? logger.error(line) : console.error(line));\n logError(`[slack] Pre-dispatch failure ${JSON.stringify({ ...correlation, error: preDispatchErrorDetails(error) })}`);\n try {\n // The message id is the lookup key for the diagnostic above.\n await thread.post(`Couldn’t start processing your message. Please try again.\\n\\`messageId: ${message.id}\\``);\n } catch (deliveryError) {\n logError(\n `[slack] Failed to deliver pre-dispatch error reply ${JSON.stringify({ ...correlation, error: preDispatchErrorDetails(deliveryError) })}`,\n );\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 let gate: Awaited<ReturnType<typeof gateDispatch>>;\n let isNewSession: boolean;\n try {\n 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 isNewSession = !(await thread.isSubscribed());\n } catch (error) {\n await reportPreDispatchError(thread, message, ctx, error);\n return;\n }\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}\n\n/**\n * An `aside` never reaches the agent, but it is still part of the conversation:\n * record it on the card the thread created. The link is resolved here rather\n * than through `gateDispatch`, which posts a Connect card an aside must not\n * trigger. Best-effort — nothing here may surface in the Slack thread.\n */\nasync function ingestAside(\n thread: HandlerThread,\n message: HandlerMessage,\n { feed, workItems, accountLinks }: SlackChannelDeps,\n): Promise<void> {\n if (!feed || !workItems) return;\n const body = resolveEmojiShortcodes(message.text.replace(/^aside\\b[,:]?\\s*/i, '').trim());\n if (!body) return;\n try {\n const teamId = slackTeamId(message);\n const workItem = await findThreadWorkItem(workItems, thread, teamId);\n if (!workItem) return;\n\n const external: FactoryActorExternalIdentity = {\n platform: thread.adapter.name,\n ...(teamId ? { teamId } : {}),\n messageId: message.id,\n userId: message.author.userId,\n userName: message.author.userName,\n fullName: message.author.fullName,\n isBot: message.author.isBot,\n };\n const link =\n accountLinks && teamId\n ? await accountLinks.getAccountLink({\n platform: external.platform,\n externalTeamId: teamId,\n externalUserId: external.userId,\n })\n : null;\n\n const result = await feed.createComment({\n orgId: workItem.orgId,\n workItemId: workItem.id,\n author: actorFromChannelAuthor(external, link),\n body,\n occurredAt: message.metadata.dateSent,\n externalSource: slackCommentSource(thread.id, message.id, teamId),\n });\n if (result.status !== 'created') {\n console.warn('[slack] aside not ingested', { thread: thread.id, messageTs: message.id, status: result.status });\n }\n } catch (error) {\n // Slack was acked before this handler ran, so a lost aside is never redelivered.\n console.warn('[slack] aside ingest failed', { thread: thread.id, messageTs: message.id, error });\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 ingestAside(thread, message, deps);\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 try {\n const gate = await gateDispatch(thread, message, deps, ctx, { requireInternalThread: true });\n if (!gate) return;\n } catch (error) {\n await reportPreDispatchError(thread, message, ctx, error);\n return;\n }\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 // Per HTTP attempt, not per call: the WebClient's own retry policy still\n // waits out a 429. Without it a dead socket hangs a post forever.\n const adapter = createSlackAdapter({ ...deps.slack, webClientOptions: { timeout: SLACK_REQUEST_TIMEOUT_MS } });\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 resolveSession: createChannelSessionResolver(deps),\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 { ChannelSessionRejectedError } from '@mastra/core/channels';\nimport type {\n ChannelHandler,\n ChannelHandlerContext,\n ChannelHandlers,\n ChannelSessionResolve,\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 { primeTenantCredentialsForRequestContext } from '../../routes/tenant-credentials.js';\nimport {\n createSourceControlSessionLookup,\n FactorySourceControlConflictError,\n hydrateFactorySession,\n resolveFactoryDefaultModelId,\n resolveFactoryProjectForSession,\n resolveFactorySourceControl,\n resolveFactorySourceRepository,\n} from '../../session/factory-session.js';\nimport { applyPersonalMemorySettings } from '../../session/memory-settings-hydration.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 { FactoryActorExternalIdentity } from '../../storage/domains/comments/actor.js';\nimport { actorFromChannelAuthor } from '../../storage/domains/comments/actor.js';\nimport type { CommentsDomain } from '../../storage/domains/comments/domain.js';\nimport type { MemorySettingsStorage } from '../../storage/domains/memory-settings/base.js';\nimport type { ActiveModelPackRecord, ModelPacksStorage } from '../../storage/domains/model-packs/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { ExternalWorkItemSource, WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { FactoryChannelsConfig } from '../base.js';\nimport { prepareSessionRunContext } from '../subscription-session.js';\n\nimport { resolveEmojiShortcodes } from './emoji.js';\nimport { slackCommentSource } from './feed-publisher.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\nconst SLACK_REQUEST_TIMEOUT_MS = 15_000;\nconst MAX_WORK_ITEM_TITLE_CHARS = 80;\nconst graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n\nclass SlackSessionStartError extends Error {}\n\n/**\n * A card is titled with what the sender wrote. Cutting counts graphemes, not\n * code points: `👍🏼` is a base plus a skin-tone modifier, and a cut between\n * them leaves the wrong emoji.\n */\nfunction workItemTitleFrom(messageText: string): string {\n const resolved = resolveEmojiShortcodes(messageText);\n const characters = [...graphemes.segment(resolved)].map(({ segment }) => segment);\n if (characters.length <= MAX_WORK_ITEM_TITLE_CHARS) return resolved;\n return `${characters.slice(0, MAX_WORK_ITEM_TITLE_CHARS - 1).join('')}…`;\n}\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 a \"pick a default factory\"\n * card posted in the thread and no run. Unset → no factory routing (runs\n * dispatch as 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 /** Registered source-control partitions used to select the provider linked to each Factory project. */\n sourceControls?: readonly 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 — and the linked sender's own settings win over them for every\n * knob the sender has saved.\n */\n memorySettings?: MemorySettingsStorage;\n /**\n * Model-packs domain. When provided, a new repo-backed session starts on the\n * linked sender's active pack build model — the model that user picked for\n * themselves — before the factory's shared default. Read once, when the\n * thread's build model is first chosen; the choice is persisted on the thread\n * and never re-resolved. Unset → the factory default, as before.\n */\n modelPacks?: ModelPacksStorage;\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 /**\n * Work-item feed. With it (and `workItems`), an `aside` lands as a comment on\n * the card the thread created. Unset → asides stay ignored, as before.\n */\n feed?: Pick<CommentsDomain, 'createComment'>;\n /** Overrides applied to the Slack channel adapter entry. */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\nfunction configuredSourceControls(deps: SlackChannelDeps): readonly SourceControlStorageHandle[] {\n return deps.sourceControls ?? (deps.sourceControl ? [deps.sourceControl] : []);\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, thread), { fallbackToDM: true });\n }\n return { status: 'blocked' };\n}\n\nconst retryHint = (thread: HandlerThread) => (thread.isDM ? 'message me again' : 'mention me again');\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, thread: HandlerThread) {\n return Card({\n title: 'Connect your account',\n children: [\n CardText(`Connect your account to use this agent, then ${retryHint(thread)}.`),\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 — a \"pick a default factory\" card\n * posted publicly in the thread so Slack notifies the sender, and the run is\n * blocked.\n */\nexport async function resolveFactoryForLink({\n thread,\n link,\n key,\n accountLinks,\n projects,\n}: {\n thread: HandlerThread;\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.post(\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 ${retryHint(thread)}.`\n : `Your account has several factories. Pick which one Slack sessions should go to, then ${retryHint(thread)}.`,\n ),\n Actions([\n LinkButton({\n url: `${publicUrl}/settings/connections`,\n label: 'Open settings',\n }),\n ]),\n ],\n }),\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.\n *\n * A gated deployment refuses a thread it cannot place, rather than falling back\n * to a chat-only session: a repo-backed thread is the whole point of the\n * integration, and answering in a chat-only one would run the sender's request\n * in no project, on the SDK's built-in defaults. A linked sender whose project\n * cannot start a session gets an actionable error in Slack; the dispatch gate\n * handles unlinked or unrouted senders before this hook runs.\n * The hook only runs when a NEW thread is actually created (`getOrCreateThread`\n * resolves the owner lazily), so an established conversation is never touched:\n * it keeps its session, its model, and its history.\n *\n * The chat-only `channel:...` id survives for two cases where refusing would be\n * wrong: a deployment that cannot produce a repo-backed thread at all (no\n * account linking, no projects, or no source-control integration registered),\n * and a message carrying no sender id. In both, a `channel:` thread is the only\n * shape a session can take.\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 } = deps;\n const sourceControls = configuredSourceControls(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 // A deployment with no account linking, no projects, or no source-control\n // integration cannot produce a repo-backed thread at all, so a `channel:`\n // thread is the only shape it has. Every refusal below is a different thing\n // entirely: a sender the bot cannot place, in a deployment that can.\n if (!accountLinks || !projects || sourceControls.length === 0) return chatOnlyResourceId;\n // No sender id at all — a malformed message, not a sender the host turned\n // away. There is nothing to place, so give it the per-thread memory key.\n if (!message.author.userId) return chatOnlyResourceId;\n\n const externalTeamId = rawTeamId(message.raw);\n if (!externalTeamId) {\n throw new ChannelSessionRejectedError('No Slack workspace id on the message — the sender cannot be identified');\n }\n try {\n const link = await accountLinks.getAccountLink({\n platform,\n externalTeamId,\n externalUserId: message.author.userId,\n });\n if (!link) {\n throw new ChannelSessionRejectedError(\n `Slack sender ${message.author.userId} is not linked to a Factory account`,\n );\n }\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) {\n throw new ChannelSessionRejectedError(`Linked Slack sender ${message.author.userId} has no Factory project`);\n }\n\n // Linked and routed, so a repo-backed session is not a preference: with no\n // repository to work in there is no thread to start, and starting one\n // anyway would answer in a project the sender never picked, on the SDK's\n // built-in defaults.\n const sourceControl = await resolveFactorySourceControl({ sourceControls, orgId, factoryProjectId });\n if (!sourceControl) {\n const connections = await Promise.all(\n sourceControls.map(sourceControl => sourceControl.connections.list({ orgId, factoryProjectId })),\n );\n throw new SlackSessionStartError(\n connections.some(rows => rows.length > 0)\n ? 'Could not start a session: link a repository to this Factory project.'\n : 'Could not start a session: connect source control to this Factory project.',\n );\n }\n const repo = await resolveFactorySourceRepository({\n sourceControl,\n orgId,\n factoryProjectId,\n firstLinkedRepository: true,\n });\n if (!repo.found) {\n throw new SlackSessionStartError(\n repo.reason === 'connection'\n ? 'Could not start a session: connect source control to this Factory project.'\n : 'Could not start a session: link a repository to this Factory project.',\n );\n }\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: globalThis.crypto.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 if (error instanceof ChannelSessionRejectedError || error instanceof SlackSessionStartError) throw error;\n if (error instanceof FactorySourceControlConflictError) {\n throw new SlackSessionStartError(\n 'Could not start a session: this Factory project has repositories from more than one source-control provider.',\n { cause: error },\n );\n }\n console.error('[slack] failed to start repo-backed session for thread', thread.id, error);\n throw new SlackSessionStartError('Could not start a session right now. Please try again later.', {\n cause: error,\n });\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/** Create channel sessions with their Factory ownership present in initial controller state. */\nexport function createChannelSessionResolver(deps: SlackChannelDeps): ChannelSessionResolve {\n const sourceControlSessions = createSourceControlSessionLookup(configuredSourceControls(deps));\n return async ({ controller, thread, requestContext }) => {\n const sourceControl = thread.resourceId.startsWith('channel:')\n ? null\n : await sourceControlSessions.getSourceControlBySessionId(thread.resourceId);\n const owner = sourceControl\n ? await resolveFactoryProjectForSession({ sourceControl, sessionId: thread.resourceId })\n : null;\n return controller.createSession({\n id: thread.resourceId,\n ownerId: controller.id,\n resourceId: thread.resourceId,\n requestContext,\n ...(owner ? { tags: { factoryProjectId: owner.factoryProjectId } } : {}),\n });\n };\n}\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 * The model this session starts on is picked here, once, in the order of who\n * chose it: the linked sender's active model pack, else the factory project's\n * default, else the SDK's built-in mode default. The choice is persisted on the\n * thread as `modeModelId_<mode>`, so it outlives the process that made it.\n *\n * Observational memory is configured here too, in the same order of who chose\n * it: the project's shared settings first, then the linked sender's own row,\n * which wins for every knob they have saved. The pair is re-applied on every\n * start rather than once — memory settings are stored preference, not a choice\n * made on this thread, and a restarted process re-resolves the project's row\n * before this hook runs.\n *\n * The model resolution is skipped on a session whose mode already has a model\n * persisted on the thread. That is the durable record of a deliberate choice —\n * either an earlier start or a user's own switch — and re-applying a preference\n * over it would undo the user's selection every time the process restarts.\n * Memory settings have no such per-thread record, so they are re-applied on\n * every start.\n */\nexport function createChannelSessionStartHook(deps: SlackChannelDeps): ChannelSessionStart {\n const { projects, memorySettings, modelPacks } = deps;\n const sourceControlSessions = createSourceControlSessionLookup(configuredSourceControls(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 model lookups below deliberately\n // skip storage on a restarted session. A channel-only thread and a thread\n // whose dispatch was ungated both land here with no org and are marked\n // unresolved rather than being left to look like a local session.\n await seedSessionOrg(session, readRequestContextOrgId(requestContext));\n\n if (!projects) return;\n if (thread.resourceId.startsWith('channel:')) return;\n\n const sourceControl = await sourceControlSessions.getSourceControlBySessionId(thread.resourceId);\n if (!sourceControl) return;\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 const persistedModelId = await session.thread.getSetting({ key: modeModelKey });\n if (typeof persistedModelId === 'string') {\n // A restarted session restores its generation model from the thread, but\n // still needs the project memory row and a provider-compatible fallback.\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n observationalMemoryModelId: persistedModelId,\n memorySettings,\n });\n // Subagent models live in session state only, so restore the ones this\n // thread pinned at its first start instead of re-resolving them.\n for (const agentType of SUBAGENT_TYPES) {\n const modelId = await session.thread.getSetting({ key: pinnedSubagentModelKey(agentType) });\n if (typeof modelId === 'string') await applySubagentModel(session, agentType, modelId);\n }\n } else {\n const factoryModelId = await resolveFactoryDefaultModelId(projects, owner.factoryProjectId);\n const userPackModels = await resolveActivePackModels(modelPacks, owner);\n const userModelId = userPackModels?.build || undefined;\n const selectedModelId = userModelId ?? factoryModelId;\n\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n defaultModelId: factoryModelId,\n // Slack runs with the linked sender's credentials. Derive OM's fallback\n // from that sender's selected model rather than the factory model, which\n // may belong to a provider the sender cannot access.\n observationalMemoryModelId: selectedModelId,\n memorySettings,\n });\n\n if (selectedModelId && selectedModelId !== factoryModelId) {\n // The sender's own choice beats the factory's. `switch` applies the model\n // and persists it as this mode's model on the thread in one step — which\n // is what makes the choice outlive this process.\n try {\n await session.model.switch({ modelId: selectedModelId });\n } catch (error) {\n console.warn(\"[slack] Failed to apply the sender's model pack model\", {\n modelId: selectedModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n const currentModelId = session.model.get();\n // The message continues on the factory/SDK model. Realign the OM\n // fallback with that model while preserving explicit project settings.\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n observationalMemoryModelId: currentModelId,\n memorySettings,\n });\n if (currentModelId && !factoryModelId) {\n try {\n await session.model.saveForMode({ modeId: session.mode.get(), modelId: currentModelId });\n } catch (saveError) {\n console.warn(\"[slack] Failed to persist the sender's model pack model\", {\n modelId: currentModelId,\n error: saveError instanceof Error ? saveError.message : String(saveError),\n });\n }\n }\n }\n } else if (!selectedModelId) {\n // Neither preference exists, so the SDK's built-in mode default is this\n // thread's model of record. Pin it too: a later SDK upgrade that moves\n // that default must not silently retarget a thread that already started.\n const currentModelId = session.model.get();\n if (currentModelId) {\n await session.model.saveForMode({ modeId: session.mode.get(), modelId: currentModelId });\n }\n }\n\n // Subagents follow the sender's pack like the TUI does (explore→fast,\n // plan→plan, execute→build); roles the pack leaves empty use the factory\n // default. Pinned on the thread like the main model, so a restart\n // restores these rather than whatever pack or default exists by then.\n const packSubagentModels = {\n explore: userPackModels?.fast,\n plan: userPackModels?.plan,\n execute: userPackModels?.build,\n };\n for (const agentType of SUBAGENT_TYPES) {\n const modelId = packSubagentModels[agentType] || factoryModelId;\n if (!modelId) continue;\n if (await applySubagentModel(session, agentType, modelId)) {\n await session.thread.setSetting({ key: pinnedSubagentModelKey(agentType), value: modelId });\n }\n }\n }\n\n // The sender's own observational-memory settings, applied last so they beat\n // the project's — and on EVERY start, not just the first. Unlike the model,\n // this is stored preference rather than a choice made on this thread: a\n // restarted process re-resolves the project row before this hook runs, so\n // skipping it here would quietly put a thread back on the project's OM\n // configuration. Chat-only threads never reach this point.\n await applyPersonalMemorySettings(session, {\n memorySettings,\n orgId: owner.orgId,\n userId: owner.userId,\n });\n };\n}\n\nconst SUBAGENT_TYPES = ['explore', 'plan', 'execute'] as const;\nconst pinnedSubagentModelKey = (agentType: string) => `slackSubagentModelId_${agentType}`;\n\n/** Best-effort: a subagent model that can't be applied must not fail the message. */\nasync function applySubagentModel(\n session: Parameters<ChannelSessionStart>[0]['session'],\n agentType: (typeof SUBAGENT_TYPES)[number],\n modelId: string,\n): Promise<boolean> {\n try {\n await session.subagents.model.set({ modelId, agentType });\n return true;\n } catch (error) {\n console.warn('[slack] Failed to apply the subagent model', {\n agentType,\n modelId,\n error: error instanceof Error ? error.message : String(error),\n });\n return false;\n }\n}\n\n/**\n * The models of the sender's active model pack — the models that user chose\n * for themselves — or `undefined` when they have no pack.\n *\n * Best-effort by design: an uninitialized model-packs domain, a read failure, or\n * a pack saved without a build model all mean \"no personal preference\", which\n * falls through to the factory default rather than failing the dispatch.\n */\nasync function resolveActivePackModels(\n modelPacks: ModelPacksStorage | undefined,\n owner: { orgId: string; userId: string },\n): Promise<ActiveModelPackRecord['models'] | undefined> {\n if (!modelPacks) return undefined;\n try {\n const active = await modelPacks.getActive({ orgId: owner.orgId, userId: owner.userId });\n return active?.models;\n } catch (error) {\n console.warn('[slack] model pack lookup failed for a new session', {\n orgId: owner.orgId,\n userId: owner.userId,\n error: error instanceof Error ? error.message : String(error),\n });\n return undefined;\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\nasync function prepareExistingSessionOwnerContext(\n thread: HandlerThread,\n deps: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n options: { expectedOrgId?: string; requireInternalThread: boolean },\n): Promise<'ready' | 'organization-mismatch'> {\n const sourceControls = configuredSourceControls(deps);\n if (sourceControls.length === 0 && !options.requireInternalThread) return 'ready';\n\n const internalThread = await findInternalThread(ctx.mastra, thread);\n if (!internalThread) {\n if (options.requireInternalThread) {\n throw new Error(`Could not resolve the internal Slack thread for ${thread.id}.`);\n }\n return 'ready';\n }\n if (sourceControls.length === 0 || internalThread.resourceId.startsWith('channel:')) return 'ready';\n if (!options.expectedOrgId) {\n throw new Error(`Could not authorize the owner of Slack Factory session ${internalThread.resourceId}.`);\n }\n\n const preparation = await prepareSessionRunContext(\n ctx.requestContext,\n internalThread.resourceId,\n { sessions: createSourceControlSessionLookup(sourceControls) },\n { expectedOrgId: options.expectedOrgId },\n );\n if (preparation === 'organization-mismatch') return preparation;\n if (preparation === 'unavailable') {\n throw new Error(`Could not authorize the owner of Slack Factory session ${internalThread.resourceId}.`);\n }\n return 'ready';\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 deps: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n options: { requireInternalThread: boolean } = { requireInternalThread: false },\n): Promise<{\n routed?: { link: ChannelAccountLink; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n} | null> {\n const { accountLinks, projects } = deps;\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 const ownerContext = await prepareExistingSessionOwnerContext(thread, deps, ctx, {\n expectedOrgId: sender.link.orgId,\n requireInternalThread: options.requireInternalThread,\n });\n if (ownerContext === 'organization-mismatch') {\n await thread.post('This thread belongs to a Factory session in another organization.');\n return null;\n }\n // Credential resolution reads an in-memory snapshot that only priming\n // fills, so prime whichever tenant now owns the run (the sender, or the\n // existing session's owner) — otherwise a cold snapshot after a restart\n // fails the run until that user happens to hit the web UI.\n await primeTenantCredentialsForRequestContext(ctx.requestContext);\n\n const route = await resolveFactoryForLink({ thread, ...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 * A channel id and a `ts` name a thread only inside the workspace that issued\n * them, so the team scopes the card's key — without it two workspaces could\n * hold one key and an aside would land on another tenant's card.\n */\nexport function slackThreadSource(thread: HandlerThread, teamId?: string): ExternalWorkItemSource {\n return {\n integrationId: thread.adapter.name,\n type: 'slack-thread',\n ...(teamId ? { workspaceId: teamId } : {}),\n externalId: thread.id,\n };\n}\n\n/** Cards written before the key carried a team are still keyed bare. */\nasync function findThreadWorkItem(\n workItems: WorkItemsStorage,\n thread: HandlerThread,\n teamId?: string,\n): Promise<WorkItemRow | null> {\n const scoped = await workItems.getBySource(slackThreadSource(thread, teamId));\n if (scoped || !teamId) return scoped;\n return workItems.getBySource(slackThreadSource(thread));\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 = workItemTitleFrom(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 externalSource: { ...slackThreadSource(thread, slackTeamId(message)), ...(url ? { url } : {}) },\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 preDispatchErrorDetails(error: unknown): { message: string; cause?: { message: string } } {\n const readMessage = (value: unknown) => {\n const message =\n typeof value === 'string'\n ? value\n : value && typeof value === 'object' && 'message' in value\n ? value.message\n : undefined;\n return typeof message === 'string' && message.length > 0 ? message : undefined;\n };\n try {\n const message = readMessage(error) ?? 'Unknown error';\n const cause = error && typeof error === 'object' && 'cause' in error ? readMessage(error.cause) : undefined;\n return { message, ...(cause ? { cause: { message: cause } } : {}) };\n } catch {\n return { message: 'Error details unavailable' };\n }\n}\n\nasync function reportPreDispatchError(\n thread: HandlerThread,\n message: HandlerMessage,\n ctx: ChannelHandlerContext,\n error: unknown,\n): Promise<void> {\n try {\n if (error instanceof ChannelSessionRejectedError) return;\n } catch {\n // A thrown proxy can fail even the refusal check; still report the failure.\n }\n const correlation = {\n platform: thread.adapter.name,\n threadId: thread.id,\n messageId: message.id,\n authorId: message.author.userId,\n };\n const logger = typeof ctx.mastra?.getLogger === 'function' ? ctx.mastra.getLogger() : undefined;\n const logError = (line: string) => (logger ? logger.error(line) : console.error(line));\n logError(`[slack] Pre-dispatch failure ${JSON.stringify({ ...correlation, error: preDispatchErrorDetails(error) })}`);\n try {\n // The message id is the lookup key for the diagnostic above.\n await thread.post(`Couldn’t start processing your message. Please try again.\\n\\`messageId: ${message.id}\\``);\n } catch (deliveryError) {\n logError(\n `[slack] Failed to deliver pre-dispatch error reply ${JSON.stringify({ ...correlation, error: preDispatchErrorDetails(deliveryError) })}`,\n );\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 let gate: Awaited<ReturnType<typeof gateDispatch>>;\n let isNewSession: boolean;\n try {\n 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 isNewSession = !(await thread.isSubscribed());\n } catch (error) {\n await reportPreDispatchError(thread, message, ctx, error);\n return;\n }\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}\n\n/**\n * An `aside` never reaches the agent, but it is still part of the conversation:\n * record it on the card the thread created. The link is resolved here rather\n * than through `gateDispatch`, which posts a Connect card an aside must not\n * trigger. Best-effort — nothing here may surface in the Slack thread.\n */\nasync function ingestAside(\n thread: HandlerThread,\n message: HandlerMessage,\n { feed, workItems, accountLinks }: SlackChannelDeps,\n): Promise<void> {\n if (!feed || !workItems) return;\n const body = resolveEmojiShortcodes(message.text.replace(/^aside\\b[,:]?\\s*/i, '').trim());\n if (!body) return;\n try {\n const teamId = slackTeamId(message);\n const workItem = await findThreadWorkItem(workItems, thread, teamId);\n if (!workItem) return;\n\n const external: FactoryActorExternalIdentity = {\n platform: thread.adapter.name,\n ...(teamId ? { teamId } : {}),\n messageId: message.id,\n userId: message.author.userId,\n userName: message.author.userName,\n fullName: message.author.fullName,\n isBot: message.author.isBot,\n };\n const link =\n accountLinks && teamId\n ? await accountLinks.getAccountLink({\n platform: external.platform,\n externalTeamId: teamId,\n externalUserId: external.userId,\n })\n : null;\n\n const result = await feed.createComment({\n orgId: workItem.orgId,\n workItemId: workItem.id,\n author: actorFromChannelAuthor(external, link),\n body,\n occurredAt: message.metadata.dateSent,\n externalSource: slackCommentSource(thread.id, message.id, teamId),\n });\n if (result.status !== 'created') {\n console.warn('[slack] aside not ingested', { thread: thread.id, messageTs: message.id, status: result.status });\n }\n } catch (error) {\n // Slack was acked before this handler ran, so a lost aside is never redelivered.\n console.warn('[slack] aside ingest failed', { thread: thread.id, messageTs: message.id, error });\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 ingestAside(thread, message, deps);\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 try {\n const gate = await gateDispatch(thread, message, deps, ctx, { requireInternalThread: true });\n if (!gate) return;\n } catch (error) {\n await reportPreDispatchError(thread, message, ctx, error);\n return;\n }\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 // Per HTTP attempt, not per call: the WebClient's own retry policy still\n // waits out a 429. Without it a dead socket hangs a post forever.\n const adapter = createSlackAdapter({ ...deps.slack, webClientOptions: { timeout: SLACK_REQUEST_TIMEOUT_MS } });\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 resolveSession: createChannelSessionResolver(deps),\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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"}
|
|
@@ -18,7 +18,8 @@ export interface SandboxCommandResult {
|
|
|
18
18
|
* started, or for providers with no separate physical id); it is carried here
|
|
19
19
|
* so callers can persist it for deterministic reattach on resume.
|
|
20
20
|
*/
|
|
21
|
-
export type ExecutableSandbox = Pick<WorkspaceSandbox, 'id'
|
|
21
|
+
export type ExecutableSandbox = Pick<WorkspaceSandbox, 'id'> & {
|
|
22
|
+
sandboxId?: string;
|
|
22
23
|
/**
|
|
23
24
|
* Runs a command. The options are core's, but the result is only the part
|
|
24
25
|
* these helpers read: core's `CommandResult` also carries `success` and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"materialization.d.ts","sourceRoot":"","sources":["../../src/sandbox/materialization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAEtF,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,IAAI,GAAG,
|
|
1
|
+
{"version":3,"file":"materialization.d.ts","sourceRoot":"","sources":["../../src/sandbox/materialization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAEtF,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAAG;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClH,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,iBAAiB,CAOxE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"materialization.js","names":[],"sources":["../../src/sandbox/materialization.ts"],"sourcesContent":["import type { ExecuteCommandOptions, WorkspaceSandbox } from '@mastra/core/workspace';\n\n/** Result of one command executed inside a sandbox. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * A sandbox that can run commands.\n *\n * `executeCommand` is optional on `WorkspaceSandbox` because some providers\n * only offer a filesystem. Every git and materialization helper here needs it,\n * so this type promotes it to required. `id` is taken from core; the command\n * signature is spelled out because core's `CommandResult` is wider than what\n * these helpers read (see below).\n *\n * `sandboxId` is the provider's physical, reattachable VM id (undefined until\n * started, or for providers with no separate physical id); it is carried here\n * so callers can persist it for deterministic reattach on resume.\n */\nexport type ExecutableSandbox = Pick<WorkspaceSandbox, 'id'
|
|
1
|
+
{"version":3,"file":"materialization.js","names":[],"sources":["../../src/sandbox/materialization.ts"],"sourcesContent":["import type { ExecuteCommandOptions, WorkspaceSandbox } from '@mastra/core/workspace';\n\n/** Result of one command executed inside a sandbox. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * A sandbox that can run commands.\n *\n * `executeCommand` is optional on `WorkspaceSandbox` because some providers\n * only offer a filesystem. Every git and materialization helper here needs it,\n * so this type promotes it to required. `id` is taken from core; the command\n * signature is spelled out because core's `CommandResult` is wider than what\n * these helpers read (see below).\n *\n * `sandboxId` is the provider's physical, reattachable VM id (undefined until\n * started, or for providers with no separate physical id); it is carried here\n * so callers can persist it for deterministic reattach on resume.\n */\nexport type ExecutableSandbox = Pick<WorkspaceSandbox, 'id'> & {\n sandboxId?: string;\n /**\n * Runs a command. The options are core's, but the result is only the part\n * these helpers read: core's `CommandResult` also carries `success` and\n * `executionTimeMs`, and asking for them would reject test doubles that\n * report exactly what the helpers consume.\n */\n executeCommand(command: string, args?: string[], options?: ExecuteCommandOptions): Promise<SandboxCommandResult>;\n};\n\n/**\n * Narrow a sandbox to one that can run commands, or fail saying which\n * capability is missing.\n *\n * Callers hold a `WorkspaceSandbox`, whose `executeCommand` is optional.\n * Optional-chaining past it would hand every caller `undefined` where it\n * expects an exit code, turning a misconfigured provider into a git command\n * that silently did nothing.\n */\nexport function requireExec(sandbox: WorkspaceSandbox): ExecutableSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not support executeCommand, which is required to run git and filesystem operations in a session.`,\n );\n }\n return sandbox as ExecutableSandbox;\n}\n"],"mappings":";;;;;;;;;;AA0CA,SAAgB,YAAY,SAA8C;CACxE,IAAI,OAAO,QAAQ,mBAAmB,YACpC,MAAM,IAAI,MACR,qBAAqB,QAAQ,SAAS,wGACxC;CAEF,OAAO;AACT"}
|
package/dist/workspace.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAE/E,OAAO,KAAK,EAAE,mBAAmB,EAA2B,MAAM,mCAAmC,CAAC;AAItG,OAAO,EAAoB,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACrE,OAAO,KAAK,EAEV,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
|
package/dist/workspace.js
CHANGED
|
@@ -2,7 +2,7 @@ import { timedPhase } from "./timing.js";
|
|
|
2
2
|
import { getFactoryAuthOrgId, getFactoryAuthUserFromContext, getFactoryAuthUserId } from "./auth.js";
|
|
3
3
|
import { mergeRequestNumberFromBranch, pullRequestNumberFromBranch } from "./work-item-branch.js";
|
|
4
4
|
import { getGithubPat } from "./integrations/github/pat.js";
|
|
5
|
-
import { registerGithubPatKind, registerGithubTokenInjector } from "./integrations/github/token-refresh.js";
|
|
5
|
+
import { registerGithubPatKind, registerGithubRefreshTarget, registerGithubTokenInjector, registerGithubTokenInjectorResolver } from "./integrations/github/token-refresh.js";
|
|
6
6
|
import { requireExec } from "./sandbox/materialization.js";
|
|
7
7
|
import { createSessionSetupHook, evictSessionSandbox, getSessionSandbox, hasFailedSetupCommand, recordFailedSetupCommand, resolveSessionWorkdir } from "./sandbox/session-sandbox.js";
|
|
8
8
|
import { DEFAULT_COMMAND_TIMEOUT_MS, SetupCommandError, checkoutSessionBranch, materializeRepo, runSetupCommand, runTeardownCommand } from "./integrations/github/sandbox.js";
|
|
@@ -282,11 +282,21 @@ function createWorkspaceFactory(options = {}) {
|
|
|
282
282
|
id: connection.installationId
|
|
283
283
|
})) throw new Error(`${sourceControl.id} installation ${connection.installationId} was not found`);
|
|
284
284
|
const repoFullName = repository.slug;
|
|
285
|
+
if (githubProvider) registerGithubRefreshTarget(requestContext, {
|
|
286
|
+
orgId: session.orgId,
|
|
287
|
+
repositoryId: repository.id
|
|
288
|
+
});
|
|
285
289
|
const runSetupOn = (target, workdir, gate) => runSessionSetup(requireExec(target), workdir, gate);
|
|
286
290
|
const guardedSetup = createSessionSetupHook(runSetupOn, session.id, repoFullName, projectRepository.setupCommand ?? void 0);
|
|
287
291
|
const setupHook = async (args) => {
|
|
288
292
|
if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) throw retiredError();
|
|
289
|
-
|
|
293
|
+
let setupError;
|
|
294
|
+
try {
|
|
295
|
+
await guardedSetup(args);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (!(error instanceof SetupCommandError)) throw error;
|
|
298
|
+
setupError = error;
|
|
299
|
+
}
|
|
290
300
|
if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) throw retiredError();
|
|
291
301
|
const target = requireExec(args.sandbox);
|
|
292
302
|
const publishStartSideEffects = async () => {
|
|
@@ -297,8 +307,12 @@ function createWorkspaceFactory(options = {}) {
|
|
|
297
307
|
});
|
|
298
308
|
constructedWorkspaces.get(workspaceId)?.skills?.refresh().catch(() => {});
|
|
299
309
|
};
|
|
300
|
-
|
|
310
|
+
const finishStart = async () => {
|
|
301
311
|
await publishStartSideEffects();
|
|
312
|
+
if (setupError) throw setupError;
|
|
313
|
+
};
|
|
314
|
+
if (!githubProvider) {
|
|
315
|
+
await finishStart();
|
|
302
316
|
return;
|
|
303
317
|
}
|
|
304
318
|
const existingRegistration = githubTokenInjectors.get(workspaceId);
|
|
@@ -312,7 +326,7 @@ function createWorkspaceFactory(options = {}) {
|
|
|
312
326
|
existingRegistration.ghToken = freshToken;
|
|
313
327
|
};
|
|
314
328
|
existingRegistration.inject(existingRegistration.ghToken);
|
|
315
|
-
await
|
|
329
|
+
await finishStart();
|
|
316
330
|
return;
|
|
317
331
|
}
|
|
318
332
|
const patKind = await resolveGithubPatKind("default");
|
|
@@ -337,7 +351,7 @@ function createWorkspaceFactory(options = {}) {
|
|
|
337
351
|
};
|
|
338
352
|
githubTokenInjectors.set(workspaceId, tokenRegistration);
|
|
339
353
|
registerGithubTokenContext(tokenRegistration);
|
|
340
|
-
await
|
|
354
|
+
await finishStart();
|
|
341
355
|
};
|
|
342
356
|
const constructSessionEntry = () => getSessionSandbox(session.id, repoFullName, () => {
|
|
343
357
|
const sandbox = createSessionSandboxInstance({
|
|
@@ -414,14 +428,25 @@ function createWorkspaceFactory(options = {}) {
|
|
|
414
428
|
return fallback;
|
|
415
429
|
}
|
|
416
430
|
};
|
|
431
|
+
const githubTokenInjector = (registered, generation) => (token) => {
|
|
432
|
+
if (githubTokenInjectors.get(workspaceId) !== registered || registered.generation !== generation) throw new Error("GitHub token refresh no longer matches the active Factory workspace role.");
|
|
433
|
+
registered.inject(token);
|
|
434
|
+
};
|
|
417
435
|
const registerGithubTokenContext = (registered) => {
|
|
418
|
-
|
|
419
|
-
registerGithubTokenInjector(requestContext, (token) => {
|
|
420
|
-
if (githubTokenInjectors.get(workspaceId) !== registered || registered.generation !== generation) throw new Error("GitHub token refresh no longer matches the active Factory workspace role.");
|
|
421
|
-
registered.inject(token);
|
|
422
|
-
});
|
|
436
|
+
registerGithubTokenInjector(requestContext, githubTokenInjector(registered, registered.generation));
|
|
423
437
|
registerGithubPatKind(requestContext, registered.patKind);
|
|
424
438
|
};
|
|
439
|
+
if (githubProvider) {
|
|
440
|
+
const registered = githubTokenInjectors.get(workspaceId);
|
|
441
|
+
if (registered) registerGithubTokenContext(registered);
|
|
442
|
+
else registerGithubTokenInjectorResolver(requestContext, () => {
|
|
443
|
+
if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) throw new Error("GitHub token refresh no longer matches the active Factory workspace role.");
|
|
444
|
+
const active = githubTokenInjectors.get(workspaceId);
|
|
445
|
+
if (!active) throw new Error("GitHub token refresh requires an active Factory sandbox workspace.");
|
|
446
|
+
registerGithubPatKind(requestContext, active.patKind);
|
|
447
|
+
return githubTokenInjector(active, 0);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
425
450
|
const reconcileGithubToken = async () => {
|
|
426
451
|
if (!githubProvider) return;
|
|
427
452
|
const reconciliation = (githubTokenReconciliations.get(workspaceId) ?? Promise.resolve()).catch(() => {}).then(async () => {
|
package/dist/workspace.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.js","names":["#bundledSource","#localSource","#fallbackSkillRoots","#isHiddenReviewPath","#layerFor","#isFactoryPath","#factoryPath","#entries","#generations"],"sources":["../src/workspace.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport path, { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';\nimport { MASTRACODE_WORKSPACE_TOOLS } from '@mastra/code-sdk/agents/tool-availability';\nimport type { getDynamicWorkspace, WorkspaceSkillExtension } from '@mastra/code-sdk/agents/workspace';\nimport { DEFAULT_CONFIG_DIR } from '@mastra/code-sdk/constants';\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport { LocalSkillSource, Workspace } from '@mastra/core/workspace';\nimport type {\n SandboxStartHook,\n SkillSource,\n SkillSourceEntry,\n SkillSourceStat,\n WorkspaceSandbox,\n} from '@mastra/core/workspace';\nimport { getFactoryAuthOrgId, getFactoryAuthUserFromContext, getFactoryAuthUserId } from './auth.js';\nimport type { VersionControl } from './capabilities/version-control.js';\nimport type { MastraFactorySandboxConfig } from './factory.js';\nimport type { GithubIntegration } from './integrations/github/integration.js';\nimport { getGithubPat } from './integrations/github/pat.js';\nimport type { GithubPatKind } from './integrations/github/pat.js';\nimport {\n checkoutSessionBranch,\n DEFAULT_COMMAND_TIMEOUT_MS,\n materializeRepo,\n runSetupCommand,\n runTeardownCommand,\n SetupCommandError,\n} from './integrations/github/sandbox.js';\nimport { registerGithubPatKind, registerGithubTokenInjector } from './integrations/github/token-refresh.js';\nimport { requireExec } from './sandbox/materialization.js';\nimport type { ExecutableSandbox } from './sandbox/materialization.js';\nimport {\n createSessionSetupHook,\n evictSessionSandbox,\n getSessionSandbox,\n hasFailedSetupCommand,\n recordFailedSetupCommand,\n resolveSessionWorkdir,\n} from './sandbox/session-sandbox.js';\nimport type { SessionSetupGate } from './sandbox/session-sandbox.js';\nimport type { FactoryProjectsStorage } from './storage/domains/projects/base.js';\nimport type { SourceControlSession, SourceControlStorageHandle } from './storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from './storage/domains/work-items/base.js';\nimport { parseSupervisorResourceId } from './supervisor/session.js';\nimport { timedPhase } from './timing.js';\nimport { mergeRequestNumberFromBranch, pullRequestNumberFromBranch } from './work-item-branch.js';\n\nconst WORKSPACE_ID_PREFIX = 'mfw';\nconst bundleDirectory = dirname(fileURLToPath(import.meta.url));\nconst bundledFactorySkillsPath = join(bundleDirectory, 'factory-skills');\nexport const BUNDLED_FACTORY_SKILLS_PATH =\n [\n // Deploy bundle: the consumer copies `factory-skills/` next to the built\n // server module (e.g. via its public/ dir).\n bundledFactorySkillsPath,\n // Package layout: `dist/../factory-skills` (also `src/../factory-skills`\n // when running tests against sources).\n join(bundleDirectory, '..', 'factory-skills'),\n ].find(existsSync) ?? bundledFactorySkillsPath;\n\n/**\n * Resolve the consumer repo's local Factory skills root, if any. Checked in\n * addition to the bundled skills so projects can add (or override) Factory\n * skills without patching the installed package. Candidates cover the cwd\n * variants the dev server runs with (`repo root`, `--dir src/mastra` which\n * runs with cwd `src/mastra/public`).\n */\nexport function resolveLocalFactorySkillsPath(cwd: string = process.cwd()): string | undefined {\n const candidates = [\n join(cwd, 'src', 'mastra', 'public', 'factory-skills'),\n join(cwd, 'public', 'factory-skills'),\n join(cwd, 'factory-skills'),\n ];\n return candidates.find(\n candidate => path.normalize(candidate) !== path.normalize(BUNDLED_FACTORY_SKILLS_PATH) && existsSync(candidate),\n );\n}\nconst FACTORY_SKILLS_MOUNT = path.resolve(path.parse(process.cwd()).root, '__mastracode_factory_skills__');\nexport const FACTORY_SKILL_NAMES = new Set([\n 'configure-factory-rules',\n 'factory-complete-issue',\n 'factory-gitlab-rereview',\n 'factory-gitlab-review',\n 'factory-plan',\n 'factory-rereview',\n 'factory-review',\n 'factory-triage',\n]);\n\n/** Review skills only sessions with an active review run binding may discover or load. */\nexport const REVIEW_ONLY_FACTORY_SKILLS = new Set([\n 'factory-gitlab-rereview',\n 'factory-gitlab-review',\n 'factory-rereview',\n 'factory-review',\n]);\n\n/**\n * Skill caches rescanned outside the workspace resolver. The resolver records\n * which role each cache was last scanned for; a rescan it did not perform makes\n * that record untrustworthy, so the next reuse must rescan again.\n */\nconst rescannedOutsideResolver = new WeakMap<object, Promise<void>>();\n\n/**\n * Rescan a session's skill cache so a role gained after the cache was built\n * (a review binding minted after the session's workspace resolved) takes effect.\n * The source still checks the live binding, so this never widens access.\n */\nexport async function rescanFactorySkills(skills: NonNullable<Workspace['skills']>): Promise<void> {\n // Registered so the resolver waits it out: a refresh started meanwhile would\n // join this scan (possibly for the old role) instead of starting a fresh one.\n const pending = skills.refresh();\n rescannedOutsideResolver.set(\n skills,\n pending.catch(() => {}),\n );\n await pending;\n}\n\nexport class FactorySkillSource implements SkillSource {\n readonly #bundledSource = new LocalSkillSource({ basePath: BUNDLED_FACTORY_SKILLS_PATH });\n readonly #localSource: LocalSkillSource | undefined;\n readonly #fallbackSkillRoots: Set<string>;\n\n constructor(\n readonly fallback: SkillSource,\n fallbackSkillRoots: string[],\n localSkillsPath: string | undefined = resolveLocalFactorySkillsPath(),\n /** Evaluated per call because a session's bound role changes across stages. */\n readonly isReviewSession: () => Promise<boolean> = () => Promise.resolve(true),\n ) {\n this.#localSource = localSkillsPath ? new LocalSkillSource({ basePath: localSkillsPath }) : undefined;\n this.#fallbackSkillRoots = new Set(fallbackSkillRoots.map(skillPath => path.normalize(skillPath)));\n }\n\n #isFactoryPath(skillPath: string): boolean {\n const normalized = path.normalize(skillPath);\n return normalized === FACTORY_SKILLS_MOUNT || normalized.startsWith(`${FACTORY_SKILLS_MOUNT}${path.sep}`);\n }\n\n #factoryPath(skillPath: string): string {\n return path.relative(FACTORY_SKILLS_MOUNT, path.normalize(skillPath));\n }\n\n /** True when the path is inside a review-only skill that this session may not use. */\n async #isHiddenReviewPath(relativePath: string): Promise<boolean> {\n const skillName = relativePath.split(path.sep)[0];\n if (!skillName || !REVIEW_ONLY_FACTORY_SKILLS.has(skillName)) return false;\n return !(await this.isReviewSession());\n }\n\n /** Pick the layer serving this mount-relative path: local wins when it has the entry. */\n async #layerFor(relativePath: string): Promise<LocalSkillSource> {\n if (this.#localSource && (await this.#localSource.exists(relativePath))) return this.#localSource;\n return this.#bundledSource;\n }\n\n async exists(skillPath: string): Promise<boolean> {\n if (!this.#isFactoryPath(skillPath)) return this.fallback.exists(skillPath);\n const relative = this.#factoryPath(skillPath);\n if (await this.#isHiddenReviewPath(relative)) return false;\n if (this.#localSource && (await this.#localSource.exists(relative))) return true;\n return this.#bundledSource.exists(relative);\n }\n\n async stat(skillPath: string): Promise<SkillSourceStat> {\n if (!this.#isFactoryPath(skillPath)) return this.fallback.stat(skillPath);\n const relative = this.#factoryPath(skillPath);\n if (await this.#isHiddenReviewPath(relative)) throw skillSourceEnoent(skillPath);\n return (await this.#layerFor(relative)).stat(relative);\n }\n\n async readFile(skillPath: string): Promise<string | Buffer> {\n if (!this.#isFactoryPath(skillPath)) return this.fallback.readFile(skillPath);\n const relative = this.#factoryPath(skillPath);\n if (await this.#isHiddenReviewPath(relative)) throw skillSourceEnoent(skillPath);\n return (await this.#layerFor(relative)).readFile(relative);\n }\n\n async readdir(skillPath: string): Promise<SkillSourceEntry[]> {\n if (this.#isFactoryPath(skillPath)) {\n const relative = this.#factoryPath(skillPath);\n if (await this.#isHiddenReviewPath(relative)) throw skillSourceEnoent(skillPath);\n const [bundledExists, localExists] = await Promise.all([\n this.#bundledSource.exists(relative),\n this.#localSource?.exists(relative) ?? Promise.resolve(false),\n ]);\n if (!bundledExists && !localExists) throw skillSourceEnoent(skillPath);\n const [bundledEntries, localEntries] = await Promise.all([\n bundledExists ? this.#bundledSource.readdir(relative) : [],\n localExists ? this.#localSource!.readdir(relative) : [],\n ]);\n const merged = new Map<string, SkillSourceEntry>();\n for (const entry of bundledEntries) merged.set(entry.name, entry);\n // Local entries override bundled names.\n for (const entry of localEntries) merged.set(entry.name, entry);\n const entries = [...merged.values()];\n if (relative !== '' || (await this.isReviewSession())) return entries;\n return entries.filter(entry => !REVIEW_ONLY_FACTORY_SKILLS.has(entry.name));\n }\n const entries = await this.fallback.readdir(skillPath);\n if (this.#fallbackSkillRoots.has(path.normalize(skillPath))) {\n return entries.filter(entry => !FACTORY_SKILL_NAMES.has(entry.name));\n }\n return entries;\n }\n\n realpath(skillPath: string): Promise<string> {\n if (this.#isFactoryPath(skillPath)) return Promise.resolve(path.normalize(skillPath));\n return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);\n }\n}\n\n/** Build a Node-style ENOENT error so callers can treat missing skills like fs misses. */\nfunction skillSourceEnoent(skillPath: string): Error {\n const error = new Error(`ENOENT: no such file or directory, '${skillPath}'`) as Error & { code: string };\n error.code = 'ENOENT';\n return error;\n}\n\n/**\n * Sandbox-backed skill fallback that stays inert until the session sandbox is\n * actually materialized. Skill discovery runs on latency-sensitive paths (the\n * Factory start coordinator resolves the kickoff skill before the start route\n * responds); without this guard the first project-root read would hit the lazy\n * sandbox handle and force full provisioning + repo materialization. While the\n * sandbox is unmaterialized, project skill roots simply appear empty — bundled\n * Factory skills resolve from local disk via `FactorySkillSource`. Once the\n * sandbox exists, every call delegates straight through.\n */\nclass UnmaterializedAwareSkillSource implements SkillSource {\n constructor(\n readonly fallback: SkillSource,\n readonly isMaterialized: () => boolean,\n ) {}\n\n async exists(skillPath: string): Promise<boolean> {\n return this.isMaterialized() ? this.fallback.exists(skillPath) : false;\n }\n\n async stat(skillPath: string): Promise<SkillSourceStat> {\n if (!this.isMaterialized()) throw skillSourceEnoent(skillPath);\n return this.fallback.stat(skillPath);\n }\n\n async readFile(skillPath: string): Promise<string | Buffer> {\n if (!this.isMaterialized()) throw skillSourceEnoent(skillPath);\n return this.fallback.readFile(skillPath);\n }\n\n async readdir(skillPath: string): Promise<SkillSourceEntry[]> {\n return this.isMaterialized() ? this.fallback.readdir(skillPath) : [];\n }\n\n realpath(skillPath: string): Promise<string> {\n if (!this.isMaterialized()) return Promise.resolve(skillPath);\n return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);\n }\n}\n\nfunction createFactorySkillExtension(isReviewSession: () => Promise<boolean>): WorkspaceSkillExtension {\n return {\n id: 'web-factory',\n paths: [FACTORY_SKILLS_MOUNT],\n createSource: (fallback, fallbackSkillRoots) =>\n new FactorySkillSource(fallback, fallbackSkillRoots, resolveLocalFactorySkillsPath(), isReviewSession),\n };\n}\n\nfunction isActiveReviewBinding(\n runBinding: { role: string; status: string; orgId: string } | null | undefined,\n orgId: string | undefined,\n): boolean {\n return runBinding?.role === 'review' && runBinding.status === 'active' && !!orgId && runBinding.orgId === orgId;\n}\n\ntype DynamicWorkspaceContext = Parameters<typeof getDynamicWorkspace>[0];\n\n/**\n * When a session's sandbox boots: on the agent's first command (`'lazy'`, the\n * default) or as soon as the session's workspace is first resolved (`'eager'`).\n * An eager start is fire-and-forget; if it fails, the lazy path still runs.\n */\nexport type FactorySandboxStart = 'lazy' | 'eager';\n\nexport interface WorkspaceSourceControlProvider {\n /** Stable integration id used to select the provider-owned storage partition. */\n id: string;\n /** Provider capability used to resolve fresh repository credentials. */\n versionControl: Pick<VersionControl, 'getRepositoryAccess'>;\n /** Provider-owned source-control rows, including Factory sessions. */\n storage: SourceControlStorageHandle;\n /** Present only for GitHub, whose CLI PAT rotation has additional semantics. */\n github?: GithubIntegration;\n}\n\nexport interface CreateWorkspaceFactoryOptions {\n /** Factory sandbox runtime config (session sandbox callback). */\n sandbox?: MastraFactorySandboxConfig;\n /** Defaults to `'lazy'`. */\n sandboxStart?: FactorySandboxStart;\n /** Source-control providers whose repositories may back Factory sessions. */\n sourceControls?: WorkspaceSourceControlProvider[];\n /** @deprecated GitHub-only compatibility path; prefer sourceControls. */\n github?: GithubIntegration;\n /** Work-items storage used to resolve the session's run-binding role, so\n * review-board sessions get the reviewer PAT as `GH_TOKEN`. Optional —\n * without it every session uses the default (worker) PAT. */\n workItems?: Pick<WorkItemsStorage, 'findActiveRunBindingForSession'>;\n /** Projects storage used to authorize workspace-free supervisor sessions. */\n projects?: Pick<FactoryProjectsStorage, 'get'>;\n /** Runtime workspace/token registrations invalidated when a session retires. */\n workspaceRegistry?: FactoryWorkspaceRegistry;\n}\n\ntype WorkspaceUnregister = () => Promise<void> | void;\n\n/** Tracks dynamic Factory workspaces by persisted session id for retirement. */\nexport class FactoryWorkspaceRegistry {\n readonly #entries = new Map<string, Map<string, WorkspaceUnregister>>();\n readonly #generations = new Map<string, number>();\n\n generation(sessionId: string): number {\n return this.#generations.get(sessionId) ?? 0;\n }\n\n async register(\n sessionId: string,\n workspaceId: string,\n generation: number,\n unregister: WorkspaceUnregister,\n ): Promise<boolean> {\n if (generation !== this.generation(sessionId)) {\n await unregister();\n return false;\n }\n const entries = this.#entries.get(sessionId) ?? new Map<string, WorkspaceUnregister>();\n entries.set(workspaceId, unregister);\n this.#entries.set(sessionId, entries);\n return true;\n }\n\n async invalidateSession(sessionId: string): Promise<void> {\n this.#generations.set(sessionId, this.generation(sessionId) + 1);\n const entries = this.#entries.get(sessionId);\n if (!entries) return;\n this.#entries.delete(sessionId);\n const results = await Promise.allSettled([...entries.values()].map(unregister => unregister()));\n const failure = results.find(result => result.status === 'rejected');\n if (failure?.status === 'rejected') throw failure.reason;\n }\n}\n\nasync function resolveSourceControlSession(\n providers: WorkspaceSourceControlProvider[],\n sessionId: string,\n): Promise<{ provider: WorkspaceSourceControlProvider; session: SourceControlSession } | null> {\n const matches = (\n await Promise.all(\n providers.map(async provider => ({\n provider,\n session: await provider.storage.sessions.getBySessionId(sessionId),\n })),\n )\n ).filter(\n (match): match is { provider: WorkspaceSourceControlProvider; session: SourceControlSession } =>\n match.session !== null,\n );\n if (matches.length > 1) {\n throw new Error(`Factory session ${sessionId} is ambiguous across source-control providers`);\n }\n return matches[0] ?? null;\n}\n\nexport function createWorkspaceFactory(options: CreateWorkspaceFactoryOptions = {}) {\n const { sandbox: sandboxConfig, github, projects, workItems } = options;\n const sourceControls: WorkspaceSourceControlProvider[] =\n options.sourceControls ??\n (github\n ? [{ id: github.id, versionControl: github.versionControl, storage: github.sourceControlStorage, github }]\n : []);\n const eagerSandboxStart = options.sandboxStart === 'eager';\n const workspaceRegistry = options.workspaceRegistry ?? new FactoryWorkspaceRegistry();\n type GithubTokenRegistration = {\n inject: (token: string) => void;\n patKind: GithubPatKind;\n ghToken: string;\n generation: number;\n tokenReplacementPending: boolean;\n };\n // The session setup path runs commands and installs credentials, so it\n // needs `executeCommand` (required by `ExecutableSandbox`) plus core's\n // optional `setEnv`, which stays optional here because the token-refresh\n // path checks for it and reports its absence.\n type SessionSandbox = ExecutableSandbox & { setEnv?: WorkspaceSandbox['setEnv'] };\n const githubTokenInjectors = new Map<string, GithubTokenRegistration>();\n const githubTokenReconciliations = new Map<string, Promise<void>>();\n // Workspace identity cache: concurrent resolutions of the same session must\n // observe the same Workspace object even when no Mastra registry is wired\n // (the registry stays the source of truth when present).\n const constructedWorkspaces = new Map<string, Workspace>();\n // Review-skill visibility last used to populate each workspace's skill cache.\n const skillCacheReviewState = new Map<string, boolean>();\n const skillCacheRefreshes = new Map<string, Promise<void>>();\n // The review check of the latest request to resolve each workspace. The skill\n // source outlives the request that built it, so it must gate on this rather\n // than on the context it was constructed with.\n const skillReviewChecks = new Map<string, () => Promise<boolean>>();\n\n return async ({ requestContext, mastra, skillExtension }: DynamicWorkspaceContext) => {\n const ctx = requestContext.get('controller') as AgentControllerRequestContext<MastraCodeState> | undefined;\n const supervisorProjectId = parseSupervisorResourceId(ctx?.resourceId);\n if (supervisorProjectId) {\n const orgId = getFactoryAuthOrgId(getFactoryAuthUserFromContext(requestContext));\n const project = orgId && projects ? await projects.get({ orgId, id: supervisorProjectId }) : null;\n if (!project) throw new Error(`Factory supervisor ${supervisorProjectId} is not available to the current user`);\n return undefined;\n }\n const resolvedSession = ctx?.resourceId ? await resolveSourceControlSession(sourceControls, ctx.resourceId) : null;\n const session = resolvedSession?.session ?? null;\n const sourceControl = resolvedSession?.provider;\n\n if (!session || !sourceControl) {\n // No factory session, no workspace. Chat still works; workspace tools\n // are simply not registered. Host-cwd behavior is opt-in via a\n // LocalSandbox callback rooted wherever the deployer wants — the\n // resolver never hands out the server host's own filesystem.\n return undefined;\n }\n\n const user = getFactoryAuthUserFromContext(requestContext);\n const userId = getFactoryAuthUserId(user);\n // No identity at all is a server-side caller that forgot to seed one\n // (webhook, cron), not someone reaching for another user's session.\n if (!user?.organizationId || !userId) {\n throw new Error(`Factory session ${session.sessionId} was resolved without a caller identity`);\n }\n // Org-visible sessions open to any member of the owning organization;\n // only private sessions stay owner-only. Cross-org access never passes.\n if (user.organizationId !== session.orgId || (session.visibility === 'private' && userId !== session.userId)) {\n throw new Error(`Factory session ${session.sessionId} is not available to the current user`);\n }\n if (!sandboxConfig) {\n throw new Error('A sandbox callback is required to create a Factory session workspace');\n }\n const createSessionSandboxInstance = sandboxConfig;\n const githubProvider = sourceControl.github;\n\n const storage = sourceControl.storage;\n const projectRepository = await storage.projectRepositories.get({\n orgId: session.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) throw new Error(`Repository link ${session.projectRepositoryId} was not found`);\n // The remaining reads only depend on the repository link — issue them in\n // parallel instead of paying four sequential storage round-trips.\n const [connection, repository] = await Promise.all([\n storage.connections.get({ orgId: session.orgId, id: projectRepository.connectionId }),\n storage.repositories.get({ orgId: session.orgId, id: projectRepository.repositoryId }),\n ]);\n if (!connection || !repository) throw new Error(`Repository link ${session.projectRepositoryId} is incomplete`);\n const installation = await storage.installations.get({ orgId: session.orgId, id: connection.installationId });\n if (!installation) {\n throw new Error(`${sourceControl.id} installation ${connection.installationId} was not found`);\n }\n const repoFullName = repository.slug;\n\n // Construct (or fetch) the session's memoized sandbox instance.\n // Construction is cheap and side-effect-free by the callback contract —\n // the VM is provisioned on `start()`, which only the materialization\n // pipeline calls. The workdir is never persisted or trusted from storage\n // or client input (the stale-workdir incident class came from reusing\n // `session.sandboxWorkdir` written under a different provider): local\n // sandboxes derive it at construction, remote sandboxes clone into the\n // VM's own home so it resolves lazily at first start.\n // `runSetupOn` references `runSessionSetup`, defined below — it is only\n // invoked during start, long after this closure fully initializes.\n const runSetupOn = (target: unknown, workdir: string, gate: SessionSetupGate) =>\n runSessionSetup(requireExec(target as WorkspaceSandbox), workdir, gate);\n const guardedSetup = createSessionSetupHook(\n runSetupOn,\n session.id,\n repoFullName,\n projectRepository.setupCommand ?? undefined,\n );\n // Composed start hook: marker-guarded repo setup, then per-start\n // credential install. It runs inside the provider's start lifecycle on\n // EVERY start (create or reconnect) — providers own lazy start\n // (`ensureRunning()` on first command) and dead-VM self-healing (E2B\n // `retryOnDead`, Platform status reset on destroy), so a replacement VM\n // re-enters this hook and heals itself with credentials at least as\n // fresh as the start that installed them.\n const setupHook: SandboxStartHook = async args => {\n // A session retired before its first start must not set anything up.\n if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) {\n throw retiredError();\n }\n await guardedSetup(args);\n // Re-check after the (long) setup: a session retired mid-setup must not\n // register credentials for a workspace whose retirement teardown has\n // already run — the entry would leak forever. The VM itself is left to\n // the provider's idle timeout (accepted).\n if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) {\n throw retiredError();\n }\n const target: SessionSandbox = requireExec(args.sandbox);\n // Sandbox persistence plus the post-checkout skill rescan run on every\n // start (create or reconnect). The persisted sandbox id is read back on\n // resume to reattach; the workdir was resolved (and memoized on the\n // entry) by the guarded setup. The skill roots were reported empty by the\n // unmaterialized-source guard before the checkout existed, so rescan now.\n // The physical-id write is awaited and its failure propagates: a start\n // that completes before the id is durable lets a concurrent resume read a\n // stale id and provision a replacement VM.\n const publishStartSideEffects = async () => {\n await storage.sessions.setSandbox({\n id: session.id,\n // Persist the provider's PHYSICAL, reattachable VM id so resume can\n // reattach to the same VM. Providers with no separate physical id\n // (e.g. local) fall back to the logical id, preserving prior behavior.\n sandboxId: target.sandboxId ?? target.id,\n sandboxWorkdir: sessionEntry.workdir ?? '',\n });\n void constructedWorkspaces\n .get(workspaceId)\n ?.skills?.refresh()\n .catch(() => {});\n };\n if (!githubProvider) {\n await publishStartSideEffects();\n return;\n }\n const existingRegistration = githubTokenInjectors.get(workspaceId);\n if (existingRegistration) {\n // Reconnect: re-point the current registration's injection target at\n // this (possibly provider-healed) sandbox and reinstall its\n // reconcile-owned credential. Do NOT mint a new registration, bump\n // authority, or re-register the constructing request context —\n // reconcileGithubToken owns role/generation and the active context's\n // injector, so replacing them here would reauthorize the stale\n // constructing context and reject the current one.\n existingRegistration.inject = freshToken => {\n if (!target.setEnv) {\n throw new Error('The active sandbox provider does not support runtime GitHub token refresh.');\n }\n target.setEnv(env => ({ ...env, GH_TOKEN: freshToken }));\n existingRegistration.ghToken = freshToken;\n };\n // Install through inject (not optional-chained setEnv) so a provider\n // that cannot accept the credential fails the reconnect here instead of\n // deferring the failure to a later token refresh.\n existingRegistration.inject(existingRegistration.ghToken);\n await publishStartSideEffects();\n return;\n }\n // First start: resolve the credential and authorize the constructing\n // request context. The `gh` CLI needs a PAT when the org configured one\n // (installation tokens 403 on integration-restricted endpoints); git\n // clone/checkout keep using the minted installation token. Resolved per\n // start so the installed credential never outlives rotation.\n const patKind = await resolveGithubPatKind('default');\n const ghCliToken =\n (await getGithubPat(() => githubProvider.integrationStorage, session.orgId, patKind)) ??\n (await getRepositoryToken());\n target.setEnv?.(env => ({ ...env, GH_TOKEN: ghCliToken }));\n const tokenRegistration: GithubTokenRegistration = {\n inject: freshToken => {\n if (!target.setEnv) {\n throw new Error('The active sandbox provider does not support runtime GitHub token refresh.');\n }\n target.setEnv(env => ({ ...env, GH_TOKEN: freshToken }));\n tokenRegistration.ghToken = freshToken;\n },\n patKind,\n ghToken: ghCliToken,\n generation: 0,\n tokenReplacementPending: false,\n };\n githubTokenInjectors.set(workspaceId, tokenRegistration);\n registerGithubTokenContext(tokenRegistration);\n await publishStartSideEffects();\n };\n const constructSessionEntry = () =>\n getSessionSandbox(session.id, repoFullName, () => {\n const sandbox = createSessionSandboxInstance({\n sessionId: session.id,\n // Physical VM id persisted from a prior start (undefined on first\n // start). Providers that reattach by physical id use it to resume the\n // original VM instead of provisioning a replacement.\n sandboxId: session.sandboxId ?? undefined,\n repoFullName,\n // Stored nullable; the context speaks `undefined` for absent.\n setupCommand: projectRepository.setupCommand ?? undefined,\n // Deferred call — only dereferenced when a provider needs the repo\n // outside the VM (template build time).\n getRepositoryAccess: () =>\n sourceControl.versionControl.getRepositoryAccess({\n orgId: session.orgId,\n repositoryId: repository.id,\n }),\n });\n // Attached inside the construction closure, so exactly once per\n // instance — `constructSessionEntry` runs on every open and would\n // stack a wrapper per call. Factory's setup runs first: a hook the\n // callback installed itself expects a prepared workspace.\n sandbox.setOnStart(previous => async args => {\n await timedPhase(`workspace.onStart(${args.outcome})`, async () => {\n await setupHook(args);\n });\n await previous?.(args);\n });\n // Only a freshly constructed instance starts eagerly; the start itself\n // waits for this resolver to finish because the start hook reads\n // bindings declared further down.\n startEagerly = eagerSandboxStart;\n return sandbox;\n });\n let startEagerly = false;\n const fireEagerStart = () => {\n if (!startEagerly) return;\n startEagerly = false;\n Promise.resolve()\n .then(() => sessionEntry.sandbox.start?.())\n .catch(error => {\n console.warn(`[factory] Eager sandbox start for session ${session.id} failed:`, error);\n });\n };\n const sessionEntry = constructSessionEntry();\n const workdir = sessionEntry.workdir;\n const isLocalSandbox = sessionEntry.sandbox.provider === 'local';\n // The SDK system prompt uses `state.projectPath` without falling back to\n // the server cwd. Pin the session workdir once known so the prompt and\n // workspace tools describe the same checkout. A remote workdir resolves at\n // the sandbox's first start, so the pin self-heals on the next resolution.\n if (ctx && workdir && ctx.getState()?.projectPath !== workdir) {\n await ctx.setState({ projectPath: workdir, projectName: repoFullName });\n }\n\n // Keyed by the session, not the request's thread: a workspace built for a\n // dispatcher kickoff has no live thread yet, but the session's binding\n // already says which role it is serving. When the request does carry a\n // thread, the binding must belong to it, so a stale thread reusing this\n // session never inherits another run's role.\n const findSessionBinding = async () => {\n const binding = await workItems!.findActiveRunBindingForSession({\n orgId: session.orgId,\n factoryProjectId: connection.factoryProjectId,\n sessionId: session.sessionId,\n });\n if (!binding || !ctx?.threadId) return binding;\n return binding.threadId === ctx.threadId && binding.resourceId === ctx.resourceId ? binding : null;\n };\n // Fails closed: without a readable active review binding, review skills stay hidden.\n const isReviewSession = async (): Promise<boolean> => {\n if (!workItems) return false;\n try {\n return isActiveReviewBinding(await findSessionBinding(), session.orgId);\n } catch {\n return false;\n }\n };\n const effectiveSkillExtension =\n skillExtension ??\n createFactorySkillExtension((): Promise<boolean> => (skillReviewChecks.get(workspaceId) ?? isReviewSession)());\n const extensionId = effectiveSkillExtension ? `-${effectiveSkillExtension.id}` : '';\n const workspaceId: string = `${WORKSPACE_ID_PREFIX}-${projectRepository.id}-${session.id}${extensionId}`;\n const workspaceGeneration = workspaceRegistry.generation(session.sessionId);\n if (!skillExtension) skillReviewChecks.set(workspaceId, isReviewSession);\n const configDir = DEFAULT_CONFIG_DIR;\n\n const getRepositoryAccess = () =>\n sourceControl.versionControl.getRepositoryAccess({\n orgId: session.orgId,\n repositoryId: repository.id,\n });\n const getRepositoryToken = async (): Promise<string> => {\n const token = (await getRepositoryAccess()).authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session');\n return token;\n };\n const resolveGithubPatKind = async (fallback: GithubPatKind): Promise<GithubPatKind> => {\n if (!workItems) return 'default';\n try {\n return isActiveReviewBinding(await findSessionBinding(), session.orgId) ? 'reviewer' : 'default';\n } catch {\n // Preserve the installed role when binding storage is temporarily unavailable.\n return fallback;\n }\n };\n const registerGithubTokenContext = (registered: GithubTokenRegistration): void => {\n const generation = registered.generation;\n registerGithubTokenInjector(requestContext, token => {\n if (githubTokenInjectors.get(workspaceId) !== registered || registered.generation !== generation) {\n throw new Error('GitHub token refresh no longer matches the active Factory workspace role.');\n }\n registered.inject(token);\n });\n registerGithubPatKind(requestContext, registered.patKind);\n };\n const reconcileGithubToken = async (): Promise<void> => {\n if (!githubProvider) return;\n const previous = githubTokenReconciliations.get(workspaceId) ?? Promise.resolve();\n const reconciliation = previous\n .catch(() => {})\n .then(async () => {\n const registered = githubTokenInjectors.get(workspaceId);\n if (!registered) return;\n\n const previousPatKind = registered.patKind;\n const patKind = await resolveGithubPatKind(previousPatKind);\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (patKind !== previousPatKind) {\n registered.patKind = patKind;\n registered.generation += 1;\n }\n if (patKind === 'reviewer') registered.tokenReplacementPending = false;\n if (previousPatKind === 'reviewer' && patKind === 'default') {\n // Invalidate reviewer refresh contexts before replacement I/O so\n // they cannot restore reviewer credentials after a failed downgrade.\n registered.tokenReplacementPending = true;\n }\n\n let token = await getGithubPat(() => githubProvider.integrationStorage, session.orgId, patKind);\n if (!token && registered.tokenReplacementPending) token = await getRepositoryToken();\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (token && token !== registered.ghToken) {\n try {\n registered.inject(token);\n } catch (error) {\n if (registered.tokenReplacementPending) throw error;\n // Same-role rotations and reviewer upgrades remain best-effort.\n }\n }\n if (token && token === registered.ghToken) registered.tokenReplacementPending = false;\n registerGithubTokenContext(registered);\n });\n githubTokenReconciliations.set(workspaceId, reconciliation);\n try {\n await reconciliation;\n } finally {\n if (githubTokenReconciliations.get(workspaceId) === reconciliation) {\n githubTokenReconciliations.delete(workspaceId);\n }\n }\n };\n const reconcileRegisteredWorkspace = async (workspace: Workspace): Promise<Workspace> => {\n const registered = githubTokenInjectors.get(workspaceId);\n try {\n await reconcileGithubToken();\n } catch (error) {\n if (registered?.tokenReplacementPending && githubTokenInjectors.get(workspaceId) === registered) {\n // The role generation already invalidated reviewer refresh contexts.\n // Keep the pending registration so failed eviction cannot make a\n // still-live reviewer workspace look safe on the next reuse.\n let evicted = false;\n try {\n evicted = (await mastra?.removeWorkspace?.(workspaceId)) === true;\n } catch {\n // Preserve the credential-replacement error and retry on the next reuse.\n }\n try {\n await workspace.destroy();\n evicted = true;\n } catch {\n // The pending registration keeps the workspace quarantined if cleanup also fails.\n }\n if (evicted && githubTokenInjectors.get(workspaceId) === registered) {\n githubTokenInjectors.delete(workspaceId);\n constructedWorkspaces.delete(workspaceId);\n skillCacheReviewState.delete(workspaceId);\n }\n }\n throw error;\n }\n if (registered && githubTokenInjectors.get(workspaceId) !== registered) {\n throw new Error('Factory workspace GitHub credential registration is no longer active.');\n }\n return workspace;\n };\n\n let existing: Workspace | undefined;\n try {\n existing = mastra?.getWorkspaceById(workspaceId) as Workspace | undefined;\n } catch {\n // Not registered yet.\n existing = undefined;\n }\n existing ??= constructedWorkspaces.get(workspaceId);\n if (existing) {\n existing.setToolsConfig(MASTRACODE_WORKSPACE_TOOLS);\n // The skill cache does not recheck the source on get(), so rescan when the\n // session's role flips; otherwise a cached review skill outlives the review binding.\n if (!skillExtension) {\n // An outside rescan makes the recorded role untrustworthy. Wait for it to\n // settle first, so the refresh below starts a scan for the current role\n // rather than joining one started for the previous role.\n const consumeOutsideRescan = async () => {\n const outside = existing.skills && rescannedOutsideResolver.get(existing.skills);\n if (!outside) return false;\n await outside;\n if (rescannedOutsideResolver.get(existing.skills!) === outside)\n rescannedOutsideResolver.delete(existing.skills!);\n skillCacheReviewState.delete(workspaceId);\n return true;\n };\n await consumeOutsideRescan();\n let isReview = await isReviewSession();\n // Concurrent reuses share one refresh, and the state is recorded only\n // once the rescan succeeds. Wait out any in-flight rescan (it may have\n // been started for the opposite role) and re-read the role afterwards.\n for (;;) {\n const inFlight = skillCacheRefreshes.get(workspaceId);\n if (inFlight) {\n await inFlight;\n isReview = await isReviewSession();\n continue;\n }\n if (await consumeOutsideRescan()) {\n isReview = await isReviewSession();\n continue;\n }\n if (skillCacheReviewState.get(workspaceId) === isReview) break;\n const target = isReview;\n const pending = (async () => {\n await existing.skills?.refresh();\n skillCacheReviewState.set(workspaceId, target);\n })().finally(() => skillCacheRefreshes.delete(workspaceId));\n skillCacheRefreshes.set(workspaceId, pending);\n await pending;\n isReview = await isReviewSession();\n }\n }\n // A materialization kicked off by another caller may still be running.\n // Deliberately do NOT wait for it: a metadata-only resolution (thread\n // list, messages, activity) must not block on the clone/setup that lazy\n // materialization exists to avoid. Token reconciliation below no-ops\n // until the leader registers the injector, and the next reuse after\n // materialization completes reconciles against the live sandbox.\n return reconcileRegisteredWorkspace(existing);\n }\n\n const retiredError = () =>\n new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);\n\n // The session's setup work: materialize the repo (disk-truth idempotent),\n // check out the session branch, run the configured setup command. Minted\n // tokens are fetched inside the run so a replacement VM healed mid-session\n // gets fresh credentials, not ones captured at workspace construction.\n const runSessionSetup = async (target: SessionSandbox, workdir: string, gate: SessionSetupGate): Promise<void> => {\n const access = await getRepositoryAccess();\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session');\n // GitHub setup commands may shell out to gh/HTTPS. Preserve its\n // role-aware PAT behavior without applying GitHub credentials to other\n // source-control providers.\n if (githubProvider) {\n const setupPatKind = await resolveGithubPatKind('default');\n const setupGhToken =\n (await getGithubPat(() => githubProvider.integrationStorage, session.orgId, setupPatKind)) ?? token;\n target.setEnv?.(env => ({ ...env, GH_TOKEN: setupGhToken }));\n }\n await materializeRepo({\n row: { id: session.id, sandboxWorkdir: workdir, materializedAt: session.materializedAt },\n repoInfo: {\n repoFullName,\n defaultBranch: repository.defaultBranch,\n cloneUrl: access.cloneUrl,\n authUsername: access.authorization?.username,\n },\n sandbox: target,\n token,\n storage: storage.sessions,\n });\n await checkoutSessionBranch(target, workdir, {\n branch: session.branch,\n baseBranch: session.baseBranch || projectRepository.branch || repository.defaultBranch,\n token,\n repoFullName,\n cloneUrl: access.cloneUrl,\n authUsername: access.authorization?.username,\n pullRequestNumber: pullRequestNumberFromBranch(session.branch),\n mergeRequestNumber: sourceControl.id === 'gitlab' ? mergeRequestNumberFromBranch(session.branch) : undefined,\n });\n if (projectRepository.setupCommand && !gate.setupDone) {\n // A setup command that already failed this session is skipped rather\n // than failing every start: the first failure surfaced loudly in the\n // tool result that triggered it, and a permanently failing onStart\n // would wedge the session — the agent could never get a shell to fix\n // the problem. Clone and checkout above still ran, so the tree is\n // real; the agent (or an edited setup command) takes it from here.\n if (hasFailedSetupCommand(session.id, projectRepository.setupCommand)) {\n console.warn('[Mastra Factory] Skipping setup command that already failed this session', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n });\n return;\n }\n try {\n await timedPhase('workspace.setup', () => runSetupCommand(target, workdir, projectRepository.setupCommand!));\n await gate.markSetupDone();\n } catch (setupError) {\n if (projectRepository.teardownCommand) {\n try {\n await runTeardownCommand(target, workdir, projectRepository.teardownCommand, {\n timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,\n });\n } catch (teardownError) {\n console.warn('[Mastra Factory] Worktree teardown after setup failure failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: teardownError instanceof Error ? teardownError.message.slice(-2000) : String(teardownError),\n });\n }\n }\n if (setupError instanceof SetupCommandError) {\n // The command ran and exited non-zero — a config problem, not an\n // infra one. Remember it so the next start recovers, and tell the\n // agent what happens next. Infra failures (transport, clone)\n // rethrow untouched and retry in full.\n recordFailedSetupCommand(session.id, projectRepository.setupCommand);\n throw new SetupCommandError(\n `${setupError.message}. The sandbox stays usable: this setup command is skipped for the rest of the session — retry your command, then fix the setup command in the repository settings or run it manually.`,\n setupError.code,\n );\n }\n throw setupError;\n }\n }\n };\n // The session's real sandbox goes straight onto the Workspace. Providers\n // own lazy start (`ensureRunning()` inside the first command/process op)\n // and dead-VM self-healing, and the composed `onStart` hook runs the repo\n // setup + credential install inside that lifecycle. Metadata-only\n // resolutions (thread-list polling) construct but never start.\n const sessionSandbox: SessionSandbox = requireExec(sessionEntry.sandbox);\n\n const filesystem = new SandboxFilesystem({\n id: `sandbox-fs:${workspaceId}`,\n sandbox: sessionSandbox,\n // Lazy: a remote workdir is only knowable once a VM runs. The first\n // file operation resolves it (starting the VM — which materializes the\n // repo via the onStart hook — when needed) and memoizes it.\n workdir: () => resolveSessionWorkdir(session.id, sessionEntry.sandbox, repoFullName),\n });\n const projectSkillPaths = [path.join(configDir, 'skills'), '.claude/skills', '.agents/skills'];\n const guardedSkillFallback = new UnmaterializedAwareSkillSource(\n filesystem,\n () => sessionEntry.sandbox.status === 'running',\n );\n const skillPaths = [...(effectiveSkillExtension?.paths ?? []), ...projectSkillPaths];\n const workspace = new Workspace({\n id: workspaceId,\n name: 'Mastra Code Factory Session Workspace',\n filesystem,\n sandbox: sessionSandbox as unknown as ConstructorParameters<typeof Workspace>[0]['sandbox'],\n tools: MASTRACODE_WORKSPACE_TOOLS,\n skills: skillPaths,\n // Project skill roots live in the sandbox checkout; guard them so skill\n // discovery before materialization (e.g. kickoff skill resolution in the\n // start coordinator) never forces sandbox provisioning.\n skillSource:\n effectiveSkillExtension?.createSource(guardedSkillFallback, projectSkillPaths) ?? guardedSkillFallback,\n });\n // Register with the Mastra instance so sync HTTP handlers that resolve\n // the workspace via `mastra.getWorkspaceById(id)` (file tree, permissions\n // probe, MCP/tool routes) find it instead of throwing\n // `MASTRA_GET_WORKSPACE_BY_ID_NOT_FOUND`. `addWorkspace` is idempotent on\n // key collision, so concurrent first resolutions stay race-safe (start\n // itself is coalesced by the sandbox base class + the session memo).\n mastra?.addWorkspace(workspace, workspaceId, { source: 'mastra' });\n // Cache synchronously with construction: the `await` below is a suspension\n // point, and a concurrent resolution for the same session must observe this\n // workspace rather than build a second one.\n constructedWorkspaces.set(workspaceId, workspace);\n // Retirement is registered against the workspace itself rather than the\n // sandbox: construction is eager while the VM start is lazy, so a session\n // retired before its first tool call still has a workspace (and possibly a\n // token injector) that must be torn down.\n const registered = await workspaceRegistry.register(\n session.sessionId,\n workspaceId,\n workspaceGeneration,\n async () => {\n githubTokenInjectors.delete(workspaceId);\n constructedWorkspaces.delete(workspaceId);\n skillCacheReviewState.delete(workspaceId);\n skillReviewChecks.delete(workspaceId);\n // Retirement drops the memoized session sandbox so a later re-open\n // constructs (and the provider resolves) fresh instead of reusing an\n // instance whose VM the retirement path may stop or destroy.\n evictSessionSandbox(session.id);\n await mastra?.removeWorkspace?.(workspaceId);\n },\n );\n if (!registered) {\n throw new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);\n }\n\n fireEagerStart();\n return workspace;\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"workspace.js","names":["#bundledSource","#localSource","#fallbackSkillRoots","#isHiddenReviewPath","#layerFor","#isFactoryPath","#factoryPath","#entries","#generations"],"sources":["../src/workspace.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport path, { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';\nimport { MASTRACODE_WORKSPACE_TOOLS } from '@mastra/code-sdk/agents/tool-availability';\nimport type { getDynamicWorkspace, WorkspaceSkillExtension } from '@mastra/code-sdk/agents/workspace';\nimport { DEFAULT_CONFIG_DIR } from '@mastra/code-sdk/constants';\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport { LocalSkillSource, Workspace } from '@mastra/core/workspace';\nimport type {\n SandboxStartHook,\n SkillSource,\n SkillSourceEntry,\n SkillSourceStat,\n WorkspaceSandbox,\n} from '@mastra/core/workspace';\nimport { getFactoryAuthOrgId, getFactoryAuthUserFromContext, getFactoryAuthUserId } from './auth.js';\nimport type { VersionControl } from './capabilities/version-control.js';\nimport type { MastraFactorySandboxConfig } from './factory.js';\nimport type { GithubIntegration } from './integrations/github/integration.js';\nimport { getGithubPat } from './integrations/github/pat.js';\nimport type { GithubPatKind } from './integrations/github/pat.js';\nimport {\n checkoutSessionBranch,\n DEFAULT_COMMAND_TIMEOUT_MS,\n materializeRepo,\n runSetupCommand,\n runTeardownCommand,\n SetupCommandError,\n} from './integrations/github/sandbox.js';\nimport {\n registerGithubPatKind,\n registerGithubRefreshTarget,\n registerGithubTokenInjector,\n registerGithubTokenInjectorResolver,\n} from './integrations/github/token-refresh.js';\nimport { requireExec } from './sandbox/materialization.js';\nimport type { ExecutableSandbox } from './sandbox/materialization.js';\nimport {\n createSessionSetupHook,\n evictSessionSandbox,\n getSessionSandbox,\n hasFailedSetupCommand,\n recordFailedSetupCommand,\n resolveSessionWorkdir,\n} from './sandbox/session-sandbox.js';\nimport type { SessionSetupGate } from './sandbox/session-sandbox.js';\nimport type { FactoryProjectsStorage } from './storage/domains/projects/base.js';\nimport type { SourceControlSession, SourceControlStorageHandle } from './storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from './storage/domains/work-items/base.js';\nimport { parseSupervisorResourceId } from './supervisor/session.js';\nimport { timedPhase } from './timing.js';\nimport { mergeRequestNumberFromBranch, pullRequestNumberFromBranch } from './work-item-branch.js';\n\nconst WORKSPACE_ID_PREFIX = 'mfw';\nconst bundleDirectory = dirname(fileURLToPath(import.meta.url));\nconst bundledFactorySkillsPath = join(bundleDirectory, 'factory-skills');\nexport const BUNDLED_FACTORY_SKILLS_PATH =\n [\n // Deploy bundle: the consumer copies `factory-skills/` next to the built\n // server module (e.g. via its public/ dir).\n bundledFactorySkillsPath,\n // Package layout: `dist/../factory-skills` (also `src/../factory-skills`\n // when running tests against sources).\n join(bundleDirectory, '..', 'factory-skills'),\n ].find(existsSync) ?? bundledFactorySkillsPath;\n\n/**\n * Resolve the consumer repo's local Factory skills root, if any. Checked in\n * addition to the bundled skills so projects can add (or override) Factory\n * skills without patching the installed package. Candidates cover the cwd\n * variants the dev server runs with (`repo root`, `--dir src/mastra` which\n * runs with cwd `src/mastra/public`).\n */\nexport function resolveLocalFactorySkillsPath(cwd: string = process.cwd()): string | undefined {\n const candidates = [\n join(cwd, 'src', 'mastra', 'public', 'factory-skills'),\n join(cwd, 'public', 'factory-skills'),\n join(cwd, 'factory-skills'),\n ];\n return candidates.find(\n candidate => path.normalize(candidate) !== path.normalize(BUNDLED_FACTORY_SKILLS_PATH) && existsSync(candidate),\n );\n}\nconst FACTORY_SKILLS_MOUNT = path.resolve(path.parse(process.cwd()).root, '__mastracode_factory_skills__');\nexport const FACTORY_SKILL_NAMES = new Set([\n 'configure-factory-rules',\n 'factory-complete-issue',\n 'factory-gitlab-rereview',\n 'factory-gitlab-review',\n 'factory-plan',\n 'factory-rereview',\n 'factory-review',\n 'factory-triage',\n]);\n\n/** Review skills only sessions with an active review run binding may discover or load. */\nexport const REVIEW_ONLY_FACTORY_SKILLS = new Set([\n 'factory-gitlab-rereview',\n 'factory-gitlab-review',\n 'factory-rereview',\n 'factory-review',\n]);\n\n/**\n * Skill caches rescanned outside the workspace resolver. The resolver records\n * which role each cache was last scanned for; a rescan it did not perform makes\n * that record untrustworthy, so the next reuse must rescan again.\n */\nconst rescannedOutsideResolver = new WeakMap<object, Promise<void>>();\n\n/**\n * Rescan a session's skill cache so a role gained after the cache was built\n * (a review binding minted after the session's workspace resolved) takes effect.\n * The source still checks the live binding, so this never widens access.\n */\nexport async function rescanFactorySkills(skills: NonNullable<Workspace['skills']>): Promise<void> {\n // Registered so the resolver waits it out: a refresh started meanwhile would\n // join this scan (possibly for the old role) instead of starting a fresh one.\n const pending = skills.refresh();\n rescannedOutsideResolver.set(\n skills,\n pending.catch(() => {}),\n );\n await pending;\n}\n\nexport class FactorySkillSource implements SkillSource {\n readonly #bundledSource = new LocalSkillSource({ basePath: BUNDLED_FACTORY_SKILLS_PATH });\n readonly #localSource: LocalSkillSource | undefined;\n readonly #fallbackSkillRoots: Set<string>;\n\n constructor(\n readonly fallback: SkillSource,\n fallbackSkillRoots: string[],\n localSkillsPath: string | undefined = resolveLocalFactorySkillsPath(),\n /** Evaluated per call because a session's bound role changes across stages. */\n readonly isReviewSession: () => Promise<boolean> = () => Promise.resolve(true),\n ) {\n this.#localSource = localSkillsPath ? new LocalSkillSource({ basePath: localSkillsPath }) : undefined;\n this.#fallbackSkillRoots = new Set(fallbackSkillRoots.map(skillPath => path.normalize(skillPath)));\n }\n\n #isFactoryPath(skillPath: string): boolean {\n const normalized = path.normalize(skillPath);\n return normalized === FACTORY_SKILLS_MOUNT || normalized.startsWith(`${FACTORY_SKILLS_MOUNT}${path.sep}`);\n }\n\n #factoryPath(skillPath: string): string {\n return path.relative(FACTORY_SKILLS_MOUNT, path.normalize(skillPath));\n }\n\n /** True when the path is inside a review-only skill that this session may not use. */\n async #isHiddenReviewPath(relativePath: string): Promise<boolean> {\n const skillName = relativePath.split(path.sep)[0];\n if (!skillName || !REVIEW_ONLY_FACTORY_SKILLS.has(skillName)) return false;\n return !(await this.isReviewSession());\n }\n\n /** Pick the layer serving this mount-relative path: local wins when it has the entry. */\n async #layerFor(relativePath: string): Promise<LocalSkillSource> {\n if (this.#localSource && (await this.#localSource.exists(relativePath))) return this.#localSource;\n return this.#bundledSource;\n }\n\n async exists(skillPath: string): Promise<boolean> {\n if (!this.#isFactoryPath(skillPath)) return this.fallback.exists(skillPath);\n const relative = this.#factoryPath(skillPath);\n if (await this.#isHiddenReviewPath(relative)) return false;\n if (this.#localSource && (await this.#localSource.exists(relative))) return true;\n return this.#bundledSource.exists(relative);\n }\n\n async stat(skillPath: string): Promise<SkillSourceStat> {\n if (!this.#isFactoryPath(skillPath)) return this.fallback.stat(skillPath);\n const relative = this.#factoryPath(skillPath);\n if (await this.#isHiddenReviewPath(relative)) throw skillSourceEnoent(skillPath);\n return (await this.#layerFor(relative)).stat(relative);\n }\n\n async readFile(skillPath: string): Promise<string | Buffer> {\n if (!this.#isFactoryPath(skillPath)) return this.fallback.readFile(skillPath);\n const relative = this.#factoryPath(skillPath);\n if (await this.#isHiddenReviewPath(relative)) throw skillSourceEnoent(skillPath);\n return (await this.#layerFor(relative)).readFile(relative);\n }\n\n async readdir(skillPath: string): Promise<SkillSourceEntry[]> {\n if (this.#isFactoryPath(skillPath)) {\n const relative = this.#factoryPath(skillPath);\n if (await this.#isHiddenReviewPath(relative)) throw skillSourceEnoent(skillPath);\n const [bundledExists, localExists] = await Promise.all([\n this.#bundledSource.exists(relative),\n this.#localSource?.exists(relative) ?? Promise.resolve(false),\n ]);\n if (!bundledExists && !localExists) throw skillSourceEnoent(skillPath);\n const [bundledEntries, localEntries] = await Promise.all([\n bundledExists ? this.#bundledSource.readdir(relative) : [],\n localExists ? this.#localSource!.readdir(relative) : [],\n ]);\n const merged = new Map<string, SkillSourceEntry>();\n for (const entry of bundledEntries) merged.set(entry.name, entry);\n // Local entries override bundled names.\n for (const entry of localEntries) merged.set(entry.name, entry);\n const entries = [...merged.values()];\n if (relative !== '' || (await this.isReviewSession())) return entries;\n return entries.filter(entry => !REVIEW_ONLY_FACTORY_SKILLS.has(entry.name));\n }\n const entries = await this.fallback.readdir(skillPath);\n if (this.#fallbackSkillRoots.has(path.normalize(skillPath))) {\n return entries.filter(entry => !FACTORY_SKILL_NAMES.has(entry.name));\n }\n return entries;\n }\n\n realpath(skillPath: string): Promise<string> {\n if (this.#isFactoryPath(skillPath)) return Promise.resolve(path.normalize(skillPath));\n return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);\n }\n}\n\n/** Build a Node-style ENOENT error so callers can treat missing skills like fs misses. */\nfunction skillSourceEnoent(skillPath: string): Error {\n const error = new Error(`ENOENT: no such file or directory, '${skillPath}'`) as Error & { code: string };\n error.code = 'ENOENT';\n return error;\n}\n\n/**\n * Sandbox-backed skill fallback that stays inert until the session sandbox is\n * actually materialized. Skill discovery runs on latency-sensitive paths (the\n * Factory start coordinator resolves the kickoff skill before the start route\n * responds); without this guard the first project-root read would hit the lazy\n * sandbox handle and force full provisioning + repo materialization. While the\n * sandbox is unmaterialized, project skill roots simply appear empty — bundled\n * Factory skills resolve from local disk via `FactorySkillSource`. Once the\n * sandbox exists, every call delegates straight through.\n */\nclass UnmaterializedAwareSkillSource implements SkillSource {\n constructor(\n readonly fallback: SkillSource,\n readonly isMaterialized: () => boolean,\n ) {}\n\n async exists(skillPath: string): Promise<boolean> {\n return this.isMaterialized() ? this.fallback.exists(skillPath) : false;\n }\n\n async stat(skillPath: string): Promise<SkillSourceStat> {\n if (!this.isMaterialized()) throw skillSourceEnoent(skillPath);\n return this.fallback.stat(skillPath);\n }\n\n async readFile(skillPath: string): Promise<string | Buffer> {\n if (!this.isMaterialized()) throw skillSourceEnoent(skillPath);\n return this.fallback.readFile(skillPath);\n }\n\n async readdir(skillPath: string): Promise<SkillSourceEntry[]> {\n return this.isMaterialized() ? this.fallback.readdir(skillPath) : [];\n }\n\n realpath(skillPath: string): Promise<string> {\n if (!this.isMaterialized()) return Promise.resolve(skillPath);\n return this.fallback.realpath ? this.fallback.realpath(skillPath) : Promise.resolve(skillPath);\n }\n}\n\nfunction createFactorySkillExtension(isReviewSession: () => Promise<boolean>): WorkspaceSkillExtension {\n return {\n id: 'web-factory',\n paths: [FACTORY_SKILLS_MOUNT],\n createSource: (fallback, fallbackSkillRoots) =>\n new FactorySkillSource(fallback, fallbackSkillRoots, resolveLocalFactorySkillsPath(), isReviewSession),\n };\n}\n\nfunction isActiveReviewBinding(\n runBinding: { role: string; status: string; orgId: string } | null | undefined,\n orgId: string | undefined,\n): boolean {\n return runBinding?.role === 'review' && runBinding.status === 'active' && !!orgId && runBinding.orgId === orgId;\n}\n\ntype DynamicWorkspaceContext = Parameters<typeof getDynamicWorkspace>[0];\n\n/**\n * When a session's sandbox boots: on the agent's first command (`'lazy'`, the\n * default) or as soon as the session's workspace is first resolved (`'eager'`).\n * An eager start is fire-and-forget; if it fails, the lazy path still runs.\n */\nexport type FactorySandboxStart = 'lazy' | 'eager';\n\nexport interface WorkspaceSourceControlProvider {\n /** Stable integration id used to select the provider-owned storage partition. */\n id: string;\n /** Provider capability used to resolve fresh repository credentials. */\n versionControl: Pick<VersionControl, 'getRepositoryAccess'>;\n /** Provider-owned source-control rows, including Factory sessions. */\n storage: SourceControlStorageHandle;\n /** Present only for GitHub, whose CLI PAT rotation has additional semantics. */\n github?: GithubIntegration;\n}\n\nexport interface CreateWorkspaceFactoryOptions {\n /** Factory sandbox runtime config (session sandbox callback). */\n sandbox?: MastraFactorySandboxConfig;\n /** Defaults to `'lazy'`. */\n sandboxStart?: FactorySandboxStart;\n /** Source-control providers whose repositories may back Factory sessions. */\n sourceControls?: WorkspaceSourceControlProvider[];\n /** @deprecated GitHub-only compatibility path; prefer sourceControls. */\n github?: GithubIntegration;\n /** Work-items storage used to resolve the session's run-binding role, so\n * review-board sessions get the reviewer PAT as `GH_TOKEN`. Optional —\n * without it every session uses the default (worker) PAT. */\n workItems?: Pick<WorkItemsStorage, 'findActiveRunBindingForSession'>;\n /** Projects storage used to authorize workspace-free supervisor sessions. */\n projects?: Pick<FactoryProjectsStorage, 'get'>;\n /** Runtime workspace/token registrations invalidated when a session retires. */\n workspaceRegistry?: FactoryWorkspaceRegistry;\n}\n\ntype WorkspaceUnregister = () => Promise<void> | void;\n\n/** Tracks dynamic Factory workspaces by persisted session id for retirement. */\nexport class FactoryWorkspaceRegistry {\n readonly #entries = new Map<string, Map<string, WorkspaceUnregister>>();\n readonly #generations = new Map<string, number>();\n\n generation(sessionId: string): number {\n return this.#generations.get(sessionId) ?? 0;\n }\n\n async register(\n sessionId: string,\n workspaceId: string,\n generation: number,\n unregister: WorkspaceUnregister,\n ): Promise<boolean> {\n if (generation !== this.generation(sessionId)) {\n await unregister();\n return false;\n }\n const entries = this.#entries.get(sessionId) ?? new Map<string, WorkspaceUnregister>();\n entries.set(workspaceId, unregister);\n this.#entries.set(sessionId, entries);\n return true;\n }\n\n async invalidateSession(sessionId: string): Promise<void> {\n this.#generations.set(sessionId, this.generation(sessionId) + 1);\n const entries = this.#entries.get(sessionId);\n if (!entries) return;\n this.#entries.delete(sessionId);\n const results = await Promise.allSettled([...entries.values()].map(unregister => unregister()));\n const failure = results.find(result => result.status === 'rejected');\n if (failure?.status === 'rejected') throw failure.reason;\n }\n}\n\nasync function resolveSourceControlSession(\n providers: WorkspaceSourceControlProvider[],\n sessionId: string,\n): Promise<{ provider: WorkspaceSourceControlProvider; session: SourceControlSession } | null> {\n const matches = (\n await Promise.all(\n providers.map(async provider => ({\n provider,\n session: await provider.storage.sessions.getBySessionId(sessionId),\n })),\n )\n ).filter(\n (match): match is { provider: WorkspaceSourceControlProvider; session: SourceControlSession } =>\n match.session !== null,\n );\n if (matches.length > 1) {\n throw new Error(`Factory session ${sessionId} is ambiguous across source-control providers`);\n }\n return matches[0] ?? null;\n}\n\nexport function createWorkspaceFactory(options: CreateWorkspaceFactoryOptions = {}) {\n const { sandbox: sandboxConfig, github, projects, workItems } = options;\n const sourceControls: WorkspaceSourceControlProvider[] =\n options.sourceControls ??\n (github\n ? [{ id: github.id, versionControl: github.versionControl, storage: github.sourceControlStorage, github }]\n : []);\n const eagerSandboxStart = options.sandboxStart === 'eager';\n const workspaceRegistry = options.workspaceRegistry ?? new FactoryWorkspaceRegistry();\n type GithubTokenRegistration = {\n inject: (token: string) => void;\n patKind: GithubPatKind;\n ghToken: string;\n generation: number;\n tokenReplacementPending: boolean;\n };\n // The session setup path runs commands and installs credentials, so it\n // needs `executeCommand` (required by `ExecutableSandbox`) plus core's\n // optional `setEnv`, which stays optional here because the token-refresh\n // path checks for it and reports its absence.\n type SessionSandbox = ExecutableSandbox & { setEnv?: WorkspaceSandbox['setEnv'] };\n const githubTokenInjectors = new Map<string, GithubTokenRegistration>();\n const githubTokenReconciliations = new Map<string, Promise<void>>();\n // Workspace identity cache: concurrent resolutions of the same session must\n // observe the same Workspace object even when no Mastra registry is wired\n // (the registry stays the source of truth when present).\n const constructedWorkspaces = new Map<string, Workspace>();\n // Review-skill visibility last used to populate each workspace's skill cache.\n const skillCacheReviewState = new Map<string, boolean>();\n const skillCacheRefreshes = new Map<string, Promise<void>>();\n // The review check of the latest request to resolve each workspace. The skill\n // source outlives the request that built it, so it must gate on this rather\n // than on the context it was constructed with.\n const skillReviewChecks = new Map<string, () => Promise<boolean>>();\n\n return async ({ requestContext, mastra, skillExtension }: DynamicWorkspaceContext) => {\n const ctx = requestContext.get('controller') as AgentControllerRequestContext<MastraCodeState> | undefined;\n const supervisorProjectId = parseSupervisorResourceId(ctx?.resourceId);\n if (supervisorProjectId) {\n const orgId = getFactoryAuthOrgId(getFactoryAuthUserFromContext(requestContext));\n const project = orgId && projects ? await projects.get({ orgId, id: supervisorProjectId }) : null;\n if (!project) throw new Error(`Factory supervisor ${supervisorProjectId} is not available to the current user`);\n return undefined;\n }\n const resolvedSession = ctx?.resourceId ? await resolveSourceControlSession(sourceControls, ctx.resourceId) : null;\n const session = resolvedSession?.session ?? null;\n const sourceControl = resolvedSession?.provider;\n\n if (!session || !sourceControl) {\n // No factory session, no workspace. Chat still works; workspace tools\n // are simply not registered. Host-cwd behavior is opt-in via a\n // LocalSandbox callback rooted wherever the deployer wants — the\n // resolver never hands out the server host's own filesystem.\n return undefined;\n }\n\n const user = getFactoryAuthUserFromContext(requestContext);\n const userId = getFactoryAuthUserId(user);\n // No identity at all is a server-side caller that forgot to seed one\n // (webhook, cron), not someone reaching for another user's session.\n if (!user?.organizationId || !userId) {\n throw new Error(`Factory session ${session.sessionId} was resolved without a caller identity`);\n }\n // Org-visible sessions open to any member of the owning organization;\n // only private sessions stay owner-only. Cross-org access never passes.\n if (user.organizationId !== session.orgId || (session.visibility === 'private' && userId !== session.userId)) {\n throw new Error(`Factory session ${session.sessionId} is not available to the current user`);\n }\n if (!sandboxConfig) {\n throw new Error('A sandbox callback is required to create a Factory session workspace');\n }\n const createSessionSandboxInstance = sandboxConfig;\n const githubProvider = sourceControl.github;\n\n const storage = sourceControl.storage;\n const projectRepository = await storage.projectRepositories.get({\n orgId: session.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) throw new Error(`Repository link ${session.projectRepositoryId} was not found`);\n // The remaining reads only depend on the repository link — issue them in\n // parallel instead of paying four sequential storage round-trips.\n const [connection, repository] = await Promise.all([\n storage.connections.get({ orgId: session.orgId, id: projectRepository.connectionId }),\n storage.repositories.get({ orgId: session.orgId, id: projectRepository.repositoryId }),\n ]);\n if (!connection || !repository) throw new Error(`Repository link ${session.projectRepositoryId} is incomplete`);\n const installation = await storage.installations.get({ orgId: session.orgId, id: connection.installationId });\n if (!installation) {\n throw new Error(`${sourceControl.id} installation ${connection.installationId} was not found`);\n }\n const repoFullName = repository.slug;\n if (githubProvider)\n registerGithubRefreshTarget(requestContext, { orgId: session.orgId, repositoryId: repository.id });\n\n // Construct (or fetch) the session's memoized sandbox instance.\n // Construction is cheap and side-effect-free by the callback contract —\n // the VM is provisioned on `start()`, which only the materialization\n // pipeline calls. The workdir is never persisted or trusted from storage\n // or client input (the stale-workdir incident class came from reusing\n // `session.sandboxWorkdir` written under a different provider): local\n // sandboxes derive it at construction, remote sandboxes clone into the\n // VM's own home so it resolves lazily at first start.\n // `runSetupOn` references `runSessionSetup`, defined below — it is only\n // invoked during start, long after this closure fully initializes.\n const runSetupOn = (target: unknown, workdir: string, gate: SessionSetupGate) =>\n runSessionSetup(requireExec(target as WorkspaceSandbox), workdir, gate);\n const guardedSetup = createSessionSetupHook(\n runSetupOn,\n session.id,\n repoFullName,\n projectRepository.setupCommand ?? undefined,\n );\n // Composed start hook: marker-guarded repo setup, then per-start\n // credential install. It runs inside the provider's start lifecycle on\n // EVERY start (create or reconnect) — providers own lazy start\n // (`ensureRunning()` on first command) and dead-VM self-healing (E2B\n // `retryOnDead`, Platform status reset on destroy), so a replacement VM\n // re-enters this hook and heals itself with credentials at least as\n // fresh as the start that installed them.\n const setupHook: SandboxStartHook = async args => {\n // A session retired before its first start must not set anything up.\n if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) {\n throw retiredError();\n }\n let setupError: SetupCommandError | undefined;\n try {\n await guardedSetup(args);\n } catch (error) {\n if (!(error instanceof SetupCommandError)) throw error;\n setupError = error;\n }\n // Re-check after the (long) setup: a session retired mid-setup must not\n // register credentials for a workspace whose retirement teardown has\n // already run — the entry would leak forever. The VM itself is left to\n // the provider's idle timeout (accepted).\n if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) {\n throw retiredError();\n }\n const target: SessionSandbox = requireExec(args.sandbox);\n // Sandbox persistence plus the post-checkout skill rescan run on every\n // start (create or reconnect). The persisted sandbox id is read back on\n // resume to reattach; the workdir was resolved (and memoized on the\n // entry) by the guarded setup. The skill roots were reported empty by the\n // unmaterialized-source guard before the checkout existed, so rescan now.\n // The physical-id write is awaited and its failure propagates: a start\n // that completes before the id is durable lets a concurrent resume read a\n // stale id and provision a replacement VM.\n const publishStartSideEffects = async () => {\n await storage.sessions.setSandbox({\n id: session.id,\n // Persist the provider's PHYSICAL, reattachable VM id so resume can\n // reattach to the same VM. Providers with no separate physical id\n // (e.g. local) fall back to the logical id, preserving prior behavior.\n sandboxId: target.sandboxId ?? target.id,\n sandboxWorkdir: sessionEntry.workdir ?? '',\n });\n void constructedWorkspaces\n .get(workspaceId)\n ?.skills?.refresh()\n .catch(() => {});\n };\n const finishStart = async () => {\n await publishStartSideEffects();\n if (setupError) throw setupError;\n };\n if (!githubProvider) {\n await finishStart();\n return;\n }\n const existingRegistration = githubTokenInjectors.get(workspaceId);\n if (existingRegistration) {\n // Reconnect: re-point the current registration's injection target at\n // this (possibly provider-healed) sandbox and reinstall its\n // reconcile-owned credential. Do NOT mint a new registration, bump\n // authority, or re-register the constructing request context —\n // reconcileGithubToken owns role/generation and the active context's\n // injector, so replacing them here would reauthorize the stale\n // constructing context and reject the current one.\n existingRegistration.inject = freshToken => {\n if (!target.setEnv) {\n throw new Error('The active sandbox provider does not support runtime GitHub token refresh.');\n }\n target.setEnv(env => ({ ...env, GH_TOKEN: freshToken }));\n existingRegistration.ghToken = freshToken;\n };\n // Install through inject (not optional-chained setEnv) so a provider\n // that cannot accept the credential fails the reconnect here instead of\n // deferring the failure to a later token refresh.\n existingRegistration.inject(existingRegistration.ghToken);\n await finishStart();\n return;\n }\n // First start: resolve the credential and authorize every request that\n // resolved this pending workspace. The `gh` CLI needs a PAT when the org configured one\n // (installation tokens 403 on integration-restricted endpoints); git\n // clone/checkout keep using the minted installation token. Resolved per\n // start so the installed credential never outlives rotation.\n const patKind = await resolveGithubPatKind('default');\n const ghCliToken =\n (await getGithubPat(() => githubProvider.integrationStorage, session.orgId, patKind)) ??\n (await getRepositoryToken());\n target.setEnv?.(env => ({ ...env, GH_TOKEN: ghCliToken }));\n const tokenRegistration: GithubTokenRegistration = {\n inject: freshToken => {\n if (!target.setEnv) {\n throw new Error('The active sandbox provider does not support runtime GitHub token refresh.');\n }\n target.setEnv(env => ({ ...env, GH_TOKEN: freshToken }));\n tokenRegistration.ghToken = freshToken;\n },\n patKind,\n ghToken: ghCliToken,\n generation: 0,\n tokenReplacementPending: false,\n };\n githubTokenInjectors.set(workspaceId, tokenRegistration);\n registerGithubTokenContext(tokenRegistration);\n await finishStart();\n };\n const constructSessionEntry = () =>\n getSessionSandbox(session.id, repoFullName, () => {\n const sandbox = createSessionSandboxInstance({\n sessionId: session.id,\n // Physical VM id persisted from a prior start (undefined on first\n // start). Providers that reattach by physical id use it to resume the\n // original VM instead of provisioning a replacement.\n sandboxId: session.sandboxId ?? undefined,\n repoFullName,\n // Stored nullable; the context speaks `undefined` for absent.\n setupCommand: projectRepository.setupCommand ?? undefined,\n // Deferred call — only dereferenced when a provider needs the repo\n // outside the VM (template build time).\n getRepositoryAccess: () =>\n sourceControl.versionControl.getRepositoryAccess({\n orgId: session.orgId,\n repositoryId: repository.id,\n }),\n });\n // Attached inside the construction closure, so exactly once per\n // instance — `constructSessionEntry` runs on every open and would\n // stack a wrapper per call. Factory's setup runs first: a hook the\n // callback installed itself expects a prepared workspace.\n sandbox.setOnStart(previous => async args => {\n await timedPhase(`workspace.onStart(${args.outcome})`, async () => {\n await setupHook(args);\n });\n await previous?.(args);\n });\n // Only a freshly constructed instance starts eagerly; the start itself\n // waits for this resolver to finish because the start hook reads\n // bindings declared further down.\n startEagerly = eagerSandboxStart;\n return sandbox;\n });\n let startEagerly = false;\n const fireEagerStart = () => {\n if (!startEagerly) return;\n startEagerly = false;\n Promise.resolve()\n .then(() => sessionEntry.sandbox.start?.())\n .catch(error => {\n console.warn(`[factory] Eager sandbox start for session ${session.id} failed:`, error);\n });\n };\n const sessionEntry = constructSessionEntry();\n const workdir = sessionEntry.workdir;\n const isLocalSandbox = sessionEntry.sandbox.provider === 'local';\n // The SDK system prompt uses `state.projectPath` without falling back to\n // the server cwd. Pin the session workdir once known so the prompt and\n // workspace tools describe the same checkout. A remote workdir resolves at\n // the sandbox's first start, so the pin self-heals on the next resolution.\n if (ctx && workdir && ctx.getState()?.projectPath !== workdir) {\n await ctx.setState({ projectPath: workdir, projectName: repoFullName });\n }\n\n // Keyed by the session, not the request's thread: a workspace built for a\n // dispatcher kickoff has no live thread yet, but the session's binding\n // already says which role it is serving. When the request does carry a\n // thread, the binding must belong to it, so a stale thread reusing this\n // session never inherits another run's role.\n const findSessionBinding = async () => {\n const binding = await workItems!.findActiveRunBindingForSession({\n orgId: session.orgId,\n factoryProjectId: connection.factoryProjectId,\n sessionId: session.sessionId,\n });\n if (!binding || !ctx?.threadId) return binding;\n return binding.threadId === ctx.threadId && binding.resourceId === ctx.resourceId ? binding : null;\n };\n // Fails closed: without a readable active review binding, review skills stay hidden.\n const isReviewSession = async (): Promise<boolean> => {\n if (!workItems) return false;\n try {\n return isActiveReviewBinding(await findSessionBinding(), session.orgId);\n } catch {\n return false;\n }\n };\n const effectiveSkillExtension =\n skillExtension ??\n createFactorySkillExtension((): Promise<boolean> => (skillReviewChecks.get(workspaceId) ?? isReviewSession)());\n const extensionId = effectiveSkillExtension ? `-${effectiveSkillExtension.id}` : '';\n const workspaceId: string = `${WORKSPACE_ID_PREFIX}-${projectRepository.id}-${session.id}${extensionId}`;\n const workspaceGeneration = workspaceRegistry.generation(session.sessionId);\n if (!skillExtension) skillReviewChecks.set(workspaceId, isReviewSession);\n const configDir = DEFAULT_CONFIG_DIR;\n\n const getRepositoryAccess = () =>\n sourceControl.versionControl.getRepositoryAccess({\n orgId: session.orgId,\n repositoryId: repository.id,\n });\n const getRepositoryToken = async (): Promise<string> => {\n const token = (await getRepositoryAccess()).authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session');\n return token;\n };\n const resolveGithubPatKind = async (fallback: GithubPatKind): Promise<GithubPatKind> => {\n if (!workItems) return 'default';\n try {\n return isActiveReviewBinding(await findSessionBinding(), session.orgId) ? 'reviewer' : 'default';\n } catch {\n // Preserve the installed role when binding storage is temporarily unavailable.\n return fallback;\n }\n };\n const githubTokenInjector = (registered: GithubTokenRegistration, generation: number) => (token: string) => {\n if (githubTokenInjectors.get(workspaceId) !== registered || registered.generation !== generation) {\n throw new Error('GitHub token refresh no longer matches the active Factory workspace role.');\n }\n registered.inject(token);\n };\n const registerGithubTokenContext = (registered: GithubTokenRegistration): void => {\n registerGithubTokenInjector(requestContext, githubTokenInjector(registered, registered.generation));\n registerGithubPatKind(requestContext, registered.patKind);\n };\n if (githubProvider) {\n const registered = githubTokenInjectors.get(workspaceId);\n if (registered) {\n registerGithubTokenContext(registered);\n } else {\n registerGithubTokenInjectorResolver(requestContext, () => {\n if (workspaceRegistry.generation(session.sessionId) !== workspaceGeneration) {\n throw new Error('GitHub token refresh no longer matches the active Factory workspace role.');\n }\n const active = githubTokenInjectors.get(workspaceId);\n if (!active) {\n throw new Error('GitHub token refresh requires an active Factory sandbox workspace.');\n }\n registerGithubPatKind(requestContext, active.patKind);\n return githubTokenInjector(active, 0);\n });\n }\n }\n const reconcileGithubToken = async (): Promise<void> => {\n if (!githubProvider) return;\n const previous = githubTokenReconciliations.get(workspaceId) ?? Promise.resolve();\n const reconciliation = previous\n .catch(() => {})\n .then(async () => {\n const registered = githubTokenInjectors.get(workspaceId);\n if (!registered) return;\n\n const previousPatKind = registered.patKind;\n const patKind = await resolveGithubPatKind(previousPatKind);\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (patKind !== previousPatKind) {\n registered.patKind = patKind;\n registered.generation += 1;\n }\n if (patKind === 'reviewer') registered.tokenReplacementPending = false;\n if (previousPatKind === 'reviewer' && patKind === 'default') {\n // Invalidate reviewer refresh contexts before replacement I/O so\n // they cannot restore reviewer credentials after a failed downgrade.\n registered.tokenReplacementPending = true;\n }\n\n let token = await getGithubPat(() => githubProvider.integrationStorage, session.orgId, patKind);\n if (!token && registered.tokenReplacementPending) token = await getRepositoryToken();\n if (githubTokenInjectors.get(workspaceId) !== registered) return;\n\n if (token && token !== registered.ghToken) {\n try {\n registered.inject(token);\n } catch (error) {\n if (registered.tokenReplacementPending) throw error;\n // Same-role rotations and reviewer upgrades remain best-effort.\n }\n }\n if (token && token === registered.ghToken) registered.tokenReplacementPending = false;\n registerGithubTokenContext(registered);\n });\n githubTokenReconciliations.set(workspaceId, reconciliation);\n try {\n await reconciliation;\n } finally {\n if (githubTokenReconciliations.get(workspaceId) === reconciliation) {\n githubTokenReconciliations.delete(workspaceId);\n }\n }\n };\n const reconcileRegisteredWorkspace = async (workspace: Workspace): Promise<Workspace> => {\n const registered = githubTokenInjectors.get(workspaceId);\n try {\n await reconcileGithubToken();\n } catch (error) {\n if (registered?.tokenReplacementPending && githubTokenInjectors.get(workspaceId) === registered) {\n // The role generation already invalidated reviewer refresh contexts.\n // Keep the pending registration so failed eviction cannot make a\n // still-live reviewer workspace look safe on the next reuse.\n let evicted = false;\n try {\n evicted = (await mastra?.removeWorkspace?.(workspaceId)) === true;\n } catch {\n // Preserve the credential-replacement error and retry on the next reuse.\n }\n try {\n await workspace.destroy();\n evicted = true;\n } catch {\n // The pending registration keeps the workspace quarantined if cleanup also fails.\n }\n if (evicted && githubTokenInjectors.get(workspaceId) === registered) {\n githubTokenInjectors.delete(workspaceId);\n constructedWorkspaces.delete(workspaceId);\n skillCacheReviewState.delete(workspaceId);\n }\n }\n throw error;\n }\n if (registered && githubTokenInjectors.get(workspaceId) !== registered) {\n throw new Error('Factory workspace GitHub credential registration is no longer active.');\n }\n return workspace;\n };\n\n let existing: Workspace | undefined;\n try {\n existing = mastra?.getWorkspaceById(workspaceId) as Workspace | undefined;\n } catch {\n // Not registered yet.\n existing = undefined;\n }\n existing ??= constructedWorkspaces.get(workspaceId);\n if (existing) {\n existing.setToolsConfig(MASTRACODE_WORKSPACE_TOOLS);\n // The skill cache does not recheck the source on get(), so rescan when the\n // session's role flips; otherwise a cached review skill outlives the review binding.\n if (!skillExtension) {\n // An outside rescan makes the recorded role untrustworthy. Wait for it to\n // settle first, so the refresh below starts a scan for the current role\n // rather than joining one started for the previous role.\n const consumeOutsideRescan = async () => {\n const outside = existing.skills && rescannedOutsideResolver.get(existing.skills);\n if (!outside) return false;\n await outside;\n if (rescannedOutsideResolver.get(existing.skills!) === outside)\n rescannedOutsideResolver.delete(existing.skills!);\n skillCacheReviewState.delete(workspaceId);\n return true;\n };\n await consumeOutsideRescan();\n let isReview = await isReviewSession();\n // Concurrent reuses share one refresh, and the state is recorded only\n // once the rescan succeeds. Wait out any in-flight rescan (it may have\n // been started for the opposite role) and re-read the role afterwards.\n for (;;) {\n const inFlight = skillCacheRefreshes.get(workspaceId);\n if (inFlight) {\n await inFlight;\n isReview = await isReviewSession();\n continue;\n }\n if (await consumeOutsideRescan()) {\n isReview = await isReviewSession();\n continue;\n }\n if (skillCacheReviewState.get(workspaceId) === isReview) break;\n const target = isReview;\n const pending = (async () => {\n await existing.skills?.refresh();\n skillCacheReviewState.set(workspaceId, target);\n })().finally(() => skillCacheRefreshes.delete(workspaceId));\n skillCacheRefreshes.set(workspaceId, pending);\n await pending;\n isReview = await isReviewSession();\n }\n }\n // A materialization kicked off by another caller may still be running.\n // Deliberately do NOT wait for it: a metadata-only resolution (thread\n // list, messages, activity) must not block on the clone/setup that lazy\n // materialization exists to avoid. Token reconciliation below no-ops\n // until the leader registers the injector, and the next reuse after\n // materialization completes reconciles against the live sandbox.\n return reconcileRegisteredWorkspace(existing);\n }\n\n const retiredError = () =>\n new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);\n\n // The session's setup work: materialize the repo (disk-truth idempotent),\n // check out the session branch, run the configured setup command. Minted\n // tokens are fetched inside the run so a replacement VM healed mid-session\n // gets fresh credentials, not ones captured at workspace construction.\n const runSessionSetup = async (target: SessionSandbox, workdir: string, gate: SessionSetupGate): Promise<void> => {\n const access = await getRepositoryAccess();\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session');\n // GitHub setup commands may shell out to gh/HTTPS. Preserve its\n // role-aware PAT behavior without applying GitHub credentials to other\n // source-control providers.\n if (githubProvider) {\n const setupPatKind = await resolveGithubPatKind('default');\n const setupGhToken =\n (await getGithubPat(() => githubProvider.integrationStorage, session.orgId, setupPatKind)) ?? token;\n target.setEnv?.(env => ({ ...env, GH_TOKEN: setupGhToken }));\n }\n await materializeRepo({\n row: { id: session.id, sandboxWorkdir: workdir, materializedAt: session.materializedAt },\n repoInfo: {\n repoFullName,\n defaultBranch: repository.defaultBranch,\n cloneUrl: access.cloneUrl,\n authUsername: access.authorization?.username,\n },\n sandbox: target,\n token,\n storage: storage.sessions,\n });\n await checkoutSessionBranch(target, workdir, {\n branch: session.branch,\n baseBranch: session.baseBranch || projectRepository.branch || repository.defaultBranch,\n token,\n repoFullName,\n cloneUrl: access.cloneUrl,\n authUsername: access.authorization?.username,\n pullRequestNumber: pullRequestNumberFromBranch(session.branch),\n mergeRequestNumber: sourceControl.id === 'gitlab' ? mergeRequestNumberFromBranch(session.branch) : undefined,\n });\n if (projectRepository.setupCommand && !gate.setupDone) {\n // A setup command that already failed this session is skipped rather\n // than failing every start: the first failure surfaced loudly in the\n // tool result that triggered it, and a permanently failing onStart\n // would wedge the session — the agent could never get a shell to fix\n // the problem. Clone and checkout above still ran, so the tree is\n // real; the agent (or an edited setup command) takes it from here.\n if (hasFailedSetupCommand(session.id, projectRepository.setupCommand)) {\n console.warn('[Mastra Factory] Skipping setup command that already failed this session', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n });\n return;\n }\n try {\n await timedPhase('workspace.setup', () => runSetupCommand(target, workdir, projectRepository.setupCommand!));\n await gate.markSetupDone();\n } catch (setupError) {\n if (projectRepository.teardownCommand) {\n try {\n await runTeardownCommand(target, workdir, projectRepository.teardownCommand, {\n timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,\n });\n } catch (teardownError) {\n console.warn('[Mastra Factory] Worktree teardown after setup failure failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: teardownError instanceof Error ? teardownError.message.slice(-2000) : String(teardownError),\n });\n }\n }\n if (setupError instanceof SetupCommandError) {\n // The command ran and exited non-zero — a config problem, not an\n // infra one. Remember it so the next start recovers, and tell the\n // agent what happens next. Infra failures (transport, clone)\n // rethrow untouched and retry in full.\n recordFailedSetupCommand(session.id, projectRepository.setupCommand);\n throw new SetupCommandError(\n `${setupError.message}. The sandbox stays usable: this setup command is skipped for the rest of the session — retry your command, then fix the setup command in the repository settings or run it manually.`,\n setupError.code,\n );\n }\n throw setupError;\n }\n }\n };\n // The session's real sandbox goes straight onto the Workspace. Providers\n // own lazy start (`ensureRunning()` inside the first command/process op)\n // and dead-VM self-healing, and the composed `onStart` hook runs the repo\n // setup + credential install inside that lifecycle. Metadata-only\n // resolutions (thread-list polling) construct but never start.\n const sessionSandbox: SessionSandbox = requireExec(sessionEntry.sandbox);\n\n const filesystem = new SandboxFilesystem({\n id: `sandbox-fs:${workspaceId}`,\n sandbox: sessionSandbox,\n // Lazy: a remote workdir is only knowable once a VM runs. The first\n // file operation resolves it (starting the VM — which materializes the\n // repo via the onStart hook — when needed) and memoizes it.\n workdir: () => resolveSessionWorkdir(session.id, sessionEntry.sandbox, repoFullName),\n });\n const projectSkillPaths = [path.join(configDir, 'skills'), '.claude/skills', '.agents/skills'];\n const guardedSkillFallback = new UnmaterializedAwareSkillSource(\n filesystem,\n () => sessionEntry.sandbox.status === 'running',\n );\n const skillPaths = [...(effectiveSkillExtension?.paths ?? []), ...projectSkillPaths];\n const workspace = new Workspace({\n id: workspaceId,\n name: 'Mastra Code Factory Session Workspace',\n filesystem,\n sandbox: sessionSandbox as unknown as ConstructorParameters<typeof Workspace>[0]['sandbox'],\n tools: MASTRACODE_WORKSPACE_TOOLS,\n skills: skillPaths,\n // Project skill roots live in the sandbox checkout; guard them so skill\n // discovery before materialization (e.g. kickoff skill resolution in the\n // start coordinator) never forces sandbox provisioning.\n skillSource:\n effectiveSkillExtension?.createSource(guardedSkillFallback, projectSkillPaths) ?? guardedSkillFallback,\n });\n // Register with the Mastra instance so sync HTTP handlers that resolve\n // the workspace via `mastra.getWorkspaceById(id)` (file tree, permissions\n // probe, MCP/tool routes) find it instead of throwing\n // `MASTRA_GET_WORKSPACE_BY_ID_NOT_FOUND`. `addWorkspace` is idempotent on\n // key collision, so concurrent first resolutions stay race-safe (start\n // itself is coalesced by the sandbox base class + the session memo).\n mastra?.addWorkspace(workspace, workspaceId, { source: 'mastra' });\n // Cache synchronously with construction: the `await` below is a suspension\n // point, and a concurrent resolution for the same session must observe this\n // workspace rather than build a second one.\n constructedWorkspaces.set(workspaceId, workspace);\n // Retirement is registered against the workspace itself rather than the\n // sandbox: construction is eager while the VM start is lazy, so a session\n // retired before its first tool call still has a workspace (and possibly a\n // token injector) that must be torn down.\n const registered = await workspaceRegistry.register(\n session.sessionId,\n workspaceId,\n workspaceGeneration,\n async () => {\n githubTokenInjectors.delete(workspaceId);\n constructedWorkspaces.delete(workspaceId);\n skillCacheReviewState.delete(workspaceId);\n skillReviewChecks.delete(workspaceId);\n // Retirement drops the memoized session sandbox so a later re-open\n // constructs (and the provider resolves) fresh instead of reusing an\n // instance whose VM the retirement path may stop or destroy.\n evictSessionSandbox(session.id);\n await mastra?.removeWorkspace?.(workspaceId);\n },\n );\n if (!registered) {\n throw new Error(`Factory session ${session.sessionId} was retired during workspace materialization`);\n }\n\n fireEagerStart();\n return workspace;\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/factory",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1-alpha.0",
|
|
4
4
|
"description": "Mastra Software Factory module: the server core behind the Mastra Software Factory — storage domains, integrations, and surfaces for agent-powered software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"hono": "^4.13.7",
|
|
58
58
|
"posthog-node": "^5.46.1",
|
|
59
59
|
"zod": "^4.6.4",
|
|
60
|
-
"@mastra/auth-studio": "1.3.8
|
|
60
|
+
"@mastra/auth-studio": "1.3.8",
|
|
61
61
|
"@mastra/auth-workos": "1.6.6",
|
|
62
|
-
"@mastra/code-sdk": "1.10.
|
|
63
|
-
"@mastra/
|
|
64
|
-
"@mastra/
|
|
62
|
+
"@mastra/code-sdk": "1.10.1-alpha.0",
|
|
63
|
+
"@mastra/core": "1.73.1-alpha.0",
|
|
64
|
+
"@mastra/slack": "1.7.1"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@types/node": "22.20.1",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
"typescript": "^6.0.3",
|
|
71
71
|
"typescript-eslint": "^8.57.0",
|
|
72
72
|
"vitest": "4.1.11",
|
|
73
|
-
"@
|
|
74
|
-
"@
|
|
75
|
-
"@
|
|
76
|
-
"@internal/
|
|
77
|
-
"@
|
|
73
|
+
"@internal/lint": "0.0.139",
|
|
74
|
+
"@mastra/libsql": "1.24.1",
|
|
75
|
+
"@mastra/pg": "1.28.1",
|
|
76
|
+
"@internal/types-builder": "0.0.114",
|
|
77
|
+
"@internal/workspace": "0.0.11"
|
|
78
78
|
},
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=22.19.0"
|