@tyhld/conductor 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +301 -19
  2. package/dist/cli.js +143 -18
  3. package/dist/ear-routing.js +57 -0
  4. package/dist/ear.js +57 -0
  5. package/dist/env-file-perm.js +67 -0
  6. package/dist/nudge.js +172 -0
  7. package/dist/realtime-parse.js +116 -0
  8. package/dist/realtime.js +251 -0
  9. package/dist/relay-runner.js +65 -0
  10. package/dist/relay.js +832 -133
  11. package/dist/websocket-transport.js +66 -0
  12. package/launchd/ear-install.sh +96 -0
  13. package/package.json +30 -1
  14. package/sales-template/README.md +185 -25
  15. package/sales-template/install.sh +890 -0
  16. package/sales-template/launchd/install.sh +151 -0
  17. package/sales-template/settings.json +26 -97
  18. package/sales-template/setup.sh +754 -117
  19. package/sales-template/systemd/README.md +28 -4
  20. package/sales-template/systemd/install.sh +54 -5
  21. package/sales-template/systemd/paste-cache-prune-install.sh +75 -0
  22. package/sales-template/systemd/tyhld-paste-cache-prune.service +25 -0
  23. package/sales-template/systemd/tyhld-paste-cache-prune.timer +19 -0
  24. package/sales-template/uninstall.sh +245 -0
  25. package/scripts/hooks/README.md +246 -0
  26. package/scripts/hooks/cc2_guard.py +145 -0
  27. package/scripts/hooks/codex-hooks.sample.json +58 -0
  28. package/scripts/hooks/hook_datalink.py +440 -0
  29. package/scripts/hooks/install-codex-hooks.sh +127 -0
  30. package/scripts/hooks/notification_hook.py +167 -0
  31. package/scripts/hooks/permission_request_hook.py +207 -0
  32. package/scripts/hooks/policy.py +759 -0
  33. package/scripts/hooks/settings.sample.json +142 -0
  34. package/scripts/hooks/stop_hook.py +275 -0
  35. package/scripts/hooks/summary_ja.py +155 -0
  36. package/scripts/hooks/test_hook_datalink.py +282 -0
  37. package/scripts/hooks/test_policy.py +1241 -0
  38. package/skills/conductor-craftsman/SKILL.md +40 -0
  39. package/systemd/conductor-ear.service +63 -0
  40. package/systemd/conductor@.service +62 -0
  41. package/systemd/ear-install.sh +131 -0
  42. package/systemd/guard-sync-install.sh +94 -0
  43. package/systemd/tyhld-guard-sync.service +28 -0
  44. package/systemd/tyhld-guard-sync.timer +25 -0
  45. package/sales-template/cc2_guard.py +0 -395
  46. package/sales-template/systemd/conductor@.service +0 -47
