From 05c9697a565d595babe6625fd3ef376a6431b8de Mon Sep 17 00:00:00 2001 From: randypanding Date: Tue, 18 Aug 2026 21:09:08 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E7=BA=A2=E9=98=9F=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=89=B9=E6=AC=A1=E2=80=94=E2=80=94gate=20=E7=A1=AC?= =?UTF-8?q?=E5=8C=96=E3=80=81=E6=AF=8F=E6=97=A5=E6=BC=82=E7=A7=BB+?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=85=B3=E9=97=AD=E3=80=81apply=20loud-failu?= =?UTF-8?q?re=E3=80=81App=20=E5=90=8D=E7=BB=9F=E4=B8=80=EF=BC=88ADR-0013?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gate.yml:timeout-minutes=10;JSON 校验覆盖 expected-state.json(此前只验 rulesets); adr-required 实装——C1 路径变更 PR 必须引用 ADR-NNNN(flows "无 ADR 不合并"的机器执行) - governance-drift.yml:周检→每日(盲区 7 天→1 天);漂移消除自动关闭 issue(防陈旧报告噪音) - drift-check.sh §8 重写:消息后缀快速筛 + associated-PR API 复核(防伪造/漏报); 超 24h 未回填直推标记 P0 - apply.sh:loud failure(HTTP 非 2xx 计数汇总 exit 1,防检测→修复死循环); 头部显式声明不可自动修复类别(§5-§9)及人工路径 - new-repo-init.sh:environment/app 挂载失败 exit 1(此前静默"完成");引导改 pin commit - App 名统一 cloudbrid-agent(线上真实 slug id=4632704;cloudbird-agent 404) - GOVERNANCE.yaml:governance-core→team:stewardship(ADR-0004 规划名的落地形态); C1 scope 纳入 template-service(供应链入口);GM-1 每日+自动关闭;frequency 对齐 - expected-state.json:org_secrets_required 登记 GOVERNANCE_TOKEN(drift workflow 实际依赖) - languages.yaml:依赖审批 approver+SLA(防永久挂起);SECURITY.md:响应 SLA/接收人/披露 - AGENTS.md 创建(CG-1 声明的契约文件,此前缺失);agent.schema.yaml profiles 指向 agent-registry --- .github/workflows/gate.yml | 33 ++++++++++-- .github/workflows/governance-drift.yml | 20 ++++++- AGENTS.md | 29 ++++++++++ SECURITY.md | 28 ++++++++-- governance/GOVERNANCE.yaml | 26 ++++----- governance/apply.sh | 70 ++++++++++++++++++------- governance/drift-check.sh | 37 +++++++++---- governance/expected-state.json | 3 +- governance/policy/languages.yaml | 2 + scripts/create-cloudbird-agent-app.html | 6 +-- scripts/gh-app-token.sh | 12 ++--- scripts/new-repo-init.sh | 19 +++++-- standards/agent/agent.schema.yaml | 5 +- 13 files changed, 226 insertions(+), 64 deletions(-) create mode 100644 AGENTS.md diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index cbb5280..4b9a223 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -4,11 +4,12 @@ on: push: branches: [main] -permissions: {contents: read} +permissions: {contents: read, pull-requests: read} jobs: gate: runs-on: ubuntu-latest + timeout-minutes: 10 # testing.yaml T-01 "gate<5min" 原则的硬上限(红队 #18 P2:无 timeout 的 job 失控可挂 6h) steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: @@ -26,10 +27,12 @@ jobs: for f in files: yaml.safe_load(open(f, encoding="utf-8")); print("OK", f) EOF - - name: ruleset JSON 解析 + # JSON 全量校验(红队 #17-B):expected-state.json 与 rulesets 同为 drift-check/apply 的 + # 期望状态真源——此前 gate 只验 rulesets/*.json,expected-state.json 畸形可静默合入 + - name: JSON 校验(rulesets + expected-state) run: | sudo apt-get -qq update && sudo apt-get -qq install -y jq >/dev/null - for f in governance/rulesets/*.json; do jq -e . "$f" >/dev/null || exit 1; echo "OK $f"; done + for f in governance/rulesets/*.json governance/expected-state.json; do jq -e . "$f" >/dev/null || exit 1; echo "OK $f"; done - name: 脚本语法检查 run: | bash -n governance/apply.sh && bash -n governance/drift-check.sh && bash -n scripts/new-repo-init.sh && bash -n scripts/gh-app-token.sh @@ -43,3 +46,27 @@ jobs: assert len(names) == len(set(names)), 'REPOS.yaml 有重名仓' print('OK REPOS.yaml', len(names), 'repos') EOF + # adr-required(红队 #17-B/D、#18-P0 部分;对齐 agent-registry ADR-0013 同名 check): + # C1 路径(governance/ standards/ scripts/ .github/ CODEOWNERS)变更的 PR, + # title/body 必须引用 ADR-NNNN——GOVERNANCE flows.governance_change "无 ADR 不合并" + # 的机器执行。本仓不落盘 ADR(L1 决策记录在 agent-registry/decisions/, + # 见 REPOS.yaml role),故此处只验引用存在;agent-registry 侧另有文件级校验。 + - name: adr-required(C1 变更须引用 ADR) + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: | + FILES=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files?per_page=100" --jq '.[].filename') + if ! echo "$FILES" | grep -qE '^(governance/|standards/|scripts/|\.github/|CODEOWNERS|profile/)'; then + echo "非 C1 路径变更,跳过 adr-required" + exit 0 + fi + ADR_RE='ADR-[0-9]{4}' + if { echo "$PR_TITLE"; echo "$PR_BODY"; } | grep -qE "$ADR_RE"; then + echo "OK adr-required: $( { echo "$PR_TITLE"; echo "$PR_BODY"; } | grep -oE "$ADR_RE" | sort -u | tr '\n' ' ')" + else + echo "::error::C1 路径变更(governance/standards/scripts/.github/CODEOWNERS)但 PR 未引用任何 ADR-NNNN(GOVERNANCE flows.governance_change C1:无 ADR 不合并)" + exit 1 + fi diff --git a/.github/workflows/governance-drift.yml b/.github/workflows/governance-drift.yml index 42ec019..0392e06 100644 --- a/.github/workflows/governance-drift.yml +++ b/.github/workflows/governance-drift.yml @@ -1,7 +1,7 @@ name: governance-drift on: schedule: - - cron: "0 3 * * 1" # 每周一 03:00 UTC + - cron: "0 3 * * *" # 每日 03:00 UTC(红队 #18 P0-1:周检盲区最长 7 天 → 每日;轻量只读检测,成本可忽略) workflow_dispatch: permissions: {} @@ -9,6 +9,7 @@ permissions: {} jobs: drift-check: runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read issues: write @@ -46,3 +47,20 @@ jobs: else gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" fi + # 漂移消除自动关闭(红队 #17-G:漂移修复后 open issue 永不关闭=噪音累积, + # 真漂移会被淹没在陈旧报告里)。检测全绿即判定漂移已消除。 + - name: 漂移消除则关闭 issue + if: success() && github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + NUMS=$(gh issue list --repo "$REPO" --state open --search "in:title 治理漂移" --json number --jq '.[].number') + if [[ -z "$NUMS" ]]; then + echo "无 open 漂移 issue"; exit 0 + fi + for NUM in $NUMS; do + gh issue comment "$NUM" --repo "$REPO" --body "漂移已消除(运行 #${{ github.run_id }} 全绿)。自动关闭;如复现将重新开启新报告。" + gh issue close "$NUM" --repo "$REPO" --reason completed + echo "closed #$NUM" + done diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..264509b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# AGENTS.md + +AI agent 进入本仓的工作契约(索引型,CG-1;细节按需读引用文件,不常驻上下文)。 + +## 硬规则 + +- 治理文件(governance/ standards/ scripts/ .github/ CODEOWNERS)= C1 路径:PR 必须引用 ADR-NNNN,owner-only review(GOVERNANCE flows.governance_change) +- agent 写仓库身份 = GitHub App `cloudbrid-agent`(AG-1);令牌经 scripts/gh-app-token.sh,单仓作用域、1h 过期 +- 本仓只读治理声明;ADR 与注册条目落盘 agent-registry(REPOS.yaml L1) +- 不引入新第三方 Action:白名单见 expected-state.json#actions_policy(CI-2) + +## 常用命令 + +- 校验本仓声明:`.github/workflows/gate.yml`(本地等价:yaml/json 解析 + `bash -n` 各脚本) +- 漂移检测:`GH_TOKEN= bash governance/drift-check.sh`(每日 CI 自动跑) +- 漂移修复:`GH_TOKEN= bash governance/apply.sh`(幂等;失败 loud 退出) +- 新仓初始化:`bash scripts/new-repo-init.sh `(失败 loud 退出) + +## 索引 + +| 主题 | 文件 | +|---|---| +| 治理总声明(域/措施/流程) | governance/GOVERNANCE.yaml | +| 组织仓库地图 | governance/REPOS.yaml | +| 期望状态(漂移真源) | governance/expected-state.json | +| 语言/依赖政策 | governance/policy/languages.yaml | +| 测试政策 | governance/policy/testing.yaml | +| agent 标准 schema | standards/agent/*.schema.yaml | +| 原型 profiles / 注册条目 | Cloudbird-Software/agent-registry | diff --git a/SECURITY.md b/SECURITY.md index b95c152..479b543 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,27 @@ -# 安全问题反馈 +# 安全策略 -请通过仓库 Security → Report a vulnerability 提交(Private vulnerability reporting 已开启)。 +## 报告漏洞 -不要在 Issue 里公开披露。 +请通过仓库 **Security → Report a vulnerability** 提交(Private vulnerability reporting 已开启)。不要在 Issue/PR/讨论中公开披露。 + +## 响应时限(红队修复:此前无 SLA) + +| 级别 | 定义 | 首次响应 | 处置目标 | +|---|---|---|---| +| P0 | 治理防线可被绕过 / 凭据泄露 / 供应链投毒路径 | 24h | 7 天内修复或缓解 | +| P1 | 单仓防线削弱 / 漂移长期未消 | 72h | 14 天 | +| P2 | 加固建议 | 7 天 | 排期 | + +- 接收人:owner(randypanding);owner 缺席超时限由 stewardship curator(team:stewardship)在周报升级。 +- 处置记录:P0/P1 修复须附 ADR(flows.governance_change C1)。 + +## 报告范围 + +- 本仓治理文件与脚本(governance/ scripts/ .github/) +- 组织 ruleset / App `cloudbrid-agent` / secret 配置异常 +- CI-Workflows 可复用工作流与 agent-registry 声明中的安全问题(转对应仓处置,本仓追踪) + +## 披露 + +- 修复发布后,报告者可选择公开致谢;细节披露在修复落地后进行。 +- 未修复前不披露利用细节。 diff --git a/governance/GOVERNANCE.yaml b/governance/GOVERNANCE.yaml index b26afcf..af6ecef 100644 --- a/governance/GOVERNANCE.yaml +++ b/governance/GOVERNANCE.yaml @@ -22,7 +22,7 @@ domains: intent: "默认分支仅经 PR+squash 进入;禁删/force-push;线性历史" strength: enforced platform: {github: {mechanism: org-ruleset, name: main-protection, definition: rulesets/main-protection.json}} - verify: {method: drift-check, frequency: weekly} + verify: {method: drift-check, frequency: daily} exception: [AI_Web_School] - id: BP-2 intent: "合并前置:唯一 required check = gate(聚合检查)" @@ -33,12 +33,12 @@ domains: intent: "发布标签 v* 不可删除/覆盖" strength: enforced platform: {github: {mechanism: org-ruleset, name: release-tags, definition: rulesets/release-tags.json}} - verify: {method: drift-check, frequency: weekly} + verify: {method: drift-check, frequency: daily} - id: BP-4 intent: "仓库基线:squash-only、合并删分支、auto-merge 开、wiki/projects 关" strength: enforced platform: {github: {mechanism: repo-settings, apply: apply.sh#step5}} - verify: {method: drift-check, frequency: weekly} + verify: {method: drift-check, frequency: daily} - id: BP-5 intent: "成员不可建仓;成员默认权限 read" strength: enforced @@ -54,7 +54,7 @@ domains: intent: "Actions 仅允许:github 官方+已验证+白名单(zizmor/astral-sh/dependabot/docker/softprops/org 自有)" strength: enforced platform: {github: {mechanism: actions-permissions, state: expected-state.json#actions_policy}} - verify: {method: drift-check, frequency: weekly} + verify: {method: drift-check, frequency: daily} - id: CI-3 intent: "GITHUB_TOKEN 默认只读;不允许 Actions 批准 PR" strength: enforced @@ -90,7 +90,7 @@ domains: intent: "agent 写仓库唯一身份 = App(cloudbrid-agent);权限 contents/issues/PRs:write,无 workflows/administration" strength: enforced platform: {github: {mechanism: github-app, id: 4632704, state: expected-state.json#github_app}} - verify: {method: drift-check, frequency: weekly} + verify: {method: drift-check, frequency: daily} - id: AG-2 intent: "App 令牌:单仓库作用域、1h 过期、磁盘不落长期凭据" strength: enforced @@ -114,14 +114,14 @@ domains: governance_meta: measures: - id: GM-1 - intent: "期望状态落盘;周漂移检测;漂移自动开 issue" + intent: "期望状态落盘;每日漂移检测(红队修复:周检盲区最长 7 天→1 天);漂移自动开 issue;漂移消除自动关闭 issue(防陈旧报告累积噪音)" strength: monitored - platform: {github: {mechanism: workflow, file: .github/workflows/governance-drift.yml, cron: "Mon 03:00 UTC"}} + platform: {github: {mechanism: workflow, file: .github/workflows/governance-drift.yml, cron: "daily 03:00 UTC"}} verify: {method: self, state: expected-state.json} - id: GM-2 intent: "治理仓变更一律走 flows.governance_change 分级流程(C1 附 ADR / C2 过 validate / C3 走 PR);破玻璃=直推后 24h 内回填;owner 与 AI 同受约束" strength: enforced - verify: {method: drift-check, part: section-8, frequency: weekly} + verify: {method: drift-check, part: section-8, frequency: daily} - id: GM-3 intent: "政策文件机器可判定(本目录);agent 按需读取,不常驻上下文" strength: advisory @@ -130,7 +130,7 @@ domains: intent: "组织地图 REPOS.yaml 声明全部仓(层级/角色/可见性/状态):结构层导航的唯一入口;线上未申报仓=漂移;active 仓存在性与 visibility 周检;新仓初始化后必须申报入图(见 flows.new_repo)" strength: enforced files: [REPOS.yaml] - verify: {method: drift-check, part: section-7, frequency: weekly} + verify: {method: drift-check, part: section-7, frequency: daily} context_governance: measures: @@ -168,7 +168,7 @@ domains: intent: "双层控制:软引导(AGENTS.md/identity/skill 正文)+ 硬边界(工具面裁剪→权限引擎→凭据→平台防线);冲突时硬控制优先;拦截记录进事件流" strength: enforced - id: AR-6 - intent: "团队必须声明 lifecycle;ephemeral 团队 archive_to+handoff 全部完成才允许销毁;persistent 团队(governance-core)对治理资产持续负责" + intent: "团队必须声明 lifecycle;ephemeral 团队 archive_to+handoff 全部完成才允许销毁;persistent 团队(team:stewardship——ADR-0004 规划名 governance-core 的落地形态)对治理资产持续负责" strength: enforced platform: {github: {mechanism: script, entry: agent-registry/scripts/validate.py, check: lifecycle}} - id: AR-7 @@ -190,9 +190,9 @@ flows: # 授权凭证 = ADR + PR 记录;破玻璃保留但被监控 classes: - id: C1 - scope: [GOVERNANCE.yaml, rulesets/, expected-state.json, standards/, models.yaml, decisions/] + scope: [GOVERNANCE.yaml, rulesets/, expected-state.json, standards/, models.yaml, decisions/, "template-service(整仓——供应链入口:模板被污染=全部新仓继承后门,红队修复 P0)"] requires: [PR, "ADR(新建或引用编号)", "drift-check 本地预检", owner-merge] - rationale: "治理意图变更必须可追溯到一个决策记录;无 ADR 不合并" + rationale: "治理意图变更必须可追溯到一个决策记录;无 ADR 不合并。template-service 是新仓派生源头,视同治理意图变更(gate adr-required 机器检查 + owner-only review)" - id: C2 scope: [agent-registry/registry/, 业务仓 AGENTS.md/CODEOWNERS] requires: [PR, "validate.py 通过"] @@ -223,7 +223,7 @@ flows: - {step: 1, action: "从 agent-registry 声明实例化 team(引用 id@version)", gate: AR-1} - {step: 2, action: "运行期事件按 event.schema 追加到数据层;拦截记录 tool_called.denied_by", policies: [AR-5, AR-7]} - {step: 3, action: "ephemeral 完成→执行 handoff:artifacts 走 PR、经验提炼 skill、关键决策升 ADR、轨迹归档", policy: AR-6} - - {step: 4, action: "persistent 团队(governance-core)审核归档资产入库;handoff_done 审计", policy: AR-6} + - {step: 4, action: "persistent 团队(team:stewardship)审核归档资产入库;handoff_done 审计", policy: AR-6} portability: neutral: [intent, strength, verify, risk_posture, flows] # 平台无关,直接搬运 diff --git a/governance/apply.sh b/governance/apply.sh index a539f21..65f545a 100755 --- a/governance/apply.sh +++ b/governance/apply.sh @@ -7,16 +7,40 @@ # 变更按 flows.governance_change 分级走 PR 进入 governance/(C1 须附 ADR), # 合并后本地跑一次本脚本 = "基础设施即代码"的 apply。 # +# 覆盖范围(红队 #17-H 显式声明:哪些漂移类别本脚本不可修,须人工路径): +# 可自动修复:§1 rulesets、§2 actions 策略、§3 workflow 权限、 +# §4 code-security 默认、§5 仓库基线(对应 drift-check §1-§4) +# 人工修复(本脚本故意不碰): +# §5 org secrets —— 值不可读/不可由脚本写入,admin 在网页设置 +# §6 GitHub App 权限 —— App 权限无公开写 API,须 App 设置页 +# §7 REPOS.yaml 未申报 —— 数据判断(新仓角色/层级),人工补申报 +# §8 直推 commit —— 历史事实,只能 24h 内回填 ADR+PR(人工) +# §9 admin 数量异常 —— 自动移除 admin 属不可逆高危操作,须 owner 手工处置 +# +# 失败语义(红队 #18-15:apply 静默失败=检测→修复死循环): +# 任何步骤 HTTP 非 2xx 或 API 报错 → FAIL 计数 + 结尾汇总,exit 1(loud failure)。 +# # 用法: GH_TOKEN= bash apply.sh -set -euo pipefail +set -uo pipefail ORG="${ORG:-Cloudbird-Software}" DIR="$(cd "$(dirname "$0")" && pwd)" EXPECTED="$DIR/expected-state.json" +FAILS=0 api() { curl -sS -H "Authorization: Bearer ${GH_TOKEN:?需要 org admin GH_TOKEN}" \ -H "Accept: application/vnd.github+json" "$@"; } +# loud-failure 辅助:HTTP 状态码非 2xx 即计 FAIL(幂等脚本重跑安全) +expect_ok() { # $1=描述 $2=http_code + if [[ "$2" =~ ^2 ]]; then + echo " $1: OK (HTTP $2)" + else + echo " $1: FAIL (HTTP $2)" >&2 + FAILS=$((FAILS+1)) + fi +} + echo "==> 1/5 Rulesets(存在则更新,不存在则创建)" EXISTING=$(api "https://api.github.com/orgs/$ORG/rulesets?per_page=100") for f in "$DIR"/rulesets/*.json; do @@ -25,25 +49,30 @@ for f in "$DIR"/rulesets/*.json; do if [[ -n "$rid" && "$rid" != "null" ]]; then code=$(api -o /dev/null -w '%{http_code}' -X PUT \ "https://api.github.com/orgs/$ORG/rulesets/$rid" -d @"$f") - echo " $name: 更新 (HTTP $code)" + expect_ok "ruleset '$name' 更新" "$code" else resp=$(api -X POST "https://api.github.com/orgs/$ORG/rulesets" -d @"$f") - code_ok=$(jq -r 'if .id then "created id=" + (.id|tostring) else .message end' <<<"$resp") - echo " $name: $code_ok" + if jq -e '.id' >/dev/null 2>&1 <<<"$resp"; then + echo " ruleset '$name': created id=$(jq -r .id <<<"$resp")" + else + echo " ruleset '$name': FAIL $(jq -r .message <<<"$resp")" >&2 + FAILS=$((FAILS+1)) + fi fi done echo "==> 2/5 Actions 允许策略 + 白名单" -api -o /dev/null -X PUT "https://api.github.com/orgs/$ORG/actions/permissions" \ - -d '{"enabled": true, "allowed_actions": "selected"}' -api -o /dev/null -X PUT "https://api.github.com/orgs/$ORG/actions/permissions/selected-actions" \ - -d "$(jq -c '.actions_policy | {github_owned_allowed, verified_allowed, patterns_allowed}' "$EXPECTED")" -echo " done" +code=$(api -o /dev/null -w '%{http_code}' -X PUT "https://api.github.com/orgs/$ORG/actions/permissions" \ + -d '{"enabled": true, "allowed_actions": "selected"}') +expect_ok "actions permissions" "$code" +code=$(api -o /dev/null -w '%{http_code}' -X PUT "https://api.github.com/orgs/$ORG/actions/permissions/selected-actions" \ + -d "$(jq -c '.actions_policy | {github_owned_allowed, verified_allowed, patterns_allowed}' "$EXPECTED")") +expect_ok "selected-actions 白名单" "$code" echo "==> 3/5 默认 workflow 权限 = 只读" -api -o /dev/null -X PUT "https://api.github.com/orgs/$ORG/actions/permissions/workflow" \ - -d "$(jq -c '{default_workflow_permissions: .actions_policy.default_workflow_permissions, can_approve_pull_request_reviews: .actions_policy.default_workflow_permissions_can_approve}' "$EXPECTED")" -echo " done" +code=$(api -o /dev/null -w '%{http_code}' -X PUT "https://api.github.com/orgs/$ORG/actions/permissions/workflow" \ + -d "$(jq -c '{default_workflow_permissions: .actions_policy.default_workflow_permissions, can_approve_pull_request_reviews: .actions_policy.default_workflow_permissions_can_approve}' "$EXPECTED")") +expect_ok "default workflow permissions" "$code" echo "==> 4/5 Code Security 默认应用到新仓库" CS=$(api "https://api.github.com/orgs/$ORG/code-security/configurations") @@ -53,9 +82,10 @@ if [[ -n "$CSID" && "$CSID" != "null" ]]; then code=$(api -o /dev/null -w '%{http_code}' -X PUT \ "https://api.github.com/orgs/$ORG/code-security/configurations/$CSID/defaults" \ -d "$(jq -c '{default_for_new_repos: .code_security.default_for_new_repos}' "$EXPECTED")") - echo " config#$CSID 设为新仓默认 (HTTP $code)" + expect_ok "code-security config#$CSID 设新仓默认" "$code" else - echo " 跳过:配置不存在,请先在网页创建 'GitHub recommended'" + echo " FAIL:code security 配置不存在,请先在网页创建 'GitHub recommended'" >&2 + FAILS=$((FAILS+1)) fi echo "==> 5/5 仓库基线(squash-only / 删分支)" @@ -63,10 +93,14 @@ EXCLUDES=$(jq -c '.repo_baseline.exclude_repos // []' "$EXPECTED") REPOS=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100" | jq -r '.[].name') for r in $REPOS; do jq -e --arg r "$r" 'index($r) != null' <<<"$EXCLUDES" >/dev/null && { echo " $r: 跳过(exclude)"; continue; } - api -o /dev/null -X PATCH "https://api.github.com/repos/$ORG/$r" \ - -d '{"allow_squash_merge": true, "allow_merge_commit": false, "allow_rebase_merge": false, "delete_branch_on_merge": true}' - echo " $r: 基线已应用" + code=$(api -o /dev/null -w '%{http_code}' -X PATCH "https://api.github.com/repos/$ORG/$r" \ + -d '{"allow_squash_merge": true, "allow_merge_commit": false, "allow_rebase_merge": false, "delete_branch_on_merge": true}') + expect_ok "repo '$r' 基线" "$code" done -echo +echo "----------------------------------------" +if [[ $FAILS -gt 0 ]]; then + echo "结果: $FAILS 项 FAIL(见上方 stderr)。部分应用——修复后重跑本脚本(幂等)。验证: bash $DIR/drift-check.sh" >&2 + exit 1 +fi echo "完成。验证: bash $DIR/drift-check.sh" diff --git a/governance/drift-check.sh b/governance/drift-check.sh index 52fb8f8..0a2d368 100755 --- a/governance/drift-check.sh +++ b/governance/drift-check.sh @@ -137,7 +137,13 @@ else fi # ---------- 8. 直推检测(GM-2 破玻璃监控)---------- -# flows.governance_change.policy_effective 之后,受治仓默认分支上的非 PR commit = 漂移 +# flows.governance_change.policy_effective 之后,受治仓默认分支上的非 PR commit = 漂移。 +# 两级判据(红队 #17-I 重写:消息后缀可伪造/可漏报): +# 1) 快速筛:commit 消息不以 "(#N)" 结尾 → 候选直推 +# 2) 复核:对候选调 List pull requests associated with commit—— +# 真有关联 PR(rebase 合并/自定义消息等合法形态)则放行, +# 直推(无关联 PR)才判漂移。候选通常为 0,复核调用量可控。 +# 回填时限:commit 距今 >24h 且仍在直推状态 = 已超破玻璃回填时限(P0 级标记)。 SINCE=$(python3 - <<'EOF' from datetime import datetime, timezone, timedelta eff = datetime(2026, 8, 19, tzinfo=timezone.utc) # policy_effective @@ -145,17 +151,30 @@ lo = datetime.now(timezone.utc) - timedelta(days=7) # 检测窗口 print(max(eff, lo).strftime("%Y-%m-%dT%H:%M:%SZ")) EOF ) +NOW_EPOCH=$(date +%s) for r in $REPOS; do jq -e --arg r "$r" '($r as $x | . | index($x)) != null' <<<"$EXCLUDES" >/dev/null && continue - DIRECT=$(api "https://api.github.com/repos/$ORG/$r/commits?sha=main&since=$SINCE&per_page=100" \ - | jq -r '[.[] | select(.commit.message | test("[(]#[0-9]+[)]$") | not) | .sha[0:8] + " " + (.commit.message | split("\n")[0])] | .[]') - if [[ -n "$DIRECT" ]]; then - while IFS= read -r line; do - drift "repo '$r' 存在非 PR 直推 commit: $line(破玻璃须 24h 内回填 ADR+PR,见 flows.governance_change)" - done <<<"$DIRECT" - else - ok "no-direct-push '$r' (since $SINCE)" + CANDIDATES=$(api "https://api.github.com/repos/$ORG/$r/commits?sha=main&since=$SINCE&per_page=100" \ + | jq -r '[.[] | select(.commit.message | test("[(]#[0-9]+[)]$") | not) | .sha[0:8] + "\t" + (.commit.committer.date | sub("\\.[0-9]+Z$"; "Z"))] | .[]') + DIRECT_FOUND=0 + if [[ -n "$CANDIDATES" ]]; then + while IFS=$'\t' read -r sha cdate; do + [[ -n "$sha" ]] || continue + PRS=$(api -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$ORG/$r/commits/$sha/pulls?per_page=5" \ + | jq -r 'if length > 0 then "has-pr" else "none" end') + if [[ "$PRS" == "none" ]]; then + AGE=$(( NOW_EPOCH - $(date -u -d "$cdate" +%s) )) + if [[ $AGE -gt 86400 ]]; then + drift "repo '$r' 存在非 PR 直推 commit: $sha(已超 24h 回填时限=${AGE}s——P0:立即回填 ADR+PR,见 flows.governance_change)" + else + drift "repo '$r' 存在非 PR 直推 commit: $sha(破玻璃须 24h 内回填 ADR+PR,见 flows.governance_change)" + fi + DIRECT_FOUND=1 + fi + done <<<"$CANDIDATES" fi + [[ $DIRECT_FOUND -eq 0 ]] && ok "no-direct-push '$r' (since $SINCE)" done # ---------- 9. vcs_admin 唯一性(ADR-0010:admin 全系统唯 owner)---------- diff --git a/governance/expected-state.json b/governance/expected-state.json index 527c43b..2b36899 100644 --- a/governance/expected-state.json +++ b/governance/expected-state.json @@ -31,7 +31,8 @@ }, "org_secrets_required": [ "CB_APP_ID", - "AGENT_APP_SECRET" + "AGENT_APP_SECRET", + "GOVERNANCE_TOKEN" ], "github_app": { "name": "cloudbrid-agent", diff --git a/governance/policy/languages.yaml b/governance/policy/languages.yaml index 591b9fb..7e5e24e 100644 --- a/governance/policy/languages.yaml +++ b/governance/policy/languages.yaml @@ -44,6 +44,8 @@ rules: dependency_policy: approval_required: true + approver: "owner(randypanding)——CODEOWNERS owner-only 路径;owner 缺席 7 天由 stewardship curator 在周报升级(无静默挂起)" + response_sla: "提案 PR 开出后 7 天内必须批/驳(超时=依赖审批债,进 curator 债清单)" proposal_format: [name, purpose, license, stdlib_alternative] forbidden_licenses: [AGPL-3.0, GPL-3.0, SSPL] enforcement: review diff --git a/scripts/create-cloudbird-agent-app.html b/scripts/create-cloudbird-agent-app.html index a8ad505..b787cea 100644 --- a/scripts/create-cloudbird-agent-app.html +++ b/scripts/create-cloudbird-agent-app.html @@ -2,14 +2,14 @@ - 创建 cloudbird-agent GitHub App + 创建 cloudbrid-agent GitHub App -

