# WS 浏览器后端认证替换设计

## 背景

当前 `sg_claw` 的 websocket service 路径已经能接收 `sg_claw_client` 请求、复用共享 task runner、连接真实浏览器 websocket 地址 `browser_ws_url`，并进入真实 skill 执行链路。但真实联调时，所有浏览器相关调用都会失败并返回：

- `invalid hmac seed: session key must not be empty`

根因已经定位：

- pipe 模式在 [src/lib.rs](src/lib.rs) 中通过 handshake 拿到 `session_key`，并用它构造 `BrowserPipeTool`
- ws service 模式在 [src/service/server.rs](src/service/server.rs) 中仍然构造 `BrowserPipeTool::new(..., vec![])`
- `BrowserPipeTool` 的认证模型要求非空 session key，因此 ws service 路径虽然使用的是浏览器 websocket 协议，仍错误地依赖了 pipe 特有的 HMAC/session-key 语义

这会导致：

1. `sg_claw_client -> sg_claw` 连接正常
2. skill 加载与模型调用正常
3. 真实浏览器动作开始执行
4. 但所有 browser tool 调用在认证层统一失败

## 目标

在 **仅限 ws 模式改动** 的前提下，让 `sg_claw` service 路径改为使用 **ws-native browser backend**，不再依赖 `BrowserPipeTool` 的 pipe session-key 认证模型，从而让真实浏览器联调可用。

## 约束

必须满足：

- 只改 ws 模式相关实现
- 不破坏 legacy pipe 模式
- 不修改 pipe handshake 语义
- 不修改 `src/lib.rs` 的 pipe 主入口行为
- 不引入临时绕过认证或 fake seed
- 不扩大到多客户端、多任务、队列、守护进程管理

## 非目标

本次不做：

- 自动拉起 sgBrowser
- 浏览器进程管理
- 多浏览器实例支持
- service/client UX 优化
- browser ws 协议扩展
- pipe 模式重构
- 统一重构所有 runtime 层去完全依赖 `BrowserBackend`

## 现状分析

### 正常 pipe 路径

pipe 模式当前在 [src/lib.rs](src/lib.rs) 中：

1. 通过 `perform_handshake(...)` 读取浏览器侧初始化消息
2. 从 handshake 中拿到 `session_key`
3. 用 `BrowserPipeTool::new(transport.clone(), mac_policy, handshake.session_key)` 构造浏览器工具
4. 后续 browser action 使用 pipe/HMAC 语义

该路径已经可用，本次不能动。

### 当前 ws service 路径

当前 ws 模式在 [src/service/server.rs](src/service/server.rs) 中：

1. `sg_claw_client` 将任务发给 `sg_claw` service
2. service 构造 `ServiceBrowserTransport`
3. service 用 `BrowserPipeTool::new(transport.clone(), mac_policy.clone(), vec![])`
4. browser action 经 `ServiceBrowserTransport` 编码为 browser websocket 请求并发给 `browser_ws_url`

问题在于第 3 步：

- service 走的是 browser websocket 协议
- 但却仍使用 `BrowserPipeTool`
- `BrowserPipeTool` 内部仍坚持要求 pipe session key
- 因此真实 ws 联调时直接失败

### 现有 ws-native 能力

代码中已经存在：

- [src/browser/ws_protocol.rs](src/browser/ws_protocol.rs)：固定 browser websocket 协议 codec
- [src/browser/ws_backend.rs](src/browser/ws_backend.rs)：`WsBrowserBackend`
- [src/browser/mod.rs](src/browser/mod.rs)：已导出 `WsBrowserBackend`

`WsBrowserBackend` 本身不依赖 pipe session key，而是：

- 使用 `WsClient` 发送/接收文本帧
- 使用 `MacPolicy` 做动作级校验
- 通过 `encode_v1_action(...)` 与 `decode_callback_frame(...)` 处理 ws 协议

这正是 ws service 模式应该使用的模型。

## 关键集成缝隙

当前共享 runner 的真实缝隙已经确认：

- [src/agent/task_runner.rs](src/agent/task_runner.rs) 的 `run_submit_task(...)` 仍直接要求 `&BrowserPipeTool<T>`
- [src/compat/runtime.rs](src/compat/runtime.rs) 与 [src/compat/orchestration.rs](src/compat/orchestration.rs) 也继续以 `BrowserPipeTool<T>` 作为主浏览器调用对象
- 同时 compat runtime 内部已经存在 `Arc<dyn BrowserBackend>` 的工具适配层，只是它目前是从 `PipeBrowserBackend::from_inner(browser_tool)` 包出来的

