pi-legion 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/design.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # pi-legion 设计文档
2
2
 
3
+ > **⚠️ 历史实验笔记(非现行契约)**
4
+ > 本文记录 herdr / pane 时代的实测与设计推演。**运行时契约以 [README](../README.md) 与 [CONTRIBUTING](../CONTRIBUTING.md) 为准**(Pi Legion 2.x = headless Pi RPC + `legion.db`;herdr pane 执行已归档到 `src/backends/legacy/`)。
5
+ > 下文 F 条中标 **✅ landed** 的是已在当前 master 落地的修复;未标注的早期结论仍有参考价值,但不要当作「今日仍坏」的缺口清单。
6
+
3
7
  > 基于 10 组实测实验(exp1–exp10)得出的设计。所有结论都标注了实验依据,
4
8
  > 未经验证的假设明确标记为「待验证」。
5
9
 
@@ -20,7 +24,7 @@
20
24
 
21
25
  ### 实现状态(相对本文档)
22
26
 
23
- 下面 F 条目里不少写的是「当时的缺口」。当前运行时已经钉死、以 [README](../README.md) 为准:
27
+ 下面 F 条目里不少写的是「当时的缺口」(文首 banner 已说明:契约以 README + CONTRIBUTING 为准)。当前运行时已经钉死:
24
28
 
25
29
  - 子进程强制加载 child-guard:拦 `herdr agent prompt|wait|send-keys|start`、外 pane read、只读角色写操作
26
30
  - 任务卡自动附录(禁止 wakeup、必须 `{"ok":…}`)
@@ -654,10 +658,11 @@ system prompt 不再注入 playbook/roster;bash 派发 guard 放行手动 herd
654
658
  ### 6.4.1 Scout guard `subagents.scoutGuard`
655
659
 
656
660
  只在主会话生效(子进程带 `PI_SUBAGENT_ROLE`,不计数、不注入)。连续 N 次
657
- 读类工具调用(`read`,以及可判定为只读的 bash:`cat`/`head`/`tail`/`less`/
658
- `rg`/`grep`/`find`/`ls`/`stat`/`wc`/`file`/`jq`、`git show|log|diff|status|blame`、
659
- 不带 `-i` 的 `sed`)且中间没有成功的 `subagent` / `vexillatio` 派发时,异步
660
- 注入一条 follow-up 提醒,不挡住当前 `tool_call`。
661
+ 读类工具调用(first-class `read`/`grep`/`find`/`ls`,以及可判定为只读的 bash:
662
+ `cat`/`head`/`tail`/`less`/`rg`/`grep`/`find`/`ls`/`stat`/`wc`/`file`/`jq`、
663
+ `git show|log|diff|status|blame`、不带 `-i` 的 `sed`)且中间没有成功的
664
+ `subagent` / `vexillatio` 派发时,异步注入一条 follow-up 提醒,不挡住当前
665
+ `tool_call`。
661
666
 
