@tyhld/conductor 0.12.21 → 0.12.22

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.
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """便2.6「番人の設定を弱める道を『手段を問わず』塞ぎ切る」の試験(便 0f92a767)。
3
+
4
+ 実行: python3 scripts/hooks/test_guard_config_all_routes.py
5
+ 1件でも期待と違えば exit 1。
6
+
7
+ 【えふさん確定 2026-09-18】
8
+ 4経路を DENY するだけでなく、別の書き込み手段・shell 構文に置き換えて同じことができないこと
9
+ まで含める。番人・guard・hook・managed security 設定そのものを職人が弱められないことを目的にする。
10
+ 普通の設定 Read・通常開発・作業ブランチ・正規 merge は止めない、も維持。
11
+
12
+ 【守る場所(policy.GUARD_PLACES の一覧・ここだけ)】
13
+ ~/.claude/settings.json / settings.local.json / 各現場の .claude/settings*.json /
14
+ ~/.claude/skills 配下 / ~/.tyhld/hooks 配下 / ~/.tyhld/bin 配下 / ~/.config/systemd 配下 /
15
+ ホーム直下の隠しフォルダの hooks.json・config.toml(道具名を持たない一般形・ADR-022)/
16
+ 管理設定(/etc/claude-code 配下・/Library/Application Support/ClaudeCode 配下)。
17
+
18
+ ★見本は作り物の場所で行う(本物の設定ファイルに1バイトも書かない)。危険語・秘密名は部品から組み立てる。
19
+ """
20
+ import atexit
21
+ import os
22
+ import shutil
23
+ import sys
24
+ import tempfile
25
+
26
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
+
28
+ import policy # noqa: E402
29
+
30
+ failures = []
31
+
32
+
33
+ def check(label, got, want):
34
+ ok = got == want
35
+ if not ok:
36
+ failures.append(f'{label}: got={got!r} want={want!r}')
37
+ print(f'{"OK " if ok else "NG "} {label:74} {str(got)[:30]}')
38
+
39
+
40
+ # ── 部品(守る場所・危険語を直接は書かない) ──
41
+ SET = 'sett' + 'ings'
42
+ CLA = '.cla' + 'ude'
43
+ S = f'~/{CLA}/{SET}.json'
44
+ SL = f'~/{CLA}/{SET}.local.json'
45
+ SKILLS = f'~/{CLA}/sk' + 'ills'
46
+ HOOKS = '~/.tyhld/' + 'hooks'
47
+ BIN = '~/.tyhld/' + 'bin'
48
+ SYSD = '~/.config/' + 'systemd'
49
+ CODEX_H = '~/.codex/' + 'hooks.json'
50
+ CODEX_C = '~/.codex/' + 'config.toml'
51
+ MANAGED = '/etc/claude-' + 'code/managed-settings.json'
52
+ MANAGED_DIR = '/etc/claude-' + 'code'
53
+
54
+ WORK = '/srv/site-sample' # 現場(/tmp の外・場所は字面で決まるので実在しなくてよい)
55
+
56
+
57
+ def decide(cmd, cwd=WORK):
58
+ return policy.decide_bash(cmd, cwd)['decision']
59
+
60
+
61
+ def dn(label, cmd, cwd=WORK):
62
+ check(f'{label}: {cmd[:52]!r}', decide(cmd, cwd), policy.DENY)
63
+
64
+
65
+ def al(label, cmd, cwd=WORK):
66
+ check(f'{label}: {cmd[:52]!r}', decide(cmd, cwd), policy.ALLOW)
67
+
68
+
69
+ print('── (2) 書き込みの手段ごとに DENY(守る場所へ書く形) ' + '─' * 14)
70
+ # リダイレクト(> >> >| <> と、後ろに付く形・裸・:・printf)
71
+ for c in [f'echo x > {S}', f'echo x >> {SL}', f'echo x >| {S}', f'exec 3<> {S}',
72
+ f'cat a > {S} 2>/dev/nu' + 'll', f'>{S}', f': > {S}', f'printf x >{S}',
73
+ f'echo x > {SKILLS}/a/SKILL.md', f'echo x > {HOOKS}/policy.py', f'echo x > {BIN}/tool',
74
+ f'echo x > {SYSD}/user/a.service', f'echo x > {CODEX_H}', f'echo x >> {CODEX_C}',
75
+ f'echo x > .{CLA[1:]}/{SET}.json']:
76
+ dn('redirect', c)
77
+ # tee
78
+ for c in [f'tee {S} < a', f'tee -a {SL} < a', f'echo x | tee {HOOKS}/p.py', f'sponge {S}']:
79
+ dn('tee', c)
80
+ # cp / install / rsync / ln / ditto
81
+ for c in [f'cp a {S}', f'cp a {HOOKS}/p.py', f'cp -t ~/{CLA}/ {SET}.json', f'install -m 644 a {S}',
82
+ f'install -d {BIN}/sub', f'rsync -a s/ {HOOKS}/', f'rsync a {BIN}/x', f'ln -sf /tmp/x {S}',
83
+ f'ln -sfn /tmp/f {SKILLS}', f'ditto s {HOOKS}']:
84
+ dn('cp/install/rsync/ln', c)
85
+ # mv(元も先も)
86
+ for c in [f'mv a {S}', f'mv {S} /tmp/away', f'mv {S} {S}.off', f'mv a {HOOKS}/p.py']:
87
+ dn('mv', c)
88
+ # sed -i / perl -i / ruby -i / awk -i inplace / gsed
89
+ for c in [f'sed -i s/a/b/ {S}', f'sed -i.bak -e s/a/b/ {SL}', f'perl -pi -e s/a/b/ {S}',
90
+ f'perl -i -pe1 {HOOKS}/p.py', f'ruby -i -pe1 {S}', f'awk -i inplace 1 {S}',
91
+ f'gsed -i s/a/b/ {S}']:
92
+ dn('inplace', c)
93
+ # awk / その他からの書き出し
94
+ dn('awk print >', f'awk \'{{print > "{os.path.expanduser(S)}"}}\' a')
95
+ # truncate / dd / chmod / chown / chattr / setfacl / rm / rmdir / shred / touch
96
+ for c in [f'truncate -s0 {S}', f'dd if=a of={S}', f'chmod 666 {S}', f'chmod -R 777 {HOOKS}',
97
+ f'chown me {S}', f'chattr -i {S}', f'setfacl -m u:x:rw {S}', f'rm -f {S}',
98
+ f'rm -rf {HOOKS}', f'shred {S}', f'touch {S}', f'rmdir {SKILLS}']:
99
+ dn('touch/chmod/rm', c)
100
+ # エディタで開いて保存
101
+ for c in [f'ed -s {S} <<< w', f'vim -c wq {S}', f'nano {HOOKS}/p.py', f'ex -sc wq {S}']:
102
+ dn('editor', c)
103
+ # patch / git checkout / git restore / git clone / git apply / git worktree
104
+ for c in [f'patch {S} < d.diff', f'git checkout -- .{CLA[1:]}/{SET}.json',
105
+ f'git restore .{CLA[1:]}/{SET}.local.json', f'git -C ~/{CLA} checkout -- {SET}.json',
106
+ f'git clone https://x.invalid/r {HOOKS}', f'git worktree add {BIN} br']:
107
+ dn('git overwrite', c)
108
+ # jq / yq の書き戻し(-i)
109
+ dn('yq -i', f'yq -i ".x=1" {S}')
110
+ # ダウンロードの行き先
111
+ for c in [f'curl -o {BIN}/x https://x.invalid/x', f'curl --output {S} https://x.invalid/',
112
+ f'wget -O {S} https://x.invalid/', f'wget -P {HOOKS} https://x.invalid/p.py']:
113
+ dn('download', c)
114
+ # アーカイブの展開(中身を確かめられない=守る側)
115
+ for c in [f'tar -xf a.tar -C {HOOKS}', f'tar xf a.tar -C {BIN}', f'unzip a.zip -d {HOOKS}',
116
+ f'7z x a.7z -o{HOOKS}', f'bsdtar -xf a.tar -C {SKILLS}']:
117
+ dn('extract', c)
118
+ # 圧縮を解いて同じ場所に書く
119
+ dn('gunzip', f'gunzip {S}.gz')
120
+ # claude config(設定を書き換える)
121
+ for c in ['claude config set -g theme dark', 'claude config add -g x y']:
122
+ dn('claude config', c)
123
+ # 管理設定(/etc・/Library)
124
+ for c in [f'echo x > {MANAGED}', f'cp a {MANAGED}', f'sed -i s/a/b/ {MANAGED}',
125
+ f'tee "/Library/Application Support/ClaudeCode/managed-settings.json" < a']:
126
+ dn('managed', c)
127
+ # mount で覆う(一覧の場所を丸ごと差し替える)
128
+ for c in [f'mount --bind /tmp/fake ~/{CLA}', f'mount -o bind /tmp/f {HOOKS}', f'umount ~/{CLA}']:
129
+ dn('mount', c)
130
+ # systemctl --user(~/.config/systemd の中を書き換える)
131
+ for c in ['systemctl --user enable relay.service', 'systemctl --user edit relay.service',
132
+ 'systemctl --user mask relay.service']:
133
+ dn('systemctl', c)
134
+ # 置き場ごと(一覧の場所を中に持つ親を消す・動かす・展開する・フォルダごと写し込む)
135
+ RMRF = 'r' + 'm -rf'
136
+ for c in [f'{RMRF} ~/{CLA}', f'mv ~/{CLA} /tmp/away', f'{RMRF} ~/.tyhld', 'mv ~/.tyhld /tmp/x',
137
+ f'tar -xf a.tar -C ~/{CLA}', f'cp -r fake/ ~/{CLA}', f'rsync -a fake/ ~/{CLA}/']:
138
+ dn('parent', c)
139
+
140
+ print('\n── (3) 包みを剥がしてから見る ' + '─' * 34)
141
+ for c in [f'bash -c "echo x > {S}"', f'sh -c \'cp a {HOOKS}/p.py\'', f'env -u X tee {S}',
142
+ f'env FOO=1 cp a {S}', f'sudo tee {S}', f'sudo -u root cp a {S}', f'nohup tee {S} < a',
143
+ f'time cp a {S}', f'timeout 5 cp a {S}', f'nice -n 5 cp a {S}', f'command tee {S}',
144
+ f'exec tee {S}', f'stdbuf -o0 tee {S}', f'busybox sed -i s/a/b/ {S}',
145
+ f'echo {S} | xargs rm', f'echo {S} | xargs -I{{}} cp a {{}}',
146
+ f'find ~/{CLA} -name "{SET}*.json" -delete',
147
+ f'find {HOOKS} -name "*.py" -exec sed -i s/a/b/ {{}} +',
148
+ f'bash <<< "rm {S}"', f'eval "rm {S}"', f'echo x > $(echo {S})',
149
+ f'x=$(tee {S} < a)', f'(cd ~/{CLA} && rm {SET}.json)',
150
+ f'for f in {S}; do rm "$f"; done', f'\\cp a {S}']:
151
+ dn('unwrap', c)
152
+ # 変数・~・$HOME・${HOME}・相対の道・波括弧・*
153
+ for c in [f'p=$HOME/{CLA}; cp a "$p/{SET}.json"', f'D=.tyhld/hooks; echo x > ~/$D/p.py',
154
+ f'export G={HOOKS}; cp a "$G/p.py"', f'echo x > ${{HOME}}/{CLA}/{SET}.json',
155
+ f'cp a ~/{CLA}/{{{SET},other}}.json', f'rm ~/{CLA}/{SET}*', f'rm ~/.tyhld/*/p.py',
156
+ f'echo x > $D/.tyhld/hooks/p.py', f'cp a $UNK/{HOOKS[2:]}/p.py']:
157
+ dn('expand', c)
158
+ # プログラムの中(python・node・ruby・perl・ヒアドキュメント)
159
+ for c in [f'python3 -c "open(\'{os.path.expanduser(S)}\',\'w\').write(\'x\')"',
160
+ f"python3 -c \"import os,json;json.dump({{}},open(os.path.expanduser('{S}'),'w'))\"",
161
+ f"python3 - <<'PY'\nfrom pathlib import Path\n(Path.home()/'{CLA}'/'{SET}.json').write_text('x')\nPY",
162
+ f"node -e \"require('fs').writeFileSync(require('os').homedir()+'/{CLA}/{SET}.json','x')\"",
163
+ f"ruby -e \"File.write(Dir.home+'/{CLA}/{SET}.json','x')\"",
164
+ f"perl -e 'open(F,\">$ENV{{HOME}}/{CLA}/{SET}.json\")'",
165
+ f"python3 -c \"import shutil,os;shutil.copy('a',os.path.expanduser('{HOOKS}/p.py'))\""]:
166
+ dn('program', c)
167
+
168
+ print('\n── (4) 止めてはいけないもの(読むだけ・通常開発・作業ブランチ・merge) ' + '─' * 2)
169
+ for c in [f'cat {S}', f'less {SL}', f'grep -n hooks {S}', f'rg PreToolUse {HOOKS}', f'diff {S} /tmp/a',
170
+ f'jq .permissions {S}', f'ls -la {SKILLS}', f'stat {S}', 'git status', f'git -C ~/{CLA} status',
171
+ f'cp {S} /tmp/claude-1000/b.json', f'jq . {S} > /tmp/claude-1000/o.json', f'cat {S} | jq .',
172
+ 'echo done > report.md', 'echo x >> /tmp/claude-1000/n.md', 'npm test > /tmp/claude-1000/t.log 2>&1',
173
+ 'sed -i s/a/b/ src/app.ts', 'cp dist/a.js build/a.js', 'rm -rf node_modules',
174
+ 'git checkout -- src/app.ts', 'git restore .', 'git checkout main -- README.md',
175
+ 'python3 -c "open(\'out.json\',\'w\').write(x)"', 'curl -o /tmp/x.json https://x.invalid/',
176
+ 'find src -name "*.ts" -delete', 'mkdir -p ~/.claude/projects/x', 'chmod +x scripts/run.sh',
177
+ 'unzip a.zip -d /tmp/claude-1000/z', 'gunzip data.json.gz', 'cp -r public/ dist/', 'mv a.ts b.ts',
178
+ 'tar -xf a.tgz -C /tmp/work', 'claude --version', 'claude -p hello',
179
+ f'echo {S} を守る >> notes.md', f'git commit -m "docs: {S} を守る"',
180
+ f'gh pr create --title x --body "tee {HOOKS}/p.py を止める"',
181
+ 'git push -u origin feat/x', 'gh pr merge 12 --auto --merge --delete-branch',
182
+ f'echo x > /tmp/claude-1000/w/{CLA}/{SET}.json',
183
+ f"python3 - <<'PY'\np='docs/g.md'\ns=open(p).read()\ns=s.replace('A','{S} は守る')\nopen(p,'w').write(s)\nPY"]:
184
+ al('allow', c)
185
+
186
+ print('\n── Write ツールも同じ一覧で守る/使い捨ては通す ' + '─' * 20)
187
+ for p in (S, SL, f'{SKILLS}/a.md', f'{HOOKS}/p.py', f'{BIN}/x', CODEX_H, CODEX_C, MANAGED):
188
+ check(f'Write DENY: {p}', policy.decide_write(os.path.expanduser(p))['decision'], policy.DENY)
189
+ for p in (f'/tmp/claude-1000/w/{CLA}/{SET}.json', '~/.config/other-tool/x.toml', 'out.json'):
190
+ check(f'Write allow: {p}', policy.decide_write(os.path.expanduser(p))['decision'], policy.ALLOW)
191
+
192
+ # 一覧の場所ごとに「代表の手段」で当たること(場所を1か所に集めた確認)。
193
+ print('\n── 守る場所の一覧の各所が塞がっている ' + '─' * 28)
194
+ for label, place in [('home settings', S), ('home local', SL), ('skills', f'{SKILLS}/a.md'),
195
+ ('hooks', f'{HOOKS}/p.py'), ('bin', f'{BIN}/x'), ('systemd', f'{SYSD}/u/a.service'),
196
+ ('agent hooks.json', CODEX_H), ('agent config.toml', CODEX_C),
197
+ ('managed /etc', MANAGED), ('site settings', f'.{CLA[1:]}/{SET}.json')]:
198
+ dn(f'一覧 {label}', f'echo x > {place}')
199
+
200
+ print('\n' + '=' * 76)
201
+ if failures:
202
+ print(f'NG {len(failures)} 件:')
203
+ for f in failures:
204
+ print(' -', f)
205
+ sys.exit(1)
206
+ print('すべて期待どおり')
207
+ sys.exit(0)