ci: add Identity Guard rejecting AI-assistant commit attribution

起因:blockchain_guide 上曾有一个 claude/novel-content-extraction-r0wys3
分支,4 个提交的 author 和 committer 都是 Claude <noreply@anthropic.com>
(内容还是与本书无关的小说抓取脚本)。分支已删除,但暴露出一个缺口:
既有的 .git/hooks/commit-msg 只查提交信息里的 Co-authored-by 尾注,
查不到身份字段;而且本地钩子不随仓库分发,那 4 个提交来自云端会话,
根本不经过本地钩子。

因此把闸门放到服务端:本工作流在 push 与 pull_request 上扫描本次新增
提交的 author/committer 身份与 Co-authored-by 尾注,命中即失败。
本地另配 pre-commit(拦当前身份)与 pre-push(拦 cherry-pick/rebase
带进来的外来提交)作为纵深防御,二者不入库。

判定刻意收窄,避免误伤本仓库群里大量讨论 Claude/Codex 的正文:
只检查身份字段与行首锚定的 Co-authored-by 尾注,绝不扫描自由文本;
姓名要求全等("Claude Dubois" 这样的真人不受影响),邮箱按厂商域名
(含子域)匹配,并覆盖 claude[bot] 这类 GitHub App 身份。

已验证:
- 用被删分支的真实 4 个提交做回归,push / pull_request / 新建分支
  三种到达形态全部拦下;
- 全部 14 个仓库的完整历史(5,715 个提交,含 docker_practice 1,591
  个多人历史与 claude_guide 339 个满是 Claude 的提交)零误报;
- dependabot 的 Co-authored-by、真人 Claude Dubois、正文提到
  co-authored-by 但非尾注的提交,均正确放行;
- 范围解析不出来时 fail closed,只有确实没有 head 提交(删分支)才放行;
- tags-ignore 避免发布 tag 触发一次重复的全量审计。
This commit is contained in:
yeasy
2026-07-22 15:24:38 -07:00
parent 69bf233381
commit f82e13017d
2 changed files with 121 additions and 1 deletions
+120
View File
@@ -0,0 +1,120 @@
name: Identity Guard
# 拒绝把 AI 助手写成提交作者/提交者或写进 Co-authored-by 尾注
# 本地 .git/hooks 不随仓库分发云端会话推上来的提交不受其保护
# 因此这道检查必须放在服务端
on:
push:
tags-ignore: ['**']
pull_request:
workflow_dispatch:
permissions: {}
jobs:
check-commit-identity:
permissions:
contents: read
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Reject AI assistant identities and co-author trailers
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
HEAD_SHA: ${{ github.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
ZERO=0000000000000000000000000000000000000000
US=$'\037' # 用不可能出现在姓名/邮箱里的 unit separator 作分隔符
# 确定扫描范围三种情况
# 1. pull_requestbase..head只看本 PR 新增的提交
# 2. push before 指向一个真实存在的提交before..head
# 3. 其余新建分支 before 0force-push before 不可达
# workflow_dispatch退化为全量审计扫描 HEAD 的完整历史
if [ "$EVENT_NAME" = "pull_request" ]; then
RANGE="${PR_BASE_SHA}..${PR_HEAD_SHA}"
elif [ "$EVENT_NAME" = "push" ] \
&& [ -n "${BEFORE_SHA:-}" ] \
&& [ "$BEFORE_SHA" != "$ZERO" ] \
&& git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null; then
RANGE="${BEFORE_SHA}..${HEAD_SHA}"
else
RANGE="$HEAD_SHA"
fi
echo "Scanning range: ${RANGE}"
# 只有"确实没有 head 提交"例如删除分支的 push才允许放行
# 其余任何解析不出范围的情况一律 fail closed不能静默通过
if [ -z "${HEAD_SHA:-}" ] || [ "$HEAD_SHA" = "$ZERO" ]; then
echo "No head commit (branch deletion?); nothing to check."
exit 0
fi
if ! COMMITS="$(git rev-list "$RANGE" 2>&1)"; then
echo "::error::cannot resolve commit range ${RANGE}: ${COMMITS}"
exit 1
fi
COUNT="$(printf '%s' "$COMMITS" | grep -c . || true)"
echo "Commits in range: ${COUNT}"
if [ "$COUNT" -eq 0 ]; then
echo "No commits to check."
exit 0
fi
# 身份判定刻意收窄避免误伤
# - 邮箱在 AI 厂商域名下含子域
# - 姓名恰好等于助手名因此真人 "Claude Dubois" 不受影响
# 正文里出现 Claude/Anthropic 是完全合法的本仓库群里有专讲 Claude
# 的书所以只检查身份字段与 Co-authored-by 尾注绝不扫描自由文本
BAD_EMAIL='@([a-z0-9.-]+\.)?(anthropic|openai)\.com$'
BAD_NAME='^(claude([ ._-]?code)?|codex|chatgpt|copilot|anthropic|openai)(\[bot\])?$'
BAD_TRAILER='^[[:space:]]*co-authored-by:.*(claude|anthropic|codex|copilot|openai)'
failed=0
# 身份检查命令替换先落地set -e 才能捕获 git 失败
# while 循环用 here-string 喂数据避免管道子 shell 吞掉 failed 赋值
IDENTS="$(git log --format="%H${US}%an${US}%ae${US}%cn${US}%ce" "$RANGE")"
while IFS="$US" read -r sha an ae cn ce; do
[ -n "$sha" ] || continue
for role in author committer; do
if [ "$role" = author ]; then name="$an"; mail="$ae"; else name="$cn"; mail="$ce"; fi
lname="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')"
lmail="$(printf '%s' "$mail" | tr '[:upper:]' '[:lower:]')"
if printf '%s' "$lmail" | grep -qE "$BAD_EMAIL" \
|| printf '%s' "$lname" | grep -qE "$BAD_NAME"; then
echo "::error::${sha} ${role} identity is an AI assistant: ${name} <${mail}>"
failed=1
fi
done
done <<< "$IDENTS"
# Co-authored-by 尾注检查行首锚定并要求冒号避免匹配正文叙述
while read -r sha; do
[ -n "$sha" ] || continue
body="$(git log -1 --format='%B' "$sha")"
if printf '%s' "$body" | grep -qiE "$BAD_TRAILER"; then
echo "::error::${sha} has a Co-authored-by trailer referencing an AI assistant"
printf '%s' "$body" | grep -iE "$BAD_TRAILER" | sed 's/^/ /'
failed=1
fi
done <<< "$COMMITS"
if [ "$failed" -ne 0 ]; then
echo ""
echo "AI assistant attribution found in the commits above."
echo "Rewrite them before pushing, e.g.:"
echo " git rebase -i --exec 'git commit --amend --no-edit --reset-author' <base>"
exit 1
fi
echo "OK: no AI assistant identity or Co-authored-by trailer in ${COUNT} commit(s)."