diff --git a/docs/issues/closed/I225.md b/docs/issues/closed/I225.md new file mode 100644 index 00000000..223abcd6 --- /dev/null +++ b/docs/issues/closed/I225.md @@ -0,0 +1,83 @@ +# I225: I223 のレビューの残り: GitHub の鍵が画面に出る・漏れたときの手順と、テスト用のセッションの抜け道を塞ぐ + +## 関連資料 +- GitHub Issue: #415 +- Draft PR: #416 +- 起点: I223(GitHub #412・PR #414)の retro からの起票(2026-11-05) +- 台帳の記録: `docs/reviews/I223_code_review_20261105_1440.md`(F7・F9・F13・F14・F15) +- 経緯: `docs/tests/open/I223_manual_test.md` の M-02(I223 のマージ後は `docs/tests/closed/`)・診断メモ `docs/reviews/I223_fix_diagnosis_20261105_1450.md` +- 優先度: **次のハーネスの作業で最優先**(`SECURITY=yes` の指摘を含むため) + +## 設計確認メモ(/grill-me) +- Q: 鍵を画面に出す `--json` なしの `gh auth status` を、どこで止めるか(A: `gh` の入口で断る+手順書の静的な検査/B: 静的な検査だけ/C: 見張りで確認) → A: 「ループエンジニアリング・ハーネスエンジニアリングのベストプラクティスと、開発をサクサク進める観点で問題がないやり方で」(ユーザー・2026-11-06)。Claude が次のとおり確定した: + - 入口 `scripts/claude/gh-app/gh` で、引数が `auth status` で `--json`・`-t`/`--show-token` を含まないときは、断らずに鍵の欄を出さない形(`auth status --json hosts --jq …` で名義・鍵の出どころ・状態だけ)に置き換えて表示し、置き換えたことを 1 行で標準エラーに出す。理由: 断ると打ち直しの往復が要る(ループが止まる)。置き換えなら Claude も人も 1 回で要る情報が得られ、鍵は出ない。`argv` で判定するので、シェルの文字列を読む判定(見張りの作り込み)にならない + - `-t`/`--show-token` の明示はそのまま通す(わざと鍵を見る操作。Claude からは見張りの S23・S35 が確認にする。見張り `pretooluse_guard.py` は変えない) + - 手順書・スキル・エージェント・スクリプト・open のテスト文書に `--json` なしの `gh auth status` が書かれたら落ちる静的な検査を `scripts/claude/tests/test_i223_gh_app.sh` に足す(closed の記録は対象外)。※計画で対象を手順書・スキル・エージェント・ハーネスのスクリプト・テンプレートに絞った(open のテスト文書を含む個々のイシューの記録は外す。理由は plan_I225.md §9) + - 予防策は新しい決まりを増やさず、上の 2 つの決定論のチェックで代える(計画レビュー役・計画の書き方は変えない) +- Q(Claude が決めた技術的な点・ユーザーに聞いていない): F13 は漏れた鍵そのものを `GH_TOKEN` に入れて `DELETE /installation/token` し、`token`・`token-readonly` の両方の控えを消す手順にする。F14 は点検コマンドを表の外のコードブロックに出し、`GH_APP_READONLY` を外し `.tokenSource` を足す。F7 は `claude -p` を起動する `scripts/claude/` のスクリプトに `--strict-mcp-config` を足す(MCP が要る判定は `--mcp-config` で必要なものだけ・既存の Q18 は維持)。2026-11-06 時点で元のリポジトリの `.mcp.json` に github は無い。F9 は `tui-capture.sh` の tmux 起動に `-e PATH=…` を足し、入口の `gh` を必ず通す + +## 背景/目的 + +**背景**: I223 で、Claude が GitHub を操作する名義を GitHub App にし、テスト用のセッション(`claude -p`・tmux の中の `claude`)には読み取りだけの鍵(`GH_APP_READONLY=1`)を渡すようにした。I223 の test-gate で、手順書どおり `gh auth status` を流したところ、App の新しい形式の長い鍵(書き込みのできる 1 時間の鍵)の大部分が画面と会話の記録に出た(`gh` は鍵の最後の部分しか伏せない)。その鍵はすぐに失効させ、手順書の確かめ方は `gh auth status --json …`(要る欄だけを取り出す)に直した。ただし、レビューで次の残りが指摘された(止める条件には当たらず、I223 では直していない)。 +**目的**: 鍵が画面に出る書き方を決定論で締め出し、出てしまったときの手順を正しくし、テスト用のセッションが読み取りだけの鍵を迂回する抜け道を塞ぐ。 + +### 現状の問題(証拠・再現手順) +- **F15(SECURITY=yes)**: `--json` なしの `gh auth status` が鍵の大部分を出す問題を、手順書の決まりだけで防いでいる。見張り(`scripts/claude/hooks/pretooluse_guard.py`)が確認にしているのは `gh auth status -t`・`gh auth token` だけ(`scripts/claude/tests/test_pretooluse_container_guard.sh` の S23・S35)。手順書・スキルに書き戻されると再発しうる +- **F13(Medium・再現あり・SECURITY=yes)**: `docs/runbooks/github-app-setup.md`「5.」の「鍵が出たときの失効手順」`gh api -X DELETE installation/token` は、そのとき入口(`scripts/claude/gh-app/gh`)が渡す鍵を失効させるので、漏れた鍵と同じとは限らない(テスト用のセッションで漏れた読み取りだけの鍵・期限間際に取り直された鍵)。消す控えも `~/.cache/github-app/token` だけで `token-readonly` が抜けている。再現: 読み取りだけの鍵が画面に出た後、通常のシェルで手順を流すと、書き込みの鍵が失効し、漏れた鍵は有効なまま残る +- **F14(Medium・再現あり)**: `docs/runbooks/harness-audit.md` の「手元に残る古い鍵」の行の点検コマンドが、表の中の `\|` のせいでそのままコピーすると jq の構文エラーになる。`GH_APP_READONLY` があると `GH_APP_DISABLE=1` が無視され、保存されたログインを見られない。`.tokenSource` が抜けていて、環境変数の鍵と保存されたログインを区別できない +- **F7(SECURITY=yes)**: `scripts/claude/` の `claude -p` の起動に `--strict-mcp-config` が無い。ワークツリーのセッションは元のリポジトリの `.mcp.json`(develop のまま GitHub の MCP がある)を読む(I223 の M-06 ② で観測)。手元に PAT が残っていると、MCP 経由の書き込みが読み取りだけの鍵を迂回しうる +- **F9(SECURITY=yes)**: `scripts/claude/tui-capture.sh` で、tmux サーバーが先に立っていて、その `PATH` に `~/.local/bin` が無いと、中の `claude` の `gh` が入口を通らない(入口を通らなければ App の鍵自体は渡らないが、本物の `gh` に保存されたログインが有効なら、そちらで書き込める) + +### 根本原因 +鍵・秘密を扱う手順の確かめ方を決めるときに、そのコマンドが秘密を画面に出すかを確かめる段取りが無く、見張りも一部の書き方しか確認にしていない。テスト用のセッションの守り(読み取りだけの鍵)は `gh` の入口を通ることを前提にしていて、入口を通らない経路(MCP・本物の `gh`・別の `PATH`)を数えていない。 + +### 解決方針 +- F15(確定): 入口 `scripts/claude/gh-app/gh` で、`--json`・`-t`/`--show-token` なしの `gh auth status` を、鍵の欄を出さない `--json` の表示に置き換える(断らない・置き換えたことを標準エラーに 1 行)。あわせて、手順書・スキル・エージェント・ハーネスのスクリプト・テンプレートに `--json` なし(または `-t`/`--show-token` 付き)の `gh auth status` が書かれたら落ちる静的な検査を `scripts/claude/tests/test_i223_gh_app.sh` に足す。見張り `pretooluse_guard.py` は変えない +- F13(確定): 失効の手順を、漏れた鍵そのものを `GH_TOKEN` に入れて `DELETE /installation/token` する形にし、`token`・`token-readonly` の両方の控えを消す手順にする +- F14(確定): 点検コマンドを表の外のコードブロックに出し、`GH_APP_READONLY` を外して流す形・`.tokenSource` を足した形にする +- F7(確定): `scripts/claude/` の `claude -p` の起動に `--strict-mcp-config`(MCP が要る判定では `--mcp-config` で必要なものだけ)を足す。既存のテスト(`test_check_permission_prompt.sh` Q18 など)は維持する +- F9(確定): `tui-capture.sh` の tmux 起動に `-e PATH=…` を足し、入口の `gh` を必ず通す +- 予防策(確定): 新しい決まりは足さず、F15 の入口の置き換えと静的な検査(決定論のチェック)で代える + +## 実装対象(既知のもの) +| ファイル | 変更内容 | +|---------|---------| +| `scripts/claude/gh-app/gh` | `--json` なしの `gh auth status` を鍵の欄を出さない表示に置き換える(F15) | +| `scripts/claude/tests/test_i223_gh_app.sh` | 入口の置き換えのテストと、手順書などに `--json` なしの `gh auth status` が無いことの静的な検査(F15) | +| `docs/runbooks/github-app-setup.md` | 失効の手順(F13) | +| `scripts/claude/gh-app/revoke.sh`(新規) | 控えの 2 つの鍵を失効させて確かめ、控えを消す。控えに無い漏れた鍵は `--stdin` で値を受けて失効させる(F13・計画で追加) | +| `docs/runbooks/harness-audit.md` | 古い鍵の点検コマンド(F14) | +| `scripts/claude/` の `claude -p` の起動(`check-permission-prompt.sh`・`review-process-lib.sh`・`code-review.sh`・`plan-issue-review.sh`・`issue-review.sh`・`fix-review-lib.sh`・`test-session.sh`) | `--strict-mcp-config` を足すか(F7) | +| `scripts/claude/tui-capture.sh` | tmux の起動に `PATH` を渡し、入口の `gh` を必ず通す(F9) | + +## スコープ +### 含む +- 上の F7・F9・F13・F14・F15 と予防策 +### 含まない +- I223 の軽い指摘の一覧(`docs/reviews/backlog/I223.md` の F2〜F6・F8・F10〜F12・B1〜B4) +- GitHub への書き込みの回数を絞る仕組み(I224 #413) + +## 関連する品質基準 +- 該当なし(ハーネスの運用の仕組み) + +## 受け入れ条件(Acceptance Criteria) +- [ ] `--json`・`-t`/`--show-token` なしの `gh auth status` を入口の `gh` で流すと、鍵の欄が出ず、名義・鍵の出どころ・状態だけが表示される(F15) +- [ ] 手順書・スキル・エージェント・ハーネスのスクリプト・テンプレートに `--json` なし(または `-t`/`--show-token` 付き)の `gh auth status` を書くと、静的な検査(CI)で落ちる(F15。個々のイシューの記録は対象外・plan_I225.md §9) +- [ ] 手順書の失効の手順が、漏れた鍵そのものを失効させ、2 つの控え(`token`・`token-readonly`)を消す形になっている(F13) +- [ ] 総点検の一覧の古い鍵の点検コマンドを、そのままコピーして流せる(F14) +- [ ] テスト用のセッション(`scripts/claude/` の `claude -p`・`tui-capture.sh`)が、MCP・`PATH` の違いによって読み取りだけの鍵を迂回しない(F7・F9) +- [ ] 既存のハーネステスト・CI がすべて pass する + +## 影響範囲(想定) +- Backend: なし +- Frontend: なし +- DB: なし +- Config/Infra: `scripts/claude/`(`gh-app/gh`・`tui-capture.sh`・`claude -p` の起動・`tests/test_i223_gh_app.sh`)・`docs/runbooks/`(見張り `pretooluse_guard.py`・`.claude/agents/` は変えない) + +## 制約・引き継ぎ情報 +- 鍵の仕組み(`scripts/claude/gh-app/`)と、テスト用のセッションの読み取りだけの鍵(`GH_APP_READONLY=1`・静的なテスト `scripts/claude/tests/test_i223_gh_app.sh` の G6)は I223 で入った +- ローカルの安全装置は「ふつうの書き方の事故」を止めるもので、わざと回避する書き方は対象外。最後の守りは GitHub の権限(App は develop・main に push・マージできない) +- 見張り(`pretooluse_guard.py`)は変えない(grill-me で確定)。入口 `gh-app/gh` の置き換えは鍵を出さない方向の変更で、ユーザーの一任(2026-11-06)で決めた + +## Danger Ops(有/無) +- 無(データ削除・破壊的マイグレーション・強制 push は無い)。ただし手動テスト M-02 で `revoke.sh` を本物の GitHub に流し、控えの 2 つの App の鍵(1 時間の鍵)を失効させる。次の `gh` で取り直すので作業は止まらないが、その時に App の鍵を環境変数で持っている別のセッション・プロセスは GitHub の操作が 401 になる(ほかのセッションが動いていない時に流す。plan_I225.md 計画変更記録 1) diff --git a/docs/plans/closed/plan_I225.md b/docs/plans/closed/plan_I225.md new file mode 100644 index 00000000..1943ecc4 --- /dev/null +++ b/docs/plans/closed/plan_I225.md @@ -0,0 +1,206 @@ +# 計画書: I225 GitHub の鍵が画面に出る・漏れたときの手順と、テスト用のセッションの抜け道を塞ぐ + +## 基本情報 +- **計画書ID**: plan_I225 +- **関連イシュー**: #415 +- **Draft PR**: #416 +- **作成根拠資料**: docs/issues/open/I225.md(I223 の retro からの起票。台帳 `docs/reviews/closed/I223_code_review_20261105_1440.md` の F7・F9・F13・F14・F15) +- **実装後評価**: docs/reviews/open/I225_review.md +- **作成日**: 2026-11-06 + +## 1. 背景/目的 +I223 で Claude の GitHub の名義を GitHub App にし、テスト用のセッションには読み取りだけの鍵(`GH_APP_READONLY=1`)を渡すようにした。I223 の test-gate で、手順書どおり `--json` なしで `auth status` を流したら、書き込みのできる App の鍵の大部分が画面と会話の記録に出た(`gh` は鍵の最後の部分しか伏せない)。I223 では手順書の確かめ方だけを直し、次の残りを I225 に回した。 + +- **目的**: 鍵が画面に出る書き方を決定論で締め出す。鍵が出たときの手順を、漏れた鍵そのものを失効させる形に正す。テスト用のセッションが、MCP・`PATH` の違いで読み取りだけの鍵を迂回する抜け道を塞ぐ。 + +## 2. 受け入れ条件 +イシュー `docs/issues/open/I225.md` の受け入れ条件と同じ(§9 の計画で絞った点を反映済み)。 +1. `--json`・`-t`/`--show-token` なしの `gh auth status` を入口の `gh` で流すと、鍵の欄が出ず、名義・鍵の出どころ・状態だけが表示される(F15) +2. 手順書・スキル・エージェント・ハーネスのスクリプト・テンプレートに、`--json` なし(または `-t`/`--show-token` 付き)の `gh auth status` を書くと、静的な検査(CI)で落ちる(F15) +3. 手順書の失効の手順が、漏れた鍵そのものを失効させ、2 つの控え(`token`・`token-readonly`)を消す形になっている(F13) +4. 総点検の一覧の古い鍵の点検コマンドを、そのままコピーして流せる(F14) +5. テスト用のセッション(`scripts/claude/` の `claude -p`・`tui-capture.sh`)が、MCP・`PATH` の違いによって読み取りだけの鍵を迂回しない(F7・F9) +6. 既存のハーネステスト・CI がすべて pass する + +## 3. 影響範囲 +- Backend: なし +- Frontend: なし +- DB: なし +- Config/Infra: `scripts/claude/gh-app/`(入口 `gh` の変更・失効のスクリプト `revoke.sh` の追加)・`scripts/claude/` の Claude の起動の行(`--strict-mcp-config`)・`scripts/claude/tui-capture.sh`・`scripts/claude/tests/`・`docs/runbooks/github-app-setup.md`・`docs/runbooks/harness-audit.md` +- 見張り `scripts/claude/hooks/pretooluse_guard.py`・`.claude/settings.json`・`.github/` は変えない(止めて聞く条件 2 の安全装置に当たらない) +- 端末の外の仕組み: 入口 `gh` はリポジトリの版を `install.sh` で `~/.local/bin/gh` に入れる。実装の後に `install.sh` を流し直して、この端末に入れる(M-01) + +## 調査結果 + +### 原因の概要 +`gh` の `auth status` は鍵の伏せ字が昔の短い鍵向けで、App の長い鍵では大部分を出す。今はそれを「手順書に書かない」という決まりだけで防いでいて、打ってしまえば出る。漏れたときの手順も、そのとき入口が渡す鍵を失効させるだけで、漏れた鍵と同じとは限らない。テスト用のセッションの守りは「`gh` の入口を通る」ことが前提で、入口を通らない経路(MCP・tmux サーバーの `PATH`)が残っている。 + +### 詳細(箇所) +- **F15**: `scripts/claude/gh-app/gh` は引数を見ずに本物の `gh` へ渡す(13 行目 `exec "$REAL_GH" "$@"`)。見張りが確認にするのは `auth token`・`--show-token`・`auth status -t` だけ(`pretooluse_guard.py:2259-2260`)。`--json` なしの `auth status` を止める決定論のチェックは無い +- **F13**: `docs/runbooks/github-app-setup.md:47` の失効の手順 `gh api -X DELETE installation/token` は入口を通るので、そのとき入口が渡す鍵(控え `token`、`GH_APP_READONLY=1` なら `token-readonly`、期限間際なら取り直した新しい鍵)を失効させる。消す控えも `token` だけ。控えの仕組みは `gh-app-token:13-19`(読み取りだけは `token-readonly`)・`:24`(残り 10 分を切ったら取り直す) +- **F14**: `docs/runbooks/harness-audit.md:29` は表の中に jq の式を書いていて、`|` を `\|` で逃がしているため、コピーすると jq の構文エラーになる。`GH_APP_DISABLE=1` は `GH_APP_READONLY` があると入口で無視される(`gh:6`)。`.tokenSource` が無く、環境変数の鍵と保存されたログインを区別できない +- **F7**: `--strict-mcp-config` があるのは `check-permission-prompt.sh:70`(bash の判定だけ)と `adversarial-review.sh:181`(実験役だけ)。`code-review.sh:154`・`review-process-lib.sh:29`(レビュー役・敵対的レビュー役)・`issue-review.sh:40`・`plan-issue-review.sh:131`・`fix-review-lib.sh:81`・`test-session.sh:7`・`tui-capture.sh:45` には無い。`check-permission-prompt.sh` の mcp の判定は `--mcp-config .mcp.json` だけで、ほかの MCP の設定も読む +- **F9**: `tui-capture.sh:45` は `tmux new-session` に `-e GH_APP_READONLY=1` だけを渡す。tmux サーバーが先に立っていると、中の `claude` の `PATH` はサーバーの環境になり、`~/.local/bin` が無ければ `gh` は本物の `/usr/bin/gh` になる(入口を通らない) + +### 環境・前提の確認(2026-11-06) +- `gh` 2.88.1: `auth status` に `-a`・`-h`・`--json hosts`・`--jq`・`--template`・`-t` がある。`--json hosts` の各要素の欄は `active`・`gitProtocol`・`host`・`login`・`state`・`tokenSource`(失敗時は `error`)で、鍵の欄は `--show-token` を付けたときだけ入る +- スパイク(成立): `gh auth status -a -h github.com --json hosts --jq '.hosts[][] | .host + " " + .login + " " + .tokenSource + " " + .state + (if .active then " (active)" else "" end)'` → `github.com osushi0404-claude[bot] GH_TOKEN success (active)`(鍵は出ない。`-a`・`-h` は `--json` と併用できる) +- `--json` を付けると、`auth status` は認証の問題があっても終了コード 0(`gh auth status --help`)。置き換えでは終了コードを自分で決める(4-1) +- スパイク(成立・2026-11-06・計画レビューの Warning を受けて): `bash scripts/claude/test-session.sh "Reply with only OK" --model haiku --strict-mcp-config --mcp-config .mcp.json --output-format stream-json --verbose …` の init の行の `mcp_servers` が `[('playwright','connected')]`。`--mcp-config` なしで `--strict-mcp-config` だけなら `[]`。どちらも結果は success。`--strict-mcp-config` と `--mcp-config` の併用で、渡した MCP だけを読む(4-5 の前提)。tmux の中の `claude`(対話)も同じオプションの読み方で、実物は M-03 で確かめる +- 元のリポジトリの `.mcp.json` に `github` は無い(`grep -c github` が 0)。F7 の今の実害は小さいが、手元の設定に GitHub の MCP が戻ったときの抜け道として塞ぐ +- tmux 3.4(`-e` は 3.2 以上)・`python3`・`/usr/bin/gh` あり +- 既存テストのベースライン(実装前・develop の 2026-11-06 時点): `test_i223_gh_app.sh` 18/18・`test_tui_capture.sh` 37/37・`test_check_permission_prompt.sh` 26/26・`test_i197_adversarial_process.sh` 113/113・`test_fix_review.sh` 66/66・`test_i193_official_flow.sh` 174/174。CI は `scripts/claude/tests/run-all.sh`(`.github/workflows/ci.yml:169`) + +## 修正アプローチ +鍵を出す書き方は、打った時点(入口)と書いた時点(CI の静的な検査)の 2 か所で決定論に止める。入口では「断る」のではなく「鍵を出さない表示に置き換える」ので、打った人も Claude も打ち直しの往復なしに必要な情報を得られる。漏れたときの手順は、控えにある 2 つの鍵を直接失効させ、失効を確かめてから控えを消すスクリプトにする(手順書の散文で手を動かすと取り違えるので、1 コマンドにしてテストで守る)。テスト用のセッションは、Claude を起こす行すべてに `--strict-mcp-config` を付け、tmux の中の `claude` には入口のある `PATH` を渡す。 + +## 4-5. 変更点一覧と実装手順 + +### 4-1. 入口 `gh` で `auth status` を置き換える(F15・受け入れ条件 1) +**修正方針**: 引数が `auth status` で、`--json`・`--jq`・`--template`・`-t`・`--show-token`(`-at` のように短いオプションをまとめた中の `t` を含む)のどれも無いときだけ、本物の `gh` を `auth status <元の引数> --json hosts` で呼び、名義の行だけを出す。`-t`/`--show-token` はわざと鍵を見る操作なので置き換えない(Claude からは見張りの S23・S35 が確認にする)。 + +- 対象: `scripts/claude/gh-app/gh`(鍵を受け取った後、`exec` の前に足す) +- 見るのは引数の 1・2 番目が `auth`・`status` のときだけ。`gh -R x auth status` のようにグローバルオプションを前に置く書き方は置き換えない(ふつうの書き方の事故が対象・手順書への書き戻しは G10 が止める。わざと避ける書き方は対象外) +- 表示: 1 アカウント 1 行 ` `、使用中のものには ` (active)`。`error` の欄は出さない(文言に鍵の一部が入りうるため) +- 標準エラーに 1 行: `gh: 鍵を画面に出さないよう、auth status を --json の形で表示しました(I225)` +- 終了コード: `state` がすべて `success` なら 0、1 つでも違えば 1(`--json` なしの元の動きに合わせる。`gh auth status` の終了コードで認証を確かめる書き方を壊さない)。本物の `gh` が失敗したらその終了コード +- JSON の整形は `python3 -I`(`gh-app-token` と同じ。jq に頼らない) +- コード例: + ```bash + if [ "${1:-}" = auth ] && [ "${2:-}" = status ]; then + plain=1 + for a in "${@:3}"; do + case "$a" in --json*|--jq*|--template*|--show-token|-t|-[!-]*t*) plain=0 ;; esac + done + if [ "$plain" = 1 ]; then + echo "gh: 鍵を画面に出さないよう、auth status を --json の形で表示しました(I225)" >&2 + json="$("$REAL_GH" auth status "${@:3}" --json hosts)" || exit $? + printf '%s' "$json" | python3 -I -c '' + exit $? + fi + fi + ``` +- この端末への反映: `bash scripts/claude/gh-app/install.sh`(M-01) +- → TC-02 の G8 参照 + +### 4-2. 失効のスクリプトを足し、手順書を直す(F13・受け入れ条件 3) +**修正方針**: `scripts/claude/gh-app/revoke.sh` を新しく作る。控えのフォルダ(`GH_APP_CACHE_DIR`、既定 `~/.cache/github-app`)の `token`・`token-readonly` の 2 つについて、控えの鍵を `GH_TOKEN` にして本物の `gh`(`GH_APP_REAL_GH`、既定 `/usr/bin/gh`。入口を通さない=入口が別の鍵に差し替えない)で `api -X DELETE installation/token` を呼ぶ。続けて同じ鍵で `api installation/repositories` を呼び、失敗する(=鍵が無効)ことを確かめてから控えを消す。 + +- **漏れた鍵そのものを失効させる口(`--stdin`)**: `bash scripts/claude/gh-app/revoke.sh --stdin` は、漏れた鍵の値を標準入力から 1 行読み(端末なら `read -rs` で画面に出さない)、その鍵で同じく DELETE → 無効の確認を行う。控えは触らない。控えが取り直された後に見つかった鍵・テスト用のセッションで出た鍵など、控えに無い鍵はこちらで失効させる(F13 の再現ケース)。値は引数に取らない(`ps`・シェルの履歴に残さない) +- 鍵の値は引数・画面に出さない(`GH_TOKEN` の環境変数で渡す・出力は控えの名前と結果だけ。`--stdin` は「渡された鍵」とだけ出す) +- 結果: 各控えについて `token: 失効を確認して控えを消しました`/`控えがありません`。DELETE の後も読めた(無効にならなかった)控えがあれば、控えは消さずに exit 1 で「App の設定画面で確かめる」よう案内する(`github-app-setup.md`「8.」) +- 終了コード: 0=すべて無効を確認(控えが無いものを含む)・1=無効にならなかった鍵がある・2=使い方の誤り(`--stdin` 以外の引数・`--stdin` で空の入力) +- 手順書 `docs/runbooks/github-app-setup.md`「5.」: 鍵が画面・記録に出たら、まず `bash scripts/claude/gh-app/revoke.sh`(控えの 2 つ)を流し、出た鍵が控えの鍵と違う(取り直しの後に気づいた・別のセッションで出た)ときは `bash scripts/claude/gh-app/revoke.sh --stdin` に出た鍵を貼って失効させる。秘密の鍵ファイル(`app.pem`)が漏れたときは「8.」の作り直し +- 入口と同じく `install.sh` では入れない(リポジトリから流す。鍵が出たときだけ使うため) +- → TC-02 の G9 参照 + +### 4-3. 鍵を出す書き方の静的な検査(F15・受け入れ条件 2) +**修正方針**: `scripts/claude/tests/test_i223_gh_app.sh` に G10 を足す。対象は `CLAUDE.md`・`docs/runbooks/`・`.claude/skills/`・`.claude/agents/`・`scripts/claude/` の下(`scripts/claude/tests/` を除く)・`docs/*/templates/`。各ファイルで `gh auth status` が出るたびに、そのあとから行末か次のバッククォートまでの語を見て、`--json` で始まる語が無い、または `--show-token`・`-t`(短いオプションをまとめた中の `t` を含む)がある書き方を不合格にする。 + +- 個々のイシューの記録(`docs/issues/`・`docs/plans/`・`docs/tests/open|closed/`・`docs/reviews/`)は対象外(§9) +- 今の手順書の散文 `github-app-setup.md:47`(`--json` を付けない `gh auth status` は使わない)は検査に当たるので、4-2 で書き直す(`auth status` は `--json` を付けて使う、の形) +- → TC-02 の G10 参照 + +### 4-4. 総点検の点検コマンド(F14・受け入れ条件 4) +**修正方針**: `docs/runbooks/harness-audit.md` の表の「手元に残る古い鍵」の行は「下のコードブロック」を指すだけにし、表の後にコードブロックで次を置く。 +```bash +grep -c -E '^[[:space:]]*export[[:space:]]+(GITHUB_PERSONAL_ACCESS_TOKEN|GH_TOKEN|GITHUB_TOKEN)=' ~/.bashrc ~/.profile +GH_APP_READONLY= GH_APP_DISABLE=1 gh auth status --json hosts --jq '.hosts["github.com"][] | .login + " " + .tokenSource + " " + .state' +``` +- 1 行目は値を出さずに数える(今の行の「`grep -c` で数える」を具体化)。`~/.profile` が無いときのエラーは数の判断に影響しない +- 2 行目は `GH_APP_READONLY` を空にして(入口は空を「指定なし」と読む・`gh:6` の `-n`)`GH_APP_DISABLE` を効かせ、入口から鍵を渡さずに本物の `gh` の保存されたログイン・環境変数の鍵を、出どころ(`.tokenSource`)つきで見る。`env -u GH_APP_READONLY …` の形は、ワークツリーのセッションの見張りが「`env` が何を動かすか読めない」として止める(2026-11-06 の計画の段階で発生)ので使わない +- 計画の段階で 2 行とも流して確かめた(2026-11-06): 1 行目は `.profile:0`・`.bashrc:1`、2 行目は `osushi0404-bot /home/tacic/.config/gh/hosts.yml error`(exit 0・jq のエラーなし・鍵の値は出ない) +- → TC-03 参照(コピーして流せることを実行で確かめる) + +### 4-5. テスト用のセッションの MCP の抜け道(F7・受け入れ条件 5) +**修正方針**: `scripts/claude/` の Claude を起こす行すべてに `--strict-mcp-config` を書く(MCP は `--mcp-config` で渡したものだけになる)。 +- `code-review.sh:154`・`review-process-lib.sh:29`(`run_claude_logged`。敵対的レビュー・コードレビューの検証役が通る)・`issue-review.sh:40`・`plan-issue-review.sh:131`・`fix-review-lib.sh:81`・`test-session.sh:7`・`tui-capture.sh:45`(`claude --permission-mode manual --strict-mcp-config`) +- `check-permission-prompt.sh`: 起動の行に `--strict-mcp-config` を直接書き、`MCP_ARGS` は mcp の判定のときの `--mcp-config .mcp.json` だけにする(mcp の判定もワークツリーの `.mcp.json` だけを読む) +- `adversarial-review.sh:181` の実験役の `--strict-mcp-config` は、`run_claude_logged` が付けるので消す(同じオプションを 2 回渡さない) +- `test-session.sh` の呼び出し元が `--mcp-config` を渡せば、その MCP だけは使える +- → TC-02 の G7 参照 + +### 4-6. tmux の中の `claude` に入口のある `PATH` を渡す(F9・受け入れ条件 5) +**修正方針**: `tui-capture.sh:45` の `tmux new-session` に `-e "PATH=$HOME/.local/bin:$PATH"` を足す(`-e GH_APP_READONLY=1` と同じく、tmux サーバーの環境に関係なく新しいセッションに入る)。入口は `install.sh` が `~/.local/bin/gh` に入れる。 +- 起動するコマンドは固定のまま(T8) +- → TC-01(`test_tui_capture.sh` の T1・FG-T1b)参照 + +### 4-7. 台帳の残りの同梱(§9)と手順書の文言 +- `docs/runbooks/github-app-setup.md`「1.」: 「Claude を起こす行はすべて G6 が止める」を、G6・G7 が見る範囲(`scripts/claude/` 直下の `.sh` の `claude -p` と、tmux で `claude` を起こす行)に合わせる(一覧 I223 F12)。`--strict-mcp-config` と `PATH` の決まりを 1 文足す +- `test_i223_gh_app.sh` 冒頭のコメントを G1〜G10 にする(一覧 I223 F11) +- `docs/reviews/backlog/I223.md` の F11・F12、`docs/reviews/backlog/I195.md` の F5 を消す + +### 依存関係 +- 4-1 → 4-3(入口の変更と同じテストファイル)。4-2 の手順書の書き直しは 4-3 の検査の前提 +- 4-5・4-6 は 4-1〜4-3 と独立(並行可)。4-5 の G7 と 4-6 の T1 は同じ `tui-capture.sh:45` を触るので 1 回の編集にまとめる +- 実装の最後に `install.sh` を流し直す(M-01 の前提) + +## 6. テスト計画 +- 自動(決定論): `docs/tests/open/I225_auto_test.md` + - TC-01: 起動の行を触るファイルを見ている既存のテスト(`test_tui_capture.sh`・`test_check_permission_prompt.sh`・`test_i197_adversarial_process.sh`・`test_fix_review.sh`・`test_i193_official_flow.sh`)が合格する + - TC-02: `test_i223_gh_app.sh` の G1〜G10(新しく G7〜G10)。各 G に FG + - TC-03: 4-4 の点検コマンドをそのまま流して exit 0・jq の構文エラーが無い +- 手動: `docs/tests/open/I225_manual_test.md`(すべて Claude。入れ直した入口での `auth status`・失効のスクリプトの本物の GitHub での確かめ・tmux の中の `claude` の MCP と `gh`) +- テストのレベル: 入口・失効・検査は偽物の `gh` を使うユニット相当(決定論)。本物の GitHub・tmux・claude での確かめは手動テスト(Claude) +- 再発防止: G8(打っても出ない)・G10(書いても CI で落ちる)・G9(失効の取り違え)・G7(MCP の付け忘れ) + +## 7. ロールバック +- PR を revert する。端末の入口は `install.sh` が `~/.cache/github-app/backup/` に前の版を写すので、revert 後に `install.sh` を流し直せば戻る +- データ・GitHub の設定は変えない + +## 8. Risk & 回避策 +| リスク | 回避策 | +|---|---| +| 入口の置き換えが、`auth status` の終了コードで認証を確かめる書き方を壊す | `state` がすべて `success` のときだけ 0 にする(G8 で 0 と 1 の両方を確かめる) | +| `-t` の判定が `-h` の値(ホスト名)に当たる | `-` で始まる語だけを見る。ホスト名は `-` で始まらない | +| 失効のスクリプトが、まだ使っている書き込みの鍵を失効させる | 失効の後は控えを消すので、次の `gh` で取り直す(作業は止まらない)。鍵が出たときだけ流す | +| `--strict-mcp-config` で、MCP が要る判定(Playwright など)が動かなくなる | MCP が要る判定は `check-permission-prompt.sh mcp` で `--mcp-config .mcp.json` を渡す(今どおり)。レビュー役は MCP を使わない(`--tools "Read,Grep,Glob"`)。M-03 で確かめる | +| 静的な検査が散文の言及にも当たり、手順書が書きにくくなる | 対象を決まりの置き場(手順書・スキル・エージェント・スクリプト・テンプレート)に絞る。散文は「`auth status` は `--json` を付けて使う」の形で書ける | + +## 9. 確定事項と自律判断 +### イシューに明記されていること(grill-me で確定・2026-11-06) +- F15 は入口で置き換える(断らない)+静的な検査。見張りは変えない。`-t`/`--show-token` の明示は置き換えない。予防策は新しい決まりを足さず決定論のチェックで代える +- F13 は漏れた鍵そのものを失効させ、`token`・`token-readonly` の両方を消す。F14 はコードブロックに出し `GH_APP_READONLY` を外し `.tokenSource` を足す。F7 は `--strict-mcp-config`。F9 は `-e PATH=…` + +### Claude が技術的に決めたこと(理由) +- 失効を手順書の散文ではなくスクリプト `revoke.sh` にする — 取り違え(F13)は手で手順を追うと起きる。1 コマンドなら Claude も人も同じ手順になり、偽物の `gh` でテスト(G9)できる。新しいファイルはイシューの実装対象に無いが、F13 の直し方の具体化で範囲は変えない +- `revoke.sh` は既定で控えの 2 つの鍵を失効させ、控えに無い漏れた鍵は `--stdin` で値を受けて失効させる — 確定事項(漏れた鍵そのものを失効させる)を、控えに無い鍵の場合も満たすため(計画レビュー 20261106_0028 の Blocker)。値を引数で受けると `ps`・履歴に残るので標準入力だけにする +- 失効の確かめに `api installation/repositories` を使う — App の鍵で読める最小の API。無効なら 401 で失敗する +- 入口の置き換えで `error` の欄を出さない — 文言に鍵の一部が入りうる(`state` で失敗は分かる) +- 静的な検査の対象から個々のイシューの記録(`docs/tests/open/` など)を外す — この計画書・テスト文書自体が「`--json` なしの書き方」を名指しする必要がある(M-01 は置き換えを確かめるために `--json` なしで流す)。打ったときは入口が置き換えるので、記録に書かれても鍵は出ない。I223 の事故の元は手順書の確かめ方で、手順書は対象に入る。grill-me のメモ(open のテスト文書を含む)からの絞り込みなので、イシューの受け入れ条件 2 も同じ範囲に直す(技術的な範囲の具体化で、価値判断は変わらない) +- 静的な検査は `-t`/`--show-token` 付きも不合格にする — 手順書が鍵を出す書き方を指示するのは、`--json` なしと同じ事故になる +- `--strict-mcp-config` の付け忘れを G7 で止める — 起動の所が増えたときに気づきにくく、MCP 経由で読み取りだけの鍵を迂回しうる(壊れても気づきにくく被害が大きい)。G6 と同じ論理行の読み方を使う +- `PATH` は `$HOME/.local/bin` を先頭に足す — 入口は `install.sh` がそこに入れる。呼び出し元の `PATH` に無くても入口を通る +- `adversarial-review.sh` の実験役の `--strict-mcp-config` を消す — `run_claude_logged` が付けるので 2 回渡さない +- 見張り(`pretooluse_guard.py`)・`.claude/settings.json`・`.github/` を触らない — 止めて聞く条件 2 に当たらない形で直せる + +### 台帳の残りの一覧 +- 一覧 I223 F11: 同梱 — `test_i223_gh_app.sh` の冒頭のコメントを直す(同じファイルを触る) +- 一覧 I223 F12: 同梱 — `github-app-setup.md`「1.」の G6 の保証の書き方を、G6・G7 の実際の範囲に合わせる(同じ節に F7 の決まりを足す) +- 一覧 I195 F5: 同梱 — tmux サーバーの環境で起動する件。鍵に関わる `PATH` を `-e` で渡す(F9 と同じ直し)。ほかの環境の差は鍵に関わらない +- 一覧 I223 F6: 残す — G6 の検出の形の狭さ。G7 も同じ読み方で、広げるのは別の作業(Low・再現なし) +- 一覧 I223 F10: 残す — tmux 3.2 未満。この端末は 3.4(Low・再現なし) +- 一覧 I195 F1・F2・F4: 残す — `tui-capture.sh` の入力・起動の待ちの件で、鍵の抜け道ではない(再現なし) +- 一覧 I195 F9(`scripts/claude/tests/test_tui_capture.sh`。計画の段階の grep で拾い漏れ、implement で見つけた): 残す — T7a の `no_start`・ダイアログ画面のテストの件で、T1 の照合の直しと関係しない(Low・再現なし) +- 一覧 I200 F7(`check-permission-prompt.sh`): 残す — write の上書き防止の件で、このイシューと関係しない +- 一覧 I208 F1〜F3・F5・I206 F4・I207 F6・F7(`code-review.sh`): 残す — 台帳の読み方の件で、起動の行と関係しない +- 一覧 I209 F6(`fix-review-lib.sh`): 残す — 診断レビューの SKIP の扱いで、起動の行と関係しない +- 一覧 I223 B1〜B4(`FILE=-`): 残す — bot の停止が解けるまでできない + +### チェック +- 要件適合性: 受け入れ条件の範囲を超える仕様追加なし(`revoke.sh` は F13 の直し方の具体化)。マルチテナント・ステータス遷移: 該当なし +- セキュリティ: 機密データ(App の鍵)の扱いを強める変更。鍵の値は引数・画面・記録に出さない(入口は `--json` で鍵の欄を取らない・`revoke.sh` は環境変数で渡し出力しない)。最小権限: テスト用のセッションの MCP を `--mcp-config` で渡したものだけにする。入力: 入口の判定は引数の配列で行い、シェルの文字列を読まない。OWASP: A02(秘密の露出)への対策。依存ライブラリの追加なし。bandit・npm audit の対象のコード変更なし +- テスト計画: 再発防止テスト(G8〜G10・G7・T1)あり。各 FG あり。認可・テナント境界: 該当なし +- P3/P5/P8 影響なし(DB・外部 API の新規・依存ファイルの変更なし) +- P6 影響なし(画面なし) +- P9 影響なし(個人情報・テナントデータを扱わない) +- 設計品質: 設定値は環境変数で上書きできる既定値(`GH_APP_REAL_GH`・`GH_APP_CACHE_DIR`)を使い、新しい定数を増やさない。エラー時は鍵を出さない側に倒す(入口: 本物の `gh` の失敗はその終了コードで返す・失効: 無効を確かめられなければ控えを消さず exit 1) + +## 計画変更記録 +- 計画変更記録 1(plan-review・2026-11-06・`docs/reviews/I225_plan_review_20261106_0028.md`): Blocker「控えの鍵だけでは、控えに無い漏れた鍵が失効しない」→ `revoke.sh --stdin`(標準入力で値を受け、その鍵を失効させる)を足し、G9 に入れた(4-2・§9)。Warning: `--strict-mcp-config` と `--mcp-config` の併用をスパイクで実証(調査結果)/M-01 の判定を `! grep -q`(合格=exit 0)にし、コマンドを分けた/イシューの Danger Ops を M-02 の失効に合わせた。Info(変更点の散らばり)は §9 の台帳の判断どおり同梱のまま。計画レビューのやり直し: 要(Blocker の対応で 4-2 の方式を足した)→ 20261106_0031 で OK +- 計画変更記録 2(plan-review・2026-11-06・`docs/reviews/I225_plan_review_20261106_0031.md` の Warning): TC-03 の合否を exit code だけにした/M-03 の期待結果を「一覧に playwright・github が出ない」にし、実際の文言は実結果に写す形にした/4-1 にグローバルオプションを前に置く書き方は置き換えない旨を足した。計画レビューのやり直し: 不要(方式・範囲・受け入れ条件は変えない) +- 計画変更記録 3(implement・2026-11-06): 4-2 の「無効の確認」を「`api installation/repositories` が失敗する」から「その標準エラーに `HTTP 401` が出る」に具体化した。理由: 通信の失敗でも `gh api` は失敗するので、失敗だけでは「無効を確認」と取り違える(失効できていないのに控えを消す・fail-open)。2026-11-06 に本物の `gh` で無効な鍵を渡し、`gh: Bad credentials (HTTP 401)`・exit 1 を確かめた。G9 の偽物も 401 の文言を出し、通信の失敗(401 なし)では控えを残して exit 1 になることを足す。計画レビューのやり直し: 不要(4-2 の方式の中の具体化) + +## レビュー結果 +- [20261106_0031 判定: ✅ 完了](../../reviews/closed/I225_plan_review_20261106_0031.md) +- [20261106_0028 判定: 差し戻し(Blocker 1件)](../../reviews/closed/I225_plan_review_20261106_0028.md) diff --git a/docs/reviews/I225_fix_diagnosis_20261106_0100.md b/docs/reviews/I225_fix_diagnosis_20261106_0100.md new file mode 100644 index 00000000..fe89c27a --- /dev/null +++ b/docs/reviews/I225_fix_diagnosis_20261106_0100.md @@ -0,0 +1,25 @@ +# I225 診断メモ(fix-loop・2026-11-06・review-gate `docs/reviews/I225_code_review_20261106_0045.md` の FINAL_VERDICT: HIGH) + +## 失敗分解 +- 再現手順: `bash scripts/claude/code-review.sh I225`(review-gate) +- 期待値: `docs/tests/open/I225_auto_test.md` の決定論ゲート 6 本をスクリプトが実走し、exit 0 の証跡が記録の先頭に入り、`omission-lint: OK`・FINAL_VERDICT が台帳の判定(止める指摘 0 件=OK)になる +- 実際値: 記録の先頭が「(決定論ゲート宣言なし)」「omission-lint: HIGH」「FINAL VERDICT: HIGH」。台帳の「止める指摘」の節は空(0 件)。code-reviewer の受け入れ条件 6 も「ゲートの実 exit code が無い」で確認不能(台帳 F7) +- ログ: `code-review.sh:330`(`extract_gate_commands` は見出し `## 決定論ゲート(自動実走)` の直後のコードブロックだけを読む)・`:412`(読めなければ「宣言なし」)・`:417-427`(`omission_lint` は宣言の節の外のコードブロックに `bash scripts/claude/tests/….sh` があれば HIGH) + +## 根本原因 +- auto_test の見出しを `## 決定論ゲート` と書いた(テンプレート `docs/tests/templates/auto_test_template.md:25` の `## 決定論ゲート(自動実走)` を写さず、前のイシュー I223 の文書の形を見て書いた)。スクリプトは見出しを完全な文字列で探すので、ゲートを宣言していないと読み、宣言の外にテストのコマンドがあるのを「書き漏れ」として HIGH にした +- 実装・テストの不具合ではない(同じ 6 本は implement で手元で全部 exit 0、CI も `CI_WAIT: OK`) +- セキュリティ観点: 該当なし(記録の書式の誤り。認証・秘密の扱いは変わらない) + +## 対応方針(全案+推奨) +- **案 A(推奨): 見出しを `## 決定論ゲート(自動実走)` に直す**。コマンドはすべて `bash scripts/claude/tests/*.sh` で、スクリプトの許可リスト(ALLOW)に入るので、次の review-gate で実走され exit code の証跡が入る(台帳 F7 の「証跡が無い」も同時に解ける) + - セキュリティ: 影響なし。BP: テンプレートどおり。保守性: 変更は 1 行。リスク: 実走で 1 本でも落ちれば BLOCKER になるが、それは正しい止まり方 +- 案 B: コマンドをコードブロックの外(表や散文)に出し、ゲートを宣言しない + - omission-lint は通るが、ゲートの実走の証跡が無くなる(受け入れ条件 6 の確認ができない)。採らない +- 案 C: スクリプトの見出しの照合をゆるめる(`## 決定論ゲート` も読む) + - 範囲外(`code-review.sh` の変更はこのイシューの計画に無い)。書く側を直せば足りる。採らない + +## 影響調査の実施有無と結果 +- 実施した。`grep -rn "^## 決定論ゲート" docs/tests/templates docs/tests/open`: テンプレートは正しい形、開いている文書で誤っているのは `I225_auto_test.md:18` だけ +- 他の呼び出し箇所: gate-check.sh・test-gate はこの見出しの節の変更を「決定論ゲートの節を変えた」として見る(`test_i193_official_flow.sh` G15)。見出しを直すのは記録の場所(`docs/tests/`)の変更で、次の review-gate がこの差分を見直すので問題ない +- 関連機能への波及: なし diff --git a/docs/reviews/backlog/I195.md b/docs/reviews/backlog/I195.md index 8e6c6752..66c2397c 100644 --- a/docs/reviews/backlog/I195.md +++ b/docs/reviews/backlog/I195.md @@ -8,7 +8,6 @@ - F1 SEV=Medium REPRO=no KIND=harness FILE=scripts/claude/tui-capture.sh | `!` の判定が引数全体の先頭にしか効かない。改行入りの入力(`$'/plan-\n!ls'`)だと、送られた改行が Enter になって claude が動く。その後の `!ls` はシェル実行になる | 一覧に回した理由: 再現なし - F2 SEV=Medium REPRO=no KIND=harness FILE=scripts/claude/tui-capture.sh | 起動の待ちが行頭の `❯` だけを見ている。初回の trust ダイアログの選択行も `❯` で始まるため、プロンプトと誤検出して入力を送るおそれがある | 一覧に回した理由: 再現なし - F4 SEV=Low REPRO=no KIND=harness FILE=scripts/claude/tui-capture.sh | claude が起動直後に終了してセッションが消えても、時間切れまで待ち続け、原因を取り違えたメッセージを出す。ループ内で `tmux has-session` を確認するとよい | 一覧に回した理由: Low/再現なし -- F5 SEV=Low REPRO=no KIND=harness FILE=scripts/claude/tui-capture.sh | tmux サーバーが既に動いていると、`new-session` はそのサーバーの環境で claude を起動する。専用ソケット(`-L`)を使うか、`-e` で環境を渡すとよい | 一覧に回した理由: Low/再現なし - F6 SEV=Low REPRO=no KIND=harness FILE=scripts/claude/check-manual-test-executor.sh | `試した手段` の読み取りが、セル内の `\|` で切れる。後ろの `check-permission-prompt.sh` が読まれず、正当な行を誤って不合格にしうる | 一覧に回した理由: Low/再現なし - F7 SEV=Low REPRO=no KIND=harness FILE=scripts/claude/check-manual-test-executor.sh | 空白の除去が `[[:space:]]` で、C ロケールでは全角空白を除けない。全角空白だけの `試した手段:` が通る | 一覧に回した理由: Low/再現なし - F9 SEV=Low REPRO=no KIND=harness FILE=scripts/claude/tests/test_tui_capture.sh | T7a が終了コードしか見ておらず `no_start` を確認していない。`❯` を含むダイアログ画面のテストも無い | 一覧に回した理由: Low/再現なし diff --git a/docs/reviews/backlog/I223.md b/docs/reviews/backlog/I223.md index c47179e6..0dbbcc6f 100644 --- a/docs/reviews/backlog/I223.md +++ b/docs/reviews/backlog/I223.md @@ -15,5 +15,3 @@ - F6 SEV=Low REPRO=no KIND=harness FILE=scripts/claude/tests/test_i223_gh_app.sh | G6 の検出が狭い(`-p` の直後が `--` か `"$` の形のみ・トップ階層のみ・ファイルに export が 1 行でもあれば除外)。将来の付け忘れを取りこぼす | 一覧に回した理由: Low/再現なし - F8 SEV=Low REPRO=no KIND=harness FILE=scripts/claude/gh-app/gh-app-token | 読み取りだけの鍵の要求が actions/checks/statuses を常に含み、App の権限が減ると 422 で読み取りまで全滅し、原因がメッセージから分からない | 一覧に回した理由: Low/再現なし - F10 SEV=Low REPRO=no KIND=harness FILE=scripts/claude/tui-capture.sh | `tmux new-session -e` は tmux 3.2 以上でしか使えない。古い tmux では、原因の分からない一般的なエラーで失敗する。 | 一覧に回した理由: Low/再現なし -- F11 SEV=Low REPRO=no KIND=harness FILE=scripts/claude/tests/test_i223_gh_app.sh | 冒頭のコメントが「G1〜G5」のままで、G6 が抜けている。 | 一覧に回した理由: Low/再現なし -- F12 SEV=Low REPRO=no KIND=harness FILE=docs/runbooks/github-app-setup.md | 手順書は「Claude を起こす行はすべて G6 が止める」と書いているが、G6 が見るのは `scripts/claude/` 直下の .sh だけで、`tmux new-session` と `claude` が 1 行にそろった形などに限られる。保証が実装より広い(前回の F6 と同種)。 | 一覧に回した理由: Low/再現なし diff --git a/docs/reviews/backlog/I225.md b/docs/reviews/backlog/I225.md new file mode 100644 index 00000000..27a1bc13 --- /dev/null +++ b/docs/reviews/backlog/I225.md @@ -0,0 +1,9 @@ +# I225 の台帳の残り(軽い指摘の一覧) + +- 記録: `docs/reviews/I225_code_review_20261106_0057.md`(close の後は `docs/reviews/closed/`) +- 一覧に回した決まり: `.claude/skills/retro/SKILL.md` 4-0(Low、または再現なし。後続イシューにしない) +- 見る場所: `.claude/skills/plan-issue/SKILL.md`「台帳の残りの一覧」(実装対象のファイルに当たる行を、同梱するか残すか決める。同梱して直した行は消し、行が無くなったらこのファイルを消す) + +- F2 SEV=Low REPRO=yes KIND=harness FILE=scripts/claude/gh-app/gh | 置き換えた `auth status` は hosts が空(ログインなし)のとき、何も出さず exit 0 になる。元の `gh` は「ログインしていない」と出して exit 1 にする | 一覧に回した理由: Low +- F3 SEV=Low REPRO=yes KIND=harness FILE=scripts/claude/gh-app/gh | `json="$(… --json hosts)" || exit $?` により、本物の `gh` が非 0 で終わると取得済みの JSON を捨てる。空や JSON でない出力のときは Python のトレースバックが出る | 一覧に回した理由: Low +- F4 SEV=Low REPRO=yes KIND=harness FILE=docs/runbooks/plan-writing-rules.md | `github-app-setup.md` は決まりの正を `plan-writing-rules.md` としている。しかしその側に `--strict-mcp-config`・tmux の `PATH`・G7 の記述が足されていない | 一覧に回した理由: Low diff --git a/docs/reviews/closed/I225_code_review_20261106_0045.md b/docs/reviews/closed/I225_code_review_20261106_0045.md new file mode 100644 index 00000000..ebf15ace --- /dev/null +++ b/docs/reviews/closed/I225_code_review_20261106_0045.md @@ -0,0 +1,119 @@ +## 決定論ゲート実行結果(code-review.sh 自動注入) + +(決定論ゲート宣言なし)- 高リスク判定: NO(計画レビュー=NO / path=NO / 台帳の目印=NO) +- 台帳(full・1 周目): OK(code-reviewer の VERDICT=OK・合否に使わない) + +- omission-lint: HIGH +- FINAL VERDICT(gate/omission/LLM 合成): HIGH + +--- + +# コードレビュー [I225] ── [2026-11-06 12:00] + +## 受け入れ条件の照合 +| # | 受け入れ条件 | 実装状況 | 備考 | +|---|------------|---------|------| +| 1 | `--json`・`-t` なしの `gh auth status` を入口で流すと、鍵の欄が出ず名義・出どころ・状態だけが出る | ✅ 実装済み | `gh-app/gh` の置き換えを確認した。G8 の実走結果は決定論ゲート欄に無い。ゲート欄は「決定論ゲート宣言なし」で、`omission-lint: HIGH` とだけ出ている。 | +| 2 | 決まりの置き場に `--json` なしの `gh auth status` を書くと静的な検査で落ちる | ✅ 実装済み | G10 を追加した。ただし `gh auth token` と複数行の書き方は検査の対象外(F6)。 | +| 3 | 失効の手順が漏れた鍵そのものを失効させ、2 つの控えを消す形になっている | ⚠️ 一部不足 | `revoke.sh` と `--stdin` を実装した。ただし失効の確認が 401 だけを根拠にしており、DELETE の成否を見ていない(F1)。 | +| 4 | 古い鍵の点検コマンドをそのままコピーして流せる | ✅ 実装済み | 表の外のコードブロックに出し、`GH_APP_READONLY=` と `.tokenSource` を足した。 | +| 5 | `claude -p`・`tui-capture.sh` が MCP・`PATH` の違いで読み取りだけの鍵を迂回しない | ✅ 実装済み | 起動の行に `--strict-mcp-config` を足し、tmux に `-e PATH=` を足した。 | +| 6 | 既存のハーネステスト・CI がすべて pass する | ⚠️ 確認不能 | 注入された決定論ゲート欄に実 exit code の記録が無く、`omission-lint: HIGH` とだけ出ている。この内容は読み取れていない。CI の結果で確認が要る(F7)。 | + +## 台帳 +FINDING: F1 SEV=Medium STATUS=open FILE=scripts/claude/gh-app/revoke.sh LINE=20-25 SECURITY=yes STOP=- | `revoke_one` は DELETE の成否を見ず、続く読み取りが HTTP 401 なら「失効を確認」とする。一度も有効でなかった鍵(末尾を伏せた・打ち間違い・すでに失効済み)でも成功扱いになる | 再現: `revoke.sh --stdin` に末尾を欠いた鍵を渡す → DELETE は失敗し、読み取りは 401 → 「渡された鍵: 失効を確認しました」と出て exit 0。本物の鍵は有効なまま残る +FINDING: F2 SEV=Low STATUS=open FILE=scripts/claude/gh-app/gh LINE=28-37 SECURITY=no STOP=- | 置き換えた `auth status` は hosts が空(ログインなし)のとき、何も出さず exit 0 になる。元の `gh` は「ログインしていない」と出して exit 1 にする | 再現: `GH_APP_DISABLE=1`・保存されたログインなしで `gh auth status` → 出力なし・exit 0 +FINDING: F3 SEV=Low STATUS=open FILE=scripts/claude/gh-app/gh LINE=22-23 SECURITY=no STOP=- | `json="$(… --json hosts)" || exit $?` により、本物の `gh` が非 0 で終わると取得済みの JSON を捨てる。空や JSON でない出力のときは Python のトレースバックが出る | 再現: 本物の `gh` が `--json` 付きで exit 1 を返すと、標準エラーの案内 1 行だけで状態の行が出ない +FINDING: F4 SEV=Low STATUS=open FILE=docs/runbooks/plan-writing-rules.md LINE=- SECURITY=no STOP=- | `github-app-setup.md` は決まりの正を `plan-writing-rules.md` としている。しかしその側に `--strict-mcp-config`・tmux の `PATH`・G7 の記述が足されていない | 再現: 新しい起動の行を足す人が `plan-writing-rules.md` だけを読むと `--strict-mcp-config` を付け忘れ、CI の G7 で初めて気づく +FINDING: F5 SEV=Low STATUS=open FILE=scripts/claude/gh-app/revoke.sh LINE=46-52 SECURITY=yes STOP=- | 失効の確認後に `rm -f` で控えを消すまでの間に、並行する `gh` が控えを取り直して書き込むと、失効していない新しい鍵の控えを消してしまう | 再現: 失効の確認と `rm` の間に別の `gh` 呼び出しが `token` を書き換える → 新しい鍵の控えが消えて「失効を確認」と表示される +FINDING: F6 SEV=Low STATUS=open FILE=scripts/claude/tests/test_i223_gh_app.sh LINE=- SECURITY=yes STOP=- | G10 は文字列 `gh auth status` だけを見る。鍵を丸ごと出す `gh auth token` や、`\` で複数行に分けた `--json` 付きの書き方は扱えない。前者は取りこぼし、後者は誤検出になる | 再現: 手順書に `` `gh auth token` `` を書いても G10 は通る +FINDING: F7 SEV=Medium STATUS=open FILE=- LINE=- SECURITY=no STOP=- | 決定論ゲート欄が「宣言なし」で、`omission-lint: HIGH` の内容が分からない。ゲートの実 exit code の証跡が無い | 再現: ゲート欄に実 exit code が無いため確認できない。`omission-lint` の指摘を確認する + +--- + +REVIEWED_COMMIT: 446e786ad98609543a465d70e7b75f544c4f860d +RISK: NO +VERDICT: HIGH + +## 台帳(累積・スクリプトが書く) +LEDGER: F1 SEV=Medium STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=yes FILE=scripts/claude/gh-app/revoke.sh | `revoke_one` は DELETE の成否を見ず、続く読み取りが HTTP 401 なら「失効を確認」とする。一度も有効でなかった鍵(末尾を伏せた・打ち間違い・すでに失効済み)でも成功扱いになる +LEDGER: F2 SEV=Low STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=no FILE=scripts/claude/gh-app/gh | 置き換えた `auth status` は hosts が空(ログインなし)のとき、何も出さず exit 0 になる。元の `gh` は「ログインしていない」と出して exit 1 にする +LEDGER: F3 SEV=Low STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=no FILE=scripts/claude/gh-app/gh | `json="$(… --json hosts)" || exit $?` により、本物の `gh` が非 0 で終わると取得済みの JSON を捨てる。空や JSON でない出力のときは Python のトレースバックが出る +LEDGER: F4 SEV=Low STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=no FILE=docs/runbooks/plan-writing-rules.md | `github-app-setup.md` は決まりの正を `plan-writing-rules.md` としている。しかしその側に `--strict-mcp-config`・tmux の `PATH`・G7 の記述が足されていない +LEDGER: F5 SEV=Low STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=yes FILE=scripts/claude/gh-app/revoke.sh | 失効の確認後に `rm -f` で控えを消すまでの間に、並行する `gh` が控えを取り直して書き込むと、失効していない新しい鍵の控えを消してしまう +LEDGER: F6 SEV=Low STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=yes FILE=scripts/claude/tests/test_i223_gh_app.sh | G10 は文字列 `gh auth status` だけを見る。鍵を丸ごと出す `gh auth token` や、`\` で複数行に分けた `--json` 付きの書き方は扱えない。前者は取りこぼし、後者は誤検出になる +LEDGER: F7 SEV=Medium STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=no FILE=- | 決定論ゲート欄が「宣言なし」で、`omission-lint: HIGH` の内容が分からない。ゲートの実 exit code の証跡が無い +LEDGER_ROUND: 1 + +## 止める指摘(スクリプトが決めた・fix-loop が直すのはこれだけ) +(再現は本文の同じ番号の FINDING 行) + +## 組み込みのレビューの出力(公式・参考) + +### 組み込み /code-review の指摘 + +Permission allow rule "Read(*)" adds nothing to Read's default, which already runs without asking in the working directories. "Read" alone allows every path. Older versions of Claude Code treat "Read(*)" as "Read", and this version will too if Anthropic turns this change off. +Permission allow rule "Grep(*)" adds nothing to Grep's default, which already runs without asking in the working directories. "Grep" alone allows every path. Older versions of Claude Code treat "Grep(*)" as "Grep", and this version will too if Anthropic turns this change off. +Permission allow rule "Glob(*)" adds nothing to Glob's default, which already runs without asking in the working directories. "Glob" alone allows every path. Older versions of Claude Code treat "Glob(*)" as "Glob", and this version will too if Anthropic turns this change off. +```json +[ + { + "file": "scripts/claude/gh-app/revoke.sh", + "line": 22, + "summary": "revoke_one counts any HTTP 401 on the follow-up read as proof the token was revoked, and it ignores the result of the DELETE. A string that was never a valid token (truncated, masked or mistyped) therefore 'passes' too.", + "failure_scenario": "The leak this script exists for is `gh auth status`, which masks the tail of the token. The user pastes the visible part, or makes a typo, into `revoke.sh --stdin`. DELETE fails because the token is invalid, the GET returns 401, and the script prints '渡された鍵: 失効を確認しました' with exit 0. The real leaked token stays valid for up to an hour. Fix: require the DELETE to return 204, or check that the token works (200) before revoking it." + }, + { + "file": "scripts/claude/gh-app/gh", + "line": 32, + "summary": "The rewritten `auth status` exits 0 with no output when the JSON has no accounts. Plain `gh auth status` prints 'You are not logged into any GitHub hosts' and exits 1.", + "failure_scenario": "With `GH_APP_DISABLE=1` and no stored login, `gh auth status` returns `{\"hosts\":{}}`. The wrapper prints nothing and exits 0, so scripts or people reading the exit code think authentication succeeded." + }, + { + "file": "scripts/claude/gh-app/gh", + "line": 23, + "summary": "`json=$(... --json hosts) || exit $?` throws away the captured JSON whenever the real gh exits non-zero. Empty or non-JSON output also crashes `json.load` with a Python traceback.", + "failure_scenario": "If gh exits 1 in --json mode because an account is in the error state (or a host is unreachable), the user sees only the stderr banner, with no host/login/state lines. That is exactly the case they need to diagnose. G8's stub always exits 0, so this path is never tested." + }, + { + "file": "scripts/claude/tui-capture.sh", + "line": 47, + "summary": "Passing `--strict-mcp-config` with no `--mcp-config` also drops the project's `.mcp.json` (Playwright), so the session that is supposed to show what a real interactive session looks like now differs from one.", + "failure_scenario": "A test-gate check uses tui-capture to look at MCP-related screens: the `/` suggestions for MCP prompts, the MCP server status, or Playwright tool approval dialogs. None of it appears, so the result is wrong or falsely reported as 'not present'." + }, + { + "file": "scripts/claude/test-session.sh", + "line": 8, + "summary": "test-session.sh now always passes `--strict-mcp-config`, which silently turns off the project `.mcp.json` (Playwright MCP) for every manual test session.", + "failure_scenario": "Running `bash scripts/claude/test-session.sh \"\"` as before (frontend-verify style) now fails with the tool unavailable or denied. The runbook does not say that `--mcp-config .mcp.json` must now be passed explicitly." + }, + { + "file": "docs/runbooks/plan-writing-rules.md", + "line": 90, + "summary": "github-app-setup.md says the authoritative rule lives in plan-writing-rules.md, but that file was not updated: it still says only GH_APP_READONLY=1 and G6, with nothing about `--strict-mcp-config`, the tmux PATH, or G7.", + "failure_scenario": "Someone adding a new Claude launch line reads the rule's authoritative source (plan-writing-rules.md), adds only GH_APP_READONLY=1, and leaves out `--strict-mcp-config`. They only find out when G7 fails in CI. The two documents disagree on what the rule is." + }, + { + "file": "scripts/claude/gh-app/revoke.sh", + "line": 50, + "summary": "The cache file is deleted with `rm -f` after the revocation check. If another gh call (via gh-app-token) rewrites the cache in that window, the new valid token's cache is deleted without being revoked.", + "failure_scenario": "A session that is running gh concurrently gets a 401 and refetches, and gh-app-token writes the new token in the gap between revoke_one and rm. revoke.sh deletes that fresh cache and reports it as '失効を確認して控えを消しました'. A token that was never revoked stays live, and the message is not accurate. Fix: check that the cache contents are unchanged before deleting." + }, + { + "file": "scripts/claude/tests/test_i223_gh_app.sh", + "line": 381, + "summary": "G10 only matches the literal string `gh auth status` and checks flags only up to the end of the line. `gh auth token`, which prints the whole token, and invocations through a variable such as `\"$GH\" auth status` are not covered. A multi-line command continued with `\\` gets flagged even when it does have --json.", + "failure_scenario": "If `gh auth token` is added to a runbook or script, G10 passes and the full token can end up on screen or in the transcript. Meanwhile, a correct command split across lines as `gh auth status \\` / ` --json hosts` fails CI." + }, + { + "file": "scripts/claude/check-permission-prompt.sh", + "line": 72, + "summary": "For mcp-kind checks, `--strict-mcp-config` drops the user-level MCP settings, so approval prompts for MCP tools defined there can no longer be judged.", + "failure_scenario": "Running `check-permission-prompt.sh mcp mcp____tool` now returns 'cannot judge' (2) or a wrong result because the tool does not exist. Before this change it judged it under the same conditions as a real session." + } +] +``` + +### 組み込み /security-review の指摘 + +(実行していない: パス判定 NO) diff --git a/docs/reviews/closed/I225_code_review_20261106_0057.md b/docs/reviews/closed/I225_code_review_20261106_0057.md new file mode 100644 index 00000000..55bc58a4 --- /dev/null +++ b/docs/reviews/closed/I225_code_review_20261106_0057.md @@ -0,0 +1,49 @@ +## 決定論ゲート実行結果(code-review.sh 自動注入) + +- `bash scripts/claude/tests/test_i223_gh_app.sh` → exit=0 ✅ +- `bash scripts/claude/tests/test_tui_capture.sh` → exit=0 ✅ +- `bash scripts/claude/tests/test_check_permission_prompt.sh` → exit=0 ✅ +- `bash scripts/claude/tests/test_i197_adversarial_process.sh` → exit=0 ✅ +- `bash scripts/claude/tests/test_fix_review.sh` → exit=0 ✅ +- `bash scripts/claude/tests/test_i193_official_flow.sh` → exit=0 ✅ +- 高リスク判定: NO(計画レビュー=NO / path=NO / 台帳の目印=NO) +- 台帳(delta・2 周目): OK(code-reviewer の VERDICT=OK・合否に使わない) + +- omission-lint: OK +- FINAL VERDICT(gate/omission/LLM 合成): OK + +--- + +# コードレビュー [I225](差分なし) + +起点 446e786ad98609543a465d70e7b75f544c4f860d の後に、記録の場所の外でブランチが変えたファイルは無い。レビュー役は呼んでいない。台帳は前の周のまま。 + +## 台帳 +指摘なし + +REVIEWED_COMMIT: fb4b563a7b4bb4980abf0ae5eb493ac8b64c6672 +RISK: NO +VERDICT: OK + +## 台帳(累積・スクリプトが書く) +LEDGER: F1 SEV=Medium STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=yes FILE=scripts/claude/gh-app/revoke.sh | `revoke_one` は DELETE の成否を見ず、続く読み取りが HTTP 401 なら「失効を確認」とする。一度も有効でなかった鍵(末尾を伏せた・打ち間違い・すでに失効済み)でも成功扱いになる +LEDGER: F2 SEV=Low STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=no FILE=scripts/claude/gh-app/gh | 置き換えた `auth status` は hosts が空(ログインなし)のとき、何も出さず exit 0 になる。元の `gh` は「ログインしていない」と出して exit 1 にする +LEDGER: F3 SEV=Low STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=no FILE=scripts/claude/gh-app/gh | `json="$(… --json hosts)" || exit $?` により、本物の `gh` が非 0 で終わると取得済みの JSON を捨てる。空や JSON でない出力のときは Python のトレースバックが出る +LEDGER: F4 SEV=Low STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=no FILE=docs/runbooks/plan-writing-rules.md | `github-app-setup.md` は決まりの正を `plan-writing-rules.md` としている。しかしその側に `--strict-mcp-config`・tmux の `PATH`・G7 の記述が足されていない +LEDGER: F5 SEV=Low STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=yes FILE=scripts/claude/gh-app/revoke.sh | 失効の確認後に `rm -f` で控えを消すまでの間に、並行する `gh` が控えを取り直して書き込むと、失効していない新しい鍵の控えを消してしまう +LEDGER: F6 SEV=Low STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=yes FILE=scripts/claude/tests/test_i223_gh_app.sh | G10 は文字列 `gh auth status` だけを見る。鍵を丸ごと出す `gh auth token` や、`\` で複数行に分けた `--json` 付きの書き方は扱えない。前者は取りこぼし、後者は誤検出になる +LEDGER: F7 SEV=Medium STATUS=open SCOPE=delta KIND=harness STOP=- REPRO=yes RISK=- SECURITY=no FILE=- | 決定論ゲート欄が「宣言なし」で、`omission-lint: HIGH` の内容が分からない。ゲートの実 exit code の証跡が無い +LEDGER_ROUND: 2 + +## 止める指摘(スクリプトが決めた・fix-loop が直すのはこれだけ) +(再現は本文の同じ番号の FINDING 行) + +## 組み込みのレビューの出力(公式・参考) + +### 組み込み /code-review の指摘 + +(実行していない: 差分なし) + +### 組み込み /security-review の指摘 + +(実行していない: パス判定 NO) diff --git a/docs/reviews/closed/I225_fix_diagnosis_review_20261106_0056.md b/docs/reviews/closed/I225_fix_diagnosis_review_20261106_0056.md new file mode 100644 index 00000000..5f251205 --- /dev/null +++ b/docs/reviews/closed/I225_fix_diagnosis_review_20261106_0056.md @@ -0,0 +1,16 @@ +# 診断レビュー [I225] + +## 指摘一覧 +| 重大度 | 観点 | 指摘内容 | 該当箇所 | +|--------|------|---------|---------| +| Info | 対応方針 | 見出しを直すと、6 本のテストがそれぞれ `GATE_TIMEOUT` の既定値 120 秒の制限付きで実走される。`test_i193_official_flow.sh`(174 件)と `test_i197_adversarial_process.sh`(113 件)は件数が多く、遅いマシンでは時間切れ(exit 124)になりうる。implement で手元の所要時間を測った記録は診断メモに無い。時間切れも「正しい止まり方」ではあるが、見出しを直す前に所要時間を確かめておくと手戻りを減らせる。 | `code-review.sh:381`(`GATE_TIMEOUT` 既定 120 秒)、`I225_auto_test.md:19-26` | +| Info | 根本原因 | 根本原因は実コードで確認できた。`I225_auto_test.md:18` は `## 決定論ゲート` で、`code-review.sh:330` と `:421` は `## 決定論ゲート(自動実走)` で探す。見出しが一致しないので「宣言なし」になる(`:412`)。宣言の外の `bash scripts/claude/tests/*.sh` は omission-lint が HIGH にする。症状の言い換えにはなっていない。 | `code-review.sh:330,412,421` | +| Info | 対応方針 | 案 A(見出しを 1 行直す)が最小で、テンプレートどおりで、台帳 F7(証跡が無い)も同時に解ける。案 B(証跡が無くなる)と案 C(範囲外)を採らない理由も妥当。6 本はすべて `bash scripts/claude/tests/*.sh` の形で、`classify_gate` の ALLOW に入る(メタ文字なし・`../` なし)。 | `code-review.sh:363` | + +## 影響調査の網羅性評価 +- 同型バグ(同じ見出しの書き間違い)は `grep "^## 決定論ゲート" docs/tests` で確認した。誤っているのは `docs/tests/open/I225_auto_test.md:18` の 1 件だけだった。`closed` の約 45 件とテンプレートはすべて `(自動実走)` 付きで、他に漏れはない。 +- 呼び出し箇所は、見出しを読む `code-review.sh` の 2 か所(`:330`、`:421`)が診断メモと一致する。gate-check.sh と test-gate への波及も確認済みで、見出しの直しは記録の変更として次の review-gate が見直す。 +- 関連機能への波及はない。 +- 欠落は、上の Info にある所要時間の確認だけで、重大なものではない。 + +VERDICT: OK diff --git a/docs/reviews/closed/I225_issue_review_20261105_1457.md b/docs/reviews/closed/I225_issue_review_20261105_1457.md new file mode 100644 index 00000000..ca9eb24c --- /dev/null +++ b/docs/reviews/closed/I225_issue_review_20261105_1457.md @@ -0,0 +1,21 @@ +# イシュー自己完結度レビュー [I225] + +## 判定: 十分 + +主要セクションはすべて埋まっていて、重大な論理矛盾はありません。未確定の箇所(F15・F13・F9 の具体策)は「未定・grill-me で確認」と明示されています。これは意図的な宣言として扱い、未記入にはしません。 + +## 未記入の重要セクション +なし + +## 指摘一覧(助言) +| 重大度 | 観点 | 指摘内容 | 該当箇所 | +|--------|------|---------|---------| +| Warning | 受け入れ条件 | F7・F9 の条件が「迂回しない、またはその残りの範囲が手順書に書かれている」という二択になっています。どちらでも満たせるので、F7 を実装しなくても条件を満たせてしまいます。「MCP 経由」「別の `PATH`」それぞれで何を確認すれば合格かが決まっていません。決定論の検証(例: 起動引数に `--strict-mcp-config` があることを見る静的テスト)を一つ入れると検証しやすくなります。 | 受け入れ条件 4 つ目 | +| Warning | 根本原因 | 根本原因が「段取りが無い」という手順面の説明に寄っています。F15・F13 には当てはまります。F7・F9(入口を通らない経路)は、後半の「入口を通ることを前提にしている」で別に説明されていて、二つの原因が一文にまとまっています。計画時に原因を分けて扱うと、解決方針との対応が追いやすくなります。 | 根本原因 | +| Warning | 実装対象 | F13 と F14 の手順書の修正は、受け入れ条件の検証手段が「そのままコピーして流せる」「正しい形になっている」と読み手の判断に頼っています。F14 は jq 構文が通ることを確かめるテストを足せば決定論にできます。F13 は、漏れた鍵そのものを失効させ、2 つの控えを消す形になっているかを文面で確かめる静的チェックを足せます。 | 受け入れ条件 2・3 つ目 | +| Info | 解決方針 | F15 は「見張りの変更」と「静的な検査」の二択です。どちらを選んでも、`docs/runbooks/workflow.md` の「止めて聞く条件」2 に当たるかどうかの判定が計画で必要になります。制約欄に書かれているので、漏れとは言えません。 | 解決方針 F15・制約 | +| Info | スコープ | F7 の対象スクリプトが 7 本と多く、`--mcp-config` が必要な判定が混ざる可能性があります。「MCP が要る判定」が現状どれなのかは未調査のようです。計画で洗い出す前提と読めるので、問題はありません。 | 実装対象・解決方針 F7 | +| Info | 自己完結度 | 参照している台帳・診断メモ(`docs/reviews/I223_*`、`docs/tests/open/I223_manual_test.md`)は、I223 のマージ前後でパスが変わる可能性があります。計画着手時に存在を確かめる必要があります。 | 関連資料 | + +## 次のアクションの推奨 +十分と判定したので、`/grill-me` をスキップして `/plan-issue` に進められます。ただし、未定の F15(見張りか静的検査か)、F13(失効の具体形)、F9(`PATH` を渡すか別の塞ぎ方か)は、計画の前に `/grill-me` で決めておくと手戻りが減ります。 diff --git a/docs/reviews/closed/I225_plan_review_20261106_0028.md b/docs/reviews/closed/I225_plan_review_20261106_0028.md new file mode 100644 index 00000000..196b99fd --- /dev/null +++ b/docs/reviews/closed/I225_plan_review_20261106_0028.md @@ -0,0 +1,27 @@ +# 計画書レビュー [I225] ── [2026-11-06 12:00] + +## 判定: 差し戻し(Blocker 1件) + +## レビュー指摘一覧 +| 重大度 | 観点 | 指摘内容 | 該当箇所 | 対応 | +|--------|------|---------|---------|------| +| Blocker | P1 要件適合性 | F13 の確定事項と受け入れ条件 3 は「漏れた鍵そのものを `GH_TOKEN` に入れて `DELETE /installation/token` する」ことです。計画の `revoke.sh` は控えの鍵だけを失効させ、画面に出た鍵の値は受け取りません(§9)。控えが取り直された後に漏れた鍵が見つかった場合、その鍵は失効しません。計画は「10 分未満で切れる」と書いています。これは F13 の再現ケース(期限間際に取り直された鍵)をそのまま残します。これは技術的な具体化ではなく、確定事項からの逸脱です。逸脱の理由は「鍵の値を引数や入力に渡すと再び記録に出る」です。この理由は、値を標準入力や `read -s` で受ける形にすれば避けられます。この選択肢が検討されていません。 | plan §4-2、§9「`revoke.sh` は控えの鍵だけを失効させ」。イシュー受け入れ条件 3 | 次のどちらかにします。(a) 漏れた鍵を標準入力などで受けて失効させる口を `revoke.sh` に足します(値は引数や画面に出さず、テストの G9 に入れます)。(b) 受け入れ条件 3 と確定事項の側を、控えの鍵に絞る形に直し、残るリスク(取り直し後の古い鍵)を明記してユーザーの了承を得ます。 | +| Warning | P1 / 運用 | イシューは Danger Ops を「無(鍵の失効はこのイシューの作業では行わない)」としています。一方、手動テスト M-02 は本物の GitHub で `revoke.sh` を流し、書き込みの鍵を失効させます。記述が矛盾しており、並行セッションの鍵も失効させます。 | I225.md「Danger Ops」と manual M-02 | イシューの Danger Ops 記述を、M-02 で控えの鍵を失効させる旨に合わせて直します。 | +| Warning | 外部挙動依存の未実証 | `--strict-mcp-config` と `--mcp-config .mcp.json` の併用、および tmux の中の `claude` の `/mcp` 表示は、実装後の M-03・M-04 で初めて確かめる流れです。計画段階のスパイクがなく、実装ステップの前倒しもありません。稼働中のセッションで確かめられます。 | plan §4-5、§8 のリスク表、manual M-03・M-04 | 実装の最初のステップで M-04 相当を実証し、結果を調査結果に記録します。 | +| Warning | コマンド衛生 | M-01 は複数コマンドを `→` でつなぎ、リダイレクトと `grep -c` を組み合わせています。M-01 の合否は `grep -c ghs_` の出力値が `0` かどうかの目視です。合格時に非ゼロ終了する判定になります。 | manual M-01 | `! grep -q ghs_ `(合格=exit 0)に直し、コマンドを分けます。 | +| Info | 保守 | 台帳 backlog(I223 F11・F12、I195 F5)の削除と、`adversarial-review.sh` の編集が同梱されています。範囲外ではありませんが、変更点が散らばります。 | plan §4-7 | 対応は任意です。 | + +セキュリティ、P3(DB)、P9 の観点は、次のとおり問題ありません。 +- 入口の判定は argv で行い、鍵の欄を取らず、`error` 欄も出さない設計です。 +- P3 は DB 変更なしのため対象外です。 +- P9 は個人情報を扱わないため対象外です。 +- 新しい安全装置の作り込み(P8)はなく、見張りも変えない設計です。 + +## 高リスク判定 +判定: No +該当条件: なし。変更対象は `scripts/claude/gh-app/` と `scripts/claude/` のハーネスと runbook です。`.github/*`・`pretooluse_guard.py`・`.claude/settings*.json` には触れません。 + +--- +指摘を直してから計画レビュー(plan-review 段階)をやり直す。 + +VERDICT: BLOCKER diff --git a/docs/reviews/closed/I225_plan_review_20261106_0031.md b/docs/reviews/closed/I225_plan_review_20261106_0031.md new file mode 100644 index 00000000..05c88a97 --- /dev/null +++ b/docs/reviews/closed/I225_plan_review_20261106_0031.md @@ -0,0 +1,39 @@ +# 計画書レビュー [I225] ── 2026-11-06 00:00 + +## 判定: ✅ 完了 + +## レビュー指摘一覧 +| 重大度 | 観点 | 指摘内容 | 該当箇所 | 対応 | +|--------|------|---------|---------|------| +| Warning | P4 | TC-03 の合否に「標準エラーに jq のエラーが無い」という目視の項目がある。合否は exit code だけにするほうがよい。例: `GH_APP_READONLY= GH_APP_DISABLE=1 gh auth status --json hosts --jq '…' 2>&1 >/dev/null` の出力が空で、かつ exit 0。出力の中身(`osushi0404-bot … error`)もこの端末の保存されたログインに依存する。 | auto_test TC-03 | 修正推奨 | +| Warning | P4 | M-03 の期待結果が「`No MCP servers configured` などの表示」で曖昧。tmux の中の `claude`(対話)で `--strict-mcp-config` が効く点は、計画自身が M-03 で初めて実証すると書いている。実証手段と実施環境は明記されているので Blocker にはしない。判定文言を、実際に出た文言を実結果に転記して決める形にしたほうがよい。 | manual_test M-03 / plan 調査結果 | 修正推奨 | +| Warning | P1 | 4-1 の入口の判定は `gh auth status` が引数の 1・2 番目にあるときだけ見る。`gh -R x auth status` のようにグローバルオプションが前にあると置き換えが効かない。普通の書き方の事故が対象で、静的な検査(G10)も併用するので許容できる。未対応であることを一文残すとよい。 | plan 4-1 | 修正推奨 | +| Info | BP | tmux の `-e` は 3.2 以上が必要。この端末は 3.4 で、tmux 3.2 未満は台帳 F10 として残す扱い。判断は妥当。 | plan 4-6 / §9 | 対応任意 | +| Info | P5 | M-02 は本物の GitHub に流して控えの鍵を失効させる。他セッションへの影響は、イシューの Danger Ops と M-02 の備考に明記済み。 | I225 Danger Ops / M-02 | 対応不要 | + +観点ごとの確認: +- **P1(要件適合性)**: 問題なし。`revoke.sh`(`--stdin` を含む)は F13 の具体化で、イシューの実装対象にも載っている。受け入れ条件 2 の範囲の絞り込みは §9 に理由つきで記録されている。 +- **ベストプラクティス**: 問題なし。 + - 入口の判定は argv で行い、シェルの文字列を読まない。見張りは変えない。 + - 検査対象は決まりの置き場に絞られている。 + - 決定論テストは、壊れても気づきにくい所(G7〜G10・T1)に限られている。 + - 検証コマンドは単体で、`git checkout` による取り消しもない。 +- **セキュリティ**: + - 鍵の値は引数・画面に出さない(環境変数と標準入力で渡す)。 + - `error` の欄は出さない。 + - 失効が確認できなければ控えを消さず exit 1 にする。 + - `--strict-mcp-config` で最小権限にする。 + - 新規パッケージの追加はない。 +- **P4(テスト妥当性)**: G7〜G10 に FG があり、偽物の `gh` で鍵の漏れと終了コードの 0・1・2 を確かめている。 +- **P3(データ整合性)**: 対象外(DB・API なし)。 +- **P5(運用性)**: 切り戻しは `install.sh` のバックアップから戻せる。 +- **P8(コスト・保守負荷)**: 入口は単純な argv の規則だけにしてあり、精密なガードを作り込んでいない。GitHub 側の最後の守りは変わらない。 +- **P9(プライバシー)**: 対象外。 +- **Claude Code ベストプラクティス**: `.claude/skills/` などの指示ファイルの変更はないので対象外。 + +## 高リスク判定 +判定: No +該当条件: なし(変更は `scripts/claude/`・`docs/runbooks/` のハーネスだけ。`.github/*`・`.pre-commit-config.yaml`・`.secrets.baseline`・`pretooluse_guard.py`・`.claude/settings*.json` は触らない) +推奨: 実装(implement 段階)へ。 + +VERDICT: OK diff --git a/docs/reviews/closed/I225_review.md b/docs/reviews/closed/I225_review.md new file mode 100644 index 00000000..f705ee8f --- /dev/null +++ b/docs/reviews/closed/I225_review.md @@ -0,0 +1,92 @@ +# I225 レビュー: GitHub の鍵が画面に出る・漏れたときの手順と、テスト用のセッションの抜け道を塞ぐ + +## 変更概要 +- `gh` の入口が、`--json` なしの `auth status` を鍵の出ない表示に置き換える。手順書などへの書き戻しは CI(G10)で止める +- 鍵が出たときの失効をスクリプト `revoke.sh` にした(控えの 2 つ・`--stdin` で控えに無い鍵)。失効は HTTP 401 で確かめる +- テスト用のセッションの起動にすべて `--strict-mcp-config`、tmux の中の `claude` に入口のある `PATH` を渡す + +## 変更点 +- `scripts/claude/gh-app/gh`(auth status の置き換え)・`scripts/claude/gh-app/revoke.sh`(新規) +- `scripts/claude/` の起動の行: `code-review.sh`・`review-process-lib.sh`・`issue-review.sh`・`plan-issue-review.sh`・`fix-review-lib.sh`・`test-session.sh`・`check-permission-prompt.sh`・`tui-capture.sh`(`adversarial-review.sh` の重複した `--strict-mcp-config` を消した) +- テスト: `test_i223_gh_app.sh`(G7〜G10・g6/g7 の共通化)・`test_tui_capture.sh`(T1・FG-T1b・FG-T8) +- 手順書: `github-app-setup.md`(1.・5.・漏れたときの手順)・`harness-audit.md`(古い鍵の点検コマンド) +- 台帳: `backlog/I223.md` の F11・F12、`backlog/I195.md` の F5 を消した + +## 影響範囲 +- Backend/Frontend/DB: なし +- Config: `scripts/claude/gh-app/`・`scripts/claude/` の Claude の起動の行・`scripts/claude/tui-capture.sh`・`scripts/claude/tests/`・`docs/runbooks/github-app-setup.md`・`docs/runbooks/harness-audit.md` + +## テスト結果 +- 自動: review-gate(fb4b563a)で CI 全緑・決定論ゲート 6 本とも exit 0(`test_i223_gh_app.sh` 33/33 ほか)。TC-03 OK +- 手動(test-gate・22ae10d4): M-01〜M-04 すべて OK(すべて Claude。Human の項目なし) + +## 計画との差分 +- なし + +## 振り返り + +### かんたんなまとめ +- 良かった点: + - 鍵を出す書き方を、打った時(入口の置き換え)と書いた時(CI の検査)の 2 か所で、機械的に止められるようになった(理由: 決まりを読んで守るだけの形だと、I223 のように手順書へ書き戻されて再発するため) + - 計画レビューが「控えに無い漏れた鍵が失効しない」穴を実装の前に見つけ、`--stdin` を足してから作れた(理由: 実装後に見つかると、作り直しとレビューのやり直しが要る) + - テストを先に書き、手順書に残っていた鍵を出す書き方をその検査で見つけてから直せた +- 今回の問題への直接の対処: + - 自動テスト文書の見出しが決まりの形と 1 語違い、レビューが「テストの宣言なし」と判定して 1 回差し戻された。前のイシューの文書を見て書き、ひな形から作らなかったため。→ 見出しを直してレビューを通し直した(レビューの機械的な判定がその場で捕まえ、直しは 1 行で済んだので、新しい手順は足さない) +- 同じ問題を次回から防ぐ改善: + - レビューで止めなかった指摘のうち、セキュリティに関わる 3 件(失効の表示の取り違え・控えを消すときの競合・検査の取りこぼし)を、後続のイシュー I226(#417)にまとめて起票した。止める条件には当たらないが、鍵の扱いの穴なので次のハーネスの作業で最優先にする + +### 詳細 +- 良かった点: 上のとおり +- 是正処置(Corrective) + +| # | カテゴリ | 根本原因 | 対処内容 | +|---|---------|---------|---------| +| C1 | 予期しない問題 | 自動テスト文書をひな形(`docs/tests/templates/auto_test_template.md`)からでなく前のイシューの文書から作り、決定論ゲートの見出しが違った | fix-loop で見出しを直した(診断メモ 20261106_0100・review-gate 2 回目 OK) | + +- 予防処置(Preventive) + +| # | カテゴリ | 根本原因 | 予防策 | 対象ファイル | +|---|---------|---------|--------|------------| +| P1 | セキュリティ(台帳の残り) | 鍵を扱うスクリプトの「確かめた」の表示が、前の状態(もともと有効だったか)を確かめていない・控えの読み書きに排他が無い・静的な検査の対象が 1 つのコマンドの書き方に限られる | 後続イシュー I226(#417)で直す(次のハーネスの作業で最優先) | `scripts/claude/gh-app/revoke.sh`・`scripts/claude/tests/test_i223_gh_app.sh` | + +- 5 Whys(C1): + - Why 1: なぜ review-gate が HIGH にしたか → auto_test の見出しがスクリプトの探す文字列と違い、ゲートが「宣言なし」になった + - Why 2: なぜ見出しが違ったか → ひな形ではなく前のイシュー(I223)の文書の形を見て書いた(I223 の文書はゲートの節の見出しが違う形だった) + - Why 3: なぜ計画の段階で気づかなかったか → plan-issue の文書品質ゲートは、見出しがスクリプトの読む形かを機械的には見ていない。見るのは review-gate の omission-lint が最初 + - 根本原因: 決まった見出しを機械が読む文書を、ひな形以外から作れてしまう。ただし review-gate の決定論の判定がその場で捕まえ、手戻りは 1 行の直しと 1 回のレビューだけだった +- 2.7 の判定: + - C1 の予防(plan-issue で omission-lint を先に流す・plan-reviewer の観点に足す): 見送り — 既存の決定論の判定(review-gate の omission-lint)がその場で捕まえ、手戻りは小さい。同じ穴を 2 つの工程で塞がない + - 計画レビューの Blocker(控えに無い鍵): 見送り — 計画レビューがその場で捕まえ、実装の手戻りは無い + - 総点検の点検コマンドの `env -u` が見張りに止められた件: 見送り — 計画の段階の試しで見つけ、別の書き方に直して手戻りは無い + - /simplify の直す深さの指摘(Claude を起こす行を 1 つの入口にまとめる): 見送り — 実害(付け忘れによる漏れ)は出ておらず、G6・G7 が付け忘れを CI で止める。8 か所の起動の形と既存のテストを作り直す大きさに見合わない。I226 のスコープの「含まない」に判断を残した + - 台帳の F7(ゲートの証跡が無い)が 2 周目でも STATUS=open のまま: 見送り — 2 周目の記録の先頭に 6 本の exit 0 の証跡があり、指摘の中身は解消済み。台帳の閉じ方の件は再現が 1 回だけで、`code-review.sh` を触る他のイシューで再発したら扱う + +## ロールバック +- PR を revert し、`bash scripts/claude/gh-app/install.sh` を流し直す(前の版は `~/.cache/github-app/backup/`) + +## 自律判断の記録 +- [plan] 失効の手順をスクリプト `scripts/claude/gh-app/revoke.sh` にし、控えの 2 つの鍵を直接失効させて確かめる — 手順書の散文では取り違え(F13)が起きる。1 コマンドなら偽物の `gh` でテストできる +- [plan] 静的な検査(G10)の対象を、決まりの置き場(手順書・スキル・エージェント・スクリプト・テンプレート)に絞り、個々のイシューの記録を外した。イシューの受け入れ条件 2 も同じ範囲に直した — 計画・テスト文書は置き換えを確かめるために `--json` なしの書き方を名指しする必要があり、打っても入口が置き換える。I223 の事故の元の手順書は対象に入る +- [plan] 総点検の点検コマンドは `env -u GH_APP_READONLY` でなく `GH_APP_READONLY=`(空)にした — 計画の段階で流したら、ワークツリーの見張りが `env` の形を止めた。入口は空を「指定なし」と読む +- [plan] GitHub への書き込みをまとめた: ブランチの push・Draft PR の作成は、計画の記録を全部コミットしてから 1 回で行う(I224 の当面の運用。段階ごとに push しない) + +- [plan-review] 1 回目の Blocker(控えに無い漏れた鍵が失効しない)は revoke.sh --stdin を足して直し、2 回目で OK。残りの Warning 3 件は計画変更記録 2 で直し、再レビューはしない — 方式・範囲・受け入れ条件を変えない直しのため +- [implement] 失効の確かめを「読みに行って失敗」でなく「HTTP 401 が出る」にした(計画変更記録 3) — 通信の失敗を「無効」と取り違えて控えを消さないため。本物の `gh` で 401 の文言を確かめた +- [implement] `--strict-mcp-config` を置く位置を、既存のテスト(S1・FG-S1)が前提にする形に合わせた(`"$@"` の前・`--permission-prompts none)` の前・複数行の呼び出しは 1 行目) — テストを直すより起動の行を合わせるほうが変更が小さく、既存の守りをそのまま使える +- [implement] /simplify の指摘のうち、G6・G7 の重複(共通の関数にした)と G9 の使わない代入を直した。見送り: 起動を 1 つの入口にまとめる案(直す深さの指摘。8 か所の起動の形と既存テストを作り直すことになり、計画の方式を変えるので retro で扱う)・控えの名前と形を共通のファイルにまとめる案(`revoke.sh` の名前の一覧は FG が守り、控えの形は 1 か所でしか書かない)・G10 を狭める案(計画 §9 で決めた範囲)・失効を並行にする案(効果が 1 秒未満) +- [review-gate] 1 回目は FINAL_VERDICT: HIGH。止める指摘は 0 件で、HIGH は自動テスト文書の決定論ゲートの見出し違い(宣言なし・omission-lint)によるもの。決まりどおり fix-loop に回した — 判定はスクリプトの決定論の出力に従う +- [fix-loop] 見出しを `## 決定論ゲート(自動実走)` に直した(診断メモ 20261106_0100 の案 A・診断レビュー PASS) — テンプレートどおりの 1 行の直しで、次の review-gate がゲートを実走して証跡を残す。台帳の F1〜F7 は止める指摘ではないので直さない(retro) +- [retro] 台帳の残りの振り分け: F1・F5・F6(SECURITY=yes)→ 後続イシュー I226(#417・次のハーネスの作業で最優先)/F2・F3・F4(Low)→ `docs/reviews/backlog/I225.md`/F7 → 振り分けない(fix-loop の見出しの直しで解消済み。2 周目の記録にゲート 6 本の exit 0 の証跡がある。台帳の STATUS だけが open のまま) — 台帳の行の欄の値で `code-review.sh --route` が振り分けた結果に従い、F7 だけ中身が解消済みのため除いた +- [retro] 改善案の振り分け: 是正 C1 は fix-loop で直し済み。予防は P1 を I226 で起票。見送り 5 件(自動テスト文書の見出しの予防・計画レビューの Blocker・`env -u` の件・起動を 1 つの入口にまとめる案・台帳の F7 の閉じ方)は「## 振り返り」の 2.7 の判定に理由を書いた — どれも既存の工程がその場で捕まえ手戻りが小さいか、実害が出ていないため +- [retro] I226 のイシューファイルは未コミットのまま app-1 に残した(plan-issue の初コミットの決まり) +- [close] 最初の push と PR の説明の更新が GitHub に拒まれた(`fatal error in commit_refs`・`EOF`。読み取りは通り、githubstatus は Operational)。書き込みを重ねず 2 回で止めてユーザーに伝え、時間を置いて 1 回だけやり直して通った — 書き込みの集中で名義が止められるおそれ(I224)を避けるため + +STAGE_DONE: plan NEXT: plan-review +STAGE_DONE: plan-review NEXT: implement +STAGE_DONE: implement NEXT: review-gate +STAGE_DONE: review-gate NEXT: fix-loop +STAGE_DONE: fix-loop NEXT: review-gate +STAGE_DONE: review-gate NEXT: test-gate +STAGE_DONE: test-gate NEXT: retro +STAGE_DONE: retro NEXT: close +STAGE_DONE: close NEXT: merge diff --git a/docs/runbooks/github-app-setup.md b/docs/runbooks/github-app-setup.md index 5b22caed..ba048b34 100644 --- a/docs/runbooks/github-app-setup.md +++ b/docs/runbooks/github-app-setup.md @@ -8,7 +8,7 @@ Claude は GitHub App `osushi0404-claude`(GitHub 上の名前は `osushi0404-c - App の鍵は 1 時間で切れる。`gh` の入口(`scripts/claude/gh-app/gh` を `~/.local/bin/gh` として入れる)が、`gh` を呼ぶたびに `gh-app-token` から鍵を受け取り、`GH_TOKEN` で本物の `gh`(`/usr/bin/gh`)に渡す。受け取った鍵は `~/.cache/github-app/` に控え、残り 10 分を切ったら取り直す - `git push` なども同じ入口を通る(git の credential helper を `!~/.local/bin/gh auth git-credential` にする) -- テスト用のセッション(`claude -p`)には読み取りだけの鍵を渡す(`GH_APP_READONLY=1`)。`gh issue`・`gh pr`・`gh api`・`git push` のどの書き方でも、書き込みは GitHub が拒否する(HTTP 403)。手で起こすときの入口は `scripts/claude/test-session.sh`。`scripts/claude/` のスクリプトが Claude を起こす行(`claude -p`・tmux の中の `claude`)にはすべて `GH_APP_READONLY=1` を付ける(レビュー役も同じ。付け忘れは `scripts/claude/tests/test_i223_gh_app.sh` の G6 が CI で止める)。決まりの正は `docs/runbooks/plan-writing-rules.md` +- テスト用のセッション(`claude -p`)には読み取りだけの鍵を渡す(`GH_APP_READONLY=1`)。`gh issue`・`gh pr`・`gh api`・`git push` のどの書き方でも、書き込みは GitHub が拒否する(HTTP 403)。手で起こすときの入口は `scripts/claude/test-session.sh`。`scripts/claude/` のスクリプトが Claude を起こす行(`claude -p`・tmux の中の `claude`)にはすべて `GH_APP_READONLY=1` と `--strict-mcp-config`(MCP は `--mcp-config` で渡したものだけ。手元の設定の MCP が読み取りだけの鍵を迂回しないように・I225)を付ける(レビュー役も同じ)。tmux の中の `claude` には、入口のある `PATH`(`~/.local/bin` が先頭)も渡す(先に立っていた tmux サーバーの `PATH` で入口を通らないことがないように・I225)。付け忘れは `scripts/claude/tests/test_i223_gh_app.sh` の G6・G7 が CI で止める(見るのは `scripts/claude/` 直下の `.sh` の、`claude -p` の呼び出しと、tmux で `claude` を起こす行。それ以外の形の起動は見ない)。決まりの正は `docs/runbooks/plan-writing-rules.md` - GitHub の MCP は使わない(I223 で外した。`docs/runbooks/mcp-usage.md`) ## 2. App の設定(今の値) @@ -44,9 +44,23 @@ bash scripts/claude/gh-app/install.sh --check ## 5. 確かめる - 名義: `gh auth status --json hosts --jq '.hosts["github.com"][] | select(.active) | .login + " " + .tokenSource + " " + .state'` が `osushi0404-claude[bot] GH_TOKEN success` を出す - - `--json` を付けない `gh auth status` は使わない(App の鍵は長く、`gh` は最後の部分しか伏せないので、鍵の大部分が画面に出る・I223 の手動テストで発生)。出してしまったら `gh api -X DELETE installation/token` で失効させ、`~/.cache/github-app/token` を消して取り直す + - 名義の確かめの `auth status` は、上のように `--json` で要る欄だけを取り出す。`--json` を付けないと、App の長い鍵の大部分が画面に出る(`gh` は最後の部分しか伏せない・I223 の手動テストで発生)。入口(`~/.local/bin/gh`)は、`--json` も `-t`/`--show-token` も無い `auth status` を、鍵の欄の無い表示(` <鍵の出どころ> <状態>`)に置き換える(I225)。本物の `gh`(`/usr/bin/gh`)を直接呼ぶと置き換わらない。手順書・スキル・スクリプトに `--json` なし(または `-t`/`--show-token` 付き)の書き方があると、`test_i223_gh_app.sh` の G10 が CI で止める - `git push` が通る +### 鍵が画面・記録に出てしまったら +1. 控えの 2 つの鍵(`~/.cache/github-app/` の `token`・`token-readonly`)を失効させる: + ```bash + bash scripts/claude/gh-app/revoke.sh + ``` + 各控えについて「失効を確認して控えを消しました」か「控えがありません」が出て exit 0 になれば終わり。次の `gh` で新しい鍵を取り直す(そのとき App の鍵を環境変数で持っている別のセッションは、GitHub の操作が 401 になる) +2. 出た鍵が控えの鍵と違うとき(控えが取り直された後に気づいた・別のセッションで出た)は、その鍵を標準入力で渡して失効させる(引数には書かない。画面にも出ない): + ```bash + bash scripts/claude/gh-app/revoke.sh --stdin + ``` + 出た鍵を貼って Enter を押す。「渡された鍵: 失効を確認しました」で終わり +3. 「無効を確かめられませんでした」(exit 1)のときは、GitHub に届くかを確かめて流し直す(失効が確かめられるまで控えは消さない) +4. 秘密の鍵ファイル(`app.pem`)が漏れたときは「8.」で鍵を作り直す + ## 6. テスト用のセッション - 手で起こすときは `bash scripts/claude/test-session.sh `。GitHub は読めるが、書き込みは GitHub が拒否する diff --git a/docs/runbooks/harness-audit.md b/docs/runbooks/harness-audit.md index 0fa76950..79c12f25 100644 --- a/docs/runbooks/harness-audit.md +++ b/docs/runbooks/harness-audit.md @@ -26,5 +26,15 @@ Claude が GitHub を操作する名義と鍵は、リポジトリの外にも | テスト用のセッションが読み取りだけの鍵で起きるか | `scripts/claude/test-session.sh`・`scripts/claude/tests/test_i223_gh_app.sh` | | リポジトリの協力者の一覧と権限 | `gh api repos/osushi0404-dev/study-app-multitenant/collaborators` | | ブランチの守りの設定 | `docs/runbooks/branch-protection-setup.md` | -| 手元に残る古い鍵(個人の鍵〔PAT〕・本物の `gh` に保存されたログイン) | `~/.bashrc` などの `export`(値は出さず `grep -c` で数える)・`GH_APP_DISABLE=1 gh auth status --json hosts --jq '.hosts["github.com"][] \| .login + " " + .state'`(`--json` なしは鍵を画面に出しうるので使わない) | +| 手元に残る古い鍵(個人の鍵〔PAT〕・本物の `gh` に保存されたログイン) | 表の下の「古い鍵の点検コマンド」 | | 自動化の量(書き込みの回数・頻度)が、名義の使い方と GitHub の利用の制限に合っているか | 直近の作業の push・コメント・イシューの書き換えの回数 | + +### 古い鍵の点検コマンド + +```bash +grep -c -E '^[[:space:]]*export[[:space:]]+(GITHUB_PERSONAL_ACCESS_TOKEN|GH_TOKEN|GITHUB_TOKEN)=' ~/.bashrc ~/.profile +GH_APP_READONLY= GH_APP_DISABLE=1 gh auth status --json hosts --jq '.hosts["github.com"][] | .login + " " + .tokenSource + " " + .state' +``` + +- 1 行目: 古い鍵を `export` する行を、値を出さずにファイルごとに数える(0 でないファイルの行を消す。`docs/runbooks/github-app-setup.md`「9.」)。ファイルが無いときのエラーは無視してよい +- 2 行目: 入口から App の鍵を渡さずに(`GH_APP_DISABLE=1`。読み取りだけの指定があると無視されるので空にする)、本物の `gh` が使う鍵を、名義・鍵の出どころ(環境変数か、保存されたログインのファイルか)・状態で出す。`--json` で要る欄だけを取り出すので鍵は出ない。`env -u` の形はワークツリーのセッションの見張りが止めるので使わない diff --git a/docs/tests/closed/I225_auto_test.md b/docs/tests/closed/I225_auto_test.md new file mode 100644 index 00000000..0e2f9b69 --- /dev/null +++ b/docs/tests/closed/I225_auto_test.md @@ -0,0 +1,39 @@ +# I225 自動テスト: GitHub の鍵が画面に出る・漏れたときの手順と、テスト用のセッションの抜け道を塞ぐ + +実行コマンド(例): 下の「決定論ゲート」(アプリのコード変更なし。pytest・Jest・E2E は CI が流す) + +結果: +- backend: 対象外 +- frontend: 対象外 + +## テストケース +| TC | 内容 | 合否の判定 | 結果 | +|---|---|---|---| +| TC-01 | 起動の行を触るファイルを見ている既存のテスト 5 本(`test_tui_capture.sh`・`test_check_permission_prompt.sh`・`test_i197_adversarial_process.sh`・`test_fix_review.sh`・`test_i193_official_flow.sh`)が合格する。`test_tui_capture.sh` の T1 は、起動の行に `-e PATH=$HOME/.local/bin:<呼び出し元の PATH>` と `claude --permission-mode manual --strict-mcp-config` があることを照合する形に直し、FG-T1b(写しから `-e PATH=…` を消すと T1 が不合格)を足す | 各スクリプトが exit 0 | 実装前のベースライン(2026-11-06・develop 由来の feature/I225): 5 本とも exit 0(37/26/113/66/174 件合格)。T1 の照合の直し・FG-T1b は実装前のため未実行 | +| TC-02 | `test_i223_gh_app.sh` の G1〜G10(`curl`・`openssl`・本物の `gh`・`claude` は一時フォルダの偽物。秘密の鍵ファイルと本物の GitHub は使わない)。既存 G1〜G6 はそのまま。新しく: **G7** `scripts/claude/*.sh` の Claude を起こす論理行(G6 と同じ読み方。`export GH_APP_READONLY=1` のファイルも除外しない)がすべて `--strict-mcp-config` を持つ **G8** 入口 `gh` で `auth status`(引数なし・`-a`・`-h github.com`)が、偽物の本物の `gh` の鍵の行(`Token: SECRET-…`)を出さず、` `(使用中に ` (active)`)だけを出し、偽物に `--json hosts` と元の引数が渡る。`state` がすべて `success` なら exit 0・1 つでも違えば exit 1。`auth status -t`・`--show-token`・`--json hosts` は置き換えず偽物にそのまま渡る。`pr list` などほかのコマンドもそのまま **G9** `revoke.sh` が、控えの `token`・`token-readonly` の両方の鍵で `api -X DELETE installation/token` を呼び(偽物の本物の `gh` に `GH_TOKEN` で渡る)、無効を確かめて 2 つの控えを消し、exit 0。標準出力・標準エラーに鍵の値が出ない。控えが 1 つも無ければ exit 0。DELETE の後も読める(偽物が失効を受け付けない)鍵があれば、その控えを消さず exit 1。`--stdin` に標準入力で鍵(`leaked-x`)を渡すと、その鍵で DELETE を呼び、控えは触らず exit 0・出力に `leaked-x` が出ない。`--stdin` で空の入力・知らない引数は exit 2 **G10** `CLAUDE.md`・`docs/runbooks/`・`.claude/skills/`・`.claude/agents/`・`scripts/claude/`(`tests/` を除く)・`docs/*/templates/` に、`--json` なし、または `-t`/`--show-token` 付きの `gh auth status` が無い | スクリプトが exit 0。各新しい G に FG(写しを壊すと該当の G が不合格になることをテストの中で確かめる): G7 `code-review.sh`・`tui-capture.sh` の写しから `--strict-mcp-config` を消す/G8 入口の写しから置き換えの分岐を消す・`state` の判定を消す(exit 0 になる)/G9 写しの控えの名前の一覧から `token-readonly` を消す・失効の確かめを消す(読める鍵でも控えを消す)・`--stdin` で読んだ値を使わず控えの鍵を使う/G10 一時フォルダの `docs/runbooks/x.md` に `` `gh auth status` `` と `` `gh auth status --json hosts -t` `` を書くとそれぞれ不合格・`--json hosts --jq …` の形だけなら合格 | 実装前のため G7〜G10 は未実行(G1〜G6 は 2026-11-06 のベースラインで 18/18) | +| TC-03 | `docs/runbooks/harness-audit.md` の古い鍵の点検コマンド(コードブロックの 2 行目)をそのまま流す | 同じコマンドの後ろに `2>&1 >/dev/null` を付けて流し、exit 0 で、出力(=標準エラー。jq の構文エラーなど)が空(`[ -z "$(<コマンド> 2>&1 >/dev/null)" ]` が exit 0)。出力の中身はこの端末の保存されたログインで変わるので合否に使わない | 計画の段階で同じコマンドを流して OK(2026-11-06): `osushi0404-bot /home/tacic/.config/gh/hosts.yml error`・exit 0。手順書への反映は実装前のため未実行 | + +新しい決定論テストは G7〜G10 と T1 の照合の直しだけにする(plan_I225.md §9: どれも、壊れても気づきにくく、壊れると鍵が画面に出る・読み取りだけの鍵を迂回する所)。手順書の文言(失効の手順の説明・総点検の一覧の並び)はレビューで見る。 + +## 決定論ゲート(自動実走) +```bash +bash scripts/claude/tests/test_i223_gh_app.sh +bash scripts/claude/tests/test_tui_capture.sh +bash scripts/claude/tests/test_check_permission_prompt.sh +bash scripts/claude/tests/test_i197_adversarial_process.sh +bash scripts/claude/tests/test_fix_review.sh +bash scripts/claude/tests/test_i193_official_flow.sh +``` + +## 再発防止記録(fix-loop・2026-11-06・review-gate 20261106_0045 の HIGH) +- なぜ失敗したか: 見出しを `## 決定論ゲート` と書き、テンプレートの `## 決定論ゲート(自動実走)` と違った。`code-review.sh` は見出しを完全な文字列で探すので、ゲートを宣言していないと読み、宣言の外のテストのコマンドを omission-lint が HIGH にした(診断メモ `docs/reviews/I225_fix_diagnosis_20261106_0100.md`・診断レビュー PASS) +- 何を変えたか: 見出しをテンプレートどおりに直した(1 行) +- セキュリティ上の考慮点: なし(記録の書式) +- 次回どう防ぐか: auto_test は前のイシューの文書でなく `docs/tests/templates/auto_test_template.md` から作る +- 確かめた結果: `grep -c "^## 決定論ゲート(自動実走)"` が 1。6 本のゲートの実走は次の review-gate が行う(implement の手元ではすべて exit 0) + +## 実行記録 +- 2026-11-06・review-gate(CI・fb4b563a): CI 全緑。決定論ゲート 6 本とも exit 0(`docs/reviews/I225_code_review_20261106_0057.md`) +- 2026-11-06・test-gate(22ae10d4): test-gate に回された重いテストは無い(pytest・Jest・E2E は CI が正で流し直さない)。TC-03: 手順書のコードブロックの 2 行目をそのまま流し `osushi0404-bot /home/tacic/.config/gh/hosts.yml error`・exit 0・標準エラーなし → OK +- 2026-11-06・plan(実装前): TC-01 の 5 本と `test_i223_gh_app.sh`(G1〜G6)すべて exit 0。TC-03 のコマンドは計画の段階で OK +- 2026-11-06・implement(手元): テストを先に書いて Red を確認(`test_i223_gh_app.sh` 22/33・G7〜G10 と新しい FG が不合格。G10 は手順書 `github-app-setup.md:47` の書き方を見つけた)→ 実装後 33/33。TC-01: `test_tui_capture.sh` 39/39(FG-T1b を含む)・`test_check_permission_prompt.sh` 26/26・`test_i197_adversarial_process.sh` 113/113・`test_fix_review.sh` 66/66・`test_i193_official_flow.sh` 174/174。途中で S1・FG-S1・FG-T8 の照合が起動の行の変更でずれたので、`--strict-mcp-config` を置く位置を既存の照合に合わせた(テストは直していない。FG-T8 の置き換え前の文字列だけ新しい起動の行に合わせた) diff --git a/docs/tests/closed/I225_manual_test.md b/docs/tests/closed/I225_manual_test.md new file mode 100644 index 00000000..c1e6b627 --- /dev/null +++ b/docs/tests/closed/I225_manual_test.md @@ -0,0 +1,16 @@ + + +# I225 手動テスト: GitHub の鍵が画面に出る・漏れたときの手順と、テスト用のセッションの抜け道を塞ぐ + +| No | 手順 | 期待結果 | 実施者 | 実施環境 | 実結果 | 備考 | +|---:|------|----------|--------|----------|--------|------| +| M-01 | この端末に入れ直す: `bash scripts/claude/gh-app/install.sh` → `bash scripts/claude/gh-app/install.sh --check` → `--json` なしの `auth status` の出力を画面に出さずにスクラッチパッドのファイルに書く: `gh auth status > <スクラッチパッド>/m01.txt 2>&1` → 鍵の形が無いことを確かめる: `! grep -q ghs_ <スクラッチパッド>/m01.txt` → `gh auth status -a` → `rm <スクラッチパッド>/m01.txt`(どれも 1 コマンドずつ流す) | `install.sh`・`--check` が exit 0。`! grep -q ghs_` が exit 0(鍵の形が無い)。`gh auth status -a` が `github.com osushi0404-claude[bot] GH_TOKEN success (active)` の 1 行と、標準エラーの置き換えの案内 1 行を出し exit 0 | Claude | 手元のシェル | OK(2026-11-06・test-gate・22ae10d4): `install.sh` で入れ直し(前の版は backup に写した)・`--check` は「そろっています」。`--json` なしの出力をファイルに書き `! grep -q ghs_` が rc=0(鍵の形なし)。中身は案内 1 行と `github.com osushi0404-claude[bot] GH_TOKEN success (active)`・`github.com osushi0404-bot /home/tacic/.config/gh/hosts.yml error` の 2 行(exit 1)。`gh auth status -a` は案内 1 行と `github.com osushi0404-claude[bot] GH_TOKEN success (active)`(exit 0)。ファイルは消した | 受け入れ条件 1。本物の `gh` と App の鍵で、入口の置き換えが効くこと。`-a` なしでは本物の `gh` に保存された停止中の bot のログイン(`error`)も並び exit 1 になる(元の `gh` と同じ) | +| M-02 | 鍵が出たときの手順を本物の GitHub で流す: `bash scripts/claude/gh-app/revoke.sh` → `gh api installation/repositories --jq .total_count` | `revoke.sh` が `token`・`token-readonly` の各行に「失効を確認して控えを消しました」または「控えがありません」を出し exit 0。鍵の値は出ない。続く `gh api` が `1` を出す(控えが消えたので新しい鍵を取り直して読める) | Claude | 手元のシェル | OK(2026-11-06・test-gate・22ae10d4): `token: 失効を確認して控えを消しました`・`token-readonly: 失効を確認して控えを消しました`(exit 0・鍵の値は出ない)。続く `gh api installation/repositories --jq .total_count` が `1` | 受け入れ条件 3。ほかのセッションが App の鍵で動いていない時に流す(失効した鍵を環境変数で持っているプロセスは、その後の GitHub の操作が 401 になる) | +| M-03 | 対話画面の MCP: `bash scripts/claude/tui-capture.sh --wait 3 '/mcp' '{Enter}'` | 画面の MCP の一覧に `playwright`・`github` が出ない(MCP サーバーが 0 件の表示。実際に出た文言を実結果に写す) | Claude | 対話画面の判定 | OK(2026-11-06・test-gate・22ae10d4): `No MCP servers configured. Run claude doctor if this is unexpected …` と出た(playwright・github は出ない) | 受け入れ条件 5(F7)。`--strict-mcp-config` が tmux の中の `claude` に効くこと。`{Enter}` で `/mcp` を開くだけでモデルは呼ばない | +| M-04 | MCP が要る承認の判定が動く: `bash scripts/claude/check-permission-prompt.sh mcp mcp__playwright__browser_tabs '{"action":"list"}'` | 終了コードが 0 または 10(判定できた)。2(判定できない)にならない | Claude | claude -p の判定 | OK(2026-11-06・test-gate・22ae10d4): exit 0「承認なしで実行された: mcp__playwright__browser_tabs」(`.mcp.json` の Playwright を読めた) | 受け入れ条件 5(F7)の副作用の確かめ。`--strict-mcp-config` と `--mcp-config .mcp.json` を併せても、ワークツリーの `.mcp.json` の Playwright を読める。2 が出たらもう 1 回流す | + +結論: OK diff --git a/scripts/claude/adversarial-review.sh b/scripts/claude/adversarial-review.sh index c3928d0b..b15c7284 100644 --- a/scripts/claude/adversarial-review.sh +++ b/scripts/claude/adversarial-review.sh @@ -178,7 +178,7 @@ run_experiment_agent() { --model haiku --tools "Bash" \ --permission-mode auto --permission-prompts none \ --setting-sources user --settings .claude/settings.json \ - --strict-mcp-config --no-session-persistence \ + --no-session-persistence \ --max-budget-usd "${ADV_EXEC_BUDGET}" } diff --git a/scripts/claude/check-permission-prompt.sh b/scripts/claude/check-permission-prompt.sh index 3915d376..07883ade 100644 --- a/scripts/claude/check-permission-prompt.sh +++ b/scripts/claude/check-permission-prompt.sh @@ -66,10 +66,12 @@ else cd "$ROOT" || exit 2 LOG="$(mktemp)" trap 'rm -f "$LOG"' EXIT + # MCP は --mcp-config で渡したものだけ(--strict-mcp-config・I225)。mcp の判定ではこのワークツリーの .mcp.json だけを読み、 # bash の判定では MCP サーバーを起動しない(判定に要らず、起動の分だけ遅くなる) - if [ "$KIND" = "mcp" ]; then MCP_ARGS=(--mcp-config .mcp.json); else MCP_ARGS=(--strict-mcp-config); fi + MCP_ARGS=() + if [ "$KIND" = "mcp" ]; then MCP_ARGS=(--mcp-config .mcp.json); fi # I223: GitHub へは読み取りだけの App の鍵(承認なしで通る判定でも、GitHub への書き込みは GitHub が拒否する) - if ! GH_APP_READONLY=1 "${CLAUDE_BIN:-claude}" -p "$PROMPT" \ + if ! GH_APP_READONLY=1 "${CLAUDE_BIN:-claude}" -p "$PROMPT" --strict-mcp-config \ --model haiku \ --permission-mode manual \ --permission-prompts none \ diff --git a/scripts/claude/code-review.sh b/scripts/claude/code-review.sh index 32b7ffaa..4e3f4da6 100644 --- a/scripts/claude/code-review.sh +++ b/scripts/claude/code-review.sh @@ -151,7 +151,7 @@ run_official_review() { # $1=プロンプト(例: /code-review medium origin local prompt="$1" out rc=0 # I197: 承認が要る操作は待たずに拒否する(--permission-prompts none。黙って止まる経路を作らない) # I223: GitHub へは読み取りだけの App の鍵(GH_APP_READONLY=1。書き込みは GitHub が拒否する) - out=$(GH_APP_READONLY=1 timeout "${OFFICIAL_REVIEW_TIMEOUT:-900}" claude -p --permission-mode plan --permission-prompts none "$prompt" 2>&1 < /dev/null) || rc=$? + out=$(GH_APP_READONLY=1 timeout "${OFFICIAL_REVIEW_TIMEOUT:-900}" claude -p --permission-mode plan --permission-prompts none --strict-mcp-config "$prompt" 2>&1 < /dev/null) || rc=$? if [ "$rc" -ne 0 ]; then printf '(実行できず: exit=%s。124 は時間切れ)\n' "$rc" return 0 diff --git a/scripts/claude/fix-review-lib.sh b/scripts/claude/fix-review-lib.sh index 6854c15d..f1d85b8e 100644 --- a/scripts/claude/fix-review-lib.sh +++ b/scripts/claude/fix-review-lib.sh @@ -77,8 +77,8 @@ run_fix_review() { # 3) タイムアウト付き起動。失敗・空・タイムアウト(exit124) は全て非ブロック SKIP+未実施記録 # I193: 公式のサブエージェント定義(.claude/agents/.md)を --agent で呼ぶ。名前はファイル名から取る - # I197: 承認が要る操作は待たずに拒否する(--permission-prompts none)。I223: GitHub へは読み取りだけの鍵 - if ! review=$(printf '%s' "$context" | GH_APP_READONLY=1 timeout "${FIX_REVIEW_TIMEOUT:-300}" claude -p \ + # I197: 承認が要る操作は待たずに拒否する(--permission-prompts none)。I223: GitHub へは読み取りだけの鍵。I225: MCP は読まない + if ! review=$(printf '%s' "$context" | GH_APP_READONLY=1 timeout "${FIX_REVIEW_TIMEOUT:-300}" claude -p --strict-mcp-config \ --agent "$agent_name" --tools "Read,Grep,Glob" --permission-prompts none) || [ -z "$review" ]; then printf '# %s fix-%s レビュー(未実施)\n\nclaude -p の失敗/空/タイムアウトのためレビュー未実施(非ブロック skip)。\n\nFIX_GATE: SKIP\n' \ "$issue" "$kind" > "$review_file" diff --git a/scripts/claude/gh-app/gh b/scripts/claude/gh-app/gh index d209e502..5a073744 100644 --- a/scripts/claude/gh-app/gh +++ b/scripts/claude/gh-app/gh @@ -10,4 +10,28 @@ if [ -n "${GH_APP_READONLY:-}" ] || [ -z "${GH_APP_DISABLE:-}" ]; then GH_TOKEN="$(bash "$dir/gh-app-token")" || { echo "gh-app-token: 鍵を受け取れませんでした" >&2; exit 1; } export GH_TOKEN fi +# I225: --json なしの auth status は、App の長い鍵の大部分を画面に出す(gh の伏せ字は昔の短い鍵向け)。 +# --json・--jq・--template・-t/--show-token のどれも無いときは、鍵の欄の無い --json hosts で取り、名義の行だけを出す。 +# わざと鍵を見る -t/--show-token は置き換えない。見るのは引数の 1・2 番目だけ(ふつうの書き方の事故を止めるためのもの) +if [ "${1:-}" = auth ] && [ "${2:-}" = status ]; then + plain=1 + for a in "${@:3}"; do + case "$a" in --json*|--jq*|--template*|--show-token|-t|-[!-]*t*) plain=0 ;; esac + done + if [ "$plain" = 1 ]; then + echo "gh: 鍵を画面に出さないよう、auth status を --json の形で表示しました(I225)" >&2 + json="$("$REAL_GH" auth status "${@:3}" --json hosts)" || exit $? + # 1 アカウント 1 行。error の欄は出さない(文言に鍵の一部が入りうる)。state がすべて success なら 0(--json なしの終了コードに合わせる) + printf '%s' "$json" | python3 -I -c ' +import json, sys +ok = True +for accounts in json.load(sys.stdin).get("hosts", {}).values(): + for a in accounts: + ok = ok and a.get("state") == "success" + print(" ".join(str(a.get(k, "")) for k in ("host", "login", "tokenSource", "state")) + (" (active)" if a.get("active") else "")) +sys.exit(0 if ok else 1) +' + exit $? + fi +fi exec "$REAL_GH" "$@" diff --git a/scripts/claude/gh-app/revoke.sh b/scripts/claude/gh-app/revoke.sh new file mode 100644 index 00000000..74212f0b --- /dev/null +++ b/scripts/claude/gh-app/revoke.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# 鍵が画面・会話の記録に出たときに、GitHub App の 1 時間の鍵を GitHub で失効させる(I225)。 +# 使い方: bash scripts/claude/gh-app/revoke.sh +# 控えの 2 つ(token・token-readonly)の鍵を失効させ、無効になったこと(HTTP 401)を確かめてから控えを消す +# bash scripts/claude/gh-app/revoke.sh --stdin +# 控えに無い鍵(取り直しの後に気づいた・別のセッションで出た)を標準入力から 1 行読み、その鍵を失効させる(控えは触らない) +# 鍵の値は引数・画面に出さない(GH_TOKEN の環境変数で本物の gh に渡す。入口を通さない=入口が別の鍵に差し替えない) +# 終了コード: 0=すべて無効を確認(控えが無いものを含む)・1=無効を確かめられない鍵がある(控えは残す)・2=使い方の誤り +# テスト用: GH_APP_REAL_GH・GH_APP_CACHE_DIR で本物の gh と控えのフォルダを差し替えられる +# 手順書: docs/runbooks/github-app-setup.md「5.」 +set -uo pipefail +REAL_GH="${GH_APP_REAL_GH:-/usr/bin/gh}" +CACHE_DIR="${GH_APP_CACHE_DIR:-${HOME}/.cache/github-app}" + +usage() { echo "使い方: $0 [--stdin]" >&2; exit 2; } + +# revoke_one <鍵>: 失効させ、その鍵で読みに行って 401 が返れば 0。 +# 通信の失敗などで 401 を確かめられないときは 1(失効できていないのに「無効」と取り違えない) +revoke_one() { + local out + GH_TOKEN="$1" "$REAL_GH" api -X DELETE installation/token --silent > /dev/null 2>&1 + out="$(GH_TOKEN="$1" "$REAL_GH" api installation/repositories --silent 2>&1 > /dev/null)" && return 1 + [[ "$out" == *"HTTP 401"* ]] +} + +FAILED="無効を確かめられませんでした。GitHub に届くかを確かめて流し直してください(docs/runbooks/github-app-setup.md「7.」「8.」)" + +case "$#:${1:-}" in + 0:) ;; + 1:--stdin) + [ -t 0 ] && echo "出た鍵を貼って Enter を押してください(画面には出ません):" >&2 + leaked="" + IFS= read -rs leaked || true + leaked="${leaked//[[:space:]]/}" + [ -n "$leaked" ] || { echo "revoke: 鍵が渡されませんでした" >&2; exit 2; } + if revoke_one "$leaked"; then echo "渡された鍵: 失効を確認しました"; exit 0; fi + echo "渡された鍵: $FAILED" >&2 + exit 1 ;; + *) usage ;; +esac + +rc=0 +for name in token token-readonly; do + f="$CACHE_DIR/$name" + if [ ! -f "$f" ]; then echo "$name: 控えがありません"; continue; fi + tok="" + read -r _ tok < "$f" || true + if [ -z "$tok" ]; then rm -f "$f"; echo "$name: 控えが空なので消しました"; continue; fi + if revoke_one "$tok"; then + rm -f "$f" + echo "$name: 失効を確認して控えを消しました" + else + echo "$name: $FAILED(控えは残しました)" >&2 + rc=1 + fi +done +exit "$rc" diff --git a/scripts/claude/issue-review.sh b/scripts/claude/issue-review.sh index 881cec9d..2a6b3c33 100644 --- a/scripts/claude/issue-review.sh +++ b/scripts/claude/issue-review.sh @@ -37,7 +37,7 @@ $(cat "$ISSUE_FILE")" # claude -p で第三者レビュー(I193: 公式のサブエージェント定義を --agent で呼ぶ。読み取り専用は定義の tools で効かせる)。 # 失敗・時間切れ(REVIEW_TIMEOUT・既定 900 秒)でも起票フローを止めない -if ! REVIEW=$(printf '%s' "$CONTEXT" | GH_APP_READONLY=1 timeout "${REVIEW_TIMEOUT:-900}" claude -p --agent "$REVIEWER_AGENT" --tools "Read,Grep,Glob" --permission-prompts none); then +if ! REVIEW=$(printf '%s' "$CONTEXT" | GH_APP_READONLY=1 timeout "${REVIEW_TIMEOUT:-900}" claude -p --agent "$REVIEWER_AGENT" --tools "Read,Grep,Glob" --strict-mcp-config --permission-prompts none); then echo "⚠️ issue-review をスキップしました(claude -p 失敗または時間切れ)。" exit 0 fi diff --git a/scripts/claude/plan-issue-review.sh b/scripts/claude/plan-issue-review.sh index d9b6983e..444b58f3 100644 --- a/scripts/claude/plan-issue-review.sh +++ b/scripts/claude/plan-issue-review.sh @@ -128,7 +128,7 @@ $(cat "$MANUAL_TEST")" # claude -p でレビュー実行(I193: 公式のサブエージェント定義を --agent で呼ぶ。読み取り専用は定義の tools で効かせる) # 時間上限 REVIEW_TIMEOUT(既定 900 秒)。時間切れ・失敗は判定できないので exit 1(fail-closed) REVIEW_RC=0 -REVIEW=$(printf '%s' "$CONTEXT" | GH_APP_READONLY=1 timeout "${REVIEW_TIMEOUT:-900}" claude -p --agent "$REVIEWER_AGENT" --tools "Read,Grep,Glob" --permission-prompts none) || REVIEW_RC=$? +REVIEW=$(printf '%s' "$CONTEXT" | GH_APP_READONLY=1 timeout "${REVIEW_TIMEOUT:-900}" claude -p --agent "$REVIEWER_AGENT" --tools "Read,Grep,Glob" --strict-mcp-config --permission-prompts none) || REVIEW_RC=$? if [ "$REVIEW_RC" -ne 0 ]; then echo "⛔ 計画レビューが終わりませんでした(claude -p exit=${REVIEW_RC}。124 は時間切れ)。判定できないため止めます。" exit 1 diff --git a/scripts/claude/review-process-lib.sh b/scripts/claude/review-process-lib.sh index cbb73662..f489e57a 100644 --- a/scripts/claude/review-process-lib.sh +++ b/scripts/claude/review-process-lib.sh @@ -26,7 +26,7 @@ run_claude_logged() { echo "[review-process-lib] --permission-prompts none の無い claude -p は起動しません" >&2 return 2 fi - GH_APP_READONLY=1 timeout "$secs" "${CLAUDE_BIN:-claude}" -p "$@" --output-format stream-json --verbose < "$input" 2> "${log}.err" \ + GH_APP_READONLY=1 timeout "$secs" "${CLAUDE_BIN:-claude}" -p --strict-mcp-config "$@" --output-format stream-json --verbose < "$input" 2> "${log}.err" \ | python3 -I -u -c ' import json, sys, time for line in sys.stdin: diff --git a/scripts/claude/test-session.sh b/scripts/claude/test-session.sh index 406dacb9..509886f0 100644 --- a/scripts/claude/test-session.sh +++ b/scripts/claude/test-session.sh @@ -2,6 +2,7 @@ # テスト用のセッション(claude -p)の入口(I223)。GitHub へは読み取りだけの App の鍵を渡し、書き込みを GitHub の側で拒否させる # (gh の入口と git の credential helper が GH_APP_READONLY を引き継ぐ。docs/runbooks/github-app-setup.md)。 # 使い方: bash scripts/claude/test-session.sh +# MCP は --mcp-config で渡したものだけ(--strict-mcp-config・I225。手元の設定の MCP が読み取りだけの鍵を迂回しないように) # テスト用: CLAUDE_BIN=<コマンド> で起動する claude を差し替えられる export GH_APP_READONLY=1 -exec "${CLAUDE_BIN:-claude}" -p "$@" +exec "${CLAUDE_BIN:-claude}" -p --strict-mcp-config "$@" diff --git a/scripts/claude/tests/test_i223_gh_app.sh b/scripts/claude/tests/test_i223_gh_app.sh index 48d6dfc2..0725c2fd 100644 --- a/scripts/claude/tests/test_i223_gh_app.sh +++ b/scripts/claude/tests/test_i223_gh_app.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # I223: GitHub App の鍵の仕組み(scripts/claude/gh-app/)と、テスト用のセッションを読み取りだけの鍵で起こすことを決定論検証する。 +# I225: 入口の auth status の置き換え(G8)・失効のスクリプト(G9)・鍵を出す書き方の静的な検査(G10)・MCP の抜け道(G7)を足した。 # 実行: bash scripts/claude/tests/test_i223_gh_app.sh -# ケースは docs/tests/open/I223_auto_test.md の TC-02(G1〜G5)と対応する。 +# ケースは docs/tests/closed/I223_auto_test.md の TC-02(G1〜G6)と docs/tests/open/I225_auto_test.md の TC-02(G7〜G10)と対応する。 # curl・openssl・本物の gh・claude は一時フォルダの偽物に差し替える(秘密の鍵ファイルと本物の GitHub は使わない)。 # FG: 各スクリプトの写しを sed で壊し、該当の G が不合格になることをこの中で確かめる。 set -uo pipefail @@ -38,6 +39,28 @@ cat > "$STUB/real-gh" <<'EOF' #!/usr/bin/env bash echo "GH_TOKEN=${GH_TOKEN:-}" EOF +cat > "$STUB/real-gh-auth" <<'EOF' +#!/usr/bin/env bash +# 偽の本物の gh(G8): 引数を $FAKE_GH_ARGS に書く。--json があれば $FAKE_AUTH_JSON を、無ければ鍵の行を出す +echo "$*" >> "$FAKE_GH_ARGS" +for a in "$@"; do + if [ "$a" = --json ]; then cat "$FAKE_AUTH_JSON"; exit 0; fi +done +echo " - Token: SECRET-abc123" +EOF +cat > "$STUB/real-gh-revoke" <<'EOF' +#!/usr/bin/env bash +# 偽の本物の gh(G9): 「鍵 引数」を $FAKE_GH_ARGS に書く。DELETE は $FAKE_REVOKED に鍵を足す($FAKE_NO_REVOKE なら足さない)。 +# 読みに行くと、失効した鍵なら 401 の文言で 1、$FAKE_NET_DOWN なら通信の失敗で 1、それ以外は 0 +echo "${GH_TOKEN:-} $*" >> "$FAKE_GH_ARGS" +if [ "${1:-} ${2:-} ${3:-}" = "api -X DELETE" ]; then + [ -n "${FAKE_NO_REVOKE:-}" ] || echo "${GH_TOKEN:-}" >> "$FAKE_REVOKED" + exit 0 +fi +if [ -n "${FAKE_NET_DOWN:-}" ]; then echo "gh: dial tcp: lookup api.github.com: no such host" >&2; exit 1; fi +if grep -qxF "${GH_TOKEN:-}" "$FAKE_REVOKED" 2>/dev/null; then echo "gh: Bad credentials (HTTP 401)" >&2; exit 1; fi +exit 0 +EOF cat > "$STUB/claude" <<'EOF' #!/usr/bin/env bash echo "GH_APP_READONLY=${GH_APP_READONLY:-}" >> "$FAKE_CLAUDE_OUT" @@ -49,16 +72,21 @@ printf '{"token":"ro-token","permissions":%s}' "$RO_PERMS" > "$T/resp_ro.json" printf '{"token":"rw-token","permissions":{"contents":"write","issues":"write"}}' > "$T/resp_rw.json" printf '{"token":"bad-token","permissions":{"contents":"read","issues":"write"}}' > "$T/resp_bad.json" -# G6: claude -p の呼び出しと、tmux で claude を起こす行(tui-capture.sh・fix-loop で追加)の論理行 -# (行末の \ でつなぐ・コメントは除く)がすべて GH_APP_READONLY=1 を持つ -# (export GH_APP_READONLY=1 するファイルは除く)。起動の所が増えても付け忘れを止める(計画変更記録 3) -g6() { +# launch_lines_missing <要る文字列> <ファイル…> +# claude -p の呼び出しと、tmux で claude を起こす行の論理行(行末の \ でつなぐ・コメントは除く)のうち、 +# <要る文字列> を持たない行を出し、1 つでもあれば 1 +# G6: すべて GH_APP_READONLY=1 を持つ(export GH_APP_READONLY=1 するファイルは除く)。起動の所が増えても付け忘れを止める(I223 計画変更記録 3) +# G7(I225): すべて --strict-mcp-config を持つ(MCP は --mcp-config で渡したものだけ。export のファイルも除外しない) +g6() { launch_lines_missing "GH_APP_READONLY=1 " 1 "$@"; } +g7() { launch_lines_missing "--strict-mcp-config" 0 "$@"; } +launch_lines_missing() { python3 -I - "$@" <<'PY' import re, sys +need, skip_export, paths = sys.argv[1], sys.argv[2] == "1", sys.argv[3:] bad = [] -for path in sys.argv[1:]: +for path in paths: text = open(path, encoding="utf-8").read() - if re.search(r"^export GH_APP_READONLY=1$", text, re.M): + if skip_export and re.search(r"^export GH_APP_READONLY=1$", text, re.M): continue logical, cur = [], "" for line in text.split("\n"): @@ -70,7 +98,7 @@ for path in sys.argv[1:]: if l.lstrip().startswith("#"): continue launch = re.search(r'claude(\}")? -p\s+(--|"\$)', l) or re.search(r'tmux new-session\b.*\bclaude\b', l) - if launch and "GH_APP_READONLY=1 " not in l: + if launch and need not in l: bad.append(f"{path}: {l.strip()[:120]}") for b in bad: print(b) @@ -78,7 +106,49 @@ sys.exit(1 if bad else 0) PY } -# ---- 1 組の写しに対して G1〜G6 を流し、各 G の合否(OK/NG)を出す ---- +# G10(I225): 決まりの置き場(CLAUDE.md・docs/runbooks・.claude/skills・.claude/agents・scripts/claude〔tests を除く〕・ +# docs/*/templates)に、鍵を画面に出す auth status の書き方(--json なし、または -t/--show-token 付き)が無い。 +# 出てくるたびに、そのあとから行末か次のバッククォートまでの語を見る。個々のイシューの記録は対象外(plan_I225.md §9) +g10() { # g10 <ルート> + python3 -I - "$1" <<'PY' +import os, re, sys +root = sys.argv[1] +targets = ["CLAUDE.md", "docs/runbooks", ".claude/skills", ".claude/agents", "scripts/claude"] +docs = os.path.join(root, "docs") +if os.path.isdir(docs): + targets += [os.path.join("docs", d, "templates") for d in sorted(os.listdir(docs))] +skip = os.path.join(root, "scripts", "claude", "tests") +files = [] +for t in targets: + p = os.path.join(root, t) + if os.path.isfile(p): + files.append(p) + for dp, dns, fns in os.walk(p): + if dp == skip or dp.startswith(skip + os.sep): + dns[:] = [] + continue + files += [os.path.join(dp, f) for f in fns] +bad = [] +for f in files: + try: + text = open(f, encoding="utf-8").read() + except (UnicodeDecodeError, OSError): + continue + for m in re.finditer(r"gh auth status", text): + rest = re.split(r"[`\n]", text[m.end():], maxsplit=1)[0] + toks = rest.split() + has_json = any(t.startswith("--json") for t in toks) + shows = any(t == "--show-token" or re.fullmatch(r"-[A-Za-z]*t[A-Za-z]*", t) for t in toks) + if not has_json or shows: + line = text.count("\n", 0, m.start()) + 1 + bad.append(f"{os.path.relpath(f, root)}:{line}: gh auth status{rest[:60]}") +for b in bad: + print(b) +sys.exit(1 if bad else 0) +PY +} + +# ---- 1 組の写しに対して G1〜G9 を流し、各 G の合否(OK/NG)を出す ---- # run_cases <写しのフォルダ> [](G を渡せばそれだけを流す。中に gh-app/・scripts/claude/*.sh の写し) run_cases() { local S="$1" only="${2:-}" W="$T/work.$RANDOM" out body r @@ -151,11 +221,87 @@ sys.exit(0 if p == json.loads(sys.argv[2]) and d.get("repositories") == ["study- if want G6; then g6 "$S"/*.sh > /dev/null && echo "G6 OK" || echo "G6 NG" fi + + if want G7; then + g7 "$S"/*.sh > /dev/null && echo "G7 OK" || echo "G7 NG" + fi + + if want G8; then + # G8: 入口は --json・-t/--show-token なしの auth status を、鍵の行を出さない --json の表示に置き換える + r=OK + local A="$W/g8args" J_OK="$W/g8ok.json" J_ERR="$W/g8err.json" rc + printf '%s' '{"hosts":{"github.com":[{"active":true,"host":"github.com","login":"bot","state":"success","tokenSource":"GH_TOKEN","gitProtocol":"https"},{"active":false,"host":"github.com","login":"old","state":"success","tokenSource":"/x/hosts.yml","gitProtocol":"https"}]}}' > "$J_OK" + printf '%s' '{"hosts":{"github.com":[{"active":true,"host":"github.com","login":"bot","state":"error","error":"SECRET-in-error","tokenSource":"GH_TOKEN","gitProtocol":"https"}]}}' > "$J_ERR" + # 鍵を受け取らない形で動かす(GH_APP_DISABLE=1・読み取りだけの指定なし)。$1=JSON・残り=gh の引数。出力は標準出力だけ + g8() { local j="$1"; shift + env -u GH_APP_READONLY GH_APP_DISABLE=1 GH_APP_REAL_GH="$STUB/real-gh-auth" FAKE_GH_ARGS="$A" FAKE_AUTH_JSON="$j" \ + bash "$S/gh-app/gh" "$@" 2>/dev/null; } + : > "$A" + out=$(g8 "$J_OK" auth status); rc=$? + [ "$rc" = 0 ] || r=NG + [ "$out" = "github.com bot GH_TOKEN success (active) +github.com old /x/hosts.yml success" ] || r=NG + [ "$(cat "$A")" = "auth status --json hosts" ] || r=NG + : > "$A" + out=$(g8 "$J_OK" auth status -a -h github.com) || r=NG + [[ "$out" != *SECRET* ]] || r=NG + [ "$(cat "$A")" = "auth status -a -h github.com --json hosts" ] || r=NG + out=$(g8 "$J_ERR" auth status); rc=$? + [ "$rc" = 1 ] || r=NG # state が success でないものがあれば 1 + [[ "$out" != *SECRET* ]] || r=NG # error の欄は出さない + [ "$out" = "github.com bot GH_TOKEN error (active)" ] || r=NG + # わざと鍵を見る書き方・--json・ほかのコマンドは置き換えない + for args in "auth status -t" "auth status --show-token" "auth status -at" "auth status --json hosts" "pr list"; do + : > "$A" + # shellcheck disable=SC2086 # 語に分けて渡す + g8 "$J_OK" $args > /dev/null + [ "$(cat "$A")" = "$args" ] || r=NG + done + echo "G8 $r" + fi + + if want G9; then + # G9: revoke.sh は控えの 2 つの鍵を失効させ、401 で無効を確かめて控えを消す。--stdin は渡された鍵だけを失効させる + r=OK + local C="$W/g9c" A="$W/g9args" V="$W/g9rev" out2 + rv() { GH_APP_REAL_GH="$STUB/real-gh-revoke" GH_APP_CACHE_DIR="$C" FAKE_GH_ARGS="$A" FAKE_REVOKED="$V" bash "$S/gh-app/revoke.sh" "$@" 2>&1; } + mkcache() { rm -rf "$C"; mkdir -p "$C"; : > "$A"; : > "$V" + printf '%s %s\n' 9999999999 rw-1 > "$C/token"; printf '%s %s\n' 9999999999 ro-1 > "$C/token-readonly"; } + [ -f "$S/gh-app/revoke.sh" ] || r=NG + # a) 2 つとも失効・確認・消す。鍵の値は出さない + mkcache + out2=$(rv) || r=NG + [ "$(sort "$V" | paste -sd' ')" = "ro-1 rw-1" ] || r=NG + [ ! -e "$C/token" ] && [ ! -e "$C/token-readonly" ] || r=NG + [[ "$out2" != *rw-1* && "$out2" != *ro-1* ]] || r=NG + grep -q '^rw-1 api -X DELETE installation/token' "$A" || r=NG + # b) 控えが無い → 0 + rm -rf "$C"; mkdir -p "$C" + rv > /dev/null || r=NG + # c) 失効しない(GitHub が受け付けない)→ 1・控えは残す + mkcache + if FAKE_NO_REVOKE=1 rv > /dev/null; then r=NG; fi + [ -f "$C/token" ] && [ -f "$C/token-readonly" ] || r=NG + # d) 通信の失敗(401 が出ない)は無効の確認にしない → 1・控えは残す + mkcache + if FAKE_NET_DOWN=1 rv > /dev/null; then r=NG; fi + [ -f "$C/token" ] || r=NG + # e) --stdin: 渡された鍵だけを失効させ、控えは触らない。値は出さない + mkcache + out2=$(printf 'leaked-x\n' | rv --stdin) || r=NG + [ "$(cat "$V")" = "leaked-x" ] || r=NG + [ -f "$C/token" ] && [ -f "$C/token-readonly" ] || r=NG + [[ "$out2" != *leaked-x* ]] || r=NG + # f) 使い方の誤り → 2 + printf '' | rv --stdin > /dev/null; [ $? = 2 ] || r=NG + rv --bogus > /dev/null; [ $? = 2 ] || r=NG + echo "G9 $r" + fi } copy_scripts() { # copy_scripts <先>(scripts/claude/*.sh と gh-app/ の写し) mkdir -p "$1/gh-app" - cp "$REPO_ROOT/scripts/claude/gh-app/gh-app-token" "$REPO_ROOT/scripts/claude/gh-app/gh" "$1/gh-app/" 2>/dev/null + cp "$REPO_ROOT/scripts/claude/gh-app/gh-app-token" "$REPO_ROOT/scripts/claude/gh-app/gh" "$REPO_ROOT/scripts/claude/gh-app/revoke.sh" "$1/gh-app/" 2>/dev/null cp "$REPO_ROOT"/scripts/claude/*.sh "$1/" 2>/dev/null # git は実行権を記録しない(core.filemode=false の端末から入る)ので、CI と同じく実行権なしの写しで確かめる(I223 fix-loop) chmod 644 "$1"/gh-app/* "$1"/*.sh 2>/dev/null @@ -164,9 +310,12 @@ copy_scripts() { # copy_scripts <先>(scripts/claude/*.sh と gh-app/ の写 # ---- 本物(リポジトリの版)---- copy_scripts "$T/real" res=$(run_cases "$T/real") -for g in G1 G2 G3 G4 G5 G6; do +for g in G1 G2 G3 G4 G5 G6 G7 G8 G9; do ck "$g リポジトリの版で合格" "$g OK" "$(grep "^$g " <<< "$res")" done +g10_out=$(g10 "$REPO_ROOT"); g10_rc=$? +[ "$g10_rc" = 0 ] || printf '%s\n' "$g10_out" +ck "G10 決まりの置き場に鍵を出す auth status の書き方が無い" 0 "$g10_rc" # ---- FG: 壊した写しでは該当の G が不合格になる ---- fg() { # fg <説明> … @@ -200,6 +349,32 @@ fg G6 "plan-issue-review.sh の指定を消す" plan-issue-review.sh 's/GH_APP_R fg G6 "複数行の呼び出し(fix-review-lib.sh)の指定を消す" fix-review-lib.sh 's/GH_APP_READONLY=1 //' fg G6 "code-review.sh の指定を消す" code-review.sh 's/GH_APP_READONLY=1 //' fg G6 "tmux で起こす tui-capture.sh の指定を消す" tui-capture.sh 's/-e GH_APP_READONLY=1 //' +fg G7 "code-review.sh の --strict-mcp-config を消す" code-review.sh 's/ --strict-mcp-config//' +fg G7 "tmux で起こす tui-capture.sh の --strict-mcp-config を消す" tui-capture.sh 's/ --strict-mcp-config//' +# shellcheck disable=SC2016 # sed の式なので $ をそのまま渡す +fg G8 "入口の置き換えの分岐を消す" gh-app/gh 's/if \[ "\${1:-}" = auth \] && \[ "\${2:-}" = status \]; then/if false; then/' +fg G8 "state の判定を消す(いつも 0)" gh-app/gh 's/sys.exit(0 if ok else 1)/sys.exit(0)/' +fg G9 "控えの名前の一覧から token-readonly を消す" gh-app/revoke.sh 's/for name in token token-readonly; do/for name in token; do/' +# shellcheck disable=SC2016 # sed の式なので $ をそのまま渡す +fg G9 "401 の確かめを消す(失敗なら無効とみなす)" gh-app/revoke.sh 's/\[\[ "\$out" == \*"HTTP 401"\* \]\]/[ -n "$out" ]/' +# shellcheck disable=SC2016 # sed の式なので $ をそのまま渡す +fg G9 "--stdin で読んだ値を使わず控えの鍵を使う" gh-app/revoke.sh 's/if revoke_one "\$leaked"; then/if revoke_one "$(cut -d" " -f2 "$CACHE_DIR\/token")"; then/' + +# G10 の FG: 一時フォルダの手順書に鍵を出す書き方を書くと不合格、--json の形だけなら合格 +g10_fg() { # g10_fg <説明> <期待の終了コード> <手順書の 1 行> + local d="$T/g10.$RANDOM" + mkdir -p "$d/docs/runbooks" + printf '%s\n' "$3" > "$d/docs/runbooks/x.md" + g10 "$d" > /dev/null; ck "FG G10 $1" "$2" "$?" +} +# shellcheck disable=SC2016 # バッククォートは手順書の文字そのもの +g10_fg "--json なしは不合格" 1 '確かめる: `gh auth status`' +# shellcheck disable=SC2016 +g10_fg "--json に -t を足すと不合格" 1 '確かめる: `gh auth status --json hosts -t`' +# shellcheck disable=SC2016 +g10_fg "本物の gh を直接呼ぶ形も不合格" 1 '確かめる: `/usr/bin/gh auth status -a`' +# shellcheck disable=SC2016 +g10_fg "--json の形だけなら合格" 0 '確かめる: `gh auth status --json hosts --jq '"'"'.hosts[][] | .login'"'"'`' echo "---" echo "pass=$pass fail=$fail" diff --git a/scripts/claude/tests/test_tui_capture.sh b/scripts/claude/tests/test_tui_capture.sh index b169186f..57893d94 100644 --- a/scripts/claude/tests/test_tui_capture.sh +++ b/scripts/claude/tests/test_tui_capture.sh @@ -65,7 +65,8 @@ t1() { [ "$(grep -cxF "kill-session -t $S" "$LOG")" -eq 1 ] || return 1 # 4 つの行がこの順にある(どれかが無ければ行番号が空で不合格): new-session < send-keys < 最後の capture-pane < kill-session local n s c k - n=$(grep -nxF "new-session -d -s $S -e GH_APP_READONLY=1 -x 200 -y 50 -c $REPO_ROOT claude --permission-mode manual" "$LOG" | cut -d: -f1) + # I225: 入口のある PATH(~/.local/bin を先頭に、呼び出し元の PATH=run_with の $FAKE:$PATH)と --strict-mcp-config を渡す + n=$(grep -nxF "new-session -d -s $S -e GH_APP_READONLY=1 -e PATH=$HOME/.local/bin:$FAKE:$PATH -x 200 -y 50 -c $REPO_ROOT claude --permission-mode manual --strict-mcp-config" "$LOG" | cut -d: -f1) s=$(grep -nxF "send-keys -t $S -l -- /plan-" "$LOG" | cut -d: -f1) c=$(grep -nxF "capture-pane -p -t $S" "$LOG" | tail -1 | cut -d: -f1) k=$(grep -nxF "kill-session -t $S" "$LOG" | cut -d: -f1) @@ -133,6 +134,10 @@ M="$(mutant fg_t1 "trap 'tmux kill-session" "trap ': tmux-kill-session")" ck "FG の写し T1 が作れた" 0 "$(made "$M")" [ -n "$M" ] && { run_with "$M" --wait 0 '/plan-' >/dev/null; ck "FG-T1 後始末を消すと T1 不合格" 1 "$(rc t1)"; } +M="$(mutant fg_t1b '-e "PATH=$HOME/.local/bin:$PATH" ' '')" +ck "FG の写し T1b が作れた" 0 "$(made "$M")" +[ -n "$M" ] && { run_with "$M" --wait 0 '/plan-' >/dev/null; ck "FG-T1b 入口のある PATH を渡さないと T1 不合格" 1 "$(rc t1)"; } + M="$(mutant fg_t2 '=~ ^[[:space:]]*!' '=~ ^NEVER-MATCH')" ck "FG の写し T2 が作れた" 0 "$(made "$M")" [ -n "$M" ] && ck "FG-T2 ! の判定を消すと通ってしまう" 0 "$(run_with "$M" --wait 0 '!ls')" @@ -145,7 +150,7 @@ M="$(mutant fg_t6 '[ "$waited" -lt "$START_TIMEOUT" ] ||' 'break;')" ck "FG の写し T6 が作れた" 0 "$(made "$M")" [ -n "$M" ] && ck "FG-T6 時間切れの判定を消すと 0 になる" 0 "$(TUI_FAKE_NOPROMPT=1 run_with "$M" --start-timeout 0 --wait 0 '/x')" -M="$(mutant fg_t8 '"claude --permission-mode manual"' '"claude --permission-mode manual $*"')" +M="$(mutant fg_t8 '"claude --permission-mode manual --strict-mcp-config"' '"claude --permission-mode manual --strict-mcp-config $*"')" ck "FG の写し T8 が作れた" 0 "$(made "$M")" [ -n "$M" ] && ck "FG-T8 起動の行に引数を足すと T8 不合格" 1 "$(rc t8 "$M")" diff --git a/scripts/claude/tui-capture.sh b/scripts/claude/tui-capture.sh index 0d964d93..d270ec0e 100644 --- a/scripts/claude/tui-capture.sh +++ b/scripts/claude/tui-capture.sh @@ -42,7 +42,9 @@ ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || err "git のワークツ S="tui-capture-$$" trap 'tmux kill-session -t "$S" >/dev/null 2>&1' EXIT # I223: テスト用のセッションなので GitHub へは読み取りだけの App の鍵(-e で渡す。tmux サーバーの環境に関係なく入る) -tmux new-session -d -s "$S" -e GH_APP_READONLY=1 -x 200 -y 50 -c "$ROOT" "claude --permission-mode manual" || err "tmux のセッションを起動できません" +# I225: PATH も -e で渡し、先頭に gh の入口(install.sh が入れる ~/.local/bin/gh)を置く。先に立っていた tmux サーバーの PATH に +# ~/.local/bin が無くても、中の claude の gh は入口を通る。MCP は読まない(--strict-mcp-config。手元の設定の MCP で鍵を迂回しない) +tmux new-session -d -s "$S" -e GH_APP_READONLY=1 -e "PATH=$HOME/.local/bin:$PATH" -x 200 -y 50 -c "$ROOT" "claude --permission-mode manual --strict-mcp-config" || err "tmux のセッションを起動できません" PROMPT_RE=$'(^|\n)❯' waited=0