文章

Agent 共用一个浏览器时,怎么隔离标签 ACL:Pilion Agent Spaces

· 9 分钟阅读 · 本文以中文撰写

2026 年 11 月 4 日,Pilion 把 feat/agent-spaces 合进 main,当天发了 v0.2.0。更新日志第一条就是 Agent Spaces:每个外部 Agent 对话一个独立 Space,子 principal 写成 external:<clientId>:<spaceId>,标签 ACL 按这个切开。

同一天还上了「安装到 Agent」、pilion CLI 和 pilion-browser skill。那是一期把终端 Agent 直接接到本机浏览器;Spaces 是二期,解决「两个对话共用一个浏览器时,怎么别互相踩页」。

刚写过 agent-desktop 深度解析。那篇讲桌面无障碍树;这篇讲本机浏览器里的权限边界。两条路都能让 Agent 动手,失败模式不一样。

为什么要切

Pilion 是给 ACP Agent 用的桌面浏览器。左边标签和工作记录,中间网页,右边接 Claude Code、Codex、Gemini CLI、Orca 一类 Agent。浏览器通过本机认证通道把标签页交给外部进程操作,动手前可以要求审批,人随时可以接管。

本机浏览器的价值,正是那份已经登好的 Cookie。两个 Agent 同时干活时,这份登录态是共享的。若标签权限也共享,A 的比价会话就能关掉 B 正在填的表,B 也能点进 A 的购物车,审批还会串到别的对话。

左边:多 Agent 共用一套标签和登录态,互相关页、点错、审批串台;右边:每个对话一个 Space,Cookie 仍是本机那份,权限按子 principal 切开,用户当前标签列表不变

设计文档写得很直:对话之间碰不到对方的标签;Agent 干活时,用户当前的标签列表和选中的标签不变;用户在空间切换器里能查看、停止、接管每一个对话。

主体怎么切

一期已经有基础主体 external:<clientId>,对应某个外部客户端。二期在后面再挂一层 Space:

user
external:<clientId>
external:<clientId>:<spaceId>

spaceId 形如 agent-<uuid>。标签 ACL 按完整子主体精确匹配。A 对话拿到的 principal 碰不到 B 对话的 tab,越权直接 PERMISSION_DENIED,并记审计。

三层主体:user 是你自己;external:clientId 是一期外部客户端;external:clientId:spaceId 是二期一次对话一个 Space,标签 ACL 按完整子主体匹配

规则引擎在 src/main/external/spaces.ts,时钟、定时器和副作用全部注入,单测不碰 Electron。上限是全局的:用户 Space 最多 20,active Agent Space 最多 8,已结束的 Agent Space 最多 8。按客户端分桶会让多个客户端一起冲破 schema 上限,所以不按 client 计。

请求怎么判

CLI 流程是:先 pilion space new "比价",记下 id,之后每条浏览器命令带 --space <id>,也可以 export PILION_SPACE=<id>。skill、status、version、space * 本身不需要 Space。

连接进来先走 hello。带 space 时调用 admit:

  • active → ok
  • 因 idle 或 restart 结束 → 重新激活,返回 resumed,并且只给 ownerPrincipal 等于该子主体的标签重新授 ACL
  • 不存在,或不属于本客户端 → 同一个 SPACE_NOT_FOUND,避免用 spaceId 探测别人的空间
  • 被关闭、停止或接管 → SPACE_ENDED,提示用 pilion space new 新建

集成客户端(「安装到 Agent」签发的那种)不带 --space 发工具帧时,服务端统一返回 SPACE_REQUIRED,文案是:先运行 pilion space new "<任务名>",再给每条命令加 --space <id>。MCP 和手动签发的客户端保持一期行为,不强制 Space。

控制帧 space.create / list / close 和工具帧分开,不进工具白名单,也不调用浏览器 runtime。建 Space、开标签都不改用户的 activeSpaceId。

请求路径:hello 校验 token;带 space 则 admit 并固化子 principal;集成客户端缺 space 得 SPACE_REQUIRED;工具帧再按标签 ACL 精确匹配,无权限则 PERMISSION_DENIED

子 principal 新开的标签直接进对应 Agent Space,后台打开,不抢你正在看的那一页。用户自己 ⌘T 新开标签如果会落到 active 的 Agent Space,会改放到第一个用户 Space。

结束、接管、淘汰

CLI 每条命令是一个短连接,所以 60 分钟空闲计时挂在 Space 上,不挂在连接上。每个被接纳的工具请求 touch(spaceId) 重置计时;到点 end(id, 'idle')。

end 的副作用顺序写死在规则和测试里:

  1. invalidatePrincipal 中止在途操作
  2. 拒绝等待该子主体的审批
  3. revokePrincipal 撤掉标签 ACL
  4. 关掉该子主体的在线连接
  5. 存盘并广播

结束原因分两类:idle、restart 可以恢复;closed、stopped 不能。启动时所有还标着 active 的 Agent Space 会改成 restart,然后跑淘汰。已结束超过 8 个时,按 endedAt 淘汰最旧的,它的标签进归档。

侧栏「接管」等于先 stop,再剥掉 agent 字段,Space 变成普通用户 Space。用户 Space 已经 20 个时拒绝接管。接管之后,旧客户端重连也不能再锁这些标签。

结束副作用顺序、可恢复与不可恢复的原因、active/ended 上限与 60 分钟空闲;短连接决定了空闲计时挂在 Space 上

第二天的 v0.2.1 补了一处短连接相关的体验:「本任务允许」跨 CLI 重连复用,在任务结束或接管后失效;客户端长期授权重启后仍保留,可在「设置 › Agent 连接」撤回。CLI 断一下再连,任务标识还在,不必每条命令重新点审批。

再往后 v0.2.3 修过一处接管后的边角:人工提交表单时,页面发起的 HTTP(S) 导航会保留原始 POST 请求体,不再被取消后重放成 GET。

几条硬规则

多客户端共用本机登录态时,共享 Cookie 和共享标签权限是两件事。Pilion 选择保留本机登录态,把权限切到对话级的子 principal。

落地时可以记这几条:

  • 身份串是 external:<clientId>:<spaceId>,ACL 精确匹配,不靠「当前前台标签」猜测
  • 集成客户端缺 --space 时统一 SPACE_REQUIRED,文案里给出正确命令
  • 结束先撤权、拒审批、断连接,再改状态;可恢复的只有空闲和重启
  • 短连接场景里,任务态和空闲计时要挂在 Space,不能挂在单次 TCP 连接上

到今天(2026-11-06)仓库已经发到 0.2.6。Spaces 主体仍是 0.2.0 引入、0.2.1 补任务授权的那一套。

材料与时效

本文机制说明来自 2026-11-06 可读的公开材料:

  • 设计文档 docs/superpowers/specs/2026-11-04-agent-spaces-design.md
  • 实施计划 docs/superpowers/plans/2026-11-04-agent-spaces.md
  • 合入提交 6d56bf02(计划)、528f875b(规则引擎)、4d8b4d13(接线)
  • 发版说明 v0.2.0、v0.2.1、v0.2.3 与 CHANGELOG.md
  • 协议面 src/main/external/space-protocol.ts、规则 src/main/external/spaces.ts

我没有在本机跑双 Agent 并行 E2E;文中的行为以仓库代码、单测约定和发版说明为准。后续版本以 GitHub 仓库为准。

继续阅读

相关文章

讨论

审核后公开 · 支持 Markdown

读到这里时加载讨论。