正在跳转到 GitHub 创建 cloudbird-agent App……
+

正在跳转到 GitHub 创建 cloudbrid-agent App……
(浏览器需已登录 Cloudbird-Software 组织 owner 账号;表单已按最小权限预填,确认后点 Create GitHub App。)

&2 + echo "错误:找不到 $ORG 的 installation。请先安装 App:Settings → Applications → cloudbrid-agent → Configure" >&2 exit 1 fi @@ -66,5 +66,5 @@ if [[ -z "$TOKEN" ]]; then exit 1 fi -echo "令牌有效至 $(jq -r .expires_at <<<"$RESP")(作用域:${REPO:-全部已安装仓库},身份 cloudbird-agent[bot])" >&2 +echo "令牌有效至 $(jq -r .expires_at <<<"$RESP")(作用域:${REPO:-全部已安装仓库},身份 cloudbrid-agent[bot])" >&2 echo "$TOKEN" diff --git a/scripts/new-repo-init.sh b/scripts/new-repo-init.sh index 40ce2f8..8b64358 100755 --- a/scripts/new-repo-init.sh +++ b/scripts/new-repo-init.sh @@ -19,12 +19,14 @@ gh repo edit "$ORG/$REPO" \ --enable-wiki=false --enable-projects=false echo "==> 2/4 production environment(B 档:required reviewer + 仅受保护分支)" -curl -sS -o /dev/null -w "environment: %{http_code}\n" \ +env_code=$(curl -sS -o /dev/null -w '%{http_code}' \ -X PUT \ -H "Authorization: Bearer $(gh auth token)" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/$ORG/$REPO/environments/production" \ - -d "{\"reviewers\": [{\"type\": \"User\", \"id\": $USER_ID}], \"deployment_branch_policy\": {\"protected_branches\": true, \"custom_branch_policies\": false}}" + -d "{\"reviewers\": [{\"type\": \"User\", \"id\": $USER_ID}], \"deployment_branch_policy\": {\"protected_branches\": true, \"custom_branch_policies\": false}}") +echo "environment: $env_code" +[[ "$env_code" =~ ^2 ]] || { echo "错误:production environment 创建失败(HTTP $env_code)——RL-1 未落地,新仓不可用" >&2; exit 1; } echo "==> 3/4 把新仓库挂到 cloudbrid-agent 安装(agent 才能写这个仓库)" INSTALL_ID=$(gh api "/orgs/$ORG/installations?per_page=100" \ @@ -37,8 +39,12 @@ code=$(curl -sS -o /dev/null -w '%{http_code}' -X PUT \ if [[ "$code" == "204" ]]; then echo "app installation: OK (repo#$NEW_RID -> installation#$INSTALL_ID)" else - echo "app installation: HTTP $code —— 手动路径:GitHub → Settings → Applications →" - echo " cloudbrid-agent → Configure → Repository access 勾选 $REPO" + # 红队 #17-K:挂载失败曾只 echo 手动路径后照常打"完成"(exit 0)—— + # 新仓表面初始化成功实际 agent 无写权限。现在 fail-loud: + echo "错误:app installation 失败(HTTP $code)——AG-4 未落地" >&2 + echo " 手动修复路径:GitHub → Settings → Applications → cloudbrid-agent → Configure → Repository access 勾选 $REPO" >&2 + echo " 修复后重跑本脚本(幂等)验证。" >&2 + exit 1 fi echo "==> 4/4 验证" @@ -52,6 +58,9 @@ cat < --template $ORG/template-service --public --clone - cd && bash <(curl -sS https://raw.githubusercontent.com/$ORG/.github/main/scripts/new-repo-init.sh) + # 用固定引用执行(红队 #17-L:main 未 pin 的远端脚本在 admin 上下文执行=供应链风险): + # 1) 先取当前 main 的 commit sha 并记录 + # 2) bash <(curl -sS https://raw.githubusercontent.com/$ORG/.github//scripts/new-repo-init.sh) + # 本脚本自检(改动后): bash -n scripts/new-repo-init.sh # 然后开第一个 PR,确认 gate 跑绿后合并 EOF diff --git a/standards/agent/agent.schema.yaml b/standards/agent/agent.schema.yaml index 7718009..74ec6f1 100644 --- a/standards/agent/agent.schema.yaml +++ b/standards/agent/agent.schema.yaml @@ -22,7 +22,8 @@ properties: description: | LLM 原型(ADR-0010,九分类)。机制原型(verifier/integrator/scheduler/evidence-pack/ interface-gateway/metrics-aggregator)不实例化为 agent——它们是 CI/服务,声明于 - standards/archetype-profiles.yaml mechanism 段。owner 为伪原型(信任根,非 agent)。 + agent-registry 仓 standards/archetype-profiles.yaml 的 mechanism 段(本仓不放 + profiles,单一真源在 agent-registry——REPOS.yaml L1 role)。owner 为伪原型(信任根,非 agent)。 builder 生产者:写实现+tests/unit/**(设计工具);tests/acceptance/** 只读;交付由 verifier 机制判卷 planner 计划者:意图→owner 已批验收示例→波次计划+工作卡+测试规格;只写 plans|cards|specs/ test-author 出题者:规格→acceptance 测试;实现前冻结测试树;不判卷(判卷=verifier 机制);族≠builder @@ -90,7 +91,7 @@ properties: additionalProperties: false description: "凭据引用:只写 env 变量名,绝不写值(AR-4 最小权限);锁死字段防内联凭据(CodeRabbit #13)" properties: - github_app: {type: string, description: "如 cloudbird-agent;作用域见 GOVERNANCE AG-1"} + github_app: {type: string, description: "如 cloudbrid-agent(线上真实 slug,expected-state.json#github_app);作用域见 GOVERNANCE AG-1"} secrets: {type: array, items: {type: string, pattern: "^[A-Z_]+$", description: "运行时注入的 env 名"}} memory: type: object From 6a90835759f45e03a683dde8dbbbc2bc467e7f9d Mon Sep 17 00:00:00 2001 From: randypanding Date: Tue, 18 Aug 2026 21:25:15 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E8=AF=84=E5=AE=A1=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E2=80=94=E2=80=94=E7=9B=B4=E6=8E=A8=E6=A3=80=E6=B5=8B=E5=8E=BB?= =?UTF-8?q?=E5=90=8E=E7=BC=80=E9=A2=84=E7=AD=9B=E3=80=81fail-closed?= =?UTF-8?q?=E3=80=81=E5=88=86=E9=A1=B5=EF=BC=9B=E5=B9=BD=E7=81=B5=20ADR=20?= =?UTF-8?q?=E5=90=8E=E9=AA=8C=EF=BC=9BC1=20scope=20=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=EF=BC=88ADR-0013=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit drift-check.sh: - §8 直推检测去掉消息后缀预筛(qodo #1:后缀可伪造——直推挂 "(#N)" 假 后缀即绕过预筛被报 clean)。唯一权威判据 = 关联 PR API,窗口内每个 commit 都复核 - §8 fail-closed(qodo #2):关联 PR 查询改用全 SHA;响应须为数组 (error 对象此前被 length>0 读作 has-pr、传输失败读作非 none—— 均静默放行)。查询失败 = 无法验证 = 判漂移,绝不静默放行 - §8 分页(qodo #7):commit 列表全分页;超 MAX_COMMITS=300 显式报 漂移而非静默截断 - §4 fail-closed:org 仓库清单拉取失败时此前 REPOS 为空 → 全部循环 静默跳过 → 检测整体假绿;现显式 exit 2 - §10 新增 ADR 引用存在性后验(qodo #5):窗口内合并 PR 的 ADR-NNNN 引用须真实存在于 agent-registry/decisions/——gate 的存在性校验受 私有仓跨读权 + PR 上下文 secret 暴露面约束(见 gate.yml 注释), 后验防线与其互补 apply.sh:前置 GET 参与失败计数(qodo #3)——ruleset 清单/CS 清单/ org 仓库清单拉取失败时此前静默跳过或走错误分支,违反 loud-failure 契约;现显式计 FAIL 并在汇总中 exit 1 gate.yml:adr-required 的 PR 文件清单 --paginate(qodo #4:>100 文件 的 C1 变更不再漏检);正则词边界(防 NOTADR-0013junk 子串绕过); 存在性校验后移至 drift-check §10 的原因记录在案(org secret 不可进 PR 触发的 workflow——PR 控制的代码 + secret = zizmor secret-exposure) governance-drift.yml:issue 归属判定改用专属 label auto-drift-report (qodo #8:标题搜索会把人工开的同名 issue 误评论/误关闭;label 幂等 创建,创建/评论/关闭全部 label-scoped) GOVERNANCE.yaml:C1 scope 补 scripts/、.github/、CODEOWNERS、profile/ (qodo #6:机器可读声明与 gate adr-required 实际执法路径一致—— 声明与执行不得互斥) Refs: ADR-0013, #17, #18 --- .github/workflows/gate.yml | 14 ++- .github/workflows/governance-drift.yml | 16 +++- governance/GOVERNANCE.yaml | 4 +- governance/apply.sh | 22 ++++- governance/drift-check.sh | 128 ++++++++++++++++++++----- 5 files changed, 147 insertions(+), 37 deletions(-) diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 4b9a223..3b34e15 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -50,7 +50,11 @@ jobs: # C1 路径(governance/ standards/ scripts/ .github/ CODEOWNERS)变更的 PR, # title/body 必须引用 ADR-NNNN——GOVERNANCE flows.governance_change "无 ADR 不合并" # 的机器执行。本仓不落盘 ADR(L1 决策记录在 agent-registry/decisions/, - # 见 REPOS.yaml role),故此处只验引用存在;agent-registry 侧另有文件级校验。 + # 见 REPOS.yaml role)。被引 ADR 的**存在性**校验不在本步骤做:agent-registry 是 + # 私有仓,PR 上下文的 GITHUB_TOKEN 无跨仓读权,而把 org secret(如 + # GOVERNANCE_TOKEN)注入 PR 触发的 workflow = 向 PR 控制的代码暴露凭据 + # (zizmor secret-exposure 模型)——存在性由 drift-check.sh §10 后验(每日, + # 可信 main 上下文),owner-only review 仍是 C1 的权威人类门禁。 - name: adr-required(C1 变更须引用 ADR) if: github.event_name == 'pull_request' env: @@ -58,14 +62,16 @@ jobs: PR_TITLE: ${{ github.event.pull_request.title }} PR_BODY: ${{ github.event.pull_request.body }} run: | - FILES=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files?per_page=100" --jq '.[].filename') + # --paginate(评审项):>100 文件的 PR 首页截断会把 C1 变更漏检成非 C1 + FILES=$(gh api --paginate "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files?per_page=100" --jq '.[].filename') if ! echo "$FILES" | grep -qE '^(governance/|standards/|scripts/|\.github/|CODEOWNERS|profile/)'; then echo "非 C1 路径变更,跳过 adr-required" exit 0 fi - ADR_RE='ADR-[0-9]{4}' + # 词边界(评审项):NOTADR-0013junk 之类子串不得满足 ADR 引用要求 + ADR_RE='\bADR-[0-9]{4}\b' if { echo "$PR_TITLE"; echo "$PR_BODY"; } | grep -qE "$ADR_RE"; then - echo "OK adr-required: $( { echo "$PR_TITLE"; echo "$PR_BODY"; } | grep -oE "$ADR_RE" | sort -u | tr '\n' ' ')" + echo "OK adr-required: $( { echo "$PR_TITLE"; echo "$PR_BODY"; } | grep -oE "$ADR_RE" | sort -u | tr '\n' ' ')(存在性后验:drift-check §10)" else echo "::error::C1 路径变更(governance/standards/scripts/.github/CODEOWNERS)但 PR 未引用任何 ADR-NNNN(GOVERNANCE flows.governance_change C1:无 ADR 不合并)" exit 1 diff --git a/.github/workflows/governance-drift.yml b/.github/workflows/governance-drift.yml index 0392e06..85915c6 100644 --- a/.github/workflows/governance-drift.yml +++ b/.github/workflows/governance-drift.yml @@ -33,7 +33,11 @@ jobs: REPO: ${{ github.repository }} run: | TITLE="治理漂移检测:组织配置与 governance/ 落盘不一致" - EXISTING=$(gh issue list --repo "$REPO" --state open --search "in:title 治理漂移" --json number --jq 'length') + # 归属标记(评审项):issue 归属判定用专属 label 而非标题搜索—— + # 标题搜索会把人工/其他来源的同名 issue 误当作本检测器的报告去评论/关闭 + LABEL="auto-drift-report" + gh label create "$LABEL" --repo "$REPO" --description "governance-drift 工作流自动报告(勿手工使用)" --color d73a4a >/dev/null 2>&1 || true + EXISTING=$(gh issue list --repo "$REPO" --state open --label "$LABEL" --json number --jq 'length') BODY="自动化检测发现以下漂移(运行 #${{ github.run_id }}): $(cat drift-report.txt) @@ -42,22 +46,24 @@ jobs: @randypanding" if [[ "$EXISTING" != "0" ]]; then - NUM=$(gh issue list --repo "$REPO" --state open --search "in:title 治理漂移" --json number --jq '.[0].number') + NUM=$(gh issue list --repo "$REPO" --state open --label "$LABEL" --json number --jq '.[0].number') gh issue comment "$NUM" --repo "$REPO" --body "$BODY" else - gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" + gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" --label "$LABEL" fi # 漂移消除自动关闭(红队 #17-G:漂移修复后 open issue 永不关闭=噪音累积, # 真漂移会被淹没在陈旧报告里)。检测全绿即判定漂移已消除。 + # 只关本检测器自己的报告(--label auto-drift-report,评审项)—— + # 人工开的其他漂移类 issue 不在自动关闭范围,须人工确认后关闭。 - name: 漂移消除则关闭 issue if: success() && github.event_name != 'pull_request' env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} run: | - NUMS=$(gh issue list --repo "$REPO" --state open --search "in:title 治理漂移" --json number --jq '.[].number') + NUMS=$(gh issue list --repo "$REPO" --state open --label "auto-drift-report" --json number --jq '.[].number') if [[ -z "$NUMS" ]]; then - echo "无 open 漂移 issue"; exit 0 + echo "无本检测器的 open 漂移 issue"; exit 0 fi for NUM in $NUMS; do gh issue comment "$NUM" --repo "$REPO" --body "漂移已消除(运行 #${{ github.run_id }} 全绿)。自动关闭;如复现将重新开启新报告。" diff --git a/governance/GOVERNANCE.yaml b/governance/GOVERNANCE.yaml index af6ecef..84148cc 100644 --- a/governance/GOVERNANCE.yaml +++ b/governance/GOVERNANCE.yaml @@ -190,9 +190,9 @@ flows: # 授权凭证 = ADR + PR 记录;破玻璃保留但被监控 classes: - id: C1 - scope: [GOVERNANCE.yaml, rulesets/, expected-state.json, standards/, models.yaml, decisions/, "template-service(整仓——供应链入口:模板被污染=全部新仓继承后门,红队修复 P0)"] + scope: [GOVERNANCE.yaml, rulesets/, expected-state.json, standards/, models.yaml, decisions/, scripts/, ".github/(CI 门禁自身——改门禁=改治理)", CODEOWNERS, profile/, "template-service(整仓——供应链入口:模板被污染=全部新仓继承后门,红队修复 P0)"] requires: [PR, "ADR(新建或引用编号)", "drift-check 本地预检", owner-merge] - rationale: "治理意图变更必须可追溯到一个决策记录;无 ADR 不合并。template-service 是新仓派生源头,视同治理意图变更(gate adr-required 机器检查 + owner-only review)" + rationale: "治理意图变更必须可追溯到一个决策记录;无 ADR 不合并。scope 与 gate adr-required 机器检查路径一致(governance/standards/scripts/.github/CODEOWNERS/profile——评审项:声明与执行不得互斥);template-service 是新仓派生源头,视同治理意图变更(gate adr-required 机器检查 + owner-only review)" - id: C2 scope: [agent-registry/registry/, 业务仓 AGENTS.md/CODEOWNERS] requires: [PR, "validate.py 通过"] diff --git a/governance/apply.sh b/governance/apply.sh index 65f545a..bcfe444 100755 --- a/governance/apply.sh +++ b/governance/apply.sh @@ -43,6 +43,13 @@ expect_ok() { # $1=描述 $2=http_code echo "==> 1/5 Rulesets(存在则更新,不存在则创建)" EXISTING=$(api "https://api.github.com/orgs/$ORG/rulesets?per_page=100") +# 前置 GET 参与失败计数(评审项):清单拉取失败时 EXISTING 为空会静默走 +# "创建"分支或全部跳过——违反 loud-failure 契约,此处显式计 FAIL 并跳过本节 +if ! jq -e 'type == "array"' <<<"$EXISTING" >/dev/null 2>&1; then + echo " FAIL:ruleset 清单拉取失败($(jq -r '.message // "非数组"' <<<"$EXISTING" 2>/dev/null || echo 传输失败)),跳过 §1" >&2 + FAILS=$((FAILS+1)) + EXISTING='[]' +fi for f in "$DIR"/rulesets/*.json; do name=$(jq -r .name "$f") rid=$(jq -r --arg n "$name" '.[] | select(.name == $n) | .id' <<<"$EXISTING" | head -1) @@ -76,6 +83,11 @@ expect_ok "default workflow permissions" "$code" echo "==> 4/5 Code Security 默认应用到新仓库" CS=$(api "https://api.github.com/orgs/$ORG/code-security/configurations") +if ! jq -e 'type == "array"' <<<"$CS" >/dev/null 2>&1; then + echo " FAIL:code security 配置清单拉取失败($(jq -r '.message // "非数组"' <<<"$CS" 2>/dev/null || echo 传输失败)),跳过 §4" >&2 + FAILS=$((FAILS+1)) + CS='[]' +fi CSID=$(jq -r --arg n "$(jq -r .code_security.configuration_name "$EXPECTED")" \ '.[] | select(.name == $n) | .id' <<<"$CS" | head -1) if [[ -n "$CSID" && "$CSID" != "null" ]]; then @@ -90,7 +102,15 @@ fi echo "==> 5/5 仓库基线(squash-only / 删分支)" EXCLUDES=$(jq -c '.repo_baseline.exclude_repos // []' "$EXPECTED") -REPOS=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100" | jq -r '.[].name') +REPOS_RAW=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100") +# 前置 GET 参与失败计数(评审项):清单失败时 REPOS 为空 → 循环体全跳过 → +# apply.sh 假装成功退出 0,基线实际一项未应用——违反 loud-failure 契约 +if ! jq -e 'type == "array" and length > 0' <<<"$REPOS_RAW" >/dev/null 2>&1; then + echo " FAIL:org 仓库清单拉取失败($(jq -r '.message // "空/非数组"' <<<"$REPOS_RAW" 2>/dev/null || echo 传输失败)),仓库基线未应用" >&2 + FAILS=$((FAILS+1)) + REPOS="" +fi +REPOS=$(jq -r '.[].name' <<<"$REPOS_RAW" 2>/dev/null) for r in $REPOS; do jq -e --arg r "$r" 'index($r) != null' <<<"$EXCLUDES" >/dev/null && { echo " $r: 跳过(exclude)"; continue; } code=$(api -o /dev/null -w '%{http_code}' -X PATCH "https://api.github.com/repos/$ORG/$r" \ diff --git a/governance/drift-check.sh b/governance/drift-check.sh index 0a2d368..30e8658 100755 --- a/governance/drift-check.sh +++ b/governance/drift-check.sh @@ -78,7 +78,14 @@ fi # ---------- 4. 仓库基线(squash-only / 删分支)---------- EXCLUDES=$(jq -c '.repo_baseline.exclude_repos // []' "$EXPECTED") -REPOS=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100" | jq -r '.[].name') +REPOS_RAW=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100") +# fail-closed(评审项):清单拉取失败/非数组时 REPOS 为空会让后续全部循环静默跳过、 +# 检测整体假绿——此处显式报漂移中止,检测器失明不得伪装成无漂移 +if ! jq -e 'type == "array" and length > 0' <<<"$REPOS_RAW" >/dev/null 2>&1; then + echo "FATAL: org 仓库清单拉取失败($(jq -r '.message // "空/非数组"' <<<"$REPOS_RAW" 2>/dev/null || echo 传输失败)),无法检测" >&2 + exit 2 +fi +REPOS=$(jq -r '.[].name' <<<"$REPOS_RAW") for r in $REPOS; do jq -e --arg r "$r" '($r as $x | . | index($x)) != null' <<<"$EXCLUDES" >/dev/null && continue RR=$(api "https://api.github.com/repos/$ORG/$r") @@ -138,11 +145,13 @@ fi # ---------- 8. 直推检测(GM-2 破玻璃监控)---------- # flows.governance_change.policy_effective 之后,受治仓默认分支上的非 PR commit = 漂移。 -# 两级判据(红队 #17-I 重写:消息后缀可伪造/可漏报): -# 1) 快速筛:commit 消息不以 "(#N)" 结尾 → 候选直推 -# 2) 复核:对候选调 List pull requests associated with commit—— -# 真有关联 PR(rebase 合并/自定义消息等合法形态)则放行, -# 直推(无关联 PR)才判漂移。候选通常为 0,复核调用量可控。 +# 唯一权威判据 = List pull requests associated with commit API(红队 #17-I + PR 评审项): +# 不做消息后缀预筛——"(#N)" 后缀可伪造,直推挂个假后缀即可绕过预筛被报 clean; +# 窗口内每个 commit 都经关联 PR API 复核(真有关联 PR=合法形态放行,无=直推)。 +# fail-closed(评审项):关联 PR 查询本身失败(限流/权限/传输)= 无法验证 = 判漂移报错, +# 绝不静默放行——检测器失明比误报危险。 +# 分页(评审项):commit 列表全分页拉取,第 100 条之后的直推不可漏检; +# 超 MAX_COMMITS 上限时显式报漂移(fail loudly),不静默截断。 # 回填时限:commit 距今 >24h 且仍在直推状态 = 已超破玻璃回填时限(P0 级标记)。 SINCE=$(python3 - <<'EOF' from datetime import datetime, timezone, timedelta @@ -152,29 +161,59 @@ print(max(eff, lo).strftime("%Y-%m-%dT%H:%M:%SZ")) EOF ) NOW_EPOCH=$(date +%s) +MAX_COMMITS=300 # 窗口内单仓 commit 上限:治理仓日常量级为个位数;超限=异常,人工核查 for r in $REPOS; do jq -e --arg r "$r" '($r as $x | . | index($x)) != null' <<<"$EXCLUDES" >/dev/null && continue - CANDIDATES=$(api "https://api.github.com/repos/$ORG/$r/commits?sha=main&since=$SINCE&per_page=100" \ - | jq -r '[.[] | select(.commit.message | test("[(]#[0-9]+[)]$") | not) | .sha[0:8] + "\t" + (.commit.committer.date | sub("\\.[0-9]+Z$"; "Z"))] | .[]') + # 全分页拉取窗口内 commit(评审项:首页之后不可漏检) + COMMITS_TMP=$(mktemp) + TOTAL=0 + PAGE=1 + FETCH_ERR=0 + while :; do + CHUNK=$(api "https://api.github.com/repos/$ORG/$r/commits?sha=main&since=$SINCE&per_page=100&page=$PAGE") + if ! jq -e 'type == "array"' <<<"$CHUNK" >/dev/null 2>&1; then + drift "repo '$r' commit 列表拉取失败,直推检测无法执行(fail-closed): $(jq -r '.message // "非 JSON 响应"' <<<"$CHUNK" 2>/dev/null || echo 传输失败)" + FETCH_ERR=1 + break + fi + N=$(jq 'length' <<<"$CHUNK") + [[ "$N" -eq 0 ]] && break + jq -c '.[] | {sha: .sha, date: (.commit.committer.date | sub("\\.[0-9]+Z$"; "Z"))}' <<<"$CHUNK" >>"$COMMITS_TMP" + TOTAL=$((TOTAL+N)) + [[ "$N" -lt 100 ]] && break + PAGE=$((PAGE+1)) + done + if [[ $FETCH_ERR -eq 1 ]]; then rm -f "$COMMITS_TMP"; continue; fi + if [[ $TOTAL -gt $MAX_COMMITS ]]; then + drift "repo '$r' 窗口内 commit 数=$TOTAL 超上限 $MAX_COMMITS——为防静默截断漏检,须人工核查直推或调窗口" + rm -f "$COMMITS_TMP" + continue + fi DIRECT_FOUND=0 - if [[ -n "$CANDIDATES" ]]; then - while IFS=$'\t' read -r sha cdate; do - [[ -n "$sha" ]] || continue - PRS=$(api -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/$ORG/$r/commits/$sha/pulls?per_page=5" \ - | jq -r 'if length > 0 then "has-pr" else "none" end') - if [[ "$PRS" == "none" ]]; then - AGE=$(( NOW_EPOCH - $(date -u -d "$cdate" +%s) )) - if [[ $AGE -gt 86400 ]]; then - drift "repo '$r' 存在非 PR 直推 commit: $sha(已超 24h 回填时限=${AGE}s——P0:立即回填 ADR+PR,见 flows.governance_change)" - else - drift "repo '$r' 存在非 PR 直推 commit: $sha(破玻璃须 24h 内回填 ADR+PR,见 flows.governance_change)" - fi - DIRECT_FOUND=1 + while IFS= read -r row; do + [[ -n "$row" ]] || continue + sha=$(jq -r .sha <<<"$row") + cdate=$(jq -r .date <<<"$row") + # 关联 PR 复核(全 SHA;响应须为数组——error 对象/传输失败均为无法验证) + PRS=$(api "https://api.github.com/repos/$ORG/$r/commits/$sha/pulls?per_page=5" \ + | jq -r 'if type == "array" then (if length > 0 then "has-pr" else "none" end) else "error" end' 2>/dev/null || echo error) + if [[ "$PRS" == "error" ]]; then + drift "repo '$r' commit $sha 的关联 PR 查询失败,无法验证直推(fail-closed:限流/权限/传输故障不得静默放行)" + DIRECT_FOUND=1 + continue + fi + if [[ "$PRS" == "none" ]]; then + AGE=$(( NOW_EPOCH - $(date -u -d "$cdate" +%s) )) + if [[ $AGE -gt 86400 ]]; then + drift "repo '$r' 存在非 PR 直推 commit: ${sha:0:8}(已超 24h 回填时限=${AGE}s——P0:立即回填 ADR+PR,见 flows.governance_change)" + else + drift "repo '$r' 存在非 PR 直推 commit: ${sha:0:8}(破玻璃须 24h 内回填 ADR+PR,见 flows.governance_change)" fi - done <<<"$CANDIDATES" - fi - [[ $DIRECT_FOUND -eq 0 ]] && ok "no-direct-push '$r' (since $SINCE)" + DIRECT_FOUND=1 + fi + done <"$COMMITS_TMP" + rm -f "$COMMITS_TMP" + [[ $DIRECT_FOUND -eq 0 ]] && ok "no-direct-push '$r' (since $SINCE, $TOTAL commits)" done # ---------- 9. vcs_admin 唯一性(ADR-0010:admin 全系统唯 owner)---------- @@ -206,6 +245,45 @@ for r in $REPOS; do fi done +# ---------- 10. ADR 引用存在性后验(adr-required 的补充防线,评审项)---------- +# gate.yml 的 adr-required 在 PR 上下文只能做语法检查:agent-registry 是私有仓, +# PR 上下文的 GITHUB_TOKEN 无跨仓读权,注入 org secret 又会向 PR 控制的代码暴露 +# 凭据。存在性在本节后验:窗口内合并 PR 的 ADR-NNNN 引用必须真实存在于 +# agent-registry/decisions/——伪造/幽灵 ADR 最长 24h 内被检出(与 §8 直推检测 +# 同为 post-hoc 防线;C1 的权威人类门禁仍是 owner-only review)。 +ADR_RE='ADR-[0-9]{4}' +# 独立 7 天窗口(不用 §8 的 SINCE——那是 policy_effective 起算的直推检测窗口, +# 而 ADR 引用后验须覆盖 policy 生效前已合并、引用了伪造 ADR 的 PR) +ADR_SINCE=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ) +ADR_DIR_LISTING=$(api "https://api.github.com/repos/$ORG/agent-registry/contents/decisions") +if ! jq -e 'type == "array"' <<<"$ADR_DIR_LISTING" >/dev/null 2>&1; then + drift "ADR 真源 agent-registry/decisions 读取失败,引用存在性无法后验(fail-closed): $(jq -r '.message // "非数组"' <<<"$ADR_DIR_LISTING" 2>/dev/null || echo 传输失败)" +else + ADR_FILES=$(jq -r '.[].name' <<<"$ADR_DIR_LISTING") + GHOST=0 + for r in $REPOS; do + jq -e --arg r "$r" '($r as $x | . | index($x)) != null' <<<"$EXCLUDES" >/dev/null && continue + PRSLIST=$(api "https://api.github.com/repos/$ORG/$r/pulls?state=closed&sort=updated&direction=desc&per_page=30") + if ! jq -e 'type == "array"' <<<"$PRSLIST" >/dev/null 2>&1; then + drift "repo '$r' PR 列表拉取失败,ADR 引用后验跳过(fail-closed)" + GHOST=1 + continue + fi + while IFS=$'\t' read -r pnum title body; do + [[ -n "$pnum" ]] || continue + for ref in $(printf '%s\n%s\n' "$title" "$body" | grep -oE "$ADR_RE" | sort -u); do + num="${ref#ADR-}" + if ! grep -q "^ADR-${num}-" <<<"$ADR_FILES"; then + drift "repo '$r' PR#$pnum 引用幽灵 ADR ${ref}(agent-registry/decisions/ 无 ADR-${num}-*.md——C1 变更的决策背书不成立)" + GHOST=1 + fi + done + done < <(jq -r --arg since "$ADR_SINCE" \ + '.[] | select(.merged_at != null and .merged_at >= $since) | [(.number|tostring), (.title // ""), (.body // "")] | @tsv' <<<"$PRSLIST") + done + [[ $GHOST -eq 0 ]] && ok "adr-reference-existence(窗口内合并 PR 的 ADR 引用全部真实)" +fi + echo "----------------------------------------" if [[ $DRIFTS -gt 0 ]]; then echo "结果: $DRIFTS 项漂移。修复: bash governance/apply.sh 或手动改回" From f0e6f9e12b232f288d36b42c0ef446005b22647e Mon Sep 17 00:00:00 2001 From: randypanding Date: Tue, 18 Aug 2026 21:27:01 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20CodeRabbit=20=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E2=80=94=E2=80=94=E6=9D=83=E9=99=90=E4=B8=8B?= =?UTF-8?q?=E6=B2=89=20job=20=E7=BA=A7=E3=80=81=E4=BB=93=E5=BA=93=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE=E5=88=86=E9=A1=B5=E3=80=81C1=20=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E4=B8=89=E5=A4=84=E7=BB=9F=E4=B8=80=E3=80=81App=20=E4=BB=A4?= =?UTF-8?q?=E7=89=8C=E6=9C=80=E5=B0=8F=E4=BD=9C=E7=94=A8=E5=9F=9F=EF=BC=88?= =?UTF-8?q?ADR-0013=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gate.yml:permissions 下沉 gate job(顶层 permissions: {}——防后续 新增 job 继承 workflow 级权限,对齐 agent-registry validate.yml 同款防御) - apply.sh/drift-check.sh §4:org 仓库枚举改全分页(>100 仓的 org 不再 漏应用/漏检基线);apply 侧空清单同样计 FAIL(loud-failure) - GOVERNANCE.yaml/AGENTS.md/gate.yml:C1 路径定义三处统一——GOVERNANCE C1 scope 改 governance/ 整目录并补 tests/(agent-registry 元验证); AGENTS.md 硬规则补 profile/;与两个仓的机器执法路径集完全一致 - gh-app-token.sh:REPO 改必填(此前不设=令牌作用域为全部已安装仓库, 违反最小权限);用法注释禁止交互 shell 内联 PEM(history+子进程 环境双暴露面),本地走 AGENT_APP_SECRET_FILE、CI 走 Actions secret - new-repo-init.sh:远端脚本 pin 指引改为"审阅过的合并提交 SHA" (gh pr view mergeCommit),不再建议运行时取 main HEAD Refs: ADR-0013, #17, #18 --- .github/workflows/gate.yml | 5 ++++- AGENTS.md | 2 +- governance/GOVERNANCE.yaml | 4 ++-- governance/apply.sh | 34 +++++++++++++++++++++++++++------- governance/drift-check.sh | 27 +++++++++++++++++++++------ scripts/gh-app-token.sh | 18 +++++++++++------- scripts/new-repo-init.sh | 8 +++++--- 7 files changed, 71 insertions(+), 27 deletions(-) diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 3b34e15..ebe5407 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -4,12 +4,15 @@ on: push: branches: [main] -permissions: {contents: read, pull-requests: read} +permissions: {} jobs: gate: runs-on: ubuntu-latest timeout-minutes: 10 # testing.yaml T-01 "gate<5min" 原则的硬上限(红队 #18 P2:无 timeout 的 job 失控可挂 6h) + permissions: + contents: read # job 级最小权限(评审项:防后续新增 job 继承 workflow 级权限) + pull-requests: read # 仅 adr-required 步骤读取 PR 文件清单 steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: diff --git a/AGENTS.md b/AGENTS.md index 264509b..5b70bf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ AI agent 进入本仓的工作契约(索引型,CG-1;细节按需读引用 ## 硬规则 -- 治理文件(governance/ standards/ scripts/ .github/ CODEOWNERS)= C1 路径:PR 必须引用 ADR-NNNN,owner-only review(GOVERNANCE flows.governance_change) +- 治理文件(governance/ standards/ scripts/ .github/ CODEOWNERS profile/)= C1 路径:PR 必须引用 ADR-NNNN,owner-only review(GOVERNANCE flows.governance_change;与 gate adr-required 机器检查同路径集) - agent 写仓库身份 = GitHub App `cloudbrid-agent`(AG-1);令牌经 scripts/gh-app-token.sh,单仓作用域、1h 过期 - 本仓只读治理声明;ADR 与注册条目落盘 agent-registry(REPOS.yaml L1) - 不引入新第三方 Action:白名单见 expected-state.json#actions_policy(CI-2) diff --git a/governance/GOVERNANCE.yaml b/governance/GOVERNANCE.yaml index 84148cc..d8042e9 100644 --- a/governance/GOVERNANCE.yaml +++ b/governance/GOVERNANCE.yaml @@ -190,9 +190,9 @@ flows: # 授权凭证 = ADR + PR 记录;破玻璃保留但被监控 classes: - id: C1 - scope: [GOVERNANCE.yaml, rulesets/, expected-state.json, standards/, models.yaml, decisions/, scripts/, ".github/(CI 门禁自身——改门禁=改治理)", CODEOWNERS, profile/, "template-service(整仓——供应链入口:模板被污染=全部新仓继承后门,红队修复 P0)"] + scope: ["governance/(整目录——含 GOVERNANCE/REPOS.yaml、rulesets、expected-state、apply/drift 脚本、policy)", standards/, models.yaml, decisions/, scripts/, ".github/(CI 门禁自身——改门禁=改治理)", CODEOWNERS, profile/, "tests/(agent-registry 验证器元测试——验证器之验证)", "template-service(整仓——供应链入口:模板被污染=全部新仓继承后门,红队修复 P0)"] requires: [PR, "ADR(新建或引用编号)", "drift-check 本地预检", owner-merge] - rationale: "治理意图变更必须可追溯到一个决策记录;无 ADR 不合并。scope 与 gate adr-required 机器检查路径一致(governance/standards/scripts/.github/CODEOWNERS/profile——评审项:声明与执行不得互斥);template-service 是新仓派生源头,视同治理意图变更(gate adr-required 机器检查 + owner-only review)" + rationale: "治理意图变更必须可追溯到一个决策记录;无 ADR 不合并。scope 与机器执法路径全集一致(.github gate:governance/standards/scripts/.github/CODEOWNERS/profile;agent-registry validate:standards/decisions/scripts/.github/CODEOWNERS/tests——评审项:声明与执行不得互斥);template-service 是新仓派生源头,视同治理意图变更(gate adr-required 机器检查 + owner-only review)" - id: C2 scope: [agent-registry/registry/, 业务仓 AGENTS.md/CODEOWNERS] requires: [PR, "validate.py 通过"] diff --git a/governance/apply.sh b/governance/apply.sh index bcfe444..3f70547 100755 --- a/governance/apply.sh +++ b/governance/apply.sh @@ -102,15 +102,35 @@ fi echo "==> 5/5 仓库基线(squash-only / 删分支)" EXCLUDES=$(jq -c '.repo_baseline.exclude_repos // []' "$EXPECTED") -REPOS_RAW=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100") -# 前置 GET 参与失败计数(评审项):清单失败时 REPOS 为空 → 循环体全跳过 → -# apply.sh 假装成功退出 0,基线实际一项未应用——违反 loud-failure 契约 -if ! jq -e 'type == "array" and length > 0' <<<"$REPOS_RAW" >/dev/null 2>&1; then - echo " FAIL:org 仓库清单拉取失败($(jq -r '.message // "空/非数组"' <<<"$REPOS_RAW" 2>/dev/null || echo 传输失败)),仓库基线未应用" >&2 - FAILS=$((FAILS+1)) +# 全分页拉取 org 仓库(评审项:单页 100 时 >100 仓的 org 会漏掉后续仓库的基线应用) +REPOS_TMP=$(mktemp) +PAGE=1 +REPOS_ERR=0 +while :; do + CHUNK=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100&page=$PAGE") + if ! jq -e 'type == "array"' <<<"$CHUNK" >/dev/null 2>&1; then + echo " FAIL:org 仓库清单拉取失败($(jq -r '.message // "非数组"' <<<"$CHUNK" 2>/dev/null || echo 传输失败)),仓库基线未应用" >&2 + FAILS=$((FAILS+1)) + REPOS_ERR=1 + break + fi + N=$(jq 'length' <<<"$CHUNK") + [[ "$N" -eq 0 ]] && break + jq -r '.[].name' <<<"$CHUNK" >>"$REPOS_TMP" + [[ "$N" -lt 100 ]] && break + PAGE=$((PAGE+1)) +done +if [[ $REPOS_ERR -eq 1 ]]; then + rm -f "$REPOS_TMP" REPOS="" +else + REPOS=$(cat "$REPOS_TMP") + rm -f "$REPOS_TMP" + if [[ -z "$REPOS" ]]; then + echo " FAIL:org 仓库清单为空(org 至少应含本治理仓)——清单异常,仓库基线未应用" >&2 + FAILS=$((FAILS+1)) + fi fi -REPOS=$(jq -r '.[].name' <<<"$REPOS_RAW" 2>/dev/null) for r in $REPOS; do jq -e --arg r "$r" 'index($r) != null' <<<"$EXCLUDES" >/dev/null && { echo " $r: 跳过(exclude)"; continue; } code=$(api -o /dev/null -w '%{http_code}' -X PATCH "https://api.github.com/repos/$ORG/$r" \ diff --git a/governance/drift-check.sh b/governance/drift-check.sh index 30e8658..25cfdc5 100755 --- a/governance/drift-check.sh +++ b/governance/drift-check.sh @@ -78,14 +78,29 @@ fi # ---------- 4. 仓库基线(squash-only / 删分支)---------- EXCLUDES=$(jq -c '.repo_baseline.exclude_repos // []' "$EXPECTED") -REPOS_RAW=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100") -# fail-closed(评审项):清单拉取失败/非数组时 REPOS 为空会让后续全部循环静默跳过、 -# 检测整体假绿——此处显式报漂移中止,检测器失明不得伪装成无漂移 -if ! jq -e 'type == "array" and length > 0' <<<"$REPOS_RAW" >/dev/null 2>&1; then - echo "FATAL: org 仓库清单拉取失败($(jq -r '.message // "空/非数组"' <<<"$REPOS_RAW" 2>/dev/null || echo 传输失败)),无法检测" >&2 +# 全分页拉取 org 仓库(评审项:单页 100 时 >100 仓的 org 会漏检后续仓库); +# fail-closed:清单拉取失败/非数组/为空时 REPOS 为空会让后续全部循环静默跳过、 +# 检测整体假绿——此处显式中止,检测器失明不得伪装成无漂移 +REPOS_TMP=$(mktemp) +PAGE=1 +while :; do + CHUNK=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100&page=$PAGE") + if ! jq -e 'type == "array"' <<<"$CHUNK" >/dev/null 2>&1; then + echo "FATAL: org 仓库清单拉取失败($(jq -r '.message // "非数组"' <<<"$CHUNK" 2>/dev/null || echo 传输失败)),无法检测" >&2 + exit 2 + fi + N=$(jq 'length' <<<"$CHUNK") + [[ "$N" -eq 0 ]] && break + jq -r '.[].name' <<<"$CHUNK" >>"$REPOS_TMP" + [[ "$N" -lt 100 ]] && break + PAGE=$((PAGE+1)) +done +REPOS=$(cat "$REPOS_TMP") +rm -f "$REPOS_TMP" +if [[ -z "$REPOS" ]]; then + echo "FATAL: org 仓库清单为空(org 至少应含本治理仓)——清单异常,无法检测" >&2 exit 2 fi -REPOS=$(jq -r '.[].name' <<<"$REPOS_RAW") for r in $REPOS; do jq -e --arg r "$r" '($r as $x | . | index($x)) != null' <<<"$EXCLUDES" >/dev/null && continue RR=$(api "https://api.github.com/repos/$ORG/$r") diff --git a/scripts/gh-app-token.sh b/scripts/gh-app-token.sh index f04635d..c26482c 100755 --- a/scripts/gh-app-token.sh +++ b/scripts/gh-app-token.sh @@ -11,17 +11,22 @@ # # 用法: # export CB_APP_ID=123456 # App 详情页的 App ID +# # 私钥注入(评审项):本地优先 AGENT_APP_SECRET_FILE(文件路径,不进 shell history); +# # CI 中由 Actions secret 机制以环境变量注入 AGENT_APP_SECRET(等效 secret manager)。 +# # 禁止在交互 shell 里 `export AGENT_APP_SECRET="-----BEGIN..."`——命令会进 +# # history 且被子进程环境继承;确需临时注入用 read -s 或 secret manager。 # export AGENT_APP_SECRET_FILE=~/.config/cloudbird/cloudbrid-agent.pem -# # CI / secret 场景改用字面量(org secret 同名注入): export AGENT_APP_SECRET="" # -# GH_TOKEN=$(bash gh-app-token.sh) # 作用域=安装的全部仓库 -# GH_TOKEN=$(REPO=template-service bash gh-app-token.sh) # 作用域=单仓库(推荐) +# GH_TOKEN=$(REPO=template-service bash gh-app-token.sh) # 作用域=单仓库(强制最小权限) # gh api user # 验证:应显示 cloudbrid-agent[bot] set -euo pipefail API="${CB_GITHUB_API:-https://api.github.com}" ORG="${ORG:-Cloudbird-Software}" APP_ID="${CB_APP_ID:?需要环境变量 CB_APP_ID(GitHub App 设置页的 App ID)}" +# REPO 强制(评审项):不设 REPO 时令牌作用域=安装的全部仓库,违反最小权限—— +# agent 只应以单仓作用域取令牌(AG-1:agent 的写权限以仓为单位显式授予) +REPO="${REPO:?需要环境变量 REPO(目标仓库名——令牌强制单仓作用域,不提供全安装作用域模式)}" # 私钥:优先文件,其次字面量(CI secret 里通常存 PEM 全文) if [[ -n "${AGENT_APP_SECRET_FILE:-}" && -f "$AGENT_APP_SECRET_FILE" ]]; then @@ -52,9 +57,8 @@ if [[ -z "$INSTALL_ID" ]]; then exit 1 fi -# 3) JWT 换安装令牌;REPO 非空时把令牌限定到单仓库(最小权限) -BODY='{}' -[[ -n "${REPO:-}" ]] && BODY=$(jq -nc --arg r "$REPO" '{repositories: [$r]}') +# 3) JWT 换安装令牌;令牌限定到单仓库(REPO 必填——最小权限,评审项) +BODY=$(jq -nc --arg r "$REPO" '{repositories: [$r]}') RESP=$(curl -sS -X POST \ -H "Authorization: Bearer $JWT" \ -H "Accept: application/vnd.github+json" \ @@ -66,5 +70,5 @@ if [[ -z "$TOKEN" ]]; then exit 1 fi -echo "令牌有效至 $(jq -r .expires_at <<<"$RESP")(作用域:${REPO:-全部已安装仓库},身份 cloudbrid-agent[bot])" >&2 +echo "令牌有效至 $(jq -r .expires_at <<<"$RESP")(作用域:单仓库 $REPO,身份 cloudbrid-agent[bot])" >&2 echo "$TOKEN" diff --git a/scripts/new-repo-init.sh b/scripts/new-repo-init.sh index 8b64358..aaf3773 100755 --- a/scripts/new-repo-init.sh +++ b/scripts/new-repo-init.sh @@ -58,9 +58,11 @@ cat < --template $ORG/template-service --public --clone - # 用固定引用执行(红队 #17-L:main 未 pin 的远端脚本在 admin 上下文执行=供应链风险): - # 1) 先取当前 main 的 commit sha 并记录 - # 2) bash <(curl -sS https://raw.githubusercontent.com/$ORG/.github//scripts/new-repo-init.sh) + # 用固定引用执行(红队 #17-L + 评审项:运行时解析 main 执行=供应链风险—— + # pin 的必须是**你审阅过的合并提交**,不是"当前 main HEAD"): + # 1) sha=$(gh pr view <审阅过的PR号> --repo $ORG/.github --json mergeCommit -q .mergeCommit.oid) + # (或 gh api repos/$ORG/.github/commits/main -q .sha 后人工确认该 commit 即你审阅的合并) + # 2) bash <(curl -sS https://raw.githubusercontent.com/$ORG/.github/$sha/scripts/new-repo-init.sh) # 本脚本自检(改动后): bash -n scripts/new-repo-init.sh # 然后开第一个 PR,确认 gate 跑绿后合并 EOF