@code-yeongyu/senpi 2026.10.26-1 → 2026.10.27-1

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.
Files changed (179) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/bundle/chunks/{anthropic-messages-M235FJQV.js → anthropic-messages-ODSFJCXR.js} +1 -1
  3. package/dist/bundle/chunks/{args-FBNJOJSR.js → args-SWKJAYQD.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-YQRTQ7H6.js → azure-openai-responses-REGXS4XO.js} +1 -1
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  6. package/dist/bundle/chunks/{chunk-DMFAVE5N.js → chunk-4QVORFQ2.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-ZXHLH4MW.js → chunk-5W3DTM2J.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-QYFOHFPS.js → chunk-6CJC6JY4.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-QREKAXKX.js → chunk-6GAMRAVM.js} +5 -5
  10. package/dist/bundle/chunks/{chunk-MSHRIZGT.js → chunk-7IZH2KQX.js} +3 -3
  11. package/dist/bundle/chunks/{chunk-PH3JDRB2.js → chunk-ARSENVDY.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-5KZH6EYQ.js → chunk-CNDPCYNT.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-DYWIFF2Q.js → chunk-CYOHFREV.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-3LPWILUZ.js → chunk-DNDZPKIQ.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-ZLIVZQDY.js → chunk-EQNUL2WB.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-LO5MYLGF.js → chunk-FQDARFHT.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-7KOUQSBX.js → chunk-GPPOPK76.js} +3 -3
  18. package/dist/bundle/chunks/{chunk-I7QWTWVZ.js → chunk-IHKHCWWG.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-MY4ZD3EW.js → chunk-IY4G44MN.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-OQ3WDMB3.js → chunk-JH7MVRNJ.js} +1 -1
  21. package/dist/bundle/chunks/chunk-KBSFTXAT.js +2 -0
  22. package/dist/bundle/chunks/{chunk-UW6M3EXY.js → chunk-KRO63T4Q.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-J5FCH7TI.js → chunk-KTDIIF2F.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-3WP5RFQR.js → chunk-NDNIMC5W.js} +1 -1
  25. package/dist/bundle/chunks/chunk-NXQFBAUI.js +2 -0
  26. package/dist/bundle/chunks/{chunk-CDQPA4FP.js → chunk-ODRGPC5S.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-5FQMNHXV.js → chunk-P5E4EVQF.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-3A3XCLNH.js → chunk-PDG4XWO2.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-5LWUUXMH.js → chunk-PY4ZQHTN.js} +1 -1
  30. package/dist/bundle/chunks/chunk-Q2A2V6NT.js +5 -0
  31. package/dist/bundle/chunks/{chunk-SOXQ42VS.js → chunk-RQ5JTXUC.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-JPGT64RV.js → chunk-TV3UI63P.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-JFQUWEN7.js → chunk-TWHF52G6.js} +4 -4
  34. package/dist/bundle/chunks/{chunk-6U3TBFBA.js → chunk-UJPG4ROT.js} +1 -1
  35. package/dist/bundle/chunks/{chunk-UDM7VO6O.js → chunk-XM36IFP3.js} +1 -1
  36. package/dist/bundle/chunks/{chunk-XQ45JMGN.js → chunk-Z6BREXV3.js} +1 -1
  37. package/dist/bundle/chunks/chunk-ZAS4NBVK.js +410 -0
  38. package/dist/bundle/chunks/{chunk-ADQAPDPV.js → chunk-ZP6JJDYV.js} +1 -1
  39. package/dist/bundle/chunks/{chunk-EZP6AYW2.js → chunk-ZXIAVLJV.js} +88 -88
  40. package/dist/bundle/chunks/{cli-main-55EQA5PS.js → cli-main-36FRZ7ZB.js} +1 -1
  41. package/dist/bundle/chunks/client-CBDPMWZJ.js +2 -0
  42. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-ZJQGIIRC.js → cloudflare-workers-ai-system-one-WDHT7BBV.js} +1 -1
  43. package/dist/bundle/chunks/cursor-agent.js +1 -1
  44. package/dist/bundle/chunks/devin-agent.js +1 -1
  45. package/dist/bundle/chunks/{dispatch-C2IBP5VV.js → dispatch-W5PENPZM.js} +3 -3
  46. package/dist/bundle/chunks/{google-generative-ai-5G4RIQA6.js → google-generative-ai-FHYFXKLA.js} +1 -1
  47. package/dist/bundle/chunks/{google-vertex-ALD4P27O.js → google-vertex-5DWE5U5K.js} +1 -1
  48. package/dist/bundle/chunks/{help-fast-path-PEIKGCKZ.js → help-fast-path-FETLIF3W.js} +1 -1
  49. package/dist/bundle/chunks/{host-command-657VCQ34.js → host-command-XEB7SHT3.js} +1 -1
  50. package/dist/bundle/chunks/{host-session-command-OV5XDOJK.js → host-session-command-FFN52LXJ.js} +1 -1
  51. package/dist/bundle/chunks/{interactive-mode-MLXSYK2H.js → interactive-mode-5S7NNV2Z.js} +1 -1
  52. package/dist/bundle/chunks/{mistral-conversations-RAE4K2XY.js → mistral-conversations-I2PI7DDF.js} +1 -1
  53. package/dist/bundle/chunks/{multi-session-host-774MACJU.js → multi-session-host-E3OPE6HJ.js} +3 -3
  54. package/dist/bundle/chunks/{openai-codex-responses-3CW4R7TG.js → openai-codex-responses-WSEX6SH7.js} +1 -1
  55. package/dist/bundle/chunks/{openai-completions-5UIJXQUF.js → openai-completions-YCBHTBSS.js} +1 -1
  56. package/dist/bundle/chunks/{openai-responses-2BO44MRW.js → openai-responses-GXNSQF4A.js} +1 -1
  57. package/dist/bundle/chunks/{package-manager-cli-MZHVCX2S.js → package-manager-cli-3XO456VE.js} +1 -1
  58. package/dist/bundle/chunks/{pi-messages-RF6GER4R.js → pi-messages-O6GISIRW.js} +1 -1
  59. package/dist/bundle/chunks/protocol-GLNWYI5G.js +2 -0
  60. package/dist/bundle/chunks/{role-ETSFSDTS.js → role-SHIZQKK4.js} +1 -1
  61. package/dist/bundle/chunks/{rotation-stream-ZMQB4GPO.js → rotation-stream-2PCZWNKF.js} +1 -1
  62. package/dist/bundle/chunks/{rpc-mode-UJXOIZCW.js → rpc-mode-GLWKLTMA.js} +1 -1
  63. package/dist/bundle/chunks/{schedule-command-KA7LFFNK.js → schedule-command-B7RRSANU.js} +1 -1
  64. package/dist/bundle/chunks/{session-control-endpoint-MFWXUR7Z.js → session-control-endpoint-KLE2X4RR.js} +1 -1
  65. package/dist/bundle/chunks/{session-picker-BP33NRSL.js → session-picker-DTGBOXDR.js} +1 -1
  66. package/dist/bundle/chunks/session-worker.js +131 -131
  67. package/dist/bundle/chunks/{typesafe-system-one-GNNZ5O5U.js → typesafe-system-one-XGYJJVSE.js} +1 -1
  68. package/dist/bundle/cli.js +2 -2
  69. package/dist/bundle/index.js +1 -1
  70. package/dist/bundle/rpc-entry.js +1 -1
  71. package/dist/bundle/runtime-manifest.json +1 -1
  72. package/dist/core/agent-session.d.ts +11 -1
  73. package/dist/core/agent-session.js +19 -2
  74. package/dist/core/extensions/builtin/anthropic-subscription/account-context.d.ts +15 -0
  75. package/dist/core/extensions/builtin/anthropic-subscription/account-context.js +53 -0
  76. package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.d.ts +3 -0
  77. package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.js +26 -9
  78. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane-boundary.d.ts +16 -0
  79. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane-boundary.js +27 -0
  80. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.d.ts +2 -13
  81. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +3 -26
  82. package/dist/core/extensions/builtin/anthropic-subscription/errors.js +3 -2
  83. package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +6 -1
  84. package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +1 -1
  85. package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +13 -2
  86. package/dist/core/extensions/builtin/compaction/index.js +7 -1
  87. package/dist/core/extensions/builtin/herdr/index.js +9 -5
  88. package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.d.ts +6 -0
  89. package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.js +25 -0
  90. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +3 -3
  91. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +50 -19
  92. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +4 -3
  93. package/dist/core/extensions/builtin/mcp/auth/oauth.js +19 -12
  94. package/dist/core/extensions/builtin/mcp/modern/consent-queue.js +1 -0
  95. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +1 -0
  96. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +4 -1
  97. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +33 -35
  98. package/dist/core/extensions/builtin/mcp/modern/request.js +6 -2
  99. package/dist/core/extensions/builtin/mcp/modern/transport.d.ts +2 -0
  100. package/dist/core/extensions/builtin/mcp/modern/transport.js +4 -2
  101. package/dist/core/extensions/builtin/mcp/sdk.lazy.d.ts +2 -0
  102. package/dist/core/extensions/builtin/mcp/sdk.lazy.js +2 -0
  103. package/dist/core/extensions/builtin/mcp/transport-sdk.js +12 -1
  104. package/dist/core/extensions/builtin/terminal/extension-state.js +2 -1
  105. package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +9 -0
  106. package/dist/core/extensions/builtin/vertex-model-garden/auth.js +17 -1
  107. package/dist/core/extensions/builtin/websearch/websearch/providers/google.d.ts +6 -0
  108. package/dist/core/extensions/builtin/websearch/websearch/providers/google.js +6 -1
  109. package/dist/core/extensions/types.d.ts +2 -0
  110. package/dist/core/lifecycle-children.d.ts +3 -1
  111. package/dist/core/lifecycle-children.js +79 -15
  112. package/dist/core/lifecycle-record.js +1 -24
  113. package/dist/core/lifecycle-session-binding.js +2 -1
  114. package/dist/core/lifecycle-wait-items.d.ts +17 -0
  115. package/dist/core/lifecycle-wait-items.js +72 -0
  116. package/dist/core/model-runtime.js +6 -1
  117. package/dist/core/provider-api-key-auth.d.ts +6 -1
  118. package/dist/core/provider-api-key-auth.js +11 -4
  119. package/dist/core/provider-composer.d.ts +8 -1
  120. package/dist/core/provider-composer.js +7 -3
  121. package/dist/core/sdk.js +4 -1
  122. package/dist/core/session-activity.js +2 -0
  123. package/dist/core/session-lifecycle-types.d.ts +20 -4
  124. package/dist/core/session-lifecycle.js +35 -4
  125. package/dist/core/structured-session-lane.d.ts +33 -0
  126. package/dist/core/structured-session-lane.js +62 -0
  127. package/dist/core/work-custody-record.d.ts +2 -1
  128. package/dist/core/work-custody-record.js +2 -0
  129. package/dist/core/work-obligations.d.ts +1 -1
  130. package/dist/core/work-obligations.js +8 -2
  131. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  132. package/dist/modes/interactive/interactive-mode.js +2 -1
  133. package/dist/modes/json-event.d.ts +79 -1
  134. package/dist/modes/json-event.js +79 -1
  135. package/dist/modes/rpc/compact-commit-dependencies.d.ts +13 -0
  136. package/dist/modes/rpc/compact-commit-dependencies.js +48 -0
  137. package/dist/modes/rpc/compact-content-index.d.ts +84 -0
  138. package/dist/modes/rpc/compact-content-index.js +243 -0
  139. package/dist/modes/rpc/compact-content-store.d.ts +10 -43
  140. package/dist/modes/rpc/compact-content-store.js +61 -163
  141. package/dist/modes/rpc/compact-index-pages.d.ts +12 -0
  142. package/dist/modes/rpc/compact-index-pages.js +31 -0
  143. package/dist/modes/rpc/compact-read-lease.d.ts +15 -0
  144. package/dist/modes/rpc/compact-read-lease.js +40 -0
  145. package/dist/modes/rpc/compact-session-state.d.ts +14 -63
  146. package/dist/modes/rpc/compact-session-state.js +69 -51
  147. package/dist/modes/rpc/compact-spool-dir.d.ts +4 -1
  148. package/dist/modes/rpc/compact-spool-dir.js +41 -8
  149. package/dist/modes/rpc/compact-spool-storage.d.ts +58 -0
  150. package/dist/modes/rpc/compact-spool-storage.js +195 -0
  151. package/dist/modes/rpc/compact-storage-io.d.ts +2 -1
  152. package/dist/modes/rpc/compact-storage-io.js +7 -1
  153. package/dist/modes/rpc/compact-text-roots.d.ts +22 -0
  154. package/dist/modes/rpc/compact-text-roots.js +138 -0
  155. package/dist/modes/rpc/compact-text-view.d.ts +22 -0
  156. package/dist/modes/rpc/compact-text-view.js +16 -0
  157. package/dist/modes/rpc/compact-transition.d.ts +41 -0
  158. package/dist/modes/rpc/compact-transition.js +12 -0
  159. package/dist/modes/rpc/connection-handler.d.ts +2 -0
  160. package/dist/modes/rpc/connection-handler.js +12 -6
  161. package/dist/modes/rpc/host-ensure-start.js +5 -3
  162. package/dist/modes/rpc/security-presentations.js +2 -1
  163. package/dist/modes/rpc/session-binding.js +10 -0
  164. package/dist/modes/rpc/session-event-writer.d.ts +2 -0
  165. package/dist/modes/rpc/session-event-writer.js +2 -0
  166. package/dist/modes/rpc/session-record-stream.d.ts +3 -3
  167. package/dist/modes/rpc/session-record-stream.js +3 -3
  168. package/dist/modes/rpc/session-structured-lanes.d.ts +12 -0
  169. package/dist/modes/rpc/session-structured-lanes.js +24 -0
  170. package/dist/modes/rpc/session-worker-client.js +6 -5
  171. package/dist/modes/rpc/structured-session-records.d.ts +12 -0
  172. package/dist/modes/rpc/structured-session-records.js +88 -0
  173. package/docs/extensions.md +7 -1
  174. package/docs/rpc.md +17 -2
  175. package/package.json +6 -6
  176. package/dist/bundle/chunks/chunk-GQJYNFMO.js +0 -2
  177. package/dist/bundle/chunks/chunk-VICDRG3J.js +0 -410
  178. package/dist/bundle/chunks/chunk-VL7O5PO4.js +0 -5
  179. package/dist/bundle/chunks/client-LOS6GSEW.js +0 -2
