如何在 VPS 上自托管 OpenTag 处理 @agent 提及
在 VPS 部署 OpenTag,让 Slack 和 GitHub 的 @提及触发 coding agent。本文涵盖 TLS 入口、Webhook 签名校验、令牌权限范围及安全默认配置。
提及 agent 时,OpenTag 的处理方式
OpenTag 会将 Slack 线程或 GitHub issue 中的 @提及转换为在您拥有的机器上运行的 coding agent。有人在 issue 中评论 @opentag investigate this。监听器接收平台事件并验证其签名,将此次提及匹配到已绑定的项目,针对本地检出版本启动 coding agent,然后将结果发布回同一线程。
该项目采用 MIT 许可证,代码位于 amplifthq/opentag。截至 2026 年 8 月,最新的标记版本是 v0.9.0,于 28 July 2026 发布,以 npm package 形式提供。官方没有 container image,因此需要固定的是 npm 版本。下面的每条命令都会固定该版本。
由于 GitHub 集成,这会成为 VPS 项目,而不是笔记本电脑项目。GitHub 通过向您注册的 URL 发起 HTTP 请求来发送 repository 事件,因此该 URL 明天仍必须在同一地址响应。
四个组成部分
事件接收器接收平台事件,每个平台都有自己的事件接收器。GitHub 事件接收器是一个 HTTP 端点,监听 3050 端口,路径为 /github/webhooks。Slack Events API 事件接收器监听 3040 端口,路径为 /slack/events。Slack 也支持 Socket Mode。在此模式下,应用会建立出站 WebSocket 连接,因此完全不需要入站端口。
调度器负责协调各组件。它默认监听 3030 端口,将运行状态保存到 OPENTAG_DATABASE_PATH 指定的本地数据库文件中,并为每次运行记录审计轨迹。任何外部设备都不应访问此端口。
运行器是本地守护进程。它会轮询任务、认领运行任务、为任务持有租约,并在任务运行期间默认每 15 秒发送一次心跳。如果某个已认领任务的项目目标不存在,或不在运行器自身配置中的允许列表内,运行器会拒绝该任务。这项检查可防止 GitHub 事件将代理指向未绑定的代码仓库。
执行器就是编码代理本身。OpenTag 通过 ACP(agent client protocol)启动执行器。ACP 是一种通过标准输入和标准输出通信的 JSON-RPC 协议,因此代理会作为子进程运行在 OpenTag 提供的工作目录中。内置名称包括 echo、codex、claude-code、cursor、opencode、hermes 和 openclaw。请从 echo 开始使用。示例配置随附的就是这个执行器,它可以在模型修改代码前验证整个流程正常工作。
顺序始终不变:平台事件、签名检查、运行记录、认领任务、代理执行、在线程中回复。
为什么笔记本电脑和隧道还不够
GitHub 设置指南要求您运行 ngrok http 3050,然后将隧道主机名粘贴到仓库 webhook 中。这样只能在最初的十分钟内正常工作。每次进程重启时,免费隧道主机名都会变化;笔记本电脑进入睡眠状态后,该主机名也会失效。GitHub 会保留旧的 payload URL,并持续尝试访问它。因此,webhook 设置中的 Recent Deliveries 选项卡会不断出现失败记录,而讨论串仍然没有任何更新。通常一周都没人注意到,因为一个不执行任何操作的 webhook,看起来和一个没人提及的机器人完全一样。
VPS 可以解决这两个问题。DNS 名称不会变化,因此您只需粘贴一次的 payload URL 始终有效。服务器不会进入睡眠状态,因此即使在 02:00 收到评论,也能得到回复。请先正确配置服务器:新 VPS 上的前十分钟介绍了本指南所假设的登录用户和防火墙配置。
Slack 是例外。在 Socket Mode 下,Slack 会主动向外建立连接,不需要公网 URL,因此仅部署 Slack 时可以保持服务不对外开放。GitHub 没有等效功能。仓库 webhook 使用入站 HTTP,这意味着需要公网端点,也意味着需要 TLS(传输层安全)和签名校验。
在 Ubuntu 上从固定版本自托管 OpenTag
OpenTag v0.9.0 要求 Node.js 22 或更高版本。Ubuntu 24.04 自带的软件仓库中是 Node 18,因此请从 NodeSource 安装。
curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs
node -vnode -v 必须输出 v22 或更高版本。在 Node 20 上,安装过程会输出 EBADENGINE 警告,CLI 启动后可能无法运行。
为服务创建专用账户。代理以该用户的权限运行,因此不应使用您的登录账户,也不应使用 root。VPS 上的最小权限用户介绍了为什么值得执行这一额外步骤。
sudo adduser --disabled-password --gecos "" opentag
sudo loginctl enable-linger opentag
sudo npm install -g @opentag/cli@0.9.0
command -v opentagcommand -v opentag 应输出类似 /usr/bin/opentag 的路径。linger 设置在 Linux 上很重要:OpenTag 通过 systemd 安装后台服务,而未启用 linger 的用户服务会在 SSH 会话关闭时立即停止。
以该用户运行设置程序。
sudo -iu opentag opentag setup设置程序会询问六项内容:CLI 语言、本地监听地址、编码代理、本地工作项目、要保存的平台凭据,以及运行方式。将监听地址保留为 127.0.0.1,因为 nginx 会终止 TLS 并将请求转发到该地址,因此监听器无需从外部访问。对于 GitHub,设置程序还会询问 owner/repo 格式的仓库、是否可以创建拉取请求、webhook 端口(默认为 3050)以及令牌。最后选择后台服务模式。如果已有配置,并希望在不提示的情况下安装服务,请使用 opentag setup --service。
配置文件位于 /home/opentag/.config/opentag/config.json,运行时状态位于 /home/opentag/.local/state/opentag。设置程序写入文件后,建议手动检查这些键。
{
"runnerId": "runner_local",
"dispatcherUrl": "http://localhost:3030",
"runnerToken": "...",
"approvalMode": "ask",
"repositories": []
}优先使用 runnerToken,即作用域限定于 runner 的 bearer token,而不是较旧的共享 pairingToken。除非将凭据替换为 secret reference,否则配置文件会以明文保存凭据;secret reference 会在启动时从环境变量或磁盘文件读取值。无论采用哪种方式,该文件都是此主机上最敏感的文件:权限设为 600,由 opentag 所有,并且绝不能放在 git 仓库中。更广泛的说明见避免将密钥交给 AI 代理。
在对外提供服务前检查安装结果。
sudo -iu opentag opentag doctor
sudo -iu opentag opentag statusopentag doctor 会检查 dispatcher、绑定、检出内容和执行器。opentag status 会输出配置和运行时状态;已有运行记录后,还可以将检查范围限定为单次运行。在将平台指向此主机前,先修复 doctor 报告的所有问题。
在前端配置 TLS,只开放两个路径
nginx 终止 TLS,并只转发两个路径。其他请求全部返回 404,因此扫描器即使发现该主机,也无法得知后端运行的内容。
在 /etc/nginx/sites-available/opentag 创建一个普通的 80 端口 server block,并加入下面两个 location,然后让 Certbot 添加 TLS 部分。
sudo apt install -y nginx certbot python3-certbot-nginx
sudo ln -s /etc/nginx/sites-available/opentag /etc/nginx/sites-enabled/opentag
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d opentag.example.comnginx -t 会输出 syntax is ok 和 test is successful。它是防止拼写错误导致站点无法通过 reload 恢复的唯一保障。Ubuntu 24.04 上使用 nginx 配置 Certbot介绍证书续期,以及 ACME(自动证书管理环境)质询失败的各种原因。完成后的 block 如下。
server {
listen 443 ssl;
server_name opentag.example.com;
ssl_certificate /etc/letsencrypt/live/opentag.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/opentag.example.com/privkey.pem;
client_max_body_size 2m;
location = /github/webhooks {
proxy_pass http://127.0.0.1:3050;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location = /slack/events {
proxy_pass http://127.0.0.1:3040;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location / {
return 404;
}
}location = /github/webhooks 中的 = 是精确匹配;proxy_pass 后面不带任何内容时,会原样传递原始 URI。删除 = 后,/github/webhooks/ 下的每个路径也会被转发,暴露面超出了该监听器的实际需要。
防火墙规则保持最小化。
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status不会开放 3030、3040 和 3050 端口。请确认这些端口绑定到 loopback,而不是绑定到所有网络接口。
sudo ss -tlnp每一行 OpenTag 都应类似 127.0.0.1:3030。如果某行写成 0.0.0.0:3050,表示监听器正向整个互联网提供服务,目前只是由 ufw 阻止访问;防火墙中的一个错误就可能使 agent trigger 处于开放状态。ufw 防火墙基础说明了该默认拒绝规则的实际作用。
通过两项检查即可验证前端入口。curl -I https://opentag.example.com/ 会从 nginx 返回 404,这表明证书有效,并且 catch-all 已关闭。向 /slack/events 或 /github/webhooks 发送不带签名的请求时,绝不能返回 200。
验证每个签名,因为 URL 是公开的
任何人都可以找到 payload URL。它可能出现在仓库设置、浏览器历史记录,或粘贴到工单中的屏幕截图里。签名是区分真实 GitHub 投递与手动构造请求的唯一依据。
GitHub 使用 webhook secret 为每次投递签名,并将结果放在 x-hub-signature-256 请求头中。OpenTag 会使用 platforms.github.webhookSecret 验证该请求头。项目加固说明直接规定:不要在 /github/webhooks 上接受未签名的源事件。Slack 使用 SLACK_SIGNING_SECRET 为每个请求签名,并包含时间戳,因此捕获的请求体无法在数小时后重放。
跳过验证并不是小风险。未验证的端点会接受手动构造的 issue_comment payload,其中包含 @opentag;随后,OpenTag 会使用您的令牌,在您的 checkout 中,根据陌生人的指令运行编码代理。回复会发送到伪造 payload 指定的任意线程。
OpenTag 还增加了两层保护。系统会按投递 ID 跟踪源投递,因此重新投递同一事件不会启动第二次运行。Runner 调用接受幂等键,因此重放某个调用只会返回成功,不会再次追加审计事件。
速率限制可配置,并且应保持启用。OPENTAG_RATE_LIMIT_WINDOW_MS 和 OPENTAG_RATE_LIMIT_MAX_REQUESTS 限制请求速率,OPENTAG_MAX_REQUEST_BODY_BYTES 限制请求体大小,超大 payload 会被 413 request_body_too_large 拒绝。OPENTAG_RATE_LIMIT_DISABLED=true 用于本地开发,不应出现在公网服务器上。同一份说明还规定:公开中继 URL 必须使用 HTTPS,CLI 仅允许对 localhost 使用纯 HTTP。
机器人实际需要哪些令牌权限?
在 GitHub 上,OpenTag 使用细粒度个人访问令牌,而不是 GitHub App。文档说明,App 方案已列入计划,目前不是 CLI 的默认配置。这会带来一个容易被忽略的后果:机器人会以令牌创建者的身份发表评论。请在一个您愿意看到其身份出现在每条分类回复中的账户下创建令牌。
请按照设置指南的要求,将权限范围限制到最低。选择 Only select repositories,然后选择一个仓库。授予 Issues: Read and write 和 Pull requests: Read and write。这些权限足以读取提及并在线程中回复。
注意缺少的权限:代码写入权限。除非将 preparePullRequestBranch 设置为 true,否则 OpenTag 不会推送分支;此外还有单独的 githubApplyToken,用于确保写入代码的令牌与发表评论的令牌相互分离。请保持两者分离,并在读写评论流程稳定运行几周前,不要启用写入令牌。
应避免的配置是:在 All repositories 范围内授予 Contents: Read and write 的令牌。现在,任何可以在这些仓库中发表评论的人,都可以操控一个拥有提交权限的代理,而审计记录会显示该操作由令牌所有者执行。请一次只扩大到一个仓库,并在代理证明其可靠性后再扩大范围。
在 Slack 上,机器人的权限范围是 app_mentions:read、chat:write、reactions:write 和 channels:history。私有频道还需要 groups:history,并订阅 message.groups 事件。Socket Mode 需要一个带有 connections:write 的应用级令牌,即以 xapp- 开头的令牌。channels:history 用于读取机器人已加入的公共频道中的消息历史,因此请将机器人添加到需要它工作的频道,而不要添加到所有频道。
端到端处理一个问题
先配置 webhook。在仓库中依次打开 Settings、Webhooks,然后选择 Add webhook。Payload URL 为 https://opentag.example.com/github/webhooks,内容类型为 application/json,密钥使用安装过程生成的密钥。仅订阅 Issue comments 和 Pull request review comments,不要订阅其他事件。
保存后,GitHub 会立即发送一次 ping 投递。打开 Recent Deliveries,确认请求至少到达了服务器。这里出现 502,表示 nginx 无法连接监听端口。这是本地问题,不是 GitHub 的问题。
现在进行实际测试。打开一个描述 bug 的 issue,并发表评论:
@opentag triage this. Reproduce the report against the current main branch, then reply with the file and function most likely responsible, plus the test you would write first.预期结果如下。Recent Deliveries 记录 issue_comment 投递,并返回 2xx 响应。调度器记录一次运行。运行器认领该运行并开始发送心跳。执行器打开 checkout 并执行任务。答案以评论形式出现在同一个 issue 线程中。sudo -iu opentag opentag status 会显示正在执行的运行,因此你可以直接监控,而不必猜测状态。
在首次实际运行前,将 approvalMode 设置为 ask。在 ask 模式下,运行会暂停,并等待人员确认后才执行任何会改变状态的操作。系统还提供 auto 和 autonomous 模式。稍后可以在已经阅读一个月运行记录的仓库中使用这些模式。
在 Slack 端,同一次运行会先在频道中发送 /bind owner/repo,然后发送一条提及消息。机器人还会响应 /help、/status、/doctor、/stop 和 /unbind confirm。使用 OPENTAG_SLACK_BINDING_ADMIN_USER_IDS 限制可以修改绑定的用户。该配置是以逗号分隔的 Slack 用户 ID 列表,因为绑定表示从公共频道到服务器上某个 checkout 的映射关系。
Triage 适合作为第一个流程,因为它只读取数据,不写入数据,而且结果容易评估。下一步是 Review。此时代理会针对 diff 发表评论,而不是针对 issue:自托管的 pull request review agent 采用的就是同一架构,只是目标改为 pull request。如果希望代理在运行期间访问你自己的系统,可以使用 VPS 上的 MCP 服务器。Web search 是 Triage 经常需要的另一项能力,而 将代理连接到自己的 SearXNG 实例 可以让这些查询留在你管理的硬件上,但代价是陌生人的文本又多了一条进入代理的通道。
当代理在所有人面前出错时会怎样?
它会出错。问题在于,这种错误的代价是什么。
公开 issue 中的错误回复,会以团队能够识别的名称发布为一条评论;发布后,GitHub 会立即将邮件发送给所有订阅者。删除评论无法撤回邮件。Slack 通知也是如此。应按答案可能公开出错来规划,而不是按答案只会私下正确来规划。
以下 4 项选择可以限制损失,其重要性超过您编写的任何提示词。
- 在
ask模式下运行,让代理提出方案,由人员批准;这样错误计划的代价只是点击一次。 - 保持
preparePullRequestBranch的默认值 false,这样错误运行的最坏结果是发布错误评论,而不是修改错误分支。 - 初始阶段只绑定一个仓库和一个频道。运行器会拒绝项目目标不在本地允许列表中的运行,因此未绑定的仓库无法将代理拉入其中。
- 将评论令牌与任何应用令牌分开,这样撤销写入权限不会同时中断问题分流。
对于方向错误的运行,Slack 提供 /stop 命令。每次运行还会留下审计记录,其中包含启动运行的提及内容以及代理执行的操作。之后可以通过这条记录确定它在哪一步出错。
社交层面的安排与配置同样重要。将机器人放在一个人们预期会有自动化程序参与、并知道它可能出错的频道中。在一个有 40 人的频道里,如果大家都以为回复经过人工审核,那么一条语气自信但错误的答案所造成的代价,会超过它节省的问题分流成本。在频道描述中写明机器人由谁负责,以及由谁检查其输出。
备份、升级和版本固定
两个路径包含全部数据:/home/opentag/.config/opentag/config.json 和 /home/opentag/.local/state/opentag。前者包含凭据,后者包含运行历史和数据库文件。请将二者都以 mode 600 备份,并存放在服务器之外。丢失这些数据意味着需要重新创建令牌和绑定关系,而不是重建服务器。
升级包括提升版本和重启。
sudo npm install -g @opentag/cli@0.9.0
sudo -iu opentag opentag service stop
sudo -iu opentag opentag service start
sudo -iu opentag opentag doctor固定版本,不要跟踪 @latest。该软件会使用有效令牌在您的代码仓库中运行编码代理,因此,夜间发布的新版本会在未经审核的情况下改变这一运行环境。该安全策略不会回移植修复,修复只会进入最新版本。因此,固定版本意味着先阅读变更日志,再有计划地升级。这不意味着永远停留在 v0.9.0。到 July 2026 为止的版本历史显示,每月会发布多个版本,因此每次升级前阅读发行说明是合理做法。
FAQ
运行 OpenTag 必须使用 VPS,还是笔记本电脑就够了?
仅运行 Slack 时,笔记本电脑就够了,因为 Socket Mode 会建立出站 WebSocket 连接,不需要入站端口。GitHub 则不同。仓库 Webhook 会通过入站 HTTP 请求发送到你注册的一次性 URL,因此该地址必须保持不变,并且在你休息时也必须能够响应。免费账户提供的隧道主机每次重启都会更换地址,而 GitHub 会继续向旧地址发送请求。这样会在仓库的 Recent Deliveries 选项卡中显示失败记录,线程中也不会出现任何内容。使用具有固定 DNS 名称和证书的 VPS 可以同时解决这两个问题。
OpenTag 需要哪些 GitHub 权限?
使用一个范围受限的 fine-grained personal access token,并将仓库范围设为 Only select repositories,同时授予 Issues: Read and write 和 Pull requests: Read and write。这些权限足以读取提及并在线程中回复。除非将 preparePullRequestBranch 设置为 true,使 OpenTag 推送分支,否则不需要代码写入权限。此外还有单独的 githubApplyToken,用于将可写入代码的令牌与发表评论的令牌分开。不要使用对所有仓库有效且具有 contents write 权限的令牌,因为任何能够在这些仓库中发表评论的人,都可能控制一个能够提交代码的代理。
如何停止正在出错的运行?
Slack 提供了专门用于此目的的 /stop 命令。在服务器上,opentag status 会显示当前正在运行的内容,opentag service stop 会停止 daemon;这会结束整个流水线,而不是只停止某次运行。若要避免使用这两个命令,请将 approvalMode 设置为 ask,让运行在进行任何更改前暂停并等待人员确认;同时保持 preparePullRequestBranch 为 false,使出错的运行发布评论,而不是创建分支。
为什么我的 Webhook 返回 502,但线程中没有任何内容?
502 由 nginx 返回,而不是由 OpenTag 返回。这表示代理无法连接到监听程序。/var/log/nginx/error.log 会显示 connect() failed (111: Connection refused) while connecting to upstream。监听程序可能已停止,也可能使用了与 proxy_pass 行指定的不同端口。运行 sudo ss -tlnp,确认 GitHub 使用 127.0.0.1:3050、Slack 使用 127.0.0.1:3040,并确认对应端口上有程序正在监听;然后运行 opentag doctor 查看绑定和执行器。