Skip to content

doeff-adr: 外部 process 起動の待ちに構造的な上限を与える(semgrep / pytest の hang で ADR 検査の門が無期限に停まるのを根治) - #600

Open
proboscis wants to merge 1 commit into
mainfrom
feat/impl-doeff-adr-2-semgrep-pytest-adr-967704
Open

doeff-adr: 外部 process 起動の待ちに構造的な上限を与える(semgrep / pytest の hang で ADR 検査の門が無期限に停まるのを根治)#600
proboscis wants to merge 1 commit into
mainfrom
feat/impl-doeff-adr-2-semgrep-pytest-adr-967704

Conversation

@proboscis

Copy link
Copy Markdown
Owner

診断した根本原因(fix_class: root-cause)

doeff-adr は外部 process(semgrep / pytest)を起動するが、外部 process 接触を所有する層が無かった。待ちの上限は各起動地点が任意に書く keyword 引数でしかなく、package 全域の起動 2 箇所とも timeout= を渡していなかった(2026-08-18 実測 0/2)。

  • registry.py:434 _run_semgrep — semgrep 起動
  • cli.py:36 verify-wiringpytest --collect-only 起動

registry.py の既存コメントは「オフライン機で hang、飽和網で timeout」と hang の危険を自ら名指ししながら、その hang への上限を持っていなかった。この待ちは ADR 検査の門の中にあるため、子が返らなければ門も返らず、門を待つ CI / land 列 / agent 席がまとめて止まる。

除去した設計上の原因 = 「上限は起動地点ごとの任意項目」という状態そのもの。上限を持つ層を新設して全起動地点をそこへ通し、raw な起動経路を lint で閉じたため、片方だけ直す対症修正(同じ欠陥が他方と将来の 3 箇所目に残る)にはなっていない。

変更

