Skip to content

fix(review): #708/#709 复核闸三处 reviewed_by 缺口 + warn 档可观测 - #710

Merged
FuRongJun-1999 merged 4 commits into
mainfrom
fix/708-writegate-observable
Oct 29, 2026
Merged

FuRongJun-1999 merged 4 commits into
mainfrom
fix/708-writegate-observable

Conversation

@FuRongJun-1999

@FuRongJun-1999 FuRongJun-1999 commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

一条 reviewed_by 通路上,三个落点都没传(#708 ② + #709 及其连带发现)

报告人 #708 报的是第 ③ 个;查它时发现同一条链上还有两个更靠前的缺口,一起修了。

# 落点 修前现象 危害
① propose(候选入 inbox) 按目标层要求复核单元 ⇒ 被拦 死锁:要写 knowledge 需 reflect → 要 reflect 需裁决 → 要裁决需提案 → 提案又要 reflect。连提案都提不进去
② review_decide 的 accept 落业务节点(add) 不传 reviewed_by ⇒ 被同一道闸拒 裁决过了、落盘被拦(「批了但写不进去」)
③ _write_review_record 写 structural 审计节点 不传 reviewers ⇒ 被同一道闸拒 审计链断裂(#709 原文)

为什么 #709 只看到 ③

报告人走的是 forget 产生的变更单(_decide_mutation 路径),绕过了 ①;② 在他的场景里恰好没过闸。

修法

# 改法
① require_layer_write 新增 skip_gate: bool = False(缺省 False);只 propose 传 skip_gate=True
② review_decide 的 accept 落盘 add(..., reviewed_by=reviewed_by)
③ _write_review_record(rec, reviewers=None) → add(..., reviewed_by=reviewers);_record_decision / _finish_decision 逐层透传

为什么 ① 用 skip_gate 而不是也传 reviewers

propose 不落记忆盘(只把候选写进 inbox),按目标层要求复核单元本身就是错的;而且它没法传——提案还没被裁决,哪来的 reflect 单元?

这不是「关闸」,三条护栏:

  1. skip_gate 缺省 False,全仓只有一处 skip_gate=True(守卫 C1 钉住);
  2. 层白名单与密级检查在 skip_gate 之前(守卫 C4 用位置断言钉住顺序);
  3. accept 落盘仍走同一道闸(守卫 E1/E2:无 reflect 仍被拒、给了 reflect 才过)。

④ warn 档不可观测(#708 ①)

security._write_gate_verdict 只写 sys.stderr,而 DSH 不把这条转发进 desktop-next.log ⇒ 用户「完全看不到」,等于没有观察期。

修法(报告人建议 2):writegate 加进程内计数,经 cg(op=info).write_gate 透出:

cg(op="info")["write_gate"]
# {mode, total, by_layer: {"knowledge@L1": 3}, recent: [{layer,tier,reason}…], hint}
  • 不依赖 stderr 转发、不写盘(诊断面零副作用);
  • recent 只留 layer/tier/reason,无内容、无节点 id;有界 20 条;
  • stderr 那行保留(宿主愿转发时仍实时可见);op=info 的透出用 try/except 收口,不得把 info 拖崩。

验证:两个新守卫,30 条断言

守卫 断言数 关键项
test_issue708_writegate_observable 17 warn 计数 +1 / 按层分桶 / recent 无内容;C2 不带 reviewers 仍被拒(对照,不是放水);E1 只给 reflect 仍被拒
test_issue709_review_chain_enforce 13 enforce 下 propose 成功(死锁已解);accept 的 record_error 为空 且 node_id/record_node_id 都有值;skip_gate 只有一处、层白名单在它之前;verify 角色 propose knowledge 仍被拒

既有关联守卫全绿:test_writegate_tiers(17) · test_review_dual_check · test_issue522_review_record_sensitivity · test_issue644_637_636 · test_issue89_zero_score。

关于报告人给的 4 个方向(#709)

方向 取舍
1. 把本次裁决已有的复核单元带入 采用
2. 审计账本走单独通道、不走闸 不采用:审计节点仍是记忆图的一部分,绕开层保护等于开特例;方向 1 已能让它正常落盘
3. 明确降级:标注「仅 jsonl 审计」而非 record_error 部分采纳:正常路径已不再失败;但保留 record_error 字段本身有价值——它是「该失败就如实报」的唯一出口,改成静默降级会让真故障也看不见
4. README 写明「enforce 会连带阻断」 不采用:那不是边界,是缺陷;已修

升级后用法

cg(op="info")["write_gate"]    # 观察期看 total / by_layer;够了再切 enforce

计数进程内累计、重启归零(不落盘),建议观察一个完整工作日再决定切档。

# 两个病灶(报告人 Starrain08 实测,均属我方实现缺口)

## ① warn 档对用户完全不可见

`security._write_gate_verdict` 在 warn 档只做 `sys.stderr.write`。而 DSH **不把子进程 stderr
转发进 `desktop-next.log`**(同进程的 bridge stderr 能转发,但这条不在其中)⇒ 用户「完全看不到
任何东西」,等于没有观察期,只能盲选要不要切 enforce。

## ② enforce 下裁决审计节点被同一个闸挡住

`_write_review_record` 写 **structural** 层(2 档:需 reflect + verify),但它的 `add(...)`
**不传 reviewers** ⇒ 被自己这道闸拒绝。后果:`decisions.jsonl` 还在,**可复核的 md 审计节点没了**,
而审计链断掉的恰恰是最需要外部复核的场景。

# 修法

| # | 改动 | 对应报告人建议 |
|---|---|---|
| ① | `writegate` 新增 `warn_record()` / `warn_report()`(**进程内计数 + 最近 5 条摘要**,有界 20 条、无内容无 id);`security` 的 warn 分支在写 stderr 之外再记一笔;`cg(op=info)` 新增 **`write_gate`** 段透出 | 建议 2(可查询计数) |
| ② | `_write_review_record(rec, reviewers=None)` → `add(..., reviewed_by=reviewers)`;`_record_decision` / `_finish_decision` 逐层透传;**`MdCGSecure.add` 把 `kw["reviewed_by"]` 交给 `require_layer_write`** | 建议 3(带上本次裁决的复核单元) |

**建议 1(走 host logger)**未做:那要改宿主侧的 stderr 转发策略,不是本仓能决定的;
改为**不依赖 stderr** 的计数通道(建议 2),这是同等甚至更强的可观测性。

**建议 4(改 README 写明"enforce 会连带阻断审计节点")**不采用——那不是「已知边界」,是缺陷;
已修好,不该把缺陷写进文档当特性。

# 关键根因(第 4 条)

只改 `_write_review_record` 的 `reviewed_by=` 是**无效**的:`MdCGSecure.add` 里

```python
self.principal.require_layer_write(layer, sens)   # ← 只传两参,kw 里的 reviewed_by 从未被消费
```

`reviewed_by` 只是躺进 `**kw`,闸照样按「无复核单元」判。**实测第一版以为是修好了,结果 C1 仍红**——
这次是守卫先把「带 reviewers 仍被拒」照出来,才顺着查到 `require_layer_write` 的调用点。

# 验证

新守卫 `md_cg/test_issue708_writegate_observable.py`:**17 通过 / 0 失败**

| 组 | 断言 |
|---|---|
| A | warn 档写入成功、**计数 +1**、按层分桶 `knowledge@L1`、recent 含 reason(无内容/无 id) |
| B | `warn_report` 键齐全、recent ≤5、有界常量 20 |
| **C1** | enforce 下**带** reviewers ⇒ 审计节点落盘成功 |
| **C2** | **不带** reviewers 仍被拒(对照:不是放水) |
| D | 全链透传 6 处锚点(`_write_review_record`→`add`→`require_layer_write`→两处 `_finish_decision`) |
| **E1** | 只给 `reflect`(缺 verify)**仍被拒**——透传不等于万能放行 |

既有关联守卫全绿:`test_writegate_tiers`(17) · `test_review_dual_check` · `test_issue644_637_636` · `test_issue522_review_record_sensitivity` · `test_run_tests_gate`(17)。

# 使用说明(升级后)

```bash
# 观察期:看有多少写入本该复核
cg(op="info")["write_gate"]   # {mode, total, by_layer, recent, hint}
```

计数是**进程内累计,重启归零**(不写盘,零副作用)。观察够了再切 `enforce`。
# 你报的是第 ③ 个,我查出整条链上还有两个在前面

`reviewed_by` 在 enforce 档下需要贯穿三个写入点,修前**只有 dual_check 用到它**,三个落点全没传:

| # | 落点 | 修前现象 | 危害 |
|---|---|---|---|
| **①** | `propose`(候选入 inbox) | 按**目标层**要求复核单元 ⇒ 被拦 | **死锁**:要写 knowledge 需 reflect → 要 reflect 需裁决 → 要裁决需提案 → 提案又要 reflect。**连提案都提不进去** |
| **②** | `review_decide` 的 accept 落业务节点(`add`) | 不传 `reviewed_by` ⇒ 被同一道闸拒 | **裁决过了、落盘被拦**(「批了但写不进去」) |
| **③** | `_write_review_record` 写 structural 审计节点 | 不传 `reviewers` ⇒ 被同一道闸拒 | **审计链断裂**(你报的这条) |

你之所以只看到 ③,是因为你走的是 `forget` 产生的**变更单**(`_decide_mutation` 路径),绕过了 ①;而 ② 在你的场景里恰好没触发。

# 修法

| # | 改法 |
|---|---|
| ① | `require_layer_write` 新增 `skip_gate: bool = False`(**缺省 False**);**只** `propose` 传 `skip_gate=True` |
| ② | `review_decide` 的 accept 落盘 `add(..., reviewed_by=reviewed_by)` |
| ③ | `_write_review_record(rec, reviewers=None)` → `add(..., reviewed_by=reviewers)`;`_record_decision` / `_finish_decision` 逐层透传 |

## 为什么 ① 用 skip_gate 而不是也传 reviewers

`propose` **不落记忆盘**(只把候选写进 inbox),它按目标层要求复核单元本身就是错的。
而且它没法传:提案还没被裁决,哪来的 reflect 单元?——所以只能跳过闸。

**这不是"关闸"**,三条护栏:
- `skip_gate` 缺省 False,**全仓只有一处** `skip_gate=True`(守卫 C1 钉住);
- **层白名单与密级检查在 skip_gate 之前**(守卫 C4 用位置断言钉住顺序);
- `accept` 落盘仍走同一道闸(守卫 E1/E2 验证:无 reflect 仍被拒、给了 reflect 才过)。

# 验证(新守卫 13 条)

| 组 | 断言 |
|---|---|
| A | enforce 下 `propose` knowledge **成功**(修前死锁) |
| **B** | accept 返回 `ok` / `node_id` 有值 / **`record_error` 为空** / `record_node_id` 有值 |
| C | `skip_gate` 只有一处、缺省 False、**层白名单在它之前** |
| D | `verify` 角色 propose knowledge **仍被层白名单拒**(skip_gate 不越权) |
| E | 无 `reviewed_by` 直写 knowledge **仍被拒**;给 `reflect`(1 档只需它)**通过** |

既有关联守卫全绿:`test_issue708_writegate_observable`(17) · `test_writegate_tiers`(17) · `test_review_dual_check` · `test_issue522_review_record_sensitivity` · `test_issue644_637_636` · `test_issue89_zero_score`。

# 关于你给的 4 个方向

| 方向 | 我的取舍 |
|---|---|
| 1. 把本次裁决已有的复核单元带入 | **采用**(本次修法) |
| 2. 审计账本走单独通道、不走闸 | **不采用**:审计节点仍是记忆图的一部分,绕开层保护等于给它开特例;方向 1 已能让它正常落盘,无需豁免 |
| 3. 明确降级:标注「仅 jsonl 审计」而非 `record_error` | **部分采纳**:正常路径已不再失败;但保留 `record_error` 字段本身有价值——**它是"该失败就如实报"的唯一出口**,改成"静默降级"会让真故障也看不见。若将来仍有意外,`record_error` 会带着具体原因出现 |
| 4. README 写明「enforce 会连带阻断」 | **不采用**:那不是边界,是缺陷;已修 |

> 另:你 #708 里提的 warn 档不可观测,已在同一批修掉(`cg(op=info).write_gate`)。
@FuRongJun-1999 FuRongJun-1999 changed the title fix(writegate): #708 warn 档可观测 + enforce 下裁决审计节点不再被同一闸挡住 fix(review): #708/#709 复核闸三处 reviewed_by 缺口 + warn 档可观测 Oct 28, 2026
…frontmatter

# 红点(PR #710)

| 红 | 病灶 |
|---|---|
| cogmap ×2 | `docs/mdcg/cogmap_ops.md` 的行号引用随 `mcp_server.py` 改动漂移,**产物没提交** |
| run-tests | 7 个测试红:`test_autonomy_*` / `test_generation_guard` / `test_i50c` / `test_issue297` / `test_issue352` / `test_is*` |

# 三个真因(逐个取证)

## ① 变更单桥 `NameError: reviewed_by is not defined`

我把 `reviewers=reviewed_by` 加在了 `_decide_mutation` 里的 `_finish_decision` 调用点,**但那个方法的作用域里没有 `reviewed_by`**。

修法:`_decide_mutation(..., reviewers=None)` 加参数,`review_decide` 调用时透传,桥内使用 `reviewers=reviewers`。
> 这是「改了调用点却没查作用域」的低级错误——`test_issue297` 的 S3 组当场报 NameError。

## ② `reviewed_by` 被写进 frontmatter

`test_i50c` 的 **B1 逐键对拍**报出白名单外差异:

```
reviewed_by | <absent> | null | **差异**
```

根因:我原来用 `kw.get("reviewed_by")` **只读不删**,`reviewed_by` 留在 `kw` 里,经 `super().add(**kw)` 落进了节点 fm。

修法:改成 **`kw.pop("reviewed_by", None)`**——它是**写入闸的输入参数,不是记忆元数据**,必须消费掉不能下传。

## ③ `op=info` 新增 `write_gate` 触发键集守卫

`test_generation_guard` 的 `PINNED_INFO_KEYS`(既有 20 键)+ `NEW_INFO_KEYS`(五新键)是**契约面**:多一个键就红。

修法:把 `write_gate` 登记进 `NEW_INFO_KEYS`(与 `code_generation` 等同类,都是新增诊断键)。

# 附带修正(都是我自己引入的)

- `test_i50c` 的变异锚点:`condition_space=item.get(...), **extra)` 被我拆成两行 ⇒ 锚点失配。
  更新为**带 `\n` 的单行 repr**(先用隐式拼接写下,结果拼出的文本**缺换行**,反而更隐蔽——这一步绕了两回)。
- `test_issue708` 的 D3 断言:原写 `reviewers=kw.get(...)`,改成 `pop` 后同步更新,并补注释说明**为什么必须 pop**。

# 验证

| 项 | 读数 |
|---|---|
| `cogmap_sync.py check` | **通过**(重建后 38 op / 5 op / 6 action / 31 个 mdcg_*) |
| 本次相关 13 个守卫 | **全绿** |
| `test_issue708_writegate_observable` | **17 通过 / 0 失败** |
| `test_issue709_review_chain_enforce` | **13 通过 / 0 失败** |
| `test_i50c_meta_passthrough` | **31 通过 / 0 失败**(含 B1 白名单外差异为 0) |
| `test_issue297_rewrite_sensitivity` | **35 通过 / 0 失败** |
| `test_generation_guard` | 五组全 PASS(含键集对拍) |
# 红点(PR #710 · run-tests)

```
FAIL ★verdict 判定成立
  [读码:写闸在位(...): md_cg/mdcos.py:3786 + md_cg/security.py:217-224]
失败:test.chaos_injection.run_all
```

# 真因

`test/chaos_injection/test_fi_m09_guest_write_denied.py` 用**精确字符串**锚定写入闸的调用形式:

```python
"self.principal.require_layer_write(layer, sens)" in harness.src("md_cg/mdcos.py")
```

而 #708/#709 把该调用改成了 `require_layer_write(layer, sens, reviewers=_reviewers)` ⇒ **右括号后多了实参,精确串失配**。

# 修法(**不削弱强度**)

| 项 | 改法 |
|---|---|
| 锚点 | `require_layer_write(layer, sens)` → **`require_layer_write(layer, sens`**(只放开右括号后的扩展实参;仍要求 `layer, sens` 两参在位 + `security.py` 里有 `def`) |
| 证据串 | `"md_cg/mdcos.py:3786 + md_cg/security.py:217-224"` → 去掉行号,改为按内容指认(行号会随改动漂移,写死等于埋定时炸弹) |
| 诚实注记 | 同上,`mdcos.py:3786` → `require_layer_write`(按符号指认) |

# 验证

| 项 | 读数 |
|---|---|
| `test_fi_m09_guest_write_denied.py` | **rc=0,verdict=pass,fails=[]** |
| 该 case 六条 check | 全 OK(读码锚点 / 红场①②③ / 对照组 / 四可) |

# 一条教训(第 4 条)

**读码锚点写成「完整调用串」是脆的**——加一个实参就失配,而且失配信息(`3786` 这样的行号)本身也会漂移。
本仓既有约定是「锚点必须逐字在位」(`test_i50c` 的 ANCHOR-MISS 机制),但**该逐字的是"判据语义",不是"调用签名的全部实参"**。
这次保留了 `layer, sens` 这两个**判据相关**的实参,放开的只是与判据无关的扩展参数面。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant