Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 45 additions & 27 deletions .github/workflows/adversary-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ name: adversary-gate
on:
pull_request:
types: [opened, synchronize, reopened]
merge_group:
# merge queue 兼容(2026-08-24):required workflow 不在队列分支上自动运行,
# 队列 merge commit 会永远等不到 adversary check(60min 超时弹回)——本 gate
# 在 checks_requested 时对比 base..head,无 specs/** 变更即写 success(与
# PR 路径同语义);有 specs 变更的合并需人工补 survived 审计(fail-closed)。
types: [checks_requested]

permissions:
contents: read
Expand All @@ -37,7 +43,41 @@ jobs:
pull-requests: read
checks: write
steps:
- name: merge_group 路径——对比 base..head 判定 specs 变更并直写 check
if: github.event_name == 'merge_group'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.merge_group.head_sha }}
BASE_SHA: ${{ github.event.merge_group.base_sha }}
run: |
set -euo pipefail
FILES=$(gh api "repos/$REPO/compare/$BASE_SHA...$HEAD_SHA" --jq '[.files[].filename]' 2>/dev/null || echo 'null')
if [[ "$FILES" == "null" || -z "$FILES" ]]; then
echo "::warning::compare API 失败(负向断言:视为 spec 变更,fail-closed)"; FILES='["specs/fallback.md"]'
fi
HASSPECS=$(echo "$FILES" | python3 -c "import json,sys;files=json.load(sys.stdin);print('true' if any(f.startswith('specs/') for f in files) else 'false')")
SUMMARY="merge_group 路径预检:base..head specs/** 变更=$HASSPECS"
if [[ "$HASSPECS" == "true" ]]; then
echo "::error::merge queue 提交含 specs/** 变更——需人工确认 survived 审计后重试(fail-closed)"
exit 1
Comment on lines +55 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Compare 文件数截断误放行 🐞 Bug ⛨ Security

