@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/dist/agent.js CHANGED
@@ -3,10 +3,22 @@
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
5
  import { LogBus, RealClock, UUIDGenerator, } from '@salesforce/agentic-common';
6
- import { toHarnessConfig } from './harness/harness-config.js';
6
+ import { toHarnessConfig, } from './harness/harness-config.js';
7
+ import { normalizeMcpAuthProviders } from './mcp-auth.js';
7
8
  import { DefaultChatSession } from './chat-session.js';
8
9
  import { AgentSDKError, AgentSDKErrorType } from './errors.js';
9
10
  import { createTelemetryBus } from './types/telemetry-events.js';
11
+ /**
12
+ * Resolves the next consumer-metadata value from the current value and an optional mutation. `replace` deep-clones the
13
+ * incoming value so the stored copy is isolated from later caller mutation; `clear` yields `undefined`; an absent
14
+ * mutation leaves the current value untouched.
15
+ */
16
+ function applyConsumerMetadataMutation(current, mutation) {
17
+ if (mutation === undefined) {
18
+ return current;
19
+ }
20
+ return mutation.action === 'replace' ? structuredClone(mutation.value) : undefined;
21
+ }
10
22
  /**
11
23
  * Default implementation of {@link Agent} that delegates
12
24
  * agent and thread operations to an {@link AgentHarness}.
@@ -16,12 +28,13 @@ export class DefaultAgent {
16
28
  agentId;
17
29
  projectRoot;
18
30
  config;
31
+ consumerMetadata;
19
32
  modelConnectivityInfo;
20
33
  orgConnection;
21
34
  orgJwt;
22
35
  agentConnectivityResolver;
23
36
  harnessSupportedProviderHints;
24
- hooksForAgent;
37
+ resolvers;
25
38
  identityStore;
26
39
  sessions = new Map();
27
40
  sessionSliceUnregisters = new Map();
@@ -40,33 +53,37 @@ export class DefaultAgent {
40
53
  * @param agentId - Unique identifier for this agent.
41
54
  * @param projectRoot - Project folder this agent is allowed to operate within.
42
55
  * @param config - Initial agent configuration (instructions, model, tools, etc.).
56
+ * @param consumerMetadata - Initial SDK-owned opaque consumer metadata (or `undefined`). Persisted beside
57
+ * `config`; never forwarded to the harness. Mutated via `updateAgentState`.
43
58
  * @param modelConnectivityInfo - Connectivity bag (model, baseUrl, nativeModelId,
44
59
  * providerHint, getHeaders) the harness uses to talk to the LLM. Replaced
45
60
  * on every `updateAgentConfig` re-resolve.
46
61
  * @param orgConnection - Authenticated org connection carrying identity and env inference.
47
62
  * @param orgJwt - Self-refreshing JWT for the resolved org (used for MCP auth injection).
48
63
  * @param agentConnectivityResolver - Used to re-resolve org connectivity when the org or model changes.
49
- * @param hooksForAgent - Per-agent hooks resolver supplied by the SDK consumer at `createAgentManager` time. The
50
- * agent re-invokes it on every `updateAgentConfig` (with `nextConfig`, and again with `previousConfig` on the
51
- * rollback path) so the bag the harness sees always reflects the current persisted config. `undefined` when
52
- * the consumer didn't pass a `hooksForAgent`.
64
+ * @param resolvers - Per-agent runtime resolvers ({@link AgentRuntimeResolvers}: `hooksForAgent`,
65
+ * `mcpAuthProviderResolver`) supplied by the SDK consumer at `createAgentManager` time. The agent re-invokes
66
+ * them on every `updateAgentConfig` (with `nextConfig`, and again with `previousConfig` on the rollback path)
67
+ * so what the harness sees always reflects the current persisted config. Each member is `undefined` when the
68
+ * consumer didn't pass it.
53
69
  * @param identityStore - SDK-owned persistence for the `{ agentId, projectRoot, AgentConfig }` triple. The agent
54
70
  * calls `write()` on a successful `updateAgentConfig` so disk state and in-memory state stay in lockstep.
55
71
  * @param router - Telemetry router used to obtain session slices when sessions are created.
56
72
  * @param inbound - Router slice delivering harness events routed to this agent (non-session-scoped).
57
73
  * @param parent - Manager's bus pair; this agent forwards its events upward into them.
58
74
  */
