@salesforce/sfdx-agent-sdk 0.51.0 → 0.53.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 CHANGED
@@ -3,6 +3,16 @@
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.53.0] - 2026-08-24
7
+
8
+ ### Features
9
+ - **agent-sdk**: persist per-agent consumer metadata for governed PATCH @W-23972709@ ([#760](https://github.com/forcedotcom/agentic-dx/pull/760))
10
+
11
+ ## [0.52.0] - 2026-08-24
12
+
13
+ ### Features
14
+ - **agent-sdk,harness-claude,harness-mastra,harness-openai**: support OAuth authProvider on remote MCP servers @W-23939243@ ([#757](https://github.com/forcedotcom/agentic-dx/pull/757))
15
+
6
16
  ## [0.51.0] - 2026-08-18
7
17
 
8
18
  ### Chores
package/README.md CHANGED
@@ -102,18 +102,18 @@ through a typed `manager.extensions` slot. `createAgentManager` infers `H` from
102
102
  explicitly. The `createAgent` config parameter narrows automatically when the harness brands itself with
103
103
  `WithAgentConfig` — see "Harness Extensibility" below.
104
104
 
105
- | Property / Method | Signature | Description |
106
- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
107
- | `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. |
108
- | `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>`. |
109
- | `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()`. |
110
- | `getAgentIds` | `() => string[]` | List all live agent IDs (successful + successfully restored). Failed-restore agents are not included — query `getRestoreFailures()` separately. |
111
- | `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). |
112
- | `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. |
113
- | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry across all managed agents. |
114
- | `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. |
115
- | `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). |
116
- | `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. |
105
+ | Property / Method | Signature | Description |
106
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
107
+ | `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. |
108
+ | `createAgent` | `(projectRoot: string, config?: ConfigOf<H> & { agentId?: string }, options?: { abortSignal?: AbortSignal; consumerMetadata?: JsonValue }) => 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>`. `options.consumerMetadata` seeds the agent's opaque consumer metadata in the same persistence write as the config (create and initial metadata land together); it is never forwarded to the harness. |
109
+ | `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()`. |
110
+ | `getAgentIds` | `() => string[]` | List all live agent IDs (successful + successfully restored). Failed-restore agents are not included — query `getRestoreFailures()` separately. |
111
+ | `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). |
112
+ | `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. |
113
+ | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry across all managed agents. |
114
+ | `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. |
115
+ | `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). |
116
+ | `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. |
117
117
 
118
118
  #### `RestoreFailure`
119
119
 
@@ -136,24 +136,26 @@ A configured AI agent. Factory for chat sessions. The optional `H` type paramete
136
136
  — harness-specific features are reached through `manager.extensions`, not `agent.extensions`. The default `AgentHarness`
137
137
  keeps unparameterized call sites working.
138
138
 
139
- | Method | Signature | Description |
140
- | -------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
141
- | `getId` | `() => string` | Agent identifier. |
142
- | `getProjectRoot` | `() => string` | Absolute project path. |
143
- | `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`). |
144
- | `getAgentConfig` | `() => AgentConfig` | Current configuration (shallow copy). |
145
- | `getMcpServerInfo` | `() => McpServerInfo[]` | MCP server status and discovered tools. |
146
- | `reconnectMcpServer` | `(serverName: string) => Promise<void>` | Recover one MCP server without recycling the agent. Semantics vary by harness — observe via `getMcpServerInfo()` and discovery telemetry. |
147
- | `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. |
148
- | `createChatSession` | `() => Promise<ChatSession>` | Open a new conversation thread. |
149
- | `getChatSession` | `(sessionId: string) => ChatSession` | Retrieve a session. Throws `AgentSDKError` (`CHAT_SESSION_NOT_FOUND`). |
150
- | `getChatSessionIds` | `() => string[]` | List active session IDs. |
151
- | `destroyChatSession` | `(sessionId: string) => Promise<void>` | Destroy a session and its history. |
152
- | `cloneChatSession` | `(sourceSessionId: string) => Promise<ChatSession>` | Clone a session with its message history. |
153
- | `compactChatSession` | `(sessionId: string) => Promise<ChatSession>` | Compact a session into a summarized new session. |
154
- | `destroy` | `() => Promise<void>` | Destroy the agent and all its sessions. |
155
- | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this agent (and its sessions). |
156
- | `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this agent (and its sessions). |
139
+ | Method | Signature | Description |
140
+ | --------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
141
+ | `getId` | `() => string` | Agent identifier. |
142
+ | `getProjectRoot` | `() => string` | Absolute project path. |
143
+ | `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`). |
144
+ | `getAgentConfig` | `() => AgentConfig` | Current configuration (shallow copy). |
145
+ | `getConsumerMetadata` | `() => JsonValue \| undefined` | Deep copy of the agent's SDK-owned opaque consumer metadata, or `undefined` if unset. Persisted beside the config; never forwarded to the harness and never on the wire. Set it via `updateAgentState`. |
146
+ | `getMcpServerInfo` | `() => McpServerInfo[]` | MCP server status and discovered tools. |
147
+ | `reconnectMcpServer` | `(serverName: string) => Promise<void>` | Recover one MCP server without recycling the agent. Semantics vary by harness observe via `getMcpServerInfo()` and discovery telemetry. |
148
+ | `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. A thin wrapper over `updateAgentState({ config }, options)`. |
149
+ | `updateAgentState` | `(update: AgentStateUpdate, options?: { abortSignal?: AbortSignal; forceResolve?: boolean }) => Promise<void>` | Atomically apply a config partial and/or a consumer-metadata mutation. When both are present they commit in one persistence write, so a failed update leaves config and metadata at their previous values together. A metadata-only update never calls the harness. See `AgentStateUpdate` / `ConsumerMetadataMutation` below. |
150
+ | `createChatSession` | `() => Promise<ChatSession>` | Open a new conversation thread. |
151
+ | `getChatSession` | `(sessionId: string) => ChatSession` | Retrieve a session. Throws `AgentSDKError` (`CHAT_SESSION_NOT_FOUND`). |
152
+ | `getChatSessionIds` | `() => string[]` | List active session IDs. |
153
+ | `destroyChatSession` | `(sessionId: string) => Promise<void>` | Destroy a session and its history. |
154
+ | `cloneChatSession` | `(sourceSessionId: string) => Promise<ChatSession>` | Clone a session with its message history. |
155
+ | `compactChatSession` | `(sessionId: string) => Promise<ChatSession>` | Compact a session into a summarized new session. |
156
+ | `destroy` | `() => Promise<void>` | Destroy the agent and all its sessions. |
157
+ | `onTelemetry` | `(callback: TelemetryEventCallback) => Unsubscribe` | Subscribe to telemetry scoped to this agent (and its sessions). |
158
+ | `onLog` | `(callback: (record: LogRecord) => void) => Unsubscribe` | Subscribe to logs scoped to this agent (and its sessions). |
157
159
 
158
160
  ### `ChatSession`
159
161
 
@@ -280,6 +282,25 @@ telemetry for a service of your own.
280
282
  | `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. |
