@mastra/factory 0.24.0-alpha.0 → 0.24.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -4
- package/dist/auth.d.ts +14 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +45 -9
- package/dist/auth.js.map +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +21 -1
- package/dist/boards/review.js.map +1 -1
- package/dist/boards/work-transition-policy.d.ts.map +1 -1
- package/dist/boards/work-transition-policy.js +12 -1
- package/dist/boards/work-transition-policy.js.map +1 -1
- package/dist/boards/work.d.ts +14 -0
- package/dist/boards/work.d.ts.map +1 -1
- package/dist/boards/work.js +70 -7
- package/dist/boards/work.js.map +1 -1
- package/dist/capabilities/version-control.d.ts +26 -0
- package/dist/capabilities/version-control.d.ts.map +1 -1
- package/dist/factory.d.ts +8 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +354 -27
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +1 -1
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/default-rules.d.ts.map +1 -1
- package/dist/integrations/github/default-rules.js +1 -16
- package/dist/integrations/github/default-rules.js.map +1 -1
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +37 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/routes.d.ts +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +19 -11
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +26 -22
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox-release.d.ts +15 -3
- package/dist/integrations/github/sandbox-release.d.ts.map +1 -1
- package/dist/integrations/github/sandbox-release.js +20 -3
- package/dist/integrations/github/sandbox-release.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +2 -3
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +78 -7
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +1 -2
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.d.ts +7 -4
- package/dist/integrations/github/subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/subscriptions.js +5 -5
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/github/webhook.d.ts +4 -2
- package/dist/integrations/github/webhook.d.ts.map +1 -1
- package/dist/integrations/github/webhook.js +59 -36
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/gitlab/api.d.ts +16 -1
- package/dist/integrations/gitlab/api.d.ts.map +1 -1
- package/dist/integrations/gitlab/api.js +9 -0
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/routes.d.ts.map +1 -1
- package/dist/integrations/gitlab/routes.js +10 -6
- package/dist/integrations/gitlab/routes.js.map +1 -1
- package/dist/integrations/gitlab/rules.d.ts.map +1 -1
- package/dist/integrations/gitlab/rules.js +28 -21
- package/dist/integrations/gitlab/rules.js.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.js +1 -2
- package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/subscriptions.d.ts +7 -4
- package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/subscriptions.js +5 -5
- package/dist/integrations/gitlab/subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/version-control.d.ts.map +1 -1
- package/dist/integrations/gitlab/version-control.js +37 -3
- package/dist/integrations/gitlab/version-control.js.map +1 -1
- package/dist/integrations/incidentio/api.d.ts +9 -0
- package/dist/integrations/incidentio/api.d.ts.map +1 -1
- package/dist/integrations/incidentio/api.js +3 -0
- package/dist/integrations/incidentio/api.js.map +1 -1
- package/dist/integrations/incidentio/intake.d.ts +1 -1
- package/dist/integrations/incidentio/intake.d.ts.map +1 -1
- package/dist/integrations/incidentio/intake.js.map +1 -1
- package/dist/integrations/incidentio/integration.d.ts.map +1 -1
- package/dist/integrations/incidentio/integration.js +1 -0
- package/dist/integrations/incidentio/integration.js.map +1 -1
- package/dist/integrations/jira/api.d.ts +7 -0
- package/dist/integrations/jira/api.d.ts.map +1 -1
- package/dist/integrations/jira/api.js +4 -0
- package/dist/integrations/jira/api.js.map +1 -1
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/platform/api-client.d.ts +6 -0
- package/dist/integrations/platform/api-client.d.ts.map +1 -1
- package/dist/integrations/platform/api-client.js +6 -1
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/connect/routes.d.ts +1 -1
- package/dist/integrations/platform/connect/routes.d.ts.map +1 -1
- package/dist/integrations/platform/connect/routes.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +27 -1
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/incidentio/integration.d.ts +1 -1
- package/dist/integrations/platform/incidentio/integration.d.ts.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js +1 -0
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.d.ts +1 -1
- package/dist/integrations/platform/jira/integration.d.ts.map +1 -1
- package/dist/integrations/platform/jira/integration.js +1 -0
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.d.ts +1 -1
- package/dist/integrations/platform/linear/event-worker.d.ts.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +33 -6
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +16 -5
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +137 -64
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +14 -0
- package/dist/integrations/subscription-session.d.ts.map +1 -1
- package/dist/integrations/subscription-session.js +30 -16
- package/dist/integrations/subscription-session.js.map +1 -1
- package/dist/mcp/disconnect.d.ts +10 -0
- package/dist/mcp/disconnect.d.ts.map +1 -0
- package/dist/mcp/disconnect.js +44 -0
- package/dist/mcp/disconnect.js.map +1 -0
- package/dist/mcp/routes.d.ts +46 -0
- package/dist/mcp/routes.d.ts.map +1 -0
- package/dist/mcp/routes.js +243 -0
- package/dist/mcp/routes.js.map +1 -0
- package/dist/provider-display-name.d.ts +2 -0
- package/dist/provider-display-name.d.ts.map +1 -0
- package/dist/provider-display-name.js +16 -0
- package/dist/provider-display-name.js.map +1 -0
- package/dist/review-verdict.d.ts +11 -0
- package/dist/review-verdict.d.ts.map +1 -1
- package/dist/review-verdict.js +16 -1
- package/dist/review-verdict.js.map +1 -1
- package/dist/routes/attention-activity.d.ts.map +1 -1
- package/dist/routes/attention-activity.js +5 -2
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-providers.d.ts +0 -1
- package/dist/routes/attention-providers.d.ts.map +1 -1
- package/dist/routes/attention-providers.js +5 -6
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/config.d.ts +44 -5
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +181 -58
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.d.ts +4 -0
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +3 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/custom-provider-source.d.ts +8 -0
- package/dist/routes/custom-provider-source.d.ts.map +1 -1
- package/dist/routes/custom-provider-source.js +22 -1
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/oauth.js +1 -1
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.d.ts +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +1 -1
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.d.ts +7 -2
- package/dist/routes/provider-credentials.d.ts.map +1 -1
- package/dist/routes/provider-credentials.js +30 -8
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/skills.d.ts +2 -4
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/skills.js +19 -65
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +9 -1
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +131 -7
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +3 -2
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +7 -3
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +60 -120
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +7 -7
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/dispatch-errors.d.ts.map +1 -1
- package/dist/rules/dispatch-errors.js +12 -0
- package/dist/rules/dispatch-errors.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +8 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +117 -12
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +2 -1
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +4 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +36 -4
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +82 -3
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.d.ts +17 -0
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +25 -0
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +2 -4
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +1 -4
- package/dist/rules/types.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js +1 -0
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/sandbox/session-retirement.d.ts +27 -3
- package/dist/sandbox/session-retirement.d.ts.map +1 -1
- package/dist/sandbox/session-retirement.js +158 -20
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/secret-encryption.d.ts.map +1 -1
- package/dist/secret-encryption.js +65 -25
- package/dist/secret-encryption.js.map +1 -1
- package/dist/session/channel-reply-processor.d.ts +36 -0
- package/dist/session/channel-reply-processor.d.ts.map +1 -0
- package/dist/session/channel-reply-processor.js +46 -0
- package/dist/session/channel-reply-processor.js.map +1 -0
- package/dist/session/channel-session-link-processor.d.ts +39 -0
- package/dist/session/channel-session-link-processor.d.ts.map +1 -0
- package/dist/session/channel-session-link-processor.js +100 -0
- package/dist/session/channel-session-link-processor.js.map +1 -0
- package/dist/session/credential-policy.d.ts +1 -1
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js +7 -6
- package/dist/session/credential-policy.js.map +1 -1
- package/dist/session/default-model-hydration.d.ts +1 -0
- package/dist/session/default-model-hydration.d.ts.map +1 -1
- package/dist/session/default-model-hydration.js +3 -2
- package/dist/session/default-model-hydration.js.map +1 -1
- package/dist/session/environment-state-processor.js +1 -1
- package/dist/session/environment-state-processor.js.map +1 -1
- package/dist/session/factory-session.d.ts +19 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +5 -2
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +3 -3
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +16 -17
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/first-exec-capture.d.ts +1 -1
- package/dist/session/first-exec-capture.d.ts.map +1 -1
- package/dist/session/first-exec-capture.js +1 -1
- package/dist/session/first-exec-capture.js.map +1 -1
- package/dist/session/first-message-capture.d.ts +1 -1
- package/dist/session/first-message-capture.d.ts.map +1 -1
- package/dist/session/first-message-capture.js +1 -1
- package/dist/session/first-message-capture.js.map +1 -1
- package/dist/session/live-sessions.d.ts +1 -1
- package/dist/session/live-sessions.js +1 -1
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/session/memory-settings-hydration.d.ts +1 -1
- package/dist/session/memory-settings-hydration.d.ts.map +1 -1
- package/dist/session/memory-settings-hydration.js +1 -1
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/reply-credential-processor.d.ts +15 -0
- package/dist/session/reply-credential-processor.d.ts.map +1 -0
- package/dist/session/reply-credential-processor.js +38 -0
- package/dist/session/reply-credential-processor.js.map +1 -0
- package/dist/session/resource-migration.d.ts +87 -0
- package/dist/session/resource-migration.d.ts.map +1 -0
- package/dist/session/resource-migration.js +163 -0
- package/dist/session/resource-migration.js.map +1 -0
- package/dist/session/run-audit.d.ts +24 -0
- package/dist/session/run-audit.d.ts.map +1 -1
- package/dist/session/run-audit.js +93 -7
- package/dist/session/run-audit.js.map +1 -1
- package/dist/session/session-threads.d.ts +49 -0
- package/dist/session/session-threads.d.ts.map +1 -0
- package/dist/session/session-threads.js +70 -0
- package/dist/session/session-threads.js.map +1 -0
- package/dist/session/source-control-tools.d.ts +19 -1
- package/dist/session/source-control-tools.d.ts.map +1 -1
- package/dist/session/source-control-tools.js +78 -11
- package/dist/session/source-control-tools.js.map +1 -1
- package/dist/session/thinking-default.d.ts.map +1 -1
- package/dist/session/thinking-default.js +3 -4
- package/dist/session/thinking-default.js.map +1 -1
- package/dist/session/thread-session-stamp.d.ts +21 -0
- package/dist/session/thread-session-stamp.d.ts.map +1 -0
- package/dist/session/thread-session-stamp.js +20 -0
- package/dist/session/thread-session-stamp.js.map +1 -0
- package/dist/session/thread-title-mirror.d.ts +1 -1
- package/dist/session/thread-title-mirror.d.ts.map +1 -1
- package/dist/session/thread-title-mirror.js +1 -1
- package/dist/session/thread-title-mirror.js.map +1 -1
- package/dist/session/work-item-repository.d.ts +1 -1
- package/dist/session/work-item-repository.d.ts.map +1 -1
- package/dist/session/work-item-repository.js.map +1 -1
- package/dist/skills/service.d.ts +6 -8
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +3 -3
- package/dist/skills/service.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +2 -2
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/domain.js +1 -1
- package/dist/storage/domains/filesystem/base.d.ts +4 -0
- package/dist/storage/domains/filesystem/base.d.ts.map +1 -1
- package/dist/storage/domains/filesystem/base.js +5 -0
- package/dist/storage/domains/filesystem/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.d.ts +8 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -1
- package/dist/storage/domains/integrations/base.js +7 -0
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/projects/base.d.ts +6 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -1
- package/dist/storage/domains/projects/base.js +12 -0
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +28 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +48 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +8 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +13 -1
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +79 -7
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +172 -30
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/timing.d.ts +23 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +85 -6
- package/dist/timing.js.map +1 -1
- package/dist/tools/annotations.d.ts +7 -0
- package/dist/tools/annotations.d.ts.map +1 -0
- package/dist/tools/annotations.js +102 -0
- package/dist/tools/annotations.js.map +1 -0
- package/dist/tools/approval-tools.d.ts +15 -0
- package/dist/tools/approval-tools.d.ts.map +1 -0
- package/dist/tools/approval-tools.js +79 -0
- package/dist/tools/approval-tools.js.map +1 -0
- package/dist/tools/deliver.d.ts +71 -0
- package/dist/tools/deliver.d.ts.map +1 -0
- package/dist/tools/deliver.js +129 -0
- package/dist/tools/deliver.js.map +1 -0
- package/dist/tools/index.d.ts +30 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/messaging-tools.d.ts +28 -0
- package/dist/tools/messaging-tools.d.ts.map +1 -0
- package/dist/tools/messaging-tools.js +396 -0
- package/dist/tools/messaging-tools.js.map +1 -0
- package/dist/tools/permissions.d.ts +31 -0
- package/dist/tools/permissions.d.ts.map +1 -0
- package/dist/tools/permissions.js +48 -0
- package/dist/tools/permissions.js.map +1 -0
- package/dist/tools/read-tools.d.ts +30 -0
- package/dist/tools/read-tools.d.ts.map +1 -0
- package/dist/tools/read-tools.js +293 -0
- package/dist/tools/read-tools.js.map +1 -0
- package/dist/tools/reports.d.ts +174 -0
- package/dist/tools/reports.d.ts.map +1 -0
- package/dist/tools/reports.js +380 -0
- package/dist/tools/reports.js.map +1 -0
- package/dist/tools/run-watch.d.ts +46 -0
- package/dist/tools/run-watch.d.ts.map +1 -0
- package/dist/tools/run-watch.js +102 -0
- package/dist/tools/run-watch.js.map +1 -0
- package/dist/tools/scope.d.ts +54 -0
- package/dist/tools/scope.d.ts.map +1 -0
- package/dist/tools/scope.js +87 -0
- package/dist/tools/scope.js.map +1 -0
- package/dist/tools/session-tools.d.ts +43 -0
- package/dist/tools/session-tools.d.ts.map +1 -0
- package/dist/tools/session-tools.js +410 -0
- package/dist/tools/session-tools.js.map +1 -0
- package/dist/tools/sessions-state-processor.d.ts +41 -0
- package/dist/tools/sessions-state-processor.d.ts.map +1 -0
- package/dist/tools/sessions-state-processor.js +190 -0
- package/dist/tools/sessions-state-processor.js.map +1 -0
- package/dist/tools/side-sessions.d.ts +53 -0
- package/dist/tools/side-sessions.d.ts.map +1 -0
- package/dist/tools/side-sessions.js +83 -0
- package/dist/tools/side-sessions.js.map +1 -0
- package/dist/tools/work-item-tools.d.ts +28 -0
- package/dist/tools/work-item-tools.d.ts.map +1 -0
- package/dist/tools/work-item-tools.js +342 -0
- package/dist/tools/work-item-tools.js.map +1 -0
- package/dist/work-items/create-work-item.d.ts +55 -0
- package/dist/work-items/create-work-item.d.ts.map +1 -0
- package/dist/work-items/create-work-item.js +101 -0
- package/dist/work-items/create-work-item.js.map +1 -0
- package/dist/work-items/decision-actions.d.ts +42 -0
- package/dist/work-items/decision-actions.d.ts.map +1 -0
- package/dist/work-items/decision-actions.js +70 -0
- package/dist/work-items/decision-actions.js.map +1 -0
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +15 -7
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-gitlab-rereview/SKILL.md +1 -1
- package/factory-skills/factory-gitlab-review/SKILL.md +1 -1
- package/factory-skills/factory-plan/SKILL.md +4 -4
- package/factory-skills/factory-rereview/SKILL.md +3 -1
- package/factory-skills/factory-review/SKILL.md +3 -1
- package/factory-skills/factory-self-review/SKILL.md +46 -0
- package/package.json +15 -15
|
@@ -3,6 +3,7 @@ import { createCipheriv, createDecipheriv } from "crypto";
|
|
|
3
3
|
const ENVELOPE_PREFIX = "mastra:factory-secret:v1:";
|
|
4
4
|
const ALGORITHM = "aes-256-gcm";
|
|
5
5
|
const IV_BYTES = 12;
|
|
6
|
+
const MAX_NESTED_ENVELOPES = 4;
|
|
6
7
|
function validateKey({ id, key }) {
|
|
7
8
|
if (!id) throw new Error("[FactorySecretEncryption] Key id is required.");
|
|
8
9
|
const buffer = Buffer.from(key);
|
|
@@ -43,38 +44,66 @@ function createFactorySecretEncryption(config) {
|
|
|
43
44
|
};
|
|
44
45
|
return `${ENVELOPE_PREFIX}${Buffer.from(JSON.stringify(envelope), "utf8").toString("base64url")}`;
|
|
45
46
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
decrypt
|
|
48
|
+
};
|
|
49
|
+
async function decrypt(value, depth = 0) {
|
|
50
|
+
if (typeof value !== "string") return {
|
|
51
|
+
value: structuredClone(value),
|
|
52
|
+
needsReencryption: true
|
|
53
|
+
};
|
|
54
|
+
if (!value.startsWith(ENVELOPE_PREFIX)) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(value);
|
|
56
58
|
} catch {
|
|
57
59
|
return {
|
|
58
60
|
value,
|
|
59
61
|
needsReencryption: true
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(envelope.iv, "base64url"));
|
|
67
|
-
decipher.setAuthTag(Buffer.from(envelope.tag, "base64url"));
|
|
68
|
-
const plaintext = Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext, "base64url")), decipher.final()]).toString("utf8");
|
|
69
|
-
return {
|
|
70
|
-
value: JSON.parse(plaintext),
|
|
71
|
-
needsReencryption: envelope.keyId !== config.primary.id
|
|
72
|
-
};
|
|
73
|
-
} catch {
|
|
74
|
-
throw new Error("[FactorySecretEncryption] Unable to decrypt encrypted value.");
|
|
75
|
-
}
|
|
64
|
+
return {
|
|
65
|
+
value: await unwrapNestedEnvelope(parsed, depth) ?? parsed,
|
|
66
|
+
needsReencryption: true
|
|
67
|
+
};
|
|
76
68
|
}
|
|
77
|
-
|
|
69
|
+
const envelope = parseEnvelope(value);
|
|
70
|
+
const key = keys.get(envelope.keyId);
|
|
71
|
+
if (!key) throw new Error(`[FactorySecretEncryption] Unknown key id "${envelope.keyId}".`);
|
|
72
|
+
let decoded;
|
|
73
|
+
try {
|
|
74
|
+
const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(envelope.iv, "base64url"));
|
|
75
|
+
decipher.setAuthTag(Buffer.from(envelope.tag, "base64url"));
|
|
76
|
+
const plaintext = Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext, "base64url")), decipher.final()]).toString("utf8");
|
|
77
|
+
decoded = JSON.parse(plaintext);
|
|
78
|
+
} catch {
|
|
79
|
+
throw new Error("[FactorySecretEncryption] Unable to decrypt encrypted value.");
|
|
80
|
+
}
|
|
81
|
+
const nested = await unwrapNestedEnvelope(decoded, depth);
|
|
82
|
+
if (nested !== void 0) return {
|
|
83
|
+
value: nested,
|
|
84
|
+
needsReencryption: true
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
value: decoded,
|
|
88
|
+
needsReencryption: envelope.keyId !== config.primary.id
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Older plaintext encryptors treated an envelope as a raw legacy secret and
|
|
93
|
+
* rewrote it as JSON text, which a keyed boot then re-encrypted. Peel those
|
|
94
|
+
* layers back to the real value so the row is repaired on the next write.
|
|
95
|
+
* A user-supplied secret can also look like an envelope, so anything that
|
|
96
|
+
* does not decrypt with our keys is kept as the literal value.
|
|
97
|
+
*/
|
|
98
|
+
async function unwrapNestedEnvelope(value, depth) {
|
|
99
|
+
if (typeof value !== "string" || !value.startsWith(ENVELOPE_PREFIX)) return void 0;
|
|
100
|
+
if (depth >= MAX_NESTED_ENVELOPES) return void 0;
|
|
101
|
+
try {
|
|
102
|
+
return (await decrypt(value, depth + 1)).value;
|
|
103
|
+
} catch {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
78
107
|
}
|
|
79
108
|
/** Explicit plaintext compatibility for local, no-auth Factory development. */
|
|
80
109
|
function createPlaintextFactorySecretEncryption() {
|
|
@@ -87,6 +116,17 @@ function createPlaintextFactorySecretEncryption() {
|
|
|
87
116
|
value: structuredClone(value),
|
|
88
117
|
needsReencryption: false
|
|
89
118
|
};
|
|
119
|
+
if (value.startsWith(ENVELOPE_PREFIX)) {
|
|
120
|
+
try {
|
|
121
|
+
parseEnvelope(value);
|
|
122
|
+
} catch {
|
|
123
|
+
return {
|
|
124
|
+
value,
|
|
125
|
+
needsReencryption: true
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
throw new Error("[FactorySecretEncryption] Value is encrypted, but this instance has no secret encryption key configured. Configure 'secretEncryption' with the same key on every Factory instance.");
|
|
129
|
+
}
|
|
90
130
|
try {
|
|
91
131
|
return {
|
|
92
132
|
value: JSON.parse(value),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-encryption.js","names":[],"sources":["../src/secret-encryption.ts"],"sourcesContent":["import { createCipheriv, createDecipheriv } from 'node:crypto';\n\nconst ENVELOPE_PREFIX = 'mastra:factory-secret:v1:';\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_BYTES = 12;\n\ninterface SecretEnvelopeV1 {\n keyId: string;\n iv: string;\n ciphertext: string;\n tag: string;\n}\n\nexport interface DecryptedFactorySecret<T> {\n value: T;\n needsReencryption: boolean;\n}\n\n/** Encrypts opaque JSON values before they cross the Factory storage boundary. */\nexport interface FactorySecretEncryption {\n encrypt<T>(value: T): Promise<string>;\n decrypt<T>(value: unknown): Promise<DecryptedFactorySecret<T>>;\n}\n\nexport interface FactorySecretEncryptionKey {\n id: string;\n key: Uint8Array;\n}\n\nexport interface FactorySecretEncryptionConfig {\n primary: FactorySecretEncryptionKey;\n previous?: FactorySecretEncryptionKey[];\n}\n\nfunction validateKey({ id, key }: FactorySecretEncryptionKey): Buffer {\n if (!id) throw new Error('[FactorySecretEncryption] Key id is required.');\n const buffer = Buffer.from(key);\n if (buffer.byteLength !== 32) {\n throw new Error(`[FactorySecretEncryption] Key \"${id}\" must be exactly 32 bytes.`);\n }\n return buffer;\n}\n\nfunction parseEnvelope(value: string): SecretEnvelopeV1 {\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.from(value.slice(ENVELOPE_PREFIX.length), 'base64url').toString('utf8'));\n } catch {\n throw new Error('[FactorySecretEncryption] Invalid encrypted value.');\n }\n\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n typeof (parsed as SecretEnvelopeV1).keyId !== 'string' ||\n typeof (parsed as SecretEnvelopeV1).iv !== 'string' ||\n typeof (parsed as SecretEnvelopeV1).ciphertext !== 'string' ||\n typeof (parsed as SecretEnvelopeV1).tag !== 'string'\n ) {\n throw new Error('[FactorySecretEncryption] Invalid encrypted value.');\n }\n return parsed as SecretEnvelopeV1;\n}\n\n/**\n * Creates a versioned AES-256-GCM encryptor. The primary key is used for new\n * writes; previous keys remain decrypt-only until stored values are rotated.\n */\nexport function createFactorySecretEncryption(config: FactorySecretEncryptionConfig): FactorySecretEncryption {\n const primaryKey = validateKey(config.primary);\n const keys = new Map<string, Buffer>([[config.primary.id, primaryKey]]);\n for (const previous of config.previous ?? []) {\n if (keys.has(previous.id)) throw new Error(`[FactorySecretEncryption] Duplicate key id \"${previous.id}\".`);\n keys.set(previous.id, validateKey(previous));\n }\n\n return {\n async encrypt<T>(value: T): Promise<string> {\n const iv = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES)));\n const cipher = createCipheriv(ALGORITHM, primaryKey, iv);\n const ciphertext = Buffer.concat([cipher.update(JSON.stringify(value), 'utf8'), cipher.final()]);\n const envelope: SecretEnvelopeV1 = {\n keyId: config.primary.id,\n iv: iv.toString('base64url'),\n ciphertext: ciphertext.toString('base64url'),\n tag: cipher.getAuthTag().toString('base64url'),\n };\n return `${ENVELOPE_PREFIX}${Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64url')}`;\n },\n\n async decrypt<T>(value: unknown): Promise<DecryptedFactorySecret<T>> {\n if (typeof value !== 'string') {\n return { value: structuredClone(value) as T, needsReencryption: true };\n }\n if (!value.startsWith(ENVELOPE_PREFIX)) {\n // JSON text written by the plaintext encryptor, or a raw pre-encryption\n // secret string. Decode it the same way the plaintext encryptor does so\n // migration encrypts the original value, not its JSON text.\n try {\n return { value: JSON.parse(value) as T, needsReencryption: true };\n } catch {\n return { value: value as T, needsReencryption: true };\n }\n }\n\n const envelope = parseEnvelope(value);\n const key = keys.get(envelope.keyId);\n if (!key) throw new Error(`[FactorySecretEncryption] Unknown key id \"${envelope.keyId}\".`);\n\n try {\n const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(envelope.iv, 'base64url'));\n decipher.setAuthTag(Buffer.from(envelope.tag, 'base64url'));\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(envelope.ciphertext, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n return {\n value: JSON.parse(plaintext) as T,\n needsReencryption: envelope.keyId !== config.primary.id,\n };\n } catch {\n throw new Error('[FactorySecretEncryption] Unable to decrypt encrypted value.');\n }\n },\n };\n}\n\n/** Explicit plaintext compatibility for local, no-auth Factory development. */\nexport function createPlaintextFactorySecretEncryption(): FactorySecretEncryption {\n return {\n async encrypt<T>(value: T): Promise<string> {\n return JSON.stringify(value);\n },\n async decrypt<T>(value: unknown): Promise<DecryptedFactorySecret<T>> {\n if (typeof value !== 'string') return { value: structuredClone(value) as T, needsReencryption: false };\n try {\n return { value: JSON.parse(value) as T, needsReencryption: false };\n } catch {\n // Pre-encryption rows stored raw secret strings (e.g. a bare\n // `custom_providers.api_key`), not JSON. Treat the raw string as the\n // value and flag it so migration rewrites it in the current format.\n return { value: value as T, needsReencryption: true };\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"secret-encryption.js","names":[],"sources":["../src/secret-encryption.ts"],"sourcesContent":["import { createCipheriv, createDecipheriv } from 'node:crypto';\n\nconst ENVELOPE_PREFIX = 'mastra:factory-secret:v1:';\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_BYTES = 12;\nconst MAX_NESTED_ENVELOPES = 4;\n\ninterface SecretEnvelopeV1 {\n keyId: string;\n iv: string;\n ciphertext: string;\n tag: string;\n}\n\nexport interface DecryptedFactorySecret<T> {\n value: T;\n needsReencryption: boolean;\n}\n\n/** Encrypts opaque JSON values before they cross the Factory storage boundary. */\nexport interface FactorySecretEncryption {\n encrypt<T>(value: T): Promise<string>;\n decrypt<T>(value: unknown): Promise<DecryptedFactorySecret<T>>;\n}\n\nexport interface FactorySecretEncryptionKey {\n id: string;\n key: Uint8Array;\n}\n\nexport interface FactorySecretEncryptionConfig {\n primary: FactorySecretEncryptionKey;\n previous?: FactorySecretEncryptionKey[];\n}\n\nfunction validateKey({ id, key }: FactorySecretEncryptionKey): Buffer {\n if (!id) throw new Error('[FactorySecretEncryption] Key id is required.');\n const buffer = Buffer.from(key);\n if (buffer.byteLength !== 32) {\n throw new Error(`[FactorySecretEncryption] Key \"${id}\" must be exactly 32 bytes.`);\n }\n return buffer;\n}\n\nfunction parseEnvelope(value: string): SecretEnvelopeV1 {\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.from(value.slice(ENVELOPE_PREFIX.length), 'base64url').toString('utf8'));\n } catch {\n throw new Error('[FactorySecretEncryption] Invalid encrypted value.');\n }\n\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n typeof (parsed as SecretEnvelopeV1).keyId !== 'string' ||\n typeof (parsed as SecretEnvelopeV1).iv !== 'string' ||\n typeof (parsed as SecretEnvelopeV1).ciphertext !== 'string' ||\n typeof (parsed as SecretEnvelopeV1).tag !== 'string'\n ) {\n throw new Error('[FactorySecretEncryption] Invalid encrypted value.');\n }\n return parsed as SecretEnvelopeV1;\n}\n\n/**\n * Creates a versioned AES-256-GCM encryptor. The primary key is used for new\n * writes; previous keys remain decrypt-only until stored values are rotated.\n */\nexport function createFactorySecretEncryption(config: FactorySecretEncryptionConfig): FactorySecretEncryption {\n const primaryKey = validateKey(config.primary);\n const keys = new Map<string, Buffer>([[config.primary.id, primaryKey]]);\n for (const previous of config.previous ?? []) {\n if (keys.has(previous.id)) throw new Error(`[FactorySecretEncryption] Duplicate key id \"${previous.id}\".`);\n keys.set(previous.id, validateKey(previous));\n }\n\n return {\n async encrypt<T>(value: T): Promise<string> {\n const iv = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES)));\n const cipher = createCipheriv(ALGORITHM, primaryKey, iv);\n const ciphertext = Buffer.concat([cipher.update(JSON.stringify(value), 'utf8'), cipher.final()]);\n const envelope: SecretEnvelopeV1 = {\n keyId: config.primary.id,\n iv: iv.toString('base64url'),\n ciphertext: ciphertext.toString('base64url'),\n tag: cipher.getAuthTag().toString('base64url'),\n };\n return `${ENVELOPE_PREFIX}${Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64url')}`;\n },\n\n decrypt,\n };\n\n async function decrypt<T>(value: unknown, depth = 0): Promise<DecryptedFactorySecret<T>> {\n if (typeof value !== 'string') {\n return { value: structuredClone(value) as T, needsReencryption: true };\n }\n if (!value.startsWith(ENVELOPE_PREFIX)) {\n // JSON text written by the plaintext encryptor, or a raw pre-encryption\n // secret string. Decode it the same way the plaintext encryptor does so\n // migration encrypts the original value, not its JSON text.\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n return { value: value as T, needsReencryption: true };\n }\n return { value: (await unwrapNestedEnvelope<T>(parsed, depth)) ?? (parsed as T), needsReencryption: true };\n }\n\n const envelope = parseEnvelope(value);\n const key = keys.get(envelope.keyId);\n if (!key) throw new Error(`[FactorySecretEncryption] Unknown key id \"${envelope.keyId}\".`);\n\n let decoded: unknown;\n try {\n const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(envelope.iv, 'base64url'));\n decipher.setAuthTag(Buffer.from(envelope.tag, 'base64url'));\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(envelope.ciphertext, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n decoded = JSON.parse(plaintext);\n } catch {\n throw new Error('[FactorySecretEncryption] Unable to decrypt encrypted value.');\n }\n const nested = await unwrapNestedEnvelope<T>(decoded, depth);\n if (nested !== undefined) return { value: nested, needsReencryption: true };\n return { value: decoded as T, needsReencryption: envelope.keyId !== config.primary.id };\n }\n\n /**\n * Older plaintext encryptors treated an envelope as a raw legacy secret and\n * rewrote it as JSON text, which a keyed boot then re-encrypted. Peel those\n * layers back to the real value so the row is repaired on the next write.\n * A user-supplied secret can also look like an envelope, so anything that\n * does not decrypt with our keys is kept as the literal value.\n */\n async function unwrapNestedEnvelope<T>(value: unknown, depth: number): Promise<T | undefined> {\n if (typeof value !== 'string' || !value.startsWith(ENVELOPE_PREFIX)) return undefined;\n if (depth >= MAX_NESTED_ENVELOPES) return undefined;\n try {\n return (await decrypt<T>(value, depth + 1)).value;\n } catch {\n return undefined;\n }\n }\n}\n\n/** Explicit plaintext compatibility for local, no-auth Factory development. */\nexport function createPlaintextFactorySecretEncryption(): FactorySecretEncryption {\n return {\n async encrypt<T>(value: T): Promise<string> {\n return JSON.stringify(value);\n },\n async decrypt<T>(value: unknown): Promise<DecryptedFactorySecret<T>> {\n if (typeof value !== 'string') return { value: structuredClone(value) as T, needsReencryption: false };\n if (value.startsWith(ENVELOPE_PREFIX)) {\n // Never hand an envelope back as the secret, and never let migration\n // rewrite it as plaintext JSON: that corrupts the row for keyed instances.\n // A raw legacy secret can merely start with the prefix; keep those as-is.\n try {\n parseEnvelope(value);\n } catch {\n return { value: value as T, needsReencryption: true };\n }\n throw new Error(\n '[FactorySecretEncryption] Value is encrypted, but this instance has no secret encryption key configured. ' +\n \"Configure 'secretEncryption' with the same key on every Factory instance.\",\n );\n }\n try {\n return { value: JSON.parse(value) as T, needsReencryption: false };\n } catch {\n // Pre-encryption rows stored raw secret strings (e.g. a bare\n // `custom_providers.api_key`), not JSON. Treat the raw string as the\n // value and flag it so migration rewrites it in the current format.\n return { value: value as T, needsReencryption: true };\n }\n },\n };\n}\n"],"mappings":";;AAEA,MAAM,kBAAkB;AACxB,MAAM,YAAY;AAClB,MAAM,WAAW;AACjB,MAAM,uBAAuB;AA8B7B,SAAS,YAAY,EAAE,IAAI,OAA2C;CACpE,IAAI,CAAC,IAAI,MAAM,IAAI,MAAM,+CAA+C;CACxE,MAAM,SAAS,OAAO,KAAK,GAAG;CAC9B,IAAI,OAAO,eAAe,IACxB,MAAM,IAAI,MAAM,kCAAkC,GAAG,4BAA4B;CAEnF,OAAO;AACT;AAEA,SAAS,cAAc,OAAiC;CACtD,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,OAAO,KAAK,MAAM,MAAM,EAAsB,GAAG,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC;CACpG,QAAQ;EACN,MAAM,IAAI,MAAM,oDAAoD;CACtE;CAEA,IACE,CAAC,UACD,OAAO,WAAW,YAClB,OAAQ,OAA4B,UAAU,YAC9C,OAAQ,OAA4B,OAAO,YAC3C,OAAQ,OAA4B,eAAe,YACnD,OAAQ,OAA4B,QAAQ,UAE5C,MAAM,IAAI,MAAM,oDAAoD;CAEtE,OAAO;AACT;;;;;AAMA,SAAgB,8BAA8B,QAAgE;CAC5G,MAAM,aAAa,YAAY,OAAO,OAAO;CAC7C,MAAM,uBAAO,IAAI,IAAoB,CAAC,CAAC,OAAO,QAAQ,IAAI,UAAU,CAAC,CAAC;CACtE,KAAK,MAAM,YAAY,OAAO,YAAY,CAAC,GAAG;EAC5C,IAAI,KAAK,IAAI,SAAS,EAAE,GAAG,MAAM,IAAI,MAAM,+CAA+C,SAAS,GAAG,GAAG;EACzG,KAAK,IAAI,SAAS,IAAI,YAAY,QAAQ,CAAC;CAC7C;CAEA,OAAO;EACL,MAAM,QAAW,OAA2B;GAC1C,MAAM,KAAK,OAAO,KAAK,WAAW,OAAO,gBAAgB,IAAI,WAAW,QAAQ,CAAC,CAAC;GAClF,MAAM,SAAS,eAAe,WAAW,YAAY,EAAE;GACvD,MAAM,aAAa,OAAO,OAAO,CAAC,OAAO,OAAO,KAAK,UAAU,KAAK,GAAG,MAAM,GAAG,OAAO,MAAM,CAAC,CAAC;GAC/F,MAAM,WAA6B;IACjC,OAAO,OAAO,QAAQ;IACtB,IAAI,GAAG,SAAS,WAAW;IAC3B,YAAY,WAAW,SAAS,WAAW;IAC3C,KAAK,OAAO,WAAW,CAAC,CAAC,SAAS,WAAW;GAC/C;GACA,OAAO,GAAG,kBAAkB,OAAO,KAAK,KAAK,UAAU,QAAQ,GAAG,MAAM,CAAC,CAAC,SAAS,WAAW;EAChG;EAEA;CACF;CAEA,eAAe,QAAW,OAAgB,QAAQ,GAAuC;EACvF,IAAI,OAAO,UAAU,UACnB,OAAO;GAAE,OAAO,gBAAgB,KAAK;GAAQ,mBAAmB;EAAK;EAEvE,IAAI,CAAC,MAAM,WAAW,eAAe,GAAG;GAItC,IAAI;GACJ,IAAI;IACF,SAAS,KAAK,MAAM,KAAK;GAC3B,QAAQ;IACN,OAAO;KAAS;KAAY,mBAAmB;IAAK;GACtD;GACA,OAAO;IAAE,OAAQ,MAAM,qBAAwB,QAAQ,KAAK,KAAO;IAAc,mBAAmB;GAAK;EAC3G;EAEA,MAAM,WAAW,cAAc,KAAK;EACpC,MAAM,MAAM,KAAK,IAAI,SAAS,KAAK;EACnC,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,6CAA6C,SAAS,MAAM,GAAG;EAEzF,IAAI;EACJ,IAAI;GACF,MAAM,WAAW,iBAAiB,WAAW,KAAK,OAAO,KAAK,SAAS,IAAI,WAAW,CAAC;GACvF,SAAS,WAAW,OAAO,KAAK,SAAS,KAAK,WAAW,CAAC;GAC1D,MAAM,YAAY,OAAO,OAAO,CAC9B,SAAS,OAAO,OAAO,KAAK,SAAS,YAAY,WAAW,CAAC,GAC7D,SAAS,MAAM,CACjB,CAAC,CAAC,CAAC,SAAS,MAAM;GAClB,UAAU,KAAK,MAAM,SAAS;EAChC,QAAQ;GACN,MAAM,IAAI,MAAM,8DAA8D;EAChF;EACA,MAAM,SAAS,MAAM,qBAAwB,SAAS,KAAK;EAC3D,IAAI,WAAW,KAAA,GAAW,OAAO;GAAE,OAAO;GAAQ,mBAAmB;EAAK;EAC1E,OAAO;GAAE,OAAO;GAAc,mBAAmB,SAAS,UAAU,OAAO,QAAQ;EAAG;CACxF;;;;;;;;CASA,eAAe,qBAAwB,OAAgB,OAAuC;EAC5F,IAAI,OAAO,UAAU,YAAY,CAAC,MAAM,WAAW,eAAe,GAAG,OAAO,KAAA;EAC5E,IAAI,SAAS,sBAAsB,OAAO,KAAA;EAC1C,IAAI;GACF,QAAQ,MAAM,QAAW,OAAO,QAAQ,CAAC,EAAA,CAAG;EAC9C,QAAQ;GACN;EACF;CACF;AACF;;AAGA,SAAgB,yCAAkE;CAChF,OAAO;EACL,MAAM,QAAW,OAA2B;GAC1C,OAAO,KAAK,UAAU,KAAK;EAC7B;EACA,MAAM,QAAW,OAAoD;GACnE,IAAI,OAAO,UAAU,UAAU,OAAO;IAAE,OAAO,gBAAgB,KAAK;IAAQ,mBAAmB;GAAM;GACrG,IAAI,MAAM,WAAW,eAAe,GAAG;IAIrC,IAAI;KACF,cAAc,KAAK;IACrB,QAAQ;KACN,OAAO;MAAS;MAAY,mBAAmB;KAAK;IACtD;IACA,MAAM,IAAI,MACR,oLAEF;GACF;GACA,IAAI;IACF,OAAO;KAAE,OAAO,KAAK,MAAM,KAAK;KAAQ,mBAAmB;IAAM;GACnE,QAAQ;IAIN,OAAO;KAAS;KAAY,mBAAmB;IAAK;GACtD;EACF;CACF;AACF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ProcessInputStepArgs, Processor } from '@mastra/core/processors';
|
|
2
|
+
export declare const SESSION_LINK_LABEL = "Open session \u2197";
|
|
3
|
+
/**
|
|
4
|
+
* Tells the agent how to open a reply when the run came in over a chat
|
|
5
|
+
* channel (Slack, …): acknowledge a job in one line that names it — "On it —
|
|
6
|
+
* sleeping 30s, then running the echo" — before starting, so the sender sees
|
|
7
|
+
* a task-specific first message rather than a silent typing status until the
|
|
8
|
+
* first tool finishes. The channel driver posts that opener on its own before
|
|
9
|
+
* the tool widget, so the instruction and the rendering agree. The session
|
|
10
|
+
* link is not the model's job: `FactoryChannelSessionLinkProcessor` appends it
|
|
11
|
+
* to the first reply of a new session, so it is there whether or not the
|
|
12
|
+
* model judged the request worth an opener. No-op for runs without channel
|
|
13
|
+
* context (Studio, API, boards).
|
|
14
|
+
*/
|
|
15
|
+
export declare class FactoryChannelReplyProcessor implements Processor {
|
|
16
|
+
readonly id = "factory-channel-reply";
|
|
17
|
+
processInputStep({ messageList, requestContext }: ProcessInputStepArgs): {
|
|
18
|
+
messageList: import("@mastra/core/agent").MessageList;
|
|
19
|
+
} | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface SessionLinkTarget {
|
|
22
|
+
threadId: string;
|
|
23
|
+
sessionId: string;
|
|
24
|
+
resourceId: string;
|
|
25
|
+
factoryProjectId?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The web URL for a session, or undefined without a public origin. Same
|
|
29
|
+
* routes the Factory UI navigates to: a repo-backed thread's Factory session
|
|
30
|
+
* id is the workspace segment; chat-only threads keep the literal `channel`
|
|
31
|
+
* segment and name their session in the query; a session with no factory
|
|
32
|
+
* falls back to the factory-agnostic /threads/ redirect, which forwards the
|
|
33
|
+
* query through.
|
|
34
|
+
*/
|
|
35
|
+
export declare function sessionLink({ threadId, sessionId, resourceId, factoryProjectId, }: SessionLinkTarget): string | undefined;
|
|
36
|
+
//# sourceMappingURL=channel-reply-processor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-reply-processor.d.ts","sourceRoot":"","sources":["../../src/session/channel-reply-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAI/E,eAAO,MAAM,kBAAkB,wBAAmB,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,qBAAa,4BAA6B,YAAW,SAAS;IAC5D,QAAQ,CAAC,EAAE,2BAAM;IAEjB,gBAAgB,CAAC,EAAE,WAAW,EAAE,cAAc,EAAE,EAAE,oBAAoB;;;CAcvE;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,UAAU,EACV,gBAAgB,GACjB,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAQxC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/session/channel-reply-processor.ts
|
|
2
|
+
const ID = "factory-channel-reply";
|
|
3
|
+
const SESSION_LINK_LABEL = "Open session ↗";
|
|
4
|
+
/**
|
|
5
|
+
* Tells the agent how to open a reply when the run came in over a chat
|
|
6
|
+
* channel (Slack, …): acknowledge a job in one line that names it — "On it —
|
|
7
|
+
* sleeping 30s, then running the echo" — before starting, so the sender sees
|
|
8
|
+
* a task-specific first message rather than a silent typing status until the
|
|
9
|
+
* first tool finishes. The channel driver posts that opener on its own before
|
|
10
|
+
* the tool widget, so the instruction and the rendering agree. The session
|
|
11
|
+
* link is not the model's job: `FactoryChannelSessionLinkProcessor` appends it
|
|
12
|
+
* to the first reply of a new session, so it is there whether or not the
|
|
13
|
+
* model judged the request worth an opener. No-op for runs without channel
|
|
14
|
+
* context (Studio, API, boards).
|
|
15
|
+
*/
|
|
16
|
+
var FactoryChannelReplyProcessor = class {
|
|
17
|
+
id = ID;
|
|
18
|
+
processInputStep({ messageList, requestContext }) {
|
|
19
|
+
if (!requestContext?.get("channel")) return void 0;
|
|
20
|
+
messageList.addSystem({
|
|
21
|
+
role: "system",
|
|
22
|
+
content: ["When a request will take tools or more than a moment to answer, reply first with one short line that says what you are about to do — \"On it — <the task in a few words>.\" — then do the work; the opener posts as its own message before any progress is shown. Give direct answers and trivial replies without an opener.", "Finish with the outcome, not a replay of the steps."].join("\n")
|
|
23
|
+
}, ID);
|
|
24
|
+
return { messageList };
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The web URL for a session, or undefined without a public origin. Same
|
|
29
|
+
* routes the Factory UI navigates to: a repo-backed thread's Factory session
|
|
30
|
+
* id is the workspace segment; chat-only threads keep the literal `channel`
|
|
31
|
+
* segment and name their session in the query; a session with no factory
|
|
32
|
+
* falls back to the factory-agnostic /threads/ redirect, which forwards the
|
|
33
|
+
* query through.
|
|
34
|
+
*/
|
|
35
|
+
function sessionLink({ threadId, sessionId, resourceId, factoryProjectId }) {
|
|
36
|
+
const origin = process.env.MASTRACODE_PUBLIC_URL;
|
|
37
|
+
if (!origin) return void 0;
|
|
38
|
+
const isChatOnly = resourceId.startsWith("channel:");
|
|
39
|
+
const sessionParam = `?sessionId=${encodeURIComponent(sessionId)}`;
|
|
40
|
+
if (!factoryProjectId) return `${origin}/threads/${encodeURIComponent(threadId)}${sessionParam}`;
|
|
41
|
+
return `${origin}/factories/${encodeURIComponent(factoryProjectId)}/workspaces/${isChatOnly ? "channel" : encodeURIComponent(sessionId)}/threads/${encodeURIComponent(threadId)}${isChatOnly ? sessionParam : ""}`;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { FactoryChannelReplyProcessor, SESSION_LINK_LABEL, sessionLink };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=channel-reply-processor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-reply-processor.js","names":[],"sources":["../../src/session/channel-reply-processor.ts"],"sourcesContent":["import type { ProcessInputStepArgs, Processor } from '@mastra/core/processors';\n\nconst ID = 'factory-channel-reply';\n\nexport const SESSION_LINK_LABEL = 'Open session ↗';\n\n/**\n * Tells the agent how to open a reply when the run came in over a chat\n * channel (Slack, …): acknowledge a job in one line that names it — \"On it —\n * sleeping 30s, then running the echo\" — before starting, so the sender sees\n * a task-specific first message rather than a silent typing status until the\n * first tool finishes. The channel driver posts that opener on its own before\n * the tool widget, so the instruction and the rendering agree. The session\n * link is not the model's job: `FactoryChannelSessionLinkProcessor` appends it\n * to the first reply of a new session, so it is there whether or not the\n * model judged the request worth an opener. No-op for runs without channel\n * context (Studio, API, boards).\n */\nexport class FactoryChannelReplyProcessor implements Processor {\n readonly id = ID;\n\n processInputStep({ messageList, requestContext }: ProcessInputStepArgs) {\n if (!requestContext?.get('channel')) return undefined;\n messageList.addSystem(\n {\n role: 'system',\n content: [\n 'When a request will take tools or more than a moment to answer, reply first with one short line that says what you are about to do — \"On it — <the task in a few words>.\" — then do the work; the opener posts as its own message before any progress is shown. Give direct answers and trivial replies without an opener.',\n 'Finish with the outcome, not a replay of the steps.',\n ].join('\\n'),\n },\n ID,\n );\n return { messageList };\n }\n}\n\nexport interface SessionLinkTarget {\n threadId: string;\n sessionId: string;\n resourceId: string;\n factoryProjectId?: string;\n}\n\n/**\n * The web URL for a session, or undefined without a public origin. Same\n * routes the Factory UI navigates to: a repo-backed thread's Factory session\n * id is the workspace segment; chat-only threads keep the literal `channel`\n * segment and name their session in the query; a session with no factory\n * falls back to the factory-agnostic /threads/ redirect, which forwards the\n * query through.\n */\nexport function sessionLink({\n threadId,\n sessionId,\n resourceId,\n factoryProjectId,\n}: SessionLinkTarget): string | undefined {\n const origin = process.env.MASTRACODE_PUBLIC_URL;\n if (!origin) return undefined;\n const isChatOnly = resourceId.startsWith('channel:');\n const sessionParam = `?sessionId=${encodeURIComponent(sessionId)}`;\n if (!factoryProjectId) return `${origin}/threads/${encodeURIComponent(threadId)}${sessionParam}`;\n const workspaceSegment = isChatOnly ? 'channel' : encodeURIComponent(sessionId);\n return `${origin}/factories/${encodeURIComponent(factoryProjectId)}/workspaces/${workspaceSegment}/threads/${encodeURIComponent(threadId)}${isChatOnly ? sessionParam : ''}`;\n}\n"],"mappings":";AAEA,MAAM,KAAK;AAEX,MAAa,qBAAqB;;;;;;;;;;;;;AAclC,IAAa,+BAAb,MAA+D;CAC7D,KAAc;CAEd,iBAAiB,EAAE,aAAa,kBAAwC;EACtE,IAAI,CAAC,gBAAgB,IAAI,SAAS,GAAG,OAAO,KAAA;EAC5C,YAAY,UACV;GACE,MAAM;GACN,SAAS,CACP,gUACA,qDACF,CAAC,CAAC,KAAK,IAAI;EACb,GACA,EACF;EACA,OAAO,EAAE,YAAY;CACvB;AACF;;;;;;;;;AAiBA,SAAgB,YAAY,EAC1B,UACA,WACA,YACA,oBACwC;CACxC,MAAM,SAAS,QAAQ,IAAI;CAC3B,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,MAAM,aAAa,WAAW,WAAW,UAAU;CACnD,MAAM,eAAe,cAAc,mBAAmB,SAAS;CAC/D,IAAI,CAAC,kBAAkB,OAAO,GAAG,OAAO,WAAW,mBAAmB,QAAQ,IAAI;CAElF,OAAO,GAAG,OAAO,aAAa,mBAAmB,gBAAgB,EAAE,cAD1C,aAAa,YAAY,mBAAmB,SAAS,EACoB,WAAW,mBAAmB,QAAQ,IAAI,aAAa,eAAe;AAC1K"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Processor, ProcessOutputStepArgs, ProcessOutputStreamArgs } from '@mastra/core/processors';
|
|
2
|
+
import type { RequestContext } from '@mastra/core/request-context';
|
|
3
|
+
import type { ChunkType } from '@mastra/core/stream';
|
|
4
|
+
/**
|
|
5
|
+
* Request-context marker a channel handler sets on the run that opens a new
|
|
6
|
+
* thread. Only that run gets the link; follow-ups in the same thread don't.
|
|
7
|
+
*/
|
|
8
|
+
export declare const NEW_CHANNEL_SESSION_KEY = "factory__newChannelSession";
|
|
9
|
+
export declare function markNewChannelSession(requestContext: RequestContext): void;
|
|
10
|
+
/**
|
|
11
|
+
* Emits an `Open session ↗` link right after the first reply of a new channel
|
|
12
|
+
* session, so the link closes the reply rather than leading it. The channel
|
|
13
|
+
* driver ends a post at `text-end`, so Slack shows the link as its own short
|
|
14
|
+
* message after the reply.
|
|
15
|
+
*
|
|
16
|
+
* "End of the reply" is not `text-end`: a reply with citations is several
|
|
17
|
+
* text blocks (`text … source … text`), each with its own `text-end`. The
|
|
18
|
+
* reply ends at the first `tool-call` (the opener before tools run) or at
|
|
19
|
+
* `step-finish` (a reply with no tools), whichever comes first after some
|
|
20
|
+
* text. A processor can only return one part, so on that chunk this returns
|
|
21
|
+
* a `text-delta` carrying the link and stashes the chunk itself under
|
|
22
|
+
* `REPROCESS_PART_KEY`, which the runner re-drives through the chain right
|
|
23
|
+
* after — the link lands before it and downstream processors see both.
|
|
24
|
+
* Factory-emitted rather than left to the model so a skipped opener can't
|
|
25
|
+
* drop it. No-op without the new-session marker or a public origin.
|
|
26
|
+
*
|
|
27
|
+
* The streamed text is also what gets persisted, and the link is a Slack
|
|
28
|
+
* display detail: in memory it would show the model its own `[↗](…)` on
|
|
29
|
+
* later turns (inviting it to fabricate links) and render a self-link in
|
|
30
|
+
* the web transcript. The link delta opens its own text part, so
|
|
31
|
+
* `processOutputStep` drops that part from the assistant message before
|
|
32
|
+
* `MessageHistory` saves it.
|
|
33
|
+
*/
|
|
34
|
+
export declare class FactoryChannelSessionLinkProcessor implements Processor {
|
|
35
|
+
readonly id = "factory-channel-session-link";
|
|
36
|
+
processOutputStream({ part, state, requestContext }: ProcessOutputStreamArgs): Promise<ChunkType>;
|
|
37
|
+
processOutputStep({ messageList, requestContext }: ProcessOutputStepArgs): import("@mastra/core/agent").MessageList;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=channel-session-link-processor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-session-link-processor.d.ts","sourceRoot":"","sources":["../../src/session/channel-session-link-processor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACzG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAKrD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,+BAA+B,CAAC;AAEpE,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,cAAc,GAAG,IAAI,CAE1E;AAaD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,kCAAmC,YAAW,SAAS;IAClE,QAAQ,CAAC,EAAE,kCAAkC;IAEvC,mBAAmB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,uBAAuB,GAAG,OAAO,CAAC,SAAS,CAAC;IAmBvG,iBAAiB,CAAC,EAAE,WAAW,EAAE,cAAc,EAAE,EAAE,qBAAqB;CAiBzE"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFactorySessionScope } from "../rules/binding-context.js";
|
|
2
|
+
import { SESSION_LINK_LABEL, sessionLink } from "./channel-reply-processor.js";
|
|
3
|
+
//#region src/session/channel-session-link-processor.ts
|
|
4
|
+
/**
|
|
5
|
+
* Request-context marker a channel handler sets on the run that opens a new
|
|
6
|
+
* thread. Only that run gets the link; follow-ups in the same thread don't.
|
|
7
|
+
*/
|
|
8
|
+
const NEW_CHANNEL_SESSION_KEY = "factory__newChannelSession";
|
|
9
|
+
function markNewChannelSession(requestContext) {
|
|
10
|
+
requestContext.set(NEW_CHANNEL_SESSION_KEY, true);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* State key the core ProcessorRunner reads to re-drive one extra part through
|
|
14
|
+
* the output chain after the part a processor returned. Internal to core
|
|
15
|
+
* (`processors/stream-reprocess.ts`, used by `BatchPartsProcessor`); mirrored
|
|
16
|
+
* here rather than exported so Factory's one use doesn't widen core's API.
|
|
17
|
+
*/
|
|
18
|
+
const REPROCESS_PART_KEY = "__mastraReprocessPart";
|
|
19
|
+
/** Text-part id of the link delta; never collides with a model block id. */
|
|
20
|
+
const LINK_PART_ID = "factory-session-link";
|
|
21
|
+
/**
|
|
22
|
+
* Emits an `Open session ↗` link right after the first reply of a new channel
|
|
23
|
+
* session, so the link closes the reply rather than leading it. The channel
|
|
24
|
+
* driver ends a post at `text-end`, so Slack shows the link as its own short
|
|
25
|
+
* message after the reply.
|
|
26
|
+
*
|
|
27
|
+
* "End of the reply" is not `text-end`: a reply with citations is several
|
|
28
|
+
* text blocks (`text … source … text`), each with its own `text-end`. The
|
|
29
|
+
* reply ends at the first `tool-call` (the opener before tools run) or at
|
|
30
|
+
* `step-finish` (a reply with no tools), whichever comes first after some
|
|
31
|
+
* text. A processor can only return one part, so on that chunk this returns
|
|
32
|
+
* a `text-delta` carrying the link and stashes the chunk itself under
|
|
33
|
+
* `REPROCESS_PART_KEY`, which the runner re-drives through the chain right
|
|
34
|
+
* after — the link lands before it and downstream processors see both.
|
|
35
|
+
* Factory-emitted rather than left to the model so a skipped opener can't
|
|
36
|
+
* drop it. No-op without the new-session marker or a public origin.
|
|
37
|
+
*
|
|
38
|
+
* The streamed text is also what gets persisted, and the link is a Slack
|
|
39
|
+
* display detail: in memory it would show the model its own `[↗](…)` on
|
|
40
|
+
* later turns (inviting it to fabricate links) and render a self-link in
|
|
41
|
+
* the web transcript. The link delta opens its own text part, so
|
|
42
|
+
* `processOutputStep` drops that part from the assistant message before
|
|
43
|
+
* `MessageHistory` saves it.
|
|
44
|
+
*/
|
|
45
|
+
var FactoryChannelSessionLinkProcessor = class {
|
|
46
|
+
id = "factory-channel-session-link";
|
|
47
|
+
async processOutputStream({ part, state, requestContext }) {
|
|
48
|
+
if (state.linked || !requestContext?.get("factory__newChannelSession")) return part;
|
|
49
|
+
if (part.type === "text-delta") {
|
|
50
|
+
state.textStarted = true;
|
|
51
|
+
return part;
|
|
52
|
+
}
|
|
53
|
+
if (part.type !== "tool-call" && part.type !== "step-finish" || !state.textStarted) return part;
|
|
54
|
+
state.linked = true;
|
|
55
|
+
const link = sessionLinkFromRequest(requestContext);
|
|
56
|
+
if (!link) return part;
|
|
57
|
+
state[REPROCESS_PART_KEY] = part;
|
|
58
|
+
return {
|
|
59
|
+
type: "text-delta",
|
|
60
|
+
runId: part.runId,
|
|
61
|
+
from: part.from,
|
|
62
|
+
payload: {
|
|
63
|
+
id: LINK_PART_ID,
|
|
64
|
+
text: linkSuffix(link)
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
processOutputStep({ messageList, requestContext }) {
|
|
69
|
+
if (!requestContext?.get("factory__newChannelSession")) return messageList;
|
|
70
|
+
const link = sessionLinkFromRequest(requestContext);
|
|
71
|
+
if (!link) return messageList;
|
|
72
|
+
const suffix = linkSuffix(link);
|
|
73
|
+
for (const message of messageList.get.response.db()) {
|
|
74
|
+
if (message.role !== "assistant") continue;
|
|
75
|
+
const index = message.content.parts.findIndex((part) => part.type === "text" && part.text === suffix);
|
|
76
|
+
if (index === -1) continue;
|
|
77
|
+
message.content.parts.splice(index, 1);
|
|
78
|
+
if (typeof message.content.content === "string" && message.content.content.endsWith(suffix)) message.content.content = message.content.content.slice(0, -suffix.length);
|
|
79
|
+
return messageList;
|
|
80
|
+
}
|
|
81
|
+
return messageList;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
function linkSuffix(link) {
|
|
85
|
+
return `\n\n[${SESSION_LINK_LABEL}](${link})`;
|
|
86
|
+
}
|
|
87
|
+
function sessionLinkFromRequest(requestContext) {
|
|
88
|
+
const controller = requestContext.get("controller");
|
|
89
|
+
if (!controller?.threadId) return void 0;
|
|
90
|
+
return sessionLink({
|
|
91
|
+
threadId: controller.threadId,
|
|
92
|
+
sessionId: controller.sessionId,
|
|
93
|
+
resourceId: controller.resourceId,
|
|
94
|
+
factoryProjectId: readFactorySessionScope(requestContext)?.factoryProjectId
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { FactoryChannelSessionLinkProcessor, NEW_CHANNEL_SESSION_KEY, markNewChannelSession };
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=channel-session-link-processor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-session-link-processor.js","names":[],"sources":["../../src/session/channel-session-link-processor.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { Processor, ProcessOutputStepArgs, ProcessOutputStreamArgs } from '@mastra/core/processors';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ChunkType } from '@mastra/core/stream';\n\nimport { readFactorySessionScope } from '../rules/binding-context.js';\nimport { SESSION_LINK_LABEL, sessionLink } from './channel-reply-processor.js';\n\n/**\n * Request-context marker a channel handler sets on the run that opens a new\n * thread. Only that run gets the link; follow-ups in the same thread don't.\n */\nexport const NEW_CHANNEL_SESSION_KEY = 'factory__newChannelSession';\n\nexport function markNewChannelSession(requestContext: RequestContext): void {\n requestContext.set(NEW_CHANNEL_SESSION_KEY, true);\n}\n\n/**\n * State key the core ProcessorRunner reads to re-drive one extra part through\n * the output chain after the part a processor returned. Internal to core\n * (`processors/stream-reprocess.ts`, used by `BatchPartsProcessor`); mirrored\n * here rather than exported so Factory's one use doesn't widen core's API.\n */\nconst REPROCESS_PART_KEY = '__mastraReprocessPart';\n\n/** Text-part id of the link delta; never collides with a model block id. */\nconst LINK_PART_ID = 'factory-session-link';\n\n/**\n * Emits an `Open session ↗` link right after the first reply of a new channel\n * session, so the link closes the reply rather than leading it. The channel\n * driver ends a post at `text-end`, so Slack shows the link as its own short\n * message after the reply.\n *\n * \"End of the reply\" is not `text-end`: a reply with citations is several\n * text blocks (`text … source … text`), each with its own `text-end`. The\n * reply ends at the first `tool-call` (the opener before tools run) or at\n * `step-finish` (a reply with no tools), whichever comes first after some\n * text. A processor can only return one part, so on that chunk this returns\n * a `text-delta` carrying the link and stashes the chunk itself under\n * `REPROCESS_PART_KEY`, which the runner re-drives through the chain right\n * after — the link lands before it and downstream processors see both.\n * Factory-emitted rather than left to the model so a skipped opener can't\n * drop it. No-op without the new-session marker or a public origin.\n *\n * The streamed text is also what gets persisted, and the link is a Slack\n * display detail: in memory it would show the model its own `[↗](…)` on\n * later turns (inviting it to fabricate links) and render a self-link in\n * the web transcript. The link delta opens its own text part, so\n * `processOutputStep` drops that part from the assistant message before\n * `MessageHistory` saves it.\n */\nexport class FactoryChannelSessionLinkProcessor implements Processor {\n readonly id = 'factory-channel-session-link';\n\n async processOutputStream({ part, state, requestContext }: ProcessOutputStreamArgs): Promise<ChunkType> {\n if (state.linked || !requestContext?.get(NEW_CHANNEL_SESSION_KEY)) return part;\n if (part.type === 'text-delta') {\n state.textStarted = true;\n return part;\n }\n if ((part.type !== 'tool-call' && part.type !== 'step-finish') || !state.textStarted) return part;\n state.linked = true;\n const link = sessionLinkFromRequest(requestContext);\n if (!link) return part;\n state[REPROCESS_PART_KEY] = part;\n return {\n type: 'text-delta',\n runId: part.runId,\n from: part.from,\n payload: { id: LINK_PART_ID, text: linkSuffix(link) },\n };\n }\n\n processOutputStep({ messageList, requestContext }: ProcessOutputStepArgs) {\n if (!requestContext?.get(NEW_CHANNEL_SESSION_KEY)) return messageList;\n const link = sessionLinkFromRequest(requestContext);\n if (!link) return messageList;\n const suffix = linkSuffix(link);\n for (const message of messageList.get.response.db()) {\n if (message.role !== 'assistant') continue;\n const index = message.content.parts.findIndex(part => part.type === 'text' && part.text === suffix);\n if (index === -1) continue;\n message.content.parts.splice(index, 1);\n if (typeof message.content.content === 'string' && message.content.content.endsWith(suffix)) {\n message.content.content = message.content.content.slice(0, -suffix.length);\n }\n return messageList;\n }\n return messageList;\n }\n}\n\nfunction linkSuffix(link: string): string {\n // Own paragraph: the link closes the message, it is not part of the last sentence.\n return `\\n\\n[${SESSION_LINK_LABEL}](${link})`;\n}\n\nfunction sessionLinkFromRequest(requestContext: RequestContext): string | undefined {\n const controller = requestContext.get('controller') as AgentControllerRequestContext | undefined;\n if (!controller?.threadId) return undefined;\n return sessionLink({\n threadId: controller.threadId,\n sessionId: controller.sessionId,\n resourceId: controller.resourceId,\n factoryProjectId: readFactorySessionScope(requestContext)?.factoryProjectId,\n });\n}\n"],"mappings":";;;;;;;AAYA,MAAa,0BAA0B;AAEvC,SAAgB,sBAAsB,gBAAsC;CAC1E,eAAe,IAAI,yBAAyB,IAAI;AAClD;;;;;;;AAQA,MAAM,qBAAqB;;AAG3B,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;AA0BrB,IAAa,qCAAb,MAAqE;CACnE,KAAc;CAEd,MAAM,oBAAoB,EAAE,MAAM,OAAO,kBAA+D;EACtG,IAAI,MAAM,UAAU,CAAC,gBAAgB,IAAA,4BAA2B,GAAG,OAAO;EAC1E,IAAI,KAAK,SAAS,cAAc;GAC9B,MAAM,cAAc;GACpB,OAAO;EACT;EACA,IAAK,KAAK,SAAS,eAAe,KAAK,SAAS,iBAAkB,CAAC,MAAM,aAAa,OAAO;EAC7F,MAAM,SAAS;EACf,MAAM,OAAO,uBAAuB,cAAc;EAClD,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,sBAAsB;EAC5B,OAAO;GACL,MAAM;GACN,OAAO,KAAK;GACZ,MAAM,KAAK;GACX,SAAS;IAAE,IAAI;IAAc,MAAM,WAAW,IAAI;GAAE;EACtD;CACF;CAEA,kBAAkB,EAAE,aAAa,kBAAyC;EACxE,IAAI,CAAC,gBAAgB,IAAA,4BAA2B,GAAG,OAAO;EAC1D,MAAM,OAAO,uBAAuB,cAAc;EAClD,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,SAAS,WAAW,IAAI;EAC9B,KAAK,MAAM,WAAW,YAAY,IAAI,SAAS,GAAG,GAAG;GACnD,IAAI,QAAQ,SAAS,aAAa;GAClC,MAAM,QAAQ,QAAQ,QAAQ,MAAM,WAAU,SAAQ,KAAK,SAAS,UAAU,KAAK,SAAS,MAAM;GAClG,IAAI,UAAU,IAAI;GAClB,QAAQ,QAAQ,MAAM,OAAO,OAAO,CAAC;GACrC,IAAI,OAAO,QAAQ,QAAQ,YAAY,YAAY,QAAQ,QAAQ,QAAQ,SAAS,MAAM,GACxF,QAAQ,QAAQ,UAAU,QAAQ,QAAQ,QAAQ,MAAM,GAAG,CAAC,OAAO,MAAM;GAE3E,OAAO;EACT;EACA,OAAO;CACT;AACF;AAEA,SAAS,WAAW,MAAsB;CAExC,OAAO,QAAQ,mBAAmB,IAAI,KAAK;AAC7C;AAEA,SAAS,uBAAuB,gBAAoD;CAClF,MAAM,aAAa,eAAe,IAAI,YAAY;CAClD,IAAI,CAAC,YAAY,UAAU,OAAO,KAAA;CAClC,OAAO,YAAY;EACjB,UAAU,WAAW;EACrB,WAAW,WAAW;EACtB,YAAY,WAAW;EACvB,kBAAkB,wBAAwB,cAAc,CAAC,EAAE;CAC7D,CAAC;AACH"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CredentialTenant } from '@mastra/code-sdk/agents/credential-resolver';
|
|
2
2
|
import type { RequestContext } from '@mastra/core/request-context';
|
|
3
3
|
import type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';
|
|
4
4
|
import type { WorkItemsStorage } from '../storage/domains/work-items/base.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credential-policy.d.ts","sourceRoot":"","sources":["../../src/session/credential-policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"credential-policy.d.ts","sourceRoot":"","sources":["../../src/session/credential-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AAEpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAInE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAIvE,UAAU,8BAA8B;IACtC,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,CAAC;IAClD,aAAa,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAClE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,wBAAwB,CAAC,CAAC;CAC7D;AAED,uGAAuG;AACvG,wBAAsB,8BAA8B,CAClD,cAAc,EAAE,cAAc,EAC9B,IAAI,EAAE,8BAA8B,GACnC,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAEvC;AAED,+FAA+F;AAC/F,wBAAsB,yBAAyB,CAC7C,cAAc,EAAE,cAAc,EAC9B,IAAI,EAAE,8BAA8B,GACnC,OAAO,CAAC;IAAE,MAAM,EAAE,gBAAgB,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,SAAS,CAAC,CAwDrH"}
|
|
@@ -12,18 +12,19 @@ async function resolveFactoryModelPolicy(requestContext, deps) {
|
|
|
12
12
|
const principal = resolveAuthenticatedCredentialTenant(requestContext);
|
|
13
13
|
if (!principal) return void 0;
|
|
14
14
|
const controller = requestContext.get("controller");
|
|
15
|
-
if (!controller?.resourceId) return { tenant: principal };
|
|
16
|
-
const
|
|
15
|
+
if (!controller?.sessionId || !controller.resourceId) return { tenant: principal };
|
|
16
|
+
const { sessionId, resourceId } = controller;
|
|
17
|
+
const sourceSession = await deps.sourceControl.getBySessionId(sessionId);
|
|
17
18
|
const binding = principal.orgId && controller.threadId ? await deps.workItems.findRunBindingByThread({
|
|
18
19
|
orgId: principal.orgId,
|
|
19
|
-
resourceId
|
|
20
|
-
sessionId
|
|
20
|
+
resourceId,
|
|
21
|
+
sessionId,
|
|
21
22
|
threadId: controller.threadId,
|
|
22
23
|
rejectAmbiguous: true
|
|
23
24
|
}) : null;
|
|
24
25
|
if (!sourceSession) {
|
|
25
|
-
if (!binding && (
|
|
26
|
-
const factoryProject = !binding && FACTORY_PROJECT_ID.test(
|
|
26
|
+
if (!binding && (resourceId === principal.userId || resourceId.startsWith("channel:"))) return { tenant: principal };
|
|
27
|
+
const factoryProject = !binding && sessionId === resourceId && FACTORY_PROJECT_ID.test(resourceId) ? await deps.projects.getById({ id: resourceId }) : null;
|
|
27
28
|
if (factoryProject && factoryProject.orgId === principal.orgId) return {
|
|
28
29
|
factoryProjectId: factoryProject.id,
|
|
29
30
|
factoryDefaultModelId: factoryProject.defaultModelId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credential-policy.js","names":[],"sources":["../../src/session/credential-policy.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"credential-policy.js","names":[],"sources":["../../src/session/credential-policy.ts"],"sourcesContent":["import { resolveAuthenticatedCredentialTenant } from '@mastra/code-sdk/agents/credential-resolver';\nimport type { CredentialTenant } from '@mastra/code-sdk/agents/credential-resolver';\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\n\nimport { UUID_PATTERN } from '../routes/contracts.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { SourceControlSessionLookup } from './factory-session.js';\n\nconst FACTORY_PROJECT_ID = new RegExp(UUID_PATTERN);\n\ninterface FactoryModelPolicyDependencies {\n projects: Pick<FactoryProjectsStorage, 'getById'>;\n sourceControl: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<WorkItemsStorage, 'findRunBindingByThread'>;\n}\n\n/** Resolve billing authority from authenticated identity and durable bindings, never session state. */\nexport async function resolveFactoryCredentialTenant(\n requestContext: RequestContext,\n deps: FactoryModelPolicyDependencies,\n): Promise<CredentialTenant | undefined> {\n return (await resolveFactoryModelPolicy(requestContext, deps))?.tenant;\n}\n\n/** Shared invocation and model-picker policy; no client-supplied owner or scope is trusted. */\nexport async function resolveFactoryModelPolicy(\n requestContext: RequestContext,\n deps: FactoryModelPolicyDependencies,\n): Promise<{ tenant: CredentialTenant; factoryProjectId?: string; factoryDefaultModelId?: string | null } | undefined> {\n const principal = resolveAuthenticatedCredentialTenant(requestContext);\n if (!principal) return undefined;\n const controller = requestContext.get('controller') as AgentControllerRequestContext | undefined;\n if (!controller?.sessionId || !controller.resourceId) return { tenant: principal };\n const { sessionId, resourceId } = controller;\n\n const sourceSession = await deps.sourceControl.getBySessionId(sessionId);\n const binding =\n principal.orgId && controller.threadId\n ? await deps.workItems.findRunBindingByThread({\n orgId: principal.orgId,\n resourceId,\n sessionId,\n threadId: controller.threadId,\n rejectAmbiguous: true,\n })\n : null;\n if (!sourceSession) {\n if (!binding && (resourceId === principal.userId || resourceId.startsWith('channel:')))\n return { tenant: principal };\n // Session-less Factory pages address the Factory itself: session and resource are both the\n // project id. A run session under its Factory's resource whose row is gone is not a page.\n const factoryProject =\n !binding && sessionId === resourceId && FACTORY_PROJECT_ID.test(resourceId)\n ? await deps.projects.getById({ id: resourceId })\n : null;\n if (factoryProject && factoryProject.orgId === principal.orgId) {\n return {\n factoryProjectId: factoryProject.id,\n factoryDefaultModelId: factoryProject.defaultModelId,\n tenant: { ...principal, credentialScope: effectiveModelScope(factoryProject.modelScope, 'session') },\n };\n }\n throw new Error('Factory session ownership is unavailable.');\n }\n if (\n sourceSession.orgId !== principal.orgId ||\n (sourceSession.visibility === 'private' && sourceSession.userId !== principal.userId)\n ) {\n throw new Error('Factory session is not available to the current user.');\n }\n if (!sourceSession.factoryProjectId) throw new Error('Factory session ownership is unavailable.');\n const projectId = binding?.factoryProjectId ?? sourceSession.factoryProjectId;\n const project = await deps.projects.getById({ id: projectId });\n if (!project || project.orgId !== sourceSession.orgId) throw new Error('Factory model policy is unavailable.');\n // Work-item threads keep their billing scope after revocation; history never restores run authority.\n return {\n factoryProjectId: project.id,\n factoryDefaultModelId: project.defaultModelId,\n tenant: {\n orgId: project.orgId,\n userId: sourceSession.userId,\n credentialScope: effectiveModelScope(project.modelScope, binding ? 'run' : 'session'),\n },\n };\n}\n"],"mappings":";;;;AAWA,MAAM,qBAAqB,IAAI,OAAO,YAAY;;AASlD,eAAsB,+BACpB,gBACA,MACuC;CACvC,QAAQ,MAAM,0BAA0B,gBAAgB,IAAI,EAAA,EAAI;AAClE;;AAGA,eAAsB,0BACpB,gBACA,MACqH;CACrH,MAAM,YAAY,qCAAqC,cAAc;CACrE,IAAI,CAAC,WAAW,OAAO,KAAA;CACvB,MAAM,aAAa,eAAe,IAAI,YAAY;CAClD,IAAI,CAAC,YAAY,aAAa,CAAC,WAAW,YAAY,OAAO,EAAE,QAAQ,UAAU;CACjF,MAAM,EAAE,WAAW,eAAe;CAElC,MAAM,gBAAgB,MAAM,KAAK,cAAc,eAAe,SAAS;CACvE,MAAM,UACJ,UAAU,SAAS,WAAW,WAC1B,MAAM,KAAK,UAAU,uBAAuB;EAC1C,OAAO,UAAU;EACjB;EACA;EACA,UAAU,WAAW;EACrB,iBAAiB;CACnB,CAAC,IACD;CACN,IAAI,CAAC,eAAe;EAClB,IAAI,CAAC,YAAY,eAAe,UAAU,UAAU,WAAW,WAAW,UAAU,IAClF,OAAO,EAAE,QAAQ,UAAU;EAG7B,MAAM,iBACJ,CAAC,WAAW,cAAc,cAAc,mBAAmB,KAAK,UAAU,IACtE,MAAM,KAAK,SAAS,QAAQ,EAAE,IAAI,WAAW,CAAC,IAC9C;EACN,IAAI,kBAAkB,eAAe,UAAU,UAAU,OACvD,OAAO;GACL,kBAAkB,eAAe;GACjC,uBAAuB,eAAe;GACtC,QAAQ;IAAE,GAAG;IAAW,iBAAiB,oBAAoB,eAAe,YAAY,SAAS;GAAE;EACrG;EAEF,MAAM,IAAI,MAAM,2CAA2C;CAC7D;CACA,IACE,cAAc,UAAU,UAAU,SACjC,cAAc,eAAe,aAAa,cAAc,WAAW,UAAU,QAE9E,MAAM,IAAI,MAAM,uDAAuD;CAEzE,IAAI,CAAC,cAAc,kBAAkB,MAAM,IAAI,MAAM,2CAA2C;CAChG,MAAM,YAAY,SAAS,oBAAoB,cAAc;CAC7D,MAAM,UAAU,MAAM,KAAK,SAAS,QAAQ,EAAE,IAAI,UAAU,CAAC;CAC7D,IAAI,CAAC,WAAW,QAAQ,UAAU,cAAc,OAAO,MAAM,IAAI,MAAM,sCAAsC;CAE7G,OAAO;EACL,kBAAkB,QAAQ;EAC1B,uBAAuB,QAAQ;EAC/B,QAAQ;GACN,OAAO,QAAQ;GACf,QAAQ,cAAc;GACtB,iBAAiB,oBAAoB,QAAQ,YAAY,UAAU,QAAQ,SAAS;EACtF;CACF;AACF"}
|
|
@@ -17,6 +17,7 @@ export interface DefaultModelApplicableSession {
|
|
|
17
17
|
}
|
|
18
18
|
export interface DefaultModelHydrationSession extends DefaultModelApplicableSession {
|
|
19
19
|
readonly identity: {
|
|
20
|
+
getId(): string;
|
|
20
21
|
getResourceId(): string;
|
|
21
22
|
};
|
|
22
23
|
model: DefaultModelApplicableSession['model'] & {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default-model-hydration.d.ts","sourceRoot":"","sources":["../../src/session/default-model-hydration.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAEvE,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE;QAAE,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;IACrD,SAAS,EAAE;QAAE,KAAK,EAAE;YAAE,GAAG,CAAC,IAAI,EAAE;gBAAE,OAAO,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;SAAE,CAAA;KAAE,CAAC;CAC/F;AAED,MAAM,WAAW,4BAA6B,SAAQ,6BAA6B;IACjF,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"default-model-hydration.d.ts","sourceRoot":"","sources":["../../src/session/default-model-hydration.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAEvE,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE;QAAE,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;IACrD,SAAS,EAAE;QAAE,KAAK,EAAE;YAAE,GAAG,CAAC,IAAI,EAAE;gBAAE,OAAO,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;SAAE,CAAA;KAAE,CAAC;CAC/F;AAED,MAAM,WAAW,4BAA6B,SAAQ,6BAA6B;IACjF,QAAQ,CAAC,QAAQ,EAAE;QAAE,KAAK,IAAI,MAAM,CAAC;QAAC,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAChE,KAAK,EAAE,6BAA6B,CAAC,OAAO,CAAC,GAAG;QAAE,GAAG,IAAI,MAAM,CAAC;QAAC,UAAU,CAAC,IAAI,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,CAAC;IAC/G,KAAK,EAAE;QACL,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;QAC3C,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;KACzD,CAAC;IACF,MAAM,EAAE;QACN,KAAK,IAAI,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;QACnC,gBAAgB,CAAC,IAAI,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;KAChE,CAAC;CACH;AAUD,MAAM,WAAW,iCAAiC;IAChD,aAAa,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAClE,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,CAAC;IAClD,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,wBAAwB,CAAC,CAAC;IAC5D,aAAa,EAAE,IAAI,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAC;CAClD;AAED,0FAA0F;AAC1F,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,4BAA4B,EACrC,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,EAAE,iCAAiC,GACvF,OAAO,CAAC,IAAI,CAAC,CA+Ef"}
|
|
@@ -14,6 +14,7 @@ async function applyDefaultModel(session, modelId) {
|
|
|
14
14
|
}
|
|
15
15
|
/** Seed a new chat's scoped default model; a chat with a user message keeps its model. */
|
|
16
16
|
async function hydrateSessionDefaultModel(session, { sourceControl, projects, workItems, modelDefaults }) {
|
|
17
|
+
const sessionId = session.identity.getId();
|
|
17
18
|
const resourceId = session.identity.getResourceId();
|
|
18
19
|
const initialState = session.state.get() ?? {};
|
|
19
20
|
if (initialState.factoryProjectId || typeof session.thread.firstUserMessage !== "function") return;
|
|
@@ -29,7 +30,7 @@ async function hydrateSessionDefaultModel(session, { sourceControl, projects, wo
|
|
|
29
30
|
});
|
|
30
31
|
let identityResolved = false;
|
|
31
32
|
try {
|
|
32
|
-
const sourceSession = await sourceControl.getBySessionId(
|
|
33
|
+
const sourceSession = await sourceControl.getBySessionId(sessionId);
|
|
33
34
|
if (!sourceSession) {
|
|
34
35
|
await clearFactoryIdentity();
|
|
35
36
|
return;
|
|
@@ -43,7 +44,7 @@ async function hydrateSessionDefaultModel(session, { sourceControl, projects, wo
|
|
|
43
44
|
orgId: sourceSession.orgId,
|
|
44
45
|
threadId,
|
|
45
46
|
resourceId,
|
|
46
|
-
sessionId
|
|
47
|
+
sessionId,
|
|
47
48
|
rejectAmbiguous: true
|
|
48
49
|
}) : null;
|
|
49
50
|
const project = await projects.getById({ id: binding?.factoryProjectId ?? sourceSession.factoryProjectId });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default-model-hydration.js","names":[],"sources":["../../src/session/default-model-hydration.ts"],"sourcesContent":["import { primeTenantCredentialsForTenant, resolveTenantProviderDefaultModel } from '../routes/tenant-credentials.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { SourceControlSessionLookup } from './factory-session.js';\n\nexport interface DefaultModelApplicableSession {\n model: { switch(modelId: string): Promise<unknown> };\n subagents: { model: { set(args: { modelId: string; agentType: string }): Promise<unknown> } };\n}\n\nexport interface DefaultModelHydrationSession extends DefaultModelApplicableSession {\n readonly identity: { getResourceId(): string };\n model: DefaultModelApplicableSession['model'] & { get(): string; setDefault(args: { modelId: string }): void };\n state: {\n get(): Record<string, unknown> | undefined;\n set(updates: Record<string, unknown>): Promise<unknown>;\n };\n thread: {\n getId(): string | null | undefined;\n firstUserMessage(args: { threadId: string }): Promise<unknown>;\n };\n}\n\nasync function applyDefaultModel(session: DefaultModelApplicableSession, modelId: string): Promise<void> {\n await session.model.switch(modelId);\n\n for (const agentType of ['explore', 'plan', 'execute']) {\n await session.subagents.model.set({ modelId, agentType });\n }\n}\n\nexport interface DefaultModelHydrationDependencies {\n sourceControl: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n projects: Pick<FactoryProjectsStorage, 'getById'>;\n workItems: Pick<WorkItemsStorage, 'findRunBindingByThread'>;\n modelDefaults: Pick<ModelDefaultsStorage, 'get'>;\n}\n\n/** Seed a new chat's scoped default model; a chat with a user message keeps its model. */\nexport async function hydrateSessionDefaultModel(\n session: DefaultModelHydrationSession,\n { sourceControl, projects, workItems, modelDefaults }: DefaultModelHydrationDependencies,\n): Promise<void> {\n const resourceId = session.identity.getResourceId();\n const initialState = session.state.get() ?? {};\n if (initialState.factoryProjectId || typeof session.thread.firstUserMessage !== 'function') return;\n const hadFactoryIdentity = initialState.factoryModelScope === 'factory' || initialState.factoryModelScope === 'user';\n const clearFactoryIdentity = () =>\n session.state.set({ factoryOrgId: undefined, factoryModelScope: undefined, factoryOwnerUserId: undefined });\n const failClosedWithoutIdentity = () =>\n session.state.set({ factoryModelScope: 'user', factoryOwnerUserId: undefined });\n let identityResolved = false;\n try {\n const sourceSession = await sourceControl.getBySessionId(
|
|
1
|
+
{"version":3,"file":"default-model-hydration.js","names":[],"sources":["../../src/session/default-model-hydration.ts"],"sourcesContent":["import { primeTenantCredentialsForTenant, resolveTenantProviderDefaultModel } from '../routes/tenant-credentials.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { SourceControlSessionLookup } from './factory-session.js';\n\nexport interface DefaultModelApplicableSession {\n model: { switch(modelId: string): Promise<unknown> };\n subagents: { model: { set(args: { modelId: string; agentType: string }): Promise<unknown> } };\n}\n\nexport interface DefaultModelHydrationSession extends DefaultModelApplicableSession {\n readonly identity: { getId(): string; getResourceId(): string };\n model: DefaultModelApplicableSession['model'] & { get(): string; setDefault(args: { modelId: string }): void };\n state: {\n get(): Record<string, unknown> | undefined;\n set(updates: Record<string, unknown>): Promise<unknown>;\n };\n thread: {\n getId(): string | null | undefined;\n firstUserMessage(args: { threadId: string }): Promise<unknown>;\n };\n}\n\nasync function applyDefaultModel(session: DefaultModelApplicableSession, modelId: string): Promise<void> {\n await session.model.switch(modelId);\n\n for (const agentType of ['explore', 'plan', 'execute']) {\n await session.subagents.model.set({ modelId, agentType });\n }\n}\n\nexport interface DefaultModelHydrationDependencies {\n sourceControl: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n projects: Pick<FactoryProjectsStorage, 'getById'>;\n workItems: Pick<WorkItemsStorage, 'findRunBindingByThread'>;\n modelDefaults: Pick<ModelDefaultsStorage, 'get'>;\n}\n\n/** Seed a new chat's scoped default model; a chat with a user message keeps its model. */\nexport async function hydrateSessionDefaultModel(\n session: DefaultModelHydrationSession,\n { sourceControl, projects, workItems, modelDefaults }: DefaultModelHydrationDependencies,\n): Promise<void> {\n const sessionId = session.identity.getId();\n const resourceId = session.identity.getResourceId();\n const initialState = session.state.get() ?? {};\n if (initialState.factoryProjectId || typeof session.thread.firstUserMessage !== 'function') return;\n const hadFactoryIdentity = initialState.factoryModelScope === 'factory' || initialState.factoryModelScope === 'user';\n const clearFactoryIdentity = () =>\n session.state.set({ factoryOrgId: undefined, factoryModelScope: undefined, factoryOwnerUserId: undefined });\n const failClosedWithoutIdentity = () =>\n session.state.set({ factoryModelScope: 'user', factoryOwnerUserId: undefined });\n let identityResolved = false;\n try {\n const sourceSession = await sourceControl.getBySessionId(sessionId);\n if (!sourceSession) {\n await clearFactoryIdentity();\n return;\n }\n if (!sourceSession.factoryProjectId) {\n await clearFactoryIdentity();\n return;\n }\n // Historical thread membership keeps completed work items on Factory billing in hybrid scope.\n const threadId = session.thread.getId();\n const binding = threadId\n ? await workItems.findRunBindingByThread({\n orgId: sourceSession.orgId,\n threadId,\n resourceId,\n sessionId,\n rejectAmbiguous: true,\n })\n : null;\n const project = await projects.getById({ id: binding?.factoryProjectId ?? sourceSession.factoryProjectId });\n if (!project || project.orgId !== sourceSession.orgId) {\n await clearFactoryIdentity();\n return;\n }\n const modelScope = effectiveModelScope(project.modelScope, binding ? 'run' : 'session');\n await session.state.set({\n factoryOrgId: sourceSession.orgId,\n factoryModelScope: modelScope,\n factoryOwnerUserId: sourceSession.userId,\n });\n identityResolved = true;\n await primeTenantCredentialsForTenant({\n orgId: sourceSession.orgId,\n userId: sourceSession.userId,\n credentialScope: modelScope,\n });\n\n if (binding) return;\n if (threadId && (await session.thread.firstUserMessage({ threadId }))) return;\n\n const scopedModelId =\n (modelScope === 'factory'\n ? project.defaultModelId\n : (await modelDefaults.get({ orgId: sourceSession.orgId, userId: sourceSession.userId }))?.modelId) ??\n // No chosen default: a connected provider's default, unless the current model is usable.\n (await resolveTenantProviderDefaultModel(\n { orgId: sourceSession.orgId, userId: sourceSession.userId, credentialScope: modelScope },\n session.model.get() || undefined,\n ).catch(error => {\n console.warn('[Factory default-model hydration] Unable to resolve a provider default model.', error);\n return undefined;\n }));\n if (!scopedModelId) return;\n // A thread the session creates later (the UI binding its conventional\n // thread after a thread-agnostic request opened the session) starts from\n // the session default, not from the thread hydrated here.\n session.model.setDefault({ modelId: scopedModelId });\n await applyDefaultModel(session, scopedModelId);\n } catch (error) {\n if (!identityResolved && !hadFactoryIdentity) {\n try {\n await failClosedWithoutIdentity();\n } catch {}\n }\n console.warn('[Factory default-model hydration] Unable to apply the scoped default model.', error);\n }\n}\n"],"mappings":";;;AAyBA,eAAe,kBAAkB,SAAwC,SAAgC;CACvG,MAAM,QAAQ,MAAM,OAAO,OAAO;CAElC,KAAK,MAAM,aAAa;EAAC;EAAW;EAAQ;CAAS,GACnD,MAAM,QAAQ,UAAU,MAAM,IAAI;EAAE;EAAS;CAAU,CAAC;AAE5D;;AAUA,eAAsB,2BACpB,SACA,EAAE,eAAe,UAAU,WAAW,iBACvB;CACf,MAAM,YAAY,QAAQ,SAAS,MAAM;CACzC,MAAM,aAAa,QAAQ,SAAS,cAAc;CAClD,MAAM,eAAe,QAAQ,MAAM,IAAI,KAAK,CAAC;CAC7C,IAAI,aAAa,oBAAoB,OAAO,QAAQ,OAAO,qBAAqB,YAAY;CAC5F,MAAM,qBAAqB,aAAa,sBAAsB,aAAa,aAAa,sBAAsB;CAC9G,MAAM,6BACJ,QAAQ,MAAM,IAAI;EAAE,cAAc,KAAA;EAAW,mBAAmB,KAAA;EAAW,oBAAoB,KAAA;CAAU,CAAC;CAC5G,MAAM,kCACJ,QAAQ,MAAM,IAAI;EAAE,mBAAmB;EAAQ,oBAAoB,KAAA;CAAU,CAAC;CAChF,IAAI,mBAAmB;CACvB,IAAI;EACF,MAAM,gBAAgB,MAAM,cAAc,eAAe,SAAS;EAClE,IAAI,CAAC,eAAe;GAClB,MAAM,qBAAqB;GAC3B;EACF;EACA,IAAI,CAAC,cAAc,kBAAkB;GACnC,MAAM,qBAAqB;GAC3B;EACF;EAEA,MAAM,WAAW,QAAQ,OAAO,MAAM;EACtC,MAAM,UAAU,WACZ,MAAM,UAAU,uBAAuB;GACrC,OAAO,cAAc;GACrB;GACA;GACA;GACA,iBAAiB;EACnB,CAAC,IACD;EACJ,MAAM,UAAU,MAAM,SAAS,QAAQ,EAAE,IAAI,SAAS,oBAAoB,cAAc,iBAAiB,CAAC;EAC1G,IAAI,CAAC,WAAW,QAAQ,UAAU,cAAc,OAAO;GACrD,MAAM,qBAAqB;GAC3B;EACF;EACA,MAAM,aAAa,oBAAoB,QAAQ,YAAY,UAAU,QAAQ,SAAS;EACtF,MAAM,QAAQ,MAAM,IAAI;GACtB,cAAc,cAAc;GAC5B,mBAAmB;GACnB,oBAAoB,cAAc;EACpC,CAAC;EACD,mBAAmB;EACnB,MAAM,gCAAgC;GACpC,OAAO,cAAc;GACrB,QAAQ,cAAc;GACtB,iBAAiB;EACnB,CAAC;EAED,IAAI,SAAS;EACb,IAAI,YAAa,MAAM,QAAQ,OAAO,iBAAiB,EAAE,SAAS,CAAC,GAAI;EAEvE,MAAM,iBACH,eAAe,YACZ,QAAQ,kBACP,MAAM,cAAc,IAAI;GAAE,OAAO,cAAc;GAAO,QAAQ,cAAc;EAAO,CAAC,EAAA,EAAI,YAE5F,MAAM,kCACL;GAAE,OAAO,cAAc;GAAO,QAAQ,cAAc;GAAQ,iBAAiB;EAAW,GACxF,QAAQ,MAAM,IAAI,KAAK,KAAA,CACzB,CAAC,CAAC,OAAM,UAAS;GACf,QAAQ,KAAK,iFAAiF,KAAK;EAErG,CAAC;EACH,IAAI,CAAC,eAAe;EAIpB,QAAQ,MAAM,WAAW,EAAE,SAAS,cAAc,CAAC;EACnD,MAAM,kBAAkB,SAAS,aAAa;CAChD,SAAS,OAAO;EACd,IAAI,CAAC,oBAAoB,CAAC,oBACxB,IAAI;GACF,MAAM,0BAA0B;EAClC,QAAQ,CAAC;EAEX,QAAQ,KAAK,+EAA+E,KAAK;CACnG;AACF"}
|
|
@@ -60,7 +60,7 @@ function escapeText(value) {
|
|
|
60
60
|
function sessionIdFromContext(requestContext) {
|
|
61
61
|
if (!requestContext || typeof requestContext.get !== "function") return null;
|
|
62
62
|
const controller = requestContext.get("controller");
|
|
63
|
-
return typeof controller?.
|
|
63
|
+
return typeof controller?.sessionId === "string" && controller.sessionId ? controller.sessionId : null;
|
|
64
64
|
}
|
|
65
65
|
function environmentCacheKey(state) {
|
|
66
66
|
return `environment:${createHash("sha256").update(JSON.stringify(state)).digest("hex").slice(0, 16)}`;
|