@@ -1,7 +1,4 @@
1
1
  import { createHash } from "node:crypto";
2
- import { loadAnthropicOAuth } from "@earendil-works/pi-ai/oauth";
3
- import { getAgentDir } from "../../../../config.js";
4
- import { AuthStorage } from "../../../auth-storage.js";
5
2
  import { CredentialStoreBusyError } from "../../../lockfile-policy.js";
6
3
  import { emitProviderAccountFailover, emitProviderAccountsChanged } from "./account-events.js";
7
4
  import { ANTHROPIC_SUBSCRIPTION_PROVIDER_ID } from "./account-management.js";
@@ -9,6 +6,7 @@ import { emptyCredential, envSlotToken, listAccounts, refreshSlot, } from "./acc
9
6
  import { isBlockedFor, selectAccount } from "./affinity.js";
10
7
  import { createAttemptMessages } from "./auth-attempt.js";
11
8
  import { recoverAuthBlockedSlots } from "./auth-block-recovery.js";
9
+ import { activeBoundary } from "./auth-lane-boundary.js";
12
10
  import { hasRequestOauthToken, mergeRequestAuthEnvironment, stripManagedAuthEnvironment } from "./auth-environment.js";
13
11
  import { writeConfigDirCredential } from "./config-dir-credentials.js";
