diff --git a/.github/workflows/adversary-gate.yml b/.github/workflows/adversary-gate.yml index 6566ca7..b0f28d6 100644 --- a/.github/workflows/adversary-gate.yml +++ b/.github/workflows/adversary-gate.yml @@ -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 @@ -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 + 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 }} @@ -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":[]}' @@ -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" diff --git a/.github/workflows/adversary-relay.yml b/.github/workflows/adversary-relay.yml new file mode 100644 index 0000000..8697f32 --- /dev/null +++ b/.github/workflows/adversary-relay.yml @@ -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; } + 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)" diff --git a/.github/workflows/g060-guard.yml b/.github/workflows/g060-guard.yml index ad89ee1..299d8b0 100644 --- a/.github/workflows/g060-guard.yml +++ b/.github/workflows/g060-guard.yml @@ -12,7 +12,7 @@ on: - 'specs/*/suite/**' schedule: # 每 6 小时巡检一次(与 butler 系列对齐) - - cron: '0 */6 * * *' + - cron: '17 */6 * * *' workflow_dispatch: inputs: issue: diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index c7ca0be..d54dd4f 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -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) diff --git a/governance/cost-check.sh b/governance/cost-check.sh index 361a892..0508459 100644 --- a/governance/cost-check.sh +++ b/governance/cost-check.sh @@ -163,8 +163,11 @@ llm_channel_account() { # 记录位于 metering-ledger 分支根(ledger-sync.sh 经 contents API 写回,路径=文件名); # 旧路径 pipeline/metering/ 下不会有 records——原 glob 必失败 INFRA(#258 根因)。 # strip-components=1 剥除 tarball 顶层 -/ 后落到提取根 = 记录文件。 + # 通配符匹配 strip 前的成员全路径:分支根文件在 tarball 内形如 + # -/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 printf 'INFRA\tmetering 账本 tar 解包失败(strip-components=1 + records-*.jsonl)\n' return 0 fi diff --git a/governance/drift-check.sh b/governance/drift-check.sh index 58052ed..db1cfb2 100644 --- a/governance/drift-check.sh +++ b/governance/drift-check.sh @@ -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") diff --git a/governance/expected-state.json b/governance/expected-state.json index aab19d2..2bb32c0 100644 --- a/governance/expected-state.json +++ b/governance/expected-state.json @@ -1,222 +1,213 @@ -{ - "org": "Cloudbird-Software", - "comment": "组织治理期望状态。drift-check.sh 据此检测漂移;apply.sh 据此幂等修复。ruleset 完整定义在 rulesets/ 目录。", - "actions_policy": { - "allowed_actions": "selected", - "github_owned_allowed": true, - "verified_allowed": true, - "patterns_allowed": [ - "zizmorcore/*", - "astral-sh/*", - "dependabot/fetch-metadata", - "softprops/action-gh-release", - "docker/*", - "Cloudbird-Software/*", - "step-security/harden-runner", - "ossf/scorecard-action" - ], - "default_workflow_permissions": "read", - "default_workflow_permissions_can_approve": false, - "enabled_repositories": "all" - }, - "code_security": { - "configuration_name": "GitHub recommended", - "default_for_new_repos": "all" - }, - "repo_baseline": { - "squash_only": true, - "delete_branch_on_merge": true, - "allow_auto_merge": true, - "exclude_repos": [ - "holdout" - ] - }, - "pr_liveness_hours": 4, - "labels": { - "comment": "W0-C2(#131/ADR-0047):治理标签全集(10 个 state:* 状态机 DECISION-04 + type:*)。apply.sh §7 幂等同步全部受管仓(沿用 §5 清单与 exclude);drift-check §16 对账存在性与形状。新增式:只增不删不改名,各仓自有业务标签不属治理面。", - "items": [ - { - "name": "type:intent", - "color": "1D76DB", - "description": "意图记录(IR)" - }, - { - "name": "type:card", - "color": "C5DEF5", - "description": "工作卡(wave-planner 产出)" - }, - { - "name": "state:ir-draft", - "color": "BFD4F2", - "description": "意图起草中(未签署)" - }, - { - "name": "state:ir-signed", - "color": "0E8A16", - "description": "意图已签署,可进入 spec 阶段" - }, - { - "name": "state:spec", - "color": "FBCA04", - "description": "spec 阶段" - }, - { - "name": "state:redteam", - "color": "D93F0B", - "description": "红队阶段(分歧度量 + 恶意合规)" - }, - { - "name": "state:wave-planned", - "color": "C2E0C6", - "description": "波次已规划,卡已开出(tasklist 挂 spec)" - }, - { - "name": "state:ready", - "color": "0E8A16", - "description": "前置已满足,可认领开工" - }, - { - "name": "state:in-progress", - "color": "FB8C04", - "description": "卡已被认领,实现进行中(BEH-08 /claim 协议)" - }, - { - "name": "state:quarantine", - "color": "B60205", - "description": "verdict 未过/风险超限,隔离待人工(/retry 重判)" - }, - { - "name": "state:needs-human", - "color": "5319E7", - "description": "升级人工:owner 被 assign,机器不再自动处理" - }, - { - "name": "state:done", - "color": "0E8A16", - "description": "卡完成(AC 验证过 + PR 合并)" - } - ] - }, - "org_secrets_required": [ - "CB_APP_ID", - "AGENT_APP_SECRET", - "GOVERNANCE_TOKEN", - "LLM_API_KEY" - ], - "github_app": { - "name": "cloudbrid-agent", - "id": 4632704, - "permissions": { - "contents": "write", - "issues": "write", - "metadata": "read", - "pull_requests": "write" - }, - "must_not_have": [ - "workflows", - "administration" - ] - }, - "verifier_app": { - "comment": "ISSUE-263 / ADR-0080:验证者 APP,仅测试/验证路径写权;holdout 挂载唯一合法身份(drift-check §18);与 cloudbrid-agent 身份分离(AG-1,ADR-0076)", - "name": "verifier-app", - "slug": "verifier-app", - "id": 4691958, - "client_id": "Iv23li3n9m7ux9lp9Mdp", - "installation_id": 155939178, - "permissions": { - "contents": "write", - "issues": "write", - "pull_requests": "write", - "metadata": "read" - }, - "must_not_have": [ - "workflows", - "administration" - ], - "single_repo_scope": true, - "token_ttl_minutes": 60, - "repositories": [ - ".github", - "CI-Workflows", - "agent-registry", - "archive", - "template-service", - "agent-tools", - "agent-platform", - "arbiter", - "Shorts_Director", - "Script_Writer", - "Use-up-Plan", - "AI_Web_School", - "mutual", - "holdout" - ] - }, - "direct_push_exemptions": { - "comment": "§8 直推检测豁免——两类,均须 ADR 背书,逐完整 SHA 登记(新直推不可能搭便车):(a) 破玻璃直推回填:GM-2 破玻璃的机器可读回填形式(ADR-0016 附录),事件定性见 ADR-0017(.github 两条:净变更仅为 Trae IDE 分享 zip 与误入 gitlink,彼时 PR 流程已运转);(b) 建仓 bootstrap 初始 commit(ADR-0021):仓库创建时序上分支/PR 尚不存在、不可能走 PR——Shorts_Director/agent-tools/Script_Writer 的 Initial commit 类(含 GitHub UI 建仓附带的 README 编辑 commit),登记为豁免而非追认破玻璃;arbiter 建仓 bootstrap 同理(W1-C2 .github#165/ADR-0054)。archive(W1-C1 .github#164/ADR-0053):bootstrap README commit 同属 (b) 类——注意 ADR-0046 后 org-required-workflows ruleset 无 bypass,空仓首推被 required workflow 拦截,豁免操作=对 org ruleset 的 archive 仓库做秒级临时 exclude 后经 contents API 建 commit 并即刻还原(全程约 5 秒,已留痕于 #164);ruleset 最终态与落盘定义一致(drift-check §1 不受影响)。", - ".github": [ - "9b056b3a925038484a8f6655ea45f81f071d5df6", - "416f5f57fd459f6ece3587f3aa20b772148ad335" - ], - "Shorts_Director": [ - "f63baf264291670aa33f4a599779dd5c84678433", - "d81fea9db47e065fd2bd5211dd1a8e3c26589f41", - "014f38986ee5338fe0d7e3ea260c9a4f289af401" - ], - "agent-tools": [ - "1050459443ce0373cffff7788c668d6f5dec0048" - ], - "Script_Writer": [ - "3446a39a0e2ec7527cab4e6da87b6e6ee0199fa7" - ], - "Use-up-Plan": [ - "ba75401f84eb915a119568c246168d8cdb8a1200", - "82132afc169a54b98869df136d1d6235124c4a96" - ], - "agent-platform": [ - "fecdb13d607b584063cadd717d2a7706327fbfa0" - ], - "mutual": [ - "f5d9b5b58d4dc30a2b0e259449862fc4a3c91f6b" - ], - "agent-registry": [ - "60bd1557481fa89d0358302e150a8f7d9381309c", - "e9424d220ded331c221b37135faa9d6e9cd1ecac" - ], - "holdout": [ - "63360c581524510ef37150dd7540cb333d283014" - ], - "arbiter": [ - "7b1a65234b1697bde11cea2ebdacd8ee0efc44c4" - ], - "archive": [ - "a32f0fcea2bc7b422ad8ad1463a8ad4a92cbeb60" - ] - }, - "merge_queue": { - "comment": "P2-7 ADR-0042:merge queue 为 repo 级 ruleset(org rulesets API 不支持 merge_queue 规则类型)。串行保守起步;扩围须修订 ADR-0042。", - "repos": [ - "agent-registry", - "template-service" - ], - "params": { - "merge_method": "squash", - "check_response_timeout_minutes": 60, - "max_entries_to_build": 5, - "min_entries_to_merge": 1, - "max_entries_to_merge": 1, - "min_entries_to_merge_wait_minutes": 0, - "grouping_strategy": "ALLGREEN" - } - }, - "org_required_workflows": { - "comment": "P3-1(#95/ADR-0046):gate 审判上移中心仓钉点。drift-check §1 对账 ruleset 文本(path/ref/repository_id 任何改动即漂移);§15 校验钉点 commit 绑定(由 tag 改为直接钉 commit SHA,ISSUE-263 W1-C5)。钉点值:CI-Workflows v1.4.2(2d368c2)——v1.4.2 修复 merge_group 下 org-adr-required 设计性 skip 配 EXPECTED_SKIP(#135,CI-Workflows PR#28)。", - "ruleset": "org-required-workflows", - "repository": "CI-Workflows", - "repository_id": 1337911551, - "path": ".github/workflows/org-gate.yml", - "ref": "2d368c29f170eab43b83f6b866f76fad3774fba9", - "ref_commit": "2d368c29f170eab43b83f6b866f76fad3774fba9" - } -} +{ + "org": "Cloudbird-Software", + "comment": "组织治理期望状态。drift-check.sh 据此检测漂移;apply.sh 据此幂等修复。ruleset 完整定义在 rulesets/ 目录。 merge_queue 已移除(2026-08-24):required workflow 不在 merge queue 分支运行,队列条目恒等缺失 check 60min 超时弹回(agent-registry#95 实测)——与 required checks 制度结构性冲突先摘除,GitHub 支持后按 ADR-0042 恢复;关联 ADR-0083。", + "actions_policy": { + "allowed_actions": "selected", + "github_owned_allowed": true, + "verified_allowed": true, + "patterns_allowed": [ + "zizmorcore/*", + "astral-sh/*", + "dependabot/fetch-metadata", + "softprops/action-gh-release", + "docker/*", + "Cloudbird-Software/*", + "step-security/harden-runner", + "ossf/scorecard-action" + ], + "default_workflow_permissions": "read", + "default_workflow_permissions_can_approve": false, + "enabled_repositories": "all" + }, + "code_security": { + "configuration_name": "GitHub recommended", + "default_for_new_repos": "all" + }, + "repo_baseline": { + "squash_only": true, + "delete_branch_on_merge": true, + "allow_auto_merge": true, + "exclude_repos": [ + "holdout" + ] + }, + "pr_liveness_hours": 4, + "labels": { + "comment": "W0-C2(#131/ADR-0047):治理标签全集(10 个 state:* 状态机 DECISION-04 + type:*)。apply.sh §7 幂等同步全部受管仓(沿用 §5 清单与 exclude);drift-check §16 对账存在性与形状。新增式:只增不删不改名,各仓自有业务标签不属治理面。", + "items": [ + { + "name": "type:intent", + "color": "1D76DB", + "description": "意图记录(IR)" + }, + { + "name": "type:card", + "color": "C5DEF5", + "description": "工作卡(wave-planner 产出)" + }, + { + "name": "state:ir-draft", + "color": "BFD4F2", + "description": "意图起草中(未签署)" + }, + { + "name": "state:ir-signed", + "color": "0E8A16", + "description": "意图已签署,可进入 spec 阶段" + }, + { + "name": "state:spec", + "color": "FBCA04", + "description": "spec 阶段" + }, + { + "name": "state:redteam", + "color": "D93F0B", + "description": "红队阶段(分歧度量 + 恶意合规)" + }, + { + "name": "state:wave-planned", + "color": "C2E0C6", + "description": "波次已规划,卡已开出(tasklist 挂 spec)" + }, + { + "name": "state:ready", + "color": "0E8A16", + "description": "前置已满足,可认领开工" + }, + { + "name": "state:in-progress", + "color": "FB8C04", + "description": "卡已被认领,实现进行中(BEH-08 /claim 协议)" + }, + { + "name": "state:quarantine", + "color": "B60205", + "description": "verdict 未过/风险超限,隔离待人工(/retry 重判)" + }, + { + "name": "state:needs-human", + "color": "5319E7", + "description": "升级人工:owner 被 assign,机器不再自动处理" + }, + { + "name": "state:done", + "color": "0E8A16", + "description": "卡完成(AC 验证过 + PR 合并)" + } + ] + }, + "org_secrets_required": [ + "CB_APP_ID", + "AGENT_APP_SECRET", + "GOVERNANCE_TOKEN", + "LLM_API_KEY" + ], + "github_app": { + "name": "cloudbrid-agent", + "id": 4632704, + "permissions": { + "contents": "write", + "issues": "write", + "metadata": "read", + "pull_requests": "write" + }, + "must_not_have": [ + "workflows", + "administration" + ] + }, + "verifier_app": { + "comment": "ISSUE-263 / ADR-0080:验证者 APP,仅测试/验证路径写权;holdout 挂载唯一合法身份(drift-check §18);与 cloudbrid-agent 身份分离(AG-1,ADR-0076)", + "name": "verifier-app", + "slug": "verifier-app", + "id": 4691958, + "client_id": "Iv23li3n9m7ux9lp9Mdp", + "installation_id": 155939178, + "permissions": { + "contents": "write", + "issues": "write", + "pull_requests": "write", + "metadata": "read" + }, + "must_not_have": [ + "workflows", + "administration" + ], + "single_repo_scope": true, + "token_ttl_minutes": 60, + "repositories": [ + ".github", + "CI-Workflows", + "agent-registry", + "archive", + "template-service", + "agent-tools", + "agent-platform", + "arbiter", + "Shorts_Director", + "Script_Writer", + "Use-up-Plan", + "AI_Web_School", + "mutual", + "holdout" + ] + }, + "direct_push_exemptions": { + "comment": "§8 直推检测豁免——两类,均须 ADR 背书,逐完整 SHA 登记(新直推不可能搭便车):(a) 破玻璃直推回填:GM-2 破玻璃的机器可读回填形式(ADR-0016 附录),事件定性见 ADR-0017(.github 两条:净变更仅为 Trae IDE 分享 zip 与误入 gitlink,彼时 PR 流程已运转);(b) 建仓 bootstrap 初始 commit(ADR-0021):仓库创建时序上分支/PR 尚不存在、不可能走 PR——Shorts_Director/agent-tools/Script_Writer 的 Initial commit 类(含 GitHub UI 建仓附带的 README 编辑 commit),登记为豁免而非追认破玻璃;arbiter 建仓 bootstrap 同理(W1-C2 .github#165/ADR-0054)。archive(W1-C1 .github#164/ADR-0053):bootstrap README commit 同属 (b) 类——注意 ADR-0046 后 org-required-workflows ruleset 无 bypass,空仓首推被 required workflow 拦截,豁免操作=对 org ruleset 的 archive 仓库做秒级临时 exclude 后经 contents API 建 commit 并即刻还原(全程约 5 秒,已留痕于 #164);ruleset 最终态与落盘定义一致(drift-check §1 不受影响)。", + ".github": [ + "9b056b3a925038484a8f6655ea45f81f071d5df6", + "416f5f57fd459f6ece3587f3aa20b772148ad335" + ], + "Shorts_Director": [ + "f63baf264291670aa33f4a599779dd5c84678433", + "d81fea9db47e065fd2bd5211dd1a8e3c26589f41", + "014f38986ee5338fe0d7e3ea260c9a4f289af401" + ], + "agent-tools": [ + "1050459443ce0373cffff7788c668d6f5dec0048" + ], + "Script_Writer": [ + "3446a39a0e2ec7527cab4e6da87b6e6ee0199fa7" + ], + "Use-up-Plan": [ + "ba75401f84eb915a119568c246168d8cdb8a1200", + "82132afc169a54b98869df136d1d6235124c4a96" + ], + "agent-platform": [ + "fecdb13d607b584063cadd717d2a7706327fbfa0" + ], + "mutual": [ + "f5d9b5b58d4dc30a2b0e259449862fc4a3c91f6b" + ], + "agent-registry": [ + "60bd1557481fa89d0358302e150a8f7d9381309c", + "e9424d220ded331c221b37135faa9d6e9cd1ecac" + ], + "holdout": [ + "63360c581524510ef37150dd7540cb333d283014" + ], + "arbiter": [ + "7b1a65234b1697bde11cea2ebdacd8ee0efc44c4" + ], + "archive": [ + "a32f0fcea2bc7b422ad8ad1463a8ad4a92cbeb60" + ] + }, + "org_required_workflows": { + "comment": "P3-1(#95/ADR-0046):gate 审判上移中心仓钉点。drift-check §1 对账 ruleset 文本。2026-08-24 平台约束修订(ADR-0083 关联):ruleset API 对 required workflows 的 ref 已只接受分支/标签名(40 位 SHA 恒 422 does not have ref——多轮实测),钉点值=main;内容完整性改由 drift-check 后验承载。同轮变更:adversary-gate.yml 纳入 required workflows(ADR-0082/0083);CI-Workflows 中心仓自身排除(org-gate job 对中心仓自跳,required 化后产生永不满足的 skipped)。", + "ruleset": "org-required-workflows", + "repository": "CI-Workflows", + "repository_id": 1337911551, + "path": ".github/workflows/org-gate.yml", + "ref": "main", + "workflows": [ + { + "path": ".github/workflows/org-gate.yml" + }, + { + "path": ".github/workflows/adversary-gate.yml" + } + ] + } +} diff --git a/governance/rulesets/main-protection.json b/governance/rulesets/main-protection.json index 6373662..6293ad2 100644 --- a/governance/rulesets/main-protection.json +++ b/governance/rulesets/main-protection.json @@ -60,8 +60,7 @@ "context": "org-gate" }, { - "context": "adversary", - "integration_id": 0 + "context": "adversary" } ] } diff --git a/governance/rulesets/org-required-workflows.json b/governance/rulesets/org-required-workflows.json index e7405d1..4d3c8cb 100644 --- a/governance/rulesets/org-required-workflows.json +++ b/governance/rulesets/org-required-workflows.json @@ -13,7 +13,9 @@ "include": [ "~ALL" ], - "exclude": [] + "exclude": [ + "CI-Workflows" + ] } }, "bypass_actors": [], @@ -25,12 +27,12 @@ "workflows": [ { "path": ".github/workflows/org-gate.yml", - "ref": "2d368c29f170eab43b83f6b866f76fad3774fba9", + "ref": "main", "repository_id": 1337911551 }, { "path": ".github/workflows/adversary-gate.yml", - "ref": "2d368c29f170eab43b83f6b866f76fad3774fba9", + "ref": "main", "repository_id": 1337911551 } ] diff --git a/governance/tests/test-issue263-w1c5.sh b/governance/tests/test-issue263-w1c5.sh index da2a6be..6b977f5 100644 --- a/governance/tests/test-issue263-w1c5.sh +++ b/governance/tests/test-issue263-w1c5.sh @@ -74,19 +74,21 @@ need("specs/" in repo_map[".github"].get("key_paths", []), need("pipeline/" in repo_map["CI-Workflows"].get("key_paths", []), "CI-Workflows key_paths 未补登 pipeline/") -# --- org-required-workflows 钉点为 commit SHA 而非 tag --- -wf = (orw.get("rules", [])[0].get("parameters", {}).get("workflows", []) + [{}])[0] -ref = wf.get("ref", "") -need(bool(re.fullmatch(r"[0-9a-f]{40}", ref)), - f"org-required-workflows.json 钉点不是 40 位 commit SHA: {ref}") -need(wf.get("path") == ".github/workflows/org-gate.yml", "org-required-workflows 工作流路径异常") +# --- org-required-workflows 钉点(2026-08-24 平台约束修订:SHA 恒被拒,钉分支名)--- +# ruleset API 对 required workflows 的 ref 只接受分支/标签(40 位 SHA 恒 422 +# "does not have ref"——多轮实测,ADR-0083 关联);供应链完整性改由 +# expected-state.content 校验与 drift-check 后验承载。钉点=main + 双工作流面。 +wf_paths = [w.get("path") for w in orw.get("rules", [])[0].get("parameters", {}).get("workflows", [])] +ref = (orw.get("rules", [])[0].get("parameters", {}).get("workflows", []) + [{}])[0].get("ref", "") +need(ref in ("main", "master"), f"org-required-workflows.json 钉点须为分支名(平台不接受 SHA): {ref}") +need(".github/workflows/org-gate.yml" in wf_paths, "org-required-workflows 缺 org-gate.yml") +need(".github/workflows/adversary-gate.yml" in wf_paths, "org-required-workflows 缺 adversary-gate.yml(ADR-0082/0083)") -# expected-state 与 ruleset 一致 +# expected-state 与 ruleset 一致(ref=分支名;ref_commit 字段随平台约束废弃) orw_exp = expected.get("org_required_workflows", {}) need(orw_exp.get("ref") == ref, f"expected-state.org_required_workflows.ref ({orw_exp.get('ref')}) 与 ruleset ({ref}) 不一致") -need(orw_exp.get("ref_commit") == ref, - f"expected-state.org_required_workflows.ref_commit 应与 ref 同值") +need("ref_commit" not in orw_exp, "ref_commit 已随 SHA 钉点废弃(平台约束)——expected-state 应删除") for e in errs: print("ERR", e) diff --git a/specs/ISSUE-263/suite/__pycache__/test_ir263_artifacts.cpython-311.pyc b/specs/ISSUE-263/suite/__pycache__/test_ir263_artifacts.cpython-311.pyc new file mode 100644 index 0000000..c99c68e Binary files /dev/null and b/specs/ISSUE-263/suite/__pycache__/test_ir263_artifacts.cpython-311.pyc differ diff --git a/specs/ISSUE-263/suite/test_ir263_artifacts.py b/specs/ISSUE-263/suite/test_ir263_artifacts.py new file mode 100644 index 0000000..8192f96 --- /dev/null +++ b/specs/ISSUE-263/suite/test_ir263_artifacts.py @@ -0,0 +1,105 @@ +"""specs/ISSUE-263 自有验收套件(T-14 第一面,ADR-0083)。 + +对 #263「卡绑定测试与红队守门制度」的治理落盘工件做结构化断言—— +IR 自身首先受 T-14 约束(此前 specs/ISSUE-263 无 suite/ 是独立验证 +发现的违规样本)。unittest 风格、零第三方依赖(yaml 断言用文本锚点, +避免 gate job 依赖面扩大)。 +""" +import json +import os +import re +import unittest + +ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +TRANSITIONS = os.path.join(ROOT, "governance", "transitions.yaml") +TESTING = os.path.join(ROOT, "governance", "policy", "testing.yaml") +GOVERNANCE = os.path.join(ROOT, "governance", "GOVERNANCE.yaml") +MAIN_PROT = os.path.join(ROOT, "governance", "rulesets", "main-protection.json") +EXPECTED = os.path.join(ROOT, "governance", "expected-state.json") +REPOS = os.path.join(ROOT, "governance", "REPOS.yaml") + + +def read(path): + with open(path, encoding="utf-8") as f: + return f.read() + + +class TestStateMachine(unittest.TestCase): + """AC-12:T5/T6 入册且语义齐全。""" + + def test_t5_t6_registered(self): + t = read(TRANSITIONS) + for tid in ("T5", "T6"): + self.assertIsNotNone( + re.search(rf"^\s*- id: {tid}\b", t, re.M), + f"transitions.yaml 缺 {tid}") + + def test_t5_suite_ready_predicate(self): + self.assertIn("suite", read(TRANSITIONS), "T5 须含 suite 就绪谓词") + + +class TestPolicyClauses(unittest.TestCase): + """W1-C5 条款入册:T-14/T-15/AR-10。""" + + def test_t14_t15_registered(self): + t = read(TESTING) + self.assertRegex(t, r"T-14,\s*name:\s*card_bound_test_required") + self.assertRegex(t, r"T-15,\s*name:\s*intent_backstop") + + def test_ar10_veto_clause(self): + g = read(GOVERNANCE) + self.assertIn("红队守门", g) + self.assertIn("needs-human", g) + self.assertIn("EXPECTED_SKIP", g) + + +class TestRulesetEnforcement(unittest.TestCase): + """W4-C3:adversary 为 required check 且无非法 integration_id(ADR-0083)。""" + + def setUp(self): + self.rs = json.loads(read(MAIN_PROT)) + + def test_adversary_required_no_integration_id(self): + checks = None + for rule in self.rs["rules"]: + if rule["type"] == "required_status_checks": + checks = rule["parameters"]["required_status_checks"] + self.assertIsNotNone(checks, "main-protection 缺 required_status_checks") + contexts = {c["context"]: c for c in checks} + self.assertIn("adversary", contexts, "adversary 不在 required checks") + for ctx, entry in contexts.items(): + self.assertNotIn( + "integration_id", entry, + f"required check '{ctx}' 携带 integration_id(非法——ADR-0083 决策 3)") + + def test_gate_and_org_gate_kept(self): + rule = next(r for r in self.rs["rules"] if r["type"] == "required_status_checks") + contexts = [c["context"] for c in rule["parameters"]["required_status_checks"]] + for c in ("gate", "org-gate"): + self.assertIn(c, contexts) + + +class TestExpectedState(unittest.TestCase): + """W2-C1:验证者 APP 登记面。""" + + def test_verifier_app_registered(self): + d = json.loads(read(EXPECTED)) + va = d.get("verifier_app") or {} + self.assertTrue(isinstance(va.get("id"), int), "verifier_app.id 须为数值") + self.assertEqual(va.get("slug"), "verifier-app") + + def test_holdout_repo_declared(self): + self.assertRegex(read(REPOS), r"name:\s*holdout\b") + + +class TestSelfSuiteValidity(unittest.TestCase): + """T-14 自反:本 suite 含真实断言(非摆拍——S1'/S2' 攻击面对象)。""" + + def test_suite_has_real_assertions(self): + src = read(os.path.abspath(__file__)) + asserts = len(re.findall(r"\bassert", src)) + self.assertGreater(asserts, 10, f"断言过少({asserts})——疑似摆拍套件") + + +if __name__ == "__main__": + unittest.main()