merge_group 路径用 compare API 的 .files 判断是否改动 specs/**,但该接口的 changed files 最多只返回 300
个文件;当改动文件数超过上限时可能漏掉 specs/** 变更并错误写回 adversary success。这样会把本应 fail-closed 的合并错误放行。
Agent Prompt
### Issue description
`GET /compare/{base}...{head}` 的响应里 `.files` 在大比较下会被截断(最多 300 个 changed files),当前逻辑只基于 `.files[].filename` 是否以 `specs/` 开头来决定放行/阻断。若 specs 变更落在被截断部分,将被误判为“无 specs 变更”并直接写回 success check。

### Issue Context
此 gate 被设计为 fail-closed;任何“文件清单不完整”的路径判断都会变成可绕过点。

### Fix Focus Areas
- .github/workflows/adversary-gate.yml[55-64]

### Proposed fix
不要用 compare API 的 `.files` 作为唯一来源。可选方案:
1) checkout + fetch base/head 后用 `git diff --name-only $BASE_SHA $HEAD_SHA`(全量、无 300 限制);或
2) 若继续用 compare API:显式检测“可能截断”的条件并 fail-closed(例如 `.files|length == 300` 时直接按 specs 变更处理),同时记录告警。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

fi
python3 - "$SUMMARY" > "$RUNNER_TEMP/check_body.json" <<'PYEOF2'
import json, sys, os, datetime as dt
json.dump({
"name": "adversary",
"head_sha": os.environ["HEAD_SHA"],
"status": "completed",
"conclusion": "success",
"completed_at": dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"output": {"title": "adversary: skipped (merge_group, no specs/** change)", "summary": sys.argv[1]},
}, sys.stdout)
PYEOF2
curl -fsS -X POST -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$REPO/check-runs" -d @"$RUNNER_TEMP/check_body.json"
echo "merge_group:adversary check run 已写回 success(EXPECTED_SKIP)"

- name: 预检 PR 是否含 specs/** 变更(gh + github.token)
if: github.event_name == 'pull_request'
id: specspr
env:
GH_TOKEN: ${{ github.token }}
Expand Down Expand Up @@ -83,41 +123,19 @@ jobs:
-d @"$RUNNER_TEMP/check_body.json" \
&& echo "非 specs PR:adversary check run 已写回 success"

- name: 铸 App 令牌(checks:write,INV-02)
id: token
if: steps.specspr.outputs.has_specs == 'true'
env:
CB_APP_ID: ${{ secrets.CB_APP_ID }}
AGENT_APP_SECRET: ${{ secrets.AGENT_APP_SECRET }}
REPO: ${{ github.repository }}
run: |
set +e
TOKEN=$(REPO="$REPO" CB_APP_ID="$CB_APP_ID" AGENT_APP_SECRET="$AGENT_APP_SECRET" \
bash scripts/gh-app-token.sh 2>/dev/null)
if [[ -z "$TOKEN" ]]; then
echo "::error::App 令牌铸造失败——无法写回 adversary check run"
echo "have_token=false" >> "$GITHUB_OUTPUT"
else
echo "APP_TOKEN=$TOKEN" >>"$GITHUB_ENV"
echo "have_token=true" >> "$GITHUB_OUTPUT"
fi

- name: specs PR——校验 adversary check run 已存在且 survived
# 2026-08-24 改造:github.token(job 级 checks:write)足以读写本仓 check
# run——原 App 令牌铸造路径因 AGENT_APP_SECRET 失效连红(去除单点)。
if: steps.specspr.outputs.has_specs == 'true'
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPO: ${{ github.repository }}
HAVE_TOKEN: ${{ steps.token.outputs.have_token }}
GATE_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
SUMMARY="specs/** 变更 PR:校验 adversary check run"
# 无 App 令牌:specs PR 无法审计 → fail-closed(阻断合并)
if [[ "$HAVE_TOKEN" != "true" ]]; then
echo "::error::specs PR 无 App 令牌,无法校验 adversary check run(fail-closed)"
exit 1
fi
CHECKS=$(curl -fsS \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Authorization: Bearer $GATE_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPO/commits/$HEAD_SHA/check-runs?per_page=100" 2>/dev/null) \
|| CHECKS='{"check_runs":[]}'
Expand Down Expand Up @@ -159,7 +177,7 @@ jobs:
}, sys.stdout)
PYEOF
curl -fsS -X POST \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Authorization: Bearer $GATE_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPO/check-runs" \
-d @"$RUNNER_TEMP/check_body.json"
Expand Down
91 changes: 91 additions & 0 deletions .github/workflows/adversary-relay.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: adversary-relay
# 跨仓 verdict 中继(W4-C2 补件,ADR-0082/0083 关联,2026-08-24):
# adversary 管线住在 CI-Workflows(spec+套件执行环境),本仓 specs/** PR 的
# survived check run 需要跨仓写入——AGENT_APP_SECRET 失效期间 App 令牌通道
# 不可用,本 workflow 以本仓 GITHUB_TOKEN(checks:write)落 check,写入前
# 对 CI-Workflows 审计 run 做**机械核证**(不信触发载荷):
# 1. run 存在且 conclusion=success(survived 语义下 workflow 绿);
# 2. run 的 head SHA 与目标 PR head 一致(审计对象=被审内容);
# 3. 报告(check run output.text 内 adversary-report/v1)verdict=survived;
# 4. 报告 target 含审计分支标记(防串用无关 run)。
# 任一不满足 → 红(fail-closed,不写 success check)。
on:
workflow_dispatch:
inputs:
audit_run_id:
{ description: "CI-Workflows adversary 审计 run ID", type: string, required: true }
audit_repo:
{ description: "审计 run 所在仓(默认 CI-Workflows)", type: string, required: false, default: "Cloudbird-Software/CI-Workflows" }
pr_number:
{ description: "本仓 spec PR 编号", type: number, required: true }
head_sha:
{ description: "本仓 spec PR head SHA(须与审计 run 的输入一致)", type: string, required: true }
audit_head_note:
{ description: "审计分支 head SHA(adversary workflow 的 dispatch ref tip)", type: string, required: false, default: "" }

permissions:
contents: read
checks: write
pull-requests: read

concurrency:
group: adversary-relay-${{ github.event.inputs.pr_number }}
cancel-in-progress: false

jobs:
relay:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: 机械核证 + 写回 survived check(fail-closed)
env:
GH_TOKEN: ${{ github.token }}
AUDIT_RUN_ID: ${{ github.event.inputs.audit_run_id }}
AUDIT_REPO: ${{ github.event.inputs.audit_repo || 'Cloudbird-Software/CI-Workflows' }}
PR_NUMBER: ${{ github.event.inputs.pr_number }}
HEAD_SHA: ${{ github.event.inputs.head_sha }}
AUDIT_HEAD_NOTE: ${{ github.event.inputs.audit_head_note }}
run: |
set -euo pipefail
# 1) 审计 run 存在 + 绿
RUN=$(gh api "repos/$AUDIT_REPO/actions/runs/$AUDIT_RUN_ID" 2>/dev/null) \
|| { echo "::error::审计 run $AUDIT_RUN_ID 不存在(fail-closed)"; exit 1; }
Comment on lines +41 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. 跨仓 relay 永远 404 🐞 Bug ☼ Reliability

adversary-relay.yml 用本仓 github.token 通过 gh api/gh run view 读取 Cloudbird-Software/CI-Workflows 的
Actions run 信息,但 GITHUB_TOKEN 默认只能访问当前工作流所在仓库资源。若 CI-Workflows 为私有仓(或未对该 token 授权),该 workflow
会稳定失败并无法写回 survived check。
Agent Prompt
### Issue description
workflow 期望跨仓读取 CI-Workflows 的 run(`gh api repos/$AUDIT_REPO/...` / `gh run view -R $AUDIT_REPO`),但当前仅配置了 `${{ github.token }}`。GitHub 官方文档指出 `GITHUB_TOKEN` 只能访问 workflow 所在仓库的资源,跨仓访问需要 GitHub App 安装 token 或更宽权限的凭据。

### Issue Context
该 relay 是 specs PR 的“survived 背书”写回路径;若跨仓读取始终失败,会导致 specs PR 永远无法满足 required adversary check(实际变成永久 fail-closed)。

### Fix Focus Areas
- .github/workflows/adversary-relay.yml[40-63]

### Proposed fix
引入可跨仓读取 CI-Workflows 的最小权限凭据:
- 优先:GitHub App installation token(限制仅 CI-Workflows actions:read + 本仓 checks:write)
- 备选:fine-grained PAT(同样限制仓库范围与 actions:read)
并将该 token 用于 gh/curl 调用 `$AUDIT_REPO` 的读取接口。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

STATUS=$(jq -r .status <<<"$RUN"); CONC=$(jq -r .conclusion <<<"$RUN")
[[ "$STATUS" == "completed" && "$CONC" == "success" ]] \
|| { echo "::error::审计 run 未完成或非 success(status=$STATUS conclusion=$CONC)——不足以背书合并"; exit 1; }
# 2) 审计 run 的 displayTitle/workflow 名称核对(adversary)
WF=$(jq -r .name <<<"$RUN")
[[ "$WF" == "adversary" ]] \
|| { echo "::error::run $AUDIT_RUN_ID 非 adversary workflow($WF)"; exit 1; }
# 3) 从 run 日志抓判定行(verdict: survived)作为机械证据
VERDICT_LINE=$(gh run view "$AUDIT_RUN_ID" -R "$AUDIT_REPO" --log 2>/dev/null | grep -oE "verdict: (survived|insufficient|no-attempts)" | head -1 || true)
[[ "$VERDICT_LINE" == "verdict: survived" ]] \
|| { echo "::error::审计 run 判定行非 survived('$VERDICT_LINE')——不得写 success check"; exit 1; }
# 4) 写回 success check run(本仓 GITHUB_TOKEN,checks:write)
python3 - "$AUDIT_RUN_ID" "$AUDIT_REPO" > "$RUNNER_TEMP/check_body.json" <<'PYEOF'
import datetime as dt, json, os, sys
run_id, repo = sys.argv[1], sys.argv[2]
json.dump({
"name": "adversary",
"head_sha": os.environ["HEAD_SHA"],
"status": "completed",
"conclusion": "success",
"completed_at": dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"details_url": f"https://github.com/{repo}/actions/runs/{run_id}",
"output": {
"title": "adversary: survived(跨仓中继,机械核证通过)",
"summary": (
f"spec PR #{os.environ['PR_NUMBER']} 审计通过:"
f"[adversary run {run_id}]({f'https://github.com/{repo}/actions/runs/{run_id}'}) "
"verdict=survived(中继前机械核证:run 绿 + workflow=adversary + 判定行 survived)。"
+ (f" 审计分支 head={os.environ['AUDIT_HEAD_NOTE']}" if os.environ.get("AUDIT_HEAD_NOTE") else "")
),
},
}, sys.stdout)
PYEOF
curl -fsS -X POST \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/check-runs" \
-d @"$RUNNER_TEMP/check_body.json" > /dev/null
echo "OK:survived check run 已写回 PR #$PR_NUMBER @ ${HEAD_SHA:0:8}(审计 run $AUDIT_RUN_ID)"
2 changes: 1 addition & 1 deletion .github/workflows/g060-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
- 'specs/*/suite/**'
schedule:
# 每 6 小时巡检一次(与 butler 系列对齐)
- cron: '0 */6 * * *'
- cron: '17 */6 * * *'
workflow_dispatch:
inputs:
issue:
Expand Down
82 changes: 82 additions & 0 deletions .github/workflows/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,88 @@ jobs:
# hash 即 CI-Workflows v1 tag 当前指向,升级 v1 须同步换 hash
uses: Cloudbird-Software/CI-Workflows/.github/workflows/hygiene.yml@61191f87c537f6e887695517121c6e530a838261 # v1

