npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@salesforce/sfdx-agent-sdk

v0.51.0

Published

Harness-agnostic agentic infrastructure for Salesforce developer experience tooling

Downloads

5,614

Readme

@salesforce/sfdx-agent-sdk

Harness-agnostic SDK for creating and managing AI agents within the Salesforce developer experience. It provides a stable API for agent lifecycle, chat sessions, streaming responses, MCP tool integration, and tool approval flows — without coupling consumer code to a specific AI framework.

Quick Start

Closed source. This package is published to npm under the Salesforce Public Code License and is for use by Salesforce only.

import { createAgentManager } from '@salesforce/sfdx-agent-sdk';
import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';

// 1. Create the manager. This validates the storage folder, gates the harness's
//    protocol version, and replays any agents the SDK persisted on a prior run
//    (one JSON file per agent under `${storageRootFolder}/agents/`).
const manager = await createAgentManager('/path/to/storage', new MastraHarnessFactory());

// Bridge SDK logs into your host logger so you observe restore failures + warnings.
manager.onLog((record) => {
  console[record.level](record.message, record.context);
});

// Boot-time restore failures are queryable as instance state — use them to seed
// per-agent UI state, not for logging (the SDK already emitted each via onLog).
for (const failure of manager.getRestoreFailures()) {
  // mark `failure.agentId` as `error` in your application state
}

// 2. Create an agent. The identity triple `{ agentId, projectRoot, config }` is
//    persisted to disk; the next call to `createAgentManager` over the same
//    storage folder will replay this agent automatically.
const agent = await manager.createAgent('/path/to/project', {
  agentId: 'developer-assistant',
  modelId: 'sfdc_ai__DefaultGPT5',
  instructions: 'You are a helpful Salesforce developer assistant.',
});

// 3. Open a chat session and stream a response
const session = await agent.createChatSession();
const { eventStream } = await session.chat('Explain Lightning Web Components.');

for await (const event of eventStream) {
  if (event.type === 'text-delta') {
    process.stdout.write(event.text);
  } else if (event.type === 'error') {
    console.error(event.error.message);
    // Don't break — the SDK invariant guarantees a synthetic
    // `FinishEvent('error')` follows; the loop exits naturally next tick.
  }
}

// 4. Shut down. The harness is torn down; persisted identity files are NOT
//    removed, so a subsequent `createAgentManager` call restores them.
await manager.shutdown();

API Reference

createAgentManager<F>(storageRootFolder, harnessFactory, options?): Promise<AgentManager<H>>

Factory function that creates an AgentManager backed by the provided HarnessFactory. The storageRootFolder must be an existing directory and is used for persistent state (the harness's runtime data plus the SDK's per-agent identity files at ${storageRootFolder}/agents/<id>.json). The SDK verifies that the constructed harness uses a supported protocol version, replays any persisted agents the harness can still serve, and returns the manager.

The third-positional options bag carries per-manager opt-ins. Production callers typically leave it unset:

| Option | Type | Purpose | | ---------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | connectivityResolver | AgentConnectivityResolver | Overrides the default sf-CLI-based org resolution — used by e2e tests and custom-auth deployments. | | hooksForAgent | HooksForAgent | Sync callback resolving a per-agent AgentHooks bag (today carries onToolResult). Invoked once per createAgent, boot-time restore, and Agent.updateAgentConfig. See "Tool-Result Redaction" below. |

The harness type H is inferred from the factory's create() return type, so consumers don't pass an explicit type argument:

import { MastraHarnessFactory } from '@salesforce/sfdx-agent-harness-mastra';

const manager = await createAgentManager(storageRoot, new MastraHarnessFactory());
//    ^? AgentManager<MastraAgentHarness>

When the factory's create() is typed as Promise<AgentHarness> (the default), the manager is AgentManager<AgentHarness> and behaves exactly as before. Harness packages that ship a branded subtype (e.g. MastraAgentHarness) lift consumers into that subtype automatically — see "Harness Extensibility" below.

Restore failures (a persisted record the SDK could not bring back online — e.g. missing project directory, harness rejection, thread rehydration failure) are queryable on the returned manager via getRestoreFailures(). Soft skips inside the persistence directory (corrupt JSON, harness-id mismatch) are silently dropped from the restore pass and emit a warn on the SDK's log bus.

