@salesforce/sfdx-agent-harness-mastra 0.23.0 → 0.25.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/CHANGELOG.md +11 -0
- package/README.md +31 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/mastra-approval-coordinator.d.ts +132 -27
- package/dist/mastra-approval-coordinator.js +183 -41
- package/dist/mastra-built-in-policies.d.ts +53 -0
- package/dist/mastra-built-in-policies.js +61 -0
- package/dist/mastra-harness-factory.js +9 -0
- package/dist/mastra-harness.d.ts +39 -0
- package/dist/mastra-harness.js +94 -13
- package/dist/mastra-tool-mapper.js +11 -0
- package/dist/skill-bridge.d.ts +32 -0
- package/dist/skill-bridge.js +37 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to `@salesforce/sfdx-agent-harness-mastra` are documented in this file.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.25.0] - 2026-06-29
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **harness-mastra**: consult tool-approval policy resolver at gate sites @W-23090028 ([#622](https://github.com/forcedotcom/agentic-dx/pull/622))
|
|
10
|
+
- **harness-mastra**: stable skill_bridge identity for capability-discovery meta-tools @W-23177289 ([#621](https://github.com/forcedotcom/agentic-dx/pull/621))
|
|
11
|
+
|
|
12
|
+
## [0.24.0] - 2026-06-24
|
|
13
|
+
|
|
14
|
+
### Chores
|
|
15
|
+
- update dependencies and disable Mastra PostHog telemetry @W-23054815 ([#619](https://github.com/forcedotcom/agentic-dx/pull/619))
|
|
16
|
+
|
|
6
17
|
## [0.23.0] - 2026-06-23
|
|
7
18
|
|
|
8
19
|
### Chores
|
package/README.md
CHANGED
|
@@ -44,10 +44,12 @@ Linux and macOS release the handle eagerly; the retry options are no-ops on thos
|
|
|
44
44
|
|
|
45
45
|
### Core
|
|
46
46
|
|
|
47
|
-
| Export
|
|
48
|
-
|
|
|
49
|
-
| `MastraHarnessFactory`
|
|
50
|
-
| `MastraHarnessFactoryConfig`
|
|
47
|
+
| Export | Kind | Description |
|
|
48
|
+
| ------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| `MastraHarnessFactory` | class | `HarnessFactory` that produces a Mastra-backed `AgentHarness` bound to a storage root. |
|
|
50
|
+
| `MastraHarnessFactoryConfig` | interface | Configuration for `MastraHarnessFactory` (`toolApprovalTimeoutMs?`). |
|
|
51
|
+
| `MASTRA_BUILT_IN_TOOL_POLICIES` | const | Frozen `ReadonlyArray<ToolPolicyRule>` the harness feeds the SDK's `resolveToolApprovalPolicy` as the `tiers.harness` slice — `allow` rules for Mastra's built-in `updateWorkingMemory` / `skill` / `skill_search` / `skill_read` tools. |
|
|
52
|
+
| `MASTRA_BUILT_IN_TOOL_NAMES` | const | The four built-in tool leaf names `MASTRA_BUILT_IN_TOOL_POLICIES` covers. |
|
|
51
53
|
|
|
52
54
|
Connectivity (model + provider auth) is supplied by the SDK's `AgentConnectivityResolver`. Use the bundled
|
|
53
55
|
`DefaultAgentConnectivityResolver` for the standard Salesforce LLM Gateway path, or `ApiKeyConnectivityResolver` for
|
|
@@ -191,9 +193,33 @@ for await (const event of result.eventStream) {
|
|
|
191
193
|
}
|
|
192
194
|
```
|
|
193
195
|
|
|
196
|
+
### Tool-approval policy
|
|
197
|
+
|
|
198
|
+
The harness gates harness-executed tools through the SDK's `resolveToolApprovalPolicy` (configure rules via
|
|
199
|
+
`AgentConfig.toolPolicies` / `AgentConfig.defaultToolDecision` — see the SDK README). For each non-consumer tool the
|
|
200
|
+
harness resolves a decision and acts on it:
|
|
201
|
+
|
|
202
|
+
- **`allow`** — the tool runs without surfacing a `tool-approval-request`.
|
|
203
|
+
- **`deny`** — the tool is refused and the model receives a `tool-result(isError=true)` so it can recover; no
|
|
204
|
+
`tool-approval-request` surfaces.
|
|
205
|
+
- **`require-approval`** — a `tool-approval-request` is emitted and the workflow suspends until the consumer settles.
|
|
206
|
+
|
|
207
|
+
Gating is **opt-in**: it engages only when the agent's config sets `toolPolicies` or `defaultToolDecision`, or the
|
|
208
|
+
`chat()` call sets the deprecated `requireToolApproval` or `batchApprovals`. With none configured, tools run without
|
|
209
|
+
gating (no suspension), matching the SDK's "no policy ⇒ no gating" default. The harness adds
|
|
210
|
+
`MASTRA_BUILT_IN_TOOL_POLICIES` (Mastra's `updateWorkingMemory` / `skill` / `skill_search` / `skill_read` built-ins) and
|
|
211
|
+
the SDK's `skill_bridge` meta-tool rules as `allow` defaults, so capability-discovery never prompts. Consumer-executed
|
|
212
|
+
tools (`AgentConfig.tools`) always bypass the gate — their execution is the consumer's responsibility via
|
|
213
|
+
`submitToolResult`. Set `StreamOptions.batchApprovals: true` to surface parallel `require-approval` requests on one
|
|
214
|
+
stream for a batch-approval UI.
|
|
215
|
+
|
|
216
|
+
> `StreamOptions.requireToolApproval` is **deprecated** — superseded by per-tool `AgentConfig.toolPolicies`. It still
|
|
217
|
+
> works for one release: a truthy value engages all-or-nothing gating (every non-consumer tool requires approval), and
|
|
218
|
+
> `'batch'` maps onto `batchApprovals`.
|
|
219
|
+
|
|
194
220
|
### Tool approval timeout
|
|
195
221
|
|
|
196
|
-
When
|
|
222
|
+
When the approval gate is engaged and a tool resolves to `require-approval`, the harness suspends the workflow on each
|
|
197
223
|
`tool-approval-request` and waits for the consumer to call `approveToolCall(...)` / `declineToolCall(...)` /
|
|
198
224
|
`submitToolResult(...)`. To bound how long a pending approval can wait — for example, when a UI process crashes or a
|
|
199
225
|
user walks away — the factory accepts a `toolApprovalTimeoutMs` option. It defaults to `600_000` (10 minutes) and
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { MastraHarnessFactory, type MastraHarnessFactoryConfig } from './mastra-harness-factory.js';
|
|
2
2
|
export type { MastraAgentHarness } from './mastra-harness.js';
|
|
3
|
+
export { MASTRA_BUILT_IN_TOOL_NAMES, MASTRA_BUILT_IN_TOOL_POLICIES } from './mastra-built-in-policies.js';
|
|
3
4
|
export { type MastraAgentConfig, type MastraSkillSearchConfig, type MastraToolSearchConfig, type SkillSearchHandle, type ToolSearchHandle, } from './tool-search.js';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
5
|
export { MastraHarnessFactory } from './mastra-harness-factory.js';
|
|
6
|
+
// Harness-specific built-in tool-approval rules — the `tiers.harness` slice the
|
|
7
|
+
// harness feeds `resolveToolApprovalPolicy`. Exported so consumers and tests can
|
|
8
|
+
// inspect which Mastra built-ins the harness auto-approves.
|
|
9
|
+
export { MASTRA_BUILT_IN_TOOL_NAMES, MASTRA_BUILT_IN_TOOL_POLICIES } from './mastra-built-in-policies.js';
|
|
6
10
|
export {} from './tool-search.js';
|
|
7
11
|
// Mastra drives requests through provider-native pass-through endpoints —
|
|
8
12
|
// Bedrock-Anthropic, direct Anthropic Messages API, OpenAI Responses, and
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Agent } from '@mastra/core/agent';
|
|
2
2
|
import type { RequestContext } from '@mastra/core/request-context';
|
|
3
3
|
import type { ChunkType } from '@mastra/core/stream';
|
|
4
|
-
import { type ChatEvent, type McpToolAnnotations } from '@salesforce/sfdx-agent-sdk';
|
|
4
|
+
import { type ChatEvent, type Decision as PolicyDecision, type McpToolAnnotations, type ToolInvocation } from '@salesforce/sfdx-agent-sdk';
|
|
5
5
|
import { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
|
|
6
6
|
import type { LogBus } from '@salesforce/agentic-common';
|
|
7
7
|
/**
|
|
@@ -36,35 +36,57 @@ export interface MastraApprovalCoordinatorOptions {
|
|
|
36
36
|
requestContext: RequestContext;
|
|
37
37
|
toolApprovalTimeoutMs: number;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
39
|
+
* Per-tool approval-decision callback (Phase 2a). When defined, gating is
|
|
40
|
+
* **active**: the harness passed `requireToolApproval: true` to Mastra so
|
|
41
|
+
* every non-consumer tool suspends with a `tool-call-approval` chunk, and
|
|
42
|
+
* the coordinator consults this callback to decide each one:
|
|
40
43
|
*
|
|
41
|
-
* - `
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `tool-call
|
|
48
|
-
*
|
|
49
|
-
* - `'batch'`: same as serial except `tool-approval-request` is
|
|
50
|
-
* front-loaded off the adapter's `tool-call` events so parallel
|
|
51
|
-
* `tool_use` batches all surface their approval-requests on the
|
|
52
|
-
* single per-turn stream.
|
|
44
|
+
* - `'allow'` → silent approve (no `tool-approval-request` ChatEvent; the
|
|
45
|
+
* pump drives `agent.approveToolCall`).
|
|
46
|
+
* - `'deny'` → silent decline (no ChatEvent; the pump drives
|
|
47
|
+
* `agent.declineToolCall`, which surfaces a `tool-result(isError=true)`
|
|
48
|
+
* to the model so it can recover).
|
|
49
|
+
* - `'require-approval'` → emit a `tool-approval-request` and park
|
|
50
|
+
* (front-loaded off `tool-call` when `batchApprovals`, else on the
|
|
51
|
+
* natural `tool-call-approval` chunk).
|
|
53
52
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
53
|
+
* `undefined` means gating is **off** (the consumer configured no policy
|
|
54
|
+
* and no `requireToolApproval` / `batchApprovals`): the harness does not
|
|
55
|
+
* pass `requireToolApproval`, Mastra suspends nothing, and this callback is
|
|
56
|
+
* never consulted — today's no-gating behavior.
|
|
57
|
+
*
|
|
58
|
+
* The coordinator builds the {@link ToolInvocation} it passes here from the
|
|
59
|
+
* same identity sources it uses for event enrichment
|
|
60
|
+
* (`skillBridgeToolNames` + `lookupMcpToolMetadata`), so the identity that
|
|
61
|
+
* drives the decision can never drift from the one stamped on the emitted
|
|
62
|
+
* ChatEvent. The callback itself closes over `AgentConfig` +
|
|
63
|
+
* `MASTRA_BUILT_IN_TOOL_POLICIES` in the harness.
|
|
64
|
+
*
|
|
65
|
+
* Consumer-executed tools bypass this callback entirely — the structural
|
|
66
|
+
* `requireApproval: false` parker gate is their primary contract; they are
|
|
67
|
+
* silently auto-approved before the resolver is consulted.
|
|
68
|
+
*/
|
|
69
|
+
lookupPolicy?: (invocation: ToolInvocation) => PolicyDecision;
|
|
70
|
+
/**
|
|
71
|
+
* When `true`, `require-approval` decisions are front-loaded off the
|
|
72
|
+
* adapter's `tool-call` events so parallel `tool_use` batches all surface
|
|
73
|
+
* their approval-requests on the single per-turn stream (the consumer
|
|
74
|
+
* iterates to natural park). When `false`, each `require-approval` surfaces
|
|
75
|
+
* on its natural `tool-call-approval` suspension chunk — one at a time.
|
|
76
|
+
*
|
|
77
|
+
* Replaces the serial/batch role of the deprecated
|
|
78
|
+
* `StreamOptions.requireToolApproval` enum (#447). No effect when
|
|
79
|
+
* {@link lookupPolicy} is `undefined` (gating off) or when no tool resolves
|
|
80
|
+
* to `require-approval`.
|
|
58
81
|
*/
|
|
59
|
-
|
|
82
|
+
batchApprovals?: boolean;
|
|
60
83
|
/**
|
|
61
84
|
* Set of tool names registered via `AgentConfig.tools`. The coordinator
|
|
62
85
|
* uses this to route `tool-call-approval` chunks: if the toolName is in
|
|
63
86
|
* the set, auto-approve silently (consumer tools bypass the gate); else
|
|
64
|
-
*
|
|
87
|
+
* consult {@link lookupPolicy}.
|
|
65
88
|
*
|
|
66
|
-
* Empty set when no consumer tools are configured
|
|
67
|
-
* chunk surfaces to the consumer in approval mode.
|
|
89
|
+
* Empty set when no consumer tools are configured.
|
|
68
90
|
*/
|
|
69
91
|
consumerToolNames: ReadonlySet<string>;
|
|
70
92
|
/**
|
|
@@ -74,6 +96,23 @@ export interface MastraApprovalCoordinatorOptions {
|
|
|
74
96
|
* `tool-result` / `tool-approval-request` events.
|
|
75
97
|
*/
|
|
76
98
|
lookupMcpToolMetadata?: MastraMcpToolMetadataLookup;
|
|
99
|
+
/**
|
|
100
|
+
* Capability-discovery meta-tool leaf names that are ACTIVE on this turn
|
|
101
|
+
* (#606). The harness populates this from the live
|
|
102
|
+
* `ToolSearchProcessor` / `SkillSearchProcessor` so it contains
|
|
103
|
+
* `search_tools` / `load_tool` only when tool-search is configured and
|
|
104
|
+
* `search_skills` / `load_skill` only when skill-search is configured.
|
|
105
|
+
*
|
|
106
|
+
* The coordinator stamps `serverName: SKILL_BRIDGE_SERVER_ID` onto
|
|
107
|
+
* `tool-call` / `tool-result` / `tool-approval-request` events for these
|
|
108
|
+
* leaves so the SDK's `mcp:skill_bridge:*` built-in rules auto-approve
|
|
109
|
+
* them, reaching parity with Claude's in-process `skill_bridge` MCP
|
|
110
|
+
* server. Gating on the ACTIVE set (not the leaf name alone) is the
|
|
111
|
+
* security boundary: a consumer-declared tool named `load_skill` with no
|
|
112
|
+
* `skillSearch` configured is never in this set, so it cannot inherit the
|
|
113
|
+
* exemption. Empty set when neither processor is active.
|
|
114
|
+
*/
|
|
115
|
+
skillBridgeToolNames?: ReadonlySet<string>;
|
|
77
116
|
logBus?: LogBus;
|
|
78
117
|
}
|
|
79
118
|
/**
|
|
@@ -82,10 +121,13 @@ export interface MastraApprovalCoordinatorOptions {
|
|
|
82
121
|
* sink, single multi-source pump loop, and single set of teardown paths.
|
|
83
122
|
*
|
|
84
123
|
* Responsibilities:
|
|
85
|
-
* -
|
|
86
|
-
* `
|
|
87
|
-
*
|
|
88
|
-
*
|
|
124
|
+
* - Policy-driven approval gating (Phase 2a): for each non-consumer tool,
|
|
125
|
+
* consult `lookupPolicy` — `allow` silently approves, `deny` silently
|
|
126
|
+
* declines (Mastra synthesizes a `tool-result(isError=true)`),
|
|
127
|
+
* `require-approval` emits a `tool-approval-request` and drives
|
|
128
|
+
* `agent.approveToolCall(...)` / `declineToolCall(...)` when the consumer
|
|
129
|
+
* settles. `batchApprovals` controls whether parallel `require-approval`
|
|
130
|
+
* requests front-load onto one stream (#447).
|
|
89
131
|
* - Consumer-tool plumbing: every `AgentConfig.tools` entry has
|
|
90
132
|
* `requireApproval: false` and a Promise-returning `execute` that parks
|
|
91
133
|
* on the coordinator's `parkConsumerToolCall(...)`. Mastra natively
|
|
@@ -114,14 +156,27 @@ export declare class MastraApprovalCoordinator {
|
|
|
114
156
|
private readonly memory;
|
|
115
157
|
private readonly requestContext;
|
|
116
158
|
private readonly toolApprovalTimeoutMs;
|
|
117
|
-
private readonly
|
|
159
|
+
private readonly lookupPolicy?;
|
|
160
|
+
private readonly batchApprovals;
|
|
118
161
|
private readonly consumerToolNames;
|
|
119
162
|
private readonly lookupMcpToolMetadata?;
|
|
163
|
+
private readonly skillBridgeToolNames;
|
|
120
164
|
private readonly adapter;
|
|
121
165
|
/** Single per-turn sink. Issue #529 contract: one stream per turn. */
|
|
122
166
|
private readonly initial;
|
|
123
167
|
/** Buffered consumer decisions keyed by `toolCallId`. */
|
|
124
168
|
private readonly decisions;
|
|
169
|
+
/**
|
|
170
|
+
* ToolCallIds the policy resolver decided `'deny'` (Phase 2a). The pump
|
|
171
|
+
* drives `agent.declineToolCall` for these, and Mastra emits a native
|
|
172
|
+
* `tool-result` on the resumed stream with `isError: false` + the text
|
|
173
|
+
* "Tool call was not approved by the user". The SDK `Decision` contract
|
|
174
|
+
* requires a denied tool to surface as `tool-result(isError=true)`, so the
|
|
175
|
+
* coordinator re-stamps `isError: true` on the matching result as it routes
|
|
176
|
+
* (see {@link routeEnriched}). Tracked separately from consumer-driven
|
|
177
|
+
* declines, which keep Mastra's native shape.
|
|
178
|
+
*/
|
|
179
|
+
private readonly policyDeniedIds;
|
|
125
180
|
/** ToolCallIds for which a `tool-approval-request` ChatEvent has been emitted. */
|
|
126
181
|
private readonly emittedApprovals;
|
|
127
182
|
/** ToolCallIds the consumer has settled. Rejects double-settle. */
|
|
@@ -352,6 +407,56 @@ export declare class MastraApprovalCoordinator {
|
|
|
352
407
|
*/
|
|
353
408
|
private routeEnriched;
|
|
354
409
|
private maybeEnrich;
|
|
410
|
+
/**
|
|
411
|
+
* Re-stamp `isError: true` on the `tool-result` for a policy-denied tool
|
|
412
|
+
* call (Phase 2a). When the resolver returns `'deny'`, the coordinator
|
|
413
|
+
* drives `agent.declineToolCall`; Mastra emits a native `tool-result` with
|
|
414
|
+
* `isError: false` and the text "Tool call was not approved by the user".
|
|
415
|
+
* The SDK `Decision` contract requires a denied tool to surface as
|
|
416
|
+
* `tool-result(isError=true)` so consumers and the model treat it as a
|
|
417
|
+
* failed call, not a successful one — so we flip the flag exactly once, on
|
|
418
|
+
* the matching result. Consumer-driven declines (not policy denials) are not
|
|
419
|
+
* in `policyDeniedIds` and keep Mastra's native shape.
|
|
420
|
+
*/
|
|
421
|
+
private policyDenyStamp;
|
|
422
|
+
/**
|
|
423
|
+
* Build the {@link ToolInvocation} the policy resolver matches against, and
|
|
424
|
+
* the single source of the `serverName` / `annotations` identity stamped on
|
|
425
|
+
* emitted events ({@link maybeEnrich}). One helper feeds both so the
|
|
426
|
+
* identity that drives the approval decision can never drift from the one
|
|
427
|
+
* the consumer observes on the `tool-call` / `tool-result` /
|
|
428
|
+
* `tool-approval-request` ChatEvent.
|
|
429
|
+
*
|
|
430
|
+
* Three disjoint cases:
|
|
431
|
+
*
|
|
432
|
+
* - **Capability-discovery meta-tool active this turn (#606)** — wins first.
|
|
433
|
+
* Gets the well-known `SKILL_BRIDGE_SERVER_ID` identity so the SDK's
|
|
434
|
+
* `mcp:skill_bridge:*` built-in rules auto-approve it, matching Claude's
|
|
435
|
+
* in-process skill-bridge MCP server. These tools are flat (un-namespaced),
|
|
436
|
+
* so the chunk's `toolName` is already the bare leaf. This branch fires
|
|
437
|
+
* even when the agent has no MCP servers (`lookupMcpToolMetadata` unset).
|
|
438
|
+
* - **Genuinely MCP-discovered tool** — recover the bare un-namespaced leaf
|
|
439
|
+
* the SDK's `mcp` matcher compares against by stripping Mastra's
|
|
440
|
+
* `${serverName}_` namespace prefix.
|
|
441
|
+
* - **Built-in / consumer / workspace tool** — bare name, no `serverName`.
|
|
442
|
+
* Matches `{ type: 'builtin', name }` rules (e.g. the Mastra built-ins in
|
|
443
|
+
* `MASTRA_BUILT_IN_TOOL_POLICIES`).
|
|
444
|
+
*
|
|
445
|
+
* The meta-tool and MCP-catalog identities are disjoint: a real MCP tool is
|
|
446
|
+
* never in `skillBridgeToolNames`, and a meta-tool is never in the catalog.
|
|
447
|
+
*/
|
|
448
|
+
private buildToolInvocation;
|
|
449
|
+
/**
|
|
450
|
+
* Resolve the approval decision for a non-consumer tool via the harness-
|
|
451
|
+
* supplied {@link MastraApprovalCoordinatorOptions.lookupPolicy} callback.
|
|
452
|
+
* Falls back to `'require-approval'` when no policy callback is wired — a
|
|
453
|
+
* defensive default so a future caller that passes `requireToolApproval`
|
|
454
|
+
* without a resolver surfaces the request rather than silently allowing.
|
|
455
|
+
* In practice this fallback is unreachable: gating is active iff
|
|
456
|
+
* `lookupPolicy` is defined, and only then does Mastra suspend non-consumer
|
|
457
|
+
* tools.
|
|
458
|
+
*/
|
|
459
|
+
private policyFor;
|
|
355
460
|
private emitApprovalRequest;
|
|
356
461
|
private armApprovalTimer;
|
|
357
462
|
private cancelTimer;
|
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
import { AgentSDKError, AgentSDKErrorType, } from '@salesforce/sfdx-agent-sdk';
|
|
6
6
|
import { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
|
|
7
7
|
import { MastraEventAdapter } from './mastra-event-adapter.js';
|
|
8
|
+
import { SKILL_BRIDGE_SERVER_ID } from './skill-bridge.js';
|
|
9
|
+
/**
|
|
10
|
+
* True when `serverName` identifies a genuinely MCP-discovered tool, as opposed
|
|
11
|
+
* to the synthetic `skill_bridge` identity stamped onto capability-discovery
|
|
12
|
+
* meta-tools (#606) or `undefined` for built-ins / consumer / workspace tools.
|
|
13
|
+
* Used to scope MCP-progress attribution to real MCP servers.
|
|
14
|
+
*/
|
|
15
|
+
function isRealMcpServer(serverName) {
|
|
16
|
+
return serverName !== undefined && serverName !== SKILL_BRIDGE_SERVER_ID;
|
|
17
|
+
}
|
|
8
18
|
function pickIdWithDecision(pendingIds, decisions) {
|
|
9
19
|
for (const id of pendingIds) {
|
|
10
20
|
if (decisions.has(id))
|
|
@@ -18,10 +28,13 @@ function pickIdWithDecision(pendingIds, decisions) {
|
|
|
18
28
|
* sink, single multi-source pump loop, and single set of teardown paths.
|
|
19
29
|
*
|
|
20
30
|
* Responsibilities:
|
|
21
|
-
* -
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
31
|
+
* - Policy-driven approval gating (Phase 2a): for each non-consumer tool,
|
|
32
|
+
* consult `lookupPolicy` — `allow` silently approves, `deny` silently
|
|
33
|
+
* declines (Mastra synthesizes a `tool-result(isError=true)`),
|
|
34
|
+
* `require-approval` emits a `tool-approval-request` and drives
|
|
35
|
+
* `agent.approveToolCall(...)` / `declineToolCall(...)` when the consumer
|
|
36
|
+
* settles. `batchApprovals` controls whether parallel `require-approval`
|
|
37
|
+
* requests front-load onto one stream (#447).
|
|
25
38
|
* - Consumer-tool plumbing: every `AgentConfig.tools` entry has
|
|
26
39
|
* `requireApproval: false` and a Promise-returning `execute` that parks
|
|
27
40
|
* on the coordinator's `parkConsumerToolCall(...)`. Mastra natively
|
|
@@ -50,14 +63,27 @@ export class MastraApprovalCoordinator {
|
|
|
50
63
|
memory;
|
|
51
64
|
requestContext;
|
|
52
65
|
toolApprovalTimeoutMs;
|
|
53
|
-
|
|
66
|
+
lookupPolicy;
|
|
67
|
+
batchApprovals;
|
|
54
68
|
consumerToolNames;
|
|
55
69
|
lookupMcpToolMetadata;
|
|
70
|
+
skillBridgeToolNames;
|
|
56
71
|
adapter;
|
|
57
72
|
/** Single per-turn sink. Issue #529 contract: one stream per turn. */
|
|
58
73
|
initial = new GenSink();
|
|
59
74
|
/** Buffered consumer decisions keyed by `toolCallId`. */
|
|
60
75
|
decisions = new Map();
|
|
76
|
+
/**
|
|
77
|
+
* ToolCallIds the policy resolver decided `'deny'` (Phase 2a). The pump
|
|
78
|
+
* drives `agent.declineToolCall` for these, and Mastra emits a native
|
|
79
|
+
* `tool-result` on the resumed stream with `isError: false` + the text
|
|
80
|
+
* "Tool call was not approved by the user". The SDK `Decision` contract
|
|
81
|
+
* requires a denied tool to surface as `tool-result(isError=true)`, so the
|
|
82
|
+
* coordinator re-stamps `isError: true` on the matching result as it routes
|
|
83
|
+
* (see {@link routeEnriched}). Tracked separately from consumer-driven
|
|
84
|
+
* declines, which keep Mastra's native shape.
|
|
85
|
+
*/
|
|
86
|
+
policyDeniedIds = new Set();
|
|
61
87
|
/** ToolCallIds for which a `tool-approval-request` ChatEvent has been emitted. */
|
|
62
88
|
emittedApprovals = new Set();
|
|
63
89
|
/** ToolCallIds the consumer has settled. Rejects double-settle. */
|
|
@@ -149,9 +175,11 @@ export class MastraApprovalCoordinator {
|
|
|
149
175
|
this.memory = opts.memory;
|
|
150
176
|
this.requestContext = opts.requestContext;
|
|
151
177
|
this.toolApprovalTimeoutMs = opts.toolApprovalTimeoutMs;
|
|
152
|
-
this.
|
|
178
|
+
this.lookupPolicy = opts.lookupPolicy;
|
|
179
|
+
this.batchApprovals = opts.batchApprovals ?? false;
|
|
153
180
|
this.consumerToolNames = opts.consumerToolNames;
|
|
154
181
|
this.lookupMcpToolMetadata = opts.lookupMcpToolMetadata;
|
|
182
|
+
this.skillBridgeToolNames = opts.skillBridgeToolNames ?? new Set();
|
|
155
183
|
this.adapter = new MastraEventAdapter(opts.logBus);
|
|
156
184
|
}
|
|
157
185
|
// ───────────────────────── lifecycle / sinks ────────────────────────────
|
|
@@ -314,6 +342,10 @@ export class MastraApprovalCoordinator {
|
|
|
314
342
|
this.initial.push({ type: 'finish', finishReason: 'error' });
|
|
315
343
|
this.initial.end();
|
|
316
344
|
this.pendingSuspendIds.clear();
|
|
345
|
+
// The turn is over; any deny re-stamp that never landed (the declined
|
|
346
|
+
// tool-result will never arrive now) is moot. Clear so the set doesn't
|
|
347
|
+
// outlive the turn.
|
|
348
|
+
this.policyDeniedIds.clear();
|
|
317
349
|
this.runTeardownCallbacks();
|
|
318
350
|
}
|
|
319
351
|
// ───────────────────────── settle methods ───────────────────────────────
|
|
@@ -536,13 +568,16 @@ export class MastraApprovalCoordinator {
|
|
|
536
568
|
}
|
|
537
569
|
if (event.type === 'tool-call') {
|
|
538
570
|
this.toolCallArgs.set(event.toolCallId, event.args);
|
|
539
|
-
//
|
|
540
|
-
// non-consumer tools
|
|
541
|
-
// surface their approvals on
|
|
542
|
-
// stream.
|
|
543
|
-
|
|
571
|
+
// Batch mode (#447): front-load the `tool-approval-request`
|
|
572
|
+
// for non-consumer tools whose policy is `require-approval`
|
|
573
|
+
// so parallel tool_use blocks all surface their approvals on
|
|
574
|
+
// the single per-turn stream. `allow` / `deny` need no
|
|
575
|
+
// ChatEvent — they're applied silently at the suspension
|
|
576
|
+
// chunk. Consumer tools always bypass the gate.
|
|
577
|
+
if (this.batchApprovals &&
|
|
544
578
|
!this.consumerToolNames.has(event.toolName) &&
|
|
545
|
-
!this.emittedApprovals.has(event.toolCallId)
|
|
579
|
+
!this.emittedApprovals.has(event.toolCallId) &&
|
|
580
|
+
this.policyFor(event.toolName) === 'require-approval') {
|
|
546
581
|
this.emitApprovalRequest(event.toolCallId, event.toolName, event.args);
|
|
547
582
|
}
|
|
548
583
|
this.routeEnriched(event);
|
|
@@ -567,10 +602,37 @@ export class MastraApprovalCoordinator {
|
|
|
567
602
|
// non-approval-mode consumer-tool flow.
|
|
568
603
|
this.settledByCoordinator.add(tc.toolCallId);
|
|
569
604
|
this.decisions.set(tc.toolCallId, { kind: 'approve' });
|
|
605
|
+
continue;
|
|
570
606
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
607
|
+
// Non-consumer tool: consult the policy resolver (Phase 2a).
|
|
608
|
+
const decision = this.policyFor(tc.toolName);
|
|
609
|
+
if (decision === 'allow') {
|
|
610
|
+
// Silent approve — no `tool-approval-request` surfaces.
|
|
611
|
+
// The pump drives `agent.approveToolCall` from the
|
|
612
|
+
// buffered decision; the tool then executes natively
|
|
613
|
+
// and its `tool-result` flows on the resumed stream.
|
|
614
|
+
this.settledByCoordinator.add(tc.toolCallId);
|
|
615
|
+
this.decisions.set(tc.toolCallId, { kind: 'approve' });
|
|
616
|
+
}
|
|
617
|
+
else if (decision === 'deny') {
|
|
618
|
+
// Silent decline — no `tool-approval-request` surfaces.
|
|
619
|
+
// The pump drives `agent.declineToolCall`; Mastra emits a
|
|
620
|
+
// native `tool-result` on the resumed stream (with
|
|
621
|
+
// `isError: false` + "Tool call was not approved by the
|
|
622
|
+
// user") so the model can recover. We mark the id
|
|
623
|
+
// `policyDeniedIds` so `routeEnriched` re-stamps that
|
|
624
|
+
// result `isError: true` to honor the SDK `Decision`
|
|
625
|
+
// contract. The un-emitted approval is marked
|
|
626
|
+
// coordinator-settled so a (spurious) consumer
|
|
627
|
+
// `approveToolCall` for this id is a no-op rather than a
|
|
628
|
+
// TOOL_CALL_NOT_FOUND throw.
|
|
629
|
+
this.settledByCoordinator.add(tc.toolCallId);
|
|
630
|
+
this.policyDeniedIds.add(tc.toolCallId);
|
|
631
|
+
this.decisions.set(tc.toolCallId, { kind: 'decline' });
|
|
632
|
+
}
|
|
633
|
+
else if (!this.batchApprovals && !this.emittedApprovals.has(tc.toolCallId)) {
|
|
634
|
+
// require-approval, serial: emit on the natural chunk
|
|
635
|
+
// (batch front-loaded it off the prior `tool-call`).
|
|
574
636
|
this.emitApprovalRequest(tc.toolCallId, tc.toolName, tc.args);
|
|
575
637
|
}
|
|
576
638
|
continue;
|
|
@@ -665,57 +727,137 @@ export class MastraApprovalCoordinator {
|
|
|
665
727
|
routeEnriched(event) {
|
|
666
728
|
if (this.aborted || this.disposed)
|
|
667
729
|
return;
|
|
668
|
-
const enriched = this.maybeEnrich(event);
|
|
730
|
+
const enriched = this.policyDenyStamp(this.maybeEnrich(event));
|
|
669
731
|
// Issue #591: track in-flight MCP tool call so progress notifications
|
|
670
732
|
// arriving via `progress.onUpdate(...)` can be attributed to the
|
|
671
|
-
// right `toolCallId`. Set on
|
|
672
|
-
// cleared on the matching tool-result.
|
|
673
|
-
//
|
|
674
|
-
// for
|
|
675
|
-
|
|
733
|
+
// right `toolCallId`. Set on a genuinely MCP-discovered tool-call;
|
|
734
|
+
// cleared on the matching tool-result. Built-ins, consumer-declared
|
|
735
|
+
// tools, and workspace tools carry no `serverName` so this never
|
|
736
|
+
// fires for them.
|
|
737
|
+
//
|
|
738
|
+
// #606: the capability-discovery meta-tools now carry a `serverName`
|
|
739
|
+
// too (the synthetic `skill_bridge` identity), but they are NOT MCP
|
|
740
|
+
// tools and emit no `notifications/progress`. Excluding them keeps a
|
|
741
|
+
// real MCP server's progress notification from being mis-attributed to
|
|
742
|
+
// an interleaved meta-tool call.
|
|
743
|
+
if (enriched.type === 'tool-call' && isRealMcpServer(enriched.serverName)) {
|
|
676
744
|
this.currentMcpToolCallId = enriched.toolCallId;
|
|
677
745
|
this.toolNameForCallId.set(enriched.toolCallId, enriched.toolName);
|
|
678
746
|
}
|
|
679
747
|
else if (enriched.type === 'tool-result' &&
|
|
680
|
-
enriched.serverName
|
|
748
|
+
isRealMcpServer(enriched.serverName) &&
|
|
681
749
|
this.currentMcpToolCallId === enriched.toolCallId) {
|
|
682
750
|
this.currentMcpToolCallId = null;
|
|
683
751
|
}
|
|
684
752
|
this.initial.push(enriched);
|
|
685
753
|
}
|
|
686
754
|
maybeEnrich(event) {
|
|
687
|
-
if (!this.lookupMcpToolMetadata)
|
|
688
|
-
return event;
|
|
689
755
|
if (event.type !== 'tool-call' && event.type !== 'tool-result' && event.type !== 'tool-approval-request') {
|
|
690
756
|
return event;
|
|
691
757
|
}
|
|
692
758
|
const toolName = event.type === 'tool-approval-request' ? event.toolCall.toolName : event.toolName;
|
|
693
|
-
const
|
|
694
|
-
|
|
759
|
+
const invocation = this.buildToolInvocation(toolName);
|
|
760
|
+
// No serverName ⇒ built-in / consumer tool ⇒ nothing to stamp. The
|
|
761
|
+
// event keeps its Mastra-namespaced display `toolName`; enrichment only
|
|
762
|
+
// adds `serverName` / `annotations`.
|
|
763
|
+
if (invocation.serverName === undefined)
|
|
695
764
|
return event;
|
|
765
|
+
const fields = {
|
|
766
|
+
serverName: invocation.serverName,
|
|
767
|
+
...(invocation.annotations ? { annotations: invocation.annotations } : {}),
|
|
768
|
+
};
|
|
696
769
|
if (event.type === 'tool-call') {
|
|
697
|
-
const out = {
|
|
698
|
-
...event,
|
|
699
|
-
serverName: metadata.serverName,
|
|
700
|
-
...(metadata.annotations ? { annotations: metadata.annotations } : {}),
|
|
701
|
-
};
|
|
770
|
+
const out = { ...event, ...fields };
|
|
702
771
|
return out;
|
|
703
772
|
}
|
|
704
773
|
if (event.type === 'tool-result') {
|
|
705
|
-
const out = {
|
|
706
|
-
...event,
|
|
707
|
-
serverName: metadata.serverName,
|
|
708
|
-
...(metadata.annotations ? { annotations: metadata.annotations } : {}),
|
|
709
|
-
};
|
|
774
|
+
const out = { ...event, ...fields };
|
|
710
775
|
return out;
|
|
711
776
|
}
|
|
712
|
-
const out = {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
777
|
+
const out = { ...event, ...fields };
|
|
778
|
+
return out;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Re-stamp `isError: true` on the `tool-result` for a policy-denied tool
|
|
782
|
+
* call (Phase 2a). When the resolver returns `'deny'`, the coordinator
|
|
783
|
+
* drives `agent.declineToolCall`; Mastra emits a native `tool-result` with
|
|
784
|
+
* `isError: false` and the text "Tool call was not approved by the user".
|
|
785
|
+
* The SDK `Decision` contract requires a denied tool to surface as
|
|
786
|
+
* `tool-result(isError=true)` so consumers and the model treat it as a
|
|
787
|
+
* failed call, not a successful one — so we flip the flag exactly once, on
|
|
788
|
+
* the matching result. Consumer-driven declines (not policy denials) are not
|
|
789
|
+
* in `policyDeniedIds` and keep Mastra's native shape.
|
|
790
|
+
*/
|
|
791
|
+
policyDenyStamp(event) {
|
|
792
|
+
if (event.type !== 'tool-result' || !this.policyDeniedIds.has(event.toolCallId)) {
|
|
793
|
+
return event;
|
|
794
|
+
}
|
|
795
|
+
// One-shot: clear so a later same-id result (shouldn't happen, but be
|
|
796
|
+
// safe) isn't mislabeled, and the set doesn't grow unbounded.
|
|
797
|
+
this.policyDeniedIds.delete(event.toolCallId);
|
|
798
|
+
if (event.isError)
|
|
799
|
+
return event;
|
|
800
|
+
const out = { ...event, isError: true };
|
|
717
801
|
return out;
|
|
718
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* Build the {@link ToolInvocation} the policy resolver matches against, and
|
|
805
|
+
* the single source of the `serverName` / `annotations` identity stamped on
|
|
806
|
+
* emitted events ({@link maybeEnrich}). One helper feeds both so the
|
|
807
|
+
* identity that drives the approval decision can never drift from the one
|
|
808
|
+
* the consumer observes on the `tool-call` / `tool-result` /
|
|
809
|
+
* `tool-approval-request` ChatEvent.
|
|
810
|
+
*
|
|
811
|
+
* Three disjoint cases:
|
|
812
|
+
*
|
|
813
|
+
* - **Capability-discovery meta-tool active this turn (#606)** — wins first.
|
|
814
|
+
* Gets the well-known `SKILL_BRIDGE_SERVER_ID` identity so the SDK's
|
|
815
|
+
* `mcp:skill_bridge:*` built-in rules auto-approve it, matching Claude's
|
|
816
|
+
* in-process skill-bridge MCP server. These tools are flat (un-namespaced),
|
|
817
|
+
* so the chunk's `toolName` is already the bare leaf. This branch fires
|
|
818
|
+
* even when the agent has no MCP servers (`lookupMcpToolMetadata` unset).
|
|
819
|
+
* - **Genuinely MCP-discovered tool** — recover the bare un-namespaced leaf
|
|
820
|
+
* the SDK's `mcp` matcher compares against by stripping Mastra's
|
|
821
|
+
* `${serverName}_` namespace prefix.
|
|
822
|
+
* - **Built-in / consumer / workspace tool** — bare name, no `serverName`.
|
|
823
|
+
* Matches `{ type: 'builtin', name }` rules (e.g. the Mastra built-ins in
|
|
824
|
+
* `MASTRA_BUILT_IN_TOOL_POLICIES`).
|
|
825
|
+
*
|
|
826
|
+
* The meta-tool and MCP-catalog identities are disjoint: a real MCP tool is
|
|
827
|
+
* never in `skillBridgeToolNames`, and a meta-tool is never in the catalog.
|
|
828
|
+
*/
|
|
829
|
+
buildToolInvocation(toolName) {
|
|
830
|
+
if (this.skillBridgeToolNames.has(toolName)) {
|
|
831
|
+
return { toolName, serverName: SKILL_BRIDGE_SERVER_ID };
|
|
832
|
+
}
|
|
833
|
+
const mcp = this.lookupMcpToolMetadata?.(toolName);
|
|
834
|
+
if (mcp) {
|
|
835
|
+
const bare = toolName.startsWith(`${mcp.serverName}_`)
|
|
836
|
+
? toolName.slice(mcp.serverName.length + 1)
|
|
837
|
+
: toolName;
|
|
838
|
+
return {
|
|
839
|
+
toolName: bare,
|
|
840
|
+
serverName: mcp.serverName,
|
|
841
|
+
...(mcp.annotations ? { annotations: mcp.annotations } : {}),
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
return { toolName };
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Resolve the approval decision for a non-consumer tool via the harness-
|
|
848
|
+
* supplied {@link MastraApprovalCoordinatorOptions.lookupPolicy} callback.
|
|
849
|
+
* Falls back to `'require-approval'` when no policy callback is wired — a
|
|
850
|
+
* defensive default so a future caller that passes `requireToolApproval`
|
|
851
|
+
* without a resolver surfaces the request rather than silently allowing.
|
|
852
|
+
* In practice this fallback is unreachable: gating is active iff
|
|
853
|
+
* `lookupPolicy` is defined, and only then does Mastra suspend non-consumer
|
|
854
|
+
* tools.
|
|
855
|
+
*/
|
|
856
|
+
policyFor(toolName) {
|
|
857
|
+
if (!this.lookupPolicy)
|
|
858
|
+
return 'require-approval';
|
|
859
|
+
return this.lookupPolicy(this.buildToolInvocation(toolName));
|
|
860
|
+
}
|
|
719
861
|
emitApprovalRequest(toolCallId, toolName, args) {
|
|
720
862
|
const event = {
|
|
721
863
|
type: 'tool-approval-request',
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ToolPolicyRule } from '@salesforce/sfdx-agent-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Leaf names of the Mastra-injected built-in tools this harness auto-approves.
|
|
4
|
+
*
|
|
5
|
+
* These are the tools Mastra's runtime registers itself — distinct from the
|
|
6
|
+
* capability-discovery meta-tools (`search_tools` / `load_tool` /
|
|
7
|
+
* `search_skills` / `load_skill`), which carry the `skill_bridge` MCP identity
|
|
8
|
+
* and are auto-approved by the SDK's `mcp:skill_bridge:*` rules in
|
|
9
|
+
* `BUILT_IN_TOOL_POLICIES` (see `skill-bridge.ts`).
|
|
10
|
+
*
|
|
11
|
+
* - `updateWorkingMemory` — Mastra's working-memory write tool, registered when
|
|
12
|
+
* `workingMemory.enabled` is set (the harness always enables it; see
|
|
13
|
+
* ARCHITECTURE.md → "Memory Strategy"). Gating it would prompt the user every
|
|
14
|
+
* time the agent updates its own scratchpad — pure friction with no security
|
|
15
|
+
* value.
|
|
16
|
+
* - `skill` / `skill_search` / `skill_read` — Mastra's eager `SkillsProcessor`
|
|
17
|
+
* workspace tools, registered when `skills` is set without `skillSearch` (the
|
|
18
|
+
* eager path; see ARCHITECTURE.md → "Skill search"). These read skill bodies
|
|
19
|
+
* the operator already vetted by configuring `skills`; gating them is the same
|
|
20
|
+
* capability-discovery friction the `skill_bridge` meta-tools avoid on the
|
|
21
|
+
* on-demand path.
|
|
22
|
+
*
|
|
23
|
+
* Each name here is the **wire `toolName`** the resolver matches against — the
|
|
24
|
+
* tools-map key Mastra registers the tool under, NOT its `createTool` `id`. The
|
|
25
|
+
* two differ for working memory: Mastra registers
|
|
26
|
+
* `{ updateWorkingMemory: createTool({ id: 'update-working-memory' }) }`, and
|
|
27
|
+
* its own approval-boundary checks key on the camelCase `toolName`
|
|
28
|
+
* (`@mastra/core`: `toolName !== 'updateWorkingMemory'`), so the matcher MUST use
|
|
29
|
+
* `updateWorkingMemory`, never the kebab-case `id`. (For the three skill tools
|
|
30
|
+
* the `id` and `toolName` coincide.) A wrong leaf name here is a silent no-op —
|
|
31
|
+
* the rule never matches and the user gets prompted anyway — so do NOT "correct"
|
|
32
|
+
* these to the `createTool` ids. The drift test in
|
|
33
|
+
* `test/mastra-built-in-policies.test.ts` pins only the shape (all `allow`, all
|
|
34
|
+
* `builtin` matchers), not the names; this caveat is the names' guard rail.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MASTRA_BUILT_IN_TOOL_NAMES: readonly ["updateWorkingMemory", "skill", "skill_search", "skill_read"];
|
|
37
|
+
/**
|
|
38
|
+
* Mastra harness-specific built-in tool-approval rules — the `tiers.harness`
|
|
39
|
+
* slice passed to `resolveToolApprovalPolicy`. Per the SDK's R4 layering
|
|
40
|
+
* invariant, harness-specific built-ins live here, not in the SDK's
|
|
41
|
+
* harness-agnostic `BUILT_IN_TOOL_POLICIES`.
|
|
42
|
+
*
|
|
43
|
+
* Frozen: adding an entry is a soft contract change consumers will have noticed;
|
|
44
|
+
* removing or modifying one is a breaking change. Every entry is an `allow`
|
|
45
|
+
* decision on a `builtin` matcher — Mastra's built-in tools carry no MCP
|
|
46
|
+
* `serverName`, so they match `{ type: 'builtin', name }`.
|
|
47
|
+
*
|
|
48
|
+
* Resolution order places this tier after the SDK built-ins and before consumer
|
|
49
|
+
* `toolPolicies`, so a consumer rule of the same matcher (e.g. an explicit
|
|
50
|
+
* `definePolicy({ skill: 'require-approval' })`) still wins via within-tier
|
|
51
|
+
* last-wins, and any `deny` anywhere wins via cross-tier deny-wins.
|
|
52
|
+
*/
|
|
53
|
+
export declare const MASTRA_BUILT_IN_TOOL_POLICIES: ReadonlyArray<ToolPolicyRule>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Leaf names of the Mastra-injected built-in tools this harness auto-approves.
|
|
7
|
+
*
|
|
8
|
+
* These are the tools Mastra's runtime registers itself — distinct from the
|
|
9
|
+
* capability-discovery meta-tools (`search_tools` / `load_tool` /
|
|
10
|
+
* `search_skills` / `load_skill`), which carry the `skill_bridge` MCP identity
|
|
11
|
+
* and are auto-approved by the SDK's `mcp:skill_bridge:*` rules in
|
|
12
|
+
* `BUILT_IN_TOOL_POLICIES` (see `skill-bridge.ts`).
|
|
13
|
+
*
|
|
14
|
+
* - `updateWorkingMemory` — Mastra's working-memory write tool, registered when
|
|
15
|
+
* `workingMemory.enabled` is set (the harness always enables it; see
|
|
16
|
+
* ARCHITECTURE.md → "Memory Strategy"). Gating it would prompt the user every
|
|
17
|
+
* time the agent updates its own scratchpad — pure friction with no security
|
|
18
|
+
* value.
|
|
19
|
+
* - `skill` / `skill_search` / `skill_read` — Mastra's eager `SkillsProcessor`
|
|
20
|
+
* workspace tools, registered when `skills` is set without `skillSearch` (the
|
|
21
|
+
* eager path; see ARCHITECTURE.md → "Skill search"). These read skill bodies
|
|
22
|
+
* the operator already vetted by configuring `skills`; gating them is the same
|
|
23
|
+
* capability-discovery friction the `skill_bridge` meta-tools avoid on the
|
|
24
|
+
* on-demand path.
|
|
25
|
+
*
|
|
26
|
+
* Each name here is the **wire `toolName`** the resolver matches against — the
|
|
27
|
+
* tools-map key Mastra registers the tool under, NOT its `createTool` `id`. The
|
|
28
|
+
* two differ for working memory: Mastra registers
|
|
29
|
+
* `{ updateWorkingMemory: createTool({ id: 'update-working-memory' }) }`, and
|
|
30
|
+
* its own approval-boundary checks key on the camelCase `toolName`
|
|
31
|
+
* (`@mastra/core`: `toolName !== 'updateWorkingMemory'`), so the matcher MUST use
|
|
32
|
+
* `updateWorkingMemory`, never the kebab-case `id`. (For the three skill tools
|
|
33
|
+
* the `id` and `toolName` coincide.) A wrong leaf name here is a silent no-op —
|
|
34
|
+
* the rule never matches and the user gets prompted anyway — so do NOT "correct"
|
|
35
|
+
* these to the `createTool` ids. The drift test in
|
|
36
|
+
* `test/mastra-built-in-policies.test.ts` pins only the shape (all `allow`, all
|
|
37
|
+
* `builtin` matchers), not the names; this caveat is the names' guard rail.
|
|
38
|
+
*/
|
|
39
|
+
export const MASTRA_BUILT_IN_TOOL_NAMES = ['updateWorkingMemory', 'skill', 'skill_search', 'skill_read'];
|
|
40
|
+
/**
|
|
41
|
+
* Mastra harness-specific built-in tool-approval rules — the `tiers.harness`
|
|
42
|
+
* slice passed to `resolveToolApprovalPolicy`. Per the SDK's R4 layering
|
|
43
|
+
* invariant, harness-specific built-ins live here, not in the SDK's
|
|
44
|
+
* harness-agnostic `BUILT_IN_TOOL_POLICIES`.
|
|
45
|
+
*
|
|
46
|
+
* Frozen: adding an entry is a soft contract change consumers will have noticed;
|
|
47
|
+
* removing or modifying one is a breaking change. Every entry is an `allow`
|
|
48
|
+
* decision on a `builtin` matcher — Mastra's built-in tools carry no MCP
|
|
49
|
+
* `serverName`, so they match `{ type: 'builtin', name }`.
|
|
50
|
+
*
|
|
51
|
+
* Resolution order places this tier after the SDK built-ins and before consumer
|
|
52
|
+
* `toolPolicies`, so a consumer rule of the same matcher (e.g. an explicit
|
|
53
|
+
* `definePolicy({ skill: 'require-approval' })`) still wins via within-tier
|
|
54
|
+
* last-wins, and any `deny` anywhere wins via cross-tier deny-wins.
|
|
55
|
+
*/
|
|
56
|
+
export const MASTRA_BUILT_IN_TOOL_POLICIES = Object.freeze(MASTRA_BUILT_IN_TOOL_NAMES.map((name) => ({
|
|
57
|
+
matcher: { type: 'builtin', name },
|
|
58
|
+
decision: 'allow',
|
|
59
|
+
source: 'built-in',
|
|
60
|
+
})));
|
|
61
|
+
//# sourceMappingURL=mastra-built-in-policies.js.map
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createProxyAwareFetch, resolveProxyDispatcher } from '@salesforce/agentic-common';
|
|
6
6
|
import { MastraHarness } from './mastra-harness.js';
|
|
7
|
+
// `@mastra/core` bundles posthog-node and, by default, sends enterprise telemetry
|
|
8
|
+
// (`ee_feature_used` / `ee_license_check`) to us.posthog.com. We never use Mastra's
|
|
9
|
+
// EE/FGA features, and our own telemetry is an independent in-process bus
|
|
10
|
+
// (HarnessBusOwner) that never touches PostHog — so hard-disable Mastra's channel.
|
|
11
|
+
// The gate (`isEETelemetryEnabled()`) is read lazily inside Mastra's `getClient()` on
|
|
12
|
+
// every capture, so setting the env var at module load — before any Mastra instance
|
|
13
|
+
// exists — suffices. `??=` leaves an explicit operator override (e.g. for debugging)
|
|
14
|
+
// intact.
|
|
15
|
+
process.env['MASTRA_TELEMETRY_DISABLED'] ??= 'true';
|
|
7
16
|
const DEFAULT_TOOL_APPROVAL_TIMEOUT_MS = 600_000;
|
|
8
17
|
/** Mastra-backed factory for creating AgentHarness instances. */
|
|
9
18
|
export class MastraHarnessFactory {
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -66,6 +66,14 @@ interface MastraAgentState {
|
|
|
66
66
|
projectRoot: string;
|
|
67
67
|
/** Mastra Agent instance. */
|
|
68
68
|
agent: Agent;
|
|
69
|
+
/**
|
|
70
|
+
* The harness config applied to this agent (re-set on every `updateAgent`).
|
|
71
|
+
* Phase 2a reads `config.toolPolicies` / `config.defaultToolDecision` to
|
|
72
|
+
* build the per-stream policy resolver. Stored as the full config (not just
|
|
73
|
+
* fragments) so the resolver sees the agent's current policy without the
|
|
74
|
+
* harness threading individual fields through `MastraAgentState`.
|
|
75
|
+
*/
|
|
76
|
+
config: HarnessAgentConfig;
|
|
69
77
|
/**
|
|
70
78
|
* The instructions string passed to the Mastra `Agent`'s constructor —
|
|
71
79
|
* `config.instructions` after rule bodies have been composed in (see
|
|
@@ -405,6 +413,37 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
405
413
|
* the thread's `ActiveRun` entry. See ARCHITECTURE.md → "Tool Approval Flow → Abort
|
|
406
414
|
* during pending approval (#409)".
|
|
407
415
|
*/
|
|
416
|
+
/**
|
|
417
|
+
* Phase 2a: decide whether the approval gate engages this turn and, if so,
|
|
418
|
+
* build the per-tool policy resolver the coordinator consults.
|
|
419
|
+
*
|
|
420
|
+
* Gating is **opt-in**. It engages when the consumer set any of:
|
|
421
|
+
* - `config.toolPolicies` (non-empty),
|
|
422
|
+
* - `config.defaultToolDecision`,
|
|
423
|
+
* - the deprecated `StreamOptions.requireToolApproval` (any non-`false`
|
|
424
|
+
* value — `deprecatedMode` is `'serial'`/`'batch'`/`undefined`), or
|
|
425
|
+
* - `StreamOptions.batchApprovals: true`.
|
|
426
|
+
*
|
|
427
|
+
* When none are set, returns `{ lookupPolicy: undefined }` — the caller
|
|
428
|
+
* omits `requireToolApproval` from Mastra's `stream()` and tools run free
|
|
429
|
+
* (today's behavior).
|
|
430
|
+
*
|
|
431
|
+
* **Deprecation bridge.** When the gate engages *only* because the deprecated
|
|
432
|
+
* `requireToolApproval` was truthy (no `toolPolicies`, no
|
|
433
|
+
* `defaultToolDecision`), the effective default must be `'require-approval'`
|
|
434
|
+
* — legacy all-or-none — not the resolver's `'allow'` default, or the old
|
|
435
|
+
* behavior would silently flip to "auto-approve everything." We apply this
|
|
436
|
+
* by resolving against a shallow-overridden config carrying
|
|
437
|
+
* `defaultToolDecision: 'require-approval'`. Built-in allow-rules
|
|
438
|
+
* (skill_bridge meta-tools, the Mastra built-ins) and any consumer
|
|
439
|
+
* `toolPolicies` still win over the default via within-tier last-wins /
|
|
440
|
+
* cross-tier deny-wins, so the capability-discovery meta-tools stay
|
|
441
|
+
* auto-approved even under legacy gating.
|
|
442
|
+
*
|
|
443
|
+
* `batchApprovals` is the serial-vs-batch emission axis: the explicit
|
|
444
|
+
* `StreamOptions.batchApprovals` OR the legacy `requireToolApproval: 'batch'`.
|
|
445
|
+
*/
|
|
446
|
+
private buildPolicyGate;
|
|
408
447
|
/**
|
|
409
448
|
* Maps an SDK `MessagePart[]` into the single AI SDK v5 user `ModelMessage` that
|
|
410
449
|
* `agent.stream()` accepts.
|
package/dist/mastra-harness.js
CHANGED
|
@@ -11,9 +11,11 @@ import { SkillSearchProcessor, ToolSearchProcessor } from '@mastra/core/processo
|
|
|
11
11
|
import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, RequestContext } from '@mastra/core/request-context';
|
|
12
12
|
import { LibSQLStore } from '@mastra/libsql';
|
|
13
13
|
import { MCPClient } from '@mastra/mcp';
|
|
14
|
-
import { AgentSDKError, AgentSDKErrorType, DEFAULT_MAX_STEPS, McpServerStatus, resolveToolApprovalMode, } from '@salesforce/sfdx-agent-sdk';
|
|
14
|
+
import { AgentSDKError, AgentSDKErrorType, DEFAULT_MAX_STEPS, McpServerStatus, resolveToolApprovalMode, resolveToolApprovalPolicy, } from '@salesforce/sfdx-agent-sdk';
|
|
15
15
|
import { HarnessBusOwner, lowerStreamInput, mcpServerConfigEqual, } from '@salesforce/sfdx-agent-sdk/harness';
|
|
16
16
|
import { MastraApprovalCoordinator } from './mastra-approval-coordinator.js';
|
|
17
|
+
import { MASTRA_BUILT_IN_TOOL_POLICIES } from './mastra-built-in-policies.js';
|
|
18
|
+
import { SKILL_SEARCH_META_TOOL_NAMES, TOOL_SEARCH_META_TOOL_NAMES } from './skill-bridge.js';
|
|
17
19
|
import { mapMastraToMessage, mapMessageToMastra, partToTranscriptText } from './mastra-message-mapper.js';
|
|
18
20
|
import { isStandardSchemaWithJSON, standardSchemaToJSONSchema } from '@mastra/schema-compat';
|
|
19
21
|
import { mapToolDefinitionsToMastraTools } from './mastra-tool-mapper.js';
|
|
@@ -251,6 +253,25 @@ function lookupMcpToolMetadata(mcpServers, namespacedToolName) {
|
|
|
251
253
|
const annotations = best.serverState.tools[namespacedToolName]?.mcp?.annotations;
|
|
252
254
|
return { serverName: best.serverName, ...(annotations ? { annotations } : {}) };
|
|
253
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Build the set of capability-discovery meta-tool leaf names that are ACTIVE
|
|
258
|
+
* for a turn (#606). Tool-search injects `search_tools` / `load_tool`;
|
|
259
|
+
* skill-search injects `search_skills` / `load_skill`. The set drives the
|
|
260
|
+
* coordinator's `skill_bridge` identity stamp — and gating it on the live
|
|
261
|
+
* processors (not the leaf names alone) is what keeps a consumer-declared
|
|
262
|
+
* `load_skill` from inheriting the auto-approval exemption when `skillSearch`
|
|
263
|
+
* is not configured.
|
|
264
|
+
*/
|
|
265
|
+
function activeSkillBridgeToolNames(state) {
|
|
266
|
+
const names = new Set();
|
|
267
|
+
if (state.toolSearchProcessor)
|
|
268
|
+
for (const n of TOOL_SEARCH_META_TOOL_NAMES)
|
|
269
|
+
names.add(n);
|
|
270
|
+
if (state.skillSearchProcessor)
|
|
271
|
+
for (const n of SKILL_SEARCH_META_TOOL_NAMES)
|
|
272
|
+
names.add(n);
|
|
273
|
+
return names;
|
|
274
|
+
}
|
|
254
275
|
/**
|
|
255
276
|
* Projects the namespaced Mastra `ToolAction` map for one MCP server into the
|
|
256
277
|
* SDK's harness-agnostic `McpToolInfo[]` shape.
|
|
@@ -641,6 +662,7 @@ export class MastraHarness {
|
|
|
641
662
|
this.activeAgents.set(agentId, {
|
|
642
663
|
projectRoot,
|
|
643
664
|
agent,
|
|
665
|
+
config,
|
|
644
666
|
composedInstructions,
|
|
645
667
|
validateFiles,
|
|
646
668
|
mcpServers: mcpServerStates,
|
|
@@ -1065,6 +1087,7 @@ export class MastraHarness {
|
|
|
1065
1087
|
const previousWorkspace = state.mastraWorkspace;
|
|
1066
1088
|
const previousSkillSearchProcessor = state.skillSearchProcessor;
|
|
1067
1089
|
state.agent = newAgent;
|
|
1090
|
+
state.config = config;
|
|
1068
1091
|
state.composedInstructions = composedInstructions;
|
|
1069
1092
|
state.validateFiles = validateFiles;
|
|
1070
1093
|
state.mcpConfig = config.mcpServers;
|
|
@@ -1298,6 +1321,51 @@ export class MastraHarness {
|
|
|
1298
1321
|
* the thread's `ActiveRun` entry. See ARCHITECTURE.md → "Tool Approval Flow → Abort
|
|
1299
1322
|
* during pending approval (#409)".
|
|
1300
1323
|
*/
|
|
1324
|
+
/**
|
|
1325
|
+
* Phase 2a: decide whether the approval gate engages this turn and, if so,
|
|
1326
|
+
* build the per-tool policy resolver the coordinator consults.
|
|
1327
|
+
*
|
|
1328
|
+
* Gating is **opt-in**. It engages when the consumer set any of:
|
|
1329
|
+
* - `config.toolPolicies` (non-empty),
|
|
1330
|
+
* - `config.defaultToolDecision`,
|
|
1331
|
+
* - the deprecated `StreamOptions.requireToolApproval` (any non-`false`
|
|
1332
|
+
* value — `deprecatedMode` is `'serial'`/`'batch'`/`undefined`), or
|
|
1333
|
+
* - `StreamOptions.batchApprovals: true`.
|
|
1334
|
+
*
|
|
1335
|
+
* When none are set, returns `{ lookupPolicy: undefined }` — the caller
|
|
1336
|
+
* omits `requireToolApproval` from Mastra's `stream()` and tools run free
|
|
1337
|
+
* (today's behavior).
|
|
1338
|
+
*
|
|
1339
|
+
* **Deprecation bridge.** When the gate engages *only* because the deprecated
|
|
1340
|
+
* `requireToolApproval` was truthy (no `toolPolicies`, no
|
|
1341
|
+
* `defaultToolDecision`), the effective default must be `'require-approval'`
|
|
1342
|
+
* — legacy all-or-none — not the resolver's `'allow'` default, or the old
|
|
1343
|
+
* behavior would silently flip to "auto-approve everything." We apply this
|
|
1344
|
+
* by resolving against a shallow-overridden config carrying
|
|
1345
|
+
* `defaultToolDecision: 'require-approval'`. Built-in allow-rules
|
|
1346
|
+
* (skill_bridge meta-tools, the Mastra built-ins) and any consumer
|
|
1347
|
+
* `toolPolicies` still win over the default via within-tier last-wins /
|
|
1348
|
+
* cross-tier deny-wins, so the capability-discovery meta-tools stay
|
|
1349
|
+
* auto-approved even under legacy gating.
|
|
1350
|
+
*
|
|
1351
|
+
* `batchApprovals` is the serial-vs-batch emission axis: the explicit
|
|
1352
|
+
* `StreamOptions.batchApprovals` OR the legacy `requireToolApproval: 'batch'`.
|
|
1353
|
+
*/
|
|
1354
|
+
buildPolicyGate(agentState, options, deprecatedMode) {
|
|
1355
|
+
const config = agentState.config;
|
|
1356
|
+
const hasPolicy = (config.toolPolicies?.length ?? 0) > 0 || config.defaultToolDecision !== undefined;
|
|
1357
|
+
const legacyGating = deprecatedMode !== undefined;
|
|
1358
|
+
const batchApprovals = options?.batchApprovals === true || deprecatedMode === 'batch';
|
|
1359
|
+
const gatingActive = hasPolicy || legacyGating || batchApprovals;
|
|
1360
|
+
if (!gatingActive) {
|
|
1361
|
+
return { batchApprovals: false };
|
|
1362
|
+
}
|
|
1363
|
+
// Apply the deprecation bridge only when the gate engages purely from
|
|
1364
|
+
// the legacy flag (no explicit policy of any kind).
|
|
1365
|
+
const resolverConfig = !hasPolicy && legacyGating ? { ...config, defaultToolDecision: 'require-approval' } : config;
|
|
1366
|
+
const lookupPolicy = (invocation) => resolveToolApprovalPolicy(invocation, resolverConfig, { harness: MASTRA_BUILT_IN_TOOL_POLICIES }).decision;
|
|
1367
|
+
return { lookupPolicy, batchApprovals };
|
|
1368
|
+
}
|
|
1301
1369
|
/**
|
|
1302
1370
|
* Maps an SDK `MessagePart[]` into the single AI SDK v5 user `ModelMessage` that
|
|
1303
1371
|
* `agent.stream()` accepts.
|
|
@@ -1322,13 +1390,21 @@ export class MastraHarness {
|
|
|
1322
1390
|
}
|
|
1323
1391
|
async stream(agentId, threadId, message, options) {
|
|
1324
1392
|
const agentState = this.getAgentStateOrThrow(agentId);
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
//
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
1331
|
-
|
|
1393
|
+
// Phase 2a: resolve whether the approval gate engages this turn, and if
|
|
1394
|
+
// so, build the per-tool policy resolver the coordinator consults.
|
|
1395
|
+
//
|
|
1396
|
+
// `resolveToolApprovalMode` still validates the deprecated
|
|
1397
|
+
// `requireToolApproval` union (throws on garbage) and tells us whether
|
|
1398
|
+
// the consumer asked for batch UX (`'batch'`). The gate is **opt-in**:
|
|
1399
|
+
// it engages only when the consumer configured `toolPolicies`,
|
|
1400
|
+
// `defaultToolDecision`, the deprecated `requireToolApproval`, or
|
|
1401
|
+
// `batchApprovals`. With none set, behavior is unchanged from today —
|
|
1402
|
+
// no suspension, no resolver, tools run free (the SDK's "no policy ⇒ no
|
|
1403
|
+
// gating" back-compat invariant), avoiding a suspend/resume round-trip
|
|
1404
|
+
// per tool call.
|
|
1405
|
+
const deprecatedMode = resolveToolApprovalMode(options?.requireToolApproval);
|
|
1406
|
+
const { lookupPolicy, batchApprovals } = this.buildPolicyGate(agentState, options, deprecatedMode);
|
|
1407
|
+
const gatingActive = lookupPolicy !== undefined;
|
|
1332
1408
|
// Dispose any coordinator the prior turn on this thread allocated.
|
|
1333
1409
|
// Without this a stale coordinator from the prior turn would still
|
|
1334
1410
|
// route settle calls and accumulate listeners. Mode-flip safe: the
|
|
@@ -1366,10 +1442,13 @@ export class MastraHarness {
|
|
|
1366
1442
|
requestContext: this.buildRequestContext(agentId, threadId),
|
|
1367
1443
|
maxSteps: options?.maxSteps ?? DEFAULT_MAX_STEPS,
|
|
1368
1444
|
abortSignal: options?.abortSignal,
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
|
|
1445
|
+
// Pass `requireToolApproval: true` to Mastra iff the gate is active
|
|
1446
|
+
// so Mastra suspends every non-consumer tool with a
|
|
1447
|
+
// `tool-call-approval` chunk and the coordinator can consult the
|
|
1448
|
+
// policy resolver per tool. When inactive, omit it — Mastra is
|
|
1449
|
+
// sensitive to the field's presence vs. explicit `false`, and
|
|
1450
|
+
// omitting preserves today's no-suspend path.
|
|
1451
|
+
...(gatingActive ? { requireToolApproval: true } : {}),
|
|
1373
1452
|
});
|
|
1374
1453
|
// Issue #529 step 2c: every stream allocates a coordinator,
|
|
1375
1454
|
// regardless of approval mode. The coordinator's pump loop owns
|
|
@@ -1381,9 +1460,11 @@ export class MastraHarness {
|
|
|
1381
1460
|
memory: { thread: threadId, resource: this.agentResourceId(agentId) },
|
|
1382
1461
|
requestContext: this.buildRequestContext(agentId, threadId),
|
|
1383
1462
|
toolApprovalTimeoutMs: this.toolApprovalTimeoutMs,
|
|
1384
|
-
|
|
1463
|
+
...(lookupPolicy ? { lookupPolicy } : {}),
|
|
1464
|
+
batchApprovals,
|
|
1385
1465
|
consumerToolNames: agentState.consumerToolNames,
|
|
1386
1466
|
lookupMcpToolMetadata: (name) => lookupMcpToolMetadata(agentState.mcpServers, name),
|
|
1467
|
+
skillBridgeToolNames: activeSkillBridgeToolNames(agentState),
|
|
1387
1468
|
logBus: this.buses.getLogBus(),
|
|
1388
1469
|
});
|
|
1389
1470
|
agentState.coordinators.set(threadId, coordinator);
|
|
@@ -43,6 +43,17 @@ export function mapToolDefinitionsToMastraTools(tools, parkerLookup) {
|
|
|
43
43
|
// JSDoc. With `requireApproval: false`, Mastra parks the
|
|
44
44
|
// agentic run on the parker Promise without suspending the
|
|
45
45
|
// workflow. The coordinator handles the parker lifecycle.
|
|
46
|
+
//
|
|
47
|
+
// Phase 2a dual-enforcement contract: this `requireApproval: false`
|
|
48
|
+
// is the PRIMARY, structural gate for consumer tools — it is
|
|
49
|
+
// hardcoded, never policy-derived. The coordinator's policy
|
|
50
|
+
// resolver is the SECONDARY gate, and the two must AGREE: the
|
|
51
|
+
// coordinator short-circuits consumer tools (by name, via
|
|
52
|
+
// `consumerToolNames`) to a silent auto-approve BEFORE it consults
|
|
53
|
+
// `resolveToolApprovalPolicy`, so a stray `toolPolicies` rule that
|
|
54
|
+
// happened to match a consumer tool can never flip it to
|
|
55
|
+
// `require-approval` / `deny`. Consumer-tool execution stays the
|
|
56
|
+
// consumer's responsibility via `submitToolResult`.
|
|
46
57
|
requireApproval: false,
|
|
47
58
|
execute: async (_inputData, context) => {
|
|
48
59
|
const toolCallId = readToolCallId(context);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SKILL_BRIDGE_SERVER_ID } from '@salesforce/sfdx-agent-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Stable harness identity for the capability-discovery meta-tools (#606).
|
|
4
|
+
*
|
|
5
|
+
* Mastra's `ToolSearchProcessor` injects `search_tools` / `load_tool` and its
|
|
6
|
+
* `SkillSearchProcessor` injects `search_skills` / `load_skill` as flat tools
|
|
7
|
+
* with no MCP `serverName`. Claude exposes the same four through its in-process
|
|
8
|
+
* `skill_bridge` MCP server, so the SDK's `mcp:skill_bridge:*` rules in
|
|
9
|
+
* `BUILT_IN_TOOL_POLICIES` match there but silently miss on Mastra.
|
|
10
|
+
*
|
|
11
|
+
* The harness closes that gap by stamping `serverName = SKILL_BRIDGE_SERVER_ID`
|
|
12
|
+
* onto these tools at the approval boundary — but ONLY for leaves whose
|
|
13
|
+
* originating processor is actually active on the turn (see
|
|
14
|
+
* {@link MastraApprovalCoordinator}'s `skillBridgeToolNames`). The identity is
|
|
15
|
+
* therefore anchored on `(serverId, leaf)` against tools the harness itself
|
|
16
|
+
* injected, never on the bare leaf name alone. A consumer-declared tool named
|
|
17
|
+
* `load_skill` (with no `skillSearch` configured) never enters the active set,
|
|
18
|
+
* so it cannot inherit the auto-approval exemption — the security hole that
|
|
19
|
+
* sank the host-side bare-leaf workaround in AFV PR #2455.
|
|
20
|
+
*/
|
|
21
|
+
/** Leaf names injected by Mastra's `ToolSearchProcessor` (no `autoLoad`). */
|
|
22
|
+
export declare const TOOL_SEARCH_META_TOOL_NAMES: readonly ["search_tools", "load_tool"];
|
|
23
|
+
/** Leaf names injected by Mastra's `SkillSearchProcessor`. */
|
|
24
|
+
export declare const SKILL_SEARCH_META_TOOL_NAMES: readonly ["search_skills", "load_skill"];
|
|
25
|
+
/**
|
|
26
|
+
* Every capability-discovery meta-tool leaf the harness gives the
|
|
27
|
+
* `skill_bridge` identity. Kept in lockstep with the SDK's `mcp:skill_bridge:*`
|
|
28
|
+
* rules in `BUILT_IN_TOOL_POLICIES` — a drift test in
|
|
29
|
+
* `test/skill-bridge.test.ts` asserts each name here has a matching SDK rule.
|
|
30
|
+
*/
|
|
31
|
+
export declare const SKILL_BRIDGE_META_TOOL_NAMES: readonly ["search_tools", "load_tool", "search_skills", "load_skill"];
|
|
32
|
+
export { SKILL_BRIDGE_SERVER_ID };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import { SKILL_BRIDGE_SERVER_ID } from '@salesforce/sfdx-agent-sdk';
|
|
6
|
+
/**
|
|
7
|
+
* Stable harness identity for the capability-discovery meta-tools (#606).
|
|
8
|
+
*
|
|
9
|
+
* Mastra's `ToolSearchProcessor` injects `search_tools` / `load_tool` and its
|
|
10
|
+
* `SkillSearchProcessor` injects `search_skills` / `load_skill` as flat tools
|
|
11
|
+
* with no MCP `serverName`. Claude exposes the same four through its in-process
|
|
12
|
+
* `skill_bridge` MCP server, so the SDK's `mcp:skill_bridge:*` rules in
|
|
13
|
+
* `BUILT_IN_TOOL_POLICIES` match there but silently miss on Mastra.
|
|
14
|
+
*
|
|
15
|
+
* The harness closes that gap by stamping `serverName = SKILL_BRIDGE_SERVER_ID`
|
|
16
|
+
* onto these tools at the approval boundary — but ONLY for leaves whose
|
|
17
|
+
* originating processor is actually active on the turn (see
|
|
18
|
+
* {@link MastraApprovalCoordinator}'s `skillBridgeToolNames`). The identity is
|
|
19
|
+
* therefore anchored on `(serverId, leaf)` against tools the harness itself
|
|
20
|
+
* injected, never on the bare leaf name alone. A consumer-declared tool named
|
|
21
|
+
* `load_skill` (with no `skillSearch` configured) never enters the active set,
|
|
22
|
+
* so it cannot inherit the auto-approval exemption — the security hole that
|
|
23
|
+
* sank the host-side bare-leaf workaround in AFV PR #2455.
|
|
24
|
+
*/
|
|
25
|
+
/** Leaf names injected by Mastra's `ToolSearchProcessor` (no `autoLoad`). */
|
|
26
|
+
export const TOOL_SEARCH_META_TOOL_NAMES = ['search_tools', 'load_tool'];
|
|
27
|
+
/** Leaf names injected by Mastra's `SkillSearchProcessor`. */
|
|
28
|
+
export const SKILL_SEARCH_META_TOOL_NAMES = ['search_skills', 'load_skill'];
|
|
29
|
+
/**
|
|
30
|
+
* Every capability-discovery meta-tool leaf the harness gives the
|
|
31
|
+
* `skill_bridge` identity. Kept in lockstep with the SDK's `mcp:skill_bridge:*`
|
|
32
|
+
* rules in `BUILT_IN_TOOL_POLICIES` — a drift test in
|
|
33
|
+
* `test/skill-bridge.test.ts` asserts each name here has a matching SDK rule.
|
|
34
|
+
*/
|
|
35
|
+
export const SKILL_BRIDGE_META_TOOL_NAMES = [...TOOL_SEARCH_META_TOOL_NAMES, ...SKILL_SEARCH_META_TOOL_NAMES];
|
|
36
|
+
export { SKILL_BRIDGE_SERVER_ID };
|
|
37
|
+
//# sourceMappingURL=skill-bridge.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,15 +36,15 @@
|
|
|
36
36
|
"@mastra/mcp": "^1.10.0",
|
|
37
37
|
"@mastra/memory": "^1.20.3",
|
|
38
38
|
"@mastra/schema-compat": "^1.2.11",
|
|
39
|
-
"@salesforce/agentic-common": "0.
|
|
39
|
+
"@salesforce/agentic-common": "0.13.0",
|
|
40
40
|
"openai": "^6.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
43
|
+
"@salesforce/sfdx-agent-sdk": "0.26.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@eslint/js": "^10.0.1",
|
|
47
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
47
|
+
"@salesforce/sfdx-agent-sdk": "0.26.0",
|
|
48
48
|
"@types/node": "^22.20.0",
|
|
49
49
|
"@vitest/coverage-istanbul": "^4.1.8",
|
|
50
50
|
"@vitest/eslint-plugin": "^1.6.20",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"vitest": "^4.1.8"
|
|
64
64
|
},
|
|
65
65
|
"engines": {
|
|
66
|
-
"node": ">=22.
|
|
66
|
+
"node": ">=22.22.0"
|
|
67
67
|
},
|
|
68
68
|
"lint-staged": {
|
|
69
69
|
"*.{js,jsx,ts,tsx,json,md}": [
|