@agentclientprotocol/claude-agent-acp 0.73.0 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-agent.d.ts +86 -3
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +527 -205
- package/dist/hide-claude-auth.d.ts +145 -0
- package/dist/hide-claude-auth.d.ts.map +1 -0
- package/dist/hide-claude-auth.js +231 -0
- package/dist/session-failure-extension.d.ts +15 -0
- package/dist/session-failure-extension.d.ts.map +1 -1
- package/dist/session-failure-extension.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--hide-claude-auth`: this integration must never bill a claude.ai
|
|
3
|
+
* subscription. The flag hides the claude.ai login method from `initialize`
|
|
4
|
+
* and makes the agent behave like a logged-out CLI whenever a subscription
|
|
5
|
+
* would pay for a turn. `acp-agent.ts` decides *when* the guard applies (flag
|
|
6
|
+
* on, no provider override) and calls in here for the *what*.
|
|
7
|
+
*
|
|
8
|
+
* The account the CLI reports at `initialize` is the source of truth. The SDK
|
|
9
|
+
* memoizes it, so it is a constant for the life of a query, and two layers
|
|
10
|
+
* keep it honest:
|
|
11
|
+
*
|
|
12
|
+
* 1. Session creation refuses an account that holds no credential this
|
|
13
|
+
* integration accepts. A logged-out session never exists, so the client's
|
|
14
|
+
* sign-in always leads to a new session with a fresh `initialize`. The
|
|
15
|
+
* per-turn guard repeats the check on the same cached account, which costs
|
|
16
|
+
* microseconds and covers a session created under a provider override that
|
|
17
|
+
* `providers/disable` later removed.
|
|
18
|
+
* 2. A sign-out during the session ends the query. The next turn recreates it,
|
|
19
|
+
* so the next `initialize` reports the real account and layer 1 judges it.
|
|
20
|
+
* The recreation resumes the stored conversation. When the CLI never wrote
|
|
21
|
+
* one, because the first turn was the one that signed out, the recreation
|
|
22
|
+
* starts a fresh query under the same session id instead.
|
|
23
|
+
*
|
|
24
|
+
* One case is left open. The CLI re-reads its credential store on its own. A
|
|
25
|
+
* file-based credential can therefore be removed and replaced by a claude.ai
|
|
26
|
+
* subscription between two turns. This covers a managed Console key and a key
|
|
27
|
+
* from a settings helper. No turn fails in between, so nothing ends the query,
|
|
28
|
+
* and the cached account still names the key that is gone. Closing this case
|
|
29
|
+
* would cost one `initialize` control request per turn. That buys too little
|
|
30
|
+
* for the price.
|
|
31
|
+
*/
|
|
32
|
+
import { RequestError } from "@agentclientprotocol/sdk";
|
|
33
|
+
import type { AccountInfo, Query } from "@anthropic-ai/claude-agent-sdk";
|
|
34
|
+
import type { SessionFailureController } from "./session-failure-extension.js";
|
|
35
|
+
export declare function shouldHideClaudeAuth(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* `data.reason` carried by the `authRequired` JSON-RPC error when the flag
|
|
38
|
+
* blocks a turn. From the client's point of view the agent is logged out (same
|
|
39
|
+
* error code and flow as a signed-out CLI); the reason lets it render a
|
|
40
|
+
* different hint. A string enum so further reasons can be added without a
|
|
41
|
+
* shape change.
|
|
42
|
+
*/
|
|
43
|
+
export declare const CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_REASON = "claude_subscription_not_supported";
|
|
44
|
+
export type AuthRequiredReason = typeof CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_REASON;
|
|
45
|
+
export declare const CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_MESSAGE = "This integration does not support using claude.ai subscriptions.";
|
|
46
|
+
/** The message of the plain sign-out refusal. It carries no `reason`: the
|
|
47
|
+
* account holds nothing this integration can bill, which is the ordinary
|
|
48
|
+
* signed-out state every ACP client already knows how to answer. */
|
|
49
|
+
export declare const CLAUDE_LOGIN_REQUIRED_MESSAGE = "Sign in with an API key or a Console account to use this integration.";
|
|
50
|
+
/**
|
|
51
|
+
* True when a turn on this account is paid by a claude.ai subscription.
|
|
52
|
+
*
|
|
53
|
+
* The SDK types `apiKeySource` and `tokenSource` as an open `string`, so this
|
|
54
|
+
* is an allowlist. An account bills the subscription only when every field
|
|
55
|
+
* proves it, and an unknown value keeps the guard on.
|
|
56
|
+
*
|
|
57
|
+
* - `subscriptionType` must be set. Without it the CLI reports no subscription.
|
|
58
|
+
* - `tokenSource` must be absent. The CLI sets it in place of the subscription
|
|
59
|
+
* when a bearer or OAuth environment token pays for the turn.
|
|
60
|
+
* - `apiProvider` must be absent or `firstParty`. The SDK documents that the
|
|
61
|
+
* Anthropic OAuth login applies only to `firstParty`; for a third-party
|
|
62
|
+
* backend such as `bedrock`, `vertex`, `foundry` or `gateway` the other
|
|
63
|
+
* fields are absent and the authentication is external.
|
|
64
|
+
* - `apiKeySource` must not be one of {@link API_KEY_SOURCES_ABOVE_SUBSCRIPTION}.
|
|
65
|
+
* Claude Code's credential precedence puts those keys above the `/login`
|
|
66
|
+
* subscription, so the key pays and the stored subscription is inert.
|
|
67
|
+
*/
|
|
68
|
+
export declare function billsClaudeSubscription(account: AccountInfo | undefined): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* True when the account already holds a credential this integration accepts:
|
|
71
|
+
* a third-party backend, a bearer or OAuth environment token, or one of the
|
|
72
|
+
* API key sources in {@link API_KEY_SOURCES_ABOVE_SUBSCRIPTION}.
|
|
73
|
+
*
|
|
74
|
+
* False therefore covers both the signed-out CLI and the account whose only
|
|
75
|
+
* credential is a claude.ai subscription. {@link billsClaudeSubscription} runs
|
|
76
|
+
* first and separates the two, so a caller that reaches a `false` here is
|
|
77
|
+
* looking at a session with no way to pay.
|
|
78
|
+
*/
|
|
79
|
+
export declare function holdsNonSubscriptionCredential(account: AccountInfo | undefined): boolean;
|
|
80
|
+
export declare function claudeSubscriptionNotSupportedError(): RequestError;
|
|
81
|
+
/** The signed-out refusal. Deliberately without `data`, so a client that reads
|
|
82
|
+
* `data.reason` sees the plain ACP sign-out it already handles. */
|
|
83
|
+
export declare function claudeLoginRequiredError(): RequestError;
|
|
84
|
+
/**
|
|
85
|
+
* The state the guard keeps for one session. It lives on the session so that a
|
|
86
|
+
* concurrent pair of turns shares one account read and one failure row, and so
|
|
87
|
+
* that each degraded-guard warning is logged once.
|
|
88
|
+
*/
|
|
89
|
+
export type ClaudeSubscriptionGuardState = {
|
|
90
|
+
/** The in-flight guard run. Set before the first `await`. */
|
|
91
|
+
pendingRun?: Promise<RequestError | undefined>;
|
|
92
|
+
/** True after the "no account source" warning was logged for this session. */
|
|
93
|
+
degradedWarningLogged?: boolean;
|
|
94
|
+
};
|
|
95
|
+
export type GuardLogger = {
|
|
96
|
+
error: (...args: unknown[]) => void;
|
|
97
|
+
warn?: (...args: unknown[]) => void;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Report once per session that the guard cannot read the account at all. The
|
|
101
|
+
* guard is then absent, not passing. The CLI build decides this, not the user,
|
|
102
|
+
* so a message on every turn would only add noise.
|
|
103
|
+
*/
|
|
104
|
+
export declare function warnClaudeSubscriptionGuardDegraded(options: {
|
|
105
|
+
sessionId: string;
|
|
106
|
+
guardState: ClaudeSubscriptionGuardState;
|
|
107
|
+
logger: GuardLogger;
|
|
108
|
+
cause: string;
|
|
109
|
+
}): void;
|
|
110
|
+
type GuardOptions = {
|
|
111
|
+
sessionId: string;
|
|
112
|
+
query: Pick<Query, "accountInfo">;
|
|
113
|
+
guardState: ClaudeSubscriptionGuardState;
|
|
114
|
+
/** Built lazily: most turns pass and never need a controller. */
|
|
115
|
+
sessionFailures: () => SessionFailureController;
|
|
116
|
+
logger: GuardLogger;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The per-turn guard for a live session.
|
|
120
|
+
*
|
|
121
|
+
* It decides on the account the SDK cached at `initialize`, which is this
|
|
122
|
+
* integration's source of truth. That account is a constant for the life of
|
|
123
|
+
* the query, so the check costs microseconds. It still earns its place: it
|
|
124
|
+
* covers a session created under a provider override that `providers/disable`
|
|
125
|
+
* later removed, so the create-time check never ran for the routing in force
|
|
126
|
+
* now. A sign-out ends the query (see `markSessionForSignOutRespawn` in
|
|
127
|
+
* `acp-agent.ts`), so the next turn recreates it and `initialize` reports the
|
|
128
|
+
* real account. This guard does not see the residual case the module header
|
|
129
|
+
* names either.
|
|
130
|
+
*
|
|
131
|
+
* A turn is refused when the account bills a subscription, and also when it
|
|
132
|
+
* holds nothing this integration accepts, which is the user signing out in
|
|
133
|
+
* mid-session. The refusal mirrors the signed-out path exactly (see
|
|
134
|
+
* `failActiveWithSessionFailure` for `auth_required` in `acp-agent.ts`): every
|
|
135
|
+
* client gets the `authRequired` JSON-RPC rejection that starts its own auth
|
|
136
|
+
* flow, and a capable client additionally gets one session-scoped access
|
|
137
|
+
* failure whose `login` action stays the way back in.
|
|
138
|
+
*
|
|
139
|
+
* Fails open when no account can be read at all: session creation already
|
|
140
|
+
* refused every account without a usable credential, and an unavailable probe
|
|
141
|
+
* must not block a session that passed that check.
|
|
142
|
+
*/
|
|
143
|
+
export declare function refuseClaudeSubscriptionTurn(options: GuardOptions): Promise<void>;
|
|
144
|
+
export {};
|
|
145
|
+
//# sourceMappingURL=hide-claude-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hide-claude-auth.d.ts","sourceRoot":"","sources":["../src/hide-claude-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAE/E,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,wCAAwC,sCAAsC,CAAC;AAC5F,MAAM,MAAM,kBAAkB,GAAG,OAAO,wCAAwC,CAAC;AAEjF,eAAO,MAAM,yCAAyC,qEACc,CAAC;AAErE;;qEAEqE;AACrE,eAAO,MAAM,6BAA6B,0EAC+B,CAAC;AAmB1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAWjF;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAWxF;AAED,wBAAgB,mCAAmC,IAAI,YAAY,CAKlE;AAED;oEACoE;AACpE,wBAAgB,wBAAwB,IAAI,YAAY,CAEvD;AAED;;;;GAIG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;IAC/C,8EAA8E;IAC9E,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IACpC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;AAMF;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,EAAE;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,4BAA4B,CAAC;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,IAAI,CAWP;AAED,KAAK,YAAY,GAAG;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;IAClC,UAAU,EAAE,4BAA4B,CAAC;IACzC,iEAAiE;IACjE,eAAe,EAAE,MAAM,wBAAwB,CAAC;IAChD,MAAM,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAavF"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--hide-claude-auth`: this integration must never bill a claude.ai
|
|
3
|
+
* subscription. The flag hides the claude.ai login method from `initialize`
|
|
4
|
+
* and makes the agent behave like a logged-out CLI whenever a subscription
|
|
5
|
+
* would pay for a turn. `acp-agent.ts` decides *when* the guard applies (flag
|
|
6
|
+
* on, no provider override) and calls in here for the *what*.
|
|
7
|
+
*
|
|
8
|
+
* The account the CLI reports at `initialize` is the source of truth. The SDK
|
|
9
|
+
* memoizes it, so it is a constant for the life of a query, and two layers
|
|
10
|
+
* keep it honest:
|
|
11
|
+
*
|
|
12
|
+
* 1. Session creation refuses an account that holds no credential this
|
|
13
|
+
* integration accepts. A logged-out session never exists, so the client's
|
|
14
|
+
* sign-in always leads to a new session with a fresh `initialize`. The
|
|
15
|
+
* per-turn guard repeats the check on the same cached account, which costs
|
|
16
|
+
* microseconds and covers a session created under a provider override that
|
|
17
|
+
* `providers/disable` later removed.
|
|
18
|
+
* 2. A sign-out during the session ends the query. The next turn recreates it,
|
|
19
|
+
* so the next `initialize` reports the real account and layer 1 judges it.
|
|
20
|
+
* The recreation resumes the stored conversation. When the CLI never wrote
|
|
21
|
+
* one, because the first turn was the one that signed out, the recreation
|
|
22
|
+
* starts a fresh query under the same session id instead.
|
|
23
|
+
*
|
|
24
|
+
* One case is left open. The CLI re-reads its credential store on its own. A
|
|
25
|
+
* file-based credential can therefore be removed and replaced by a claude.ai
|
|
26
|
+
* subscription between two turns. This covers a managed Console key and a key
|
|
27
|
+
* from a settings helper. No turn fails in between, so nothing ends the query,
|
|
28
|
+
* and the cached account still names the key that is gone. Closing this case
|
|
29
|
+
* would cost one `initialize` control request per turn. That buys too little
|
|
30
|
+
* for the price.
|
|
31
|
+
*/
|
|
32
|
+
import { RequestError } from "@agentclientprotocol/sdk";
|
|
33
|
+
export function shouldHideClaudeAuth() {
|
|
34
|
+
return process.argv.includes("--hide-claude-auth");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `data.reason` carried by the `authRequired` JSON-RPC error when the flag
|
|
38
|
+
* blocks a turn. From the client's point of view the agent is logged out (same
|
|
39
|
+
* error code and flow as a signed-out CLI); the reason lets it render a
|
|
40
|
+
* different hint. A string enum so further reasons can be added without a
|
|
41
|
+
* shape change.
|
|
42
|
+
*/
|
|
43
|
+
export const CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_REASON = "claude_subscription_not_supported";
|
|
44
|
+
export const CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_MESSAGE = "This integration does not support using claude.ai subscriptions.";
|
|
45
|
+
/** The message of the plain sign-out refusal. It carries no `reason`: the
|
|
46
|
+
* account holds nothing this integration can bill, which is the ordinary
|
|
47
|
+
* signed-out state every ACP client already knows how to answer. */
|
|
48
|
+
export const CLAUDE_LOGIN_REQUIRED_MESSAGE = "Sign in with an API key or a Console account to use this integration.";
|
|
49
|
+
/**
|
|
50
|
+
* The `apiKeySource` values that outrank a stored subscription.
|
|
51
|
+
*
|
|
52
|
+
* The SDK documents its `ApiKeySource` type as the origin of the credential
|
|
53
|
+
* used for API requests. `ANTHROPIC_API_KEY` is the environment variable,
|
|
54
|
+
* `apiKeyHelper` is the configured helper command, and `/login managed key` is
|
|
55
|
+
* a key that `/login` created with an Anthropic Console account. Each of the
|
|
56
|
+
* three is an API key that pays instead of the subscription. Every other
|
|
57
|
+
* member means no API key is in use: `none`, and the legacy `user`, `project`,
|
|
58
|
+
* `org`, `temporary` and `oauth` that current CLIs never emit.
|
|
59
|
+
*/
|
|
60
|
+
const API_KEY_SOURCES_ABOVE_SUBSCRIPTION = new Set([
|
|
61
|
+
"ANTHROPIC_API_KEY",
|
|
62
|
+
"apiKeyHelper",
|
|
63
|
+
"/login managed key",
|
|
64
|
+
]);
|
|
65
|
+
/**
|
|
66
|
+
* True when a turn on this account is paid by a claude.ai subscription.
|
|
67
|
+
*
|
|
68
|
+
* The SDK types `apiKeySource` and `tokenSource` as an open `string`, so this
|
|
69
|
+
* is an allowlist. An account bills the subscription only when every field
|
|
70
|
+
* proves it, and an unknown value keeps the guard on.
|
|
71
|
+
*
|
|
72
|
+
* - `subscriptionType` must be set. Without it the CLI reports no subscription.
|
|
73
|
+
* - `tokenSource` must be absent. The CLI sets it in place of the subscription
|
|
74
|
+
* when a bearer or OAuth environment token pays for the turn.
|
|
75
|
+
* - `apiProvider` must be absent or `firstParty`. The SDK documents that the
|
|
76
|
+
* Anthropic OAuth login applies only to `firstParty`; for a third-party
|
|
77
|
+
* backend such as `bedrock`, `vertex`, `foundry` or `gateway` the other
|
|
78
|
+
* fields are absent and the authentication is external.
|
|
79
|
+
* - `apiKeySource` must not be one of {@link API_KEY_SOURCES_ABOVE_SUBSCRIPTION}.
|
|
80
|
+
* Claude Code's credential precedence puts those keys above the `/login`
|
|
81
|
+
* subscription, so the key pays and the stored subscription is inert.
|
|
82
|
+
*/
|
|
83
|
+
export function billsClaudeSubscription(account) {
|
|
84
|
+
if (!account?.subscriptionType) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (account.tokenSource) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (account.apiProvider !== undefined && account.apiProvider !== "firstParty") {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return !API_KEY_SOURCES_ABOVE_SUBSCRIPTION.has(account.apiKeySource ?? "");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* True when the account already holds a credential this integration accepts:
|
|
97
|
+
* a third-party backend, a bearer or OAuth environment token, or one of the
|
|
98
|
+
* API key sources in {@link API_KEY_SOURCES_ABOVE_SUBSCRIPTION}.
|
|
99
|
+
*
|
|
100
|
+
* False therefore covers both the signed-out CLI and the account whose only
|
|
101
|
+
* credential is a claude.ai subscription. {@link billsClaudeSubscription} runs
|
|
102
|
+
* first and separates the two, so a caller that reaches a `false` here is
|
|
103
|
+
* looking at a session with no way to pay.
|
|
104
|
+
*/
|
|
105
|
+
export function holdsNonSubscriptionCredential(account) {
|
|
106
|
+
if (!account) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (account.apiProvider !== undefined && account.apiProvider !== "firstParty") {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (account.tokenSource) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return API_KEY_SOURCES_ABOVE_SUBSCRIPTION.has(account.apiKeySource ?? "");
|
|
116
|
+
}
|
|
117
|
+
export function claudeSubscriptionNotSupportedError() {
|
|
118
|
+
return RequestError.authRequired({ reason: CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_REASON }, CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_MESSAGE);
|
|
119
|
+
}
|
|
120
|
+
/** The signed-out refusal. Deliberately without `data`, so a client that reads
|
|
121
|
+
* `data.reason` sees the plain ACP sign-out it already handles. */
|
|
122
|
+
export function claudeLoginRequiredError() {
|
|
123
|
+
return RequestError.authRequired(undefined, CLAUDE_LOGIN_REQUIRED_MESSAGE);
|
|
124
|
+
}
|
|
125
|
+
function warn(logger, message) {
|
|
126
|
+
(logger.warn ?? logger.error).call(logger, message);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Report once per session that the guard cannot read the account at all. The
|
|
130
|
+
* guard is then absent, not passing. The CLI build decides this, not the user,
|
|
131
|
+
* so a message on every turn would only add noise.
|
|
132
|
+
*/
|
|
133
|
+
export function warnClaudeSubscriptionGuardDegraded(options) {
|
|
134
|
+
const { guardState } = options;
|
|
135
|
+
if (guardState.degradedWarningLogged) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
guardState.degradedWarningLogged = true;
|
|
139
|
+
warn(options.logger, `Session ${options.sessionId}: the --hide-claude-auth subscription guard is degraded ` +
|
|
140
|
+
`(${options.cause}). This session cannot detect a claude.ai subscription.`);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The per-turn guard for a live session.
|
|
144
|
+
*
|
|
145
|
+
* It decides on the account the SDK cached at `initialize`, which is this
|
|
146
|
+
* integration's source of truth. That account is a constant for the life of
|
|
147
|
+
* the query, so the check costs microseconds. It still earns its place: it
|
|
148
|
+
* covers a session created under a provider override that `providers/disable`
|
|
149
|
+
* later removed, so the create-time check never ran for the routing in force
|
|
150
|
+
* now. A sign-out ends the query (see `markSessionForSignOutRespawn` in
|
|
151
|
+
* `acp-agent.ts`), so the next turn recreates it and `initialize` reports the
|
|
152
|
+
* real account. This guard does not see the residual case the module header
|
|
153
|
+
* names either.
|
|
154
|
+
*
|
|
155
|
+
* A turn is refused when the account bills a subscription, and also when it
|
|
156
|
+
* holds nothing this integration accepts, which is the user signing out in
|
|
157
|
+
* mid-session. The refusal mirrors the signed-out path exactly (see
|
|
158
|
+
* `failActiveWithSessionFailure` for `auth_required` in `acp-agent.ts`): every
|
|
159
|
+
* client gets the `authRequired` JSON-RPC rejection that starts its own auth
|
|
160
|
+
* flow, and a capable client additionally gets one session-scoped access
|
|
161
|
+
* failure whose `login` action stays the way back in.
|
|
162
|
+
*
|
|
163
|
+
* Fails open when no account can be read at all: session creation already
|
|
164
|
+
* refused every account without a usable credential, and an unavailable probe
|
|
165
|
+
* must not block a session that passed that check.
|
|
166
|
+
*/
|
|
167
|
+
export async function refuseClaudeSubscriptionTurn(options) {
|
|
168
|
+
// Concurrent turns must not each re-read the account and each publish a row.
|
|
169
|
+
// Store the run before the first await, and let the others await the same one.
|
|
170
|
+
const { guardState } = options;
|
|
171
|
+
const run = (guardState.pendingRun ??= evaluateGuard(options).finally(() => {
|
|
172
|
+
if (guardState.pendingRun === run) {
|
|
173
|
+
guardState.pendingRun = undefined;
|
|
174
|
+
}
|
|
175
|
+
}));
|
|
176
|
+
const refusal = await run;
|
|
177
|
+
if (refusal) {
|
|
178
|
+
throw refusal;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Read the account, publish at most one failure row, and return the error the
|
|
182
|
+
* turn must be rejected with. Never throws: an unreadable account fails open. */
|
|
183
|
+
async function evaluateGuard(options) {
|
|
184
|
+
const account = await readGuardAccount(options);
|
|
185
|
+
if (!account) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
if (billsClaudeSubscription(account)) {
|
|
189
|
+
await publishRefusal(options.sessionFailures(), {
|
|
190
|
+
details: CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_MESSAGE,
|
|
191
|
+
reason: CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_REASON,
|
|
192
|
+
});
|
|
193
|
+
return claudeSubscriptionNotSupportedError();
|
|
194
|
+
}
|
|
195
|
+
if (!holdsNonSubscriptionCredential(account)) {
|
|
196
|
+
await publishRefusal(options.sessionFailures(), { details: CLAUDE_LOGIN_REQUIRED_MESSAGE });
|
|
197
|
+
return claudeLoginRequiredError();
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
/** The account the SDK cached at `initialize`. `undefined` when the query
|
|
202
|
+
* cannot answer at all. */
|
|
203
|
+
async function readGuardAccount(options) {
|
|
204
|
+
const { query } = options;
|
|
205
|
+
if (typeof query.accountInfo !== "function") {
|
|
206
|
+
warnClaudeSubscriptionGuardDegraded({
|
|
207
|
+
...options,
|
|
208
|
+
cause: "the SDK query has no accountInfo method",
|
|
209
|
+
});
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
return await query.accountInfo();
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
options.logger.error(`Session ${options.sessionId}: accountInfo failed; skipping the subscription check for this turn:`, error instanceof Error ? error.message : String(error));
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** The refusal state does not change between turns: publish once, and skip a
|
|
221
|
+
* republish while the previous failure with this reason is still active. */
|
|
222
|
+
async function publishRefusal(sessionFailures, failure) {
|
|
223
|
+
if (sessionFailures.hasActiveSessionError("auth_required", failure.reason)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
await sessionFailures.publish("auth_required", {
|
|
227
|
+
sessionScoped: true,
|
|
228
|
+
details: failure.details,
|
|
229
|
+
...(failure.reason ? { reason: failure.reason } : {}),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
@@ -15,6 +15,10 @@ export type PublishedSessionFailure = {
|
|
|
15
15
|
severity: AirSessionFailureSeverity;
|
|
16
16
|
title: string;
|
|
17
17
|
details?: string;
|
|
18
|
+
/** A machine-readable refinement of `kind`, e.g. the `--hide-claude-auth`
|
|
19
|
+
* subscription guard's reason on an `auth_required` failure. Absent for a
|
|
20
|
+
* plain sign-out. */
|
|
21
|
+
reason?: string;
|
|
18
22
|
actions: AirSessionFailureAction[];
|
|
19
23
|
recoveryPolicy: SessionFailureRecoveryPolicy;
|
|
20
24
|
};
|
|
@@ -24,6 +28,7 @@ type SessionFailureOptions = {
|
|
|
24
28
|
title?: string;
|
|
25
29
|
details?: string;
|
|
26
30
|
severity?: AirSessionFailureSeverity;
|
|
31
|
+
reason?: string;
|
|
27
32
|
};
|
|
28
33
|
export type SessionFailureState = {
|
|
29
34
|
epoch: string;
|
|
@@ -71,6 +76,16 @@ export declare class SessionFailureController {
|
|
|
71
76
|
});
|
|
72
77
|
private isSupported;
|
|
73
78
|
private emit;
|
|
79
|
+
/** Whether a session-scoped error of `kind` is active. A turn-scoped
|
|
80
|
+
* warning of the same kind (an `api_retry` notice) does not count: it
|
|
81
|
+
* reports a retry in progress, not the state the error would publish.
|
|
82
|
+
*
|
|
83
|
+
* `reason` refines `kind`, so the active record must carry the same one,
|
|
84
|
+
* and an omitted `reason` matches only a record with no reason. A plain
|
|
85
|
+
* sign-out and the `--hide-claude-auth` subscription refusal are both
|
|
86
|
+
* `auth_required`, but they tell the user different things. Neither may
|
|
87
|
+
* suppress the other. */
|
|
88
|
+
hasActiveSessionError(kind: ClaudeFailureKind, reason?: string): boolean;
|
|
74
89
|
recordActive(failure: PublishedSessionFailure): void;
|
|
75
90
|
clear(shouldClear?: (failure: PublishedSessionFailure) => boolean): Promise<boolean>;
|
|
76
91
|
prepare(kind: ClaudeFailureKind, failureOptions?: SessionFailureOptions): Promise<PublishedSessionFailure | undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-failure-extension.d.ts","sourceRoot":"","sources":["../src/session-failure-extension.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACxF,OAAO,EACL,kBAAkB,EAClB,KAAK,wBAAwB,EAE9B,MAAM,gCAAgC,CAAC;AASxC,wBAAgB,+BAA+B,CAAC,GAAG,sBAAsB,EAAE,MAAM,EAAE,2BAElF;AAED,MAAM,MAAM,iBAAiB,GACzB,UAAU,GACV,eAAe,GACf,aAAa,GACb,kBAAkB,GAClB,mBAAmB,GACnB,gBAAgB,GAChB,YAAY,GACZ,gBAAgB,GAChB,iBAAiB,GACjB,cAAc,GACd,gBAAgB,GAChB,iBAAiB,CAAC;AAEtB,KAAK,yBAAyB,GAC5B,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAExE,KAAK,yBAAyB,GAAG,SAAS,GAAG,OAAO,CAAC;AACrD,KAAK,uBAAuB,GAAG,OAAO,GAAG,OAAO,GAAG,aAAa,CAAC;AAEjE,KAAK,4BAA4B,GAC/B,OAAO,GAAG,cAAc,GAAG,oBAAoB,GAAG,aAAa,GAAG,gBAAgB,CAAC;AAErF,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,EAAE,yBAAyB,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,yBAAyB,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,uBAAuB,EAAE,CAAC;IACnC,cAAc,EAAE,4BAA4B,CAAC;CAC9C,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"session-failure-extension.d.ts","sourceRoot":"","sources":["../src/session-failure-extension.ts"],"names":[],"mappings":"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"}
|
|
@@ -81,6 +81,7 @@ export function sessionFailureMeta(failure) {
|
|
|
81
81
|
severity: failure.severity,
|
|
82
82
|
title: failure.title,
|
|
83
83
|
...(failure.details ? { details: failure.details } : {}),
|
|
84
|
+
...(failure.reason ? { reason: failure.reason } : {}),
|
|
84
85
|
actions: failure.actions,
|
|
85
86
|
});
|
|
86
87
|
}
|
|
@@ -200,6 +201,26 @@ export class SessionFailureController {
|
|
|
200
201
|
return false;
|
|
201
202
|
}
|
|
202
203
|
}
|
|
204
|
+
/** Whether a session-scoped error of `kind` is active. A turn-scoped
|
|
205
|
+
* warning of the same kind (an `api_retry` notice) does not count: it
|
|
206
|
+
* reports a retry in progress, not the state the error would publish.
|
|
207
|
+
*
|
|
208
|
+
* `reason` refines `kind`, so the active record must carry the same one,
|
|
209
|
+
* and an omitted `reason` matches only a record with no reason. A plain
|
|
210
|
+
* sign-out and the `--hide-claude-auth` subscription refusal are both
|
|
211
|
+
* `auth_required`, but they tell the user different things. Neither may
|
|
212
|
+
* suppress the other. */
|
|
213
|
+
hasActiveSessionError(kind, reason) {
|
|
214
|
+
for (const failure of this.state.active.values()) {
|
|
215
|
+
if (failure.kind === kind &&
|
|
216
|
+
failure.severity === "error" &&
|
|
217
|
+
failure.turnId === undefined &&
|
|
218
|
+
failure.reason === reason) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
203
224
|
recordActive(failure) {
|
|
204
225
|
this.state.revisions.set(failure.id, failure.revision);
|
|
205
226
|
this.state.active.set(failure.id, failure);
|
|
@@ -255,6 +276,7 @@ export class SessionFailureController {
|
|
|
255
276
|
severity: failureOptions.severity ?? "error",
|
|
256
277
|
title,
|
|
257
278
|
...(failureOptions.details ? { details: failureOptions.details } : {}),
|
|
279
|
+
...(failureOptions.reason ? { reason: failureOptions.reason } : {}),
|
|
258
280
|
actions: failureOptions.severity === "warning" ? [] : policy.actions,
|
|
259
281
|
recoveryPolicy: sessionFailureRecoveryPolicy(kind, failureOptions.turnId && !failureOptions.sessionScoped ? failureOptions.turnId : undefined),
|
|
260
282
|
...(failureOptions.turnId && !failureOptions.sessionScoped
|
package/package.json
CHANGED