fix(acp): detect keychain OAuth hijack on claude-code 'Request not allowed'

This commit is contained in:
matevip 2026-05-01 20:15:01 +08:00
parent 3d0797787b
commit e5dff7acb1
3 changed files with 43 additions and 3 deletions

View File

@ -84,6 +84,22 @@ public class AcpRuntimeSupport {
* and the cost of a false positive (a slightly more verbose error
* banner) is much smaller than a false negative (user staring at a
* raw JSON-RPC blob).
*
* <p>Special case: a claude-code endpoint returning {@code 403
* "Request not allowed"} is almost always the keychain-hijack
* scenario rather than a wrong API key. The third-party
* {@code @zed-industries/claude-agent-acp} package wraps
* {@code @anthropic-ai/claude-agent-sdk}, whose auth dispatcher
* checks the macOS keychain ({@code Claude Code-credentials}) /
* {@code ~/.claude/credentials.json} BEFORE the
* {@code ANTHROPIC_API_KEY} env var. So a host that's done
* {@code claude login} silently shadows whatever API key the user
* configured in the endpoint env, and Anthropic's API rejects the
* subscription OAuth token (first-party-only) with the very
* specific {@code "Request not allowed"} error string. We detect
* that exact combination and surface the keychain-clearing remedy
* instead of the generic "set ANTHROPIC_API_KEY" hint, which
* doesn't apply here.
*/
public String translateAuthError(AcpEndpointEntity endpoint, String originalMessage) {
if (originalMessage == null) return null;
@ -99,8 +115,32 @@ public class AcpRuntimeSupport {
|| lower.contains("authenticate");
if (!looksLikeAuth) return null;
String envVar = expectedAuthEnvVar(endpoint);
String name = endpoint != null && endpoint.getName() != null ? endpoint.getName() : "(unknown)";
String slug = lower(name);
String command = endpoint != null ? lower(endpoint.getCommand()) : "";
// Keychain-hijack detection must come before the generic env-
// missing branch because both would superficially match.
boolean keychainHijack = lower.contains("request not allowed")
&& (slug.contains("claude") || command.contains("claude-agent-acp"));
if (keychainHijack) {
StringBuilder sb = new StringBuilder();
sb.append("ACP endpoint '").append(name).append("' upstream auth failed with ");
sb.append("'Request not allowed' — almost always means the host CLI's OAuth ");
sb.append("credentials are hijacking the SDK auth path. ");
sb.append("The Claude Agent SDK reads ~/.claude/ / macOS keychain BEFORE the ");
sb.append("ANTHROPIC_API_KEY env var, so the API key you configured here is ");
sb.append("never sent — Anthropic rejects the subscription OAuth token because ");
sb.append("third-party processes aren't allowed to use it. ");
sb.append("To fix: ");
sb.append("(macOS) run `claude logout`, or `security delete-generic-password ");
sb.append("-s \"Claude Code-credentials\"`; ");
sb.append("(Linux / Windows) delete ~/.claude/credentials.json. ");
sb.append("Then click Test connection again. Original: ").append(originalMessage);
return sb.toString();
}
String envVar = expectedAuthEnvVar(endpoint);
StringBuilder sb = new StringBuilder();
sb.append("ACP endpoint '").append(name).append("' upstream auth failed. ");
sb.append("Most likely the endpoint env has no API key. ");

View File

@ -1168,7 +1168,7 @@ export default {
openai: 'OpenAI API key (sk-…). Get one at https://platform.openai.com/api-keys',
dashscope: 'Aliyun DashScope key — same value as MateClaw .env DASHSCOPE_API_KEY',
google: 'Google AI Studio API key. Get one at https://aistudio.google.com/apikey',
claudeOauth: 'Heads up: claude-code OAuth login (the one stored in ~/.claude/) does NOT work here. The third-party Zed wrapper uses the public Anthropic API, which only accepts a real API key. Get one at https://console.anthropic.com/settings/keys, or switch to qwen-code / opencode if you want to reuse a CLI login.',
claudeOauth: 'Heads up: claude-code OAuth login (the one stored in ~/.claude/) does NOT work here, and worse — the Claude Agent SDK reads it BEFORE this env var, silently shadowing your API key. If you have ever run `claude login` on this host, clear the keychain first: (macOS) `claude logout` or `security delete-generic-password -s "Claude Code-credentials"`; (Linux/Windows) delete ~/.claude/credentials.json. Then put a real API key (sk-ant-…) above. Or switch to qwen-code / opencode to reuse a CLI login.',
},
},
modal: {

View File

@ -1168,7 +1168,7 @@ export default {
openai: 'OpenAI API keysk-…)。申请: https://platform.openai.com/api-keys',
dashscope: '阿里云 DashScope key — 跟 MateClaw .env 里的 DASHSCOPE_API_KEY 同一个值',
google: 'Google AI Studio API key。申请: https://aistudio.google.com/apikey',
claudeOauth: '注意: claude-code 的 OAuth 登录(~/.claude/ 里那个)在这里不能用。第三方 Zed wrapper 直接调 Anthropic 公开 API只认真正的 API key。请到 https://console.anthropic.com/settings/keys 申请;或改用 qwen-code / opencode 复用各自 CLI 的本地登录态。',
claudeOauth: '注意: claude-code 的 OAuth 登录(~/.claude/ 里那个)在这里不能用——更糟的是 Claude Agent SDK 会"优先读 OAuth、再读 API key",导致你下面填的 key 被静默忽略。如果你这台机器上跑过 `claude login`,先清钥匙串: (macOS) `claude logout` 或 `security delete-generic-password -s "Claude Code-credentials"`(Linux/Windows) 删 ~/.claude/credentials.json。然后再回来填真正的 API keysk-ant-…)。或者改用 qwen-code / opencode 复用各自 CLI 的本地登录态。',
},
},
modal: {