@mastra/factory 0.13.0-alpha.9 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -10
- package/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +29 -4
- package/dist/auth.js.map +1 -1
- package/dist/boards/define-board.d.ts +36 -2
- package/dist/boards/define-board.d.ts.map +1 -1
- package/dist/boards/define-board.js +58 -1
- package/dist/boards/define-board.js.map +1 -1
- package/dist/boards/index.d.ts +4 -2
- package/dist/boards/index.d.ts.map +1 -1
- package/dist/boards/index.js +2 -1
- package/dist/boards/index.js.map +1 -1
- package/dist/boards/relocate.d.ts +17 -0
- package/dist/boards/relocate.d.ts.map +1 -0
- package/dist/boards/relocate.js +53 -0
- package/dist/boards/relocate.js.map +1 -0
- package/dist/boards/review.d.ts +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +17 -4
- package/dist/boards/review.js.map +1 -1
- package/dist/boards/semantics.d.ts +28 -0
- package/dist/boards/semantics.d.ts.map +1 -0
- package/dist/boards/semantics.js +41 -0
- package/dist/boards/semantics.js.map +1 -0
- package/dist/boards/work-tool-rules.d.ts +8 -0
- package/dist/boards/work-tool-rules.d.ts.map +1 -0
- package/dist/boards/work-tool-rules.js +20 -0
- package/dist/boards/work-tool-rules.js.map +1 -0
- package/dist/boards/work.d.ts +1 -1
- package/dist/boards/work.d.ts.map +1 -1
- package/dist/boards/work.js +13 -0
- package/dist/boards/work.js.map +1 -1
- package/dist/capabilities/intake.d.ts +2 -0
- package/dist/capabilities/intake.d.ts.map +1 -1
- package/dist/factory.d.ts +5 -6
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +41 -12
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +7 -4
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/default-rules.d.ts +2 -2
- package/dist/integrations/github/default-rules.d.ts.map +1 -1
- package/dist/integrations/github/default-rules.js +2 -2
- package/dist/integrations/github/default-rules.js.map +1 -1
- package/dist/integrations/github/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/github/issue-reconciler.js +4 -3
- package/dist/integrations/github/issue-reconciler.js.map +1 -1
- package/dist/integrations/github/rules.d.ts +6 -2
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +96 -15
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +6 -3
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +21 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/issue-reconciler.d.ts +2 -0
- package/dist/integrations/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/issue-reconciler.js +1 -3
- package/dist/integrations/issue-reconciler.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/linear/default-rules.d.ts +2 -2
- package/dist/integrations/linear/default-rules.d.ts.map +1 -1
- package/dist/integrations/linear/default-rules.js +2 -2
- package/dist/integrations/linear/default-rules.js.map +1 -1
- package/dist/integrations/linear/integration.js +1 -0
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/linear/issue-reconciler.js +9 -4
- package/dist/integrations/linear/issue-reconciler.js.map +1 -1
- package/dist/integrations/linear/reconciliation-config.d.ts.map +1 -1
- package/dist/integrations/linear/reconciliation-config.js.map +1 -1
- package/dist/integrations/linear/routes.d.ts.map +1 -1
- package/dist/integrations/linear/routes.js +19 -26
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.d.ts +7 -2
- package/dist/integrations/linear/rules.d.ts.map +1 -1
- package/dist/integrations/linear/rules.js +20 -8
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/api-client.d.ts.map +1 -1
- package/dist/integrations/platform/api-client.js +25 -4
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts +1 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +5 -0
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
- package/dist/integrations/platform/linear/integration.js +5 -2
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/emoji-shortcodes.generated.d.ts +8 -0
- package/dist/integrations/slack/emoji-shortcodes.generated.d.ts.map +1 -0
- package/dist/integrations/slack/emoji-shortcodes.generated.js +1985 -0
- package/dist/integrations/slack/emoji-shortcodes.generated.js.map +1 -0
- package/dist/integrations/slack/emoji.d.ts +3 -0
- package/dist/integrations/slack/emoji.d.ts.map +1 -0
- package/dist/integrations/slack/emoji.js +12 -0
- package/dist/integrations/slack/emoji.js.map +1 -0
- package/dist/integrations/slack/integration.js +1 -1
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +16 -2
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/routes/attention-activity.d.ts +2 -3
- package/dist/routes/attention-activity.d.ts.map +1 -1
- package/dist/routes/attention-activity.js +12 -3
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-parked.d.ts +22 -0
- package/dist/routes/attention-parked.d.ts.map +1 -0
- package/dist/routes/attention-parked.js +136 -0
- package/dist/routes/attention-parked.js.map +1 -0
- package/dist/routes/attention-providers.js +8 -8
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/attention.d.ts +2 -0
- package/dist/routes/attention.d.ts.map +1 -1
- package/dist/routes/attention.js +47 -118
- package/dist/routes/attention.js.map +1 -1
- package/dist/routes/contracts.d.ts +627 -0
- package/dist/routes/contracts.d.ts.map +1 -0
- package/dist/routes/contracts.js +468 -0
- package/dist/routes/contracts.js.map +1 -0
- package/dist/routes/intake.d.ts +18 -0
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +265 -15
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +26 -76
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/supervisor.d.ts +2 -0
- package/dist/routes/supervisor.d.ts.map +1 -1
- package/dist/routes/supervisor.js +7 -6
- package/dist/routes/supervisor.js.map +1 -1
- package/dist/routes/surface.d.ts +7 -4
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +17 -11
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/telemetry.d.ts +13 -0
- package/dist/routes/telemetry.d.ts.map +1 -0
- package/dist/routes/telemetry.js +60 -0
- package/dist/routes/telemetry.js.map +1 -0
- package/dist/routes/work-items.d.ts +3 -3
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +134 -250
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatch-errors.d.ts +2 -2
- package/dist/rules/dispatch-errors.d.ts.map +1 -1
- package/dist/rules/dispatch-errors.js +10 -6
- package/dist/rules/dispatch-errors.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +9 -2
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +76 -23
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.d.ts +4 -5
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +3 -4
- package/dist/rules/processor.d.ts +3 -3
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +24 -31
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.d.ts +1 -3
- package/dist/rules/resolve.d.ts.map +1 -1
- package/dist/rules/resolve.js +1 -4
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +3 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +20 -10
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.d.ts +7 -7
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +21 -25
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +16 -11
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.d.ts +12 -2
- package/dist/rules/validation.d.ts.map +1 -1
- package/dist/rules/validation.js +29 -20
- package/dist/rules/validation.js.map +1 -1
- package/dist/session/live-sessions.d.ts +33 -3
- package/dist/session/live-sessions.d.ts.map +1 -1
- package/dist/session/live-sessions.js +67 -5
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/storage/domains/intake/base.d.ts +54 -1
- package/dist/storage/domains/intake/base.d.ts.map +1 -1
- package/dist/storage/domains/intake/base.js +136 -4
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +22 -7
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +41 -26
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/supervisor/health-worker.d.ts +2 -0
- package/dist/supervisor/health-worker.d.ts.map +1 -1
- package/dist/supervisor/health-worker.js +3 -1
- package/dist/supervisor/health-worker.js.map +1 -1
- package/dist/supervisor/health.d.ts +12 -14
- package/dist/supervisor/health.d.ts.map +1 -1
- package/dist/supervisor/health.js +24 -52
- package/dist/supervisor/health.js.map +1 -1
- package/dist/supervisor/instructions.d.ts +1 -1
- package/dist/supervisor/instructions.d.ts.map +1 -1
- package/dist/supervisor/instructions.js +10 -8
- package/dist/supervisor/instructions.js.map +1 -1
- package/dist/supervisor/read-tools.d.ts +2 -0
- package/dist/supervisor/read-tools.d.ts.map +1 -1
- package/dist/supervisor/read-tools.js +2 -2
- package/dist/supervisor/read-tools.js.map +1 -1
- package/dist/supervisor/write-tools.d.ts.map +1 -1
- package/dist/supervisor/write-tools.js +6 -4
- package/dist/supervisor/write-tools.js.map +1 -1
- package/dist/telemetry-types.d.ts +7 -0
- package/dist/telemetry-types.d.ts.map +1 -0
- package/dist/telemetry-types.js +24 -0
- package/dist/telemetry-types.js.map +1 -0
- package/dist/telemetry.d.ts +9 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +47 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/work-item-branch.d.ts +2 -0
- package/dist/work-item-branch.d.ts.map +1 -1
- package/dist/work-item-branch.js +6 -1
- package/dist/work-item-branch.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +3 -1
- package/dist/workspace.js.map +1 -1
- package/factory-skills/configure-factory-rules/SKILL.md +30 -10
- package/package.json +12 -10
- package/dist/rules/defaults.d.ts +0 -9
- package/dist/rules/defaults.d.ts.map +0 -1
- package/dist/rules/defaults.js +0 -50
- package/dist/rules/defaults.js.map +0 -1
package/README.md
CHANGED
|
@@ -29,6 +29,32 @@ A host application calls `MastraFactory.prepare()`, constructs its `Mastra` inst
|
|
|
29
29
|
|
|
30
30
|
`prepare()` initializes the Factory-owned resources needed before Mastra is constructed. `finalize()` connects those resources to the completed host, including Factory routes, integrations, storage-backed behavior, and agent-controller features. Consumers should keep frontend concerns in `factory-ui` and host-specific environment or deployment wiring in `web` rather than adding them to this package.
|
|
31
31
|
|
|
32
|
+
### Product telemetry
|
|
33
|
+
|
|
34
|
+
Factory records `factory_web_activity` in Mastra's existing PostHog project for users signed in through the default `mastra-studio` auth provider. The browser sends only a known page category and an activity type to the authenticated `/web/telemetry/activity` endpoint. The server adds the verified account and deployment context.
|
|
35
|
+
|
|
36
|
+
| Property | Meaning |
|
|
37
|
+
| ------------------------------------- | -------------------------------------------------------------------------------- |
|
|
38
|
+
| `activity` | `page_view` for a visible screen, or `interaction` for pointer/keyboard activity |
|
|
39
|
+
| `page` | A bounded category such as `work`, `review`, or `settings`; never a URL |
|
|
40
|
+
| `platform_user_id`, `platform_org_id` | Opaque IDs from the authenticated platform account |
|
|
41
|
+
| `platform_project_id` | `MASTRA_PROJECT_ID`, when configured |
|
|
42
|
+
| `platform_hosted` | Whether a nonempty `MASTRA_DEPLOYMENT_ID` is present |
|
|
43
|
+
| `deployment_id`, `platform_region` | `MASTRA_DEPLOYMENT_ID` and `MASTRA_PLATFORM_REGION`, when configured |
|
|
44
|
+
| `schema_version` | `1` |
|
|
45
|
+
|
|
46
|
+
The person ID is `factory:platform:<user ID>`, consistent across local and hosted servers. This does not merge existing CLI or platform analytics profiles. A local server can have a platform project ID and use platform services while `platform_hosted` remains `false`. Non-platform hosting includes both local and other self-hosted servers; this event does not distinguish them.
|
|
47
|
+
|
|
48
|
+
Count unique people per day for visitors, and filter to `activity = interaction` for engaged users. Group by `platform_project_id` for project adoption. Interaction events are limited to once per minute per mounted browser app, and the server caps captures at 60 per minute per account/organization per process. Hidden tabs do not capture activity, and there is no background heartbeat. These are best-effort usage signals, not a record of successful product actions.
|
|
49
|
+
|
|
50
|
+
To disable collection, set this on the Factory Server:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
MASTRA_TELEMETRY_DISABLED=true
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`1`, `true`, and `yes` are accepted, ignoring case and surrounding whitespace. Older servers without the explicit capability do not receive browser telemetry requests. Custom auth providers are outside this initial measurement scope until they have a stable identity namespace. No names, emails, tokens, prompts, input values, raw URLs, session replay, or anonymous browser identity are collected by this event. Account IDs are identifiable account data, not anonymous data.
|
|
57
|
+
|
|
32
58
|
### Board lifecycle rules
|
|
33
59
|
|
|
34
60
|
Installed board definitions exclusively own phase entry and exit handlers. Work and Review are installed automatically with Mastra's preferred defaults; no rule configuration is needed. Custom boards declare source-specific `onEnter` and `onExit` handlers through `defineBoard()`:
|
|
@@ -43,9 +69,10 @@ const releaseBoard = defineBoard({
|
|
|
43
69
|
title: 'Release',
|
|
44
70
|
initialPhase: 'queued',
|
|
45
71
|
phases: {
|
|
46
|
-
queued: { title: 'Queued', next: 'shipped' },
|
|
72
|
+
queued: { title: 'Queued', kind: 'resting', next: 'shipped' },
|
|
47
73
|
shipped: {
|
|
48
74
|
title: 'Shipped',
|
|
75
|
+
kind: 'terminal',
|
|
49
76
|
onEnter: {
|
|
50
77
|
manual: () => ({ type: 'reject', code: 'release_held', reason: 'Release is held.' }),
|
|
51
78
|
},
|
|
@@ -64,15 +91,56 @@ Handlers return one typed decision or `undefined`. Supported sources are `issue`
|
|
|
64
91
|
|
|
65
92
|
**Migration:** Remove former global `rules.work` and `rules.review` configuration. Built-in customization is deferred; there is no built-in override or replacement API. Define custom-board handlers on their phases instead. The web deployment now uses the guarded Work default rather than its former unconditional intake handler, so noncandidate or manual arrivals no longer start merely from entering Intake.
|
|
66
93
|
|
|
67
|
-
|
|
94
|
+
There is no global rules object. Every rule has one owner: boards own lifecycle handlers, transition policy, phase semantics, and tool-result rules; integrations own their event handlers. The runtime only executes rules.
|
|
95
|
+
|
|
96
|
+
### Board tool-result rules
|
|
97
|
+
|
|
98
|
+
A board may react to a tool result produced inside one of its seats. Declare handlers under `tools`, keyed by tool name:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { defineBoard } from '@mastra/factory/boards';
|
|
102
|
+
import type { BoardToolResultRuleHandler } from '@mastra/factory/boards';
|
|
103
|
+
|
|
104
|
+
const shipIt: BoardToolResultRuleHandler = context => {
|
|
105
|
+
if (context.result.status !== 'success' || context.item.stages[0] !== 'queued') return;
|
|
106
|
+
return { type: 'notify', idempotencyKey: `${context.ingress.id}:shipped`, title: 'Release shipped' };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const releaseBoard = defineBoard({
|
|
110
|
+
id: 'release',
|
|
111
|
+
title: 'Release',
|
|
112
|
+
initialPhase: 'queued',
|
|
113
|
+
phases: {
|
|
114
|
+
queued: { title: 'Queued', kind: 'resting', next: 'shipped' },
|
|
115
|
+
shipped: { title: 'Shipped', kind: 'terminal' },
|
|
116
|
+
},
|
|
117
|
+
tools: { ship_it: { onResult: shipIt } },
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The handler receives the bound item, actor, board, tool name, normalized result, and `configVersion`, and returns one decision or `undefined`. Tool names follow the identifier rules for decision roles; `onResult` must be a function and the leaf may contain nothing else. Violations are `BoardDefinitionError`s at definition time.
|
|
122
|
+
|
|
123
|
+
Work declares one rule: `submit_plan`. When a `plan`-seated agent on a Planning card reports a result starting with `Plan approved.`, the card transitions to Execute. Review declares none. Resolution is fail-closed: a tool result on a card whose board is not installed, or whose board does not declare that tool, fires no rule — a custom board inherits nothing from Work even if it reuses Work's phase names.
|
|
124
|
+
|
|
125
|
+
### Config version
|
|
126
|
+
|
|
127
|
+
`configVersion` is an operator-maintained deployment label stamped onto transition audit rows, deferred decisions, reconciler audit, and the session kickoff header (`Config: …`). Nothing branches on it; it exists so an audit row can be traced back to the deployment that produced it. It defaults to `factory-config-v1` and must be a non-empty bounded string. The storage column keeps its shipped name, `rule_set_version`.
|
|
68
128
|
|
|
69
129
|
```typescript
|
|
70
|
-
|
|
130
|
+
new MastraFactory({ storage, configVersion: 'deployment-v2' });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Migration:** The `rules` option, `FactoryRules`, `defaultFactoryRules`, and the `@mastra/factory/rules/defaults` subpath are gone. Passing `rules` throws at construction with a pointer to the replacements.
|
|
71
134
|
|
|
72
|
-
|
|
73
|
-
//
|
|
135
|
+
```typescript
|
|
136
|
+
// before
|
|
137
|
+
new MastraFactory({ storage, rules: defaultFactoryRules({ version: 'v2', overrides: { tools: { my_tool: { onResult } } } }) });
|
|
138
|
+
// after
|
|
139
|
+
new MastraFactory({ storage, configVersion: 'v2', boards: [defineBoard({ ..., tools: { my_tool: { onResult } } })] });
|
|
74
140
|
```
|
|
75
141
|
|
|
142
|
+
Contexts that carried `ruleSetVersion` now carry `configVersion`. Work's `submit_plan` rule cannot be replaced from config; built-in customization remains deferred.
|
|
143
|
+
|
|
76
144
|
### Board transition policy
|
|
77
145
|
|
|
78
146
|
Boards own three separate concerns: topology declares which moves exist, `transitionPolicy` restricts those moves, and lifecycle handlers return entry/exit effects. The transition service uses the policy on the item's persisted, installed board, with no fallback to Work policy.
|
|
@@ -95,8 +163,8 @@ const releaseBoard = defineBoard({
|
|
|
95
163
|
initialPhase: 'approval',
|
|
96
164
|
transitionPolicy: releasePolicy,
|
|
97
165
|
phases: {
|
|
98
|
-
approval: { title: 'Approval', next: 'shipped' },
|
|
99
|
-
shipped: { title: 'Shipped' },
|
|
166
|
+
approval: { title: 'Approval', kind: 'resting', next: 'shipped' },
|
|
167
|
+
shipped: { title: 'Shipped', kind: 'working', role: 'release' },
|
|
100
168
|
},
|
|
101
169
|
});
|
|
102
170
|
// Install through new MastraFactory({ storage, boards: [releaseBoard] }).
|
|
@@ -110,7 +178,161 @@ Policies must be side-effect-free. They run on initial entry, reentry, and same-
|
|
|
110
178
|
|
|
111
179
|
A policy cannot bypass topology, ingress authorization, board ownership, external-author safety, revision checks, decision validation, replay handling, or atomic persistence. Returning `allow` is not an authorization override.
|
|
112
180
|
|
|
113
|
-
**Remaining limitations:**
|
|
181
|
+
**Remaining limitations:** Built-in board replacement and customization remain unsupported.
|
|
182
|
+
|
|
183
|
+
### Board phase semantics
|
|
184
|
+
|
|
185
|
+
Every phase declares what it _is_ with a required `kind`; `defineBoard()` rejects a phase without one.
|
|
186
|
+
|
|
187
|
+
- `resting` — the card is parked. A human move out of a resting phase arms autonomy; a move back into one disarms it. `initialPhase` must be resting: a card cannot arrive already seated or already finished.
|
|
188
|
+
- `working` — an agent seat carries the card. `role` is required (same identifier rules as decision roles) and names the seat a human kickoff opens and the lane a rule-started run leaves rest for. Two working phases may share a role; `phaseForRole` returns the first in declaration order.
|
|
189
|
+
- `terminal` — the card is finished. Entering it releases the sandbox and lets sweeps supersede stale decisions and revoke run bindings. `role` is not allowed.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const releaseBoard = defineBoard({
|
|
193
|
+
id: 'release',
|
|
194
|
+
title: 'Release',
|
|
195
|
+
initialPhase: 'queued',
|
|
196
|
+
phases: {
|
|
197
|
+
queued: { title: 'Queued', kind: 'resting', next: 'shipping' },
|
|
198
|
+
shipping: { title: 'Shipping', kind: 'working', role: 'release', next: 'shipped' },
|
|
199
|
+
shipped: { title: 'Shipped', kind: 'terminal' },
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Work declares `intake` resting; `triage`, `planning`, `execute`, and `review` working with roles `triage`, `plan`, `work`, and `work`; `done` and `canceled` terminal. Review declares `intake` resting, `review` working with role `review`, and `done`/`canceled` terminal. The definition exposes the derived helpers `phaseKind`, `isWorking`, `isTerminal`, `roleForPhase`, and `phaseForRole`.
|
|
205
|
+
|
|
206
|
+
Consent, the external-author guard, kickoff seating, run-start lanes, terminal cleanup, the closed-PR and issue sweeps, and supervisor findings all read the installed board's declarations; nothing name-matches phases. A board that reuses Work's phase names gets exactly what it declared. Persisted `board` is authoritative; rows without one are read as Review for pull requests and Work otherwise.
|
|
207
|
+
|
|
208
|
+
Unknown semantics fail closed. When the board is not installed or the phase is not declared: external-event transitions ask for consent, no sweep or cleanup treats the card as finished, the supervisor neither revokes nor starts a seat for it, and a rule-started run from rest with no lane for its role is rejected.
|
|
209
|
+
|
|
210
|
+
**Migration:** Existing `defineBoard()` calls must add `kind` to every phase and `role` to working phases.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// before
|
|
214
|
+
phases: { queued: { title: 'Queued', next: 'shipped' }, shipped: { title: 'Shipped' } }
|
|
215
|
+
// after
|
|
216
|
+
phases: {
|
|
217
|
+
queued: { title: 'Queued', kind: 'resting', next: 'shipped' },
|
|
218
|
+
shipped: { title: 'Shipped', kind: 'terminal' },
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Remaining limitations:** The `held-waiting` supervisor finding stays Work-specific. Throughput and lead-time metrics still count completions by Work's `done` phase. `factory-ui` still renders the built-in stage and role pipeline. Built-in board replacement and customization remain unsupported.
|
|
223
|
+
|
|
224
|
+
### Execute a custom board
|
|
225
|
+
|
|
226
|
+
Installed custom boards can create linked items, start working roles, transition through bound tools, and react to completed tool results. Board and phase identifiers contain 1–128 letters, digits, underscores, or hyphens and start with a letter or digit. Identifiers are case-sensitive and cannot contain surrounding whitespace.
|
|
227
|
+
|
|
228
|
+
This configuration runs a release rehearsal through `queued → preparing → shipping → shipped`. Add it to an existing Factory host with configured storage, a GitHub integration, a connected project repository, a sandbox, and organization-scoped model credentials. Enable automatic runs for the project. The repository must contain a `release:check` package script; it should validate the release without publishing it.
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { MastraFactory } from '@mastra/factory';
|
|
232
|
+
import type { MastraFactoryConfig } from '@mastra/factory';
|
|
233
|
+
import { defineBoard } from '@mastra/factory/boards';
|
|
234
|
+
import type { BoardPhaseDefinition } from '@mastra/factory/boards';
|
|
235
|
+
|
|
236
|
+
type ReleasePhase = 'queued' | 'preparing' | 'shipping' | 'shipped';
|
|
237
|
+
|
|
238
|
+
const releaseBoard = defineBoard<'release', Record<ReleasePhase, BoardPhaseDefinition<ReleasePhase>>>({
|
|
239
|
+
id: 'release',
|
|
240
|
+
title: 'Release',
|
|
241
|
+
initialPhase: 'queued',
|
|
242
|
+
transitionPolicy: context => {
|
|
243
|
+
if (context.fromStage === 'queued' && context.toStage === 'preparing' && !context.isHumanTransition) {
|
|
244
|
+
return {
|
|
245
|
+
type: 'reject',
|
|
246
|
+
code: 'approval_required',
|
|
247
|
+
reason: 'A person must start the release rehearsal.',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
phases: {
|
|
252
|
+
queued: { title: 'Queued', kind: 'resting', next: 'preparing' },
|
|
253
|
+
preparing: {
|
|
254
|
+
title: 'Preparing',
|
|
255
|
+
kind: 'working',
|
|
256
|
+
role: 'release-preparer',
|
|
257
|
+
next: 'shipping',
|
|
258
|
+
onEnter: {
|
|
259
|
+
issue: context => ({
|
|
260
|
+
type: 'invokeSkill',
|
|
261
|
+
idempotencyKey: `${context.ingress.id}:prepare`,
|
|
262
|
+
role: 'release-preparer',
|
|
263
|
+
prompt:
|
|
264
|
+
'Inspect the release changes. When ready, call factory_transition_work_item with stage shipping and the current expectedRevision from the Factory phase signal.',
|
|
265
|
+
}),
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
shipping: {
|
|
269
|
+
title: 'Shipping',
|
|
270
|
+
kind: 'working',
|
|
271
|
+
role: 'release-publisher',
|
|
272
|
+
next: 'shipped',
|
|
273
|
+
onEnter: {
|
|
274
|
+
issue: context => ({
|
|
275
|
+
type: 'invokeSkill',
|
|
276
|
+
idempotencyKey: `${context.ingress.id}:check`,
|
|
277
|
+
role: 'release-publisher',
|
|
278
|
+
prompt:
|
|
279
|
+
'Run npm run release:check && printf "RELEASE_CHECK_PASSED\\n" with execute_command. This is a rehearsal; do not publish anything.',
|
|
280
|
+
}),
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
shipped: { title: 'Shipped', kind: 'terminal' },
|
|
284
|
+
},
|
|
285
|
+
tools: {
|
|
286
|
+
execute_command: {
|
|
287
|
+
onResult: context => {
|
|
288
|
+
if (
|
|
289
|
+
context.item.stages[0] !== 'shipping' ||
|
|
290
|
+
context.result.status !== 'success' ||
|
|
291
|
+
typeof context.result.value !== 'string' ||
|
|
292
|
+
!context.result.value.trimEnd().endsWith('RELEASE_CHECK_PASSED')
|
|
293
|
+
) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
type: 'transition',
|
|
298
|
+
idempotencyKey: `${context.ingress.id}:checked`,
|
|
299
|
+
board: 'release',
|
|
300
|
+
stage: 'shipped',
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
export function createFactory(config: Omit<MastraFactoryConfig, 'boards' | 'includeDefaultBoards'>) {
|
|
308
|
+
return new MastraFactory({ ...config, boards: [releaseBoard], includeDefaultBoards: true });
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Use the host's normal `prepare()`, `new Mastra(...)`, and `finalize()` sequence. Change the hardcoded `includeDefaultBoards: true` in `createFactory` to `false` to run without Work or Review. Working roles name bindings on the shared Code Agent; they do not register separate agents. Factory has no per-role agent configuration option. The board's kickoff prompts supply the role-specific instructions.
|
|
313
|
+
|
|
314
|
+
Create a card with `POST /web/factory/projects/:id/work-items`, passing `board: 'release'`, a title, and the GitHub issue's `externalSource`. The card starts in `queued`. Then use `POST /web/factory/projects/:id/work-items/:workItemId/transition` with `board: 'release'`, `stage: 'preparing'`, the returned `expectedRevision`, a unique `requestId`, and a `cause`. Send these requests as an authorized user using the host's authentication. The human transition satisfies this board's policy and starts the preparer. Its bound tool advances to `shipping`; the publisher's completed check produces a deferred transition to `shipped`. Terminal entry revokes the binding and releases the sandbox. The Factory UI performs the same operations against installed boards (see below); verify the persisted card's board, phase, and deferred decision status either way.
|
|
315
|
+
|
|
316
|
+
Lifecycle, tool-result, and integration handlers may return `upsertLinkedWorkItem` targeting a different installed board. Linked cards enter that board's declared initial phase before moving to the requested phase. Existing cards cannot be reassigned to another board through either decision type. Targets are validated against their own installed board before acceptance and again before uncommitted deferred effects execute; a phase declared only on another board is not valid. Committed replay retains its recorded result and original `configVersion`.
|
|
317
|
+
|
|
318
|
+
Custom phase signals and persisted tool-result ingestion use the item's installed board. Bound tools re-resolve the live binding, retain revision and topology checks, and reject session reassignment. A custom role named `triage` does not inherit Work's classification requirements. Existing authorization, external-author safeguards, and board-owned policies still apply.
|
|
319
|
+
|
|
320
|
+
### Custom boards in the Factory UI
|
|
321
|
+
|
|
322
|
+
`GET /web/factory/projects/:id/boards` returns the installed board catalog for an authorized project: each board's `id`, `title`, `initialPhase`, declaration-ordered `phases` (with `kind` and working `role`), and transition topology. Handlers, policies, and prompts are never serialized. The Factory UI reads this catalog instead of assuming Work and Review:
|
|
323
|
+
|
|
324
|
+
- Installed boards appear in the sidebar — built-ins first, then custom boards in registry order. Work and Review keep `/factories/:id/work` and `/factories/:id/review`; custom boards open at `/factories/:id/boards/:boardId`. An unknown board ID or a failed catalog request shows an explicit unavailable state rather than falling back to Work.
|
|
325
|
+
- Columns, phase labels, and working/terminal presentation come from the phase definitions. Card creation sends the selected `board`, so the server picks the initial phase. Card menus offer only moves the topology declares; the server still enforces policy, authorization, and revisions.
|
|
326
|
+
- A card's persisted `board` decides which board it belongs to. UI actions cannot move a card to a different board.
|
|
327
|
+
- Settings › Skills groups built-in skills under Work and Review and lists each custom board's declared roles. Custom-board kickoff instructions live in code; there is no skill or per-role agent configuration for them.
|
|
328
|
+
- Not yet board-aware: the Overview funnel and stage-hold metrics (Work-only), search scopes (Work/Review sessions), and the automation toggles that are specific to Work triage and Review. Custom-board cards still count toward in-flight totals and appear in activity and audit logs.
|
|
329
|
+
|
|
330
|
+
### Route intake to custom boards
|
|
331
|
+
|
|
332
|
+
Intake bindings are explicit. A Linear project (Settings › Intake › Linear routing) or GitHub repository is only offered as a candidate feed once it is bound to a board; nothing is materialized just by opening a board. Review only accepts pull requests and is not offered for issue routing.
|
|
333
|
+
|
|
334
|
+
- **Linear:** each project binding selects one installed board. Changing the board moves that source's existing cards to the new board's initial phase, skipping terminal cards and cards with an active session.
|
|
335
|
+
- **GitHub:** Settings › Intake › GitHub routing maps a label to a board per Factory project (`GET`/`PUT /web/intake/label-routes`). Labels match case-insensitively; unrouted issues go to Work. Saving a route relocates matching cards the same way, and `issues.labeled` / `issues.unlabeled` webhooks move a card between its routed board and Work while refreshing its label metadata. Routes apply to every repository linked to the project; label input is free text (no repository label autocomplete yet).
|
|
114
336
|
|
|
115
337
|
### GitHub event rules
|
|
116
338
|
|
|
@@ -143,7 +365,7 @@ const overrides = { github: { issueCommentCreated: { onEvent: null } } };
|
|
|
143
365
|
const github = new PlatformGithubIntegration({ rules: { issueCommentCreated: null } });
|
|
144
366
|
```
|
|
145
367
|
|
|
146
|
-
Board definitions own lifecycle
|
|
368
|
+
Board definitions own lifecycle, transition-policy, phase-semantics, and tool-result rules; nothing is configured globally. `MastraFactory({ configVersion })` supplies the deployment-owned label stamped on audit records and GitHub evaluations; it is not a hash of custom handler code, not ingress identity, and does not change delivery replay semantics. Update `configVersion` when changing handler behavior.
|
|
147
369
|
|
|
148
370
|
Handlers receive the existing typed GitHub context and return one decision or `undefined`. External titles, bodies, and comments remain untrusted data after webhook authentication. Custom handlers must preserve any required actor-permission checks explicitly.
|
|
149
371
|
|
|
@@ -180,7 +402,7 @@ const linear = new PlatformLinearIntegration({ rules: { issueClosed: null } });
|
|
|
180
402
|
|
|
181
403
|
Linear event handlers are configured exclusively on the integration. Fetched issues, platform polling, and issue reconciliation use that instance's handlers. Defaults create intake items for observed open issues and close linked non-terminal Work items as Done or Canceled; closed unlinked issues do not create new items. Custom handlers receive the existing typed Linear context and return one decision or `undefined`. Treat issue titles, descriptions, and other external content as untrusted data.
|
|
182
404
|
|
|
183
|
-
|
|
405
|
+
`MastraFactory({ configVersion })` is the deployment-owned label stamped on Linear evaluations and audit records. Update it when handler behavior changes; it is neither ingress identity nor replay state.
|
|
184
406
|
|
|
185
407
|
### GitHub review commands
|
|
186
408
|
|
package/dist/auth.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface FactoryAuthUser {
|
|
|
41
41
|
* isolated building instances. Absent for personal (no-org) accounts.
|
|
42
42
|
*/
|
|
43
43
|
organizationId?: string;
|
|
44
|
+
/** Organization ids proven by the provider's authenticated membership response. */
|
|
45
|
+
organizationMembershipIds?: string[];
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
48
|
* Tenant identity: the org is the top-level tenant, and each user inside it is
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAWvD,OAAO,KAAK,EAAE,QAAQ,EAAE,mBAAmB,EAAoB,MAAM,qBAAqB,CAAC;AAC3F,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { actorFromAuthUser } from "./storage/domains/comments/actor.js";
|
|
2
|
+
import { isFactoryTelemetryEnabled } from "./telemetry.js";
|
|
2
3
|
import { timedAboveThreshold } from "./timing.js";
|
|
3
4
|
import { MastraAuthWorkos } from "@mastra/auth-workos";
|
|
4
5
|
import { MASTRA_MESSAGE_AUTHOR_KEY } from "@mastra/core/request-context";
|
|
5
6
|
import { isAuthHttpHandler, isCredentialsProvider, isOrganizationsProvider, isSSOProvider, isSessionProvider, registerApiRoute } from "@mastra/core/server";
|
|
7
|
+
import { HTTPException } from "hono/http-exception";
|
|
6
8
|
//#region src/auth.ts
|
|
9
|
+
const ORGANIZATION_ID_HEADER = "X-Mastra-Organization-Id";
|
|
7
10
|
/**
|
|
8
11
|
* Validate that a `returnTo` value is a safe same-site path, to prevent
|
|
9
12
|
* open-redirect attacks. Only absolute local paths (`/foo`) are allowed;
|
|
@@ -136,13 +139,21 @@ function toFactoryAuthUser(result) {
|
|
|
136
139
|
const id = typeof flat.id === "string" ? flat.id : void 0;
|
|
137
140
|
const workosId = typeof flat.workosId === "string" ? flat.workosId : void 0;
|
|
138
141
|
if (!id && !workosId) return null;
|
|
142
|
+
const membershipOrganizationIds = Array.isArray(flat.memberships) ? flat.memberships.flatMap((membership) => {
|
|
143
|
+
if (!membership || typeof membership !== "object") return [];
|
|
144
|
+
const organizationId = membership.organizationId;
|
|
145
|
+
return typeof organizationId === "string" ? [organizationId] : [];
|
|
146
|
+
}) : [];
|
|
147
|
+
const memberOrgIds = Array.isArray(flat.memberOrgIds) ? flat.memberOrgIds.filter((organizationId) => typeof organizationId === "string") : [];
|
|
148
|
+
const organizationMembershipIds = [.../* @__PURE__ */ new Set([...membershipOrganizationIds, ...memberOrgIds])];
|
|
139
149
|
return {
|
|
140
150
|
id,
|
|
141
151
|
workosId,
|
|
142
152
|
email: typeof flat.email === "string" ? flat.email : void 0,
|
|
143
153
|
name: typeof flat.name === "string" ? flat.name : void 0,
|
|
144
154
|
avatarUrl: typeof flat.avatarUrl === "string" ? flat.avatarUrl : void 0,
|
|
145
|
-
organizationId: typeof flat.organizationId === "string" ? flat.organizationId : void 0
|
|
155
|
+
organizationId: typeof flat.organizationId === "string" ? flat.organizationId : void 0,
|
|
156
|
+
organizationMembershipIds
|
|
146
157
|
};
|
|
147
158
|
}
|
|
148
159
|
/**
|
|
@@ -174,6 +185,12 @@ async function ensureUserOrg(provider, user) {
|
|
|
174
185
|
if (orgId) user.organizationId = orgId;
|
|
175
186
|
} catch {}
|
|
176
187
|
}
|
|
188
|
+
function selectRequestedOrganization(user, requestedOrganizationId) {
|
|
189
|
+
if (user.organizationId === requestedOrganizationId) return true;
|
|
190
|
+
if (!user.organizationMembershipIds?.includes(requestedOrganizationId)) return false;
|
|
191
|
+
user.organizationId = requestedOrganizationId;
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
177
194
|
/**
|
|
178
195
|
* `Set-Cookie` values that clear the provider's session cookie(s), from the
|
|
179
196
|
* provider's (possibly partial) `ISessionProvider.getClearSessionHeaders`.
|
|
@@ -244,9 +261,13 @@ async function ensureFactoryAuthUser(provider, c) {
|
|
|
244
261
|
const existing = getFactoryAuthUser(c);
|
|
245
262
|
if (existing) return existing;
|
|
246
263
|
if (!provider) return void 0;
|
|
247
|
-
const
|
|
264
|
+
const token = getBearerToken(c.req.header("Authorization"));
|
|
265
|
+
const user = await authenticateRequest(provider, token, c.req.raw);
|
|
248
266
|
if (!user) return void 0;
|
|
249
|
-
|
|
267
|
+
const requestedOrganizationId = token ? c.req.header(ORGANIZATION_ID_HEADER)?.trim() : void 0;
|
|
268
|
+
if (requestedOrganizationId) {
|
|
269
|
+
if (!selectRequestedOrganization(user, requestedOrganizationId)) throw new HTTPException(403, { message: "organization_forbidden" });
|
|
270
|
+
} else await ensureUserOrg(provider, user);
|
|
250
271
|
c.set(FACTORY_AUTH_USER_KEY, user);
|
|
251
272
|
return user;
|
|
252
273
|
}
|
|
@@ -280,6 +301,7 @@ async function handleAuthMe(provider, c) {
|
|
|
280
301
|
await ensureUserOrg(provider, user);
|
|
281
302
|
return c.json({
|
|
282
303
|
authenticated: true,
|
|
304
|
+
telemetryEnabled: isFactoryTelemetryEnabled(provider.name),
|
|
283
305
|
user: {
|
|
284
306
|
userId: getFactoryAuthUserId(user),
|
|
285
307
|
email: user.email,
|
|
@@ -516,7 +538,10 @@ function createFactoryAuthGate(provider) {
|
|
|
516
538
|
const token = getBearerToken(c.req.header("Authorization"));
|
|
517
539
|
const user = await timedAboveThreshold("auth.gate.authenticate", 1e3, () => authenticateRequest(provider, token, c.req.raw));
|
|
518
540
|
if (user) {
|
|
519
|
-
|
|
541
|
+
const requestedOrganizationId = token ? c.req.header(ORGANIZATION_ID_HEADER)?.trim() : void 0;
|
|
542
|
+
if (requestedOrganizationId) {
|
|
543
|
+
if (!selectRequestedOrganization(user, requestedOrganizationId)) return c.json({ error: "organization_forbidden" }, 403);
|
|
544
|
+
} else await ensureUserOrg(provider, user);
|
|
520
545
|
c.set(FACTORY_AUTH_USER_KEY, user);
|
|
521
546
|
const requestContext = c.get("requestContext");
|
|
522
547
|
requestContext?.set("user", user);
|