@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 +10 -0
- package/README.md +96 -35
- package/dist/agent-manager.d.ts +18 -4
- package/dist/agent-manager.js +24 -10
- package/dist/agent.d.ts +70 -7
- package/dist/agent.js +86 -13
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +1 -0
- package/dist/harness/agent-harness.d.ts +22 -5
- package/dist/harness/harness-config.d.ts +57 -1
- package/dist/harness/public.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/internal/agent-identity-store.d.ts +3 -1
- package/dist/internal/agent-identity-store.js +7 -3
- package/dist/mcp-auth.d.ts +20 -0
- package/dist/mcp-auth.js +37 -0
- package/dist/mcp-config.d.ts +80 -2
- package/dist/mcp-config.js +6 -0
- package/package.json +4 -4
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
|
-
|
|
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
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* the
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
183
|
-
*
|
|
184
|
-
* -
|
|
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
|
*/
|
package/dist/harness/public.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/mcp-auth.d.ts
CHANGED
|
@@ -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
|
package/dist/mcp-config.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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`
|
|
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
|