@mastra/factory 0.24.0 → 0.25.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -9
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +18 -4
- package/dist/auth.js.map +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +3 -2
- package/dist/boards/review.js.map +1 -1
- package/dist/factory.d.ts +3 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +90 -97
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +14 -1
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/integration.d.ts +3 -0
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +4 -0
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +4 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +2 -0
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts +3 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +5 -0
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/{session → integrations/slack}/channel-reply-processor.d.ts +4 -3
- package/dist/integrations/slack/channel-reply-processor.d.ts.map +1 -0
- package/dist/integrations/slack/channel-reply-processor.js +61 -0
- package/dist/integrations/slack/channel-reply-processor.js.map +1 -0
- package/dist/integrations/slack/channel-session-link-processor.d.ts.map +1 -0
- package/dist/{session → integrations/slack}/channel-session-link-processor.js +13 -4
- package/dist/integrations/slack/channel-session-link-processor.js.map +1 -0
- package/dist/integrations/slack/integration.d.ts +25 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +55 -5
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/reply-reminder-processor.d.ts +23 -0
- package/dist/integrations/slack/reply-reminder-processor.d.ts.map +1 -0
- package/dist/integrations/slack/reply-reminder-processor.js +58 -0
- package/dist/integrations/slack/reply-reminder-processor.js.map +1 -0
- package/dist/integrations/slack/reply-tool.d.ts +31 -0
- package/dist/integrations/slack/reply-tool.d.ts.map +1 -0
- package/dist/integrations/slack/reply-tool.js +203 -0
- package/dist/integrations/slack/reply-tool.js.map +1 -0
- package/dist/integrations/slack/share-file-tool.d.ts +45 -0
- package/dist/integrations/slack/share-file-tool.d.ts.map +1 -0
- package/dist/integrations/slack/share-file-tool.js +106 -0
- package/dist/integrations/slack/share-file-tool.js.map +1 -0
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +54 -6
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/mcp/events.d.ts +73 -0
- package/dist/mcp/events.d.ts.map +1 -0
- package/dist/mcp/events.js +158 -0
- package/dist/mcp/events.js.map +1 -0
- package/dist/mcp/routes.d.ts +0 -1
- package/dist/mcp/routes.d.ts.map +1 -1
- package/dist/mcp/routes.js +30 -3
- package/dist/mcp/routes.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/platform-auth-host.d.ts.map +1 -1
- package/dist/platform-auth-host.js +1 -1
- package/dist/platform-auth-host.js.map +1 -1
- package/dist/routes/config.d.ts +2 -0
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +52 -11
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.d.ts +2 -0
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +1 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/session-access.d.ts +7 -2
- package/dist/routes/session-access.d.ts.map +1 -1
- package/dist/routes/session-access.js +41 -4
- package/dist/routes/session-access.js.map +1 -1
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/skills.js +4 -3
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +2 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +161 -24
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +2 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -4
- package/dist/routes/surface.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +11 -3
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts +2 -2
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +3 -3
- package/dist/rules/processor.js.map +1 -1
- package/dist/sandbox/session-retirement.d.ts +11 -0
- package/dist/sandbox/session-retirement.d.ts.map +1 -1
- package/dist/sandbox/session-retirement.js +38 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/sandbox/session-sandbox.d.ts +2 -0
- package/dist/sandbox/session-sandbox.d.ts.map +1 -1
- package/dist/sandbox/session-sandbox.js +5 -1
- package/dist/sandbox/session-sandbox.js.map +1 -1
- package/dist/session/credential-policy.d.ts +4 -2
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js +11 -1
- package/dist/session/credential-policy.js.map +1 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +14 -1
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/memory-settings-hydration.d.ts +0 -8
- package/dist/session/memory-settings-hydration.d.ts.map +1 -1
- package/dist/session/memory-settings-hydration.js +1 -19
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/run-audit.d.ts +1 -0
- package/dist/session/run-audit.d.ts.map +1 -1
- package/dist/session/run-audit.js +7 -2
- package/dist/session/run-audit.js.map +1 -1
- package/dist/session/session-threads.d.ts +5 -0
- package/dist/session/session-threads.d.ts.map +1 -1
- package/dist/session/session-threads.js +8 -1
- package/dist/session/session-threads.js.map +1 -1
- package/dist/session/thinking-default.d.ts +6 -13
- package/dist/session/thinking-default.d.ts.map +1 -1
- package/dist/session/thinking-default.js +18 -22
- package/dist/session/thinking-default.js.map +1 -1
- package/dist/session-owner-signal.d.ts +2 -0
- package/dist/session-owner-signal.d.ts.map +1 -0
- package/dist/session-owner-signal.js +6 -0
- package/dist/session-owner-signal.js.map +1 -0
- package/dist/skills/service.d.ts +6 -7
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +9 -19
- package/dist/skills/service.js.map +1 -1
- package/dist/storage/domains/audit/actions.d.ts +1 -0
- package/dist/storage/domains/audit/actions.d.ts.map +1 -1
- package/dist/storage/domains/audit/actions.js +2 -1
- package/dist/storage/domains/audit/actions.js.map +1 -1
- package/dist/storage/domains/audit/base.d.ts +18 -2
- package/dist/storage/domains/audit/base.d.ts.map +1 -1
- package/dist/storage/domains/audit/base.js +22 -4
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/model-defaults/base.d.ts +8 -0
- package/dist/storage/domains/model-defaults/base.d.ts.map +1 -1
- package/dist/storage/domains/model-defaults/base.js +18 -0
- package/dist/storage/domains/model-defaults/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +26 -1
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +69 -5
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +19 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +30 -2
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/permissions.js +2 -1
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/sandbox-tools.d.ts +12 -0
- package/dist/tools/sandbox-tools.d.ts.map +1 -0
- package/dist/tools/sandbox-tools.js +40 -0
- package/dist/tools/sandbox-tools.js.map +1 -0
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +2 -5
- package/dist/tools/scope.js.map +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +6 -2
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/work-item-branch.d.ts +11 -3
- package/dist/work-item-branch.d.ts.map +1 -1
- package/dist/work-item-branch.js +34 -9
- package/dist/work-item-branch.js.map +1 -1
- package/dist/workspace.d.ts +2 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +10 -2
- package/dist/workspace.js.map +1 -1
- package/factory-skills/configure-factory-rules/SKILL.md +2 -2
- package/package.json +15 -14
- package/dist/session/channel-reply-processor.d.ts.map +0 -1
- package/dist/session/channel-reply-processor.js +0 -46
- package/dist/session/channel-reply-processor.js.map +0 -1
- package/dist/session/channel-session-link-processor.d.ts.map +0 -1
- package/dist/session/channel-session-link-processor.js.map +0 -1
- /package/dist/{session → integrations/slack}/channel-session-link-processor.d.ts +0 -0
package/dist/mcp/routes.js
CHANGED
|
@@ -5,6 +5,7 @@ import { auditActorProfile } from "../storage/domains/audit/base.js";
|
|
|
5
5
|
import { FACTORY_TOOL_ANNOTATIONS } from "../tools/annotations.js";
|
|
6
6
|
import { createFactoryTools } from "../tools/index.js";
|
|
7
7
|
import { propagateClientDisconnect } from "./disconnect.js";
|
|
8
|
+
import { factoryActivityEvent } from "./events.js";
|
|
8
9
|
import { registerApiRoute } from "@mastra/core/server";
|
|
9
10
|
import { createHash } from "crypto";
|
|
10
11
|
import { z } from "zod";
|
|
@@ -19,6 +20,18 @@ const PROTECTED_RESOURCE_PREFIX = "/.well-known/oauth-protected-resource";
|
|
|
19
20
|
* that started this one; a connector session is started by a person, not a session.
|
|
20
21
|
*/
|
|
21
22
|
const EXCLUDED_TOOLS = /* @__PURE__ */ new Set(["factory_message_origin"]);
|
|
23
|
+
/**
|
|
24
|
+
* MCP requires a server version. The Factory doesn't expose its package
|
|
25
|
+
* version at runtime, so this versions the endpoint's interface instead.
|
|
26
|
+
*/
|
|
27
|
+
const FACTORY_MCP_SERVER_VERSION = "1.0.0";
|
|
28
|
+
/** What a connected client is told the server is for and where to start. */
|
|
29
|
+
const FACTORY_MCP_INSTRUCTIONS = [
|
|
30
|
+
"A Mastra Factory tracks software work as work items on a board; agent sessions plan and build them.",
|
|
31
|
+
"Start with `factory_list_attention` for what needs a person, or `search` and `fetch` to find a work item.",
|
|
32
|
+
"Work item tools file, update, move and follow items; session tools start, message and follow the agent sessions working on them.",
|
|
33
|
+
"Every call runs as the signed-in user. When the Factory has a repository, session tools act from a private connector session of theirs."
|
|
34
|
+
].join(" ");
|
|
22
35
|
/** Mastra Platform's WorkOS AuthKit, which issues tokens Platform's `/auth/verify` accepts. */
|
|
23
36
|
const PLATFORM_MCP_AUTHORIZATION_SERVER = "https://auth-kit.mastra.ai";
|
|
24
37
|
function resolveMcpAuthorizationServers(providerName, config, env = process.env) {
|
|
@@ -180,9 +193,22 @@ async function handleMcp(deps, c) {
|
|
|
180
193
|
const server = new MCPServer({
|
|
181
194
|
id: `factory-${factoryProjectId}`,
|
|
182
195
|
name: `${project.name ?? "Mastra"} Factory`,
|
|
183
|
-
version:
|
|
196
|
+
version: FACTORY_MCP_SERVER_VERSION,
|
|
184
197
|
description: "Manage work items, sessions and decisions in a Mastra Factory.",
|
|
185
|
-
|
|
198
|
+
instructions: FACTORY_MCP_INSTRUCTIONS,
|
|
199
|
+
legacy: "stateless",
|
|
200
|
+
tools: createFactoryMcpTools(scope, deps, { persistedSession: persisted }),
|
|
201
|
+
events: { "factory.activity": factoryActivityEvent({
|
|
202
|
+
orgId: tenant.orgId,
|
|
203
|
+
factoryProjectId,
|
|
204
|
+
audit: deps.tools.audit,
|
|
205
|
+
workItems: deps.tools.workItems,
|
|
206
|
+
sessions: deps.tools.sessions,
|
|
207
|
+
viewer: {
|
|
208
|
+
orgId: tenant.orgId,
|
|
209
|
+
userId: tenant.userId
|
|
210
|
+
}
|
|
211
|
+
}) }
|
|
186
212
|
});
|
|
187
213
|
const { req, res } = toReqRes(c.req.raw);
|
|
188
214
|
const done = server.startHTTP({
|
|
@@ -190,7 +216,8 @@ async function handleMcp(deps, c) {
|
|
|
190
216
|
httpPath: mcpPath,
|
|
191
217
|
req,
|
|
192
218
|
res
|
|
193
|
-
}).catch(() => {
|
|
219
|
+
}).catch((error) => {
|
|
220
|
+
console.warn("[factory] MCP request failed:", error);
|
|
194
221
|
if (!res.headersSent) {
|
|
195
222
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
196
223
|
res.end(JSON.stringify({
|
package/dist/mcp/routes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","names":[],"sources":["../../src/mcp/routes.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport { createTool } from '@mastra/core/tools';\nimport type { Tool } from '@mastra/core/tools';\nimport { MCPServer } from '@mastra/mcp';\nimport { toFetchResponse, toReqRes } from 'fetch-to-node';\nimport type { Context } from 'hono';\nimport { z } from 'zod';\n\nimport { getFactoryAuthUser } from '../auth.js';\nimport { PLATFORM_AUTH_PROVIDER } from '../platform-auth-host.js';\nimport type { RouteAuth } from '../routes/route.js';\nimport { ensureFactorySourceSession, FactorySourceSessionResolutionError } from '../session/factory-session.js';\nimport { auditActorProfile } from '../storage/domains/audit/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { FACTORY_TOOL_ANNOTATIONS } from '../tools/annotations.js';\nimport { createFactoryTools } from '../tools/index.js';\nimport type { FactoryToolDependencies, FactoryToolScope } from '../tools/index.js';\nimport { propagateClientDisconnect } from './disconnect.js';\n\nexport const FACTORY_MCP_PATH = '/web/factory/projects/:projectId/mcp';\nconst PROTECTED_RESOURCE_PREFIX = '/.well-known/oauth-protected-resource';\n\n/**\n * Tools an MCP client never gets. `factory_message_origin` answers the session\n * that started this one; a connector session is started by a person, not a session.\n */\nconst EXCLUDED_TOOLS = new Set(['factory_message_origin']);\n\nexport interface FactoryMcpConfig {\n /** Serve the MCP endpoint. Default `true`; it is never served without an auth provider. */\n enabled?: boolean;\n /**\n * OAuth authorization servers advertised in the protected-resource metadata\n * (RFC 9728) so clients like ChatGPT know where to sign in. Defaults to the\n * `MASTRA_FACTORY_MCP_AUTHORIZATION_SERVER` env var, then to Mastra Platform's\n * AuthKit when the Factory uses Platform auth. The endpoint isn't served\n * without one, since clients would have nowhere to sign in.\n */\n authorizationServers?: string[];\n}\n\n/** Mastra Platform's WorkOS AuthKit, which issues tokens Platform's `/auth/verify` accepts. */\nexport const PLATFORM_MCP_AUTHORIZATION_SERVER = 'https://auth-kit.mastra.ai';\n\nexport function resolveMcpAuthorizationServers(\n providerName: string | undefined,\n config: FactoryMcpConfig | undefined,\n env: Record<string, string | undefined> = process.env,\n): string[] {\n if (config?.authorizationServers?.length) return config.authorizationServers;\n const fromEnv = env.MASTRA_FACTORY_MCP_AUTHORIZATION_SERVER?.trim();\n if (fromEnv) return [fromEnv];\n if (providerName === PLATFORM_AUTH_PROVIDER) return [PLATFORM_MCP_AUTHORIZATION_SERVER];\n return [];\n}\n\nexport interface FactoryMcpRoutesDeps {\n auth: RouteAuth;\n publicOrigin: string;\n authorizationServers: string[];\n projects: Pick<FactoryProjectsStorage, 'get'>;\n /** Source-control handles a connector session can be filed under, tried in order. */\n sourceControls: readonly SourceControlStorageHandle[];\n tools: FactoryToolDependencies;\n version?: string;\n}\n\n/** The connector session a user's MCP client acts from: one per user and Factory, stable across requests. */\nexport function connectorSessionId(orgId: string, userId: string, factoryProjectId: string): string {\n const hex = createHash('sha256').update(`mcp\\0${orgId}\\0${userId}\\0${factoryProjectId}`).digest('hex');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;\n}\n\n/**\n * Get or create the private user session behind a person's MCP client, so\n * session-scoped tools (start/signal sessions, follow work items) have a real\n * session to act from. A Factory without a connected repository has nowhere to\n * file one; its tools still run against the stable id.\n */\nasync function ensureConnectorSession(\n deps: FactoryMcpRoutesDeps,\n args: { orgId: string; userId: string; factoryProjectId: string },\n): Promise<{ sessionId: string; persisted: boolean }> {\n const sessionId = connectorSessionId(args.orgId, args.userId, args.factoryProjectId);\n if (await deps.tools.sessions.getBySessionId(sessionId)) return { sessionId, persisted: true };\n for (const sourceControl of deps.sourceControls) {\n try {\n await ensureFactorySourceSession({\n sourceControl,\n orgId: args.orgId,\n factoryProjectId: args.factoryProjectId,\n branch: `factory/mcp-${sessionId.slice(0, 8)}`,\n attributeToUserId: args.userId,\n sessionId,\n visibility: 'private',\n });\n return { sessionId, persisted: true };\n } catch (error) {\n if (!(error instanceof FactorySourceSessionResolutionError)) throw error;\n }\n }\n return { sessionId, persisted: false };\n}\n\nfunction workItemUrl(publicOrigin: string, factoryProjectId: string, workItemId: string): string {\n return `${publicOrigin}/factory/${encodeURIComponent(factoryProjectId)}/work-items/${encodeURIComponent(workItemId)}`;\n}\n\n/**\n * `search` and `fetch`, the two tools ChatGPT's connectors and deep research\n * look for, answered from the Factory's work items.\n */\nfunction createSearchFetchTools(\n scope: FactoryToolScope,\n deps: FactoryMcpRoutesDeps,\n inspect: Tool<any, any, any, any> | undefined,\n): Record<string, Tool<any, any, any, any>> {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n return {\n search: createTool({\n id: 'search',\n description:\n 'Search this Factory’s work items by title or id. Returns matching items with ids to pass to `fetch`.',\n inputSchema: z.object({ query: z.string().describe('Words to look for in work item titles.') }),\n execute: async ({ query }) => {\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const items = await deps.tools.workItems.list(project);\n const results = items\n .filter(item => {\n const haystack = `${item.id} ${item.title}`.toLowerCase();\n return terms.every(term => haystack.includes(term));\n })\n .slice(0, 20)\n .map(item => ({\n id: item.id,\n title: item.title,\n url: workItemUrl(deps.publicOrigin, scope.factoryProjectId, item.id),\n }));\n return { results };\n },\n }),\n fetch: createTool({\n id: 'fetch',\n description: 'Fetch everything the Factory knows about one work item, by the id `search` returned.',\n inputSchema: z.object({ id: z.string().describe('Work item id.') }),\n execute: async ({ id }, context) => {\n if (!inspect?.execute) throw new Error('Work item details are unavailable.');\n const details = (await inspect.execute({ workItem: id }, context)) as { item?: { title?: string } };\n return {\n id,\n title: details.item?.title ?? id,\n text: JSON.stringify(details, null, 2),\n url: workItemUrl(deps.publicOrigin, scope.factoryProjectId, id),\n metadata: { factoryProjectId: scope.factoryProjectId },\n };\n },\n }),\n };\n}\n\n/** The tools a signed-in person's MCP client sees for one Factory. */\nexport function createFactoryMcpTools(\n scope: FactoryToolScope,\n deps: FactoryMcpRoutesDeps,\n options: { persistedSession?: boolean } = {},\n): Record<string, Tool<any, any, any, any>> {\n const factoryTools = createFactoryTools({ scope, deps: deps.tools }) as Record<string, Tool<any, any, any, any>>;\n const tools: Record<string, Tool<any, any, any, any>> = {};\n for (const [name, tool] of Object.entries(factoryTools)) {\n if (EXCLUDED_TOOLS.has(name)) continue;\n // Starting a sub-session needs a real parent session row to file it under.\n if (name === 'factory_start_session' && options.persistedSession === false) continue;\n tools[name] = tool;\n }\n Object.assign(tools, createSearchFetchTools(scope, deps, factoryTools.factory_inspect_work_item));\n for (const [name, tool] of Object.entries(tools)) {\n const annotations = FACTORY_TOOL_ANNOTATIONS[name];\n if (annotations) tool.mcp = { ...tool.mcp, annotations };\n }\n return tools;\n}\n\nfunction resourceMetadataUrl(deps: FactoryMcpRoutesDeps, mcpPath: string): string {\n return `${deps.publicOrigin}${PROTECTED_RESOURCE_PREFIX}${mcpPath}`;\n}\n\nfunction unauthorized(deps: FactoryMcpRoutesDeps, mcpPath: string, error = 'unauthorized'): Response {\n return new Response(JSON.stringify({ error }), {\n status: 401,\n headers: {\n 'Content-Type': 'application/json',\n 'WWW-Authenticate': `Bearer resource_metadata=\"${resourceMetadataUrl(deps, mcpPath)}\"`,\n },\n });\n}\n\nasync function handleMcp(deps: FactoryMcpRoutesDeps, c: Context): Promise<Response> {\n const factoryProjectId = c.req.param('projectId') ?? '';\n const mcpPath = new URL(c.req.url).pathname;\n\n await deps.auth.ensureUser(c);\n const tenant = deps.auth.tenant(c);\n if (!tenant) return unauthorized(deps, mcpPath);\n if (!tenant.orgId) return c.json({ error: 'organization_required' }, 403);\n\n const project = await deps.projects.get({ orgId: tenant.orgId, id: factoryProjectId });\n if (!project) return c.json({ error: 'not_found' }, 404);\n\n const { sessionId, persisted } = await ensureConnectorSession(deps, {\n orgId: tenant.orgId,\n userId: tenant.userId,\n factoryProjectId,\n });\n const actorProfile = auditActorProfile(getFactoryAuthUser(c));\n const scope: FactoryToolScope = {\n orgId: tenant.orgId,\n factoryProjectId,\n userId: tenant.userId,\n sessionId,\n // A user session's memory lives under its owner.\n resourceId: tenant.userId,\n threadId: sessionId,\n kind: 'user',\n // A signed-in person is driving the client: the same reach as a web UI turn.\n interactive: true,\n ...(actorProfile ? { actorProfile } : {}),\n };\n\n const server = new MCPServer({\n id: `factory-${factoryProjectId}`,\n name: `${project.name ?? 'Mastra'} Factory`,\n version: deps.version ?? '1.0.0',\n description: 'Manage work items, sessions and decisions in a Mastra Factory.',\n tools: createFactoryMcpTools(scope, deps, { persistedSession: persisted }),\n });\n\n const { req, res } = toReqRes(c.req.raw);\n const done = server\n .startHTTP({ url: new URL(c.req.url), httpPath: mcpPath, req, res })\n .catch(() => {\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null }),\n );\n }\n })\n .finally(() => void server.close());\n const response = propagateClientDisconnect(await toFetchResponse(res), res);\n if (res.writableEnded) await done;\n return response;\n}\n\n/**\n * The Factory's MCP endpoint: Streamable HTTP at\n * `/web/factory/projects/:projectId/mcp`, plus its RFC 9728 protected-resource\n * metadata. Callers sign in with OAuth through the Factory's auth provider and\n * get the tools a web UI session would, under the same permissions.\n */\nexport function buildFactoryMcpRoutes(deps: FactoryMcpRoutesDeps): ApiRoute[] {\n const handler = (c: unknown) => handleMcp(deps, c as Context);\n return [\n ...(['POST', 'GET', 'DELETE'] as const).map(method =>\n registerApiRoute(FACTORY_MCP_PATH, { method, requiresAuth: false, handler }),\n ),\n registerApiRoute(`${PROTECTED_RESOURCE_PREFIX}${FACTORY_MCP_PATH}`, {\n method: 'GET',\n requiresAuth: false,\n handler: c => {\n const mcpPath = new URL(c.req.url).pathname.slice(PROTECTED_RESOURCE_PREFIX.length);\n return c.json({\n resource: `${deps.publicOrigin}${mcpPath}`,\n authorization_servers: deps.authorizationServers,\n bearer_methods_supported: ['header'],\n resource_name: 'Mastra Factory',\n });\n },\n }),\n ];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../src/mcp/routes.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport { createTool } from '@mastra/core/tools';\nimport type { Tool } from '@mastra/core/tools';\nimport { MCPServer } from '@mastra/mcp';\nimport { toFetchResponse, toReqRes } from 'fetch-to-node';\nimport type { Context } from 'hono';\nimport { z } from 'zod';\n\nimport { getFactoryAuthUser } from '../auth.js';\nimport { PLATFORM_AUTH_PROVIDER } from '../platform-auth-host.js';\nimport type { RouteAuth } from '../routes/route.js';\nimport { ensureFactorySourceSession, FactorySourceSessionResolutionError } from '../session/factory-session.js';\nimport { auditActorProfile } from '../storage/domains/audit/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { FACTORY_TOOL_ANNOTATIONS } from '../tools/annotations.js';\nimport { createFactoryTools } from '../tools/index.js';\nimport type { FactoryToolDependencies, FactoryToolScope } from '../tools/index.js';\nimport { propagateClientDisconnect } from './disconnect.js';\nimport { factoryActivityEvent } from './events.js';\n\nexport const FACTORY_MCP_PATH = '/web/factory/projects/:projectId/mcp';\nconst PROTECTED_RESOURCE_PREFIX = '/.well-known/oauth-protected-resource';\n\n/**\n * Tools an MCP client never gets. `factory_message_origin` answers the session\n * that started this one; a connector session is started by a person, not a session.\n */\nconst EXCLUDED_TOOLS = new Set(['factory_message_origin']);\n\nexport interface FactoryMcpConfig {\n /** Serve the MCP endpoint. Default `true`; it is never served without an auth provider. */\n enabled?: boolean;\n /**\n * OAuth authorization servers advertised in the protected-resource metadata\n * (RFC 9728) so clients like ChatGPT know where to sign in. Defaults to the\n * `MASTRA_FACTORY_MCP_AUTHORIZATION_SERVER` env var, then to Mastra Platform's\n * AuthKit when the Factory uses Platform auth. The endpoint isn't served\n * without one, since clients would have nowhere to sign in.\n */\n authorizationServers?: string[];\n}\n\n/**\n * MCP requires a server version. The Factory doesn't expose its package\n * version at runtime, so this versions the endpoint's interface instead.\n */\nconst FACTORY_MCP_SERVER_VERSION = '1.0.0';\n\n/** What a connected client is told the server is for and where to start. */\nconst FACTORY_MCP_INSTRUCTIONS = [\n 'A Mastra Factory tracks software work as work items on a board; agent sessions plan and build them.',\n 'Start with `factory_list_attention` for what needs a person, or `search` and `fetch` to find a work item.',\n 'Work item tools file, update, move and follow items; session tools start, message and follow the agent sessions working on them.',\n 'Every call runs as the signed-in user. When the Factory has a repository, session tools act from a private connector session of theirs.',\n].join(' ');\n\n/** Mastra Platform's WorkOS AuthKit, which issues tokens Platform's `/auth/verify` accepts. */\nexport const PLATFORM_MCP_AUTHORIZATION_SERVER = 'https://auth-kit.mastra.ai';\n\nexport function resolveMcpAuthorizationServers(\n providerName: string | undefined,\n config: FactoryMcpConfig | undefined,\n env: Record<string, string | undefined> = process.env,\n): string[] {\n if (config?.authorizationServers?.length) return config.authorizationServers;\n const fromEnv = env.MASTRA_FACTORY_MCP_AUTHORIZATION_SERVER?.trim();\n if (fromEnv) return [fromEnv];\n if (providerName === PLATFORM_AUTH_PROVIDER) return [PLATFORM_MCP_AUTHORIZATION_SERVER];\n return [];\n}\n\nexport interface FactoryMcpRoutesDeps {\n auth: RouteAuth;\n publicOrigin: string;\n authorizationServers: string[];\n projects: Pick<FactoryProjectsStorage, 'get'>;\n /** Source-control handles a connector session can be filed under, tried in order. */\n sourceControls: readonly SourceControlStorageHandle[];\n tools: FactoryToolDependencies;\n}\n\n/** The connector session a user's MCP client acts from: one per user and Factory, stable across requests. */\nexport function connectorSessionId(orgId: string, userId: string, factoryProjectId: string): string {\n const hex = createHash('sha256').update(`mcp\\0${orgId}\\0${userId}\\0${factoryProjectId}`).digest('hex');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;\n}\n\n/**\n * Get or create the private user session behind a person's MCP client, so\n * session-scoped tools (start/signal sessions, follow work items) have a real\n * session to act from. A Factory without a connected repository has nowhere to\n * file one; its tools still run against the stable id.\n */\nasync function ensureConnectorSession(\n deps: FactoryMcpRoutesDeps,\n args: { orgId: string; userId: string; factoryProjectId: string },\n): Promise<{ sessionId: string; persisted: boolean }> {\n const sessionId = connectorSessionId(args.orgId, args.userId, args.factoryProjectId);\n if (await deps.tools.sessions.getBySessionId(sessionId)) return { sessionId, persisted: true };\n for (const sourceControl of deps.sourceControls) {\n try {\n await ensureFactorySourceSession({\n sourceControl,\n orgId: args.orgId,\n factoryProjectId: args.factoryProjectId,\n branch: `factory/mcp-${sessionId.slice(0, 8)}`,\n attributeToUserId: args.userId,\n sessionId,\n visibility: 'private',\n });\n return { sessionId, persisted: true };\n } catch (error) {\n if (!(error instanceof FactorySourceSessionResolutionError)) throw error;\n }\n }\n return { sessionId, persisted: false };\n}\n\nfunction workItemUrl(publicOrigin: string, factoryProjectId: string, workItemId: string): string {\n return `${publicOrigin}/factory/${encodeURIComponent(factoryProjectId)}/work-items/${encodeURIComponent(workItemId)}`;\n}\n\n/**\n * `search` and `fetch`, the two tools ChatGPT's connectors and deep research\n * look for, answered from the Factory's work items.\n */\nfunction createSearchFetchTools(\n scope: FactoryToolScope,\n deps: FactoryMcpRoutesDeps,\n inspect: Tool<any, any, any, any> | undefined,\n): Record<string, Tool<any, any, any, any>> {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n return {\n search: createTool({\n id: 'search',\n description:\n 'Search this Factory’s work items by title or id. Returns matching items with ids to pass to `fetch`.',\n inputSchema: z.object({ query: z.string().describe('Words to look for in work item titles.') }),\n execute: async ({ query }) => {\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const items = await deps.tools.workItems.list(project);\n const results = items\n .filter(item => {\n const haystack = `${item.id} ${item.title}`.toLowerCase();\n return terms.every(term => haystack.includes(term));\n })\n .slice(0, 20)\n .map(item => ({\n id: item.id,\n title: item.title,\n url: workItemUrl(deps.publicOrigin, scope.factoryProjectId, item.id),\n }));\n return { results };\n },\n }),\n fetch: createTool({\n id: 'fetch',\n description: 'Fetch everything the Factory knows about one work item, by the id `search` returned.',\n inputSchema: z.object({ id: z.string().describe('Work item id.') }),\n execute: async ({ id }, context) => {\n if (!inspect?.execute) throw new Error('Work item details are unavailable.');\n const details = (await inspect.execute({ workItem: id }, context)) as { item?: { title?: string } };\n return {\n id,\n title: details.item?.title ?? id,\n text: JSON.stringify(details, null, 2),\n url: workItemUrl(deps.publicOrigin, scope.factoryProjectId, id),\n metadata: { factoryProjectId: scope.factoryProjectId },\n };\n },\n }),\n };\n}\n\n/** The tools a signed-in person's MCP client sees for one Factory. */\nexport function createFactoryMcpTools(\n scope: FactoryToolScope,\n deps: FactoryMcpRoutesDeps,\n options: { persistedSession?: boolean } = {},\n): Record<string, Tool<any, any, any, any>> {\n const factoryTools = createFactoryTools({ scope, deps: deps.tools }) as Record<string, Tool<any, any, any, any>>;\n const tools: Record<string, Tool<any, any, any, any>> = {};\n for (const [name, tool] of Object.entries(factoryTools)) {\n if (EXCLUDED_TOOLS.has(name)) continue;\n // Starting a sub-session needs a real parent session row to file it under.\n if (name === 'factory_start_session' && options.persistedSession === false) continue;\n tools[name] = tool;\n }\n Object.assign(tools, createSearchFetchTools(scope, deps, factoryTools.factory_inspect_work_item));\n for (const [name, tool] of Object.entries(tools)) {\n const annotations = FACTORY_TOOL_ANNOTATIONS[name];\n if (annotations) tool.mcp = { ...tool.mcp, annotations };\n }\n return tools;\n}\n\nfunction resourceMetadataUrl(deps: FactoryMcpRoutesDeps, mcpPath: string): string {\n return `${deps.publicOrigin}${PROTECTED_RESOURCE_PREFIX}${mcpPath}`;\n}\n\nfunction unauthorized(deps: FactoryMcpRoutesDeps, mcpPath: string, error = 'unauthorized'): Response {\n return new Response(JSON.stringify({ error }), {\n status: 401,\n headers: {\n 'Content-Type': 'application/json',\n 'WWW-Authenticate': `Bearer resource_metadata=\"${resourceMetadataUrl(deps, mcpPath)}\"`,\n },\n });\n}\n\nasync function handleMcp(deps: FactoryMcpRoutesDeps, c: Context): Promise<Response> {\n const factoryProjectId = c.req.param('projectId') ?? '';\n const mcpPath = new URL(c.req.url).pathname;\n\n await deps.auth.ensureUser(c);\n const tenant = deps.auth.tenant(c);\n if (!tenant) return unauthorized(deps, mcpPath);\n if (!tenant.orgId) return c.json({ error: 'organization_required' }, 403);\n\n const project = await deps.projects.get({ orgId: tenant.orgId, id: factoryProjectId });\n if (!project) return c.json({ error: 'not_found' }, 404);\n\n const { sessionId, persisted } = await ensureConnectorSession(deps, {\n orgId: tenant.orgId,\n userId: tenant.userId,\n factoryProjectId,\n });\n const actorProfile = auditActorProfile(getFactoryAuthUser(c));\n const scope: FactoryToolScope = {\n orgId: tenant.orgId,\n factoryProjectId,\n userId: tenant.userId,\n sessionId,\n // A user session's memory lives under its owner.\n resourceId: tenant.userId,\n threadId: sessionId,\n kind: 'user',\n // A signed-in person is driving the client: the same reach as a web UI turn.\n interactive: true,\n ...(actorProfile ? { actorProfile } : {}),\n };\n\n const server = new MCPServer({\n id: `factory-${factoryProjectId}`,\n name: `${project.name ?? 'Mastra'} Factory`,\n version: FACTORY_MCP_SERVER_VERSION,\n description: 'Manage work items, sessions and decisions in a Mastra Factory.',\n instructions: FACTORY_MCP_INSTRUCTIONS,\n // VS Code and Cursor still open with 2025-era `initialize`; serve them statelessly.\n legacy: 'stateless',\n tools: createFactoryMcpTools(scope, deps, { persistedSession: persisted }),\n events: {\n 'factory.activity': factoryActivityEvent({\n orgId: tenant.orgId,\n factoryProjectId,\n audit: deps.tools.audit,\n workItems: deps.tools.workItems,\n sessions: deps.tools.sessions,\n viewer: { orgId: tenant.orgId, userId: tenant.userId },\n }),\n },\n });\n\n const { req, res } = toReqRes(c.req.raw);\n const done = server\n .startHTTP({ url: new URL(c.req.url), httpPath: mcpPath, req, res })\n .catch(error => {\n console.warn('[factory] MCP request failed:', error);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null }),\n );\n }\n })\n .finally(() => void server.close());\n const response = propagateClientDisconnect(await toFetchResponse(res), res);\n if (res.writableEnded) await done;\n return response;\n}\n\n/**\n * The Factory's MCP endpoint: Streamable HTTP at\n * `/web/factory/projects/:projectId/mcp`, plus its RFC 9728 protected-resource\n * metadata. Callers sign in with OAuth through the Factory's auth provider and\n * get the tools a web UI session would, under the same permissions.\n */\nexport function buildFactoryMcpRoutes(deps: FactoryMcpRoutesDeps): ApiRoute[] {\n const handler = (c: unknown) => handleMcp(deps, c as Context);\n return [\n ...(['POST', 'GET', 'DELETE'] as const).map(method =>\n registerApiRoute(FACTORY_MCP_PATH, { method, requiresAuth: false, handler }),\n ),\n registerApiRoute(`${PROTECTED_RESOURCE_PREFIX}${FACTORY_MCP_PATH}`, {\n method: 'GET',\n requiresAuth: false,\n handler: c => {\n const mcpPath = new URL(c.req.url).pathname.slice(PROTECTED_RESOURCE_PREFIX.length);\n return c.json({\n resource: `${deps.publicOrigin}${mcpPath}`,\n authorization_servers: deps.authorizationServers,\n bearer_methods_supported: ['header'],\n resource_name: 'Mastra Factory',\n });\n },\n }),\n ];\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../../packages/_internals/workspace/dist/index.js"],"sourcesContent":["import { createHash } from \"crypto\";\nimport { posix } from \"path\";\nimport { Buffer } from \"buffer\";\nimport { DirectoryNotEmptyError, DirectoryNotFoundError, FileExistsError, FileNotFoundError, IsDirectoryError, MastraFilesystem, NotDirectoryError } from \"@mastra/core/workspace\";\n//#region src/setup-marker.ts\n/** Directory, relative to the template's build cwd, that holds every marker below. */\nconst SETUP_MARKER_DIR = \".mastra-sandbox\";\n/**\n* Setup completion marker shared by repo templates and their consumers.\n*\n* A repo template writes this file beside the checkout as its last build\n* step, so it exists only in images where every setup command succeeded. Its\n* content is a digest of the setup commands the image ran, letting a sandbox\n* booted from the image tell whether the setup it is about to run already\n* happened. Relative to the template's build cwd, which is also the runtime\n* working directory the repo was cloned into.\n*\n* This is the single-repository marker. Templates built from a repository\n* list write one marker per repository instead ({@link repoSetupMarkerPath}).\n*/\nconst SETUP_MARKER_PATH = \".mastra-sandbox/setup\";\n/**\n* Marker for the workspace-level setup of a multi-repository template: the\n* commands that ran at the working directory after every repository was set\n* up. Written even when there were no such commands, so a consumer can tell\n* \"ran nothing\" from \"never built\".\n*/\nconst WORKSPACE_SETUP_MARKER_PATH = \".mastra-sandbox/workspace-setup\";\n/**\n* One line per repository directory whose setup failed while the build was\n* told to continue. Absent when nothing failed. Consumers check this before\n* trusting a per-repository marker: that marker means every step ran, not\n* that every step succeeded.\n*/\nconst SETUP_FAILED_MARKER_PATH = \".mastra-sandbox/setup-failed\";\n/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */\nfunction repoSetupMarkerPath(repoDir) {\n\treturn `${SETUP_MARKER_DIR}/repos/${repoDir}`;\n}\n/** Blank entries never become build steps, so they never count toward the digest either. */\nfunction normalizeSetupCommands(setupCommand) {\n\treturn (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== \"\");\n}\n/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */\nfunction setupMarkerContent(setupCommand) {\n\treturn `sha256:${createHash(\"sha256\").update(normalizeSetupCommands(setupCommand).join(\"\\n\")).digest(\"hex\")}`;\n}\n/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */\nfunction setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {\n\treturn `mkdir -p \"$(dirname \"${markerPath}\")\" && printf '%s' '${content}' > \"${markerPath}\"`;\n}\n/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */\nconst REPO_DIR_PATTERN = /^[\\w.-]+$/;\n/**\n* Shell step that runs one setup command inside a repository clone.\n*\n* Without the guard this is the plain `cd \"<dir>\" && <cmd>` step. With it,\n* the `cd` sits inside the parenthesized group so that the failure branch\n* runs back at the build cwd and the failure list lands at the workspace\n* level, never inside the repository.\n*/\nfunction guardedSetupCommand({ repoDir, command, continueOnFailure }) {\n\tif (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory \"${repoDir}\" must match ${REPO_DIR_PATTERN}.`);\n\tif (!continueOnFailure) return `cd \"${repoDir}\" && ${command}`;\n\treturn `( cd \"${repoDir}\" && sh -c ${shellQuote$2(command)} ) || { mkdir -p \"${SETUP_MARKER_DIR}\" && grep -qxF -- '${repoDir}' \"${SETUP_FAILED_MARKER_PATH}\" 2>/dev/null || printf '%s\\\\n' '${repoDir}' >> \"${SETUP_FAILED_MARKER_PATH}\"; }`;\n}\nfunction shellQuote$2(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\n//#region src/repo-clone.ts\nfunction repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {\n\treturn `git ${tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : \"\"}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote$1(branch)} ` : \"\"}${shellQuote$1(cloneUrl)} ${shellQuote$1(destination)}`;\n}\n/** Per-invocation auth header; `-c` config never reaches `.git/config`. */\nfunction gitAuthFlag(tokenEnv) {\n\treturn `-c http.extraheader=\"AUTHORIZATION: basic $(printf 'x-access-token:%s' \"$${tokenEnv}\" | base64 -w0)\"`;\n}\nfunction shellQuote$1(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\n//#region src/file-commands.ts\n/**\n* Shell scripts for the file operations a sandbox can only do atomically (or\n* without moving file bytes over the network) by running a command inside the\n* VM. Each builder takes absolute paths and returns one POSIX `sh` script; the\n* caller runs it with `sh -c` and maps the exit codes below to typed errors.\n*/\n/** Exit codes the scripts use for outcomes the caller turns into typed errors. Any other non-zero code is a plain failure. */\nconst FILE_COMMAND_EXIT = {\n\tnotFound: 20,\n\tisDirectory: 21,\n\texists: 22,\n\tnotDirectory: 23,\n\tdestIsDirectory: 24\n};\n/** Single-quote a string for safe POSIX shell interpolation. */\nfunction shellQuote(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\nconst { notFound, isDirectory, exists, notDirectory, destIsDirectory } = FILE_COMMAND_EXIT;\n/** `[ -e ]` is false for a broken symlink, which still occupies the path. */\nconst present = (variable) => `{ [ -e \"$${variable}\" ] || [ -L \"$${variable}\" ]; }`;\n/**\n* A sibling temp path for `path`, so a later `ln` / `mv` onto `path` stays on\n* one filesystem. Hidden, and unique per call.\n*/\nfunction siblingTempPath(path) {\n\tconst suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;\n\treturn posix.join(posix.dirname(path), `.${posix.basename(path)}.mastra-tmp-${suffix}`);\n}\n/**\n* Hard-link `$tmp` to `$dest` and delete `$tmp`, failing with `exists` when\n* something is at `$dest`. `ln` onto a directory (or a link to one) would\n* create the link inside it and succeed, so that case is undone and reported\n* as taken too.\n*/\nfunction linkTempIntoPlace() {\n\treturn [\n\t\t`ln \"$tmp\" \"$dest\" 2>/dev/null || { rm -f \"$tmp\"; ${present(\"dest\")} && exit ${exists}; exit 1; }`,\n\t\t`if [ -d \"$dest\" ]; then rm -f \"$dest/$(basename \"$tmp\")\" \"$tmp\"; exit ${exists}; fi`,\n\t\t`rm -f \"$tmp\"`\n\t];\n}\n/**\n* Copy `src` to `dest` inside the sandbox, creating `dest`'s parent.\n*\n* A file source copies its content (a symlinked source is followed). A\n* directory source with `recursive` copies its contents into `dest`, keeping\n* symlinks inside the tree as links. With `overwrite: false` the destination\n* is claimed atomically: a directory with an exclusive `mkdir`, a file by\n* copying to a temp name and hard-linking it into place (`link(2)` fails when\n* the destination exists).\n*\n* Exits: `notFound` (no source), `isDirectory` (directory source without\n* `recursive`), `destIsDirectory` (a file onto an existing directory or a link\n* to one), `exists`.\n*/\nfunction copyCommand(src, dest, options = {}) {\n\tconst noClobber = options.overwrite === false;\n\treturn [\n\t\t`src=${shellQuote(src)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`[ -e \"$src\" ] || exit ${notFound}`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" || exit 1`,\n\t\t`if [ -d \"$src\" ]; then`,\n\t\toptions.recursive ? ` :` : ` exit ${isDirectory}`,\n\t\tnoClobber ? ` mkdir \"$dest\" 2>/dev/null || exit ${exists}` : ` mkdir -p \"$dest\" || exit 1`,\n\t\t` cp -R -P \"$src\"/. \"$dest\"/ || exit 1`,\n\t\t` exit 0`,\n\t\t`fi`,\n\t\tnoClobber ? `${present(\"dest\")} && exit ${exists}` : `if [ -d \"$dest\" ]; then exit ${destIsDirectory}; fi`,\n\t\t...noClobber ? [\n\t\t\t`tmp=${shellQuote(siblingTempPath(dest))}`,\n\t\t\t`cp \"$src\" \"$tmp\" || { rm -f \"$tmp\"; exit 1; }`,\n\t\t\t...linkTempIntoPlace()\n\t\t] : [`cp \"$src\" \"$dest\" || exit 1`]\n\t].join(\"\\n\");\n}\n/**\n* Move `src` to `dest` without replacing anything at `dest`, creating `dest`'s\n* parent. A skipped `mv -n` is detected by the source surviving, whatever\n* its exit code. If a directory appears at `dest` between the check and the\n* move, `mv` moves `src` into it (BSD `mv` has no `-T`), and that is reported\n* as success; the no-clobber rename itself is atomic.\n*\n* Exits: `notFound` (no source), `exists`.\n*/\nfunction moveNoClobberCommand(src, dest) {\n\treturn [\n\t\t`src=${shellQuote(src)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`${present(\"src\")} || exit ${notFound}`,\n\t\t`${present(\"dest\")} && exit ${exists}`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" || exit 1`,\n\t\t`mv -n \"$src\" \"$dest\" 2>/dev/null || { ${present(\"src\")} && ${present(\"dest\")} && exit ${exists}; exit 1; }`,\n\t\t`${present(\"src\")} && exit ${exists}`,\n\t\t`exit 0`\n\t].join(\"\\n\");\n}\n/**\n* Append the bytes already written to `tmp` onto `dest`, then delete `tmp`.\n* The content travels as a file rather than a command argument, so it has no\n* size limit. Creates `dest` (and its parent) when missing.\n*\n* Exits: `isDirectory` (dest is a directory).\n*/\nfunction appendFromCommand(tmp, dest) {\n\treturn [\n\t\t`tmp=${shellQuote(tmp)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`if [ -d \"$dest\" ]; then rm -f \"$tmp\"; exit ${isDirectory}; fi`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" && cat \"$tmp\" >> \"$dest\"`,\n\t\t`status=$?`,\n\t\t`rm -f \"$tmp\"`,\n\t\t`exit $status`\n\t].join(\"\\n\");\n}\n/**\n* Publish the file already written to `tmp` at `dest` only if nothing is\n* there, then delete `tmp`. `ln` (`link(2)`) is the atomic exclusivity check;\n* `tmp` must be on the same filesystem, e.g. from {@link siblingTempPath}.\n*\n* Exits: `exists`, `notDirectory` (dest's parent is not a directory).\n*/\nfunction linkNoClobberCommand(tmp, dest) {\n\treturn [\n\t\t`tmp=${shellQuote(tmp)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`if [ ! -d \"$(dirname \"$dest\")\" ]; then rm -f \"$tmp\"; exit ${notDirectory}; fi`,\n\t\t`if ${present(\"dest\")}; then rm -f \"$tmp\"; exit ${exists}; fi`,\n\t\t...linkTempIntoPlace()\n\t].join(\"\\n\");\n}\n//#endregion\n//#region src/files-api-filesystem.ts\n/** Default recursion depth for `readdir({ recursive: true })`, matching `LocalFilesystem`. */\nconst DEFAULT_MAX_DEPTH = 100;\nconst toAbsolute = (path) => posix.resolve(\"/\", path);\n/** The entry's own type: a symlink is a link even when it points at a directory. */\nconst isSymlink = (info) => info.symlinkTarget !== void 0;\nfunction toWriteData(content) {\n\tif (typeof content === \"string\") return content;\n\treturn content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);\n}\nfunction matchesExtension(name, extension) {\n\tif (!extension) return true;\n\tconst ext = posix.extname(name);\n\treturn (Array.isArray(extension) ? extension : [extension]).some((e) => e === ext || e === ext.slice(1));\n}\n/**\n* A sandbox's disk, read and written through E2B's envd files API, with the\n* operations the files API can't do in one call (copy, append, no-clobber\n* writes and moves) run as one shell command inside the VM so they are atomic\n* and file bytes never round-trip through the host. Paths are absolute inside\n* the sandbox; this class does not confine paths or symlinks to any root.\n*\n* Providers supply how to reach the files API and the shell, and how to read\n* their SDK's errors.\n*\n* If a provider's `runCommand` re-runs a command whose first run already\n* finished (e.g. a retry after a dropped connection), the second run sees the\n* first's result: a no-clobber copy, write or move reports `FileExistsError`\n* or `FileNotFoundError`, and an append fails, even though the work was done.\n*/\nvar FilesApiFilesystem = class extends MastraFilesystem {\n\tasync readFile(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst bytes = await this.runFiles(\"readFile\", async (files) => {\n\t\t\ttry {\n\t\t\t\treturn await files.read(abs, { format: \"bytes\" });\n\t\t\t} catch (error) {\n\t\t\t\tconst kind = this.errorKind(error);\n\t\t\t\tif (kind === \"not-found\") throw new FileNotFoundError(path);\n\t\t\t\tif (kind === \"invalid-argument\") throw new IsDirectoryError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t\tconst buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\t\treturn options?.encoding ? buffer.toString(options.encoding) : buffer;\n\t}\n\tasync writeFile(path, content, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tif (options?.overwrite !== false) {\n\t\t\tawait this.runFiles(\"writeFile\", async (files) => {\n\t\t\t\tif (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\t\tawait this.#write(files, abs, path, toWriteData(content));\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst tmp = siblingTempPath(abs);\n\t\tconst upload = async (files) => {\n\t\t\tif (options.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\tawait this.#write(files, tmp, path, toWriteData(content));\n\t\t};\n\t\tawait this.#publishTemp(tmp, \"writeFile\", upload, linkNoClobberCommand(tmp, abs), path, {\n\t\t\texists: () => new FileExistsError(path),\n\t\t\tnotDirectory: () => new NotDirectoryError(posix.dirname(path))\n\t\t});\n\t}\n\tasync appendFile(path, content) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst tmp = siblingTempPath(abs);\n\t\tconst upload = (files) => this.#write(files, tmp, path, toWriteData(content));\n\t\tawait this.#publishTemp(tmp, \"appendFile\", upload, appendFromCommand(tmp, abs), path, { isDirectory: () => new IsDirectoryError(path) });\n\t}\n\tasync deleteFile(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"deleteFile\", async (files) => {\n\t\t\tconst info = await this.#info(files, abs);\n\t\t\tif (!info) {\n\t\t\t\tif (options?.force) return;\n\t\t\t\tthrow new FileNotFoundError(path);\n\t\t\t}\n\t\t\tif (!isSymlink(info) && info.type === \"dir\") throw new IsDirectoryError(path);\n\t\t\tawait files.remove(abs);\n\t\t});\n\t}\n\t/**\n\t* One `cp` inside the VM. A recursive copy keeps symlinks inside the tree as\n\t* links; `overwrite: false` claims the destination atomically.\n\t*/\n\tasync copyFile(src, dest, options) {\n\t\tconst command = copyCommand(toAbsolute(src), toAbsolute(dest), {\n\t\t\trecursive: options?.recursive,\n\t\t\toverwrite: options?.overwrite\n\t\t});\n\t\tawait this.#command(\"copyFile\", command, `${src} -> ${dest}`, {\n\t\t\tnotFound: () => new FileNotFoundError(src),\n\t\t\tisDirectory: () => new IsDirectoryError(src),\n\t\t\tdestIsDirectory: () => new IsDirectoryError(dest),\n\t\t\texists: () => new FileExistsError(dest)\n\t\t});\n\t}\n\tasync moveFile(src, dest, options) {\n\t\tconst srcAbs = toAbsolute(src);\n\t\tconst destAbs = toAbsolute(dest);\n\t\tif (options?.overwrite === false) {\n\t\t\tawait this.#command(\"moveFile\", moveNoClobberCommand(srcAbs, destAbs), `${src} -> ${dest}`, {\n\t\t\t\tnotFound: () => new FileNotFoundError(src),\n\t\t\t\texists: () => new FileExistsError(dest)\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tawait this.runFiles(\"moveFile\", async (files) => {\n\t\t\ttry {\n\t\t\t\tawait files.rename(srcAbs, destAbs);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorKind(error) === \"not-found\") throw new FileNotFoundError(src);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\tasync mkdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"mkdir\", async (files) => {\n\t\t\tif (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\ttry {\n\t\t\t\tawait files.makeDir(abs);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorKind(error) === \"invalid-argument\") throw new FileExistsError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\tasync rmdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"rmdir\", async (files) => {\n\t\t\tconst info = await this.#info(files, abs);\n\t\t\tif (!info) {\n\t\t\t\tif (options?.force) return;\n\t\t\t\tthrow new DirectoryNotFoundError(path);\n\t\t\t}\n\t\t\tif (!options?.recursive) {\n\t\t\t\tif (isSymlink(info) || info.type !== \"dir\") throw new NotDirectoryError(path);\n\t\t\t\tif ((await files.list(abs)).length > 0) throw new DirectoryNotEmptyError(path);\n\t\t\t} else if (!isSymlink(info) && info.type !== \"dir\") throw new NotDirectoryError(path);\n\t\t\tawait files.remove(abs);\n\t\t});\n\t}\n\tasync readdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst depth = options?.recursive ? (options.maxDepth ?? DEFAULT_MAX_DEPTH) + 1 : 1;\n\t\tconst entries = await this.runFiles(\"readdir\", async (files) => {\n\t\t\ttry {\n\t\t\t\treturn await files.list(abs, { depth });\n\t\t\t} catch (error) {\n\t\t\t\tconst kind = this.errorKind(error);\n\t\t\t\tif (kind === \"not-found\") throw new DirectoryNotFoundError(path);\n\t\t\t\tif (kind === \"invalid-argument\") throw new NotDirectoryError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t\tconst result = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.type) continue;\n\t\t\tconst name = posix.relative(abs, entry.path);\n\t\t\tconst type = entry.type === \"dir\" ? \"directory\" : \"file\";\n\t\t\tif (type === \"file\" && !matchesExtension(name, options?.extension)) continue;\n\t\t\tconst link = isSymlink(entry);\n\t\t\tresult.push({\n\t\t\t\tname,\n\t\t\t\ttype,\n\t\t\t\t...type === \"file\" && !link && { size: entry.size },\n\t\t\t\t...link && {\n\t\t\t\t\tisSymlink: true,\n\t\t\t\t\tsymlinkTarget: entry.symlinkTarget\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t}\n\tasync exists(path) {\n\t\tconst abs = toAbsolute(path);\n\t\treturn this.runFiles(\"exists\", async (files) => Boolean((await this.#info(files, abs))?.type));\n\t}\n\tasync stat(path) {\n\t\tconst abs = toAbsolute(path);\n\t\treturn this.runFiles(\"stat\", async (files) => {\n\t\t\tlet info = await this.#info(files, abs);\n\t\t\tif (info?.type && isSymlink(info)) info = await this.#info(files, posix.resolve(posix.dirname(abs), info.symlinkTarget));\n\t\t\tif (!info?.type) throw new FileNotFoundError(path);\n\t\t\tconst modifiedAt = info.modifiedTime ?? /* @__PURE__ */ new Date(0);\n\t\t\treturn {\n\t\t\t\tname: posix.basename(abs),\n\t\t\t\tpath: abs,\n\t\t\t\ttype: info.type === \"dir\" ? \"directory\" : \"file\",\n\t\t\t\tsize: info.type === \"dir\" ? 0 : info.size,\n\t\t\t\tcreatedAt: modifiedAt,\n\t\t\t\tmodifiedAt\n\t\t\t};\n\t\t});\n\t}\n\t/** `getInfo` without following the entry itself, or `undefined` when nothing is there. */\n\tasync #info(files, abs) {\n\t\ttry {\n\t\t\treturn await files.getInfo(abs);\n\t\t} catch (error) {\n\t\t\tif (this.errorKind(error) === \"not-found\") return void 0;\n\t\t\tthrow error;\n\t\t}\n\t}\n\tasync #assertParentDirectory(files, abs, path) {\n\t\tconst parent = await this.#info(files, posix.dirname(abs));\n\t\tif (!parent?.type) throw new DirectoryNotFoundError(posix.dirname(path));\n\t\tif (parent.type !== \"dir\") throw new NotDirectoryError(posix.dirname(path));\n\t}\n\tasync #write(files, abs, path, data) {\n\t\ttry {\n\t\t\tawait files.write(abs, data);\n\t\t} catch (error) {\n\t\t\tif (this.errorKind(error) === \"invalid-argument\") throw new IsDirectoryError(path);\n\t\t\tthrow error;\n\t\t}\n\t}\n\t/**\n\t* Upload the temp file `tmp`, then run the command that consumes it. The\n\t* script deletes it on every path it controls; if the upload or the command\n\t* fails part-way, delete it here so it doesn't linger in the user's tree.\n\t*/\n\tasync #publishTemp(tmp, operation, upload, script, subject, errors) {\n\t\ttry {\n\t\t\tawait this.runFiles(operation, upload);\n\t\t\tawait this.#command(operation, script, subject, errors);\n\t\t} catch (error) {\n\t\t\tawait this.runFiles(operation, (files) => files.remove(tmp)).catch(() => {});\n\t\t\tthrow error;\n\t\t}\n\t}\n\t/** Run a file command and turn its known exit codes into typed errors. */\n\tasync #command(operation, script, subject, errors) {\n\t\tconst result = await this.runCommand(operation, script);\n\t\tif (result.exitCode === 0) return;\n\t\tfor (const [outcome, code] of Object.entries(FILE_COMMAND_EXIT)) {\n\t\t\tconst toError = errors[outcome];\n\t\t\tif (code === result.exitCode && toError) throw toError();\n\t\t}\n\t\tthrow new Error(`${operation} ${subject} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);\n\t}\n};\n//#endregion\nexport { FILE_COMMAND_EXIT, FilesApiFilesystem, SETUP_FAILED_MARKER_PATH, SETUP_MARKER_DIR, SETUP_MARKER_PATH, WORKSPACE_SETUP_MARKER_PATH, appendFromCommand, copyCommand, guardedSetupCommand, linkNoClobberCommand, moveNoClobberCommand, normalizeSetupCommands, repoCloneCommand, repoSetupMarkerPath, setupMarkerCommand, setupMarkerContent, shellQuote, siblingTempPath };\n\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;AAMA,MAAM,mBAAmB;;;;;;;;;;;;;;AAczB,MAAM,oBAAoB;;;;;;;AAO1B,MAAM,8BAA8B;;;;;;;AAOpC,MAAM,2BAA2B;;AAEjC,SAAS,oBAAoB,SAAS;CACrC,OAAO,GAAG,iBAAiB,SAAS;AACrC;;AAEA,SAAS,uBAAuB,cAAc;CAC7C,QAAQ,iBAAiB,KAAK,IAAI,CAAC,IAAI,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,YAAY,EAAA,CAAG,QAAQ,YAAY,QAAQ,KAAK,MAAM,EAAE;AAC9I;;AAEA,SAAS,mBAAmB,cAAc;CACzC,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,uBAAuB,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK;AAC3G;;;;;;;;AA4CA,MAAM,oBAAoB;CACzB,UAAU;CACV,aAAa;CACb,QAAQ;CACR,cAAc;CACd,iBAAiB;AAClB;AAKA,MAAM,EAAE,UAAU,aAAa,QAAQ,cAAc,oBAAoB"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../../packages/_internals/workspace/dist/index.js"],"sourcesContent":["import { createHash } from \"crypto\";\nimport { posix } from \"path\";\nimport { Buffer } from \"buffer\";\nimport { DirectoryNotEmptyError, DirectoryNotFoundError, FileExistsError, FileNotFoundError, IsDirectoryError, MastraFilesystem, NotDirectoryError, ProcessHandle, SandboxProcessManager, UnsupportedStdinCloseError } from \"@mastra/core/workspace\";\n//#region src/setup-marker.ts\n/** Directory, relative to the template's build cwd, that holds every marker below. */\nconst SETUP_MARKER_DIR = \".mastra-sandbox\";\n/**\n* Setup completion marker shared by repo templates and their consumers.\n*\n* A repo template writes this file beside the checkout as its last build\n* step, so it exists only in images where every setup command succeeded. Its\n* content is a digest of the setup commands the image ran, letting a sandbox\n* booted from the image tell whether the setup it is about to run already\n* happened. Relative to the template's build cwd, which is also the runtime\n* working directory the repo was cloned into.\n*\n* This is the single-repository marker. Templates built from a repository\n* list write one marker per repository instead ({@link repoSetupMarkerPath}).\n*/\nconst SETUP_MARKER_PATH = \".mastra-sandbox/setup\";\n/**\n* Marker for the workspace-level setup of a multi-repository template: the\n* commands that ran at the working directory after every repository was set\n* up. Written even when there were no such commands, so a consumer can tell\n* \"ran nothing\" from \"never built\".\n*/\nconst WORKSPACE_SETUP_MARKER_PATH = \".mastra-sandbox/workspace-setup\";\n/**\n* One line per repository directory whose setup failed while the build was\n* told to continue. Absent when nothing failed. Consumers check this before\n* trusting a per-repository marker: that marker means every step ran, not\n* that every step succeeded.\n*/\nconst SETUP_FAILED_MARKER_PATH = \".mastra-sandbox/setup-failed\";\n/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */\nfunction repoSetupMarkerPath(repoDir) {\n\treturn `${SETUP_MARKER_DIR}/repos/${repoDir}`;\n}\n/** Blank entries never become build steps, so they never count toward the digest either. */\nfunction normalizeSetupCommands(setupCommand) {\n\treturn (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== \"\");\n}\n/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */\nfunction setupMarkerContent(setupCommand) {\n\treturn `sha256:${createHash(\"sha256\").update(normalizeSetupCommands(setupCommand).join(\"\\n\")).digest(\"hex\")}`;\n}\n/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */\nfunction setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {\n\treturn `mkdir -p \"$(dirname \"${markerPath}\")\" && printf '%s' '${content}' > \"${markerPath}\"`;\n}\n/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */\nconst REPO_DIR_PATTERN = /^[\\w.-]+$/;\n/**\n* Shell step that runs one setup command inside a repository clone.\n*\n* Without the guard this is the plain `cd \"<dir>\" && <cmd>` step. With it,\n* the `cd` sits inside the parenthesized group so that the failure branch\n* runs back at the build cwd and the failure list lands at the workspace\n* level, never inside the repository.\n*/\nfunction guardedSetupCommand({ repoDir, command, continueOnFailure }) {\n\tif (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory \"${repoDir}\" must match ${REPO_DIR_PATTERN}.`);\n\tif (!continueOnFailure) return `cd \"${repoDir}\" && ${command}`;\n\treturn `( cd \"${repoDir}\" && sh -c ${shellQuote$2(command)} ) || { mkdir -p \"${SETUP_MARKER_DIR}\" && grep -qxF -- '${repoDir}' \"${SETUP_FAILED_MARKER_PATH}\" 2>/dev/null || printf '%s\\\\n' '${repoDir}' >> \"${SETUP_FAILED_MARKER_PATH}\"; }`;\n}\nfunction shellQuote$2(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\n//#region src/repo-clone.ts\nfunction repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {\n\treturn `git ${tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : \"\"}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote$1(branch)} ` : \"\"}${shellQuote$1(cloneUrl)} ${shellQuote$1(destination)}`;\n}\n/** Per-invocation auth header; `-c` config never reaches `.git/config`. */\nfunction gitAuthFlag(tokenEnv) {\n\treturn `-c http.extraheader=\"AUTHORIZATION: basic $(printf 'x-access-token:%s' \"$${tokenEnv}\" | base64 -w0)\"`;\n}\nfunction shellQuote$1(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\n//#region src/file-commands.ts\n/**\n* Shell scripts for the file operations a sandbox can only do atomically (or\n* without moving file bytes over the network) by running a command inside the\n* VM. Each builder takes absolute paths and returns one POSIX `sh` script; the\n* caller runs it with `sh -c` and maps the exit codes below to typed errors.\n*/\n/** Exit codes the scripts use for outcomes the caller turns into typed errors. Any other non-zero code is a plain failure. */\nconst FILE_COMMAND_EXIT = {\n\tnotFound: 20,\n\tisDirectory: 21,\n\texists: 22,\n\tnotDirectory: 23,\n\tdestIsDirectory: 24\n};\n/** Single-quote a string for safe POSIX shell interpolation. */\nfunction shellQuote(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\nconst { notFound, isDirectory, exists, notDirectory, destIsDirectory } = FILE_COMMAND_EXIT;\n/** `[ -e ]` is false for a broken symlink, which still occupies the path. */\nconst present = (variable) => `{ [ -e \"$${variable}\" ] || [ -L \"$${variable}\" ]; }`;\n/**\n* A sibling temp path for `path`, so a later `ln` / `mv` onto `path` stays on\n* one filesystem. Hidden, and unique per call.\n*/\nfunction siblingTempPath(path) {\n\tconst suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;\n\treturn posix.join(posix.dirname(path), `.${posix.basename(path)}.mastra-tmp-${suffix}`);\n}\n/**\n* Hard-link `$tmp` to `$dest` and delete `$tmp`, failing with `exists` when\n* something is at `$dest`. `ln` onto a directory (or a link to one) would\n* create the link inside it and succeed, so that case is undone and reported\n* as taken too.\n*/\nfunction linkTempIntoPlace() {\n\treturn [\n\t\t`ln \"$tmp\" \"$dest\" 2>/dev/null || { rm -f \"$tmp\"; ${present(\"dest\")} && exit ${exists}; exit 1; }`,\n\t\t`if [ -d \"$dest\" ]; then rm -f \"$dest/$(basename \"$tmp\")\" \"$tmp\"; exit ${exists}; fi`,\n\t\t`rm -f \"$tmp\"`\n\t];\n}\n/**\n* Copy `src` to `dest` inside the sandbox, creating `dest`'s parent.\n*\n* A file source copies its content (a symlinked source is followed). A\n* directory source with `recursive` copies its contents into `dest`, keeping\n* symlinks inside the tree as links. With `overwrite: false` the destination\n* is claimed atomically: a directory with an exclusive `mkdir`, a file by\n* copying to a temp name and hard-linking it into place (`link(2)` fails when\n* the destination exists).\n*\n* Exits: `notFound` (no source), `isDirectory` (directory source without\n* `recursive`), `destIsDirectory` (a file onto an existing directory or a link\n* to one), `exists`.\n*/\nfunction copyCommand(src, dest, options = {}) {\n\tconst noClobber = options.overwrite === false;\n\treturn [\n\t\t`src=${shellQuote(src)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`[ -e \"$src\" ] || exit ${notFound}`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" || exit 1`,\n\t\t`if [ -d \"$src\" ]; then`,\n\t\toptions.recursive ? ` :` : ` exit ${isDirectory}`,\n\t\tnoClobber ? ` mkdir \"$dest\" 2>/dev/null || exit ${exists}` : ` mkdir -p \"$dest\" || exit 1`,\n\t\t` cp -R -P \"$src\"/. \"$dest\"/ || exit 1`,\n\t\t` exit 0`,\n\t\t`fi`,\n\t\tnoClobber ? `${present(\"dest\")} && exit ${exists}` : `if [ -d \"$dest\" ]; then exit ${destIsDirectory}; fi`,\n\t\t...noClobber ? [\n\t\t\t`tmp=${shellQuote(siblingTempPath(dest))}`,\n\t\t\t`cp \"$src\" \"$tmp\" || { rm -f \"$tmp\"; exit 1; }`,\n\t\t\t...linkTempIntoPlace()\n\t\t] : [`cp \"$src\" \"$dest\" || exit 1`]\n\t].join(\"\\n\");\n}\n/**\n* Move `src` to `dest` without replacing anything at `dest`, creating `dest`'s\n* parent. A skipped `mv -n` is detected by the source surviving, whatever\n* its exit code. If a directory appears at `dest` between the check and the\n* move, `mv` moves `src` into it (BSD `mv` has no `-T`), and that is reported\n* as success; the no-clobber rename itself is atomic.\n*\n* Exits: `notFound` (no source), `exists`.\n*/\nfunction moveNoClobberCommand(src, dest) {\n\treturn [\n\t\t`src=${shellQuote(src)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`${present(\"src\")} || exit ${notFound}`,\n\t\t`${present(\"dest\")} && exit ${exists}`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" || exit 1`,\n\t\t`mv -n \"$src\" \"$dest\" 2>/dev/null || { ${present(\"src\")} && ${present(\"dest\")} && exit ${exists}; exit 1; }`,\n\t\t`${present(\"src\")} && exit ${exists}`,\n\t\t`exit 0`\n\t].join(\"\\n\");\n}\n/**\n* Append the bytes already written to `tmp` onto `dest`, then delete `tmp`.\n* The content travels as a file rather than a command argument, so it has no\n* size limit. Creates `dest` (and its parent) when missing.\n*\n* Exits: `isDirectory` (dest is a directory).\n*/\nfunction appendFromCommand(tmp, dest) {\n\treturn [\n\t\t`tmp=${shellQuote(tmp)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`if [ -d \"$dest\" ]; then rm -f \"$tmp\"; exit ${isDirectory}; fi`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" && cat \"$tmp\" >> \"$dest\"`,\n\t\t`status=$?`,\n\t\t`rm -f \"$tmp\"`,\n\t\t`exit $status`\n\t].join(\"\\n\");\n}\n/**\n* Publish the file already written to `tmp` at `dest` only if nothing is\n* there, then delete `tmp`. `ln` (`link(2)`) is the atomic exclusivity check;\n* `tmp` must be on the same filesystem, e.g. from {@link siblingTempPath}.\n*\n* Exits: `exists`, `notDirectory` (dest's parent is not a directory).\n*/\nfunction linkNoClobberCommand(tmp, dest) {\n\treturn [\n\t\t`tmp=${shellQuote(tmp)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`if [ ! -d \"$(dirname \"$dest\")\" ]; then rm -f \"$tmp\"; exit ${notDirectory}; fi`,\n\t\t`if ${present(\"dest\")}; then rm -f \"$tmp\"; exit ${exists}; fi`,\n\t\t...linkTempIntoPlace()\n\t].join(\"\\n\");\n}\n//#endregion\n//#region src/files-api-filesystem.ts\n/** Default recursion depth for `readdir({ recursive: true })`, matching `LocalFilesystem`. */\nconst DEFAULT_MAX_DEPTH = 100;\nconst toAbsolute = (path) => posix.resolve(\"/\", path);\n/** The entry's own type: a symlink is a link even when it points at a directory. */\nconst isSymlink = (info) => info.symlinkTarget !== void 0;\nfunction toWriteData(content) {\n\tif (typeof content === \"string\") return content;\n\treturn content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);\n}\nfunction matchesExtension(name, extension) {\n\tif (!extension) return true;\n\tconst ext = posix.extname(name);\n\treturn (Array.isArray(extension) ? extension : [extension]).some((e) => e === ext || e === ext.slice(1));\n}\n/**\n* A sandbox's disk, read and written through E2B's envd files API, with the\n* operations the files API can't do in one call (copy, append, no-clobber\n* writes and moves) run as one shell command inside the VM so they are atomic\n* and file bytes never round-trip through the host. Paths are absolute inside\n* the sandbox; this class does not confine paths or symlinks to any root.\n*\n* Providers supply how to reach the files API and the shell, and how to read\n* their SDK's errors.\n*\n* If a provider's `runCommand` re-runs a command whose first run already\n* finished (e.g. a retry after a dropped connection), the second run sees the\n* first's result: a no-clobber copy, write or move reports `FileExistsError`\n* or `FileNotFoundError`, and an append fails, even though the work was done.\n*/\nvar FilesApiFilesystem = class extends MastraFilesystem {\n\tasync readFile(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst bytes = await this.runFiles(\"readFile\", async (files) => {\n\t\t\ttry {\n\t\t\t\treturn await files.read(abs, { format: \"bytes\" });\n\t\t\t} catch (error) {\n\t\t\t\tconst kind = this.errorKind(error);\n\t\t\t\tif (kind === \"not-found\") throw new FileNotFoundError(path);\n\t\t\t\tif (kind === \"invalid-argument\") throw new IsDirectoryError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t\tconst buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\t\treturn options?.encoding ? buffer.toString(options.encoding) : buffer;\n\t}\n\tasync writeFile(path, content, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tif (options?.overwrite !== false) {\n\t\t\tawait this.runFiles(\"writeFile\", async (files) => {\n\t\t\t\tif (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\t\tawait this.#write(files, abs, path, toWriteData(content));\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst tmp = siblingTempPath(abs);\n\t\tconst upload = async (files) => {\n\t\t\tif (options.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\tawait this.#write(files, tmp, path, toWriteData(content));\n\t\t};\n\t\tawait this.#publishTemp(tmp, \"writeFile\", upload, linkNoClobberCommand(tmp, abs), path, {\n\t\t\texists: () => new FileExistsError(path),\n\t\t\tnotDirectory: () => new NotDirectoryError(posix.dirname(path))\n\t\t});\n\t}\n\tasync appendFile(path, content) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst tmp = siblingTempPath(abs);\n\t\tconst upload = (files) => this.#write(files, tmp, path, toWriteData(content));\n\t\tawait this.#publishTemp(tmp, \"appendFile\", upload, appendFromCommand(tmp, abs), path, { isDirectory: () => new IsDirectoryError(path) });\n\t}\n\tasync deleteFile(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"deleteFile\", async (files) => {\n\t\t\tconst info = await this.#info(files, abs);\n\t\t\tif (!info) {\n\t\t\t\tif (options?.force) return;\n\t\t\t\tthrow new FileNotFoundError(path);\n\t\t\t}\n\t\t\tif (!isSymlink(info) && info.type === \"dir\") throw new IsDirectoryError(path);\n\t\t\tawait files.remove(abs);\n\t\t});\n\t}\n\t/**\n\t* One `cp` inside the VM. A recursive copy keeps symlinks inside the tree as\n\t* links; `overwrite: false` claims the destination atomically.\n\t*/\n\tasync copyFile(src, dest, options) {\n\t\tconst command = copyCommand(toAbsolute(src), toAbsolute(dest), {\n\t\t\trecursive: options?.recursive,\n\t\t\toverwrite: options?.overwrite\n\t\t});\n\t\tawait this.#command(\"copyFile\", command, `${src} -> ${dest}`, {\n\t\t\tnotFound: () => new FileNotFoundError(src),\n\t\t\tisDirectory: () => new IsDirectoryError(src),\n\t\t\tdestIsDirectory: () => new IsDirectoryError(dest),\n\t\t\texists: () => new FileExistsError(dest)\n\t\t});\n\t}\n\tasync moveFile(src, dest, options) {\n\t\tconst srcAbs = toAbsolute(src);\n\t\tconst destAbs = toAbsolute(dest);\n\t\tif (options?.overwrite === false) {\n\t\t\tawait this.#command(\"moveFile\", moveNoClobberCommand(srcAbs, destAbs), `${src} -> ${dest}`, {\n\t\t\t\tnotFound: () => new FileNotFoundError(src),\n\t\t\t\texists: () => new FileExistsError(dest)\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tawait this.runFiles(\"moveFile\", async (files) => {\n\t\t\ttry {\n\t\t\t\tawait files.rename(srcAbs, destAbs);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorKind(error) === \"not-found\") throw new FileNotFoundError(src);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\tasync mkdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"mkdir\", async (files) => {\n\t\t\tif (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\ttry {\n\t\t\t\tawait files.makeDir(abs);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorKind(error) === \"invalid-argument\") throw new FileExistsError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\tasync rmdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"rmdir\", async (files) => {\n\t\t\tconst info = await this.#info(files, abs);\n\t\t\tif (!info) {\n\t\t\t\tif (options?.force) return;\n\t\t\t\tthrow new DirectoryNotFoundError(path);\n\t\t\t}\n\t\t\tif (!options?.recursive) {\n\t\t\t\tif (isSymlink(info) || info.type !== \"dir\") throw new NotDirectoryError(path);\n\t\t\t\tif ((await files.list(abs)).length > 0) throw new DirectoryNotEmptyError(path);\n\t\t\t} else if (!isSymlink(info) && info.type !== \"dir\") throw new NotDirectoryError(path);\n\t\t\tawait files.remove(abs);\n\t\t});\n\t}\n\tasync readdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst depth = options?.recursive ? (options.maxDepth ?? DEFAULT_MAX_DEPTH) + 1 : 1;\n\t\tconst entries = await this.runFiles(\"readdir\", async (files) => {\n\t\t\ttry {\n\t\t\t\treturn await files.list(abs, { depth });\n\t\t\t} catch (error) {\n\t\t\t\tconst kind = this.errorKind(error);\n\t\t\t\tif (kind === \"not-found\") throw new DirectoryNotFoundError(path);\n\t\t\t\tif (kind === \"invalid-argument\") throw new NotDirectoryError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t\tconst result = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.type) continue;\n\t\t\tconst name = posix.relative(abs, entry.path);\n\t\t\tconst type = entry.type === \"dir\" ? \"directory\" : \"file\";\n\t\t\tif (type === \"file\" && !matchesExtension(name, options?.extension)) continue;\n\t\t\tconst link = isSymlink(entry);\n\t\t\tresult.push({\n\t\t\t\tname,\n\t\t\t\ttype,\n\t\t\t\t...type === \"file\" && !link && { size: entry.size },\n\t\t\t\t...link && {\n\t\t\t\t\tisSymlink: true,\n\t\t\t\t\tsymlinkTarget: entry.symlinkTarget\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t}\n\tasync exists(path) {\n\t\tconst abs = toAbsolute(path);\n\t\treturn this.runFiles(\"exists\", async (files) => Boolean((await this.#info(files, abs))?.type));\n\t}\n\tasync stat(path) {\n\t\tconst abs = toAbsolute(path);\n\t\treturn this.runFiles(\"stat\", async (files) => {\n\t\t\tlet info = await this.#info(files, abs);\n\t\t\tif (info?.type && isSymlink(info)) info = await this.#info(files, posix.resolve(posix.dirname(abs), info.symlinkTarget));\n\t\t\tif (!info?.type) throw new FileNotFoundError(path);\n\t\t\tconst modifiedAt = info.modifiedTime ?? /* @__PURE__ */ new Date(0);\n\t\t\treturn {\n\t\t\t\tname: posix.basename(abs),\n\t\t\t\tpath: abs,\n\t\t\t\ttype: info.type === \"dir\" ? \"directory\" : \"file\",\n\t\t\t\tsize: info.type === \"dir\" ? 0 : info.size,\n\t\t\t\tcreatedAt: modifiedAt,\n\t\t\t\tmodifiedAt\n\t\t\t};\n\t\t});\n\t}\n\t/** `getInfo` without following the entry itself, or `undefined` when nothing is there. */\n\tasync #info(files, abs) {\n\t\ttry {\n\t\t\treturn await files.getInfo(abs);\n\t\t} catch (error) {\n\t\t\tif (this.errorKind(error) === \"not-found\") return void 0;\n\t\t\tthrow error;\n\t\t}\n\t}\n\tasync #assertParentDirectory(files, abs, path) {\n\t\tconst parent = await this.#info(files, posix.dirname(abs));\n\t\tif (!parent?.type) throw new DirectoryNotFoundError(posix.dirname(path));\n\t\tif (parent.type !== \"dir\") throw new NotDirectoryError(posix.dirname(path));\n\t}\n\tasync #write(files, abs, path, data) {\n\t\ttry {\n\t\t\tawait files.write(abs, data);\n\t\t} catch (error) {\n\t\t\tif (this.errorKind(error) === \"invalid-argument\") throw new IsDirectoryError(path);\n\t\t\tthrow error;\n\t\t}\n\t}\n\t/**\n\t* Upload the temp file `tmp`, then run the command that consumes it. The\n\t* script deletes it on every path it controls; if the upload or the command\n\t* fails part-way, delete it here so it doesn't linger in the user's tree.\n\t*/\n\tasync #publishTemp(tmp, operation, upload, script, subject, errors) {\n\t\ttry {\n\t\t\tawait this.runFiles(operation, upload);\n\t\t\tawait this.#command(operation, script, subject, errors);\n\t\t} catch (error) {\n\t\t\tawait this.runFiles(operation, (files) => files.remove(tmp)).catch(() => {});\n\t\t\tthrow error;\n\t\t}\n\t}\n\t/** Run a file command and turn its known exit codes into typed errors. */\n\tasync #command(operation, script, subject, errors) {\n\t\tconst result = await this.runCommand(operation, script);\n\t\tif (result.exitCode === 0) return;\n\t\tfor (const [outcome, code] of Object.entries(FILE_COMMAND_EXIT)) {\n\t\t\tconst toError = errors[outcome];\n\t\t\tif (code === result.exitCode && toError) throw toError();\n\t\t}\n\t\tthrow new Error(`${operation} ${subject} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);\n\t}\n};\n//#endregion\n//#region src/commands-api-process-manager.ts\n/**\n* Attaching sets no deadline of its own: without one the SDK cuts the stream\n* after 60s, and the process's own timeout was set when it was spawned.\n*/\nconst ATTACH_TIMEOUT_MS = 0;\n/**\n* Output callbacks for `run`/`connect` that hold output until the handle\n* exists. The SDK's handle reads events from construction, before `run()`\n* returns, so output that arrives with the start event reaches the\n* callbacks first.\n*/\nfunction outputUntilHandled() {\n\tlet handle;\n\tconst early = [];\n\treturn {\n\t\tonStdout: (data) => {\n\t\t\tif (handle) handle.emitStdout(data);\n\t\t\telse early.push({\n\t\t\t\tstream: \"stdout\",\n\t\t\t\tdata\n\t\t\t});\n\t\t},\n\t\tonStderr: (data) => {\n\t\t\tif (handle) handle.emitStderr(data);\n\t\t\telse early.push({\n\t\t\t\tstream: \"stderr\",\n\t\t\t\tdata\n\t\t\t});\n\t\t},\n\t\thandledBy(created) {\n\t\t\tfor (const { stream, data } of early) if (stream === \"stdout\") created.emitStdout(data);\n\t\t\telse created.emitStderr(data);\n\t\t\tearly.length = 0;\n\t\t\thandle = created;\n\t\t\treturn created;\n\t\t}\n\t};\n}\n/** envd runs every command as `/bin/bash -l -c <command>`; show the command itself. */\nfunction commandOf(proc) {\n\tconst [flag, c, command] = proc.args;\n\tif (proc.cmd === \"/bin/bash\" && flag === \"-l\" && c === \"-c\" && command !== void 0 && proc.args.length === 3) return command;\n\treturn [proc.cmd, ...proc.args].join(\" \");\n}\n/**\n* A process started or attached through a {@link CommandsApi}. `kill` and\n* `sendStdin` go back through the manager, so they use whatever commands\n* handle the provider currently supplies.\n*/\nvar CommandsApiProcessHandle = class extends ProcessHandle {\n\tinner;\n\tmanager;\n\tstartTime;\n\tstdinMode;\n\tpid;\n\t/** The exit code `wait()` saw, for a handle that doesn't update its own. */\n\twaitedExitCode;\n\tconstructor(inner, manager, startTime, stdinMode, options) {\n\t\tsuper(options);\n\t\tthis.inner = inner;\n\t\tthis.manager = manager;\n\t\tthis.startTime = startTime;\n\t\tthis.stdinMode = stdinMode;\n\t\tthis.pid = String(inner.pid);\n\t}\n\t/** Read from the commands handle, so it reflects server-side state without `wait()`. */\n\tget exitCode() {\n\t\treturn this.inner.exitCode ?? this.waitedExitCode;\n\t}\n\t/** Wait for the process to exit. Providers whose output stream can drop while the process runs override this. */\n\twaitForExit() {\n\t\treturn this.inner.wait();\n\t}\n\tasync wait() {\n\t\ttry {\n\t\t\tconst result = await this.waitForExit();\n\t\t\tthis.waitedExitCode = result.exitCode;\n\t\t\treturn {\n\t\t\t\tsuccess: result.exitCode === 0,\n\t\t\t\texitCode: result.exitCode,\n\t\t\t\tstdout: this.stdout,\n\t\t\t\tstderr: this.stderr,\n\t\t\t\texecutionTimeMs: Date.now() - this.startTime\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tconst errorObj = error;\n\t\t\tconst reportedExitCode = errorObj.result?.exitCode ?? errorObj.exitCode;\n\t\t\tif (reportedExitCode !== void 0) this.waitedExitCode = reportedExitCode;\n\t\t\tconst exitCode = reportedExitCode ?? this.exitCode ?? 1;\n\t\t\tconst attachedStdout = errorObj.result?.stdout || errorObj.stdout;\n\t\t\tconst attachedStderr = errorObj.result?.stderr || errorObj.stderr;\n\t\t\tif (!this.stdout && !this.stdoutTruncated && attachedStdout) this.emitStdout(attachedStdout);\n\t\t\tif (!this.stderr && !this.stderrTruncated && attachedStderr) this.emitStderr(attachedStderr);\n\t\t\tconst stdout = this.stdout;\n\t\t\tconst stderr = this.stderr;\n\t\t\tconst terminalError = errorObj.result?.error || errorObj.error || (error instanceof Error ? error.message : String(error));\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\texitCode,\n\t\t\t\tstdout,\n\t\t\t\tstderr: [stderr, terminalError && !stderr.includes(terminalError) ? `Error: ${terminalError}` : \"\"].filter(Boolean).join(\"\\n\"),\n\t\t\t\texecutionTimeMs: Date.now() - this.startTime\n\t\t\t};\n\t\t}\n\t}\n\tasync kill() {\n\t\tif (this.exitCode !== void 0) return false;\n\t\treturn this.manager.runCommands(\"kill\", (commands) => commands.kill(this.inner.pid));\n\t}\n\tasync sendStdin(data) {\n\t\tif (this.exitCode !== void 0) throw new Error(`Process ${this.pid} has already exited with code ${this.exitCode}`);\n\t\tif (this.stdinMode === \"ignore\") throw new Error(`Process ${this.pid} was not started with stdin support`);\n\t\tawait this.manager.runCommands(\"sendStdin\", (commands) => commands.sendStdin(this.inner.pid, data));\n\t}\n\tasync closeStdin() {\n\t\tthrow new UnsupportedStdinCloseError(\"The commands API does not expose a way to close stdin for a running command\");\n\t}\n};\n/**\n* A process manager over E2B's envd commands API. Providers supply the\n* commands handle through {@link runCommands}; everything else is shared.\n*/\nvar CommandsApiProcessManager = class extends SandboxProcessManager {\n\tdefaultTimeout;\n\tconstructor({ defaultTimeout, ...options } = {}) {\n\t\tsuper(options);\n\t\tthis.defaultTimeout = defaultTimeout;\n\t}\n\t/** Wrap a started or attached command. Providers override this to supply their own handle. */\n\tcreateHandle(inner, stdinMode, options) {\n\t\treturn new CommandsApiProcessHandle(inner, this, Date.now(), stdinMode, options);\n\t}\n\t/** Where a command runs when the caller gives no `cwd`. */\n\tdefaultCwd() {\n\t\treturn this.sandbox.workingDirectory;\n\t}\n\tasync spawn(command, options = {}) {\n\t\tconst envs = Object.fromEntries(Object.entries({ ...options.env }).filter((entry) => entry[1] !== void 0));\n\t\treturn this.runCommands(\"spawn\", async (commands) => {\n\t\t\tconst output = outputUntilHandled();\n\t\t\tconst inner = await commands.run(command, {\n\t\t\t\tbackground: true,\n\t\t\t\tstdin: options.stdinMode !== \"ignore\",\n\t\t\t\tcwd: options.cwd ?? this.defaultCwd(),\n\t\t\t\tenvs,\n\t\t\t\ttimeoutMs: options.timeout ?? this.defaultTimeout,\n\t\t\t\tonStdout: output.onStdout,\n\t\t\t\tonStderr: output.onStderr\n\t\t\t});\n\t\t\tconst handle = output.handledBy(this.createHandle(inner, options.stdinMode, options));\n\t\t\tthis._tracked.set(handle.pid, handle);\n\t\t\treturn handle;\n\t\t});\n\t}\n\t/**\n\t* Running processes as the VM reports them, plus exited processes this\n\t* manager still holds, so a finished command stays visible until it is released.\n\t*/\n\tasync list() {\n\t\tconst infos = (await this.runCommands(\"list\", (commands) => commands.list())).map((proc) => ({\n\t\t\tpid: String(proc.pid),\n\t\t\tcommand: commandOf(proc),\n\t\t\trunning: true\n\t\t}));\n\t\tconst listed = new Set(infos.map((info) => info.pid));\n\t\tfor (const [pid, handle] of this._tracked) {\n\t\t\tif (handle.exitCode === void 0 || listed.has(pid)) continue;\n\t\t\tinfos.push({\n\t\t\t\tpid,\n\t\t\t\tcommand: handle.command,\n\t\t\t\trunning: false,\n\t\t\t\texitCode: handle.exitCode\n\t\t\t});\n\t\t}\n\t\treturn infos;\n\t}\n\t/**\n\t* A tracked process, or one this manager didn't start, attached through\n\t* `connect`. Output from before attaching is not available.\n\t*/\n\tasync get(pid, _options) {\n\t\tconst tracked = this._tracked.get(pid);\n\t\tif (tracked) return tracked;\n\t\tconst numericPid = /^\\d+$/.test(pid) ? Number(pid) : void 0;\n\t\tif (numericPid === void 0) return void 0;\n\t\tconst output = outputUntilHandled();\n\t\ttry {\n\t\t\tconst inner = await this.runCommands(\"connect\", (commands) => commands.connect(numericPid, {\n\t\t\t\tonStdout: output.onStdout,\n\t\t\t\tonStderr: output.onStderr,\n\t\t\t\ttimeoutMs: 0\n\t\t\t}));\n\t\t\tconst handle = output.handledBy(this.createHandle(inner, void 0));\n\t\t\tthis._tracked.set(handle.pid, handle);\n\t\t\treturn handle;\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t}\n};\n//#endregion\nexport { ATTACH_TIMEOUT_MS, CommandsApiProcessHandle, CommandsApiProcessManager, FILE_COMMAND_EXIT, FilesApiFilesystem, SETUP_FAILED_MARKER_PATH, SETUP_MARKER_DIR, SETUP_MARKER_PATH, WORKSPACE_SETUP_MARKER_PATH, appendFromCommand, copyCommand, guardedSetupCommand, linkNoClobberCommand, moveNoClobberCommand, normalizeSetupCommands, repoCloneCommand, repoSetupMarkerPath, setupMarkerCommand, setupMarkerContent, shellQuote, siblingTempPath };\n\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;AAMA,MAAM,mBAAmB;;;;;;;;;;;;;;AAczB,MAAM,oBAAoB;;;;;;;AAO1B,MAAM,8BAA8B;;;;;;;AAOpC,MAAM,2BAA2B;;AAEjC,SAAS,oBAAoB,SAAS;CACrC,OAAO,GAAG,iBAAiB,SAAS;AACrC;;AAEA,SAAS,uBAAuB,cAAc;CAC7C,QAAQ,iBAAiB,KAAK,IAAI,CAAC,IAAI,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,YAAY,EAAA,CAAG,QAAQ,YAAY,QAAQ,KAAK,MAAM,EAAE;AAC9I;;AAEA,SAAS,mBAAmB,cAAc;CACzC,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,uBAAuB,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK;AAC3G;;;;;;;;AA4CA,MAAM,oBAAoB;CACzB,UAAU;CACV,aAAa;CACb,QAAQ;CACR,cAAc;CACd,iBAAiB;AAClB;AAKA,MAAM,EAAE,UAAU,aAAa,QAAQ,cAAc,oBAAoB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-auth-host.d.ts","sourceRoot":"","sources":["../src/platform-auth-host.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AACtD,eAAO,MAAM,+BAA+B,8BAA8B,CAAC;AAK3E,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"platform-auth-host.d.ts","sourceRoot":"","sources":["../src/platform-auth-host.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AACtD,eAAO,MAAM,+BAA+B,8BAA8B,CAAC;AAK3E,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOrE"}
|
|
@@ -9,7 +9,7 @@ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
|
9
9
|
]);
|
|
10
10
|
function isPlatformAuthSupportedHost(hostname) {
|
|
11
11
|
const normalizedHostname = hostname.toLowerCase();
|
|
12
|
-
if (LOOPBACK_HOSTS.has(normalizedHostname)) return true;
|
|
12
|
+
if (LOOPBACK_HOSTS.has(normalizedHostname) || normalizedHostname.endsWith(".localhost")) return true;
|
|
13
13
|
return PLATFORM_AUTH_DOMAINS.some((domain) => normalizedHostname === domain || normalizedHostname.endsWith(`.${domain}`));
|
|
14
14
|
}
|
|
15
15
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-auth-host.js","names":[],"sources":["../src/platform-auth-host.ts"],"sourcesContent":["export const PLATFORM_AUTH_PROVIDER = 'mastra-studio';\nexport const CUSTOM_DOMAIN_UNSUPPORTED_ERROR = 'custom_domain_unsupported';\n\nconst PLATFORM_AUTH_DOMAINS = ['mastra.cloud', 'mastra.ai'] as const;\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);\n\nexport function isPlatformAuthSupportedHost(hostname: string): boolean {\n const normalizedHostname = hostname.toLowerCase();\n if (LOOPBACK_HOSTS.has(normalizedHostname)) return true;\n return PLATFORM_AUTH_DOMAINS.some(\n domain => normalizedHostname === domain || normalizedHostname.endsWith(`.${domain}`),\n );\n}\n"],"mappings":";AAAA,MAAa,yBAAyB;AACtC,MAAa,kCAAkC;AAE/C,MAAM,wBAAwB,CAAC,gBAAgB,WAAW;AAC1D,MAAM,iCAAiB,IAAI,IAAI;CAAC;CAAa;CAAa;AAAO,CAAC;AAElE,SAAgB,4BAA4B,UAA2B;CACrE,MAAM,qBAAqB,SAAS,YAAY;
|
|
1
|
+
{"version":3,"file":"platform-auth-host.js","names":[],"sources":["../src/platform-auth-host.ts"],"sourcesContent":["export const PLATFORM_AUTH_PROVIDER = 'mastra-studio';\nexport const CUSTOM_DOMAIN_UNSUPPORTED_ERROR = 'custom_domain_unsupported';\n\nconst PLATFORM_AUTH_DOMAINS = ['mastra.cloud', 'mastra.ai'] as const;\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);\n\nexport function isPlatformAuthSupportedHost(hostname: string): boolean {\n const normalizedHostname = hostname.toLowerCase();\n // `*.localhost` is loopback too (RFC 6761), e.g. a dev proxy's per-worktree names.\n if (LOOPBACK_HOSTS.has(normalizedHostname) || normalizedHostname.endsWith('.localhost')) return true;\n return PLATFORM_AUTH_DOMAINS.some(\n domain => normalizedHostname === domain || normalizedHostname.endsWith(`.${domain}`),\n );\n}\n"],"mappings":";AAAA,MAAa,yBAAyB;AACtC,MAAa,kCAAkC;AAE/C,MAAM,wBAAwB,CAAC,gBAAgB,WAAW;AAC1D,MAAM,iCAAiB,IAAI,IAAI;CAAC;CAAa;CAAa;AAAO,CAAC;AAElE,SAAgB,4BAA4B,UAA2B;CACrE,MAAM,qBAAqB,SAAS,YAAY;CAEhD,IAAI,eAAe,IAAI,kBAAkB,KAAK,mBAAmB,SAAS,YAAY,GAAG,OAAO;CAChG,OAAO,sBAAsB,MAC3B,WAAU,uBAAuB,UAAU,mBAAmB,SAAS,IAAI,QAAQ,CACrF;AACF"}
|
package/dist/routes/config.d.ts
CHANGED
|
@@ -194,8 +194,10 @@ export interface ProviderOMDefaultsResponse {
|
|
|
194
194
|
/** Dependencies injected into {@link ConfigRoutes}. */
|
|
195
195
|
export interface ConfigRoutesDeps extends RouteDependencies {
|
|
196
196
|
controller: ModelCatalog;
|
|
197
|
+
/** `mcp`: the Factory MCP endpoint is mounted (auth plus an MCP authorization server). */
|
|
197
198
|
features?: {
|
|
198
199
|
knowledge: boolean;
|
|
200
|
+
mcp?: boolean;
|
|
199
201
|
};
|
|
200
202
|
authStorage?: AuthStorage;
|
|
201
203
|
/** Tenant credential domain handle; absent in local (no-DB) mode. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/routes/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/routes/config.ts"],"names":[],"mappings":"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"}
|
package/dist/routes/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { WEB_OAUTH_PROVIDERS, credentialLabel, getAuthProviderId, listTenantCredentialsForRequest, resolveCredentialContext, tenantOrgId } from "./provider-credentials.js";
|
|
2
2
|
import { effectiveModelScope } from "../storage/domains/projects/base.js";
|
|
3
|
-
import {
|
|
3
|
+
import { THINKING_LEVELS } from "./contracts.js";
|
|
4
|
+
import { resolveFactoryModelPolicy, routedPersonalDefaults } from "../session/credential-policy.js";
|
|
4
5
|
import "../session/memory-settings-hydration.js";
|
|
5
6
|
import { factoryMemorySettingsUserId } from "../storage/domains/memory-settings/base.js";
|
|
6
7
|
import { Route } from "./route.js";
|
|
@@ -306,8 +307,8 @@ function readStoredOMConfig(record, currentModelId, autoModelSource, availablePr
|
|
|
306
307
|
* Resolve the memory-settings context for a request, or a ready-to-return
|
|
307
308
|
* error response. When `factoryProjectId` is provided the row addressed is the
|
|
308
309
|
* factory project's shared settings (a sentinel user id in the caller's org)
|
|
309
|
-
* instead of the caller's personal row —
|
|
310
|
-
*
|
|
310
|
+
* instead of the caller's personal row — the row requests paid with the
|
|
311
|
+
* Factory's credentials read.
|
|
311
312
|
*/
|
|
312
313
|
async function resolveMemorySettingsContext({ c, auth, memorySettings, factoryProjectId, factoryProjects }) {
|
|
313
314
|
await auth.ensureUser(c);
|
|
@@ -407,7 +408,10 @@ var ConfigRoutes = class extends Route {
|
|
|
407
408
|
registerApiRoute("/web/config/features", {
|
|
408
409
|
method: "GET",
|
|
409
410
|
requiresAuth: false,
|
|
410
|
-
handler: async (c) => c.json({
|
|
411
|
+
handler: async (c) => c.json({
|
|
412
|
+
knowledge: options.features?.knowledge ?? false,
|
|
413
|
+
mcp: options.features?.mcp ?? false
|
|
414
|
+
})
|
|
411
415
|
}),
|
|
412
416
|
registerApiRoute("/web/config/providers", {
|
|
413
417
|
method: "GET",
|
|
@@ -688,17 +692,17 @@ var ConfigRoutes = class extends Route {
|
|
|
688
692
|
modelScope
|
|
689
693
|
});
|
|
690
694
|
let defaultModelId = policy.factoryDefaultModelId ?? null;
|
|
695
|
+
let personalThinkingLevel = null;
|
|
691
696
|
if (modelScope === "user") {
|
|
692
697
|
if (!options.modelDefaults) return c.json({ error: "model_defaults_unavailable" }, 503);
|
|
693
|
-
await options.modelDefaults
|
|
694
|
-
defaultModelId =
|
|
695
|
-
|
|
696
|
-
userId: policy.tenant.userId
|
|
697
|
-
}))?.modelId ?? null;
|
|
698
|
+
const ownerDefaults = await routedPersonalDefaults(policy.tenant, options.modelDefaults);
|
|
699
|
+
defaultModelId = ownerDefaults?.modelId ?? null;
|
|
700
|
+
personalThinkingLevel = ownerDefaults?.thinkingLevel ?? null;
|
|
698
701
|
}
|
|
699
702
|
sessionPolicy = {
|
|
700
703
|
modelScope,
|
|
701
|
-
defaultModelId
|
|
704
|
+
defaultModelId,
|
|
705
|
+
personalThinkingLevel
|
|
702
706
|
};
|
|
703
707
|
}
|
|
704
708
|
} else if (c.req.query("scope") !== void 0) {
|
|
@@ -755,7 +759,10 @@ var ConfigRoutes = class extends Route {
|
|
|
755
759
|
orgId: context.orgId,
|
|
756
760
|
userId: context.userId
|
|
757
761
|
});
|
|
758
|
-
return c.json({
|
|
762
|
+
return c.json({
|
|
763
|
+
modelId: record?.modelId ?? null,
|
|
764
|
+
thinkingLevel: record?.thinkingLevel ?? null
|
|
765
|
+
});
|
|
759
766
|
} catch (error) {
|
|
760
767
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
761
768
|
}
|
|
@@ -874,6 +881,40 @@ var ConfigRoutes = class extends Route {
|
|
|
874
881
|
}
|
|
875
882
|
}
|
|
876
883
|
}),
|
|
884
|
+
registerApiRoute("/web/config/default-model/thinking", {
|
|
885
|
+
method: "PUT",
|
|
886
|
+
requiresAuth: false,
|
|
887
|
+
handler: async (c) => {
|
|
888
|
+
const context = await resolveModelDefaultsContext({
|
|
889
|
+
c: loose(c),
|
|
890
|
+
auth,
|
|
891
|
+
modelDefaults: options.modelDefaults
|
|
892
|
+
});
|
|
893
|
+
if ("response" in context) return context.response;
|
|
894
|
+
let body;
|
|
895
|
+
try {
|
|
896
|
+
body = await c.req.json();
|
|
897
|
+
} catch {
|
|
898
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
899
|
+
}
|
|
900
|
+
const parsed = z.object({ thinkingLevel: z.enum(THINKING_LEVELS).nullable() }).safeParse(body);
|
|
901
|
+
if (!parsed.success) return c.json({ error: "Invalid thinkingLevel" }, 400);
|
|
902
|
+
const { thinkingLevel } = parsed.data;
|
|
903
|
+
try {
|
|
904
|
+
if (!await context.storage.setThinkingLevel({
|
|
905
|
+
orgId: context.orgId,
|
|
906
|
+
userId: context.userId,
|
|
907
|
+
thinkingLevel
|
|
908
|
+
})) return c.json({ error: "default_model_not_set" }, 400);
|
|
909
|
+
return c.json({
|
|
910
|
+
ok: true,
|
|
911
|
+
thinkingLevel
|
|
912
|
+
});
|
|
913
|
+
} catch (error) {
|
|
914
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}),
|
|
877
918
|
registerApiRoute("/web/config/default-model", {
|
|
878
919
|
method: "DELETE",
|
|
879
920
|
requiresAuth: false,
|