gstack /pair-agent 实战:把你的浏览器安全共享给另一个 AI Agent
在 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.ts、browse/src/server.ts、browse/src/cli.ts 等源码补充实现细节。
核心机制:一次性 setup key 换 24 小时 session token
gstack 的 browse 浏览器自带一个本地 HTTP 服务器。/pair-agent 的工作方式是:
- 本地 CLI 用 root token 调用
POST /pair,服务器生成一个一次性 setup key; - 技能打印一段"指令块"(instruction block),你把它粘给对方 Agent;
- 对方 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-L330(createSetupKey):如果 key 泄露,它会在被人滥用前自然失效。 - session token 的默认参数(scopes 默认
['read','write']、rateLimit = 10即每秒 10 请求、tabPolicy = 'own-only')见 token-registry.ts#L246-L262(createToken)。 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 秒):
- 告知用户需要构建并等待确认;
- 在技能目录运行
cd <SKILL_DIR> && ./setup; - 若机器上没装
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.ts 的 getHostConfig() 解析各主机的 globalRoot,再用 writeSecureFile(安全权限写入)落盘 browse-remote.json,内容包含 url、setup_key、scopes、expires_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/start 和 BROWSE_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-L1390:pair_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 root(root 是绕过一切 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-L325 中 TUNNEL_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。
安全设计小结(源码佐证)
围绕这个"把浏览器交给别人"的功能,仓库里能看到一整套纵深防御:
- 最小暴露面:隧道只放行
/connect与/command两个路径(server.ts#L292-L295),路径集更新被源码注释明确标注为"刻意的安全决策"; - 常量时间比较:
isRootToken用crypto.timingSafeEqual比较(token-registry.ts#L221-L236),避免经隧道可达的调用方通过比较耗时逐字节猜出 root token; - root clientId 保留:
assertValidClientId拒绝空名和root(token-registry.ts#L130-L137),恢复状态文件时遇到坏条目是跳过并告警而不是抛错炸掉启动; - 同意门分层:
pair_agent配置键是机器级长期同意,--force-restart是每次的破坏性操作同意,--control/--admin是权限级同意,三者互不替代; - 凭据卫生:
--local写入用writeSecureFile,ngrok authtoken 被明确禁止进入聊天记录。
相关测试可作进一步验证入口:browse/test/pair-agent-e2e.test.ts、browse/test/token-registry.test.ts、browse/test/tab-isolation.test.ts、browse/test/tunnel-revoke-cli.test.ts、browse/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。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00