Files
docker_practice/.github/workflows/identity-guard.yaml
T
yeasy f82e13017d 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 触发一次重复的全量审计。
2026-07-22 15:24:38 -07:00

121 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)."