14
12
  import { asksLegacyLongContext } from "./context-window.js";
@@ -17,33 +15,11 @@ import { markLongContextRefused } from "./long-context.js";
17
15
  import { classifyForRequest, retryAtStandardWindowOnce, sdkFailure, visibleSdkMessage } from "./long-context-retry.js";
18
16
  import { isGrantRejected, refreshFailure } from "./refresh-failure.js";
19
17
  export { ANTHROPIC_SUBSCRIPTION_PROVIDER_ID } from "./account-management.js";
18
+ export { authLaneRefresher, overrideAuthLaneBoundary, resetAuthLaneBoundary } from "./auth-lane-boundary.js";
20
19
  export const EXPIRING_WITHIN_MS = 5 * 60_000;
21
20
  /** A managed lane with an empty pool must refuse rather than spawn the SDK against ambient host credentials. */
22
21
  const NO_MANAGED_ACCOUNTS_ERROR = "authentication_failed: No Anthropic Subscription accounts configured for the managed lane; " +
23
22
  "run /login anthropic-subscription or set CLAUDE_CODE_OAUTH_TOKEN";
24
- async function refreshWithAnthropicOAuth(refresh, signal) {
25
- const oauth = await loadAnthropicOAuth();
26
- const credential = await oauth.refresh({ type: "oauth", access: "", refresh, expires: 0 }, signal);
27
- return { access: credential.access, refresh: credential.refresh, expires: credential.expires };
28
- }
29
- const defaultBoundary = {
30
- createStore: () => AuthStorage.create(),
31
- env: () => process.env,
32
- getAgentDir,
33
- now: () => Date.now(),
34
- refresher: refreshWithAnthropicOAuth,
35
- };
36
- let activeBoundary = defaultBoundary;
37
- export function overrideAuthLaneBoundary(override) {
38
- activeBoundary = { ...activeBoundary, ...override };
39
- }
40
- export function resetAuthLaneBoundary() {
41
- activeBoundary = defaultBoundary;
42
- }
43
- /** The refresher turns use, so the usage poller renews an expiring account the same way (senpi#3684). */
44
- export function authLaneRefresher() {
45
- return activeBoundary.refresher;
46
- }
47
23
  export function resolveEffectiveLane(settings, accounts) {
48
24
  return settings.tokenInjection ?? (accounts.length > 0 ? "oauth-slots" : "ambient");
49
25
  }
@@ -193,6 +169,7 @@ export async function* queryWithAuthLane(input) {
193
169
  accountName: slot.name,
194
170
  accounts,
195
171
  authLane: pool.lane,
172
+ pinSource: input.pinnedAccount !== undefined ? "cli" : pool.pinSource,
196
173
  options,
197
174
  credentialDigest: prepared.credentialDigest,
198
175
  });
@@ -1,4 +1,5 @@
1
1
  import { rateLimitModelFamily } from "../../../credential-pool/model-scope.js";
