#!/bin/bash # scripts/stage 的測試(inkstone/ISEP#137) # # 它守什麼: 「雲端總管對 stage 的每一種寫入都有一支固定形狀的正門,而且**只**寫得進 stage」這件事的機器那一半。 # 09-07 三種寫入(`npx wrangler secret put`/`stage-deploy-artifacts.py`/curl POST 到 arcrun-yuga3bse)在雲端 # 全被 auto mode 分類器擋(複合指令與任意 curl 對不上白名單任何一條)。 # 收成 `stage secret …`(CF API)與 `stage api …`(curl 打實例);值只從環境變數名讀; # 判準是 CF 帳號 ID/stage 主機往放行方向比對,不看名字。 # # 🔴 全程離線:CF API 與 stage 實例都指到本機一個假伺服器,不打真 Cloudflare、不碰任何 worker。 # 真的種一把進 youlin、真的打 stage 實例的實跑在 docs/TESTING.md A36 另記。 # 假伺服器對 secret PUT 回 **201**——真 CF 就是回 201,第一次實跑把 201 講成失敗就是這樣抓到的。 # # 四個方向都要驗,缺一個就是假綠: # A 拒絕(非 stage/缺變數/值在指令列)⇒ 離開碼 2,而且**沒打網路** # B token 打不到 stage ⇒ 拒絕、印出它打得到誰、沒有任何 PUT # C 種進 stage ⇒ PUT 的路徑帶 stage ID、body 對、值不出現在任何輸出、種完用列表確認 # D api:主機只會是 stage;標頭值從環境變數來;body 三種來源;非 2xx 離開碼 1 # E 接線:白名單有它四種形狀+Arcrun 那支;ISEP 自己的 Bash 閘對這些呼叫形狀一支都不擋(誤攔比漏擋嚴重) cd "$(dirname "$0")/.." || exit 1 ROOT=$(pwd) S=scripts/stage STAGE=1129efd7df2e8899d537e9c8fbabb6cb TMP=$(mktemp -d); trap 'kill $SRV 2>/dev/null; rm -rf "$TMP"' EXIT PASS=0; FAIL=0 ok(){ PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; } bad(){ FAIL=$((FAIL+1)); printf ' ❌ %s\n' "$1"; } LOG="$TMP/log.jsonl"; MODE="$TMP/mode"; STATE="$TMP/secrets.json" export STAGE_TEST_VALUE="v-ISEP137-$(python3 -c 'import secrets;print(secrets.token_hex(12))')-must-never-print" # 每次隨機:C8 才驗得出「值落地」 export TOK_STAGE='tok-stage-abcdefghijklmnopqrstuvwxyz0123456789' export KBDB_TOK_TEST="kbdb-$(python3 -c 'import secrets;print(secrets.token_hex(8))')" # ── 假伺服器:/accounts… 扮 CF API;//… 扮 stage 實例。mode 檔決定 CF 扮誰 ── cat > "$TMP/stub.py" <<'PY' import json, os, sys from http.server import BaseHTTPRequestHandler, HTTPServer TMP = sys.argv[1]; PORT = int(sys.argv[2]) STAGE = "1129efd7df2e8899d537e9c8fbabb6cb" def mode(): return open(os.path.join(TMP, "mode")).read().strip() def state(): p = os.path.join(TMP, "secrets.json") return json.load(open(p)) if os.path.exists(p) else ["KBDB_INTERNAL_TOKEN"] def save(s): json.dump(s, open(os.path.join(TMP, "secrets.json"), "w")) class H(BaseHTTPRequestHandler): def log_message(self, *a): pass def _send(self, code, obj): b = json.dumps(obj).encode(); self.send_response(code) self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(b))) self.end_headers(); self.wfile.write(b) def _body(self): n = int(self.headers.get("Content-Length", "0")); return self.rfile.read(n).decode() if n else "" def _log(self, raw=None): rec = {"method": self.command, "path": self.path, "auth": self.headers.get("Authorization", ""), "ua": self.headers.get("User-Agent", ""), "ct": self.headers.get("Content-Type", ""), "xkey": self.headers.get("X-Arcrun-API-Key", "")} if raw is not None: try: body = json.loads(raw) except ValueError: body = {} rec["body_name"] = body.get("name"); rec["body_type"] = body.get("type") rec["text_ok"] = body.get("text") == os.environ.get("STAGE_TEST_VALUE") rec["body_raw"] = raw if os.environ.get("STAGE_TEST_VALUE") not in raw else "" with open(os.path.join(TMP, "log.jsonl"), "a") as f: f.write(json.dumps(rec) + "\n") def _instance(self): # 扮 stage 實例:POST 沒帶 Bearer ⇒ 401(真 kbdb 就是這樣);其餘 200 回聲 raw = self._body(); self._log(raw) if self.command != "GET" and not self.headers.get("Authorization"): return self._send(401, {"error": "Unauthorized"}) self._send(200, {"ok": True, "echo_path": self.path, "echo_method": self.command}) def do_GET(self): if not self.path.startswith("/accounts"): return self._instance() self._log(); m = mode() if self.path == "/accounts": if m == "401": return self._send(401, {"success": False, "errors": [{"code": 10000, "message": "Authentication error"}]}) acct = {"stage": [{"id": STAGE, "name": "Youlin.hsieh.dev@gmail.com's Account"}], "leo21c": [{"id": "9f3b2c1d000000000000000000000leo", "name": "Leo21c's Account"}], "geek": [{"id": "77aa11bb000000000000000000000geek", "name": "geek6688's Account"}], "both": [{"id": STAGE, "name": "Youlin"}, {"id": "9f3b2c1d000000000000000000000leo", "name": "Leo21c"}]} return self._send(200, {"success": True, "errors": [], "result": acct.get(m, acct["stage"])}) if self.path.endswith("/secrets"): names = state() if m == "put-ok-list-missing": names = [n for n in names if n != "ISEP_137_PROBE"] return self._send(200, {"success": True, "errors": [], "result": [{"name": n, "type": "secret_text"} for n in names]}) self._send(404, {"success": False, "errors": [{"code": 7003, "message": "not found"}]}) def do_PUT(self): if not self.path.startswith("/accounts"): return self._instance() raw = self._body(); self._log(raw); body = json.loads(raw or "{}") s = state(); s = [x for x in s if x != body.get("name")] + [body.get("name")]; save(s) self._send(201, {"success": True, "errors": [], "result": {"name": body.get("name"), "type": "secret_text"}}) # 真 CF 回 201 def do_DELETE(self): if not self.path.startswith("/accounts"): return self._instance() self._log(); name = self.path.rsplit("/", 1)[-1] save([x for x in state() if x != name]); self._send(200, {"success": True, "errors": [], "result": None}) def do_POST(self): self._instance() def do_PATCH(self): self._instance() HTTPServer(("127.0.0.1", PORT), H).serve_forever() PY PORT=$(python3 -c "import socket;s=socket.socket();s.bind(('127.0.0.1',0));print(s.getsockname()[1])") python3 "$TMP/stub.py" "$TMP" "$PORT" & SRV=$! for _ in $(seq 1 50); do python3 -c "import socket;socket.create_connection(('127.0.0.1',$PORT),1)" 2>/dev/null && break; sleep 0.1; done export STAGE_SECRET_PUT_API_BASE="http://127.0.0.1:$PORT" export STAGE_API_BASE_OVERRIDE="http://127.0.0.1:$PORT" echo stage > "$MODE"; : > "$LOG" reset(){ : > "$LOG"; rm -f "$STATE"; echo "${1:-stage}" > "$MODE"; } run(){ OUT=$(python3 "$S" "$@" 2>&1); RC=$?; } hits(){ grep -c "\"method\": \"$1\"" "$LOG" 2>/dev/null || true; } no_value(){ ! printf '%s' "$OUT" | grep -qF "$STAGE_TEST_VALUE"; } echo "── A. 本工具自己拒絕的:離開碼 2,而且沒打網路 ──" reset; run secret put --worker w --secret X --from-env STAGE_TEST_VALUE --token-env TOK_STAGE --account 58309bb90fd93ad6d0fe0aae99170e9d [ "$RC" = 2 ] && printf '%s' "$OUT" | grep -q "$STAGE" && printf '%s' "$OUT" | grep -q '交回地端' && [ "$(wc -l < "$LOG")" = 0 ] \ && ok "A1 --account 給非 stage(uncle6 的 ID)⇒ 拒絕、講出 stage 是誰、出路是交回地端、一個請求都沒發" || bad "A1 rc=$RC hits=$(wc -l < "$LOG") $OUT" reset; run secret put --worker w --secret X --from-env STAGE_TEST_VALUE --token-env NOPE_TOKEN_VAR [ "$RC" = 2 ] && printf '%s' "$OUT" | grep -q 'NOPE_TOKEN_VAR' && [ "$(wc -l < "$LOG")" = 0 ] \ && ok "A2 token 的環境變數沒值 ⇒ 拒絕、點名那個變數、沒打網路" || bad "A2 rc=$RC $OUT" reset; run secret put --worker w --secret X --from-env NOPE_VALUE_VAR --token-env TOK_STAGE [ "$RC" = 2 ] && printf '%s' "$OUT" | grep -q 'NOPE_VALUE_VAR' && printf '%s' "$OUT" | grep -q '紅線' && [ "$(wc -l < "$LOG")" = 0 ] \ && ok "A3 值的環境變數沒值 ⇒ 拒絕、點名變數、明講不准改寫進指令列、沒打網路" || bad "A3 rc=$RC $OUT" reset; run secret put --worker w --secret X --value "$STAGE_TEST_VALUE" --token-env TOK_STAGE [ "$RC" = 2 ] && printf '%s' "$OUT" | grep -q -- '--value' && no_value && [ "$(wc -l < "$LOG")" = 0 ] \ && ok "A4 --value(值在指令列)⇒ 拒收,而且拒絕訊息裡不回顯那個值" || bad "A4 rc=$RC $OUT" reset; run secret put --secret X --from-env STAGE_TEST_VALUE --token-env TOK_STAGE [ "$RC" = 2 ] && printf '%s' "$OUT" | grep -q -- '--worker' && ok "A5 缺 --worker ⇒ 拒絕、講缺哪個" || bad "A5 rc=$RC $OUT" reset; run frobnicate --worker w [ "$RC" = 2 ] && ok "A6 不認得的子命令 ⇒ 拒絕" || bad "A6 rc=$RC $OUT" run --help; [ "$RC" = 0 ] && printf '%s' "$OUT" | grep -q -- '--from-env' && printf '%s' "$OUT" | grep -q -- '--bearer-env' && ok "A7 --help 印兩個子命令的用法、離開碼 0" || bad "A7 rc=$RC" echo "── B. token 打不到 stage ⇒ 拒絕、印出它打得到誰、沒有 PUT ──" reset leo21c; run secret put --worker w --secret X --from-env STAGE_TEST_VALUE --token-env TOK_STAGE [ "$RC" = 2 ] && printf '%s' "$OUT" | grep -q "Leo21c's Account" && printf '%s' "$OUT" | grep -q "$STAGE" && [ "$(hits PUT)" = 0 ] && no_value \ && ok "B1 token 只打得到 leo21c ⇒ 拒絕、列出那個帳號、講出 stage 是哪個 ID、零 PUT" || bad "B1 rc=$RC PUT=$(hits PUT) $OUT" reset geek; run secret put --worker w --secret X --from-env STAGE_TEST_VALUE --token-env TOK_STAGE [ "$RC" = 2 ] && printf '%s' "$OUT" | grep -q "geek6688" && [ "$(hits PUT)" = 0 ] \ && ok "B2 token 只打得到 geek6688(出貨機)⇒ 同樣拒絕、零 PUT" || bad "B2 rc=$RC $OUT" reset 401; run secret put --worker w --secret X --from-env STAGE_TEST_VALUE --token-env TOK_STAGE [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q 'Authentication error' && [ "$(hits PUT)" = 0 ] \ && ok "B3 CF 回 401 ⇒ 離開碼 1(API 失敗,不是「拒絕」也不是 000)、帶 CF 的錯誤字串、零 PUT" || bad "B3 rc=$RC $OUT" reset; STAGE_SECRET_PUT_API_BASE="http://127.0.0.1:1" run secret put --worker w --secret X --from-env STAGE_TEST_VALUE --token-env TOK_STAGE [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q '__agentproxy/status' && no_value \ && ok "B4 連不到 CF ⇒ 離開碼 1、印錯誤類型、指到 agentproxy status(不把三種病記成 000)" || bad "B4 rc=$RC $OUT" reset both; run secret put --worker w --secret ISEP_137_PROBE --from-env STAGE_TEST_VALUE --token-env TOK_STAGE [ "$RC" = 0 ] && grep -q "\"path\": \"/accounts/$STAGE/workers/scripts/w/secrets\"" "$LOG" && ! grep -q '0000leo' "$LOG" \ && ok "B5 token 兩個帳號都打得到 ⇒ 放行,但 URL 只帶 stage 的 ID(寫入永遠只到 stage)" || bad "B5 rc=$RC $(cat "$LOG")" echo "── C. 種進 stage(假 CF 對 PUT 回 201,跟真的一樣)──" reset; run secret put --worker arcrun-cypher-executor --secret ISEP_137_PROBE --from-env STAGE_TEST_VALUE --token-env TOK_STAGE [ "$RC" = 0 ] && ok "C1 離開碼 0(201 算成功——第一次實跑把 201 講成失敗,就是這格抓的)" || bad "C1 rc=$RC $OUT" grep -q "\"method\": \"PUT\", \"path\": \"/accounts/$STAGE/workers/scripts/arcrun-cypher-executor/secrets\"" "$LOG" \ && ok "C2 PUT 打的是 stage 帳號 ID 底下那顆 worker" || bad "C2 $(cat "$LOG")" grep '"method": "PUT"' "$LOG" | grep -q '"body_name": "ISEP_137_PROBE", "body_type": "secret_text", "text_ok": true' \ && ok "C3 body 的 name/type 對,而且 text 就是環境變數裡那個值(不是變數名)" || bad "C3 $(grep PUT "$LOG")" no_value && ok "C4 stdout/stderr 不含那個值" || bad "C4 值印出來了" grep -q '"auth": "Bearer tok-stage-' "$LOG" && ok "C5 Authorization 帶的是 --token-env 指的那把" || bad "C5 $(head -1 "$LOG")" printf '%s' "$OUT" | grep -q 'ISEP_137_PROBE' && printf '%s' "$OUT" | grep -q 'KBDB_INTERNAL_TOKEN' \ && ok "C6 種完把列表印出來(新的那把+原本就有的)" || bad "C6 $OUT" [ "$(grep -c '"method": "GET", "path": "/accounts/'"$STAGE"'/workers/scripts/arcrun-cypher-executor/secrets"' "$LOG")" = 1 ] \ && ok "C7 種完真的去列了一次(不是只信 PUT 的回應)" || bad "C7 $(cat "$LOG")" ! grep -rqF "$STAGE_TEST_VALUE" "$ROOT" 2>/dev/null \ && ok "C8 值沒落在 repo 樹裡任何檔案" || bad "C8 值落地了:$(grep -rlF "$STAGE_TEST_VALUE" "$ROOT")" reset put-ok-list-missing; run secret put --worker w --secret ISEP_137_PROBE --from-env STAGE_TEST_VALUE --token-env TOK_STAGE [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q '列表卻沒有' \ && ok "C9 CF 說成功但列表沒有 ⇒ 離開碼 1(不把 2xx 講成種好了)" || bad "C9 rc=$RC $OUT" reset; run secret list --worker w --token-env TOK_STAGE [ "$RC" = 0 ] && printf '%s' "$OUT" | grep -q 'KBDB_INTERNAL_TOKEN' && [ "$(hits PUT)" = 0 ] && ok "C10 secret list 印名字、不寫" || bad "C10 rc=$RC $OUT" reset; run secret delete --worker w --secret KBDB_INTERNAL_TOKEN --token-env TOK_STAGE [ "$RC" = 0 ] && grep -q "\"method\": \"DELETE\", \"path\": \"/accounts/$STAGE/workers/scripts/w/secrets/KBDB_INTERNAL_TOKEN\"" "$LOG" \ && ok "C11 secret delete 打對路徑、刪完列表確認" || bad "C11 rc=$RC $OUT $(cat "$LOG")" reset leo21c; run secret delete --worker w --secret X --token-env TOK_STAGE [ "$RC" = 2 ] && [ "$(hits DELETE)" = 0 ] && ok "C12 delete 對非 stage 的 token 一樣拒絕" || bad "C12 rc=$RC" echo "── D. api:主機只會是 stage、標頭值從環境變數來、body 三種來源 ──" reset; run api POST https://arcrun-cypher-executor.leo21c.workers.dev/kbdb/entries --json '{}' [ "$RC" = 2 ] && printf '%s' "$OUT" | grep -q 'leo21c.workers.dev' && printf '%s' "$OUT" | grep -q 'arcrun-yuga3bse' && [ "$(wc -l < "$LOG")" = 0 ] \ && ok "D1 完整網址指到 leo21c ⇒ 拒絕、講出 stage 主機是什麼、沒打網路" || bad "D1 rc=$RC $OUT" reset; run api POST https://arcrun-kbdb.youlin-hsieh-dev.workers.dev/records --json '{}' [ "$RC" = 2 ] && [ "$(wc -l < "$LOG")" = 0 ] && ok "D2 09-02 已死的舊子網域 youlin-hsieh-dev 也拒絕(不是 stage 的識別碼)" || bad "D2 rc=$RC $OUT" reset; run api POST arcrun-kbdb/records --bearer 'Bearer xyz' --json '{}' [ "$RC" = 2 ] && printf '%s' "$OUT" | grep -q -- '--bearer' && [ "$(wc -l < "$LOG")" = 0 ] && ok "D3 --bearer/--header(值在指令列)⇒ 拒收" || bad "D3 rc=$RC $OUT" reset; run api POST arcrun-kbdb/records --bearer-env NOPE_KBDB --json '{}' [ "$RC" = 2 ] && printf '%s' "$OUT" | grep -q 'NOPE_KBDB' && [ "$(wc -l < "$LOG")" = 0 ] && ok "D4 --bearer-env 指的變數沒值 ⇒ 拒絕、點名" || bad "D4 rc=$RC $OUT" reset; run api POST 'arcrun-kbdb/map/recompute?library=rt-lib&owner_id=yuga3bse' --bearer-env KBDB_TOK_TEST [ "$RC" = 0 ] && grep -q '"method": "POST", "path": "/arcrun-kbdb/map/recompute?library=rt-lib&owner_id=yuga3bse", "auth": "Bearer kbdb-' "$LOG" \ && printf '%s' "$OUT" | grep -q '^HTTP 200' && ok "D5 POST /?query:主機由工具補、query 原樣、Bearer 從環境變數來、印 HTTP 200、離開碼 0" || bad "D5 rc=$RC $OUT $(cat "$LOG")" reset; run api POST arcrun-kbdb/records --bearer-env KBDB_TOK_TEST --json '{"template":"triplet","values":{"s":"a"}}' grep -q '"ct": "application/json"' "$LOG" && grep -q '"body_raw": "{\\"template\\":\\"triplet\\",\\"values\\":{\\"s\\":\\"a\\"}}"' "$LOG" \ && ok "D6 --json:body 原樣送到、自動帶 Content-Type: application/json" || bad "D6 $(cat "$LOG")" reset; STAGE_BODY_TEST='{"secret_ref":"'"$STAGE_TEST_VALUE"'"}' run api PUT arcrun-cypher-executor/credentials/gitea_token --bearer-env KBDB_TOK_TEST --data-env STAGE_BODY_TEST [ "$RC" = 0 ] && grep -q '"method": "PUT", "path": "/arcrun-cypher-executor/credentials/gitea_token"' "$LOG" && grep -q '"body_raw": ""' "$LOG" && no_value \ && ok "D7 --data-env:body 從環境變數來(機敏 body 不進指令列),值沒印出來" || bad "D7 rc=$RC $OUT $(cat "$LOG")" reset; printf '{"x":1}' > "$TMP/body.json"; run api POST arcrun-kbdb/records --bearer-env KBDB_TOK_TEST --data-file "$TMP/body.json" [ "$RC" = 0 ] && grep -q '"body_raw": "{\\"x\\":1}"' "$LOG" && ok "D8 --data-file:body 從檔案來" || bad "D8 rc=$RC $(cat "$LOG")" reset; run api POST arcrun-kbdb/records --json '{}' [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q '^HTTP 401' && ok "D9 沒帶 Bearer 的 POST ⇒ 實例回 401 ⇒ 離開碼 1(不把非 2xx 講成成功)" || bad "D9 rc=$RC $OUT" reset; run api GET arcrun-cypher-executor/health [ "$RC" = 0 ] && grep -q '"method": "GET", "path": "/arcrun-cypher-executor/health"' "$LOG" && ok "D10 GET 不帶 body、不帶 Content-Type" || bad "D10 rc=$RC $OUT" grep -q '"ct": ""' "$LOG" && ok "D11 GET 的請求真的沒有 Content-Type" || bad "D11 $(cat "$LOG")" reset; run api POST arcrun-cypher-executor/webhooks/named/yuga3bse/x/trigger --header-env X-Arcrun-API-Key=KBDB_TOK_TEST --bearer-env KBDB_TOK_TEST --json '{}' grep -q '"xkey": "kbdb-' "$LOG" && ok "D12 --header-env Name=VAR:任意標頭值從環境變數來" || bad "D12 $(cat "$LOG")" reset; run api POST https://arcrun-kbdb.arcrun-yuga3bse.workers.dev/records --bearer-env KBDB_TOK_TEST --json '{}' [ "$RC" = 0 ] && grep -q '"path": "/arcrun-kbdb/records"' "$LOG" && ok "D13 完整網址但主機就是 stage ⇒ 收" || bad "D13 rc=$RC $OUT" reset; run api POST 'arcrun-kbdb/records' --bearer-env KBDB_TOK_TEST --json '{}' --data-file x [ "$RC" = 2 ] && [ "$(wc -l < "$LOG")" = 0 ] && ok "D14 --json 與 --data-file 同時給 ⇒ 拒絕" || bad "D14 rc=$RC" reset; run api FROB arcrun-kbdb/records [ "$RC" = 2 ] && ok "D15 不認得的 METHOD ⇒ 拒絕" || bad "D15 rc=$RC" reset; STAGE_API_BASE_OVERRIDE="http://127.0.0.1:1" run api GET arcrun-cypher-executor/health [ "$RC" = 1 ] && printf '%s' "$OUT" | grep -q 'curl 失敗' && printf '%s' "$OUT" | grep -q '__agentproxy/status' && ok "D16 curl 連不上 ⇒ 離開碼 1、印 curl 的錯誤字串與 agentproxy 出路(不記成 000)" || bad "D16 rc=$RC $OUT" grep -q '"ua": "isep-stage/1' "$LOG" 2>/dev/null || { reset; run api GET arcrun-cypher-executor/health; } grep -q '"ua": "isep-stage/1' "$LOG" && ok "D17 UA 是自己的名字(youlin 邊緣擋 python-urllib UA,Arcrun#176 comment 6487)" || bad "D17 $(cat "$LOG")" echo "── E. 接線 ──" python3 - <<'PY' && ok "E1 docs/permissions-allow.json 有 stage 四種呼叫形狀+Arcrun 的 stage-deploy-artifacts.py,沒放整類" || bad "E1 白名單形狀不齊或放了整類" import json rules = json.load(open("docs/permissions-allow.json"))["allow"] need = ['Bash(python3 ~/.claude/plugins/cache/inkstone/isep/*/scripts/stage *)', 'Bash(python3 "$CLAUDE_PLUGIN_ROOT/scripts/stage" *)', 'Bash(python3 "$CLAUDE_PLUGIN_ROOT"/scripts/stage *)', 'Bash(python3 scripts/stage *)', 'Bash(python3 scripts/stage-deploy-artifacts.py *)'] for n in need: assert n in rules, "缺 " + n assert not any(r in ("Bash(npx *)", "Bash(wrangler *)", "Bash(npx wrangler *)", "Bash(python3 *)", "Bash(curl *)") for r in rules) PY # ISEP 自己掛在 PreToolUse Bash 上的每一支閘,對三種真實呼叫形狀都不准擋(清單當場從 hooks.json 讀,不寫死) BLOCKED="" while IFS= read -r CMD; do for h in $(python3 -c " import json d=json.load(open('hooks/hooks.json')) for g in d['hooks']['PreToolUse']: if 'Bash' in (g.get('matcher') or ''): for x in g['hooks']: print(x['command'].split('/')[-1]) "); do [ -f "hooks/$h" ] || continue printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$CMD")},\"session_id\":\"t137\",\"cwd\":\"$ROOT\"}" \ | CLAUDE_PLUGIN_ROOT="$ROOT" CLAUDE_PROJECT_DIR="$ROOT" bash "hooks/$h" >/dev/null 2>"$TMP/hook.err"; rc=$? [ "$rc" = 2 ] && BLOCKED="$BLOCKED $h←$(printf '%s' "$CMD" | cut -c1-40)" done done <<'CMDS' python3 ~/.claude/plugins/cache/inkstone/isep/0.24.0/scripts/stage secret put --worker arcrun-cypher-executor --secret CF_SECRETS_API_TOKEN --from-env CF_SECRETS_API_TOKEN_VALUE python3 "$CLAUDE_PLUGIN_ROOT/scripts/stage" api POST 'arcrun-kbdb/map/recompute?library=rt-lib&owner_id=yuga3bse' --bearer-env KBDB_INTERNAL_TOKEN python3 scripts/stage-deploy-artifacts.py all --confirm CMDS [ -z "$BLOCKED" ] && ok "E2 ISEP 掛在 Bash 上的閘,對三種真實呼叫形狀(含 secret 字樣、POST、--confirm)一支都不擋——誤攔比漏擋嚴重" || bad "E2 這幾支誤攔:$BLOCKED" grep -q 'scripts/stage ' scripts/test-settings-allow-sync.sh && grep -q 'stage-deploy-artifacts.py' scripts/test-settings-allow-sync.sh \ && ok "E3 settings-allow-sync 的測試把兩支都算進正門工具(少了會紅)" || bad "E3 settings-allow-sync 測試不認得它們" [ ! -e scripts/stage-secret-put ] && ok "E4 沒有第二支平行的工具(stage-secret-put 已收進 stage secret)" || bad "E4 還留著 stage-secret-put" echo; echo "$PASS/$((PASS+FAIL)) 通過" [ "$FAIL" -eq 0 ]