@tyhld/conductor 0.12.17 → 0.12.19
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 +117 -29
- package/scripts/hooks/cc2_guard.py +134 -59
- package/scripts/hooks/permission_request_hook.py +57 -4
- package/scripts/hooks/policy.py +434 -29
- package/scripts/hooks/settings.sample.json +1 -0
- package/scripts/hooks/test_autorun.py +117 -32
- package/scripts/hooks/test_bypass_deny.py +299 -0
- package/scripts/hooks/test_gate_wait.py +218 -0
- package/scripts/hooks/test_policy.py +56 -35
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""関門は入口の番人が管制に聞いて【答えが返るまで実行させない】(便 ab6e36ca)の実物の試験。
|
|
3
|
+
|
|
4
|
+
実行: python3 scripts/hooks/test_gate_wait.py
|
|
5
|
+
1件でも期待と違えば exit 1。
|
|
6
|
+
|
|
7
|
+
【なぜ要るか】
|
|
8
|
+
便 c525bb2b の実機: 職人が auto モードで動くと、関門を ask で返しても Claude Code は
|
|
9
|
+
確認の番人(管制の答え待ち)を待たずに実行を許した。入口の番人の deny はどのモードでも効くので、
|
|
10
|
+
入口の番人が自分で聞いて待ち、答えが無ければ deny を返す形にした。
|
|
11
|
+
ここでは【実物の cc2_guard.py を別の手続きとして起動】し、手元に立てた偽の管制と
|
|
12
|
+
本物の通信(HTTP)でやり取りさせて確かめる(関数の差し替えではない)。
|
|
13
|
+
|
|
14
|
+
【何を守っているか】
|
|
15
|
+
① 答えが来ない関門は deny(=どのモードでも実行されない)。カードを閉じる(DELETE)
|
|
16
|
+
② 「いいえ」は deny/「はい」は allow(この1回だけ)
|
|
17
|
+
③ 管制に届かない関門は deny
|
|
18
|
+
④ 管制が応答しないまま固まっても、最後の歯止めで上限の内側に deny を返す
|
|
19
|
+
⑤ ふつうの操作は管制に1回も聞かない
|
|
20
|
+
⑥ カードは2択・期限(waitMs)は待つ上限と同じ
|
|
21
|
+
⑦ 同じ操作の答えを使い回さない(1回の実行ごとに新しい鍵・登録の時点の答えは使わない)
|
|
22
|
+
|
|
23
|
+
★空振り確認: 待つ処理(ask_gate_now)を外して ask を返す形に戻すと ①②④ が落ちる。
|
|
24
|
+
"""
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import socket
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import tempfile
|
|
31
|
+
import threading
|
|
32
|
+
import time
|
|
33
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
34
|
+
from urllib.parse import parse_qs, urlparse
|
|
35
|
+
|
|
36
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
37
|
+
GUARD = os.path.join(HERE, 'cc2_guard.py')
|
|
38
|
+
|
|
39
|
+
failures = []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check(label, got, want):
|
|
43
|
+
ok = got == want
|
|
44
|
+
if not ok:
|
|
45
|
+
failures.append(f'{label}: got={got!r} want={want!r}')
|
|
46
|
+
print(f'{"OK " if ok else "NG "} {label:62} {str(got)[:50]}')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FakeConsole:
|
|
50
|
+
"""偽の管制。mode で答え方を決める: allow / deny / none(答えない)/ hang(固まる)。"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, mode):
|
|
53
|
+
self.mode = mode
|
|
54
|
+
self.posts, self.gets, self.deletes = [], 0, 0
|
|
55
|
+
console = self
|
|
56
|
+
|
|
57
|
+
class Handler(BaseHTTPRequestHandler):
|
|
58
|
+
def log_message(self, *a): # 試験の出力を汚さない
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def _send(self, obj):
|
|
62
|
+
body = json.dumps(obj).encode()
|
|
63
|
+
self.send_response(200)
|
|
64
|
+
self.send_header('Content-Type', 'application/json')
|
|
65
|
+
self.send_header('Content-Length', str(len(body)))
|
|
66
|
+
self.end_headers()
|
|
67
|
+
self.wfile.write(body)
|
|
68
|
+
|
|
69
|
+
def do_POST(self):
|
|
70
|
+
n = int(self.headers.get('Content-Length') or 0)
|
|
71
|
+
console.posts.append(json.loads(self.rfile.read(n) or b'{}'))
|
|
72
|
+
if console.mode == 'preanswered':
|
|
73
|
+
# 管制が同じ鍵の行をまとめ、前に出た答えを返してきた形
|
|
74
|
+
self._send({'id': 'req-1', 'status': 'answered', 'decision': 'allow'})
|
|
75
|
+
return
|
|
76
|
+
self._send({'id': 'req-1', 'status': 'waiting'})
|
|
77
|
+
|
|
78
|
+
def do_GET(self):
|
|
79
|
+
console.gets += 1
|
|
80
|
+
if console.mode == 'hang':
|
|
81
|
+
time.sleep(30)
|
|
82
|
+
return
|
|
83
|
+
if console.mode in ('allow', 'deny'):
|
|
84
|
+
self._send({'status': 'answered', 'outcome': 'decided', 'decision': console.mode})
|
|
85
|
+
return
|
|
86
|
+
wait_ms = int((parse_qs(urlparse(self.path).query).get('waitMs') or ['0'])[0])
|
|
87
|
+
time.sleep(wait_ms / 1000)
|
|
88
|
+
self._send({'status': 'waiting', 'outcome': 'pending'})
|
|
89
|
+
|
|
90
|
+
def do_DELETE(self):
|
|
91
|
+
console.deletes += 1
|
|
92
|
+
self._send({})
|
|
93
|
+
|
|
94
|
+
self.server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
|
95
|
+
self.server.daemon_threads = True
|
|
96
|
+
self.url = f'http://127.0.0.1:{self.server.server_address[1]}'
|
|
97
|
+
threading.Thread(target=self.server.serve_forever, daemon=True).start()
|
|
98
|
+
|
|
99
|
+
def close(self):
|
|
100
|
+
self.server.shutdown()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def free_port():
|
|
104
|
+
s = socket.socket()
|
|
105
|
+
s.bind(('127.0.0.1', 0))
|
|
106
|
+
port = s.getsockname()[1]
|
|
107
|
+
s.close()
|
|
108
|
+
return port
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def run_guard(command, url, extra_env=None, home=None):
|
|
112
|
+
"""実物の番人を起動し、(permissionDecision, 理由, かかった秒) を返す。"""
|
|
113
|
+
env = {
|
|
114
|
+
'PATH': os.environ.get('PATH', ''),
|
|
115
|
+
'HOME': home,
|
|
116
|
+
'CONDUCTOR_URL': url,
|
|
117
|
+
'CONDUCTOR_TOKEN': 'test-token-not-secret',
|
|
118
|
+
'CONDUCTOR_GUARD_LOG': os.path.join(home, 'guard.log'),
|
|
119
|
+
'CONDUCTOR_HOOK_LOG': os.path.join(home, 'hook.log'),
|
|
120
|
+
'CC2_GATE_WAIT_SEC': '2',
|
|
121
|
+
}
|
|
122
|
+
env.update(extra_env or {})
|
|
123
|
+
event = {'tool_name': 'Bash', 'tool_input': {'command': command, 'description': '見本'},
|
|
124
|
+
'cwd': '/home/x/projects/demo', 'session_id': 'gate-wait-test'}
|
|
125
|
+
t0 = time.time()
|
|
126
|
+
p = subprocess.run([sys.executable, GUARD], input=json.dumps(event), capture_output=True,
|
|
127
|
+
text=True, env=env, timeout=60)
|
|
128
|
+
took = time.time() - t0
|
|
129
|
+
if not p.stdout.strip():
|
|
130
|
+
return None, None, took
|
|
131
|
+
out = json.loads(p.stdout)['hookSpecificOutput']
|
|
132
|
+
return out.get('permissionDecision'), out.get('permissionDecisionReason'), took
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# 関門 db の見本(危険語を含まない・実行はしない)。★psql は便 fc22bc34 で deny へ移したので、
|
|
136
|
+
# 今も関門のまま残る「台本に SQL の字面を渡す形」を使う。
|
|
137
|
+
GATE_CMD = 'python3 apply.py --mode grant'
|
|
138
|
+
ROUTINE_CMD = 'npm test'
|
|
139
|
+
|
|
140
|
+
with tempfile.TemporaryDirectory() as home:
|
|
141
|
+
print('── ① 答えが来ない関門は deny(どのモードでも実行されない) ' + '─' * 10)
|
|
142
|
+
c = FakeConsole('none')
|
|
143
|
+
d, reason, took = run_guard(GATE_CMD, c.url, home=home)
|
|
144
|
+
check('① 答えが来ない → deny', d, 'deny')
|
|
145
|
+
check('① 理由は人の言葉(確認が取れなかった)', '確認' in (reason or '') and '実行しません' in reason, True)
|
|
146
|
+
check('① ★ask を返さない(auto モードで通りうる返事をしない)', d != 'ask', True)
|
|
147
|
+
check('① 管制へ1回だけ聞いた(カード1枚)', len(c.posts), 1)
|
|
148
|
+
check('① 答えを待った(問い合わせ1回以上)', c.gets >= 1, True)
|
|
149
|
+
check('① 答えが来ないのでカードを閉じた', c.deletes, 1)
|
|
150
|
+
check('① 待つ上限(2秒)で返した', 1.5 <= took < 10, True)
|
|
151
|
+
c.close()
|
|
152
|
+
|
|
153
|
+
print('── ② 「いいえ」は deny/「はい」は allow ' + '─' * 26)
|
|
154
|
+
c = FakeConsole('deny')
|
|
155
|
+
d, reason, _ = run_guard(GATE_CMD, c.url, home=home)
|
|
156
|
+
check('② 「いいえ」→ deny', d, 'deny')
|
|
157
|
+
check('② 理由に「いいえ」', '「いいえ」' in (reason or ''), True)
|
|
158
|
+
c.close()
|
|
159
|
+
c = FakeConsole('allow')
|
|
160
|
+
d, reason, _ = run_guard(GATE_CMD, c.url, home=home)
|
|
161
|
+
check('② 「はい」→ allow', d, 'allow')
|
|
162
|
+
check('② 「はい」のときカードは閉じない(答えがある)', c.deletes, 0)
|
|
163
|
+
c.close()
|
|
164
|
+
|
|
165
|
+
print('── ③ 管制に届かない関門は deny ' + '─' * 36)
|
|
166
|
+
d, reason, took = run_guard(GATE_CMD, f'http://127.0.0.1:{free_port()}', home=home)
|
|
167
|
+
check('③ 届かない → deny', d, 'deny')
|
|
168
|
+
check('③ 理由は人の言葉(つながらない)', 'つながらない' in (reason or ''), True)
|
|
169
|
+
check('③ すぐ返る', took < 10, True)
|
|
170
|
+
|
|
171
|
+
print('── ④ 管制が固まっても、最後の歯止めで上限の内側に deny ' + '─' * 12)
|
|
172
|
+
c = FakeConsole('hang')
|
|
173
|
+
d, reason, took = run_guard(GATE_CMD, c.url, home=home,
|
|
174
|
+
extra_env={'CC2_GATE_WAIT_SEC': '20', 'CC2_GATE_HARD_LIMIT_SEC': '3'})
|
|
175
|
+
check('④ 固まった管制 → deny', d, 'deny')
|
|
176
|
+
check('④ 歯止め(3秒)で返した', 2.5 <= took < 8, True)
|
|
177
|
+
check('④ 理由は人の言葉(時間内に取れなかった)', '時間内' in (reason or ''), True)
|
|
178
|
+
c.close()
|
|
179
|
+
|
|
180
|
+
print('── ⑤ ふつうの操作は管制に1回も聞かない ' + '─' * 28)
|
|
181
|
+
c = FakeConsole('none')
|
|
182
|
+
d, _, took = run_guard(ROUTINE_CMD, c.url, home=home)
|
|
183
|
+
check('⑤ npm test → allow', d, 'allow')
|
|
184
|
+
check('⑤ 管制へ0回', (len(c.posts), c.gets), (0, 0))
|
|
185
|
+
check('⑤ すぐ返る', took < 5, True)
|
|
186
|
+
c.close()
|
|
187
|
+
|
|
188
|
+
print('── ⑥ カードは2択・期限は待つ上限と同じ ' + '─' * 28)
|
|
189
|
+
c = FakeConsole('allow')
|
|
190
|
+
run_guard(GATE_CMD, c.url, home=home)
|
|
191
|
+
p = c.posts[0] if c.posts else {}
|
|
192
|
+
check('⑥ 2択(今後は聞かないを出さない)', [x.get('value') for x in p.get('choices', [])],
|
|
193
|
+
['allow', 'deny'])
|
|
194
|
+
check('⑥ カードの期限 = 待つ上限(2秒)', p.get('waitMs'), 2000)
|
|
195
|
+
check('⑥ 題はデータベースの変更', (p.get('summary') or '').startswith('【お客さまの確認:データベース'),
|
|
196
|
+
True)
|
|
197
|
+
c.close()
|
|
198
|
+
|
|
199
|
+
print('── ⑦ 同じ操作の答えを使い回さない ' + '─' * 34)
|
|
200
|
+
c = FakeConsole('allow')
|
|
201
|
+
run_guard(GATE_CMD, c.url, home=home)
|
|
202
|
+
run_guard(GATE_CMD, c.url, home=home)
|
|
203
|
+
keys = [x.get('requestKey') for x in c.posts]
|
|
204
|
+
check('⑦ 同じ操作を2回 → カードも2枚・鍵は毎回ちがう', len(set(k for k in keys if k)), 2)
|
|
205
|
+
c.close()
|
|
206
|
+
c = FakeConsole('preanswered')
|
|
207
|
+
d, _, _ = run_guard(GATE_CMD, c.url, home=home)
|
|
208
|
+
check('⑦ ★登録の時点で付いてきた「はい」は使わず、答えを待つ(来なければ deny)', d, 'deny')
|
|
209
|
+
c.close()
|
|
210
|
+
|
|
211
|
+
print('\n' + '=' * 72)
|
|
212
|
+
if failures:
|
|
213
|
+
print(f'NG {len(failures)} 件:')
|
|
214
|
+
for f in failures:
|
|
215
|
+
print(' -', f)
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
print('すべて期待どおり')
|
|
218
|
+
sys.exit(0)
|
|
@@ -40,11 +40,8 @@ def check(label, got, want):
|
|
|
40
40
|
# (a) 3関門は必ず gate(=人へ)
|
|
41
41
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
42
42
|
GATE_CASES = [
|
|
43
|
-
# 関門1
|
|
44
|
-
|
|
45
|
-
('git push -u origin main', policy.GATE_MERGE),
|
|
46
|
-
('git push origin HEAD:master', policy.GATE_MERGE),
|
|
47
|
-
('git push origin production', policy.GATE_MERGE),
|
|
43
|
+
# ★旧 関門1(main など保護ブランチへの直接 push)は便 fc22bc34 で deny へ移した
|
|
44
|
+
# → test_bypass_deny.py で確かめる。PRのマージは関門ではない → (a4)。
|
|
48
45
|
# 関門2 本番確認
|
|
49
46
|
('vercel --prod', policy.GATE_RELEASE),
|
|
50
47
|
('vercel deploy --prod', policy.GATE_RELEASE),
|
|
@@ -56,14 +53,10 @@ GATE_CASES = [
|
|
|
56
53
|
('supabase functions deploy hello', policy.GATE_RELEASE),
|
|
57
54
|
('npm run deploy -- --production', policy.GATE_RELEASE),
|
|
58
55
|
# 関門3 DB実行
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
('supabase db push', policy.GATE_DB),
|
|
63
|
-
('supabase migration up', policy.GATE_DB),
|
|
64
|
-
('npx drizzle-kit push', policy.GATE_DB),
|
|
56
|
+
# ★本番 DB を直接変える道具(psql・prisma db push / migrate deploy・supabase db push /
|
|
57
|
+
# migration up・drizzle-kit push 等)は便 fc22bc34 で deny へ移した(設定の deny と揃える)
|
|
58
|
+
# → test_bypass_deny.py。ここに残るのは、それ以外の DB 実行(見分けられないので人に聞く)。
|
|
65
59
|
('createdb newdb', policy.GATE_DB),
|
|
66
|
-
('echo "CREATE TABLE x (id uuid)" | psql', policy.GATE_DB),
|
|
67
60
|
('supabase db query "GRANT SELECT ON x TO authenticated"', policy.GATE_DB),
|
|
68
61
|
('run-sql "ALTER TABLE x ENABLE ROW LEVEL SECURITY"', policy.GATE_DB),
|
|
69
62
|
('apply "CREATE POLICY p ON x"', policy.GATE_DB),
|
|
@@ -81,10 +74,12 @@ for cmd, want_gate in GATE_CASES:
|
|
|
81
74
|
class GuardDatalink:
|
|
82
75
|
"""cc2_guard 用の偽 datalink(到達可否を固定し、ネットワークもディスクも触らない)。"""
|
|
83
76
|
|
|
84
|
-
def __init__(self, cfg, reachable):
|
|
77
|
+
def __init__(self, cfg, reachable, answer='allow'):
|
|
85
78
|
self._cfg = cfg
|
|
86
79
|
self._reachable = reachable
|
|
80
|
+
self._answer = answer
|
|
87
81
|
self.decisions = []
|
|
82
|
+
self.registered = 0
|
|
88
83
|
|
|
89
84
|
def load_config(self):
|
|
90
85
|
return self._cfg
|
|
@@ -92,20 +87,40 @@ class GuardDatalink:
|
|
|
92
87
|
def ping(self, cfg):
|
|
93
88
|
return self._reachable
|
|
94
89
|
|
|
90
|
+
# ★便 ab6e36ca: 入口の番人が関門をその場で聞く。届かなければ登録できない。
|
|
91
|
+
def register(self, cfg, payload):
|
|
92
|
+
self.registered += 1
|
|
93
|
+
return {'id': 'r1', 'status': 'waiting'} if self._reachable else None
|
|
94
|
+
|
|
95
|
+
def wait_for_decision(self, cfg, request_id, max_wait_sec=None):
|
|
96
|
+
return self._answer
|
|
97
|
+
|
|
98
|
+
def abandon(self, cfg, request_id):
|
|
99
|
+
pass
|
|
100
|
+
|
|
95
101
|
def record_decision(self, tool_name, decision, gate=None):
|
|
96
102
|
self.decisions.append((tool_name, decision, gate))
|
|
97
103
|
|
|
98
104
|
|
|
99
|
-
GATE_CMDS = ['
|
|
100
|
-
'
|
|
101
|
-
'psql -c "select 1"']
|
|
105
|
+
GATE_CMDS = ['vercel --prod', 'npm publish', 'createdb newdb',
|
|
106
|
+
'supabase db query "select 1"', 'run-sql "ALTER TABLE x ADD COLUMN y int"']
|
|
102
107
|
|
|
103
|
-
print('\n=== (a2)
|
|
108
|
+
print('\n=== (a2) 関門は入口の番人がその場で管制に聞き、答えで allow / deny を返す(便 ab6e36ca) ===')
|
|
104
109
|
gd_ok = GuardDatalink({'url': 'http://x', 'token': 't'}, reachable=True)
|
|
105
110
|
for cmd in GATE_CMDS:
|
|
106
111
|
out = cc2_guard.build_output({'tool_name': 'Bash', 'tool_input': {'command': cmd}}, datalink=gd_ok)
|
|
107
|
-
check(f'{cmd} →
|
|
112
|
+
check(f'{cmd} → allow(「はい」)', out['hookSpecificOutput']['permissionDecision'] if out else None,
|
|
113
|
+
'allow')
|
|
114
|
+
check('関門ごとに管制へ1回聞いた', gd_ok.registered, len(GATE_CMDS))
|
|
108
115
|
check('起動記録が残る(呼ばれた事実)', len(gd_ok.decisions) >= len(GATE_CMDS), True)
|
|
116
|
+
for answer in ('deny', None, 'weird'):
|
|
117
|
+
gd = GuardDatalink({'url': 'http://x', 'token': 't'}, reachable=True, answer=answer)
|
|
118
|
+
out = cc2_guard.build_output({'tool_name': 'Bash', 'tool_input': {'command': 'npm publish'}},
|
|
119
|
+
datalink=gd)
|
|
120
|
+
check(f'答え {answer!r} → deny', out['hookSpecificOutput']['permissionDecision'], 'deny')
|
|
121
|
+
for cmd in GATE_CMDS:
|
|
122
|
+
out = cc2_guard.build_output({'tool_name': 'Bash', 'tool_input': {'command': cmd}}, datalink=gd_ok)
|
|
123
|
+
check(f'★ask は返さない: {cmd[:40]}', out['hookSpecificOutput']['permissionDecision'] != 'ask', True)
|
|
109
124
|
|
|
110
125
|
print('\n=== (a3) ★フェイルクローズ: 聞けないとき関門は deny(素通りさせない) ===')
|
|
111
126
|
gd_ng = GuardDatalink({'url': 'http://x', 'token': 't'}, reachable=False)
|
|
@@ -115,7 +130,7 @@ for cmd in GATE_CMDS:
|
|
|
115
130
|
check(f'{cmd} → deny(管制不通)', got, 'deny')
|
|
116
131
|
|
|
117
132
|
gd_unset = GuardDatalink(None, reachable=False) # 管制未設定(load_config が None)
|
|
118
|
-
out = cc2_guard.build_output({'tool_name': 'Bash', 'tool_input': {'command': '
|
|
133
|
+
out = cc2_guard.build_output({'tool_name': 'Bash', 'tool_input': {'command': 'npm publish'}},
|
|
119
134
|
datalink=gd_unset)
|
|
120
135
|
check('管制未設定でも関門は deny', out['hookSpecificOutput']['permissionDecision'], 'deny')
|
|
121
136
|
|
|
@@ -127,11 +142,17 @@ out = cc2_guard.build_output({'tool_name': 'Bash', 'tool_input': {'command': 'ca
|
|
|
127
142
|
datalink=gd_unset)
|
|
128
143
|
check('危険は管制不通でも deny のまま', out['hookSpecificOutput']['permissionDecision'], 'deny')
|
|
129
144
|
|
|
130
|
-
#
|
|
145
|
+
# ★便 ab6e36ca: 以前の無効化フラグ(CC2_GATE_FAILCLOSE=0 で止めずに ask)は外した。
|
|
146
|
+
# ask は auto モードでは人の答えを待たずに実行されうる(便 c525bb2b)=止めが消える回避路になるため。
|
|
131
147
|
os.environ['CC2_GATE_FAILCLOSE'] = '0'
|
|
132
|
-
out = cc2_guard.build_output({'tool_name': 'Bash', 'tool_input': {'command': '
|
|
148
|
+
out = cc2_guard.build_output({'tool_name': 'Bash', 'tool_input': {'command': 'npm publish'}},
|
|
133
149
|
datalink=gd_unset)
|
|
134
|
-
check('CC2_GATE_FAILCLOSE=0
|
|
150
|
+
check('★CC2_GATE_FAILCLOSE=0 でも関門は deny(回避路を作らない)',
|
|
151
|
+
out['hookSpecificOutput']['permissionDecision'], 'deny')
|
|
152
|
+
out = cc2_guard.build_output({'tool_name': 'Bash', 'tool_input': {'command': 'npm publish'}},
|
|
153
|
+
datalink=gd_ng)
|
|
154
|
+
check('★CC2_GATE_FAILCLOSE=0 でも管制不通の関門は deny',
|
|
155
|
+
out['hookSpecificOutput']['permissionDecision'], 'deny')
|
|
135
156
|
os.environ.pop('CC2_GATE_FAILCLOSE', None)
|
|
136
157
|
|
|
137
158
|
# 関門でも、人に見せる日本語1行は用意できている(管制のカードへ送る文言)。
|
|
@@ -150,7 +171,8 @@ MERGE_ALLOW = [
|
|
|
150
171
|
'gh pr merge --auto 12',
|
|
151
172
|
'gh pr merge 3 --merge --delete-branch',
|
|
152
173
|
'gh pr merge --rebase 7',
|
|
153
|
-
|
|
174
|
+
# ★API での合流(gh api -X PUT …/pulls/N/merge)は便 fc22bc34 で deny へ移した
|
|
175
|
+
# (保護の迂回・えふさん確定 2026-09-18)→ test_bypass_deny.py。
|
|
154
176
|
'cd /srv/app && gh pr merge 12 --squash', # 複合コマンドでも自走
|
|
155
177
|
'bash -c "gh pr merge 12 --squash"', # 包んでも自走
|
|
156
178
|
'gh pr merge 12 --squash && git pull',
|
|
@@ -191,12 +213,12 @@ for _gd in (GuardDatalink({'url': 'http://x', 'token': 't'}, reachable=True),
|
|
|
191
213
|
out['hookSpecificOutput']['permissionDecision'], 'allow')
|
|
192
214
|
|
|
193
215
|
# ★マージを外しても、他の関門と危険は一切緩めていない(取り違え防止の回帰テスト)。
|
|
194
|
-
check('main 直 push
|
|
195
|
-
policy.decide_bash('git push origin main')['
|
|
216
|
+
check('main 直 push は deny(便 fc22bc34 で関門から移した)',
|
|
217
|
+
policy.decide_bash('git push origin main')['decision'], policy.DENY)
|
|
196
218
|
check('force push は deny のまま',
|
|
197
219
|
policy.decide_bash('git push --force origin main')['decision'], policy.DENY)
|
|
198
|
-
check('DB
|
|
199
|
-
policy.decide_bash('
|
|
220
|
+
check('DB実行(見分けられないもの)は関門のまま',
|
|
221
|
+
policy.decide_bash('createdb newdb')['gate'], policy.GATE_DB)
|
|
200
222
|
check('SQL DDL は関門のまま',
|
|
201
223
|
policy.decide_bash('run-sql "CREATE TABLE x (id int)"')['gate'], policy.GATE_DB)
|
|
202
224
|
check('本番反映は関門のまま',
|
|
@@ -537,12 +559,11 @@ check('枝への push は通常の文言',
|
|
|
537
559
|
summary_ja.summarize_bash('git push origin feat/main-fix'),
|
|
538
560
|
'作業内容をGitHubへ送ってよいですか')
|
|
539
561
|
# ★見出しは「関門に当たったセグメント」から作る(複合コマンドの先頭語 cd に引きずられない)。
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
'【お客さまの確認:データベースの変更】データベースの構造を変更してよいですか')
|
|
562
|
+
# ★main 直 push と本番 DB を直接変える道具は、便 fc22bc34 で関門から deny へ移した
|
|
563
|
+
# (カードは出ない)。見出しの作り方は、今も関門のままの操作で確かめる。
|
|
564
|
+
check('複合コマンドでも関門セグメントから見出し(公開)',
|
|
565
|
+
summary_ja.summarize_bash('cd /srv/app && npm publish', 'release'),
|
|
566
|
+
'【お客さまの確認:本番へ反映】部品を公開(配布)してよいですか')
|
|
546
567
|
|
|
547
568
|
|
|
548
569
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -634,7 +655,7 @@ check('保存先は session(設定を汚さない)', up[0]['destination'], '
|
|
|
634
655
|
|
|
635
656
|
# 関門には恒久許可を与えない。
|
|
636
657
|
fd = FakeDatalink('allow_always')
|
|
637
|
-
GATE_EV = {'tool_name': 'Bash', 'tool_input': {'command': '
|
|
658
|
+
GATE_EV = {'tool_name': 'Bash', 'tool_input': {'command': 'npm publish'},
|
|
638
659
|
'cwd': '/home/fshim/projects/conductor', 'session_id': 's1'}
|
|
639
660
|
out = permission_request_hook.decide(GATE_EV, cfg={'url': 'http://x', 'token': 't'}, datalink=fd)
|
|
640
661
|
check('関門に恒久許可を与えない',
|
|
@@ -664,7 +685,7 @@ check('送る payload に choices が入る(3択)',
|
|
|
664
685
|
|
|
665
686
|
# ★フェイルクローズ: 関門は「聞けない」とき deny を返す(委譲=素通りにしない)。
|
|
666
687
|
print('\n=== (g2) PermissionRequest フックのフェイルクローズ(関門のみ) ===')
|
|
667
|
-
GATE_PR_EV = {'tool_name': 'Bash', 'tool_input': {'command': '
|
|
688
|
+
GATE_PR_EV = {'tool_name': 'Bash', 'tool_input': {'command': 'npm publish'},
|
|
668
689
|
'cwd': '/home/fshim/projects/conductor', 'session_id': 's1'}
|
|
669
690
|
|
|
670
691
|
|