diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index cbb5280..ebe5407 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -4,11 +4,15 @@ on: push: branches: [main] -permissions: {contents: 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: @@ -26,10 +30,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 +49,33 @@ 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)。被引 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: + GH_TOKEN: ${{ github.token }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: | + # --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 + # 词边界(评审项):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' ' ')(存在性后验:drift-check §10)" + 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..85915c6 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 @@ -32,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) @@ -41,8 +46,27 @@ 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 --label "auto-drift-report" --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/.trae-html-share-packages/scripts/create-cloudbird-agent-app.html.zip b/.trae-html-share-packages/scripts/create-cloudbird-agent-app.html.zip deleted file mode 100644 index df4a965..0000000 Binary files a/.trae-html-share-packages/scripts/create-cloudbird-agent-app.html.zip and /dev/null differ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5b70bf5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# AGENTS.md + +AI agent 进入本仓的工作契约(索引型,CG-1;细节按需读引用文件,不常驻上下文)。 + +## 硬规则 + +- 治理文件(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) + +## 常用命令 + +- 校验本仓声明:`.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/agent-registry b/agent-registry deleted file mode 160000 index 5ce5acd..0000000 --- a/agent-registry +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5ce5acdc37ad54a43929f08ae32640e5be5f9738 diff --git a/governance/GOVERNANCE.yaml b/governance/GOVERNANCE.yaml index b26afcf..d8042e9 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/(整目录——含 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 不合并" + 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 通过"] @@ -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..3f70547 100755 --- a/governance/apply.sh +++ b/governance/apply.sh @@ -7,66 +7,140 @@ # 变更按 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") +# 前置 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) 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") +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 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 / 删分支)" EXCLUDES=$(jq -c '.repo_baseline.exclude_repos // []' "$EXPECTED") -REPOS=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100" | jq -r '.[].name') +# 全分页拉取 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 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..25cfdc5 100755 --- a/governance/drift-check.sh +++ b/governance/drift-check.sh @@ -78,7 +78,29 @@ 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') +# 全分页拉取 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 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") @@ -137,7 +159,15 @@ else fi # ---------- 8. 直推检测(GM-2 破玻璃监控)---------- -# flows.governance_change.policy_effective 之后,受治仓默认分支上的非 PR commit = 漂移 +# flows.governance_change.policy_effective 之后,受治仓默认分支上的非 PR commit = 漂移。 +# 唯一权威判据 = 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 eff = datetime(2026, 8, 19, tzinfo=timezone.utc) # policy_effective @@ -145,17 +175,60 @@ lo = datetime.now(timezone.utc) - timedelta(days=7) # 检测窗口 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 - 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)" + # 全分页拉取窗口内 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 + 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 + 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)---------- @@ -187,6 +260,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 或手动改回" 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 -# 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:-全部已安装仓库},身份 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..aaf3773 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,11 @@ 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 的必须是**你审阅过的合并提交**,不是"当前 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 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