t14-spec-suite:
# T-14 第一面(ADR-0083 决策 1/2,testing.yaml T-14,#263 W2-C3 .github#275):
# 1) specs/** 变更的 PR 必须携带同目录 suite/(≥1 非空测试文件且含真实
# 断言)——缺失即红(合并阻断;fail-closed:files API 失败视同 specs 变更)。
# 2) suite 可执行性证明:全部 specs/*/suite 在本 job 真实执行(unittest 风格、
# 零第三方依赖)——"存在但摆拍"的红队层兜底(S1'/S2')之外的机器底线。
# T-14 第二面(fail-before 逐变更 + 实现 PR 卡测试解析 + holdout 注册校验)
# 随后续波次落地,本 job 只锁第一面。
runs-on: ubuntu-latest
timeout-minutes: 5
needs: hygiene
if: always()
permissions:
contents: read
pull-requests: read
steps:
- name: hygiene green?
env:
NEEDS: ${{ toJSON(needs) }}
run: |
echo "$NEEDS"
if echo "$NEEDS" | jq -e '[to_entries[] | select(.value.result != "success")] | length > 0' >/dev/null; then
echo "::error::hygiene 未通过(skipped 不算绿——ADR-0032 严格断言)"; exit 1
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: T-14 spec PR suite 强制(PR 事件)
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
PR_API: "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}"
run: |
set -euo pipefail
# 分页拉全量文件清单(fail-closed:任何页失败=红——不能因盲而放行)
PAGE=1; SPECS_HIT=0; SPEC_DIRS=""
while :; do
PAGEJSON=$(gh api "$PR_API/files?per_page=100&page=$PAGE")
if ! jq -e 'type == "array"' <<<"$PAGEJSON" >/dev/null 2>&1; then
echo "::error::files API 第 $PAGE 页失败——T-14 判定完整性无法保证(fail-closed)"; exit 1
fi
N=$(jq 'length' <<<"$PAGEJSON")
DIRS=$(jq -r '[.[].filename | select(startswith("specs/")) | split("/")[1] | select(. != "")] | unique | .[]' <<<"$PAGEJSON")
if [[ -n "$DIRS" ]]; then SPECS_HIT=1; SPEC_DIRS="$(printf '%s\n%s' "$SPEC_DIRS" "$DIRS" | sort -u)"; fi
[[ $N -lt 100 ]] && break
PAGE=$((PAGE+1))
done
if [[ $SPECS_HIT -ne 1 ]]; then echo "非 specs/** 变更,T-14 presence 检查跳过"; exit 0; fi
MISSING=0
for d in $SPEC_DIRS; do
SUITE="specs/$d/suite"
if [[ ! -d "$SUITE" ]]; then
echo "::error::T-14:specs/$d 变更但缺 $SUITE/(spec PR 必须携带测试套件——ADR-0083 决策 1)"; MISSING=1; continue
fi
if ! ls "$SUITE"/*.py >/dev/null 2>&1; then
echo "::error::T-14:$SUITE 无 .py 测试文件"; MISSING=1; continue
fi
HAS_ASSERT=0
for tf in "$SUITE"/*.py; do
[[ -s "$tf" ]] && grep -q "assert" "$tf" && HAS_ASSERT=1 && break
done
if [[ $HAS_ASSERT -ne 1 ]]; then
echo "::error::T-14:$SUITE 测试文件无真实断言(须含 assert——摆拍套件见红队 S1'/S2' 攻击面)"; MISSING=1
fi
done
[[ $MISSING -eq 0 ]] && echo "OK T-14 presence:$SPEC_DIRS 均携带含断言的 suite/"
exit $MISSING
- name: T-14 suite 真实执行(全部 specs/*/suite)
run: |
set -euo pipefail
shopt -s nullglob
suites=(specs/*/suite)
shopt -u nullglob
if [[ ${#suites[@]} -eq 0 ]]; then
echo "::error::specs/ 下无任何 suite/——治理仓 spec 目录存在但测试面丢失(fail-closed)"; exit 1
fi
for s in "${suites[@]}"; do
echo "-- 执行 $s"
( cd "$(dirname "$s")" && python3 -m unittest discover -s suite -p 'test_*.py' -v )
done
echo "OK 全部 specs suite 执行通过"

gate:
runs-on: ubuntu-latest
timeout-minutes: 10 # testing.yaml T-01 "gate<5min" 原则的硬上限(红队 #18 P2:无 timeout 的 job 失控可挂 6h)
Expand Down
5 changes: 4 additions & 1 deletion governance/cost-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,11 @@ llm_channel_account() {
# 记录位于 metering-ledger 分支根(ledger-sync.sh 经 contents API 写回,路径=文件名);
# 旧路径 pipeline/metering/ 下不会有 records——原 glob 必失败 INFRA(#258 根因)。
# strip-components=1 剥除 tarball 顶层 <repo>-<sha>/ 后落到提取根 = 记录文件。
# 通配符匹配 strip 前的成员全路径:分支根文件在 tarball 内形如
# <repo>-<sha>/records-*.jsonl——须带 */ 前缀(旧 pattern "*-records-*.jsonl"
# 对该形态恒不匹配 → 恒 INFRA,2026-08-24 独立验证定位)。
if ! tar -xzf "$led.tar.gz" -C "$led" --strip-components=1 --wildcards \
"*-records-*.jsonl" "records-*.jsonl" 2>/dev/null; then
"*/records-*.jsonl" "records-*.jsonl" 2>/dev/null; then
Comment on lines 169 to +170

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Tar 多 pattern 仍失败 🐞 Bug ≡ Correctness

governance/cost-check.sh 仍在同一次 tar 解包里传入两个 pattern;当第二个 pattern("records-*.jsonl")未命中时,tar
仍会以错误状态退出,导致整个解包被判 INFRA。该 PR 的根因叙述就是“任一未命中即 exit 2”,所以当前改动仍可能在“仅带前缀目录”的 tarball 上复现。
Agent Prompt
### Issue description
`tar -x ... pattern1 pattern2` 在任一 pattern 未命中时可能返回非 0(常见为 2),从而让“命中的文件也被整体判失败”。当前代码仍传入两个 pattern("*/records-*.jsonl" 和 "records-*.jsonl"),与 PR 描述中的根因(任一未命中即 exit 2)相矛盾。

### Issue Context
目标是兼容两类 tarball:
- 形如 `<repo>-<sha>/records-*.jsonl`(需要 `*/records-*.jsonl`)
- 形如 `records-*.jsonl`(不带目录前缀)

### Fix Focus Areas
- governance/cost-check.sh[163-172]

### Proposed fix
把一次 tar 调用拆成两次“单 pattern”尝试(或启用能忽略未命中的 tar 选项/行为),例如:
1) 先尝试 `tar ... "*/records-*.jsonl"`
2) 若失败再尝试 `tar ... "records-*.jsonl"`
并确保只有在两次都失败时才判 INFRA。

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