59
- constructor(harness, agentId, projectRoot, config, modelConnectivityInfo, orgConnection, orgJwt, agentConnectivityResolver, harnessSupportedProviderHints, hooksForAgent, identityStore, router, inbound, parent, clock = new RealClock(), idGenerator = new UUIDGenerator()) {
75
+ constructor(harness, agentId, projectRoot, config, consumerMetadata, modelConnectivityInfo, orgConnection, orgJwt, agentConnectivityResolver, harnessSupportedProviderHints, resolvers, identityStore, router, inbound, parent, clock = new RealClock(), idGenerator = new UUIDGenerator()) {
60
76
  this.harness = harness;
61
77
  this.agentId = agentId;
62
78
  this.projectRoot = projectRoot;
63
79
  this.config = config;
80
+ this.consumerMetadata = consumerMetadata;
64
81
  this.modelConnectivityInfo = modelConnectivityInfo;
65
82
  this.orgConnection = orgConnection;
66
83
  this.orgJwt = orgJwt;
67
84
  this.agentConnectivityResolver = agentConnectivityResolver;
68
85
  this.harnessSupportedProviderHints = harnessSupportedProviderHints;
69
- this.hooksForAgent = hooksForAgent;
86
+ this.resolvers = resolvers;
70
87
  this.identityStore = identityStore;
71
88
  this.router = router;
72
89
  this.clock = clock;
@@ -101,6 +118,15 @@ export class DefaultAgent {
101
118
  this.assertNotDisposed();
102
119
  return { ...this.config };
103
120
  }
121
+ /**
122
+ * @requirements
123
+ * - MUST return a deep copy so a caller cannot mutate the agent's persisted consumer metadata in place.
124
+ * - MUST return `undefined` when no metadata has been set.
125
+ */
126
+ getConsumerMetadata() {
127
+ this.assertNotDisposed();
128
+ return this.consumerMetadata === undefined ? undefined : structuredClone(this.consumerMetadata);
129
+ }
104
130
  getMcpServerInfo() {
105
131
  this.assertNotDisposed();
106
132
  return this.harness.getMcpServerInfo(this.agentId);
@@ -126,11 +152,41 @@ export class DefaultAgent {
126
152
  * against its current (possibly partially-updated) state and reverts only the actual deltas.
127
153
  */
128
154
  async updateAgentConfig(config = {}, options) {
155
+ return this.updateAgentState({ config }, options);
156
+ }
157
+ /**
158
+ * @requirements
159
+ * - MUST merge `update.config` (when present) with the internal `config` object; `agentId` stays unchanged.
160
+ * - MUST apply the merged config to the harness via `this.harness.updateAgent(...)` ONLY when `update.config`
161
+ * is present. A metadata-only update MUST NOT call the harness at all (no server cycling, no resolver run).
162
+ * - MUST compute the next consumer metadata from `update.consumerMetadata`: `replace` stores a deep copy of the
163
+ * value; `clear` removes it; an absent mutation leaves it unchanged.
164
+ * - MUST persist config + metadata together in ONE `this.identityStore.write(...)` after any `harness.updateAgent`
165
+ * succeeds and before the in-memory swaps, so a write failure rolls back through the same catch path.
166
+ * - MUST preserve BOTH the previous config AND the previous metadata if `updateAgent` or persistence fails.
167
+ */
168
+ async updateAgentState(update, options) {
129
169
  this.assertNotDisposed();
170
+ const { config, consumerMetadata: metadataMutation } = update;
130
171
  const previousConfig = { ...this.config };
172
+ const previousConsumerMetadata = this.consumerMetadata;
131
173
  const previousModelConnectivityInfo = this.modelConnectivityInfo;
132
174
  const previousOrgJwt = this.orgJwt;
133
- const nextConfig = { ...this.config, ...config };
175
+ const nextConfig = config === undefined ? this.config : { ...this.config, ...config };
176
+ const nextConsumerMetadata = applyConsumerMetadataMutation(this.consumerMetadata, metadataMutation);
177
+ // A metadata-only update never touches the harness, the resolver, or the connectivity bag — it just
178
+ // rewrites the persisted record and swaps the in-memory metadata.
179
+ if (config === undefined) {
180
+ try {
181
+ await this.writeIdentity(nextConfig, nextConsumerMetadata);
182
+ this.consumerMetadata = nextConsumerMetadata;
183
+ }
184
+ catch (error) {
185
+ this.consumerMetadata = previousConsumerMetadata;
186
+ throw error;
187
+ }
188
+ return;
189
+ }
134
190
  const orgAliasRequested = Object.prototype.hasOwnProperty.call(config, 'orgAlias');
135
191
  const modelIdRequested = Object.prototype.hasOwnProperty.call(config, 'modelId');
136
192
  let nextModelConnectivityInfo = previousModelConnectivityInfo;
@@ -157,16 +213,19 @@ export class DefaultAgent {
157
213
  nextOrgJwt = runtime.orgJwt;
158
214
  }
159
215
  try {
160
- const nextHooks = this.hooksForAgent?.(this.agentId, nextConfig) ?? {};
216
+ const nextHooks = this.resolvers.hooksForAgent?.(this.agentId, nextConfig) ?? {};
217
+ const nextMcpAuthProviders = normalizeMcpAuthProviders(nextConfig.mcpServers, this.resolvers.mcpAuthProviderResolver?.(this.agentId, nextConfig));
161
218
  await this.harness.updateAgent(this.agentId, nextModelConnectivityInfo, toHarnessConfig(nextConfig, nextOrgJwt), {
162
219
  ...(options?.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}),
163
220
  hooks: nextHooks,
221
+ mcpAuthProviders: nextMcpAuthProviders,
164
222
  });
165
223
  // Persist before the in-memory swaps so a write failure flows through the same
166
224
  // catch block as an updateAgent failure: the rollback re-runs updateAgent against
167
225
  // previousConfig and disk state remains the pre-update record.
168
- await this.identityStore.write(this.agentId, this.projectRoot, nextConfig);
226
+ await this.writeIdentity(nextConfig, nextConsumerMetadata);
169
227
  this.config = nextConfig;
228
+ this.consumerMetadata = nextConsumerMetadata;
170
229
  this.modelConnectivityInfo = nextModelConnectivityInfo;
171
230
  this.orgConnection = nextConnection;
172
231
  this.orgJwt = nextOrgJwt;
@@ -177,8 +236,9 @@ export class DefaultAgent {
177
236
  // against its current state — if updateAgent partially applied (e.g. some MCP
178
237
  // servers were already cycled), reverting via updateAgent restores them too.
179
238
  try {
180
- const previousHooks = this.hooksForAgent?.(this.agentId, previousConfig) ?? {};
181
- await this.harness.updateAgent(this.agentId, previousModelConnectivityInfo, toHarnessConfig(previousConfig, previousOrgJwt), { hooks: previousHooks });
239
+ const previousHooks = this.resolvers.hooksForAgent?.(this.agentId, previousConfig) ?? {};
240
+ const previousMcpAuthProviders = normalizeMcpAuthProviders(previousConfig.mcpServers, this.resolvers.mcpAuthProviderResolver?.(this.agentId, previousConfig));
241
+ await this.harness.updateAgent(this.agentId, previousModelConnectivityInfo, toHarnessConfig(previousConfig, previousOrgJwt), { hooks: previousHooks, mcpAuthProviders: previousMcpAuthProviders });
182
242
  }
183
243
  catch {
184
244
  // Ignore restoration errors; rethrow the original failure.
@@ -186,6 +246,19 @@ export class DefaultAgent {
186
246
  throw error;
187
247
  }
188
248
  }
249
+ /**
250
+ * Writes the identity record, calling `identityStore.write` with a 4th argument only when consumer metadata is
251
+ * present. Omitting the argument (rather than passing `undefined`) keeps a config-only write byte-identical to the
252
+ * pre-metadata call shape and clears any previously-persisted metadata.
253
+ */
254
+ async writeIdentity(config, consumerMetadata) {
255
+ if (consumerMetadata === undefined) {
256
+ await this.identityStore.write(this.agentId, this.projectRoot, config);
257
+ }
258
+ else {
259
+ await this.identityStore.write(this.agentId, this.projectRoot, config, consumerMetadata);
260
+ }
261
+ }
189
262
  /**
190
263
  * @requirements
191
264
  * - MUST delegate to `this.harness.createThread(this.config.agentId)` to generate a new thread ID.
package/dist/errors.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare const AgentSDKErrorType: {
4
4
  readonly COMPACTION_FAILED: 'COMPACTION_FAILED';
5
5
  readonly DISPOSED: 'DISPOSED';
6
6
  readonly INCOMPATIBLE_HARNESS: 'INCOMPATIBLE_HARNESS';
7
+ readonly INVALID_MCP_AUTH_CONFIG: 'INVALID_MCP_AUTH_CONFIG';
7
8
  readonly INVALID_MESSAGE_CONTENT: 'INVALID_MESSAGE_CONTENT';
8
9
  readonly MCP_SERVER_DISABLED: 'MCP_SERVER_DISABLED';
9
10
  readonly MCP_SERVER_NOT_FOUND: 'MCP_SERVER_NOT_FOUND';
package/dist/errors.js CHANGED
@@ -8,6 +8,7 @@ export const AgentSDKErrorType = {
8
8
  COMPACTION_FAILED: 'COMPACTION_FAILED',
9
9
  DISPOSED: 'DISPOSED',
10
10
  INCOMPATIBLE_HARNESS: 'INCOMPATIBLE_HARNESS',
11
+ INVALID_MCP_AUTH_CONFIG: 'INVALID_MCP_AUTH_CONFIG',
11
12
  INVALID_MESSAGE_CONTENT: 'INVALID_MESSAGE_CONTENT',
12
13
  MCP_SERVER_DISABLED: 'MCP_SERVER_DISABLED',
13
14
  MCP_SERVER_NOT_FOUND: 'MCP_SERVER_NOT_FOUND',
@@ -1,5 +1,5 @@
1
1
  import type { LogRecord, Unsubscribe } from '@salesforce/agentic-common';
2
- import type { McpServerInfo } from '../mcp-config.js';
2
+ import type { McpServerInfo, McpAuthProviders } from '../mcp-config.js';
3
3
  import type { ChatStreamResult } from '../types/events.js';
4
4
  import type { Message, MessagePart } from '../types/messages.js';
5
5
  import type { TelemetryEventCallback } from '../types/telemetry-events.js';
@@ -142,11 +142,20 @@ export interface AgentHarness {
142
142
  * native error path so the original tool output never leaks to the
143
143
  * model. The SDK does not own fail-closed semantics; consumers
144
144
  * wrap their hook bodies in `try`/`catch` themselves when they
145
- * want a richer fail-closed substitute.
145
+ * want a richer fail-closed substitute.
146
+ * - `mcpAuthProviders` — per-agent OAuth provider map (remote MCP server
147
+ * name → {@link McpOAuthClientProvider}), resolved by the SDK from
148
+ * `createAgentManager`'s `mcpAuthProviderResolver` and already normalized
149
+ * (Salesforce URLs / stdio / disabled / missing servers dropped, static-
150
+ * Authorization conflicts rejected — see `normalizeMcpAuthProviders`).
151
+ * Providers are runtime bindings, NEVER part of `HarnessAgentConfig`;
152
+ * harnesses store the map on per-agent state and forward `providers[name]`
153
+ * into the remote transport for that server.
146
154
  */
147
155
  createAgent(agentId: string, projectRoot: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
148
156
  abortSignal?: AbortSignal;
149
157
  hooks?: AgentHooks;
158
+ mcpAuthProviders?: McpAuthProviders;
150
159
  }): Promise<void>;
151
160
  /**
152
161
  * Destroy an agent and release its resources (MCP connections, workspace, memory).
@@ -179,9 +188,12 @@ export interface AgentHarness {
179
188
  * matching the rest of the cross-harness contract.
180
189
  * - preserve the in-memory MCP client (and its discovered tool catalog)
181
190
  * for any server name whose config is `mcpServerConfigEqual` to the
182
- * currently-applied one. No transport teardown, no `tools/list`
183
- * re-run, no per-server discovery telemetry.
184
- * - cycle (disconnect-then-reconnect) any server whose config differs.
191
+ * currently-applied one AND whose applied OAuth provider reference (from
192
+ * `options.mcpAuthProviders`) is unchanged. No transport teardown, no
193
+ * `tools/list` re-run, no per-server discovery telemetry.
194
+ * - cycle (disconnect-then-reconnect) any server whose config differs OR
195
+ * whose applied provider reference changed (a swapped provider instance
196
+ * must cycle the server so the new one owns auth).
185
197
  * - disconnect any server present in the currently-applied config but
186
198
  * absent from the next config, removing it from
187
199
  * `getMcpServerInfo()`'s output.
@@ -233,10 +245,15 @@ export interface AgentHarness {
233
245
  * re-resolved on every `updateAgent` so consumers can vary hooks by
234
246
  * config (and so a rollback `updateAgent(previousConfig)` restores
235
247
  * the prior hook bag too).
248
+ * - `mcpAuthProviders` — per-agent normalized OAuth provider map, re-resolved
249
+ * on every `updateAgent` (so a provider swap cycles its server, and a
250
+ * rollback `updateAgent(previousConfig)` restores the prior providers).
251
+ * Same runtime-only semantics as `createAgent.options.mcpAuthProviders`.
236
252
  */
237
253
  updateAgent(agentId: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
238
254
  abortSignal?: AbortSignal;
239
255
  hooks?: AgentHooks;
256
+ mcpAuthProviders?: McpAuthProviders;
240
257
  }): Promise<void>;
241
258
  /**
242
259
  * List the IDs of all currently registered agents.
@@ -1,7 +1,8 @@
1
1
  import type { Decision, ToolDefinition, ToolPolicyRule } from '../types/tools.js';
2
- import type { MCPConfiguration } from '../mcp-config.js';
2
+ import type { MCPConfiguration, McpAuthProviders } from '../mcp-config.js';
3
3
  import type { JSONWebToken } from '@salesforce/agentic-common';
4
4
  import type { Model, ModelName } from '../models/index.js';
5
+ import type { JsonValue } from '../types/session-context.js';
5
6
  /**
6
7
  * Configuration for an agent's behavior and capabilities.
7
8
  * This excludes identity; `agentId` is handled separately.
@@ -88,6 +89,25 @@ export type AgentConfig = {
88
89
  */
89
90
  defaultToolDecision?: Decision;
90
91
  };
92
+ /**
93
+ * Resolves the per-agent OAuth {@link McpAuthProviders} map (remote MCP server
94
+ * name → provider) from the agent's id and the config the SDK has on file.
95
+ * Invoked by `AgentManager` once per agent install (`createAgent`, boot-time
96
+ * restore) and by `Agent.updateAgentConfig`; the resolved map is normalized by
97
+ * the SDK (see `normalizeMcpAuthProviders`) and threaded to the harness via
98
+ * `AgentHarness.createAgent` / `updateAgent`'s `options.mcpAuthProviders`.
99
+ *
100
+ * This is the seam that keeps credential-bearing provider objects OFF the
101
+ * persisted {@link AgentConfig}: providers are supplied here at runtime, never
102
+ * serialized. Mirrors `HooksForAgent`.
103
+ *
104
+ * The callback is sync — the SDK does not await. Return **stable, pre-constructed**
105
+ * provider instances: returning a NEW provider object for a server on each call
106
+ * intentionally cycles that server on every update (reference identity is how a
107
+ * provider swap is detected). Consumers needing async provider setup pre-construct
108
+ * before returning.
109
+ */
110
+ export type McpAuthProviderResolver = (agentId: string, config: AgentConfig) => McpAuthProviders | undefined;
91
111
  /**
92
112
  * Harness-facing configuration for creating/updating an agent.
93
113
  *
@@ -121,6 +141,42 @@ export type HarnessAgentConfig = Omit<AgentConfig, 'orgAlias'> & {
121
141
  * `test/harness/harness-config.test.ts` that asserts unknown fields survive.
122
142
  */
123
143
  export declare function toHarnessConfig(config: AgentConfig, orgJwt?: JSONWebToken): HarnessAgentConfig;
144
+ /**
145
+ * A mutation applied to an agent's persisted consumer metadata by
146
+ * {@link AgentStateUpdate}. Two explicit actions rather than a bare value so
147
+ * "set it to this" is distinguishable from "delete it": `undefined` reads as
148
+ * "field omitted / leave unchanged" and `null` is a legal JSON value a
149
+ * consumer may legitimately want to store, so neither can double as a clear
150
+ * signal.
151
+ *
152
+ * - `replace` — set the metadata to `value` (stored as an opaque deep copy).
153
+ * - `clear` — remove the metadata entirely (a subsequent
154
+ * {@link Agent.getConsumerMetadata} returns `undefined`).
155
+ */
156
+ export type ConsumerMetadataMutation = {
157
+ action: 'replace';
158
+ value: JsonValue;
159
+ } | {
160
+ action: 'clear';
161
+ };
162
+ /**
163
+ * A single atomic update to an agent's persisted state, applied by
164
+ * {@link Agent.updateAgentState}. Either or both members may be present:
165
+ *
166
+ * - `config` — a partial {@link AgentConfig} merged into the live config and
167
+ * applied to the harness (identical semantics to {@link Agent.updateAgentConfig}).
168
+ * - `consumerMetadata` — a {@link ConsumerMetadataMutation} on the agent's
169
+ * opaque consumer metadata. This metadata is SDK-owned persistence only: it
170
+ * is never placed on {@link AgentConfig}, never passed through
171
+ * {@link toHarnessConfig}, and never forwarded to the harness.
172
+ *
173
+ * When both are present the SDK commits them in one persistence write, so a
174
+ * failed update leaves config and metadata at their previous values together.
175
+ */
176
+ export type AgentStateUpdate = {
177
+ config?: AgentConfig;
178
+ consumerMetadata?: ConsumerMetadataMutation;
179
+ };
124
180
  /**
125
181
  * Per-call options controlling streaming behavior.
126
182
  */
@@ -45,6 +45,7 @@ export type { AgentHarness, HarnessFactory, WithAgentConfig, ConfigOf } from './
45
45
  export type { AgentHooks } from '../types/redaction.js';
46
46
  export { SUPPORTED_PROTOCOL_VERSIONS } from './agent-harness.js';
47
47
  export { mcpServerConfigEqual } from '../mcp-config.js';
48
+ export type { McpAuthProviders, McpOAuthClientProvider } from '../mcp-config.js';
48
49
  export { HarnessBusOwner } from './harness-bus-owner.js';
49
50
  export { lowerStreamInput, type InputMessagePart } from './stream-input.js';
50
51
  export { GenSink } from './gen-sink.js';
package/dist/index.d.ts CHANGED
@@ -6,9 +6,9 @@ export { BUILT_IN_TOOL_POLICIES, SKILL_BRIDGE_SERVER_ID, definePolicy, matcherMa
6
6
  export type { ResolverResult, ResolverTiers, ToolInvocation } from './policy-resolver.js';
7
7
  export type { ContextUsage, FinishReason, UsageMetadata } from './types/usage.js';
8
8
  export type { AgentHooks, HooksForAgent, ToolResultRedactor, ToolResultRedactionInput, ToolResultRedactionResult, } from './types/redaction.js';
9
- export type { AgentConfig, HarnessAgentConfig, StreamOptions } from './harness/harness-config.js';
9
+ export type { AgentConfig, AgentStateUpdate, ConsumerMetadataMutation, HarnessAgentConfig, McpAuthProviderResolver, StreamOptions, } from './harness/harness-config.js';
10
10
  export { DEFAULT_MAX_STEPS } from './harness/harness-config.js';
11
- export type { MCPConfiguration, MCPServerConfig, MCPStdioServerConfig, MCPRemoteServerConfig, McpServerInfo, McpServerErrorCategory, McpServerErrorDetail, McpToolInfo, McpToolAnnotations, } from './mcp-config.js';
11
+ export type { MCPConfiguration, MCPServerConfig, MCPStdioServerConfig, MCPRemoteServerConfig, McpOAuthClientProvider, McpAuthProviders, McpServerInfo, McpServerErrorCategory, McpServerErrorDetail, McpToolInfo, McpToolAnnotations, } from './mcp-config.js';
12
12
  export { McpServerStatus, mcpServerConfigEqual } from './mcp-config.js';
13
13
  export { Model, ModelName, createClaudeModel, Models, validateMultimodalFiles, ACCEPTED_MODEL_WIRE_IDS, } from './models/index.js';
14
14
  export type { ClaudeModelOverrides, MultimodalFile, SupportedFileFormat, ThinkingMode } from './models/index.js';
@@ -28,7 +28,7 @@ export type { AgentCreatedEvent, AgentDestroyedEvent, ChatStreamCompletedEvent,
28
28
  export type { LogLevel, LogRecord, Unsubscribe } from '@salesforce/agentic-common';
29
29
  export type { EnvironmentFields } from '@salesforce/agentic-common';
30
30
  export { readEnvironmentContext, resolveFeatureId } from '@salesforce/agentic-common';
31
- export { resolveMcpServerHeaders } from './mcp-auth.js';
31
+ export { resolveMcpServerHeaders, isSalesforcePlatformMcpUrl } from './mcp-auth.js';
32
32
  export type { OrgConnection, OrgConnectionFactory } from '@salesforce/agentic-common';
33
33
  export type { JSONWebToken, JWTOptions, RequiredJWTHeaders, RequiredJWTPayload } from '@salesforce/agentic-common';
34
34
  export { Workspace } from './workspace.js';
package/dist/index.js CHANGED
@@ -23,6 +23,6 @@ export { WireCommunicationFileWriter, } from './wire-communication-file-writer.j
23
23
  export { AgentSDKError, AgentSDKErrorType } from './errors.js';
24
24
  export { readEnvironmentContext, resolveFeatureId } from '@salesforce/agentic-common';
25
25
  // ── MCP Auth ────────────────────────────────────────────────────────
26
- export { resolveMcpServerHeaders } from './mcp-auth.js';
26
+ export { resolveMcpServerHeaders, isSalesforcePlatformMcpUrl } from './mcp-auth.js';
27
27
  export { Workspace } from './workspace.js';
28
28
  //# sourceMappingURL=index.js.map
@@ -1,9 +1,11 @@
1
1
  import { type LogBus } from '@salesforce/agentic-common';
2
2
  import type { AgentConfig } from '../harness/harness-config.js';
3
+ import type { JsonValue } from '../types/session-context.js';
3
4
  export type AgentIdentityRecord = {
4
5
  agentId: string;
5
6
  projectRoot: string;
6
7
  config: AgentConfig;
8
+ consumerMetadata?: JsonValue;
7
9
  };
8
10
  /**
9
11
  * SDK-owned persistence for the agent-identity triple
@@ -33,7 +35,7 @@ export declare class AgentIdentityStore {
33
35
  */
34
36
  private readonly inflightWrites;
35
37
  constructor(storageRootFolder: string, harnessId: string, logBus: LogBus);
36
- write(agentId: string, projectRoot: string, config: AgentConfig): Promise<void>;
38
+ write(agentId: string, projectRoot: string, config: AgentConfig, consumerMetadata?: JsonValue): Promise<void>;
37
39
  private writeImmediate;
38
40
  remove(agentId: string): Promise<void>;
39
41
  list(): Promise<AgentIdentityRecord[]>;
@@ -39,11 +39,13 @@ export class AgentIdentityStore {
39
39
  this.harnessId = harnessId;
40
40
  this.logBus = logBus;
41
41
  }
42
- async write(agentId, projectRoot, config) {
42
+ async write(agentId, projectRoot, config, consumerMetadata) {
43
43
  const previous = this.inflightWrites.get(agentId) ?? Promise.resolve();
44
44
  // `.catch(() => undefined)` so a previous failure doesn't poison the next caller's await.
45
45
  // Each caller still observes its own write's success or failure via the returned promise.
46
- const next = previous.catch(() => undefined).then(() => this.writeImmediate(agentId, projectRoot, config));
46
+ const next = previous
47
+ .catch(() => undefined)
48
+ .then(() => this.writeImmediate(agentId, projectRoot, config, consumerMetadata));
47
49
  this.inflightWrites.set(agentId, next);
48
50
  try {
49
51
  await next;
@@ -56,7 +58,7 @@ export class AgentIdentityStore {
56
58
  }
57
59
  }
58
60
  }
59
- async writeImmediate(agentId, projectRoot, config) {
61
+ async writeImmediate(agentId, projectRoot, config, consumerMetadata) {
60
62
  const dir = this.dir();
61
63
  await mkdir(dir, { recursive: true });
62
64
  const payload = {
@@ -65,6 +67,7 @@ export class AgentIdentityStore {
65
67
  agentId,
66
68
  projectRoot,
67
69
  config,
70
+ ...(consumerMetadata !== undefined ? { consumerMetadata } : {}),
68
71
  };
69
72
  const target = join(dir, `${agentId}.json`);
70
73
  const tmp = `${target}.tmp`;
@@ -130,6 +133,7 @@ export class AgentIdentityStore {
130
133
  agentId: parsed.agentId,
131
134
  projectRoot: parsed.projectRoot,
132
135
  config: parsed.config,
136
+ ...(parsed.consumerMetadata !== undefined ? { consumerMetadata: parsed.consumerMetadata } : {}),
133
137
  });
134
138
  }
135
139
  return records;
@@ -1,4 +1,5 @@
1
1
  import type { JSONWebToken } from '@salesforce/agentic-common';
2
+ import type { MCPConfiguration, McpAuthProviders } from './mcp-config.js';
2
3
  /**
3
4
  * Returns true if the URL matches a Salesforce Hosted MCP Server endpoint.
4
5
  * Covers all environments (prod, dev, test, perf, stage) and all API versions
@@ -18,3 +19,22 @@ export declare function isSalesforcePlatformMcpUrl(url: string | URL): boolean;
18
19
  * @returns The resolved headers — original headers plus any injected auth.
19
20
  */
20
21
  export declare function resolveMcpServerHeaders(url: string | URL, orgJwt: JSONWebToken | undefined, headers?: Record<string, string>): Promise<Record<string, string>>;
22
+ /**
23
+ * Normalizes the raw provider map returned by a consumer's
24
+ * `mcpAuthProviderResolver` into the map the harness may act on, enforcing the
25
+ * "one Authorization owner per server" rule centrally (so no harness reimplements
26
+ * this policy). For each provider entry, keyed by server name:
27
+ *
28
+ * - **Dropped** (provider ignored) when the named server is missing, is a stdio
29
+ * server, is disabled, or is a Salesforce Platform URL. Salesforce URLs are
30
+ * authenticated by the rotating org JWT, which owns auth there; forwarding a
31
+ * provider alongside it would arm two competing Authorization owners.
32
+ * - **Rejected** (throws) when the server carries a static `Authorization`
33
+ * header AND a provider — the MCP SDK transport spreads static `requestInit`
34
+ * headers OVER the provider token, so the static header would silently win and
35
+ * strand the provider in a 401 loop. Failing loud at resolve time beats a
36
+ * silent auth failure at runtime.
37
+ *
38
+ * Returns a fresh, reference-only map (the same provider instances, never mutated).
39
+ */
40
+ export declare function normalizeMcpAuthProviders(mcpServers: MCPConfiguration | undefined, resolved: McpAuthProviders | undefined): McpAuthProviders;
package/dist/mcp-auth.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * Copyright 2026, Salesforce, Inc. All rights reserved.
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
+ import { AgentSDKError, AgentSDKErrorType } from './errors.js';
5
6
  const SALESFORCE_PLATFORM_MCP_URL_REGEX = /^https:\/\/((dev|test|perf|stage)\.)?api\.salesforce\.com\/platform\/mcp\//;
6
7
  /**
7
8
  * Returns true if the URL matches a Salesforce Hosted MCP Server endpoint.
@@ -37,4 +38,40 @@ export async function resolveMcpServerHeaders(url, orgJwt, headers = {}) {
37
38
  function hasAuthorizationHeader(headers) {
38
39
  return Object.keys(headers).some((key) => key.toLowerCase() === 'authorization');
39
40
  }
41
+ /**
42
+ * Normalizes the raw provider map returned by a consumer's
43
+ * `mcpAuthProviderResolver` into the map the harness may act on, enforcing the
44
+ * "one Authorization owner per server" rule centrally (so no harness reimplements
45
+ * this policy). For each provider entry, keyed by server name:
46
+ *
47
+ * - **Dropped** (provider ignored) when the named server is missing, is a stdio
48
+ * server, is disabled, or is a Salesforce Platform URL. Salesforce URLs are
49
+ * authenticated by the rotating org JWT, which owns auth there; forwarding a
50
+ * provider alongside it would arm two competing Authorization owners.
51
+ * - **Rejected** (throws) when the server carries a static `Authorization`
52
+ * header AND a provider — the MCP SDK transport spreads static `requestInit`
53
+ * headers OVER the provider token, so the static header would silently win and
54
+ * strand the provider in a 401 loop. Failing loud at resolve time beats a
55
+ * silent auth failure at runtime.
56
+ *
57
+ * Returns a fresh, reference-only map (the same provider instances, never mutated).
58
+ */
59
+ export function normalizeMcpAuthProviders(mcpServers, resolved) {
60
+ if (!resolved || !mcpServers)
61
+ return {};
62
+ const normalized = {};
63
+ for (const [name, provider] of Object.entries(resolved)) {
64
+ const server = mcpServers[name];
65
+ if (!server || server.type !== 'remote' || server.enabled === false)
66
+ continue;
67
+ if (isSalesforcePlatformMcpUrl(server.url))
68
+ continue;
69
+ if (server.headers && hasAuthorizationHeader(server.headers)) {
70
+ throw new AgentSDKError(`MCP server "${name}" has both a static Authorization header and an OAuth authProvider. ` +
71
+ `Remove the static Authorization header — the provider owns auth for this server.`, AgentSDKErrorType.INVALID_MCP_AUTH_CONFIG);
72
+ }
73
+ normalized[name] = provider;
74
+ }
75
+ return normalized;
76
+ }
40
77
  //# sourceMappingURL=mcp-auth.js.map
@@ -35,12 +35,82 @@ export type MCPStdioServerConfig = {
35
35
  /** Timeout in milliseconds for individual requests to the server. */
36
36
  timeout?: number;
37
37
  };
38
+ /**
39
+ * Harness-neutral contract for an MCP OAuth client provider, letting an
40
+ * OAuth-protected remote MCP server recover the RFC 9728 `401` challenge its
41
+ * transport receives (the static {@link MCPRemoteServerConfig.headers} seam
42
+ * cannot — headers are snapshotted at publish time).
43
+ *
44
+ * This package is harness-runtime-free (a lint rule bans importing
45
+ * `@modelcontextprotocol/*`), so this structurally mirrors the *required* core
46
+ * of the MCP SDK's `OAuthClientProvider` — same pattern as {@link McpToolAnnotations}.
47
+ * It is bidirectionally compatible: a real provider assigns to this field, and
48
+ * this field assigns into the transport option with no cast. Optional upstream
49
+ * members are omitted (a real instance still carries them at runtime); the
50
+ * harness-side compile-time drift-guard pins the compatibility.
51
+ */
52
+ export interface McpOAuthClientProvider {
53
+ /** URL the user agent is redirected to after authorization (or `undefined` for non-interactive flows). */
54
+ readonly redirectUrl: string | URL | undefined;
55
+ /** Metadata about this OAuth client. */
56
+ readonly clientMetadata: {
57
+ redirect_uris: string[];
58
+ };
59
+ /** Loads this client's registration, or `undefined` if not yet registered. */
60
+ clientInformation(): {
61
+ client_id: string;
62
+ } | undefined | Promise<{
63
+ client_id: string;
64
+ } | undefined>;
65
+ /** Persists a dynamic client registration for later {@link clientInformation} reads. */
66
+ saveClientInformation?(clientInformation: {
67
+ client_id: string;
68
+ }): void | Promise<void>;
69
+ /** Loads any existing OAuth tokens for the current session, or `undefined`. */
70
+ tokens(): {
71
+ access_token: string;
72
+ token_type: string;
73
+ } | undefined | Promise<{
74
+ access_token: string;
75
+ token_type: string;
76
+ } | undefined>;
77
+ /** Stores new OAuth tokens after a successful authorization. */
78
+ saveTokens(tokens: {
79
+ access_token: string;
80
+ token_type: string;
81
+ }): void | Promise<void>;
82
+ /** Redirects the user agent to the given URL to begin the authorization flow. */
83
+ redirectToAuthorization(authorizationUrl: URL): void | Promise<void>;
84
+ /** Saves a PKCE code verifier before redirecting to the authorization flow. */
85
+ saveCodeVerifier(codeVerifier: string): void | Promise<void>;
86
+ /** Loads the PKCE code verifier needed to validate the authorization result. */
87
+ codeVerifier(): string | Promise<string>;
88
+ }
89
+ /**
90
+ * A per-agent map of remote MCP server name → its OAuth {@link McpOAuthClientProvider}.
91
+ *
92
+ * Providers are behavioral, credential-bearing runtime objects, so they are
93
+ * NOT part of the persisted {@link MCPConfiguration}. They are supplied at
94
+ * runtime through the manager's `mcpAuthProviderResolver` and threaded to the
95
+ * harness via the `createAgent` / `updateAgent` options bag — never serialized
96
+ * to disk. A harness cycles a server when its provider reference changes.
97
+ */
98
+ export type McpAuthProviders = Readonly<Record<string, McpOAuthClientProvider>>;
38
99
  /** MCP server accessible over HTTP/SSE at a remote URL. */
39
100
  export type MCPRemoteServerConfig = {
40
101
  type: 'remote';
41
102
  /** URL of the remote MCP server endpoint. */
42
103
  url: string | URL;
43
- /** HTTP headers sent with requests to the server. */
104
+ /**
105
+ * HTTP headers sent with requests to the server.
106
+ *
107
+ * When an OAuth `authProvider` is supplied for this server (via the
108
+ * manager's `mcpAuthProviderResolver`), this must NOT carry an
109
+ * `Authorization` header — the MCP SDK transport spreads static
110
+ * `requestInit` headers OVER the provider token, so a static
111
+ * `Authorization` would silently win and strand the provider. The SDK
112
+ * rejects that combination at resolve time (see `normalizeMcpAuthProviders`).
113
+ */
44
114
  headers?: Record<string, string>;
45
115
  /** Whether this server is enabled. Defaults to `true`. */
46
116
  enabled?: boolean;
@@ -187,6 +257,8 @@ export type McpToolInfo = {
187
257
  export type McpServerErrorCategory =
188
258
  /** 3s default or configured timeout exceeded. */
189
259
  'connect-timeout'
260
+ /** OAuth needs user interaction; the host must authorize before reconnecting. */
261
+ | 'oauth-required'
190
262
  /** Unauthorized — JWT missing/expired. */
191
263
  | 'http-401'
192
264
  /** Forbidden — JWT valid but the principal lacks permission. */
@@ -230,7 +302,7 @@ export type McpServerErrorDetail = {
230
302
  code?: number;
231
303
  /**
232
304
  * Whether the failure is transient (worth a retry via
233
- * `Agent.reconnectMcpServer` / `Agent.refreshMcpAuth`) vs fatal (consumer
305
+ * `Agent.reconnectMcpServer`) vs fatal (consumer
234
306
  * must fix config). Harness implementations populate this with their best
235
307
  * heuristic; consumers may override per-category.
236
308
  */
@@ -259,6 +331,12 @@ export type McpServerErrorDetail = {
259
331
  * strings round-trip; `headers` (key-order-insensitive); `timeout`,
260
332
  * `reconnectionOptions` (field-wise).
261
333
  *
334
+ * This predicate compares only the serializable {@link MCPServerConfig}. A
335
+ * server's OAuth `authProvider` is NOT part of the config (it is a runtime
336
+ * binding supplied per-agent via `mcpAuthProviderResolver`); a harness cycles
337
+ * a server when this predicate fails OR its applied provider reference
338
+ * changed. See each harness's `updateAgent` diff.
339
+ *
262
340
  * Two configs that pass this predicate but produce different runtime tools
263
341
  * (e.g. an upstream stdio server whose binary was overwritten on disk) are
264
342
  * NOT detected here — the predicate compares declared config, not runtime