这意味着本次实现不能只在 `src/service/server.rs` 里替换构造逻辑，而必须在 **ws 专用调用面** 增加一个最小适配缝隙，让 service 模式能把 `WsBrowserBackend` 传入 compat/runtime/orchestration，而 pipe 继续保持 `BrowserPipeTool` 原样。

允许的最小缝隙定义如下：

1. `run_submit_task(...)` 的 pipe 版本保持不动，供 pipe 入口继续使用
2. 新增一个 **仅供 ws service 使用** 的并行入口，例如：
   - `run_submit_task_with_browser_backend(...)`
   - 或 service 侧调用的等价 ws-only adapter
3. ws-only 入口内部允许把浏览器依赖类型降到 `Arc<dyn BrowserBackend>`
4. `src/lib.rs`、pipe handshake、pipe `BrowserPipeTool` 构造逻辑不允许改行为

## 设计决策

### 决策 1：ws service 路径弃用 `BrowserPipeTool`

在 ws service 路径中，不再构造 `BrowserPipeTool`。

替代方案：

- service 侧提供一个 `WsClient` 实现
- 直接构造 `WsBrowserBackend`
- 让 ws service 的 browser action 通过 `WsBrowserBackend` 执行

### 决策 2：pipe 路径保持原样

pipe 模式继续：

- handshake
- `session_key`
- `BrowserPipeTool`

不做语义调整，不引入兼容层，不改动已存在的验证路径。

### 决策 3：runner 只在 ws 调用面做最小接线

当前共享 task runner 复用已经存在，本次不做大重构。

策略是：

- 只在 ws service 用到的调用面，改成可使用 `WsBrowserBackend`
- 如果必须扩共享调用接口，则仅做**最小、兼容、对 pipe 零影响**的改动
- 任何涉及 pipe 行为变更的改动都不允许

### 决策 4：保留现有 browser websocket 连接生命周期

本次不重做连接管理架构。

继续维持：

- 单客户端
- 单任务串行
- 按现有 service 生命周期维护 browser websocket 连接

只替换认证错误的执行路径，不顺手做生命周期优化。

## 目标架构

### 目标调用链

```text
sg_claw_client
  -> sg_claw service
    -> ws-native browser backend
      -> browser_ws_url
        -> sgBrowser
```

### 与 pipe 的并行关系

```text
pipe mode:
  browser process <-> stdio/pipe <-> sgclaw::run() <-> BrowserPipeTool

ws mode:
  sg_claw_client <-> sg_claw service <-> WsBrowserBackend <-> sgBrowser websocket
```

两条路径并行存在，互不混用认证模型。

## 模块设计

### 1. `src/service/server.rs`

这是本次核心改动文件。

#### 当前职责

- 管理 service client websocket 收发
- 将 service 请求转入共享 runner
- 维护 service->browser 的 websocket 传输桥

#### 本次改动

- 将“service->browser 的桥”从 `Transport + BrowserPipeTool` 组合改为 `WsClient + WsBrowserBackend`
- 删除 ws service 路径中对空 `session_key` 的依赖
- 继续保留 service socket 生命周期与 session 状态机

#### 目标结构

可接受的目标形态：

- `ServiceBrowserWsClient`：实现 `WsClient`
- 内部继续维护真实 browser websocket 连接
- `serve_client(...)` 在处理任务时构造 `WsBrowserBackend`
- 共享 runner 或其 ws 调用包装层通过该 backend 执行 browser action

### 2. 共享 runner / ws 调用包装层

本次不要求把全项目统一改成 `BrowserBackend`。

但 ws service 模式必须能把 browser action 接到 `WsBrowserBackend`。

可接受的最小方案：

- 在 ws service 使用的一层引入一个只服务 ws 模式的 adapter
- 该 adapter 把 runner 所需的 browser 调用能力委托给 `WsBrowserBackend`

要求：

- pipe 现有调用签名不变，或即使扩展也必须保证 pipe 行为完全一致
- 不允许为了 ws 把 pipe 入口重写

### 3. `src/browser/ws_backend.rs`

原则上复用现有实现。

只有在以下情况下才允许最小补改：

- service 真实联调发现它缺一个 ws service 必需但当前未暴露的能力
- 该补改只服务 ws-native 路径
- 不影响现有测试语义

## 连接职责与边界

为避免 service 侧与 `WsBrowserBackend` 重复实现责任，本次显式约束如下：