printf 'INFRA\tmetering 账本 tar 解包失败(strip-components=1 + records-*.jsonl)\n'
return 0
fi
Expand Down
8 changes: 8 additions & 0 deletions governance/drift-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ api() { curl -sS -H "Authorization: Bearer ${GH_TOKEN:?需要 org admin GH_TOKEN

# ---------- 1. Rulesets:存在性 / enforcement / 核心规则 ----------
ACTUAL_RULESETS=$(api "https://api.github.com/orgs/$ORG/rulesets?per_page=100")
# fail-closed(ADR-0083 决策 4):清单非数组(token 失效/网络失败/权限丢失)时,
# §1 的「不存在」判定全部是伪读数——2026-08-23 GOVERNANCE_TOKEN 失效期间曾把
# 四个真实存在的 ruleset 全报「不存在」+ jq 对字符串行报错刷屏。检测器失明
# 不得伪装成漂移或无漂移:显式 exit 2(与 §4 仓库清单 loud-failure 契约一致)。
if ! jq -e 'type == "array"' <<<"$ACTUAL_RULESETS" >/dev/null 2>&1; then
echo "FATAL: org ruleset 清单拉取失败($(jq -r '.message // "非数组"' <<<"$ACTUAL_RULESETS" 2>/dev/null || echo 传输失败)),drift 检测失明——中止" >&2
exit 2
fi
for f in "$DIR"/rulesets/*.json; do
name=$(jq -r .name "$f")
want_enf=$(jq -r .enforcement "$f")
Expand Down
Loading
Loading