Files
ISEP/scripts/test-stage.sh
T
Leo d19bf88503 雲端對 stage 的寫入有正門:scripts/stage(secret/api)只寫 youlin,白名單放固定形狀(inkstone/ISEP#137)
09-07 三種寫入在雲端全被 auto mode 分類器擋:npx wrangler secret put、
stage-deploy-artifacts.py --confirm、curl POST 到 arcrun-yuga3bse——
白名單是逐條比前綴,複合指令與任意 curl 永遠對不上;放整類又是紅線。

- scripts/stage secret put|list|delete:直接打 CF API(不經 npx);先 GET /accounts
  看這把 token 打得到誰,stage 的 ID 不在裡面就拒絕並印出它打得到誰;URL 永遠只帶
  1129efd7…;值只從 --from-env 讀,--value 拒收。
- scripts/stage api <METHOD> <worker>/<path>:curl 打實例,主機只會是
  *.arcrun-yuga3bse.workers.dev,別的主機拒絕;標頭與 body 走 stdin 設定檔+管線,
  不走 argv;--bearer-env/--header-env/--data-env 從環境變數讀。
- docs/permissions-allow.json:stage 四種形狀+Arcrun 自己那支 stage-deploy-artifacts.py
  的相對形狀(它已寫死只認 youlin 帳號 ID、不收 --account/--token,不重造)。
- scripts/test-stage.sh 45 條(假 CF 對 PUT 回 201——第一次實跑 CF 回 201 被講成失敗,
  就是這格抓的);test-settings-allow-sync 仍 19/19。
- 真跑:探針種進 youlin arcrun-cypher-executor、CF API 獨立列到、刪掉恢復原狀;
  api GET health 200、無 Bearer POST 401、leo21c 主機拒絕零請求。
  「帶 Bearer 真寫 KBDB」缺 KBDB_INTERNAL_TOKEN 沒跑(TESTING A36 標明)。

盤點在本樹實數:hooks 61、註冊 85(都沒動)、scripts 56(+stage、+test-stage.sh)。
版本待總管定版。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTZ9QtvjY7MNxjfQbexAm7
2026-09-07 02:31:18 +00:00

248 lines
21 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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/<worker>/… 扮 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 "<redacted>"
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 給非 stageuncle6 的 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 的 nametype 對,而且 text 就是環境變數裡那個值(不是變數名)" || bad "C3 $(grep PUT "$LOG")"
no_value && ok "C4 stdoutstderr 不含那個值" || 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 <worker>/<path>?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 --jsonbody 原樣送到、自動帶 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": "<redacted>"' "$LOG" && no_value \
&& ok "D7 --data-envbody 從環境變數來(機敏 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-filebody 從檔案來" || 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 UAArcrun#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 ]