#!/usr/bin/env bash # reset_agent.sh — ARC Council 席位重置工具(新 ARM 機版 · 2026-09-30) # 放喺 /root/reset_agent.sh(0700 root:root)。CA (ca.js) 會以 ` --yes` 調用。 # # 用法(全部要 sudo): # sudo /root/reset_agent.sh # 現況一覽(唯讀,唔會掂 DB 以外嘢) # sudo /root/reset_agent.sh A5 # 重置 A5:撤銷舊 session → mint 新入場碼(會問 y) # sudo /root/reset_agent.sh A5 A7 A9 # 一次重置幾個(只影響列出嘅席位) # sudo /root/reset_agent.sh A5 --check # 只檢查+列計劃,零改動 # sudo /root/reset_agent.sh A5 --yes # 唔問,直接做(CA 用呢個) # --endpoint https://xxx.trycloudflare.com # 手動指定 relay 端點(或用 ARC_ENDPOINT 環境變數) # # 端點解析次序:--endpoint > ARC_ENDPOINT > /root/public/endpoint.txt 等檔案 > journal 自動偵測 # (quick tunnel URL 會轉;本檔絕不 hardcode endpoint — 搵唔到就停手教你設) # 做乜(每席位,只動該席位): # ① councilctl slot-clear 撤銷該席位 session(其他 9 席零影響) # ② 若席位未啟用(fresh council 只預啟 A1–A3)→ 嘗試 councilctl slot-enable(進化版先有) # ③ mint 一個新入場碼(consume-once;冇 prompts-create 副作用) # ④ 寫 $STATE/exports/PROMPT-A.txt(0600)+ 複製 $HOME/public/PROMPT-A.txt(0644, # CA postcondition 要見到新檔;首問貼文用) # ⑤ 印下一步指示 —— 入場碼永不出現在畫面/log # 安全:預設互動確認(y/N);--check 零改動;每步失敗即停;唔掂其他席位;唔印 credential。 set -Eeuo pipefail umask 077 ROOT="${ARC_ROOT:-/opt/ai-research-council}" STATE="${ARC_STATE_ROOT:-/var/lib/ai-research-council/council}" SEATS_CEILING="${SEATS_CEILING:-100}" PUB="${ARC_PUBLIC_DIR:-/root/public}" CC="$ROOT/councilctl.py" DB="$STATE/state.sqlite3" export RESET_RC_ROOT="$ROOT" # inline python 用嚟 import councilctl EP_FILES=("/root/public/endpoint.txt" "/etc/ai-research-council/endpoint.txt") SLOTS=(); CHECK=0; YES=0; ENDPOINT_ARG="" die(){ echo "❌ $1" >&2; exit "${2:-1}"; } while [ $# -gt 0 ]; do case "$1" in --check) CHECK=1; shift;; --yes|-y) YES=1; shift;; --endpoint) [ $# -ge 2 ] || die "--endpoint 要有值" 2; ENDPOINT_ARG="$2"; shift 2;; -h|--help) sed -n '2,22p' "$0"; exit 0;; A[0-9]*) SLOTS+=("$1"); shift;; *) echo "未知參數: $1(席位格式 A1..A$SEATS_CEILING)" >&2; exit 2;; esac done for S in "${SLOTS[@]:-}"; do [ -n "$S" ] || continue [[ "$S" =~ ^A([1-9][0-9]*)$ ]] || die "席位格式唔啱: $S(A1..A$SEATS_CEILING)" 2 _n="${S#A}"; [ "$_n" -ge 1 ] && [ "$_n" -le "$SEATS_CEILING" ] || die "席位超出範圍: $S(1..$SEATS_CEILING)" 2 done echo "══ reset_agent(新機版)══ ROOT=$ROOT" [ "$(id -u)" = "0" ] || die "要用 sudo 跑(讀 master key 要 root)" [ -f "$CC" ] || die "缺檔: $CC" 3 resolve_ep(){ # 成功 → stdout 印 "EP<->SRC"(die 會去 stderr) if [ -n "$ENDPOINT_ARG" ]; then printf '%s<->%s\n' "$ENDPOINT_ARG" "--endpoint"; return 0; fi if [ -n "${ARC_ENDPOINT:-}" ]; then printf '%s<->%s\n' "$ARC_ENDPOINT" "ARC_ENDPOINT"; return 0; fi local f u="" for f in "${EP_FILES[@]}"; do [ -f "$f" ] || continue u="$(grep -oE 'https://[a-z0-9.-]+\.(trycloudflare\.com|ts\.net)' "$f" 2>/dev/null | head -1 || true)" [ -n "$u" ] && { printf '%s<->%s\n' "$u" "file:$f"; return 0; } done u="$(journalctl --no-pager -u arc-relay.service -u ai-council-quick-tunnel.service -n 500 2>/dev/null \ | grep -oE 'https://[a-z0-9.-]+\.(trycloudflare\.com|ts\.net)' | tail -1 || true)" [ -n "$u" ] && { printf '%s<->%s\n' "$u" "journal:units"; return 0; } u="$(journalctl --no-pager -g 'https://[a-z0-9-]+\.trycloudflare\.com' -n 20 2>/dev/null \ | grep -oE 'https://[a-z0-9.-]+\.(trycloudflare\.com|ts\.net)' | tail -1 || true)" [ -n "$u" ] && { printf '%s<->%s\n' "$u" "journal:global"; return 0; } die "搵唔到 relay endpoint(tunnel URL 會轉,本檔唔會 hardcode)—— 設咗再跑:export ARC_ENDPOINT=https://.trycloudflare.com 或將現行 URL 寫入 /root/public/endpoint.txt" 3 } slot_enabled(){ # → 1/0;冇 DB 或冇 row → 2 python3 - "$DB" "${1#A}" <<'PY' import sqlite3, sys, os db, n = sys.argv[1], int(sys.argv[2]) if not os.path.isfile(db): print(2); raise SystemExit try: con = sqlite3.connect(f"file:{db}?mode=ro", uri=True) row = con.execute("SELECT enabled FROM agent_slots WHERE slot=?", (n,)).fetchone() con.close() print(2 if row is None else int(row[0])) except Exception: print(2) PY } ensure_enabled(){ # 只喺實跑;啟用唔到 → exit 5(v18 bundle 冇 slot-enable) # 舊線(live_extract/P6)驗證要求 enabled = 連續前綫 1..K:要啟 A5 就要連 A4 一齊啟, # 否則 gap 會令下一次 open_core 永久 "agent slot capacity mismatch"。補啟只改 enabled, # 唔掂其他席位嘅 session/prompt。 local t="${1#A}" n out rc for n in $(seq 4 "$t"); do [ "$(slot_enabled "A$n")" = "1" ] && continue set +e out="$(python3 "$CC" slot-enable --slot "A$n" 2>&1)"; rc=$? set -e if [ "$rc" = "0" ]; then echo " ✓ 已啟用 A$n(slot-enable;前綫補啟)" elif echo "$out" | grep -qiE 'invalid choice|unrecognized'; then die "$1 未啟用,而且此 councilctl 冇 slot-enable 子命令 (fresh v18 只預啟 A1–A3;A4–A10 要 council 側先有 enable 能力,另一單)" 5 else die "slot-enable A$n 失敗:$(echo "$out" | tail -1)" 5 fi done } view_mode(){ echo "現況(唯讀;只讀 DB,唔掂任何嘢):" python3 - "$DB" <<'PY' import sqlite3, sys, os db = sys.argv[1] if not os.path.isfile(db): print(" (state.sqlite3 未建 —— council 未起過,冇嘢可睇)") raise SystemExit(0) con = sqlite3.connect(f"file:{db}?mode=ro", uri=True) rows = con.execute( "SELECT slot,enabled,status,agent_id_digest IS NOT NULL,last_seen_at " "FROM agent_slots ORDER BY slot").fetchall() con.close() print(" slot enabled status 已綁定 last_seen") for s, en, st, bound, seen in rows: print(f" A{s:<-4} {'是' if en else '否':<6} {st:<16} {'是' if bound else '否':<4} {seen or '-'}") PY echo echo "用法:sudo $0 A5 (重置 A5)" echo " sudo $0 A5 A7 --check (只看計劃,零改動)" exit 0 } [ ${#SLOTS[@]} -eq 0 ] && view_mode echo "目標席位: ${SLOTS[*]}" if [ "$CHECK" = "1" ]; then echo "模式: --check(零改動:唔 slot-clear、唔 mint、唔寫檔)" EP="(未解析到 —— 實跑時要 ARC_ENDPOINT/檔案/journal 其中一個)"; EP_SRC="(check)" if r="$(resolve_ep 2>/dev/null)"; then EP="${r%%<->*}"; EP_SRC="${r#*<->}"; fi echo "端點: $EP ← $EP_SRC" rc_all=0 for S in "${SLOTS[@]}"; do echo "──────── $S ────────" en="$(slot_enabled "$S")" if [ "$en" = "2" ]; then if [ ! -f "$DB" ]; then echo " · DB 未建(council 未起過)→ 實跑會建+種子 A1–A3 enabled" else echo " · DB 冇 $S 行(首次 open 會補種子)" fi echo " 計劃: slot-clear → mint → 寫 exports/PROMPT-$S.txt(0600) → 複製 $PUB/(0644)" continue fi if [ "$en" = "0" ]; then echo " ⚠ $S 而家 disabled(fresh council 只預啟 A1–A3)" set +e; probe="$(python3 "$CC" slot-enable --slot "$S" 2>&1)"; prc=$?; set -e if [ "$prc" = "0" ]; then echo " (slot-enable 子命令存在但 check 模式唔會執行佢;實跑會先啟用)" elif echo "$probe" | grep -qiE 'invalid choice|unrecognized'; then echo " ✗ 此 councilctl 冇 slot-enable → 實跑會喺 $S 停(exit 5);A4–A10 要 council 側處理" else echo " ✗ slot-enable probe 失敗:$(echo "$probe" | tail -1)" fi else echo " ✓ $S enabled" fi bound="$(python3 - "$DB" "${S#A}" <<'PY' import sqlite3, sys, os db, n = sys.argv[1], int(sys.argv[2]) con = sqlite3.connect(f"file:{db}?mode=ro", uri=True) row = con.execute("SELECT agent_id_digest IS NOT NULL FROM agent_slots WHERE slot=?", (n,)).fetchone() con.close() print("?" if row is None else int(row[0])) PY )" [ "$bound" = "1" ] && echo " ⚠ $S 仍然綁住 → 實跑第一步 slot-clear 會撤銷(其他席位唔郁)" || echo " ✓ $S 未綁定" echo " 計劃: slot-clear → mint → 寫 exports/PROMPT-$S.txt(0600) → 複製 $PUB/(0644)" done echo "══ --check 完(零改動)══" exit "$rc_all" fi # ---- 實跑 ---- if [ "$YES" = "0" ]; then if [ -t 0 ]; then printf "確認重置 %s?(會撤銷該席位現有 session;其他席位不受影響)[y/N] " "${SLOTS[*]}" read -r ans || ans="" case "$ans" in y|Y|yes|YES) ;; *) echo "已取消"; exit 0;; esac else echo "非互動終端又冇 --yes → 已取消(唔郁檔)"; exit 0 fi fi r="$(resolve_ep)" # set -e:失敗(die/exit3)會即停,die 訊息喺 stderr 可見 EP="${r%%<->*}"; EP_SRC="${r#*<->}" echo "端點: $EP($EP_SRC)" # 端點格式先行驗證(fail-closed:格式錯就唔郁任何嘢,唔會 clear 完先死) python3 - "$EP" <<'PY' import os, sys sys.path.insert(0, os.environ["RESET_RC_ROOT"]) import councilctl as C try: C.validate_endpoint(sys.argv[1]) except BaseException as e: print(f"❌ 端點唔啱(乜都未郁):{e}"); raise SystemExit(3) print(" 端點格式 ✓") PY for S in "${SLOTS[@]}"; do echo "──────── 處理 $S ────────" # ① 先驗席位可用性(v18 argparse choices=上市場 A1-A3;disabled → slot-enable 探測) ensure_enabled "$S" # ② 解綁(只動一個 slot;冇绑 = ALREADY_AVAILABLE 都係 exit 0) out="$(python3 "$CC" slot-clear --slot "$S" 2>&1)" || die "slot-clear $S 失敗:$(echo "$out" | tail -1)" echo " ① slot-clear: $(echo "$out" | tail -1)" # ②.5 exports 目錄(controller 未起時由我哋補;有就零掂) [ -d "$STATE/exports" ] || install -d -m 700 -o ai-council "$STATE/exports" # ③+④ mint + 寫檔(0600;入場碼只喺檔內) python3 - "$S" "$EP" <<'PY' import os, secrets, sys, hashlib sys.path.insert(0, os.environ["RESET_RC_ROOT"]) import councilctl as C slot, ep = sys.argv[1], sys.argv[2] n = int(slot[1:]) try: C.validate_endpoint(ep) except BaseException as e: print(f" ❌ 端點唔啱:{e}"); raise SystemExit(3) try: key = C.read_master_key() except BaseException as e: print(f" ❌ 讀唔到 master key:{e}(要用 sudo 跑)"); raise SystemExit(7) C.drop_to_service_user() core = C.open_core(key) try: row = core.db.execute("SELECT * FROM agent_slots WHERE slot=?", (n,)).fetchone() if row is None: print(f" ❌ DB 冇 {slot}"); raise SystemExit(4) if int(row["enabled"]) != 1: print(f" ❌ {slot} 未啟用 → 此 councilctl 冇 slot-enable,要 council 側先enable"); raise SystemExit(5) if row["agent_id_digest"] is not None: print(f" ⚠ {slot} 仍然綁住(slot-clear 未生效?)—— 停手,請人手檢查"); raise SystemExit(6) pid = f"prompt-{slot.casefold()}-{secrets.token_hex(8)}" # bundle 無 MAXT(佢自己 :355 都係 latent NameError);create_enrollment 預設 # ttl=2147483647 即允許最大值=「長效入場碼」,同 refs C.MAXT 意圖一致 code = core.create_enrollment(n, pid) content = C.prompt_text(slot, pid, code, ep).encode("utf-8") finally: core.close() path = C.EXPORT_ROOT / f"PROMPT-{slot}.txt" C.atomic_write(path, content) print(f" ✅ 新提示檔:{path}") print(f" bytes={os.path.getsize(path)} sha256={hashlib.sha256(open(path,'rb').read()).hexdigest()[:16]}…") print(f" prompt_id={pid}") print(" 入場碼只喺檔內(0600);唔准貼上 TG/唔准出現在 log。") print(f" ➜ 下一步:開新對話 → 貼該檔全文(開頭 AGENT_ID: <…> 欄填上名)→ 貼「Continue」") PY # ④ 檔案權限 belt+braces chmod 0600 "$STATE/exports/PROMPT-$S.txt" echo " ④ exports/PROMPT-$S.txt 0600 ✓" done # 公開副本(CA postcondition:$HOME/public/PROMPT-A.txt 本次新寫 + 首問用) mkdir -p "$PUB" for S in "${SLOTS[@]}"; do cp -f "$STATE/exports/PROMPT-$S.txt" "$PUB/PROMPT-$S.txt" chmod 0644 "$PUB/PROMPT-$S.txt" # -- auto-sync checksum (patched 2026-10-03) -- _rsum=$(sha256sum "$PUB/PROMPT-$S.txt" | cut -d" " -f1) if grep -q "PROMPT-$S.txt$" "$PUB/sha256sums.txt" 2>/dev/null; then sed -i "s/^[a-f0-9]* PROMPT-$S.txt$/$_rsum PROMPT-$S.txt/" "$PUB/sha256sums.txt" else echo "$_rsum PROMPT-$S.txt" >> "$PUB/sha256sums.txt" fi chmod 644 "$PUB/sha256sums.txt" 2>/dev/null || true # -- auto-gen a[N].txt sample (patched 2026-10-03) -- _anum="${S#A}" _aname=$(echo "$S" | tr "A-Z" "a-z") cat > "$PUB/${_aname}.txt" </dev/null || true echo "════ 完成 ════" echo "提示:想保留記憶 → 新對話入面順手貼埋該席位嘅 brain pack URL(會自報 slot/session/cursor)。"