AgentManager<H extends AgentHarness = AgentHarness>

Top-level orchestrator that owns the harness and manages agent lifecycle. AgentManager is an interface; the concrete implementation is internal — createAgentManager is the only public entry point.

The optional H type parameter (default AgentHarness) lets harness-aware consumers reach harness-specific features through a typed manager.extensions slot. createAgentManager infers H from the factory; you usually don't write it explicitly. The createAgent config parameter narrows automatically when the harness brands itself with WithAgentConfig — see "Harness Extensibility" below.

| Property / Method | Signature | Description | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | extensions | H['extensions'] | Harness-specific extensions namespace (read-only). Re-exposes the harness's extensions slot typed off H. Per-agent accessors take the agent id as their first argument. The SDK never reads or interprets this — see "Harness Extensibility" below. | | createAgent | (projectRoot: string, config?: ConfigOf<H> & { agentId?: string }, options?: { abortSignal?: AbortSignal }) => Promise<Agent<H>> | Create and register a new agent and persist its identity triple. projectRoot must be an existing directory. If agentId is omitted a UUID is generated. The config type is inferred from the harness — see ConfigOf<H>. | | getAgent | (agentId: string) => Agent<H> | Retrieve a live agent by ID. Throws AgentSDKError (AGENT_NOT_FOUND) for unknown ids and for ids that are only present in getRestoreFailures(). | | getAgentIds | () => string[] | List all live agent IDs (successful + successfully restored). Failed-restore agents are not included — query getRestoreFailures() separately. | | destroyAgent | (agentId: string) => Promise<void> | Destroy an agent, remove its identity record from disk, and clear any matching getRestoreFailures() entry. Failed-restore-only ids are accepted (no harness call made). | | shutdown | () => Promise<void> | Destroy all live agents and shut down the harness. Identity files survive (that's the whole point) — restart createAgentManager over the same root to bring them back. | | onTelemetry | (callback: TelemetryEventCallback) => Unsubscribe | Subscribe to telemetry across all managed agents. | | onLog | (callback: (record: LogRecord) => void) => Unsubscribe | Subscribe to structured logs across all managed agents. Bridge this into your host logger to observe restore-failure events + soft-skip warnings. | | onWireCommunication | (callback: WireCommunicationEventCallback) => Unsubscribe | Subscribe to wire-level communication events from the harness. Opt-in diagnostic channel that surfaces outbound LLM requests, responses, and harness-specific monitoring metadata. Subscriber-gated end-to-end — harnesses pay no cost when nobody listens. See "Wire-Communication Events" below for the event-shape catalog and the harness-asymmetric coverage (Mastra emits per-call request/response pairs; Claude emits a per-stream pointer to a debug log file). | | getRestoreFailures | () => RestoreFailure[] | Snapshot of agents the SDK could not restore on this boot. Each entry carries the persisted { agentId, projectRoot, config } plus the underlying error. |

RestoreFailure

type RestoreFailure = {
  agentId: string;
  projectRoot: string;
  config: AgentConfig;
  error: unknown;
};

Returned by AgentManager.getRestoreFailures(). Use it to seed error-state placeholders in your application; do not iterate it for logging — the SDK already emitted each failure via onLog at error level during the restore pass, before this function returned.

Agent<H extends AgentHarness = AgentHarness>

A configured AI agent. Factory for chat sessions. The optional H type parameter is currently informational on Agent — harness-specific features are reached through manager.extensions, not agent.extensions. The default AgentHarness keeps unparameterized call sites working.