@@ -0,0 +1,246 @@
1
+ # 番人(フック)— 段2で作り直した版
2
+
3
+ 職人(AI担当)の手が動く直前に割り込み、**人間の関門**と**危険**だけを止める仕組みです。
4
+
5
+ ## 1. 何がどう変わったか
6
+
7
+ **旧版**は「安全な動詞を列挙し、載っていないものは全部確認」でした。運用方針は逆で、
8
+ **人間の関門は3つだけ(main直変更/本番確認/DB実行)、それ以外は職人が自走してよい**です。
9
+
10
+ 向きが逆だったため、新しいコマンド(`docker` / `make` / `go` / `cargo` / `gh workflow` /
11
+ `git worktree` / `journalctl` …)が出るたび自動的に**間違った側=確認**へ落ち、人が許可リストへ
12
+ 1行ずつ足して埋めていました(実測200行超)。そしてその許可リストが逆に番人の境界を迂回していました。
13
+
14
+ この版では判定の軸を反転しています。
15
+
16
+ | 順番 | 何に当たるか | どうなるか |
17
+ | --- | --- | --- |
18
+ | 1 | **危険**(本番破壊・force push・データ消失・認証情報の露出/持ち出し) | `deny`(実行させない) |
19
+ | 2 | **3関門**(main直変更/本番確認/DB実行) | 管制に到達できれば `defer`(人の判断へ渡す)。**聞けないときは `deny`**(フェイルクローズ・ADR-003) |
20
+ | 3 | それ以外 | `allow`(自走) |
21
+
22
+ ★**新しいコマンドは必ず 3 に落ちます**。これが「将来出てくる道具が自動的に正しい側へ行く」形です。
23
+
24
+ ### ★PRのマージは関門ではありません(ADR-007)
25
+
26
+ `gh pr merge`(および `gh api …/pulls/N/merge`)は**関門から外し、職人が自走できます**。
27
+ マージを関門にすると、職人は毎回**最後の1手で人を待って止まる**ため、「指示を出したら終わりまで進む」が
28
+ 成立しませんでした。マージはPRという記録が残り、レビューとCIの結果が付いた上での操作です。
29
+
30
+ **外したのはマージだけ**です。`git push origin main` のような**本番ブランチへの直接 push** は
31
+ 関門のまま残ります(PRもレビューもCIも通らずに本番の元が変わるため、性質がまったく違います)。
32
+ 関門1の呼び名も「マージ」から「**main直変更**」へ変わりました(種別の字面 `merge` は管制の表示・
33
+ 集計が使っているのでそのままです)。
34
+
35
+ ## 2. ファイルの役割
36
+
37
+ | ファイル | 役割 |
38
+ | --- | --- |
39
+ | `policy.py` | 判定そのもの(純粋関数だけ・標準ライブラリのみ)。**製品固有の名前を持たない=切り出し可能** |
40
+ | `cc2_guard.py` | PreToolUse フックの入口。標準入力を読み、policy に聞き、決まった形で答えるだけ |
41
+ | `permission_request_hook.py` | PermissionRequest フック。管制へ確認を送り、人の答えを待って返す |
42
+ | `stop_hook.py` | Stop フック。職人が人へ**問いかけて**止まったとき、管制へ知らせる(ADR-008) |
43
+ | `hook_datalink.py` | フックと管制のやり取り(HTTP)。**製品固有の前提を持たない=切り出し可能** |
44
+ | `summary_ja.py` | 確認カードに出す**日本語1行**の組み立て |
45
+ | `test_policy.py` | 単体テスト(297件)。`python3 scripts/hooks/test_policy.py` |
46
+ | (`../check_permission_rules.py`) | 現場の許可設定に残った「番人と食い違う行」を洗い出す検査(読むだけ・ADR-009) |
47
+ | `test_hook_datalink.py` | 連絡線の単体テスト。`python3 scripts/hooks/test_hook_datalink.py` |
48
+ | `settings.sample.json` | フック登録の見本(★人間が `~/.claude/settings.json` へ反映する) |
49
+
50
+ ### 連絡線が通らないときに見る場所
51
+
52
+ `hook_datalink.py` は「フックを絶対に落とさない」ため通信の例外を握りますが、**握った理由は1行残します**。
53
+
54
+ ```
55
+ ~/.conductor-hook.log # 環境変数 CONDUCTOR_HOOK_LOG で変更可
56
+ ```
57
+
58
+ ```
59
+ 2026-07-23T15:30:00+0900 POST /api/conductor/hook-requests -> HTTP 307 body="Redirecting..."
60
+ ```
61
+
62
+ 失敗したときだけ書かれます(正常時は1行も増えません)。残すのは**メソッド・パス・状態・応答の先頭120字**だけで、
63
+ **トークン・Authorization ヘッダ・接続先ホスト・URLのクエリは載せません**。
64
+
65
+ よくある読み方:
66
+
67
+ | 状態 | 意味 |
68
+ | --- | --- |
69
+ | `HTTP 307` | 受け口の**手前**で折り返されている(管制側 `middleware.ts` の公開パス漏れ) |
70
+ | `HTTP 401` | 鍵が違う/届いていない |
71
+ | `HTTP 404` | 受け口がまだ配備されていない |
72
+ | `HTTP 500` | 管制側で失敗している |
73
+ | `URLError` / `TimeoutError` | 管制へ届いていない(ネットワーク・URL 設定) |
74
+
75
+ ## 3. 危険は「動詞」ではなく「対象と作用」で見る
76
+
77
+ 旧版は動詞を白に入れた瞬間に対象を見なくなり、次がすべて素通りしていました(棚卸しで実測)。
78
+
79
+ - `curl -X POST … -d @.env` … 秘密をそのまま外へ送る
80
+ - `cp .env /tmp/x` / `mv` / `tar` / `base64` … 秘密の複製・持ち出し
81
+ - `sed -i` / `tee` / `> /etc/hosts` … 任意パスの書き換え
82
+
83
+ この版では **対象が秘密なら動詞が何であれ止め**、**システム領域へ書く作用があれば止め**ます。
84
+ 同じ語彙を Bash とファイル編集ツール(Write/Edit/…)の両方に適用するので、
85
+ **経路による強度差がありません**。
86
+
87
+ ## 4. なぜ関門は `defer` で返すのか(★前提条件つき)
88
+
89
+ 当初は `ask` にしていました。ところが実機で確かめたところ(検証 26c878b7)、
90
+
91
+ | フックの返し方 | `permission_suggestions` | 画面の選択肢 |
92
+ | --- | --- | --- |
93
+ | `ask` | **null** | `[Yes]` `[No]` の**2択** |
94
+ | `defer` | **入る** | `[Yes]` `[Yes, and don't ask again for: …]` `[No]` の**3択** |
95
+
96
+ 3択(はい/今後は聞かない/いいえ)を残す方針のため、**関門は(到達できるとき)`defer`** で返します。
97
+
98
+ ### ★聞けないときは止める(フェイルクローズ・ADR-003)
99
+
100
+ `defer` は **管制に到達できること** が前提で、到達できないと**黙って素通り**します(実際に3関門が
101
+ 素通りしていました・調査 `09c70615`)。そこで関門に限り:
102
+
103
+ - 管制へ到達でき認証も通る(`hook_datalink.ping` が真)→ 従来どおり `defer`(3択カード)
104
+ - 管制が未設定/到達不能/認証不可/答えが取れない → **`deny`**(安全のため止める)
105
+
106
+ `deny` は PreToolUse(`cc2_guard`)で返すので、**確認を出さない実行モードでも確実に止まります**。
107
+ 関門3種以外は従来どおり(全部は止めません)。既定で有効。`CC2_GATE_FAILCLOSE=0` で無効化できます
108
+ (管制が不通の間だけ現場を止めたくない、という人間の判断用)。`ping` はカードを作らない GET で
109
+ 到達だけを確かめます(副作用ゼロ・どんな例外でも False=安全側へ倒れる)。
110
+
111
+ ### ★成立の前提条件(満たさないと関門は素通りします)
112
+
113
+ 1. **関門コマンドに一致する `allow` 行が、どのスコープにも無いこと。**
114
+ 規則の評価順は **deny → ask → allow** で、スコープ(user / project / local)は順序に影響しません。
115
+ 一致する `allow` 行が1つでもあると、**確認ダイアログも PermissionRequest も出ずに実行されます**
116
+ (一時環境で再現確認済み)。消すべき行の一覧と手順は
117
+ [`docs/guard-cleanup-手順書.md`](../../docs/guard-cleanup-手順書.md) にあります。
118
+
119
+ 2. **「今後は聞かない」は必ず管制画面の3ボタンで受けること。**
120
+ 端末のダイアログで `2. Yes, and don't ask again for: …` を押すと、
121
+ **`localSettings` に `allow` 行が書き込まれ、その関門が恒久的に消えます**
122
+ (`permission_suggestions` の `destination` が `localSettings` であることが根拠)。
123
+ 管制経由なら PermissionRequest フックが `destination: 'session'` で処理するので、
124
+ 設定ファイルは汚れず、**セッションが変われば関門は復活**します。
125
+
126
+ ### ★逆向きの残骸=`ask` 行(関門が増えるのではなく、職人が止まります・ADR-009)
127
+
128
+ `allow` 行は関門を消しますが、**`ask` 行は逆に「毎回かならず確認を出す」登録**です。
129
+ 番人が自走させる操作に `ask` 行が残っていると、その操作は永久に人待ちになります。
130
+ 実測(2026-08-30)では、ある現場の `settings.json` に `"ask": ["Bash(gh pr merge:*)"]` が残っており、
131
+ マージを関門から外した(ADR-007)あとも**最後の1手だけが止まり続ける**状態でした。
132
+
133
+ 両方向の残骸をまとめて洗い出す検査があります(**読むだけ・設定は書き換えません**)。
134
+
135
+ ```
136
+ python3 scripts/check_permission_rules.py
137
+ ```
138
+
139
+ ### 将来の論点(記載のみ)
140
+
141
+ ホーム設定を管理できない環境(他社へ提供した先など)では、前提条件1を保証できないため
142
+ `defer` では関門を守れません。その場合は `ask`(2択)に倒す必要があります。
143
+ **3択と「どの環境でも関門が消えない」は、現在の Claude Code の仕様では同時に満たせません。**
144
+
145
+ ## 5. 管制との連絡線(段1の受け口へつなぐ)
146
+
147
+ ```
148
+ 職人のフック 管制(devlog-tracker)
149
+ PermissionRequest ──POST──▶ /api/conductor/hook-requests (確認を登録)
150
+ ──GET───▶ /api/conductor/hook-requests/{id} (最大600秒待つ)
151
+ ──DELETE▶ /api/conductor/hook-requests/{id} (去るとき=カードを閉じる)
152
+ Stop ──POST──▶ 同上(kind='message')
153
+ ```
154
+
155
+ - 答えは `allow` / `allow_always` / `deny` の3値。
156
+ - `allow_always` は `updatedPermissions` の**配列形式**で返します(文字列形式は 2.1.218 で無効・検証済)。
157
+ 保存先は既定 **`session`**(設定ファイルを汚さない)。環境変数 `CC2_ALLOW_ALWAYS_DESTINATION` で変更可。
158
+ - **答えが取れない(時間切れ・通信不能・管制未設定)ときは何も返しません**。
159
+ =答えないことを許可に読み替えません。
160
+
161
+ 接続先は `CONDUCTOR_URL` と `CONDUCTOR_TOKEN`(無ければ `$HOME/.conductor.env`)。
162
+ **未設定の環境ではフックは何もしません**(入れても壊れません)。
163
+
164
+ ## 6. Stop フックがカードを出す条件(★2つとも満たしたときだけ)
165
+
166
+ **(1) 紐づく指示(ConductorCommand)がまだ終わっていないこと**
167
+
168
+ - 待ち時間では判定しません(遅い返事と区別できない)。
169
+ - 指示IDは会話の記録にある管理用ジョブ印 `CONDUCTOR_JOB:<uuid>` から拾います。
170
+ - 指示IDが分からない/管制に聞けないときは**送りません**(安全側・カードを増やさない)。
171
+
172
+ **(2) 最後の発言が【人の判断を求める問いかけ】であること(ADR-008)**
173
+
174
+ 以前は (1) だけで判定していました。指示が未完了なのは作業中なら当たり前なので、
175
+ 「CIの完走を待っています。」のような**ただの進捗報告**でも「担当者から質問があります」の
176
+ カードが出て、職人が人を待って止まっていました(実機 2026-08-30)。
177
+
178
+ | | 例 | どうなるか |
179
+ |---|---|---|
180
+ | 問いかけ | `どちらにしますか?` / `このままマージしてよいですか` / `いかがでしょう` | **出す** |
181
+ | 進捗報告 | `CIの完走を待っています。` / `修正しました。` / `テストは全緑です。` | 出さない |
182
+
183
+ - 日本語は**文末**で問いかけが決まるので、文に切って文末だけを見ます
184
+ (文中の「か」=`確認できるかどうかを見ています` では出しません)。
185
+ - コード塊・インラインコード・引用行(`>`)・URL は先に落とします
186
+ (`gh pr view 12?x` の `?` を問いかけと読まないため)。
187
+ - **迷う文は出さない側へ倒します**。止まって人を待つより、報告して進む方が実害が小さく、
188
+ 本当に必要なら職人はもう一度聞けるためです。
189
+
190
+ **(2') 「?」で終わるだけでは出しません(ADR-011)**
191
+
192
+ ADR-008 のあとも「?で終われば問いかけ」としていたため、次のものが質問として出ていました。
193
+ 実機の会話記録7,581件を走査したところ、質問と判定された37件のうち**30件がこの形**でした。
194
+
195
+ | 落とすもの | 実例 |
196
+ |---|---|
197
+ | 表のセルの「?」 | <code>&#124; 2 &#124; ? &#124;</code> |
198
+ | 英語の自問(調べものの見出し) | `Now the decisive test — are the font files the cause?` |
199
+ | 引用・記事名の中の「?」 | `記事「北上市で内装をするなら?」を公開しました。` |
200
+ | 記号だけの断片 | `「?` |
201
+
202
+ 「?」で終わる文を出すのは、**人に判断を求める合図**があるときだけです
203
+ (`どちら` / `よろしい` / `許可` / `いかが` / `Should I …` / `Do you want …` など)。
204
+ `〜しますか` `〜でしょうか` `〜ましょうか` `〜でしたか` のような**問いかけの語尾**は従来どおり出します。
205
+
206
+ **(3) 待つ時間には上限があります**
207
+
208
+ 返事が来なくても `STOP_WAIT_SEC`(**9分30秒**)で切り上げ、職人はそのまま先へ進みます。
209
+ 共有の上限(`hook_datalink.MAX_WAIT_SEC` = 10分)より**30秒早く**切り上げるのは、
210
+ Claude Code 側のフック打ち切り(`timeout: 600`)と同時に終わると、カードを閉じる後始末が
211
+ 走らず**残骸カード**が残るためです。
212
+
213
+ ★ここは危ない操作の確認ではありません。3関門・秘密の持ち出し・SQL は
214
+ `cc2_guard.py` / `policy.py` / `permission_request_hook.py` が止めており、**この節の変更で1つも弱まりません**。
215
+
216
+ ## 7. ★人間がやる移行・掃除(AIは実行しません)
217
+
218
+ **手順書はこちら → [`docs/guard-cleanup-手順書.md`](../../docs/guard-cleanup-手順書.md)**
219
+
220
+ 1ステップ=1コマンドの形で、バックアップ・置き換え・戻し方まで書いてあります。
221
+ この掃除を**やり終えるまで関門は効きません**(前提条件1が満たされないため)。
222
+
223
+ ## 8. 分かっている限界(正直な記載)
224
+
225
+ - 関門は到達できるとき `defer` です。**掃除(`docs/guard-cleanup-手順書.md`)を済ませるまでは
226
+ `allow` 行が勝って関門が素通りします**(前提条件1)。掃除は人の作業です。管制が不通のときは
227
+ フェイルクローズで `deny` になります(ADR-003)。
228
+ - `git push` の関門判定は**明示された参照名**(`origin main` / `HEAD:master` 等)を見ます。
229
+ 引数なしの `git push` は、フックの入力から今いるブランチが分からないため**素通り**します。
230
+ - SQL の関門判定は字面(`CREATE TABLE` / `GRANT` / `ROW LEVEL SECURITY` …)で見ますが、
231
+ **文字列を扱うだけの動詞(`echo` / `grep` / `git` / `cat` …)では関門にしません**
232
+ (`grep "alter table"` / `git commit -m "grant …"` などの誤検知を消すため・ADR-003)。
233
+ そのぶん、`run-sql` / `apply` のような**未知のSQL実行入口**は字面で拾い、本物のDB道具
234
+ (`psql` / `mysql` / `prisma` / `supabase` / `drizzle-kit`)は**道具名**側で確実に拾います。
235
+ 破壊的な `DROP TABLE` / `DROP DATABASE` は関門ではなく **`deny`** で止まります。
236
+ - `sudo` は「ラッパ」として読み飛ばし、中の実体で判定します(`sudo rm` は従来どおり deny)。
237
+ 権限昇格そのものを止める規則は置いていません。
238
+ - 秘密の判定は**「読む・書く・持ち出す」動詞が対象に取ったとき**だけ効きます。
239
+ 未知の道具に秘密を渡す形(`mytool .env`)は素通りします。
240
+ ここを「引数に字面があれば止める」まで広げると、コミットメッセージや説明文に `.env` と
241
+ 書いただけで止まってしまい(実際に止まりました)、日常の作業が回らなくなるためです。
242
+ - Stop フックは**問いかけの形(`〜ですか` / `?`)で書かれた発言だけ**を取り次ぎます(ADR-008)。
243
+ 「許可が下りず進めません。」のような平叙文の相談は**取り次ぎません**。誤発火で全現場が止まる方が
244
+ 重いので、意図してこちらへ倒しています。職人が「〜してよいですか」と書き直せば出ます。
245
+ - relay の画面読み(tmux スクレイプ)は**今回は消していません**(撤去は段3)。
246
+ 当面はフック経路と画面読み経路が並走します。
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """cc2_guard.py — PreToolUse フック(番人の入口・段2で作り直し)。
3
+
4
+ 【この版で何が変わったか】
5
+ 旧版は「安全な動詞を列挙し、載っていないものは全部確認」だった。運用方針は逆で
6
+ 「人間の関門は3つだけ(マージ/本番確認/DB実行)、他は自走してよい」。
7
+ 向きを反転し、判定そのものは policy.py(純粋関数)へ出した。このファイルは
8
+ 「標準入力を読む → policy に聞く → 決められた形で答える」だけの薄い入口。
9
+
10
+ 【判定と返し方】
11
+ policy の 'deny' → permissionDecision="deny" … 実行させない
12
+ policy の 'gate' → 原則 defer(何も出力しない)… 通常の許可フローへ渡す=人が判断する。
13
+ ★ただし「聞けない」ときは deny(フェイルクローズ・下記)。
14
+ policy の 'allow' → permissionDecision="allow" … 自走
15
+
16
+ 【★聞けないときは止める(フェイルクローズ・報告 09c70615 の根治)】
17
+ 関門(gate)は「defer→ハーネスが確認→管制カード」で人へ渡す設計だが、これは
18
+ 【管制に到達できること】が前提。到達できないとき defer は【黙って素通り】する
19
+ (実際にマージ・DB・認証の3関門が素通りしていた)。よって関門に限り:
20
+ ・管制へ到達でき認証も通る(ping OK)→ 従来どおり defer(3択カードを出す)
21
+ ・管制が未設定 / 到達不能 / 認証不可(ping NG)→ deny(安全のため止める)
22
+ ★deny は PreToolUse で返すため、たとえ確認を出さない実行モードでも確実に止まる
23
+ (defer は素通りしうるが deny は素通りしない)。関門3種以外は従来どおり(全部は止めない)。
24
+ ★既定は有効。環境変数 CC2_GATE_FAILCLOSE=0 で無効化できる(管制が不通の間だけ現場を
25
+ 止めたくない、という人間の判断用。既定 '1')。
26
+
27
+ 【★なぜ関門は "defer" で返すのか(実機検証 26c878b7 に基づく決定)】
28
+ 当初は "ask" にしていた。理由は「defer だとユーザー設定の allow 規則に拾われて素通りする」。
29
+ それは事実(下記の前提条件を参照)だが、"ask" には確認カードが【2択になる】という
30
+ 副作用があった。実機で確かめた結果:
31
+ ・"ask" … permission_suggestions が null。画面は [Yes] [No] の2択。
32
+ ・"defer" … permission_suggestions が入る。画面は
33
+ [Yes] [Yes, and don't ask again for: …] [No] の3択。
34
+ 3択(はい/今後は聞かない/いいえ)を残す方針のため、関門は defer で返す。
35
+
36
+ ★defer が成立するための前提条件(満たさないと関門は素通りする・実機で再現済み)
37
+ 1. 関門コマンドに一致する allow 行が、どのスコープ(user / project / local)にも無いこと。
38
+ 規則の評価順は deny → ask → allow で、スコープは順序に影響しない。
39
+ 一致する allow 行が1つでもあると、確認ダイアログも PermissionRequest も出ずに実行される。
40
+ 2. 「今後は聞かない」は【管制画面の3ボタン】で受けること。
41
+ 端末のダイアログで 2番を押すと localSettings に allow 行が書かれ、その関門が恒久的に消える。
42
+ 管制経由なら PermissionRequest フックが destination='session' で処理するため、
43
+ 設定ファイルは汚れず、セッションが変われば関門は復活する。
44
+ 詳しい手順は docs/guard-cleanup-手順書.md(人間がやる掃除)を参照。
45
+
46
+ 【フェイルセーフ】
47
+ 例外時は何も出力せず exit 0(=委譲)。誤って allow を出さない。
48
+
49
+ 【入出力(公式 hooks 仕様 v2.1.x)】
50
+ 入力(stdin JSON): {"tool_name":"Bash","tool_input":{...},"cwd":"..."}
51
+ 出力(stdout JSON, exit 0):
52
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse",
53
+ "permissionDecision":"allow"|"deny"|"ask","permissionDecisionReason":"..."}}
54
+ """
55
+ import json
56
+ import os
57
+ import sys
58
+
59
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
60
+
61
+ import hook_datalink # noqa: E402 … 起動記録 と 管制への到達確認(ping)に使う
62
+ import policy # noqa: E402
63
+
64
+ # policy の判定 → フックが返す permissionDecision。
65
+ # ★GATE は載せない(=何も出力しない=defer)。3択を残すための決定(上の説明を参照)。
66
+ _DECISION_MAP = {
67
+ policy.DENY: 'deny',
68
+ policy.ALLOW: 'allow',
69
+ }
70
+
71
+
72
+ def _failclose_enabled(env=None):
73
+ """関門のフェイルクローズが有効か。既定は有効('1')。'0' で無効。"""
74
+ env = os.environ if env is None else env
75
+ return (env.get('CC2_GATE_FAILCLOSE') or '1').strip() != '0'
76
+
77
+
78
+ def _deny_output(reason):
79
+ return {
80
+ 'hookSpecificOutput': {
81
+ 'hookEventName': 'PreToolUse',
82
+ 'permissionDecision': 'deny',
83
+ 'permissionDecisionReason': reason,
84
+ }
85
+ }
86
+
87
+
88
+ def build_output(data, datalink=hook_datalink):
89
+ """入力1件から出力(dict) を作る。返り値 None は「何も出力しない(委譲=defer)」。
90
+
91
+ ★datalink は差し替え可能(テストで到達可否を固定するため)。
92
+ """
93
+ result = policy.decide_event(data)
94
+
95
+ # 起動記録: 番人が呼ばれた事実と判定を1行残す(秘密・コマンド全文は残さない)。
96
+ try:
97
+ datalink.record_decision(data.get('tool_name'), result['decision'], result.get('gate'))
98
+ except Exception: # noqa: BLE001 記録失敗で番人は止めない
99
+ pass
100
+
101
+ decision = _DECISION_MAP.get(result['decision'])
102
+ if decision is not None:
103
+ return {
104
+ 'hookSpecificOutput': {
105
+ 'hookEventName': 'PreToolUse',
106
+ 'permissionDecision': decision,
107
+ 'permissionDecisionReason': result['reason'],
108
+ }
109
+ }
110
+
111
+ # ここに来るのは関門(gate)だけ。既定は defer(3択カードを出す)だが、
112
+ # 「聞けない」状況(管制不通・未設定)では素通りさせず deny する=フェイルクローズ。
113
+ if _failclose_enabled():
114
+ try:
115
+ cfg = datalink.load_config()
116
+ except Exception: # noqa: BLE001
117
+ cfg = None
118
+ reachable = datalink.ping(cfg) if cfg is not None else False
119
+ if not reachable:
120
+ return _deny_output(
121
+ 'お客さまへの確認(管制)に今つながらないため、安全のためこの操作を止めました。'
122
+ 'マージ・本番反映・データベースの変更は、確認できないときは実行しません。'
123
+ '連絡線が回復してから、もう一度お試しください。'
124
+ )
125
+ # 管制に到達できる(または無効化された)→ 従来どおり defer(人が3択で判断)。
126
+ return None
127
+
128
+
129
+ def main() -> int:
130
+ try:
131
+ raw = sys.stdin.read()
132
+ data = json.loads(raw) if raw.strip() else {}
133
+ except Exception: # noqa: BLE001
134
+ return 0 # 入力不正 → 委譲(フェイルセーフ)
135
+ try:
136
+ out = build_output(data)
137
+ if out is not None:
138
+ sys.stdout.write(json.dumps(out, ensure_ascii=False))
139
+ return 0
140
+ except Exception: # noqa: BLE001
141
+ return 0 # 想定外でも allow を出さない=委譲
142
+
143
+
144
+ if __name__ == '__main__':
145
+ sys.exit(main())
@@ -0,0 +1,58 @@
1
+ {
2
+ "_comment": [
3
+ "Codex の職人へ番人(段2)を登録するための見本。★このファイルは読むだけの見本で、これ自体は効きません。",
4
+ "配布は scripts/hooks/install-codex-hooks.sh が行い、コマンドのパスだけ固定パス",
5
+ "$HOME/.tyhld/hooks/ へ書き換えて ~/.codex/hooks.json へ登録します(Claude 側と同じ流儀)。",
6
+ "",
7
+ "★なぜ Claude 用(settings.sample.json)と別ファイルなのか: 登録先も書式も別だから。",
8
+ " Claude … ~/.claude/settings.json の hooks(イベント名 PreToolUse / PermissionRequest / Stop / Notification)",
9
+ " Codex … ~/.codex/hooks.json の hooks(イベント名は同じだが Notification は存在しない)",
10
+ "",
11
+ "★実測(2026-09-02・codex 0.152.1・ADR-022):",
12
+ " ・PreToolUse は Claude とまったく同じ形の JSON で deny を返せる(実機で遮断を確認)",
13
+ " {\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",",
14
+ " \"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"…\"}}",
15
+ " ・渡ってくる項目も同じ: cwd / hook_event_name / session_id / tool_name / tool_input /",
16
+ " transcript_path / model / permission_mode / turn_id(PreToolUse は tool_use_id も)",
17
+ " ・Stop には last_assistant_message が入る(Claude と同じ名前)",
18
+ " ・★Notification は【存在しない】→ ADR-016 の「端末でしか押せない問い」は Codex では出せない",
19
+ "",
20
+ "★人の手番: Codex は【フックを信頼する】操作を1回求めます(実測の画面)。",
21
+ " 『Hooks need review / 3 hooks are new or changed.』→『2. Trust all and continue』",
22
+ " 番人を入れ替えたあとも、同じ確認がもう一度出ます(内容のハッシュで見ているため)。"
23
+ ],
24
+ "hooks": {
25
+ "PreToolUse": [
26
+ {
27
+ "hooks": [
28
+ {
29
+ "type": "command",
30
+ "command": "python3 $HOME/projects/conductor/scripts/hooks/cc2_guard.py"
31
+ }
32
+ ]
33
+ }
34
+ ],
35
+ "PermissionRequest": [
36
+ {
37
+ "hooks": [
38
+ {
39
+ "type": "command",
40
+ "timeout": 600,
41
+ "command": "python3 $HOME/projects/conductor/scripts/hooks/permission_request_hook.py"
42
+ }
43
+ ]
44
+ }
45
+ ],
46
+ "Stop": [
47
+ {
48
+ "hooks": [
49
+ {
50
+ "type": "command",
51
+ "timeout": 600,
52
+ "command": "python3 $HOME/projects/conductor/scripts/hooks/stop_hook.py"
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ }
58
+ }