#!/usr/bin/env bash # 外部事实登记表逐条重验:对 policy/facts.md 每条事实执行其验证命令并报告 PASS/FAIL。 # 规则依据:AGENTS.md「证据与验证门禁」(外部事实零信任);verification.md「声明与证据来源」。 # 用法:make facts-verify [ARGS=--update] (--update:把 PASS 项 last_verified 刷为当天并写回) # 表内验证命令约定不含竖线;含 "\|" 转义时按字面还原后执行。 # 登记表引用的探针子命令(单独运行时在 stderr 说明判定,不输出凭据): # api-token URL URL 须逐字等于登记端点 https://git-home.u1680832.nyat.app:30319/api/v1/user, # 校验通过后才进程内读 ~/.secrets/gitea HOME 段 ACCESS_TOKEN 并 GET(不跟随重定向); # 0=接受 3=缺失 4=格式不符 5=请求失败/被拒/重定向 6=非登记端点 # tls-handshake-fails HOST PORT 仅当 TCP 已连通且 TLS 层握手报错时 0; # 3=DNS 失败 4=超时 5=连接失败 6=握手成功(端点仍可用) set -uo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" case "${1:-}" in api-token) [ $# -eq 2 ] || { echo "usage: facts-verify.sh api-token URL" >&2; exit 2; } exec python3 - "$SCRIPT_DIR" "$2" <<'PY' import sys, urllib.request sys.path.insert(0, sys.argv[1]) import task_triage ENDPOINT = "https://git-home.u1680832.nyat.app:30319/api/v1/user" if sys.argv[2] != ENDPOINT: print("api-token: URL 不是 policy/facts.md F2 登记端点,拒绝发送凭据", file=sys.stderr) sys.exit(6) class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, *args, **kwargs): return None urllib.request.install_opener(urllib.request.build_opener(NoRedirect)) url = ENDPOINT token = task_triage.secrets_file_token() if not token: print("api-token: ~/.secrets/gitea HOME 段 ACCESS_TOKEN 缺失", file=sys.stderr) sys.exit(3) if not task_triage.TOKEN_RE.fullmatch(token): print("api-token: ~/.secrets/gitea HOME 段 ACCESS_TOKEN 格式不符(应为单个无空白值)", file=sys.stderr) sys.exit(4) try: task_triage.http_get_json(url, token) except task_triage.Stop as stop: print("api-token: " + stop.message, file=sys.stderr) sys.exit(5) print("api-token: accepted by " + url, file=sys.stderr) PY ;; tls-handshake-fails) [ $# -eq 3 ] || { echo "usage: facts-verify.sh tls-handshake-fails HOST PORT" >&2; exit 2; } exec python3 - "$2" "$3" <<'PY' import socket, ssl, sys host, port = sys.argv[1], int(sys.argv[2]) def verdict(code, text): print(f"tls-handshake-fails {host}:{port}: {text}", file=sys.stderr) sys.exit(code) try: sock = socket.create_connection((host, port), timeout=8) except socket.gaierror: verdict(3, "DNS resolution failed (not a TLS observation)") except TimeoutError: verdict(4, "TCP connect timed out (not a TLS observation)") except OSError as exc: verdict(5, f"TCP connect failed ({type(exc).__name__}; not a TLS observation)") try: with sock, ssl.create_default_context().wrap_socket(sock, server_hostname=host) as tls: version = tls.version() except ssl.SSLError as exc: verdict(0, f"TLS handshake failed ({type(exc).__name__}: {getattr(exc, 'reason', None) or 'unknown'})") except TimeoutError: verdict(4, "TLS handshake timed out (not a TLS-layer error)") except OSError as exc: verdict(5, f"connection failed during handshake ({type(exc).__name__}; not a TLS-layer error)") verdict(6, f"TLS handshake succeeded ({version}); endpoint still answers") PY ;; esac F="${FACTS:-policy/facts.md}" [ -f "$F" ] || { echo "ERROR: 事实登记表 $F 不存在" >&2; exit 1; } python3 - "$F" "${1:-}" <<'PY' import re, subprocess, sys, datetime path, update = sys.argv[1], sys.argv[2] == "--update" lines = open(path, encoding="utf-8").read().splitlines(keepends=True) today = datetime.date.today().isoformat() total = fails = 0 for i, line in enumerate(lines): if not re.match(r"^\|\s*F\d+\s*\|", line): continue raw = line.strip().strip("|").replace("\\|", "\x00") cells = [c.strip().replace("\x00", "|") for c in raw.split("|")] if len(cells) < 4: print(f"[row {i+1}] SKIP 格式不符(列数<4)", file=sys.stderr) continue fid, cmd = cells[0], cells[2].strip("`") total += 1 try: r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=90) ok = r.returncode == 0 except subprocess.TimeoutExpired: ok = False print(f"[{fid}] TIMEOUT >90s", file=sys.stderr) print(f"[{fid}] {'PASS' if ok else 'FAIL'} :: {cmd[:80]}") if not ok: fails += 1 elif update and len(cells) >= 4 and re.match(r"\d{4}-\d{2}-\d{2}", cells[3]): cells[3] = today lines[i] = "| " + " | ".join(c.replace("|", "\\|") for c in cells) + " |\n" if update: open(path, "w", encoding="utf-8").writelines(lines) print(f"[facts-verify] total={total} pass={total - fails} fail={fails}" + (";last_verified 已刷新为 " + today if update and total else "")) sys.exit(1 if fails else 0) PY