@tyhld/conductor 0.12.22 → 0.12.24
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.
- package/package.json +1 -1
- package/scripts/hooks/README.md +3 -1
- package/scripts/hooks/policy.py +128 -0
- package/scripts/hooks/test_deny_remaining_routes.py +4 -2
- package/scripts/hooks/test_env_write_parity.py +100 -0
- package/scripts/hooks/test_policy.py +17 -5
- package/scripts/update-guard.sh +29 -0
- package/skills/conductor-craftsman/SKILL.md +7 -2
package/package.json
CHANGED
package/scripts/hooks/README.md
CHANGED
|
@@ -126,7 +126,9 @@ branch protection は「CI が通っていない物を main に入れない」
|
|
|
126
126
|
5. **SQL の字面**は、実行しうる入口のときだけ見ます。入口とは、DB の道具・移行の道具・その場で走らせる形
|
|
127
127
|
(`python3 -c`・`node -e`・`python3 - <<EOF`・DB の道具へ流す形)・知らない実行ファイルです。
|
|
128
128
|
6. 本番反映は、`wrangler deploy`(`--dry-run` を除く)・`wrangler versions/pages deploy`・`vercel --prod`・
|
|
129
|
-
`vercel promote/alias/rollback`・`
|
|
129
|
+
`vercel promote/alias/rollback`・`vercel domains add/rm/buy/move/transfer`(本番ドメインの変更・便 6d6f2ff8)・
|
|
130
|
+
`npm/pnpm/yarn publish`・`supabase functions deploy`・「deploy の語+`--prod`」です。
|
|
131
|
+
★Preview への公開(`vercel`・`vercel deploy`・`--prod` 無し)と、読み取り(`vercel domains ls`・`vercel ls` 等)は自走です。
|
|
130
132
|
|
|
131
133
|
★**止める側(deny)は変えていません**。危険と抜け道の判定は、今までどおり機械的な区切り(`split_segments`)で見ます
|
|
132
134
|
(引用符の中まで見る方が、止める側としては広いため)。7日分の記録では、DENY の件数は前後で同じ(1,559件)でした。
|
package/scripts/hooks/policy.py
CHANGED
|
@@ -812,6 +812,9 @@ def _wrapper_danger(command: str, depth: int = 0, cwd=None):
|
|
|
812
812
|
if hit:
|
|
813
813
|
return f'{hit} ({where})'
|
|
814
814
|
hit = _guard_write_scan(inner, cwd)
|
|
815
|
+
if hit:
|
|
816
|
+
return f'{hit} ({where})'
|
|
817
|
+
hit = _env_write_scan(inner, cwd)
|
|
815
818
|
if hit:
|
|
816
819
|
return f'{hit} ({where})'
|
|
817
820
|
for seg in _real_segments(inner):
|
|
@@ -2421,6 +2424,115 @@ def _git_write_targets(args, cwd):
|
|
|
2421
2424
|
return where, []
|
|
2422
2425
|
|
|
2423
2426
|
|
|
2427
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
2428
|
+
# .env 類への書き込み(DENY・便 3d11f913=便2.8)
|
|
2429
|
+
#
|
|
2430
|
+
# 【なぜ要るか(調査 55283164 の重大発見1)】Write/Edit ツールは設定 deny
|
|
2431
|
+
# (sales-template/settings.json の Write/Edit(**/.env) と Write/Edit(**/.env.*))で
|
|
2432
|
+
# .env.example への書き込みが止まるのに、Bash(echo・tee・sed -i・cp/mv)は番人の雛形除外で
|
|
2433
|
+
# 通っていた=中身を見ずに .env.example に任意の中身を書ける抜け道。書き込みの結果を
|
|
2434
|
+
# Write/Edit の設定 deny とそろえる。
|
|
2435
|
+
# 【範囲】設定 deny と同じ=書き込む【先】のファイル名が `.env`(本体)か `.env.` で始まるもの
|
|
2436
|
+
# (.env.local・.env.production・.env.example など雛形も含む)。★読み込みは変えない(雛形は
|
|
2437
|
+
# 今までどおり読める・本物は読めない=secret_reason のまま)。
|
|
2438
|
+
# 【誤検知を避ける】書き込む【先】だけを見る。`cp .env.example backup.txt`(雛形を別名へ写す)は
|
|
2439
|
+
# 先が backup.txt なので通す。フォルダ宛て(末尾 / ・-t・複数元)のときだけ、元の名前が先の
|
|
2440
|
+
# フォルダに .env.* として書かれるかを見る。
|
|
2441
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
2442
|
+
|
|
2443
|
+
def _is_env_family(name: str) -> bool:
|
|
2444
|
+
"""設定 deny `**/.env`・`**/.env.*` と同じ範囲か(書き込み先の名前にだけ使う)。"""
|
|
2445
|
+
return name == '.env' or name.startswith('.env.')
|
|
2446
|
+
|
|
2447
|
+
|
|
2448
|
+
def _env_base(path: str) -> str:
|
|
2449
|
+
"""書き込み先の字面から、飾りを外した末尾の名前(basename)。"""
|
|
2450
|
+
p = normalize_redirect_target(path).replace('\\', '/').rstrip('/')
|
|
2451
|
+
return p.split('/')[-1]
|
|
2452
|
+
|
|
2453
|
+
|
|
2454
|
+
# 引数ぜんぶが書き込み先になる道具(設定 deny と同じ結果へそろえるための最小集合)。
|
|
2455
|
+
_ENV_WRITE_ALL_VERBS = frozenset({'tee', 'sponge', 'truncate', 'touch'})
|
|
2456
|
+
# 書き込み先が dest(末尾の位置引数か -t の値)になる道具。
|
|
2457
|
+
_ENV_WRITE_DEST_VERBS = frozenset({'cp', 'install', 'mv', 'rsync', 'ditto', 'ln', 'scp'})
|
|
2458
|
+
|
|
2459
|
+
|
|
2460
|
+
def _env_write_dests(verb: str, args, tokens, pos):
|
|
2461
|
+
"""1コマンドが .env 類へ書き込む可能性のある「先の名前」を集める(書き込みにだけ使う)。"""
|
|
2462
|
+
paths = [a for a in args if not a.startswith('-')]
|
|
2463
|
+
if verb == 'dd':
|
|
2464
|
+
return [a[3:] for a in args if a.startswith('of=')]
|
|
2465
|
+
if verb in _ENV_WRITE_ALL_VERBS:
|
|
2466
|
+
return paths
|
|
2467
|
+
if verb in _INPLACE_VERBS and (
|
|
2468
|
+
is_inplace_edit('sed' if verb in ('gsed', 'yq') else verb, tokens[pos:])
|
|
2469
|
+
or any(re.match(r'^-[a-zA-Z]*i', a) for a in args if not a.startswith('--'))
|
|
2470
|
+
or (verb in _AWK_VERBS and 'inplace' in args)):
|
|
2471
|
+
return paths # その場書き換え=その名前へ書く
|
|
2472
|
+
if verb in _ENV_WRITE_DEST_VERBS:
|
|
2473
|
+
tdir = None
|
|
2474
|
+
# ★値を1つ取る旗(-m 644・-o user 等)を飛ばして、本当の位置引数だけを数える。
|
|
2475
|
+
pos_args, skip = [], False
|
|
2476
|
+
for k, a in enumerate(args):
|
|
2477
|
+
if skip:
|
|
2478
|
+
skip = False
|
|
2479
|
+
continue
|
|
2480
|
+
if a in ('-t', '--target-directory') and k + 1 < len(args):
|
|
2481
|
+
tdir = args[k + 1]
|
|
2482
|
+
skip = True
|
|
2483
|
+
continue
|
|
2484
|
+
if a.startswith('--target-directory='):
|
|
2485
|
+
tdir = a.split('=', 1)[1]
|
|
2486
|
+
continue
|
|
2487
|
+
if a in _ENV_DEST_VALUE_FLAGS and k + 1 < len(args):
|
|
2488
|
+
skip = True
|
|
2489
|
+
continue
|
|
2490
|
+
if not a.startswith('-'):
|
|
2491
|
+
pos_args.append(a)
|
|
2492
|
+
if verb == 'install' and '-d' in args:
|
|
2493
|
+
return pos_args # ディレクトリ作成
|
|
2494
|
+
if tdir is not None:
|
|
2495
|
+
return [_env_base(s) for s in pos_args] # 各元がそのフォルダへ同じ名前で入る
|
|
2496
|
+
if not pos_args:
|
|
2497
|
+
return []
|
|
2498
|
+
dest, srcs = pos_args[-1], pos_args[:-1]
|
|
2499
|
+
if ':' in dest.split('/')[0] and verb in ('scp', 'rsync'):
|
|
2500
|
+
return [] # 別の機械への書き込み(ここは見ない)
|
|
2501
|
+
# ★書き込む先はまず dest 自身。フォルダ宛て(末尾 / ・複数元)なら元の名前も入る。
|
|
2502
|
+
out = [dest]
|
|
2503
|
+
if dest.endswith('/') or len(srcs) > 1:
|
|
2504
|
+
out += [_env_base(s) for s in srcs]
|
|
2505
|
+
return out
|
|
2506
|
+
return []
|
|
2507
|
+
|
|
2508
|
+
|
|
2509
|
+
# cp/install/mv などで値を1つ取る旗(この値は元でも先でもない)。
|
|
2510
|
+
_ENV_DEST_VALUE_FLAGS = frozenset({
|
|
2511
|
+
'-m', '--mode', '-o', '--owner', '-g', '--group', '-S', '--suffix', '--backup',
|
|
2512
|
+
})
|
|
2513
|
+
|
|
2514
|
+
|
|
2515
|
+
def _env_write_scan(command: str, cwd=None):
|
|
2516
|
+
"""コマンド全体で、.env 類(雛形を含む)への書き込みを探す(本物の区切り・書き込み先だけ)。"""
|
|
2517
|
+
if not command:
|
|
2518
|
+
return None
|
|
2519
|
+
for seg in split_commands(command, bodies=False):
|
|
2520
|
+
s = _normalize_segment(seg)
|
|
2521
|
+
for t in _real_redirect_targets(s):
|
|
2522
|
+
if _is_env_family(_env_base(t)):
|
|
2523
|
+
return f'env file write (redirect into {_env_base(t)})'
|
|
2524
|
+
head = s.split('\n', 1)[0] if '<<' in s else s
|
|
2525
|
+
toks = tokens_of(head)
|
|
2526
|
+
p, _ = _unwrap(toks)
|
|
2527
|
+
if p >= len(toks):
|
|
2528
|
+
continue
|
|
2529
|
+
verb = toks[p].split('/')[-1]
|
|
2530
|
+
for d in _env_write_dests(verb, _plain_args(toks, p + 1), toks, p):
|
|
2531
|
+
if _is_env_family(_env_base(d)):
|
|
2532
|
+
return f'env file write ({_env_base(d)} by {verb})'
|
|
2533
|
+
return None
|
|
2534
|
+
|
|
2535
|
+
|
|
2424
2536
|
def _guard_write_segment(segment: str, cwd=None, command='', depth=0, env=None):
|
|
2425
2537
|
"""本物の1コマンドが番人の設定を書き換えるなら理由。"""
|
|
2426
2538
|
segment = _normalize_segment(segment)
|
|
@@ -2804,6 +2916,13 @@ def _gate_release(words, tokens):
|
|
|
2804
2916
|
for w in words[1:]:
|
|
2805
2917
|
if w in ('promote', 'alias', 'rollback'):
|
|
2806
2918
|
return f'vercel {w}'
|
|
2919
|
+
# ★本番ドメインの変更(追加・削除・移管・購入)だけ関門。ls・inspect は読み取りなので自走
|
|
2920
|
+
# (便 6d6f2ff8・えふさん確定 #1: プレビューは自走・本番へ影響する操作は関門・強める向きのみ)。
|
|
2921
|
+
if 'domains' in words[1:]:
|
|
2922
|
+
after = [w for w in words[words.index('domains') + 1:] if not w.startswith('-')]
|
|
2923
|
+
if after[:1] and after[0] in ('add', 'rm', 'remove', 'buy', 'move',
|
|
2924
|
+
'transfer', 'transfer-in'):
|
|
2925
|
+
return f'vercel domains {after[0]}'
|
|
2807
2926
|
if v == 'wrangler' and '--dry-run' not in tokens and (
|
|
2808
2927
|
words[1:2] in (['deploy'], ['publish'])
|
|
2809
2928
|
or words[1:3] in (['versions', 'deploy'], ['pages', 'deploy'])):
|
|
@@ -2934,6 +3053,11 @@ def decide_bash(command: str, cwd=None):
|
|
|
2934
3053
|
if hit:
|
|
2935
3054
|
return _r(DENY, f'danger: {hit}')
|
|
2936
3055
|
|
|
3056
|
+
# ---- 1c'') .env 類への書き込み(便 3d11f913・Write/Edit の設定 deny と結果をそろえる) ----
|
|
3057
|
+
hit = _env_write_scan(command, cwd)
|
|
3058
|
+
if hit:
|
|
3059
|
+
return _r(DENY, f'danger: {hit}')
|
|
3060
|
+
|
|
2937
3061
|
# ---- 1d) 抜け道(便 fc22bc34・main 直 push・保護の迂回と変更・秘密の値・検査の省略) ----
|
|
2938
3062
|
hit = _bypass_scan(command, cwd)
|
|
2939
3063
|
if hit:
|
|
@@ -2995,6 +3119,10 @@ def decide_write(file_path: str, cwd=None):
|
|
|
2995
3119
|
comps = [c for c in path.split('/') if c]
|
|
2996
3120
|
if '.tyhld' in comps:
|
|
2997
3121
|
return _r(DENY, 'danger: write to guard directory (.tyhld)')
|
|
3122
|
+
# ★.env 類(雛形を含む)への書き込みは、Bash と同じく止める(便 3d11f913・番人でも結果を一致)。
|
|
3123
|
+
# 読み込みは変えない(secret_reason は雛形を通す)。
|
|
3124
|
+
if _is_env_family(_env_base(path)):
|
|
3125
|
+
return _r(DENY, f'danger: env file write ({_env_base(path)})')
|
|
2998
3126
|
return _r(ALLOW, 'no gate, no danger')
|
|
2999
3127
|
|
|
3000
3128
|
|
|
@@ -186,11 +186,13 @@ SECRET_READS = [
|
|
|
186
186
|
]
|
|
187
187
|
for cmd in SECRET_READS:
|
|
188
188
|
expect('(2)', cmd, policy.DENY)
|
|
189
|
-
for cmd in [f'grep -n "x|y" {E}.example', f'cat {E}.example',
|
|
190
|
-
f'grep -n "{DBURL}|API_KEY" src/a.ts',
|
|
189
|
+
for cmd in [f'grep -n "x|y" {E}.example', f'cat {E}.example',
|
|
190
|
+
f'grep -n "{DBURL}|API_KEY" src/a.ts',
|
|
191
191
|
f'grep -rn "{DBURL}|API_KEY" src', f'git grep -n "{DBURL}|process\\.env"',
|
|
192
192
|
f'git ls-files | grep "\\{E}"', f'grep -n "x|y" README.md', f'rg -n "a|b" src']:
|
|
193
193
|
expect('(2) 雛形・名前を探すだけは allow', cmd, policy.ALLOW)
|
|
194
|
+
# ★雛形への【書き込み】は便 3d11f913 で止める(読みは通るが書きは止まる)。
|
|
195
|
+
expect('(2) 雛形への書き込みは deny', f'echo "NEW_KEY=" >> {E}.example', policy.DENY)
|
|
194
196
|
|
|
195
197
|
print('\n── (3) GitHub の API で main の先端を書き換える形 ' + '─' * 20)
|
|
196
198
|
API_DENY = [
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""便2.8(便 3d11f913): .env 類への書き込みで、Write/Edit と Bash の守りをそろえる試験。
|
|
3
|
+
|
|
4
|
+
実行: python3 scripts/hooks/test_env_write_parity.py
|
|
5
|
+
1件でも期待と違えば exit 1。
|
|
6
|
+
|
|
7
|
+
【背景(調査 55283164 の重大発見1)】Write/Edit ツールは設定 deny(**/.env・**/.env.*)で
|
|
8
|
+
.env.example への書き込みが止まるのに、Bash(echo・tee・sed -i・cp/mv)は番人の雛形除外で
|
|
9
|
+
中身を見ずに .env.example に書けた=抜け道。この便で Bash も止め、結果をそろえた。
|
|
10
|
+
|
|
11
|
+
【固定すること】
|
|
12
|
+
① Bash で .env.example・.env・.env.local への書き込みが3つとも止まる。
|
|
13
|
+
② Write/Edit と Bash の結果が一致(番人の判定で、書き込みは3経路とも deny)。
|
|
14
|
+
③ 雛形の読み込みは今までどおり通る・本物は止まる(★読み込みは変えない)。
|
|
15
|
+
④ は既存の D1 試験(他ファイル)で担保。⑤ 空振り確認は spot スクリプト(報告に記載)。
|
|
16
|
+
|
|
17
|
+
★見本は作り物の場所で行い、本物の .env・.env.example に1バイトも書かない。危険語は組み立てる。
|
|
18
|
+
"""
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
23
|
+
|
|
24
|
+
import policy # noqa: E402
|
|
25
|
+
|
|
26
|
+
failures = []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check(label, got, want):
|
|
30
|
+
ok = got == want
|
|
31
|
+
if not ok:
|
|
32
|
+
failures.append(f'{label}: got={got!r} want={want!r}')
|
|
33
|
+
print(f'{"OK " if ok else "NG "} {label:66} {str(got)[:24]}')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ENV = '.e' + 'nv' # 本体
|
|
37
|
+
EX = ENV + '.example' # 雛形
|
|
38
|
+
LOCAL = ENV + '.local' # 本物(値が入る)
|
|
39
|
+
CWD = '/home/fshim/projects/x'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def dbash(cmd):
|
|
43
|
+
return policy.decide_bash(cmd, CWD)['decision']
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def dwrite(name):
|
|
47
|
+
return policy.decide_write('/home/fshim/projects/x/' + name, CWD)['decision']
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def dread(name):
|
|
51
|
+
return policy.decide_read('/home/fshim/projects/x/' + name, CWD)['decision']
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
print('── ① Bash で .env.example・.env・.env.local への書き込みが3つとも止まる ' + '─' * 4)
|
|
55
|
+
# 手段を問わず(リダイレクト・tee・sed -i・cp・mv・dd・truncate・touch・install・ln・包み)。
|
|
56
|
+
for target in (EX, ENV, LOCAL):
|
|
57
|
+
for cmd in [f'echo x > {target}', f'echo x >> {target}', f'printf x > {target}', f': > {target}',
|
|
58
|
+
f'tee {target} < a', f'tee -a {target}', f'sed -i s/a/b/ {target}',
|
|
59
|
+
f'sed -i.bak -e s/a/b/ {target}', f'perl -pi -e s/a/b/ {target}',
|
|
60
|
+
f'cp a {target}', f'cp -f a {target}', f'install -m 644 a {target}', f'mv a {target}',
|
|
61
|
+
f'dd if=a of={target}', f'truncate -s0 {target}', f'touch {target}',
|
|
62
|
+
f'ln -sf /tmp/x {target}', f'cp a src/{target}', f'cp a dir/ && cp b {target}',
|
|
63
|
+
f'bash -c "echo x > {target}"', f"sh -c 'sed -i s/a/b/ {target}'",
|
|
64
|
+
f'env FOO=1 tee {target}', f'cat a > {target} 2>/dev/nu' + 'll']:
|
|
65
|
+
check(f'① deny: {cmd[:50]}', dbash(cmd), policy.DENY)
|
|
66
|
+
|
|
67
|
+
print('\n── ② Write/Edit と Bash の結果が一致(3経路とも番人が deny) ' + '─' * 6)
|
|
68
|
+
for target in (EX, ENV, LOCAL):
|
|
69
|
+
check(f'② Write deny {target}', dwrite(target), policy.DENY)
|
|
70
|
+
check(f'② Edit deny {target}',
|
|
71
|
+
policy.decide_event({'tool_name': 'Edit', 'tool_input': {'file_path': CWD + '/' + target}})['decision'],
|
|
72
|
+
policy.DENY)
|
|
73
|
+
check(f'② Bash deny {target}', dbash(f'echo x > {target}'), policy.DENY)
|
|
74
|
+
# 雛形の派生(.env.example.md も **/.env.* に当たる=設定 deny と一致)。
|
|
75
|
+
check('② Write deny .env.example.md', dwrite(EX + '.md'), policy.DENY)
|
|
76
|
+
|
|
77
|
+
print('\n── ③ 読み込みは今までどおり(雛形は読める・本物は読めない)★変えない ' + '─' * 2)
|
|
78
|
+
for cmd in [f'cat {EX}', f'less {EX}', f'grep -n NAME {EX}', f'diff {EX} /tmp/a', f'head {EX}',
|
|
79
|
+
f'bash -c "cat {EX}"']:
|
|
80
|
+
check(f'③ 雛形は読める: {cmd[:44]}', dbash(cmd), policy.ALLOW)
|
|
81
|
+
check('③ Read 雛形は allow', dread(EX), policy.ALLOW)
|
|
82
|
+
for cmd in [f'cat {ENV}', f'cat {LOCAL}', f'grep TOKEN {ENV}', f'bash -c "cat {ENV}"']:
|
|
83
|
+
check(f'③ 本物は読めない: {cmd[:44]}', dbash(cmd), policy.DENY)
|
|
84
|
+
check('③ Read 本物は deny', dread(ENV), policy.DENY)
|
|
85
|
+
|
|
86
|
+
print('\n── ★誤検知しない(雛形を別名ファイルへ写す・字面だけ・普通の開発) ' + '─' * 4)
|
|
87
|
+
for cmd in [f'cp {EX} backup.txt', f'cp {EX} /tmp/claude-1000/x', f'mv {EX} notes.txt',
|
|
88
|
+
f'cat {EX} | grep X', f'git commit -m "docs: {EX} の項目"', f'echo "{EX} を直す" >> notes.md',
|
|
89
|
+
'sed -i s/a/b/ src/app.ts', 'cp dist/a.js build/a.js', 'echo done > report.md',
|
|
90
|
+
'cp a b/', 'tee out.log < a']:
|
|
91
|
+
check(f'誤検知しない allow: {cmd[:44]}', dbash(cmd), policy.ALLOW)
|
|
92
|
+
|
|
93
|
+
print('\n' + '=' * 72)
|
|
94
|
+
if failures:
|
|
95
|
+
print(f'NG {len(failures)} 件:')
|
|
96
|
+
for f in failures:
|
|
97
|
+
print(' -', f)
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
print('すべて期待どおり')
|
|
100
|
+
sys.exit(0)
|
|
@@ -418,13 +418,19 @@ print('\n=== (b5) 経路を問わず同じ結論(書き込みツール・Bash
|
|
|
418
418
|
for fp in ['/home/fshim/projects/x/.env', '/home/fshim/projects/x/.env.local']:
|
|
419
419
|
r = policy.decide_write(fp, '/home/fshim/projects/x')
|
|
420
420
|
check(f'write deny 維持 {fp[:48]}', r['decision'], policy.DENY)
|
|
421
|
+
# ★雛形(.env.example)は【読める】が【書けない】(便 3d11f913・Write/Edit の設定 deny と結果を一致)。
|
|
421
422
|
for fp in ['/home/fshim/projects/x/.env.example', '/home/fshim/projects/x/.env.local.example']:
|
|
422
423
|
r = policy.decide_write(fp, '/home/fshim/projects/x')
|
|
423
|
-
check(f'write
|
|
424
|
+
check(f'write deny 雛形も書けない {fp[:44]}', r['decision'], policy.DENY)
|
|
424
425
|
for cmd in ['cat .env', 'cat .env.local', 'sed -i "s/a/b/" .env']:
|
|
425
426
|
check(f'bash deny 維持 {cmd[:48]}', policy.decide_bash(cmd)['decision'], policy.DENY)
|
|
426
|
-
|
|
427
|
-
|
|
427
|
+
# 読み込みは今までどおり雛形を通す(本文の除外は変えない)。
|
|
428
|
+
for cmd in ['cat .env.example', 'cat config.sample', 'grep -n NAME .env.example']:
|
|
429
|
+
check(f'bash 雛形は読める {cmd[:48]}', policy.decide_bash(cmd)['decision'], policy.ALLOW)
|
|
430
|
+
# ★書き込みは雛形でも止める(Bash 経路の抜け道を塞ぐ・便 3d11f913)。
|
|
431
|
+
for cmd in ['sed -i "s/a/b/" .env.example', 'echo x > .env.example', 'tee .env.example < a',
|
|
432
|
+
'cp a .env.example']:
|
|
433
|
+
check(f'bash 雛形への書き込みは deny {cmd[:44]}', policy.decide_bash(cmd)['decision'], policy.DENY)
|
|
428
434
|
|
|
429
435
|
# ★包んだ書き方でも同じ結論にする。`cat .env` は止まるのに `bash -c "cat .env"` は
|
|
430
436
|
# 素通りしていた(調査 d7a1c150 で実測)。ラッパの中身を取り出して測り直す。
|
|
@@ -1606,8 +1612,11 @@ for cmd in ['cat README.md', 'grep -rn relayTick src', 'head -20 package.json',
|
|
|
1606
1612
|
# (g-6) ★書き込みの禁止が1つも変わっていない(判定点を1つにしても後退しない)
|
|
1607
1613
|
for p in SECRET_PATHS:
|
|
1608
1614
|
check(f'write deny {p[len(FAKE):][:48]}', ev('Write', {'file_path': p})['decision'], policy.DENY)
|
|
1615
|
+
# ★.env 類(雛形含む)への書き込みは deny(便 3d11f913)。ほかの普通のファイルは allow。
|
|
1609
1616
|
for p in PLAIN_PATHS:
|
|
1610
|
-
|
|
1617
|
+
want = policy.DENY if policy._is_env_family(os.path.basename(p)) else policy.ALLOW
|
|
1618
|
+
label = 'write deny .env系' if want == policy.DENY else 'write allow'
|
|
1619
|
+
check(f'{label} {p[len(FAKE):][:41]}', ev('Write', {'file_path': p})['decision'], want)
|
|
1611
1620
|
|
|
1612
1621
|
# (g-7) ★空振り確認=判定点(secret_reason)を外すと、上の deny が全部 allow に落ちる
|
|
1613
1622
|
# これが落ちないなら「読み取りの禁止が secret_reason を通っていない」=写しの一覧を
|
|
@@ -1619,7 +1628,10 @@ try:
|
|
|
1619
1628
|
for p in SECRET_PATHS:
|
|
1620
1629
|
if ev('Read', {'file_path': p})['decision'] != policy.ALLOW:
|
|
1621
1630
|
broke.append(f'Read {p}')
|
|
1622
|
-
|
|
1631
|
+
# ★.env 類の書き込みは secret_reason とは別の専用の判定(便 3d11f913)で止まるので、
|
|
1632
|
+
# secret_reason を外しても allow に落ちない=ここでは数えない(別の空振りで固定)。
|
|
1633
|
+
if not policy._is_env_family(os.path.basename(p)) \
|
|
1634
|
+
and ev('Write', {'file_path': p})['decision'] != policy.ALLOW:
|
|
1623
1635
|
broke.append(f'Write {p}')
|
|
1624
1636
|
if ev('Grep', {'pattern': 'x', 'path': p})['decision'] != policy.ALLOW:
|
|
1625
1637
|
broke.append(f'Grep {p}')
|
package/scripts/update-guard.sh
CHANGED
|
@@ -1028,6 +1028,35 @@ if "allow" in perms:
|
|
|
1028
1028
|
del perms["allow"]
|
|
1029
1029
|
changes.append(f"permissions.allow を全削除({n} 件・関門素通りの原因を除去。deny は非改変)")
|
|
1030
1030
|
|
|
1031
|
+
# --- (2c) 自動許可封じのキー名・値を正す(便 6d6f2ff8=便2.7) ---
|
|
1032
|
+
# 【なぜ要るか(調査 6fd6eee0 の重大発見3)】utatane-backend の設定は、bypassPermissions モード
|
|
1033
|
+
# (全部自動で許可)を封じるキーが disableBypassPermissions: true になっていて、正しい名前と違う。
|
|
1034
|
+
# 間違ったキー名は Claude Code から無視される=封じが効かない。見本と他の全現場は
|
|
1035
|
+
# disableBypassPermissionsMode: "disable"。
|
|
1036
|
+
# 【正しいキーと値(出典)】permissions.disableBypassPermissionsMode を "disable" にする。
|
|
1037
|
+
# 出典: https://code.claude.com/docs/en/permissions (91行・632行。確認日 2026-09-18)
|
|
1038
|
+
# 「set permissions.disableBypassPermissionsMode ... to "disable" in any settings file」。
|
|
1039
|
+
# ★managed settings が最も強いが、どのスコープの settings でも効く(同ページ 632行)。
|
|
1040
|
+
# ★触るのはこのキーだけ。allow・ask・deny・hooks・sandbox・defaultMode には一切触れない。
|
|
1041
|
+
# ★冪等: 既に正しい名前と値なら何もしない(2回流しても差分は出ない)。
|
|
1042
|
+
_BYPASS_WRONG = "disableBypassPermissions" # 誤ったキー名(値も true 等)
|
|
1043
|
+
_BYPASS_RIGHT = "disableBypassPermissionsMode" # 正しいキー名
|
|
1044
|
+
_BYPASS_VALUE = "disable" # 正しい値(文字列・公式)
|
|
1045
|
+
if _BYPASS_WRONG in perms:
|
|
1046
|
+
_had = perms.pop(_BYPASS_WRONG)
|
|
1047
|
+
if perms.get(_BYPASS_RIGHT) != _BYPASS_VALUE:
|
|
1048
|
+
perms[_BYPASS_RIGHT] = _BYPASS_VALUE
|
|
1049
|
+
changes.append(
|
|
1050
|
+
f'自動許可封じのキーを是正: {_BYPASS_WRONG}={json.dumps(_had)} → '
|
|
1051
|
+
f'{_BYPASS_RIGHT}="{_BYPASS_VALUE}"(誤ったキー名は本体に無視され封じが効かない)')
|
|
1052
|
+
else:
|
|
1053
|
+
changes.append(f'古い重複キー {_BYPASS_WRONG} を削除({_BYPASS_RIGHT} は既に正しい)')
|
|
1054
|
+
elif _BYPASS_RIGHT in perms and perms.get(_BYPASS_RIGHT) != _BYPASS_VALUE:
|
|
1055
|
+
# 正しいキー名だが値が "disable" 以外(例 true)=公式の値へ揃えて確実に効かせる。
|
|
1056
|
+
_old = perms[_BYPASS_RIGHT]
|
|
1057
|
+
perms[_BYPASS_RIGHT] = _BYPASS_VALUE
|
|
1058
|
+
changes.append(f'自動許可封じの値を是正: {_BYPASS_RIGHT}={json.dumps(_old)}→"{_BYPASS_VALUE}"')
|
|
1059
|
+
|
|
1031
1060
|
# --- (3) サンドボックスのネット許可の不足分を追記 ---
|
|
1032
1061
|
# 職人が通信するたびに Claude Code のサンドボックスが「Network request outside of sandbox」を
|
|
1033
1062
|
# 聞いてくる。これは番人(cc2_guard)の判定とは別の OS レベルのゲートなので、番人では消えない。
|
|
@@ -8,9 +8,9 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
|
|
|
8
8
|
采配くんの便(プロンプト末尾に `CONDUCTOR_JOB:<id>` の印が付いた作業指示)は、
|
|
9
9
|
非エンジニアのお客さまの現場で、人が横に付かないまま実行される。
|
|
10
10
|
だから「速さ」より「戻せること・止まれること・報せること」を優先する。
|
|
11
|
-
以下の
|
|
11
|
+
以下の14項を、着手前に読み、作業中も判断の基準にすること。
|
|
12
12
|
|
|
13
|
-
## 鉄則
|
|
13
|
+
## 鉄則14項
|
|
14
14
|
|
|
15
15
|
1. **着手時の確認と枝分かれ** — `git status && git log --oneline -3 && git stash list` を確認してから main を最新化し、新ブランチを切る。
|
|
16
16
|
- ★**リポ全体の差分が「読めないファイル」で落ちたとき**(`unsupported file type` / `cannot hash <名前>` が出る/`.npmrc` のような設定が身に覚えなく `M` で出る)。**これは壊れていない。**
|
|
@@ -56,3 +56,8 @@ description: 采配くんの便(プロンプト末尾に CONDUCTOR_JOB:<id>
|
|
|
56
56
|
- **実機検品・検証用の使い捨てファイル(HTML・画像・ログ等)は、リポジトリの中に作らない。`/tmp/claude-1000/` に置く。リポジトリ直下に作ってしまった場合は自分で消そうとせず、報告に「残骸あり・場所」と書く。**
|
|
57
57
|
- **なぜ。** 実機 2026-09-03: ai-saku の画面便(PR#258・PR#269)で、検品用の使い捨てページ `_layout-check.html` をリポジトリ直下に作り、消そうとして番人(`rm` on dangerous path)に止められ、人が2回 `rm` した。**番人の `rm` 制限は正しい(緩めない)。悪いのは置き場。**
|
|
58
58
|
- 置き場を `/tmp/claude-1000/` にすれば、番人に止められることも、残骸がコミットに混ざることも起きない(根治=鉄則10)。Playwright の検品ページも同じ(鉄則8-④)。
|
|
59
|
+
14. **`.env` 類(`.env`・`.env.local`・`.env.example` などの雛形も含む)は職人が書き換えない(えふさん確定 2026-09-18・便 3d11f913)**
|
|
60
|
+
- **番人は `.env` 本体も `.env.example` などの雛形も、書き込みを止める**(Write/Edit の設定 deny と、Bash の `echo`/`tee`/`sed -i`/`cp`/`mv` などを、経路を問わず同じ結果にそろえた)。読み込みは今までどおり(雛形は読める・本物は読めない)。
|
|
61
|
+
- **なぜ。** 雛形(`.env.example`)は普通は秘密値を置かないが、「exampleだから常に安全」と番人へ例外を作ると、将来誤って実値が書かれたときの穴になる(利便性より安全側・えふさん確定)。
|
|
62
|
+
- **雛形(`.env.example` など)を直すときの正しい形=「案を作る→人が1回で反映」。** 職人は変更後の中身を**リポジトリの外(`/tmp/claude-1000/` の下)**に作り、報告に次の2つを載せる。① 変更の**差分**(何を足す/消すか)。② 人が**素のターミナルで1回貼るだけで反映できる1つのコマンド**(例:`cp /tmp/claude-1000/<下書き> <リポ>/.env.example`)。★このコマンドは**人が自分のターミナルで**打つ(職人の番人は人の端末には掛からない)。職人が `.env.example` を直接書き換えようとしても番人が止めるので、**回避せず、この形で案を出す**。
|
|
63
|
+
- **本物の `.env`・`.env.local` は、これまでどおり中身も書き込みも扱わない**(秘密の値は1文字も読まない・書かない=鉄則12の金庫)。
|