| Method | Signature | Description | | -------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | getId | () => string | Agent identifier. | | getProjectRoot | () => string | Absolute project path. | | getOrgConnection | () => OrgConnection \| undefined | The Salesforce OrgConnection resolved for this agent (or undefined if the connectivity resolver omitted it — non-Salesforce hosts on the BYOK / api-key path return undefined). | | getAgentConfig | () => AgentConfig | Current configuration (shallow copy). | | getMcpServerInfo | () => McpServerInfo[] | MCP server status and discovered tools. | | reconnectMcpServer | (serverName: string) => Promise<void> | Recover one MCP server without recycling the agent. Semantics vary by harness — observe via getMcpServerInfo() and discovery telemetry. | | updateAgentConfig | (config?: AgentConfig, options?: { abortSignal?: AbortSignal; forceResolve?: boolean }) => Promise<void> | Merge new config into the live agent. Pass forceResolve: true to re-run the connectivity resolver even when the partial config doesn't include orgAlias or modelId — for consumer-side state changes (BYOK toggle, feature-id flip, rate-limit gate) the resolver reads but the SDK can't observe directly. | | createChatSession | () => Promise<ChatSession> | Open a new conversation thread. | | getChatSession | (sessionId: string) => ChatSession | Retrieve a session. Throws AgentSDKError (CHAT_SESSION_NOT_FOUND). | | getChatSessionIds | () => string[] | List active session IDs. | | destroyChatSession | (sessionId: string) => Promise<void> | Destroy a session and its history. | | cloneChatSession | (sourceSessionId: string) => Promise<ChatSession> | Clone a session with its message history. | | compactChatSession | (sessionId: string) => Promise<ChatSession> | Compact a session into a summarized new session. | | destroy | () => Promise<void> | Destroy the agent and all its sessions. | | onTelemetry | (callback: TelemetryEventCallback) => Unsubscribe | Subscribe to telemetry scoped to this agent (and its sessions). | | onLog | (callback: (record: LogRecord) => void) => Unsubscribe | Subscribe to logs scoped to this agent (and its sessions). |

ChatSession

A single conversation thread.

| Method | Signature | Description | | ------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | getId | () => string | Session/thread identifier. | | 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. | | 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. | | 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. | | 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. | | getMessageHistory | () => Promise<Message[]> | Retrieve all messages in chronological order. | | clearHistory | () => Promise<void> | Delete all messages. | | getContextUsage | () => ContextUsage | Snapshot of how much of the model's context window the most recent turn used. | | addMessages | (message: string \| Message[]) => Promise<void> | Append real transcript messages (user / assistant / tool) to the thread without requesting an agent response — the write-only half of a turn. The messages persist, appear in getMessageHistory(), and replay to the model as prior conversation on the next chat(). Use it to seed earlier turns (e.g. file contents as a user message) before the first live prompt; the SDK equivalent of the service's POST /messages with noReply=true. Not setSessionContext: this writes transcript history (visible in getMessageHistory, additive); setSessionContext writes an out-of-history overlay object (never in history, whole-object replace). 'system' is not a valid role here — system-level state rides setSessionContext / AgentConfig.instructions. | | addContext | (message: string \| Message[]) => Promise<void> | Deprecated — renamed to addMessages (identical signature/behavior); delegates to it. The old name read as a sibling of setSessionContext, but the two are distinct channels. Will be removed in a future release; migrate to addMessages. | | setSessionContext | (content: SessionContext) => Promise<void> | Replace this session's session-context object in full (whole-object set, not a merge). Persisted per-thread and durable across restart; kept out of message history, so it never appears in getMessageHistory(). Currently persistence-only — the stored object is not yet rendered into the model's system-level context; per-harness rendering on subsequent turns lands in a follow-up. Delegates to AgentHarness.setSessionContext — see that method's JSDoc for the full delivery/durability/isolation contract. | | getSessionContext | () => Promise<SessionContext> | Read this session's current session-context object. Returns {} (an empty object) — never null or undefined — when nothing has been set on this thread yet, so callers never need a null-check. Unrelated to getContextUsage(), which reports context-window token occupancy, not the seeded context object. | | subscribe | (callback: (event: ChatEvent) => void) => void | Register a real-time event listener. | | unsubscribe | (callback: (event: ChatEvent) => void) => void | Remove a listener. | | onTelemetry | (callback: TelemetryEventCallback) => Unsubscribe | Subscribe to telemetry scoped to this session. | | onLog | (callback: (record: LogRecord) => void) => Unsubscribe | Subscribe to logs scoped to this session. | | dispose | () => void | Release session-level event resources. Idempotent. |

ChatStreamResult

