@salesforce/sfdx-agent-harness-claude 0.28.0 → 0.30.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,26 @@
3
3
  All notable changes to `@salesforce/sfdx-agent-harness-claude` are documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.30.0] - 2026-07-15
7
+
8
+ ### Features
9
+ - **agent-sdk,harness-claude,harness-mastra**: per-model gateway config — geo-aware ids, thinking mode, param exclusion @W-23447370@ ([#672](https://github.com/forcedotcom/agentic-dx/pull/672))
10
+
11
+ ## [0.29.0] - 2026-07-14
12
+
13
+ ### Features
14
+ - **agent-sdk,harness-mastra,harness-claude**: cross-harness conformance test suite @W-23372341@ ([#663](https://github.com/forcedotcom/agentic-dx/pull/663))
15
+ - **sdk,harness-mastra,harness-claude**: delete deprecated tool-approval surfaces @W-23090034 ([#637](https://github.com/forcedotcom/agentic-dx/pull/637))
16
+
17
+ ### Fixes
18
+ - **harness-conformance**: static version instead of -dev.N so release check passes @W-23446682@ ([#670](https://github.com/forcedotcom/agentic-dx/pull/670))
19
+
20
+ ### Chores
21
+ - **deps-dev**: bump typescript-eslint from 8.62.1 to 8.63.0 in the dev-dependencies group across 1 directory ([#665](https://github.com/forcedotcom/agentic-dx/pull/665))
22
+ - **deps-dev**: bump the eslint group across 1 directory with 2 updates ([#659](https://github.com/forcedotcom/agentic-dx/pull/659))
23
+ - **deps-dev**: bump @vitest/eslint-plugin from 1.6.22 to 1.6.23 in the vitest group ([#660](https://github.com/forcedotcom/agentic-dx/pull/660))
24
+ - **deps-dev**: bump the vitest group across 1 directory with 3 updates ([#655](https://github.com/forcedotcom/agentic-dx/pull/655))
25
+
6
26
  ## [0.28.0] - 2026-07-08
7
27
 
8
28
  ### Fixes
package/README.md CHANGED
@@ -29,8 +29,8 @@ const manager = await createAgentManager('/path/to/storage', harnessFactory, {
29
29
 
30
30
  Connectivity (gateway URL, JWT, headers, native model id) is resolved by an `AgentConnectivityResolver` on the SDK and
31
31
  handed to the harness as a `ModelConnectivityInfo` bag. The harness factory itself owns harness-only concerns
32
- (permission mode, tool-approval timeout, bypass list, query defaults). For BYOK / direct-Anthropic / LLMG-Express
33
- deployments use `ApiKeyConnectivityResolver`; for non-Salesforce hosts implement your own resolver. See the
32
+ (permission mode, tool-approval timeout, query defaults). For BYOK / direct-Anthropic / LLMG-Express deployments use
33
+ `ApiKeyConnectivityResolver`; for non-Salesforce hosts implement your own resolver. See the
34
34
  [`@salesforce/sfdx-agent-sdk` README](../sfdx-agent-sdk/README.md) for the full resolver surface.
35
35
 
36
36
  See the [`@salesforce/sfdx-agent-sdk` README](../sfdx-agent-sdk/README.md) for the full consumer-facing API. For
@@ -41,7 +41,7 @@ internal architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
41
41
  | Export | Kind | Description |
42
42
  | ------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
43
43
  | `ClaudeHarnessFactory` | class | `HarnessFactory` that produces a Claude-backed `AgentHarness` bound to a storage root. |
44
- | `ClaudeHarnessFactoryConfig` | interface | Configuration for `ClaudeHarnessFactory` (`permissionMode`, optional `queryDefaults`, `toolApprovalTimeoutMs`, deprecated `bypassApprovalTools`, `subprocessEnv`). |
44
+ | `ClaudeHarnessFactoryConfig` | interface | Configuration for `ClaudeHarnessFactory` (`permissionMode`, optional `queryDefaults`, `toolApprovalTimeoutMs`, `subprocessEnv`). |
45
45
  | `CLAUDE_BUILT_IN_TOOL_POLICIES` | const | Frozen `ReadonlyArray<ToolPolicyRule>` the harness feeds the SDK's `resolveToolApprovalPolicy` as the `tiers.harness` slice — `require-approval` rules for Claude's mutating built-ins `Bash` / `Edit` / `MultiEdit` / `Write` / `NotebookEdit`. |
46
46
  | `CLAUDE_BUILT_IN_TOOL_NAMES` | const | The five built-in tool names `CLAUDE_BUILT_IN_TOOL_POLICIES` covers. |
47
47
  | `ClaudeHarnessPermissionMode` | type | Narrowed permission mode: `'bypassPermissions' \| 'acceptEdits' \| 'plan'`. |
@@ -172,19 +172,14 @@ decision site: for each non-consumer tool it resolves a decision and acts on it:
172
172
  - **`require-approval`** — a `tool-approval-request` is emitted and the SDK hook parks until the consumer settles via
173
173
  `chatSession.approveToolCall(toolCallId)` / `declineToolCall(toolCallId)`.
174
174
 
175
- Gating is **opt-in**: it engages only when the agent's config sets `toolPolicies` or `defaultToolDecision`, or the
176
- `chat()` call sets the deprecated `requireToolApproval` or `batchApprovals`. With none configured, tools run without
177
- gating (no `PreToolUse` hook is installed), matching the SDK's "no policy ⇒ no gating" default. The harness adds
178
- `CLAUDE_BUILT_IN_TOOL_POLICIES` (`require-approval` rules for the mutating built-ins `Bash` / `Edit` / `MultiEdit` /
179
- `Write` / `NotebookEdit`) as the `tiers.harness` slice and the SDK's `skill_bridge` meta-tool rules as `allow` defaults,
180
- so capability-discovery never prompts and read-only built-ins (`Read` / `Glob` / `Grep` / `Task`) run free by absence.
181
- Consumer-executed tools (`AgentConfig.tools`) always bypass the gate — their execution is the consumer's responsibility
182
- via `submitToolResult`. Set `StreamOptions.batchApprovals: true` to surface parallel `require-approval` requests on one
183
- stream for a batch-approval UI.
184
-
185
- > `StreamOptions.requireToolApproval` is **deprecated** — superseded by per-tool `AgentConfig.toolPolicies`. It still
186
- > works for one release: a truthy value engages all-or-nothing gating (every non-consumer tool requires approval), and
187
- > `'batch'` maps onto `batchApprovals`.
175
+ Gating is **opt-in**: it engages only when the agent's config sets `toolPolicies` or `defaultToolDecision`. With none
176
+ configured, tools run without gating (no `PreToolUse` hook is installed), matching the SDK's "no policy ⇒ no gating"
177
+ default. The harness adds `CLAUDE_BUILT_IN_TOOL_POLICIES` (`require-approval` rules for the mutating built-ins `Bash` /
178
+ `Edit` / `MultiEdit` / `Write` / `NotebookEdit`) as the `tiers.harness` slice and the SDK's `skill_bridge` meta-tool
179
+ rules as `allow` defaults, so capability-discovery never prompts and read-only built-ins (`Read` / `Glob` / `Grep` /
180
+ `Task`) run free by absence. Consumer-executed tools (`AgentConfig.tools`) always bypass the gate — their execution is
181
+ the consumer's responsibility via `submitToolResult`. Set `StreamOptions.batchApprovals: true` to surface parallel
182
+ `require-approval` requests on one stream for a batch-approval UI.
188
183
 
189
184
  Unlike Mastra (which suspends every tool and re-stamps a declined native result), Claude's `deny` is the SDK's native
190
185
  `permissionDecision: 'deny'` — the Claude Agent SDK synthesizes the `tool_result(isError=true)` itself, so no
@@ -206,7 +201,7 @@ patterns. Single-tool approve / decline, parallel batches (`batchApprovals: true
206
201
 
207
202
  #### Abort signal
208
203
 
209
- When `StreamOptions.abortSignal` fires during an active turn with `requireToolApproval: true`:
204
+ When `StreamOptions.abortSignal` fires during an active turn with approval gating on:
210
205
 
211
206
  - All pending permission Promises reject with an `AbortError`.
212
207
  - The underlying Claude subprocess is killed (existing `AbortController` wiring).
@@ -391,45 +386,16 @@ const agent = await manager.createAgent(projectRoot, {
391
386
  // `tool-approval-request`, regardless of any policy configured.
392
387
  ```
393
388
 
394
- This is the recommended pattern when the consumer wants identical behavior across Mastra and Claude. For Claude-only
395
- consumers who prefer to drive the built-in directly, see "Bypassing the approval gate for specific tools" below.
396
-
397
- ### Bypassing the approval gate for specific tools (deprecated)
398
-
399
- > **Deprecated.** `ClaudeHarnessFactoryConfig.bypassApprovalTools` is superseded by per-tool `AgentConfig.toolPolicies`
400
- > (e.g. `definePolicy({ AskUserQuestion: 'allow' })`). It still works for one release — the harness runtime-merges the
401
- > list into the policy resolver as synthetic `'allow'` rules (the `tiers.factory` slice) and logs a once-per-process
402
- > warning pointing at `toolPolicies`. It is removed in a later phase. Prefer `toolPolicies` for new code.
403
-
404
- When gating is active, every built-in / MCP tool call reaches the `PreToolUse` hook, which resolves it against the
405
- policy resolver. (Consumer-declared tools from `AgentConfig.tools[i]` always bypass — see "Tool-approval policy" above.)
406
- `bypassApprovalTools` opts specific tool names to `'allow'`:
407
-
408
- ```ts
409
- const harnessFactory = new ClaudeHarnessFactory({
410
- permissionMode: 'bypassPermissions',
411
- // DEPRECATED — equivalent to:
412
- // toolPolicies: definePolicy({ AskUserQuestion: 'allow' })
413
- // Calls to these tools resolve to 'allow'; they still emit `tool-call` /
414
- // `tool-result` ChatEvents so the consumer can drive UX off the `tool-call`.
415
- bypassApprovalTools: ['AskUserQuestion'],
416
- });
417
- ```
418
-
419
- The names become synthetic `{ type: 'builtin', name, decision: 'allow' }` rules in the resolver's `tiers.factory` slice.
420
- They sit below the SDK built-ins and the harness built-ins but above consumer `toolPolicies`, so a consumer `deny` of
421
- the same tool still wins (cross-tier deny-wins). Bare-name form only — consumer-declared tools already bypass via the
422
- always-on consumer-tool prefix, so the old dual-form (`mcp__sfdx-agent-sdk-consumer-tools__<name>`) expansion is no
423
- longer needed.
389
+ This is the recommended pattern when the consumer wants identical behavior across Mastra and Claude. To auto-approve a
390
+ specific built-in the consumer drives directly (e.g. `AskUserQuestion`), add an `'allow'` rule for it to
391
+ `AgentConfig.toolPolicies` (e.g. `definePolicy({ AskUserQuestion: 'allow' })`) — it resolves to `'allow'` and still
392
+ emits `tool-call` / `tool-result` ChatEvents so the consumer can drive UX off the `tool-call`.
424
393
 
425
394
  > **Note:** `permissionMode: 'plan'` denies mutating built-ins (`Edit`, `Write`, `Bash`, …) at the SDK's permission
426
395
  > cache rather than running them natively. Resolving such a tool to `'allow'` does not "unlock" it under plan mode — the
427
396
  > SDK still denies it. Plan mode applies to the whole subprocess; the policy resolver only chooses whether the harness's
428
397
  > coordinator gates the call.
429
398
 
430
- When `bypassApprovalTools` is undefined or empty and no `toolPolicies` / `defaultToolDecision` is set, gating is off and
431
- no `PreToolUse` hook is installed — no behavior change for existing consumers. (Consumer-declared tools always bypass.)
432
-
433
399
  ### Tool-result redaction
434
400
 
435
401
  Tool-result redaction is configured at the manager layer, not the factory. Pass a `hooksForAgent` callback to
@@ -37,10 +37,10 @@ export interface ClaudeApprovalCoordinatorOptions {
37
37
  * `tool_result(is_error=true)` natively, so no re-stamp is needed);
38
38
  * `'require-approval'` ⇒ emit `tool-approval-request` and park. The harness
39
39
  * supplies this (closing over the SDK's `resolveToolApprovalPolicy` +
40
- * `CLAUDE_BUILT_IN_TOOL_POLICIES` + the deprecated `bypassApprovalTools`
41
- * factory tier) only when its opt-in gating predicate engaged; the hook is
42
- * wired iff this is present, so a missing resolver here can only be reached
43
- * defensively (it falls back to `'require-approval'`).
40
+ * `CLAUDE_BUILT_IN_TOOL_POLICIES`) only when its opt-in gating predicate
41
+ * engaged; the hook is wired iff this is present, so a missing resolver
42
+ * here can only be reached defensively (it falls back to
43
+ * `'require-approval'`).
44
44
  */
45
45
  lookupPolicy?: (invocation: ToolInvocation) => Decision;
46
46
  /**
@@ -27,9 +27,9 @@ import { GenSink } from '@salesforce/sfdx-agent-sdk/harness';
27
27
  * `chat()`. There are no per-tool sinks and no `lastGenSinkStale` flag.
28
28
  *
29
29
  * Consumer-executed tools bypass the approval gate by construction
30
- * (issue #529 step 2b): `claude-query-options-builder.ts` adds the
31
- * consumer-tool wire prefix `mcp__sfdx-agent-sdk-consumer-tools__.*` to
32
- * the `bypassApprovalTools` matcher unconditionally, so the SDK skips
30
+ * (issue #529 step 2b): `claude-query-options-builder.ts` anchors the
31
+ * consumer-tool wire prefix `mcp__sfdx-agent-sdk-consumer-tools__.*` into
32
+ * the `PreToolUse` matcher's negative lookahead, so the SDK skips
33
33
  * `PreToolUse` for consumer tools entirely. This coordinator therefore
34
34
  * never sees a `PreToolUse` invocation for a consumer tool — the parker
35
35
  * path handles their lifecycle alone.
@@ -5,36 +5,21 @@ import { type ClaudeAgentHarness } from './claude-harness.js';
5
5
  * Subset of the Claude SDK's `PermissionMode` enum that the harness supports.
6
6
  *
7
7
  * Excludes `'default'`, `'dontAsk'`, and `'auto'` because those require an
8
- * interactive prompt source that the harness does not provide. Use
9
- * `StreamOptions.requireToolApproval` for human-in-the-loop gating; the
10
- * harness overrides `permissionMode` to `'default'` per-turn when approval is
11
- * requested.
8
+ * interactive prompt source that the harness does not provide. Configure
9
+ * `AgentConfig.toolPolicies` / `defaultToolDecision` for human-in-the-loop
10
+ * gating; the harness wires the coordinator's `PreToolUse` hook per-turn when
11
+ * gating is active.
12
12
  */
13
13
  export type ClaudeHarnessPermissionMode = 'bypassPermissions' | 'acceptEdits' | 'plan';
14
14
  export interface ClaudeHarnessFactoryConfig {
15
15
  permissionMode: ClaudeHarnessPermissionMode;
16
16
  queryDefaults?: ClaudeQueryDefaults;
17
17
  /**
18
- * How long to wait for a `requireToolApproval` consumer to call
18
+ * How long to wait for a consumer to call
19
19
  * `approveToolCall`/`declineToolCall` before auto-denying with interrupt.
20
20
  * Defaults to `600_000` (10 minutes), matching the Mastra MCP timeout.
21
21
  */
22
22
  toolApprovalTimeoutMs?: number;
23
- /**
24
- * @deprecated Use `AgentConfig.toolPolicies` (e.g.
25
- * `definePolicy({ <tool>: 'allow' })`) instead. Runtime-merged into the
26
- * tool-approval policy resolver as synthetic `'allow'` rules during the
27
- * deprecation window; removed in Phase 4. Removing a name takes effect on
28
- * the next stream (not persisted). A once-per-process `LogBus.warn` fires
29
- * when a non-empty list is configured.
30
- *
31
- * Tool names that bypass the approval gate. Useful for built-ins like
32
- * `AskUserQuestion` where the consumer drives the UX off the `tool-call`
33
- * event directly and doesn't want a `tool-approval-request` for it. The
34
- * subprocess runs these tools natively (per the factory-pinned
35
- * `permissionMode`); they still emit `tool-call` / `tool-result` ChatEvents.
36
- */
37
- bypassApprovalTools?: string[];
38
23
  /**
39
24
  * Static operational environment variables to set on every Claude
40
25
  * subprocess spawn. Layered above the inherited host env (so these win
@@ -37,13 +37,7 @@ export class ClaudeHarnessFactory {
37
37
  // call site inside `ClaudeHarness` (createAgent, reconnect, updateAgent)
38
38
  // routes MCP HTTP transports through the same proxy-aware fetch.
39
39
  const mcpClientFactory = (config, orgJwt) => buildMcpClients(config, orgJwt, innerFetch);
40
- return new ClaudeHarness(envBuilder, optionsBuilder, this.config.toolApprovalTimeoutMs ?? DEFAULT_TOOL_APPROVAL_TIMEOUT_MS, storageRootFolder,
41
- // `bypassApprovalTools` is now consumed by the harness (folded into
42
- // the policy resolver as synthetic `tiers.factory` allow rules)
43
- // rather than baked into a constructor-time options-builder regex —
44
- // the matcher is per-stream now and the resolver is the decision
45
- // site. Deprecated; removed in Phase 4.
46
- { mcpClientFactory, bypassApprovalTools: this.config.bypassApprovalTools });
40
+ return new ClaudeHarness(envBuilder, optionsBuilder, this.config.toolApprovalTimeoutMs ?? DEFAULT_TOOL_APPROVAL_TIMEOUT_MS, storageRootFolder, { mcpClientFactory });
47
41
  }
48
42
  }
49
43
  //# sourceMappingURL=claude-harness-factory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"claude-harness-factory.js","sourceRoot":"","sources":["../src/claude-harness-factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAG3F,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,aAAa,EAA2B,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAa1D,MAAM,gCAAgC,GAAG,OAAO,CAAC;AAiDjD,MAAM,OAAO,oBAAoB;IAYA;IAXpB,SAAS,GAAG,QAAQ,CAAC;IACrB,eAAe,GAAG,CAAC,CAAC;IAC7B;;;;;;OAMG;IACM,sBAAsB,GAA4B,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAC;IAE9F,YAA6B,MAAkC;QAAlC,WAAM,GAAN,MAAM,CAA4B;IAAG,CAAC;IAEnE,KAAK,CAAC,MAAM,CAAC,iBAAyB;QAClC,MAAM,UAAU,GAAG,qBAAqB,CAAC,sBAAsB,EAAE,CAAC,CAAC;QACnE,MAAM,UAAU,GAAG,IAAI,qBAAqB,CAAC,iBAAiB,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACtG,MAAM,kBAAkB,GAAG;YACvB,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc;YAC1C,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,mBAAmB;gBAClD,CAAC,CAAC,EAAE,+BAA+B,EAAE,IAAa,EAAE;gBACpD,CAAC,CAAC,EAAE,CAAC;SACZ,CAAC;QACF,MAAM,cAAc,GAAG,IAAI,yBAAyB,CAAC,EAAE,GAAG,kBAAkB,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;QAC9G,iEAAiE;QACjE,yEAAyE;QACzE,iEAAiE;QACjE,MAAM,gBAAgB,GAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAChE,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;QAChD,OAAO,IAAI,aAAa,CACpB,UAAU,EACV,cAAc,EACd,IAAI,CAAC,MAAM,CAAC,qBAAqB,IAAI,gCAAgC,EACrE,iBAAiB;QACjB,oEAAoE;QACpE,gEAAgE;QAChE,oEAAoE;QACpE,iEAAiE;QACjE,wCAAwC;QACxC,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,IAAI,CAAC,MAAM,CAAC,mBAAmB,EAAE,CAC7E,CAAC;IACN,CAAC;CACJ"}
1
+ {"version":3,"file":"claude-harness-factory.js","sourceRoot":"","sources":["../src/claude-harness-factory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAG3F,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,aAAa,EAA2B,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAa1D,MAAM,gCAAgC,GAAG,OAAO,CAAC;AAkCjD,MAAM,OAAO,oBAAoB;IAYA;IAXpB,SAAS,GAAG,QAAQ,CAAC;IACrB,eAAe,GAAG,CAAC,CAAC;IAC7B;;;;;;OAMG;IACM,sBAAsB,GAA4B,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAC;IAE9F,YAA6B,MAAkC;QAAlC,WAAM,GAAN,MAAM,CAA4B;IAAG,CAAC;IAEnE,KAAK,CAAC,MAAM,CAAC,iBAAyB;QAClC,MAAM,UAAU,GAAG,qBAAqB,CAAC,sBAAsB,EAAE,CAAC,CAAC;QACnE,MAAM,UAAU,GAAG,IAAI,qBAAqB,CAAC,iBAAiB,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACtG,MAAM,kBAAkB,GAAG;YACvB,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc;YAC1C,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,mBAAmB;gBAClD,CAAC,CAAC,EAAE,+BAA+B,EAAE,IAAa,EAAE;gBACpD,CAAC,CAAC,EAAE,CAAC;SACZ,CAAC;QACF,MAAM,cAAc,GAAG,IAAI,yBAAyB,CAAC,EAAE,GAAG,kBAAkB,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;QAC9G,iEAAiE;QACjE,yEAAyE;QACzE,iEAAiE;QACjE,MAAM,gBAAgB,GAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAChE,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;QAChD,OAAO,IAAI,aAAa,CACpB,UAAU,EACV,cAAc,EACd,IAAI,CAAC,MAAM,CAAC,qBAAqB,IAAI,gCAAgC,EACrE,iBAAiB,EACjB,EAAE,gBAAgB,EAAE,CACvB,CAAC;IACN,CAAC;CACJ"}
@@ -47,14 +47,6 @@ export interface ClaudeHarnessOptions {
47
47
  sessionStore?: DiskBackedSessionStore;
48
48
  mcpClientFactory?: typeof buildMcpClients;
49
49
  threadContextStore?: ThreadContextStore;
50
- /**
51
- * Deprecated `ClaudeHarnessFactoryConfig.bypassApprovalTools` names. The
52
- * factory threads them here so the harness can fold them into the policy
53
- * resolver as synthetic `'allow'` rules (the `tiers.factory` slice) during
54
- * the deprecation window — runtime-merged, not persisted, removed in Phase
55
- * 4. The deprecation `LogBus.warn` (once per process) fires from here.
56
- */
57
- bypassApprovalTools?: string[];
58
50
  }
59
51
  export declare class ClaudeHarness implements ClaudeAgentHarness {
60
52
  private readonly envBuilder;
@@ -94,13 +86,6 @@ export declare class ClaudeHarness implements ClaudeAgentHarness {
94
86
  private readonly mcpClientFactory;
95
87
  private readonly storageRootFolder;
96
88
  private shuttingDown;
97
- /**
98
- * Synthetic `'allow'` rules translated from the deprecated factory
99
- * `bypassApprovalTools` list — the `tiers.factory` slice passed to
100
- * `resolveToolApprovalPolicy`. Empty when no bypass list was configured;
101
- * removed entirely in Phase 4. See {@link ClaudeHarnessOptions.bypassApprovalTools}.
102
- */
103
- private readonly bypassApprovalToolsFactoryTier;
104
89
  constructor(envBuilder: ClaudeQueryEnvBuilder, optionsBuilder: ClaudeQueryOptionsBuilder, toolApprovalTimeoutMs: number, storageRootFolder: string, opts?: ClaudeHarnessOptions);
105
90
  private coordinatorKey;
106
91
  /**
@@ -328,31 +313,19 @@ export declare class ClaudeHarness implements ClaudeAgentHarness {
328
313
  */
329
314
  private buildPostToolUseRedactionHook;
330
315
  /**
331
- * Phase 2b: decide whether the approval gate engages this turn and, if so,
332
- * build the per-tool policy resolver the coordinator's `preToolUseHook`
333
- * consults. Near-verbatim port of `MastraHarness.buildPolicyGate` so both
334
- * harnesses share the same opt-in semantics and deprecation bridge.
316
+ * Decide whether the approval gate engages this turn and, if so, build the
317
+ * per-tool policy resolver the coordinator's `preToolUseHook` consults.
318
+ * Mirrors `MastraHarness.buildPolicyGate` so both harnesses share the same
319
+ * opt-in semantics.
335
320
  *
336
- * Gating is **opt-in**. It engages when the consumer set any of:
337
- * - `config.toolPolicies` (non-empty),
338
- * - `config.defaultToolDecision`,
339
- * - the deprecated `StreamOptions.requireToolApproval` (any non-`false`
340
- * value — `deprecatedMode` is `'serial'` / `'batch'` / `undefined`), or
341
- * - `StreamOptions.batchApprovals: true`.
321
+ * Gating is **opt-in**. It engages when the consumer set either:
322
+ * - `config.toolPolicies` (non-empty), or
323
+ * - `config.defaultToolDecision`.
342
324
  *
343
- * When none are set, returns `{ lookupPolicy: undefined }` — the caller
325
+ * When neither is set, returns `{ lookupPolicy: undefined }` — the caller
344
326
  * omits the `PreToolUse` hook and tools run free (the SDK's "no policy ⇒ no
345
327
  * gating" back-compat invariant), with no per-tool hook round-trip.
346
328
  *
347
- * **Deprecation bridge.** When the gate engages *only* because the
348
- * deprecated `requireToolApproval` was truthy (no `toolPolicies`, no
349
- * `defaultToolDecision`), the effective default must be `'require-approval'`
350
- * — legacy all-or-none — not the resolver's `'allow'` default. Built-in
351
- * allow-rules (the skill_bridge meta-tools) and any consumer `toolPolicies`
352
- * still win via within-tier last-wins / cross-tier deny-wins, so the
353
- * capability-discovery meta-tools stay auto-approved even under legacy
354
- * gating.
355
- *
356
329
  * **Live config read (#638).** `lookupPolicy` reads `agentState.config` on
357
330
  * every call rather than closing over a snapshot taken at stream start. A
358
331
  * mid-turn `updateAgentConfig` — notably the `source: 'remember'` rule an
@@ -363,22 +336,12 @@ export declare class ClaudeHarness implements ClaudeAgentHarness {
363
336
  * auto-resolve subsequent matching tool calls within the same turn. Mirrors
364
337
  * `MastraHarness.buildPolicyGate`.
365
338
  *
366
- * Two decisions stay **frozen at stream start** and MUST NOT be recomputed
367
- * live:
368
- * - `gatingActive` — the turn either installed the `PreToolUse` hook or it
369
- * didn't; gating cannot begin mid-turn. The closure is built (and
370
- * returned) iff gating engaged now.
371
- * - `applyLegacyBridge` (`!hasPolicy && legacyGating`) — whether the
372
- * deprecation bridge injects `defaultToolDecision: 'require-approval'`.
373
- * If this were recomputed live, the FIRST remembered rule would flip
374
- * `hasPolicy` false→true, turn the bridge OFF mid-turn, and silently drop
375
- * the effective default back to the resolver's `'allow'` — auto-approving
376
- * every remaining legacy-gated tool. Freezing the boolean and applying it
377
- * to the live config preserves legacy all-or-none semantics while still
378
- * honoring any rule the turn appends.
339
+ * `gatingActive` stays **frozen at stream start** — the turn either
340
+ * installed the `PreToolUse` hook or it didn't; gating cannot begin
341
+ * mid-turn. The closure is built (and returned) iff gating engaged now.
379
342
  *
380
- * `batchApprovals` is the serial-vs-batch emission axis: the explicit
381
- * `StreamOptions.batchApprovals` OR the legacy `requireToolApproval: 'batch'`.
343
+ * `batchApprovals` is the serial-vs-batch emission axis, read straight off
344
+ * `StreamOptions.batchApprovals`.
382
345
  */
383
346
  private buildPolicyGate;
384
347
  stream(agentId: string, threadId: string, message: string | MessagePart[], streamOptions?: StreamOptions): Promise<ChatStreamResult>;
@@ -5,7 +5,7 @@
5
5
  import { appendFile, mkdir, realpath } from 'node:fs/promises';
6
6
  import { join } from 'node:path';
7
7
  import { RealClock, UUIDGenerator, backfillCreatedAt, buildSummaryPrompt, getErrorMessage, isAbortError, } from '@salesforce/agentic-common';
8
- import { AgentSDKError, AgentSDKErrorType, McpServerStatus, resolveToolApprovalMode, resolveToolApprovalPolicy, } from '@salesforce/sfdx-agent-sdk';
8
+ import { AgentSDKError, AgentSDKErrorType, McpServerStatus, resolveToolApprovalPolicy, } from '@salesforce/sfdx-agent-sdk';
9
9
  import { HarnessBusOwner, lowerStreamInput, matchesAlwaysActive, mcpServerConfigEqual, splitToolResultsIntoToolMessages, validateAlwaysActiveEntry, } from '@salesforce/sfdx-agent-sdk/harness';
10
10
  import { validateMultimodalFiles } from '@salesforce/sfdx-agent-sdk';
11
11
  import { deleteSession, forkSession, getSessionMessages, listSessions, query, } from '@anthropic-ai/claude-agent-sdk';
@@ -86,13 +86,6 @@ export class ClaudeHarness {
86
86
  mcpClientFactory;
87
87
  storageRootFolder;
88
88
  shuttingDown = false;
89
- /**
90
- * Synthetic `'allow'` rules translated from the deprecated factory
91
- * `bypassApprovalTools` list — the `tiers.factory` slice passed to
92
- * `resolveToolApprovalPolicy`. Empty when no bypass list was configured;
93
- * removed entirely in Phase 4. See {@link ClaudeHarnessOptions.bypassApprovalTools}.
94
- */
95
- bypassApprovalToolsFactoryTier;
96
89
  constructor(envBuilder, optionsBuilder, toolApprovalTimeoutMs, storageRootFolder, opts = {}) {
97
90
  this.envBuilder = envBuilder;
98
91
  this.optionsBuilder = optionsBuilder;
@@ -109,28 +102,6 @@ export class ClaudeHarness {
109
102
  this.threadContextStore =
110
103
  opts.threadContextStore ?? new ThreadContextStore(storageRootFolder, this.buses.getLogBus());
111
104
  this.mcpClientFactory = opts.mcpClientFactory ?? buildMcpClients;
112
- // Translate the deprecated `bypassApprovalTools` factory list into
113
- // synthetic `'allow'` rules (the resolver's `tiers.factory` slice).
114
- // Bare-name `builtin` matchers only: the always-on consumer-tool prefix
115
- // bypass in the options builder's matcher already covers the
116
- // consumer-tool-by-name case the old dual-form expansion served, so the
117
- // bridge-prefixed form is unnecessary under the resolver. `source` is
118
- // `'built-in'` (not `'agent-config'`) because these are harness-injected
119
- // — "which tier fired" telemetry should attribute the bypass to the
120
- // harness, not the consumer's AgentConfig.
121
- const bypassNames = opts.bypassApprovalTools ?? [];
122
- this.bypassApprovalToolsFactoryTier = Object.freeze(bypassNames.map((name) => ({
123
- matcher: { type: 'builtin', name },
124
- decision: 'allow',
125
- source: 'built-in',
126
- })));
127
- if (bypassNames.length > 0) {
128
- this.buses
129
- .getLogBus()
130
- ?.warn('ClaudeHarnessFactoryConfig.bypassApprovalTools is deprecated; it is runtime-merged into the ' +
131
- 'tool-approval policy resolver as synthetic allow rules and will be removed. Migrate to ' +
132
- 'AgentConfig.toolPolicies (e.g. definePolicy({ <tool>: "allow" })).', { bypassApprovalTools: bypassNames });
133
- }
134
105
  }
135
106
  coordinatorKey(agentId, threadId) {
136
107
  return `${agentId}::${threadId}`;
@@ -893,7 +864,11 @@ export class ClaudeHarness {
893
864
  * `canUseTool` broker — the task is text-in / text-out.
894
865
  */
895
866
  async summarize(agent, prompt) {
896
- const { env, nativeModelId } = await this.envBuilder.build(agent.modelConnectivityInfo);
867
+ const { env, nativeModelId } = await this.envBuilder.build(agent.modelConnectivityInfo,
868
+ // Drive the same per-model thinking shape as a normal turn — the summary
869
+ // call hits the same gateway backend and would 400 on Opus 4.7/4.8 without
870
+ // the adaptive-capability env injection (see ClaudeQueryEnvBuilder).
871
+ agent.model.thinkingMode);
897
872
  const summaryConfig = {
898
873
  orgJwt: agent.config.orgJwt,
899
874
  // The role declaration + summarization imperative + the
@@ -908,7 +883,16 @@ export class ClaudeHarness {
908
883
  'Preserve numeric values, dates, and proper names verbatim. ' +
909
884
  'Do not invent details that are not present in the transcript.',
910
885
  };
911
- const options = this.optionsBuilder.build(agent.projectRoot, summaryConfig, { maxSteps: 1 }, env, undefined, nativeModelId, undefined, undefined, undefined);
886
+ const options = this.optionsBuilder.build(agent.projectRoot, summaryConfig, { maxSteps: 1 }, env, undefined, // sessionOptions
887
+ nativeModelId, undefined, // brokerPreToolUseHook
888
+ undefined, // mcpServers
889
+ undefined, // rulesAppend
890
+ undefined, // postToolUseRedactionHook
891
+ undefined, // skillDirectories
892
+ // Same per-model thinking shape as a normal turn — the summary call
893
+ // hits the same gateway backend and would 400 on Opus 4.7/4.8 with
894
+ // the CLI's default `"enabled"` shape otherwise.
895
+ agent.model.thinkingMode);
912
896
  // Override post-build: ephemeral session, no persistence, no
913
897
  // sessionStore (rejected together with persistSession:false).
914
898
  options.persistSession = false;
@@ -1048,31 +1032,19 @@ export class ClaudeHarness {
1048
1032
  };
1049
1033
  }
1050
1034
  /**
1051
- * Phase 2b: decide whether the approval gate engages this turn and, if so,
1052
- * build the per-tool policy resolver the coordinator's `preToolUseHook`
1053
- * consults. Near-verbatim port of `MastraHarness.buildPolicyGate` so both
1054
- * harnesses share the same opt-in semantics and deprecation bridge.
1035
+ * Decide whether the approval gate engages this turn and, if so, build the
1036
+ * per-tool policy resolver the coordinator's `preToolUseHook` consults.
1037
+ * Mirrors `MastraHarness.buildPolicyGate` so both harnesses share the same
1038
+ * opt-in semantics.
1055
1039
  *
1056
- * Gating is **opt-in**. It engages when the consumer set any of:
1057
- * - `config.toolPolicies` (non-empty),
1058
- * - `config.defaultToolDecision`,
1059
- * - the deprecated `StreamOptions.requireToolApproval` (any non-`false`
1060
- * value — `deprecatedMode` is `'serial'` / `'batch'` / `undefined`), or
1061
- * - `StreamOptions.batchApprovals: true`.
1040
+ * Gating is **opt-in**. It engages when the consumer set either:
1041
+ * - `config.toolPolicies` (non-empty), or
1042
+ * - `config.defaultToolDecision`.
1062
1043
  *
1063
- * When none are set, returns `{ lookupPolicy: undefined }` — the caller
1044
+ * When neither is set, returns `{ lookupPolicy: undefined }` — the caller
1064
1045
  * omits the `PreToolUse` hook and tools run free (the SDK's "no policy ⇒ no
1065
1046
  * gating" back-compat invariant), with no per-tool hook round-trip.
1066
1047
  *
1067
- * **Deprecation bridge.** When the gate engages *only* because the
1068
- * deprecated `requireToolApproval` was truthy (no `toolPolicies`, no
1069
- * `defaultToolDecision`), the effective default must be `'require-approval'`
1070
- * — legacy all-or-none — not the resolver's `'allow'` default. Built-in
1071
- * allow-rules (the skill_bridge meta-tools) and any consumer `toolPolicies`
1072
- * still win via within-tier last-wins / cross-tier deny-wins, so the
1073
- * capability-discovery meta-tools stay auto-approved even under legacy
1074
- * gating.
1075
- *
1076
1048
  * **Live config read (#638).** `lookupPolicy` reads `agentState.config` on
1077
1049
  * every call rather than closing over a snapshot taken at stream start. A
1078
1050
  * mid-turn `updateAgentConfig` — notably the `source: 'remember'` rule an
@@ -1083,49 +1055,26 @@ export class ClaudeHarness {
1083
1055
  * auto-resolve subsequent matching tool calls within the same turn. Mirrors
1084
1056
  * `MastraHarness.buildPolicyGate`.
1085
1057
  *
1086
- * Two decisions stay **frozen at stream start** and MUST NOT be recomputed
1087
- * live:
1088
- * - `gatingActive` — the turn either installed the `PreToolUse` hook or it
1089
- * didn't; gating cannot begin mid-turn. The closure is built (and
1090
- * returned) iff gating engaged now.
1091
- * - `applyLegacyBridge` (`!hasPolicy && legacyGating`) — whether the
1092
- * deprecation bridge injects `defaultToolDecision: 'require-approval'`.
1093
- * If this were recomputed live, the FIRST remembered rule would flip
1094
- * `hasPolicy` false→true, turn the bridge OFF mid-turn, and silently drop
1095
- * the effective default back to the resolver's `'allow'` — auto-approving
1096
- * every remaining legacy-gated tool. Freezing the boolean and applying it
1097
- * to the live config preserves legacy all-or-none semantics while still
1098
- * honoring any rule the turn appends.
1058
+ * `gatingActive` stays **frozen at stream start** — the turn either
1059
+ * installed the `PreToolUse` hook or it didn't; gating cannot begin
1060
+ * mid-turn. The closure is built (and returned) iff gating engaged now.
1099
1061
  *
1100
- * `batchApprovals` is the serial-vs-batch emission axis: the explicit
1101
- * `StreamOptions.batchApprovals` OR the legacy `requireToolApproval: 'batch'`.
1062
+ * `batchApprovals` is the serial-vs-batch emission axis, read straight off
1063
+ * `StreamOptions.batchApprovals`.
1102
1064
  */
1103
- buildPolicyGate(agentState, streamOptions, deprecatedMode) {
1065
+ buildPolicyGate(agentState, streamOptions) {
1104
1066
  const config = agentState.config;
1105
- const hasPolicy = (config.toolPolicies?.length ?? 0) > 0 || config.defaultToolDecision !== undefined;
1106
- const legacyGating = deprecatedMode !== undefined;
1107
- const batchApprovals = streamOptions?.batchApprovals === true || deprecatedMode === 'batch';
1108
- const gatingActive = hasPolicy || legacyGating || batchApprovals;
1067
+ const gatingActive = (config.toolPolicies?.length ?? 0) > 0 || config.defaultToolDecision !== undefined;
1109
1068
  if (!gatingActive) {
1110
1069
  return { batchApprovals: false };
1111
1070
  }
1112
- // Freeze the deprecation-bridge decision at stream start (see JSDoc) —
1113
- // whether to inject `defaultToolDecision: 'require-approval'` must not
1114
- // change when a mid-turn remember rule flips `hasPolicy`.
1115
- const applyLegacyBridge = !hasPolicy && legacyGating;
1116
- const lookupPolicy = (invocation) => {
1117
- // Read the config live so a mid-turn `updateAgentConfig` (e.g. a
1118
- // remembered rule) is honored for the rest of this turn.
1119
- const liveConfig = agentState.config;
1120
- const resolverConfig = applyLegacyBridge
1121
- ? { ...liveConfig, defaultToolDecision: 'require-approval' }
1122
- : liveConfig;
1123
- return resolveToolApprovalPolicy(invocation, resolverConfig, {
1124
- harness: CLAUDE_BUILT_IN_TOOL_POLICIES,
1125
- factory: this.bypassApprovalToolsFactoryTier,
1126
- }).decision;
1127
- };
1128
- return { lookupPolicy, batchApprovals };
1071
+ const lookupPolicy = (invocation) =>
1072
+ // Read the config live so a mid-turn `updateAgentConfig` (e.g. a
1073
+ // remembered rule) is honored for the rest of this turn (#638).
1074
+ resolveToolApprovalPolicy(invocation, agentState.config, {
1075
+ harness: CLAUDE_BUILT_IN_TOOL_POLICIES,
1076
+ }).decision;
1077
+ return { lookupPolicy, batchApprovals: streamOptions?.batchApprovals === true };
1129
1078
  }
1130
1079
  async stream(agentId, threadId, message, streamOptions) {
1131
1080
  const agentState = this.agents.get(agentId);
@@ -1140,7 +1089,11 @@ export class ClaudeHarness {
1140
1089
  // synchronously here so failures reject the `stream()` promise (pre-stream contract) rather
1141
1090
  // than surfacing mid-iteration.
1142
1091
  const prompt = typeof message === 'string' ? message : this.buildMultimodalPrompt(agentState, message);
1143
- const { env, nativeModelId } = await this.envBuilder.build(agentState.modelConnectivityInfo);
1092
+ const { env, nativeModelId } = await this.envBuilder.build(agentState.modelConnectivityInfo,
1093
+ // When 'adaptive', injects the capability-override env pair so the CLI drives
1094
+ // adaptive extended-thinking on our gateway-alias id instead of 400ing on the
1095
+ // legacy 'enabled' shape (see ClaudeQueryEnvBuilder).
1096
+ agentState.model.thinkingMode);
1144
1097
  // Pass the underlying claudeSessionId to the Claude SDK, NOT the
1145
1098
  // user-facing threadId. They equal each other for legacy threads
1146
1099
  // but diverge after `clearMessages()` (see issue #511 / ThreadState
@@ -1163,13 +1116,11 @@ export class ClaudeHarness {
1163
1116
  const lookup = (toolName) => lookupMcpToolMetadata(agentState.mcpServers, toolName);
1164
1117
  const adapter = new ClaudeEventAdapter(this.buses.getLogBus(), lookup);
1165
1118
  const controller = new AbortController();
1166
- // Phase 2b: resolve whether the approval gate engages this turn and, if
1167
- // so, build the per-tool policy resolver the coordinator consults.
1168
- // `resolveToolApprovalMode` still validates the deprecated
1169
- // `requireToolApproval` union (throws on garbage) and yields the
1170
- // serial/batch axis; gating itself is opt-in via `buildPolicyGate`.
1171
- const deprecatedMode = resolveToolApprovalMode(streamOptions?.requireToolApproval);
1172
- const { lookupPolicy, batchApprovals } = this.buildPolicyGate(agentState, streamOptions, deprecatedMode);
1119
+ // Resolve whether the approval gate engages this turn and, if so, build
1120
+ // the per-tool policy resolver the coordinator consults. Gating is
1121
+ // opt-in via `buildPolicyGate` — active only when the consumer set
1122
+ // `toolPolicies` or `defaultToolDecision`.
1123
+ const { lookupPolicy, batchApprovals } = this.buildPolicyGate(agentState, streamOptions);
1173
1124
  const gatingActive = lookupPolicy !== undefined;
1174
1125
  const consumerTools = config.tools ?? [];
1175
1126
  // Always allocate a coordinator per turn (issue #591). The
@@ -1240,7 +1191,12 @@ export class ClaudeHarness {
1240
1191
  // Grant the subprocess read access to the configured skills'
1241
1192
  // directories so the model can open a skill's sibling files after
1242
1193
  // `load_skill` surfaces the skill location (W-23231661 / #626).
1243
- skillDirectories(agentState.skillMap));
1194
+ skillDirectories(agentState.skillMap),
1195
+ // The model's declared thinking shape. Overrides the bundled CLI's
1196
+ // per-model default, which the Bedrock backend rejects for Opus
1197
+ // 4.7/4.8 (`"enabled"` unsupported) and for Claude 4.5 (`"adaptive"`
1198
+ // unsupported). See SDK `Model.thinkingMode`.
1199
+ agentState.model.thinkingMode);
1244
1200
  options.abortController = controller;
1245
1201
  // Mirror subprocess writes into the harness-owned store. With this
1246
1202
  // set, the host-side `getSessionMessages` / `deleteSession` calls
@@ -1410,6 +1366,13 @@ export class ClaudeHarness {
1410
1366
  * onto the same `initial` sink the consumer is iterating from `chat()`.
1411
1367
  */
1412
1368
  async submitToolResult(agentId, threadId, toolResult) {
1369
+ // Unknown agent → AGENT_NOT_FOUND first, so an unregistered agent is
1370
+ // distinguishable from a registered agent with no in-flight run (#464
1371
+ // cross-harness contract; matches Mastra's two-tier distinction and the
1372
+ // shared conformance table in `@salesforce/harness-conformance`).
1373
+ if (!this.agents.has(agentId)) {
1374
+ throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
1375
+ }
1413
1376
  const coordinator = this.coordinators.get(this.coordinatorKey(agentId, threadId));
1414
1377
  if (!coordinator) {
1415
1378
  throw new Error(`ClaudeHarness.submitToolResult(): no active consumer-tool stream for thread "${threadId}" on agent "${agentId}"`);
@@ -1420,6 +1383,10 @@ export class ClaudeHarness {
1420
1383
  coordinator.submitToolResult(toolResult.toolCallId, toolResult.result, toolResult.isError);
1421
1384
  }
1422
1385
  async approveToolCall(agentId, threadId, toolCallId) {
1386
+ // Unknown agent → AGENT_NOT_FOUND first (see submitToolResult above).
1387
+ if (!this.agents.has(agentId)) {
1388
+ throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
1389
+ }
1423
1390
  const coordinator = this.coordinators.get(this.coordinatorKey(agentId, threadId));
1424
1391
  if (!coordinator) {
1425
1392
  throw new Error(`ClaudeHarness.approveToolCall(): no active approval session for thread "${threadId}" on agent "${agentId}"`);
@@ -1427,6 +1394,10 @@ export class ClaudeHarness {
1427
1394
  coordinator.approve(toolCallId);
1428
1395
  }
1429
1396
  async declineToolCall(agentId, threadId, toolCallId) {
1397
+ // Unknown agent → AGENT_NOT_FOUND first (see submitToolResult above).
1398
+ if (!this.agents.has(agentId)) {
1399
+ throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
1400
+ }
1430
1401
  const coordinator = this.coordinators.get(this.coordinatorKey(agentId, threadId));
1431
1402
  if (!coordinator) {
1432
1403
  throw new Error(`ClaudeHarness.declineToolCall(): no active approval session for thread "${threadId}" on agent "${agentId}"`);