diff --git a/governance/REPOS.yaml b/governance/REPOS.yaml index cebb90a..ec1e8e3 100644 --- a/governance/REPOS.yaml +++ b/governance/REPOS.yaml @@ -129,7 +129,8 @@ repos: visibility: public status: active role: 试卷层——封存验收场景+golden+泄漏诱饵(宪法 §1/§4B,IR-0003 W1-C4 .github#167, - ADR-0056);owner 直管;App 不挂载(DECISION-02 隔离不变量,drift-check §18 断言) + ADR-0056/ADR-0080);owner 直管;cloudbrid-agent 严禁挂载,验证者 APP 可挂载并 + 仅用于测试/验证路径写权(DECISION-02 隔离不变量,drift-check §18 断言) key_paths: [entries/, schema/, canary/] # 上游依赖(不属于本组织;不 fork、不 submodule) diff --git a/governance/drift-check.sh b/governance/drift-check.sh index 375bbc4..a7df5cf 100644 --- a/governance/drift-check.sh +++ b/governance/drift-check.sh @@ -832,50 +832,65 @@ for r in $PROTO_REPOS; do done [[ $PROTO_OK -eq 0 ]] && ok "统一入口协议块一致(真源 template-service × $PROTO_N 个 entry_protocol 仓,逐字节比对)" -# ---------- 18. holdout 隔离断言(DECISION-02:App 安装差异隔离,W1-C4/ADR-0056)---------- -# 试卷层 holdout 的读隔离不靠保密(公开仓,ADR-0056/DECISION-02),靠两条: -# cloudbrid-agent App 不安装到该仓(agent 的组织级凭据通道物理不可达)+ 泄漏诱饵 -# 周检(.github 仓 holdout-canary-sweep,宪法 §6/§11)。本节断言第一条: -# App installation 的仓清单不得包含 holdout——在清单 = P0(隔离失效,agent 可持 -# 组织凭据读考卷)。正向对照防检测器失明假绿(§4 同思想):installation 清单必须 -# 非空且包含 .github——连 .github 都不在清单 = 端点读法错/权限变了 → 按漂移报错, +# ---------- 18. holdout 隔离断言(DECISION-02:App 安装差异隔离,W1-C4/ADR-0056,ADR-0080 修订)---------- +# 试卷层 holdout 的读隔离不靠保密(公开仓,ADR-0056/DECISION-02),靠三条: +# - cloudbrid-agent App 不安装到该仓(agent 的组织级凭据通道物理不可达); +# - 验证者 APP 可安装,但仅作为测试/验证路径写入主体(ADR-0080); +# - 泄漏诱饵周检(.github 仓 holdout-canary-sweep,宪法 §6/§11)。 +# 本节断言 App 挂载差异: +# - cloudbrid-agent installation 的仓清单含 holdout = P0 漂移; +# - 验证者 APP installation 的仓清单含 holdout = OK(合法); +# - 验证者 APP 尚未安装 → INFO/SKIP(实施阶段再断言)。 +# 正向对照防检测器失明假绿(§4 同思想):cloudbrid-agent 的 installation 清单必须 +# 非空且含 .github——连 .github 都不在清单 = 端点读法错/权限变了 → 按漂移报错, # 绝不把"看不见"当"没有"。API 任何失败 = fail-closed 报漂移。 # §17 编号预留给并行卡 W1-C3(多代理并行修改本文件,编号互不占段)。 # 端点注(2026-08-21 实测):installation id 走 /orgs//installations(admin # 端点,new-repo-init.sh §3 同款)——/user/installations 要求 GitHub App 令牌, # 经典 PAT 一律 403;仓清单走 /user/installations//repositories(经典 PAT 可读)。 HOLDOUT_REPO="holdout" + +# 拉取一次 org 安装列表,供 cloudbrid-agent 与验证者 APP 共用。 HOLDOUT_INST=$(api "https://api.github.com/orgs/$ORG/installations?per_page=100") if ! jq -e 'type == "object" and (.installations | type == "array")' <<<"$HOLDOUT_INST" >/dev/null 2>&1; then drift "holdout 隔离断言失败:org installations 拉取失败(fail-closed——检测器失明不得伪装通过,ADR-0056 §18): $(jq -r '.message // "非 JSON 响应"' <<<"$HOLDOUT_INST" 2>/dev/null || echo 传输失败)" else - # app 名取 expected-state 单一真源(与 §6 一致),不重复硬编码 + # 辅助函数:拉取指定 installation 下的全部仓名;stdout 为排序去重后的仓名列表; + # 失败时返回非 0(stdout 为空,不应使用)。 + holdout_install_repos() { + local iid="$1" tmp fail page n chunk + tmp=$(mktemp) + fail=0 + page=1 + while :; do + chunk=$(api "https://api.github.com/user/installations/$iid/repositories?per_page=100&page=$page") + if ! jq -e 'type == "object" and (.repositories | type == "array")' <<<"$chunk" >/dev/null 2>&1; then + fail=1; break + fi + n=$(jq '.repositories | length' <<<"$chunk") + [[ "$n" -eq 0 ]] && break + jq -r '.repositories[].name' <<<"$chunk" >>"$tmp" + [[ "$n" -lt 100 ]] && break + page=$((page+1)) + done + if [[ $fail -ne 0 ]]; then + rm -f "$tmp" + return 1 + fi + sort -u "$tmp" + rm -f "$tmp" + } + + # cloudbrid-agent:老 DECISION-02 主体,必须不挂载 holdout。 HOLDOUT_APP=$(jq -r .github_app.name "$EXPECTED") INST_ID=$(jq -r --arg s "$HOLDOUT_APP" '.installations[]? | select(.app_slug == $s) | .id' <<<"$HOLDOUT_INST" | head -1) if [[ -z "$INST_ID" || "$INST_ID" == "null" ]]; then drift "app '$HOLDOUT_APP' 的 installation 未找到——holdout 隔离断言无法执行(fail-closed,ADR-0056 §18)" else - # 全分页列出该 installation 下的仓(>100 仓单页漏检——§1/§4 同教训) - INST_TMP=$(mktemp) - INST_FAIL=0 - INST_PAGE=1 - while :; do - CHUNK=$(api "https://api.github.com/user/installations/$INST_ID/repositories?per_page=100&page=$INST_PAGE") - if ! jq -e 'type == "object" and (.repositories | type == "array")' <<<"$CHUNK" >/dev/null 2>&1; then - INST_FAIL=1; break - fi - INST_N=$(jq '.repositories | length' <<<"$CHUNK") - [[ "$INST_N" -eq 0 ]] && break - jq -r '.repositories[].name' <<<"$CHUNK" >>"$INST_TMP" - [[ "$INST_N" -lt 100 ]] && break - INST_PAGE=$((INST_PAGE+1)) - done - if [[ $INST_FAIL -ne 0 ]]; then + INST_REPOS=$(holdout_install_repos "$INST_ID") + if [[ $? -ne 0 ]]; then drift "installation#$INST_ID 仓清单拉取失败,holdout 隔离断言无法执行(fail-closed,ADR-0056 §18)" - rm -f "$INST_TMP" else - INST_REPOS=$(sort -u "$INST_TMP") - rm -f "$INST_TMP" HOLDOUT_HIT=0 grep -qx "$HOLDOUT_REPO" <<<"$INST_REPOS" && HOLDOUT_HIT=1 # 正向对照(防失明):清单非空且含 .github,否则断言结论不可信 @@ -887,10 +902,28 @@ else HOLDOUT_HIT=-1 fi if [[ $HOLDOUT_HIT -eq 1 ]]; then - drift "holdout 出现在 app '$HOLDOUT_APP' installation#$INST_ID 仓清单——P0:App 挂上 holdout = 试卷层隔离失效(DECISION-02/ADR-0056 §18;立即在 App 设置页移除该仓访问并追查何时挂载)" + drift "holdout 出现在 app '$HOLDOUT_APP' installation#$INST_ID 仓清单——P0:cloudbrid-agent 挂上 holdout = 试卷层隔离失效(DECISION-02/ADR-0056 §18;立即在 App 设置页移除该仓访问并追查何时挂载)" elif [[ $HOLDOUT_HIT -eq 0 ]]; then - ok "holdout 隔离成立(installation#$INST_ID 共 $(grep -c . <<<"$INST_REPOS") 仓不含 holdout;正向对照 .github 在清单)" + ok "holdout 隔离成立(cloudbrid-agent installation#$INST_ID 共 $(grep -c . <<<"$INST_REPOS") 仓不含 holdout;正向对照 .github 在清单)" + fi + fi + fi + + # 验证者 APP(ADR-0080):允许挂载 holdout,作为测试/验证路径写入主体。 + VERIFIER_APP=$(jq -r '.verifier_app.name // empty' "$EXPECTED") + if [[ -n "$VERIFIER_APP" && "$VERIFIER_APP" != "null" ]]; then + VERIFIER_INST_ID=$(jq -r --arg s "$VERIFIER_APP" '.installations[]? | select(.app_slug == $s) | .id' <<<"$HOLDOUT_INST" | head -1) + if [[ -n "$VERIFIER_INST_ID" && "$VERIFIER_INST_ID" != "null" ]]; then + VERIFIER_REPOS=$(holdout_install_repos "$VERIFIER_INST_ID") + if [[ $? -ne 0 ]]; then + drift "验证者 APP '$VERIFIER_APP' installation#$VERIFIER_INST_ID 仓清单拉取失败,无法校验 holdout 挂载合法性(fail-closed,ADR-0080 §18)" + elif grep -qx "$HOLDOUT_REPO" <<<"$VERIFIER_REPOS"; then + ok "holdout 由验证者 APP '$VERIFIER_APP' 合法挂载(installation#$VERIFIER_INST_ID,ADR-0080)" + else + ok "验证者 APP '$VERIFIER_APP' 已安装但未挂载 holdout(ADR-0080 允许,非漂移)" fi + else + echo "INFO 验证者 APP '$VERIFIER_APP' 尚未安装——holdout 挂载合法性断言跳过(实施阶段再校验,ADR-0080 §18)" fi fi fi diff --git a/governance/expected-state.json b/governance/expected-state.json index e0ff813..4c07647 100644 --- a/governance/expected-state.json +++ b/governance/expected-state.json @@ -117,6 +117,11 @@ "administration" ] }, + "verifier_app": { + "comment": "ISSUE-263 / ADR-0080:验证者 APP,仅测试/验证路径写权;holdout 挂载需经 drift-check §18 允许;id 在实施阶段填入", + "name": "verifier-app", + "id": null + }, "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": [