首页
/ gstack /pair-agent 实战:把你的浏览器安全共享给另一个 AI Agent

gstack /pair-agent 实战:把你的浏览器安全共享给另一个 AI Agent

2026-09-06 16:07:23作者:吴年前Myrtle

在 gstack(Garry Tan 的 Claude Code 工作流工具集)中,/pair-agent 技能解决一个具体问题:你在一台机器上跑着 Claude Code + gstack 的 browse 无头浏览器,同时又开着另一个 AI Agent(OpenClaw、Hermes、Codex、Cursor 等),希望后者也能"用你的浏览器上网"。读完本文,你能掌握完整的配对流程(同机直连与 ngrok 远程隧道两条路径)、理解 setup key 换发 session token 的一次性凭据机制,并能在出现 "Tab not owned"、"Rate limit exceeded" 等报错时快速定位原因。

本文主体来自 pair-agent/SKILL.md(由 pair-agent/SKILL.md.tmpl 生成),并结合 browse/src/token-registry.tsbrowse/src/server.tsbrowse/src/cli.ts 等源码补充实现细节。

核心机制:一次性 setup key 换 24 小时 session token

gstack 的 browse 浏览器自带一个本地 HTTP 服务器。/pair-agent 的工作方式是:

  1. 本地 CLI 用 root token 调用 POST /pair,服务器生成一个一次性 setup key
  2. 技能打印一段"指令块"(instruction block),你把它粘给对方 Agent;
  3. 对方 Agent 用 setup key 调用 POST /connect 换发一个 session token,随后用这个 token 打开自己的标签页开始浏览。

关键的生命周期参数(均可在 token-registry.ts 源码中确认):

凭据 前缀 有效期 用途
setup key gsk_setup_ 5 分钟,只能用一次 换发 session token
session token gsk_sess_ 默认 24 小时(expiresSeconds = 86400 驱动浏览命令
root token 由服务器启动时生成 只在本地,可铸造任意子 token
  • setup key 的 5 分钟过期与 usesRemaining: 1 一次性限制见 token-registry.ts#L299-L330createSetupKey):如果 key 泄露,它会在被人滥用前自然失效。
  • session token 的默认参数(scopes 默认 ['read','write']rateLimit = 10 即每秒 10 请求、tabPolicy = 'own-only')见 token-registry.ts#L246-L262createToken)。
  • exchangeSetupKey 还做了幂等处理:同一 key 重复出示且对应 session 尚未执行过任何命令时,返回同一个 session token,专门用于容忍隧道断连后的重试(token-registry.ts#L332-L376)。

隔离是硬性的:每个 Agent 拥有自己的标签页,互相不能操作对方的标签页tabPolicy: 'own-only' 是默认值,尝试操作非自己创建的 tab 会得到 "Tab not owned by your agent" 错误。

前置准备与 SETUP 检查

配对前先定位 browse 二进制(技能中的 $B 变量)。按 pair-agent/SKILL.md 的 SETUP 段执行:

_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
B=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/.claude/skills/gstack/browse/dist/browse" ] && B="$_ROOT/.claude/skills/gstack/browse/dist/browse"
[ -z "$B" ] && B="$HOME/.claude/skills/gstack/browse/dist/browse"
if [ -x "$B" ]; then
  echo "READY: $B"
else
  echo "NEEDS_SETUP"
fi

如果输出 NEEDS_SETUP,需要先询问用户后执行一次性构建(约 10 秒):

  1. 告知用户需要构建并等待确认;
  2. 在技能目录运行 cd <SKILL_DIR> && ./setup
  3. 若机器上没装 bun,SKILL.md 内置了一段带 SHA-256 校验和验证 的 bun 安装脚本(固定版本 1.3.10,校验失败即报错退出),保证安装脚本未被篡改。

Step 1:确保 browse 服务器在运行

$B status 2>/dev/null

若服务器未运行,用一条无害命令把它拉起来:

$B goto about:blank

这一步保证配对前服务器是健康的。

Step 2:确认要配对的 Agent

通过 AskUserQuestion 询问目标 Agent,并映射为 TARGET_HOST

  • A) OpenClaw(本地或远程)→ openclaw
  • B) Codex / OpenAI Agents(本地)→ codex
  • C) Cursor(本地)→ cursor
  • D) 另一个 Claude Code 会话(本地或远程)→ claude
  • E) 其他(通用 HTTP 指令,Hermes 用这个)→ generic,不写主机专属配置

