Files
AgentCompany/docs/proposals/M9-browser-automation-utility-process.md
T

269 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Proposal M9: browser / automation 拆出独立 utility process
> 来源:问题.md M9(行 272-276)、附录 I 第 3 条(行 589-590)
> 状态:proposed
> 提出:Roundtable 2026-04-20(Linus + Karpathy + Steve Jobs)
> Owner: TBD
> 估计工时:3 周
> Blocking dependencies:C2(RuntimeBus 就绪后,跨进程 dispatch 更容易)
## 问题陈述
`src/main/browser/` 和 `src/main/automation/` 两个子系统目前运行在主进程:
- `browser/` 跑 Electron 的 BrowserWindow / BrowserView,做网页自动化(填表单、点按钮、截图)
- `automation/` 跑 rule engine,触发 browser actions 或其他 side effects
后果:
1. **Crash blast radius 大**:browser page crash / 内存泄漏直接拖垮 main 进程,整个 desktop app 挂掉
2. **事件循环阻塞**:自动化脚本跑长时间同步操作(例如等待 CAPTCHA)时,main 进程 UI IPC 都受影响
3. **Cloudflare / CAPTCHA 等防爬虫对抗**:反爬逻辑跑在 main 进程意味着主程序有"机器人"指纹,可能被下游服务整体封禁
4. **资源占用不可见**:browser 内存和 CPU 混在 main 进程里,不便于监控 / 限额
5. **Attack surface**:自动化脚本触达外部网站,一旦被攻击(XSS / sandbox escape)直接污染 main 进程的数据库连接和文件系统权限
## 根因分析
Linus 视角:**crash blast radius 是进程边界的第一目的**。把 browser 跑在 main 进程 = 把不稳定的组件和稳定的组件绑一起了。Electron 提供 utility process 正是为此——crash 就让它 crash,主进程重启它即可。
当前架构把"永远不 crash 的组件(main)"和"经常 crash 的组件(browser automation)"放在同一个进程里——这是 colocation 反模式。
Karpathy 视角:agent 跑 browser automation 是典型的 stochastic task——成功率不是 100%。需要**独立的失败域 + 重试机制 + resource cap**。跑在 main 进程无法给 browser 设单独的 memory limit / CPU quota。
## 设计方案
### 架构
```
Main process
├── Orchestration / DB / IPC to renderer
│
├──[fork]──> Worker utility process (agent run loop)
│
├──[fork]──> Browser utility process (browser automation)
│ ├── Electron BrowserWindow / Puppeteer
│ ├── 独立内存 / CPU quota
│ └── 通过 bus dispatch "browserAction.dispatch" 消费
│
└──[fork]──> Automation utility process (rule engine)
├── Rule evaluation loop
├── Scheduled triggers
└── 通过 bus dispatch 发送到 browser / agent
```
### 对 C2 bus 的依赖
本 proposal 依赖 C2 的 RuntimeBus——拆进程后跨进程 dispatch 最自然的实现是:
- Main 的 bus 是 hub
- utility process 和 main 之间通过 `process.parentPort.postMessage` 传递 command/event
- Bus 在 main 侧做 multiplexing:根据 command kind 路由到对应 utility process
这让 C2 的抽象同时覆盖 in-process 和 cross-process 两种 dispatch,**代码层面完全一致**。
### Browser utility process 实现
新建 `src/main/browser-process/entry.ts`:
```ts
import { parentPort } from "node:worker_threads"; // 或 utilityProcess API
const browser = new BrowserAutomationService(...);
parentPort.on("message", async (msg) => {
if (msg.kind === "browserAction.dispatch") {
try {
const result = await browser.execute(msg.payload);
parentPort.postMessage({ kind: "browserAction.result", runId: msg.runId, result });
} catch (e) {
parentPort.postMessage({ kind: "browserAction.error", runId: msg.runId, error: serializeError(e) });
}
}
});
```
### Automation utility process
rule engine 比 browser 轻量,但同样的道理——独立进程 = 独立故障域。新建 `src/main/automation-process/entry.ts`:
```ts
const engine = new AutomationRuleEngine(db);
engine.on("fire", (payload) => {
parentPort.postMessage({ kind: "automation.fired", ...payload });
});
// tick loop
setInterval(() => engine.evaluate(), 1000);
```
### DB 访问
utility process **不直接访问 DB**——所有 DB 操作通过 bus 让 main 进程的 writer actor 处理(与 B4 的 single-writer actor 配合)。
这避免 SQLite 跨进程连接的复杂度(SQLite 支持跨进程但要处理 WAL 同步)。
### Resource cap
- Electron utility process 可设 `memoryLimit`(v27+)
- 配置 `maxOldSpaceSize` 控制 V8 heap
- Main 进程定期 ping utility process,超时自动 kill + restart
### Crash recovery
Browser utility process crash 时:
1. Main 收到 `process.exit` 事件
2. 发"browser 不可用"告警
3. 根据 restart policy 重启进程
4. 重启后已在运行的 browser action 状态从 DB 恢复
Automation utility process 同理。
## Migration 计划
1. **Phase 0(前置)**:C2 RuntimeBus 完成
2. **Phase 1(1 周)**:设计 cross-process bus 协议(message envelope / error serialization / backpressure)
3. **Phase 2(1 周)**:browser 拆进程——browser automation 代码搬到 `browser-process/`,main 侧改为通过 bus dispatch
4. **Phase 3(3 天)**:automation 拆进程——同样方式
5. **Phase 4(3 天)**:monitoring & restart policy—— main 侧加 utility process health check + telemetry
6. **Phase 5(1 周观察期)**:生产压力测试 / crash 注入测试,确认 blast radius 隔离
## 风险与回滚
| 风险 | 触发条件 | 回滚方法 |
|------|---------|---------|
| IPC latency 升高 | browserAction 从本地函数调用变成跨进程 | 每次 action 多 1-2ms 可忽略;如果某 action 需要高频通信则重新评估 |
| State 丢失 | utility process crash 时正在执行的 action 状态 | 所有 action 状态进 DB 持久化,crash 后恢复 |
| 开发体验差 | 调试需要 attach 多个进程 | VS Code 多 process debug configuration |
| macOS entitlements | utility process 需要特殊 entitlement 才能跑某些 Electron API | 研究 Apple 文档,提前在 entitlements.plist 加 |
**回滚路径**:拆进程过程保留旧 in-process 代码路径作为 feature flag(`BROWSER_USE_UTILITY_PROCESS=true/false`),回滚时切 flag。
## 验证标准
1. **Blast radius 验证**:故意在 browser utility process 内 `process.exit(1)`,main 进程应不受影响,UI 仍可用
2. **Memory isolation**:browser automation 跑长时间任务,main 进程 memory 稳定不增长
3. **Restart 自动化**:kill -9 browser process,main 检测到后自动重启,丢失 ≤1 个 in-flight action
4. **Latency baseline**:browserAction.dispatch 的 P99 latency 在可接受区间(< 50ms 额外开销)
5. **Crash rate telemetry**:生产观察一周,browser process crash 数据可见且独立于 main 进程崩溃
## 不在范围
- **不把 worker 也一起重构**:worker 已经是 utility process(见 worker-entry.ts),本 proposal 只补 browser / automation
- **不实现跨机器 dispatch**:bus 仍是单机进程间,不做分布式
- **不重构 rule engine 的规则 DSL**:只是搬进程
- **不做 browser farm**:单个 browser utility process 就够,不是并发 N 个实例
- **不改 renderer**:renderer 层对 browser / automation 的交互仍走 main → utility process 中转
## 参考文件清单
- `src/main/browser/`(当前 browser automation 代码)
- `src/main/automation/`(当前 automation rule engine 代码)
- `src/main/automation-service.ts`(当前 rule engine 入口)
- `src/main/browser/browser-action-service.ts`(action 执行)
- `src/main/browser/browser-action-executor.ts`(具体 action)
- `src/main/service-graph.ts:110-113`(browserActionRef / automationRef 的 LateBinding,C2 会改)
- Electron utilityProcess 文档:https://www.electronjs.org/docs/latest/api/utility-process
## 附:为什么现在修
单个 browser page 的崩溃已经是生产痛点——任何 agent 的网页操作触发 chromium crash 都拖垮用户整个 desktop app。随着 agent 被用于更多 web automation 场景(社交登录、paid subscription 填表、CAPTCHA 对抗),crash 频率只会升。
C2 做完后,跨进程 bus 成本极低——本 proposal 是把 C2 的 bus 抽象延伸到进程边界,很自然。
Jobs 视角:**"app 不能崩"是 desktop 软件的基础承诺**。当前架构把 desktop 稳定性和 agent browser 稳定性绑定了,这违反了承诺。
## 附:gating matrix
| Gate | 条件 | 放行判断人 |
|------|------|----------|
| G1 | C2 RuntimeBus 完成 + 跨进程 dispatch 协议定稿 | Linus |
| G2 | browser utility process 跑 long-running automation 1 小时无 main 进程回压 | Owner |
| G3 | Kill utility process 后 main 进程自动重启,in-flight action 损失 ≤ 1 | Owner |
| G4 | Browser process 内存隔离验证:独立 heap,不与 main 共享 | Linus |
| G5 | macOS entitlements 配置完整,通过 notarization | Owner |
## 附:跨进程 message envelope
```ts
interface CrossProcessMessage {
id: string; // correlation id
kind: CommandKind | EventKind;
payload: unknown; // 由 kind 决定 shape
source: "main" | "browser-process" | "automation-process" | "worker";
timestamp: number;
// Error case
error?: { name: string; message: string; stack?: string };
}
```
序列化走 JSON(Electron utilityProcess 默认);大 payload 用 MessagePort + structured clone。
## 附:跨进程 DB 访问禁令
Utility process 内禁止直接 `new DatabaseSync(...)` ——所有 DB 操作走 bus dispatch 到 main 的 writer actor(B4)。原因:
1. SQLite WAL 跨进程同步复杂
2. Main 的 writer actor 保持单 writer 不变式
3. Utility process crash 不会导致 DB 文件锁残留
## 附:反模式对比
**Before**:
- Main 进程 import browser automation service
- 浏览器 crash → main 进程 crash → desktop app 关闭
- 用户 log off,30+ 秒后重新登录才能恢复
**After**:
- Main 进程通过 bus dispatch 到 browser utility process
- 浏览器 crash → utility process 独立退出 → main 进程 2 秒内重启它
- 用户看到 "browser automation recovering",UI 其他部分无感
## 附:为什么不选其他方案
**方案 A(当前 proposal)**:Electron utilityProcess 隔离。
**方案 B(完全拆出 desktop app 外,跑独立 daemon)**:过度工程——用户不希望后台跑多个进程。
**方案 C(保留 main 进程但加 try-catch)**:JS 层的 try-catch 无法 catch native crash(chromium renderer crash)。
方案 A 是 Electron 官方推荐的隔离路径。
## 附:restart policy 细节
| 情况 | 行为 |
|-----|------|
| Browser utility process 正常退出(exit 0) | 不重启(可能是主动 stop) |
| 异常退出(exit ≠ 0) | 立即重启,最多 3 次/分钟 |
| 超过 3 次/分钟 | 进入 backoff(冷却 5 分钟) |
| 冷却期间 browserAction.dispatch | 直接返回 `BROWSER_UNAVAILABLE` 错误 |
| 冷却结束 | 自动重启;成功则清 backoff 计数 |
| 连续 3 次 backoff 失败 | 告警 + 停止重启,等用户手动恢复 |
## 附:resource cap 配置
```ts
const browserProcess = utilityProcess.fork(entryPath, args, {
serviceName: "agent-company-browser",
stdio: "pipe",
execArgv: ["--max-old-space-size=2048"], // 2GB heap 上限
});
```
macOS 的 resource monitor 可以看到 `agent-company-browser` 的 CPU/memory 独立使用,用户也能直接感知 automation 占用。
## 附:与 C2 的跨进程扩展
C2 的 InProcessRuntimeBus 扩展为支持 cross-process:
```ts
class CrossProcessRuntimeBus implements RuntimeBus {
constructor(private child: UtilityProcess, private local: InProcessRuntimeBus) {}
async dispatch(kind, payload) {
if (this.isLocalKind(kind)) {
return this.local.dispatch(kind, payload);
}
return this.sendToChild(kind, payload);
}
}
```
消费者代码不变——bus 抽象隐藏了进程边界。