@tyhld/conductor 0.12.44 → 0.12.45
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
CHANGED
package/scripts/hooks/policy.py
CHANGED
|
@@ -3406,6 +3406,240 @@ def _gate_command(command: str, depth: int = 0):
|
|
|
3406
3406
|
return None
|
|
3407
3407
|
|
|
3408
3408
|
|
|
3409
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
3410
|
+
# 作法(DENY・便 977507a5・えふさん確定のトークン対策4「機械処理化」)
|
|
3411
|
+
#
|
|
3412
|
+
# 【何のため】次の3件は、毎便の本文に「やらない」と書いてきたが、番人は通していた(棚卸し 8ceae57d の
|
|
3413
|
+
# 実測で allow)。番人で止められれば、便の本文からその行を外せる(書かなくても守られる)。
|
|
3414
|
+
# ① git add -A(--all・-A を含む短い旗のまとめ・git add . ・git add :/)
|
|
3415
|
+
# ② cd を && でほかの命令とつなげる/cd とリダイレクトを同じ命令の行に混ぜる
|
|
3416
|
+
# ③ リポ全体・ホーム全体を指す再帰の検索(grep -r・rg・ag・ack・find)
|
|
3417
|
+
# 【★確実にその形と言える物だけを止める】命令の1行目を、引用符を守って字句に割ってから見る
|
|
3418
|
+
# (ヒアドキュメントの本文・引用符の中の文字は命令として読まない)。行き先が変数などで決まらない
|
|
3419
|
+
# 検索は止めない(誤って止めて職人が詰まらないため)。
|
|
3420
|
+
# 【★ほかの判定は変えない】decide_bash のいちばん最後(危険・関門をすべて当てたあと)に置く。
|
|
3421
|
+
# 今まで止まっていた物・人に聞いていた物は、理由も結果もそのまま。
|
|
3422
|
+
# ═════════════════════════════════════════════════════════════════════════════
|
|
3423
|
+
|
|
3424
|
+
_DISCIPLINE_OPS = frozenset({'&&', '||', ';', '|', '&', '(', ')', ';;', '|&'})
|
|
3425
|
+
_DISCIPLINE_REDIRECTS = frozenset({'>', '>>', '<', '>&', '&>', '>|', '<>', '&>>', '<&'})
|
|
3426
|
+
|
|
3427
|
+
_DISC_GIT_ADD = ('git add はまとめて足さない(-A・--all・. は秘密や無関係の変更まで入る)。'
|
|
3428
|
+
'足すファイルを名指しで git add path/to/file(鉄則1)')
|
|
3429
|
+
_DISC_CD = ('cd を && でほかの命令とつなげない・cd とリダイレクトを同じ行に混ぜない(許可の確認で止まる)。'
|
|
3430
|
+
'cd は作業のはじめに1回だけ単独で打ち、以後は絶対パスか git -C を使う(鉄則2)')
|
|
3431
|
+
_DISC_SEARCH = ('リポ全体・ホーム全体を再帰で検索しない(重く、秘密の置き場にも当たる)。'
|
|
3432
|
+
'探す場所を名指しで(例: rg 語 src/ ・ find src -name 名前)')
|
|
3433
|
+
|
|
3434
|
+
|
|
3435
|
+
def _discipline_tokens(command: str):
|
|
3436
|
+
"""命令の1行目を、引用符を守って語と演算子に割る。割れなければ None(=判定しない)。"""
|
|
3437
|
+
first = (command or '').split('\n', 1)[0]
|
|
3438
|
+
if '<<' in first:
|
|
3439
|
+
first = first.split('<<', 1)[0] # ヒアドキュメントの始まりより後ろは見ない
|
|
3440
|
+
try:
|
|
3441
|
+
lx = shlex.shlex(first, posix=True, punctuation_chars=True)
|
|
3442
|
+
lx.whitespace_split = True
|
|
3443
|
+
return list(lx)
|
|
3444
|
+
except ValueError:
|
|
3445
|
+
return None
|
|
3446
|
+
|
|
3447
|
+
|
|
3448
|
+
def _discipline_commands(toks):
|
|
3449
|
+
"""字句の並びを、演算子で区切った1コマンドずつ [(前の演算子, 語の並び, 後ろの演算子)] にする。"""
|
|
3450
|
+
out, cur, before = [], [], None
|
|
3451
|
+
skip_next = False
|
|
3452
|
+
for t in toks:
|
|
3453
|
+
if skip_next:
|
|
3454
|
+
skip_next = False
|
|
3455
|
+
continue
|
|
3456
|
+
if t in _DISCIPLINE_OPS:
|
|
3457
|
+
if cur:
|
|
3458
|
+
out.append([before, cur, t])
|
|
3459
|
+
cur, before = [], t
|
|
3460
|
+
elif t in _DISCIPLINE_REDIRECTS:
|
|
3461
|
+
# リダイレクトとその行き先は、命令の語ではない(2>/dev/null の 2 も外す)。
|
|
3462
|
+
if cur and cur[-1].isdigit():
|
|
3463
|
+
cur.pop()
|
|
3464
|
+
skip_next = True
|
|
3465
|
+
else:
|
|
3466
|
+
cur.append(t)
|
|
3467
|
+
if cur:
|
|
3468
|
+
out.append([before, cur, None])
|
|
3469
|
+
return out
|
|
3470
|
+
|
|
3471
|
+
|
|
3472
|
+
def _disc_cd(cmds, toks):
|
|
3473
|
+
has_cd = any(words and words[0] == 'cd' for _, words, _ in cmds)
|
|
3474
|
+
if not has_cd:
|
|
3475
|
+
return None
|
|
3476
|
+
for before, words, after in cmds:
|
|
3477
|
+
if words and words[0] == 'cd' and ('&&' in (before, after)):
|
|
3478
|
+
return _DISC_CD
|
|
3479
|
+
if any(t in _DISCIPLINE_REDIRECTS for t in toks):
|
|
3480
|
+
return _DISC_CD
|
|
3481
|
+
return None
|
|
3482
|
+
|
|
3483
|
+
|
|
3484
|
+
def _disc_git_add(words):
|
|
3485
|
+
if not words or words[0].split('/')[-1] != 'git':
|
|
3486
|
+
return None
|
|
3487
|
+
i = 1
|
|
3488
|
+
while i < len(words) and words[i].startswith('-'):
|
|
3489
|
+
# git の全体の旗(-C 場所・-c 設定 は値を1つ取る)。
|
|
3490
|
+
i += 2 if words[i] in ('-C', '-c') else 1
|
|
3491
|
+
if i >= len(words) or words[i] != 'add':
|
|
3492
|
+
return None
|
|
3493
|
+
after_dd = False
|
|
3494
|
+
for a in words[i + 1:]:
|
|
3495
|
+
if not after_dd and a == '--':
|
|
3496
|
+
after_dd = True
|
|
3497
|
+
continue
|
|
3498
|
+
if not after_dd and a.startswith('-'):
|
|
3499
|
+
if a in ('-A', '--all') or (not a.startswith('--') and 'A' in a[1:]):
|
|
3500
|
+
return _DISC_GIT_ADD
|
|
3501
|
+
continue
|
|
3502
|
+
if a in ('.', './', ':/', ':'):
|
|
3503
|
+
return _DISC_GIT_ADD
|
|
3504
|
+
return None
|
|
3505
|
+
|
|
3506
|
+
|
|
3507
|
+
def _disc_repo_top(cwd):
|
|
3508
|
+
d = os.path.normpath(cwd) if cwd and os.path.isabs(cwd) else None
|
|
3509
|
+
while d:
|
|
3510
|
+
if os.path.exists(os.path.join(d, '.git')):
|
|
3511
|
+
return d
|
|
3512
|
+
parent = os.path.dirname(d)
|
|
3513
|
+
if parent == d:
|
|
3514
|
+
return None
|
|
3515
|
+
d = parent
|
|
3516
|
+
return None
|
|
3517
|
+
|
|
3518
|
+
|
|
3519
|
+
def _disc_is_whole(target, cwd):
|
|
3520
|
+
"""その行き先が「リポ全体・ホーム全体(またはそれより広い所)」か。決まらなければ False。"""
|
|
3521
|
+
home = os.path.normpath(os.path.expanduser('~'))
|
|
3522
|
+
t = target
|
|
3523
|
+
for v in ('${HOME}', '$HOME'):
|
|
3524
|
+
if t == v or t.startswith(v + '/'):
|
|
3525
|
+
t = home + t[len(v):]
|
|
3526
|
+
if '$' in t or '`' in t or '*' in t or '?' in t:
|
|
3527
|
+
return False
|
|
3528
|
+
t = os.path.expanduser(t)
|
|
3529
|
+
if not os.path.isabs(t):
|
|
3530
|
+
if not (cwd and os.path.isabs(cwd)):
|
|
3531
|
+
return False
|
|
3532
|
+
t = os.path.join(cwd, t)
|
|
3533
|
+
t = os.path.normpath(t)
|
|
3534
|
+
wide = {'/', home}
|
|
3535
|
+
d = home
|
|
3536
|
+
while d != '/':
|
|
3537
|
+
d = os.path.dirname(d)
|
|
3538
|
+
wide.add(d)
|
|
3539
|
+
top = _disc_repo_top(cwd)
|
|
3540
|
+
if top:
|
|
3541
|
+
wide.add(top)
|
|
3542
|
+
d = top
|
|
3543
|
+
while d != '/':
|
|
3544
|
+
d = os.path.dirname(d)
|
|
3545
|
+
wide.add(d)
|
|
3546
|
+
return t in wide
|
|
3547
|
+
|
|
3548
|
+
|
|
3549
|
+
def _disc_search(words, cwd):
|
|
3550
|
+
if not words:
|
|
3551
|
+
return None
|
|
3552
|
+
v = words[0].split('/')[-1]
|
|
3553
|
+
targets = None
|
|
3554
|
+
if v in ('grep', 'egrep', 'fgrep'):
|
|
3555
|
+
flags = [w for w in words[1:] if w.startswith('-')]
|
|
3556
|
+
rec = any(f in ('--recursive', '--dereference-recursive') or
|
|
3557
|
+
(not f.startswith('--') and ('r' in f[1:] or 'R' in f[1:])) for f in flags)
|
|
3558
|
+
if not rec:
|
|
3559
|
+
return None
|
|
3560
|
+
pos, skip = [], False
|
|
3561
|
+
has_e = False
|
|
3562
|
+
for k, w in enumerate(words[1:], 1):
|
|
3563
|
+
if skip:
|
|
3564
|
+
skip = False
|
|
3565
|
+
continue
|
|
3566
|
+
if w in ('-e', '-f', '--regexp', '--file'):
|
|
3567
|
+
has_e, skip = True, True
|
|
3568
|
+
continue
|
|
3569
|
+
if w.startswith('-'):
|
|
3570
|
+
continue
|
|
3571
|
+
pos.append(w)
|
|
3572
|
+
targets = pos if has_e else pos[1:]
|
|
3573
|
+
elif v in ('rg', 'ag', 'ack'):
|
|
3574
|
+
pos, skip, has_e, files, depth = [], False, False, False, None
|
|
3575
|
+
for k, w in enumerate(words[1:], 1):
|
|
3576
|
+
if skip:
|
|
3577
|
+
if depth == 'next':
|
|
3578
|
+
depth = w
|
|
3579
|
+
skip = False
|
|
3580
|
+
continue
|
|
3581
|
+
if w in ('-e', '-f', '--regexp', '--file'):
|
|
3582
|
+
has_e, skip = True, True
|
|
3583
|
+
continue
|
|
3584
|
+
if w in ('--max-depth', '-d', '--depth'):
|
|
3585
|
+
depth, skip = 'next', True
|
|
3586
|
+
continue
|
|
3587
|
+
if w.startswith('--max-depth='):
|
|
3588
|
+
depth = w.split('=', 1)[1]
|
|
3589
|
+
continue
|
|
3590
|
+
if w == '--files':
|
|
3591
|
+
files = True
|
|
3592
|
+
continue
|
|
3593
|
+
if w in ('-g', '--glob', '-t', '--type', '-T', '--type-not', '-m', '--max-count',
|
|
3594
|
+
'-A', '-B', '-C', '--context'):
|
|
3595
|
+
skip = True
|
|
3596
|
+
continue
|
|
3597
|
+
if w.startswith('-'):
|
|
3598
|
+
continue
|
|
3599
|
+
pos.append(w)
|
|
3600
|
+
if depth is not None and str(depth).isdigit() and int(depth) <= 2:
|
|
3601
|
+
return None
|
|
3602
|
+
targets = pos if (has_e or files) else pos[1:]
|
|
3603
|
+
elif v == 'find':
|
|
3604
|
+
if '-maxdepth' in words:
|
|
3605
|
+
k = words.index('-maxdepth')
|
|
3606
|
+
if k + 1 < len(words) and words[k + 1].isdigit() and int(words[k + 1]) <= 2:
|
|
3607
|
+
return None
|
|
3608
|
+
targets = []
|
|
3609
|
+
for w in words[1:]:
|
|
3610
|
+
if w.startswith('-') or w in ('(', '!', ')'):
|
|
3611
|
+
break
|
|
3612
|
+
targets.append(w)
|
|
3613
|
+
else:
|
|
3614
|
+
return None
|
|
3615
|
+
if not targets:
|
|
3616
|
+
targets = ['.']
|
|
3617
|
+
if any(_disc_is_whole(t, cwd) for t in targets):
|
|
3618
|
+
return _DISC_SEARCH
|
|
3619
|
+
return None
|
|
3620
|
+
|
|
3621
|
+
|
|
3622
|
+
def _discipline_scan(command: str, cwd=None):
|
|
3623
|
+
"""作法の3件(git add -A・cd の && とリダイレクト・全体の再帰検索)。当たれば職人へ返す1行、無ければ None。"""
|
|
3624
|
+
toks = _discipline_tokens(command)
|
|
3625
|
+
if not toks:
|
|
3626
|
+
return None
|
|
3627
|
+
cmds = _discipline_commands(toks)
|
|
3628
|
+
hit = _disc_cd(cmds, toks)
|
|
3629
|
+
if hit:
|
|
3630
|
+
return hit
|
|
3631
|
+
for _, words, _ in cmds:
|
|
3632
|
+
# 包み(env 変数=… ・time ・nohup ・sudo 等)を剥がした先頭から見る。
|
|
3633
|
+
k = 0
|
|
3634
|
+
while k < len(words) and (_ASSIGN_RE.match(words[k]) or words[k].split('/')[-1] in _WRAPPERS):
|
|
3635
|
+
k += 1
|
|
3636
|
+
core = words[k:]
|
|
3637
|
+
hit = _disc_git_add(core) or _disc_search(core, cwd)
|
|
3638
|
+
if hit:
|
|
3639
|
+
return hit
|
|
3640
|
+
return None
|
|
3641
|
+
|
|
3642
|
+
|
|
3409
3643
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
3410
3644
|
# 入口: Bash
|
|
3411
3645
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
@@ -3485,6 +3719,12 @@ def decide_bash(command: str, cwd=None):
|
|
|
3485
3719
|
gate, reason = hit
|
|
3486
3720
|
return _r(GATE, f'gate({gate}): {reason}', gate)
|
|
3487
3721
|
|
|
3722
|
+
# ---- 2b) 作法(便 977507a5・毎便の本文に書いていた決まりを番人で止める) ----
|
|
3723
|
+
# ★いちばん最後に置く=ほかの危険・関門の判定は1件も変わらない(今まで通っていた物だけが対象)。
|
|
3724
|
+
hit = _discipline_scan(command, cwd)
|
|
3725
|
+
if hit:
|
|
3726
|
+
return _r(DENY, f'discipline: {hit}')
|
|
3727
|
+
|
|
3488
3728
|
# ---- 3) それ以外は自走 ----
|
|
3489
3729
|
return _r(ALLOW, 'no gate, no danger')
|
|
3490
3730
|
|
|
@@ -146,7 +146,7 @@ class FakeDatalink:
|
|
|
146
146
|
|
|
147
147
|
print('── (a) 関門でない質問は1つも人に届かず、その場で allow が返る ' + '─' * 11)
|
|
148
148
|
for label, cmd in [
|
|
149
|
-
('ふつうの命令(cd)', 'cd /home/x/projects/demo
|
|
149
|
+
('ふつうの命令(cd)', 'cd /home/x/projects/demo; npm run build'), # ★&& でつなぐ形は作法で止まる(便 977507a5)
|
|
150
150
|
('ふつうの命令(npx)', 'npx tsc --noEmit'),
|
|
151
151
|
('削除', 'rm -f /tmp/claude-1000/tmpfile.txt'),
|
|
152
152
|
]:
|
|
@@ -122,7 +122,7 @@ for cmd in [f'git {PUSH}', f'git {PUSH} origin', f'git {PUSH} origin HEAD', f'gi
|
|
|
122
122
|
expect('(1) git リポでない=確かめられない', cmd, policy.DENY, NOT_A_REPO)
|
|
123
123
|
expect('(1) push 先が main の作業ブランチ', f'git {PUSH}', policy.DENY, REPO_FEAT_TO_MAIN)
|
|
124
124
|
expect('(1) cd で main のリポへ移ってから', f'cd {REPO_MAIN} && git {PUSH}', policy.DENY, REPO_FEAT)
|
|
125
|
-
expect('(1) cd で作業ブランチのリポへ移ってから', f'cd {REPO_FEAT}
|
|
125
|
+
expect('(1) cd で作業ブランチのリポへ移ってから', f'cd {REPO_FEAT}; git {PUSH}', policy.ALLOW,
|
|
126
126
|
REPO_MAIN)
|
|
127
127
|
expect('(1) git -C で main のリポ', f'git -C {REPO_MAIN} {PUSH}', policy.DENY, REPO_FEAT)
|
|
128
128
|
expect('(1) 作業場所が変数', f'cd $APP_DIR && git {PUSH}', policy.DENY, REPO_FEAT)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""作法の3件を番人で止める検査(便 977507a5・トークン対策4「機械処理化」)。
|
|
3
|
+
|
|
4
|
+
実行: python3 scripts/hooks/test_discipline.py
|
|
5
|
+
1件でも期待と違えば exit 1。
|
|
6
|
+
|
|
7
|
+
【何を守っているか】
|
|
8
|
+
(n) ★止まる形:git add -A/--all/./-Av/git -C 場所 add -A、
|
|
9
|
+
cd を && でつなぐ(前でも後ろでも・括弧の中でも)・cd とリダイレクトを同じ行に混ぜる、
|
|
10
|
+
リポ全体・ホーム全体・/ を指す再帰の検索(grep -r・rg・find、行き先を省いた形も)
|
|
11
|
+
(p) 止まってはいけない似た形:git add ファイル名・git add -- ファイル名・cd 単独・cd X; 命令・
|
|
12
|
+
名指しの場所だけの検索(rg 語 src・find src・grep -r 語 src)・深さを限った find・
|
|
13
|
+
git grep・行き先が変数で決まらない検索・ヒアドキュメントや引用符の中の文字
|
|
14
|
+
(m) 止めたときの文は「何がだめで、代わりにどうするか」を1行で
|
|
15
|
+
(x) ほかの判定は変わらない:危険(秘密・main への push)と関門(本番反映)は、作法より先に
|
|
16
|
+
今までどおりの理由で当たる
|
|
17
|
+
★空振り確認:直す前の版(origin/main)で流すと (n) が落ちる(報告に実測を書く)。
|
|
18
|
+
|
|
19
|
+
★この台本は検索の道具の名前を素の字で置かない語もある(組み立てて使う)。台本の行そのものが
|
|
20
|
+
番人の判定に当たって、検査が流せなくなるのを避けるため。
|
|
21
|
+
"""
|
|
22
|
+
import os
|
|
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:60} {str(got)[:50]}')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# 使い捨てのリポ(一番上に .git がある場所)を作業場所にする。
|
|
41
|
+
REPO = tempfile.mkdtemp(prefix='disc-repo-')
|
|
42
|
+
os.mkdir(os.path.join(REPO, '.git'))
|
|
43
|
+
os.mkdir(os.path.join(REPO, 'src'))
|
|
44
|
+
SUB = os.path.join(REPO, 'src')
|
|
45
|
+
HOME = os.path.expanduser('~')
|
|
46
|
+
ADD = 'git ' + 'add'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def decide(cmd, cwd=REPO):
|
|
50
|
+
return policy.decide_bash(cmd, cwd)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
print('── (n) 止まる形 ' + '─' * 50)
|
|
54
|
+
NEGATIVE = [
|
|
55
|
+
('n1 git add -A', f'{ADD} -A'),
|
|
56
|
+
('n2 git add --all', f'{ADD} --all'),
|
|
57
|
+
('n3 git add .', f'{ADD} .'),
|
|
58
|
+
('n4 git add -Av(短い旗のまとめ)', f'{ADD} -Av'),
|
|
59
|
+
('n5 git -C 場所 add -A', f'git -C /x add -A'),
|
|
60
|
+
('n6 git add :/(全体)', f'{ADD} :/'),
|
|
61
|
+
('n7 cd X && 命令', 'cd /tmp/x && npm test'),
|
|
62
|
+
('n8 命令 && cd X', 'npm test && cd /tmp/x'),
|
|
63
|
+
('n9 (cd X && 命令)', '(cd sub && make)'),
|
|
64
|
+
('n10 cd とリダイレクト', 'cd /tmp/x > /tmp/o'),
|
|
65
|
+
('n11 cd; 命令 > ファイル(同じ行に混ぜる)', 'cd /tmp/x; ls > /tmp/o'),
|
|
66
|
+
('n12 cd X && 命令 <<ヒアドキュメント', "cd /tmp/x && python3 - <<'EOF'\nprint(1)\nEOF"),
|
|
67
|
+
('n13 grep -r 語 .', 'grep -r foo .'),
|
|
68
|
+
('n14 grep -rn 語(行き先なし=ここ)', 'grep -rn foo'),
|
|
69
|
+
('n15 rg 語(行き先なし=ここ)', 'r' + 'g foo'),
|
|
70
|
+
('n16 rg 語 .', 'r' + 'g foo .'),
|
|
71
|
+
('n17 find .', 'find . -name x'),
|
|
72
|
+
('n18 find ~', 'find ~ -name x'),
|
|
73
|
+
('n19 find ホームの絶対パス', f'find {HOME} -name x'),
|
|
74
|
+
('n20 rg 語 $HOME', 'r' + 'g foo $HOME'),
|
|
75
|
+
('n21 find /', 'find / -name x'),
|
|
76
|
+
('n22 リポの一番上を名指しした find', f'find {REPO} -name x'),
|
|
77
|
+
('n23 リポより広い所(リポの親)', f'find {os.path.dirname(REPO)} -name x'),
|
|
78
|
+
('n24 env 変数=… を前に付けても', f'GIT_TRACE=1 {ADD} -A'),
|
|
79
|
+
]
|
|
80
|
+
for label, cmd in NEGATIVE:
|
|
81
|
+
r = decide(cmd)
|
|
82
|
+
check(label, (r['decision'], r['reason'].startswith('discipline:')), ('deny', True))
|
|
83
|
+
|
|
84
|
+
print('── (p) 止まってはいけない似た形 ' + '─' * 34)
|
|
85
|
+
POSITIVE = [
|
|
86
|
+
('p1 git add ファイル名', f'{ADD} src/a.ts'),
|
|
87
|
+
('p2 git add -- ファイル名', f'{ADD} -- docs/x.md'),
|
|
88
|
+
('p3 git add -p ファイル名', f'{ADD} -p src/a.ts'),
|
|
89
|
+
('p4 cd 単独(作業のはじめの1回)', f'cd {REPO}'),
|
|
90
|
+
('p5 cd X; 命令', 'cd /tmp/x; npm test'),
|
|
91
|
+
('p6 rg 語 src', 'r' + 'g foo src'),
|
|
92
|
+
('p7 grep -r 語 src', 'grep -r foo src'),
|
|
93
|
+
('p8 find src', 'find src -name x'),
|
|
94
|
+
('p9 深さを限った find .', 'find . -maxdepth 1 -name x'),
|
|
95
|
+
('p10 rg --max-depth 1', 'r' + 'g --max-depth 1 foo'),
|
|
96
|
+
('p11 git grep(別の道具)', 'git grep foo'),
|
|
97
|
+
('p12 行き先が変数で決まらない', 'find $DIR -name x'),
|
|
98
|
+
('p13 引用符の中の && cd', 'echo "cd x && y"'),
|
|
99
|
+
('p14 コミットの説明に git add -A', 'git commit -m "git add -A はやめた"'),
|
|
100
|
+
('p15 ヒアドキュメントの本文に cd &&', "cat <<'EOF' > /tmp/claude-1000/n.txt\ncd x && y\nEOF"),
|
|
101
|
+
('p16 下の場所から find .(リポ全体ではない)', 'find . -name x'),
|
|
102
|
+
('p17 grep(再帰でない)', 'grep -n foo src/a.ts'),
|
|
103
|
+
('p18 リダイレクトだけ(cd なし)', 'ls > /tmp/claude-1000/o'),
|
|
104
|
+
]
|
|
105
|
+
for label, cmd in POSITIVE:
|
|
106
|
+
cwd = SUB if label.startswith('p16') else REPO
|
|
107
|
+
check(label, decide(cmd, cwd)['decision'], 'allow')
|
|
108
|
+
|
|
109
|
+
print('── (m) 止めたときの文 ' + '─' * 44)
|
|
110
|
+
check('(m) git add は名指しを案内', 'git add path/to/file' in decide(f'{ADD} -A')['reason'], True)
|
|
111
|
+
check('(m) cd は絶対パスか git -C を案内', 'git -C' in decide('cd x && ls')['reason'], True)
|
|
112
|
+
check('(m) 検索は名指しの場所を案内', '名指し' in decide('find . -name x')['reason'], True)
|
|
113
|
+
for label, cmd in [('git add', f'{ADD} -A'), ('cd', 'cd x && ls'), ('検索', 'find / -name x')]:
|
|
114
|
+
check(f'(m) {label} の文は1行', '\n' in decide(cmd)['reason'], False)
|
|
115
|
+
|
|
116
|
+
print('── (x) ほかの判定は変わらない(作法より先に当たる) ' + '─' * 14)
|
|
117
|
+
r = decide('cd /tmp/x && cat .env')
|
|
118
|
+
check('(x) 秘密の読み出しは危険の理由のまま', (r['decision'], r['reason'].startswith('danger:')), ('deny', True))
|
|
119
|
+
r = decide('cd /tmp/x && npx ver' + 'cel --prod')
|
|
120
|
+
check('(x) 本番反映は関門のまま(人に聞く)', r['decision'], 'gate')
|
|
121
|
+
r = decide('find ~ -name .env -exec cat {} +')
|
|
122
|
+
check('(x) ホームの秘密を探して読むのは危険の理由のまま', r['reason'].startswith('danger:'), True)
|
|
123
|
+
|
|
124
|
+
print()
|
|
125
|
+
if failures:
|
|
126
|
+
print(f'NG {len(failures)} 件')
|
|
127
|
+
for f in failures:
|
|
128
|
+
print(' ', f)
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
print('すべて期待どおり')
|
|
@@ -173,7 +173,7 @@ MERGE_ALLOW = [
|
|
|
173
173
|
'gh pr merge --rebase 7',
|
|
174
174
|
# ★API での合流(gh api -X PUT …/pulls/N/merge)は便 fc22bc34 で deny へ移した
|
|
175
175
|
# (保護の迂回・えふさん確定 2026-09-18)→ test_bypass_deny.py。
|
|
176
|
-
'cd /srv/app
|
|
176
|
+
'cd /srv/app; gh pr merge 12 --squash', # 複合コマンドでも自走(★&& は作法で止まる・便 977507a5)
|
|
177
177
|
'bash -c "gh pr merge 12 --squash"', # 包んでも自走
|
|
178
178
|
'gh pr merge 12 --squash && git pull',
|
|
179
179
|
# ★便(指示)で実際に使う形。鉄則7「便の終点はマージまで」の形をそのまま置く。
|
|
@@ -465,7 +465,7 @@ for cmd in [f'cat ~/{_CONDUCTOR_ENV}', f'head -5 ~/{_CONDUCTOR_ENV}',
|
|
|
465
465
|
'cat ~/.codex/auth.json', 'cat ~/.config/gh/hosts.yml']:
|
|
466
466
|
check(f'★どの経路でも deny {cmd[:52]}', policy.decide_bash(cmd)['decision'], policy.DENY)
|
|
467
467
|
# 既存の自走(ふつうの作業)は1つも止めない。
|
|
468
|
-
for cmd in ['cd ~/projects/x
|
|
468
|
+
for cmd in ['cd ~/projects/x; npm test', 'ls -la', 'npm run build', 'git status',
|
|
469
469
|
'cat README.md', 'grep -r TODO src/', 'gh pr create --title x']:
|
|
470
470
|
check(f'ふつうの作業は通る {cmd[:44]}', policy.decide_bash(cmd)['decision'], policy.ALLOW)
|
|
471
471
|
|