Returned by chat(). The single eventStream covers the entire chat turn — including post-resume events from submitToolResult / approveToolCall / declineToolCall. Settle methods return Promise<void>; the consumer keeps iterating the same eventStream until it sees a terminal finish event.

| Property | Type | Description | | ------------- | --------------------------- | ---------------------------------------------------------------------------- | | eventStream | AsyncGenerator<ChatEvent> | Full lifecycle event stream for the turn (one stream per chat() call). | | textStream | AsyncGenerator<string> | Convenience stream of text-only tokens, derived from the same eventStream. |

ChatEvent

Discriminated union (event.type) of streaming events:

| Type | Key Fields | Description | | ----------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | start | — | Stream has begun. | | text-delta | text | Incremental response text. | | reasoning-delta | text | Chain-of-thought fragment. | | tool-call | toolCallId, toolName, args, annotations?, serverName?, bareToolName? | Tool invocation. annotations is the MCP-spec hints (readOnlyHint, destructiveHint, …) when the source declared them; serverName is set when the tool came from an MCP server. toolName is the harness-namespaced display name; bareToolName is the un-namespaced leaf paired with serverName (set for MCP tools only) — use the (serverName, bareToolName) pair, never string-split toolName, for cross-harness identity. | | tool-call-delta | toolCallId, toolName?, argsTextDelta | Incremental fragment of a tool call's args JSON, emitted while the model composes the call. Concatenate successive deltas for the same toolCallId to build the args text; the parsed result matches the terminal tool-call.args. Useful for live-typing tool inputs UI; consumers that don't need streaming-args can ignore this event and continue reading the parsed args on the terminal tool-call. toolName is optional (Claude's signal does not carry it on the wire). | | tool-approval-request | toolCall: ToolCallInfo, annotations?, serverName?, bareToolName? | Engine requests approval before executing a tool. Same annotations / serverName / bareToolName semantics as tool-call. bareToolName is the field the SDK reads to build the remember policy matcher (see Tool Approval). | | tool-result | toolCallId, toolName, result, isError?, error?, annotations?, serverName?, bareToolName? | Tool execution completed. error is present when isError is true (best-effort: harnesses may synthesize an Error from a string payload, so error.stack is not guaranteed to point at the tool's throw site; the field may be absent on empty error payloads). Same annotations / serverName / bareToolName semantics as tool-call. | | tool-progress | toolCallId, toolName, output?, parentToolCallId? | Incremental progress signal from a long-running tool call. Distinct from tool-result: zero or more tool-progress events may be emitted before exactly one terminal tool-result. output and parentToolCallId are best-effort enrichment that depends on the tool — the event itself is the load-bearing "tool is still working" signal; consumers SHOULD NOT branch on which optional fields are present. Useful for "tool is working" UI on long-running tools (build, test, deploy, large search, sub-agent tasks). | | step-start | stepIndex | New LLM invocation step began. | | step-finish | stepIndex, finishReason, usage? | Step completed with per-step token usage. | | error | error, code? | Mid-stream error (yielded, not thrown). | | finish | finishReason, usage? | Stream completed with aggregate token usage. |

Diagnostic logging. The ChatEvent union is the harness-agnostic public stream — it never carries harness-internal chunk shapes. When a harness encounters a chunk type its adapter does not recognize (typically after an upstream Mastra / Claude SDK upgrade), the chunk is skipped on the public stream and surfaced via LogBus.debug with chunkType and rawChunk in the record's context. Subscribe via manager.onLog (or agent.onLog / session.onLog) at debug level to observe these. Production consumers do not need to filter for unrecognized chunks.

Per-variant event types

Every ChatEvent variant is exported as a named type so consumers can write narrowed callbacks without re-declaring the shape: StartEvent, TextDeltaEvent, ReasoningDeltaEvent, ToolCallEvent, ToolCallDeltaEvent, ToolApprovalRequestEvent, ToolResultEvent, ToolProgressEvent, StepStartEvent, StepFinishEvent, ErrorEvent, FinishEvent. Useful when factoring per-event handlers out of a for await loop:

import type { ToolApprovalRequestEvent } from '@salesforce/sfdx-agent-sdk';

function onApprovalRequest(event: ToolApprovalRequestEvent): Promise<boolean> {
  // typed access to event.toolCall, event.annotations, event.serverName
  return promptUser(event);
}