281
283
  | `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). |
282
284
 
285
+ #### `AgentStateUpdate` and `ConsumerMetadataMutation`
286
+
287
+ Inputs to `Agent.updateAgentState`. Consumer metadata is SDK-owned opaque JSON persisted beside the agent config — it
288
+ never sits on `AgentConfig`, never reaches the harness, and never appears on the REST wire.
289
+
290
+ ```typescript
291
+ type ConsumerMetadataMutation = { action: 'replace'; value: JsonValue } | { action: 'clear' };
292
+
293
+ type AgentStateUpdate = {
294
+ config?: AgentConfig;
295
+ consumerMetadata?: ConsumerMetadataMutation;
296
+ };
297
+ ```
298
+
299
+ `replace` stores a deep copy of `value`; `clear` removes the metadata; omitting `consumerMetadata` leaves it unchanged.
300
+ When `update.config` is present, `updateAgentState` applies it to the harness exactly as `updateAgentConfig` does and
301
+ persists config plus metadata in one write; when it is absent, the update is metadata-only and never calls the harness.
302
+ `getConsumerMetadata()` reads the current value back as a deep copy.
303
+
283
304
  #### `StreamOptions`
284
305
 
285
306
  | Field | Type | Description |
@@ -386,6 +407,45 @@ type MCPRemoteServerConfig = {
386
407
  };
387
408
  ```
388
409
 
