Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@

### 修复 Fixed

- **健康轮 SYNTH 全灭修复:CJK 检索词在 C locale 下被 ANSI-C 包裹致 rg 崩(2026-10-26;回归源=#196 C4 spawn env 白名单丢 LANG)**:线上 v1.4.2-721 起每个 health 轮 SYNTH 步必炸 `inject_precedents: rg 检索失败(rc=2)`——C4 白名单后轮子进程无 LANG→bash 退 C locale→`printf %q` 给 CJK 检索词(如「限流」)输出 `$'\351\…'` 字面包裹,而命令替换输出不再经词法解析→rg 模式含 `$'`/八进制转义被解析为反向引用→parse error rc=2;回归起点=2026-10-25-1900-health(10-24-1800 及之前全绿)。两层修复:①根治层=lib.sh inject_precedents 弃 `$(printf -- '-e %q ' …)` 命令替换拼参,改 argv 数组直传(`rg_args+=(-e "$kw")`)——任何 locale 下 CJK 原样字节进 argv,与 locale 彻底解耦(输出块格式零变);②环境层=_round_env() 桥注入 LANG(继承操作员值,缺省 C.UTF-8)防同类 locale 敏感行为复发。门:selftest.sh 119/0(基 116+新 C-locale 回归钉三断言;红证:旧 lib.sh 下恰该例两断言红 PASS=117 FAIL=2)、t4 63/0(golden 三方字节对拍照旧)、backend test_ops_rounds+test_ops_config 81 passed(基 80;白名单对拍集同步 LANG+LANG 继承/缺省两断言)、`env -i` 手工钉死复现消亡(限流 排他率→注入块 9 命中、exit 0)
- **Health-round SYNTH wipeout fixed: CJK keywords ANSI-C-wrapped under C locale crash rg (2026-10-26; regression source = the #196 C4 spawn-env allowlist dropping LANG)**: from v1.4.2-721 every health round's SYNTH step died with `inject_precedents: rg 检索失败(rc=2)` — post-allowlist the round child had no LANG → bash fell back to C locale → `printf %q` wrapped CJK keywords (e.g. 限流) as `$'\351\…'` literals, and command-substitution output is never re-parsed as quoting → rg received `$'`/octal-escape bytes in the pattern, parsed as backreferences → parse error rc=2; regression onset = 2026-10-25-1900-health (10-24-1800 and earlier all green). Two-layer fix: ① root layer — lib.sh inject_precedents drops the `$(printf -- '-e %q ' …)` command-substitution argv assembly for a direct argv array (`rg_args+=(-e "$kw")`) — CJK bytes reach argv verbatim under any locale, fully locale-decoupled (output block format unchanged); ② environment layer — _round_env() bridge-injects LANG (operator value inherited, default C.UTF-8) against locale-sensitive recurrences. Gates: selftest.sh 119/0 (base 116 + the new C-locale regression pin's three assertions; red proof: on the old lib.sh exactly that case reddens, PASS=117 FAIL=2), t4 63/0 (golden three-way byte comparison unchanged), backend test_ops_rounds + test_ops_config 81 passed (base 80; allowlist comparison set synced with LANG + two LANG inheritance/default assertions), `env -i` manual repro pinned dead (限流 排他率 → injection block with 9 hits, exit 0)

- **驾驶舱「展开全量日志」中途开页点击无感修复(2026-10-26,单写者直修)**:根因=task_log 事件只发不留——用户在轮中开页时订阅前已发的 stage 行全丢,日志面只剩订阅后 1 行,展开/收起视觉无差;修复三层:①后端 UpdateManager 增 ops stderr 日志尾(`_OPS_LOG_KEEP=200` deque,与前端 LOG_KEEP_LINES 同窗;start 建、下一轮 start 清扫、终态后仍随快照出——轮后开页也能播上一轮尾),snapshot() 增 `logs` 面(键=task_id,经 TasksSnapshotOut extra="allow" 透传);未知 task_id 照发事件不建尾(恢复轮替身不破);②前端 TaskProvider seedLogs 快照播种(SSE snapshot 事件+resync fetch 两口同接),合并律=尾更长才替换——同 task 的 append-only 流里更短的旧快照(fetch/SSE 竞态)不得缩水已收行;③驾驶舱 UX:展开钮带行数计数(单行时展开/收起也有可见差),新一轮起跑上升沿复位展开态(全新日志面不跨轮携带)。门:backend test_ops_rounds+test_tasks+pruning 90/0(新三例:尾随快照出+终态留+下轮清扫/205 行窗 200/未知 tid 只发不建),frontend vitest 437/437(新三例:快照播种+播种后追加/更短旧快照不缩水/计数+复位)+tsc+build 绿
- **Cockpit "show full log" mid-round open click-deadness fix (2026-10-26, single-writer direct fix)**: root cause = task_log events are fire-and-forget — a page opened mid-round loses every stage line emitted before subscribe, leaving a 1-line log face where expand/collapse looks identical; fix in three layers: ① backend UpdateManager gains an ops stderr tail (`_OPS_LOG_KEEP=200` deque, same window as the frontend LOG_KEEP_LINES; created at start_ops_round, swept by the next round's start, retained past terminal state so a post-round page open seeds the previous round's tail too), snapshot() gains a `logs` face (key=task_id, flowing through TasksSnapshotOut extra="allow"); unknown task_id still emits without a tail (recovery-round stand-ins don't break); ② frontend TaskProvider seedLogs seeds from the snapshot (both the SSE snapshot event and the resync fetch), merge rule = replace only if the tail is longer — a shorter stale snapshot (fetch/SSE race) never shrinks already-received lines on the same append-only stream; ③ cockpit UX: the expand button carries a line count (a visible difference even with a single line), and a new round's rising edge resets the expanded state (a fresh log face never carries over). Gates: backend test_ops_rounds + test_tasks + pruning 90/0 (three new: tail in snapshot + survives terminal + swept next round / 205 lines capped at 200 / unknown tid emits without tail), frontend vitest 437/437 (three new: snapshot seeding + post-seed append / shorter stale snapshot never shrinks / count + reset) + tsc + build green

Expand Down
2 changes: 2 additions & 0 deletions backend/ipdb/_ops_rounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ def _round_env() -> dict[str, str]:
"DOCS_ROOT": str(root.parent),
"PRECEDENTS_DIR": str(root / "precedents"),
"LEDGER_FILE": str(root / "ledger.md"),
# 2026-10-26:C4 白名单剔 LANG→子进程 bash 退 C locale,%q 对 CJK 输出 ANSI-C 包裹致 rg 解析炸(健康轮 SYNTH 全灭回归);bash 面 locale 敏感行为需确定性:继承操作员 LANG,缺省 C.UTF-8。
"LANG": os.environ.get("LANG") or "C.UTF-8",
"OPS_EVAL_PY": sys.executable,
"OPS_DATA_DIR": str(_registry.DATA_DIR),
}
Expand Down
16 changes: 15 additions & 1 deletion backend/tests/test_ops_rounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,10 +521,13 @@ def test_spawn_env_injects_ops_tree_and_fences(rounds, ops_env, fake_agent):
# 契约单源 = ops/agent/agent.mjs SPAWN_CONTRACT_ENV(OPS_*/PI_* 前缀 + 精确名)
# + 运行必需(PATH/HOME/TMPDIR/IP_RADAR_DATA_DIR)。IP_RADAR_ALERT_URLS 是
# #184 finding webhook 出口契约员(与 backend _alerts.push 双消费面),保留透传。
# LANG = _round_env 桥注入面(2026-10-26 判例:C4 白名单剔 LANG→子进程 bash 退
# C locale,%q 给 CJK 包 ANSI-C 字面量→rg rc=2 健康轮 SYNTH 全灭;继承操作员值
# 缺省 C.UTF-8,DOCS_ROOT 族同型)。
SPAWN_ENV_EXACT = {"PATH", "HOME", "TMPDIR", "GH_TOKEN",
"IP_RADAR_DATA_DIR", "IP_RADAR_ALERT_URLS",
"ZAI_CODING_CN_API_KEY", "JINA_API_KEY",
"DOCS_ROOT", "PRECEDENTS_DIR", "LEDGER_FILE"}
"DOCS_ROOT", "PRECEDENTS_DIR", "LEDGER_FILE", "LANG"}


def test_spawn_env_allowlist_excludes_secrets(
Expand Down Expand Up @@ -556,6 +559,17 @@ def test_spawn_env_exactly_contract_face(
assert extra == [], f"白名单外 env 透传子进程: {extra}"


def test_round_env_lang_locale_determinism(rounds, ops_env, monkeypatch):
"""2026-10-26 判例:C4 白名单剔 LANG→子进程 bash 退 C locale,%q 给 CJK 检索词
包 ANSI-C 字面量→rg 反向引用 rc=2(线上 10-25-1900 起健康轮 SYNTH 全灭)——
桥注入 LANG 钉 bash 面 locale 确定性:os.environ 无 LANG 缺省 C.UTF-8,
有值继承操作员原值。"""
monkeypatch.delenv("LANG", raising=False)
assert rounds._round_env()["LANG"] == "C.UTF-8"
monkeypatch.setenv("LANG", "zh_CN.UTF-8")
assert rounds._round_env()["LANG"] == "zh_CN.UTF-8"


def test_round_env_secret_files_0600(rounds, ops_env, fake_agent):
"""C4 附带(#196):物化密钥件(auth.json/web-search.json)权限 0600——
mkstemp 面已隐式满足,显式钉住契约防实现漂移(arch 评审 C4 附带项)。"""
Expand Down
5 changes: 4 additions & 1 deletion ops/agent/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ inject_precedents() {
echo "### 注入判例(检索词: ${kws[*]})"
local hits="" rg_out rg_rc=0
if (( ${#kws[@]} )); then
# 2026-10-26 判例:C4 白名单丢 LANG→C locale 下 %q 给 CJK 包 $'..' 字面量→rg 反向引用 rc=2(线上 10-25-1900 起健康轮 SYNTH 全灭);argv 直传与 locale 解耦
local rg_args=() kw
for kw in "${kws[@]}"; do rg_args+=(-e "$kw"); done
rg_out="$(rg -i -l -g '*.md' -g '!_*' \
$(printf -- '-e %q ' "${kws[@]}") "$PRECEDENTS_DIR" 2>/dev/null)" || rg_rc=$?
"${rg_args[@]}" "$PRECEDENTS_DIR" 2>/dev/null)" || rg_rc=$?
fi
if (( rg_rc > 1 )); then
# 评审 Minor-3:rc≥2 = 目录缺失/非法检索词等基础设施故障,必须报错,
Expand Down
13 changes: 13 additions & 0 deletions ops/agent/selftest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ OUT="$(inject_precedents 排他率 2>&1)"
OUT="$(inject_precedents 不存在的词xyz 2>&1)"
[[ "$OUT" == *"无判例(检索词: 不存在的词xyz)"* ]] && ok "inject:零命中显式无判例行" || bad "inject:零命中显式无判例行"

# C-locale 回归钉(2026-10-26 判例:C4 spawn 白名单丢 LANG→子进程 bash 退 C locale,
# %q 给 CJK 检索词包 $'..' ANSI-C 字面量→rg 反向引用 parse error rc=2 报「基础设施
# 故障」,线上 10-25-1900 起健康轮 SYNTH 步全灭):env -i 子壳(仅 PATH)带 CJK 词
# 调用必须正常检索命中,绝不 rc=2。
OUT="$(env -i PATH="$PATH" PRECEDENTS_DIR="$FIXDIR" bash -c \
". \"$AGENT/lib.sh\"; inject_precedents 排他率 不存在的词xyz" 2>&1)"; CJK_RC=$?
[[ "$OUT" == "### 注入判例(检索词: 排他率 不存在的词xyz)"* ]] \
&& ok "inject:C-locale CJK 检索不炸(首行注入判例头)" || bad "inject:C-locale CJK 检索不炸(首行注入判例头)"
[[ "$OUT" == *"2026-01-01-fixture-aggregator.md"* ]] \
&& ok "inject:C-locale CJK 词真命中(判例行在场)" || bad "inject:C-locale CJK 词真命中(判例行在场)"
[[ "$OUT" != *"rg 检索失败"* && "$CJK_RC" == 0 ]] \
&& ok "inject:C-locale 零基础设施故障行且 rc=0" || bad "inject:C-locale 零基础设施故障行且 rc=0"

# 真实判例库冒烟(只读):frontmatter 解析对 12 条种子成立。子壳隔离:bash 对
# 「VAR=x 函数调用」的前缀赋值会在函数返回后泄漏到当前 shell(非 POSIX 模式)。
# docs 符号链接缺席(CI checkout)→ SKIP 显式计 PASS(T2 守卫,理由见 skipok 注释)。
Expand Down
Loading