- cloud/evidence_binding.py: bind/validate/nonce single-use store; release endpoint consumes nonce on first validated submission, replay -> 409 evidence_replayed, stale/future -> 409 evidence_stale, partial -> 400, absent -> old path + label. Binding blocks exact replay and stale evidence, NOT restamping (unsigned evidence cannot prove content freshness). - cloud/quota.py docstring: actual-only is the v2 integration position (already converged in v1 base via fef09ae, NOT in main); original PR semantics (#86 all-rows vs #90 actual-only) still pending coordinator confirmation at merge time. - cloud/ledger_onboarding.py: DDL checklist referencing PG_DDL/PG_MARKER_DDL constants, read-only verify_ledger_ready and ledger_setup_report; default stays OFF, PG DDL stays an operator action (never claimed as executed). - cloud/ledger_outbox.py + worker hook: ledger write failures go to a JSONL outbox in addition to the log and can be replayed; task outcome still unaffected (rule 7 kept), audit fail-closed untouched -- asymmetry documented, not silently changed. - cloud/gate_verification.py: read-only verifier over an injected reader (match/mismatch/skipped); live wiring in merge/release is NOT possible on the current contract (no repo/ref in body or task row, no guaranteed trusted token), proven in the module header; no fake verification claimed. - cloud/tests/test_v2_evidence.py: 18 new tests, all passing. (cherry picked from commit bd85e33cd5e846fa6ee5d7e2440182dbd9c758b0)
141 lines
6.8 KiB
Python
141 lines
6.8 KiB
Python
"""合并/发布门禁证据的只读核对器(集成 v2,代码层)。
|
||
|
||
首轮缺口(门禁与审计 9.0 扣 0.5):硬门禁与发布门禁强制
|
||
的是“调用方提交的证据齐全且全绿”,不是“服务端已向
|
||
GitHub 求证”——调用方谎报 CI 全绿 / 保护状态 / 协调员
|
||
批准时,门禁会放行;docstring 称覆盖控制是“审计与
|
||
PR/Checks 的只读核对”,但在合并/发布路径上没有这样
|
||
一段“调 GitHub Checks API 核对证据”的代码(#210 的
|
||
实时保护检查只属于 auto-config execute 端点,不属于
|
||
merge/release 端点)。
|
||
|
||
本轨的结论分两半,缺一不可:
|
||
|
||
A. 核对器本身已实现(本模块,纯函数 + 注入式 reader,
|
||
有测试):verify_gate_evidence(evidence, reader) 把
|
||
调用方证据与 reader 给出的实时数据逐项比对——CI
|
||
状态、每项 required 检查的状态、分支保护状态;不
|
||
一致即 mismatch(逐条列出),一致即 verified。reader
|
||
是调用方注入的 callable:reader(repo, ref) -> {
|
||
"ci_checks": {name: status}, "branch_protection":
|
||
"protected"|"unprotected"|... },真实 reader 用
|
||
github_app 的 _github_api_request 走 GET Checks /
|
||
GET 分支保护(只读,绝不写 PR、不改保护、不合并)。
|
||
reader 为 None 或抛异常时结果是“未核对”
|
||
(checked=False, verified=False),绝不是“通过”——
|
||
调用方把 skipped 当 pass 用,是调用方的 bug,本
|
||
模块的返回值结构让这种误用一眼可见(verified 恒
|
||
False,除非逐项比对全过)。
|
||
|
||
B. 实时求证在当前合并/发布路径上**接不上**(不可实现
|
||
证明,不是未做):① 请求体里没有 repo 与 ref——
|
||
merge/release 的证据字段只有 files_changed、ci_checks、
|
||
branch_protection 等,没有仓库名、没有提交 SHA,
|
||
reader 不知道向哪个仓库的哪次提交求证,猜仓库
|
||
就是编造求证;② 服务端没有可信的求证通道保证——
|
||
github_app.resolve_app_token 对未安装 App 的仓库
|
||
返回 None(调用方回落 PAT),PAT 是调用方自己的
|
||
凭证,用它“核对”调用方自己的证据不是独立求证;
|
||
③ 任务行里没有 repo/ref 列,服务端无法从任务 id
|
||
反推求证目标。接上实时求证须先改契约(请求体或任
|
||
务行加 repo + ref,安装保证或服务端侧可信 token),
|
||
那是契约决定,须协调员/用户在合轨时定;本轨不编
|
||
一个“看起来在求证”的假 reader、不把调用方证据
|
||
自己与自己比对后写成 verified。
|
||
|
||
当前真实控制(写清,不拔高):门禁强制证据齐全全绿
|
||
(硬模式 fail-closed);审计记 outcome 与 blocks
|
||
(merge_gate_blocked / merge_task / release_task),
|
||
但审计 detail 不保存证据体全文——事后追责能查到
|
||
“谁在何时以何 blocks 通过/被挡”,查不到“当时提交
|
||
的 ci_checks 原文”。谎报证据的发现仍靠事后审计与
|
||
GitHub 侧记录比对,不是当场发现。本模块的核对器
|
||
是为契约补齐后的接线准备的:届时服务端只需注入
|
||
真实 reader 并把 verified=False(无论 skipped 还是
|
||
mismatch)当作阻断,核对器本身不用改。
|
||
|
||
零第三方依赖,只用标准库。本模块不 import cloud
|
||
其它模块(reader 由调用方注入),不发网络请求。
|
||
"""
|
||
|
||
|
||
def verify_gate_evidence(evidence, reader=None, repo=None, ref=None):
|
||
"""只读核对调用方门禁证据与实时数据。
|
||
|
||
evidence:调用方提交的门禁证据 dict(与硬门禁同款
|
||
字段:ci_status、ci_checks[{name, status}]、
|
||
required_checks、branch_protection)。
|
||
reader:callable (repo, ref) -> {"ci_checks": {名: 状态},
|
||
"branch_protection": str},或 None(无可信求证通道)。
|
||
repo/ref:求证目标;reader 非 None 但 repo/ref 缺失
|
||
时同样视为未核对(不猜目标)。
|
||
|
||
返回 {"checked": bool, "verified": bool, "mismatches": [...],
|
||
"reason": str|None}:
|
||
- checked=False:未核对(无 reader、无目标、reader 失败),
|
||
verified 恒 False,reason 说明原因;调用方绝不能把
|
||
checked=False 当通过。
|
||
- checked=True 且 mismatches 为空:逐项比对全过,
|
||
verified=True。
|
||
- checked=True 且 mismatches 非空:证据与实时数据不
|
||
一致,verified=False,mismatches 逐条说明(ci 状态、
|
||
某项检查、分支保护)。
|
||
比对规则:只比对调用方声称的项——调用方没声称的
|
||
检查不判(缺失由硬门禁的证据缺失码处理,不归本
|
||
模块);实时数据缺失某项时记 mismatch(声称全绿却
|
||
查无此项,不是“查不到所以通过”)。
|
||
"""
|
||
if reader is None:
|
||
return {"checked": False, "verified": False,
|
||
"mismatches": [],
|
||
"reason": "no_trusted_reader"}
|
||
if not repo or not ref:
|
||
return {"checked": False, "verified": False,
|
||
"mismatches": [],
|
||
"reason": "no_verification_target"}
|
||
try:
|
||
live = reader(repo, ref)
|
||
except Exception as exc:
|
||
return {"checked": False, "verified": False,
|
||
"mismatches": [],
|
||
"reason": "reader_failed: %s" % exc}
|
||
if not isinstance(live, dict):
|
||
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)
|
||
return {"checked": True, "verified": not mismatches,
|
||
"mismatches": mismatches, "reason": None}
|