410
+ #### OAuth-protected remote MCP servers
411
+
412
+ To connect an OAuth-protected third-party remote MCP server (Figma, Linear, Notion, Sentry, Neon, Hugging Face, …),
413
+ supply an `McpOAuthClientProvider` through the manager's **`mcpAuthProviderResolver`**, not on the server config. The
414
+ provider is a credential-bearing runtime object, so it is deliberately kept **off** the persisted `AgentConfig` — it is
415
+ resolved per agent at install / restore / update and threaded to the harness at runtime, never serialized to disk.
416
+
417
+ ```ts
418
+ const manager = await createAgentManager(storageRoot, harnessFactory, {
419
+ // (agentId, config) => Record<remoteServerName, McpOAuthClientProvider> | undefined
420
+ mcpAuthProviderResolver: (_agentId, _config) => ({ figma: myFigmaProvider }),
421
+ });
422
+ ```
423
+
424
+ The harness forwards the provider into the underlying HTTP transport (`@modelcontextprotocol/sdk`'s
425
+ `StreamableHTTPClientTransport` on Claude / OpenAI, `@mastra/mcp`'s `HttpServerDefinition` on Mastra). The transport
426
+ attaches existing tokens and silently refreshes them when possible. If user interaction is required, it invokes the
427
+ provider's `redirectToAuthorization` callback and surfaces `McpServerErrorDetail.category === 'oauth-required'`; ADX
428
+ does not open a browser or accept the authorization callback code. The static `headers` seam cannot provide this
429
+ recovery because headers are snapshotted at publish time.
430
+
431
+ - **`McpOAuthClientProvider`** (exported) is a harness-neutral interface that structurally mirrors the required core of
432
+ the MCP SDK's `OAuthClientProvider`, so a real provider assigns to it with no cast and the SDK stays free of a direct
433
+ `@modelcontextprotocol/sdk` dependency. **`McpAuthProviders`** (exported) is the
434
+ `Readonly<Record<serverName, McpOAuthClientProvider>>` the resolver returns.
435
+ - **One provider instance per server**, and return the **same** instance across calls — the upstream contract forbids
436
+ tokens / codes / verifiers crossing sessions, and a harness cycles a server when its provider reference changes (so
437
+ returning a fresh instance each update reconnects that server every time).
438
+ - **One auth owner per server (enforced centrally).** The SDK normalizes the resolver's output before handing it to a
439
+ harness: a provider is **dropped** for a Salesforce Platform URL (the rotating org JWT owns auth there), a stdio /
440
+ disabled / unknown server; and a provider **paired with a static `Authorization` header** is **rejected**
441
+ (`AgentSDKErrorType.INVALID_MCP_AUTH_CONFIG`) because the MCP SDK transport would let the static header silently win
442
+ over the provider token. For a provider-owned non-Salesforce server the harness attaches the proxy-aware inner `fetch`
443
+ (no Salesforce JWT injected, `HTTPS_PROXY` preserved) while the provider drives auth.
444
+
445
+ > **Interactive authorization stays with the consumer.** AFV's provider owns the Sign in UI, browser, VS Code callback,
446
+ > and code exchange. After it saves the new tokens, call `Agent.reconnectMcpServer` to connect. ADX owns runtime
447
+ > provider forwarding and silent refresh only; the transport's `finishAuth(code)` seam is not part of the public API.
448
+
389
449
  **Tool-exposure policy** (which tools bypass the active runtime's tool-search deferral) is configured per-agent on the
390
450
  harness extension surface, not per-server here. See `MastraAgentConfig.toolSearch.alwaysActive` and
391
451
  `ClaudeAgentConfig.toolSearch.alwaysActive` for the entry shape that covers "all tools from server X", "tool Y on server
@@ -411,17 +471,18 @@ only `MCPRemoteServerConfig` carries it.
411
471
 
412
472
  #### `McpServerErrorDetail`
413
473
 
414
- Structured projection of an MCP server failure. Mirror is also attached to the `mcp-server-discovery-failed` telemetry
415
- event so subscribers can route on it without pattern-matching `error.message`.
474
+ Structured projection of an MCP server failure. Route interactive authorization on `category === 'oauth-required'`; an
475
+ ordinary missing or invalid bearer remains `http-401`. The same detail is attached to `mcp-server-discovery-failed`
476
+ telemetry so subscribers can route on it without pattern-matching `error.message`.
416
477
 
417
478
  | Field | Type | Description |
418
479
  | ----------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
419
480
  | `category` | `McpServerErrorCategory` | Stable category for routing logic. Set is additive across minor versions — values are added but never renamed or removed. |
420
481
  | `code?` | `number` | JSON-RPC error code from the underlying `McpError`, when the failure originated as a JSON-RPC error. Undefined for transport-level failures and for harnesses whose underlying SDK does not surface the code (e.g. Claude). |
421
- | `retriable` | `boolean` | Whether the SDK considers the failure transient (worth `Agent.reconnectMcpServer` / `Agent.refreshMcpAuth`) versus fatal. |
482
+ | `retriable` | `boolean` | Whether the SDK considers the failure transient (worth `Agent.reconnectMcpServer`) versus fatal. |
422
483
 
423
- `McpServerErrorCategory` values: `'connect-timeout'`, `'http-401'`, `'http-403'`, `'http-4xx'`, `'http-5xx'`,
424
- `'transport-eof'`, `'protocol-error'`, `'config-error'`, `'aborted'`, `'unknown'`.
484
+ `McpServerErrorCategory` values: `'connect-timeout'`, `'oauth-required'`, `'http-401'`, `'http-403'`, `'http-4xx'`,
485
+ `'http-5xx'`, `'transport-eof'`, `'protocol-error'`, `'config-error'`, `'aborted'`, `'unknown'`.
425
486
 
426
487
  #### `McpToolInfo`
427
488
 
@@ -1,8 +1,9 @@
1
1
  import { type Clock, LogBus, type LogRecord, type Unsubscribe, type UniqueIDGenerator } from '@salesforce/agentic-common';
2
2
  import { type AgentHarness, type ConfigOf } from './harness/agent-harness.js';
3
3
  import type { HarnessFactory } from './harness/harness-factory.js';
4
- import { type AgentConfig } from './harness/harness-config.js';
5
- import { type Agent } from './agent.js';
4
+ import { type AgentConfig, type McpAuthProviderResolver } from './harness/harness-config.js';
5
+ import type { JsonValue } from './types/session-context.js';
6
+ import { type Agent, type AgentRuntimeResolvers } from './agent.js';
6
7
  import type { HooksForAgent } from './types/redaction.js';
7
8
  import { type TelemetryEventCallback } from './types/telemetry-events.js';
8
9
  import type { WireCommunicationEventCallback } from './types/wire-communication-event.js';
@@ -50,6 +51,10 @@ export interface AgentManager<H extends AgentHarness = AgentHarness> {
50
51
  * without an explicit generic at the call site. Extra fields thread
51
52
  * through opaquely to the harness, which narrows what it cares about.
52
53
  *
54
+ * `options.consumerMetadata` seeds the agent's SDK-owned opaque consumer
55
+ * metadata, persisted in the same identity write as the config so create
56
+ * and initial metadata land together. It is never forwarded to the harness.
57
+ *
53
58
  * @throws If `projectRoot` does not exist or is not a directory.
54
59
  * @throws If `config.agentId` is provided and an agent with that ID is
55
60
  * already registered (live or in restore-failure state).
@@ -58,6 +63,7 @@ export interface AgentManager<H extends AgentHarness = AgentHarness> {
58
63
  agentId?: string;
59
64
  }, options?: {
60
65
  abortSignal?: AbortSignal;
66
+ consumerMetadata?: JsonValue;
61
67
  }): Promise<Agent>;
62
68
  /**
63
69
  * Returns the live {@link Agent} for the given id.
@@ -138,7 +144,7 @@ export declare class DefaultAgentManager<H extends AgentHarness = AgentHarness>
138
144
  private readonly harnessSupportedProviderHints;
139
145
  private readonly agentIdGenerator;
140
146
  private readonly agentConnectivityResolver;
141
- private readonly hooksForAgent;
147
+ private readonly resolvers;
142
148
  private readonly clock;
143
149
  private readonly identityStore;
144
150
  private readonly agents;
@@ -161,13 +167,14 @@ export declare class DefaultAgentManager<H extends AgentHarness = AgentHarness>
161
167
  * is private, so this is the only way to obtain an instance, but
162
168
  * consumers should always go through {@link createAgentManager}.
163
169
  */
164
- static __build<H extends AgentHarness>(harness: H, harnessSupportedProviderHints: readonly ProviderHint[], agentConnectivityResolver: AgentConnectivityResolver, hooksForAgent: HooksForAgent | undefined, storageRootFolder: string, agentIdGenerator: UniqueIDGenerator, clock: Clock, logBus: LogBus): Promise<DefaultAgentManager<H>>;
170
+ static __build<H extends AgentHarness>(harness: H, harnessSupportedProviderHints: readonly ProviderHint[], agentConnectivityResolver: AgentConnectivityResolver, resolvers: AgentRuntimeResolvers, storageRootFolder: string, agentIdGenerator: UniqueIDGenerator, clock: Clock, logBus: LogBus): Promise<DefaultAgentManager<H>>;
165
171
  private init;
166
172
  shutdown(): Promise<void>;
167
173
  createAgent(projectRoot: string, config?: ConfigOf<H> & {
168
174
  agentId?: string;
169
175
  }, options?: {
170
176
  abortSignal?: AbortSignal;
177
+ consumerMetadata?: JsonValue;
171
178
  }): Promise<Agent>;
172
179
  /**
173
180
  * Shared install path for {@link createAgent} and the boot-time restore
@@ -228,4 +235,11 @@ export declare class DefaultAgentManager<H extends AgentHarness = AgentHarness>
228
235
  export declare function createAgentManager<H extends AgentHarness = AgentHarness>(storageRootFolder: string, harnessFactory: HarnessFactory<H>, options?: {
229
236
  connectivityResolver?: AgentConnectivityResolver;
230
237
  hooksForAgent?: HooksForAgent;
238
+ /**
239
+ * Resolves the per-agent OAuth provider map for remote MCP servers.
240
+ * See {@link McpAuthProviderResolver}. Providers are supplied here at
241
+ * runtime and never persisted; required to connect OAuth-protected
242
+ * third-party remote MCP servers.
243
+ */
244
+ mcpAuthProviderResolver?: McpAuthProviderResolver;
231
245
  }): Promise<AgentManager<H>>;