変更 内容
新設 doeff_adr/process.py package が外部 process と接する唯一の層。run_external_process が常に上限を適用(既定 600 秒 / 環境変数 DOEFF_ADR_EXTERNAL_PROCESS_TIMEOUT_SECONDS で上書き)。既定値の根拠は module 定数の直上に 1 行コメント。読めない値・非正・非有限は黙って無期限に落とさず ValueError
registry.py 起動を helper 経由へ。打ち切りは既存作法どおり AssertionError へ変換し、command と秒数を名指す。「findings なし」「検査通過」と読ませない。
cli.py 起動を helper 経由へ。打ち切りは wiring verified ではなく exit 124(GNU timeout 慣行・pytest の 0..5 と非衝突)+ stderr に診断行。
.semgrep.yaml 新規 doeff-adr-external-process-must-be-bounded(ERROR)— **/doeff_adr/**process.py 以外の raw な subprocess / os.system 系起動を禁止。台帳 semgrep_rules 251→252。
tests packages/doeff-adr/tests/test_external_process_bounds.py 新設(6 本)+ test_wiring.py の CLI テストを新しい seam へ移設・強化。

Verification(受け入れ条件 1:1 対応)

受け入れ条件 実装/テスト
1. 2 箇所とも上限を持つ・ハードコードしない(module 定数 + env 上書き・既定値根拠のコメント) process.py:19-23(DEFAULT_EXTERNAL_PROCESS_TIMEOUT_SECONDS / EXTERNAL_PROCESS_TIMEOUT_ENV + 根拠コメント)、registry.py:_run_semgrepcli.py:mainpackages/doeff-adr/tests/test_external_process_bounds.py::test_external_process_timeout_seconds_is_overridable_by_env / ::test_default_bound_is_finite_and_reaches_subprocess / packages/doeff-adr/tests/test_wiring.py::test_verify_wiring_cli_runs_strict_collection(起動へ timeout が渡ることを seam で検査)
2. TimeoutExpired を握りつぶさず診断可能な例外へ(registry は AssertionError 作法) process.py:ExternalProcessTimeoutError(command / 秒数 / 部分 stderr)、registry.pyexcept ExternalProcessTimeoutErrorAssertionError::test_run_semgrep_timeout_is_loud_not_empty_findings(秒数・command が message にあり、「発火なし」に化けない)/ ::test_verify_wiring_timeout_exits_nonzero_with_diagnosis(wiring verified を出さず exit 124)
3. 新しい起動地点で書き忘れられない形(共通ヘルパへ集約) doeff_adr/process.py へ集約 + semgrep doeff-adr-external-process-must-be-bounded。実測: 同 rule を doeff_adr 配下の raw 起動 probe(import subprocess + subprocess.run(...))へ当てて 2 hit を確認し、process.py と修正後の 2 箇所には 0 hit(probe は検証後に削除)。
4. 上限が現に効くテスト(固まる子 process を短い上限で) ::test_run_external_process_stops_waiting_at_the_bound(sleep 30 を 0.5 秒上限で起動 → ExternalProcessTimeoutError、経過 < 20 秒)/ ::test_run_external_process_applies_the_bound_without_being_asked / ::test_run_semgrep_timeout_is_loud_not_empty_findings / ::test_verify_wiring_timeout_exits_nonzero_with_diagnosis

実行したコマンド

  • uv run pytest packages/doeff-adr -q27 passed(既存 3 本 test_defadr_macros.py / test_semgrep_runner_tree_only.py / test_wiring.py + 追加分)
  • uv run pytest tests/test_adr_wiring_gate.py tests/test_enforcement_ledger.py tests/test_semgrep_gate.py tests/test_semgrep_dependency_config.py -q7 passed(semgrep baseline ratchet 0 findings・enforcement 台帳 252 一致)
  • uv run semgrep --metrics=off --disable-version-check --config .semgrep.yaml doeff/ packages/ --error0 findings(201 rules / 903 files)
  • uv run ruff check packages/doeff-adr → clean、uv run pyright packages/doeff-adr/src → 0 errors

Verification deviations

なし(受け入れ条件の代替・弱化はしていない)。

備考

  • test_wiring.py::test_verify_wiring_cli_runs_strict_collectiondoeff_adr.cli.subprocess を patch していたため、起動が helper へ移った時点で seam が消える。同テストを doeff_adr.process.subprocess に付け替え、command の形に加えて timeout が起動へ渡ること も検査するよう強化した(検査の弱化ではなく追加)。
  • ruff format が同じファイル内の無関係な既存整形ドリフトを直そうとしたぶんは差分に含めていない(この変更の主題外)。

出所

sandbox:responsibility:host-load-steward 掲出 gen 55 に対する operator 裁定 withdraw-and-route-via-repair-ticket-v189(delivered 2026-08-18T04:49:20Z / requestId dav-h558ce886bcfb9d5b)の実施。ACP issue doeff-adr-2-semgrep-pytest-adr-967704

🤖 Generated with Claude Code

doeff-adr は semgrep と pytest を子 process として起動するが、外部 process 接触を
所有する層が無く、待ちの上限は起動地点ごとの任意 keyword だった。結果、package
全域の起動 2 箇所(registry.py の semgrep / cli.py の pytest --collect-only)ともに
timeout なし = 0/2 で、子が返らなければ門も返らず、門を待つ CI / land 列 / agent 席
がまとめて止まる。

- 新設 doeff_adr/process.py = package が外部 process と接する唯一の層。
  run_external_process が常に上限を適用し(既定 600 秒・環境変数
  DOEFF_ADR_EXTERNAL_PROCESS_TIMEOUT_SECONDS で上書き、読めない値は黙って
  無期限に落とさず ValueError)、subprocess.TimeoutExpired を command と秒数を
  名指す ExternalProcessTimeoutError へ変換する。
- registry.py: 打ち切りを既存作法どおり AssertionError へ変換 — 「findings なし」
  「検査通過」と読ませない(走らなかった scan を発火なしと黙読する失敗と同型)。
- cli.py: 打ち切りは「wiring verified」ではなく exit 124(pytest の 0..5 と非衝突)
  + stderr に診断。
- .semgrep.yaml: doeff-adr-external-process-must-be-bounded — doeff_adr 配下で
  process.py 以外の raw な subprocess / os.system 起動を ERROR で禁止。新しい起動
  地点で上限を書き忘れる経路自体を閉じる。台帳 semgrep_rules 251→252。
- tests/test_external_process_bounds.py: 固まる子(sleep 30)を短い上限で起動し、
  無期限に待たず所定の例外/非ゼロ終了になることを helper・semgrep runner・
  verify-wiring の 3 経路で検査。test_wiring.py の CLI テストは seam を
  doeff_adr.process 側へ移し、timeout が起動へ渡ることも併せて検査する。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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