662
667
  ```json
663
668
  { "subagents": { "scoutGuard": { "enabled": true, "threshold": 8, "cooldownMs": 120000 } } }
@@ -928,12 +933,14 @@ subagent({
928
933
  | `resume` | **已退出后恢复**(F12) | 新 pane + `--session <file>` |
929
934
  | `retire` | 回收 | ctrl+d / pane close / tab close |
930
935
  | `status` / `collect` / `list` | 查询 | child record / session JSONL |
931
- | `wait` | 等待指定或全部活动 child 完成;由 RPC settle 事件唤醒;ESC 中断时级联优雅关闭 children 后返回 | session runtime + RPC supervisor |
936
+ | `wait` | 等待指定或全部活动 child 完成;由 RPC settle 事件唤醒;ESC 中断只结束等待(children 继续跑并 re-arm watch) | session runtime + RPC supervisor |
932
937
  | `stop` | 优雅停止指定或全部 tracked、非 retired children | `retire` 全链 + runtime release |
933
938
 
934
939
  ### ESC abort 语义 + stop
935
940
 
936
- 父 turn 的 ESC / RPC abort signal 会立即打断当前工具等待,并静默级联优雅关闭该 session 所拥有的 tracked children(包括 vexillatio utility children)。`wait` 返回 `interrupted (parent turn aborted)` 前先完成级联 retire;`collect` / inline collect 被中断时也会级联关闭 children,不生成显式 stop 的工具结果文案。级联先取消 completion watch,再并行 retire 并释放 runtime tracking;并发 abort 复用同一级联 promise。新的 turn 会建立新的级联周期。正常完成的 `wait` 仍由 RPC settle 事件立即解除,不需要等满 timeout。
941
+ > **v3(2026-10-15)**:见 `docs/contracts/esc-no-retire-v3.md`。ESC / 父 turn abort **不再**级联 retire。
942
+
943
+ 父 turn 的 ESC / RPC abort signal 会立即打断当前工具等待,但 **children 继续运行**;`wait` 返回 `interrupted (parent turn ended; children still running)` 并 re-arm watch,settle 仍通知父会话。显式 `/legion stop` / `stop` action,或主进程退出(`session_shutdown`,print/json 先 drain)才会 retire。正常完成的 `wait` 仍由 RPC settle 事件立即解除,不需要等满 timeout。
937
944
 
938
945
  `stop` 是显式停止操作:`subagent({ action: "stop", name: "worker-0" })` 停一个 child,`subagent({ action: "stop", all: true })` 逐个停止当前跟踪的全部非 retired children。它复用 `retire` 生命周期(优雅关闭、超时后升级信号、后代清理)并释放 runtime 跟踪;`stop` 本身不接父 turn abort signal。`name` 与 `all` 互斥且必须指定其一。
939
946
 
@@ -1132,12 +1139,24 @@ index.ts(132,4): error TS7006: Parameter 'signal' implicitly has an 'any' type.
1132
1139
 
1133
1140
  ---
1134
1141
 
1135
- ## 16. 交付前安全/健壮性审计(F38–F45)
1142
+ ## 16. 交付前安全/健壮性审计(F38–F46)— Historical findings(✅ landed)
1136
1143
 
1137
1144
  对全部模块做了对抗式输入扫描(畸形会话文件、路径穿越、恶意配置、并发写)。
1138
- 以下 8 条是真缺陷,其余区域已验证健壮。
1145
+ 以下条目**当时**是真缺陷;对照当前 master 均已落地(见各条 ✅),勿再当 open gap。
1139
1146
 
1140
- ### F38 — ReDoS:glob → RegExp 的嵌套量词(安全)
1147
+ | # | 当时缺口 | master 落点(摘要) |
1148
+ | --- | --- | --- |
1149
+ | F38 | glob→RegExp ReDoS | `matchesScopePattern` 双指针 glob,**不用 RegExp** |
1150
+ | F39 | `openSync`/`closeSync` fd 泄漏 | session 预创建改 `writeFileSync(..., { flag: "wx" })` |
1151
+ | F40 | `JSON.parse` 标量拖垮会话解析 | `decodeEvent` 非对象计入 `tornLines` |
1152
+ | F41 | `tasks[]` 条目未校验 | `firstInvalidStep` / 启动前校验 agent+task |
1153
+ | F42 | 空白 `task` 被 `Boolean` 放行 | 存在性按 `trim()` |
1154
+ | F43 | 角色 `timeoutMs` 被忽略 | launch 记入 `timeoutByName`,follow/wait 使用 |
1155
+ | F44 | `acceptance.criteria` 静默丢弃 | `pendingCriteria` + command 才升 `verified` |
1156
+ | F45 | 未生效配置看起来生效 | `UNENFORCED_FIELDS` 现为空;list 不再假警告 |
1157
+ | F46 | 嵌套 YAML acceptance 未解析 | `parseAcceptance` + `parseIndentedBlock` |
1158
+
1159
+ ### F38 — ✅ landed — ReDoS:glob → RegExp 的嵌套量词(安全)
1141
1160
 
1142
1161
  `modelScope.allow` 的 glob 被翻译成 `.*`,于是 `*a*a*a…b` 变成 `.*a.*a.*a…b`——
1143
1162
  嵌套量词在**不匹配**的输入上指数回溯。
@@ -1152,7 +1171,7 @@ index.ts(132,4): error TS7006: Parameter 'signal' implicitly has an 'any' type.
1152
1171
  **修法**:不用正则,改用双指针贪心 glob 匹配(只回溯最近一个 `*`)。
1153
1172
  修复后 n=6400 仅 2ms。
1154
1173
 
1155
- ### F39 — fd 泄漏:`openSync`/`closeSync` 无保护
1174
+ ### F39 — ✅ landed — fd 泄漏:`openSync`/`closeSync` 无保护
1156
1175
 
1157
1176
  ```ts
1158
1177
  const fd = fs.openSync(file, "w", 0o600);
@@ -1165,7 +1184,7 @@ launcher 为**每个 child** 创建一个 session 文件,所以泄漏会随 fa
1165
1184
  **修法**:`fs.writeFileSync(file, "", { flag: "wx", mode: 0o600 })`——
1166
1185
  由 Node 自己管理描述符,并且 `wx` 顺带消除了 `existsSync` 检查留下的 TOCTOU 窗口。
1167
1186
 
1168
- ### F40 — `JSON.parse("null")` 拖垮整个会话解析
1187
+ ### F40 — ✅ landed — `JSON.parse("null")` 拖垮整个会话解析
1169
1188
 
1170
1189
  `JSON.parse` 对标量是成功的:`null` / `42` / `true` / `"x"`。
1171
1190
  随后 `event.type` 在 `null` 上抛异常,**整个 `parseSessionText` 崩溃**,
