# OpenAI4S 源码分析 · Code-as-Action 科学研究 Agent

> **分析对象**：[PKU-YuanGroup/OpenAI4S](https://github.com/PKU-YuanGroup/OpenAI4S)（Open AI for Scientist）  
> **基线 commit**：`85e9fa0`（`Feat/retrosynthesis workflow triage #70`，`85e9fa062c354b2ea9a40a668f03669a5452e56d`）  
> **许可**：MIT  
> **产品一句话**：用火山方舟 / 豆包 ¥9.9 套餐复刻 Claude Science 体验的**开源混合式科研智能体**——原生 JSON 工具做编排与权限控制平面，持久 Python/R 内核做科学执行平面；核心零第三方依赖（pure stdlib）。  
> **读者对象**：已经读过本系列 Claude Code / Open Design / Codex / OpenManus 至少一份分析的产品经理与资深工程师；希望把「科研 Agent」从口号落到可核对源码的人。  
> **配套教程**：[从零构建OpenAI4S-开发全流程教程.md](./从零构建OpenAI4S-开发全流程教程.md) · [HTML](./从零构建OpenAI4S-开发全流程教程.html)  
> **本地基线路径**：`参考项目/OpenAI4S @ 85e9fa0`

---

## 读前约定

| 约定 | 说明 |
|---|---|
| 引用格式 | 重大主张尽量落到 `文件:行号` 或至少 `文件` + 符号名，便于回源码核对 |
| 术语 | **控制平面** = provider-native JSON Tool；**科学平面** = Python/R Cell；**Host** = 内核内 `host` 单例背后的编排信封 |
| 完成信号 | 非科学回合 → Engine 自有 `finalize_response`；科学 Python Cell → 唯一内部完成信号 `host.submit_output(...)` |
| HTML | 配套站：`OpenAI4S-解析.html`（由 `build-html.py` 生成） |

## 目录

**Part I · 它是什么**  
1. [一句话定位：JSON 控制平面 + Python/R 科学平面](#ch1)  
2. [仓库地图与技术栈](#ch2)  
3. [与 Claude Science / CodeAct / ReAct 的关系](#ch3)  

**Part II · 双平面混合引擎**  
4. [AgentEngine 外循环](#ch4)  
5. [route_action 路由铁律](#ch5)  
6. [finalize_response vs host.submit_output](#ch6)  
7. [Action Ledger 与 Store SQLite 概览](#ch7)  

**Part III · Host · Kernel · Tools**  
8. [host singleton 与 host_dispatch 编排信封](#ch8)  
9. [LazyKernel、沙箱与环境 allowlist](#ch9)  
10. [tools/ 类目录与只读并行波次](#ch10)  
11. [llm/：ark\|openai\|anthropic\|gemini over urllib](#ch11)  

**Part IV · Web 工作台**  
12. [serve → gateway.SessionRunner](#ch12)  
13. [WebSocket `/api/v1/ws` 事件面](#ch13)  
14. [一次用户提示词的完整路径（核心时序）](#ch14)  
15. [Notebook 只读默认、Artifacts、Branch/Revert](#ch15)  

**Part V · Skills · Compute · Security**  
16. [Skills：34 份代码食谱，非 JSON schema](#ch16)  
17. [BYOC 与 `openai4s_compute_provider`](#ch17)  
18. [权限、审批持久化、biosecurity、egress](#ch18)  

**Part VI · 内核深挖与开发者品味**（本章是全文重心之一）  
19. [内核协议：dup2、帧上限与「错误比没有更糟」](#ch19)  
20. [十条开发选择：反推搭建思维与品味](#ch20)  

**Part VII · 横向对比与总结**  
21. [对比表：Claude Code \| Open Design \| Codex \| OpenManus \| OpenAI4S](#ch21)  
22. [五个独特设计决策与诚实边界](#ch22)  
23. [源码导览索引](#ch23)  

---

# Part I · 它是什么

<h2 id="ch1">第 1 章 一句话定位：JSON 控制平面 + Python/R 科学平面</h2>

### 1.1 它不是又一个 ReAct 编程助手

OpenAI4S（Open AI for Scientist）把自己钉在一个很窄、也很硬的产品位上：

> **原生 JSON Tool Call 负责编排与权限；持久 Python/R Code-as-Action 内核负责科学执行。**

官方中文 README 的口号是「💸 9.9 元豆包 API 复刻 Claude Science」（`README_zh.md:7-10`）。工程上它不是「Claude Code 的科研皮肤」，也不是「再写一个带 shell 工具的 ReAct」。它刻意拆成**两个永不在同一步竞争的动作通道**（`docs/architecture.md:3-20`）：

| 平面 | 动作单元 | 适合干什么 | 不适合干什么 |
|---|---|---|---|
| **JSON 控制平面** | 一个有序原生工具批次，或单独的 `FinalizeAction` | 权限、元数据、外部服务、工作流控制、会话分叉/回退 | 十万行 DataFrame 分析、仿真循环、图绘制流水线 |
| **Python/R 科学平面** | **恰好一个**完整 fenced Cell | 计算、探索、分析、仿真、长时任务；Python 可中途同步 `host.*` RPC | 把「读文件 / 搜网页 / 改权限」拆成十几次 tool_use 往返 |

README 里那张对照表写得很直白（`README_zh.md:52-68`）：同样「找匹配文件 → 排序 → 读 CSV → 画图」的活，ReAct 可能要 ~14 次往返；OpenAI4S 压成**一个代码 cell**，大对象留在内核内存，上下文里只剩一句 `"<DataFrame 100000×20>"`。

### 1.2 产品经理视角：它在卖什么体验

| 卖点 | 工程落点 |
|---|---|
| ¥9.9 豆包 / 火山方舟 | `llm/capabilities.py:206-221` 内置 `ark` provider，`wire="openai"`，默认 `doubao-seed-2.0-pro` |
| Claude Science 级科研工作台 | 静态 WebUI（无打包）+ Notebook 投影 + 版本化 Artifacts + 审批卡 |
| 开源、可自托管 | MIT；daemon 默认绑 `127.0.0.1:8760` |
| 核心不拖科学栈 | `pyproject.toml:9` `dependencies = []`；numpy/pandas 走 optional `science` extra |

> 🧠 **一句话**：别家在优化「怎么把 bash / 编辑器工具调得更聪明」；OpenAI4S 在优化「怎么让模型写完一段真的科学代码，并在内核里把状态留下」。

```mermaid
flowchart LR
    U["科学家 / 用户提示"] --> CP["① JSON 控制平面<br/>permissions · metadata · web · MCP · remote"]
    U --> SP["② Python/R 科学平面<br/>persistent kernel · host RPC · artifacts"]
    CP -->|"route 铁律：native > finalize > one cell"| ENG["AgentEngine 外循环"]
    SP --> ENG
    ENG --> DONE["完成信号<br/>finalize_response 或 host.submit_output"]
```

---

<h2 id="ch2">第 2 章 仓库地图与技术栈</h2>

### 2.1 数字化的项目形状（基线 85e9fa0）

| 指标 | 量级（约） |
|---|---|
| 包内 Python | `openai4s/` ≈ **255** 个 `.py`，约 **114k** LOC |
| 测试 | `tests/` ≈ **286** 个 `test_*.py`（默认离线门禁） |
| WebUI | `server/webui/app.js` ≈ **9.7k** 行；无 bundler |
| Gateway | `server/gateway.py` ≈ **12.9k** 行（HTTP + 手写 WS 组合门面） |
| 内置 Skills | **34** 个 `skills/*/SKILL.md` |
| 控制工具 | `TOOL_TYPES` **70** 个 `Tool` 子类（`tools/registry.py:118-189`） |
| 核心依赖 | **0**（`pyproject.toml:9`） |

```
OpenAI4S/                          # commit 85e9fa0 · MIT
├── openai4s/                      # 产品主体（stdlib）
│   ├── agent/                     # 外循环：engine · actions · finalize · ledger · loop
│   ├── kernel/                    # 科学平面：manager · worker · lazy · r_kernel · sandbox 对接
│   ├── host/ + host_dispatch.py   # Host 能力服务 + 共享编排信封
│   ├── sdk/host.py                # 注入到 Python 内核的 host 单例门面
│   ├── tools/                     # 70 个 native Tool 子类 + registry
│   ├── llm/                       # urllib 传输 + openai/responses/anthropic/gemini 线
│   ├── server/                    # gateway · agent_run · branching · webui/
│   ├── security/                  # sandbox · permissions · biosecurity · injection
│   ├── skills_loader/             # SKILL.md 发现与渐进披露
│   ├── store.py + storage/        # 单连接 SQLite + 仓储
│   ├── compute/                   # BYOC 主机侧
│   ├── egress.py                  # 出站域名 allowlist
│   └── …
├── openai4s_compute_provider/     # 远端 GPU 上跑的 stdlib 沙箱 SDK
├── openai4s_worker_runtime/       # worker 运行时附属包
├── skills/                        # 34 份代码食谱（非 JSON tool schema）
├── envs/                          # conda 环境配方
├── workflows/                     # 科学工作流 benchmark 清单
├── harness/                       # 脚本化场景 / golden（在生产 import 图之外）
├── docs/                          # architecture · security · compute · webapp…
├── pyproject.toml                 # dependencies=[] ；science/chemistry extras
├── setup.sh / start.sh
└── tests/                         # 离线默认；external/network/live_llm 需显式 -m
```

### 2.2 技术栈选择：为什么是「纯标准库」

| 层 | 选型 | 源码锚点 |
|---|---|---|
| 包管理 / 运行 | `uv` + `setuptools` | `pyproject.toml`；`./setup.sh`、`./start.sh` |
| Agent 核心 | 纯 Python stdlib | `agent/engine.py` 无第三方 import |
| LLM 客户端 | `urllib.request` | `llm/transport.py:27-28` |
| HTTP 服务 | `http.server` | `server/gateway.py` 模块头注释 |
| WebSocket | **手写**帧编解码 | `gateway.py` 顶部：`GET /api/v1/ws` |
| 前端 | 静态 `app.js` / `index.html` / `style.css` | 无 build step（`CLAUDE.md:100`） |
| 持久化 | SQLite 单连接 | `store.py` |
| 可选科学栈 | `uv sync --extra science` | `pyproject.toml:36-41`（numpy/pandas/matplotlib/sklearn） |
| 可选化学 | `--extra chemistry`（RDKit） | `pyproject.toml:45-47` |

硬约束写在 `CLAUDE.md:90`（核查修正 2026-08-04：原稿写 :11，但所引 "Never add a hard third-party import to the core" 这句真实位置是 :90；:11 是相关但不同的一句 "Core is zero-dependency by design"）与 `pyproject.toml:9`：**Never add a hard third-party import to the core.** 科学库可以出现在 agent *cells* 里（内核继承 venv），但引擎本身必须 `try/except ImportError` 守护每一次 in-tree 使用。

### 2.3 两个入口，同一台引擎

```mermaid
flowchart TB
    subgraph CLI["CLI 路径"]
        RUN["openai4s run '…'"] --> AGENT["agent/loop.py · Agent.run"]
        AGENT --> LK["LazyKernel"]
        AGENT --> LAE["LocalActionExecutor"]
    end
    subgraph WEB["Web 路径"]
        SERVE["openai4s serve / start.sh"] --> GW["server/gateway.py"]
        GW --> SR["SessionRunner._loop"]
        SR --> WAE["WebActionExecutor"]
        SR --> WES["WebEventSink"]
    end
    AGENT --> ENG["AgentEngine.run while"]
    SR --> ENG
    ENG --> RA["route_action"]
```

CLI 用 `Agent.run`（`agent/loop.py:352-447`）组装 `LazyKernel` + `LocalActionExecutor` + `ChatModel`；Web 用 `SessionRunner._loop`（`gateway.py:6337-6447`）组装同一 `AgentEngine`，只是把执行器与事件投影换成 Web 版本。这是全文最重要的架构对称性：**一个引擎，两层薄适配器**（`actions.py:3-6` 自称 CoreCoder-style）。

---

<h2 id="ch3">第 3 章 与 Claude Science / CodeAct / ReAct 的关系</h2>

### 3.1 三组对照

| 范式 | 典型形态 | OpenAI4S 怎么站队 |
|---|---|---|
| **ReAct** | Thought → Action(tool) → Observation，工具原子、无中途回调 | 控制平面借用其「结构化工具」；科学平面**拒绝**把分析拆成原子 tool |
| **CodeAct** | 模型主要产出可执行代码 | 科学平面就是 Code-as-Action；但外循环仍保留 JSON 控制通道 |
| **Claude Science（闭源）** | 科研工作台 + 强模型 + 闭源运行时 | 产品体验对标；实现上用开源双平面 + ¥9.9 Ark 替代 |

### 3.2 关键主张：为什么「没有注册 shell 工具」

`tools/registry.py:115-117` 与 `:192` 写死：

- 故意 **NO shell tool**；
- `_FORBIDDEN_CONTROL_NAMES = frozenset({"bash", "submit_output"})`；
- `register_tool` 遇到它们直接 `ValueError`（`:211`）。

Shell 只能发生在 Python 内核里：`host.bash(...)`（`sdk/host.py:854-865`）。这不是风格偏好，而是平面分离的承重墙——一旦 shell 变成 JSON tool，模型就会退回 ReAct 式「一步一命令」，十万行分析再次被拆碎。

### 3.3 与「纯 tool_use」的关键差分：中途 Host RPC

`docs/architecture.md:56`：

> This inner RPC loop does not exist in a `tool_use` architecture — there, actions are atomic and never call back into the host mid-execution.

Python Cell 执行中，`host.llm` / `host.delegate` / `host.compute` 走独立于 stdout 的 `host_call → host_ack → host_response` 通道；Cell 阻塞、Host 服务、Cell 恢复。这是 Code-as-Action 相对 ReAct 的**结构性**优势，不是提示词技巧。

```mermaid
sequenceDiagram
    participant M as Model
    participant E as AgentEngine
    participant K as Python Kernel
    participant H as HostDispatcher
    M->>E: assistant reply (fenced python)
    E->>K: execute one CodeCell
    K->>H: host_call(llm / compute / …)
    H-->>K: host_response
    K->>H: host.submit_output(...)
    H-->>E: completion signal
    E-->>M: stop_reason=submitted
```

---

# Part II · 双平面混合引擎

<h2 id="ch4">第 4 章 AgentEngine 外循环</h2>

### 4.1 状态机本体只有 ~143 行

`AgentEngine`（`agent/engine.py:34-143`）是 provider-neutral 的外循环。它**不** import 具体 kernel、dispatcher、store、server——那些都是 ports，由 CLI / Web 适配器注入（`docs/architecture.md:58-59`）。

核心 `run` while（`engine.py:60-108`）可以读成：

```
state ← RunState(messages, max_turns)
emit RunStarted
while turn < max_turns:
    if cancelled → finish("cancelled")
    if completion.completion() → finish("submitted")   # 例如已有 submit_output
    prepare context → model.complete(stream deltas)
    append assistant message
    action ← route_action(content, tool_calls)
    outcome ← executor.execute(action, reply, state)
    append history_messages; turn++
    if outcome.stop_reason → finish(that)
    if outcome.completion or completion port → finish("submitted")
finish("max_turns")
```

默认 `max_turns=32`（`engine.py:47`）；Web 会话常从 `cfg.max_turns` 取（gateway 侧默认常见为 12，explore 可抬高，`gateway.py:6349-6351`）。

### 4.2 端口化的好处

| Port | 作用 | 默认实现 |
|---|---|---|
| `ModelPort` | 调模型 | CLI/Web 的 `ChatModel` |
| `ActionExecutor` | 执行 route 出的动作 | `LocalActionExecutor` / `WebActionExecutor` |
| `ContextPolicy` | 压缩上下文 | `CompactionPolicy` / `PassthroughContext` |
| `EventSink` | 投影事件 | Transcript / `WebEventSink` / Null |
| `CancellationPort` | Stop | `NeverCancelled` / `EventCancellation` |
| `CompletionPort` | 侦测 `submit_output` | `CompletionSignal(dispatcher.last_output)` |

> 🧠 **产品含义**：测试可以在不启动 daemon、不 spawn 内核的情况下，单独验证「取消 / 最大回合 / finalize / 路由优先级」。这是工程成熟度信号，不是过度抽象。

```mermaid
stateDiagram-v2
    [*] --> Running: RunStarted
    Running --> ModelCall: TurnStarted
    ModelCall --> Routing: ReplyReceived
    Routing --> Executing: ActionRouted
    Executing --> Running: OutcomeProduced (continue)
    Executing --> Submitted: stop_reason / completion
    Running --> Cancelled: cancellation
    Running --> MaxTurns: turn == max_turns
    Submitted --> [*]
    Cancelled --> [*]
    MaxTurns --> [*]
```

---

<h2 id="ch5">第 5 章 route_action 路由铁律</h2>

### 5.1 单通道决策

`route_action`（`agent/actions.py:165-185`）是双循环共用的**唯一**动作决策点：

```python
# 伪代码忠实于 actions.py:176-185
calls = normalize(tool_calls)
if len(calls) == 1 and calls[0].name == "finalize_response":
    return FinalizeAction(calls[0])
if calls:
    return NativeToolBatch(calls)          # 任意原生调用压过代码
return extract_action(content)             # 第一个完整 python/r fence
```

铁律可以背成三句：

1. **有结构化原生调用 → 绝不跑代码**（控制平面不能和科学平面抢同一回合）；
2. **唯一且名为 `finalize_response` → FinalizeAction**（混在其他 tool 里不算完成）；
3. **否则 → 文档顺序上第一个完整的 python/r Cell**（一步一格；未闭合 fence 不可执行）。

`FINALIZE_RESPONSE_NAME` 字面量放在 routing 边界（`actions.py:40-43`），刻意不 import registry——避免 actions 模块沾上工具注册副作用。

### 5.2 动作类型代数

| 类型 | 定义位置 | 含义 |
|---|---|---|
| `CodeCell` | `actions.py:46-51` | `language ∈ {python,r}` + code |
| `NativeToolCall` | `:55-71` | 无损保留 raw_arguments / parse_error / provider_meta |
| `NativeToolBatch` | `:74-78` | 有序元组 |
| `FinalizeAction` | `:81-90` | Engine 自有终态声明 |
| `Action` | `:93` | 以上三者的并集 |

`extract_action`（`:146-162`）只认：

- Python：`info ∈ {"", "python", "py"}`（裸 ``` 默认 python）；
- R：必须显式 `` ```r ``。

### 5.3 隐藏「纯完成 Cell」

`is_completion_only_cell`（`actions.py:96-143`）用 AST 判断：若 Python Cell **整段**只是 `host.submit_output(...)` 且参数里没有嵌套计算，则 Web Notebook **隐藏**该格——完成是真实 RPC，但不是科学分析，不应污染只读 Notebook（`docs/architecture.md` / CLAUDE.md 用户可见完成投影约定）。

```mermaid
flowchart TD
    R["Model reply"] --> Q1{"有 native tool_calls?"}
    Q1 -->|否| EX["extract_action → 至多一个 Cell"]
    Q1 -->|是| Q2{"恰好 1 个且名=finalize_response?"}
    Q2 -->|是| F["FinalizeAction"]
    Q2 -->|否| B["NativeToolBatch（整批）"]
    EX --> C["CodeCell | None"]
    B --> NOTE["代码 fence 即使存在也被忽略"]
```

---

<h2 id="ch6">第 6 章 finalize_response vs host.submit_output</h2>

这是整份分析里最容易被「工具列表截图」误导的一章。

### 6.1 finalize_response：Engine 自有，不是 Tool 注册项

`agent/finalize.py:1-12` 开宗明义：

> `finalize_response` is deliberately **not** a control-plane `Tool` and is **never** registered in `openai4s.tools.registry`.

它做什么：

| 机制 | 位置 | 行为 |
|---|---|---|
| 封闭 schema | `finalize.py:35-83` | 要求 `summary` + `completion_bullets`；可选 findings/metrics/artifacts… |
| 提供给模型的 spec | `finalize_response_tool_spec()` `:92-111` | metadata-only `ToolSpec` |
| 拼进工具目录 | `with_finalize_response()` `:114-123` | 追加 spec；若目录里已有同名 → **抛错**（防止插件冒充终态） |
| 路由 | `route_action` | 仅当它是**唯一** native call 时变成 `FinalizeAction` |

科学 Python Cell **不能**用它替代内部完成：`description` 明文写了 *does not replace host.submit_output*（`:106-107`）。

### 6.2 host.submit_output：Cell 内唯一完成信号

`sdk/host.py:803-827`：

```text
host.submit_output(output, completion_bullets, output_schema=None)
→ HostDispatcher CompletionService
→ dispatcher.last_output
→ AgentEngine CompletionPort 侦测到 → stop_reason="submitted"
```

约束：`completion_bullets` 必须 1–4 条「已完成动作」短语；可选 `output_schema` 校验失败则 soft-fail `{"error":...}` 让模型重试。

### 6.3 对照表（务必记住）

| | `finalize_response` | `host.submit_output` |
|---|---|---|
| 谁拥有 | Engine（`finalize.py`） | Kernel 内 Host RPC |
| 是否在 `TOOL_TYPES` | **否** | **否**（且禁止注册） |
| 何时用 | 对话 / 纯工具回合收尾 | 科学 Python Cell 收尾 |
| 能否与其它 tool 同回合 | **不能**（必须 sole） | N/A（在代码里调用） |
| R Cell | 不从 R 内发出 | R **无** Cell 内完成信号 |
| 普通 prose / 最大回合 | **不是**完成 | **不是**完成 |

```mermaid
flowchart LR
    subgraph NonSci["非科学回合"]
        T["Native tools…"] --> F["sole finalize_response"]
        F --> CR["CompletionRecord"]
    end
    subgraph Sci["科学 Python Cell"]
        CELL["```python …"] --> SO["host.submit_output"]
        SO --> CR2["CompletionRecord 同源结构"]
    end
    CR --> UI["Gateway 投影：output + bullets + artifact delta"]
    CR2 --> UI
```

---

<h2 id="ch7">第 7 章 Action Ledger 与 Store SQLite 概览</h2>

### 7.1 Ledger：先记账，再执行

`agent/ledger.py` 的模块文档（`:1-12`）说明设计目标：

- storage 仓储**不懂** agent 语义；
- `RuntimeActionLedger` 把 typed `AgentEngine` 事件翻译成不可变 groups/events；
- Native 声明与结果**原子归约**；崩溃时用规范错误/取消结果补齐半开 batch，**绝不**把半开 tool batch 回灌给 LLM。

CLI 在 `Agent.run` 里构造 ledger（`loop.py:405-409, 463+`）；Web 在 `SessionRunner._loop` 把 `action_ledger` 传入 `WebEventSink`（`gateway.py:6343-6377`）。

引擎是 **ledger-first**（`docs/architecture.md:59-64`）：打开 append-only action group → 执行 → 以 tool result / Cell milestone 关闭 → 终态追加而非从 UI transcript 反推。

### 7.2 Store：一进程一连接的真相源

`store.py` 持有唯一 SQLite 连接与 schema/migration。基线可见的核心表包括（节选，`store.py:114+` 与 `storage/`）：

| 表 | 职责 |
|---|---|
| `frames` | 会话 / turn 帧（树状深度、模型、token） |
| `messages` | 聊天消息投影 |
| `execution_log` | 执行日志 |
| `artifacts` / `artifact_versions` | 版本化产物 |
| `lineage_edges` | 数据血缘 |
| `host_call_log` | Host RPC 审计 |
| `permission_rules` / `permission_requests` | 持久审批 |
| `plans` | 计划模式 |
| `annotations` / `annotation_admissions` | 图像批注与客户端 admission |
| `compute_jobs` / `compute_job_events` | 远程算力作业 |
| `session_branches` / `session_checkpoints` | 分支与检查点（`storage/snapshots.py`） |
| `kernel_generations` | 内核世代（`storage/kernels.py`） |
| `recovery_journal` | 恢复日志 |
| `settings` / `connectors` / `memories` / `shares` | 配置、连接器、记忆、分享 |

Agent 侧通过 `host.query` **只读**暴露 SQL（CLAUDE.md / architecture），写路径一律走服务与仓储。

```mermaid
erDiagram
    frames ||--o{ messages : contains
    frames ||--o{ artifacts : produces
    artifacts ||--o{ artifact_versions : versions
    artifact_versions ||--o{ lineage_edges : lineage
    frames ||--o{ permission_requests : asks
    frames ||--o{ session_checkpoints : checkpoints
    session_branches ||--o{ session_checkpoints : head
    frames ||--o{ kernel_generations : generations
    frames ||--o{ compute_jobs : remote
```

---

# Part III · Host · Kernel · Tools

<h2 id="ch8">第 8 章 host singleton 与 host_dispatch 编排信封</h2>

### 8.1 内核里的 `host`

`sdk/host.py`（约 1177 行）是注入 Python worker 的兼容门面。架构文档列出的能力面（`docs/architecture.md:77-88`）包括：

- 网络：`web_search` / `web_fetch` / `web_download`
- 文件系统：workspace-jailed 的 read/write/edit/grep/glob/list
- 模型与子代理：`llm` / `delegate` / `collect`
- 科学 API：`science.*`
- 远程算力：`compute.*` / `fold`
- 产物：`save_artifact` / `artifacts` / `view_image`
- 技能 / 环境 / MCP / 只读 SQL
- **完成**：`submit_output`
- **Shell**：`bash`（仅内核内）

### 8.2 HostDispatcher：共享编排信封，不是上帝类实现桶

`HostDispatcher`（`host_dispatch.py:646+`）注释写清定位：

> Backs control tools and worker host.* RPC. One instance per session.

`__call__(method, args)`（`:996`）是统一入口。能力实现拆到 `openai4s/host/*` 服务：files、llm、completion、data、delegation、remote_science、progress、skills、mcp、endpoints、credentials…（模块 import 区 `:26-51`）。

**软失败契约**：handler 可返回单键 `{"error": msg}`；worker 转成 `RuntimeError`。未捕获异常由 manager 同样压成 error 字典上线。

控制平面的 native Tool `execute()` 与内核 `host.*` **共用同一 dispatcher**——权限、egress、注入筛查、activity step、审计日志共享（`docs/architecture.md:142-147`）。

```mermaid
flowchart TB
    subgraph Surfaces["调用表面"]
        NT["Native Tool.execute"]
        HC["kernel host.* RPC"]
    end
    NT --> HD["HostDispatcher.__call__"]
    HC --> HD
    HD --> PERM["permissions / approval"]
    HD --> AUD["audit / replay / injection"]
    HD --> SVC["host/* services"]
    SVC --> STORE[(SQLite Store)]
    SVC --> LLM["llm.chat"]
    SVC --> NET["webtools + egress"]
```

---

<h2 id="ch9">第 9 章 LazyKernel、沙箱与环境 allowlist</h2>

### 9.1 LazyKernel：工具回合不白白 spawn

`LazyKernel`（`kernel/lazy.py:15-22`）：

> Create a worker only when code first needs an interpreter. Control-tool and structured-finalization turns can carry this object without spawning a process.

`Agent.run` 组装方式（`loop.py:385-389`）：factory + skill bootstrap + foreground publish。属性 `spawned` / `generation` / `execute` / `shutdown` 都围绕「线程安全的一次性所有权」。

R 内核是兄弟通道：`r_kernel.py` + `r_worker.R`，同一 manager 协议；通过 fd3/fd4 走帧，避免 print 污染协议（CLAUDE.md）。

### 9.2 OS 沙箱：Seatbelt / bubblewrap

`security/sandbox.py` 头注释（`:7-8`）：

- macOS → `sandbox-exec`（Seatbelt）
- Linux → `bwrap`（bubblewrap）

模式：`OPENAI4S_KERNEL_SANDBOX=auto|enforce|off`。`auto` 自检失败则**可见降级**；`enforce` 失败即关闭。写入限制在 workspace/private temp；默认拒绝对外原始网络（architecture / CLAUDE）。

### 9.3 子进程环境 allowlist

spawn 时 worker 环境从**严格 allowlist 重建**，而不是 `os.environ.copy()`——防止 daemon 的 provider/API/cloud secrets 与 loader 注入变量泄漏进 Python/R 及其子进程（`docs/architecture.md:111-113`，`kernel/environment.py`）。

### 9.4 host.bash 的一击令牌

CLAUDE.md 强调：`host.bash` 仍在内核本地执行，但子进程启动需要绑定 command hash、cwd、active worker generation、challenge 的一次性 Host token；**Host 授权/审计，永不在 daemon 进程里执行 shell**。

```mermaid
flowchart LR
    CELL["Python Cell"] --> BASH["host.bash(cmd)"]
    BASH --> TOK["one-shot Host token"]
    TOK --> AUTH["BashAuthorizationService"]
    AUTH -->|ok| SUB["subprocess inside worker"]
    AUTH -->|deny| ERR["soft error"]
    SUB --> SANDBOX["Seatbelt / bwrap wrapper"]
```



### 9.5 内核协议的硬纪律（读 worker 注释才能懂的品味）

`kernel/worker.py` 文件头不是装饰性 docstring，而是一份**协议设计说明书**。把它当成「开发者在怕什么」来读：

| 机制 | 怕什么 | 做法 |
|---|---|---|
| **dup2 换轨** | C 扩展 / 杂讯 `print` 污染协议线 | 真协议 stdin/stdout 挪到高位不可继承 fd，并发布在 `sys._openai4s_protocol_*`；`dup2(2,1)` 让 fd1 别名到 stderr（`worker.py:86-119`） |
| **两把锁** | 帧交错 / 读到别人的 response | `_PROTOCOL_WRITE_LOCK`（写帧）+ `_HOST_CALL_LOCK`（整段 host_call 事务）（`worker.py:12-15`） |
| **15MB host_call 上限** | 一次 RPC 拖垮 daemon | `_HOST_CALL_WIRE_CAP = 15_000_000` |
| **stdout 分块 64KB** | `print("x"*2e8)` 一次变 200MB JSON 行 | `_MAX_CHUNK_CHARS = 64_000`（`worker.py:47-51`） |
| **帧字节上限按 UTF-8 最坏情形推导** | CJK / emoji 把「字符上限」撑破「字节上限」 | `_JSON_WORST_BYTES_PER_CHAR = 12`（代理对）；注释明确写：六字节推演被测试打脸（`worker.py:52-77`） |
| **SIGINT 纪律** | 用户 `raise KeyboardInterrupt` 与真信号混淆 | one-shot handler + `_in_user_code` + `_sigint_delivered` |
| **crash recovery** | 读阻塞中 fd 身份漂了 | 每次读前后核对 `(st_dev, st_ino)`；预算一次 `os.dup(reserve)` 重建 |

```mermaid
flowchart LR
    subgraph Worker["worker.py 进程"]
      CODE["用户 Cell / C 扩展"] -->|fd1 已 alias| STDERR["stderr 捕获"]
      HOST["host.* SDK"] -->|高位 fd| PROTO["protocol JSON lines"]
      HOST --> LOCK["_HOST_CALL_LOCK"]
      LOCK --> CALL["host_call 帧"]
    end
    subgraph Manager["manager.py"]
      CALL --> READ["单 reader 循环"]
      READ --> DISP["HostDispatcher"]
      DISP --> RESP["host_response"]
      RESP --> LOCK
    end
```

**品味观察**：他们不怕把协议写成「难读」——他们怕的是**静默错乱**。宁可注释写满「为什么这个数字是 12 不是 6」，也不愿意让一条 CJK 输出把 `error_lineno`/`usage` 整帧丢掉。

### 9.6 Artifact 环境指纹：绑定 kernel generation，不是 daemon

CLAUDE.md 写得很重的一句话（大意）：

> Artifact 的环境 provenance 来自**产生文件的那个内核 generation**，不是 daemon 进程的零参数冻结。曾经把 R Cell 产物盖上 Python 包列表——**错误的 provenance 比没有更糟，因为它会被相信**。

这是整份代码库反复出现的认识论：

| 坏味道 | OpenAI4S 的纠正 |
|---|---|
| 静默成功 / 假绿 | harness：声明应失败的场景若成功则判失败 |
| 假 provenance | 记「为何缺失」而不是借 daemon 的包列表 |
| stub 形状进契约 | `stubbed_backend` marker 暂停 schema recorder |
| 审批重启后重放参数 | 只记决议，要求 Continue/replan |

### 9.7 R 通道的「同协议、不同完成语义」

R 复用同一 `Kernel` manager 与帧合同，但：

- 协议走 fd3/fd4，杂讯进 stderr（shell 重定向等价于 Python 的 dup2）；
- **没有** mid-cell `host` RPC，也没有 Cell 内 `submit_output`；
- 完成叙事仍偏 Python finalize / 后续 Python cell。

**品味**：不是「两个并列完整运行时」，而是「一个科学平面主通道 + 一个统计/绘图兄弟通道」。产品诚实标注差异，而不是假装对称。


---

<h2 id="ch10">第 10 章 tools/ 类目录与只读并行波次</h2>

### 10.1 TOOL_TYPES：70 个具名 Tool 子类

`tools/registry.py:118-189` 是唯一组合根。按域粗分：

| 域 | 代表工具 |
|---|---|
| 文件 | ListDirectory / ReadTextFile / WriteFile / Glob / ContentSearch / Edit |
| 环境 | EnvList / EnvUse / EnvCreate |
| Web | WebSearch / WebDownload / WebFetch |
| 科学库 | ScienceListDatabases / ScienceSearch（背后归一化 UniProt/PDB/…） |
| Skills | Search / Load / Status / History / Rollback |
| Artifacts | List / Metadata / Versions / Save / Restore |
| 会话控制 | Query / Frames / Lineage / Todos / Plan / Review / Checkpoint / Fork / RevertPreview / Permissions |
| 委托 | Delegate / ListChildren / Collect / Stop / SendMessage |
| 后台执行 | Submit / List / Peek / Interrupt |
| MCP | ListServers/Tools/Resources/Prompts + Call |
| 网络放行 | RequestNetworkAccess |
| 远程算力 | RemoteGPU / Register / Submit / Status / Result / Cancel / Close |
| 动态工具 | Define / List / Promote / Versions / Activate / Rollback |

**没有**：`bash`、`submit_output`、`finalize_response`。

### 10.2 只读并行波次

`agent/control.py`：

- `tool_parallel_policy`（`:152+`）识别只读调度；
- `_execute_read_only_waves`（`:167+`）按资源键冲突分波；
- 第一个 mutating / unknown 调用是**屏障**，之后串行；
- 结果按 provider **原始顺序**写回历史（`docs/architecture.md:149-153`）。

这让「连读多个文件 / 多库检索」降延迟，又不破坏 canonical tool group 顺序。

### 10.3 科学库不膨胀 tool 数量

只有 `science_list_dbs` + `science_search` 两个控制工具；连接器服务在背后归一化多个公共数据库，并附带 provenance envelope（时间戳 + 请求 + SHA-256）（architecture 后半）。Cell 内对应 `host.science.*`。

---

<h2 id="ch11">第 11 章 llm/：ark \| openai \| anthropic \| gemini over urllib</h2>

### 11.1 四条线，一个传输

`SUPPORTED_WIRES = frozenset({"openai", "responses", "anthropic", "gemini"})`（`llm/capabilities.py:24`）。

传输层 `llm/transport.py` 纯 stdlib：`urllib.request` + 有界重试（仅可安全重放的状态码；尊重 `Retry-After`；可取消；总预算上限）。

Provider 适配文件：

| 文件 | 线 |
|---|---|
| `llm/providers/openai.py` | OpenAI Chat 兼容（**ark 走这条 wire**） |
| `llm/providers/responses.py` | OpenAI Responses |
| `llm/providers/anthropic.py` | Anthropic Messages |
| `llm/providers/gemini.py` | Gemini `generateContent` |

### 11.2 ark / 豆包是一等公民

`capabilities.py:206-221`：

```text
ark:
  wire=openai
  base=https://ark.cn-beijing.volces.com/api/plan/v3
  default_model=doubao-seed-2.0-pro
  context_window=262_144
  tool_calling=True, parallel_tool_calls=True, vision=True, streaming=True
```

`llm/catalog.py` 预置多条 Ark 模型（doubao-seed 系列、glm、kimi、deepseek、minimax…）。这是「¥9.9 复刻」的工程底座，不是营销文案空转。

### 11.3 配置分层

CLAUDE.md：每个 api_key / base_url / model 解析为  
`per-provider 变量 → OPENAI4S_LLM_* → provider default`。  
Daemon 可无 key 启动，随后在 UI Customize → Models 或 `.env` 配置。

`register_provider`（`llm/registry.py:47-60`）只允许挂到已有 wire——**不能**动态加载任意传输代码。

```mermaid
flowchart TB
    CFG["Config / UI model profile"] --> RES["llm/resolve"]
    RES --> CAP["ProviderCapabilities"]
    CAP --> WIRE{"wire"}
    WIRE -->|openai| OA["providers/openai.py"]
    WIRE -->|responses| RP["providers/responses.py"]
    WIRE -->|anthropic| AN["providers/anthropic.py"]
    WIRE -->|gemini| GE["providers/gemini.py"]
    OA --> TR["transport.post_json · urllib"]
    RP --> TR
    AN --> TR
    GE --> TR
```

---

# Part IV · Web 工作台

<h2 id="ch12">第 12 章 serve → gateway.SessionRunner</h2>

### 12.1 进程与绑定

`openai4s serve`（`start.sh` 薄封装）拉起单例 daemon（pidfile）；默认 `OPENAI4S_HOST=127.0.0.1`、`OPENAI4S_PORT=8760`。文档明确：信任网络上不要 `0.0.0.0` 裸奔，用 SSH tunnel。

`gateway.py` 是 stdlib HTTP/WebSocket **组合适配器**（CLAUDE.md：对外科手术式修改，禁止整文件重写）。

### 12.2 SessionRunner._loop：Web 回合的组装现场

`SessionRunner._loop`（`gateway.py:6337-6447`）做的事：

1. 取 `max_turns`（explore 可抬高）；
2. 构造 `WebEventSink(emit, rid, assistant_visible, add_usage, …)`；
3. 非 plan 模式：从 dispatcher 取 `tool_catalog`，用 `with_finalize_response(...)` 拼终态 spec；
4. `AgentEngine(ChatModel(...), WebActionExecutor(...), CompactionPolicy, …)`；
5. `WebActionExecutor` 注入：`execute_cell` → `_execute_and_log`、native wrapper → `_invoke_control_with_artifacts`、plan/explore 钩子。

这与 CLI `Agent.run` 对称，只是事件沉到 WebSocket，Cell 执行走会话 FIFO coordinator。

### 12.3 POST /frames/{id}/message

路由匹配（`gateway.py:9654+`）：

- 解析 `input_data.request`（兼容顶层 `request`）；
- 处理 `annotation_ids` + **客户端生成的** `annotation_reservation_id`；
- `wait:false` 时返回 **202** + `execution_id`，turn 在后台跑（fire-and-forget MessageJob）。

前端契约见下一章时序。

---

<h2 id="ch13">第 13 章 WebSocket `/api/v1/ws` 事件面</h2>

### 13.1 通道职责

`gateway.py` 文件头（`:11` 附近）：

> WebSocket `GET /api/v1/ws`（`view_session` / `ping`；`text_reset` / `text_chunk` / …）

`WSHub`（`:650+`）维护：

- 订阅连接集合；
- **每帧 live-turn buffer**（断线重连可 replay）；
- 单调 `seq` + 进程 `epoch`（防 daemon 重启后假「你已追上」）。

### 13.2 高频事件类型（产品可见）

| 事件 | 含义 |
|---|---|
| `text_reset` / `text_chunk` | 流式助手正文 |
| `artifact_created` / `artifact_ref_problems` | 产物面板（新产物 / 引用问题） |
| `permission_resolved` | 人工审批结果 |
| `kernel_status` / `plan_progress` / `execution_queue` / `branch_activation_state` | 内核状态 / 计划进度 / 执行队列 / 分支激活 |

> ⚠️ **核查修正（2026-08-04）**：原表把 `cell` / `artifacts` / `permissions` 列为 WS 事件 `type`，这是**错误的**。`cell` 是 live-buffer 的 `scope` 值（`gateway.py:1057`），`artifacts` / `permissions` 是投影字典的**字段名**，三者**都不是** WS 事件 `type`。真实的事件 type 是上面三行所列（`gateway.py` 源码枚举）。
| `frame_update` | 帧级状态 |
| `step` / `step_update` | Host 活动时间线 |

turn-scoped 类型集合见 `_TURN_SCOPED_TYPES`（约 `:954`）：`text_reset`、`text_chunk`、`frame_update`。

---

<h2 id="ch14">第 14 章 一次用户提示词的完整路径（最重要）</h2>

这是工作台的「主血管」。下列步骤均可在基线源码核对。

### 14.1 前端 `send()`（`openai4s/server/webui/app.js:5574+`）

关键顺序（注释写明了为什么必须这样排）：

1. 处理 plan / explore / `/skillname` 指令改写；
2. 若无会话 → `POST /frames` 建帧并 `sub(id)`；
3. 乐观插入 user bubble；
4. **若有图像批注**：用 CSPRNG 生成 `admissionId = "resv-" + hex`，**先** `rememberAdmission`，再发请求（`:5640-5656`）；
5. **`sub(S.currentId)` 必须在 POST 之前**（`:5663-5669`）——否则首回合 `text_chunk` 在订阅集合外被丢掉；
6. `POST /frames/{id}/message`，body：

```json
{
  "input_data": { "request": "<payload>" },
  "plan": false,
  "explore": false,
  "annotation_ids": [...],
  "annotation_reservation_id": "resv-...",
  "wait": false
}
```

（`app.js:5676`）

7. 用 202 的 `execution_id` 关联气泡；按 `annotations` 字段 reconcile admission。

### 14.2 服务端接球 → SessionRunner → AgentEngine

```mermaid
sequenceDiagram
    autonumber
    participant UI as app.js send()
    participant API as gateway HTTP
    participant Hub as WSHub
    participant SR as SessionRunner
    participant ENG as AgentEngine
    participant EX as WebActionExecutor
    participant K as LazyKernel/Worker
    participant HD as HostDispatcher

    UI->>UI: sub(frameId) 先订阅
    UI->>API: POST /frames/{id}/message wait:false
    API-->>UI: 202 execution_id
    API->>SR: 后台 MessageJob / run turn
    SR->>ENG: _loop 组装 Engine+Executor+Sink
    loop 每回合
        ENG->>ENG: model.complete → route_action
        alt NativeToolBatch
            ENG->>EX: 控制工具（可只读并行波）
            EX->>HD: 同源 HostDispatcher
            HD-->>Hub: step / permissions / artifacts…
        else FinalizeAction
            ENG->>ENG: CompletionRecord → stop
        else CodeCell
            ENG->>K: 执行一格
            K->>HD: host_call mid-cell
            HD-->>K: host_response
            K->>HD: host.submit_output?
        end
        ENG-->>Hub: text_chunk / cell / frame_update
        Hub-->>UI: WS 推送（含 seq）
    end
    ENG-->>SR: EngineResult
    SR-->>Hub: 终态投影（bullets + artifact delta）
```

### 14.3 为什么 `wait:false` + 先 `sub()` 是产品级细节

阻塞 POST 会让浏览器像「卡死的表单」；202 + WS 才像科研工作台。但异步带来经典竞态：线程已开始 `broadcast(text_chunk)`，而客户端还没进 `conn.subs`。`send()` 里那两行注释（`:5663-5669`）不是废话，是线上事故的墓碑。

同理，`admissionId` 必须**客户端先生成再发出**——因为机制要覆盖的恰恰是「永远收不到 202」的情况；服务端铸造的 id 在那种情形下对浏览器毫无用处（`:5640-5646`，服务端回声在 `gateway.py:9680-9685`）。

---

<h2 id="ch15">第 15 章 Notebook 只读默认、Artifacts、Branch/Revert</h2>

### 15.1 Notebook 默认只读

`config.py:364-368`：

> read-only Notebook by default; set `OPENAI4S_NOTEBOOK_REPL=1` to re-enable the in-Notebook developer REPL.

`notebook_repl` 默认 `False`。`kernel_routes.py` 多处 `if not runner.cfg.notebook_repl` 直接拒绝交互式写入路径。产品语义：Notebook 是 **Agent 执行的只读投影**，不是第二个 Jupyter 主循环（Jupyter KernelSpec bridge 也是可选旁路，不带 tool batch / finalize，见 CLAUDE.md）。

### 15.2 Artifacts 版本

- 表：`artifacts` + `artifact_versions`；
- Cell 写文件 / `host.save_artifact` / 声明 `writes_files=True` 的 native tool（Web 边界包装）都会生成版本；
- 环境 provenance 绑到 **kernel generation**，避免 R 产物盖上 Python 包列表（CLAUDE.md 那句 *wrong rather than absent*）；
- 完成投影会带上「本回合实际产生的 artifact-version delta」。

### 15.3 Branch / Revert

`server/session_branching.py`：

- checkpoint 不可变；
- `fork` 物化隔离 workspace（`:116+`）；
- `preview_revert` / `revert_and_continue`：先记当前检查点，再安全恢复，**追加** revert，不改写历史（`:9-10, :240+`）；
- 从 cell fork 仅当该 cell 带 cursor checkpoint，否则 **409**（CLAUDE.md）——没有检查点无法重建状态。

```mermaid
gitGraph
    commit id: "checkpoint A"
    commit id: "checkpoint B"
    branch experiment
    checkout experiment
    commit id: "fork workspace"
    commit id: "new cells"
    checkout main
    commit id: "continue"
    commit id: "revert→B (append-only)"
```

---

# Part V · Skills · Compute · Security

<h2 id="ch16">第 16 章 Skills：34 份代码食谱，非 JSON schema</h2>

### 16.1 模型

`skills_loader/loader.py:1-8`：

1. **Discovery** — 扫 `skills/<name>/SKILL.md`（+ 可选 `kernel.py`）；  
2. **Progressive disclosure** — 系统提示只列 name + 一行 summary；全文经 `host.search_skills` / `load_skill` 拉取；  
3. **Sidecar gate** — `kernel.py` 先 compile-check。

基线 `find skills -name SKILL.md` = **34**。目录覆盖：蛋白折叠 / 对接 / 单细胞 / 文献 / 图表 / 远程 compute provider 食谱 / **retrosynthesis_planning**（本基线 PR 主题）等。

### 16.2 与 Tool 的本质区别

| | Native Tool | Skill |
|---|---|---|
| 形态 | Python `Tool` 子类 + JSON schema | Markdown 食谱 + 可选 sidecar |
| 注册 | `TOOL_TYPES` | 文件系统发现 |
| 何时进上下文 | schema 进 tool catalogue | 默认仅索引；按需加载全文 |
| 失败模式 | schema / permission | 模型没 load → 技能从不跑（UI 用 `/skill` 指令硬注入，`app.js:5590-5601`） |

bundled 只读；用户技能在 `<data_dir>/user-skills`；重名时 bundled 胜出。

---

<h2 id="ch17">第 17 章 BYOC 与 `openai4s_compute_provider`</h2>

### 17.1 分工

| 包 | 跑在哪 | 职责 |
|---|---|---|
| `openai4s/compute/` | 本机 daemon | 注册表、作业编排、与 Host 对接 |
| `openai4s_compute_provider/` | **远端机器** | stdlib-only 沙箱 SDK；oneshot/repl |
| `skills/remote-compute-*` | 技能树 | 具体 provider shim（如 nvidia；**注意 `remote-compute-ssh` 只有 SKILL.md + README，无 `provider.py`，是纯食谱而非 shim，不宜作 shim 例证**） |

`__main__.py` 强调两阶段 secret scrub：在 import `provider.py` **之前**先做通用 scrub，resident prologue 再按 provider 声明的 `secret_env_prefixes` 二次擦除；凭证从 stdin/fd-3 读入，**不进环境变量**。

### 17.2 控制平面入口

Registry 中的 Remote* Tool（Submit/Status/Result/Cancel/Close…）与 `host.compute.*` / `host.fold` 共用审批与审计。`host.fold`（单序列折叠类）走严格 no-fabrication 策略（CLAUDE.md）。

寻址形态产品文档常见 `ssh:` / `byoc:` 风格端点——本机只编排，重计算在你自己的 GPU 上。

```mermaid
flowchart LR
    AGENT["Agent / Cell"] --> HOST["host.compute / Remote* tools"]
    HOST --> MGR["openai4s/compute manager"]
    MGR -->|SSH/BYOC| REM["remote GPU"]
    REM --> PROV["openai4s_compute_provider __main__"]
    PROV --> SHIM["skills/.../provider.py"]
```

---

<h2 id="ch18">第 18 章 权限、审批持久化、biosecurity、egress</h2>

### 18.1 多层防御（独立层，不互相替代）

| 层 | 模块 | 要点 |
|---|---|---|
| OS sandbox | `security/sandbox.py` | Seatbelt/bwrap；auto/enforce/off |
| 环境 allowlist | `kernel/environment.py` | 防 secret 泄漏进 worker |
| 权限 / 审批 | `openai4s/permissions.py`（771 行，opencode 式 allow/deny/ask gate；**注意 `security/permissions.py` 是另一个文件**，仅管数据目录文件权限位，与审批无关）+ `storage/permissions.py` Store 表 | 持久规则；无人值守默认 deny |

> ⚠️ **核查修正（2026-08-04）**：原稿写 `security/permissions.py`，属**张冠李戴**。审批 broker 是顶层 `openai4s/permissions.py`；同名的 `security/permissions.py`（116 行）管的是 SQLite 明文凭证默认 0644 的收紧，并非审批逻辑。
| 代码门 | `security/classifier.py` + loop `_pre_exec_gate` | 拒跑不安全 Cell |
| 生物安全 | `security/biosecurity.py` | trajectory screener；BLOCK 停 Cell |
| 注入筛查 | `security/injection.py` | 不可信输出 |
| 出站 | `egress.py` | `OPENAI4S_EGRESS=allowlist` 时后缀域名白名单 |
| bash 令牌 | `host/bash.py` | generation 绑定 one-shot |

### 18.2 审批重启语义（容易做错的产品细节）

`docs/architecture.md:121-128`：

- 持久化的是**决策**，不是 Python 调用栈；
- 进程内可恢复 exact blocked gate；
- daemon 重启后：从 SQLite 露出请求；记录无参 `permission_resolution` ledger marker；声明旧操作未执行；要求显式重新继续；
- restart-only `once` 授权 15 分钟过期，精确匹配 conversation/tool/target，原子消费。

测试默认姿态（CLAUDE.md）：`OPENAI4S_UNATTENDED_APPROVAL=deny`。

### 18.3 egress 的诚实边界

`egress.py:9-19` 自称是 host-tool 边界上的 **best-effort** fence（`web_*` 与静态 `bash` 检查）；默认 mode `off`。打开 `allowlist` 后，科学数据库 / 包索引 / 数据仓库可达，其它域需 `request_network_access` 经审批放宽。它**不是** OS sandbox 的替代品。

> ⚠️ **核查修正（2026-08-04）**：`egress.py:9-10` 的 docstring 原文写「openai4s does not ship an OS-level sandbox (Seatbelt/bubblewrap)」，**与 `security/sandbox.py`（863 行、真实实现 Seatbelt/bwrap）直接冲突**——该 docstring 已**过期**。原稿把改写后的话仍归因给 `egress.py:9-19`，读者按图索骥会读到相反表述。此处应显式标注 egress docstring 陈旧，而非替作者打补丁。

---


# Part VI · 内核深挖与开发者品味

> 本章是全文重心之一。OpenAI4S 的作者群（北大—元空联合实验室开源）在公开叙事上对标 Claude Science，但源码里真正的签名不是「又一个 Agent while」，而是：**对「假真相」的过敏、对依赖边界的偏执、以及对门面文件的外科手术纪律**。下面先把内核协议读透，再反推十条搭建选择。

---

<h2 id="ch19">第 19 章 内核协议：dup2、帧上限与「错误比没有更糟」</h2>

### 19.1 技术链路（从 Cell 到 Host 再回来）

```mermaid
sequenceDiagram
    participant M as 模型
    participant Eng as AgentEngine
    participant Ex as Web/Local Executor
    participant Km as Kernel.manager
    participant W as worker.py
    participant H as HostDispatcher
    participant Store as SQLite Store

    M->>Eng: assistant + tool_calls 或 ```python
    Eng->>Ex: route_action → CodeCell
    Ex->>Km: execute(code)
    Km->>W: {"type":"execute",...}
    Note over W: stdout→捕获；协议在高位 fd
    W->>Km: host_call(method,args)  [持锁]
    Km->>H: dispatcher(method,args)
    H->>Store: audit / permission / ledger
    H-->>Km: data | {"error":...}
    Km->>W: host_response
    W-->>Km: result(stdout, usage, error_lineno)
    Km-->>Ex: observation
    Ex-->>Eng: history + maybe CompletionSignal
```

关键不变量（CLAUDE.md + `manager.py` / `worker.py`）：

1. **单 reader**：只有 `Kernel` 读协议出站；supervisor/watchdog 禁偷读帧。  
2. **单飞行 host_call**：整段 RPC 一把锁；并发会交错。  
3. **generation 单调**：respawn 必 bump；旧 bash token / lease 失效。  
4. **软失败契约**：Host 可返回 `{"error": msg}`，worker 转 `RuntimeError`——Cell 看见异常而不是 daemon 崩。  
5. **不要在 worker 里 autoclose matplotlib**：gateway 要先 `savefig` 捕 Artifact（CLAUDE.md 明确禁令）。

### 19.2 为什么这套内核「品味很重」

大多数 Agent 把「跑代码」做成：

```
tool Bash → subprocess → 文本结果 → 塞回上下文
```

OpenAI4S 把「跑代码」做成：

```
持久 REPL + 协议分家 + 中途同步 Host RPC + 环境指纹 + 账本
```

前者优化的是「模型会不会用工具」；后者优化的是「科学计算会不会在一周后仍可辩护」。**品味差在问题定义，不在循环语法。**

### 19.3 搭建思维链（反推：他们先信什么）

按依赖顺序还原一条开发者内心独白：

1. **若科学状态不能持久，Code-as-Action 就是笑话** → 先做 persistent kernel，而不是先堆 70 个 Tool。  
2. **若 print 能砸协议，一切可观测性都假** → 先做 dup2 换轨，再谈 notebook。  
   > 核查修正（2026-08-04）：原稿写「dup2 / fd3·fd4」混淆了两套机制。Python `worker.py` 用 `os.dup()` 得到**内核分配的匿名高位 fd** + `dup2(2,1)` 让 fd1 别名到 stderr，**全文件无 fd3/fd4 常量**；`fd3/fd4` 是 **R 通道专有**（`r_kernel.py:12-13`「protocol OUT rides fd 3 / protocol IN rides fd 4」）。应改为「Python 走匿名高位 fd，R 走固定 fd3/fd4」。 
3. **若 mid-cell 不能回调 Host，科研循环次数会爆炸** → 内环 RPC 与外环 tool_use 必须并存。  
4. **若控制与科学抢同一回合，审计与计算会互相污染** → `route_action` 铁律。  
5. **若 UI 气泡是真相源，重启必说谎** → Ledger-first；气泡是投影（`completions.py`）。  
6. **若 provenance 可以「猜」** → 宁愿记缺失原因。  
7. **若核心引入重依赖** → 边界糊掉、审计变难、¥9.9 叙事也站不住 → stdlib hard constraint。  
8. **若门面文件被整页重写** → 兼容/路由/传输契约会静默蒸发 → 外科修改约定写进 CLAUDE.md / PR template。

```mermaid
flowchart TB
    P1["① 科学要持久状态"] --> P2["② 协议必须抗污染"]
    P2 --> P3["③ 需要 mid-cell Host RPC"]
    P3 --> P4["④ 控制/科学分平面"]
    P4 --> P5["⑤ 账本先于 UI"]
    P5 --> P6["⑥ 假真相不可接受"]
    P6 --> P7["⑦ 核心零依赖"]
    P7 --> P8["⑧ 巨石门面只许外科改"]
```

---

<h2 id="ch20">第 20 章 十条开发选择：反推搭建思维与品味</h2>

> 体例对齐本系列 CodexMonitor「品味十章」：每条选择给**主张 → 源码证据 → 放弃了什么 → 品味标签**。

### 选择一：对标体验，不抄实现 ——「Claude Science 开源复现」是产品叙事，不是 fork

主张：用公开架构思想（Code-as-Action、持久内核、host RPC、安全层）独立复现，MIT 开源；模型可走方舟 ¥9.9。  
证据：README Acknowledgement；`docs/architecture.md` 双平面图；`llm/capabilities.py` 的 `ark` 一等公民。  
放弃：逐行兼容闭源、绑定单一前沿模型。  
品味：**垂直领域的开放替代**，用价格与可审计性换封闭飞轮。

### 选择二：stdlib 是硬约束，不是风格偏好

主张：engine / urllib LLM / http.server+手写 WS 零第三方；科学库只能 `try/except ImportError`。  
证据：`pyproject.toml` `dependencies = []`；CLAUDE.md「Never add a hard third-party import」。  
放弃：FastAPI、websockets 库、httpx、ORM。  
品味：**依赖纪律 = 信任边界纪律**。少一个包，就少一条供应链与审计盲区。

### 选择三：双平面不竞争 —— ReAct「万物皆 tool」被显式拒绝

主张：每回合至多一个动作通道；native tools 优先；sole `finalize_response` 另案。  
证据：`actions.py:route_action`；禁止注册 `bash`/`submit_output`（`registry.py:_FORBIDDEN_CONTROL_NAMES`）。  
放弃：把 shell、submit、科学计算都塞进 JSON tool 表的「统一感」。  
品味：**统一感让位于可审计性**。科学计算需要语言；编排需要 schema。

### 选择四：完成信号是产品契约，不是模型习惯

主张：对话/工具完成 ≠ 科学完成；UI 完成是投影。  
证据：`agent/finalize.py`；`host.submit_output`；`server/completions.py`；纯 submit Cell 不进 Notebook 展示。  
放弃：「模型说完再见就算完」。  
品味：**把「完了」从自然语言降级为结构化事实**——科研产品必须能回答「你到底提交了什么指标」。

### 选择五：内核协议按「最坏字符」设计，不按 ASCII 幻想

主张：上限从测量推导；CJK/emoji 代理对要把字节预算拉到 12×。  
证据：`worker.py` 关于 `_MAX_FRAME_BYTES` 的长注释与测试驱动修正。  
放弃：拍脑袋常量、默契「输出不会太大」。  
品味：**注释里写失败史**——这是实验室工程文化，不是教程工程文化。

### 选择六：假绿比红更可耻

主张：应失败的场景若成功，benchmark/harness 判失败；stub 不得污染 response schema 契约。  
证据：CLAUDE.md harness/workflows 段；`stubbed_backend` marker；`capture_response_schemas.py --check`。  
放弃：「无异常 = 通过」的偷懒评分。  
品味：**把拒绝能力当成产品一半**——科学 Agent 的价值经常是「敢说不」。

### 选择七：巨石门面允许存在，但禁止整页重写

主张：`gateway.py` / `host_dispatch.py` / `store.py` / `app.js` / `worker.py` / `manager.py` / `sdk/host.py`（核查修正 2026-08-04：CLAUDE.md:98 列 7 个门面文件，原稿漏了 `sdk/host.py`）是兼容与路由合同的堆叠；新算法进 service/repo/tool。  
证据：CLAUDE.md「Edit the compatibility/composition facades surgically」；PR template 确认项。  
放弃：清爽的「理想分层一次到位」。  
品味：**承认历史债务，用纪律管理，而不是用大爆炸重构制造静默回归**。

### 选择八：Skills 是食谱，不是插件 SDK 幻想

主张：`SKILL.md` + 可选 `kernel.py`；渐进披露；bundled 赢名字冲突；用户技能不能冒充信任。  
证据：`skills_loader/loader.py`；34 个 bundled skills；Customize CRUD 进 `user-skills`。  
放弃：把每个科学流程做成 JSON tool（爆炸）或动态 import 任意代码当一等公民。  
品味：**扩展面放在「代码食谱」**，信任面仍收在 Host 与沙箱。

### 选择九：安全是叠层，默认姿态偏偏执

主张：loopback、env allowlist、Seatbelt/bwrap、审批（无头 deny）、biosecurity、injection screen、egress（默认 off）彼此独立。  
证据：`docs/security.md`；`OPENAI4S_UNATTENDED_APPROVAL=deny` 测试默认；审批重启不重放参数。  
放弃：单层「沙箱万能」叙事；无头自动放行的 DX 快感。  
品味：**每层只承诺自己能承诺的**；egress 自称 best-effort 反而是诚实。

### 选择十：Web 是零构建工作台，不是前端事业部

主张：`webui/` 静态直出 working tree；编辑即热；没有 bundler。  
证据：`server/webui/app.js` 近万行仍手写；CLAUDE.md「no build step」。  
放弃：React/Vite 工程美学与组件生态。  
品味：**产品重心在运行时与证据，不在前端栈**——用一门语言（Python）+ 静态 JS 压运维面，服务「科学家本机一键起」。

### 一句话总结品味

> **用纯标准库守住可审计边界，用双平面拆开编排与计算，用内核协议对抗静默错乱，用账本与 provenance 对抗假真相——工程上偏执保守，产品上对科研完成定义进取，架构上宁可巨石门面加外科纪律，也不用大重构换虚假干净。**

---

# Part VII · 横向对比与总结

<h2 id="ch21">第 21 章 对比表</h2>

| 维度 | Claude Code | Open Design | OpenAI Codex | OpenManus / Suna | **OpenAI4S** | Claude Science（闭源） |
|---|---|---|---|---|---|---|
| 产品位 | 终端编程 Agent | 设计 Agent **宿主**（不自研循环） | 云+本地编程 Agent | 通用任务 / 浏览器 Agent | **科研 Code-as-Action** | 闭源科研工作台 |
| 主循环 | 自研 tool 循环 | 委托外部 CLI | 自研 | 自研 ReAct 变体 | **双平面 AgentEngine** | 闭源 |
| 工具哲学 | Shell/编辑器一等公民 | 文件系统技能喂给 CLI | 沙箱+工具 | 工具多多益善 | **无 shell Tool；bash 仅内核** | 未知 |
| 科学内核 | 无持久科研内核 | 无 | 无 | 无 | **持久 Python/R + mid-cell host RPC** | 有（闭源） |
| 完成信号 | 会话/任务约定 | CLI 退出/产物文件 | 任务完成事件 | 常靠最终消息 | **finalize ⊕ submit_output** | 未知 |
| 前端 | TUI | Next.js 重前端 | 多端 | Web | **零构建静态工作台** | 专有 UI |
| 核心依赖 | Node 生态 | 大 monorepo | Rust/TS 等 | Python 重依赖 | **stdlib only** | 云服务 |
| 扩展 | MCP/hooks/skills | 26 CLI + 内容库 | MCP/插件 | 工具/浏览器 | **SKILL.md 食谱 + BYOC** | 封闭 |
| 对标价格叙事 | Pro 订阅 | 自托管+自备 CLI | Plus/API | 自备 key | **¥9.9 Ark/Doubao** | 高价闭源 |

```mermaid
quadrantChart
    title Agent 产品地图（示意）
    x-axis 通用任务 --> 垂直领域
    y-axis 委托他人循环 --> 自研循环
    quadrant-1 垂直自研
    quadrant-2 通用自研
    quadrant-3 通用宿主
    quadrant-4 垂直宿主
    Claude-Code: [0.25, 0.8]
    Codex: [0.3, 0.75]
    OpenManus: [0.35, 0.7]
    Open-Design: [0.55, 0.2]
    OpenAI4S: [0.85, 0.78]
    Claude-Science: [0.9, 0.85]
```

---

<h2 id="ch22">第 22 章 五个独特设计决策与诚实边界</h2>

### 22.1 五个独特决策（浓缩；展开见第 20 章）

1. **双平面不竞争** — `route_action` 铁律把控制与科学拆开；这是对 ReAct「万物皆 tool」的显式拒绝。  
2. **完成信号双轨且互不冒充** — `finalize_response` 永不进 registry；`submit_output` / `bash` 禁止注册为控制工具。  
3. **中途 Host RPC** — tool_use 架构做不到的「Cell 内同步回调」。  
4. **Ledger-first + 投影分离** — UI transcript 不是真相；Action Ledger / Store 才是。  
5. **纯标准库核心** — 用依赖纪律换可审计边界与「科学栈可选」。

### 22.2 诚实边界

| 边界 | 说明 |
|---|---|
| 不是 Claude Science 的逐行克隆 | 体验对标；模型质量、实验室集成、闭源数据飞轮不可等同 |
| Notebook 默认只读 | 需要 REPL 请显式开 `OPENAI4S_NOTEBOOK_REPL` |
| egress 默认 off | 打开 allowlist 才是强约束；且仍是应用层 fence |
| R 无 Cell 内完成 | R 是分析通道，完成叙事仍偏 Python/`finalize` |
| Gateway / app.js 巨石 | 组合门面巨大；约定是外科修改，重构成本高 |
| 离线测试 ≠ 全部门禁 | harness、response schema capture、browser smoke、Linux CI 分支都是独立闸 |

### 22.3 何时选它

- 你要的是**科研分析 Agent**，不是又一个编程助手；  
- 你接受「模型写代码，内核持状态」；  
- 你希望核心可审计、少依赖、可挂廉价国内模型；  
- 你需要 BYOC 把重计算放到自己的 GPU。

何时不选：纯软件工程仓库遍历、需要重型浏览器操作员、或要「零代码只点工具」的运营 Agent——那些更像 Claude Code / Manus 赛道。

---

<h2 id="ch23">第 23 章 源码导览索引</h2>

### 23.1 按阅读顺序（建议 1 个下午）

| 顺序 | 路径 | 看什么 |
|---|---|---|
| 1 | `docs/architecture.md` | 官方双循环叙事 |
| 2 | `agent/actions.py` | `route_action` 铁律 |
| 3 | `agent/engine.py` | `AgentEngine.run` while |
| 4 | `agent/finalize.py` | 为何 finalize 不是 Tool |
| 5 | `agent/loop.py` | CLI `Agent.run` 组装 |
| 6 | `server/gateway.py` → `SessionRunner._loop` | Web 组装 |
| 7 | `server/webui/app.js` → `send()` | 202 + 先 sub + admission |
| 8 | `host_dispatch.py` + `sdk/host.py` | 编排信封与内核门面 |
| 9 | `kernel/lazy.py` + `manager.py` + `worker.py` | 科学平面协议（**先读 worker 文件头注释**） |
| 9b | 本文第 19–20 章 | 内核品味与十条选择 |
| 10 | `tools/registry.py` | 70 tools 与禁止项 |
| 11 | `llm/capabilities.py` + `transport.py` | ark 与 urllib |
| 12 | `skills_loader/loader.py` + `skills/*/SKILL.md` | 食谱模型 |
| 13 | `security/sandbox.py` + `openai4s/egress.py`（核查修正：在顶层，非 `security/`） + `openai4s/permissions.py`（审批，非 `security/permissions.py`） | 安全层 |
| 14 | `store.py` / `storage/snapshots.py` | 持久化与分支 |
| 15 | `openai4s_compute_provider/` | BYOC 远端 SDK |

### 23.2 按符号速查

| 符号 | 文件 |
|---|---|
| `AgentEngine.run` | `openai4s/agent/engine.py` |
| `route_action` / `FinalizeAction` / `CodeCell` | `openai4s/agent/actions.py` |
| `with_finalize_response` | `openai4s/agent/finalize.py` |
| `RuntimeActionLedger` | `openai4s/agent/ledger.py` |
| `Agent.run` / `LazyKernel` 组装 | `openai4s/agent/loop.py` |
| `_execute_read_only_waves` | `openai4s/agent/control.py` |
| `HostDispatcher` | `openai4s/host_dispatch.py` |
| `host.submit_output` / `host.bash` | `openai4s/sdk/host.py` |
| `TOOL_TYPES` | `openai4s/tools/registry.py` |
| `SessionRunner._loop` | `openai4s/server/gateway.py` |
| `send` / `admissionId` | `openai4s/server/webui/app.js` |
| `WebActionExecutor` / `WebEventSink` | `openai4s/server/agent_run.py` |
| `SessionBranchingService` | `openai4s/server/session_branching.py` |
| `SUPPORTED_WIRES` / `ark` | `openai4s/llm/capabilities.py` |
| `SkillLoader` | `openai4s/skills_loader/loader.py` |

### 23.3 本仓库内相关文档

- 系列对比与目录：`总目录.md`、`项目分析/` 下各项目源码分析  
- 配套教程：`从零构建OpenAI4S-开发全流程教程.md` / `从零构建OpenAI4S-开发全流程教程.html`  
- 上游：https://github.com/PKU-YuanGroup/OpenAI4S  

---

## 结语

OpenAI4S 的源码魅力不在「又实现了一个 Agent while 循环」，而在它把循环**劈成两半还不肯让它们抢回合**，并且在内核协议层对「静默错乱 / 假 provenance」表现出近乎过敏的工程品味：JSON 控制平面负责可审计的编排与权限，Python/R 科学平面负责把真实计算留在持久内核里，并用 mid-cell `host` RPC 补上 tool_use 缺失的那一截。`finalize_response` 与 `host.submit_output` 的双轨完成、`TOOL_TYPES` 对 shell 的显式缺席、以及 Web 上 `sub()`-before-`POST` / 客户端 `admissionId` 这类「小到像 bugfix、大到像产品契约」的细节，共同构成了它相对 Claude Code / Open Design / Manus 系的差异化。

基线停留在 `85e9fa0`。对照本文时请以该 commit 为准；主线继续演进时，优先核对 `agent/actions.py`、`agent/engine.py`、`tools/registry.py` 与 `server/webui/app.js::send` 四条承重墙是否仍成立。

---

*文档生成说明：面向本系列「产品经理 + 资深工程师」笔法；主张尽量回源。配套 HTML：`OpenAI4S-解析.html`。*
