v7 mech-6: production-evidence fail-closed live wiring seam (no real reader, no endpoint wiring yet)

- LiveEvidenceReader protocol: get_checks(repo, ref) /
  get_branch_protection(repo, branch), results carry status
  plus raw evidence; production code has no HTTP call, no
  token read, no real GitHub API call (AST-asserted).
- verify_live_evidence: no reader / no repo-ref / reader
  failure / mismatch all verified=False; comparison shared
  with verify_gate_evidence via _compare_claimed_vs_live.
- live_gate_decision: verified=False -> allowed=False,
  http_status=409, status=blocked; verified=True -> 200.
- Endpoints _merge_task/_release_task NOT wired yet (no
  production reader exists; wiring None would block all
  merges/releases); real live verification still pending
  B6 real App install and B1 user-side items.

(cherry picked from commit 7e8bfdee1f22c28329602314f3e166cde34eb6cf)
This commit is contained in:
host committed 2026-11-03 02:49:27 +00:00
1 parent ae012a8e7c
commit 3978dbbbf2
2 files changed
+639 -33

No files matched your search

+328 -33
View File
@@ -56,8 +56,95 @@ mismatch)当作阻断,核对器本身不用改。
零第三方依赖,只用标准库。本模块不 import cloud
其它模块(reader 由调用方注入),不发网络请求。
v7 机制第 6 项(生产证据 fail-closed live 接线,本批新增,
只动本模块,不动其它 7 项):
- 新增可注入 reader 接口 LiveEvidenceReader(协议):
get_checks(repo, ref) 与 get_branch_protection(repo,
branch),返回结构含状态与原始证据(LiveChecksResult /
LiveProtectionResult,或等价 dict)。本模块的生产代码
里绝不实现真实 HTTP 调用、绝不读真实 token、绝不调
用真实 GitHub API——接口是未来真实 reader 的插入缝,
不是真实 reader 本身。
- 新增 verify_live_evidence(claimed, reader, repo, ref,
branch=None):协议 reader 或旧式 callable reader 均可;
无 reader、无 repo/ref、reader 失败、reader 返回与声
称不一致时 verified 一律 False;声称 CI 全绿但实时
数据失败/缺失时记 mismatch。比对规则与
verify_gate_evidence 逐项一致(共用 _compare),不另
造口径;空声称不由本函数放行——缺失证据由硬门禁
(merge_gate_hard)阻断,本函数只核对“声称了的项”。
- 新增 live_gate_decision:端点层判定函数,verified
为 False 时返回 allowed=False、http_status=409、
status="blocked"、blocked=True(mismatch 与未核对用
不同 code 区分:live_evidence_mismatch /
live_evidence_unverified);verified 为 True 时返回
allowed=True、http_status=200、status="allowed"。
- 诚实残留(不得冒充已接线):HTTP 端点 _merge_task /
_release_task 本批未接 live_gate_decision——当前无
生产 reader 可注入:以 reader=None 接线会挡住全部合
并/发布(fail-closed 的本意不是停摆),以桩 reader
接线则是假求证。真实 live 验证仍待 B6 真实 App 安装
与 B1 等用户侧事项,未在本批完成。本批不改 #238
auto_apply(仍 False)、不改 #247“纠正为 pass 本身
不解封”、不改 v1.1 契约、不改任何阈值(阈值仍
test_default_unapproved)。
"""
from dataclasses import dataclass
try:
from typing import Protocol
from typing import runtime_checkable
except ImportError: # pragma: no cover - Python < 3.8
Protocol = object # type: ignore
def runtime_checkable(cls):
return cls
def _compare_claimed_vs_live(evidence, live):
"""声称证据与实时数据的逐项比对(两核对函数共用,同一口径)。
只比对调用方声称的项;实时数据缺失某项时记 mismatch
(声称全绿却查无此项,不是“查不到所以通过”)。
返回 mismatches 列表(空即逐项全过)。
"""
mismatches = []
live_checks = live.get("ci_checks") or {}
if not isinstance(live_checks, dict):
live_checks = {}
evidence = evidence or {}
if evidence.get("ci_status") == "success":
failed_live = sorted(
name for name, status in live_checks.items()
if status != "success")
if failed_live:
mismatches.append(
"ci_not_green_live: %s" % ", ".join(failed_live))
for check in (evidence.get("ci_checks") or []):
if not isinstance(check, dict):
continue
name = check.get("name")
if not name:
continue
if check.get("status") == "success" \
and live_checks.get(name) != "success":
mismatches.append(
"check_not_green_live: %s" % name)
for name in (evidence.get("required_checks") or []):
if live_checks.get(name) != "success":
mismatches.append(
"required_check_not_green_live: %s" % name)
claimed_protection = evidence.get("branch_protection")
live_protection = live.get("branch_protection")
if claimed_protection == "protected" \
and live_protection != "protected":
mismatches.append(
"branch_protection_not_protected_live: %s"
% live_protection)
return mismatches
def verify_gate_evidence(evidence, reader=None, repo=None, ref=None):
"""只读核对调用方门禁证据与实时数据。
@@ -103,38 +190,246 @@ def verify_gate_evidence(evidence, reader=None, repo=None, ref=None):
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "reader_failed: non-dict result"}
mismatches = []
live_checks = live.get("ci_checks") or {}
if not isinstance(live_checks, dict):
live_checks = {}
evidence = evidence or {}
if evidence.get("ci_status") == "success":
failed_live = sorted(
name for name, status in live_checks.items()
if status != "success")
if failed_live:
mismatches.append(
"ci_not_green_live: %s" % ", ".join(failed_live))
for check in (evidence.get("ci_checks") or []):
if not isinstance(check, dict):
continue
name = check.get("name")
if not name:
continue
if check.get("status") == "success" \
and live_checks.get(name) != "success":
mismatches.append(
"check_not_green_live: %s" % name)
for name in (evidence.get("required_checks") or []):
if live_checks.get(name) != "success":
mismatches.append(
"required_check_not_green_live: %s" % name)
claimed_protection = evidence.get("branch_protection")
live_protection = live.get("branch_protection")
if claimed_protection == "protected" \
and live_protection != "protected":
mismatches.append(
"branch_protection_not_protected_live: %s"
% live_protection)
mismatches = _compare_claimed_vs_live(evidence, live)
return {"checked": True, "verified": not mismatches,
"mismatches": mismatches, "reason": None}
@dataclass(frozen=True)
class LiveChecksResult:
"""get_checks 的返回结构:状态 + 原始证据。
checks:{检查名: 状态},状态与声称口径一致
("success" 为绿,其它一律非绿)。
raw:原始证据体(真实 reader 未来填 GitHub Checks
API 的原始 payload;假 reader 可填任意可追溯值,
也可为 None)。比对只用 checks,raw 只供审计追溯,
绝不参与“通过”判定。
"""
checks: dict
raw: object = None
@dataclass(frozen=True)
class LiveProtectionResult:
"""get_branch_protection 的返回结构:状态 + 原始证据。
status:"protected" / "unprotected" / 其它;None 表
示实时数据无此项(声称 protected 时即 mismatch)。
raw:原始证据体(真实 reader 未来填分支保护 API
的原始 payload;假 reader 可填任意可追溯值,也可
为 None),同样只供追溯、不参与判定。
"""
status: object = None
raw: object = None
@runtime_checkable
class LiveEvidenceReader(Protocol):
"""可注入的实时证据 reader 接口(v7 机制第 6 项)。
生产代码只定义接口、不实现真实 reader:真实实现
(走 GitHub Checks / 分支保护只读接口)须在 B6 真
实 App 安装与 B1 等用户侧事项就绪后另行接入,本
模块绝不内置 HTTP 调用、token 读取或 GitHub API
调用。测试与验收一律用实现本协议的假 reader。
"""
def get_checks(self, repo, ref):
"""返回 LiveChecksResult(或等价 dict,见
_normalize_checks):repo 在 ref 处的 CI 检查
实时状态与原始证据。"""
raise NotImplementedError
def get_branch_protection(self, repo, branch):
"""返回 LiveProtectionResult(或等价 dict / 纯
状态字符串,见 _normalize_protection):repo 的
branch 分支保护实时状态与原始证据。"""
raise NotImplementedError
def _normalize_checks(result):
"""归一化 get_checks 返回值 -> (checks dict, raw) 或 None。
接受 LiveChecksResult、含 "checks"/"ci_checks" dict
的 dict、以及本身就是 {名: 状态} 的 dict;其它形状
(非 dict checks、非字符串状态)返回 None,调用方
按 reader_failed 处理,不猜、不放行。
"""
if isinstance(result, LiveChecksResult):
checks, raw = result.checks, result.raw
elif isinstance(result, dict):
raw = result.get("raw")
if isinstance(result.get("checks"), dict):
checks = result["checks"]
elif isinstance(result.get("ci_checks"), dict):
checks = result["ci_checks"]
else:
checks = result
else:
return None
if not isinstance(checks, dict):
return None
if any(not isinstance(status, str) for status in checks.values()):
return None
return checks, raw
def _normalize_protection(result):
"""归一化 get_branch_protection 返回值。
返回 (status, raw, malformed):status 为字符串或
None(无此项);malformed 为 True 时调用方按
reader_failed 处理(例如返回了整数等非法形状)。
纯字符串即状态本身;无 "status"/"branch_protection"
键的 dict 视为无此项(None),不是 malformed——
声称 protected 时它仍会记 mismatch。
"""
if isinstance(result, LiveProtectionResult):
status, raw = result.status, result.raw
elif isinstance(result, str):
status, raw = result, None
elif result is None:
status, raw = None, None
elif isinstance(result, dict):
raw = result.get("raw")
if isinstance(result.get("status"), str):
status = result["status"]
elif isinstance(result.get("branch_protection"), str):
status = result["branch_protection"]
else:
status = None
else:
return None, None, True
if status is not None and not isinstance(status, str):
return None, None, True
return status, raw, False
def _is_live_reader(reader):
"""是否协议 reader:同时有 get_checks 与
get_branch_protection 两个方法(鸭子类型,不强制
isinstance,因为假 reader 未必显式继承协议)。"""
return callable(getattr(reader, "get_checks", None)) \
and callable(getattr(reader, "get_branch_protection", None))
def verify_live_evidence(claimed, reader=None, repo=None, ref=None,
branch=None):
"""实时证据比对(v7 机制第 6 项,fail-closed)。
claimed:调用方声称的门禁证据 dict(与硬门禁同款
字段:ci_status、ci_checks[{name, status}]、
required_checks、branch_protection)。
reader:LiveEvidenceReader 协议实例,或旧式 callable
(repo, ref) -> {"ci_checks": {...}, "branch_protection":
str}(与 verify_gate_evidence 同款;兼容旧假 reader)。
repo/ref:求证目标;branch:分支保护的求证分支,
未给时用 ref 作分支名(调用方须保证 ref 在此语义
下就是分支;SHA 求证时请显式传 branch,绝不由本
函数猜分支与 SHA 的对应关系)。
返回 {"checked", "verified", "mismatches", "reason",
"live"}:live 为归一化后的实时数据(含原始证据
raw)或 None(未核对时)。verified 为 False 的情形:
无 reader、无 repo/ref、reader 抛异常、reader 返回
恶化形状、实时数据与声称不一致——一律不是“通过”。
比对规则与 verify_gate_evidence 共用 _compare,口
径一致;本函数不发网络请求、不读 token。
"""
if reader is None:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "no_trusted_reader", "live": None}
if not repo or not ref:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "no_verification_target", "live": None}
if _is_live_reader(reader):
try:
checks_result = reader.get_checks(repo, ref)
except Exception as exc:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "reader_failed: %s" % exc,
"live": None}
normalized = _normalize_checks(checks_result)
if normalized is None:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "reader_failed: malformed checks",
"live": None}
checks, checks_raw = normalized
try:
protection_result = reader.get_branch_protection(
repo, branch if branch else ref)
except Exception as exc:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "reader_failed: %s" % exc,
"live": None}
protection, protection_raw, malformed = \
_normalize_protection(protection_result)
if malformed:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "reader_failed: malformed protection",
"live": None}
live = {"ci_checks": checks,
"branch_protection": protection,
"checks_raw": checks_raw,
"protection_raw": protection_raw}
elif callable(reader):
try:
live_raw = reader(repo, ref)
except Exception as exc:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "reader_failed: %s" % exc,
"live": None}
if not isinstance(live_raw, dict):
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "reader_failed: non-dict result",
"live": None}
live = live_raw
else:
return {"checked": False, "verified": False,
"mismatches": [],
"reason": "reader_failed: unsupported reader type",
"live": None}
mismatches = _compare_claimed_vs_live(claimed, live)
return {"checked": True, "verified": not mismatches,
"mismatches": mismatches, "reason": None,
"live": live}
def live_gate_decision(claimed, reader=None, repo=None, ref=None,
branch=None):
"""端点层 live 门禁判定(v7 机制第 6 项,fail-closed)。
verified=False 一律阻断,返回 allowed=False、
http_status=409、status="blocked"、blocked=True:
code 为 live_evidence_mismatch(已核对但不一致)
或 live_evidence_unverified(未核对:无 reader、
无目标、reader 失败)。verified=True 时返回
allowed=True、http_status=200、status="allowed"、
code="live_evidence_verified"。返回里附完整 live
比对结果(live 键),供端点写进 409 响应与审计。
注意:本函数是判定缝,不是端点接线——HTTP 端点
须在有生产 reader 可注入时才调用它;无 reader 时
调用它会阻断(这是 fail-closed 的正确语义),所
以在生产 reader 就绪前端点不得接线(见模块头残
留)。本函数本身是纯函数,不写审计、不改状态。
"""
result = verify_live_evidence(claimed, reader, repo, ref,
branch=branch)
if result["verified"]:
return {"allowed": True, "blocked": False,
"status": "allowed", "http_status": 200,
"code": "live_evidence_verified",
"live": result}
code = "live_evidence_mismatch" if result["checked"] \
else "live_evidence_unverified"
return {"allowed": False, "blocked": True,
"status": "blocked", "http_status": 409,
"code": code, "live": result}