-
Notifications
You must be signed in to change notification settings - Fork 0
fix(conductor): sender_role 两级解析(GOV_TOKEN 成员资格 + APP_TOKEN 仓级 admin 兜底)(W1-C3 #166,ADR-0055) #213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -128,14 +128,30 @@ jobs: | |
| audit("event=unrecognized verdict=noop"); raise SystemExit(0) | ||
|
|
||
| # ---- sender_role(INV-02:API 判定,不硬编码用户名)---- | ||
| # 两级解析(W1-C3 e2e .github#206 实测教训:GOVERNANCE_TOKEN 读 | ||
| # /orgs/{org}/memberships 拿不到 200,role 曾静默落 none——旧 T3 guard | ||
| # 靠 author_association 兜底掩盖了该单点,arbiter 转介按 role 判权后 | ||
| # 把 owner 也拒了【run 32500211932 role-not-allowed】): | ||
| # ① GOV_TOKEN org 成员资格 role==admin → owner(首选,组织级语义) | ||
| # ② 失败时 APP_TOKEN 读仓级协作者权限 ==admin → owner(兜底; | ||
| # 与 drift-check §9 vcs-admin 唯一性同源——受管仓 admin 唯 owner) | ||
| # 两路全败=none(arbiter 默认拒绝)。均 API 判定,无硬编码用户名。 | ||
| actor = E.get("ACTOR") or "" | ||
| role = "none" | ||
| role_src = "none" | ||
| if actor == "cloudbrid-agent[bot]": | ||
| role = "agent" | ||
| role_src = "app-slug" | ||
| else: | ||
| st, m = api(E["GOV_TOKEN"], f"/orgs/{ORG}/memberships/{actor}") | ||
| if st == 200 and m.get("role") == "admin": | ||
| role = "owner" | ||
| role_src = "org-membership" | ||
| else: | ||
| st, p = api(E["APP_TOKEN"], f"/repos/{REPO}/collaborators/{actor}/permission") | ||
|
Comment on lines
146
to
+151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
# 检查 API 辅助函数是否处理传输异常并设置超时。
rg -n -C 6 'def api|urlopen|HTTPError|URLError|TimeoutError|timeout|GOV_TOKEN|APP_TOKEN' .github/workflows/conductor.ymlRepository: Cloudbird-Software/.github Length of output: 8302 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
p = Path(".github/workflows/conductor.yml")
s = p.read_text(encoding="utf-8")
start = s.index(" def api(")
end = s.index(" def audit(", start)
api = s[start:end]
print("API helper:")
print(api.rstrip())
print("\nChecks:")
print("has_urlopen_timeout=", bool(re.search(r"urlopen\s*\([^)]*,\s*timeout\s*=", api)))
print("catches_HTTPError=", "except urllib.error.HTTPError" in api)
print("catches_URLError=", "except urllib.error.URLError" in api)
print("catches_TimeoutError=", "except TimeoutError" in api or "except urllib.error.TimeoutError" in api)
role_start = s.index(" else:\n", s.index(' if actor == "cloudbrid-agent[bot]":'))
role_end = s.index(" assoc =", role_start)
role = s[role_start:role_end]
print("\nRole fallback:")
print(role.rstrip())
print("fallback_on_non_200=", "if st == 200" in role and "api(E[\"APP_TOKEN\"]" in role)
PY
python3 - <<'PY'
# Model urllib.request.urlopen's exception boundary without importing repository code.
import urllib.error
def modeled_api(urlopen):
try:
with urlopen() as response:
return response.status, {}
except urllib.error.HTTPError as e:
return e.code, {}
class TransportFailure(Exception):
pass
def raises_transport():
raise TransportFailure("network failure")
try:
modeled_api(raises_transport)
except Exception as e:
print("transport_exception_escapes=", type(e).__name__, str(e))
else:
print("transport_exception_escapes=False")
class HttpFailure:
code = 503
def raises_http():
raise urllib.error.HTTPError("https://api.github.com", 503, "unavailable", {}, None)
print("http_error_result=", modeled_api(raises_http))
PYRepository: Cloudbird-Software/.github Length of output: 1757 为 API 请求设置超时并处理传输异常。
🤖 Prompt for AI Agents |
||
| if st == 200 and p.get("permission") == "admin": | ||
|
Comment on lines
+150
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Owner fallback widens privileges The repo-collaborator admin fallback runs not only when the org-membership API call fails, but also when it succeeds with role!=admin, potentially treating any repo admin as owner even if they’re only an org member. This contradicts the stated “失败时兜底” behavior and can become a real privilege expansion if additional repo admins exist (drift) before governance detects it. Agent Prompt
|
||
| role = "owner" | ||
| role_src = "repo-collab-admin(gov-membership HTTP %s)" % st | ||
|
Comment on lines
146
to
+154
Comment on lines
+151
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Misleading role_src http code role_src formats "gov-membership HTTP %s" using st after it has been overwritten by the repo-permission API call, so audits will always log 200 in the branch where role becomes owner. This defeats the new auditability goal by hiding the actual GOV membership failure status. Agent Prompt
Comment on lines
+131
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
# 检查组织查询结果与仓库权限兜底的实际分支条件。
rg -n -C 6 'GOV_TOKEN|APP_TOKEN|memberships|collaborators|role_src|if st|else:' .github/workflows/conductor.ymlRepository: Cloudbird-Software/.github Length of output: 9760 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- conductor structure and API helper ---'
ast-grep outline .github/workflows/conductor.yml
rg -n -C 12 'def api|HTTPError|urlopen|sender_role|repo-collab-admin|org-membership|GOVERNANCE_TOKEN|受管仓 admin|vcs-admin|唯一性' .github/workflows/conductor.yml .github governance standards scripts profile 2>/dev/null || true
printf '%s\n' '--- policy and ADR references ---'
rg -n -C 5 'ADR-0055|sender_role|owner|org-membership|collaborator|permission.*admin|admin.*owner|GOVERNANCE_TOKEN|APP_TOKEN' .github governance standards scripts profile 2>/dev/null || trueRepository: Cloudbird-Software/.github Length of output: 50383 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- exact policy references ---'
rg -n -C 8 'ADR-0010|admin 全系统唯 owner|admin.*owner|owner.*admin|org-membership|repo-collab-admin|失败时' \
governance .github/workflows/conductor.yml
printf '%s\n' '--- relevant patch context ---'
git diff --unified=20 -- .github/workflows/conductor.yml | sed -n '1,220p'
printf '%s\n' '--- deterministic branch probe ---'
python3 - <<'PY'
def resolve(org_status, org_role, repo_status, repo_permission):
role = "none"
if org_status == 200 and org_role == "admin":
role = "owner"
else:
if repo_status == 200 and repo_permission == "admin":
role = "owner"
return role
cases = [
("org admin", 200, "admin", 404, None),
("org member + repo admin", 200, "member", 200, "admin"),
("org member + repo write", 200, "member", 200, "push"),
("org lookup failure + repo admin", 403, None, 200, "admin"),
]
for name, *args in cases:
print(f"{name}: {resolve(*args)}")
PYRepository: Cloudbird-Software/.github Length of output: 9935 将仓库权限兜底限制为组织查询失败 注释定义的策略是“组织查询失败时”才使用 潜在严重级别:Major。 🤖 Prompt for AI Agents
Comment on lines
+151
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 保存两次 API 查询的独立状态。 Line [151] 覆盖了组织查询的 请使用 依据 PR 目标,审计日志必须记录正确的 owner 判定来源和组织查询状态。 建议修复- st, m = api(E["GOV_TOKEN"], f"/orgs/{ORG}/memberships/{actor}")
- if st == 200 and m.get("role") == "admin":
+ gov_st, m = api(E["GOV_TOKEN"], f"/orgs/{ORG}/memberships/{actor}")
+ if gov_st == 200 and m.get("role") == "admin":
role = "owner"
role_src = "org-membership"
else:
- st, p = api(E["APP_TOKEN"], f"/repos/{REPO}/collaborators/{actor}/permission")
- if st == 200 and p.get("permission") == "admin":
+ collab_st, p = api(E["APP_TOKEN"], f"/repos/{REPO}/collaborators/{actor}/permission")
+ if collab_st == 200 and p.get("permission") == "admin":
role = "owner"
- role_src = "repo-collab-admin(gov-membership HTTP %s)" % st
+ role_src = "repo-collab-admin(gov-membership HTTP %s)" % gov_st🤖 Prompt for AI Agents |
||
| assoc = E.get("COMMENT_ASSOC") or "NONE" # label 事件无此字段→NONE(guard 用 role) | ||
|
|
||
| # ---- 当前状态与标签集 ---- | ||
|
|
@@ -175,11 +191,11 @@ jobs: | |
| if ev == "comment:/release": | ||
| rc = adjudicate("/release") | ||
| if rc == 0: | ||
| audit(f"event={ev} from={current} sender_role={role} arbiter=allow " | ||
| audit(f"event={ev} from={current} sender_role={role}({role_src}) arbiter=allow " | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Missing card: metadata line The PR description body (as provided) does not include the required single `Card: <owner>/<repo>#<n>` metadata line. Downstream automation that relies on this metadata may fail to associate the change with a work item. Agent Prompt
|
||
| f"verdict=ALLOWED release(租约已释放——无标签转移定义,纯租约面)") | ||
| raise SystemExit(0) | ||
| if rc == 1: | ||
| audit(f"event={ev} sender_role={role} verdict=DENIED-by-arbiter " | ||
| audit(f"event={ev} sender_role={role}({role_src}) verdict=DENIED-by-arbiter " | ||
|
Comment on lines
+194
to
+198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
# 检查审计输出与共享审计消费者的格式契约。
rg -n -C 5 'def audit|audit_emit|butler-audit|role_src|actions=' .github/workflows governanceRepository: Cloudbird-Software/.github Length of output: 32218 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- canonical audit contract ---'
cat -n governance/butler-audit.sh | sed -n '1,125p'
printf '%s\n' '--- conductor audit implementation and call sites ---'
cat -n .github/workflows/conductor.yml | sed -n '95,115p;135,156p;185,207p;224,247p'
printf '%s\n' '--- consumers, tests, and documentation ---'
rg -n -C 4 --glob '!governance/butler-audit.sh' \
'AUDIT[[:space:]]*\||sender_role|role_src|actions=<JSON>|actions=' \
.github governance docs README.md 2>/dev/null || true
printf '%s\n' '--- candidate audit parsers ---'
rg -n -C 3 \
'split\(.*\||IFS=.*\||AUDIT.*parse|audit.*parse|butler.*audit|jq.*actions|role_src' \
--glob '*.{sh,py,js,ts,yml,yaml,json,md}' . 2>/dev/null || true
printf '%s\n' '--- deterministic parsing probe ---'
python3 - <<'PY'
import json
lines = [
"AUDIT | issue=`#1` | actor=alice | event=comment:/release sender_role=owner(org-membership) arbiter=allow verdict=ALLOWED",
"AUDIT | butler=conductor | trigger=comment:/release | outcome=ok | actions={\"sender_role\":\"owner\",\"role_src\":\"org-membership\"}",
]
for line in lines:
fields = {}
for part in line.split("|"):
part = part.strip()
if "=" in part:
key, value = part.split("=", 1)
fields[key] = value
print(json.dumps({"line": line, "fields": fields,
"top_level_role_src": fields.get("role_src"),
"actions_json": json.loads(fields["actions"]) if "actions" in fields else None},
ensure_ascii=False))
PYRepository: Cloudbird-Software/.github Length of output: 38332 让
🤖 Prompt for AI Agents |
||
| f"(非 holder/无租约/过期——无租约变更、无标签变更)") | ||
| raise SystemExit(0) | ||
| audit(f"event={ev} arbiter=infra rc={rc} verdict=ABORT " | ||
|
|
@@ -226,7 +242,7 @@ jobs: | |
| audit(f"event={ev} transition={t['id']} arbiter=infra rc={rc} verdict=ABORT " | ||
| f"(fail-closed——不许绕过仲裁;delivery 幂等可安全重投)") | ||
| raise SystemExit(1) | ||
| audit(f"event={ev} transition={t['id']} sender_role={role} arbiter=allow " | ||
| audit(f"event={ev} transition={t['id']} sender_role={role}({role_src}) arbiter=allow " | ||
| f"(租约已建——T3 落地;TTL 到期由下一 /claim 原子接管,ADR-0054)") | ||
|
Comment on lines
+245
to
246
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
# 检查所有 claim 审计结果是否记录角色来源。
rg -n -C 3 'comment:/claim|sender_role|role_src|arbiter=infra|DENIED-by-arbiter' .github/workflows/conductor.ymlRepository: Cloudbird-Software/.github Length of output: 5395 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- workflow audit implementation and claim paths ---'
sed -n '1,90p' .github/workflows/conductor.yml
sed -n '220,310p' .github/workflows/conductor.yml
printf '%s\n' '--- audit consumers and role_src contracts ---'
rg -n -C 3 'role_src|sender_role|arbiter=infra|DENIED-by-arbiter|comment:/claim|ADR-0054|ADR-0055' \
.github governance standards scripts profile README.md 2>/dev/null || true
printf '%s\n' '--- relevant repository files ---'
git ls-files | rg '(^|/)(ADR|adr|audit|conductor|governance|standards|CODEOWNERS|expected-state)' | head -200Repository: Cloudbird-Software/.github Length of output: 37563 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- conductor audit helper and complete route context ---'
sed -n '90,225p' .github/workflows/conductor.yml
printf '%s\n' '--- workflow tests and audit-schema references ---'
rg -n -C 4 'role_src|sender_role|audit\(|DENIED-by-arbiter|verdict=ABORT|claim' \
.github/workflows/conductor-negtest.yml governance standards scripts .github/CODEOWNERS \
2>/dev/null || true
printf '%s\n' '--- all ADR and audit-related files ---'
git ls-files | rg -i '(^|/)(adr[^/]*|.*audit.*|.*conductor.*|.*lease.*)(/|$|\\.)' | head -250
printf '%s\n' '--- static claim-audit coverage check ---'
python3 - <<'PY'
from pathlib import Path
import re
text = Path(".github/workflows/conductor.yml").read_text(encoding="utf-8")
for match in re.finditer(r'if ev == "comment:/claim":', text):
start = match.start()
end = text.find("\n # ---- 执行转移", start)
block = text[start:end]
print("claim block:")
for line in block.splitlines():
if "audit(" in line or "role_src" in line or "arbiter=" in line:
print(line.strip())
print("claim audit calls:", block.count("audit("))
print("claim calls with role_src:", sum("role_src" in call for call in re.findall(r'audit\\(.*?(?=\\n\\s*(?:raise|if|audit|$))', block, re.S)))
PYRepository: Cloudbird-Software/.github Length of output: 15730 为所有 🤖 Prompt for AI Agents |
||
|
|
||
| # ---- 执行转移(状态标签写=App 身份,INV-02;写失败=fail-closed, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. Missing adr reference in description
📘 Rule violation§ ComplianceAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools