# 熵守卫 · Entropy Guard

**把 `entropy-sdk` 的档位安全控制层接进 DeepSeek Harness 的工具调用链。**

上游 [`entropy-sdk`](https://github.com/CYD-PRC/entropy-sdk) 是 EntropyRuntime 论文
（[arXiv:2607.00334](https://arxiv.org/abs/2607.00334)）的零依赖可嵌入版，纯 Python。
本插件是它的**等义 ESM 移植**（`lib/core.js` 与 SDK 抽象一一对应）**加上 Harness 绑定**
（`lib/config.js` / `lib/controller.js` / `index.js` / `client.js`）。
不引入任何 `@deepseek-ai/*` 包，只用 Node 内置模块与浏览器原生能力。

> 论文的一句话主张——**让 AI 的自主程度可观测、可治理、可问责**——就是本插件的功能清单：
> 档位与 σ 实时可见、门是唯一调度通道、每次判定落进只追加审计链并且能导出成论文级数据。

---

## 一、为什么值得装

1. **档位是动态的，不是预设。** DSH 原生的 sandbox / approval 是**会话级静态**选择：选定就不动。
   这里每次工具调用都在改变状态——不稳定 → σ 上升 → **立刻降档**；干净周期攒够了才**逐级挣回**。
   「慢升快降」在 DSH 里此前没有对应物。
2. **自主权变成模型自己看得见的变量。** 每轮运行时上下文里会多一行：
   `Entropy guard: gear G3 Execute · sigma=0.00 · clean streak 0/3 · ...`，
   agent 在**选动作之前**就知道自己现在被允许做什么。另有 Web UI 实时显示档位阶梯与集群视图。
3. **只追加的审计链 + 论文级导出。** 每个判定、迁移、执行、挂起都落 JSONL；
   `/entropy export` 直接产出「门接受率 / 档位迁移直方图 / σ 轨迹 / 分工具判定统计」的
   JSON + Markdown（对应论文 §8 的实证要求）。
4. **零风险试用。** `enforcement: observe` 下**什么都不拒绝**，只计算、只审计、只统计
   「本来会拒绝什么」。先观测跑一段，看过 `/entropy export` 的数字再决定是否切到 `gate`。

---

## 二、一次工具调用 = 一个控制周期

| SDK 抽象 | 在本插件中的落点 |
|---|---|
| `Gear` G0–G4 档位阶梯（定义 1） | `lib/core.js`；动作空间单调嵌套 |
| `UtilityGate` `U(s,a) ≥ θ`（定义 2–3、定理 2） | 每次调用都过门，门是唯一调度通道 |
| `GearPolicy` 慢升快降（§4） | 每 `patience` 个干净周期升一档，不稳定即降 |
| `FallbackConfig`（定理 4） | `maxConsecutiveRejections` 次拒绝 → 挂起在 G0 等人工复核 |
| `RuntimeState` ρ=(g,σ,ϵ)（定义 4） | 每个 agent 一份，按 agent id 隔离（子 agent 有各自的自主权） |
| `AuditLog`（§5/§8） | `$DSH_HOME/entropy-guard/<agent>.jsonl`，只追加 |
| `EntropyRuntime`（算法 1） | `guard()` 过门 + `tools/result` 结清，等价于 `step()` |

周期映射：**观察/读档位 → 动作生成（分类到档位并打分）→ 效用门 →
执行与反馈**。拒绝通过 `ctx.tools.guard()` 返回——这是**单调、与顺序无关**的注册表否决，
其他插件无法把它翻回放行，所以**被拒绝的调用绝不执行**。

效用函数按论文推荐形态注入（SDK 不内置 U，必须由部署方给出）：

```
U(s,a) = task·gain(a) + safety·(1 − risk(a)) − cost·cost(a)
```

---

## 三、安装

```text
plugin_manager  action: install_bundle  target: <本包目录的绝对路径>
```

或在本包已发布到 npm 后：

```text
plugin_manager  action: install_bundle  target: dsh-entropy-guard
```

安装返回里的 `application` 字段决定它是否已经生效。安装后**建议先跑观测模式**：把
`cordis.patch.yml`（或插件行配置）里的 `enforcement` 设为 `observe`，工作一段时间，
`/entropy export` 看数字，再改回 `gate`。

---

## 四、你在界面上会看到什么

- **工具** `entropy_status`（只读，**豁免于守卫本身**，即使挂起也能回答）：
  档位、σ、连续干净周期、连续拒绝、挂起状态、执行模式、门接受率、档位直方图、最近判定。
- **命令** `/entropy`：

  | 子命令 | 作用 |
  |---|---|
  | `status`（默认） | 完整报告：档位、σ、周期数、接受率、直方图、最近链上条目 |
  | `fleet` | 集群视图：所有受治 agent 的档位/σ/挂起 + **集群最低档** |
  | `export [目录]` | 写出论文级 JSON + Markdown 报告 |
  | `resume` | 解除挂起。**σ 故意不清零**：档位必须重新挣 |
  | `gear <0-4>` | 人工直接设定档位（同时解除挂起），记 `gear_manual` |
  | `reset` | 重建该 agent 的运行时 |

- **Web UI 两处**（`ui.dock` / `ui.settings` 可关）：
  1. 输入框上方一条**实时徽章**：档位阶梯 + σ + 干净周期 + 接受率；挂起时红字提示。
     能拿到会话 id 时显示本会话，否则显示**集群最低档**（G4 的定义是系统级协调，
     所以集群真正可信的档位是**最弱**那个，不是最强那个）。
  2. 设置里的 **Entropy Guard 页**：档位阶梯、控制量、受治 agent 表、分工具判定表、档位迁移历史。
  样式只用 `--dsw-alias-*` 主题令牌，明暗主题自动跟随；不引入任何客户端 SDK 包。

- **运行时上下文一行**：每个 agent 一行，和 DSH 文件策略那行并排。

---

## 五、配置参考

每个字段都可省略。完整形状：

```yaml
config:
  enabled: true
  enforcement: gate          # gate | observe（observe = 只测量，不拒绝）
  initialGear: 3             # 0-4；G3 Execute 让普通会话保持原有自主权
  theta: 1                   # 门阈值，有限且 >= 0
  weights: { task: 1, safety: 2, cost: 0.5 }
  tools:                     # 逐工具口供；`null` 删除某个默认项
    read:  { gear: 0, risk: 0.05, cost: 0.2 }
    write: { gear: 3, risk: 0.55, cost: 0.5 }
  defaultTool: { gear: 3, risk: 0.5, cost: 0.5 }   # 陌生工具按 G3 有副作用处理
  rules:                     # 正则；取 max(risk)、max(cost)
    - { label: recursive-force-delete, match: '...', dangerousTarget: '...', risk: 1, cost: 0.8 }
  contentKeys: [content, old_string, new_string]    # 这些参数键永不参与规则扫描
  policy:
    sigmaLow: 0.3            # 低于它才允许升档
    sigmaHigh: 1.0           # 高于它立刻降档
    patience: 3              # 升一档所需的连续干净周期
    sigmaDecay: 0.1
    sigmaStep: 0.1
    fastDown: error          # error = SDK 原律；overflow = 先累积 σ 再降档
  fallback:
    maxAlternatives: 0       # Harness 无法改写待执行调用，见「有意偏离」
    maxConsecutiveRejections: 8
    countGearDenials: false  # true = SDK 原样的「连续拒绝」计数
  audit: { dir: null, includeArguments: false }     # null = $DSH_HOME/entropy-guard；"" = 只留内存
  suspendedBehavior: observe-only                   # 或 deny-all
  controlTools: [entropy_status]
  promptContext: true
  humanCommands: true
  ui: { dock: true, settings: true }
```

非法值在**激活时直接抛错**，不会静默降级——与 Python SDK 的构造期 fail-closed 校验一致。

### 档位阶梯

| 档位 | 名称 | 允许的动作 |
|---|---|---|
| G0 | Observe | 只读观察、安全保持 |
| G1 | Suggest | 无副作用的候选计划 |
| G2 | Plan | 有界、可逆的恢复动作 |
| G3 | Execute | 可独立选择的有副作用动作 |
| G4 | Integrate | 系统级协调（多智能体下为涌现属性） |

随包规则里，两条递归删除规则额外用 `dangerousTarget` 收窄：**只对绝对路径、家目录、
工作目录本身或裸通配符触发**；对相对路径的定向删除是日常工作，仍然放行。
其余规则（历史改写、管道进 shell、磁盘级操作、fork 炸弹、凭据库访问、注册表/系统改动、
全局可写根）在任意档位都会把 U 压到阈值之下，直接否决。

---

## 六、与 Python SDK 的四处有意偏离（都可配置）

1. **`enforcement: observe` 可选，且推荐作为首次安装。** 判定照算、照审计、照统计，
   只是不拒绝。`/entropy export` 会告诉你**本来会拒绝什么**。
2. **`fallback.maxAlternatives` 默认 0**（关闭 fallback——SDK v0.1.2 起明确合法的配置）。
   Harness 无法把一个待执行的调用改写成另一个调用，提议者的备选**没有地方派发**；
   拒绝理由本身就是 agent 下一轮的动作依据，这与上游适配器的说明一致。
   提议者逻辑本身在 `lib/core.js` 中完整实现并有测试覆盖。
3. **`fallback.countGearDenials` 默认 false**：挂起端点对准的是**被拒绝的动作**，
   而不是阶梯自身的拒绝。否则一个暂时低于工作档位的 agent，会因为继续调用它本职工作
   需要的工具而把自己送进挂起。
4. **`suspendedBehavior` 默认 `observe-only`**：挂起期间保留 G0 只读工具——
   这是论文定理 4 的恢复路径，也让被嵌入的 agent 还能读自己的审计链和要修的代码。
   设 `deny-all` 可得到 SDK 的 `suspended_skip` 原样语义。

---

## 七、威胁模型边界

**继承上游：**

1. 门只是**经过 Harness 工具注册表**的调用上的唯一调度通道；进程内持有原始 callable 的一方绕开它。
2. `requiredGear` 是**调用方口供**。本插件由部署方自己的表推导，而不是相信模型；
   但表是策略不是证明——真实爆炸半径超出表项的工具就是被低标了，扩充 `tools`/`rules` 是部署方责任。
3. 门管**调用**，不管**事务**：已执行工具内部的副作用不会回滚。
4. `resume()` 故意不清 σ：人工复核不等于信任立即恢复。
5. 审计文件被删除时读作空（读路径 fail-open），而写失败是 fail-closed 且会拒绝调用；
   链文件消失本身就是该告警的事件。
6. `includeArguments` **默认关闭**：每条决策仍会记录规范化参数的 SHA-256 摘要（`args_digest`），
   所以链能点名"判定的对象"、能证明它判定的是哪一次调用；但载荷本身只在部署显式开启时存储
   （`audit.includeArguments: true`，每个字符串上限 2000 字符）。

**本绑定额外提醒——控制面：**

守卫管的是**工具调用**，管不到**管理守卫的那些工具**。`plugin_manager` 默认被归到
**G4 Integrate**（安装/卸载插件本来就是系统级协调，也正是 G4 的定义），
所以在 G4 以下无法用它卸载本插件；到达 G4 后该调用会被放行（`U = 1.0`）。
如果希望彻底不可绕过，把它继续抬高到阈值之上即可：

```yaml
tools:
  plugin_manager: { gear: 4, risk: 0.95, cost: 0.65 }   # U = 0.775 < 1 → 恒拒绝
```

请像 SDK 的威胁模型对待门一样对待本插件：它是**它所治理的执行路径内部**的控制层，不是内核。

---

## 八、实测验证（真实读数，不是推断）

在 `danger-full-access` 桌面 profile 上安装本包后实测：

- `install_bundle` → `application: applied`；每 agent 的运行时上下文行出现在模型上下文里。
- 只读调用以 `U = 2.80`（读文件）/ `U = 2.89`（搜索）放行，被登记为 pending，
  再由 `tools/result` 结清为干净周期：σ = 0、耐心计数推进、门接受率 1.0。
- 一条**文本**命中破坏性规则的 shell 调用在派发前被拒：
  `U = 0.6000 < theta = 1.0000`，`flagged [recursive-force-delete]`，
  档位 G3 → G2、σ = 0.10，**该调用从未到达 shell**。
- 阶梯靠 3 个干净 G0 周期把 G3 挣回，并在持续干净工作下升到 G4 Integrate。
  一次中途读数：41 周期、门接受率 0.878、档位直方图 `{"2":3,"3":6,"4":4}`。
- 每个判定、迁移、拒绝、挂起候选都在只追加的 JSONL 链里。

---

## 九、已知限制

1. **已安装包的模块不会热重载。** 本包自身代码的改动不会被运行中的 Host 加载，
   旧一代逻辑会继续生效，直到重启 Harness 载入新一代。
   （对已安装包重复 `install_bundle` 会返回 `ambiguous-install`，并不会重载。）
2. **任何报错的工具结果都算一次被拒绝的周期。** 这是 SDK 的 ϵ=1 语义，
   但在 Harness 层面多数工具报错并不是安全事件——搜索没找到、edit 锚点过期都算。
   **默认保持 `fastDown: error`，因为默认值就是论文原律的指纹**：在这里换掉它，
   evaluation 节的采样口径就永久分叉。交互式编码场景**推荐 `overflow`**——
   一次失败调用只花 0.1 的 σ，阶梯不再因为一个过期锚点就换挡。
   本 profile 实测：`error` 下 2 次失败调用 + 3 次被阶梯拒绝的重试，把会话从 G4 一路打到 G0。
3. **规则是启发式，不是分类器。** 它对内容不敏感，控制自己载荷的 agent 总能换种说法绕开；
   这也正是为什么扫描面排除了文件内容、而否决权放在执行边界。
4. **守卫只看得到进入注册表的调用**，也无法改写待执行的调用（故 `maxAlternatives: 0`）。
5. Web UI 的视觉验证需要浏览器页面；无浏览器控制时只能验证 JS 语法、清单与槽位注册。

---

## 附：给运行 agent 的人一条注意（血泪账）

**失败的工具调用同样是一次被拒绝的周期**，哪怕失败原因与 agent 的判断无关。
最典型的一种：把「读文件」和「改同一个文件」放进**同一批调用**——fs 观察策略是**按轮**要求的，
同一批里并行执行时读还没生效，改就失败；在忠实的 `fastDown: error` 下这要掉一档。
本 profile 实测：两次这样的失败，加上随后三次被阶梯拒绝的重试，**一个回合把会话从 G4 打到 G0**。

给 agent 一句明确的行为规范就能省掉这些：**一轮只读，下一轮才改。**

（守卫本身不替宿主策略做预检——它记录的是后果，不是意图。这条边界是有意的。）

## 十、测试

```
node --test test/core.test.mjs      # 89 项，无依赖
```

覆盖移植契约（fail-closed 口供与门校验、慢升快降、挂起端点与 `resume()` **不**做什么、
审计链的消毒/深拷贝隔离/文件模式缓存与坏行计数、`admit()`/`settle()` 与 `step()` 的等价性、
备选提议器）以及绑定规则（观测模式不拒绝、`fastDown` 两种语义、
阶梯拒绝对挂起计数的影响、可执行的拒绝理由、导出报告、`plugin_manager` 归 G4、配置校验）。

---

## 十二、审计链可验证性与读数纪律

**链是防篡改的，不是"有日志"。** 每条记录都带单调 `seq` 与前一条的 SHA-256 `hash`，
每次报告与导出要么校验通过，要么点名被改写的那个 `seq`。校验是**分级**的：

- `tampered`——链化区间内有记录被改写 / 链路被重接 / 有记录消失（`seq` 出现缺口）。**这才是告警。**
- `forked`——`seq` 重复或回退：两个写入者从同一个尾部各自追加，历史分叉。属运维问题而非攻击，
  而且**除了这里没有任何地方能看见它**（另给 `forks` 计数）。
- `discontinuity`——链化记录之间夹着未链化记录。这正是**原地升级时两代同写一条链**的样子，
  是每次升级都会走的正常路径，因此按软提示处理（另给 `interleaved` 计数与 `chainedRange`）。
- `verified`——干净。

把升级路径和篡改报成同一个信号，警报就会失去意义（哨兵刷屏）。v0.1 时期写下的旧记录（没有链字段）
会被如实计成"未链化的 legacy 前缀"，而不是默默当成可信。

**关于上游能力的准确说法**：`entropy-sdk` 的 `src/entropy_sdk/state.py` 用「只追加写入 + 读取深拷贝
隔离 + 坏行计数」保证审计链完整；源码里没有任何摘要或校验逻辑（其 `tests/test_v016.py`、`test_v017.py`
中的 tamper 用例针对的是**返回值被涂改**的隔离性，而不是写入被改写）。本实现额外加了 `seq` + `prev-hash`
自校验，用来回答"日志本身有没有被改写"——**这是新增能力，不是对上游的否定**。

**`/entropy seal [备注]`**：把当前条目之前的运维历史**在链内**封存。封存本身是一条普通记录——
可审计，而不是谁都能随手翻的配置开关；被封存的计数仍然照报，只是不再参与当前状态判级；
**篡改级别的发现永远不会被封存掩盖**。这条命令只有人能敲（模型够不到命令处理器），
所以"自己把自己的历史洗白"在结构上不成立。
适合的场景：一次原地升级留下了几个历史分叉，你不希望它永久污染本会话的当前读数。

**离线校验器**（不需要跑 Harness 就能复核问责性）：

```bash
node tools/verify-chain.mjs <chain.jsonl>          # 校验 + 打印 Markdown 报告
node tools/verify-chain.mjs <chain.jsonl> --json   # 完整 JSON 报告
node tools/verify-chain.mjs --demo                 # 用副本演示篡改检测
```

在一条真实的、已原地升级过的链上实测（先追加两条链化记录，再改写最后一条）——分级后的形态：

```
after appending two chained entries: DISCONTINUITY · entries 254 · chained 53 · unchained legacy 194 · interleaved 7 · corrupt lines 0 · 7 unchained entries among chained ones (two plugin generations wrote this chain)
after editing the last entry on disk: TAMPERED · entries 254 · chained 53 · unchained legacy 194 · interleaved 7 · corrupt lines 0 · seq 246 was modified after it was written
```

**读数纪律：两种模式分账，绝不混算。**

- `gateAcceptanceRate` 只统计**执行模式（gate）**下的判定；观测模式（observe）那批判定单独计入
  `observedDecisions`，对应的指标是 `wouldDenyRate`（"本来会拒绝的比例"）。
- 观测期**不会**稀释执行期的实证效力；跨部署比较不会混口径。
- `fastDown`（`error` / `overflow`）**字段级写进导出的 `policy`**，与 `theta` 并列——
  两次在不同降档语义下采样得到的数字本来就不可比，所以语义必须跟着数字走。

**`denials: actionable | terse`**（可观测性 vs 策略保密）：默认 `actionable`，会把
"当前允许的工具清单"和"还差几个干净周期能挣回哪一档"直接告诉模型——这正是本插件的卖点；
但规则是启发式的，把阈值和仍被允许的动作集公布出去，会让探边界更便宜。把策略当敏感资产的部署
应设为 `terse`（只报拒绝与实时状态）。这个取舍属于部署方。

**包名与行名**：本包已改名为 **`@cyd-prc/dsh-entropy-guard`**（与 upstream 的 GitHub org 对齐，
发布溯源链为 org → 仓库 → 包 → 论文）。安装时 `install_bundle` 的 target 用新名或本包绝对路径；
客户端模块 id 为 `@cyd-prc/dsh-entropy-guard`，补丁行 id 为 `cyd-prc-entropy-guard`。
**行 id 是故意不沿用旧值的**：改名后若继续用 `entropy-guard`，新旧两个 bundle 会声明同一个
`entryId`，旧 bundle 就无法 `remove_bundle`（DSH 返回 `bundle-in-use`，实测确认）。

## 0.2.1

- **规则改为逐语句匹配（缺陷 8）。** Windows 递归删除规则的两个前瞻扫的是**整条命令**，
  于是"一处列目录带一个开关、另一处删单个文件带另一个开关"的脚本被判成一次递归强删——
  它在 0.2.0 的发布过程中否掉了本包自己的发布命令。现在按语句切分匹配（`;`、`&&`、`||`、转义换行），
  管道保持完整（`|` 是一条流水线）。测试 66 项（0.2.0 里这里印的是 51）。

## 0.2.2

- **两个平台画同一条灾难性根线（缺陷 9）。** Unix 目标模式读作"任何以路径分隔符开头的 token"，
  于是**每个绝对路径都被否决**——`/tmp/build` 要花掉一档和一次拒绝；而 Windows 侧只否决盘符绝对路径，
  `C:\tmp\build` 直接放行。**同一操作因平台得到相反判定，而这个差别哪儿都没写下来过。**
  现在两边都只列**根**（`/`、`/etc`、`/home`、`C:\`、`C:\Windows`、`C:\Users`、用户配置文件根、裸通配符）；
  限定子树（`/tmp/...`、`$env:TEMP\...`、`C:\tmp\...`、`./build`）在两边都是普通工作。
  `bash` 也以与 `pwsh` 相同的基线进入工具表，Linux 部署不再因为表是在 Windows 上写的而继承更低的定级。
- **链开始记录"判定的对象"（缺陷 10）。** 每条 `gate_decision` 现在带 `args_digest`
  （对规范化参数取 SHA-256，键序不影响结果）与 `tool_source`（`table` 或 `default`）。
  `audit.includeArguments: true` 现在**真的**存储参数的限量副本；此前这个开关在本文件与
  `cordis.patch.yml` 里都有文档，**却没有任何代码读取它**——开了等于没开。
  理由就是账本自己的：模式表判不了一个不透明的载荷（`python3 -c exec(base64…)` 对它不可见），
  所以能把它点名出来的必须是记录。
- **凭据目录，而不是某一个文件名（缺陷 11）。** 规则原先点名 `.ssh/id_*`，于是**读**私钥被否决，
  而**装** `authorized_keys`（影响更大——它是授予持久访问，而不是仅仅暴露一个密钥）不被否决。
  现在目标是凭据目录本身，目录里哪个文件被碰都一样。
- `status` 与 `/entropy status` 新增 `defaultedTools`：统计落到 `defaultTool` 的判定数，
  于是宿主改工具名会变成一个**数字**，而不是从显式条目静默漂移到通用基线。

## 0.2.3

- **发布的包现在带上它自己的补丁行（缺陷 12）。** `package.json` 声明了
  `dsh.bundle.patch: "./cordis.patch.yml"`，而 `files` 没有列这个文件——于是 npm 包只装了 13 个文件，
  **元数据指向的补丁行不在里面**：走**注册表**安装（用包规格而不是本地路径）会解析到一个没有行可应用
  的包，只有本地路径安装能用。这是把已发布的 0.2.2 实件与其提交逐文件比对时发现的。
  修复是 `files` 里加一行；本次发布的校验会断言 `package.json` 引用的每个路径
  （`icon`、`exports`、`dsh.bundle.patch`）都存在于 tarball 内。

## 0.2.4

- **发布的包现在带上自己的测试。** `test/` 进了 `files`，于是认证一次发布的套件可以被任何安装它的人**重跑**——
  这一点是在**已发布的 tarball 内部**跑出来验证的（71/71）。在此之前，包里有一个验链器，却要求读者对测试数**凭信**，
  这与本项目其余部分所遵循的标准相矛盾。代价约 42 KB。
- **探针与审计脚本进 `tools/`**：`coverage-audit.mjs`（用 PRE-GHR XLI 语料跑规则表）、`probe-boundary.mjs`、
  `probe-encoded.mjs`、`probe-filewrite.mjs`。本文里那些数字就是它们产出的，因此也是它们可以被重算的原因。
- `node --test test/core.test.mjs` —— 传**文件**而不是目录：Node 24 下 `node --test test/` 什么都不报，
  这一点已经被误当成"套件坏了"两次。套件自己的文件头现在写明了。

## 0.3.0

- **阶梯状态在激活时从链上折回（缺陷 13）。** 状态原本只作为轨迹存在于条目里，于是每次重启阶梯都从 `initialGear`
  重来，而链仍在描述一个已经爬升过的会话——在真实 profile 上实测为 **413 decisions 而 `cycles 0`**。
  现在一次新的激活会把 gear、σ、cycle、clean streak、连续拒绝数与挂起状态按"最后写入者胜"折回来、应用它，
  并记一条 `restore` 条目说明应用了什么。`init` 是激活标记、**不是**原点——这正是自主权能跨重启存活的原因。
  `reset` **是**原点，所以人工重置仍意味着白纸一张。**被篡改**的链永不作为状态来源；`restoreState: false`
  恢复此前"每次全新会话"的行为。`tool_error` 现在带转移后的状态，所以"报错后的 settle"折得精确。
  有界的保留说明：折算是精确到**最后一条带状态的条目**——最后一条决策之后的**成功** settle 本身不是条目，
  因此 σ 与 clean streak 可能相差一个周期。

## 0.3.1

- **`tools/verify-release.mjs` —— 测试看不见的那一层。** 套件跑的是源码树，它没法告诉你：已发布的实件是否仍等于
  它自称的那个提交（0.2.0 的失效），或者清单是否指向一个 tarball 里并不存在的文件（0.2.2 的失效）。
  这个工具两件都做，也正是"这两项检查不再依赖维护者记不记得"的方式：`--spec name@version` 下载实件并与工作树
  逐文件比对，`--pack` 在发布前检查**本地** tarball，`--run-tests` 在解出的实件内部跑套件，`--json` 供机器读取。
  它不依赖任何外部工具——`zlib` 加一个进程内 tar 读取器——并且对 `.json` 走**语义比较**，因为已发布的
  `package.json` 就是工作树那份去掉打包器末尾换行：会对这个"喊狼来了"的检查器会被忽略，而被忽略的检查器比没有更糟。
  glob 那一类（`exports: { "./locale/*.json" }` 不是文件名）在这里是一条**单元测试**，
  因为对这个包的一次手工审计恰好产生过这个假阳性。
- **运维注意：0.3.0 改变了"重启"的含义。** 有了状态折叠，一个曾被降档的会话在下一次激活时会**继承**档位，
  而不是从 `initialGear` 重新挣。重启不再等于复位：先前从干净阶梯开始的复现，现在可能从 G2 起步，
  并在爬回来之前拒绝普通工作。要刻意开新局，用 **`/entropy reset`**（它会写下折叠的原点）、
  或让部署指向干净的链目录、或设 `restoreState: false`。这一点由一次外部审计撞出来后记录在案；
  它是缺陷 13 修复的**预期语义**，不是新缺陷。

## 0.3.2

- **两个写入者、一条链——现在串行化了（缺陷 14）。** `AuditLog` 每个实例只缓存一次链尾，于是共享一条链的两个插件各自从同一个 `seq` 续写，文件里出现了重复序号——线上实测为 `seq 245 repeats after 245`，此后那条链的每次读数都永远挂着 `forked`。现在每次追加都取一把独占锁（`<chain>.lock`：`wx` 原子创建、超时破旧锁、持锁过久则 fail-closed），并在**锁内**重读磁盘上的链尾。机制移植自兄弟守卫的账本——它早就解决了恰好这个问题。
- **`verify()` 改为分叉感知，不再停在第一个分叉（缺陷 15）。** 顺序与链接检查过去在第一个重复 `seq` 处整体停摆，于是在一条真实的混合世代链上把 8 个分叉数成 1 个、并把它判成 `verified`——其中还包括分叉之后**有条目被删除**的情形。现在遍历永远走完全程：每条链化条目都重算哈希；`prev` 链接对**文件里存在的全部哈希**解析（能解析到非前行的是分支，悬空的就是"有条目被删"）；每个重复 `seq` 都计数；封存历史内部的序号缺口只报告不定级。分叉轴相对封存的归属按 `seq` 判定而非文件位置——封存之后写入的分支条目仍可能带着被封存的序号。分级语义即共享的 `CHAIN-GRADING-CONTRACT.md`，本包与兄弟守卫现在实现的是同一份；冻结的真实 fixture 在两个校验器上都判 `forked · 8 forks (2 live)`。
- **`verify-chain` 不再写它审的那条链（缺陷 16）。** 报告路径过去把 controller 绑到目标链上，而 controller 的构造函数固定追加 `init` + `restore`——仪器成了参与者，每跑一次 +2 条。现在审计是裸的只读 `AuditLog.verify()`，报告在临时目录的副本上构建。锚定测试：跑两次、对链文件求哈希、要求字节不变。
- **崩溃撕断的尾行不再吞掉下一条追加（缺陷 19）。** 崩溃发生在追加中途会留下一行没有结尾换行的破行，而下一次追加会**融合**进去：条目丢失、文件行数不增——测试侧的 M4 正是这种形状：对这样的链调 `seal()` "看起来成功"却没写下任何可解析的东西（此处已逐字节复现）。现在每次追加先终止破行；破行留在原地计入 `corrupt`——报出，绝不静默吞掉。
- **`verify-release` 护栏（缺陷 17–18）。** 工具以自身位置为树锚，于是拿**本**副本去验**别的**包会比错树并打出十四条假 `DIFF`——现在 `--spec` 指向别的包名会被直接拒绝并说明原因。工具还会扫描实件内已交付代码里的文件字面量，凡在实件内无处解析的都列出（兄弟守卫曾把不变量检查器交付出去，而它读的 `DESIGN-v1.md` 和外部锚根本没进 tarball）。`--wait <秒>` 会在新发布传播期间轮询注册表——packument 与 tarball 各自独立传播，这个项目两个方向都被咬过。
- **兄弟守卫的观察者也算控制面工具。** `shape_status` 与 `threat_status` 加入 `entropy_status` 所在的 G0/豁免集合。未知工具按 `defaultTool` 自证到 G3，于是过去会话一降档就会**先失掉**同链上其他守卫的**诊断读数**——恰恰在最需要读数的时候。跨插件约定：每个守卫都必须把 `*_status` 观察者归为只读。
- **守卫抓到了它自己的构建者——链作证。** 在兄弟包 0.1.x 施工期间，一个构建 agent 用一个会改编码的编辑器写坏了 JSON 清单（触发原因来自构建者自述；链记录的是其后发生的事），随后它顶着拒绝重试同一个 shell 工具，直到阶梯在一秒内把会话从 G4 连降到 G1（`gear_transition` 在 seq 652/655/658，σ 0.1 → 0.3），并 outright 拒绝了下一次调用（`call_rejected`，seq 659：`gear G2 Plan does not permit G3 Execute`）。本 README 承诺的脱身通路在真实条件下成立：拒绝文案里列出了当时可用的只读工具，agent 停止重试，并用约六分钟的干净周期从 G1 逐级挣回 G4（seq 663/668/671），全程零新增拒绝。整段事件在链上 hash 链接、可用 `tools/verify-chain.mjs` 复算——降档是真的，脱身通路是真的，恢复也是真的。

## 0.3.3

- **发布校验器不再把 Windows 检出读成伪证（缺陷 20）。** `core.autocrlf=true` 是 Windows 默认，
  于是一次新检出里全是 CRLF 而实件里是 LF——按原始字节比对就把每个文本文件都报成假 `DIFF`。
  测试侧在完好的 0.3.2 发布上量出 17 处幻报、差点宣布它是坏的；第一版修复还漏了 `LICENSE`，
  因为没有扩展名的文件也是文本。现在文本性按字节嗅探（无 NUL、合法 UTF-8），文本按行尾归一比较，
  并如实报成 `match (line endings normalized by the checkout)`——与打包器改写 `package.json`
  末尾换行同属一个等价类。头部行还会点名被验树的 HEAD 提交号（`· tree 42782e3`），
  读数永远说清它比的是哪棵树。
- **`verified` 读数现在点名它的不可解析行（契约第 8 条的字面）。** 计数一直在，但一条
  "除此之外无事"的破行链上 `reason` 是空的。现在它说 `1 unparsable line(s) (reported, not
  graded)`——与兄弟分级器逐字一致，同一份字节在两个校验器上读出同一句话。

## 0.3.5

- **fleet 头条读数不再读幽灵（缺陷 21）。** 无 agent 归属的进程级作用域——`controllerFor(undefined)`
  预热所建，让无归属调用也被治理——坐在 fleet 集合里，而 `weakestGear` 取最小值，于是 UI 最显眼的
  读数永远钉在幽灵的档位上。代行验证方在活 profile 上实测：**真实会话 G4、274 次判定，而 fleet 读
  G0**——那是幽灵的档位，从"工具表当年还没有 bash"时代的拒绝记录折出来的。读数与事实相反。现在
  fleet 只聚合**有 id 的真实会话**，进程级作用域以诊断身份单列（`processScope` 字段 + `/entropy fleet`
  里一行标注）。聚合量（decisions/acceptance/suspended）不再把幽灵拌进去。幽灵对无归属调用的治理
  行为本身不变——变的只是报告。

## 0.3.4

- **链迁到 `@cyd-prc/dsh-audit-chain`。** `AuditLog` 现在是共享的 `ChainLog` 加上只有本插件在用的
  读数（`replayState`、`gearHistogram`、`gateAcceptanceRate`）；写入者、锁、破行规则、封存与分级器
  在三个守卫间只剩一处实现。这是本文件缺陷账的结构性收口：链的两份副本曾漂移到同一文件上
  （S3 搭进两个包、锁隔了一次实测分叉事故才过去）——一条链、一份实现，分级修复再也不会
  只修一边。依赖是**内嵌**的（`bundleDependencies`、精确钉版），安装出的实件仍然自足，
  发布校验会核对内嵌副本与钉版一致。一处预期内措辞变化：交错原因现在说「two generations
  wrote this chain」（共享包不预设插件）。`tools/verify-release.mjs` 随本版升到最新代
  （根 packument 检查、bundleDependencies 感知、代码引用扫描跳过 `node_modules/`）。
  **打包注意**：`bundleDependencies` 需要 **npm** 来打包——pnpm 在默认的 isolated linker 下
  会拒绝内嵌依赖（`ERR_PNPM_BUNDLED_DEPENDENCIES_WITHOUT_HOISTED`）；用 pnpm 复算打包的用户
  需加 `nodeLinker: hoisted`。

## 十三、缺陷账（v0.1 → v0.3.5）

以下每一条都是在真实 profile 上跑出来的，并注明关闭它的修复。规律本身就是结论：
缺陷集中在"Python 形状的控制律撞上工具注册表"的地方，其中三条是守卫**误判了自己的维护动作**。

| # | 缺陷 | 观察到的后果 | 修复 |
|---|---|---|---|
| 1 | 规则扫描了文件**内容** | 本包拒绝了自己的源码编辑——改自己的规则表恰好触发正在被改的那条规则，被治理的 agent 无法修复线上那一代 | `contentKeys` 把"内容型参数"移出扫描面；否决权归于执行边界，shell 工具的命令参数仍全量扫描 |
| 2 | 规则只匹配操作、不匹配目标 | 清理本插件自己的临时目录被当成擦盘否决 | `dangerousTarget` 把两条递归删除规则收窄到绝对路径、家目录、工作目录本身或裸通配符 |
| 3 | 任何报错的工具结果都掉一整档 | edit 锚点过期、搜索无结果、网络调用被拒，都会掉一档并短暂拿走恢复所需的工具 | `policy.fastDown`（`overflow` 只花 σ）＋ `fallback.countGearDenials: false` |
| 4 | 阶梯自身的拒绝计入挂起 | 暂时低于工作档位的 agent 会因为继续调用本职工具而**自我挂起** | `countGearDenials: false` 与更高的 `maxConsecutiveRejections` |
| 5 | 审计字段名 `kind` 覆盖事件类型 | 链上每条 `would_deny` / `call_rejected` 都丢了事件类型（Python 会直接报错，JS 静默覆盖） | 事件类型最后展开，调用方字段再也无法改写记录类型 |
| 6 | 已安装包不热重载 | 磁盘上已修好的代码继续按旧逻辑执行——包括缺陷 1、2 | 记为固定限制 1；重复 `install_bundle` 返回 `ambiguous-install` 且不重载 |
| 7 | 守卫管工具调用，却管不到管理守卫的工具 | G3 会话可以用守卫给的自主权卸载守卫 | `plugin_manager` 归入 G4 Integrate |
| 8 | 规则跨整条命令匹配，而不是逐语句 | 一处列目录带一个开关、另一处删单个文件带另一个开关的脚本被判成一次递归强删——它否掉了本包自己的发布命令 | 规则逐语句匹配（`;`、`&&`、`\|\|`、转义换行），管道保持完整（`\|` 是一条流水线） |
| 9 | 销毁性目标线在两个平台上画得不同 | Unix 否决 `rm -rf /tmp/build`，Windows 放行 `C:\tmp\build`——同一操作、相反判定，而这个差别哪儿都没写 | 两边都只列灾难性根；限定子树在两边都是普通工作；`bash` 以 `pwsh` 的基线入表 |
| 10 | `audit.includeArguments` 有文档却从未被读取，且决策条目不记录对象 | 打开载荷记录的部署得到的是沉默；一次编码 `exec` 调用在链里只留下"shell admitted, U=…"，既没有载荷也没有目标 | 每条决策带 `args_digest`，旁边带 `tool_source`，开关现在真的存限量副本 |
| 11 | 凭据规则点名 `.ssh/id_*` | 读私钥被否决，而写 `authorized_keys`（持久访问）不被否决 | 目标是凭据目录，目录里哪个文件被碰都一样 |
| 12 | 包元数据指向一个 tarball 里不存在的文件 | `dsh.bundle.patch` 指向补丁行而 `files` 没列它，于是注册表安装解析到没有行可应用的包——社区路径坏了，而本地路径却能用 | 补丁行进 `files`，并在发布校验中断言元数据引用的每个路径都在 tarball 内 |
| 13 | 链记录了会话，而会话在每次重启时把自己的状态丢掉 | 真实 profile 实测：**413 decisions 而 `cycles 0`**——一个爬升过、又被拒绝打回 G2 的会话，重启后按 `initialGear` 反而**更宽松**，而"本可以做得更好"的证据就躺在链里 | 新激活从链上折回 gear/σ/cycle/clean streak/连续拒绝/挂起，记一条 `restore` 条目，并拒绝把被篡改的链当作状态来源 |
| 14 | 链尾每个写入实例只缓存一次 | 共享一条链的两个插件各自从同一个 `seq` 续写——线上实测 `seq 245 repeats after 245`——此后该链永远读作 `forked` | 每次追加取独占 `wx` 锁，并在锁内重读磁盘链尾（移植自兄弟守卫的账本） |
| 15 | 校验器在第一个分叉处停止顺序与链接检查 | 8 个分叉数成 1 个；一条在分叉后**被删过条目**的链被判 `verified`——而兄弟校验器对同样的字节判 `TAMPERED` | 遍历永远走完：逐条重算内容哈希；`prev` 对文件内全部哈希解析（悬空 = 改写，可解析 = 分支）；每个重复序号都计数；封存归属按 `seq` 判定 |
| 16 | 离线校验器往它审的链里追加 | 每次 `verify-chain` 运行往证据里加 `init` + `restore`（实测 +2 条/次） | 审计是裸的 `AuditLog.verify()`；报告在临时目录的副本上构建 |
| 17 | 发布校验器的树锚是它自己的位置 | 拿一个包的副本验另一个包会打出十四条假 `DIFF`，且不说明原因 | `--spec` 指向别的包名时被拒绝并附解释 |
| 18 | 已交付代码可能读取实件并未携带的文件，低于元数据那一环 | 兄弟守卫交付的不变量检查器所读的 `DESIGN-v1.md` 与外部锚不在它的 tarball 里 | `verify-release` 扫描实件内已交付代码中的文件字面量，凡无处解析者列出 |
| 19 | 追加融合进崩溃撕断的尾行 | 条目丢失、行数不增——对这样的链调 `seal()` "看起来成功"却没写下可解析的条目（测试侧的 M4） | 每次追加先终止破行；破行留在原地计入 `corrupt`——报出，绝不静默吞掉 |
| 20 | 发布校验器拿检出原始字节对实件字节 | Windows 上（`core.autocrlf=true` 是默认）每个文本文件都读成假 `DIFF`——测试侧在完好的 0.3.2 上量出 17 处幻报、差点宣布好发布是坏的；第一版修复还漏了 `LICENSE`（无扩展名的文件也是文本） | 文本性按字节嗅探（无 NUL、合法 UTF-8），文本按行尾归一比较并如实报出（`match (line endings normalized by the checkout)`）；头部行点名被验树的 HEAD 提交号 |
| 21 | fleet 头条读数来自合成幽灵 | 无 agent 归属的进程级作用域（为治理无归属调用而预热的构造）坐在 fleet 集合里，`weakestGear` 取最小值——活 profile 上**永远读 G0**，而真实会话在 G4、274 次判定；幽灵的档位是从"工具表当年还没有 bash"时代的拒绝折出来的 | fleet 只聚合有 id 的真实会话；进程级作用域以诊断身份单列（`processScope` + `/entropy fleet` 一行标注）；它对无归属调用的治理不变 |

## 十一、溯源与许可

上游 `entropy-sdk` 为 MIT 许可，作者王苗生（ORCID: 0009-0003-2767-2421）。
本包移植其公开的核心语义，是该作者的 SDK 到 DeepSeek Harness 的独立绑定。