Environment Context

Every TelemetryEvent and LogRecord the SDK emits carries optional, flat identity fields — stamped once, at the event's construction site, by the origin bus (not appended per-subscriber). Subscribe via onTelemetry / onLog at any scope and read the fields directly (e.g. event.orgId); because they're flat top-level fields (not nested), log pipelines like Pino → Splunk can filter on them without dotted-path escaping.

type EnvironmentFields = {
  instanceType?: string; // INSTANCE_TYPE — what kind of running instance this is
  instanceId?: string; // INSTANCE_ID   — this specific running instance
  orgId?: string; // ORG_ID
  userId?: string; // USER_ID
  featureId?: string; // FEATURE_ID, falling back to legacy LLMG_FEATURE_ID
};

The fields are populated from these generic environment variables, read once when the buses are constructed. Each field is optional — present only when its variable holds a non-empty value, so in a plain local process the fields are absent entirely. A deployment maps its own concepts onto the generic vars at the environment boundary (e.g. a DX Workspace container exports INSTANCE_TYPE=$DXW_TEMPLATE_NAME / INSTANCE_ID=$SFDX_INSTANCE_ID), so the SDK never learns deployment-specific names. These fields are a convenience for single-tenant deployments and are subject to change; the type and readers are re-exported from @salesforce/agentic-common:

  • readEnvironmentContext(env?) — maps the generic env vars onto an EnvironmentFields bag.
  • resolveFeatureId(env?)FEATURE_ID → legacy LLMG_FEATURE_IDundefined.

The bus hierarchy that stamps these (TimestampedEventBus / EnvironmentAwareEventBus / the reusable BaseTelemetryEventBus) lives in @salesforce/agentic-common — see that package's README to build env-stamped telemetry for a service of your own.

Configuration Types

AgentConfig

| Field | Type | Description | | ---------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | orgAlias? | string | Salesforce org alias or username. Falls back to project/default org. | | modelId? | ModelName \| Model | LLM model selector. Pass a ModelName enum value for an in-tree model (e.g. 'sfdc_ai__DefaultGPT5'), 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. Legacy pinned llmgateway__* ids still resolve via a back-compat alias map. | | name? | string | Human-readable agent name. | | description? | string | Agent purpose description. | | instructions? | string | System instructions for the agent. | | tools? | ToolDefinition[] | Consumer-executed tool schemas. | | mcpServers? | MCPConfiguration | MCP server connections. | | 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. | | 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. | | 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. | | 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). |

StreamOptions

| Field | Type | Description | | ----------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | abortSignal? | AbortSignal | Abort the streaming operation. | | 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). Controls only the serial-vs-batch emission UX; the gating on/off decision lives on AgentConfig.toolPolicies / AgentConfig.defaultToolDecision. No effect when no tool in the turn resolves to 'require-approval'. | | 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. |

Tool Approval Policy

Per-tool approval is configured on the agent, not per chat() call. AgentConfig.toolPolicies is an ordered list of ToolPolicyRules; for each harness-executed tool call the harness consults resolveToolApprovalPolicy(...), a pure function exported from this package.

export type Decision = 'allow' | 'deny' | 'require-approval';

export type ToolMatcher =
  | { type: 'builtin'; name: string } // harness built-in (e.g. Claude `Bash`)
  | { type: 'mcp'; serverName?: string; toolName?: string } // MCP tool(s); omitted fields are wildcards
  | { type: 'mcp-annotation'; readOnlyHint?: boolean; destructiveHint?: boolean }; // by discovered annotation

export type ToolPolicyRule = {
  matcher: ToolMatcher;
  decision: Decision;
  source?: 'built-in' | 'agent-config' | 'remember'; // advisory provenance; ignored by the resolver
};

Resolution — cross-tier deny-wins / within-tier last-wins. The resolver concatenates four tiers in precedence order — [...BUILT_IN_TOOL_POLICIES, ...harness, ...factory, ...AgentConfig.toolPolicies] — then: any matching 'deny' in any tier wins; otherwise the last matching non-deny rule decides; if nothing matched, the result is AgentConfig.defaultToolDecision ?? 'allow'. So a later rule overrides an earlier one of the same matcher (a consumer rule beats a built-in), but no consumer rule can override a deny.

