diff --git a/.github/workflows/elevation.yml b/.github/workflows/elevation.yml new file mode 100644 index 0000000..eeba438 --- /dev/null +++ b/.github/workflows/elevation.yml @@ -0,0 +1,184 @@ +name: elevation +# JIT 提权 v0(IR-0006 W2-C4 / BEH-05 / 卡 #415 / HO 场景 3)——/elevate 评论 +# 走策略表裁决(governance/policy/elevation.yaml,默认拒绝)+ 批准/拒绝/收回 +# 记录进统一账本(elevation-ledger 分支,kind=approval,subject.card 可查询, +# AC-9c);每小时 sweep 到期 grant 补 revoke + open-check 断言(AC-9d:提权是 +# 瞬时能力——无长期驻留提权的机器锚点,驻留=run 红)。 +# +# 铁律对齐: +# - 默认拒绝:reason/spec_ref 缺失(HO 场景 3)、能力未声明、角色不匹配、 +# TTL 越界 → deny(裁决逻辑唯一真源=governance/elevation.py,纯函数可测)。 +# - 判定锚点机械(INV-01/02):role 取 GitHub author_association(OWNER→owner) +# 与 actor 身份(cloudbrid-agent[bot]→agent),workflow 不自判。 +# - append-only(ADR-0062):账本经 evidence_shadow.py 链式追加,验链后推送; +# 幂等=delivery_id(评论 id)已入账即 no-op(重复投递不双写)。 +# - v0 边界:裁决+记账+TTL 收回闭环;不铸造真实平台凭证(提权档 +# org-variable-write 等的实际代签执行面归后续卡——本卡 AC 只锚 +# 裁决记录与收回断言)。 +on: + issue_comment: + types: [created] + schedule: + - cron: "17 * * * *" # 每小时 sweep 到期 grant(错峰:避开 :00 整点治理高峰) + workflow_dispatch: {} + +permissions: {} # 写操作走 job 级授权 + GOVERNANCE_TOKEN(台账)/ GITHUB_TOKEN(评论) + +# 台账 append-only 不容忍并发追加:elevation 全局串行(评论裁决与 sweep 抢同一 +# 台账文件),排队不取消(INV-09 同款语义) +concurrency: + group: elevation-ledger + cancel-in-progress: false + +jobs: + adjudicate: + if: ${{ github.event_name == 'issue_comment' && github.repository == 'Cloudbird-Software/.github' && startsWith(github.event.comment.body, '/elevate') }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read # checkout 治理真源(elevation.py + 策略表) + issues: write # 裁决结果回复评论(seed-drill P0 同款先例) + env: + LEDGER_REPO: Cloudbird-Software/.github + LEDGER_BRANCH: elevation-ledger + LEDGER_FILE: governance/elevation/shadow-evidence.jsonl + GOV_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} + GH_TOKEN: ${{ github.token }} + COMMENT_BODY: ${{ github.event.comment.body }} + COMMENT_ID: ${{ github.event.comment.id }} + ISSUE_N: ${{ github.event.issue.number }} + REQUESTER: ${{ github.event.comment.user.login }} + AUTHOR_ASSOC: ${{ github.event.comment.author_association }} + ACTOR: ${{ github.actor }} + RUN_ID: ${{ github.run_id }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: 解析 + 策略裁决 + 台账追加 + 回复 + run: | + set -euo pipefail + TMP=$(mktemp -d) + CARD="Cloudbird-Software/.github#${ISSUE_N}" + printf '%s\n' "$COMMENT_BODY" >"$TMP/comment.txt" + + # role 判定锚点机械(INV-01):OWNER→owner;cloudbrid-agent App→agent;其余→none + if [[ "$AUTHOR_ASSOC" == "OWNER" ]]; then ROLE=owner + elif [[ "$ACTOR" == "cloudbrid-agent[bot]" ]]; then ROLE=agent + else ROLE=none; fi + echo "role=$ROLE (assoc=$AUTHOR_ASSOC actor=$ACTOR)" + + # ---- 台账基线(clone elevation-ledger;不存在=创世) ---- + git config --global user.name elevation-bot + git config --global user.email elevation-bot@users.noreply.github.com + if ! git clone --depth 1 "https://x-access-token:${GOV_TOKEN}@github.com/${LEDGER_REPO}.git" "$TMP/ledger" -b "$LEDGER_BRANCH" 2>/dev/null; then + git clone --depth 1 "https://x-access-token:${GOV_TOKEN}@github.com/${LEDGER_REPO}.git" "$TMP/ledger" + git -C "$TMP/ledger" checkout -b "$LEDGER_BRANCH" + fi + LED="$TMP/ledger/$LEDGER_FILE" + mkdir -p "$(dirname "$LED")"; touch "$LED" + + # ---- 幂等:delivery_id 已入账(payload 内转义形态)→ no-op ---- + if grep -Fq "\\\"delivery_id\\\":\\\"${COMMENT_ID}" "$LED"; then + echo "OK 评论 ${COMMENT_ID} 已裁决入账(幂等 no-op)" + exit 0 + fi + + # ---- parse(exit 2=命令形状非法,fail-closed 记 deny) ---- + if ! python3 governance/elevation.py parse --comment-file "$TMP/comment.txt" \ + --card "$CARD" --requester "$REQUESTER" --delivery-id "$COMMENT_ID" \ + >"$TMP/request.json" 2>"$TMP/parse.err"; then + PREASON=$(tr -d '\n' <"$TMP/parse.err" | cut -c1-300) + jq -n --arg requester "$REQUESTER" --arg card "$CARD" --arg id "$COMMENT_ID" \ + --arg reason "parse-failed: $PREASON" \ + '{verdict:"deny", requester:$requester, card:$card, delivery_id:$id, reason:$reason}' \ + >"$TMP/verdict.json" + else + # ---- adjudicate(策略表唯一授权真源;role 机械锚点) ---- + python3 governance/elevation.py adjudicate --request-file "$TMP/request.json" \ + --role "$ROLE" --policy governance/policy/elevation.yaml >"$TMP/verdict.json" + fi + cat "$TMP/verdict.json" + + # ---- 台账追加(schema v1 链式 hash;kind=approval,AC-9c subject 可查询) ---- + V=$(cat "$TMP/verdict.json") + jq -n --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg card "$CARD" \ + --arg requester "$REQUESTER" --arg role "$ROLE" --argjson v "$V" \ + '{ts:$ts, kind:"approval", action:("elevation."+$v.verdict), verdict:$v.verdict, + subject:{card:$card, tenant:"cloudbird-internal"}, + actor:{identity:$requester, role:$role}, + payload:($v|tojson)}' >"$TMP/event.json" + python3 governance/evidence_shadow.py append --file "$LED" --event-file "$TMP/event.json" + python3 governance/evidence_shadow.py verify --file "$LED" + VERD=$(jq -r .verdict "$TMP/verdict.json") + git -C "$TMP/ledger" add "$LEDGER_FILE" + git -C "$TMP/ledger" commit -m "elevation: ${CARD} 裁决 ${VERD}(run ${RUN_ID};IR-0006 W2-C4)" + for i in 1 2 3; do git -C "$TMP/ledger" push "https://x-access-token:${GOV_TOKEN}@github.com/${LEDGER_REPO}.git" HEAD:refs/heads/"$LEDGER_BRANCH" && break + git -C "$TMP/ledger" pull --rebase "https://x-access-token:${GOV_TOKEN}@github.com/${LEDGER_REPO}.git" "$LEDGER_BRANCH" || true; sleep 5; done + # 推送校验:重试全败不得静默绿(远端头≠本地头=append-only 被无声违反 → 红) + RHEAD=$(git ls-remote "https://x-access-token:${GOV_TOKEN}@github.com/${LEDGER_REPO}.git" "refs/heads/${LEDGER_BRANCH}" | cut -f1) + LHEAD=$(git -C "$TMP/ledger" rev-parse HEAD) + [[ "$RHEAD" == "$LHEAD" ]] || { echo "::error::台账推送失败(远端 $RHEAD ≠ 本地 $LHEAD)" >&2; exit 1; } + + # ---- 回复裁决结果(grant=能力+TTL+到期时点;deny=拒绝理由) ---- + jq -r 'if .verdict == "grant" then + "**elevation: GRANT**(capability=`\(.capability)`,TTL=\(.effective_ttl_minutes)min,expires_at=\(.expires_at))\n\n- elevation_id: `\(.elevation_id)`\n- 记录已入账(elevation-ledger / kind=approval)\n- TTL 到期由每小时 sweep 自动收回(AC-9d)" + else + "**elevation: DENY**(fail-closed)\n\n- 理由: \(.reason)" + end' "$TMP/verdict.json" >"$TMP/reply.md" + gh issue comment "$ISSUE_N" --repo "$LEDGER_REPO" --body-file "$TMP/reply.md" + + sweep: + if: ${{ github.event_name != 'issue_comment' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + env: + LEDGER_REPO: Cloudbird-Software/.github + LEDGER_BRANCH: elevation-ledger + LEDGER_FILE: governance/elevation/shadow-evidence.jsonl + GOV_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} + RUN_ID: ${{ github.run_id }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: sweep 到期 grant 补 revoke + open-check 断言(AC-9d) + run: | + set -euo pipefail + git config --global user.name elevation-bot + git config --global user.email elevation-bot@users.noreply.github.com + if ! git clone --depth 1 "https://x-access-token:${GOV_TOKEN}@github.com/${LEDGER_REPO}.git" ledger -b "$LEDGER_BRANCH" 2>/dev/null; then + echo "OK elevation-ledger 分支不存在(尚无裁决记录)——open-check 空账本通过" + exit 0 + fi + LED="ledger/$LEDGER_FILE" + [[ -f "$LED" ]] || { echo "OK 账本文件不存在(创世前)——no-op"; exit 0; } + # 到期未收回 grant → 补 revoke 事件(AC-9d:提权是瞬时能力) + python3 governance/elevation.py sweep --ledger-dir "$(dirname "$LED")" >expired.json + N=$(jq 'length' expired.json) + echo "到期未收回 grant: $N 条" + i=0 + while [[ $i -lt $N ]]; do + G=$(jq -c ".[$i]" expired.json) + jq -n --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --argjson g "$G" \ + '{ts:$ts, kind:"approval", action:"elevation.revoke", verdict:"revoked", + subject:{card:$g.card, tenant:"cloudbird-internal"}, + actor:{identity:"elevation-bot", role:"bot"}, + payload:{elevation_id:$g.elevation_id, capability:$g.capability, cause:"ttl-expired"}}' >event.json + python3 governance/evidence_shadow.py append --file "$LED" --event-file event.json + i=$((i + 1)) + done + # 断言:sweep 后零过期未收回 grant(无长期驻留提权;驻留=exit 3 → run 红) + python3 governance/elevation.py open-check --ledger-dir "$(dirname "$LED")" + if [[ $N -gt 0 ]]; then + git -C ledger add "$LEDGER_FILE" + git -C ledger commit -m "elevation: sweep 收回 $N 条到期 grant(run ${RUN_ID};AC-9d)" + for i in 1 2 3; do git -C ledger push "https://x-access-token:${GOV_TOKEN}@github.com/${LEDGER_REPO}.git" HEAD:refs/heads/"$LEDGER_BRANCH" && break + git -C ledger pull --rebase "https://x-access-token:${GOV_TOKEN}@github.com/${LEDGER_REPO}.git" "$LEDGER_BRANCH" || true; sleep 5; done + # 推送校验(同 adjudicate job:重试全败不得静默绿) + RHEAD=$(git ls-remote "https://x-access-token:${GOV_TOKEN}@github.com/${LEDGER_REPO}.git" "refs/heads/${LEDGER_BRANCH}" | cut -f1) + LHEAD=$(git -C ledger rev-parse HEAD) + [[ "$RHEAD" == "$LHEAD" ]] || { echo "::error::sweep 台账推送失败(远端 $RHEAD ≠ 本地 $LHEAD)" >&2; exit 1; } + fi diff --git a/governance/evidence-query.sh b/governance/evidence-query.sh index d8e4d54..7e6f9e1 100644 --- a/governance/evidence-query.sh +++ b/governance/evidence-query.sh @@ -1,11 +1,13 @@ #!/usr/bin/env bash -# evidence-query.sh —— 三源统一证据查询(IR-0006 W1-B2 / BEH-03 / ADR-0103,AC-4a) +# evidence-query.sh —— 四源统一证据查询(IR-0006 W1-B2 / BEH-03 / ADR-0103,AC-4a) # -# 一条命令跨三源拉取 schema v1 影子账本、逐源验链(fail-closed:链断=红)、 +# 一条命令跨源拉取 schema v1 影子账本、逐源验链(fail-closed:链断=红)、 # 按时间归并输出统一 JSONL(stdout)+ 分源统计(stderr): # 源 1 metering Cloudbird-Software/CI-Workflows @ metering-ledger shadow-evidence-*.jsonl(根) # 源 2 drill Cloudbird-Software/.github @ drill-ledger governance/drill/shadow-evidence.jsonl # 源 3 butler Cloudbird-Software/.github @ butler-ledger governance/butler/shadow-evidence.jsonl +# 源 4 elevation Cloudbird-Software/.github @ elevation-ledger governance/elevation/shadow-evidence.jsonl +# (W2-C4 JIT 提权裁决/收回记录——subject 可查询即 AC-9c 锚点) # # 用法: # bash governance/evidence-query.sh [--card owner/repo#n] [--json] # --json=汇总行也走 stdout @@ -67,19 +69,21 @@ else fi DRILL_OK=0; fetch_file "Cloudbird-Software/.github" "drill-ledger" "governance/drill/shadow-evidence.jsonl" "$TMP/drill.jsonl" && DRILL_OK=1 || [[ $? -eq 1 ]] || exit 2 BUTLER_OK=0; fetch_file "Cloudbird-Software/.github" "butler-ledger" "governance/butler/shadow-evidence.jsonl" "$TMP/butler.jsonl" && BUTLER_OK=1 || [[ $? -eq 1 ]] || exit 2 +ELEV_OK=0; fetch_file "Cloudbird-Software/.github" "elevation-ledger" "governance/elevation/shadow-evidence.jsonl" "$TMP/elev.jsonl" && ELEV_OK=1 || [[ $? -eq 1 ]] || exit 2 # ---- 逐源验链 + 归并输出(链断=exit 3:不可信数据不出结果) ---- -export CARD_FILTER JSON_ONLY DRILL_OK BUTLER_OK -python3 - "$DIR/evidence_shadow.py" "$SRC_METER" "$TMP/drill.jsonl" "$TMP/butler.jsonl" "$TMP" <<'PYEOF' +export CARD_FILTER JSON_ONLY DRILL_OK BUTLER_OK ELEV_OK +python3 - "$DIR/evidence_shadow.py" "$SRC_METER" "$TMP/drill.jsonl" "$TMP/butler.jsonl" "$TMP/elev.jsonl" "$TMP" <<'PYEOF' import glob, json, os, sys sys.path.insert(0, os.path.dirname(os.path.abspath(sys.argv[1]))) import evidence_shadow # noqa: E402 验链与 CI-Workflows 侧同源语义 -metering_dir, drill_f, butler_f, tmp = sys.argv[2:6] +metering_dir, drill_f, butler_f, elev_f, tmp = sys.argv[2:7] sources = {"metering": sorted(glob.glob(os.path.join(metering_dir, "shadow-evidence-*.jsonl"))), "drill": [drill_f] if os.environ.get("DRILL_OK") == "1" else [], - "butler": [butler_f] if os.environ.get("BUTLER_OK") == "1" else []} + "butler": [butler_f] if os.environ.get("BUTLER_OK") == "1" else [], + "elevation": [elev_f] if os.environ.get("ELEV_OK") == "1" else []} errs, recs = [], [] for src, files in sources.items(): for f in files: @@ -102,7 +106,7 @@ for r in out: summary = { "total": len(out), - "by_source": {s: sum(1 for r in out if r["source"] == s) for s in ("metering", "drill", "butler")}, + "by_source": {s: sum(1 for r in out if r["source"] == s) for s in ("metering", "drill", "butler", "elevation")}, "by_tenant": {}, "by_card_top": {}, } diff --git a/governance/tests/test-evidence-shadow.sh b/governance/tests/test-evidence-shadow.sh index 7dc1f63..6db5be0 100644 --- a/governance/tests/test-evidence-shadow.sh +++ b/governance/tests/test-evidence-shadow.sh @@ -123,18 +123,45 @@ mkdir -p "$TMP/shadow-is-dir" BUTLER_SHADOW_FILE="$TMP/shadow-is-dir" bash "$DIR/governance/butler-audit.sh" test-butler cron ok '{"k":1}' >/dev/null 2>&1 [[ $? -ne 0 ]] && pass "影子写入失败 → butler-audit 非零退出(fail-closed)" || fail "影子写失败未 fail-closed" -# ---- evidence-query 三源统一查询(AC-4a:归并 + 验链 + fail-closed) ---- +# ---- elevation 裁决入账(W2-C4 / AC-9c:evidence-query 第 4 源,subject 可查询) ---- +ELEVDIR="$TMP/elevation"; mkdir -p "$ELEVDIR" +ESHADOW="$ELEVDIR/shadow-evidence.jsonl" +cat >"$TMP/ereq.json" <<'EOF' +{"kind": "elevate", "card": "Cloudbird-Software/.github#415", "requester": "agent-x", + "delivery_id": "cmt-1", "capability": "label-write", "reason": "状态机补偿写", + "spec_ref": "specs/IR-0006/spec.md#AC-9"} +EOF +python3 "$DIR/governance/elevation.py" adjudicate --request-file "$TMP/ereq.json" --role agent \ + --policy "$DIR/governance/policy/elevation.yaml" --now 2026-08-29T00:00:00Z >"$TMP/everd.json" \ + && pass "elevation adjudicate 出裁决(策略表路径)" || fail "elevation adjudicate" +python3 - "$TMP/everd.json" >"$TMP/eev.json" <<'PYEOF' +import json, sys +v = json.load(open(sys.argv[1], encoding="utf-8")) +ev = {"ts": "2026-08-29T00:00:02Z", "kind": "approval", "action": "elevation." + v["verdict"], + "verdict": v["verdict"], + "subject": {"card": v["card"], "tenant": "cloudbird-internal"}, + "actor": {"identity": v["requester"], "role": "agent"}, + "payload": json.dumps(v, ensure_ascii=False, sort_keys=True)} +print(json.dumps(ev, ensure_ascii=False, sort_keys=True)) +PYEOF +python3 "$DIR/governance/evidence_shadow.py" append --file "$ESHADOW" --event-file "$TMP/eev.json" >/dev/null \ + && pass "elevation 裁决入影子账本(链式 append)" || fail "elevation 影子 append" +python3 "$DIR/governance/evidence_shadow.py" verify --file "$ESHADOW" >/dev/null \ + && pass "elevation 影子验链绿" || fail "elevation 影子链断" + +# ---- evidence-query 四源统一查询(AC-4a:归并 + 验链 + fail-closed) ---- GHSTUB="$TMP/gh-stub" mkdir -p "$TMP/fixtures" -# fixture:metering=周分片列表($SHADOW);drill/butler=各自影子(独立成链) -python3 - "$SHADOW" "$DSHADOW" "$BUTLER_SHADOW" "$TMP/fixtures" <<'PYEOF' +# fixture:metering=周分片列表($SHADOW);drill/butler/elevation=各自影子(独立成链) +python3 - "$SHADOW" "$DSHADOW" "$BUTLER_SHADOW" "$ESHADOW" "$TMP/fixtures" <<'PYEOF' import base64, json, sys -shadow, dshadow, bshadow, fixdir = sys.argv[1:5] +shadow, dshadow, bshadow, eshadow, fixdir = sys.argv[1:6] b64 = lambda p: base64.b64encode(open(p, "rb").read()).decode() json.dump([{"type": "file", "name": "shadow-evidence-2026-W35.jsonl", "content": b64(shadow)}], open(f"{fixdir}/metering-list.json", "w")) json.dump({"type": "file", "content": b64(dshadow)}, open(f"{fixdir}/drill.json", "w")) json.dump({"type": "file", "content": b64(bshadow)}, open(f"{fixdir}/butler.json", "w")) +json.dump({"type": "file", "content": b64(eshadow)}, open(f"{fixdir}/elev.json", "w")) PYEOF cat >"$GHSTUB" <<'STUBEOF' #!/usr/bin/env bash @@ -156,6 +183,11 @@ case "$url" in echo 'gh: No commit found for the ref butler-ledger (HTTP 404)' >&2; exit 1 fi cat "$F/butler.json" ;; + "repos/Cloudbird-Software/.github/contents/governance/elevation/shadow-evidence.jsonl?ref=elevation-ledger") + if [[ "${GH_STUB_ELEV_MISSING:-}" == "1" ]]; then + echo 'gh: No commit found for the ref elevation-ledger (HTTP 404)' >&2; exit 1 + fi + cat "$F/elev.json" ;; *) echo "gh-stub: 意外 URL $url" >&2; exit 1 ;; esac STUBEOF @@ -164,33 +196,45 @@ nlines() { grep -c . <<<"$1" || true; } # 非空行计数(wc 对空 herestrin QOUT=$(GH="$GHSTUB" GH_STUB_FIXTURES="$TMP/fixtures" GH_TOKEN=stub \ bash "$DIR/governance/evidence-query.sh" 2>"$TMP/q.err"); QRC=$? N=$(nlines "$QOUT") -[[ $QRC -eq 0 && "$N" -eq 5 ]] && pass "统一查询归并三源(5 条,rc=0)" || fail "统一查询(rc=$QRC 行=$N)" +[[ $QRC -eq 0 && "$N" -eq 6 ]] && pass "统一查询归并四源(6 条,rc=0)" || fail "统一查询(rc=$QRC 行=$N)" grep -q '"source":"metering"' <<<"$QOUT" && grep -q '"source":"drill"' <<<"$QOUT" && grep -q '"source":"butler"' <<<"$QOUT" \ - && pass "三源标记齐全(source 字段)" || fail "source 标记缺失" + && grep -q '"source":"elevation"' <<<"$QOUT" \ + && pass "四源标记齐全(source 字段)" || fail "source 标记缺失" grep -q '^SUMMARY ' "$TMP/q.err" && grep -Eq '"metering": ?2' "$TMP/q.err" \ && pass "分源统计(stderr SUMMARY)" || fail "SUMMARY 统计缺失" -# --card 过滤:metering 影子绑 #407,drill/butler 哨兵 #0 → 仅 2 条 +# --card 过滤:metering 影子绑 #407,drill/butler 哨兵 #0,elevation 绑 #415 COUT=$(GH="$GHSTUB" GH_STUB_FIXTURES="$TMP/fixtures" GH_TOKEN=stub \ bash "$DIR/governance/evidence-query.sh" --card Cloudbird-Software/.github#407 2>/dev/null); CRC=$? CN=$(nlines "$COUT") [[ $CRC -eq 0 && "$CN" -eq 2 ]] && pass "--card 过滤(#407 → 2 条)" || fail "--card 过滤(rc=$CRC 行=$CN)" +# AC-9c:elevation 记录按 subject 可查询(#415 → 1 条,source=elevation) +EOUT=$(GH="$GHSTUB" GH_STUB_FIXTURES="$TMP/fixtures" GH_TOKEN=stub \ + bash "$DIR/governance/evidence-query.sh" --card Cloudbird-Software/.github#415 2>/dev/null); ERC=$? +EN=$(nlines "$EOUT") +[[ $ERC -eq 0 && "$EN" -eq 1 ]] && grep -q '"source":"elevation"' <<<"$EOUT" \ + && pass "AC-9c elevation 记录 subject 可查询(#415 → 1 条)" || fail "elevation 查询(rc=$ERC 行=$EN)" # 源缺席(404):过渡期合法,非红(须在篡改 fixture 生成前跑——桩对篡改片优先回放) MOUT=$(GH="$GHSTUB" GH_STUB_FIXTURES="$TMP/fixtures" GH_TOKEN=stub GH_STUB_BUTLER_MISSING=1 \ bash "$DIR/governance/evidence-query.sh" 2>"$TMP/m.err"); MRC=$? MN=$(nlines "$MOUT") -[[ $MRC -eq 0 && "$MN" -eq 4 ]] && pass "butler 源缺席(404)→ 跳过非红(4 条)" || fail "源缺席误红(rc=$MRC 行=$MN)" +[[ $MRC -eq 0 && "$MN" -eq 5 ]] && pass "butler 源缺席(404)→ 跳过非红(5 条)" || fail "源缺席误红(rc=$MRC 行=$MN)" # 源缺席(404 第二报文形态——账本分支未建 "No ref found"):同跳过非红 # (2026-08-29 波次通道实测回归:该形态曾被误判"非 404"→ INFRA exit 2) ROUT=$(GH="$GHSTUB" GH_STUB_FIXTURES="$TMP/fixtures" GH_TOKEN=stub GH_STUB_BUTLER_MISSING=2 \ bash "$DIR/governance/evidence-query.sh" 2>"$TMP/r.err"); RRC=$? RN=$(nlines "$ROUT") -[[ $RRC -eq 0 && "$RN" -eq 4 ]] && pass "源缺席(No ref found 分支未建)→ 跳过非红(4 条)" || fail "分支未建误红(rc=$RRC 行=$RN)" +[[ $RRC -eq 0 && "$RN" -eq 5 ]] && pass "源缺席(No ref found 分支未建)→ 跳过非红(5 条)" || fail "分支未建误红(rc=$RRC 行=$RN)" # 源缺席(404 第三报文形态——分支缺失 "No commit found for the ref",本地实测 # 真实报文):按 HTTP 404 状态码判定后同跳过非红 COUT2=$(GH="$GHSTUB" GH_STUB_FIXTURES="$TMP/fixtures" GH_TOKEN=stub GH_STUB_BUTLER_MISSING=3 \ bash "$DIR/governance/evidence-query.sh" 2>"$TMP/c2.err"); C2RC=$? C2N=$(nlines "$COUT2") -[[ $C2RC -eq 0 && "$C2N" -eq 4 ]] && pass "源缺席(No commit found for the ref)→ 跳过非红(4 条)" || fail "分支缺失误红(rc=$C2RC 行=$C2N)" +[[ $C2RC -eq 0 && "$C2N" -eq 5 ]] && pass "源缺席(No commit found for the ref)→ 跳过非红(5 条)" || fail "分支缺失误红(rc=$C2RC 行=$C2N)" +# 源缺席(elevation 第 4 源分支未建——"No commit found"):同跳过非红 +EOUT2=$(GH="$GHSTUB" GH_STUB_FIXTURES="$TMP/fixtures" GH_TOKEN=stub GH_STUB_ELEV_MISSING=1 \ + bash "$DIR/governance/evidence-query.sh" 2>"$TMP/e2.err"); E2RC=$? +E2N=$(nlines "$EOUT2") +[[ $E2RC -eq 0 && "$E2N" -eq 5 ]] && pass "elevation 源缺席(分支未建)→ 跳过非红(5 条)" || fail "elevation 缺席误红(rc=$E2RC 行=$E2N)" # 负向:任一源链断 → exit 3 且 stdout 零输出(不可信数据不出结果) python3 - "$TMP/fixtures" <<'PYEOF' import base64, json, sys