diff --git a/mateclaw-server/src/main/java/vip/mate/acp/service/AcpRuntimeSupport.java b/mateclaw-server/src/main/java/vip/mate/acp/service/AcpRuntimeSupport.java index b91aa096..53a237c6 100644 --- a/mateclaw-server/src/main/java/vip/mate/acp/service/AcpRuntimeSupport.java +++ b/mateclaw-server/src/main/java/vip/mate/acp/service/AcpRuntimeSupport.java @@ -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). + * + *

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. "); diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index b9a6cdef..ec2d22e2 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -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: { diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index fea23f43..9736067b 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -1168,7 +1168,7 @@ export default { openai: 'OpenAI API key(sk-…)。申请: 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 key(sk-ant-…)。或者改用 qwen-code / opencode 复用各自 CLI 的本地登录态。', }, }, modal: {