Built-in rules. BUILT_IN_TOOL_POLICIES (exported, frozen) ships cross-harness rules only: MCP-annotation defaults (destructiveHint ⇒ require-approval, readOnlyHint ⇒ allow) and the skill_bridge capability-discovery meta-tools (anchored on the exported SKILL_BRIDGE_SERVER_ID). Harness-specific built-ins (Claude's Bash, Mastra's updateWorkingMemory) live in the harness packages' <HARNESS>_BUILT_IN_TOOL_POLICIES arrays, not here.

definePolicy helper. Compresses the common cases into a ToolPolicyRule[] (source: 'agent-config'):

import { definePolicy } from '@salesforce/sfdx-agent-sdk';

const config: AgentConfig = {
  defaultToolDecision: 'require-approval', // fail-closed
  toolPolicies: definePolicy({
    Bash: 'deny', // builtin matcher
    'mcp:sfdx': 'require-approval', // every tool from the 'sfdx' MCP server
    'mcp:sfdx/list_orgs': 'allow', // one tool from the 'sfdx' server
  }),
};

definePolicy covers builtin and mcp:server[/tool] keys only — author the structured ToolPolicyRule form for mcp-annotation matchers, the bare { type: 'mcp' } wildcard, or tool names containing /.

AG-UI button mapping. The settle methods grow a symmetric, honored remember flag:

| AG-UI button | SDK call | Effect | | ------------ | ----------------------------------------- | -------------------------------------------------------- | | Allow | approveToolCall(id) | Settle only. | | Allow always | approveToolCall(id, { remember: true }) | Append an allow remember rule, persist, then settle. | | Deny | declineToolCall(id) | Settle only. | | Deny always | declineToolCall(id, { remember: true }) | Append a deny remember rule, persist, then settle. |

With { remember: true } the SDK derives the matcher from the pending tool-approval-request's (serverName?, bareToolName ?? toolName)bareToolName is the harness-supplied un-namespaced leaf for MCP tools (building the matcher from the namespaced display toolName would never match on replay); built-in tools have no bareToolName and their toolName is already bare. It appends a source: 'remember' rule to AgentConfig.toolPolicies and persists it via updateAgentConfig before settling — so the decision survives a restart and a persistence failure surfaces as the settle's rejection. A remember settle for a toolCallId with no pending approval throws TOOL_CALL_NOT_FOUND.

MCPConfiguration

type MCPConfiguration = Record<string, MCPServerConfig>;

// Stdio server (local subprocess)
type MCPStdioServerConfig = {
  type: 'stdio';
  command: string;
  args?: string[];
  env?: Record<string, string>;
  enabled?: boolean;
  timeout?: number;
};

// Remote server (HTTP/SSE)
type MCPRemoteServerConfig = {
  type: 'remote';
  url: string | URL;
  headers?: Record<string, string>;
  enabled?: boolean;
  timeout?: number;
  reconnectionOptions?: {
    maxRetries?: number;
    initialReconnectionDelay?: number;
    maxReconnectionDelay?: number;
    reconnectionDelayGrowFactor?: number;
  };
};

Tool-exposure policy (which tools bypass the active runtime's tool-search deferral) is configured per-agent on the harness extension surface, not per-server here. See MastraAgentConfig.toolSearch.alwaysActive and ClaudeAgentConfig.toolSearch.alwaysActive for the entry shape that covers "all tools from server X", "tool Y on server X", and "tool Y from any source".

reconnectionOptions tunes the HTTP MCP transport's retry / backoff behavior. Forwarded to the underlying SDK transport on both harnesses (Claude's @modelcontextprotocol/sdk StreamableHTTPClientTransport and Mastra's @mastra/mcp HttpServerDefinition, which is itself typed off the same MCP SDK shape). Each field is optional; unspecified fields fall back to the MCP SDK's built-in defaults — maxRetries: 2, initialReconnectionDelay: 1000 ms, maxReconnectionDelay: 30000 ms, reconnectionDelayGrowFactor: 1.5. Partial overrides are merged with those defaults at the harness boundary so a consumer setting only maxRetries