mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +08:00
feat(acp): visual env editor with per-endpoint key suggestions
This commit is contained in:
parent
5e6764ecf1
commit
888200e1cd
@ -84,22 +84,6 @@ public class AcpRuntimeSupport {
|
|||||||
* and the cost of a false positive (a slightly more verbose error
|
* and the cost of a false positive (a slightly more verbose error
|
||||||
* banner) is much smaller than a false negative (user staring at a
|
* banner) is much smaller than a false negative (user staring at a
|
||||||
* raw JSON-RPC blob).
|
* 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) {
|
public String translateAuthError(AcpEndpointEntity endpoint, String originalMessage) {
|
||||||
if (originalMessage == null) return null;
|
if (originalMessage == null) return null;
|
||||||
@ -115,32 +99,8 @@ public class AcpRuntimeSupport {
|
|||||||
|| lower.contains("authenticate");
|
|| lower.contains("authenticate");
|
||||||
if (!looksLikeAuth) return null;
|
if (!looksLikeAuth) return null;
|
||||||
|
|
||||||
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);
|
String envVar = expectedAuthEnvVar(endpoint);
|
||||||
|
String name = endpoint != null && endpoint.getName() != null ? endpoint.getName() : "(unknown)";
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("ACP endpoint '").append(name).append("' upstream auth failed. ");
|
sb.append("ACP endpoint '").append(name).append("' upstream auth failed. ");
|
||||||
sb.append("Most likely the endpoint env has no API key. ");
|
sb.append("Most likely the endpoint env has no API key. ");
|
||||||
|
|||||||
@ -228,41 +228,25 @@ public class AgentBindingService {
|
|||||||
* {@link #getEffectiveToolNames} allowlist completely.
|
* {@link #getEffectiveToolNames} allowlist completely.
|
||||||
*/
|
*/
|
||||||
private static final Set<String> SYSTEM_LEVEL_TOOLS = Set.of(
|
private static final Set<String> SYSTEM_LEVEL_TOOLS = Set.of(
|
||||||
// Structured memory primitives — used by every agent regardless
|
// Memory write/read primitives — every agent needs these
|
||||||
// of skill bindings, otherwise the self-evolution path collapses
|
// regardless of skill bindings, otherwise the self-evolution
|
||||||
// (§11.3 / §11.4).
|
// path collapses (§11.3 / §11.4).
|
||||||
"record_lesson",
|
"record_lesson",
|
||||||
"remember",
|
"remember",
|
||||||
"remember_structured",
|
"remember_structured",
|
||||||
"recall_structured",
|
"recall_structured",
|
||||||
"forget_structured",
|
"forget_structured",
|
||||||
// Workspace memory file CRUD (PROFILE.md / MEMORY.md / SOUL.md /
|
// Workspace memory file CRUD (PROFILE.md / MEMORY.md / SOUL.md)
|
||||||
// memory/YYYY-MM-DD.md). Prior versions whitelisted
|
"read_workspace_file",
|
||||||
// "read_workspace_file" / "write_workspace_file" /
|
"write_workspace_file",
|
||||||
// "list_workspace_files" — those names match no @Tool bean; the
|
"list_workspace_files",
|
||||||
// actual function names carry the "_memory" segment, so the
|
|
||||||
// earlier carve-out was silently dead.
|
|
||||||
"list_workspace_memory_files",
|
|
||||||
"read_workspace_memory_file",
|
|
||||||
"write_workspace_memory_file",
|
|
||||||
"edit_workspace_memory_file",
|
|
||||||
// Skill discovery / dispatch — skills are docs, not callables;
|
// Skill discovery / dispatch — skills are docs, not callables;
|
||||||
// these helpers let the LLM read SKILL.md / run scripts.
|
// these helpers let the LLM read SKILL.md / run scripts.
|
||||||
"readSkillFile",
|
"readSkillFile",
|
||||||
"runSkillScript",
|
"runSkillScript",
|
||||||
"listSkillFiles",
|
// Date/time + delegate — fundamental cross-skill utilities
|
||||||
"listAvailableSkills",
|
"datetime",
|
||||||
// Date / time — prior whitelist had a fictional "datetime"; the
|
"delegate_agent",
|
||||||
// real DateTimeTool exposes three separate methods.
|
|
||||||
"getCurrentDate",
|
|
||||||
"getCurrentDateTime",
|
|
||||||
"getCurrentTime",
|
|
||||||
// Multi-agent delegation — prior whitelist had "delegate_agent",
|
|
||||||
// but DelegateAgentTool's @Tool methods are delegateToAgent /
|
|
||||||
// delegateParallel / listAvailableAgents. Same dead-name bug.
|
|
||||||
"delegateToAgent",
|
|
||||||
"delegateParallel",
|
|
||||||
"listAvailableAgents",
|
|
||||||
// Document / media generation — agent-wide capabilities, never
|
// Document / media generation — agent-wide capabilities, never
|
||||||
// declared inside any skill manifest. Pre-Phase-2b these were
|
// declared inside any skill manifest. Pre-Phase-2b these were
|
||||||
// universally visible; the new gate silently strips them whenever
|
// universally visible; the new gate silently strips them whenever
|
||||||
@ -276,26 +260,7 @@ public class AgentBindingService {
|
|||||||
"renderDocxFromFiles",
|
"renderDocxFromFiles",
|
||||||
"image_generate",
|
"image_generate",
|
||||||
"music_generate",
|
"music_generate",
|
||||||
"video_generate",
|
"video_generate"
|
||||||
// Universal capabilities the global system prompts (SOUL.md /
|
|
||||||
// AGENTS.md / "Web Search Capability" / "File Reading Guidelines")
|
|
||||||
// explicitly tell the LLM exist. Pre-Phase-2b they were globally
|
|
||||||
// available; the new gate silently hid them on any agent with
|
|
||||||
// skills bound, so the prompt promises a tool the registry then
|
|
||||||
// refuses ("Tool not found: search"). Observed 2026-05-01 on the
|
|
||||||
// Code Reviewer agent — the model called search → got
|
|
||||||
// not-found → gave up before ever reaching renderDocx.
|
|
||||||
"search",
|
|
||||||
"browser_use",
|
|
||||||
"read_file",
|
|
||||||
"write_file",
|
|
||||||
"edit_file",
|
|
||||||
"execute_shell_command",
|
|
||||||
"detect_file_type",
|
|
||||||
"extract_document_text",
|
|
||||||
"extract_pdf_text",
|
|
||||||
"extract_docx_text",
|
|
||||||
"readMateClawDoc"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private ResolvedSkill findResolvedSkillById(Long skillId) {
|
private ResolvedSkill findResolvedSkillById(Long skillId) {
|
||||||
|
|||||||
@ -1168,7 +1168,7 @@ export default {
|
|||||||
openai: 'OpenAI API key (sk-…). Get one at https://platform.openai.com/api-keys',
|
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',
|
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',
|
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, 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.',
|
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.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
|
|||||||
@ -277,7 +277,7 @@ export default {
|
|||||||
mcpConnections: 'MCP 连接',
|
mcpConnections: 'MCP 连接',
|
||||||
toolsCatalog: '工具目录',
|
toolsCatalog: '工具目录',
|
||||||
activity: '活动记录',
|
activity: '活动记录',
|
||||||
acpEndpoints: 'ACP 端点',
|
acpEndpoints: 'ACP Endpoints',
|
||||||
settingsGroup: '设置',
|
settingsGroup: '设置',
|
||||||
agents: '智能体',
|
agents: '智能体',
|
||||||
security: '安全',
|
security: '安全',
|
||||||
@ -1121,23 +1121,23 @@ export default {
|
|||||||
},
|
},
|
||||||
acp: {
|
acp: {
|
||||||
kicker: '外部 Agent',
|
kicker: '外部 Agent',
|
||||||
title: 'ACP 端点',
|
title: 'ACP Endpoints',
|
||||||
desc: '通过 stdio 委派给外部编码 Agent(codex / claude-code / opencode / qwen-code)。安装对应 CLI 后开启该入口。',
|
desc: '通过 stdio 委派给外部编码 Agent(codex / claude-code / opencode / qwen-code)。安装对应 CLI 后开启该入口。',
|
||||||
addEndpoint: '新增端点',
|
addEndpoint: '新增 Endpoint',
|
||||||
builtin: '内置',
|
builtin: '内置',
|
||||||
test: '测试',
|
test: '测试',
|
||||||
testing: '测试中…',
|
testing: '测试中…',
|
||||||
statusUnknown: '未测试',
|
statusUnknown: '未测试',
|
||||||
empty: '暂无 ACP 端点。',
|
empty: '暂无 ACP endpoint。',
|
||||||
loadFailed: '加载 ACP 端点列表失败',
|
loadFailed: '加载 ACP endpoint 列表失败',
|
||||||
saveFailed: '保存端点失败',
|
saveFailed: '保存 endpoint 失败',
|
||||||
deleteTitle: '确认删除',
|
deleteTitle: '确认删除',
|
||||||
deleteConfirm: '确认删除 ACP 端点 "{name}"?内置端点不可删除。',
|
deleteConfirm: '确认删除 ACP endpoint "{name}"?内置 endpoint 不可删除。',
|
||||||
deleteFailed: '删除端点失败',
|
deleteFailed: '删除 endpoint 失败',
|
||||||
toggleFailed: '切换端点状态失败',
|
toggleFailed: '切换 endpoint 状态失败',
|
||||||
invalidJson: 'JSON 格式错误',
|
invalidJson: 'JSON 格式错误',
|
||||||
columns: {
|
columns: {
|
||||||
name: '端点',
|
name: 'Endpoint',
|
||||||
command: '命令',
|
command: '命令',
|
||||||
status: '最近一次测试',
|
status: '最近一次测试',
|
||||||
enabled: '启用',
|
enabled: '启用',
|
||||||
@ -1168,12 +1168,12 @@ export default {
|
|||||||
openai: 'OpenAI API key(sk-…)。申请: https://platform.openai.com/api-keys',
|
openai: 'OpenAI API key(sk-…)。申请: https://platform.openai.com/api-keys',
|
||||||
dashscope: '阿里云 DashScope key — 跟 MateClaw .env 里的 DASHSCOPE_API_KEY 同一个值',
|
dashscope: '阿里云 DashScope key — 跟 MateClaw .env 里的 DASHSCOPE_API_KEY 同一个值',
|
||||||
google: 'Google AI Studio API key。申请: https://aistudio.google.com/apikey',
|
google: 'Google AI Studio API key。申请: https://aistudio.google.com/apikey',
|
||||||
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 的本地登录态。',
|
claudeOauth: '注意: claude-code 的 OAuth 登录(~/.claude/ 里那个)在这里不能用。第三方 Zed wrapper 直接调 Anthropic 公开 API,只认真正的 API key。请到 https://console.anthropic.com/settings/keys 申请;或改用 qwen-code / opencode 复用各自 CLI 的本地登录态。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
newTitle: '新增 ACP 端点',
|
newTitle: '新增 ACP Endpoint',
|
||||||
editTitle: '编辑 ACP 端点',
|
editTitle: '编辑 ACP Endpoint',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user