Step 3:同机还是远程?

  • 同机:跳过复制粘贴,凭据直接写入对方 Agent 的配置目录,不需要隧道;
  • 异机:生成 setup key 和指令块;若已装 ngrok 则自动起隧道,否则按提示完成安装。

Step 4:执行配对

活体守护进程同意门(one-way door)

配对可能触发浏览器守护进程重启,而重启会杀死正在运行的 headless 守护进程——打开的标签页、cookie、登录态全部丢失。CLI 遵守"铁律":只有显式 --force-restart 才允许杀掉活体守护进程(见 cli.ts#L1846-L1896 中 #2219 IRON RULE 的注释)。所以先检查:

$B status 2>/dev/null | head -5

若有活体守护进程,必须询问用户:

  • A) 重启(传 --force-restart;当前标签页/cookie/登录态丢失)
  • B) 保留活体守护进程(推荐——直接对着现有守护进程配对)

只有在用户明确选 A 后才能传 --force-restart。模糊回答绝不能默认选 A,这是破坏性确认。

同机路径(option A)

$B pair-agent --local TARGET_HOST

--local 让 CLI 跳过指令块,直接把凭据写入目标 Agent 的配置目录。从源码看(cli.ts#L1402-L1433),它通过 hosts/index.tsgetHostConfig() 解析各主机的 globalRoot,再用 writeSecureFile(安全权限写入)落盘 browse-remote.json,内容包含 urlsetup_keyscopesexpires_at 四个字段。各平台落盘位置见 SKILL.md 的平台说明:

目标 Agent 凭据写入路径
OpenClaw / AlphaClaw ~/.openclaw/skills/gstack/browse-remote.json
Codex ~/.codex/skills/gstack/browse-remote.json
Cursor ~/.cursor/skills/gstack/browse-remote.json

成功后告诉用户"X 现在可以使用你的浏览器了,试试让它导航到一个 URL"。若失败(host 不存在、写权限错误),展示错误并建议改用通用远程流程。

远程路径(option B)

机器级同意门。 隧道会把浏览器暴露到本机之外,因此默认关闭——守护进程会拒绝 /tunnel/startBROWSE_TUNNEL=1。先检查长期同意:

~/.claude/skills/gstack/bin/gstack-config get pair_agent 2>/dev/null || echo "unset"

若不为 on,用 one-way-door 姿态询问用户("这等于从互联网到本地浏览器开了一条路")。选启用则运行 gstack-config set pair_agent on 并确认读回 on;已为 on 则静默继续,同意持续有效直到 gstack-config set pair_agent off。CLI 侧对应 cli.ts#L1355-L1390pair_agent 未开启时绝不会自动起隧道,只会回退到 localhost 并提示开启方式。

接着探测 ngrok 状态:

which ngrok 2>/dev/null && echo "NGROK_INSTALLED" || echo "NGROK_NOT_INSTALLED"
ngrok config check 2>/dev/null && echo "NGROK_AUTHED" || echo "NGROK_NOT_AUTHED"

已安装且已认证: 直接运行,CLI 会自动检测 ngrok、启动隧道并打印带隧道 URL 的指令块:

$B pair-agent --client TARGET_HOST

若对方还需要 admin 权限(JS 执行、cookie、storage):

$B pair-agent --admin --client TARGET_HOST

关键约束:必须把完整指令块输出给用户。 命令打印 ═══ 分隔线之间的全部内容,要原样复制进回复(放在 markdown 代码块里方便选区),不能总结、不能省略,并提醒"setup key 5 分钟过期"。

已安装但未认证: 引导用户在自己的终端里完成 ngrok 认证。安全红线:ngrok authtoken 绝不能经过聊天、Bash 工具调用或 shell 历史——粘进聊天的 token 会留在会话记录里。用户若已粘贴,应要求其到 ngrok 控制台轮换 token 后重新认证。完成后只需验证结果,不碰 token:

ngrok config check 2>/dev/null && echo "NGROK_AUTHED" || echo "NGROK_NOT_AUTHED"

未安装: 引导用户在 ngrok 官网注册(免费档即可)、安装(macOS 用 Homebrew,Linux 用 snap 或官网下载)并认证,然后重新运行 /pair-agent

权限收敛参数

CLI 还支持更精细的控制(cli.ts#L1273-L1302):

  • --domain <d1,d2,...>:域名白名单(glob 模式,如 *.myapp.com);
  • --restrict read--restrict "read,write":收窄 scope(control 不允许通过 --restrict 传入);
  • --client <name>:指定 Agent 的 clientId,重配对时复用同名才会覆盖/收窄旧授权,不带 --client 则铸造一个全新 Agent(CLI 会打印警告);
  • --control / --admin:授予浏览器级破坏性操作。

防误用有一道前置校验 validatePairAgentFlags:裸 --restrict(历史上会解析成"无限制"从而静默授予全部权限)和 --client rootroot 是绕过一切 scope 检查的哨兵值)都会直接报错退出。

Step 5:验证连接

用户把指令块粘给对方 Agent 后,稍等片刻检查:

$B status

输出中出现该 Agent 即表示连接成功,对方拥有了自己的标签页;如果打开了 GStack Browser 扩展,可以在侧边栏看到它的活动。

权限模型:五种 scope 与隧道命令白名单

SKILL.md 说远程 Agent"默认 read+write,按请求给 admin",源码把这件事拆得更细。token-registry.ts#L44-L88 定义了五个 scope 类别:

scope 代表命令 说明
read snapshot、text、html、links、forms、console、screenshot、pdf 等 21 个 纯读取
write goto、click、fill、newtab、closetab、upload、download 等 22 个 修改页面状态或导航
admin eval、js、cookies、storage、useragent 等 14 个 页面级"权力工具",可访问凭据
control state、handoff、stop、restart、connect、disconnect 浏览器级破坏操作,默认拒绝,只能显式 --control 授予
meta tab、diff、frame、responsive、watch 等 元命令,chain 子命令逐个单独检查 scope

配对默认授予的是 DEFAULT_PAIR_SCOPES = ['read','write','admin','meta']token-registry.ts#L90-L98)——源码注释解释了设计取舍:信任边界是"配对仪式"本身而不是 scope 收窄,--restrict 用于主动收窄,唯独 control 保持显式 opt-in。

默认(read+write 语义)下对方 Agent 能做与不能做:

  • 能:导航、点击、填表、截图、读取页面内容(text/HTML/snapshot)、创建新标签页;
  • 不能(未加 --admin 时按文档口径):执行任意 JavaScript、读 cookie、访问 storage。

隧道面的二次收窄:即使 scope 足够,远程隧道只放行固定命令集合。server.ts#L292-L325TUNNEL_PATHS 只含 /connect/command 两个路径,TUNNEL_COMMANDS 恰好 26 个命令(17 个原始命令 + newtab/tabs/back/forward/reload + snapshot/fill/url/closetab),与 SKILL.md 中"locked to a 26-command allowlist"的表述一致。canDispatchOverTunnel 还有一条额外规则:任何带 --out 参数的调用(例如 eval --out <file>)一律禁止走隧道,因为 --out 会把只读命令变成对本地磁盘的写操作。

限流:每个 token 默认 rateLimit = 10(每秒 10 请求,0 表示不限),触发后返回 429 + Retry-After 头。另外 /connect 端点有一个全局防洪限制:300 次/分钟(token-registry.ts#L684-L704)——setup key 本身是 24 随机字节无法暴破,限流挡的是带宽/CPU/日志洪泛。

撤销访问与轮换

断开指定 Agent:

$B tunnel revoke AGENT_NAME

断开所有 Agent 并轮换 root token(立即使所有 scope 化 token 失效):

$B tunnel rotate

源码侧的细节值得注意(token-registry.ts#L478-L517):

  • revokeToken(clientId) 会删除该 clientId 下的全部 token(session token 和已用/未用的 setup key)。注释说明只删第一个匹配项曾留下两个漏洞:已用 setup key 因幂等重放机制被保留,会"影子"住 session token,导致 revoke 报成功但会话仍活着;
  • revokeSetupKeys(clientId) 只清未使用的 setup key,保留已用 key 的幂等重放记录;
  • 重配对是收窄还是放宽由 grantReducesAccess 判定token-registry.ts#L538-L600):scope 变少、域名收窄、限流变紧、tabPolicy 从 shared 变 own-only 任何一种成立,就立即撤销旧 session(收窄不能等一次"可能永远不会来的"重连);放宽/刷新则不打断现有会话。在域名集合无法证明是超集时倾向于判为收窄——宁可多踢一次重连,不可漏掉宽授权。

故障排查

SKILL.md 的 Troubleshooting 段列出的五种典型报错:

报错 原因与处理
"Tab not owned by your agent" 对方试图操作非自己创建的标签页。让它先跑 newtab 拿到自己的 tab
"Domain not allowed" token 带域名限制。用更宽的域名白名单或无限制重配对
"Rate limit exceeded" 对方发送超过 10 请求/秒。应等待 Retry-After 头并降速
"Token expired" 24 小时 session 到期。重新运行 /pair-agent 生成新 setup key
Agent 连不上服务器 远程:检查 ngrok 隧道是否在跑($B status);本地:检查 browse 服务器是否在运行

平台差异说明

  • OpenClaw / AlphaClaw:Agent 用 exec 工具而非 Bash,指令块采用 exec curl 语法;--local openclaw 时凭据写入 ~/.openclaw/skills/gstack/browse-remote.json
  • Codex:通过 codex exec 执行 shell 命令,指令块中的 curl 命令可直接执行;--local codex 写入 ~/.codex/skills/gstack/browse-remote.json
  • Cursor:AI 可运行终端命令,指令块原样可用;--local cursor 写入 ~/.cursor/skills/gstack/browse-remote.json

安全设计小结(源码佐证)

围绕这个"把浏览器交给别人"的功能,仓库里能看到一整套纵深防御:

  1. 最小暴露面:隧道只放行 /connect/command 两个路径(server.ts#L292-L295),路径集更新被源码注释明确标注为"刻意的安全决策";
  2. 常量时间比较isRootTokencrypto.timingSafeEqual 比较(token-registry.ts#L221-L236),避免经隧道可达的调用方通过比较耗时逐字节猜出 root token;
  3. root clientId 保留assertValidClientId 拒绝空名和 roottoken-registry.ts#L130-L137),恢复状态文件时遇到坏条目是跳过并告警而不是抛错炸掉启动;
  4. 同意门分层pair_agent 配置键是机器级长期同意,--force-restart 是每次的破坏性操作同意,--control/--admin 是权限级同意,三者互不替代;
  5. 凭据卫生--local 写入用 writeSecureFile,ngrok authtoken 被明确禁止进入聊天记录。

相关测试可作进一步验证入口:browse/test/pair-agent-e2e.test.tsbrowse/test/token-registry.test.tsbrowse/test/tab-isolation.test.tsbrowse/test/tunnel-revoke-cli.test.tsbrowse/test/server-auth.test.ts

适用前提与限制

  • 依赖 gstack 的 browse 组件已完成一次性构建(./setup,约 10 秒),二进制位于 .claude/skills/gstack/browse/dist/browse~/.claude/skills/gstack/browse/dist/browse
  • 远程配对依赖 ngrok 隧道,需要 ngrok 已安装并完成认证,且用户已通过 gstack-config set pair_agent on 授予机器级同意;
  • session token 默认 24 小时过期,setup key 5 分钟且一次性——配对后应尽快让对方 Agent 完成 /connect 换发 token。
登录后查看全文
热门项目推荐
相关项目推荐