#238 (2026-08 レビューのまとめ Issue) をサブシステム別に分割したうちの verify-cli 分。
項目番号 (c1〜c3) は #238 のトリアージコメントの採番。現存は 2026-08-24 / HEAD d61f126 で再確認済み。
トリアージ時点では「#218 / #266 の実装 PR に同乗が最も衝突が少ない」としていたが、
両方とも着地済み・closed (#218 → e87243d / #266 → d61f126) なので独立した PR が要る。
c3 — 外部 analyzer が検証結果を書き換えられる (最優先)
packages/verify-cli/src/verify.ts:177
const analysis = await runAnalysis({ proof, verification: result }, options.analyzers);
AnalysisInput.verification は検証結果オブジェクトそのものの参照で、
result.valid / chainValid / metadataValid / poswSkipped は runAnalysis の後 (:204-271) に読まれる。
つまり analyze() の中で input.verification.valid = true と書くだけで、
改ざん proof が valid: true (exit 0) になり、assurance.integrity も failed 以外になり、
pasteEvents の捏造まで通る (トリアージで一時 vitest により確認済み)。
脅威主体は --analyzer を渡す採点者自身なので実害の深刻度は低いが、
ADR-0009 / ADR-0023 の「advisory は valid / exit code に漏れない」不変条件が構造的に破れている。
なお deriveAssurance 自体は analysis を process 層 (notableSignals / reviewPriority) にしか流さない
(packages/shared/src/assurance.ts:120-121)。破れの経路はこの共有参照だけ。
方針 (順序で保証し、凍結でトリップワイヤを張る):
result / events / proof から導出される値をすべて runAnalysis より前に確定させ、
runAnalysis 以降に読むのは analysis だけにする
runAnalysis へは Object.freeze({ ...result }) の浅いコピーを渡す。ES module は strict mode なので
書き込みは TypeError で throw し、orchestrator が握り潰す (Analyzer の契約どおり他の analyzer は止まらない)
AnalysisInput の型 (shared) は変えない。深い凍結は event 数に比例するのでしない
~15-20 行 + 悪意 analyzer を注入して valid: false のままであることを assert するテスト。
c1 — process.exit() によるパイプ時の stdout 切り捨て
packages/verify-cli/src/cli.ts:42,51,59,87,94,109,143,154,226,229 (10 箇所)
stdout がパイプ かつ exit 時点で未 flush の出力が ≈64 KiB を超えると末尾が欠ける (POSIX のみ。
TTY / ファイル redirect では起きない)。1 proof 分は最小 700 B なので単一 proof では起きないが、
クラス単位の ZIP (数十 proof) を | tee / | less すると最後の === Summary: N/M proofs passed ===
から欠ける (トリアージで再現済み)。
方針: 全 10 箇所を process.exitCode = n; return; に置換 (usage / flag error の早期 exit も揃える)。
main() は async でトップレベルから 1 回だけ呼ばれるので return でそのまま自然終了する。~10 行。
副作用: --analyzer の外部モジュールが timer / socket を残すとプロセスが終わらなくなる。
README / CLAUDE.md の --analyzer の項に「ハンドルを残さないこと」を一言足す。
c2 — audit モードの help が実装と食い違う
packages/verify-cli/src/output.ts:519
audit - fast + deterministic PoSW sampling (placeholder)
は「fast 相当」と読めるが、実装は full と同等
(packages/verify/src/services/proofVerification.ts:72-79 の poswModeFor が audit → 'full')。
spec §6.1 の記述「部分的 PoSW 検証 (未実装、現状 full と同等)」に揃える。help 1 行。
対になる web 側の dead な PoswMode = 'sampled' の掃除は verify 側の Issue で扱う
(触るファイルが packages/verify/src/** に閉じるため、パッケージ単位で分けたほうが衝突しない)。
備考
- 深刻度は c1 / c3 が medium 相当、c2 が low
- 触るファイルが
packages/verify-cli/src/** に閉じるので、他クラスタと並行して進められる
Refs #238 #243
#238 (2026-08 レビューのまとめ Issue) をサブシステム別に分割したうちの verify-cli 分。
項目番号 (c1〜c3) は #238 のトリアージコメントの採番。現存は 2026-08-24 / HEAD
d61f126で再確認済み。c3 — 外部 analyzer が検証結果を書き換えられる (最優先)
packages/verify-cli/src/verify.ts:177AnalysisInput.verificationは検証結果オブジェクトそのものの参照で、result.valid/chainValid/metadataValid/poswSkippedはrunAnalysisの後 (:204-271) に読まれる。つまり
analyze()の中でinput.verification.valid = trueと書くだけで、改ざん proof が
valid: true(exit 0) になり、assurance.integrityもfailed以外になり、pasteEventsの捏造まで通る (トリアージで一時 vitest により確認済み)。脅威主体は
--analyzerを渡す採点者自身なので実害の深刻度は低いが、ADR-0009 / ADR-0023 の「advisory は valid / exit code に漏れない」不変条件が構造的に破れている。
なお
deriveAssurance自体は analysis をprocess層 (notableSignals/reviewPriority) にしか流さない(
packages/shared/src/assurance.ts:120-121)。破れの経路はこの共有参照だけ。方針 (順序で保証し、凍結でトリップワイヤを張る):
result/events/proofから導出される値をすべてrunAnalysisより前に確定させ、runAnalysis以降に読むのはanalysisだけにするrunAnalysisへはObject.freeze({ ...result })の浅いコピーを渡す。ES module は strict mode なので書き込みは TypeError で throw し、orchestrator が握り潰す (
Analyzerの契約どおり他の analyzer は止まらない)AnalysisInputの型 (shared) は変えない。深い凍結は event 数に比例するのでしない~15-20 行 + 悪意 analyzer を注入して
valid: falseのままであることを assert するテスト。c1 —
process.exit()によるパイプ時の stdout 切り捨てpackages/verify-cli/src/cli.ts:42,51,59,87,94,109,143,154,226,229(10 箇所)stdout がパイプ かつ exit 時点で未 flush の出力が ≈64 KiB を超えると末尾が欠ける (POSIX のみ。
TTY / ファイル redirect では起きない)。1 proof 分は最小 700 B なので単一 proof では起きないが、
クラス単位の ZIP (数十 proof) を
| tee/| lessすると最後の=== Summary: N/M proofs passed ===から欠ける (トリアージで再現済み)。
方針: 全 10 箇所を
process.exitCode = n; return;に置換 (usage / flag error の早期 exit も揃える)。main()は async でトップレベルから 1 回だけ呼ばれるのでreturnでそのまま自然終了する。~10 行。副作用:
--analyzerの外部モジュールが timer / socket を残すとプロセスが終わらなくなる。README / CLAUDE.md の
--analyzerの項に「ハンドルを残さないこと」を一言足す。c2 —
auditモードの help が実装と食い違うpackages/verify-cli/src/output.ts:519は「fast 相当」と読めるが、実装は full と同等
(
packages/verify/src/services/proofVerification.ts:72-79のposwModeForがaudit → 'full')。spec §6.1 の記述「部分的 PoSW 検証 (未実装、現状 full と同等)」に揃える。help 1 行。
対になる web 側の dead な
PoswMode = 'sampled'の掃除は verify 側の Issue で扱う(触るファイルが
packages/verify/src/**に閉じるため、パッケージ単位で分けたほうが衝突しない)。備考
packages/verify-cli/src/**に閉じるので、他クラスタと並行して進められるRefs #238 #243