@salesforce/sfdx-agent-sdk 0.25.0 → 0.27.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 +13 -0
- package/README.md +103 -36
- package/dist/agent.js +10 -1
- package/dist/chat-session.d.ts +104 -12
- package/dist/chat-session.js +93 -15
- package/dist/harness/harness-config.d.ts +69 -1
- package/dist/harness/harness-config.js +5 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -0
- package/dist/policy-resolver.d.ts +126 -0
- package/dist/policy-resolver.js +175 -0
- package/dist/types/telemetry-events.d.ts +42 -1
- package/dist/types/tools.d.ts +83 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
All notable changes to `@salesforce/sfdx-agent-sdk` are documented in this file.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.27.0] - 2026-06-29
|
|
7
|
+
|
|
8
|
+
### Fixes
|
|
9
|
+
- **harness-claude**: make multi-file skills reachable via load_skill @W-23231661 ([#627](https://github.com/forcedotcom/agentic-dx/pull/627))
|
|
10
|
+
|
|
11
|
+
## [0.26.0] - 2026-06-29
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
- **harness-claude**: consult tool-approval policy resolver at gate sites @W-23090029 ([#624](https://github.com/forcedotcom/agentic-dx/pull/624))
|
|
15
|
+
- **harness-mastra**: consult tool-approval policy resolver at gate sites @W-23090028 ([#622](https://github.com/forcedotcom/agentic-dx/pull/622))
|
|
16
|
+
- **harness-mastra**: stable skill_bridge identity for capability-discovery meta-tools @W-23177289 ([#621](https://github.com/forcedotcom/agentic-dx/pull/621))
|
|
17
|
+
- **agent-sdk**: tool-approval policy resolver, types, and remember persistence @W-23090026 ([#620](https://github.com/forcedotcom/agentic-dx/pull/620))
|
|
18
|
+
|
|
6
19
|
## [0.25.0] - 2026-06-24
|
|
7
20
|
|
|
8
21
|
### Chores
|
package/README.md
CHANGED
|
@@ -159,22 +159,22 @@ keeps unparameterized call sites working.
|
|
|
159
159
|
|
|
160
160
|
A single conversation thread.
|
|
161
161
|
|
|
162
|
-
| Method | Signature | Description
|
|
163
|
-
| ------------------- | ------------------------------------------------------------------------- |
|
|
164
|
-
| `getId` | `() => string` | Session/thread identifier.
|
|
165
|
-
| `chat` | `(message: string, options?: ChatOptions) => Promise<ChatStreamResult>` | Send a message and stream the response. The returned `eventStream` is the single iterator for the entire chat turn.
|
|
166
|
-
| `submitToolResult` | `(toolResult: ToolResultInfo) => Promise<void>` | Return a consumer-executed tool result. Control message on the existing turn — post-resume events flow on the same stream.
|
|
167
|
-
| `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Approve a pending tool call.
|
|
168
|
-
| `declineToolCall` | `(toolCallId: string) => Promise<void>`
|
|
169
|
-
| `getMessageHistory` | `() => Promise<Message[]>` | Retrieve all messages in chronological order.
|
|
170
|
-
| `clearHistory` | `() => Promise<void>` | Delete all messages.
|
|
171
|
-
| `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used.
|
|
172
|
-
| `addContext` | `(message: string \| Message[]) => Promise<void>` | Inject context without triggering an LLM response.
|
|
173
|
-
| `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener.
|
|
174
|
-
| `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener.
|
|
175
|
-
| `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this session.
|
|
176
|
-
| `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this session.
|
|
177
|
-
| `dispose` | `() => void` | Release session-level event resources. Idempotent.
|
|
162
|
+
| Method | Signature | Description |
|
|
163
|
+
| ------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
164
|
+
| `getId` | `() => string` | Session/thread identifier. |
|
|
165
|
+
| `chat` | `(message: string, options?: ChatOptions) => Promise<ChatStreamResult>` | Send a message and stream the response. The returned `eventStream` is the single iterator for the entire chat turn. |
|
|
166
|
+
| `submitToolResult` | `(toolResult: ToolResultInfo) => Promise<void>` | Return a consumer-executed tool result. Control message on the existing turn — post-resume events flow on the same stream. |
|
|
167
|
+
| `approveToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Approve a pending tool call. `{ remember: true }` ("Allow always") appends an `allow` rule to `AgentConfig.toolPolicies` and persists it before settling. Control message on the existing turn. |
|
|
168
|
+
| `declineToolCall` | `(toolCallId: string, options?: { remember?: boolean }) => Promise<void>` | Decline a pending tool call. `{ remember: true }` ("Deny always") appends a `deny` rule and persists it before settling. Control message on the existing turn. |
|
|
169
|
+
| `getMessageHistory` | `() => Promise<Message[]>` | Retrieve all messages in chronological order. |
|
|
170
|
+
| `clearHistory` | `() => Promise<void>` | Delete all messages. |
|
|
171
|
+
| `getContextUsage` | `() => ContextUsage` | Snapshot of how much of the model's context window the most recent turn used. |
|
|
172
|
+
| `addContext` | `(message: string \| Message[]) => Promise<void>` | Inject context without triggering an LLM response. |
|
|
173
|
+
| `subscribe` | `(callback: (event: ChatEvent) => void) => void` | Register a real-time event listener. |
|
|
174
|
+
| `unsubscribe` | `(callback: (event: ChatEvent) => void) => void` | Remove a listener. |
|
|
175
|
+
| `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this session. |
|
|
176
|
+
| `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this session. |
|
|
177
|
+
| `dispose` | `() => void` | Release session-level event resources. Idempotent. |
|
|
178
178
|
|
|
179
179
|
### `ChatStreamResult`
|
|
180
180
|
|
|
@@ -232,30 +232,97 @@ function onApprovalRequest(event: ToolApprovalRequestEvent): Promise<boolean> {
|
|
|
232
232
|
|
|
233
233
|
#### `AgentConfig`
|
|
234
234
|
|
|
235
|
-
| Field
|
|
236
|
-
|
|
|
237
|
-
| `orgAlias?`
|
|
238
|
-
| `modelId?`
|
|
239
|
-
| `name?`
|
|
240
|
-
| `description?`
|
|
241
|
-
| `instructions?`
|
|
242
|
-
| `tools?`
|
|
243
|
-
| `mcpServers?`
|
|
244
|
-
| `skills?`
|
|
245
|
-
| `rules?`
|
|
235
|
+
| Field | Type | Description |
|
|
236
|
+
| ---------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
237
|
+
| `orgAlias?` | `string` | Salesforce org alias or username. Falls back to project/default org. |
|
|
238
|
+
| `modelId?` | `ModelName \| Model` | LLM model selector. Pass a `ModelName` enum value for an in-tree model (e.g. `'llmgateway__OpenAIGPT5'`), or a pre-built `Model` instance to opt into a Bedrock-Anthropic Claude variant the SDK has not yet released — see `createClaudeModel(gatewayId, overrides)` exported from this package. |
|
|
239
|
+
| `name?` | `string` | Human-readable agent name. |
|
|
240
|
+
| `description?` | `string` | Agent purpose description. |
|
|
241
|
+
| `instructions?` | `string` | System instructions for the agent. |
|
|
242
|
+
| `tools?` | `ToolDefinition[]` | Consumer-executed tool schemas. |
|
|
243
|
+
| `mcpServers?` | `MCPConfiguration` | MCP server connections. |
|
|
244
|
+
| `skills?` | `string[]` | Each entry is either an individual skill folder (containing `SKILL.md`) or a parent folder containing skill subfolders. Relative and absolute paths supported; forms can be mixed in the same array. |
|
|
245
|
+
| `rules?` | `string[]` | Each entry is either an individual `.md` rule file or a directory of `.md` rule files (scanned one level deep, alphabetical, non-`.md` skipped). Bodies are composed verbatim into the agent's effective system prompt; YAML frontmatter is optional and stripped if present. Matches Claude Code's `.claude/rules/*.md` convention. |
|
|
246
|
+
| `toolPolicies?` | `ToolPolicyRule[]` | Ordered per-tool approval rules resolved by `resolveToolApprovalPolicy` (cross-tier deny-wins / within-tier last-wins). Author directly or via `definePolicy(...)`. See "Tool Approval Policy" below. Has no effect until a harness wires the resolver (Phase 2); until then gating uses the deprecated `StreamOptions.requireToolApproval`. |
|
|
247
|
+
| `defaultToolDecision?` | `Decision` | Fallback decision when no rule matches. Defaults to `'allow'` (no policy ⇒ no gating). Set to `'require-approval'` for a fail-closed posture (recommended for catalogs with un-annotated MCP servers). |
|
|
246
248
|
|
|
247
249
|
#### `StreamOptions`
|
|
248
250
|
|
|
249
|
-
| Field | Type | Description
|
|
250
|
-
| ---------------------- | ----------------------------- |
|
|
251
|
-
| `abortSignal?` | `AbortSignal` | Abort the streaming operation.
|
|
252
|
-
| `requireToolApproval?` | `boolean \| ToolApprovalMode` |
|
|
253
|
-
| `
|
|
251
|
+
| Field | Type | Description |
|
|
252
|
+
| ---------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
253
|
+
| `abortSignal?` | `AbortSignal` | Abort the streaming operation. |
|
|
254
|
+
| `requireToolApproval?` | `boolean \| ToolApprovalMode` | **Deprecated.** Per-call all-or-nothing gating. Superseded by per-tool policy on `AgentConfig.toolPolicies`; the serial-vs-batch UX axis moved to `batchApprovals`. Kept functional for one release so harness PRs migrate independently. `true` / `'serial'` emits one approval per stream; `'batch'` surfaces parallel approvals on one stream (requires Pattern A iterators). See "Tool Approval Flow" below. |
|
|
255
|
+
| `batchApprovals?` | `boolean` | When `true`, parallel approval-requests within a turn surface on the same stream so the consumer can render a batch approval card (requires Pattern A iterators — collect-all-then-settle). Defaults to `false` (serial). The self-documenting replacement for the UX half of the deprecated `requireToolApproval` enum; the gating half moves to `AgentConfig.toolPolicies`. No effect when no tool in the turn resolves to `'require-approval'`. |
|
|
256
|
+
| `maxSteps?` | `number` | Maximum number of LLM call steps the agent may take per `stream()` invocation. Each step is one LLM call (which may produce text, tool calls, or both). Must be `>= 1`. Defaults to `DEFAULT_MAX_STEPS` (1024) — high enough to be effectively unlimited for real tasks; the practical ceiling is the context window and cost. The constant is exported so consumers and harness authors share one source of truth. |
|
|
254
257
|
|
|
255
|
-
`ToolApprovalMode` is the exported type alias `'serial' | 'batch'
|
|
256
|
-
`
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
`ToolApprovalMode` is the exported (deprecated) type alias `'serial' | 'batch'`. Pair with the (deprecated)
|
|
259
|
+
`resolveToolApprovalMode(boolean | ToolApprovalMode | undefined)` to normalize consumer input the same way the SDK does
|
|
260
|
+
internally (`undefined` / `false` → `undefined`, `true` → `'serial'`, strings pass through, unknown strings throw). New
|
|
261
|
+
code should configure `AgentConfig.toolPolicies` and set `batchApprovals` for the UX axis instead.
|
|
262
|
+
|
|
263
|
+
#### Tool Approval Policy
|
|
264
|
+
|
|
265
|
+
Per-tool approval is configured on the agent, not per `chat()` call. `AgentConfig.toolPolicies` is an ordered list of
|
|
266
|
+
`ToolPolicyRule`s; for each harness-executed tool call the harness consults `resolveToolApprovalPolicy(...)`, a pure
|
|
267
|
+
function exported from this package.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
export type Decision = 'allow' | 'deny' | 'require-approval';
|
|
271
|
+
|
|
272
|
+
export type ToolMatcher =
|
|
273
|
+
| { type: 'builtin'; name: string } // harness built-in (e.g. Claude `Bash`)
|
|
274
|
+
| { type: 'mcp'; serverName?: string; toolName?: string } // MCP tool(s); omitted fields are wildcards
|
|
275
|
+
| { type: 'mcp-annotation'; readOnlyHint?: boolean; destructiveHint?: boolean }; // by discovered annotation
|
|
276
|
+
|
|
277
|
+
export type ToolPolicyRule = {
|
|
278
|
+
matcher: ToolMatcher;
|
|
279
|
+
decision: Decision;
|
|
280
|
+
source?: 'built-in' | 'agent-config' | 'remember'; // advisory provenance; ignored by the resolver
|
|
281
|
+
};
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Resolution — cross-tier deny-wins / within-tier last-wins.** The resolver concatenates four tiers in precedence order
|
|
285
|
+
— `[...BUILT_IN_TOOL_POLICIES, ...harness, ...factory, ...AgentConfig.toolPolicies]` — then: any matching `'deny'` in
|
|
286
|
+
any tier wins; otherwise the last matching non-deny rule decides; if nothing matched, the result is
|
|
287
|
+
`AgentConfig.defaultToolDecision ?? 'allow'`. So a later rule overrides an earlier one of the same matcher (a consumer
|
|
288
|
+
rule beats a built-in), but no consumer rule can override a `deny`.
|
|
289
|
+
|
|
290
|
+
**Built-in rules.** `BUILT_IN_TOOL_POLICIES` (exported, frozen) ships cross-harness rules only: MCP-annotation defaults
|
|
291
|
+
(`destructiveHint ⇒ require-approval`, `readOnlyHint ⇒ allow`) and the `skill_bridge` capability-discovery meta-tools
|
|
292
|
+
(anchored on the exported `SKILL_BRIDGE_SERVER_ID`). Harness-specific built-ins (Claude's `Bash`, Mastra's
|
|
293
|
+
`updateWorkingMemory`) live in the harness packages' `<HARNESS>_BUILT_IN_TOOL_POLICIES` arrays, not here.
|
|
294
|
+
|
|
295
|
+
**`definePolicy` helper.** Compresses the common cases into a `ToolPolicyRule[]` (`source: 'agent-config'`):
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { definePolicy } from '@salesforce/sfdx-agent-sdk';
|
|
299
|
+
|
|
300
|
+
const config: AgentConfig = {
|
|
301
|
+
defaultToolDecision: 'require-approval', // fail-closed
|
|
302
|
+
toolPolicies: definePolicy({
|
|
303
|
+
Bash: 'deny', // builtin matcher
|
|
304
|
+
'mcp:sfdx': 'require-approval', // every tool from the 'sfdx' MCP server
|
|
305
|
+
'mcp:sfdx/list_orgs': 'allow', // one tool from the 'sfdx' server
|
|
306
|
+
}),
|
|
307
|
+
};
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`definePolicy` covers `builtin` and `mcp:server[/tool]` keys only — author the structured `ToolPolicyRule` form for
|
|
311
|
+
`mcp-annotation` matchers, the bare `{ type: 'mcp' }` wildcard, or tool names containing `/`.
|
|
312
|
+
|
|
313
|
+
**AG-UI button mapping.** The settle methods grow a symmetric, honored `remember` flag:
|
|
314
|
+
|
|
315
|
+
| AG-UI button | SDK call | Effect |
|
|
316
|
+
| ------------ | ----------------------------------------- | -------------------------------------------------------- |
|
|
317
|
+
| Allow | `approveToolCall(id)` | Settle only. |
|
|
318
|
+
| Allow always | `approveToolCall(id, { remember: true })` | Append an `allow` `remember` rule, persist, then settle. |
|
|
319
|
+
| Deny | `declineToolCall(id)` | Settle only. |
|
|
320
|
+
| Deny always | `declineToolCall(id, { remember: true })` | Append a `deny` `remember` rule, persist, then settle. |
|
|
321
|
+
|
|
322
|
+
With `{ remember: true }` the SDK derives the matcher from the pending `tool-approval-request`'s
|
|
323
|
+
`(toolName, serverName?)`, appends a `source: 'remember'` rule to `AgentConfig.toolPolicies`, and persists it via
|
|
324
|
+
`updateAgentConfig` **before** settling — so the decision survives a restart and a persistence failure surfaces as the
|
|
325
|
+
settle's rejection. A `remember` settle for a `toolCallId` with no pending approval throws `TOOL_CALL_NOT_FOUND`.
|
|
259
326
|
|
|
260
327
|
#### `MCPConfiguration`
|
|
261
328
|
|
package/dist/agent.js
CHANGED
|
@@ -332,10 +332,19 @@ export class DefaultAgent {
|
|
|
332
332
|
// `contextWindow`; #507's decoupling work must preserve that, so this
|
|
333
333
|
// access is contractually safe.
|
|
334
334
|
const getContextWindow = () => this.modelConnectivityInfo.model.contextWindow;
|
|
335
|
+
// Persists a `'remember'` policy rule for an `approveToolCall` /
|
|
336
|
+
// `declineToolCall` settle with `{ remember: true }`. Reads `this.config`
|
|
337
|
+
// live (so concurrent sessions and prior remembers compound correctly)
|
|
338
|
+
// and forwards a `toolPolicies`-only partial — `updateAgentConfig` skips
|
|
339
|
+
// connectivity re-resolution when neither `orgAlias` nor `modelId` is
|
|
340
|
+
// present, so an "Allow always" click never re-mints the org JWT.
|
|
341
|
+
const persistRememberedRule = async (rule) => {
|
|
342
|
+
await this.updateAgentConfig({ toolPolicies: [...(this.config.toolPolicies ?? []), rule] });
|
|
343
|
+
};
|
|
335
344
|
const session = new DefaultChatSession(this.harness, this.agentId, threadId, slice, {
|
|
336
345
|
telemetry: this.telemetryBus,
|
|
337
346
|
log: this.logBus,
|
|
338
|
-
}, getContextWindow, this.clock, this.idGenerator);
|
|
347
|
+
}, getContextWindow, { clock: this.clock, idGenerator: this.idGenerator, persistRememberedRule });
|
|
339
348
|
this.sessions.set(threadId, session);
|
|
340
349
|
this.sessionSliceUnregisters.set(threadId, () => this.router.unregisterSession(threadId));
|
|
341
350
|
this.telemetryBus.emit({
|
package/dist/chat-session.d.ts
CHANGED
|
@@ -5,12 +5,42 @@ import type { TelemetrySlice } from './internal/telemetry-router.js';
|
|
|
5
5
|
import type { ChatEvent, ChatStreamResult } from './types/events.js';
|
|
6
6
|
import type { Message, MessagePart } from './types/messages.js';
|
|
7
7
|
import type { TelemetryBus, TelemetryEventCallback } from './types/telemetry-events.js';
|
|
8
|
-
import type { ToolResultInfo } from './types/tools.js';
|
|
8
|
+
import type { ToolPolicyRule, ToolResultInfo } from './types/tools.js';
|
|
9
9
|
import type { ContextUsage } from './types/usage.js';
|
|
10
10
|
/**
|
|
11
11
|
* Options for a single chat interaction.
|
|
12
12
|
*/
|
|
13
13
|
export type ChatOptions = StreamOptions;
|
|
14
|
+
/**
|
|
15
|
+
* Persists a `'remember'` tool-policy rule appended by a settle call with
|
|
16
|
+
* `{ remember: true }`. Injected by {@link DefaultAgent} so the session can
|
|
17
|
+
* write through to the agent's config (and the SDK-owned identity store)
|
|
18
|
+
* without holding an `Agent` reference. The implementation reads the agent's
|
|
19
|
+
* current `toolPolicies`, appends `rule`, and calls `updateAgentConfig` with a
|
|
20
|
+
* `toolPolicies`-only partial so no connectivity re-resolution is triggered.
|
|
21
|
+
*/
|
|
22
|
+
export type RememberedRulePersister = (rule: ToolPolicyRule) => Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Optional injected dependencies for a {@link DefaultChatSession}. Bundled into
|
|
25
|
+
* one trailing bag (rather than a growing tail of positional params) so a new
|
|
26
|
+
* dependency is a one-field addition here plus a one-line spread at the
|
|
27
|
+
* construction site, not a positional append every caller must match.
|
|
28
|
+
*
|
|
29
|
+
* Every field is optional with a production default resolved in the
|
|
30
|
+
* constructor — tests override only what they need.
|
|
31
|
+
*/
|
|
32
|
+
export type ChatSessionDeps = {
|
|
33
|
+
/** Source of monotonic timestamps for telemetry events. Defaults to `RealClock`. */
|
|
34
|
+
clock?: Clock;
|
|
35
|
+
/** Source of message ids for `addContext()`. Defaults to `UUIDGenerator`. */
|
|
36
|
+
idGenerator?: UniqueIDGenerator;
|
|
37
|
+
/**
|
|
38
|
+
* Persists a `'remember'` policy rule for a settle call with `{ remember: true }`.
|
|
39
|
+
* Supplied by `DefaultAgent`; omitted in unit tests, where a `remember: true` settle
|
|
40
|
+
* then behaves as a one-shot with no write. See {@link RememberedRulePersister}.
|
|
41
|
+
*/
|
|
42
|
+
persistRememberedRule?: RememberedRulePersister;
|
|
43
|
+
};
|
|
14
44
|
/**
|
|
15
45
|
* Parent bus pair used to wire upward forwarding at construction time.
|
|
16
46
|
*/
|
|
@@ -99,10 +129,14 @@ export interface ChatSession {
|
|
|
99
129
|
*
|
|
100
130
|
* @param toolCallId - ID of the pending tool call to approve.
|
|
101
131
|
* @param options - Optional approval metadata.
|
|
102
|
-
* @param options.remember - When `true
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
132
|
+
* @param options.remember - When `true` ("Allow always"), the SDK appends a
|
|
133
|
+
* `{ decision: 'allow', source: 'remember' }` rule to the agent's
|
|
134
|
+
* `toolPolicies` (matching this tool) and persists it via
|
|
135
|
+
* `updateAgentConfig` **before** settling the approval, so the decision
|
|
136
|
+
* survives a restart. Throws `TOOL_CALL_NOT_FOUND` if `toolCallId` does
|
|
137
|
+
* not match a pending `tool-approval-request` (e.g. a consumer-executed
|
|
138
|
+
* tool, which is never gated). When `false`/omitted, the approval is a
|
|
139
|
+
* one-shot settle with no persistence.
|
|
106
140
|
*/
|
|
107
141
|
approveToolCall(toolCallId: string, options?: {
|
|
108
142
|
remember?: boolean;
|
|
@@ -118,8 +152,17 @@ export interface ChatSession {
|
|
|
118
152
|
* `ErrorEvent` + `FinishEvent` on the chat stream before the rejection.
|
|
119
153
|
*
|
|
120
154
|
* @param toolCallId - ID of the pending tool call to decline.
|
|
155
|
+
* @param options - Optional decline metadata.
|
|
156
|
+
* @param options.remember - When `true` ("Deny always"), the SDK appends a
|
|
157
|
+
* `{ decision: 'deny', source: 'remember' }` rule to the agent's
|
|
158
|
+
* `toolPolicies` (matching this tool) and persists it via
|
|
159
|
+
* `updateAgentConfig` **before** settling the decline. Symmetric with
|
|
160
|
+
* {@link approveToolCall}'s `remember`. Throws `TOOL_CALL_NOT_FOUND` if
|
|
161
|
+
* `toolCallId` does not match a pending `tool-approval-request`.
|
|
121
162
|
*/
|
|
122
|
-
declineToolCall(toolCallId: string
|
|
163
|
+
declineToolCall(toolCallId: string, options?: {
|
|
164
|
+
remember?: boolean;
|
|
165
|
+
}): Promise<void>;
|
|
123
166
|
/**
|
|
124
167
|
* Retrieve message history for this session.
|
|
125
168
|
*
|
|
@@ -200,6 +243,16 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
200
243
|
* are stale and should not bleed into the next turn).
|
|
201
244
|
*/
|
|
202
245
|
private readonly toolStartMs;
|
|
246
|
+
/**
|
|
247
|
+
* Tracks the `(toolName, serverName?)` of every tool call currently awaiting
|
|
248
|
+
* approval, keyed by `toolCallId`. Populated when a `tool-approval-request`
|
|
249
|
+
* ChatEvent flows through {@link wrapEventStream}; read by
|
|
250
|
+
* {@link approveToolCall} / {@link declineToolCall} when `remember: true` so
|
|
251
|
+
* the appended policy rule carries the right matcher. An entry is removed
|
|
252
|
+
* once its approval settles (or the turn ends), so a `remember` settle for an
|
|
253
|
+
* unknown / already-settled `toolCallId` throws `TOOL_CALL_NOT_FOUND`.
|
|
254
|
+
*/
|
|
255
|
+
private readonly pendingApprovalsByToolCallId;
|
|
203
256
|
/**
|
|
204
257
|
* Live getter for the agent's currently-bound model's context window.
|
|
205
258
|
* Called by {@link getContextUsage} so reads reflect the model in
|
|
@@ -207,6 +260,14 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
207
260
|
* (an `Agent.updateAgentConfig()` swap can change it mid-life).
|
|
208
261
|
*/
|
|
209
262
|
private readonly getContextWindow;
|
|
263
|
+
/**
|
|
264
|
+
* Persists a `'remember'` policy rule on behalf of a settle call with
|
|
265
|
+
* `{ remember: true }`. Injected by {@link DefaultAgent}; `undefined` only
|
|
266
|
+
* in unit tests that construct a session without the persister, in which
|
|
267
|
+
* case a `remember: true` settle is treated as a one-shot (no write). See
|
|
268
|
+
* {@link RememberedRulePersister}.
|
|
269
|
+
*/
|
|
270
|
+
private readonly persistRememberedRule;
|
|
210
271
|
/**
|
|
211
272
|
* Last per-step usage reading observed on this session. Initialized
|
|
212
273
|
* to `{}` (every token field undefined) so {@link getContextUsage}
|
|
@@ -226,10 +287,10 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
226
287
|
* @param parent - Parent agent's buses; this session forwards its events upward into them.
|
|
227
288
|
* @param getContextWindow - Live getter for the agent's currently-bound model's `contextWindow`.
|
|
228
289
|
* Called by `getContextUsage()` so reads stay correct across `Agent.updateAgentConfig()` model swaps.
|
|
229
|
-
* @param
|
|
230
|
-
*
|
|
290
|
+
* @param deps - Optional injected dependencies ({@link ChatSessionDeps}): `clock`, `idGenerator`,
|
|
291
|
+
* `persistRememberedRule`. Each has a production default; tests override only what they need.
|
|
231
292
|
*/
|
|
232
|
-
constructor(harness: AgentHarness, agentId: string, threadId: string, inbound: TelemetrySlice, parent: ChatSessionParentBuses, getContextWindow: () => number,
|
|
293
|
+
constructor(harness: AgentHarness, agentId: string, threadId: string, inbound: TelemetrySlice, parent: ChatSessionParentBuses, getContextWindow: () => number, deps?: ChatSessionDeps);
|
|
233
294
|
getId(): string;
|
|
234
295
|
/**
|
|
235
296
|
* @requirements
|
|
@@ -290,9 +351,12 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
290
351
|
* `tool-approval-requested` by `toolCallId` and observe the failure on the chat-stream contract.
|
|
291
352
|
* - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
|
|
292
353
|
* before returning a stream result.
|
|
293
|
-
* -
|
|
354
|
+
* - WHEN `options.remember` is `true`, MUST append an `'allow'` `'remember'` rule to the agent's
|
|
355
|
+
* `toolPolicies` and persist it via the injected persister BEFORE delegating the settle to the
|
|
356
|
+
* harness, so a persistence failure surfaces as the settle's rejection and the decision is durable
|
|
357
|
+
* before the tool runs. Throws `TOOL_CALL_NOT_FOUND` if `toolCallId` has no pending approval.
|
|
294
358
|
*/
|
|
295
|
-
approveToolCall(toolCallId: string,
|
|
359
|
+
approveToolCall(toolCallId: string, options?: {
|
|
296
360
|
remember?: boolean;
|
|
297
361
|
}): Promise<void>;
|
|
298
362
|
/**
|
|
@@ -305,8 +369,12 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
305
369
|
* and intentionally skip approval-resolved emission.
|
|
306
370
|
* - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
|
|
307
371
|
* before returning a stream result.
|
|
372
|
+
* - WHEN `options.remember` is `true`, MUST append a `'deny'` `'remember'` rule and persist it
|
|
373
|
+
* BEFORE delegating the settle to the harness — symmetric with {@link approveToolCall}.
|
|
308
374
|
*/
|
|
309
|
-
declineToolCall(toolCallId: string
|
|
375
|
+
declineToolCall(toolCallId: string, options?: {
|
|
376
|
+
remember?: boolean;
|
|
377
|
+
}): Promise<void>;
|
|
310
378
|
/**
|
|
311
379
|
* @requirements
|
|
312
380
|
* - MUST delegate to `this.harness.getMessages()`, passing `this.agentId` and `this.threadId`.
|
|
@@ -364,6 +432,16 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
364
432
|
onLog(callback: (record: LogRecord) => void): Unsubscribe;
|
|
365
433
|
dispose(): void;
|
|
366
434
|
private emitToolApprovalResolved;
|
|
435
|
+
/**
|
|
436
|
+
* Clears the per-turn tracking maps at a terminal `finish`. Both maps are
|
|
437
|
+
* scoped to one logical chat turn: `toolStartMs` pairs `tool-call` with
|
|
438
|
+
* `tool-result` for `tool-execution-completed.durationMs`, and
|
|
439
|
+
* `pendingApprovalsByToolCallId` lets a `remember` settle build the right
|
|
440
|
+
* matcher. A stale entry surviving into the next turn would mispair a
|
|
441
|
+
* duration or remember the wrong tool, so the two clears must always fire
|
|
442
|
+
* together — hence one helper rather than two call sites.
|
|
443
|
+
*/
|
|
444
|
+
private clearPerTurnTracking;
|
|
367
445
|
/**
|
|
368
446
|
* Derives `tool-execution-*` and `tool-approval-requested` telemetry from `ChatEvent`s as
|
|
369
447
|
* they pass through the stream wrapper. Centralizing the derivation here keeps every harness
|
|
@@ -379,6 +457,20 @@ export declare class DefaultChatSession implements ChatSession {
|
|
|
379
457
|
* `tool-execution-started`.
|
|
380
458
|
*/
|
|
381
459
|
private deriveToolTelemetry;
|
|
460
|
+
/**
|
|
461
|
+
* If `remember` is requested, builds the matcher for the pending approval
|
|
462
|
+
* referenced by `toolCallId` and persists a `'remember'` rule with the
|
|
463
|
+
* given `decision` via the injected {@link RememberedRulePersister}, BEFORE
|
|
464
|
+
* the caller settles with the harness. Returns whether a rule was written.
|
|
465
|
+
*
|
|
466
|
+
* - A `remember` settle for a `toolCallId` with no pending approval throws
|
|
467
|
+
* `TOOL_CALL_NOT_FOUND` — the same outcome as settling an unknown id, and
|
|
468
|
+
* the correct outcome for a consumer-executed tool (never gated, so never
|
|
469
|
+
* in the pending map).
|
|
470
|
+
* - When no persister was injected (unit-test construction), `remember`
|
|
471
|
+
* degrades to a one-shot settle: no write, returns `false`.
|
|
472
|
+
*/
|
|
473
|
+
private maybePersistRememberedRule;
|
|
382
474
|
/**
|
|
383
475
|
* Emits a `chat-stream-started` telemetry event and returns the `startedAt` timestamp the
|
|
384
476
|
* caller threads through to the stream wrapper / pre-stream error notifier so terminal
|
package/dist/chat-session.js
CHANGED
|
@@ -31,6 +31,16 @@ export class DefaultChatSession {
|
|
|
31
31
|
* are stale and should not bleed into the next turn).
|
|
32
32
|
*/
|
|
33
33
|
toolStartMs = new Map();
|
|
34
|
+
/**
|
|
35
|
+
* Tracks the `(toolName, serverName?)` of every tool call currently awaiting
|
|
36
|
+
* approval, keyed by `toolCallId`. Populated when a `tool-approval-request`
|
|
37
|
+
* ChatEvent flows through {@link wrapEventStream}; read by
|
|
38
|
+
* {@link approveToolCall} / {@link declineToolCall} when `remember: true` so
|
|
39
|
+
* the appended policy rule carries the right matcher. An entry is removed
|
|
40
|
+
* once its approval settles (or the turn ends), so a `remember` settle for an
|
|
41
|
+
* unknown / already-settled `toolCallId` throws `TOOL_CALL_NOT_FOUND`.
|
|
42
|
+
*/
|
|
43
|
+
pendingApprovalsByToolCallId = new Map();
|
|
34
44
|
/**
|
|
35
45
|
* Live getter for the agent's currently-bound model's context window.
|
|
36
46
|
* Called by {@link getContextUsage} so reads reflect the model in
|
|
@@ -38,6 +48,14 @@ export class DefaultChatSession {
|
|
|
38
48
|
* (an `Agent.updateAgentConfig()` swap can change it mid-life).
|
|
39
49
|
*/
|
|
40
50
|
getContextWindow;
|
|
51
|
+
/**
|
|
52
|
+
* Persists a `'remember'` policy rule on behalf of a settle call with
|
|
53
|
+
* `{ remember: true }`. Injected by {@link DefaultAgent}; `undefined` only
|
|
54
|
+
* in unit tests that construct a session without the persister, in which
|
|
55
|
+
* case a `remember: true` settle is treated as a one-shot (no write). See
|
|
56
|
+
* {@link RememberedRulePersister}.
|
|
57
|
+
*/
|
|
58
|
+
persistRememberedRule;
|
|
41
59
|
/**
|
|
42
60
|
* Last per-step usage reading observed on this session. Initialized
|
|
43
61
|
* to `{}` (every token field undefined) so {@link getContextUsage}
|
|
@@ -57,16 +75,17 @@ export class DefaultChatSession {
|
|
|
57
75
|
* @param parent - Parent agent's buses; this session forwards its events upward into them.
|
|
58
76
|
* @param getContextWindow - Live getter for the agent's currently-bound model's `contextWindow`.
|
|
59
77
|
* Called by `getContextUsage()` so reads stay correct across `Agent.updateAgentConfig()` model swaps.
|
|
60
|
-
* @param
|
|
61
|
-
*
|
|
78
|
+
* @param deps - Optional injected dependencies ({@link ChatSessionDeps}): `clock`, `idGenerator`,
|
|
79
|
+
* `persistRememberedRule`. Each has a production default; tests override only what they need.
|
|
62
80
|
*/
|
|
63
|
-
constructor(harness, agentId, threadId, inbound, parent, getContextWindow,
|
|
81
|
+
constructor(harness, agentId, threadId, inbound, parent, getContextWindow, deps = {}) {
|
|
64
82
|
this.harness = harness;
|
|
65
83
|
this.agentId = agentId;
|
|
66
84
|
this.threadId = threadId;
|
|
67
85
|
this.getContextWindow = getContextWindow;
|
|
68
|
-
this.clock = clock;
|
|
69
|
-
this.idGenerator = idGenerator;
|
|
86
|
+
this.clock = deps.clock ?? new RealClock();
|
|
87
|
+
this.idGenerator = deps.idGenerator ?? new UUIDGenerator();
|
|
88
|
+
this.persistRememberedRule = deps.persistRememberedRule;
|
|
70
89
|
this.inboundUnsubs = [inbound.telemetry.forwardTo(this.telemetryBus), inbound.log.forwardTo(this.logBus)];
|
|
71
90
|
this.parentUnsubs = [this.telemetryBus.forwardTo(parent.telemetry), this.logBus.forwardTo(parent.log)];
|
|
72
91
|
}
|
|
@@ -185,7 +204,7 @@ export class DefaultChatSession {
|
|
|
185
204
|
// would lose tracking for tool-calls whose tool-result lands on a later
|
|
186
205
|
// continuation stream. Only terminal `FinishReason`s end the turn.
|
|
187
206
|
if (event.finishReason !== 'tool-calls') {
|
|
188
|
-
this.
|
|
207
|
+
this.clearPerTurnTracking();
|
|
189
208
|
}
|
|
190
209
|
}
|
|
191
210
|
if (event.type === 'error')
|
|
@@ -204,9 +223,8 @@ export class DefaultChatSession {
|
|
|
204
223
|
this.chatEventBus.emit(finishEvent);
|
|
205
224
|
yield finishEvent;
|
|
206
225
|
// Match the natural-finish branch: every terminal `finish` clears the
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
this.toolStartMs.clear();
|
|
226
|
+
// per-turn tracking maps.
|
|
227
|
+
this.clearPerTurnTracking();
|
|
210
228
|
}
|
|
211
229
|
const finishedAt = this.clock.now();
|
|
212
230
|
const durationMs = finishedAt.getTime() - startedAt.getTime();
|
|
@@ -242,13 +260,17 @@ export class DefaultChatSession {
|
|
|
242
260
|
* `tool-approval-requested` by `toolCallId` and observe the failure on the chat-stream contract.
|
|
243
261
|
* - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
|
|
244
262
|
* before returning a stream result.
|
|
245
|
-
* -
|
|
263
|
+
* - WHEN `options.remember` is `true`, MUST append an `'allow'` `'remember'` rule to the agent's
|
|
264
|
+
* `toolPolicies` and persist it via the injected persister BEFORE delegating the settle to the
|
|
265
|
+
* harness, so a persistence failure surfaces as the settle's rejection and the decision is durable
|
|
266
|
+
* before the tool runs. Throws `TOOL_CALL_NOT_FOUND` if `toolCallId` has no pending approval.
|
|
246
267
|
*/
|
|
247
|
-
async approveToolCall(toolCallId,
|
|
268
|
+
async approveToolCall(toolCallId, options) {
|
|
248
269
|
this.assertNotDisposed();
|
|
249
270
|
// issue #529 contract change: see `submitToolResult` for the rationale.
|
|
250
271
|
// Settle is a control message on the existing turn; events flow on
|
|
251
272
|
// the chat()-returned stream.
|
|
273
|
+
const policyWritten = await this.maybePersistRememberedRule(toolCallId, 'allow', options?.remember === true);
|
|
252
274
|
try {
|
|
253
275
|
await this.harness.approveToolCall(this.agentId, this.threadId, toolCallId);
|
|
254
276
|
}
|
|
@@ -256,7 +278,8 @@ export class DefaultChatSession {
|
|
|
256
278
|
this.notifySettleRejection(err);
|
|
257
279
|
throw err;
|
|
258
280
|
}
|
|
259
|
-
this.
|
|
281
|
+
this.pendingApprovalsByToolCallId.delete(toolCallId);
|
|
282
|
+
this.emitToolApprovalResolved(toolCallId, true, policyWritten);
|
|
260
283
|
}
|
|
261
284
|
/**
|
|
262
285
|
* @requirements
|
|
@@ -268,10 +291,13 @@ export class DefaultChatSession {
|
|
|
268
291
|
* and intentionally skip approval-resolved emission.
|
|
269
292
|
* - MUST notify listeners with `ErrorEvent` + `FinishEvent` and re-throw if the harness throws
|
|
270
293
|
* before returning a stream result.
|
|
294
|
+
* - WHEN `options.remember` is `true`, MUST append a `'deny'` `'remember'` rule and persist it
|
|
295
|
+
* BEFORE delegating the settle to the harness — symmetric with {@link approveToolCall}.
|
|
271
296
|
*/
|
|
272
|
-
async declineToolCall(toolCallId) {
|
|
297
|
+
async declineToolCall(toolCallId, options) {
|
|
273
298
|
this.assertNotDisposed();
|
|
274
299
|
// issue #529 contract change: see `submitToolResult` for the rationale.
|
|
300
|
+
const policyWritten = await this.maybePersistRememberedRule(toolCallId, 'deny', options?.remember === true);
|
|
275
301
|
try {
|
|
276
302
|
await this.harness.declineToolCall(this.agentId, this.threadId, toolCallId);
|
|
277
303
|
}
|
|
@@ -279,7 +305,8 @@ export class DefaultChatSession {
|
|
|
279
305
|
this.notifySettleRejection(err);
|
|
280
306
|
throw err;
|
|
281
307
|
}
|
|
282
|
-
this.
|
|
308
|
+
this.pendingApprovalsByToolCallId.delete(toolCallId);
|
|
309
|
+
this.emitToolApprovalResolved(toolCallId, false, policyWritten);
|
|
283
310
|
}
|
|
284
311
|
/**
|
|
285
312
|
* @requirements
|
|
@@ -408,7 +435,7 @@ export class DefaultChatSession {
|
|
|
408
435
|
this.logBus.dispose();
|
|
409
436
|
this.disposed = true;
|
|
410
437
|
}
|
|
411
|
-
emitToolApprovalResolved(toolCallId, approved) {
|
|
438
|
+
emitToolApprovalResolved(toolCallId, approved, policyWritten) {
|
|
412
439
|
this.telemetryBus.emit({
|
|
413
440
|
type: 'tool-approval-resolved',
|
|
414
441
|
timestamp: this.clock.now(),
|
|
@@ -416,8 +443,22 @@ export class DefaultChatSession {
|
|
|
416
443
|
threadId: this.threadId,
|
|
417
444
|
toolCallId,
|
|
418
445
|
approved,
|
|
446
|
+
...(policyWritten ? { policyWritten: true } : {}),
|
|
419
447
|
});
|
|
420
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* Clears the per-turn tracking maps at a terminal `finish`. Both maps are
|
|
451
|
+
* scoped to one logical chat turn: `toolStartMs` pairs `tool-call` with
|
|
452
|
+
* `tool-result` for `tool-execution-completed.durationMs`, and
|
|
453
|
+
* `pendingApprovalsByToolCallId` lets a `remember` settle build the right
|
|
454
|
+
* matcher. A stale entry surviving into the next turn would mispair a
|
|
455
|
+
* duration or remember the wrong tool, so the two clears must always fire
|
|
456
|
+
* together — hence one helper rather than two call sites.
|
|
457
|
+
*/
|
|
458
|
+
clearPerTurnTracking() {
|
|
459
|
+
this.toolStartMs.clear();
|
|
460
|
+
this.pendingApprovalsByToolCallId.clear();
|
|
461
|
+
}
|
|
421
462
|
/**
|
|
422
463
|
* Derives `tool-execution-*` and `tool-approval-requested` telemetry from `ChatEvent`s as
|
|
423
464
|
* they pass through the stream wrapper. Centralizing the derivation here keeps every harness
|
|
@@ -466,6 +507,15 @@ export class DefaultChatSession {
|
|
|
466
507
|
});
|
|
467
508
|
}
|
|
468
509
|
else if (event.type === 'tool-approval-request') {
|
|
510
|
+
// Record the (toolName, serverName?) so a later settle with
|
|
511
|
+
// `remember: true` can build the right matcher for the persisted
|
|
512
|
+
// rule. Drained on settle, and on every terminal `finish` (see
|
|
513
|
+
// `wrapEventStream`) so a stale entry can't be remembered after the
|
|
514
|
+
// turn that requested it has ended.
|
|
515
|
+
this.pendingApprovalsByToolCallId.set(event.toolCall.toolCallId, {
|
|
516
|
+
toolName: event.toolCall.toolName,
|
|
517
|
+
...(event.serverName ? { serverName: event.serverName } : {}),
|
|
518
|
+
});
|
|
469
519
|
this.telemetryBus.emit({
|
|
470
520
|
type: 'tool-approval-requested',
|
|
471
521
|
timestamp: this.clock.now(),
|
|
@@ -478,6 +528,34 @@ export class DefaultChatSession {
|
|
|
478
528
|
});
|
|
479
529
|
}
|
|
480
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* If `remember` is requested, builds the matcher for the pending approval
|
|
533
|
+
* referenced by `toolCallId` and persists a `'remember'` rule with the
|
|
534
|
+
* given `decision` via the injected {@link RememberedRulePersister}, BEFORE
|
|
535
|
+
* the caller settles with the harness. Returns whether a rule was written.
|
|
536
|
+
*
|
|
537
|
+
* - A `remember` settle for a `toolCallId` with no pending approval throws
|
|
538
|
+
* `TOOL_CALL_NOT_FOUND` — the same outcome as settling an unknown id, and
|
|
539
|
+
* the correct outcome for a consumer-executed tool (never gated, so never
|
|
540
|
+
* in the pending map).
|
|
541
|
+
* - When no persister was injected (unit-test construction), `remember`
|
|
542
|
+
* degrades to a one-shot settle: no write, returns `false`.
|
|
543
|
+
*/
|
|
544
|
+
async maybePersistRememberedRule(toolCallId, decision, remember) {
|
|
545
|
+
if (!remember)
|
|
546
|
+
return false;
|
|
547
|
+
const ref = this.pendingApprovalsByToolCallId.get(toolCallId);
|
|
548
|
+
if (!ref) {
|
|
549
|
+
throw new AgentSDKError(`No pending tool approval found with id: "${toolCallId}"`, AgentSDKErrorType.TOOL_CALL_NOT_FOUND);
|
|
550
|
+
}
|
|
551
|
+
if (!this.persistRememberedRule)
|
|
552
|
+
return false;
|
|
553
|
+
const matcher = ref.serverName !== undefined
|
|
554
|
+
? { type: 'mcp', serverName: ref.serverName, toolName: ref.toolName }
|
|
555
|
+
: { type: 'builtin', name: ref.toolName };
|
|
556
|
+
await this.persistRememberedRule({ matcher, decision, source: 'remember' });
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
481
559
|
/**
|
|
482
560
|
* Emits a `chat-stream-started` telemetry event and returns the `startedAt` timestamp the
|
|
483
561
|
* caller threads through to the stream wrapper / pre-stream error notifier so terminal
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolDefinition } from '../types/tools.js';
|
|
1
|
+
import type { Decision, ToolDefinition, ToolPolicyRule } from '../types/tools.js';
|
|
2
2
|
import type { MCPConfiguration } from '../mcp-config.js';
|
|
3
3
|
import type { JSONWebToken } from '@salesforce/agentic-common';
|
|
4
4
|
import type { Model, ModelName } from '../models/index.js';
|
|
@@ -59,6 +59,39 @@ export type AgentConfig = {
|
|
|
59
59
|
* opaquely.
|
|
60
60
|
*/
|
|
61
61
|
rules?: string[];
|
|
62
|
+
/**
|
|
63
|
+
* Ordered list of tool-approval rules. For each harness-executed tool call,
|
|
64
|
+
* `resolveToolApprovalPolicy` walks this list (concatenated after the SDK's
|
|
65
|
+
* `BUILT_IN_TOOL_POLICIES`, the harness's built-in array, and any harness
|
|
66
|
+
* factory rules) and returns a {@link Decision} via **cross-tier deny-wins /
|
|
67
|
+
* within-tier last-wins** precedence: any `'deny'` in any tier wins;
|
|
68
|
+
* otherwise the last matching non-deny rule decides. A later consumer rule
|
|
69
|
+
* beats an earlier one, and a `'remember'` rule appended by
|
|
70
|
+
* `approveToolCall(id, { remember: true })` beats an earlier built-in
|
|
71
|
+
* `'require-approval'` of the same matcher.
|
|
72
|
+
*
|
|
73
|
+
* Author rules with the structured {@link ToolPolicyRule} shape, or the
|
|
74
|
+
* `definePolicy({ Bash: 'deny', 'mcp:sfdx': 'require-approval' })` helper for
|
|
75
|
+
* the common cases. Per-MCP-server policy is expressed here via
|
|
76
|
+
* `{ matcher: { type: 'mcp', serverName } }` rules — `MCPServerConfig`
|
|
77
|
+
* carries no policy fields.
|
|
78
|
+
*
|
|
79
|
+
* Has no effect until a harness wires the resolver into its gate site
|
|
80
|
+
* (Phase 2). Until then, harnesses gate via the deprecated
|
|
81
|
+
* {@link StreamOptions.requireToolApproval}.
|
|
82
|
+
*/
|
|
83
|
+
toolPolicies?: ToolPolicyRule[];
|
|
84
|
+
/**
|
|
85
|
+
* Fallback decision for tool invocations that match no rule in
|
|
86
|
+
* {@link toolPolicies} (or any built-in / harness tier). Defaults to
|
|
87
|
+
* `'allow'` for back-compat ("no policy ⇒ no gating," matching today's
|
|
88
|
+
* behavior when `requireToolApproval` is unset).
|
|
89
|
+
*
|
|
90
|
+
* Set to `'require-approval'` for a fail-closed posture — recommended for
|
|
91
|
+
* tenants whose MCP catalog includes un-annotated servers, since
|
|
92
|
+
* annotation-matcher rules don't fire on tools with no annotations.
|
|
93
|
+
*/
|
|
94
|
+
defaultToolDecision?: Decision;
|
|
62
95
|
};
|
|
63
96
|
/**
|
|
64
97
|
* Harness-facing configuration for creating/updating an agent.
|
|
@@ -102,6 +135,11 @@ export declare function toHarnessConfig(config: AgentConfig, orgJwt?: JSONWebTok
|
|
|
102
135
|
* surface on the same stream so the consumer can render them as a
|
|
103
136
|
* batch approval card). See `requireToolApproval` for the safety
|
|
104
137
|
* note on choosing `batch`.
|
|
138
|
+
*
|
|
139
|
+
* @deprecated Tool-approval gating is moving to per-tool policy on
|
|
140
|
+
* {@link AgentConfig.toolPolicies}, resolved by `resolveToolApprovalPolicy`.
|
|
141
|
+
* The serial-vs-batch UX axis survives as {@link StreamOptions.batchApprovals}.
|
|
142
|
+
* Removed in a future major once harnesses and consumers have migrated.
|
|
105
143
|
*/
|
|
106
144
|
export type ToolApprovalMode = 'serial' | 'batch';
|
|
107
145
|
/**
|
|
@@ -115,6 +153,11 @@ export type ToolApprovalMode = 'serial' | 'batch';
|
|
|
115
153
|
* - `true` → `'serial'` (back-compat shorthand for the original `boolean` shape).
|
|
116
154
|
* - `'serial'` → `'serial'` (explicit, equivalent to `true`).
|
|
117
155
|
* - `'batch'` → `'batch'`.
|
|
156
|
+
*
|
|
157
|
+
* @deprecated Superseded by per-tool policy on {@link AgentConfig.toolPolicies}
|
|
158
|
+
* (resolved by `resolveToolApprovalPolicy`) plus {@link StreamOptions.batchApprovals}
|
|
159
|
+
* for the UX axis. Kept functional for one release so harness PRs can migrate
|
|
160
|
+
* independently; removed in a future major.
|
|
118
161
|
*/
|
|
119
162
|
export declare function resolveToolApprovalMode(requireToolApproval: boolean | ToolApprovalMode | undefined): ToolApprovalMode | undefined;
|
|
120
163
|
/**
|
|
@@ -152,8 +195,33 @@ export type StreamOptions = {
|
|
|
152
195
|
* Does not affect consumer-executed tools (those defined via
|
|
153
196
|
* `AgentConfig.tools` without an execute handler) — the consumer
|
|
154
197
|
* already controls execution for those via `submitToolResult()`.
|
|
198
|
+
*
|
|
199
|
+
* @deprecated Per-call all-or-nothing gating is superseded by per-tool
|
|
200
|
+
* policy on {@link AgentConfig.toolPolicies}. The serial-vs-batch UX axis
|
|
201
|
+
* moved to {@link batchApprovals}. Kept functional for one release so
|
|
202
|
+
* harness PRs can migrate independently; removed in a future major. New
|
|
203
|
+
* code should configure `AgentConfig.toolPolicies` and, if a batch UX is
|
|
204
|
+
* wanted, set `batchApprovals: true`.
|
|
155
205
|
*/
|
|
156
206
|
requireToolApproval?: boolean | ToolApprovalMode;
|
|
207
|
+
/**
|
|
208
|
+
* When `true`, parallel approval-requests within a turn (the model emits
|
|
209
|
+
* multiple `tool_use` blocks in one batch, more than one of which resolves
|
|
210
|
+
* to `'require-approval'`) surface on the same stream so the consumer can
|
|
211
|
+
* render a batch approval card. The consumer's iterator MUST drain to
|
|
212
|
+
* natural park before settling.
|
|
213
|
+
*
|
|
214
|
+
* Defaults to `false` (serial: one approval-request at a time; the consumer
|
|
215
|
+
* settles before the next surfaces). Identical to pre-#447 behavior under
|
|
216
|
+
* `requireToolApproval: true`.
|
|
217
|
+
*
|
|
218
|
+
* Has no effect when no tool in the turn resolves to `'require-approval'` —
|
|
219
|
+
* there is nothing to batch. This is the self-documenting replacement for
|
|
220
|
+
* the UX half of the deprecated {@link requireToolApproval} enum; the
|
|
221
|
+
* "gating on/off" half moves to per-tool policy on
|
|
222
|
+
* {@link AgentConfig.toolPolicies}.
|
|
223
|
+
*/
|
|
224
|
+
batchApprovals?: boolean;
|
|
157
225
|
/**
|
|
158
226
|
* Maximum number of LLM call steps the agent may take per `stream()` invocation.
|
|
159
227
|
* Each step is one LLM call (which may produce text, tool calls, or both).
|
|
@@ -35,6 +35,11 @@ export function toHarnessConfig(config, orgJwt) {
|
|
|
35
35
|
* - `true` → `'serial'` (back-compat shorthand for the original `boolean` shape).
|
|
36
36
|
* - `'serial'` → `'serial'` (explicit, equivalent to `true`).
|
|
37
37
|
* - `'batch'` → `'batch'`.
|
|
38
|
+
*
|
|
39
|
+
* @deprecated Superseded by per-tool policy on {@link AgentConfig.toolPolicies}
|
|
40
|
+
* (resolved by `resolveToolApprovalPolicy`) plus {@link StreamOptions.batchApprovals}
|
|
41
|
+
* for the UX axis. Kept functional for one release so harness PRs can migrate
|
|
42
|
+
* independently; removed in a future major.
|
|
38
43
|
*/
|
|
39
44
|
export function resolveToolApprovalMode(requireToolApproval) {
|
|
40
45
|
if (requireToolApproval === undefined || requireToolApproval === false)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type { Message, MessagePart, ImagePart, FilePart } from './types/messages.js';
|
|
2
2
|
export type { ChatEvent, StartEvent, TextDeltaEvent, ReasoningDeltaEvent, ToolCallEvent, ToolCallDeltaEvent, ToolApprovalRequestEvent, ToolResultEvent, ToolProgressEvent, StepStartEvent, StepFinishEvent, ErrorEvent, FinishEvent, ChatStreamResult, } from './types/events.js';
|
|
3
|
-
export type { ToolDefinition, ToolCallInfo, ToolResultInfo } from './types/tools.js';
|
|
3
|
+
export type { Decision, ToolDefinition, ToolCallInfo, ToolMatcher, ToolPolicyRule, ToolResultInfo, } from './types/tools.js';
|
|
4
|
+
export { BUILT_IN_TOOL_POLICIES, SKILL_BRIDGE_SERVER_ID, definePolicy, matcherMatches, resolveToolApprovalPolicy, } from './policy-resolver.js';
|
|
5
|
+
export type { ResolverResult, ResolverTiers, ToolInvocation } from './policy-resolver.js';
|
|
4
6
|
export type { ContextUsage, FinishReason, UsageMetadata } from './types/usage.js';
|
|
5
7
|
export type { AgentHooks, HooksForAgent, ToolResultRedactor, ToolResultRedactionInput, ToolResultRedactionResult, } from './types/redaction.js';
|
|
6
8
|
export type { AgentConfig, HarnessAgentConfig, StreamOptions, ToolApprovalMode } from './harness/harness-config.js';
|
|
@@ -21,7 +23,7 @@ export type { LlmRequestEvent, LlmResponseEvent, WireCommunicationEvent, WireCom
|
|
|
21
23
|
export { WireCommunicationFileWriter, type WireCommunicationEmitter, type WireCommunicationFileWriterOptions, } from './wire-communication-file-writer.js';
|
|
22
24
|
export type { AgentHarness, HarnessFactory, WithAgentConfig, ConfigOf } from './harness/index.js';
|
|
23
25
|
export { AgentSDKError, AgentSDKErrorType } from './errors.js';
|
|
24
|
-
export type { AgentCreatedEvent, AgentDestroyedEvent, ChatStreamCompletedEvent, ChatStreamErrorEvent, ChatStreamStartedEvent, ChatStreamTrigger, McpServerDiscoveryCompletedEvent, McpServerDiscoveryFailedEvent, McpServerDiscoveryStartedEvent, McpServerStatusChangedEvent, SessionCreatedEvent, SessionDestroyedEvent, TelemetryEvent, TelemetryEventCallback, ToolApprovalRequestedEvent, ToolApprovalResolvedEvent, ToolExecutionCompletedEvent, ToolExecutionStartedEvent, } from './types/telemetry-events.js';
|
|
26
|
+
export type { AgentCreatedEvent, AgentDestroyedEvent, ChatStreamCompletedEvent, ChatStreamErrorEvent, ChatStreamStartedEvent, ChatStreamTrigger, McpServerDiscoveryCompletedEvent, McpServerDiscoveryFailedEvent, McpServerDiscoveryStartedEvent, McpServerStatusChangedEvent, SessionCreatedEvent, SessionDestroyedEvent, TelemetryEvent, TelemetryEventCallback, ToolApprovalPolicyResolvedEvent, ToolApprovalRequestedEvent, ToolApprovalResolvedEvent, ToolExecutionCompletedEvent, ToolExecutionStartedEvent, } from './types/telemetry-events.js';
|
|
25
27
|
export type { LogLevel, LogRecord, Unsubscribe } from '@salesforce/agentic-common';
|
|
26
28
|
export { resolveMcpServerHeaders } from './mcp-auth.js';
|
|
27
29
|
export type { OrgConnection, OrgConnectionFactory } from '@salesforce/agentic-common';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
3
|
* See LICENSE.txt for license terms.
|
|
4
4
|
*/
|
|
5
|
+
// ── Tool-approval policy ─────────────────────────────────────────────
|
|
6
|
+
// The resolver, the SDK-shipped cross-harness rule set, the matcher, the
|
|
7
|
+
// ergonomics helper, and the well-known skill-bridge serverId. Harnesses wire
|
|
8
|
+
// `resolveToolApprovalPolicy` into their gate sites (Phase 2); consumers author
|
|
9
|
+
// `AgentConfig.toolPolicies` (directly or via `definePolicy`).
|
|
10
|
+
export { BUILT_IN_TOOL_POLICIES, SKILL_BRIDGE_SERVER_ID, definePolicy, matcherMatches, resolveToolApprovalPolicy, } from './policy-resolver.js';
|
|
5
11
|
export { DEFAULT_MAX_STEPS, resolveToolApprovalMode } from './harness/harness-config.js';
|
|
6
12
|
export { McpServerStatus, mcpServerConfigEqual } from './mcp-config.js';
|
|
7
13
|
export { Model, ModelName, createClaudeModel, Models, validateMultimodalFiles } from './models/index.js';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { McpToolAnnotations } from './mcp-config.js';
|
|
2
|
+
import type { AgentConfig } from './harness/harness-config.js';
|
|
3
|
+
import type { Decision, ToolMatcher, ToolPolicyRule } from './types/tools.js';
|
|
4
|
+
/**
|
|
5
|
+
* Well-known MCP server identity for the capability-discovery meta-tools
|
|
6
|
+
* (`search_tools`, `load_tool`, `search_skills`, `load_skill`). Both harnesses
|
|
7
|
+
* expose these tools through this serverId — Claude via its in-process
|
|
8
|
+
* skill-bridge MCP server, Mastra gains it as a Phase 2a prerequisite
|
|
9
|
+
* ([#606](https://github.com/forcedotcom/agentic-dx/issues/606)). The
|
|
10
|
+
* `mcp:skill_bridge:*` rules in {@link BUILT_IN_TOOL_POLICIES} anchor on this
|
|
11
|
+
* constant so operator-side rule authors and harness implementations share one
|
|
12
|
+
* identity rather than each hard-coding the string.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SKILL_BRIDGE_SERVER_ID = "skill_bridge";
|
|
15
|
+
/**
|
|
16
|
+
* A single tool invocation the resolver decides on. The harness strips its own
|
|
17
|
+
* tool-name namespacing before constructing this — `serverName` / `toolName`
|
|
18
|
+
* are the un-namespaced MCP pair, `toolName` alone is the built-in name.
|
|
19
|
+
*/
|
|
20
|
+
export type ToolInvocation = {
|
|
21
|
+
/** The bare tool name (un-namespaced for MCP tools). */
|
|
22
|
+
toolName: string;
|
|
23
|
+
/** Originating MCP server name; `undefined` for built-in / consumer tools. */
|
|
24
|
+
serverName?: string;
|
|
25
|
+
/** Discovered MCP annotations, when the server declared them. */
|
|
26
|
+
annotations?: McpToolAnnotations;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Optional rule tiers supplied by the harness layer.
|
|
30
|
+
*
|
|
31
|
+
* The shape extends additively when future tiers ship: a future
|
|
32
|
+
* `tiers.managed` slot will sit between consumer rules and `'remember'` rules
|
|
33
|
+
* without reshaping this type or the resolver signature.
|
|
34
|
+
*/
|
|
35
|
+
export type ResolverTiers = {
|
|
36
|
+
/** Harness-shipped built-in rules (e.g. `MASTRA_BUILT_IN_TOOL_POLICIES`). */
|
|
37
|
+
harness?: ReadonlyArray<ToolPolicyRule>;
|
|
38
|
+
/**
|
|
39
|
+
* Synthetic rules from harness factory inputs during a deprecation window.
|
|
40
|
+
* Today: Claude's `bypassApprovalTools` translation. Empty after Phase 4.
|
|
41
|
+
*/
|
|
42
|
+
factory?: ReadonlyArray<ToolPolicyRule>;
|
|
43
|
+
};
|
|
44
|
+
/** The outcome of resolving a tool invocation against the concatenated rule list. */
|
|
45
|
+
export type ResolverResult = {
|
|
46
|
+
/** The resolved decision. */
|
|
47
|
+
decision: Decision;
|
|
48
|
+
/**
|
|
49
|
+
* The rule that drove the decision: the last `'deny'` when the decision is
|
|
50
|
+
* `'deny'`, otherwise the last matching non-deny rule. `undefined` when no
|
|
51
|
+
* rule matched and the decision came from `defaultToolDecision`.
|
|
52
|
+
*/
|
|
53
|
+
matchedRule?: ToolPolicyRule;
|
|
54
|
+
/** Every matching rule, in resolved-list order. Debug verbosity for sinks. */
|
|
55
|
+
allMatchedRules?: ToolPolicyRule[];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Cross-harness tool-approval rules shipped with the SDK. Frozen — adding
|
|
59
|
+
* entries is a soft contract change consumers will have noticed; removing or
|
|
60
|
+
* modifying an entry is a breaking change.
|
|
61
|
+
*
|
|
62
|
+
* Per the R4 layering invariant, this array holds **only harness-agnostic
|
|
63
|
+
* rules** — rules meaningful regardless of which harness is loaded. Harness-
|
|
64
|
+
* specific built-ins (Claude's `Bash`, Mastra's `updateWorkingMemory`) live in
|
|
65
|
+
* the respective harness package's `<HARNESS>_BUILT_IN_TOOL_POLICIES` array.
|
|
66
|
+
*/
|
|
67
|
+
export declare const BUILT_IN_TOOL_POLICIES: ReadonlyArray<ToolPolicyRule>;
|
|
68
|
+
/**
|
|
69
|
+
* Tests a single matcher against a tool invocation. Pure; one switch on
|
|
70
|
+
* `matcher.type`.
|
|
71
|
+
*
|
|
72
|
+
* - `'builtin'` matches when the invocation has no `serverName` and its
|
|
73
|
+
* `toolName` equals the matcher name. (MCP tools always carry a `serverName`,
|
|
74
|
+
* so a built-in matcher never matches one even if the bare names collide.)
|
|
75
|
+
* - `'mcp'` matches an invocation that has a `serverName`, optionally narrowed
|
|
76
|
+
* by the matcher's `serverName` / `toolName` (each `undefined` field is a
|
|
77
|
+
* wildcard).
|
|
78
|
+
* - `'mcp-annotation'` matches an MCP invocation whose `annotations` carry
|
|
79
|
+
* every hint set on the matcher. An invocation with no annotations never
|
|
80
|
+
* matches.
|
|
81
|
+
*/
|
|
82
|
+
export declare function matcherMatches(matcher: ToolMatcher, invocation: ToolInvocation): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Resolves the approval decision for a tool invocation.
|
|
85
|
+
*
|
|
86
|
+
* Concatenates the four rule tiers in precedence order
|
|
87
|
+
* `[...BUILT_IN_TOOL_POLICIES, ...tiers.harness, ...tiers.factory, ...agentConfig.toolPolicies]`
|
|
88
|
+
* and applies **cross-tier deny-wins / within-tier last-wins**:
|
|
89
|
+
*
|
|
90
|
+
* - If any matching rule is a `'deny'`, the decision is `'deny'` and
|
|
91
|
+
* `matchedRule` is the **last** `'deny'` (closest-to-consumer — most useful
|
|
92
|
+
* for "why was my tool blocked" debugging).
|
|
93
|
+
* - Otherwise, the **last** matching non-deny rule decides.
|
|
94
|
+
* - If nothing matched, the decision is `agentConfig.defaultToolDecision`, or
|
|
95
|
+
* `'allow'` when that is unset (back-compat: "no policy ⇒ no gating").
|
|
96
|
+
*
|
|
97
|
+
* **Pure function** — no I/O, no logging, no side effects. The unmatched-rule
|
|
98
|
+
* `LogBus.warn` is the harness's responsibility (after observing
|
|
99
|
+
* `matchedRule === undefined`), keeping the resolver dependency-free and
|
|
100
|
+
* trivially testable.
|
|
101
|
+
*/
|
|
102
|
+
export declare function resolveToolApprovalPolicy(invocation: ToolInvocation, agentConfig: AgentConfig, tiers?: ResolverTiers): ResolverResult;
|
|
103
|
+
/**
|
|
104
|
+
* Compresses the common policy cases into a `ToolPolicyRule[]` with
|
|
105
|
+
* `source: 'agent-config'`. Each key is a tool selector, each value its
|
|
106
|
+
* {@link Decision}:
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* definePolicy({
|
|
110
|
+
* Bash: 'deny', // builtin matcher
|
|
111
|
+
* 'mcp:sfdx': 'require-approval', // every tool from the 'sfdx' MCP server
|
|
112
|
+
* 'mcp:sfdx/list_orgs': 'allow', // one tool from the 'sfdx' server
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* Key grammar:
|
|
117
|
+
* - `'<name>'` → `{ type: 'builtin', name }`.
|
|
118
|
+
* - `'mcp:<server>'` → `{ type: 'mcp', serverName }`.
|
|
119
|
+
* - `'mcp:<server>/<tool>'` → `{ type: 'mcp', serverName, toolName }`.
|
|
120
|
+
*
|
|
121
|
+
* Consumers needing `mcp-annotation` matchers, the bare `{ type: 'mcp' }`
|
|
122
|
+
* server-agnostic wildcard, or any tool name containing `/` must author the
|
|
123
|
+
* structured {@link ToolPolicyRule} form directly — the shorthand intentionally
|
|
124
|
+
* covers only the simple `builtin` and `mcp:server[/tool]` cases.
|
|
125
|
+
*/
|
|
126
|
+
export declare function definePolicy(spec: Record<string, Decision>): ToolPolicyRule[];
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Well-known MCP server identity for the capability-discovery meta-tools
|
|
7
|
+
* (`search_tools`, `load_tool`, `search_skills`, `load_skill`). Both harnesses
|
|
8
|
+
* expose these tools through this serverId — Claude via its in-process
|
|
9
|
+
* skill-bridge MCP server, Mastra gains it as a Phase 2a prerequisite
|
|
10
|
+
* ([#606](https://github.com/forcedotcom/agentic-dx/issues/606)). The
|
|
11
|
+
* `mcp:skill_bridge:*` rules in {@link BUILT_IN_TOOL_POLICIES} anchor on this
|
|
12
|
+
* constant so operator-side rule authors and harness implementations share one
|
|
13
|
+
* identity rather than each hard-coding the string.
|
|
14
|
+
*/
|
|
15
|
+
export const SKILL_BRIDGE_SERVER_ID = 'skill_bridge';
|
|
16
|
+
/**
|
|
17
|
+
* Cross-harness tool-approval rules shipped with the SDK. Frozen — adding
|
|
18
|
+
* entries is a soft contract change consumers will have noticed; removing or
|
|
19
|
+
* modifying an entry is a breaking change.
|
|
20
|
+
*
|
|
21
|
+
* Per the R4 layering invariant, this array holds **only harness-agnostic
|
|
22
|
+
* rules** — rules meaningful regardless of which harness is loaded. Harness-
|
|
23
|
+
* specific built-ins (Claude's `Bash`, Mastra's `updateWorkingMemory`) live in
|
|
24
|
+
* the respective harness package's `<HARNESS>_BUILT_IN_TOOL_POLICIES` array.
|
|
25
|
+
*/
|
|
26
|
+
export const BUILT_IN_TOOL_POLICIES = Object.freeze([
|
|
27
|
+
// MCP annotation-driven defaults. Under within-tier last-wins, a consumer
|
|
28
|
+
// rule appended later beats these — so a "remember: allow" on a
|
|
29
|
+
// destructiveHint tool actually takes effect.
|
|
30
|
+
{ matcher: { type: 'mcp-annotation', destructiveHint: true }, decision: 'require-approval', source: 'built-in' },
|
|
31
|
+
{ matcher: { type: 'mcp-annotation', readOnlyHint: true }, decision: 'allow', source: 'built-in' },
|
|
32
|
+
// Capability-discovery meta-tools — exposed by both harnesses through the
|
|
33
|
+
// well-known SKILL_BRIDGE_SERVER_ID MCP serverId (Mastra gains this
|
|
34
|
+
// identity per #606). Closes the consumer pain that motivated AFV PR #2455.
|
|
35
|
+
{
|
|
36
|
+
matcher: { type: 'mcp', serverName: SKILL_BRIDGE_SERVER_ID, toolName: 'search_tools' },
|
|
37
|
+
decision: 'allow',
|
|
38
|
+
source: 'built-in',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
matcher: { type: 'mcp', serverName: SKILL_BRIDGE_SERVER_ID, toolName: 'load_tool' },
|
|
42
|
+
decision: 'allow',
|
|
43
|
+
source: 'built-in',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
matcher: { type: 'mcp', serverName: SKILL_BRIDGE_SERVER_ID, toolName: 'search_skills' },
|
|
47
|
+
decision: 'allow',
|
|
48
|
+
source: 'built-in',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
matcher: { type: 'mcp', serverName: SKILL_BRIDGE_SERVER_ID, toolName: 'load_skill' },
|
|
52
|
+
decision: 'allow',
|
|
53
|
+
source: 'built-in',
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
/**
|
|
57
|
+
* Tests a single matcher against a tool invocation. Pure; one switch on
|
|
58
|
+
* `matcher.type`.
|
|
59
|
+
*
|
|
60
|
+
* - `'builtin'` matches when the invocation has no `serverName` and its
|
|
61
|
+
* `toolName` equals the matcher name. (MCP tools always carry a `serverName`,
|
|
62
|
+
* so a built-in matcher never matches one even if the bare names collide.)
|
|
63
|
+
* - `'mcp'` matches an invocation that has a `serverName`, optionally narrowed
|
|
64
|
+
* by the matcher's `serverName` / `toolName` (each `undefined` field is a
|
|
65
|
+
* wildcard).
|
|
66
|
+
* - `'mcp-annotation'` matches an MCP invocation whose `annotations` carry
|
|
67
|
+
* every hint set on the matcher. An invocation with no annotations never
|
|
68
|
+
* matches.
|
|
69
|
+
*/
|
|
70
|
+
export function matcherMatches(matcher, invocation) {
|
|
71
|
+
switch (matcher.type) {
|
|
72
|
+
case 'builtin':
|
|
73
|
+
return invocation.serverName === undefined && invocation.toolName === matcher.name;
|
|
74
|
+
case 'mcp':
|
|
75
|
+
return (invocation.serverName !== undefined &&
|
|
76
|
+
(matcher.serverName === undefined || invocation.serverName === matcher.serverName) &&
|
|
77
|
+
(matcher.toolName === undefined || invocation.toolName === matcher.toolName));
|
|
78
|
+
case 'mcp-annotation': {
|
|
79
|
+
if (invocation.serverName === undefined || invocation.annotations === undefined)
|
|
80
|
+
return false;
|
|
81
|
+
const { readOnlyHint, destructiveHint } = matcher;
|
|
82
|
+
if (readOnlyHint !== undefined && invocation.annotations.readOnlyHint !== readOnlyHint)
|
|
83
|
+
return false;
|
|
84
|
+
if (destructiveHint !== undefined && invocation.annotations.destructiveHint !== destructiveHint)
|
|
85
|
+
return false;
|
|
86
|
+
// At least one hint must be set on the matcher for it to mean
|
|
87
|
+
// anything — an empty annotation matcher matching every annotated
|
|
88
|
+
// tool would be a footgun. Treat a hint-less matcher as a non-match.
|
|
89
|
+
return readOnlyHint !== undefined || destructiveHint !== undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolves the approval decision for a tool invocation.
|
|
95
|
+
*
|
|
96
|
+
* Concatenates the four rule tiers in precedence order
|
|
97
|
+
* `[...BUILT_IN_TOOL_POLICIES, ...tiers.harness, ...tiers.factory, ...agentConfig.toolPolicies]`
|
|
98
|
+
* and applies **cross-tier deny-wins / within-tier last-wins**:
|
|
99
|
+
*
|
|
100
|
+
* - If any matching rule is a `'deny'`, the decision is `'deny'` and
|
|
101
|
+
* `matchedRule` is the **last** `'deny'` (closest-to-consumer — most useful
|
|
102
|
+
* for "why was my tool blocked" debugging).
|
|
103
|
+
* - Otherwise, the **last** matching non-deny rule decides.
|
|
104
|
+
* - If nothing matched, the decision is `agentConfig.defaultToolDecision`, or
|
|
105
|
+
* `'allow'` when that is unset (back-compat: "no policy ⇒ no gating").
|
|
106
|
+
*
|
|
107
|
+
* **Pure function** — no I/O, no logging, no side effects. The unmatched-rule
|
|
108
|
+
* `LogBus.warn` is the harness's responsibility (after observing
|
|
109
|
+
* `matchedRule === undefined`), keeping the resolver dependency-free and
|
|
110
|
+
* trivially testable.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveToolApprovalPolicy(invocation, agentConfig, tiers) {
|
|
113
|
+
const allRules = [
|
|
114
|
+
...BUILT_IN_TOOL_POLICIES,
|
|
115
|
+
...(tiers?.harness ?? []),
|
|
116
|
+
...(tiers?.factory ?? []),
|
|
117
|
+
...(agentConfig.toolPolicies ?? []),
|
|
118
|
+
];
|
|
119
|
+
const matched = allRules.filter((rule) => matcherMatches(rule.matcher, invocation));
|
|
120
|
+
// Cross-tier deny-wins: any 'deny' anywhere wins. Report the last one.
|
|
121
|
+
const denyRules = matched.filter((rule) => rule.decision === 'deny');
|
|
122
|
+
if (denyRules.length > 0) {
|
|
123
|
+
return { decision: 'deny', matchedRule: denyRules[denyRules.length - 1], allMatchedRules: matched };
|
|
124
|
+
}
|
|
125
|
+
// Within-tier last-wins: the last matching non-deny rule decides.
|
|
126
|
+
if (matched.length > 0) {
|
|
127
|
+
const last = matched[matched.length - 1];
|
|
128
|
+
return { decision: last.decision, matchedRule: last, allMatchedRules: matched };
|
|
129
|
+
}
|
|
130
|
+
// Nothing matched — fall back to the agent's default (or 'allow').
|
|
131
|
+
return { decision: agentConfig.defaultToolDecision ?? 'allow' };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Compresses the common policy cases into a `ToolPolicyRule[]` with
|
|
135
|
+
* `source: 'agent-config'`. Each key is a tool selector, each value its
|
|
136
|
+
* {@link Decision}:
|
|
137
|
+
*
|
|
138
|
+
* ```ts
|
|
139
|
+
* definePolicy({
|
|
140
|
+
* Bash: 'deny', // builtin matcher
|
|
141
|
+
* 'mcp:sfdx': 'require-approval', // every tool from the 'sfdx' MCP server
|
|
142
|
+
* 'mcp:sfdx/list_orgs': 'allow', // one tool from the 'sfdx' server
|
|
143
|
+
* });
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* Key grammar:
|
|
147
|
+
* - `'<name>'` → `{ type: 'builtin', name }`.
|
|
148
|
+
* - `'mcp:<server>'` → `{ type: 'mcp', serverName }`.
|
|
149
|
+
* - `'mcp:<server>/<tool>'` → `{ type: 'mcp', serverName, toolName }`.
|
|
150
|
+
*
|
|
151
|
+
* Consumers needing `mcp-annotation` matchers, the bare `{ type: 'mcp' }`
|
|
152
|
+
* server-agnostic wildcard, or any tool name containing `/` must author the
|
|
153
|
+
* structured {@link ToolPolicyRule} form directly — the shorthand intentionally
|
|
154
|
+
* covers only the simple `builtin` and `mcp:server[/tool]` cases.
|
|
155
|
+
*/
|
|
156
|
+
export function definePolicy(spec) {
|
|
157
|
+
return Object.entries(spec).map(([key, decision]) => ({
|
|
158
|
+
matcher: parsePolicyKey(key),
|
|
159
|
+
decision,
|
|
160
|
+
source: 'agent-config',
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
/** Parses a {@link definePolicy} key into its structured {@link ToolMatcher}. */
|
|
164
|
+
function parsePolicyKey(key) {
|
|
165
|
+
if (!key.startsWith('mcp:')) {
|
|
166
|
+
return { type: 'builtin', name: key };
|
|
167
|
+
}
|
|
168
|
+
const rest = key.slice('mcp:'.length);
|
|
169
|
+
const slash = rest.indexOf('/');
|
|
170
|
+
if (slash === -1) {
|
|
171
|
+
return { type: 'mcp', serverName: rest };
|
|
172
|
+
}
|
|
173
|
+
return { type: 'mcp', serverName: rest.slice(0, slash), toolName: rest.slice(slash + 1) };
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=policy-resolver.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EventBus } from '@salesforce/agentic-common';
|
|
2
2
|
import type { McpServerErrorDetail, McpServerStatus, McpToolAnnotations } from '../mcp-config.js';
|
|
3
|
+
import type { Decision, ToolPolicyRule } from './tools.js';
|
|
3
4
|
import type { UsageMetadata } from './usage.js';
|
|
4
5
|
/**
|
|
5
6
|
* Telemetry events emitted by the Agent SDK.
|
|
@@ -85,6 +86,46 @@ export type ToolApprovalResolvedEvent = Base<'tool-approval-resolved'> & {
|
|
|
85
86
|
threadId: string;
|
|
86
87
|
toolCallId: string;
|
|
87
88
|
approved: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* `true` when the settle's `remember` flag triggered an
|
|
91
|
+
* `updateAgentConfig` call that appended a `'remember'` policy rule.
|
|
92
|
+
* Absent / `false` when the settle did not persist a rule.
|
|
93
|
+
*/
|
|
94
|
+
policyWritten?: boolean;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Emitted by a harness on every harness-executed tool call, after
|
|
98
|
+
* `resolveToolApprovalPolicy` decides and before any gate work happens.
|
|
99
|
+
* Lets telemetry sinks answer "why did this tool prompt / not prompt / get
|
|
100
|
+
* denied?" by reading the rule that fired.
|
|
101
|
+
*
|
|
102
|
+
* Emitted by the harness layer (Phase 2), not the SDK — the SDK exports the
|
|
103
|
+
* variant so the public `TelemetryEvent` union is complete before the harness
|
|
104
|
+
* PRs land.
|
|
105
|
+
*/
|
|
106
|
+
export type ToolApprovalPolicyResolvedEvent = Base<'tool-approval-policy-resolved'> & {
|
|
107
|
+
agentId: string;
|
|
108
|
+
threadId: string;
|
|
109
|
+
toolCallId: string;
|
|
110
|
+
toolName: string;
|
|
111
|
+
/** Originating MCP server name, when the tool was discovered through MCP. */
|
|
112
|
+
serverName?: string;
|
|
113
|
+
/** The resolved decision. */
|
|
114
|
+
decision: Decision;
|
|
115
|
+
/**
|
|
116
|
+
* The rule that drove the decision under cross-tier-deny-wins /
|
|
117
|
+
* within-tier-last-wins precedence (the last `'deny'` when denied, else the
|
|
118
|
+
* last matching non-deny rule). `undefined` when no rule matched and the
|
|
119
|
+
* resolver fell back to `defaultToolDecision ?? 'allow'`; the harness emits
|
|
120
|
+
* a `LogBus.warn` in that case.
|
|
121
|
+
*/
|
|
122
|
+
matchedRule?: ToolPolicyRule;
|
|
123
|
+
/**
|
|
124
|
+
* Every matching rule in resolved-list order. Optional debug verbosity for
|
|
125
|
+
* sinks that surface "remembered allow overridden by built-in deny"
|
|
126
|
+
* scenarios; most consumers use {@link matchedRule}.
|
|
127
|
+
*/
|
|
128
|
+
allMatchedRules?: ToolPolicyRule[];
|
|
88
129
|
};
|
|
89
130
|
export type McpServerDiscoveryStartedEvent = Base<'mcp-server-discovery-started'> & {
|
|
90
131
|
agentId: string;
|
|
@@ -137,7 +178,7 @@ export type McpServerStatusChangedEvent = Base<'mcp-server-status-changed'> & {
|
|
|
137
178
|
/** Populated when `nextStatus === 'error'`. */
|
|
138
179
|
error?: string;
|
|
139
180
|
};
|
|
140
|
-
export type TelemetryEvent = AgentCreatedEvent | AgentDestroyedEvent | SessionCreatedEvent | SessionDestroyedEvent | ChatStreamStartedEvent | ChatStreamCompletedEvent | ChatStreamErrorEvent | ToolExecutionStartedEvent | ToolExecutionCompletedEvent | ToolApprovalRequestedEvent | ToolApprovalResolvedEvent | McpServerDiscoveryStartedEvent | McpServerDiscoveryCompletedEvent | McpServerDiscoveryFailedEvent | McpServerStatusChangedEvent;
|
|
181
|
+
export type TelemetryEvent = AgentCreatedEvent | AgentDestroyedEvent | SessionCreatedEvent | SessionDestroyedEvent | ChatStreamStartedEvent | ChatStreamCompletedEvent | ChatStreamErrorEvent | ToolExecutionStartedEvent | ToolExecutionCompletedEvent | ToolApprovalRequestedEvent | ToolApprovalResolvedEvent | ToolApprovalPolicyResolvedEvent | McpServerDiscoveryStartedEvent | McpServerDiscoveryCompletedEvent | McpServerDiscoveryFailedEvent | McpServerStatusChangedEvent;
|
|
141
182
|
export type TelemetryEventCallback = (event: TelemetryEvent) => void;
|
|
142
183
|
export type TelemetryBus = EventBus<TelemetryEvent>;
|
|
143
184
|
export {};
|
package/dist/types/tools.d.ts
CHANGED
|
@@ -46,6 +46,89 @@ export type ToolDefinition = {
|
|
|
46
46
|
*/
|
|
47
47
|
annotations?: McpToolAnnotations;
|
|
48
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* The policy decision for a single tool invocation.
|
|
51
|
+
*
|
|
52
|
+
* - `'allow'` — the harness executes the tool without pausing.
|
|
53
|
+
* - `'deny'` — the harness refuses the call and synthesizes a
|
|
54
|
+
* `tool-result(isError=true)` so the model can recover; no
|
|
55
|
+
* `tool-approval-request` surfaces.
|
|
56
|
+
* - `'require-approval'` — the harness emits a `tool-approval-request`
|
|
57
|
+
* and suspends until the consumer settles via `approveToolCall` /
|
|
58
|
+
* `declineToolCall`.
|
|
59
|
+
*/
|
|
60
|
+
export type Decision = 'allow' | 'deny' | 'require-approval';
|
|
61
|
+
/**
|
|
62
|
+
* Structured matcher selecting which tool invocations a {@link ToolPolicyRule}
|
|
63
|
+
* applies to.
|
|
64
|
+
*
|
|
65
|
+
* The three shapes cover every tool category a harness gates:
|
|
66
|
+
*
|
|
67
|
+
* - `'builtin'` — harness-built-in tools (e.g. Claude's `Bash` / `Edit`;
|
|
68
|
+
* Mastra's `updateWorkingMemory`). Names are **harness-specific**, so a
|
|
69
|
+
* `'builtin'` rule applies asymmetrically across harnesses — `Bash` matches
|
|
70
|
+
* on Claude and is a no-op on Mastra. A `'builtin'` matcher matches only when
|
|
71
|
+
* the invocation has no `serverName` (MCP tools carry one).
|
|
72
|
+
* - `'mcp'` — MCP-discovered tools. Both `serverName` and `toolName` are
|
|
73
|
+
* optional: omitting `toolName` matches every tool from the given server;
|
|
74
|
+
* omitting `serverName` matches every tool from every MCP server; omitting
|
|
75
|
+
* both matches every MCP tool (a server-agnostic catch-all). MCP rules are
|
|
76
|
+
* **portable** — the same rule fires on both harnesses for the same tool.
|
|
77
|
+
* - `'mcp-annotation'` — matches any MCP tool whose discovered
|
|
78
|
+
* {@link McpToolAnnotations} carry the specified hint(s). Every field set on
|
|
79
|
+
* the matcher must equal the corresponding field on the discovered
|
|
80
|
+
* annotation (logical AND across set fields). A tool with no annotations does
|
|
81
|
+
* **not** match — the resolver falls through. This is fail-open by default;
|
|
82
|
+
* tenants needing fail-closed for un-annotated MCP servers set
|
|
83
|
+
* {@link AgentConfig.defaultToolDecision} to `'require-approval'`.
|
|
84
|
+
*
|
|
85
|
+
* There is intentionally no `consumer` matcher. Consumer-declared tools (those
|
|
86
|
+
* in {@link AgentConfig.tools} without an `execute`) are gated structurally at
|
|
87
|
+
* the harness boundary and never reach the resolver, so a matcher variant for
|
|
88
|
+
* them would be API surface for an unreachable case.
|
|
89
|
+
*/
|
|
90
|
+
export type ToolMatcher = {
|
|
91
|
+
type: 'builtin';
|
|
92
|
+
name: string;
|
|
93
|
+
} | {
|
|
94
|
+
type: 'mcp';
|
|
95
|
+
serverName?: string;
|
|
96
|
+
toolName?: string;
|
|
97
|
+
} | {
|
|
98
|
+
type: 'mcp-annotation';
|
|
99
|
+
readOnlyHint?: boolean;
|
|
100
|
+
destructiveHint?: boolean;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* One entry in an agent's tool-approval policy list.
|
|
104
|
+
*
|
|
105
|
+
* Resolution concatenates the SDK's `BUILT_IN_TOOL_POLICIES`, the harness's
|
|
106
|
+
* built-in array, any harness factory rules, and `AgentConfig.toolPolicies`,
|
|
107
|
+
* then applies **cross-tier deny-wins / within-tier last-wins** precedence (see
|
|
108
|
+
* `resolveToolApprovalPolicy`).
|
|
109
|
+
*/
|
|
110
|
+
export type ToolPolicyRule = {
|
|
111
|
+
/** Selects which tool invocations this rule applies to. */
|
|
112
|
+
matcher: ToolMatcher;
|
|
113
|
+
/** The decision to apply when {@link matcher} matches. */
|
|
114
|
+
decision: Decision;
|
|
115
|
+
/**
|
|
116
|
+
* Where this rule originated. Advisory metadata only — the resolver
|
|
117
|
+
* **ignores** `source` when computing a decision; telemetry surfaces it so
|
|
118
|
+
* operators can answer "which tier fired."
|
|
119
|
+
*
|
|
120
|
+
* - `'built-in'` — shipped by the SDK's `BUILT_IN_TOOL_POLICIES` or a
|
|
121
|
+
* harness package's `<HARNESS>_BUILT_IN_TOOL_POLICIES` array.
|
|
122
|
+
* - `'agent-config'` — authored by whoever wrote the `AgentConfig`. The
|
|
123
|
+
* default when unset on a consumer-supplied rule.
|
|
124
|
+
* - `'remember'` — written by `approveToolCall(id, { remember: true })` /
|
|
125
|
+
* `declineToolCall(id, { remember: true })`.
|
|
126
|
+
*
|
|
127
|
+
* v1 ships these three values. A future tenant-managed-policy tier will add
|
|
128
|
+
* `'managed'` to this union; that widening is strictly additive.
|
|
129
|
+
*/
|
|
130
|
+
source?: 'built-in' | 'agent-config' | 'remember';
|
|
131
|
+
};
|
|
49
132
|
/**
|
|
50
133
|
* Base shape for a tool call.
|
|
51
134
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Harness-agnostic agentic infrastructure for Salesforce developer experience tooling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@eslint/js": "^10.0.1",
|
|
47
|
-
"@salesforce/sfdx-agent-harness-claude": "0.
|
|
48
|
-
"@salesforce/sfdx-agent-harness-mastra": "0.
|
|
47
|
+
"@salesforce/sfdx-agent-harness-claude": "0.23.0",
|
|
48
|
+
"@salesforce/sfdx-agent-harness-mastra": "0.26.0",
|
|
49
49
|
"@types/node": "^22.20.0",
|
|
50
50
|
"@vitest/coverage-istanbul": "^4.1.8",
|
|
51
51
|
"@vitest/eslint-plugin": "^1.6.20",
|