### `WsBrowserBackend` 负责

- 单次 `invoke(...)` 的请求串行化
- 调用 `encode_v1_action(...)`
- 发送 websocket 文本帧
- 等待即时状态帧
- 如有 callback，等待 callback 帧并做名称匹配
- 将结果统一为 `CommandOutput`
- 按现有 `WsBrowserBackend` 语义产出 timeout / protocol 错误

### service 侧 `WsClient` 适配器负责

- 持有真实 browser websocket 连接
- 在第一次请求时建立到 `browser_ws_url` 的连接
- 把 `send_text(...)` / `recv_text_timeout(...)` 委托到真实 websocket
- 将底层关闭、reset、timeout 统一映射为既有 `PipeError` 语义
- 不实现 request/response correlation，不解析 browser ws 协议 payload

### 明确不允许

- service 侧继续手写 callback 轮询逻辑
- service 侧继续直接调用 `encode_v1_action(...)` 组包作为主路径
- 在 service 侧复制 `WsBrowserBackend` 的协议处理逻辑

这样可以保证：

- `src/service/server.rs` 只负责“连线”
- `src/browser/ws_backend.rs` 继续负责“ws 浏览器调用语义”

## 数据流设计

### 成功路径

1. `sg_claw_client` 向 `sg_claw` 发 `SubmitTask`
2. service 收到任务并进入共享 runner
3. 当 runner 需要浏览器动作时：
   - ws service 调用 `WsBrowserBackend.invoke(...)`
4. `WsBrowserBackend`：
   - 用 `MacPolicy` 校验动作
   - 用 `encode_v1_action(...)` 编码请求
   - 发往 `browser_ws_url`
   - 等待状态帧
   - 如有 callback，继续等 callback 帧
5. 结果返回到 runner
6. runner 继续执行并向 client 流式输出日志和 completion

### 失败路径

#### browser websocket 不可连

- 返回明确的 browser websocket connect 错误
- 不冒充认证错误

#### 浏览器返回非 0 状态

- 返回明确协议错误：`browser returned non-zero status`

#### callback 超时

- 返回 timeout

#### websocket 断开

- 返回 `PipeError::PipeClosed`
- 由 service 生命周期逻辑处理

#### 不再允许的错误

- `invalid hmac seed: session key must not be empty`

该错误在 ws 模式下应彻底消失。

## 失败语义

为便于测试与实现，ws-only 路径的 outward error 语义固定如下：

### browser websocket connect 失败

- outward: `PipeError::Protocol("browser websocket connect failed: ...")`

### 浏览器返回非 0 状态码

- outward: `PipeError::Protocol("browser returned non-zero status: ...")`

### callback 超时

- outward: `PipeError::Timeout`
- timeout 来源：沿用 `WsBrowserBackend` / ws service 当前 response timeout 配置，默认 30 秒

### websocket 被对端正常关闭或 reset

- outward: `PipeError::PipeClosed`
- 不允许使用“等价错误”这类不精确表述

### 本次必须消除的错误

- `invalid hmac seed: session key must not be empty`

任何 ws service 联调路径再出现该错误，都视为实现未完成。

## 测试设计

### 分层测试策略

为避免依赖 LLM/planner 的非确定性行为，本次测试必须分成两层，且各自断言不同目标：

#### A. backend / adapter 层测试（确定性）

这一层不经过 `sg_claw_client`、不经过真实模型规划，直接验证 ws-only 技术行为。

目标：

1. `ServiceBrowserWsClient` 与 `WsBrowserBackend` 的组合可以：
   - 发送 `Navigate`
   - 接收 `0` 状态
   - 在 callback 场景下读取 callback 文本
2. 当 fake browser server 主动关闭/reset 时：
   - 在 `WsClient` / `WsBrowserBackend.invoke(...)` 观察层断言 outward error 必须是 `PipeError::PipeClosed`
3. 当 fake browser server 不返回 callback 时：
   - 在 `WsBrowserBackend.invoke(...)` 观察层断言 outward error 必须是 `PipeError::Timeout`
4. 该层测试完全不依赖 LLM、planner、skills 路由

建议：

- 新增 focused ws service/backend test
- 输入动作固定为代码直接调用 `invoke(Action::Navigate, ...)` 等，而不是自然语言任务

#### B. client -> service 集成测试（链路验证）

这一层验证 ws-only 接线已经替换掉空 session key 路径，但不承担细粒度协议语义断言。

目标：