@@ -1173,7 +1192,7 @@ launcher 为**每个 child** 创建一个 session 文件,所以泄漏会随 fa
1173
1192
 
1174
1193
  **修法**:非对象、非数组的标量按损坏行计入 `tornLines`,继续解析其余行。
1175
1194
 
1176
- ### F41 — `tasks[]` 的条目未校验
1195
+ ### F41 — ✅ landed — `tasks[]` 的条目未校验
1177
1196
 
1178
1197
  单任务路径用 `Boolean(params.agent && params.task)` 把关,但数组路径直接放行:
1179
1198
 
@@ -1184,12 +1203,12 @@ tasks:[{task:"do it"}] → 放行,agent 名为 undefined
1184
1203
 
1185
1204
  **修法**:每个 step 在分配任何资源之前校验 `agent` 与 `task`。
1186
1205
 
1187
- ### F42 — `Boolean(" ")` 为真:纯空白 task 被放行
1206
+ ### F42 — ✅ landed — `Boolean(" ")` 为真:纯空白 task 被放行
1188
1207
 
1189
1208
  即使单任务路径,`task: " "` 也会通过,生成一个毫无意义的 `Task:` prompt。
1190
1209
  **修法**:存在性按 `trim()` 后的内容判定,而非 `Boolean()`。
1191
1210
 
1192
- ### F43 — `timeoutMs` 被解析却从不生效
1211
+ ### F43 — ✅ landed — `timeoutMs` 被解析却从不生效
1193
1212
 
1194
1213
  5 个自带角色都声明了预算,运行时却全部忽略,永远用全局默认 15 分钟:
1195
1214
 
@@ -1201,7 +1220,7 @@ DEFAULTS.turnTimeoutMs: 900000 (15min) ← 实际生效
1201
1220
 
1202
1221
  **修法**:`collect` 采用 `agent.timeoutMs`(按 child 记录,逐个子 agent 生效)。
1203
1222
 
1204
- ### F44 — `acceptance.criteria` 被静默丢弃
1223
+ ### F44 — ✅ landed — `acceptance.criteria` 被静默丢弃
1205
1224
 
1206
1225
  角色可以声明 `must: "测试通过"` 这类**语义**验收条件,代码解析后就不管了。
1207
1226
  调用方无法知道「没有任何东西验证过它」。
@@ -1212,7 +1231,7 @@ DEFAULTS.turnTimeoutMs: 900000 (15min) ← 实际生效
1212
1231
  **修法**:criteria 作为**待验证清单**回传调用方(`pendingCriteria`),
1213
1232
  且 `level` 保持 `attested`——**`verified` 只留给真正被人核对过的证据**。
1214
1233
 
1215
- ### F45 — 配置字段「看起来生效、实际无效」
1234
+ ### F45 — ✅ landed — 配置字段「看起来生效、实际无效」
1216
1235
 
1217
1236
  自带角色携带 `onBlocked` / `allowNestedSubagents` / `fallbackModels` / `toolTimeoutMs`,
1218
1237
  全部被解析、全部不生效。其中 `allowNestedSubagents: false` 最危险——
@@ -1227,7 +1246,7 @@ DEFAULTS.turnTimeoutMs: 900000 (15min) ← 实际生效
1227
1246
  **原则**:**一个被接受却不做事的键,比一个未知的键更危险**——
1228
1247
  用户会相信它已经在保护自己。
1229
1248
 
1230
- ### F46 — 嵌套 YAML 块从未被解析:自带角色的 acceptance 全部丢失
1249
+ ### F46 — ✅ landed — 嵌套 YAML 块从未被解析:自带角色的 acceptance 全部丢失
1231
1250
 
1232
1251
  frontmatter 解析器是**扁平的**(只处理 `key: value`);一个嵌套块会以
1233
1252
  **剥掉公共缩进后的原始文本**返回:
@@ -121,7 +121,7 @@ const VexillatioParams = Type.Object({
121
121
 
122
122
  **既有测试文件除 §3 明确授权的 `test/unit/child-mode.test.ts` 两处断言数字修订外一律不修改**;§4 要求的新增测试全部落在两个新文件里。
123
123
 
124
- **断言影响总则(REV-17.5)**:契约导致的注册行为变化若使某既有断言失效,属于本契约的影响面;授权范围以本 §3 该行所列为准,本行已全量扫描(grep `pi.tools\|tools.length\|tools.find\|tools.get\|herdrSubagents(` 全测试目录)并一次列尽。后续若再发现同类冲突,上报照旧。
124
+ **断言影响总则(REV-17.5)**:契约导致的注册行为变化若使某既有断言失效,属于本契约的影响面;授权范围以本 §3 该行所列为准,本行已全量扫描(grep `pi.tools\|tools.length\|tools.find\|tools.get\|piLegion(` 全测试目录)并一次列尽。后续若再发现同类冲突,上报照旧。
125
125
 
126
126
  ## 4. 验收清单(可机器检查)
127
127