2
+ import { inheritAccountContext } from "./account-context.js";
2
3
  const SDK_ERROR_CLASSIFICATIONS = {
3
4
  authentication_failed: { kind: "auth_error", retryable: true },
4
5
  oauth_org_not_allowed: { kind: "org_not_allowed", retryable: true },
@@ -60,7 +61,7 @@ export function sdkResultFailure(message) {
60
61
  const status = "api_error_status" in message && message.api_error_status != null ? `HTTP ${message.api_error_status}` : "";
61
62
  const reason = "terminal_reason" in message && typeof message.terminal_reason === "string" ? message.terminal_reason : "";
62
63
  const suffix = [status, reason].filter(Boolean).join(", ");
63
- return new SdkResultFailure(suffix ? `${detail} (${suffix})` : detail, message.usage);
64
+ return inheritAccountContext(message, new SdkResultFailure(suffix ? `${detail} (${suffix})` : detail, message.usage));
64
65
  }
65
66
  export function sdkAssistantFailure(message) {
66
67
  if (!message.error)
@@ -72,7 +73,7 @@ export function sdkAssistantFailure(message) {
72
73
  .filter((block) => block.type === "text" && "text" in block && typeof block.text === "string")
73
74
  .map((block) => ("text" in block && typeof block.text === "string" ? block.text : ""))
74
75
  .join(" ")).trim();
75
- return new Error(text ? (message.error === "unknown" ? text : `${text} (${message.error})`) : message.error);
76
+ return inheritAccountContext(message, new Error(text ? (message.error === "unknown" ? text : `${text} (${message.error})`) : message.error));
76
77
  }
77
78
  /** Classifies Anthropic Subscription error codes and HTTP-shaped fallback text in one place. */
78
79
  export function classifySdkError(error) {
@@ -1,8 +1,13 @@
1
- import type { UnavailableAccount } from "./affinity.ts";
1
+ import type { PinSource, UnavailableAccount } from "./affinity.ts";
2
2
  import type { SdkErrorKind } from "./errors.ts";
3
3
  import type { ClaudeCodeRun } from "./executable.ts";
4
4
  export declare function noAccountGuidance(hasAnthropicCredential: boolean): string;
5
5
  export declare function allAccountsBlockedGuidance(soonestUnblockAt: number | undefined, blockReason?: "auth_error"): string;
6
+ /**
7
+ * How to move off a pinned account. `/claude-account pin` only writes the stored pin, which a CLI or a
8
+ * settings pin outranks (`resolveClaudeAccountPin`), so for those it would change nothing (senpi#3672).
9
+ */
10
+ export declare function switchAccountLine(pinnedBy: PinSource | undefined): string;
6
11
  /**
7
12
  * With account fallback off, only one account may serve (senpi#3465), so the pool-wide advice is wrong:
8
13
  * other accounts may be healthy, and adding one does not help because it is never used. Name the account
@@ -43,7 +43,7 @@ const ENABLE_ACCOUNT_FALLBACK = " Set anthropicSubscriptionProvider.accountFall
43
43
  * How to move off a pinned account. `/claude-account pin` only writes the stored pin, which a CLI or a
44
44
  * settings pin outranks (`resolveClaudeAccountPin`), so for those it would change nothing (senpi#3672).
45
45
  */
46
- function switchAccountLine(pinnedBy) {
46
+ export function switchAccountLine(pinnedBy) {
47
47
  switch (pinnedBy) {
48
48
  case "cli":
49
49
  return " This session was started with --claude-account; start a new session with another --claude-account to switch.";
@@ -1,12 +1,23 @@
1
1
  import { AllAccountsBlockedError } from "./affinity.js";
2
+ import { errorAccountContext } from "./account-context.js";
2
3
  import { classifySdkError } from "./errors.js";
3
- import { allAccountsBlockedGuidance, claudeCodeVersionFloorGuidance, sdkErrorGuidance, singleAccountUnavailableGuidance, } from "./guidance.js";
4
+ import { ClassifiedSdkError } from "./failover.js";
5
+ import { allAccountsBlockedGuidance, claudeCodeVersionFloorGuidance, sdkErrorGuidance, singleAccountUnavailableGuidance, switchAccountLine, } from "./guidance.js";
4
6
  export function withAuthGuidance(error, message, ran) {
5
7
  if (error instanceof AllAccountsBlockedError)
6
8
  return error.unavailableAccount === undefined
7
9
  ? allAccountsBlockedGuidance(error.soonestUnblockAt, error.blockReason)
8
10
  : singleAccountUnavailableGuidance(error.unavailableAccount, error.soonestUnblockAt, error.blockReason);
9
- const guidance = sdkErrorGuidance(classifySdkError(error).kind);
11
+ const kind = classifySdkError(error).kind;
12
+ const account = errorAccountContext(error instanceof ClassifiedSdkError ? error.original : error);
13
+ let guidance = sdkErrorGuidance(kind);
14
+ if (account && (kind === "billing" || /\bout of extra usage\b/i.test(message))) {
15
+ const selected = `Claude account '${account.accountLabel}' (source: ${account.accountSource}).`;
16
+ const action = account.accountSource === "ambient"
17
+ ? "Switch the Claude Code login, or top up at claude.ai/settings/usage."
18
+ : `${switchAccountLine(account.pinSource).trim()} Or top up this account at claude.ai/settings/usage.`;
19
+ guidance = `${selected} ${action}`;
20
+ }
10
21
  const versionGuidance = claudeCodeVersionFloorGuidance(message, ran);
11
22
  const hints = [guidance, versionGuidance].filter((hint) => hint !== undefined);
12
23
  return hints.length > 0 ? `${message}\n${hints.join("\n")}` : message;
@@ -648,8 +648,14 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
648
648
  catch (error) {
649
649
  const message = error instanceof Error ? error.message : String(error);
650
650
  const failureKind = classifyRequiredCompactionFallbackFailure(error);
651
+ // A terminal provider failure (dead model, dead credential) has no cheaper
652
+ // recovery on any compaction kind: without this, a soft-threshold pre_prompt
653
+ // compaction cancelled instead, the session rejected it, and the pending
654
+ // prompt failed with RequiredCompactionError while shouldCompact still said
655
+ // compact (the "Compaction rejected: compaction generator failed" wedge).
651
656
  if ((requiresDeterministicCompactionFallback(event, ctx.getContextUsage()) ||
652
- failureKind === "unsafe-retained-content") &&
657
+ failureKind === "unsafe-retained-content" ||
658
+ failureKind === "summarization-provider-failure") &&
653
659
  failureKind !== undefined &&
654
660
  !event.signal.aborted) {
655
661
  const recovery = recoverRequiredCompaction(ctx, snapshot, failureKind, error);
@@ -2,6 +2,7 @@ import { closeSync, openSync, readdirSync, readFileSync, readSync } from "node:f
2
2
  import { createConnection } from "node:net";
3
3
  import { join, win32 } from "node:path";
4
4
  import { debuglog } from "node:util";
5
+ import { APP_COMMAND } from "../../../../config.js";
5
6
  import { isTerminalMonitorStateEvent, isWakeSourceStateEvent, TERMINAL_MONITOR_STATE_EVENT, WAKE_SOURCE_STATE_EVENT, } from "../monitor-state-event.js";
6
7
  import { HerdrClient } from "./herdr-client.js";
7
8
  import { initialHerdrState, isHerdrBlockedEvent, reduceHerdrState, selectHerdrReport } from "./herdr-state.js";
@@ -115,11 +116,14 @@ export function createHerdrExtension(deps) {
115
116
  void publish();
116
117
  }, 4000);
117
118
  poll.unref();
118
- await Promise.all([
119
- reportTitle(ctx),
120
- send("pane.report_agent_session", { agent: "pi", ...sessionRef(), session_start_source: _event.reason }),
121
- publish(),
122
- ]);
119
+ // Herdr accepts a resume command only after this reporter holds the pane.
120
+ await Promise.all([reportTitle(ctx), publish()]);
121
+ await send("pane.report_agent_session", {
122
+ agent: "pi",
123
+ ...sessionRef(),
124
+ resume_argv: [APP_COMMAND, "--session", bound.getSessionFile() ?? bound.getSessionId()],
125
+ session_start_source: _event.reason,
126
+ });
123
127
  });
124
128
  pi.on("session_info_changed", (_event, ctx) => {
125
129
  if (owns(ctx))
@@ -0,0 +1,6 @@
1
+ import type { OAuthServerInfo } from "@modelcontextprotocol/sdk/client/auth.js";
2
+ import type { McpStoredAuth } from "./token-store.ts";
3
+ export declare function canonicalAuthorizationServerIssuer(issuer: string): string;
4
+ export declare function validatedAuthorizationServerIssuer(info: OAuthServerInfo, serverName: string): string;
5
+ export declare function credentialsMatchIssuer(record: McpStoredAuth | undefined, issuer: string): boolean;
6
+ //# sourceMappingURL=oauth-issuer.d.ts.map
@@ -0,0 +1,25 @@
1
+ import { OAuthFlowError } from "./oauth-errors.js";
2
+ // Credential stamps tolerate only the slash spelling of an origin-only URL, not URL normalization.
3
+ export function canonicalAuthorizationServerIssuer(issuer) {
4
+ return issuer.replace(/^(https?:\/\/[^/?#]+)\/$/, "$1");
5
+ }
6
+ // RFC 8414 section 3.3: identifiers from metadata are compared verbatim, not as normalized URLs.
7
+ export function validatedAuthorizationServerIssuer(info, serverName) {
8
+ // The SDK's legacy fallback discovers root metadata and spells the origin with a trailing slash.
9
+ // Only that configured-resource fallback is normalized; PRM-provided identifiers remain exact.
10
+ const origin = new URL(info.authorizationServerUrl).origin;
11
+ const issuer = !info.resourceMetadata?.authorization_servers?.length && info.authorizationServerUrl === `${origin}/`
12
+ ? canonicalAuthorizationServerIssuer(info.authorizationServerUrl)
13
+ : info.authorizationServerUrl;
14
+ if (info.authorizationServerMetadata !== undefined && info.authorizationServerMetadata.issuer !== issuer) {
15
+ throw new OAuthFlowError("needs_auth", `MCP server ${serverName} authorization server metadata issuer mismatch; restart authorization after correcting the server metadata.`, { serverName });
16
+ }
17
+ return issuer;
18
+ }
19
+ export function credentialsMatchIssuer(record, issuer) {
20
+ const grantIssuer = record?.issuer ?? record?.discoveryState?.authorizationServerUrl;
21
+ const clientIssuer = record?.clientInfo?.issuer ?? grantIssuer;
22
+ return [grantIssuer, clientIssuer].every((stamp) => stamp === undefined ||
23
+ canonicalAuthorizationServerIssuer(stamp) === canonicalAuthorizationServerIssuer(issuer));
24
+ }
25
+ //# sourceMappingURL=oauth-issuer.js.map
@@ -15,8 +15,6 @@ export interface McpOAuthProviderOptions {
15
15
  onRedirect?: (url: URL) => void | Promise<void>;
16
16
  }
17
17
  export declare function tokenExpiresAt(tokens: OAuthTokens, now?: number): number | undefined;
18
- /** Compares authorization server identifiers the way the SDK does: parsed, tolerating one trailing `/`. */
19
- export declare function sameAuthorizationServer(a: string, b: string): boolean;
20
18
  /**
21
19
  * The authorization server a stored grant belongs to (senpi#2940, GHSA-6qxp-vccf-f47h): its `issuer` stamp, or
22
20
  * for a record saved before the stamp existed, the authorization server its sign-in discovered. A record with
@@ -37,7 +35,9 @@ export declare class McpOAuthProvider implements OAuthClientProvider {
37
35
  get lastAuthorizationUrl(): URL | undefined;
38
36
  get redirectUrl(): string | URL | undefined;
39
37
  get clientMetadataUrl(): string | undefined;
40
- get clientMetadata(): OAuthClientMetadata;
38
+ get clientMetadata(): OAuthClientMetadata & {
39
+ application_type?: "native" | "web";
40
+ };
41
41
  state(): string;
42
42
  consumeState(candidate: string | undefined): boolean;
43
43
  clientInformation(): OAuthClientInformationMixed | undefined;
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { fingerprintSecret } from "../log.js";
3
3
  import { OAuthFlowError } from "./oauth-errors.js";
4
+ import { canonicalAuthorizationServerIssuer, credentialsMatchIssuer, validatedAuthorizationServerIssuer, } from "./oauth-issuer.js";
4
5
  // Buffer before absolute token expiry at which a refresh is considered due.
5
6
  export const REFRESH_LEEWAY_MS = 5 * 60 * 1000;
6
7
  export function tokenExpiresAt(tokens, now = Date.now()) {
@@ -8,20 +9,6 @@ export function tokenExpiresAt(tokens, now = Date.now()) {
8
9
  return undefined;
9
10
  return now + tokens.expires_in * 1000;
10
11
  }
11
- /** Compares authorization server identifiers the way the SDK does: parsed, tolerating one trailing `/`. */
12
- export function sameAuthorizationServer(a, b) {
13
- const normalize = (value) => {
14
- try {
15
- return new URL(value).href.replace(/\/$/, "");
16
- }
17
- catch (error) {
18
- if (error instanceof TypeError)
19
- return value.replace(/\/$/, "");
20
- throw error;
21
- }
22
- };
23
- return normalize(a) === normalize(b);
24
- }
25
12
  /**
26
13
  * The authorization server a stored grant belongs to (senpi#2940, GHSA-6qxp-vccf-f47h): its `issuer` stamp, or
27
14
  * for a record saved before the stamp existed, the authorization server its sign-in discovered. A record with
@@ -101,6 +88,14 @@ export class McpOAuthProvider {
101
88
  get clientMetadata() {
102
89
  const redirect = this.#options.redirectUrl;
103
90
  return {
91
+ ...(redirect === undefined
92
+ ? {}
93
+ : {
94
+ application_type: !["http:", "https:"].includes(new URL(redirect).protocol) ||
95
+ ["127.0.0.1", "localhost", "[::1]"].includes(new URL(redirect).hostname)
96
+ ? "native"
97
+ : "web",
98
+ }),
104
99
  redirect_uris: redirect === undefined ? [] : [String(redirect)],
105
100
  grant_types: ["authorization_code", "refresh_token"],
106
101
  response_types: ["code"],
@@ -127,20 +122,56 @@ export class McpOAuthProvider {
127
122
  return this.#store.read()?.clientInfo;
128
123
  }
129
124
  async saveClientInformation(info) {
130
- // The SDK stamps `info.issuer`; it is persisted with the rest so the binding survives a restart.
131
- await this.#store.update((current) => ({ ...(current ?? {}), clientInfo: info }));
125
+ const discovery = this.discoveryState();
126
+ const issuer = discovery === undefined
127
+ ? info.issuer && canonicalAuthorizationServerIssuer(info.issuer)
128
+ : validatedAuthorizationServerIssuer(discovery, this.serverName);
129
+ await this.#store.update((current) => ({ ...(current ?? {}), clientInfo: { ...info, issuer } }));
132
130
  }
133
131
  async saveDiscoveryState(state) {
134
- await this.#store.update((current) => ({ ...freezeGrantAttribution(current), discoveryState: state }));
132
+ const issuer = validatedAuthorizationServerIssuer(state, this.serverName);
133
+ const discoveryState = { ...state, authorizationServerUrl: issuer };
134
+ await this.#store.update((current) => {
135
+ const attributed = freezeGrantAttribution(current);
136
+ if (!credentialsMatchIssuer(attributed, issuer))
137
+ return { discoveryState };
138
+ const clientIssuer = attributed.clientInfo?.issuer ?? storedGrantIssuer(attributed);
139
+ return {
140
+ ...attributed,
141
+ // Unattributed legacy registrations cannot be sent to the server discovered now.
142
+ clientInfo: clientIssuer === undefined || attributed.clientInfo === undefined
143
+ ? undefined
144
+ : { ...attributed.clientInfo, issuer },
145
+ discoveryState,
146
+ };
147
+ });
135
148
  }
136
149
  discoveryState() {
137
- return this.#store.read()?.discoveryState;
150
+ const current = this.#store.read();
151
+ const state = current?.discoveryState;
152
+ if (state !== undefined) {
153
+ try {
154
+ const issuer = validatedAuthorizationServerIssuer(state, this.serverName);
155
+ if (!credentialsMatchIssuer(current, issuer))
156
+ return undefined;
157
+ }
158
+ catch (error) {
159
+ if (error instanceof OAuthFlowError)
160
+ return undefined;
161
+ throw error;
162
+ }
163
+ }
164
+ return state;
138
165
  }
139
166
  tokens() {
140
167
  return storedAuthToTokens(this.#store.read());
141
168
  }
142
169
  async saveTokens(tokens) {
143
- await this.#store.update((current) => mergeTokensIntoStoredAuth(current, tokens, this.serverUrl));
170
+ const discovery = this.discoveryState();
171
+ const issuer = discovery === undefined
172
+ ? tokens.issuer && canonicalAuthorizationServerIssuer(tokens.issuer)
173
+ : validatedAuthorizationServerIssuer(discovery, this.serverName);
174
+ await this.#store.update((current) => mergeTokensIntoStoredAuth(current, { ...tokens, issuer }, this.serverUrl));
144
175
  this.#logFingerprint("saveTokens", tokens.access_token);
145
176
  }
146
177
  async saveCodeVerifier(codeVerifier) {
@@ -2,7 +2,8 @@ import { loadMcpSdkAuth } from "../sdk.lazy.js";
2
2
  import { safeDelay } from "../wrap.js";
3
3
  import { isInvalidGrant, isTransientTokenError, OAuthFlowError } from "./oauth-errors.js";
4
4
  import { oauthFetch } from "./oauth-fetch.js";
5
- import { mergeTokensIntoStoredAuth, REFRESH_LEEWAY_MS, sameAuthorizationServer, storedAuthToTokens, storedGrantIssuer, } from "./oauth-provider.js";
5
+ import { credentialsMatchIssuer, validatedAuthorizationServerIssuer } from "./oauth-issuer.js";
6
+ import { mergeTokensIntoStoredAuth, REFRESH_LEEWAY_MS, storedAuthToTokens, storedGrantIssuer, } from "./oauth-provider.js";
6
7
  export function isTokenStale(record, now) {
7
8
  const token = record?.accessToken;
8
9
  if (token === undefined || token.length === 0)
@@ -67,8 +68,8 @@ export class McpRefreshManager {
67
68
  // elsewhere, or one that cannot be attributed at all, is a continuity break: drop it and require a fresh
68
69
  // sign-in rather than refreshing silently against whatever server discovery now names.
69
70
  const issuer = storedGrantIssuer(current);
70
- const authorizationServer = String(info.authorizationServerUrl);
71
- if (issuer === undefined || !sameAuthorizationServer(issuer, authorizationServer)) {
71
+ const authorizationServer = validatedAuthorizationServerIssuer(info, this.#provider.serverName);
72
+ if (issuer === undefined || !credentialsMatchIssuer(current, authorizationServer)) {
72
73
  this.#provider.store.writeUnlocked(undefined);
73
74
  throw new OAuthFlowError("needs_auth", issuer === undefined
74
75
  ? `MCP server ${this.#provider.serverName} has a saved sign-in with no record of the authorization server that issued it; credentials cleared, re-authentication required.`
@@ -1,6 +1,7 @@
1
1
  import { loadMcpSdkAuth } from "../sdk.lazy.js";
2
2
  import { isInvalidGrant, OAuthFlowError } from "./oauth-errors.js";
3
3
  import { oauthFetch } from "./oauth-fetch.js";
4
+ import { validatedAuthorizationServerIssuer } from "./oauth-issuer.js";
4
5
  import { assertS256Supported } from "./oauth-refresh.js";
5
6
  // Pre-flight: discover the authorization server and REFUSE (typed) if it does
6
7
  // not advertise PKCE S256. Runs before any authorize redirect.
@@ -20,7 +21,7 @@ export async function beginAuthorization(provider, options = {}) {
20
21
  }
21
22
  // Paste flow: validates single-use state itself, then exchanges the code.
22
23
  export async function completeAuthorization(provider, redirectInput, options = {}) {
23
- const { code, state, iss } = parseRedirect(redirectInput, provider.serverName);
24
+ const { code, state, iss } = parseRedirect(redirectInput, provider);
24
25
  if (!provider.consumeState(state)) {
25
26
  throw new OAuthFlowError("state_mismatch", `MCP server ${provider.serverName} authorization state did not match (possible CSRF or a stale/replayed link); restart with /mcp auth-start.`, { serverName: provider.serverName });
26
27
  }
@@ -56,15 +57,19 @@ export async function finishAuthorization(provider, code, options = {}, iss) {
56
57
  // RFC 9207: an authorization response that names another authorization server, or that omits `iss`
57
58
  // although this server promises to send it, may have been injected by a mix-up attack; never exchange its code.
58
59
  function assertAuthorizationResponseIssuer(provider, iss) {
59
- const metadata = provider.discoveryState()?.authorizationServerMetadata;
60
- if (metadata === undefined)
61
- return;
60
+ const discovery = provider.discoveryState();
61
+ if (discovery === undefined) {
62
+ throw new OAuthFlowError("needs_auth", `MCP server ${provider.serverName} has no trusted authorization discovery; restart with /mcp auth-start ${provider.serverName}.`, { serverName: provider.serverName });
63
+ }
64
+ const metadata = discovery.authorizationServerMetadata;
65
+ const issuer = validatedAuthorizationServerIssuer(discovery, provider.serverName);
62
66
  // The SDK keeps unknown metadata fields (loose schema) but does not type this RFC 9207 one.
63
- const promised = metadata
64
- .authorization_response_iss_parameter_supported === true;
67
+ const promised = metadata !== undefined &&
68
+ "authorization_response_iss_parameter_supported" in metadata &&
69
+ metadata.authorization_response_iss_parameter_supported === true;
65
70
  if (iss === undefined && !promised)
66
71
  return;
67
- if (iss !== undefined && iss === metadata.issuer)
72
+ if (iss !== undefined && iss === issuer)
68
73
  return;
69
74
  throw new OAuthFlowError("needs_auth", iss === undefined
70
75
  ? `MCP server ${provider.serverName} authorization response has no issuer although its authorization server promises one; restart with /mcp auth-start ${provider.serverName}.`
@@ -111,19 +116,19 @@ export async function clientCredentialsGrant(provider, options = {}) {
111
116
  resource: new URL(provider.serverUrl),
112
117
  fetchFn: oauthFetch(options.fetchFn),
113
118
  });
114
- await provider.saveTokens({ ...tokens, issuer: String(info.authorizationServerUrl) });
119
+ await provider.saveTokens({ ...tokens, issuer: validatedAuthorizationServerIssuer(info, provider.serverName) });
115
120
  }
116
121
  export async function logout(provider) {
117
122
  await provider.store.clear();
118
123
  }
119
124
  async function discover(provider, options) {
120
- if (options.discover !== undefined)
121
- return options.discover(provider.serverUrl);
122
125
  const cached = provider.discoveryState();
123
126
  if (cached !== undefined)
124
127
  return cached;
125
128
  const { discoverOAuthServerInfo } = await loadMcpSdkAuth();
126
- const info = await discoverOAuthServerInfo(provider.serverUrl, { fetchFn: oauthFetch(options.fetchFn) });
129
+ const info = options.discover !== undefined
130
+ ? await options.discover(provider.serverUrl)
131
+ : await discoverOAuthServerInfo(provider.serverUrl, { fetchFn: oauthFetch(options.fetchFn) });
127
132
  await provider.saveDiscoveryState(toDiscoveryState(info));
128
133
  return info;
129
134
  }
@@ -134,7 +139,8 @@ function toDiscoveryState(info) {
134
139
  resourceMetadata: info.resourceMetadata,
135
140
  };
136
141
  }
137
- function parseRedirect(input, serverName) {
142
+ function parseRedirect(input, provider) {
143
+ const serverName = provider.serverName;
138
144
  let url;
139
145
  try {
140
146
  url = new URL(input.trim());
@@ -142,6 +148,7 @@ function parseRedirect(input, serverName) {
142
148
  catch {
143
149
  throw new OAuthFlowError("needs_auth", `MCP server ${serverName} received a malformed redirect URL; paste the full http://127.0.0.1/... address from your browser.`, { serverName });
144
150
  }
151
+ assertAuthorizationResponseIssuer(provider, url.searchParams.get("iss") ?? undefined);
145
152
  const error = url.searchParams.get("error");
146
153
  if (error !== null) {
147
154
  throw new OAuthFlowError("needs_auth", `MCP server ${serverName} authorization failed: ${error}`, { serverName });
@@ -47,6 +47,7 @@ export function queuedForm(binding, signal, run) {
47
47
  current.jobs.splice(index, 1);
48
48
  signal.removeEventListener("abort", job.abort);
49
49
  reject(new McpMrtrError("MCP_MRTR_PROCESSING_UNAVAILABLE"));
50
+ pump();
50
51
  }
51
52
  };
52
53
  current.jobs.push(job);
@@ -39,6 +39,7 @@ export async function materializeNegotiatedTransport(options, serverName, timeou
39
39
  clientInfo,
40
40
  clientCapabilities: {},
41
41
  protocolVersion: outcome.version,
42
+ logger,
42
43
  }), outcome.discovery, outcome.version);
43
44
  const version = outcome.version;
44
45
  logVersion(transport, () => version);
@@ -2,10 +2,13 @@
2
2
  export declare function encodeMcpHeaderValue(value: string): string;
3
3
  /** Configured headers must never supply parameter metadata absent from the request body. */
4
4
  export declare function clearMcpParameterHeaders(headers: Headers): void;
5
+ /** A local argument error, not a failed transport or an untrusted server diagnostic. */
6
+ export declare class ParameterHeaderValidationError extends Error {
7
+ }
5
8
  /** Connection-local catalog: never shared with another server or transport generation. */
6
9
  export declare class ModernParameterHeaders {
7
10
  #private;
8
- filterTools(tools: readonly unknown[], firstPage: boolean): unknown[];
11
+ filterTools(tools: readonly unknown[], firstPage: boolean, warn: (message: string) => void): unknown[];
9
12
  append(headers: Headers, name: unknown, args: unknown): void;
10
13
  }
11
14
  //# sourceMappingURL=parameter-headers.d.ts.map
@@ -18,22 +18,31 @@ export function clearMcpParameterHeaders(headers) {
18
18
  function parseParameterHeaders(schema) {
19
19
  const headers = [];
20
20
  const names = new Set();
21
+ let reason = "";
21
22
  const visit = (node, path) => {
23
+ if (Array.isArray(node))
24
+ return node.every((child) => visit(child, undefined));
22
25
  if (!isRecord(node))
23
26
  return true;
24
27
  if (Object.hasOwn(node, "x-mcp-header")) {
25
28
  const name = node["x-mcp-header"];
26
29
  const type = node.type;
27
- if (!path?.length ||
28
- typeof name !== "string" ||
29
- !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name) ||
30
- names.has(name.toLowerCase()) ||
31
- (type !== "string" && type !== "integer" && type !== "boolean"))
30
+ if (!path?.length)
31
+ reason = "x-mcp-header must be on a properties-only path";
32
+ else if (typeof name !== "string" || !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name))
33
+ reason = "x-mcp-header must be a nonempty HTTP field-name token";
34
+ else if (names.has(name.toLowerCase()))
35
+ reason = "x-mcp-header names must be case-insensitively unique";
36
+ else if (type !== "string" && type !== "integer" && type !== "boolean")
37
+ reason = "x-mcp-header type must be string, integer, or boolean";
38
+ else {
39
+ names.add(name.toLowerCase());
40
+ headers.push({ name, path, type });
41
+ }
42
+ if (reason)
32
43
  return false;
33
- names.add(name.toLowerCase());
34
- headers.push({ name, path, type });
35
44
  }
36
- // Only schema-valued keywords are traversed: examples/default/const are instance data.
45
+ // "Anywhere else" includes unknown keywords; instance-data keywords are not schemas.
37
46
  for (const [keyword, value] of Object.entries(node)) {
38
47
  if (["properties", "patternProperties", "$defs", "definitions", "dependentSchemas", "dependencies"].includes(keyword)) {
39
48
  if (!isRecord(value))
@@ -44,46 +53,31 @@ function parseParameterHeaders(schema) {
44
53
  return false;
45
54
  }
46
55
  }
47
- else if ([
48
- "items",
49
- "prefixItems",
50
- "allOf",
51
- "anyOf",
52
- "oneOf",
53
- "additionalItems",
54
- "additionalProperties",
55
- "unevaluatedItems",
56
- "unevaluatedProperties",
57
- "contains",
58
- "propertyNames",
59
- "not",
60
- "if",
61
- "then",
62
- "else",
63
- "contentSchema",
64
- ].includes(keyword)) {
65
- for (const child of Array.isArray(value) ? value : [value]) {
66
- if (!visit(child, undefined))
67
- return false;
68
- }
56
+ else if (!["examples", "default", "const", "enum"].includes(keyword)) {
57
+ if (!visit(value, undefined))
58
+ return false;
69
59
  }
70
60
  }
71
61
  return true;
72
62
  };
73
- return visit(schema, []) ? headers : undefined;
63
+ return visit(schema, []) ? headers : reason;
64
+ }
65
+ /** A local argument error, not a failed transport or an untrusted server diagnostic. */
66
+ export class ParameterHeaderValidationError extends Error {
74
67
  }
75
68
  /** Connection-local catalog: never shared with another server or transport generation. */
76
69
  export class ModernParameterHeaders {
77
70
  #tools = new Map();
78
- filterTools(tools, firstPage) {
71
+ filterTools(tools, firstPage, warn) {
79
72
  if (firstPage)
80
73
  this.#tools.clear();
81
74
  return tools.filter((tool) => {
82
75
  if (!isRecord(tool) || typeof tool.name !== "string")
83
76
  return true;
84
77
  const headers = parseParameterHeaders(tool.inputSchema);
85
- if (headers === undefined) {
78
+ if (typeof headers === "string") {
86
79
  this.#tools.delete(tool.name);
80
+ warn(`Rejected MCP tool ${JSON.stringify(tool.name)}: ${headers}`);
87
81
  return false;
88
82
  }
89
83
  this.#tools.set(tool.name, headers);
@@ -98,11 +92,15 @@ export class ModernParameterHeaders {
98
92
  for (const key of parameter.path) {
99
93
  value = isRecord(value) && Object.hasOwn(value, key) ? value[key] : undefined;
100
94
  }
95
+ if (value === undefined)
96
+ continue;
101
97
  const valid = (parameter.type === "string" && typeof value === "string") ||
102
98
  (parameter.type === "boolean" && typeof value === "boolean") ||
103
99
  (parameter.type === "integer" && typeof value === "number" && Number.isSafeInteger(value));
104
- if (valid)
105
- headers.set(`Mcp-Param-${parameter.name}`, encodeMcpHeaderValue(String(value)));
100
+ if (!valid) {
101
+ throw new ParameterHeaderValidationError(`MCP tool ${JSON.stringify(name)} argument ${JSON.stringify(parameter.path)} must be ${parameter.type === "integer" ? "a safe integer" : `a ${parameter.type}`}`);
102
+ }
103
+ headers.set(`Mcp-Param-${parameter.name}`, encodeMcpHeaderValue(String(value)));
106
104
  }
107
105
  }
108
106
  }
@@ -1,12 +1,13 @@
1
1
  import { beginLegacyMcpOperation, physicalMcpClient, takeMcpRequestOrigin, } from "../request-origin.js";
2
2
  import { AuthError } from "../errors.js";
3
- import { loadMcpSdkTypes, loadMcpSdkZodCompat } from "../sdk.lazy.js";
3
+ import { loadMcpSdkProtocol, loadMcpSdkTypes, loadMcpSdkZodCompat } from "../sdk.lazy.js";
4
4
  import { mcpElapsedClock } from "../wrap.js";
5
5
  import { collectMrtrForm } from "./elicitation.js";
6
6
  import { isRecord, validateMrtrForm } from "./form-schema.js";
7
7
  import { McpMrtrError } from "./errors.js";
8
8
  import { queuedForm } from "./consent-queue.js";
9
9
  import { parseInputs } from "./inputs.js";
10
+ import { ParameterHeaderValidationError } from "./parameter-headers.js";
10
11
  export const MAX_MCP_MRTR_ROUNDS = 8;
11
12
  const capableMethods = new Set(["tools/call", "resources/read", "prompts/get"]);
12
13
  const capabilitiesKey = "io.modelcontextprotocol/clientCapabilities";
@@ -44,8 +45,9 @@ export function installModernRequestAdapter(client, protocol, clock = mcpElapsed
44
45
  if (own(request.params ?? {}, "requestState") || own(request.params ?? {}, "inputResponses") || options?.task)
45
46
  throw new McpMrtrError("MCP_MRTR_INVALID_INPUT");
46
47
  const params = structuredClone(request.params ?? {});
48
+ const { DEFAULT_REQUEST_TIMEOUT_MSEC } = await loadMcpSdkProtocol();
47
49
  const start = clock.now();
48
- const timeout = sdkOptions.timeout ?? 60_000;
50
+ const timeout = sdkOptions.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
49
51
  const duration = Math.min(sdkOptions.maxTotalTimeout ?? Infinity, 9 * timeout + 300_000);
50
52
  const deadline = start + duration;
51
53
  const operation = new AbortController();
@@ -259,6 +261,8 @@ export function installModernRequestAdapter(client, protocol, clock = mcpElapsed
259
261
  throw error;
260
262
  if (error instanceof AuthError)
261
263
  throw error;
264
+ if (error instanceof ParameterHeaderValidationError)
265
+ throw error;
262
266
  const remoteCode = isRecord(error) && typeof error.code === "number" ? error.code : undefined;
263
267
  throw new McpMrtrError("MCP_MRTR_TRANSPORT_FAILED", remoteCode);
264
268
  }