From 260f843b43fa9a97aca81c7d8709c4a71fa09b14 Mon Sep 17 00:00:00 2001 From: cloudbrid-agent Date: Sun, 23 Aug 2026 20:14:21 +0800 Subject: [PATCH 1/9] feat(ISSUE-263): backfill g060-guard and adversary-gate workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W2-C2 g060-guard: specs/*/suite/** IR 分片锁定 + 裁决闭环 W4-C3 adversary-gate: specs/** PR 必含 adversary check + EXPECTED_SKIP Relates: Cloudbird-Software/.github#274, Cloudbird-Software/.github#284 --- .github/workflows/adversary-gate.yml | 196 +++++++++++++++++++++++++++ .github/workflows/g060-guard.yml | 74 ++++++++++ 2 files changed, 270 insertions(+) create mode 100644 .github/workflows/adversary-gate.yml create mode 100644 .github/workflows/g060-guard.yml diff --git a/.github/workflows/adversary-gate.yml b/.github/workflows/adversary-gate.yml new file mode 100644 index 0000000..e82fb3a --- /dev/null +++ b/.github/workflows/adversary-gate.yml @@ -0,0 +1,196 @@ +name: adversary-gate +# W4-C3 adversary gate(Cloudbird-Software/.github#284,AC-14/AC-19,ADR-0067/0082) +# +# 目标:specs/** 路径 PR 必须含 adversary check(漏配/摘除/跳过即红);开发路径 +# 豁免谓词由 diff 路径集确定性派生(禁人工打标,AC-14)。 +# +# 机制: +# - 本 workflow 在每 PR 上运行(org-required-workflows required workflow), +# 产出名为 "adversary" 的 check run(与 main-protection.json required_status_checks +# 登记的 context 一致)。 +# - 纯开发路径 PR(diff 无 specs/** 实质内容变更):经 expected_skip.py 判定 +# EXPECTED_SKIP=True → 写 "success"(留痕 reason),不阻塞合并。 +# - specs/** 实质变更 PR(未全命中 owner 豁免清单):EXPECTED_SKIP=False → +# 必须由 adversary workflow(W3-C1/W4-C2)产出 verdict=survived 的 adversary +# check run 才能合并;本 gate 检查该 check run 是否存在且结论为 success—— +# 不存在/结论非 success 即红(fail-closed,负向断言)。 +# +# 部署注意: +# - 本文件 + CI-Workflows .github/workflows/adversary.yml 当前 App 无 +# workflows 权限,待权限到位后补推;rulesets JSON 已含 adversary context。 +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: read + +concurrency: + group: adversary-gate-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + gate: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + steps: + - name: Checkout pipeline/adversary(expected_skip.py) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + sparse-checkout: | + pipeline/adversary/expected_skip.py + sparse-checkout-cone-mode: false + + - name: 派生 PR diff 路径集 + EXPECTED_SKIP(禁人工打标,AC-14) + id: skip + env: + GH_TOKEN: ${{ github.token }} + PR_API: "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" + run: | + set -euo pipefail + set +e + FILES=$(gh api "$PR_API/files?per_page=300" \ + --jq '[.[].filename]' 2>/dev/null) + RC=$? + set -e + if [[ $RC -ne 0 || -z "$FILES" || "$FILES" == "null" ]]; then + echo "::warning::取 PR files 失败(负向断言:视为 spec 变更)" + FILES='["specs/_api_failed_fallback/spec.md"]' + fi + # 调 expected_skip.py judge 派生(确定性,禁人工打标) + SKIP_OUT=$(python3 pipeline/adversary/expected_skip.py judge --paths "$FILES" 2>/dev/null) \ + || SKIP_OUT='{"expected_skip":false,"reason":"expected_skip.py 执行失败(fail-closed)","specs_paths":[]}' + echo "$SKIP_OUT" > "$RUNNER_TEMP/skip_result.json" + SKIP=$(echo "$SKIP_OUT" | python3 -c "import json,sys;print(str(json.load(sys.stdin)['expected_skip']).lower())" 2>/dev/null || echo "false") + echo "expected_skip=$SKIP" >> "$GITHUB_OUTPUT" + echo "skip_result=$SKIP_OUT" + echo "EXPECTED_SKIP=$SKIP" + + - name: 铸 App 令牌(checks:write,INV-02) + id: token + 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: 开发路径豁免——写 success check run(EXPECTED_SKIP=True) + if: steps.skip.outputs.expected_skip == 'true' && steps.token.outputs.have_token == 'true' + env: + SKIP_RESULT: ${{ steps.skip.outputs.skip_result }} + run: | + set -euo pipefail + SUMMARY=$(echo "$SKIP_RESULT" | python3 -c " + import json,sys + d=json.load(sys.stdin) + lines=['EXPECTED_SKIP=True(开发路径豁免,AC-14)',d.get('reason','')] + sp=d.get('specs_paths',[]) + if sp: lines.append('specs_paths(已豁免): '+', '.join(sp)) + lines.append('派生方式=diff路径集确定性派生(禁人工打标)') + sys.stdout.write('\\n'.join(lines)) + ") + python3 - "$SUMMARY" > "$RUNNER_TEMP/check_body.json" <<'PYEOF' + import json, sys, datetime as dt + summary = sys.argv[1] + body = { + "name": "adversary", + "head_sha": "${{ github.event.pull_request.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 (dev path EXPECTED_SKIP)", "summary": summary}, + } + json.dump(body, sys.stdout) + PYEOF + curl -fsS -X POST \ + -H "Authorization: Bearer $APP_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/${{ github.repository }}/check-runs" \ + -d @"$RUNNER_TEMP/check_body.json" \ + && echo "开发路径:adversary check run 已写回 success(EXPECTED_SKIP)" + + - name: specs/** PR——校验 adversary check run 已存在且 survived + if: steps.skip.outputs.expected_skip == 'false' + env: + SKIP_RESULT: ${{ steps.skip.outputs.skip_result }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + REPO: ${{ github.repository }} + HAVE_TOKEN: ${{ steps.token.outputs.have_token }} + run: | + set -euo pipefail + SUMMARY=$(echo "$SKIP_RESULT" | python3 -c " + import json,sys + d=json.load(sys.stdin) + sp=d.get('remaining_specs',[]) or d.get('specs_paths',[]) + head=', '.join(sp[:5])+('...' if len(sp)>5 else '') + sys.stdout.write('EXPECTED_SKIP=False: specs/'+head) + ") + # 查 head sha 上名为 adversary 的 check run(取 completed 最新) + if [[ "$HAVE_TOKEN" != "true" ]]; then + echo "::error::无 App 令牌,无法校验 adversary check run(fail-closed)" + exit 1 + fi + CHECKS=$(curl -fsS \ + -H "Authorization: Bearer $APP_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":[]}' + VERDICT=$(echo "$CHECKS" | python3 -c " + import json,sys + runs=json.loads(sys.stdin.read()).get('check_runs',[]) + adv=sorted([r for r in runs if r.get('name')=='adversary'], key=lambda r:(r.get('status')!='completed',)) + if not adv: + print('MISSING') + else: + a=adv[-1] + if a.get('status')=='completed' and a.get('conclusion')=='success': + print('SURVIVED') + elif a.get('status')=='completed': + print('RED:'+str(a.get('conclusion'))) + else: + print('PENDING:'+str(a.get('status'))) + ") + if [[ "$VERDICT" == "SURVIVED" ]]; then + echo "adversary check run 已存在且 survived:$SUMMARY" + exit 0 + fi + # 未审计/未 survived:写 failure check run(阻断合并,AC-4 负向断言) + if [[ "$VERDICT" == MISSING* ]]; then + TITLE="adversary: 缺失(specs/** PR 未含 adversary check,阻断)" + else + TITLE="adversary: ${VERDICT}(specs/** PR 审计未通过,阻断)" + fi + python3 - "$TITLE" "$SUMMARY" > "$RUNNER_TEMP/check_body.json" <<'PYEOF' + import json, sys, datetime as dt + title, summary = sys.argv[1], sys.argv[2] + json.dump({ + "name": "adversary", + "head_sha": "${{ github.event.pull_request.head.sha }}", + "status": "completed", + "conclusion": "failure", + "completed_at": dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "output": {"title": title[:255], "summary": summary}, + }, sys.stdout) + PYEOF + curl -fsS -X POST \ + -H "Authorization: Bearer $APP_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/check-runs" \ + -d @"$RUNNER_TEMP/check_body.json" + echo "阻断:$VERDICT —— $SUMMARY" + exit 1 diff --git a/.github/workflows/g060-guard.yml b/.github/workflows/g060-guard.yml new file mode 100644 index 0000000..e7f1d4b --- /dev/null +++ b/.github/workflows/g060-guard.yml @@ -0,0 +1,74 @@ +name: g060 guard + +# ADR-0061 g060 语义扩展至治理仓(ISSUE-263 W2-C2): +# - PR 变更 specs/*/suite/** 时校验写者身份; +# - 定时扫描未裁决的 g060 阻断 issue,超 TTL 触发 dead-man 提醒。 +# +# 注意:当前 App 无 workflows 权限,本文件已完整实现但暂无法推送至上游, +# 阻塞记录在 ISSUE-263 W2-C2 交付说明中。 +on: + pull_request: + paths: + - 'specs/*/suite/**' + schedule: + # 每 6 小时巡检一次(与 butler 系列对齐) + - cron: '0 */6 * * *' + workflow_dispatch: + inputs: + issue: + description: '指定裁决 issue 编号(未指定则处理全部 open g060 issue)' + required: false + type: string + +permissions: + contents: read + +jobs: + g060-lock: + # 仅在 PR 事件且命中 paths 时触发 + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read # checkout + issues: write # 非法修改时创建裁决 issue + pull-requests: read # 读取 PR 文件清单 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + fetch-depth: 0 # 需要完整历史做 base..head diff 兜底 + + - name: g060 lock check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_EVENT_PATH: ${{ github.event_path }} + run: bash scripts/g060-lock.sh + + g060-escalation: + # 定时/手动触发:处理未裁决 issue + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + issues: write + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: g060 escalation + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + if [[ -n "${{ inputs.issue }}" ]]; then + python3 scripts/g060-escalation.py --issue "${{ inputs.issue }}" + else + python3 scripts/g060-escalation.py --all + fi From 281ba065e0309263cb16967b030be9edf12bf332 Mon Sep 17 00:00:00 2001 From: cloudbrid-agent Date: Sun, 23 Aug 2026 20:17:54 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix(adversary-gate):=20=E7=94=A8=20gh=20api?= =?UTF-8?q?=20=E9=A2=84=E6=A3=80=20specs/=20=E8=B7=AF=E5=BE=84=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9D=9E=20specs=20PR=20=E8=AF=AF=E6=96=AD?= =?UTF-8?q?=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原实现依赖跨仓 sparse-checkout 获取 expected_skip.py,但文件在 CI-Workflows 仓而非 .github 仓,导致 python3 调用失败、fail-closed 误判为 specs PR。 修复: - 用 gh + github.token 预检 PR diff 是否含 specs/ 前缀文件 - 非 specs PR 直接写 success check run 放行(零外部依赖) - specs PR 走原 App 令牌 + adversary survived 校验路径 --- .github/workflows/adversary-gate.yml | 125 ++++++++++----------------- 1 file changed, 47 insertions(+), 78 deletions(-) diff --git a/.github/workflows/adversary-gate.yml b/.github/workflows/adversary-gate.yml index e82fb3a..76798c3 100644 --- a/.github/workflows/adversary-gate.yml +++ b/.github/workflows/adversary-gate.yml @@ -6,18 +6,15 @@ name: adversary-gate # # 机制: # - 本 workflow 在每 PR 上运行(org-required-workflows required workflow), -# 产出名为 "adversary" 的 check run(与 main-protection.json required_status_checks -# 登记的 context 一致)。 -# - 纯开发路径 PR(diff 无 specs/** 实质内容变更):经 expected_skip.py 判定 -# EXPECTED_SKIP=True → 写 "success"(留痕 reason),不阻塞合并。 -# - specs/** 实质变更 PR(未全命中 owner 豁免清单):EXPECTED_SKIP=False → -# 必须由 adversary workflow(W3-C1/W4-C2)产出 verdict=survived 的 adversary -# check run 才能合并;本 gate 检查该 check run 是否存在且结论为 success—— -# 不存在/结论非 success 即红(fail-closed,负向断言)。 +# 产出名为 "adversary" 的 check run。 +# - 预检:用 gh + github.token 判断 PR 是否含 specs/** 变更。 +# - 非 specs PR → 直接写 success check run,放行(零外部依赖)。 +# - specs PR → 铸 App 令牌,查 head sha 上是否存在 verdict=survived 的 +# adversary check run;不存在/结论非 success 即红(fail-closed)。 # # 部署注意: -# - 本文件 + CI-Workflows .github/workflows/adversary.yml 当前 App 无 -# workflows 权限,待权限到位后补推;rulesets JSON 已含 adversary context。 +# - expected_skip.py 在 CI-Workflows 仓;本 gate 用 gh api 预检 specs/ +# 路径,不依赖 expected_skip.py,避免跨仓 sparse-checkout 失败。 on: pull_request: types: [opened, synchronize, reopened] @@ -38,111 +35,83 @@ jobs: contents: read pull-requests: read steps: - - name: Checkout pipeline/adversary(expected_skip.py) - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: | - pipeline/adversary/expected_skip.py - sparse-checkout-cone-mode: false - - - name: 派生 PR diff 路径集 + EXPECTED_SKIP(禁人工打标,AC-14) - id: skip + - name: 预检 PR 是否含 specs/** 变更(gh + github.token) + id: specspr env: GH_TOKEN: ${{ github.token }} PR_API: "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" run: | set -euo pipefail set +e - FILES=$(gh api "$PR_API/files?per_page=300" \ - --jq '[.[].filename]' 2>/dev/null) + FILES=$(gh api "$PR_API/files?per_page=300" --jq '[.[].filename]' 2>/dev/null) RC=$? set -e if [[ $RC -ne 0 || -z "$FILES" || "$FILES" == "null" ]]; then + # API 失败 → 负向断言:视为 spec 变更,走完整审计路径 + echo "has_specs=true" >> "$GITHUB_OUTPUT" echo "::warning::取 PR files 失败(负向断言:视为 spec 变更)" - FILES='["specs/_api_failed_fallback/spec.md"]' - fi - # 调 expected_skip.py judge 派生(确定性,禁人工打标) - SKIP_OUT=$(python3 pipeline/adversary/expected_skip.py judge --paths "$FILES" 2>/dev/null) \ - || SKIP_OUT='{"expected_skip":false,"reason":"expected_skip.py 执行失败(fail-closed)","specs_paths":[]}' - echo "$SKIP_OUT" > "$RUNNER_TEMP/skip_result.json" - SKIP=$(echo "$SKIP_OUT" | python3 -c "import json,sys;print(str(json.load(sys.stdin)['expected_skip']).lower())" 2>/dev/null || echo "false") - echo "expected_skip=$SKIP" >> "$GITHUB_OUTPUT" - echo "skip_result=$SKIP_OUT" - echo "EXPECTED_SKIP=$SKIP" - - - name: 铸 App 令牌(checks:write,INV-02) - id: token - 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" + 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')") + echo "has_specs=$HASSPECS" >> "$GITHUB_OUTPUT" fi - - name: 开发路径豁免——写 success check run(EXPECTED_SKIP=True) - if: steps.skip.outputs.expected_skip == 'true' && steps.token.outputs.have_token == 'true' + - name: 非 specs PR——写 success check run 并放行(github.token) + if: steps.specspr.outputs.has_specs == 'false' env: - SKIP_RESULT: ${{ steps.skip.outputs.skip_result }} + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - SUMMARY=$(echo "$SKIP_RESULT" | python3 -c " - import json,sys - d=json.load(sys.stdin) - lines=['EXPECTED_SKIP=True(开发路径豁免,AC-14)',d.get('reason','')] - sp=d.get('specs_paths',[]) - if sp: lines.append('specs_paths(已豁免): '+', '.join(sp)) - lines.append('派生方式=diff路径集确定性派生(禁人工打标)') - sys.stdout.write('\\n'.join(lines)) - ") + SUMMARY="specs/** 未变更:EXPECTED_SKIP=True(路径预检:diff 无 specs/ 前缀文件)" python3 - "$SUMMARY" > "$RUNNER_TEMP/check_body.json" <<'PYEOF' import json, sys, datetime as dt summary = sys.argv[1] - body = { + json.dump({ "name": "adversary", "head_sha": "${{ github.event.pull_request.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 (dev path EXPECTED_SKIP)", "summary": summary}, - } - json.dump(body, sys.stdout) + "output": {"title": "adversary: skipped (no specs/** change)", "summary": summary}, + }, sys.stdout) PYEOF curl -fsS -X POST \ - -H "Authorization: Bearer $APP_TOKEN" \ + -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" \ - && echo "开发路径:adversary check run 已写回 success(EXPECTED_SKIP)" + && 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 - if: steps.skip.outputs.expected_skip == 'false' + - name: specs PR——校验 adversary check run 已存在且 survived + if: steps.specspr.outputs.has_specs == 'true' env: - SKIP_RESULT: ${{ steps.skip.outputs.skip_result }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} REPO: ${{ github.repository }} HAVE_TOKEN: ${{ steps.token.outputs.have_token }} run: | set -euo pipefail - SUMMARY=$(echo "$SKIP_RESULT" | python3 -c " - import json,sys - d=json.load(sys.stdin) - sp=d.get('remaining_specs',[]) or d.get('specs_paths',[]) - head=', '.join(sp[:5])+('...' if len(sp)>5 else '') - sys.stdout.write('EXPECTED_SKIP=False: specs/'+head) - ") - # 查 head sha 上名为 adversary 的 check run(取 completed 最新) + SUMMARY="specs/** 变更 PR:校验 adversary check run" + # 无 App 令牌:specs PR 无法审计 → fail-closed(阻断合并) if [[ "$HAVE_TOKEN" != "true" ]]; then - echo "::error::无 App 令牌,无法校验 adversary check run(fail-closed)" + echo "::error::specs PR 无 App 令牌,无法校验 adversary check run(fail-closed)" exit 1 fi CHECKS=$(curl -fsS \ From 6fa1047551429ef74f396f0c74037e695093c640 Mon Sep 17 00:00:00 2001 From: cloudbrid-agent Date: Sun, 23 Aug 2026 20:18:38 +0800 Subject: [PATCH 3/9] feat(ISSUE-263): backfill adversary.yml + conductor.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W3-C1 adversary.yml: repository_dispatch 触发 + 沙箱 + harden-runner W4-C1 conductor.yml: T5/T6 路由 + suite 就绪谓词 + 三元组校验 --- .github/workflows/adversary.yml | 175 ++++++++++++++++++++++++++++++++ .github/workflows/conductor.yml | 160 ++++++++++++++++++++++++++++- 2 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/adversary.yml diff --git a/.github/workflows/adversary.yml b/.github/workflows/adversary.yml new file mode 100644 index 0000000..a51cea4 --- /dev/null +++ b/.github/workflows/adversary.yml @@ -0,0 +1,175 @@ +name: adversary +# W3-C1 + W4-C2 adversary workflow(Cloudbird-Software/.github#277,ADR-0067) +# - repository_dispatch:event_type 白名单精确匹配(W3-C1),由 conductor/T5/T6 触发 +# - workflow_dispatch:spec PR 阶段/波次收口按需手动运行(W4-C2) +# 判定语义:judge-deep 档(配置锁定+AR-8 跨族)产出"通过全部测试的最偷懒实现", +# 在套件上全绿 → 判"套件不充分"(本 run 红=blocking:实现 PR 须先补强套件); +# 攻击失败 → 套件通过考验(绿);白卷 → exit 3 infra(恒绿防御)。 +# LLM 调用唯一入口 = pipeline/metering/metering-wrapper.sh(ADR-0062,judge-deep +# 成本入 BUDGET-01);账本验链后同步 metering-ledger 分支(cost-check 数据源)。 +# 安全语义:adversary 产物是故意生成的不可信代码,judge 步会真实执行——本 +# workflow 跑在一次性 GitHub-hosted runner 上(凭据仅 provider key,无 repo 写权)。 +on: + repository_dispatch: + types: + - adversary-run + - adversary-replay + workflow_dispatch: + inputs: + target: + { description: "目标目录(须含 spec.md + suite/ + run-suite.sh)", type: string, required: true } + replay_file: + { description: "离线回放响应文件(可选——复现审计/无凭据环境)", type: string, required: false, default: "" } + +permissions: + contents: read + +concurrency: + group: adversary-${{ github.event.client_payload.issue || github.ref }} + cancel-in-progress: false # 判定报告不并发覆盖(同 target 重跑按序排队) + +jobs: + attack: + runs-on: ubuntu-latest + timeout-minutes: 15 # judge-deep 档 + 逐尝试套件执行的硬上限 + # job 级授权(顶层仍最小读):contents:write 仅为账本同步步推 metering-ledger + # 分支所需(同仓 GITHUB_TOKEN——llm-connectivity 同模式);攻击步本身只读 + permissions: + contents: write + + env: + # 配置面:repository_dispatch 载荷字段通过 env 注入,避免 template-injection + ADV_EVENT_TYPE: ${{ github.event.action }} + ADV_ISSUE: ${{ github.event.client_payload.issue }} + ADV_SPEC_PATH: ${{ github.event.client_payload.spec_path }} + ADV_CARD_ID: ${{ github.event.client_payload.card_id }} + ADV_AUDIT_RUN_ID: ${{ github.event.client_payload.audit_run_id }} + LLM_ENDPOINT: ${{ vars.LLM_ENDPOINT }} + LLM_MODEL: ${{ vars.LLM_MODEL }} + + steps: + - name: 出向白名单(仅 github + provider + CNB,INV-06 / AC-13) + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 + with: + egress-policy: block + # harden-runner allowed-endpoints 为静态列表;LLM endpoint 域名须与 vars.LLM_ENDPOINT 同步维护 + allowed-endpoints: | + github.com:443 + api.github.com:443 + objects.githubusercontent.com:443 + api.cnb.cool:443 + open.bigmodel.cn:443 + api.openai.com:443 + api.anthropic.com:443 + - name: Checkout 完整代码库 + sparse-checkout 治理规范 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false # 账本同步走显式 GH_TOKEN 的 contents API,不落 git 凭据 + sparse-checkout: | + governance/ + specs/ + pipeline/adversary/ + pipeline/metering/ + sparse-checkout-cone-mode: false + + - name: 校验 repository_dispatch event_type 白名单(不信任 dispatch 载荷) + if: github.event_name == 'repository_dispatch' + run: | + set -euo pipefail + case "$ADV_EVENT_TYPE" in + adversary-run|adversary-replay) ;; + *) echo "::error::event_type '$ADV_EVENT_TYPE' 不在白名单内"; exit 2 ;; + esac + echo "event_type=$ADV_EVENT_TYPE 校验通过" + + - name: 解析目标路径(workflow_dispatch / repository_dispatch 兼容) + id: target + run: | + set -euo pipefail + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + t="${{ inputs.target }}" + r="${{ inputs.replay_file }}" + else + t="${ADV_SPEC_PATH:-}" + r="" + # replay 模式下 repository_dispatch 可透传 audit_run_id 作为回放键 + if [[ "$ADV_EVENT_TYPE" == "adversary-replay" ]]; then + r="${ADV_AUDIT_RUN_ID:-}" + fi + fi + echo "target=$t" >> "$GITHUB_OUTPUT" + echo "replay=$r" >> "$GITHUB_OUTPUT" + + - name: 凭据形状扫描(AC-6:禁止出现第二个凭据) + run: | + set -euo pipefail + python3 pipeline/adversary/run.py --mode credential-scan --env-dump "$RUNNER_TEMP/credential-scan.json" + + - name: PyYAML(adversary-config / attack-strategies / 角色档表解析) + run: python3 -m pip install --disable-pip-version-check --quiet pyyaml==6.0.3 + + - name: 目标契约预检(fail-closed:路径错不在 runner 上瞎跑) + env: + ADV_TARGET: ${{ steps.target.outputs.target }} + run: | + set -eu + # 输入只经 env 进 shell(zizmor template-injection 面收敛) + [[ "$ADV_TARGET" != /* && "$ADV_TARGET" != *..* ]] \ + || { echo "::error::target 须为仓内相对路径且不含 .."; exit 2; } + [[ -f "$ADV_TARGET/spec.md" && -d "$ADV_TARGET/suite" && -f "$ADV_TARGET/run-suite.sh" ]] \ + || { echo "::error::目标目录契约不满足(spec.md + suite/ + run-suite.sh):$ADV_TARGET"; exit 2; } + ls -1 "$ADV_TARGET"/suite | head + + - name: 攻击(judge-deep 锁定 + 计量 wrapper,ADR-0067/0062) + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_ENDPOINT: ${{ vars.LLM_ENDPOINT }} + LLM_MODEL: ${{ vars.LLM_MODEL }} + ADV_TARGET: ${{ steps.target.outputs.target }} + ADV_REPLAY: ${{ steps.target.outputs.replay }} + run: | + args=(--target "$ADV_TARGET" --report-out "$RUNNER_TEMP/adversary-report.json") + if [[ -n "$ADV_REPLAY" ]]; then + args+=(--replay-file "$ADV_REPLAY") + else + [[ -n "${LLM_API_KEY:-}" ]] || { echo "::error::LLM_API_KEY 缺失且未给 replay_file(fail-closed)"; exit 2; } + fi + # 退出码语义:0=套件通过考验 | 1=套件不充分(blocking,本 run 红) + # | 3=adversary 白卷(恒绿防御 infra)| 2/4=环境/provider 失败 + bash pipeline/adversary/run-adversary.sh "${args[@]}" + - name: 报告 schema 校验(白卷即失败,AC-15) + if: always() + run: | + set -euo pipefail + f="$RUNNER_TEMP/adversary-report.json" + if [[ ! -f "$f" ]]; then + echo "::error::未产出 adversary 报告(白卷)" + exit 3 + fi + python3 pipeline/adversary/run.py --mode validate --report "$f" + + - name: 判定报告入 run 摘要(无论成败都留痕) + if: always() + run: | + f="$RUNNER_TEMP/adversary-report.json" + if [[ -f "$f" ]]; then + { + echo "## 恶意合规 adversary 报告(ADR-0067)" + echo '```json' + cat "$f" + echo '```' + } >>"$GITHUB_STEP_SUMMARY" + else + echo "## 恶意合规 adversary 报告:未产出(前置步骤失败)" >>"$GITHUB_STEP_SUMMARY" + fi + - name: 计量账本验链 + 同步(metering-ledger 分支,BUDGET-01 数据源) + if: always() + env: + GH_TOKEN: ${{ github.token }} + run: | + if ! ls .metering/records-*.jsonl >/dev/null 2>&1; then + echo "无账本周片(调用未发生或预检即红)——跳过同步" + exit 0 + fi + bash pipeline/metering/metering-verify.sh --dir .metering + bash pipeline/metering/ledger-sync.sh --dir .metering --branch metering-ledger diff --git a/.github/workflows/conductor.yml b/.github/workflows/conductor.yml index 1f77910..b6a9d5b 100644 --- a/.github/workflows/conductor.yml +++ b/.github/workflows/conductor.yml @@ -2,6 +2,8 @@ name: conductor # 状态机路由器(IR-0001 W0-C3 / ADR-0049)。W0 事件面=本仓(issues.labeled + # issue_comment);跨仓扩展随产品仓接入。全部状态标签写操作以 cloudbrid-agent # App 令牌执行(INV-02:GITHUB_TOKEN 身份不持有状态写权)。 +# W4-C1(ADR-0079 / ISSUE-263 AC-12):T5/T6 路由增强——suite 就绪谓词 + +# 三元组 survived 记录校验 + needs-human 不可直跳 wave-planned 断言。 # W1-C3(ADR-0055 决策 6):写入类 front-desk 命令(/claim /release)前置转介 # arbiter 裁决(宪法 §11 唤醒矩阵事件行"仲裁请求处理(/claim 等,转 arbiter)"); # 仲裁是叠加授权层,transitions.yaml 转移表语义不变。三态:0=allow 继续原动作、 @@ -11,6 +13,13 @@ on: types: [labeled] issue_comment: types: [created] + # W4-C1:跨仓触发面——adversary 完成 survived 后经 repository_dispatch 通知 conductor + repository_dispatch: + types: [conductor] + # W4-C1:adversary workflow_run 完成后触发 T6 评估 + workflow_run: + workflows: [adversary] + types: [completed] # INV-09:每 issue 一个 concurrency group、cancel-in-progress=false——重复投递 # 排队串行而非并发竞态;幂等由 from_state 匹配承担(重复事件=当前态已变=no-op)。 @@ -28,11 +37,12 @@ jobs: route: if: github.repository == 'Cloudbird-Software/.github' runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 outputs: invoke: ${{ steps.route.outputs.invoke }} issue: ${{ steps.route.outputs.issue }} ir_ref: ${{ steps.route.outputs.ir_ref }} + verdict: ${{ steps.route.outputs.verdict }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -62,7 +72,7 @@ jobs: # run 日志——AC-11)。注释/标签正文绝不进入任何求值(命令白名单精确匹配)。 # W1-C3:/claim(T3)/release 前置转介 arbiter(ADR-0055;参数以 arbiter 仓 # cli.py 为准);delivery-id=comment node_id(稳定幂等键,重投→arbiter noop)。 - - name: route(INV-02/09) + - name: route(INV-02/09/AC-12) id: route env: APP_TOKEN: ${{ env.APP_TOKEN }} @@ -79,6 +89,8 @@ jobs: ISSUE_TITLE: ${{ github.event.issue.title }} REPO: ${{ github.repository }} RUN_ID: ${{ github.run_id }} + # W4-C1:repository_dispatch / workflow_run 载荷 + EVENT_PAYLOAD: ${{ toJson(github.event) }} run: | python3 - <<'PYEOF' import json, os, re, subprocess, urllib.parse, urllib.request, urllib.error, yaml @@ -111,6 +123,7 @@ jobs: # ---- 事件规范化(白名单精确匹配,正文不进任何求值)---- ev = None + dispatch_payload = None if E["EVENT_NAME"] == "issues" and E["ACTION"] == "labeled": ln = E.get("LABEL_NAME") or "" if ln.startswith("state:"): @@ -124,6 +137,40 @@ jobs: ev = f"comment:{token}" else: raise SystemExit(0) # 普通评论:无审计面(噪音) + elif E["EVENT_NAME"] == "repository_dispatch" and E["ACTION"] == "conductor": + # W4-C1:跨仓 conductor 事件——载荷含 event_type 白名单精确匹配 + raw_payload = E.get("EVENT_PAYLOAD") or "{}" + try: + dispatch_payload = json.loads(raw_payload) + except Exception: + audit("event=repository_dispatch verdict=noop payload=unparseable"); raise SystemExit(0) + etype = (dispatch_payload.get("event_type") or "").strip() + # 白名单精确匹配:只接受 adversary-survived / adversary-insufficient 等 + if etype == "adversary-survived": + ev = "dispatch:adversary-survived" + elif etype == "adversary-insufficient": + ev = "dispatch:adversary-insufficient" + elif etype == "adversary-needs-human": + ev = "dispatch:adversary-needs-human" + else: + audit(f"event=repository_dispatch event_type={etype} verdict=noop(白名单外)") + raise SystemExit(0) + elif E["EVENT_NAME"] == "workflow_run": + # W4-C1:workflow_run 完成事件——仅处理 adversary workflow 的 T6 路由 + raw_payload = E.get("EVENT_PAYLOAD") or "{}" + try: + wr_payload = json.loads(raw_payload) + except Exception: + audit("event=workflow_run verdict=noop payload=unparseable"); raise SystemExit(0) + wf_name = (wr_payload.get("workflow_run") or {}).get("name", "") + conclusion = (wr_payload.get("workflow_run") or {}).get("conclusion", "") + if "adversary" not in wf_name.lower(): + audit(f"event=workflow_run workflow={wf_name} verdict=noop(非 adversary)") + raise SystemExit(0) + # workflow_run 触发 T6 评估(redteam→wave-planned) + ev = "workflow_run:adversary-completed" + dispatch_payload = {"workflow_run": wr_payload.get("workflow_run", {}), + "conclusion": conclusion} if ev is None: audit("event=unrecognized verdict=noop"); raise SystemExit(0) @@ -164,6 +211,14 @@ jobs: if len(states) > 1: audit(f"verdict=abort 多状态标签并存: {states}"); raise SystemExit(1) + # ---- W4-C1:needs-human 不可直跳 wave-planned 断言(AC-12)---- + # 任何试图从 needs-human 直接进入 wave-planned 的转移一律拒绝 + if current == "needs-human" and ev in ("label:state:wave-planned", "workflow_run:adversary-completed"): + audit(f"verdict=DENIED-needs-human-bypass 当前态=needs-human 不可直跳 wave-planned(AC-12)") + # 回退标签 + api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/labels/state%3Awave-planned", "DELETE") + raise SystemExit(0) + # ---- 转移表匹配(幂等:from_state 不符=no-op)---- # /release 不在表内(transitions.yaml 未列该事件)——纯租约面命令: # 直达 arbiter 裁决,不产生任何标签转移(ADR-0055 决策 6) @@ -243,10 +298,105 @@ jobs: f"(fail-closed——不许绕过仲裁;delivery 幂等可安全重投)") raise SystemExit(1) audit(f"event={ev} transition={t['id']} sender_role={role}({role_src}) arbiter=allow " - f"(租约已建——T3 落地;TTL 到期由下一 /claim 原子接管,ADR-0054)") + f"(租约已建——T3 落地;TTL 到期由下一 /claim 原子接管,ADR-0054)") + + # ---- W4-C1:T5 suite 就绪谓词(AC-12)---- + # suite 就绪 = 确定性谓词:suite/ 存在 + 含非空测试文件 + 可解析 + # 不信任 dispatch 载荷——conductor 侧重新断言 + def check_suite_ready(issue_number): + """检查卡对应的 suite/ 是否就绪(存在+非空+可解析)。返回 (ready, reason)。""" + # 从 issue body 中提取 spec 路径或 suite 路径 + body = iss.get("body") or "" + # 默认 suite 路径:specs//suite/ + m = re.search(r"(IR-\d+|ISSUE-\d+)", iss.get("title") or "") + task_id = m.group(1) if m else f"ISSUE-{issue_number}" + suite_rel = f"specs/{task_id}/suite" + # 检查 suite 目录是否存在且含非空测试文件 + suite_abs = os.path.join(os.getcwd(), suite_rel) + if not os.path.isdir(suite_abs): + return False, f"suite 目录不存在: {suite_rel}" + test_files = [] + for root, _dirs, files in os.walk(suite_abs): + for fn in files: + if fn.startswith("test_") and fn.endswith(".py"): + fpath = os.path.join(root, fn) + # 非空检查 + if os.path.getsize(fpath) > 0: + test_files.append(fpath) + if not test_files: + return False, f"suite 目录无有效测试文件: {suite_rel}" + # 可解析检查:python ast.parse + import ast + for tf in test_files: + try: + with open(tf, encoding="utf-8") as f: + ast.parse(f.read()) + except SyntaxError as e: + return False, f"测试文件不可解析 {tf}: {e}" + return True, f"suite 就绪: {len(test_files)} 个有效测试文件" + + if t["id"] == "T5": + ready, reason = check_suite_ready(ISSUE) + audit(f"T5 suite 就绪谓词: ready={ready} reason={reason}") + if not ready: + audit(f"verdict=DENIED-suite-not-ready T5 拒绝——{reason}") + # 回退标签 + api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/labels/state%3Aredteam", "DELETE") + raise SystemExit(0) + + # ---- W4-C1:T6 三元组 survived 记录校验(AC-12)---- + # 进入 wave-planned 必须存在该卡本次生命周期内、卡 ID+specVersion+审计 run ID + # 三元组对应的 survived 审计记录(禁止跨卡/历史记录短路) + def check_triple_survived(issue_number, payload): + """校验三元组 survived 记录。返回 (ok, reason, triple)。""" + # 从 issue 提取卡 ID 与 specVersion + body = iss.get("body") or "" + m_card = re.search(r"Card:\s*(\S+)", body) + card_id = m_card.group(1) if m_card else f"{REPO}#{issue_number}" + m_spec = re.search(r"[Ss]pec[Vv]ersion:\s*(\d+)", body) + spec_version = m_spec.group(1) if m_spec else None + # 从 adversary 审计记录中提取 run ID + # 优先取 payload 中的 run_id,否则从 issue 注释中查找 + run_id = None + verdict_from_dispatch = None + if payload: + run_id = (payload.get("client_payload") or {}).get("run_id") or payload.get("run_id") + verdict_from_dispatch = (payload.get("client_payload") or {}).get("verdict") + # 三元组完整性检查 + if not spec_version: + return False, "缺少 specVersion(issue body 未含 specVersion 字段)", None + if not run_id: + return False, "缺少审计 run ID(adversary 记录未含 run_id)", None + triple = {"card_id": card_id, "specVersion": spec_version, "audit_run_id": run_id} + # 验证 survived 语义:verdict 必须是 survived + if verdict_from_dispatch and verdict_from_dispatch != "survived": + return False, f"adversary verdict={verdict_from_dispatch}(非 survived)", triple + # 验证 run ID 未被跨卡复用(检查 issue 注释中是否有该 run ID 的 survived 记录) + st_com, comments = api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{issue_number}/comments") + if st_com == 200: + survived_runs = set() + for c in comments: + cb = c.get("body", "") + if "adversary:survived" in cb or "verdict=survived" in cb: + # 提取 run id + m_run = re.search(r"run[_-]?id[=:]\s*([A-Za-z0-9_\-]+)", cb, re.I) + if m_run: + survived_runs.add(m_run.group(1)) + if survived_runs and run_id not in survived_runs: + return False, f"run ID {run_id} 不在本卡 survived 记录中(防跨卡短路)", triple + return True, f"三元组校验通过: {triple}", triple + + if t["id"] == "T6": + ok_triple, triple_reason, triple = check_triple_survived(ISSUE, dispatch_payload) + audit(f"T6 三元组校验: ok={ok_triple} reason={triple_reason}") + if not ok_triple: + audit(f"verdict=DENIED-triple-mismatch T6 拒绝——{triple_reason}") + # 回退标签 + api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/labels/state%3Awave-planned", "DELETE") + raise SystemExit(0) # ---- 执行转移(状态标签写=App 身份,INV-02;写失败=fail-closed, - # /claim 已建租约时先补偿回滚——杜绝“租约在、卡未变”的不一致面,ADR-0055)---- + # /claim 已建租约时先补偿回滚——杜绝"租约在、卡未变"的不一致面,ADR-0055)---- class WriteFail(Exception): pass @@ -292,6 +442,8 @@ jobs: out.write("invoke=none\n") audit(f"event={ev} transition={t['id']} sender_role={role} verdict=ALLOWED " f"{t['from_state']}->{t['to_state']} action=noop") + # W4-C1:输出 conductor 路由结果供下游消费 + out.write(f"verdict=allowed\ntransition={t['id']}\n") except WriteFail as e: audit(f"event={ev} verdict=ABORT 状态写失败 {e}(fail-closed;delivery 幂等可安全重投)") raise SystemExit(1) From 65c99416dd1f4baa16362b9980c7a18c1937a79d Mon Sep 17 00:00:00 2001 From: cloudbrid-agent Date: Sun, 23 Aug 2026 20:20:35 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix(adversary-gate):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20checks:write=20=E6=9D=83=E9=99=90=EF=BC=8C=E5=85=81=E8=AE=B8?= =?UTF-8?q?=E5=86=99=20check=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/adversary-gate.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/adversary-gate.yml b/.github/workflows/adversary-gate.yml index 76798c3..6566ca7 100644 --- a/.github/workflows/adversary-gate.yml +++ b/.github/workflows/adversary-gate.yml @@ -22,6 +22,7 @@ on: permissions: contents: read pull-requests: read + checks: write concurrency: group: adversary-gate-${{ github.event.pull_request.number }} @@ -34,6 +35,7 @@ jobs: permissions: contents: read pull-requests: read + checks: write steps: - name: 预检 PR 是否含 specs/** 变更(gh + github.token) id: specspr From 3dfca31de9db648621f8a0f762d0b5b383746221 Mon Sep 17 00:00:00 2001 From: cloudbrid-agent Date: Sun, 23 Aug 2026 20:27:25 +0800 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=20adversary.yml?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20=E5=BA=94=E4=BD=8D=E4=BA=8E=20CI-Workfl?= =?UTF-8?q?ows=20=E4=BB=93=E8=80=8C=E9=9D=9E=20.github=20=E4=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/adversary.yml | 175 -------------------------------- 1 file changed, 175 deletions(-) delete mode 100644 .github/workflows/adversary.yml diff --git a/.github/workflows/adversary.yml b/.github/workflows/adversary.yml deleted file mode 100644 index a51cea4..0000000 --- a/.github/workflows/adversary.yml +++ /dev/null @@ -1,175 +0,0 @@ -name: adversary -# W3-C1 + W4-C2 adversary workflow(Cloudbird-Software/.github#277,ADR-0067) -# - repository_dispatch:event_type 白名单精确匹配(W3-C1),由 conductor/T5/T6 触发 -# - workflow_dispatch:spec PR 阶段/波次收口按需手动运行(W4-C2) -# 判定语义:judge-deep 档(配置锁定+AR-8 跨族)产出"通过全部测试的最偷懒实现", -# 在套件上全绿 → 判"套件不充分"(本 run 红=blocking:实现 PR 须先补强套件); -# 攻击失败 → 套件通过考验(绿);白卷 → exit 3 infra(恒绿防御)。 -# LLM 调用唯一入口 = pipeline/metering/metering-wrapper.sh(ADR-0062,judge-deep -# 成本入 BUDGET-01);账本验链后同步 metering-ledger 分支(cost-check 数据源)。 -# 安全语义:adversary 产物是故意生成的不可信代码,judge 步会真实执行——本 -# workflow 跑在一次性 GitHub-hosted runner 上(凭据仅 provider key,无 repo 写权)。 -on: - repository_dispatch: - types: - - adversary-run - - adversary-replay - workflow_dispatch: - inputs: - target: - { description: "目标目录(须含 spec.md + suite/ + run-suite.sh)", type: string, required: true } - replay_file: - { description: "离线回放响应文件(可选——复现审计/无凭据环境)", type: string, required: false, default: "" } - -permissions: - contents: read - -concurrency: - group: adversary-${{ github.event.client_payload.issue || github.ref }} - cancel-in-progress: false # 判定报告不并发覆盖(同 target 重跑按序排队) - -jobs: - attack: - runs-on: ubuntu-latest - timeout-minutes: 15 # judge-deep 档 + 逐尝试套件执行的硬上限 - # job 级授权(顶层仍最小读):contents:write 仅为账本同步步推 metering-ledger - # 分支所需(同仓 GITHUB_TOKEN——llm-connectivity 同模式);攻击步本身只读 - permissions: - contents: write - - env: - # 配置面:repository_dispatch 载荷字段通过 env 注入,避免 template-injection - ADV_EVENT_TYPE: ${{ github.event.action }} - ADV_ISSUE: ${{ github.event.client_payload.issue }} - ADV_SPEC_PATH: ${{ github.event.client_payload.spec_path }} - ADV_CARD_ID: ${{ github.event.client_payload.card_id }} - ADV_AUDIT_RUN_ID: ${{ github.event.client_payload.audit_run_id }} - LLM_ENDPOINT: ${{ vars.LLM_ENDPOINT }} - LLM_MODEL: ${{ vars.LLM_MODEL }} - - steps: - - name: 出向白名单(仅 github + provider + CNB,INV-06 / AC-13) - uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 - with: - egress-policy: block - # harden-runner allowed-endpoints 为静态列表;LLM endpoint 域名须与 vars.LLM_ENDPOINT 同步维护 - allowed-endpoints: | - github.com:443 - api.github.com:443 - objects.githubusercontent.com:443 - api.cnb.cool:443 - open.bigmodel.cn:443 - api.openai.com:443 - api.anthropic.com:443 - - name: Checkout 完整代码库 + sparse-checkout 治理规范 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false # 账本同步走显式 GH_TOKEN 的 contents API,不落 git 凭据 - sparse-checkout: | - governance/ - specs/ - pipeline/adversary/ - pipeline/metering/ - sparse-checkout-cone-mode: false - - - name: 校验 repository_dispatch event_type 白名单(不信任 dispatch 载荷) - if: github.event_name == 'repository_dispatch' - run: | - set -euo pipefail - case "$ADV_EVENT_TYPE" in - adversary-run|adversary-replay) ;; - *) echo "::error::event_type '$ADV_EVENT_TYPE' 不在白名单内"; exit 2 ;; - esac - echo "event_type=$ADV_EVENT_TYPE 校验通过" - - - name: 解析目标路径(workflow_dispatch / repository_dispatch 兼容) - id: target - run: | - set -euo pipefail - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - t="${{ inputs.target }}" - r="${{ inputs.replay_file }}" - else - t="${ADV_SPEC_PATH:-}" - r="" - # replay 模式下 repository_dispatch 可透传 audit_run_id 作为回放键 - if [[ "$ADV_EVENT_TYPE" == "adversary-replay" ]]; then - r="${ADV_AUDIT_RUN_ID:-}" - fi - fi - echo "target=$t" >> "$GITHUB_OUTPUT" - echo "replay=$r" >> "$GITHUB_OUTPUT" - - - name: 凭据形状扫描(AC-6:禁止出现第二个凭据) - run: | - set -euo pipefail - python3 pipeline/adversary/run.py --mode credential-scan --env-dump "$RUNNER_TEMP/credential-scan.json" - - - name: PyYAML(adversary-config / attack-strategies / 角色档表解析) - run: python3 -m pip install --disable-pip-version-check --quiet pyyaml==6.0.3 - - - name: 目标契约预检(fail-closed:路径错不在 runner 上瞎跑) - env: - ADV_TARGET: ${{ steps.target.outputs.target }} - run: | - set -eu - # 输入只经 env 进 shell(zizmor template-injection 面收敛) - [[ "$ADV_TARGET" != /* && "$ADV_TARGET" != *..* ]] \ - || { echo "::error::target 须为仓内相对路径且不含 .."; exit 2; } - [[ -f "$ADV_TARGET/spec.md" && -d "$ADV_TARGET/suite" && -f "$ADV_TARGET/run-suite.sh" ]] \ - || { echo "::error::目标目录契约不满足(spec.md + suite/ + run-suite.sh):$ADV_TARGET"; exit 2; } - ls -1 "$ADV_TARGET"/suite | head - - - name: 攻击(judge-deep 锁定 + 计量 wrapper,ADR-0067/0062) - env: - LLM_API_KEY: ${{ secrets.LLM_API_KEY }} - LLM_ENDPOINT: ${{ vars.LLM_ENDPOINT }} - LLM_MODEL: ${{ vars.LLM_MODEL }} - ADV_TARGET: ${{ steps.target.outputs.target }} - ADV_REPLAY: ${{ steps.target.outputs.replay }} - run: | - args=(--target "$ADV_TARGET" --report-out "$RUNNER_TEMP/adversary-report.json") - if [[ -n "$ADV_REPLAY" ]]; then - args+=(--replay-file "$ADV_REPLAY") - else - [[ -n "${LLM_API_KEY:-}" ]] || { echo "::error::LLM_API_KEY 缺失且未给 replay_file(fail-closed)"; exit 2; } - fi - # 退出码语义:0=套件通过考验 | 1=套件不充分(blocking,本 run 红) - # | 3=adversary 白卷(恒绿防御 infra)| 2/4=环境/provider 失败 - bash pipeline/adversary/run-adversary.sh "${args[@]}" - - name: 报告 schema 校验(白卷即失败,AC-15) - if: always() - run: | - set -euo pipefail - f="$RUNNER_TEMP/adversary-report.json" - if [[ ! -f "$f" ]]; then - echo "::error::未产出 adversary 报告(白卷)" - exit 3 - fi - python3 pipeline/adversary/run.py --mode validate --report "$f" - - - name: 判定报告入 run 摘要(无论成败都留痕) - if: always() - run: | - f="$RUNNER_TEMP/adversary-report.json" - if [[ -f "$f" ]]; then - { - echo "## 恶意合规 adversary 报告(ADR-0067)" - echo '```json' - cat "$f" - echo '```' - } >>"$GITHUB_STEP_SUMMARY" - else - echo "## 恶意合规 adversary 报告:未产出(前置步骤失败)" >>"$GITHUB_STEP_SUMMARY" - fi - - name: 计量账本验链 + 同步(metering-ledger 分支,BUDGET-01 数据源) - if: always() - env: - GH_TOKEN: ${{ github.token }} - run: | - if ! ls .metering/records-*.jsonl >/dev/null 2>&1; then - echo "无账本周片(调用未发生或预检即红)——跳过同步" - exit 0 - fi - bash pipeline/metering/metering-verify.sh --dir .metering - bash pipeline/metering/ledger-sync.sh --dir .metering --branch metering-ledger From 18ab927b34f43f7547ac73e3119c52978f8418c7 Mon Sep 17 00:00:00 2001 From: cloudbrid-agent Date: Sun, 23 Aug 2026 20:30:41 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix(g060-guard):=20=E4=BF=AE=E5=A4=8D=20ziz?= =?UTF-8?q?mor=20template-injection=20=E2=80=94=E2=80=94=20inputs.issue=20?= =?UTF-8?q?=E7=BB=8F=20env=20=E8=BF=9B=20shell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/g060-guard.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/g060-guard.yml b/.github/workflows/g060-guard.yml index e7f1d4b..ad89ee1 100644 --- a/.github/workflows/g060-guard.yml +++ b/.github/workflows/g060-guard.yml @@ -66,9 +66,10 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} + G060_ISSUE: ${{ inputs.issue }} run: | - if [[ -n "${{ inputs.issue }}" ]]; then - python3 scripts/g060-escalation.py --issue "${{ inputs.issue }}" + if [[ -n "$G060_ISSUE" ]]; then + python3 scripts/g060-escalation.py --issue "$G060_ISSUE" else python3 scripts/g060-escalation.py --all fi From c2cfd70d419852a2949b7cad5f25da1601320a2a Mon Sep 17 00:00:00 2001 From: randypanding <66171646+randypanding@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:40:20 +0800 Subject: [PATCH 7/9] =?UTF-8?q?fix(conductor):=20=E7=94=A8=20step=20output?= =?UTF-8?q?=20=E6=9B=BF=E4=BB=A3=20GITHUB=5FENV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/conductor.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/conductor.yml b/.github/workflows/conductor.yml index b6a9d5b..4732b32 100644 --- a/.github/workflows/conductor.yml +++ b/.github/workflows/conductor.yml @@ -55,18 +55,21 @@ jobs: path: arbiter persist-credentials: false - name: 铸 App 令牌(AG-2:本仓 + arbiter 各一枚单仓作用域) + id: tokens env: CB_APP_ID: ${{ secrets.CB_APP_ID }} AGENT_APP_SECRET: ${{ secrets.AGENT_APP_SECRET }} run: | + set -euo pipefail TOKEN=$(REPO=.github CB_APP_ID="$CB_APP_ID" AGENT_APP_SECRET="$AGENT_APP_SECRET" \ bash scripts/gh-app-token.sh) - echo "APP_TOKEN=$TOKEN" >>"$GITHUB_ENV" + # 用 step output 传递(避免 GITHUB_ENV 的 zizmor github-env 告警) + echo "app_token=$TOKEN" >>"$GITHUB_OUTPUT" # 第二枚(ADR-0055):REPO=arbiter 单仓作用域(租约宿主仓,installation # #154584760)——adjudicate.sh 优先取 env 令牌、免二次铸币 ATOKEN=$(REPO=arbiter CB_APP_ID="$CB_APP_ID" AGENT_APP_SECRET="$AGENT_APP_SECRET" \ bash scripts/gh-app-token.sh) - echo "ARBITER_TOKEN=$ATOKEN" >>"$GITHUB_ENV" + echo "arbiter_token=$ATOKEN" >>"$GITHUB_OUTPUT" # 事件路由与守卫:transitions.yaml 是唯一转移定义;guard 受限求值 # (变量白名单注入、无内建);非授权=静默丢弃(回退标签、不评论、审计进 # run 日志——AC-11)。注释/标签正文绝不进入任何求值(命令白名单精确匹配)。 @@ -75,8 +78,8 @@ jobs: - name: route(INV-02/09/AC-12) id: route env: - APP_TOKEN: ${{ env.APP_TOKEN }} - ARBITER_TOKEN: ${{ env.ARBITER_TOKEN }} + APP_TOKEN: ${{ steps.tokens.outputs.app_token }} + ARBITER_TOKEN: ${{ steps.tokens.outputs.arbiter_token }} GOV_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} EVENT_NAME: ${{ github.event_name }} ACTION: ${{ github.event.action }} From ee018a9147d96ac8514f97c6f375c0bd754e83b5 Mon Sep 17 00:00:00 2001 From: randypanding <66171646+randypanding@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:43:36 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix(conductor):=20workflow=5Frun=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20branches=20=E8=BF=87=E6=BB=A4=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/conductor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/conductor.yml b/.github/workflows/conductor.yml index 4732b32..d937283 100644 --- a/.github/workflows/conductor.yml +++ b/.github/workflows/conductor.yml @@ -20,6 +20,7 @@ on: workflow_run: workflows: [adversary] types: [completed] + branches: [main] # INV-09:每 issue 一个 concurrency group、cancel-in-progress=false——重复投递 # 排队串行而非并发竞态;幂等由 from_state 匹配承担(重复事件=当前态已变=no-op)。 From 210a06be6addb4441e07e0dd91a54617e34e4461 Mon Sep 17 00:00:00 2001 From: randypanding <66171646+randypanding@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:46:31 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix(conductor):=20=E7=A7=BB=E9=99=A4=20work?= =?UTF-8?q?flow=5Frun=20=E8=A7=A6=E5=8F=91=E5=99=A8=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20repository=5Fdispatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/conductor.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/conductor.yml b/.github/workflows/conductor.yml index d937283..d08ac83 100644 --- a/.github/workflows/conductor.yml +++ b/.github/workflows/conductor.yml @@ -16,11 +16,7 @@ on: # W4-C1:跨仓触发面——adversary 完成 survived 后经 repository_dispatch 通知 conductor repository_dispatch: types: [conductor] - # W4-C1:adversary workflow_run 完成后触发 T6 评估 - workflow_run: - workflows: [adversary] - types: [completed] - branches: [main] + # W4-C1:adversary 完成后经 repository_dispatch 触发 T6 评估(zizmor: workflow_run 不安全) # INV-09:每 issue 一个 concurrency group、cancel-in-progress=false——重复投递 # 排队串行而非并发竞态;幂等由 from_state 匹配承担(重复事件=当前态已变=no-op)。