在本地 AI 代理系统中,浏览器自动化是一项非常有用的能力。代理可以打开网页、搜索信息、读取页面、协助登录后台、处理表单,甚至通过 CDP 控制完整浏览器。
但浏览器自动化也有一个明显风险:如果代理没有严格区分“自己可管理的浏览器”和“用户正在使用的普通浏览器”,它就可能误伤用户的真实桌面环境。
本篇记录的是一次典型的边界事故:AI 代理本来只应该打开和关闭自己的专用浏览器,却因为使用了过宽的进程匹配,把用户正常使用的普通 Brave 浏览器也关闭了。
这不是单纯的命令错误,而是一次权限边界设计错误。
背景:主实例和浏览器分离
迁移后的结构中,OpenClaw 主实例运行在常驻主机上,桌面浏览器运行在另一台桌面主机上。
浏览器调用链路大致是:
OpenClaw 主实例
-> SSH 到桌面主机
-> 启动桌面主机上的 Brave
-> 指定专用 user-data-dir
-> 开启 remote-debugging-port
-> 通过 SSH tunnel 连接 CDP
为了隔离用户日常浏览器和 AI 代理浏览器,桌面主机上准备了一个专用 profile:
<desktop-cdp-profile>/user-data
OpenClaw 只能使用这个专用 profile。用户日常使用的普通 Brave profile、普通窗口、普通标签页和普通会话,都不属于 OpenClaw 的管理范围。
从设计上看,这个边界是清楚的。
但事故说明:只靠“人知道边界”不够,必须让 AI 代理的规则和命令也严格遵守这个边界。
事故经过
最初的任务很简单:让 AI 代理在桌面主机上远程打开一个可见 Brave 窗口,用于测试浏览器/CDP 能力。
代理确实打开了一个新的可见窗口。
但在打开之前,它先关闭了桌面主机上原本已经打开的普通 Brave 窗口。
随后,又让代理关闭刚才打开的浏览器窗口。结果它在关闭时,再次把用户后来重新打开的普通 Brave 窗口关闭了。
也就是说,事故不是一次偶发误操作,而是在两个阶段都发生了边界错误:
启动前:
- 为了清理环境,关闭了用户普通 Brave
关闭时:
- 为了关闭代理浏览器,又关闭了用户普通 Brave
这说明代理没有把“OpenClaw 专用浏览器”和“用户普通浏览器”区分开。
真正的问题:它管理了 Brave 进程整体
事后回溯命令时发现,代理执行过类似以下危险操作:
pkill brave
pkill -f brave
pkill -f /opt/brave-bin/brave
ps ... grep brave ... xargs kill
ps ... grep /opt/brave-bin/brave ... xargs kill -9
这些命令的问题在于,它们匹配的是 Brave 程序本体,而不是 OpenClaw 专用 profile。
但普通 Brave 和 OpenClaw 专用 Brave 使用的是同一个浏览器二进制文件。也就是说,从程序路径上看,它们可能都是:
/usr/bin/brave
/opt/brave-bin/brave
如果用 brave、/opt/brave-bin/brave、Chrome、remote-debugging-port 这类宽泛关键词识别进程,就很容易把用户正在使用的普通窗口一起杀掉。
正确边界不应该是“是不是 Brave”,而应该是:
这个 Brave 进程是否明确使用了 OpenClaw 专用 profile?
专用 profile 才是唯一可靠边界
在这种结构中,判断一个 Brave 进程是否属于 AI 代理,唯一可靠依据是命令行中是否包含专用 profile 路径。
例如:
--user-data-dir=<desktop-cdp-profile>/user-data
如果要进一步确认它是 CDP 浏览器,还需要确认包含:
--remote-debugging-port=<cdp-port>
因此,OpenClaw 可管理的浏览器进程必须同时满足:
1. 命令行包含专用 user-data-dir
2. 命令行包含指定 remote-debugging-port
不能因为一个进程叫 Brave 就管理它。
不能因为它使用同一个浏览器二进制文件就管理它。
不能因为它占用了某个端口就直接杀掉它。
不能因为“需要清理环境”就扩大到所有 Brave 进程。
启动阶段的错误:为了干净状态而清理过度
AI 代理很容易形成一种危险倾向:为了让任务成功,先清理环境。
在服务器或临时容器里,这种思路有时能工作。例如先杀掉旧进程、释放端口、清理残留,然后重新启动服务。
但桌面环境不是一次性容器。桌面主机上有用户正在使用的窗口、登录状态、网页会话和未保存内容。
因此,浏览器自动化任务不能采用“先杀干净再启动”的思路。
错误思路是:
为了确保 CDP 可用,先清理所有 Brave。
正确思路应该是:
先检查专用 profile 的 CDP 是否已经运行。
如果没有运行,就启动专用 profile。
如果端口冲突或状态不明,先报告,不要清理普通 Brave。
尤其是在桌面环境中,保护用户现有窗口的优先级必须高于完成自动化任务。
关闭阶段的错误:把“关闭浏览器”理解得太宽
这次事故的第二个问题出现在关闭阶段。
用户说“关闭浏览器”或“可以关闭浏览器了”时,在 Mars 浏览器任务语境里,这里的“浏览器”应该只指 OpenClaw 专用浏览器,而不应该指用户日常使用的普通 Brave。
但代理把这个指令扩大理解成了“关闭 Brave”。
这说明自然语言指令本身也需要定义语义边界。
正确规则应该是:
在 OpenClaw 浏览器任务语境里:
“浏览器”默认只指 OpenClaw 专用 CDP 浏览器。
不指用户普通 Brave。
不指所有 Brave。
不指桌面上所有浏览器窗口。
因此:
用户说“打开浏览器”:
只允许打开 OpenClaw 专用浏览器。
用户说“关闭浏览器”:
只允许关闭 OpenClaw 专用浏览器。
用户说“重启浏览器”:
只允许重启 OpenClaw 专用浏览器。
用户说“清理浏览器”:
只允许处理 OpenClaw 专用 profile 的运行状态。
如果代理无法确认哪个进程属于专用浏览器,就应该报告无法安全关闭,而不是扩大操作范围。
禁止模糊 kill
事故后,必须写入硬规则。不是“尽量不要”,而是“绝对禁止”。
以下命令或同类命令都应该被禁止:
pkill brave
pkill -f brave
pkill -f /opt/brave-bin/brave
killall brave
ps ... grep brave ... xargs kill
ps ... grep /opt/brave-bin/brave ... xargs kill
原因很简单:这些命令无法区分普通 Brave 和专用 Brave。
即使 OpenClaw 专用 Brave 也是 Brave,它也不能让代理获得管理所有 Brave 的权力。
正确关闭方式只能是:
1. 列出候选 PID
2. 输出完整命令行
3. 确认命令行包含专用 user-data-dir
4. 确认命令行包含 remote-debugging-port
5. 只对明确确认过的 PID 操作
6. 关闭后再次列出进程,证明普通 Brave 未受影响
如果找不到满足条件的 PID,就应该输出:
没有找到 OpenClaw 专用浏览器进程,因此没有关闭任何浏览器。
这比误杀普通浏览器安全得多。
主 browser 进程和子进程也要区分
Chromium 系浏览器会启动多个进程,包括:
browser 主进程
renderer 进程
gpu-process
utility 进程
zygote 进程
crashpad handler
extension process
如果要关闭专用浏览器,最好优先识别主 browser 进程,而不是随意杀子进程。
一个较严格的主进程判断方式是:
命令行包含:
- 专用 user-data-dir
- remote-debugging-port
命令行不包含:
- --type=renderer
- --type=gpu-process
- --type=utility
- --type=zygote
这能减少误判,也能避免只杀掉子进程导致浏览器状态异常。
当然,在实际执行前仍然应该列出完整 PID 和命令行,让用户或规则引擎确认。
端口占用不是 kill 的理由
另一个需要写死的规则是:端口占用不是 kill 的充分理由。
如果 CDP 端口被占用,代理不应该直接执行:
kill 占用 **** 的进程
而应该先报告:
端口被占用。
占用 PID 是什么。
完整命令行是什么。
是否属于 OpenClaw 专用 profile。
如果占用者不是专用 profile,就不能 kill。
如果无法判断,也不能 kill。
自动化系统经常会把“释放端口”视为技术问题,但在桌面环境里,它可能意味着关闭用户正在使用的程序。
不追求“干净状态”
这次事故的根本诱因之一,是代理追求“干净状态”。
在服务器运维中,干净状态很有吸引力:
旧进程清掉
端口释放
服务重新启动
状态重新建立
但在个人桌面环境中,干净状态不一定是正确目标。
更重要的目标是:
不影响用户当前使用状态
因此,OpenClaw 调用桌面浏览器时,原则应该是:
可以启动
可以连接
可以使用
默认不关闭
默认不清理
默认不 kill
不为了干净状态影响普通浏览器
如果专用浏览器状态异常,应该优先报告,而不是强行修复。
规则应该写在哪里
这类事故不能只靠一次口头提醒解决。
必须把规则写入 AI 代理的长期工具规则文件中,例如:
TOOLS.md
因为这不是普通记忆,而是工具调用边界。
它应该记录:
- 浏览器工具的主机
- 专用 profile 路径
- CDP 端口
- SSH tunnel 规则
- 可见窗口启动规则
- headless 使用限制
- 禁止 kill 普通 Brave
- 关闭浏览器的语义解释
- PID 识别和关闭前确认规则
尤其要明确一点:
普通 Brave 不属于 OpenClaw 的可管理资源。
AI 代理只有专用 profile 和专用 CDP 进程的管理权,没有用户普通浏览器的管理权。
自然语言也要写边界
这次事故还说明,规则不能只写命令层,还要写自然语言层。
例如,“关闭浏览器”这句话本身是模糊的。
在普通聊天里,它可能指用户面前的浏览器。
但在 OpenClaw 浏览器任务语境中,它必须被解释为:
关闭 OpenClaw 专用 Mars CDP Brave。
而不是:
关闭所有 Brave。
关闭用户普通 Brave。
关闭桌面上所有浏览器窗口。
因此,工具规则中应该明确:
如果用户没有明确说“普通 Brave”,代理不能把指令解释为普通 Brave。
如果用户说“浏览器”,默认只指代理专用浏览器。
这能避免代理把简短指令扩大解释。
事故后的最终边界
事故后,OpenClaw 的浏览器权限边界可以重新定义为:
属于 OpenClaw 的:
- 专用浏览器 profile
- 通过该 profile 启动的 Brave 主进程
- 明确带有指定 CDP 端口的专用浏览器进程
- 树莓派到桌面机的 SSH tunnel
不属于 OpenClaw 的:
- 用户普通 Brave profile
- 用户普通 Brave 窗口
- 用户普通 Brave 标签页
- 用户普通 Brave 会话
- 所有未明确使用专用 profile 的 Brave 进程
所有操作都必须基于这个边界进行。
启动可以按需执行。
连接可以按需建立。
关闭默认不执行,除非明确要求关闭专用浏览器。
清理默认不执行。
kill 默认不执行。
这比“尽量小心”可靠得多。
对 AI 代理系统的启示
这次事故说明,AI 代理的风险并不只来自“模型回答错了”,还来自“模型执行命令时边界太宽”。
尤其是当代理具备 SSH、浏览器、文件系统、进程管理能力时,必须把权限范围写成具体可执行的规则。
抽象地说:
不要只告诉代理“做什么”。
还要告诉代理“绝对不能碰什么”。
例如:
做什么:
- 启动专用浏览器
- 建立 tunnel
- 访问 CDP endpoint
不能碰什么:
- 普通 Brave
- 普通 profile
- 普通窗口
- 所有未确认归属的进程
这类禁止规则不是多余的。对自动化代理来说,负面边界和正向任务同样重要。
小结
这次浏览器事故可以总结为一句话:
代理本来只应该管理自己的专用浏览器,却因为使用了全局 Brave 进程匹配,误伤了用户普通浏览器。
根因不是 Brave 本身,也不是 SSH tunnel,也不是 CDP,而是权限边界没有被写死。
最终修正方向是:
- 专用 profile 是唯一边界
- 普通 Brave 永远不可管理
- 禁止模糊 kill
- 禁止追求干净状态而清理普通进程
- “浏览器”默认只指 OpenClaw 专用浏览器
- 关闭前必须列 PID 和完整命令行
- 无法确认归属就不关闭
本地 AI 代理要可靠运行,不能只依赖模型“理解”。它需要明确、细致、长期保存的工具规则。
在涉及桌面环境、浏览器、进程和用户会话时,这些规则尤其重要。因为一次过宽的 pkill,就足以让一个看似简单的浏览器任务变成桌面事故。