@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.
- package/CHANGELOG.md +39 -0
- package/dist/bundle/chunks/{anthropic-messages-M235FJQV.js → anthropic-messages-ODSFJCXR.js} +1 -1
- package/dist/bundle/chunks/{args-FBNJOJSR.js → args-SWKJAYQD.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-YQRTQ7H6.js → azure-openai-responses-REGXS4XO.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/{chunk-DMFAVE5N.js → chunk-4QVORFQ2.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZXHLH4MW.js → chunk-5W3DTM2J.js} +1 -1
- package/dist/bundle/chunks/{chunk-QYFOHFPS.js → chunk-6CJC6JY4.js} +1 -1
- package/dist/bundle/chunks/{chunk-QREKAXKX.js → chunk-6GAMRAVM.js} +5 -5
- package/dist/bundle/chunks/{chunk-MSHRIZGT.js → chunk-7IZH2KQX.js} +3 -3
- package/dist/bundle/chunks/{chunk-PH3JDRB2.js → chunk-ARSENVDY.js} +1 -1
- package/dist/bundle/chunks/{chunk-5KZH6EYQ.js → chunk-CNDPCYNT.js} +1 -1
- package/dist/bundle/chunks/{chunk-DYWIFF2Q.js → chunk-CYOHFREV.js} +1 -1
- package/dist/bundle/chunks/{chunk-3LPWILUZ.js → chunk-DNDZPKIQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZLIVZQDY.js → chunk-EQNUL2WB.js} +1 -1
- package/dist/bundle/chunks/{chunk-LO5MYLGF.js → chunk-FQDARFHT.js} +1 -1
- package/dist/bundle/chunks/{chunk-7KOUQSBX.js → chunk-GPPOPK76.js} +3 -3
- package/dist/bundle/chunks/{chunk-I7QWTWVZ.js → chunk-IHKHCWWG.js} +1 -1
- package/dist/bundle/chunks/{chunk-MY4ZD3EW.js → chunk-IY4G44MN.js} +1 -1
- package/dist/bundle/chunks/{chunk-OQ3WDMB3.js → chunk-JH7MVRNJ.js} +1 -1
- package/dist/bundle/chunks/chunk-KBSFTXAT.js +2 -0
- package/dist/bundle/chunks/{chunk-UW6M3EXY.js → chunk-KRO63T4Q.js} +1 -1
- package/dist/bundle/chunks/{chunk-J5FCH7TI.js → chunk-KTDIIF2F.js} +1 -1
- package/dist/bundle/chunks/{chunk-3WP5RFQR.js → chunk-NDNIMC5W.js} +1 -1
- package/dist/bundle/chunks/chunk-NXQFBAUI.js +2 -0
- package/dist/bundle/chunks/{chunk-CDQPA4FP.js → chunk-ODRGPC5S.js} +1 -1
- package/dist/bundle/chunks/{chunk-5FQMNHXV.js → chunk-P5E4EVQF.js} +1 -1
- package/dist/bundle/chunks/{chunk-3A3XCLNH.js → chunk-PDG4XWO2.js} +1 -1
- package/dist/bundle/chunks/{chunk-5LWUUXMH.js → chunk-PY4ZQHTN.js} +1 -1
- package/dist/bundle/chunks/chunk-Q2A2V6NT.js +5 -0
- package/dist/bundle/chunks/{chunk-SOXQ42VS.js → chunk-RQ5JTXUC.js} +1 -1
- package/dist/bundle/chunks/{chunk-JPGT64RV.js → chunk-TV3UI63P.js} +1 -1
- package/dist/bundle/chunks/{chunk-JFQUWEN7.js → chunk-TWHF52G6.js} +4 -4
- package/dist/bundle/chunks/{chunk-6U3TBFBA.js → chunk-UJPG4ROT.js} +1 -1
- package/dist/bundle/chunks/{chunk-UDM7VO6O.js → chunk-XM36IFP3.js} +1 -1
- package/dist/bundle/chunks/{chunk-XQ45JMGN.js → chunk-Z6BREXV3.js} +1 -1
- package/dist/bundle/chunks/chunk-ZAS4NBVK.js +410 -0
- package/dist/bundle/chunks/{chunk-ADQAPDPV.js → chunk-ZP6JJDYV.js} +1 -1
- package/dist/bundle/chunks/{chunk-EZP6AYW2.js → chunk-ZXIAVLJV.js} +88 -88
- package/dist/bundle/chunks/{cli-main-55EQA5PS.js → cli-main-36FRZ7ZB.js} +1 -1
- package/dist/bundle/chunks/client-CBDPMWZJ.js +2 -0
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-ZJQGIIRC.js → cloudflare-workers-ai-system-one-WDHT7BBV.js} +1 -1
- package/dist/bundle/chunks/cursor-agent.js +1 -1
- package/dist/bundle/chunks/devin-agent.js +1 -1
- package/dist/bundle/chunks/{dispatch-C2IBP5VV.js → dispatch-W5PENPZM.js} +3 -3
- package/dist/bundle/chunks/{google-generative-ai-5G4RIQA6.js → google-generative-ai-FHYFXKLA.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-ALD4P27O.js → google-vertex-5DWE5U5K.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-PEIKGCKZ.js → help-fast-path-FETLIF3W.js} +1 -1
- package/dist/bundle/chunks/{host-command-657VCQ34.js → host-command-XEB7SHT3.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-OV5XDOJK.js → host-session-command-FFN52LXJ.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-MLXSYK2H.js → interactive-mode-5S7NNV2Z.js} +1 -1
- package/dist/bundle/chunks/{mistral-conversations-RAE4K2XY.js → mistral-conversations-I2PI7DDF.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-774MACJU.js → multi-session-host-E3OPE6HJ.js} +3 -3
- package/dist/bundle/chunks/{openai-codex-responses-3CW4R7TG.js → openai-codex-responses-WSEX6SH7.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-5UIJXQUF.js → openai-completions-YCBHTBSS.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-2BO44MRW.js → openai-responses-GXNSQF4A.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-MZHVCX2S.js → package-manager-cli-3XO456VE.js} +1 -1
- package/dist/bundle/chunks/{pi-messages-RF6GER4R.js → pi-messages-O6GISIRW.js} +1 -1
- package/dist/bundle/chunks/protocol-GLNWYI5G.js +2 -0
- package/dist/bundle/chunks/{role-ETSFSDTS.js → role-SHIZQKK4.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-ZMQB4GPO.js → rotation-stream-2PCZWNKF.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-UJXOIZCW.js → rpc-mode-GLWKLTMA.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-KA7LFFNK.js → schedule-command-B7RRSANU.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-MFWXUR7Z.js → session-control-endpoint-KLE2X4RR.js} +1 -1
- package/dist/bundle/chunks/{session-picker-BP33NRSL.js → session-picker-DTGBOXDR.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +131 -131
- package/dist/bundle/chunks/{typesafe-system-one-GNNZ5O5U.js → typesafe-system-one-XGYJJVSE.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.d.ts +11 -1
- package/dist/core/agent-session.js +19 -2
- package/dist/core/extensions/builtin/anthropic-subscription/account-context.d.ts +15 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-context.js +53 -0
- package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.d.ts +3 -0
- package/dist/core/extensions/builtin/anthropic-subscription/auth-attempt.js +26 -9
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane-boundary.d.ts +16 -0
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane-boundary.js +27 -0
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.d.ts +2 -13
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +3 -26
- package/dist/core/extensions/builtin/anthropic-subscription/errors.js +3 -2
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +6 -1
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +13 -2
- package/dist/core/extensions/builtin/compaction/index.js +7 -1
- package/dist/core/extensions/builtin/herdr/index.js +9 -5
- package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.d.ts +6 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.js +25 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +3 -3
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +50 -19
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +4 -3
- package/dist/core/extensions/builtin/mcp/auth/oauth.js +19 -12
- package/dist/core/extensions/builtin/mcp/modern/consent-queue.js +1 -0
- package/dist/core/extensions/builtin/mcp/modern/dispatch.js +1 -0
- package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +4 -1
- package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +33 -35
- package/dist/core/extensions/builtin/mcp/modern/request.js +6 -2
- package/dist/core/extensions/builtin/mcp/modern/transport.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/modern/transport.js +4 -2
- package/dist/core/extensions/builtin/mcp/sdk.lazy.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/sdk.lazy.js +2 -0
- package/dist/core/extensions/builtin/mcp/transport-sdk.js +12 -1
- package/dist/core/extensions/builtin/terminal/extension-state.js +2 -1
- package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +9 -0
- package/dist/core/extensions/builtin/vertex-model-garden/auth.js +17 -1
- package/dist/core/extensions/builtin/websearch/websearch/providers/google.d.ts +6 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/google.js +6 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/lifecycle-children.d.ts +3 -1
- package/dist/core/lifecycle-children.js +79 -15
- package/dist/core/lifecycle-record.js +1 -24
- package/dist/core/lifecycle-session-binding.js +2 -1
- package/dist/core/lifecycle-wait-items.d.ts +17 -0
- package/dist/core/lifecycle-wait-items.js +72 -0
- package/dist/core/model-runtime.js +6 -1
- package/dist/core/provider-api-key-auth.d.ts +6 -1
- package/dist/core/provider-api-key-auth.js +11 -4
- package/dist/core/provider-composer.d.ts +8 -1
- package/dist/core/provider-composer.js +7 -3
- package/dist/core/sdk.js +4 -1
- package/dist/core/session-activity.js +2 -0
- package/dist/core/session-lifecycle-types.d.ts +20 -4
- package/dist/core/session-lifecycle.js +35 -4
- package/dist/core/structured-session-lane.d.ts +33 -0
- package/dist/core/structured-session-lane.js +62 -0
- package/dist/core/work-custody-record.d.ts +2 -1
- package/dist/core/work-custody-record.js +2 -0
- package/dist/core/work-obligations.d.ts +1 -1
- package/dist/core/work-obligations.js +8 -2
- package/dist/modes/interactive/interactive-mode.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode.js +2 -1
- package/dist/modes/json-event.d.ts +79 -1
- package/dist/modes/json-event.js +79 -1
- package/dist/modes/rpc/compact-commit-dependencies.d.ts +13 -0
- package/dist/modes/rpc/compact-commit-dependencies.js +48 -0
- package/dist/modes/rpc/compact-content-index.d.ts +84 -0
- package/dist/modes/rpc/compact-content-index.js +243 -0
- package/dist/modes/rpc/compact-content-store.d.ts +10 -43
- package/dist/modes/rpc/compact-content-store.js +61 -163
- package/dist/modes/rpc/compact-index-pages.d.ts +12 -0
- package/dist/modes/rpc/compact-index-pages.js +31 -0
- package/dist/modes/rpc/compact-read-lease.d.ts +15 -0
- package/dist/modes/rpc/compact-read-lease.js +40 -0
- package/dist/modes/rpc/compact-session-state.d.ts +14 -63
- package/dist/modes/rpc/compact-session-state.js +69 -51
- package/dist/modes/rpc/compact-spool-dir.d.ts +4 -1
- package/dist/modes/rpc/compact-spool-dir.js +41 -8
- package/dist/modes/rpc/compact-spool-storage.d.ts +58 -0
- package/dist/modes/rpc/compact-spool-storage.js +195 -0
- package/dist/modes/rpc/compact-storage-io.d.ts +2 -1
- package/dist/modes/rpc/compact-storage-io.js +7 -1
- package/dist/modes/rpc/compact-text-roots.d.ts +22 -0
- package/dist/modes/rpc/compact-text-roots.js +138 -0
- package/dist/modes/rpc/compact-text-view.d.ts +22 -0
- package/dist/modes/rpc/compact-text-view.js +16 -0
- package/dist/modes/rpc/compact-transition.d.ts +41 -0
- package/dist/modes/rpc/compact-transition.js +12 -0
- package/dist/modes/rpc/connection-handler.d.ts +2 -0
- package/dist/modes/rpc/connection-handler.js +12 -6
- package/dist/modes/rpc/host-ensure-start.js +5 -3
- package/dist/modes/rpc/security-presentations.js +2 -1
- package/dist/modes/rpc/session-binding.js +10 -0
- package/dist/modes/rpc/session-event-writer.d.ts +2 -0
- package/dist/modes/rpc/session-event-writer.js +2 -0
- package/dist/modes/rpc/session-record-stream.d.ts +3 -3
- package/dist/modes/rpc/session-record-stream.js +3 -3
- package/dist/modes/rpc/session-structured-lanes.d.ts +12 -0
- package/dist/modes/rpc/session-structured-lanes.js +24 -0
- package/dist/modes/rpc/session-worker-client.js +6 -5
- package/dist/modes/rpc/structured-session-records.d.ts +12 -0
- package/dist/modes/rpc/structured-session-records.js +88 -0
- package/docs/extensions.md +7 -1
- package/docs/rpc.md +17 -2
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-GQJYNFMO.js +0 -2
- package/dist/bundle/chunks/chunk-VICDRG3J.js +0 -410
- package/dist/bundle/chunks/chunk-VL7O5PO4.js +0 -5
- 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 {
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
71
|
-
if (issuer === undefined || !
|
|
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
|
|
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
|
|
60
|
-
if (
|
|
61
|
-
|
|
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
|
-
|
|
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 ===
|
|
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:
|
|
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 =
|
|
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,
|
|
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 });
|
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
49
|
-
|
|
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 :
|
|
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 ===
|
|
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
|
-
|
|
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 ??
|
|
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
|
}
|