1. 通过真实 `sg_claw_client -> sg_claw service` 发起一个最小自然语言任务
2. fake browser websocket server 至少收到一个来自 ws-only 路径的文本帧
3. client/service 输出中不再出现：
   - `invalid hmac seed: session key must not be empty`
4. 该层只证明：
   - ws service 已不再走空 session key 的 pipe 认证路径
   - 真实端到端链路已能到达 browser websocket

该层不用于断言精确 enum 身份，也不用于覆盖 callback timeout / reset 细节。

### 新增红测 1：ws-only backend/adapter 基本调用可用

目标：

- 不走自然语言任务
- 直接构造 ws service 使用的 `WsClient` + `WsBrowserBackend`
- 调用固定动作：`Action::Navigate`，目标 url 固定为 `https://www.zhihu.com/hot`
- fake browser websocket server 返回 `0`
- 断言：
  - `invoke(...)` 成功
  - fake server 收到的首个文本帧可按 `ws_protocol` 语义解释为 `Navigate`

### 新增红测 2：ws-only backend/adapter 断链语义固定

目标：

- 不走自然语言任务
- fake browser websocket server 在接受请求后主动关闭或 reset
- 在 `invoke(...)` 观察层断言：
  - outward error 固定为 `PipeError::PipeClosed`

### 新增红测 3：ws-only backend/adapter callback timeout 语义固定

目标：

- 不走自然语言任务
- fake browser websocket server 返回 `0` 但不返回 callback 帧
- 在 `invoke(...)` 观察层断言：
  - outward error 固定为 `PipeError::Timeout`

### 新增红测 4：client->service 链路不再触发空 session key 错误

目标：

- 通过真实 `sg_claw_client -> sg_claw service` 链路触发浏览器动作
- 用 fake browser websocket 服务端接住请求
- 任务输入固定为：`打开知乎热榜并读取页面主区域文本`
- 断言 client/service 输出中不再出现：
  - `invalid hmac seed: session key must not be empty`
- 断言 fake browser server 至少收到了一个文本帧

### 回归测试

必须重新运行并保持通过：

#### pipe 回归

```bash
cargo test --test pipe_handshake_test -- --nocapture
```

如实现涉及 browser tool 上层接线，还需补跑：

```bash
cargo test --test browser_tool_test --test compat_browser_tool_test --test runtime_task_flow_test -- --nocapture
```

#### ws 回归

```bash
cargo test --test service_ws_session_test --test service_task_flow_test --test browser_ws_protocol_test --test browser_ws_backend_test -- --nocapture
```

## 手工验收

使用真实配置和真实已启动 sgBrowser：

1. 启动 sgBrowser，并确保 `browserWsUrl` 可用
2. 启动 `sg_claw`
3. 运行：
   - `sg_claw_client`
4. 发送知乎最小任务：
   - 打开知乎热榜并读取页面主区域文本
5. 观察：
   - 不再出现 `invalid hmac seed`
   - 出现真实 browser action 日志
   - 能返回单次 completion
6. 再运行旧知乎 skill：
   - `读取知乎热榜数据，并导出 excel 文件`
7. 验证旧知乎 skill 进入真实 browser 执行路径
8. 最后确认 legacy pipe 入口仍可启动（仅验证，不允许为此修改 pipe 实现）

## 风险

### 风险 1：ws service 与共享 runner 接口耦合过深

控制：

- 只在 ws 使用面做 adapter
- 不对 pipe 主入口做结构性改造

### 风险 2：为适配 ws-native backend 误改 pipe 调用链

控制：

- 所有 pipe 回归必须在每轮修改后重跑
- `src/lib.rs` 不允许改行为

### 风险 3：ws service 内联连接逻辑与 `WsBrowserBackend` 责任重复

控制：

- 本次先以最小变更消除认证阻塞
- 不顺手做大规模整理

## 通过标准

满足以下全部条件才算完成：

1. ws service 路径不再依赖空 session key
2. 不再出现 `invalid hmac seed: session key must not be empty`
3. 真实 browser websocket 请求能发到 sgBrowser/fake browser server
4. 旧知乎 skill 至少能进入真实 browser action 执行链路
5. pipe 模式零回归
6. 所有新增/相关测试通过

## 实施建议

按以下顺序实施：

1. 先补红测，锁定“ws 不再触发 invalid hmac seed”
2. 再把 ws service 路径切到 `WsBrowserBackend`
3. 跑 ws 测试
4. 跑 pipe 回归
5. 做真实知乎最小任务 smoke
6. 再做旧知乎 skill smoke
