Skip to content

fix(verify-cli): analyzer 隔離 / exitCode 化 / audit help の是正 (#283) - #286

Merged
shinyaoguri merged 3 commits into
mainfrom
fix/verify-cli-analyzer-isolation-exitcode
Aug 24, 2026
Merged

fix(verify-cli): analyzer 隔離 / exitCode 化 / audit help の是正 (#283)#286
shinyaoguri merged 3 commits into
mainfrom
fix/verify-cli-analyzer-isolation-exitcode

Conversation

@shinyaoguri

Copy link
Copy Markdown
Owner

#238 を分割した 5 本のうち、verify-cli 分 (#283) の 3 項目をまとめて直す。触るファイルは
packages/verify-cli/** に閉じているので、他の分割先 (#281 #282 #284 #285) と並行して進められる。

目的

c3 — 外部 analyzer が検証結果を書き換えられる構造を塞ぐ (本命)

runAnalysis へ渡す AnalysisInput.verification は検証結果オブジェクトそのものの参照で、
valid / chainValid / metadataValid / poswSkippedrunAnalysisに読まれていた。
そのため --analyzer で渡した分析器が input.verification.valid = true と書くだけで、
改ざん proof が exit 0 で通り、三層保証の integrity も表示上の統計も汚染できた。

脅威主体は --analyzer を渡す採点者自身なので実害の深刻度は低いが、
ADR-0009 / ADR-0023 の「advisory は valid / exit code に漏れない」不変条件が
外部 analyzer に対して構造的に破れている
状態だった。

c1 — process.exit() によるパイプ時の stdout 切り捨て

stdout がパイプのとき Node の書き込みは非同期なので、process.exit() は未 flush の出力を捨てる。
クラス単位の ZIP を | tee report.txt / | less で受ける採点運用がまさに壊れる形で欠ける。

c2 — audit モードの help が実装と食い違う

audit - fast + deterministic PoSW sampling (placeholder) は「fast 相当で軽い」と読めるが、
実装は full と同等 (poswModeForaudit → 'full')。spec §6.1 も「未実装、現状 full と同等」。

変更点

コミット 内容
8c2ec53 c3: 検証結果を deep freeze してから分析層へ渡し、判定に効く値を分析層より前にすべて確定させる
af1c29b c1: cli.tsprocess.exit() 10 箇所を process.exitCode + return
f554a08 c2: audit の help を spec §6.1 の記述に揃える

c3 の守りは二重にしてある。どちらか片方が将来崩れても事故にならないようにするため:

  1. 凍結deepFreeze(result) してから runAnalysis へ渡す。ES module は strict mode なので
    書き込みは TypeError で throw し、orchestrator が握り潰す契約どおり他の分析器は止まらない。
    凍結対象は event 数に比例しない小さなサマリのみ (proof 本体は分析層が読む必要があるので触らない)
  2. 順序valid / exam / screenshots / language / processSummary、および
    deriveAssurance へ渡す result 由来のフィールドを分析層よりに確定させ、
    分析層より後に読むのは analysis だけにする

確認方法

自動

npm run lint                                  # exit 0
npm run test:run --workspaces --if-present    # 820 passed / 3 skipped
npm run build                                 # 成功

新規テスト (いずれも先に修正を戻して赤くなることを確認済み):

  • analyzerIsolation.test.ts — 検証結果の boolean を反転させにくる分析器を通しても、
    改ざん proof は valid: false / integrity: 'failed' のまま、健全な proof は valid: true のまま
    (固定値ではなく反転にしたのは、「もともとその値だった」ケースで退行を見逃さないため)。
    行儀の悪い分析器が throw しても他の分析器のシグナルが失われないことも見ている
  • exitCode.test.tscli.tsprocess.exit( が再導入されたら落ちる。
    挙動そのものはプロセスを跨ぐので e2e の領分だが、再導入を止めるのはここ
    (1 箇所足すだけで同じ穴が開き、しかも普段の TTY 実行では再現しない)
  • output.test.tsprintUsageaudit 行の文言を固定

手動 (c1 の実測)

60 proof の ZIP を生成し、遅い読み手へパイプして比較した:

node <tsx> packages/verify-cli/src/cli.ts proofs.zip --mode fast | (sleep 2; cat) > out.txt
出力バイト数 === Summary: 60/60 proofs passed === 末尾
修正前 (process.exit) 65536 (パイプバッファ 1 個分ちょうど) 無し 行の途中で切断
修正後 (process.exitCode) 79320 (全量) 有り ✓ file059_proof.json

| cat のような速い読み手では再現しない (パイプが常に空くため)。TTY 実行とファイル redirect でも
再現しないので、気付かずに再導入しやすい。

終了コードが全経路で維持されていることも実プロセスで確認した
(正常 0 / 改ざん 1 / 不正フラグ 1 / --help 0 / 引数なし 1 / 存在しないファイル 1)。

補足

  • 対になる web 側の dead な PoswMode = 'sampled' の掃除は [verify] レビュー由来の表示不整合まとめ (v1-v5, v7 + dead な PoswMode sampled) #281 で扱う
    (触るファイルが packages/verify/src に閉じるため、パッケージ単位で分けたほうが衝突しない)
  • --analyzer の外部モジュールがハンドル (タイマー・ソケット) を残すと自然終了できなくなるので、
    README に明記した。process.exit() を使わない理由は verify-cli の CLAUDE.md 不変条件 2 に追記

Closes #283
Refs #238 #243

`runAnalysis` へ渡す `AnalysisInput.verification` は検証結果オブジェクトそのものの
参照で、`valid` / `chainValid` / `metadataValid` / `poswSkipped` は `runAnalysis` の
**後**に読まれていた。そのため `--analyzer` で渡した分析器が
`input.verification.valid = true` と書くだけで、改ざん proof が exit 0 で通り、
三層保証の integrity や表示上の統計まで汚染できた。

脅威主体は `--analyzer` を渡す採点者自身なので深刻度は低いが、ADR-0009 / ADR-0023 の
「advisory は valid / exit code に漏れない」不変条件が外部 analyzer に対して構造的に
破れている状態だった。

守りを二重にする:

1. 検証結果を deep freeze してから分析層へ渡す。ES module は strict mode なので
   書き込みは TypeError で throw し、orchestrator が握り潰す契約どおり他の分析器は
   止まらない。凍結対象は event 数に比例しない小さなサマリのみ
2. 判定に効く値 (valid / exam / screenshots / language / processSummary、および
   deriveAssurance へ渡す result 由来のフィールド) を分析層より前にすべて確定させ、
   分析層より後に読むのは `analysis` だけにする

Assisted-by: Claude <noreply@anthropic.com>
stdout がパイプのとき Node の書き込みは非同期なので、`process.exit()` は未 flush の
出力を捨ててプロセスを落とす。60 proof の ZIP を遅い読み手へ流して実測したところ、
**ちょうど 65536 バイト (パイプバッファ 1 個分) で行の途中から切れ、
`=== Summary: 60/60 proofs passed ===` ごと消えた** (修正後は 79320 バイト全量)。
クラス単位の ZIP を `| tee report.txt` / `| less` で受ける採点運用がまさに壊れる形で、
TTY 実行とファイル redirect では再現しないので気付きにくい。

cli.ts の `process.exit()` 10 箇所を `process.exitCode` + `return` に置換した
(usage / flag error の早期終了も揃えた)。`main()` は async でトップレベルから
1 回だけ呼ばれるので、return したあとイベントループが空になった時点で Node が
flush してから終了する。

副作用として `--analyzer` の外部モジュールがハンドル (タイマー・ソケット) を残すと
自然終了できなくなるので、README に明記した。再導入は exitCode.test.ts が落とす。

Assisted-by: Claude <noreply@anthropic.com>
`--mode` の help は `audit - fast + deterministic PoSW sampling (placeholder)` と
書いており「fast 相当で軽い」と読めるが、実装は full と同等 (verify の poswModeFor が
audit → 'full')。spec §6.1 も「部分的 PoSW 検証 (未実装、現状 full と同等)」なので、
help だけが実装と食い違っていた。採点者が「audit なら軽くて十分」と誤読すると、
実際にはフルコスト (10,000 iter/event の再計算) を払う。

spec の記述に揃え、printUsage の文言を固定するテストを足した。

対になる web 側の dead な PoswMode = 'sampled' の掃除は #281 で扱う
(触るファイルが packages/verify/src に閉じるため)。

Assisted-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Name URL
Preview https://2adcd2f2.typedcode.pages.dev

Deployed from commit f762a3a
API: staging Workers

@shinyaoguri
shinyaoguri merged commit 07277b8 into main Aug 24, 2026
8 checks passed
@shinyaoguri
shinyaoguri deleted the fix/verify-cli-analyzer-isolation-exitcode branch August 24, 2026 03:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation pkg:verify packages/verify + verify-cli

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[verify-cli] 外部 analyzer が検証結果を書き換えられる / process.exit でパイプ出力が欠ける / audit help が実装と不一致

1 participant