大家好,我是张晋涛。
有一段时间没有更新长文了,这几天假期正好把博客重新整理了下,心情都变好了,正好用这篇来恢复下更新。
熟悉我的小伙伴都知道,对于当前的 Coding Agent 工具,我主要推荐的就 4 个,Amp,Claude Code,Codex 和 Warp。
https://x.com/zhangjintao9020/status/2019571842009428399?s=20
这篇主要就和 Amp 有关,而且其中的涉及到的代码变更也基本上都是通过 Amp 完成的。
背景
先简单说下 ACP 以及我做的项目 amp-acp。
ACP(Agent Client Protocol)是一种标准化的协议,用于连接 AI Agent 和客户端编辑器(如 Zed)。 在 ACP 发布之后,我觉得这个协议蛮有用的,并且众多 coding agent 工具都快速的进行了跟进,唯独 Amp 团队官方并没有打算实现 ACP 的支持。 基于对 ACP 的兴趣以及对于 Amp 的喜欢,所以我发布了这个 amp-acp 项目。 amp-acp 是一个 ACP adapter,让用户可以在 Zed 中使用 Amp 这个 Coding Agent。
amp-acp 我一开始只是对 Amp CLI 的封装,但是我发现这样用户在使用的时候安装步骤比较繁琐,所以我将它重构为基于 Amp SDK 的方式进行构建,但是 Amp SDK 并没有那么完善,尤其是它也不支持 login 操作。
所以在 v0.6.1 之前,amp-acp 的认证方式非常原始:要求用户手动在 Zed 的 settings.json 中配置 AMP_API_KEY 环境变量,或者先通过 Amp CLI 执行 amp login。我对这个流程并不满意。
因此在 v0.7.0 中,我实现了完整的认证流程,让用户可以直接在 Zed 的 Agent Panel 中完成 API Key 的配置。
ACP 协议中的认证机制
协议层的设计
ACP 在 initialize 响应中定义了 authMethods 字段,用于声明 Agent Server 支持的认证方式:
{
"protocolVersion": 1,
"authMethods": [
{
"id": "setup",
"name": "Amp API Key Setup",
"description": "Run interactive setup to configure your Amp API key",
"_meta": { ... }
}
]
}
协议本身只定义了 id、name、description 和一个通用的 _meta 扩展字段。认证的具体实现机制并不是协议标准的一部分,而是通过 _meta 扩展来实现的。
terminal-auth:Zed 的扩展认证机制
Zed 在 ACP 的 _meta 字段中定义了一个实验性的 terminal-auth 扩展,用于支持基于终端的交互式认证。客户端在 initialize 请求中通过 _meta 声明支持此能力:
// Zed 客户端初始化时声明支持 terminal-auth
.meta(acp::Meta::from_iter([
("terminal_output".into(), true.into()),
("terminal-auth".into(), true.into()),
]))
当 Agent Server 在 authMethods 的 _meta 中返回 terminal-auth 配置时,Zed 会提取其中的 command、args 和 env,在 Zed 内置终端中打开一个新的 terminal tab 来执行认证命令:
// Agent Server 返回的 authMethods
authMethods: [
{
id: 'setup',
name: 'Amp API Key Setup',
description: 'Run interactive setup to configure your Amp API key',
_meta: {
'terminal-auth': {
command: getTerminalAuthCommand(), // 可执行文件路径
args: ['--setup'], // 命令参数
label: 'Amp API Key Setup', // 终端 tab 标题
},
},
},
],
认证流程的完整生命周期
下面是整个认证流程从触发到完成的详细过程:
1. 初始化阶段
Zed 启动 ACP Server 进程后,发送 initialize 请求。Server 返回 authMethods 列表,Zed 将其存储起来。
2. 触发认证
认证可以在两个时机被触发:
- 主动触发:用户在 Agent Panel 中使用
/login命令 - 被动触发:当
session/prompt请求返回authRequired错误(JSON-RPC error code-32000)时,Zed 自动弹出认证 UI
3. 执行认证
Zed 的处理流程(从其 Rust 源码中可以看到):
用户点击 "Amp API Key Setup" 按钮
→ Zed 检查 authMethod 的 _meta 中是否有 terminal-auth
→ 提取 command / args / env
→ 通过 SpawnInTerminal 在内置终端中执行命令
→ 等待进程退出并检查退出码
→ 退出码为 0 → 认证成功,reset 连接
→ 退出码非 0 → 认证失败,显示错误
4. 认证后重连
认证成功后,Zed 调用 this.reset(window, cx) 重置整个 Agent 视图。这意味着会重新走一遍 initialize → authenticate 的流程。
一些不得不提的坑
经验 1:terminal-auth 的退出码决定成功与否
Zed 对于 terminal-auth 类型的认证,会等待进程退出并检查退出码:
// Zed 源码 crates/agent_ui/src/acp/thread_view.rs
match exit_status {
Some(status) if status.success() => Ok(()),
Some(status) => Err(anyhow!(
"Login command failed with exit code: {:?}",
status.code()
)),
None => Err(anyhow!("Login command terminated without exit status")),
}
因此,--setup 命令必须:
- 成功时以
process.exit(0)退出 - 失败时以非零退出码退出
- 不能是一个持续运行的进程
经验 2:label 是 terminal-auth 的必填字段
这个点是这篇文章中我觉得最值得说的地方。。。
虽然 terminal-auth 不是 ACP 协议的正式规范,没有文档说明哪些字段是必填的,但从 Zed 源码可以看到一个容易踩坑的细节:
// Zed 源码 crates/agent_ui/src/acp/thread_view.rs
if let (Some(command), Some(label)) = (
terminal_auth.get("command").and_then(|v| v.as_str()),
terminal_auth.get("label").and_then(|v| v.as_str()),
) {
// 只有 command 和 label 同时存在才进入 terminal-auth 逻辑
...
}
Zed 使用 Rust 的 tuple pattern matching,command 和 label 必须同时存在才会进入 terminal-auth 分支。如果你只提供了 command 而缺少 label,整个条件不成立,Zed 会静默跳过 terminal-auth,fallback 到普通的 connection.authenticate() 路径——不会有任何错误提示,认证就是不工作。
所以 terminal-auth 的最小可用配置是:
{
"terminal-auth": {
"command": "/path/to/binary",
"label": "My Auth Setup"
}
}
args 和 env 是可选的,但 label 绝对不能省。
经验 3:Bun 编译二进制的 /$bunfs/ 虚拟路径问题
当使用 bun build --compile 打包成独立二进制文件时,process.argv[1] 会变成一个虚拟路径(如 /$bunfs/root/amp-acp),这个路径在文件系统中并不存在。如果直接用它作为 terminal-auth 的 command,Zed 将无法执行:
export function getTerminalAuthCommand(
argv1: string | undefined = process.argv[1],
execPath: string = process.execPath,
): string {
const resolvedArgv1 = argv1 ? path.resolve(argv1) : '';
// Bun 编译的二进制中,argv[1] 是 /$bunfs/ 开头的虚拟路径
if (!resolvedArgv1 || resolvedArgv1.startsWith('/$bunfs/')) {
return execPath; // 使用实际的可执行文件路径
}
return resolvedArgv1; // 非编译模式,使用脚本路径
}
在开发时用 bun dist/index.js 运行一切正常,但打包成二进制后就会出问题。
经验 4:运行时 Auth 错误检测需要模式匹配
即使初始认证成功了,API Key 也可能在运行过程中失效(过期、被撤销等)。Amp SDK 在这种情况下不会抛出特定的错误类型,而是返回包含错误信息的字符串。因此需要通过模式匹配来检测:
export function isAuthError(message: string): boolean {
const lower = message.toLowerCase();
return lower.includes('invalid or missing api key') ||
lower.includes("run 'amp login'") ||
lower.includes('authentication') ||
lower.includes('unauthorized') ||
lower.includes('no api key found') ||
(lower.includes('api key') && lower.includes('login flow')) ||
(lower.includes('api key') && (lower.includes('missing') || lower.includes('invalid')));
}
检测到 auth 错误后,在 prompt 的执行流程中抛出 RequestError.authRequired(),让 Zed 重新触发认证流程:
// 在 streaming 结果中检测
if (message.type === 'result' && message.is_error) {
if (typeof message.error === 'string' && isAuthError(message.error)) {
throw RequestError.authRequired();
}
}
// 在 catch 块中检测
if (err instanceof Error && isAuthError(err.message)) {
throw RequestError.authRequired();
}
经验 5:authenticate 方法的角色
ACP 协议中的 authenticate 方法在流程中承担的是验证确认的角色,而非执行认证:
async authenticate(_params: AuthenticateRequest): Promise<AuthenticateResponse> {
if (process.env.AMP_API_KEY) {
return {}; // API Key 已配置,认证成功
}
throw RequestError.authRequired(); // 仍未配置,继续要求认证
}
对于 terminal-auth 模式,实际的认证动作(用户输入 API Key)发生在终端进程中。Zed 在终端进程成功退出后,会调用 authenticate 来确认认证状态。
经验 6:凭证存储策略
由于 ACP Server 是一个 stdio 进程,没有持久化的状态,需要自行管理凭证存储:
function getConfigDir(): string {
if (process.platform === 'win32') {
return path.join(
process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming'),
'amp-acp'
);
}
return path.join(
process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config'),
'amp-acp'
);
}
凭证文件以 0o600 权限写入,确保只有当前用户可读。同时在启动时检查:环境变量 AMP_API_KEY 优先 → 凭证文件次之 → 都没有则等待认证流程触发。
经验 7:stdout 是协议通道,日志必须走 stderr
ACP 使用 stdin/stdout 进行 JSON-RPC 通信。--setup 的交互式提示和所有日志都必须输出到 stderr,否则会破坏协议通信:
// 在入口文件最开始就重定向
console.log = console.error;
console.info = console.error;
console.warn = console.error;
console.debug = console.error;
// readline 也要指向 stderr
const rl = readline.createInterface({
input: process.stdin,
output: process.stderr // 不是 stdout!
});
整体流程图
总结
这篇中其他的部分其实都不重要,就是顺手记录下。唯一有价值的地方(也是我踩到坑的地方)就是 label 不能少:Zed 要求 command 和 label 同时存在才启用 terminal-auth,缺少 label 会静默失败。不翻 Zed 的代码还真没想到是这么个逻辑(当然,也不是我自己看的,我把 Zed 的源码路径丢给了 Amp 让它自己来找的)
最近 Amp 团队打算把它的编辑器插件都自动销毁了,如果你在使用 Zed 和 Amp,那么不妨来试试看这个项目:amp-acp
欢迎订阅我的文章公众号【MoeLove】

Comments