@mastra/factory 0.12.0-alpha.1 → 0.12.0-alpha.11
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/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +31 -14
- package/dist/factory.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrations/base.d.ts +9 -0
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/commits.d.ts +29 -0
- package/dist/integrations/github/commits.d.ts.map +1 -0
- package/dist/integrations/github/commits.js +29 -0
- package/dist/integrations/github/commits.js.map +1 -0
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +47 -10
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/github/issue-reconciler.js +22 -14
- package/dist/integrations/github/issue-reconciler.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +2 -2
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +35 -0
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts +6 -0
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +58 -12
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/settle-or-abort.d.ts +7 -0
- package/dist/integrations/github/settle-or-abort.d.ts.map +1 -0
- package/dist/integrations/github/settle-or-abort.js +25 -0
- package/dist/integrations/github/settle-or-abort.js.map +1 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +3 -3
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +37 -5
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/slack/feed-publisher.d.ts +26 -0
- package/dist/integrations/slack/feed-publisher.d.ts.map +1 -0
- package/dist/integrations/slack/feed-publisher.js +44 -0
- package/dist/integrations/slack/feed-publisher.js.map +1 -0
- package/dist/integrations/slack/integration.d.ts +2 -0
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +5 -0
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +13 -1
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +78 -6
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js +25 -0
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -0
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +4 -0
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/skills.js +9 -3
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/surface.d.ts +5 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +34 -6
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.d.ts +4 -4
- package/dist/routes/tenant-credentials.d.ts.map +1 -1
- package/dist/routes/tenant-credentials.js +22 -7
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +12 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/defaults.d.ts.map +1 -1
- package/dist/rules/defaults.js +20 -8
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatch-errors.d.ts.map +1 -1
- package/dist/rules/dispatch-errors.js +8 -0
- package/dist/rules/dispatch-errors.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +5 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +125 -62
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +2 -3
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +2 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +6 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/transition-service.d.ts +2 -2
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +33 -12
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +27 -1
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +37 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.d.ts.map +1 -1
- package/dist/rules/validation.js +3 -1
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/session-sandbox.d.ts +22 -8
- package/dist/sandbox/session-sandbox.d.ts.map +1 -1
- package/dist/sandbox/session-sandbox.js +35 -39
- package/dist/sandbox/session-sandbox.js.map +1 -1
- package/dist/sandbox/workdir.d.ts +7 -2
- package/dist/sandbox/workdir.d.ts.map +1 -1
- package/dist/sandbox/workdir.js +11 -4
- package/dist/sandbox/workdir.js.map +1 -1
- package/dist/skills/catalog.d.ts +4 -1
- package/dist/skills/catalog.d.ts.map +1 -1
- package/dist/skills/catalog.js +11 -4
- package/dist/skills/catalog.js.map +1 -1
- package/dist/skills/service.d.ts +2 -1
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +5 -2
- package/dist/skills/service.js.map +1 -1
- package/dist/storage/domains/comments/base.d.ts.map +1 -1
- package/dist/storage/domains/comments/base.js +2 -4
- package/dist/storage/domains/comments/base.js.map +1 -1
- package/dist/storage/domains/comments/domain.d.ts +9 -2
- package/dist/storage/domains/comments/domain.d.ts.map +1 -1
- package/dist/storage/domains/comments/domain.js +18 -15
- package/dist/storage/domains/comments/domain.js.map +1 -1
- package/dist/storage/domains/comments/feed-context.d.ts +2 -0
- package/dist/storage/domains/comments/feed-context.d.ts.map +1 -1
- package/dist/storage/domains/comments/feed-context.js +6 -4
- package/dist/storage/domains/comments/feed-context.js.map +1 -1
- package/dist/storage/domains/comments/feed-sync.d.ts +9 -26
- package/dist/storage/domains/comments/feed-sync.d.ts.map +1 -1
- package/dist/storage/domains/projects/base.d.ts +3 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -1
- package/dist/storage/domains/projects/base.js +7 -0
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +5 -1
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +32 -3
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +100 -51
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/workspace.d.ts +20 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +54 -19
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-rereview/SKILL.md +13 -12
- package/factory-skills/factory-review/SKILL.md +13 -11
- package/factory-skills/factory-triage/SKILL.md +27 -6
- package/package.json +11 -11
- package/CHANGELOG.md +0 -3359
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.js","names":[],"sources":["../../src/rules/validation.ts"],"sourcesContent":["import {\n FACTORY_GITHUB_EVENTS,\n FACTORY_LINEAR_EVENTS,\n FACTORY_RULE_BOARDS,\n FACTORY_RULE_SOURCES,\n FACTORY_RULE_STAGES,\n} from './types.js';\nimport type {\n FactoryBoardRules,\n FactoryCommitDecision,\n FactoryRuleDecision,\n FactoryRuleJsonValue,\n FactoryRules,\n FactoryRuleRejectionCode,\n WorkItemSource,\n} from './types.js';\n\nexport const MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;\n\nconst MAX_VERSION_LENGTH = 128;\nconst MAX_IDEMPOTENCY_KEY_LENGTH = 256;\nconst MAX_REASON_LENGTH = 512;\nconst MAX_TITLE_LENGTH = 512;\nconst MAX_MESSAGE_LENGTH = 8_192;\nconst MAX_ARGUMENTS_LENGTH = 4_096;\nconst MAX_ROLE_LENGTH = 32;\nconst MAX_SKILL_NAME_LENGTH = 128;\nconst MAX_SOURCE_KEY_LENGTH = 256;\nconst MAX_URL_LENGTH = 2_048;\nconst MAX_METADATA_JSON_LENGTH = 16_384;\nconst MAX_JSON_DEPTH = 8;\nconst MAX_JSON_COLLECTION_SIZE = 100;\n\nconst IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;\nconst SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nconst SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;\nconst WORK_ITEM_SOURCES: readonly WorkItemSource[] = ['github-issue', 'github-pr', 'linear-issue', 'manual'];\nconst REJECTION_CODES: readonly FactoryRuleRejectionCode[] = [\n 'forbidden',\n 'invalid_transition',\n 'missing_binding',\n 'stale',\n 'timeout',\n 'rule_error',\n 'causal_depth_exceeded',\n 'repeated_transition',\n];\n\nexport class FactoryRuleValidationError extends Error {\n readonly code = 'invalid_factory_rule';\n\n constructor(message: string) {\n super(message);\n this.name = 'FactoryRuleValidationError';\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction assertExactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {\n const allowed = new Set(keys);\n if (Object.keys(value).some(key => !allowed.has(key))) {\n throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);\n }\n}\n\nfunction boundedString(value: unknown, label: string, max: number, pattern?: RegExp): string {\n if (typeof value !== 'string') throw new FactoryRuleValidationError(`${label} must be a string.`);\n const normalized = value.trim();\n if (normalized.length === 0 || normalized.length > max || (pattern && !pattern.test(normalized))) {\n throw new FactoryRuleValidationError(`${label} is invalid.`);\n }\n return normalized;\n}\n\nfunction optionalBoundedString(value: unknown, label: string, max: number): string | undefined {\n if (value === undefined) return undefined;\n return boundedString(value, label, max);\n}\n\nfunction enumValue<T extends string>(value: unknown, allowed: readonly T[], label: string): T {\n if (typeof value !== 'string' || !allowed.includes(value as T)) {\n throw new FactoryRuleValidationError(`${label} is invalid.`);\n }\n return value as T;\n}\n\nexport function normalizeFactoryRuleJsonValue(\n value: unknown,\n depth = 0,\n seen = new Set<object>(),\n): FactoryRuleJsonValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new FactoryRuleValidationError('Rule metadata must contain finite numbers.');\n return value;\n }\n if (depth >= MAX_JSON_DEPTH || (typeof value !== 'object' && !Array.isArray(value))) {\n throw new FactoryRuleValidationError('Rule metadata is not bounded JSON.');\n }\n if (seen.has(value as object)) throw new FactoryRuleValidationError('Rule metadata must not contain cycles.');\n seen.add(value as object);\n try {\n if (Array.isArray(value)) {\n if (value.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Rule metadata contains too many entries.');\n }\n return value.map(entry => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));\n }\n if (!isPlainObject(value)) throw new FactoryRuleValidationError('Rule metadata must use plain objects.');\n const entries = Object.entries(value);\n if (entries.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Rule metadata contains too many fields.');\n }\n const sanitized: Record<string, FactoryRuleJsonValue> = {};\n for (const [key, entry] of entries) {\n const normalizedKey = boundedString(key, 'Rule metadata key', 128, IDENTIFIER_RE);\n sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey)\n ? '[REDACTED]'\n : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);\n }\n return sanitized;\n } finally {\n seen.delete(value as object);\n }\n}\n\nfunction sanitizeMetadata(value: unknown): Record<string, FactoryRuleJsonValue> | undefined {\n if (value === undefined) return undefined;\n const sanitized = normalizeFactoryRuleJsonValue(value);\n if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError('Rule metadata must be an object.');\n if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {\n throw new FactoryRuleValidationError('Rule metadata is too large.');\n }\n return sanitized;\n}\n\nfunction validateBoardRules(rules: unknown, label: string): asserts rules is FactoryBoardRules {\n if (!isPlainObject(rules)) throw new FactoryRuleValidationError(`${label} must be an object.`);\n for (const [stage, sources] of Object.entries(rules)) {\n enumValue(stage, FACTORY_RULE_STAGES, `${label} stage`);\n if (!isPlainObject(sources)) throw new FactoryRuleValidationError(`${label}.${stage} must be an object.`);\n for (const [source, leaf] of Object.entries(sources)) {\n enumValue(source, FACTORY_RULE_SOURCES, `${label}.${stage} source`);\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`${label}.${stage}.${source} must be an object.`);\n assertExactKeys(leaf, ['onEnter', 'onExit'], `${label}.${stage}.${source}`);\n for (const handler of Object.values(leaf)) {\n if (handler !== undefined && typeof handler !== 'function') {\n throw new FactoryRuleValidationError(`${label}.${stage}.${source} handlers must be functions.`);\n }\n }\n }\n }\n}\n\nexport function assertFactoryRules(rules: unknown): asserts rules is FactoryRules {\n if (!isPlainObject(rules)) throw new FactoryRuleValidationError('Factory rules must be an object.');\n assertExactKeys(rules, ['version', 'work', 'review', 'tools', 'github', 'linear'], 'Factory rules');\n boundedString(rules.version, 'Factory rule version', MAX_VERSION_LENGTH);\n validateBoardRules(rules.work, 'Factory rules.work');\n validateBoardRules(rules.review, 'Factory rules.review');\n\n if (!isPlainObject(rules.tools)) throw new FactoryRuleValidationError('Factory rules.tools must be an object.');\n for (const [toolName, leaf] of Object.entries(rules.tools)) {\n boundedString(toolName, 'Factory tool name', 128, IDENTIFIER_RE);\n if (!isPlainObject(leaf))\n throw new FactoryRuleValidationError(`Factory rules.tools.${toolName} must be an object.`);\n assertExactKeys(leaf, ['onResult'], `Factory rules.tools.${toolName}`);\n if (leaf.onResult !== undefined && typeof leaf.onResult !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.tools.${toolName}.onResult must be a function.`);\n }\n }\n\n if (!isPlainObject(rules.github)) throw new FactoryRuleValidationError('Factory rules.github must be an object.');\n for (const [event, leaf] of Object.entries(rules.github)) {\n enumValue(event, FACTORY_GITHUB_EVENTS, 'Factory GitHub event');\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`Factory rules.github.${event} must be an object.`);\n assertExactKeys(leaf, ['onEvent'], `Factory rules.github.${event}`);\n if (leaf.onEvent !== undefined && typeof leaf.onEvent !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.github.${event}.onEvent must be a function.`);\n }\n }\n\n if (!isPlainObject(rules.linear)) throw new FactoryRuleValidationError('Factory rules.linear must be an object.');\n for (const [event, leaf] of Object.entries(rules.linear)) {\n enumValue(event, FACTORY_LINEAR_EVENTS, 'Factory Linear event');\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`Factory rules.linear.${event} must be an object.`);\n assertExactKeys(leaf, ['onEvent'], `Factory rules.linear.${event}`);\n if (leaf.onEvent !== undefined && typeof leaf.onEvent !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.linear.${event}.onEvent must be a function.`);\n }\n }\n}\n\nfunction commonCommitFields(value: Record<string, unknown>): { idempotencyKey: string } {\n return {\n idempotencyKey: boundedString(value.idempotencyKey, 'Factory decision idempotencyKey', MAX_IDEMPOTENCY_KEY_LENGTH),\n };\n}\n\nexport function validateFactoryRuleDecision(value: unknown, causalDepth = 0): FactoryRuleDecision {\n if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {\n throw new FactoryRuleValidationError('Factory rule causal depth exceeded.');\n }\n if (!isPlainObject(value)) throw new FactoryRuleValidationError('Factory rule decision must be an object.');\n const type = value.type;\n if (typeof type !== 'string') throw new FactoryRuleValidationError('Factory rule decision type is required.');\n\n switch (type) {\n case 'reject': {\n assertExactKeys(value, ['type', 'code', 'reason'], 'Factory reject decision');\n return {\n type,\n code: enumValue(value.code, REJECTION_CODES, 'Factory rejection code'),\n reason: boundedString(value.reason, 'Factory rejection reason', MAX_REASON_LENGTH),\n };\n }\n case 'transition': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'board', 'stage', 'message', 'reenter'],\n 'Factory transition decision',\n );\n if (value.reenter !== undefined && typeof value.reenter !== 'boolean') {\n throw new FactoryRuleValidationError('Factory transition reenter must be a boolean.');\n }\n let message: { text: string; role?: string } | undefined;\n if (value.message !== undefined) {\n if (!isPlainObject(value.message)) {\n throw new FactoryRuleValidationError('Factory transition message must be an object.');\n }\n assertExactKeys(value.message, ['text', 'role'], 'Factory transition message');\n const role =\n value.message.role === undefined\n ? undefined\n : boundedString(value.message.role, 'Factory transition message role', MAX_ROLE_LENGTH, IDENTIFIER_RE);\n message = {\n text: boundedString(value.message.text, 'Factory transition message text', MAX_MESSAGE_LENGTH),\n ...(role ? { role } : {}),\n };\n }\n return {\n type,\n ...commonCommitFields(value),\n board: enumValue(value.board, FACTORY_RULE_BOARDS, 'Factory transition board'),\n stage: enumValue(value.stage, FACTORY_RULE_STAGES, 'Factory transition stage'),\n ...(message ? { message } : {}),\n ...(value.reenter === true ? { reenter: true } : {}),\n };\n }\n case 'upsertLinkedWorkItem': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'board', 'source', 'sourceKey', 'title', 'url', 'stage', 'metadata'],\n 'Factory linked work item decision',\n );\n const url = value.url;\n if (url !== null && (typeof url !== 'string' || url.length > MAX_URL_LENGTH || !/^https?:\\/\\//.test(url))) {\n throw new FactoryRuleValidationError('Factory linked work item URL is invalid.');\n }\n const metadata = sanitizeMetadata(value.metadata);\n return {\n type,\n ...commonCommitFields(value),\n board: enumValue(value.board, FACTORY_RULE_BOARDS, 'Factory linked work item board'),\n source: enumValue(value.source, WORK_ITEM_SOURCES, 'Factory linked work item source'),\n sourceKey: boundedString(value.sourceKey, 'Factory linked work item sourceKey', MAX_SOURCE_KEY_LENGTH),\n title: boundedString(value.title, 'Factory linked work item title', MAX_TITLE_LENGTH),\n url,\n stage: enumValue(value.stage, FACTORY_RULE_STAGES, 'Factory linked work item stage'),\n ...(metadata ? { metadata } : {}),\n };\n }\n case 'invokeSkill': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'role', 'skillName', 'prompt', 'arguments', 'precedingMessage', 'cancelInFlight'],\n 'Factory invoke skill decision',\n );\n // A run activates a skill or carries a prompt, never both: they are two\n // ways to author the same kickoff message, so accepting both would leave\n // the dispatcher picking a winner.\n if ((value.skillName === undefined) === (value.prompt === undefined)) {\n throw new FactoryRuleValidationError('Factory skill invocation needs exactly one of skillName or prompt.');\n }\n const args = optionalBoundedString(value.arguments, 'Factory skill arguments', MAX_ARGUMENTS_LENGTH);\n const precedingMessage = optionalBoundedString(\n value.precedingMessage,\n 'Factory skill preceding message',\n MAX_MESSAGE_LENGTH,\n );\n if (value.cancelInFlight !== undefined && typeof value.cancelInFlight !== 'boolean') {\n throw new FactoryRuleValidationError('Factory skill cancelInFlight must be a boolean.');\n }\n return {\n type,\n ...commonCommitFields(value),\n role: boundedString(value.role, 'Factory skill role', MAX_ROLE_LENGTH, IDENTIFIER_RE),\n ...(value.skillName === undefined\n ? { prompt: boundedString(value.prompt, 'Factory skill prompt', MAX_MESSAGE_LENGTH) }\n : { skillName: boundedString(value.skillName, 'Factory skill name', MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE) }),\n ...(args ? { arguments: args } : {}),\n ...(precedingMessage ? { precedingMessage } : {}),\n ...(value.cancelInFlight === true ? { cancelInFlight: true } : {}),\n };\n }\n case 'sendMessage': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'role', 'message', 'priority', 'idleBehavior', 'prepareBinding'],\n 'Factory send message decision',\n );\n const priority =\n value.priority === undefined\n ? undefined\n : enumValue(value.priority, ['medium', 'high', 'urgent'] as const, 'Factory message priority');\n const idleBehavior =\n value.idleBehavior === undefined\n ? undefined\n : enumValue(value.idleBehavior, ['persist', 'wake'] as const, 'Factory message idle behavior');\n if (value.prepareBinding !== undefined && typeof value.prepareBinding !== 'boolean') {\n throw new FactoryRuleValidationError('Factory message prepareBinding must be a boolean.');\n }\n return {\n type,\n ...commonCommitFields(value),\n role: boundedString(value.role, 'Factory message role', MAX_ROLE_LENGTH, IDENTIFIER_RE),\n message: boundedString(value.message, 'Factory message', MAX_MESSAGE_LENGTH),\n ...(priority ? { priority } : {}),\n ...(idleBehavior ? { idleBehavior } : {}),\n ...(value.prepareBinding === true ? { prepareBinding: true } : {}),\n };\n }\n case 'notify': {\n assertExactKeys(value, ['type', 'idempotencyKey', 'title', 'body', 'level'], 'Factory notify decision');\n const body = optionalBoundedString(value.body, 'Factory notification body', MAX_MESSAGE_LENGTH);\n const level =\n value.level === undefined\n ? undefined\n : enumValue(value.level, ['info', 'warning', 'error'] as const, 'Factory notification level');\n return {\n type,\n ...commonCommitFields(value),\n title: boundedString(value.title, 'Factory notification title', MAX_TITLE_LENGTH),\n ...(body ? { body } : {}),\n ...(level ? { level } : {}),\n };\n }\n default:\n throw new FactoryRuleValidationError('Factory rule decision type is unsupported.');\n }\n}\n\nexport function validateFactoryRuleDecisions(values: readonly unknown[], causalDepth = 0): FactoryCommitDecision[] {\n if (values.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Factory rule produced too many decisions.');\n }\n const decisions: FactoryCommitDecision[] = [];\n for (const value of values) {\n const decision = validateFactoryRuleDecision(value, causalDepth);\n if (decision.type === 'reject') {\n throw new FactoryRuleValidationError('A rejection cannot be persisted with commit decisions.');\n }\n decisions.push(decision);\n }\n const keys = decisions.map(decision => decision.idempotencyKey);\n if (new Set(keys).size !== keys.length) {\n throw new FactoryRuleValidationError('Factory decisions require unique idempotency keys.');\n }\n return decisions;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validation.js","names":[],"sources":["../../src/rules/validation.ts"],"sourcesContent":["import {\n FACTORY_GITHUB_EVENTS,\n FACTORY_LINEAR_EVENTS,\n FACTORY_RULE_BOARDS,\n FACTORY_RULE_SOURCES,\n FACTORY_RULE_STAGES,\n} from './types.js';\nimport type {\n FactoryBoardRules,\n FactoryCommitDecision,\n FactoryRuleDecision,\n FactoryRuleJsonValue,\n FactoryRules,\n FactoryRuleRejectionCode,\n WorkItemSource,\n} from './types.js';\n\nexport const MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;\n\nconst MAX_VERSION_LENGTH = 128;\nconst MAX_IDEMPOTENCY_KEY_LENGTH = 256;\nconst MAX_REASON_LENGTH = 512;\nconst MAX_TITLE_LENGTH = 512;\nconst MAX_MESSAGE_LENGTH = 8_192;\nconst MAX_ARGUMENTS_LENGTH = 4_096;\nconst MAX_ROLE_LENGTH = 32;\nconst MAX_SKILL_NAME_LENGTH = 128;\nconst MAX_SOURCE_KEY_LENGTH = 256;\nconst MAX_URL_LENGTH = 2_048;\nconst MAX_METADATA_JSON_LENGTH = 16_384;\nconst MAX_JSON_DEPTH = 8;\nconst MAX_JSON_COLLECTION_SIZE = 100;\n\nconst IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;\nconst SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\nconst SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;\nconst WORK_ITEM_SOURCES: readonly WorkItemSource[] = ['github-issue', 'github-pr', 'linear-issue', 'manual'];\nconst REJECTION_CODES: readonly FactoryRuleRejectionCode[] = [\n 'forbidden',\n 'invalid_transition',\n 'missing_binding',\n 'stale',\n 'timeout',\n 'rule_error',\n 'causal_depth_exceeded',\n 'repeated_transition',\n];\n\nexport class FactoryRuleValidationError extends Error {\n readonly code = 'invalid_factory_rule';\n\n constructor(message: string) {\n super(message);\n this.name = 'FactoryRuleValidationError';\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction assertExactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {\n const allowed = new Set(keys);\n if (Object.keys(value).some(key => !allowed.has(key))) {\n throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);\n }\n}\n\nfunction boundedString(value: unknown, label: string, max: number, pattern?: RegExp): string {\n if (typeof value !== 'string') throw new FactoryRuleValidationError(`${label} must be a string.`);\n const normalized = value.trim();\n if (normalized.length === 0 || normalized.length > max || (pattern && !pattern.test(normalized))) {\n throw new FactoryRuleValidationError(`${label} is invalid.`);\n }\n return normalized;\n}\n\nfunction optionalBoundedString(value: unknown, label: string, max: number): string | undefined {\n if (value === undefined) return undefined;\n return boundedString(value, label, max);\n}\n\nfunction enumValue<T extends string>(value: unknown, allowed: readonly T[], label: string): T {\n if (typeof value !== 'string' || !allowed.includes(value as T)) {\n throw new FactoryRuleValidationError(`${label} is invalid.`);\n }\n return value as T;\n}\n\nexport function normalizeFactoryRuleJsonValue(\n value: unknown,\n depth = 0,\n seen = new Set<object>(),\n): FactoryRuleJsonValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new FactoryRuleValidationError('Rule metadata must contain finite numbers.');\n return value;\n }\n if (depth >= MAX_JSON_DEPTH || (typeof value !== 'object' && !Array.isArray(value))) {\n throw new FactoryRuleValidationError('Rule metadata is not bounded JSON.');\n }\n if (seen.has(value as object)) throw new FactoryRuleValidationError('Rule metadata must not contain cycles.');\n seen.add(value as object);\n try {\n if (Array.isArray(value)) {\n if (value.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Rule metadata contains too many entries.');\n }\n return value.map(entry => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));\n }\n if (!isPlainObject(value)) throw new FactoryRuleValidationError('Rule metadata must use plain objects.');\n const entries = Object.entries(value);\n if (entries.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Rule metadata contains too many fields.');\n }\n const sanitized: Record<string, FactoryRuleJsonValue> = {};\n for (const [key, entry] of entries) {\n const normalizedKey = boundedString(key, 'Rule metadata key', 128, IDENTIFIER_RE);\n sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey)\n ? '[REDACTED]'\n : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);\n }\n return sanitized;\n } finally {\n seen.delete(value as object);\n }\n}\n\nfunction sanitizeMetadata(value: unknown): Record<string, FactoryRuleJsonValue> | undefined {\n if (value === undefined) return undefined;\n const sanitized = normalizeFactoryRuleJsonValue(value);\n if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError('Rule metadata must be an object.');\n if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {\n throw new FactoryRuleValidationError('Rule metadata is too large.');\n }\n return sanitized;\n}\n\nfunction validateBoardRules(rules: unknown, label: string): asserts rules is FactoryBoardRules {\n if (!isPlainObject(rules)) throw new FactoryRuleValidationError(`${label} must be an object.`);\n for (const [stage, sources] of Object.entries(rules)) {\n enumValue(stage, FACTORY_RULE_STAGES, `${label} stage`);\n if (!isPlainObject(sources)) throw new FactoryRuleValidationError(`${label}.${stage} must be an object.`);\n for (const [source, leaf] of Object.entries(sources)) {\n enumValue(source, FACTORY_RULE_SOURCES, `${label}.${stage} source`);\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`${label}.${stage}.${source} must be an object.`);\n assertExactKeys(leaf, ['onEnter', 'onExit'], `${label}.${stage}.${source}`);\n for (const handler of Object.values(leaf)) {\n if (handler !== undefined && typeof handler !== 'function') {\n throw new FactoryRuleValidationError(`${label}.${stage}.${source} handlers must be functions.`);\n }\n }\n }\n }\n}\n\nexport function assertFactoryRules(rules: unknown): asserts rules is FactoryRules {\n if (!isPlainObject(rules)) throw new FactoryRuleValidationError('Factory rules must be an object.');\n assertExactKeys(rules, ['version', 'work', 'review', 'tools', 'github', 'linear'], 'Factory rules');\n boundedString(rules.version, 'Factory rule version', MAX_VERSION_LENGTH);\n validateBoardRules(rules.work, 'Factory rules.work');\n validateBoardRules(rules.review, 'Factory rules.review');\n\n if (!isPlainObject(rules.tools)) throw new FactoryRuleValidationError('Factory rules.tools must be an object.');\n for (const [toolName, leaf] of Object.entries(rules.tools)) {\n boundedString(toolName, 'Factory tool name', 128, IDENTIFIER_RE);\n if (!isPlainObject(leaf))\n throw new FactoryRuleValidationError(`Factory rules.tools.${toolName} must be an object.`);\n assertExactKeys(leaf, ['onResult'], `Factory rules.tools.${toolName}`);\n if (leaf.onResult !== undefined && typeof leaf.onResult !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.tools.${toolName}.onResult must be a function.`);\n }\n }\n\n if (!isPlainObject(rules.github)) throw new FactoryRuleValidationError('Factory rules.github must be an object.');\n for (const [event, leaf] of Object.entries(rules.github)) {\n enumValue(event, FACTORY_GITHUB_EVENTS, 'Factory GitHub event');\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`Factory rules.github.${event} must be an object.`);\n assertExactKeys(leaf, ['onEvent'], `Factory rules.github.${event}`);\n if (leaf.onEvent !== undefined && typeof leaf.onEvent !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.github.${event}.onEvent must be a function.`);\n }\n }\n\n if (!isPlainObject(rules.linear)) throw new FactoryRuleValidationError('Factory rules.linear must be an object.');\n for (const [event, leaf] of Object.entries(rules.linear)) {\n enumValue(event, FACTORY_LINEAR_EVENTS, 'Factory Linear event');\n if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`Factory rules.linear.${event} must be an object.`);\n assertExactKeys(leaf, ['onEvent'], `Factory rules.linear.${event}`);\n if (leaf.onEvent !== undefined && typeof leaf.onEvent !== 'function') {\n throw new FactoryRuleValidationError(`Factory rules.linear.${event}.onEvent must be a function.`);\n }\n }\n}\n\nfunction commonCommitFields(value: Record<string, unknown>): { idempotencyKey: string } {\n return {\n idempotencyKey: boundedString(value.idempotencyKey, 'Factory decision idempotencyKey', MAX_IDEMPOTENCY_KEY_LENGTH),\n };\n}\n\nexport function validateFactoryRuleDecision(value: unknown, causalDepth = 0): FactoryRuleDecision {\n if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {\n throw new FactoryRuleValidationError('Factory rule causal depth exceeded.');\n }\n if (!isPlainObject(value)) throw new FactoryRuleValidationError('Factory rule decision must be an object.');\n const type = value.type;\n if (typeof type !== 'string') throw new FactoryRuleValidationError('Factory rule decision type is required.');\n\n switch (type) {\n case 'reject': {\n assertExactKeys(value, ['type', 'code', 'reason'], 'Factory reject decision');\n return {\n type,\n code: enumValue(value.code, REJECTION_CODES, 'Factory rejection code'),\n reason: boundedString(value.reason, 'Factory rejection reason', MAX_REASON_LENGTH),\n };\n }\n case 'transition': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'board', 'stage', 'message', 'reenter'],\n 'Factory transition decision',\n );\n if (value.reenter !== undefined && typeof value.reenter !== 'boolean') {\n throw new FactoryRuleValidationError('Factory transition reenter must be a boolean.');\n }\n let message: { text: string; role?: string } | undefined;\n if (value.message !== undefined) {\n if (!isPlainObject(value.message)) {\n throw new FactoryRuleValidationError('Factory transition message must be an object.');\n }\n assertExactKeys(value.message, ['text', 'role'], 'Factory transition message');\n const role =\n value.message.role === undefined\n ? undefined\n : boundedString(value.message.role, 'Factory transition message role', MAX_ROLE_LENGTH, IDENTIFIER_RE);\n message = {\n text: boundedString(value.message.text, 'Factory transition message text', MAX_MESSAGE_LENGTH),\n ...(role ? { role } : {}),\n };\n }\n return {\n type,\n ...commonCommitFields(value),\n board: enumValue(value.board, FACTORY_RULE_BOARDS, 'Factory transition board'),\n stage: enumValue(value.stage, FACTORY_RULE_STAGES, 'Factory transition stage'),\n ...(message ? { message } : {}),\n ...(value.reenter === true ? { reenter: true } : {}),\n };\n }\n case 'upsertLinkedWorkItem': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'board', 'source', 'sourceKey', 'title', 'url', 'stage', 'metadata'],\n 'Factory linked work item decision',\n );\n const url = value.url;\n if (url !== null && (typeof url !== 'string' || url.length > MAX_URL_LENGTH || !/^https?:\\/\\//.test(url))) {\n throw new FactoryRuleValidationError('Factory linked work item URL is invalid.');\n }\n const metadata = sanitizeMetadata(value.metadata);\n return {\n type,\n ...commonCommitFields(value),\n board: enumValue(value.board, FACTORY_RULE_BOARDS, 'Factory linked work item board'),\n source: enumValue(value.source, WORK_ITEM_SOURCES, 'Factory linked work item source'),\n sourceKey: boundedString(value.sourceKey, 'Factory linked work item sourceKey', MAX_SOURCE_KEY_LENGTH),\n title: boundedString(value.title, 'Factory linked work item title', MAX_TITLE_LENGTH),\n url,\n stage: enumValue(value.stage, FACTORY_RULE_STAGES, 'Factory linked work item stage'),\n ...(metadata ? { metadata } : {}),\n };\n }\n case 'invokeSkill': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'role', 'skillName', 'prompt', 'arguments', 'precedingMessage', 'cancelInFlight'],\n 'Factory invoke skill decision',\n );\n // A run activates a skill or carries a prompt, never both: they are two\n // ways to author the same kickoff message, so accepting both would leave\n // the dispatcher picking a winner.\n if ((value.skillName === undefined) === (value.prompt === undefined)) {\n throw new FactoryRuleValidationError('Factory skill invocation needs exactly one of skillName or prompt.');\n }\n const args = optionalBoundedString(value.arguments, 'Factory skill arguments', MAX_ARGUMENTS_LENGTH);\n const precedingMessage = optionalBoundedString(\n value.precedingMessage,\n 'Factory skill preceding message',\n MAX_MESSAGE_LENGTH,\n );\n if (value.cancelInFlight !== undefined && typeof value.cancelInFlight !== 'boolean') {\n throw new FactoryRuleValidationError('Factory skill cancelInFlight must be a boolean.');\n }\n return {\n type,\n ...commonCommitFields(value),\n role: boundedString(value.role, 'Factory skill role', MAX_ROLE_LENGTH, IDENTIFIER_RE),\n ...(value.skillName === undefined\n ? { prompt: boundedString(value.prompt, 'Factory skill prompt', MAX_MESSAGE_LENGTH) }\n : { skillName: boundedString(value.skillName, 'Factory skill name', MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE) }),\n ...(args ? { arguments: args } : {}),\n ...(precedingMessage ? { precedingMessage } : {}),\n ...(value.cancelInFlight === true ? { cancelInFlight: true } : {}),\n };\n }\n case 'sendMessage': {\n assertExactKeys(\n value,\n ['type', 'idempotencyKey', 'role', 'message', 'priority', 'idleBehavior', 'prepareBinding'],\n 'Factory send message decision',\n );\n const priority =\n value.priority === undefined\n ? undefined\n : enumValue(value.priority, ['medium', 'high', 'urgent'] as const, 'Factory message priority');\n const idleBehavior =\n value.idleBehavior === undefined\n ? undefined\n : enumValue(value.idleBehavior, ['persist', 'wake'] as const, 'Factory message idle behavior');\n if (value.prepareBinding !== undefined && typeof value.prepareBinding !== 'boolean') {\n throw new FactoryRuleValidationError('Factory message prepareBinding must be a boolean.');\n }\n if (value.prepareBinding === true && value.role === undefined) {\n throw new FactoryRuleValidationError('Factory message prepareBinding requires a role.');\n }\n const role =\n value.role === undefined\n ? undefined\n : boundedString(value.role, 'Factory message role', MAX_ROLE_LENGTH, IDENTIFIER_RE);\n return {\n type,\n ...commonCommitFields(value),\n ...(role ? { role } : {}),\n message: boundedString(value.message, 'Factory message', MAX_MESSAGE_LENGTH),\n ...(priority ? { priority } : {}),\n ...(idleBehavior ? { idleBehavior } : {}),\n ...(value.prepareBinding === true ? { prepareBinding: true } : {}),\n };\n }\n case 'notify': {\n assertExactKeys(value, ['type', 'idempotencyKey', 'title', 'body', 'level'], 'Factory notify decision');\n const body = optionalBoundedString(value.body, 'Factory notification body', MAX_MESSAGE_LENGTH);\n const level =\n value.level === undefined\n ? undefined\n : enumValue(value.level, ['info', 'warning', 'error'] as const, 'Factory notification level');\n return {\n type,\n ...commonCommitFields(value),\n title: boundedString(value.title, 'Factory notification title', MAX_TITLE_LENGTH),\n ...(body ? { body } : {}),\n ...(level ? { level } : {}),\n };\n }\n default:\n throw new FactoryRuleValidationError('Factory rule decision type is unsupported.');\n }\n}\n\nexport function validateFactoryRuleDecisions(values: readonly unknown[], causalDepth = 0): FactoryCommitDecision[] {\n if (values.length > MAX_JSON_COLLECTION_SIZE) {\n throw new FactoryRuleValidationError('Factory rule produced too many decisions.');\n }\n const decisions: FactoryCommitDecision[] = [];\n for (const value of values) {\n const decision = validateFactoryRuleDecision(value, causalDepth);\n if (decision.type === 'reject') {\n throw new FactoryRuleValidationError('A rejection cannot be persisted with commit decisions.');\n }\n decisions.push(decision);\n }\n const keys = decisions.map(decision => decision.idempotencyKey);\n if (new Set(keys).size !== keys.length) {\n throw new FactoryRuleValidationError('Factory decisions require unique idempotency keys.');\n }\n return decisions;\n}\n"],"mappings":"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"}
|
|
@@ -52,8 +52,22 @@ export interface FactorySandboxContext {
|
|
|
52
52
|
* ```
|
|
53
53
|
*/
|
|
54
54
|
export type MastraFactorySandboxConfig = (ctx: FactorySandboxContext) => MastraSandbox;
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* What the start hook learned about the setup command before running the
|
|
57
|
+
* session setup, and how to record its completion afterwards.
|
|
58
|
+
*/
|
|
59
|
+
export interface SessionSetupGate {
|
|
60
|
+
/** True when the sandbox already carries a marker for the current setup command. */
|
|
61
|
+
setupDone: boolean;
|
|
62
|
+
/** Write the marker once the setup command succeeded. Best-effort. */
|
|
63
|
+
markSetupDone: () => Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The session's setup work, run against a started sandbox on EVERY start.
|
|
67
|
+
* Materialize and checkout are idempotent and must always run (a warm boot
|
|
68
|
+
* still needs its pull); only the setup command consults `gate`.
|
|
69
|
+
*/
|
|
70
|
+
export type SessionSetupRun = (sandbox: WorkspaceSandbox, workdir: string, gate: SessionSetupGate) => Promise<void>;
|
|
57
71
|
/**
|
|
58
72
|
* Per-process session-id → sandbox instance memo.
|
|
59
73
|
*
|
|
@@ -106,12 +120,12 @@ export declare function recordFailedSetupCommand(sessionId: string, command: str
|
|
|
106
120
|
export declare function hasFailedSetupCommand(sessionId: string, command: string): boolean;
|
|
107
121
|
/**
|
|
108
122
|
* Build the session setup hook, which factory attaches to the constructed
|
|
109
|
-
* sandbox with `setOnStart`. Runs inside the sandbox start
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* onStart errors as fatal.
|
|
123
|
+
* sandbox with `setOnStart`. Runs inside the sandbox start lifecycle on
|
|
124
|
+
* every start, fresh VM or reconnect: materialize and checkout always run,
|
|
125
|
+
* and the setup command runs unless the sandbox already carries the marker
|
|
126
|
+
* for it (a warm template image, or an earlier successful start). Throwing
|
|
127
|
+
* fails `start()` loudly; core treats onStart errors as fatal.
|
|
114
128
|
*/
|
|
115
|
-
export declare function createSessionSetupHook(run: SessionSetupRun, sessionId: string, repoFullName: string): SandboxStartHook;
|
|
129
|
+
export declare function createSessionSetupHook(run: SessionSetupRun, sessionId: string, repoFullName: string, setupCommand: string | undefined): SandboxStartHook;
|
|
116
130
|
export {};
|
|
117
131
|
//# sourceMappingURL=session-sandbox.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox/session-sandbox.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session-sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox/session-sandbox.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAChG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAG3E;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,mBAAmB,EAAE,CAAC,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC,GAAG,SAAS,CAAC;CACpE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,GAAG,EAAE,qBAAqB,KAAK,aAAa,CAAC;AAEvF;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oFAAoF;IACpF,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpH;;;;;;;;;GASG;AACH,UAAU,mBAAmB;IAC3B,OAAO,EAAE,gBAAgB,CAAC;IAC1B;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,gBAAgB,GAChC,mBAAmB,CAQrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,gBAAgB,EACzB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CASjB;AAmBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAErF;AAED,uFAAuF;AACvF,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAG3D;AAED,4DAA4D;AAC5D,wBAAgB,+BAA+B,IAAI,IAAI,CAGtD;AAcD,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEjF;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAEjF;AAqCD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,eAAe,EACpB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,gBAAgB,CAkBlB"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SETUP_MARKER_PATH, setupMarkerContent } from "../packages/_internals/workspace/dist/index.js";
|
|
2
|
+
import { deriveLocalWorkdir, deriveRemoteRepoDir, repoDirUnder } from "./workdir.js";
|
|
3
|
+
import path from "path";
|
|
2
4
|
//#region src/sandbox/session-sandbox.ts
|
|
3
5
|
const sessionSandboxes = /* @__PURE__ */ new Map();
|
|
4
6
|
/**
|
|
@@ -31,7 +33,7 @@ function getSessionSandbox(sessionId, repoFullName, construct) {
|
|
|
31
33
|
async function resolveSessionWorkdir(sessionId, sandbox, repoFullName) {
|
|
32
34
|
const entry = sessionSandboxes.get(sessionId);
|
|
33
35
|
if (entry?.workdir && entry.sandbox === sandbox) return entry.workdir;
|
|
34
|
-
const workdir = deriveLocalWorkdir(sandbox, repoFullName) ??
|
|
36
|
+
const workdir = deriveLocalWorkdir(sandbox, repoFullName) ?? deriveRemoteRepoDir(sandbox, repoFullName) ?? repoDirUnder(await probeHome(sandbox), repoFullName);
|
|
35
37
|
if (entry && entry.sandbox === sandbox) entry.workdir = workdir;
|
|
36
38
|
return workdir;
|
|
37
39
|
}
|
|
@@ -79,50 +81,44 @@ function hasFailedSetupCommand(sessionId, command) {
|
|
|
79
81
|
return failedSetupCommands.get(sessionId) === command;
|
|
80
82
|
}
|
|
81
83
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
84
|
+
* The setup completion marker is a convention shared with the repo templates
|
|
85
|
+
* (`@internal/workspace`): `.mastra-sandbox/setup` beside the checkout,
|
|
86
|
+
* containing a digest of the setup commands. Templates write it as their last
|
|
87
|
+
* build step, so a sandbox booted from a warm image already carries it; the
|
|
88
|
+
* start hook writes it after a successful runtime setup. It is a skip cache,
|
|
89
|
+
* not a correctness mechanism: the setup command is assumed idempotent, and a
|
|
90
|
+
* missing or mismatched marker only re-runs it.
|
|
91
|
+
*
|
|
92
|
+
* The working directory is the parent of the repo dir, which is also the
|
|
93
|
+
* template's build cwd, so this is the file the template's marker step wrote.
|
|
87
94
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return sandbox.provider === "local" ? `./${SESSION_SETUP_MARKER}` : `$HOME/${SESSION_SETUP_MARKER}`;
|
|
95
|
+
function markerShellPath(workdir) {
|
|
96
|
+
return `${path.posix.dirname(workdir)}/${SETUP_MARKER_PATH}`;
|
|
91
97
|
}
|
|
92
|
-
async function
|
|
93
|
-
|
|
98
|
+
async function markerMatches(sandbox, workdir, content) {
|
|
99
|
+
const marker = markerShellPath(workdir);
|
|
100
|
+
return (await sandbox.executeCommand(`test -d "${workdir}/.git" && test -f "${marker}" && [ "$(cat "${marker}")" = "${content}" ]`)).exitCode === 0;
|
|
94
101
|
}
|
|
95
|
-
async function writeMarker(sandbox) {
|
|
96
|
-
const marker = markerShellPath(
|
|
97
|
-
await sandbox.executeCommand(`mkdir -p "$(dirname "${marker}")" &&
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Run the session setup, marker-guarded: skip when the marker exists (unless
|
|
101
|
-
* the VM is known-fresh), otherwise run and write the marker only on
|
|
102
|
-
* success. Setup failures propagate — no marker is written, so the next
|
|
103
|
-
* attempt re-runs.
|
|
104
|
-
*/
|
|
105
|
-
async function runGuardedSetup(sandbox, run, { skipMarkerProbe, sessionId, repoFullName }) {
|
|
106
|
-
if (!sandbox.executeCommand) throw new Error(`Sandbox '${sandbox.id}' cannot run the session setup: no executeCommand implementation`);
|
|
107
|
-
const workdir = await resolveSessionWorkdir(sessionId, sandbox, repoFullName);
|
|
108
|
-
if (!skipMarkerProbe && await markerPresent(sandbox, workdir)) return;
|
|
109
|
-
await run(sandbox, workdir);
|
|
110
|
-
await writeMarker(sandbox);
|
|
102
|
+
async function writeMarker(sandbox, workdir, content) {
|
|
103
|
+
const marker = markerShellPath(workdir);
|
|
104
|
+
await sandbox.executeCommand(`mkdir -p "$(dirname "${marker}")" && printf '%s' '${content}' > "${marker}"`).catch(() => {});
|
|
111
105
|
}
|
|
112
106
|
/**
|
|
113
107
|
* Build the session setup hook, which factory attaches to the constructed
|
|
114
|
-
* sandbox with `setOnStart`. Runs inside the sandbox start
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* onStart errors as fatal.
|
|
108
|
+
* sandbox with `setOnStart`. Runs inside the sandbox start lifecycle on
|
|
109
|
+
* every start, fresh VM or reconnect: materialize and checkout always run,
|
|
110
|
+
* and the setup command runs unless the sandbox already carries the marker
|
|
111
|
+
* for it (a warm template image, or an earlier successful start). Throwing
|
|
112
|
+
* fails `start()` loudly; core treats onStart errors as fatal.
|
|
119
113
|
*/
|
|
120
|
-
function createSessionSetupHook(run, sessionId, repoFullName) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
function createSessionSetupHook(run, sessionId, repoFullName, setupCommand) {
|
|
115
|
+
const marker = setupCommand?.trim() ? setupMarkerContent(setupCommand) : void 0;
|
|
116
|
+
return async ({ sandbox }) => {
|
|
117
|
+
if (!sandbox.executeCommand) throw new Error(`Sandbox '${sandbox.id}' cannot run the session setup: no executeCommand implementation`);
|
|
118
|
+
const workdir = await resolveSessionWorkdir(sessionId, sandbox, repoFullName);
|
|
119
|
+
await run(sandbox, workdir, {
|
|
120
|
+
setupDone: marker ? await markerMatches(sandbox, workdir, marker) : true,
|
|
121
|
+
markSetupDone: () => marker ? writeMarker(sandbox, workdir, marker) : Promise.resolve()
|
|
126
122
|
});
|
|
127
123
|
};
|
|
128
124
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-sandbox.js","names":[],"sources":["../../src/sandbox/session-sandbox.ts"],"sourcesContent":["import type { MastraSandbox, SandboxStartHook, WorkspaceSandbox } from '@mastra/core/workspace';\nimport type { RepositoryAccess } from '../capabilities/version-control.js';\nimport { deriveLocalWorkdir, remoteWorkdirFromHome } from './workdir.js';\n\n/**\n * Everything factory knows about a session's sandbox needs — the whole\n * contract between factory and the deployer's sandbox callback. Factory owns\n * intent; the provider owns resolving `sessionId` to a runnable VM.\n */\nexport interface FactorySandboxContext {\n /** Stable session id — the sandbox identity. */\n sessionId: string;\n /** owner/name of the repository, when the session is repo-backed. */\n repoFullName?: string;\n /**\n * Configured repo setup command, when present. Part of a repo template's\n * identity: a different setup command produces a different template.\n */\n setupCommand?: string;\n /**\n * Resolves the session repository's clone URL and a fresh short-lived\n * credential for it. Providers use it for authenticated work that runs\n * outside the VM — resolving a private repo's head, or cloning it during\n * a template build. The credential is minted per call (installation\n * tokens expire in ~1h); never an org PAT.\n *\n * `undefined` when the session has no repository, which is how a provider\n * knows to build no repo template. The key is always present so that\n * passing the whole context to a provider helper keeps working when this\n * field changes, instead of silently resolving to \"no repository\".\n */\n getRepositoryAccess: (() => Promise<RepositoryAccess>) | undefined;\n}\n\n/**\n * The deploy's sandbox configuration: construct a session's sandbox from\n * intent. The sandbox identity is the session id; the provider must honor\n * id-keyed getOrCreate on `start()` (reconnect/resume an existing VM for the\n * id, create otherwise). Construction must be cheap and side-effect-free —\n * VMs are provisioned on `start()` only. Local sandboxes should root their\n * `workingDirectory` at a per-session directory (e.g.\n * `join(root, ctx.sessionId)`); the repo checks out as a subdirectory of it.\n *\n * Returns a `MastraSandbox`, not the bare `WorkspaceSandbox` interface:\n * factory relies on the base class for the start lifecycle and the runtime\n * env, so providers extend it rather than reimplementing the contract.\n *\n * Factory attaches its own session setup to the returned sandbox, so the\n * callback never has to wire it up. A callback may still pass its own\n * `onStart`; it runs after factory's setup, against a prepared workspace.\n *\n * @example\n * ```typescript\n * sandbox: ({ sessionId }) => new E2BSandbox({ id: sessionId })\n * ```\n */\nexport type MastraFactorySandboxConfig = (ctx: FactorySandboxContext) => MastraSandbox;\n\n/** The session's setup work, run against a started sandbox. Must be idempotent. */\nexport type SessionSetupRun = (sandbox: WorkspaceSandbox, workdir: string) => Promise<void>;\n\n/**\n * Per-process session-id → sandbox instance memo.\n *\n * The provider contract is id-keyed getOrCreate, but provider find-then-create\n * has a real double-create race across independent instances. Memoizing the\n * instance per session makes the base class's per-instance start coalescing\n * apply process-wide per session — the same single-flight scope the fleet's\n * per-binding coalescing provided. Cross-replica races are accepted (the\n * fleet was also per-replica).\n */\ninterface SessionSandboxEntry {\n sandbox: WorkspaceSandbox;\n /**\n * The session's repo checkout root, recorded for passive readers (fs\n * routes, capture, authz). Local sandboxes derive it at construction;\n * remote sandboxes clone into the VM's own home, so it is undefined until\n * `resolveSessionWorkdir` probes the first started VM — passive readers\n * treat an unresolved workdir as \"nothing materialized\".\n */\n workdir?: string;\n}\n\nconst sessionSandboxes = new Map<string, SessionSandboxEntry>();\n\n/**\n * Get the session's memoized sandbox entry, constructing (and memoizing) it on\n * first access. Construction is cheap and side-effect-free by contract; VMs\n * are provisioned on `start()` only. Local sandboxes get their workdir here;\n * remote workdirs are a runtime fact of the VM, resolved on first start.\n */\nexport function getSessionSandbox(\n sessionId: string,\n repoFullName: string,\n construct: () => WorkspaceSandbox,\n): SessionSandboxEntry {\n const existing = sessionSandboxes.get(sessionId);\n if (existing) return existing;\n const sandbox = construct();\n const local = deriveLocalWorkdir(sandbox, repoFullName);\n const entry: SessionSandboxEntry = { sandbox, ...(local ? { workdir: local } : {}) };\n sessionSandboxes.set(sessionId, entry);\n return entry;\n}\n\n/**\n * Resolve (and memoize on the session entry) the session's repo checkout\n * root. Local sandboxes answer synchronously from their configured\n * `workingDirectory`; remote sandboxes clone into the VM's own default cwd,\n * so the first resolution probes it with one `pwd` — the VM tells us where\n * home is, we never invent a path. Calling this against a stopped sandbox\n * lazily starts it (the probe is a command), so passive readers must peek\n * `entry.workdir` instead.\n */\nexport async function resolveSessionWorkdir(\n sessionId: string,\n sandbox: WorkspaceSandbox,\n repoFullName: string,\n): Promise<string> {\n const entry = sessionSandboxes.get(sessionId);\n if (entry?.workdir && entry.sandbox === sandbox) return entry.workdir;\n const workdir =\n deriveLocalWorkdir(sandbox, repoFullName) ?? remoteWorkdirFromHome(await probeHome(sandbox), repoFullName);\n if (entry && entry.sandbox === sandbox) entry.workdir = workdir;\n return workdir;\n}\n\n/** One `pwd` in the VM's default shell cwd — its home dir, by provider convention. */\nasync function probeHome(sandbox: WorkspaceSandbox): Promise<string> {\n if (!sandbox.executeCommand) {\n throw new Error(`Sandbox '${sandbox.id}' cannot resolve its workdir: no executeCommand implementation`);\n }\n const probe = await sandbox.executeCommand('pwd');\n const home = probe.stdout.trim().split('\\n').pop()?.trim() ?? '';\n if (probe.exitCode !== 0 || !home.startsWith('/')) {\n throw new Error(\n `Sandbox '${sandbox.id}' default cwd probe failed (exit ${probe.exitCode}): ${\n probe.stderr.trim() || probe.stdout.trim() || 'empty output'\n }`,\n );\n }\n return home;\n}\n\n/**\n * The session's memoized sandbox (and its workdir) when one was already\n * constructed in this process, else undefined. Never constructs — passive\n * read paths use this so browsing files cannot provision a VM.\n */\nexport function peekSessionSandbox(sessionId: string): SessionSandboxEntry | undefined {\n return sessionSandboxes.get(sessionId);\n}\n\n/** Drop the memoized instance (on stop/destroy/retirement or construction failure). */\nexport function evictSessionSandbox(sessionId: string): void {\n sessionSandboxes.delete(sessionId);\n failedSetupCommands.delete(sessionId);\n}\n\n/** Test-only: reset the process-wide memo between tests. */\nexport function __clearSessionSandboxesForTests(): void {\n sessionSandboxes.clear();\n failedSetupCommands.clear();\n}\n\n/**\n * Setup commands that already failed once for a session. The first failure\n * fails the start loudly — the agent sees the real error in the tool result\n * that triggered it. Recording it lets the next start skip the known-bad\n * command instead of wedging the session behind a permanently failing\n * onStart: clone and checkout still run, and the agent can fix or re-run\n * the setup itself. Keyed by the exact command so an edited setup command\n * runs fresh. In-memory only — a server restart re-runs the (idempotent)\n * setup.\n */\nconst failedSetupCommands = new Map<string, string>();\n\nexport function recordFailedSetupCommand(sessionId: string, command: string): void {\n failedSetupCommands.set(sessionId, command);\n}\n\nexport function hasFailedSetupCommand(sessionId: string, command: string): boolean {\n return failedSetupCommands.get(sessionId) === command;\n}\n\n/**\n * Completion marker for the session setup. Factory owns this end-to-end:\n * the `onStart` hook and the post-start fallback guard the exact same path,\n * so setup never double-runs regardless of which layer executed it. It is a\n * skip cache, not a correctness mechanism — the setup work is idempotent by\n * construction (materialize probes the disk, checkout/setup re-run safely).\n */\nconst SESSION_SETUP_MARKER = '.mastra-factory/bootstrap';\n\nfunction markerShellPath(sandbox: Pick<WorkspaceSandbox, 'provider'>): string {\n // Local sandboxes exec with cwd = the session working directory; remote\n // VMs are one-per-session so $HOME is private to the session.\n return sandbox.provider === 'local' ? `./${SESSION_SETUP_MARKER}` : `$HOME/${SESSION_SETUP_MARKER}`;\n}\n\nasync function markerPresent(sandbox: WorkspaceSandbox, workdir: string): Promise<boolean> {\n // The marker is a skip cache — it sits beside the checkout, not inside it,\n // so it can outlive a removed checkout (e.g. a wiped local session dir or\n // a recovered VM). Trust it only when the checkout it describes exists.\n const probe = await sandbox.executeCommand!(`test -f \"${markerShellPath(sandbox)}\" && test -d \"${workdir}/.git\"`);\n return probe.exitCode === 0;\n}\n\nasync function writeMarker(sandbox: WorkspaceSandbox): Promise<void> {\n // Best-effort: a missing marker only re-runs the idempotent setup later.\n const marker = markerShellPath(sandbox);\n await sandbox.executeCommand!(`mkdir -p \"$(dirname \"${marker}\")\" && touch \"${marker}\"`).catch(() => {});\n}\n\n/**\n * Run the session setup, marker-guarded: skip when the marker exists (unless\n * the VM is known-fresh), otherwise run and write the marker only on\n * success. Setup failures propagate — no marker is written, so the next\n * attempt re-runs.\n */\nasync function runGuardedSetup(\n sandbox: WorkspaceSandbox,\n run: SessionSetupRun,\n { skipMarkerProbe, sessionId, repoFullName }: { skipMarkerProbe: boolean; sessionId: string; repoFullName: string },\n): Promise<void> {\n if (!sandbox.executeCommand) {\n throw new Error(`Sandbox '${sandbox.id}' cannot run the session setup: no executeCommand implementation`);\n }\n // Resolved from the live instance (the hook runs inside `start()`, so the\n // VM is up) and memoized on the session entry for passive readers.\n const workdir = await resolveSessionWorkdir(sessionId, sandbox, repoFullName);\n if (!skipMarkerProbe && (await markerPresent(sandbox, workdir))) return;\n await run(sandbox, workdir);\n await writeMarker(sandbox);\n}\n\n/**\n * Build the session setup hook, which factory attaches to the constructed\n * sandbox with `setOnStart`. Runs inside the sandbox start\n * lifecycle: a fresh VM (`outcome: 'created'`) runs setup with no probe; a\n * reconnect probes the marker first, which re-runs setup after a failed or\n * crash-interrupted attempt. Throwing fails `start()` loudly — core treats\n * onStart errors as fatal.\n */\nexport function createSessionSetupHook(\n run: SessionSetupRun,\n sessionId: string,\n repoFullName: string,\n): SandboxStartHook {\n return async ({ sandbox, outcome }) => {\n await runGuardedSetup(sandbox, run, { skipMarkerProbe: outcome === 'created', sessionId, repoFullName });\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-sandbox.js","names":[],"sources":["../../src/sandbox/session-sandbox.ts"],"sourcesContent":["import path from 'node:path';\nimport { SETUP_MARKER_PATH, setupMarkerContent } from '@internal/workspace';\n\nimport type { MastraSandbox, SandboxStartHook, WorkspaceSandbox } from '@mastra/core/workspace';\nimport type { RepositoryAccess } from '../capabilities/version-control.js';\nimport { deriveLocalWorkdir, deriveRemoteRepoDir, repoDirUnder } from './workdir.js';\n\n/**\n * Everything factory knows about a session's sandbox needs — the whole\n * contract between factory and the deployer's sandbox callback. Factory owns\n * intent; the provider owns resolving `sessionId` to a runnable VM.\n */\nexport interface FactorySandboxContext {\n /** Stable session id — the sandbox identity. */\n sessionId: string;\n /** owner/name of the repository, when the session is repo-backed. */\n repoFullName?: string;\n /**\n * Configured repo setup command, when present. Part of a repo template's\n * identity: a different setup command produces a different template.\n */\n setupCommand?: string;\n /**\n * Resolves the session repository's clone URL and a fresh short-lived\n * credential for it. Providers use it for authenticated work that runs\n * outside the VM — resolving a private repo's head, or cloning it during\n * a template build. The credential is minted per call (installation\n * tokens expire in ~1h); never an org PAT.\n *\n * `undefined` when the session has no repository, which is how a provider\n * knows to build no repo template. The key is always present so that\n * passing the whole context to a provider helper keeps working when this\n * field changes, instead of silently resolving to \"no repository\".\n */\n getRepositoryAccess: (() => Promise<RepositoryAccess>) | undefined;\n}\n\n/**\n * The deploy's sandbox configuration: construct a session's sandbox from\n * intent. The sandbox identity is the session id; the provider must honor\n * id-keyed getOrCreate on `start()` (reconnect/resume an existing VM for the\n * id, create otherwise). Construction must be cheap and side-effect-free —\n * VMs are provisioned on `start()` only. Local sandboxes should root their\n * `workingDirectory` at a per-session directory (e.g.\n * `join(root, ctx.sessionId)`); the repo checks out as a subdirectory of it.\n *\n * Returns a `MastraSandbox`, not the bare `WorkspaceSandbox` interface:\n * factory relies on the base class for the start lifecycle and the runtime\n * env, so providers extend it rather than reimplementing the contract.\n *\n * Factory attaches its own session setup to the returned sandbox, so the\n * callback never has to wire it up. A callback may still pass its own\n * `onStart`; it runs after factory's setup, against a prepared workspace.\n *\n * @example\n * ```typescript\n * sandbox: ({ sessionId }) => new E2BSandbox({ id: sessionId })\n * ```\n */\nexport type MastraFactorySandboxConfig = (ctx: FactorySandboxContext) => MastraSandbox;\n\n/**\n * What the start hook learned about the setup command before running the\n * session setup, and how to record its completion afterwards.\n */\nexport interface SessionSetupGate {\n /** True when the sandbox already carries a marker for the current setup command. */\n setupDone: boolean;\n /** Write the marker once the setup command succeeded. Best-effort. */\n markSetupDone: () => Promise<void>;\n}\n\n/**\n * The session's setup work, run against a started sandbox on EVERY start.\n * Materialize and checkout are idempotent and must always run (a warm boot\n * still needs its pull); only the setup command consults `gate`.\n */\nexport type SessionSetupRun = (sandbox: WorkspaceSandbox, workdir: string, gate: SessionSetupGate) => Promise<void>;\n\n/**\n * Per-process session-id → sandbox instance memo.\n *\n * The provider contract is id-keyed getOrCreate, but provider find-then-create\n * has a real double-create race across independent instances. Memoizing the\n * instance per session makes the base class's per-instance start coalescing\n * apply process-wide per session — the same single-flight scope the fleet's\n * per-binding coalescing provided. Cross-replica races are accepted (the\n * fleet was also per-replica).\n */\ninterface SessionSandboxEntry {\n sandbox: WorkspaceSandbox;\n /**\n * The session's repo checkout root, recorded for passive readers (fs\n * routes, capture, authz). Local sandboxes derive it at construction;\n * remote sandboxes clone into the VM's own home, so it is undefined until\n * `resolveSessionWorkdir` probes the first started VM — passive readers\n * treat an unresolved workdir as \"nothing materialized\".\n */\n workdir?: string;\n}\n\nconst sessionSandboxes = new Map<string, SessionSandboxEntry>();\n\n/**\n * Get the session's memoized sandbox entry, constructing (and memoizing) it on\n * first access. Construction is cheap and side-effect-free by contract; VMs\n * are provisioned on `start()` only. Local sandboxes get their workdir here;\n * remote workdirs are a runtime fact of the VM, resolved on first start.\n */\nexport function getSessionSandbox(\n sessionId: string,\n repoFullName: string,\n construct: () => WorkspaceSandbox,\n): SessionSandboxEntry {\n const existing = sessionSandboxes.get(sessionId);\n if (existing) return existing;\n const sandbox = construct();\n const local = deriveLocalWorkdir(sandbox, repoFullName);\n const entry: SessionSandboxEntry = { sandbox, ...(local ? { workdir: local } : {}) };\n sessionSandboxes.set(sessionId, entry);\n return entry;\n}\n\n/**\n * Resolve (and memoize on the session entry) the session's repo checkout\n * root. Local sandboxes answer synchronously from their configured\n * `workingDirectory`; remote sandboxes clone into the VM's own default cwd,\n * so the first resolution probes it with one `pwd` — the VM tells us where\n * home is, we never invent a path. Calling this against a stopped sandbox\n * lazily starts it (the probe is a command), so passive readers must peek\n * `entry.workdir` instead.\n */\nexport async function resolveSessionWorkdir(\n sessionId: string,\n sandbox: WorkspaceSandbox,\n repoFullName: string,\n): Promise<string> {\n const entry = sessionSandboxes.get(sessionId);\n if (entry?.workdir && entry.sandbox === sandbox) return entry.workdir;\n const workdir =\n deriveLocalWorkdir(sandbox, repoFullName) ??\n deriveRemoteRepoDir(sandbox, repoFullName) ??\n repoDirUnder(await probeHome(sandbox), repoFullName);\n if (entry && entry.sandbox === sandbox) entry.workdir = workdir;\n return workdir;\n}\n\n/** One `pwd` in the VM's default shell cwd — its home dir, by provider convention. */\nasync function probeHome(sandbox: WorkspaceSandbox): Promise<string> {\n if (!sandbox.executeCommand) {\n throw new Error(`Sandbox '${sandbox.id}' cannot resolve its workdir: no executeCommand implementation`);\n }\n const probe = await sandbox.executeCommand('pwd');\n const home = probe.stdout.trim().split('\\n').pop()?.trim() ?? '';\n if (probe.exitCode !== 0 || !home.startsWith('/')) {\n throw new Error(\n `Sandbox '${sandbox.id}' default cwd probe failed (exit ${probe.exitCode}): ${\n probe.stderr.trim() || probe.stdout.trim() || 'empty output'\n }`,\n );\n }\n return home;\n}\n\n/**\n * The session's memoized sandbox (and its workdir) when one was already\n * constructed in this process, else undefined. Never constructs — passive\n * read paths use this so browsing files cannot provision a VM.\n */\nexport function peekSessionSandbox(sessionId: string): SessionSandboxEntry | undefined {\n return sessionSandboxes.get(sessionId);\n}\n\n/** Drop the memoized instance (on stop/destroy/retirement or construction failure). */\nexport function evictSessionSandbox(sessionId: string): void {\n sessionSandboxes.delete(sessionId);\n failedSetupCommands.delete(sessionId);\n}\n\n/** Test-only: reset the process-wide memo between tests. */\nexport function __clearSessionSandboxesForTests(): void {\n sessionSandboxes.clear();\n failedSetupCommands.clear();\n}\n\n/**\n * Setup commands that already failed once for a session. The first failure\n * fails the start loudly — the agent sees the real error in the tool result\n * that triggered it. Recording it lets the next start skip the known-bad\n * command instead of wedging the session behind a permanently failing\n * onStart: clone and checkout still run, and the agent can fix or re-run\n * the setup itself. Keyed by the exact command so an edited setup command\n * runs fresh. In-memory only — a server restart re-runs the (idempotent)\n * setup.\n */\nconst failedSetupCommands = new Map<string, string>();\n\nexport function recordFailedSetupCommand(sessionId: string, command: string): void {\n failedSetupCommands.set(sessionId, command);\n}\n\nexport function hasFailedSetupCommand(sessionId: string, command: string): boolean {\n return failedSetupCommands.get(sessionId) === command;\n}\n\n/**\n * The setup completion marker is a convention shared with the repo templates\n * (`@internal/workspace`): `.mastra-sandbox/setup` beside the checkout,\n * containing a digest of the setup commands. Templates write it as their last\n * build step, so a sandbox booted from a warm image already carries it; the\n * start hook writes it after a successful runtime setup. It is a skip cache,\n * not a correctness mechanism: the setup command is assumed idempotent, and a\n * missing or mismatched marker only re-runs it.\n *\n * The working directory is the parent of the repo dir, which is also the\n * template's build cwd, so this is the file the template's marker step wrote.\n */\nfunction markerShellPath(workdir: string): string {\n return `${path.posix.dirname(workdir)}/${SETUP_MARKER_PATH}`;\n}\n\nasync function markerMatches(sandbox: WorkspaceSandbox, workdir: string, content: string): Promise<boolean> {\n // The marker sits beside the checkout, not inside it, so it can outlive a\n // removed checkout (a wiped local session dir, a recovered VM). Trust it\n // only when the checkout it describes exists and it names this command.\n const marker = markerShellPath(workdir);\n const probe = await sandbox.executeCommand!(\n `test -d \"${workdir}/.git\" && test -f \"${marker}\" && [ \"$(cat \"${marker}\")\" = \"${content}\" ]`,\n );\n return probe.exitCode === 0;\n}\n\nasync function writeMarker(sandbox: WorkspaceSandbox, workdir: string, content: string): Promise<void> {\n // Best-effort: a missing marker only re-runs the idempotent setup later.\n const marker = markerShellPath(workdir);\n await sandbox.executeCommand!(`mkdir -p \"$(dirname \"${marker}\")\" && printf '%s' '${content}' > \"${marker}\"`).catch(\n () => {},\n );\n}\n\n/**\n * Build the session setup hook, which factory attaches to the constructed\n * sandbox with `setOnStart`. Runs inside the sandbox start lifecycle on\n * every start, fresh VM or reconnect: materialize and checkout always run,\n * and the setup command runs unless the sandbox already carries the marker\n * for it (a warm template image, or an earlier successful start). Throwing\n * fails `start()` loudly; core treats onStart errors as fatal.\n */\nexport function createSessionSetupHook(\n run: SessionSetupRun,\n sessionId: string,\n repoFullName: string,\n setupCommand: string | undefined,\n): SandboxStartHook {\n // No command, no marker: nothing to gate.\n const marker = setupCommand?.trim() ? setupMarkerContent(setupCommand) : undefined;\n return async ({ sandbox }) => {\n if (!sandbox.executeCommand) {\n throw new Error(`Sandbox '${sandbox.id}' cannot run the session setup: no executeCommand implementation`);\n }\n // Resolved from the live instance (the hook runs inside `start()`, so the\n // VM is up) and memoized on the session entry for passive readers.\n const workdir = await resolveSessionWorkdir(sessionId, sandbox, repoFullName);\n // Probed before materialize so a wiped checkout reads as \"not done\" even\n // though materialize is about to restore it.\n const setupDone = marker ? await markerMatches(sandbox, workdir, marker) : true;\n await run(sandbox, workdir, {\n setupDone,\n markSetupDone: () => (marker ? writeMarker(sandbox, workdir, marker) : Promise.resolve()),\n });\n };\n}\n"],"mappings":"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"}
|
|
@@ -23,8 +23,13 @@ export declare function deriveLocalWorkdir(sandbox: {
|
|
|
23
23
|
provider: string;
|
|
24
24
|
workingDirectory?: unknown;
|
|
25
25
|
}, repoFullName: string): string | undefined;
|
|
26
|
-
/** `<
|
|
27
|
-
export declare function
|
|
26
|
+
/** Derive `<workingDirectory>/<repo>` when a remote sandbox declares an absolute root. */
|
|
27
|
+
export declare function deriveRemoteRepoDir(sandbox: {
|
|
28
|
+
provider: string;
|
|
29
|
+
workingDirectory?: unknown;
|
|
30
|
+
}, repoFullName: string): string | undefined;
|
|
31
|
+
/** Join a parent directory and sanitized repository name. */
|
|
32
|
+
export declare function repoDirUnder(parent: string, repoFullName: string): string;
|
|
28
33
|
/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */
|
|
29
34
|
export declare function resolveContainedLocalWorkdir(root: string, ...segments: string[]): string;
|
|
30
35
|
//# sourceMappingURL=workdir.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workdir.d.ts","sourceRoot":"","sources":["../../src/sandbox/workdir.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAE,EACzD,YAAY,EAAE,MAAM,GACnB,MAAM,GAAG,SAAS,CAOpB;AAED,
|
|
1
|
+
{"version":3,"file":"workdir.d.ts","sourceRoot":"","sources":["../../src/sandbox/workdir.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAE,EACzD,YAAY,EAAE,MAAM,GACnB,MAAM,GAAG,SAAS,CAOpB;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CACjC,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAE,EACzD,YAAY,EAAE,MAAM,GACnB,MAAM,GAAG,SAAS,CAMpB;AAED,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAKzE;AAED,0FAA0F;AAC1F,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAKxF"}
|
package/dist/sandbox/workdir.js
CHANGED
|
@@ -30,10 +30,17 @@ function deriveLocalWorkdir(sandbox, repoFullName) {
|
|
|
30
30
|
return resolveContainedLocalWorkdir(wd, sanitizeSegment(name || "repo"));
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
/** `<
|
|
34
|
-
function
|
|
33
|
+
/** Derive `<workingDirectory>/<repo>` when a remote sandbox declares an absolute root. */
|
|
34
|
+
function deriveRemoteRepoDir(sandbox, repoFullName) {
|
|
35
|
+
const wd = sandbox.workingDirectory;
|
|
36
|
+
if (sandbox.provider !== "local" && typeof wd === "string" && wd.startsWith("/")) return repoDirUnder(wd, repoFullName);
|
|
37
|
+
}
|
|
38
|
+
/** Join a parent directory and sanitized repository name. */
|
|
39
|
+
function repoDirUnder(parent, repoFullName) {
|
|
35
40
|
const [, name] = repoFullName.split("/", 2);
|
|
36
|
-
|
|
41
|
+
let end = parent.length;
|
|
42
|
+
while (end > 0 && parent[end - 1] === "/") end--;
|
|
43
|
+
return `${parent.slice(0, end)}/${sanitizeSegment(name || "repo")}`;
|
|
37
44
|
}
|
|
38
45
|
/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */
|
|
39
46
|
function resolveContainedLocalWorkdir(root, ...segments) {
|
|
@@ -43,6 +50,6 @@ function resolveContainedLocalWorkdir(root, ...segments) {
|
|
|
43
50
|
throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);
|
|
44
51
|
}
|
|
45
52
|
//#endregion
|
|
46
|
-
export { deriveLocalWorkdir,
|
|
53
|
+
export { deriveLocalWorkdir, deriveRemoteRepoDir, repoDirUnder, resolveContainedLocalWorkdir, sanitizeSegment };
|
|
47
54
|
|
|
48
55
|
//# sourceMappingURL=workdir.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workdir.js","names":["path"],"sources":["../../src/sandbox/workdir.ts"],"sourcesContent":["import * as path from 'node:path';\n\n/**\n * Session workdir derivation. Workdirs are never persisted or trusted from\n * storage or client input — the stale-workdir class of production bugs came\n * from reading `session.sandboxWorkdir` written under a different provider.\n *\n * Local sandboxes derive their workdir synchronously from the sandbox's own\n * `workingDirectory` (the per-session directory the deploy's callback chose).\n * Remote sandboxes have no invented path: the repo clones into the VM's own\n * default cwd (its home dir), so the workdir is only knowable once a VM is\n * running — resolved lazily by `resolveSessionWorkdir` via a one-time `pwd`\n * probe and memoized on the session entry.\n */\n\n/** Keep each path piece a single safe segment (no separators or traversal). */\nexport function sanitizeSegment(segment: string): string {\n const cleaned = segment.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\\.+/, '');\n return cleaned || 'repo';\n}\n\n/**\n * Synchronously derivable workdir: local sandboxes expose their host\n * `workingDirectory`; the repo checks out as a contained subdirectory so the\n * setup marker sits beside the clone instead of polluting `git status`\n * inside it. Returns undefined for remote providers, whose workdir is a\n * runtime fact of the VM (`<home>/<repo>`).\n */\nexport function deriveLocalWorkdir(\n sandbox: { provider: string; workingDirectory?: unknown },\n repoFullName: string,\n): string | undefined {\n const wd = sandbox.workingDirectory;\n if (sandbox.provider === 'local' && typeof wd === 'string' && wd.length > 0) {\n const [, name] = repoFullName.split('/', 2);\n return resolveContainedLocalWorkdir(wd, sanitizeSegment(name || 'repo'));\n }\n return undefined;\n}\n\n/** `<
|
|
1
|
+
{"version":3,"file":"workdir.js","names":["path"],"sources":["../../src/sandbox/workdir.ts"],"sourcesContent":["import * as path from 'node:path';\n\n/**\n * Session workdir derivation. Workdirs are never persisted or trusted from\n * storage or client input — the stale-workdir class of production bugs came\n * from reading `session.sandboxWorkdir` written under a different provider.\n *\n * Local sandboxes derive their workdir synchronously from the sandbox's own\n * `workingDirectory` (the per-session directory the deploy's callback chose).\n * Remote sandboxes have no invented path: the repo clones into the VM's own\n * default cwd (its home dir), so the workdir is only knowable once a VM is\n * running — resolved lazily by `resolveSessionWorkdir` via a one-time `pwd`\n * probe and memoized on the session entry.\n */\n\n/** Keep each path piece a single safe segment (no separators or traversal). */\nexport function sanitizeSegment(segment: string): string {\n const cleaned = segment.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\\.+/, '');\n return cleaned || 'repo';\n}\n\n/**\n * Synchronously derivable workdir: local sandboxes expose their host\n * `workingDirectory`; the repo checks out as a contained subdirectory so the\n * setup marker sits beside the clone instead of polluting `git status`\n * inside it. Returns undefined for remote providers, whose workdir is a\n * runtime fact of the VM (`<home>/<repo>`).\n */\nexport function deriveLocalWorkdir(\n sandbox: { provider: string; workingDirectory?: unknown },\n repoFullName: string,\n): string | undefined {\n const wd = sandbox.workingDirectory;\n if (sandbox.provider === 'local' && typeof wd === 'string' && wd.length > 0) {\n const [, name] = repoFullName.split('/', 2);\n return resolveContainedLocalWorkdir(wd, sanitizeSegment(name || 'repo'));\n }\n return undefined;\n}\n\n/** Derive `<workingDirectory>/<repo>` when a remote sandbox declares an absolute root. */\nexport function deriveRemoteRepoDir(\n sandbox: { provider: string; workingDirectory?: unknown },\n repoFullName: string,\n): string | undefined {\n const wd = sandbox.workingDirectory;\n if (sandbox.provider !== 'local' && typeof wd === 'string' && wd.startsWith('/')) {\n return repoDirUnder(wd, repoFullName);\n }\n return undefined;\n}\n\n/** Join a parent directory and sanitized repository name. */\nexport function repoDirUnder(parent: string, repoFullName: string): string {\n const [, name] = repoFullName.split('/', 2);\n let end = parent.length;\n while (end > 0 && parent[end - 1] === '/') end--;\n return `${parent.slice(0, end)}/${sanitizeSegment(name || 'repo')}`;\n}\n\n/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */\nexport function resolveContainedLocalWorkdir(root: string, ...segments: string[]): string {\n const resolvedRoot = path.resolve(root);\n const resolved = path.resolve(resolvedRoot, ...segments);\n if (resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`)) return resolved;\n throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,SAAgB,gBAAgB,SAAyB;CAEvD,OADgB,QAAQ,QAAQ,oBAAoB,GAAG,CAAC,CAAC,QAAQ,QAAQ,EAC5D,KAAK;AACpB;;;;;;;;AASA,SAAgB,mBACd,SACA,cACoB;CACpB,MAAM,KAAK,QAAQ;CACnB,IAAI,QAAQ,aAAa,WAAW,OAAO,OAAO,YAAY,GAAG,SAAS,GAAG;EAC3E,MAAM,GAAG,QAAQ,aAAa,MAAM,KAAK,CAAC;EAC1C,OAAO,6BAA6B,IAAI,gBAAgB,QAAQ,MAAM,CAAC;CACzE;AAEF;;AAGA,SAAgB,oBACd,SACA,cACoB;CACpB,MAAM,KAAK,QAAQ;CACnB,IAAI,QAAQ,aAAa,WAAW,OAAO,OAAO,YAAY,GAAG,WAAW,GAAG,GAC7E,OAAO,aAAa,IAAI,YAAY;AAGxC;;AAGA,SAAgB,aAAa,QAAgB,cAA8B;CACzE,MAAM,GAAG,QAAQ,aAAa,MAAM,KAAK,CAAC;CAC1C,IAAI,MAAM,OAAO;CACjB,OAAO,MAAM,KAAK,OAAO,MAAM,OAAO,KAAK;CAC3C,OAAO,GAAG,OAAO,MAAM,GAAG,GAAG,EAAE,GAAG,gBAAgB,QAAQ,MAAM;AAClE;;AAGA,SAAgB,6BAA6B,MAAc,GAAG,UAA4B;CACxF,MAAM,eAAeA,OAAK,QAAQ,IAAI;CACtC,MAAM,WAAWA,OAAK,QAAQ,cAAc,GAAG,QAAQ;CACvD,IAAI,aAAa,gBAAgB,SAAS,WAAW,GAAG,eAAeA,OAAK,KAAK,GAAG,OAAO;CAC3F,MAAM,IAAI,MAAM,+DAA+D,UAAU;AAC3F"}
|
package/dist/skills/catalog.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export interface FactorySkillInfo {
|
|
|
4
4
|
/** SKILL.md body with the frontmatter block removed. */
|
|
5
5
|
content: string;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* List the Factory skills, preferring repo-local versions over bundled ones and
|
|
9
|
+
* skipping any skill missing from both roots.
|
|
10
|
+
*/
|
|
8
11
|
export declare function listFactorySkills(): Promise<FactorySkillInfo[]>;
|
|
9
12
|
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/skills/catalog.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;CACjB;AAcD
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/skills/catalog.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;CACjB;AAcD;;;GAGG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAmBrE"}
|
package/dist/skills/catalog.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BUNDLED_FACTORY_SKILLS_PATH, FACTORY_SKILL_NAMES, resolveLocalFactorySkillsPath } from "../workspace.js";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { readFile } from "fs/promises";
|
|
4
4
|
//#region src/skills/catalog.ts
|
|
@@ -25,16 +25,23 @@ function parseSkillMarkdown(name, raw) {
|
|
|
25
25
|
content: content.trim()
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* List the Factory skills, preferring repo-local versions over bundled ones and
|
|
30
|
+
* skipping any skill missing from both roots.
|
|
31
|
+
*/
|
|
29
32
|
async function listFactorySkills() {
|
|
30
33
|
const skills = [];
|
|
34
|
+
const localRoot = resolveLocalFactorySkillsPath();
|
|
35
|
+
const roots = localRoot ? [localRoot, BUNDLED_FACTORY_SKILLS_PATH] : [BUNDLED_FACTORY_SKILLS_PATH];
|
|
31
36
|
for (const name of [...FACTORY_SKILL_NAMES].sort()) {
|
|
32
37
|
let raw;
|
|
33
|
-
try {
|
|
34
|
-
raw = await readFile(join(
|
|
38
|
+
for (const root of roots) try {
|
|
39
|
+
raw = await readFile(join(root, name, "SKILL.md"), "utf8");
|
|
40
|
+
break;
|
|
35
41
|
} catch {
|
|
36
42
|
continue;
|
|
37
43
|
}
|
|
44
|
+
if (raw === void 0) continue;
|
|
38
45
|
skills.push(parseSkillMarkdown(name, raw));
|
|
39
46
|
}
|
|
40
47
|
return skills;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.js","names":[],"sources":["../../src/skills/catalog.ts"],"sourcesContent":["/**\n * Read-only catalog of the Factory skills bundled with the server.\n *\n * These are the built-in skills the Factory pipeline invokes at each stage\n * (triage, plan, review, …). The catalog reads the bundled `SKILL.md` files\n * so the settings UI can show users exactly what each skill instructs the\n * agent to do.\n */\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { FACTORY_SKILL_NAMES,
|
|
1
|
+
{"version":3,"file":"catalog.js","names":[],"sources":["../../src/skills/catalog.ts"],"sourcesContent":["/**\n * Read-only catalog of the Factory skills bundled with the server.\n *\n * These are the built-in skills the Factory pipeline invokes at each stage\n * (triage, plan, review, …). The catalog reads the bundled `SKILL.md` files\n * so the settings UI can show users exactly what each skill instructs the\n * agent to do.\n */\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { BUNDLED_FACTORY_SKILLS_PATH, FACTORY_SKILL_NAMES, resolveLocalFactorySkillsPath } from '../workspace.js';\n\nexport interface FactorySkillInfo {\n name: string;\n description: string;\n /** SKILL.md body with the frontmatter block removed. */\n content: string;\n}\n\nfunction parseSkillMarkdown(name: string, raw: string): FactorySkillInfo {\n const frontmatterMatch = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?/);\n let description = '';\n let content = raw;\n if (frontmatterMatch?.[1] !== undefined) {\n content = raw.slice(frontmatterMatch[0].length);\n const descriptionLine = frontmatterMatch[1].match(/^description:\\s*(.+)$/m);\n if (descriptionLine?.[1]) description = descriptionLine[1].trim();\n }\n return { name, description, content: content.trim() };\n}\n\n/**\n * List the Factory skills, preferring repo-local versions over bundled ones and\n * skipping any skill missing from both roots.\n */\nexport async function listFactorySkills(): Promise<FactorySkillInfo[]> {\n const skills: FactorySkillInfo[] = [];\n const localRoot = resolveLocalFactorySkillsPath();\n const roots = localRoot ? [localRoot, BUNDLED_FACTORY_SKILLS_PATH] : [BUNDLED_FACTORY_SKILLS_PATH];\n for (const name of [...FACTORY_SKILL_NAMES].sort()) {\n let raw: string | undefined;\n // Repo-local skills override the bundled versions, matching runtime resolution.\n for (const root of roots) {\n try {\n raw = await readFile(join(root, name, 'SKILL.md'), 'utf8');\n break;\n } catch {\n continue;\n }\n }\n if (raw === undefined) continue;\n skills.push(parseSkillMarkdown(name, raw));\n }\n return skills;\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAS,mBAAmB,MAAc,KAA+B;CACvE,MAAM,mBAAmB,IAAI,MAAM,mCAAmC;CACtE,IAAI,cAAc;CAClB,IAAI,UAAU;CACd,IAAI,mBAAmB,OAAO,KAAA,GAAW;EACvC,UAAU,IAAI,MAAM,iBAAiB,EAAE,CAAC,MAAM;EAC9C,MAAM,kBAAkB,iBAAiB,EAAE,CAAC,MAAM,wBAAwB;EAC1E,IAAI,kBAAkB,IAAI,cAAc,gBAAgB,EAAE,CAAC,KAAK;CAClE;CACA,OAAO;EAAE;EAAM;EAAa,SAAS,QAAQ,KAAK;CAAE;AACtD;;;;;AAMA,eAAsB,oBAAiD;CACrE,MAAM,SAA6B,CAAC;CACpC,MAAM,YAAY,8BAA8B;CAChD,MAAM,QAAQ,YAAY,CAAC,WAAW,2BAA2B,IAAI,CAAC,2BAA2B;CACjG,KAAK,MAAM,QAAQ,CAAC,GAAG,mBAAmB,CAAC,CAAC,KAAK,GAAG;EAClD,IAAI;EAEJ,KAAK,MAAM,QAAQ,OACjB,IAAI;GACF,MAAM,MAAM,SAAS,KAAK,MAAM,MAAM,UAAU,GAAG,MAAM;GACzD;EACF,QAAQ;GACN;EACF;EAEF,IAAI,QAAQ,KAAA,GAAW;EACvB,OAAO,KAAK,mBAAmB,MAAM,GAAG,CAAC;CAC3C;CACA,OAAO;AACT"}
|
package/dist/skills/service.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface SkillSession {
|
|
|
13
13
|
getWorkspace(): Workspace;
|
|
14
14
|
sendMessage(input: {
|
|
15
15
|
content: string;
|
|
16
|
+
requestContext?: RequestContext;
|
|
16
17
|
}): Promise<unknown>;
|
|
17
18
|
sendNotificationSignal(input: {
|
|
18
19
|
source: string;
|
|
@@ -50,7 +51,7 @@ export declare function resolveSkillInvocation(controller: Pick<AgentController<
|
|
|
50
51
|
skillName: string;
|
|
51
52
|
message: string;
|
|
52
53
|
}>;
|
|
53
|
-
export declare function dispatchSkillInvocation(controller: Pick<AgentController<MastraCodeState>, 'getSessionByResource'>, input: SkillInvocationInput): Promise<{
|
|
54
|
+
export declare function dispatchSkillInvocation(controller: Pick<AgentController<MastraCodeState>, 'getSessionByResource'>, input: SkillInvocationInput, requestContext?: RequestContext): Promise<{
|
|
54
55
|
skillName: string;
|
|
55
56
|
message: string;
|
|
56
57
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/skills/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,IAAI,SAAS,CAAC;IAC1B,WAAW,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/skills/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,IAAI,SAAS,CAAC;IAC1B,WAAW,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,cAAc,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3F,sBAAsB,CACpB,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;QAChD,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,EACD,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;SAAE,CAAC;QAChD,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;SAAE,CAAC;QAC3C,cAAc,CAAC,EAAE,cAAc,CAAC;KACjC,GACA,OAAO,CAAC,IAAI,CAAC,qBAAqB,EAAE,WAAW,GAAG,UAAU,CAAC,CAAC,CAAC;CACnE;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,IAAI,EAAE,mBAAmB,GAAG,iBAAiB,CAAC;gBAE3C,IAAI,EAAE,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM;CAKhE;AAMD,4EAA4E;AAC5E,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,eAAe,CAAC,EAAE,sBAAsB,CAAC,EAC1E,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,OAAO,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAIrD;AAED,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,eAAe,CAAC,EAAE,sBAAsB,CAAC,EAC1E,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC;IAAE,OAAO,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBxE;AAED,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,eAAe,CAAC,EAAE,sBAAsB,CAAC,EAC1E,KAAK,EAAE,oBAAoB,EAC3B,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAIjD"}
|
package/dist/skills/service.js
CHANGED
|
@@ -35,9 +35,12 @@ async function resolveSkillInvocation(controller, input) {
|
|
|
35
35
|
message: `<skill name="${skill.name}">\n${escapeSkillBoundary(content)}\n</skill>`
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
async function dispatchSkillInvocation(controller, input) {
|
|
38
|
+
async function dispatchSkillInvocation(controller, input, requestContext) {
|
|
39
39
|
const resolved = await resolveSkillInvocation(controller, input);
|
|
40
|
-
await resolved.session.sendMessage({
|
|
40
|
+
await resolved.session.sendMessage({
|
|
41
|
+
content: resolved.message,
|
|
42
|
+
...requestContext ? { requestContext } : {}
|
|
43
|
+
});
|
|
41
44
|
return {
|
|
42
45
|
skillName: resolved.skillName,
|
|
43
46
|
message: resolved.message
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/skills/service.ts"],"sourcesContent":["import type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { SendAgentSignalResult } from '@mastra/core/agent';\nimport type { AgentController } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { formatSkillActivation } from '@mastra/core/workspace';\nimport type { Workspace } from '@mastra/core/workspace';\n\nexport interface SkillInvocationInput {\n resourceId: string;\n scope?: string;\n name: string;\n arguments?: string;\n}\n\nexport interface SkillSession {\n getWorkspace(): Workspace;\n sendMessage(input: { content: string }): Promise<unknown>;\n sendNotificationSignal(\n input: {\n source: string;\n kind: string;\n summary: string;\n priority?: 'low' | 'medium' | 'high' | 'urgent';\n payload?: unknown;\n dedupeKey?: string;\n sourceId?: string;\n },\n options?: {\n ifActive?: { behavior?: 'deliver' | 'persist' };\n ifIdle?: { behavior?: 'persist' | 'wake' };\n requestContext?: RequestContext;\n },\n ): Promise<Pick<SendAgentSignalResult, 'persisted' | 'accepted'>>;\n}\n\nexport class SkillInvocationError extends Error {\n readonly code: 'session_not_found' | 'skill_not_found';\n\n constructor(code: SkillInvocationError['code'], message: string) {\n super(message);\n this.name = 'SkillInvocationError';\n this.code = code;\n }\n}\n\nfunction escapeSkillBoundary(value: string): string {\n return value.replaceAll('</skill>', '</skill>');\n}\n\n/** Kicks a run off from a plain prompt, for runs that activate no skill. */\nexport async function resolvePromptInvocation(\n controller: Pick<AgentController<MastraCodeState>, 'getSessionByResource'>,\n input: { resourceId: string; scope?: SkillInvocationInput['scope']; prompt: string },\n): Promise<{ session: SkillSession; message: string }> {\n const session = (await controller.getSessionByResource(input.resourceId, input.scope)) as SkillSession | undefined;\n if (!session) throw new SkillInvocationError('session_not_found', 'Agent controller session not found.');\n return { session, message: input.prompt };\n}\n\nexport async function resolveSkillInvocation(\n controller: Pick<AgentController<MastraCodeState>, 'getSessionByResource'>,\n input: SkillInvocationInput,\n): Promise<{ session: SkillSession; skillName: string; message: string }> {\n const session = (await controller.getSessionByResource(input.resourceId, input.scope)) as SkillSession | undefined;\n if (!session) throw new SkillInvocationError('session_not_found', 'Agent controller session not found.');\n\n const skills = session.getWorkspace().skills;\n await skills?.maybeRefresh();\n const skill = await skills?.get(input.name);\n if (!skill || skill['user-invocable'] === false) {\n throw new SkillInvocationError('skill_not_found', `Skill not found: ${input.name}.`);\n }\n\n const args = input.arguments?.trim();\n const content = `${formatSkillActivation(skill)}${args ? `\\n\\nARGUMENTS: ${args}` : ''}`.trim();\n return {\n session,\n skillName: skill.name,\n message: `<skill name=\"${skill.name}\">\\n${escapeSkillBoundary(content)}\\n</skill>`,\n };\n}\n\nexport async function dispatchSkillInvocation(\n controller: Pick<AgentController<MastraCodeState>, 'getSessionByResource'>,\n input: SkillInvocationInput,\n): Promise<{ skillName: string; message: string }> {\n const resolved = await resolveSkillInvocation(controller, input);\n await resolved.session.sendMessage({ content: resolved.message });\n return { skillName: resolved.skillName, message: resolved.message };\n}\n"],"mappings":";;AAmCA,IAAa,uBAAb,cAA0C,MAAM;CAC9C;CAEA,YAAY,MAAoC,SAAiB;EAC/D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAEA,SAAS,oBAAoB,OAAuB;CAClD,OAAO,MAAM,WAAW,YAAY,gBAAgB;AACtD;;AAGA,eAAsB,wBACpB,YACA,OACqD;CACrD,MAAM,UAAW,MAAM,WAAW,qBAAqB,MAAM,YAAY,MAAM,KAAK;CACpF,IAAI,CAAC,SAAS,MAAM,IAAI,qBAAqB,qBAAqB,qCAAqC;CACvG,OAAO;EAAE;EAAS,SAAS,MAAM;CAAO;AAC1C;AAEA,eAAsB,uBACpB,YACA,OACwE;CACxE,MAAM,UAAW,MAAM,WAAW,qBAAqB,MAAM,YAAY,MAAM,KAAK;CACpF,IAAI,CAAC,SAAS,MAAM,IAAI,qBAAqB,qBAAqB,qCAAqC;CAEvG,MAAM,SAAS,QAAQ,aAAa,CAAC,CAAC;CACtC,MAAM,QAAQ,aAAa;CAC3B,MAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC1C,IAAI,CAAC,SAAS,MAAM,sBAAsB,OACxC,MAAM,IAAI,qBAAqB,mBAAmB,oBAAoB,MAAM,KAAK,EAAE;CAGrF,MAAM,OAAO,MAAM,WAAW,KAAK;CACnC,MAAM,UAAU,GAAG,sBAAsB,KAAK,IAAI,OAAO,kBAAkB,SAAS,KAAK,KAAK;CAC9F,OAAO;EACL;EACA,WAAW,MAAM;EACjB,SAAS,gBAAgB,MAAM,KAAK,MAAM,oBAAoB,OAAO,EAAE;CACzE;AACF;AAEA,eAAsB,wBACpB,YACA,
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/skills/service.ts"],"sourcesContent":["import type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { SendAgentSignalResult } from '@mastra/core/agent';\nimport type { AgentController } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { formatSkillActivation } from '@mastra/core/workspace';\nimport type { Workspace } from '@mastra/core/workspace';\n\nexport interface SkillInvocationInput {\n resourceId: string;\n scope?: string;\n name: string;\n arguments?: string;\n}\n\nexport interface SkillSession {\n getWorkspace(): Workspace;\n sendMessage(input: { content: string; requestContext?: RequestContext }): Promise<unknown>;\n sendNotificationSignal(\n input: {\n source: string;\n kind: string;\n summary: string;\n priority?: 'low' | 'medium' | 'high' | 'urgent';\n payload?: unknown;\n dedupeKey?: string;\n sourceId?: string;\n },\n options?: {\n ifActive?: { behavior?: 'deliver' | 'persist' };\n ifIdle?: { behavior?: 'persist' | 'wake' };\n requestContext?: RequestContext;\n },\n ): Promise<Pick<SendAgentSignalResult, 'persisted' | 'accepted'>>;\n}\n\nexport class SkillInvocationError extends Error {\n readonly code: 'session_not_found' | 'skill_not_found';\n\n constructor(code: SkillInvocationError['code'], message: string) {\n super(message);\n this.name = 'SkillInvocationError';\n this.code = code;\n }\n}\n\nfunction escapeSkillBoundary(value: string): string {\n return value.replaceAll('</skill>', '</skill>');\n}\n\n/** Kicks a run off from a plain prompt, for runs that activate no skill. */\nexport async function resolvePromptInvocation(\n controller: Pick<AgentController<MastraCodeState>, 'getSessionByResource'>,\n input: { resourceId: string; scope?: SkillInvocationInput['scope']; prompt: string },\n): Promise<{ session: SkillSession; message: string }> {\n const session = (await controller.getSessionByResource(input.resourceId, input.scope)) as SkillSession | undefined;\n if (!session) throw new SkillInvocationError('session_not_found', 'Agent controller session not found.');\n return { session, message: input.prompt };\n}\n\nexport async function resolveSkillInvocation(\n controller: Pick<AgentController<MastraCodeState>, 'getSessionByResource'>,\n input: SkillInvocationInput,\n): Promise<{ session: SkillSession; skillName: string; message: string }> {\n const session = (await controller.getSessionByResource(input.resourceId, input.scope)) as SkillSession | undefined;\n if (!session) throw new SkillInvocationError('session_not_found', 'Agent controller session not found.');\n\n const skills = session.getWorkspace().skills;\n await skills?.maybeRefresh();\n const skill = await skills?.get(input.name);\n if (!skill || skill['user-invocable'] === false) {\n throw new SkillInvocationError('skill_not_found', `Skill not found: ${input.name}.`);\n }\n\n const args = input.arguments?.trim();\n const content = `${formatSkillActivation(skill)}${args ? `\\n\\nARGUMENTS: ${args}` : ''}`.trim();\n return {\n session,\n skillName: skill.name,\n message: `<skill name=\"${skill.name}\">\\n${escapeSkillBoundary(content)}\\n</skill>`,\n };\n}\n\nexport async function dispatchSkillInvocation(\n controller: Pick<AgentController<MastraCodeState>, 'getSessionByResource'>,\n input: SkillInvocationInput,\n requestContext?: RequestContext,\n): Promise<{ skillName: string; message: string }> {\n const resolved = await resolveSkillInvocation(controller, input);\n await resolved.session.sendMessage({ content: resolved.message, ...(requestContext ? { requestContext } : {}) });\n return { skillName: resolved.skillName, message: resolved.message };\n}\n"],"mappings":";;AAmCA,IAAa,uBAAb,cAA0C,MAAM;CAC9C;CAEA,YAAY,MAAoC,SAAiB;EAC/D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAEA,SAAS,oBAAoB,OAAuB;CAClD,OAAO,MAAM,WAAW,YAAY,gBAAgB;AACtD;;AAGA,eAAsB,wBACpB,YACA,OACqD;CACrD,MAAM,UAAW,MAAM,WAAW,qBAAqB,MAAM,YAAY,MAAM,KAAK;CACpF,IAAI,CAAC,SAAS,MAAM,IAAI,qBAAqB,qBAAqB,qCAAqC;CACvG,OAAO;EAAE;EAAS,SAAS,MAAM;CAAO;AAC1C;AAEA,eAAsB,uBACpB,YACA,OACwE;CACxE,MAAM,UAAW,MAAM,WAAW,qBAAqB,MAAM,YAAY,MAAM,KAAK;CACpF,IAAI,CAAC,SAAS,MAAM,IAAI,qBAAqB,qBAAqB,qCAAqC;CAEvG,MAAM,SAAS,QAAQ,aAAa,CAAC,CAAC;CACtC,MAAM,QAAQ,aAAa;CAC3B,MAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC1C,IAAI,CAAC,SAAS,MAAM,sBAAsB,OACxC,MAAM,IAAI,qBAAqB,mBAAmB,oBAAoB,MAAM,KAAK,EAAE;CAGrF,MAAM,OAAO,MAAM,WAAW,KAAK;CACnC,MAAM,UAAU,GAAG,sBAAsB,KAAK,IAAI,OAAO,kBAAkB,SAAS,KAAK,KAAK;CAC9F,OAAO;EACL;EACA,WAAW,MAAM;EACjB,SAAS,gBAAgB,MAAM,KAAK,MAAM,oBAAoB,OAAO,EAAE;CACzE;AACF;AAEA,eAAsB,wBACpB,YACA,OACA,gBACiD;CACjD,MAAM,WAAW,MAAM,uBAAuB,YAAY,KAAK;CAC/D,MAAM,SAAS,QAAQ,YAAY;EAAE,SAAS,SAAS;EAAS,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;CAAG,CAAC;CAC/G,OAAO;EAAE,WAAW,SAAS;EAAW,SAAS,SAAS;CAAQ;AACpE"}
|