@@ -7,6 +7,7 @@ import { resolve } from 'node:path';
7
7
  import { stat } from 'node:fs/promises';
8
8
  import { SUPPORTED_PROTOCOL_VERSIONS } from './harness/agent-harness.js';
9
9
  import { toHarnessConfig } from './harness/harness-config.js';
10
+ import { normalizeMcpAuthProviders } from './mcp-auth.js';
10
11
  import { DefaultAgent } from './agent.js';
11
12
  import { AgentSDKError, AgentSDKErrorType } from './errors.js';
12
13
  import { TelemetryRouter } from './internal/telemetry-router.js';
@@ -28,7 +29,7 @@ export class DefaultAgentManager {
28
29
  harnessSupportedProviderHints;
29
30
  agentIdGenerator;
30
31
  agentConnectivityResolver;
31
- hooksForAgent;
32
+ resolvers;
32
33
  clock;
33
34
  identityStore;
34
35
  agents = new Map();
@@ -43,11 +44,11 @@ export class DefaultAgentManager {
43
44
  wireRouter;
44
45
  unroutedUnsubs;
45
46
  disposed = false;
46
- constructor(harness, harnessSupportedProviderHints, agentConnectivityResolver, hooksForAgent, identityStore, agentIdGenerator, clock, logBus) {
47
+ constructor(harness, harnessSupportedProviderHints, agentConnectivityResolver, resolvers, identityStore, agentIdGenerator, clock, logBus) {
47
48
  this.harness = harness;
48
49
  this.harnessSupportedProviderHints = harnessSupportedProviderHints;
49
50
  this.agentConnectivityResolver = agentConnectivityResolver;
50
- this.hooksForAgent = hooksForAgent;
51
+ this.resolvers = resolvers;
51
52
  this.identityStore = identityStore;
52
53
  this.agentIdGenerator = agentIdGenerator;
53
54
  this.clock = clock;
@@ -75,9 +76,9 @@ export class DefaultAgentManager {
75
76
  * is private, so this is the only way to obtain an instance, but
76
77
  * consumers should always go through {@link createAgentManager}.
77
78
  */
78
- static async __build(harness, harnessSupportedProviderHints, agentConnectivityResolver, hooksForAgent, storageRootFolder, agentIdGenerator, clock, logBus) {
79
+ static async __build(harness, harnessSupportedProviderHints, agentConnectivityResolver, resolvers, storageRootFolder, agentIdGenerator, clock, logBus) {
79
80
  const identityStore = new AgentIdentityStore(storageRootFolder, harness.harnessId, logBus);
80
- const manager = new DefaultAgentManager(harness, harnessSupportedProviderHints, agentConnectivityResolver, hooksForAgent, identityStore, agentIdGenerator, clock, logBus);
81
+ const manager = new DefaultAgentManager(harness, harnessSupportedProviderHints, agentConnectivityResolver, resolvers, identityStore, agentIdGenerator, clock, logBus);
81
82
  await manager.init();
82
83
  return manager;
83
84
  }
@@ -88,6 +89,7 @@ export class DefaultAgentManager {
88
89
  try {
89
90
  await this.installAgent(record.agentId, record.projectRoot, record.config, {
90
91
  rehydrateThreads: true,
92
+ consumerMetadata: record.consumerMetadata,
91
93
  });
92
94
  }
93
95
  catch (err) {
@@ -130,19 +132,26 @@ export class DefaultAgentManager {
130
132
  const resolvedProjectRoot = resolve(projectRoot);
131
133
  const { agentId: providedAgentId, ...agentConfig } = config;
132
134
  const agentId = providedAgentId ?? this.agentIdGenerator.getUniqueId();
135
+ const consumerMetadata = options?.consumerMetadata;
133
136
  if (this.agents.has(agentId)) {
134
137
  throw new Error(`Agent with id "${agentId}" already exists`);
135
138
  }
136
139
  // installAgent validates projectRoot existence — same path as the restore loop.
137
140
  const agent = await this.installAgent(agentId, resolvedProjectRoot, agentConfig, {
138
141
  abortSignal: options?.abortSignal,
142
+ consumerMetadata,
139
143
  });
140
144
  // If the disk write fails (disk full, permissions, fs error), the in-memory install
141
145
  // is now stale — the harness has the agent, the manager has it in `agents`, but no
142
146
  // record on disk means a subsequent restart loses it. Roll back the install so the
143
147
  // failure is observable and the id stays reusable, then rethrow.
144
148
  try {
145
- await this.identityStore.write(agentId, resolvedProjectRoot, agentConfig);
149
+ if (consumerMetadata === undefined) {
150
+ await this.identityStore.write(agentId, resolvedProjectRoot, agentConfig);
151
+ }
152
+ else {
153
+ await this.identityStore.write(agentId, resolvedProjectRoot, agentConfig, consumerMetadata);
154
+ }
146
155
  }
147
156
  catch (err) {
148
157
  await this.rollbackInstall(agentId, agent);
@@ -183,8 +192,13 @@ export class DefaultAgentManager {
183
192
  if (!this.harnessSupportedProviderHints.includes(providerHint)) {
184
193
  throw new AgentSDKError(`Harness "${this.harness.harnessId}" does not support providerHint "${providerHint}". Supported: ${this.harnessSupportedProviderHints.join(', ')}.`, AgentSDKErrorType.MODEL_NOT_SUPPORTED_BY_HARNESS);
185
194
  }
186
- const hooks = this.hooksForAgent?.(agentId, config) ?? {};
187
- await this.harness.createAgent(agentId, projectRoot, runtime.modelConnectivityInfo, toHarnessConfig(config, runtime.orgJwt), { ...(options.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}), hooks });
195
+ const hooks = this.resolvers.hooksForAgent?.(agentId, config) ?? {};
196
+ const mcpAuthProviders = normalizeMcpAuthProviders(config.mcpServers, this.resolvers.mcpAuthProviderResolver?.(agentId, config));
197
+ await this.harness.createAgent(agentId, projectRoot, runtime.modelConnectivityInfo, toHarnessConfig(config, runtime.orgJwt), {
198
+ ...(options.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}),
199
+ hooks,
200
+ mcpAuthProviders,
201
+ });
188
202
  const agentSlice = this.router.registerAgent(agentId);
189
203
  // Forward-compat: register the agent against the wire-communication router
190
204
  // too. Today every WireCommunicationEvent lands on the unrouted slice (no
@@ -192,7 +206,7 @@ export class DefaultAgentManager {
192
206
  // symmetrically with the telemetry router so the wiring is in place if/when
193
207
  // the event shape grows an agentId field.
194
208
  this.wireRouter.registerAgent(agentId);
195
- const agent = new DefaultAgent(this.harness, agentId, projectRoot, config, runtime.modelConnectivityInfo, runtime.orgConnection, runtime.orgJwt, this.agentConnectivityResolver, this.harnessSupportedProviderHints, this.hooksForAgent, this.identityStore, this.router, agentSlice, { telemetry: this.telemetryBus, log: this.logBus }, this.clock, this.agentIdGenerator);
209
+ const agent = new DefaultAgent(this.harness, agentId, projectRoot, config, options.consumerMetadata, runtime.modelConnectivityInfo, runtime.orgConnection, runtime.orgJwt, this.agentConnectivityResolver, this.harnessSupportedProviderHints, this.resolvers, this.identityStore, this.router, agentSlice, { telemetry: this.telemetryBus, log: this.logBus }, this.clock, this.agentIdGenerator);
196
210
  this.agents.set(agentId, agent);
197
211
  const agentCreatedAt = this.clock.now();
198
212
  const modelName = runtime.modelConnectivityInfo.model.name;
@@ -359,7 +373,7 @@ export async function createAgentManager(storageRootFolder, harnessFactory, opti
359
373
  }
360
374
  const agentConnectivityResolver = options?.connectivityResolver ?? new DefaultAgentConnectivityResolver();
361
375
  const clock = new RealClock();
362
- return DefaultAgentManager.__build(harness, harnessFactory.supportedProviderHints, agentConnectivityResolver, options?.hooksForAgent, storageRootFolder, new UUIDGenerator(), clock,
376
+ return DefaultAgentManager.__build(harness, harnessFactory.supportedProviderHints, agentConnectivityResolver, { hooksForAgent: options?.hooksForAgent, mcpAuthProviderResolver: options?.mcpAuthProviderResolver }, storageRootFolder, new UUIDGenerator(), clock,
363
377
  // The manager's root log bus shares the manager clock and self-reads the process environment context.
364
378
  new LogBus(clock));
365
379
  }
package/dist/agent.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { type Clock, type JSONWebToken, LogBus, type LogRecord, type OrgConnection, type UniqueIDGenerator, type Unsubscribe } from '@salesforce/agentic-common';
2
2
  import type { AgentHarness } from './harness/agent-harness.js';
3
- import { type AgentConfig } from './harness/harness-config.js';
3
+ import { type AgentConfig, type AgentStateUpdate, type McpAuthProviderResolver } from './harness/harness-config.js';
4
4
  import { type ChatSession } from './chat-session.js';
5
+ import type { JsonValue } from './types/session-context.js';
5
6
  import type { McpServerInfo } from './mcp-config.js';
6
7
  import type { AgentConnectivityResolver } from './agent-connectivity-resolver.js';
7
8
  import type { ModelConnectivityInfo, ProviderHint } from './types/model-connectivity-info.js';
@@ -40,6 +41,13 @@ export interface Agent {
40
41
  getOrgConnection(): OrgConnection | undefined;
41
42
  /** Returns the current agent configuration. */
42
43
  getAgentConfig(): AgentConfig;
44
+ /**
45
+ * Returns a deep copy of the agent's persisted consumer metadata, or
46
+ * `undefined` if none has been set. This is SDK-owned opaque JSON: it is
47
+ * never placed on {@link AgentConfig}, never forwarded to the harness, and
48
+ * never appears on the REST wire. Set it via {@link updateAgentState}.
49
+ */
50
+ getConsumerMetadata(): JsonValue | undefined;
43
51
  /**
44
52
  * Returns runtime information about MCP servers attached to this agent,
45
53
  * including connection status and discovered tool names. This is a synchronous
@@ -97,6 +105,21 @@ export interface Agent {
97
105
  abortSignal?: AbortSignal;
98
106
  forceResolve?: boolean;
99
107
  }): Promise<void>;
108
+ /**
109
+ * Atomically update the agent's configuration and/or its persisted consumer
110
+ * metadata. `update.config` is merged and applied to the harness exactly as
111
+ * {@link updateAgentConfig} does; `update.consumerMetadata` mutates the
112
+ * SDK-owned opaque metadata. When both are present they commit in one
113
+ * persistence write, so a failed update leaves config and metadata at their
114
+ * previous values together. A metadata-only update never calls the harness.
115
+ *
116
+ * @param update - The config partial and/or consumer-metadata mutation to apply.
117
+ * @param options - Same execution options as {@link updateAgentConfig}.
118
+ */
119
+ updateAgentState(update: AgentStateUpdate, options?: {
120
+ abortSignal?: AbortSignal;
121
+ forceResolve?: boolean;
122
+ }): Promise<void>;
100
123
  /**
101
124
  * Create a new chat session (conversation thread) for this agent.
102
125
  * @returns A new {@link ChatSession} with a unique thread ID.
@@ -144,6 +167,15 @@ export interface Agent {
144
167
  /** Subscribe to structured log records scoped to this agent (and its sessions). Returns an unsubscribe function. */
145
168
  onLog(callback: (record: LogRecord) => void): Unsubscribe;
146
169
  }
170
+ /**
171
+ * The per-agent runtime resolvers the SDK threads from `createAgentManager` into each agent — callbacks the consumer
172
+ * supplies once and the SDK re-invokes per install / restore / update. Bundled so a new runtime binding is one field
173
+ * here, not another parameter through every manager → agent seam. Each member is optional (unset by the consumer).
174
+ */
175
+ export type AgentRuntimeResolvers = {
176
+ hooksForAgent?: HooksForAgent;
177
+ mcpAuthProviderResolver?: McpAuthProviderResolver;
178
+ };
147
179
  /**
148
180
  * Default implementation of {@link Agent} that delegates
149
181
  * agent and thread operations to an {@link AgentHarness}.
@@ -153,12 +185,13 @@ export declare class DefaultAgent implements Agent {
153
185
  private readonly agentId;
154
186
  private readonly projectRoot;
155
187
  private config;
188
+ private consumerMetadata;
156
189
  private modelConnectivityInfo;
157
190
  private orgConnection;
158
191
  private orgJwt;
159
192
  private readonly agentConnectivityResolver;
160
193
  private readonly harnessSupportedProviderHints;
161
- private readonly hooksForAgent;
194
+ private readonly resolvers;
162
195
  private readonly identityStore;
163
196
  private readonly sessions;
164
197
  private readonly sessionSliceUnregisters;
@@ -175,23 +208,26 @@ export declare class DefaultAgent implements Agent {
175
208
  * @param agentId - Unique identifier for this agent.
176
209
  * @param projectRoot - Project folder this agent is allowed to operate within.
177
210
  * @param config - Initial agent configuration (instructions, model, tools, etc.).
211
+ * @param consumerMetadata - Initial SDK-owned opaque consumer metadata (or `undefined`). Persisted beside
212
+ * `config`; never forwarded to the harness. Mutated via `updateAgentState`.
178
213
  * @param modelConnectivityInfo - Connectivity bag (model, baseUrl, nativeModelId,
179
214
  * providerHint, getHeaders) the harness uses to talk to the LLM. Replaced
180
215
  * on every `updateAgentConfig` re-resolve.
181
216
  * @param orgConnection - Authenticated org connection carrying identity and env inference.
182
217
  * @param orgJwt - Self-refreshing JWT for the resolved org (used for MCP auth injection).
183
218
  * @param agentConnectivityResolver - Used to re-resolve org connectivity when the org or model changes.
184
- * @param hooksForAgent - Per-agent hooks resolver supplied by the SDK consumer at `createAgentManager` time. The
185
- * agent re-invokes it on every `updateAgentConfig` (with `nextConfig`, and again with `previousConfig` on the
186
- * rollback path) so the bag the harness sees always reflects the current persisted config. `undefined` when
187
- * the consumer didn't pass a `hooksForAgent`.
219
+ * @param resolvers - Per-agent runtime resolvers ({@link AgentRuntimeResolvers}: `hooksForAgent`,
220
+ * `mcpAuthProviderResolver`) supplied by the SDK consumer at `createAgentManager` time. The agent re-invokes
221
+ * them on every `updateAgentConfig` (with `nextConfig`, and again with `previousConfig` on the rollback path)
222
+ * so what the harness sees always reflects the current persisted config. Each member is `undefined` when the
223
+ * consumer didn't pass it.
188
224
  * @param identityStore - SDK-owned persistence for the `{ agentId, projectRoot, AgentConfig }` triple. The agent
189
225
  * calls `write()` on a successful `updateAgentConfig` so disk state and in-memory state stay in lockstep.
190
226
  * @param router - Telemetry router used to obtain session slices when sessions are created.
191
227
  * @param inbound - Router slice delivering harness events routed to this agent (non-session-scoped).
192
228
  * @param parent - Manager's bus pair; this agent forwards its events upward into them.
193
229
  */
194
- constructor(harness: AgentHarness, agentId: string, projectRoot: string, config: AgentConfig, modelConnectivityInfo: ModelConnectivityInfo, orgConnection: OrgConnection | undefined, orgJwt: JSONWebToken | undefined, agentConnectivityResolver: AgentConnectivityResolver, harnessSupportedProviderHints: readonly ProviderHint[], hooksForAgent: HooksForAgent | undefined, identityStore: AgentIdentityStore, router: TelemetryRouter, inbound: TelemetrySlice, parent: AgentParentBuses, clock?: Clock, idGenerator?: UniqueIDGenerator);
230
+ constructor(harness: AgentHarness, agentId: string, projectRoot: string, config: AgentConfig, consumerMetadata: JsonValue | undefined, modelConnectivityInfo: ModelConnectivityInfo, orgConnection: OrgConnection | undefined, orgJwt: JSONWebToken | undefined, agentConnectivityResolver: AgentConnectivityResolver, harnessSupportedProviderHints: readonly ProviderHint[], resolvers: AgentRuntimeResolvers, identityStore: AgentIdentityStore, router: TelemetryRouter, inbound: TelemetrySlice, parent: AgentParentBuses, clock?: Clock, idGenerator?: UniqueIDGenerator);
195
231
  /**
196
232
  * @requirements
197
233
  * - MUST return the agent's ID.
@@ -205,6 +241,12 @@ export declare class DefaultAgent implements Agent {
205
241
  * - MUST return a shallow copy of the internal `config` object to prevent external mutation of the agent's state.
206
242
  */
207
243
  getAgentConfig(): AgentConfig;
244
+ /**
245
+ * @requirements
246
+ * - MUST return a deep copy so a caller cannot mutate the agent's persisted consumer metadata in place.
247
+ * - MUST return `undefined` when no metadata has been set.
248
+ */
249
+ getConsumerMetadata(): JsonValue | undefined;
208
250
  getMcpServerInfo(): McpServerInfo[];
209
251
  reconnectMcpServer(serverName: string): Promise<void>;
210
252
  /**
@@ -227,6 +269,27 @@ export declare class DefaultAgent implements Agent {
227
269
  abortSignal?: AbortSignal;
228
270
  forceResolve?: boolean;
229
271
  }): Promise<void>;
272
+ /**
273
+ * @requirements
274
+ * - MUST merge `update.config` (when present) with the internal `config` object; `agentId` stays unchanged.
275
+ * - MUST apply the merged config to the harness via `this.harness.updateAgent(...)` ONLY when `update.config`
276
+ * is present. A metadata-only update MUST NOT call the harness at all (no server cycling, no resolver run).
277
+ * - MUST compute the next consumer metadata from `update.consumerMetadata`: `replace` stores a deep copy of the
278
+ * value; `clear` removes it; an absent mutation leaves it unchanged.
279
+ * - MUST persist config + metadata together in ONE `this.identityStore.write(...)` after any `harness.updateAgent`
280
+ * succeeds and before the in-memory swaps, so a write failure rolls back through the same catch path.
281
+ * - MUST preserve BOTH the previous config AND the previous metadata if `updateAgent` or persistence fails.
282
+ */
283
+ updateAgentState(update: AgentStateUpdate, options?: {
284
+ abortSignal?: AbortSignal;
285
+ forceResolve?: boolean;
286
+ }): Promise<void>;
287
+ /**
288
+ * Writes the identity record, calling `identityStore.write` with a 4th argument only when consumer metadata is
289
+ * present. Omitting the argument (rather than passing `undefined`) keeps a config-only write byte-identical to the
290
+ * pre-metadata call shape and clears any previously-persisted metadata.
291
+ */
292
+ private writeIdentity;
230
293
  /**
231
294
  * @requirements
232
295
  * - MUST delegate to `this.harness.createThread(this.config.agentId)` to generate a new thread ID.