diff --git a/.githooks/pre-push b/.githooks/pre-push index d6c2904..e4f869c 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -90,4 +90,100 @@ for path in "${FORBIDDEN[@]}"; do done done +# ── 第二道:构建产物里内嵌 AGC 真身(2026-09-28 新增)──────────────────── +# +# 为什么不能只靠上面的路径清单:**hap/ipa 之类的打包产物会把 +# `agconnect-services.json` 原封不动打进去**,而它在产物里只是一个 zip 条目。 +# 逐条列举路径追不上产物形态(下次换个构建目标又漏一次)。 +# +# 实测(`git ls-files` 抓到的):两个 36MB 的 .hap,内含 +# `resources/rawfile/agconnect-services.json`,其中 client.client_id / +# code.code1..4(4 个 32 位十六进制)/ oauth_client.client_id 全是真值。 +# +# 口径:只看**本次推送范围内被新增/修改的二进制产物**,不扫全历史 —— +# ① 全历史扫描在大仓上很慢,而这道闸的目标是"拦住下一次手滑"; +# ② 已经推出去的那部分由 `criteria-hygiene` 那条报警负责(泄露之后响的闹钟)。 +# +# ★ 这里用 `git log --diff-filter=A --name-only` 而不是 `git diff`(2026-09-28 实测踩坑): +# `RANGES` 里的元素是「`rsha..lsha`」或(新分支时)**单个 `lsha`**。 +# 把单个 sha 传给 `git diff` 并不会得到"这个范围里新增的文件"—— 它是 +# 「工作区 vs 该 sha」的差异,**在干净工作区上恒为空** ⇒ 判据**静默失效**, +# 而失效的判据与通过的长得一模一样。(第一版就这么写的,实测:含凭证的 hap 被放行, +# push exit=0。) +# `git log` 才是"这些提交里动过什么"的那个读数,与第一道检查同源。 +# +# 判据形态:**新增或修改的构建产物** + 在该提交的文件内容里搜 +# `agconnect-services.json` 这个**条目名**。用条目名而不是凭证值本身, +# 因为值会变而条目名稳定,且搜值需要把凭证读进内存。 +for range in "${RANGES[@]}"; do + if ! bins="$(git log --diff-filter=AM --name-only --format= "$range" -- '*.hap' '*.app' '*.ipa' 2>/dev/null)"; then + echo "pre-push: 无法解析范围 '$range'(查构建产物)—— 中止推送" >&2 + exit 1 + fi + bins="$(printf '%s\n' "$bins" | sort -u | grep -v '^$')" + [ -z "$bins" ] && continue + while IFS= read -r b; do + [ -n "$b" ] || continue + # ★★ 取 blob 必须先把范围解析成**单个 commit**,再 `ls-tree`(2026-09-28 实测两次踩坑): + # + # ① `git rev-parse "a..b:path"` 输出**两行**(blob sha + `^a`), + # 赋进变量是多行值,`cat-file` 失败而被 `||` 兼掉 ⇒ 静默放行。 + # (手工喂真实待推范围给钩子,exit=0;而同一范围内 `git log` 明确列出了两个 hap。 + # 判据"看起来在检查"而实际没执行 —— 与本仓 python-probe-shadowing 那笔同族。) + # ② 改用 `ls-tree` **也报错**:它不接受范围 + # (`fatal: Not a valid object name a..b`)。 + # + # ⇒ 正确形状:先取范围内**最后一个** commit(`git rev-list --max-count=1`), + # 再 `ls-tree <那个 commit>`。第一道检查用的是 `git log`(天生吃范围), + # 所以它没踩这个坑 —— **同一条数据,git 子命令的输入契约不一样**。 + tip="$(git rev-list --max-count=1 "$range" 2>/dev/null | head -1)" + if [ -z "$tip" ]; then + echo "pre-push: 范围 '$range' 解析不出 commit(查构建产物)—— 中止推送" >&2 + exit 1 + fi + blob_sha="$(git ls-tree "$tip" -- "$b" 2>/dev/null | awk 'NR==1{print $3}')" + if [ -z "$blob_sha" ]; then + # 该 tip 上没有这个文件(可能在范围内被删了)—— 读不到就不当违规,但**说明**, + # 因为"没检查到"与"没问题"不是一回事。 + echo "pre-push: 读不到 $b 在待推范围里的内容(可能已被删除)—— 未据此判红" >&2 + continue + fi + # ★★ **必须扫全文件**(2026-09-28 实测修正):原先只取头尾各 4MB, + # 理由是"zip 的中央目录在尾部"。实测本仓真 hap 里那个条目在 + # **byte 28,239,718 与 35,958,749**(文件共 35MB)—— 头 4MB 扫不到、 + # 尾 4MB 也只勉强够到第二处。**第一版会把这个含真凭证的 hap 放过去。** + # ⇒ 改全量流式扫(grep -a 直接吃管道,不进内存)。 + # + # 代价:每个 35MB 产物要读一遍。推送路径上、非热路径,可以接受。 + # 真的嫌慢就改成 `grep -a -m1`(找到一个就停),语义不变且更快。 + # ★★★ **不能用 `grep -q` 接管道**(2026-09-28 实测,方向与直觉相反): + # `grep -q` 找到就**立即退出**,于是上游 `git cat-file` 在写管道时收到 SIGPIPE + # ⇒ 它的退出码是 **141**。而本钩子开头有 `set -uo pipefail`(`pipefail` 把管道 + # 判成取**各段非零的合取**)⇒ 整条管道变成 141 ⇒ `if` 判为**假**。 + # + # 实测读数:命中时 `PIPESTATUS=141 0`(cat-file=141, grep=0)⇒ `if` 不成立。 + # **即"找到了凭证"被读成"没找到"** —— 而失效方向恰好是**放行**, + # 与"宁可推不出去"的正确方向相反,是最坏的那种失效。 + # + # ⇒ 不用 `-q`:让 grep 读完整条流、正常退出 0,最后一个命令自然是 grep 的码。 + # (代价是扫完整个 35MB 而不是命中即停;推送路径上可接受。) + if git cat-file -p "$blob_sha" 2>/dev/null | grep -a 'agconnect-services\.json' >/dev/null; then + problems=1 + { + echo "" + echo "✗ 这次推送会带上构建产物 **$b**,而它内嵌了 agconnect-services.json。" + echo " 那是 AGC 客户端凭证(api_key 信封),而本仓镜像是**匿名可 clone 的公开项目**。" + echo "" + echo " 怎么处置:" + echo " 1) 构建前把 AGC 真身从打包输入里去掉(用 .example.json 占位)," + echo " 或在构建产物生成后立刻 git rm --cached 掉。" + echo " 2) 已经进了要推的提交:用 filter-repo / rebase 把该 blob 清掉," + echo " 确认这些提交**没有**被推过;推过了就按已泄露处理 —— 去 AGC 轮换。" + echo "" + echo " (这一格是**按内容**查的,不是按路径 —— 产物名会变,凭证的容器也会变。)" + } >&2 + fi + done <<<"$bins" +done + exit "$problems" diff --git a/.gitignore b/.gitignore index 12ea887..24c3b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,20 @@ server/agentmail-gateway server/gw *.test +# ---- 构建产物(不入库)---- +# +# ★ 2026-09-28 新增 `*.hap`:鸿蒙构建产物**内嵌 `agconnect-services.json` 真身** +# (实测:client_id / 4 个 32 位 code / oauth_client —— 即 AGC api_key 信封)。 +# 两个 36MB 的 hap 被 `f51c9c8`(一个标题为「workspace 谓词抽成共享构造器」的重构提交) +# 顺手带进了版本库。 +# +# 为什么必须 ignore 而不是"记得别 add":`.githooks/pre-push` 原先只按**路径**拦 +# AGC json,而 hap 是**二进制**里的凭证 —— 逐条路径列举追不上产物形态。 +# `*.hap` 顺带堵住"下次构建又手滑 add 一次"。 +*.hap +*.app +*.ipa + # ---- 运行态数据 ---- # # SQLite 库与附件目录:生产在 /opt/agentmail/data,