多 Agent 同机:Git 作者、凭据与提交归属怎么分
十个 agent 共用一个 GitHub 账号,你得到的是署名,失去的是归属、互审与审计。四个层面拆开:Git 作者、平台身份、提交里的三行、谁手里有钥匙。
一句话结论:在同一台机器上跑多个 coding agent,"设个名字"是不够的——你要分别处理四件互相独立的事:Git 的 author/committer、平台侧的身份(token/App/账号)、提交里的归属标记、以及进程能读到的凭据。少做任何一层,你都会得到一份看起来正常、但事后无法解释的提交历史。
披露:本文编译整理自四篇 2026 年 9–10 月的一手实践(Vitruvian Software 的十个 agent 与 GitHub App、Obi 的提交 trailer 设计、Orquesta 的 git-native 方案、Bill Mongan 的九 agent 单机与按 agent 分配的 MCP 凭据),并补充 Safeguard 2026-10-18 的身份风险清单。本文没有自己搭环境复现,所有命令、数字与"实测结果"都来自原文;文末逐条给出出处。
一、先看清代价:三个"看起来没事"的坑
Vitruvian Software 的 monorepo 里天天有十个 agent 干活:Forge 管 Bazel 构建、Atlas 管基础设施、Ridge 管 Kubernetes、Wren 写应用代码、Scout 管测试与 CI、Aegis 做安全评审、Quill 写文档……它们贡献了相当大比例的 PR。
在此之前,GitHub 把这十个 agent 看成同一个人——所有 push、PR、merge 都走仓库所有者那个账号。表面无害,实际丢了三样东西:
| 丢掉的东西 | 具体表现 |
|---|---|
| 归属 | "Ridge 开了 #1440" 只是聊天记录里的说法,git 和 GitHub 都无法作证 |
| 互审 | GitHub 禁止自己批准自己,而在平台看来每个 PR 都是自己写的 → 安全 agent 没办法在基础设施 agent 的 diff 上留下有效批准 |
| 审计 | 记录显示"一个人做了所有事",包括这个人从没看过的工作 |
第二条是真正的痛点。想让 agent 互审,就得让平台认为"这是两个不同的身份"。用同一个身份时,你能做的最多是让 agent 留个评论,然后祈祷有人读。
这条经验对单人开发同样成立:你自己 + 三个 agent 共用一个 user.name,等于把未来每一次 git blame 都变成猜谜。
二、Git 层:token 决定"谁能推送",环境变量决定"谁署名"
最容易踩的坑在这里,而且只踩一次就足够毁掉整份历史。
把 agent 的身份做对,需要同时设置两样东西:
# 1) 谁推送(API + git push 都用它)
export GH_TOKEN="<installation token>"
# 2) 谁署名(这一项跟 token 完全无关)
export GIT_AUTHOR_NAME="vitruvian-wren-agent[bot]"
export GIT_AUTHOR_EMAIL="<bot_user_id>+vitruvian-wren-agent[bot]@users.noreply.github.com"
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
Vitruvian 一开始只设了 token。结果是:PR 上显示的是 bot,commit log 里还是机器主人的名字——因为 git 的作者信息取自本地配置与环境变量,不取自"是谁推送的"。
这解释了一个很常见的错觉:"我已经给它单独的 token 了,为什么历史还是乱的?"
几个值得抄的细节:
- 用 bot 的 noreply 地址:GitHub 会把这个地址绑定到具体 App,于是提交在 GitHub 上能正确回指到
xxx[bot]。 - token 一小时过期是特性,不是麻烦:长期密钥只有私钥(在 vault 里),token 是每次会话现取的,用完即弃,也就不存在"粘贴到某个配置文件里忘了删"的问题。
GIT_AUTHOR_*与GIT_COMMITTER_*要成对设:只改 author,rebase / cherry-pick 之后 committer 会泄露真实操作者。
一句话记忆:token 是权限,环境变量是署名,两者必须同时设。只设一个,你得到一份误导性的历史。
三、平台层:为什么是 GitHub App,而不是"每个 agent 一个 bot 账号"
直觉方案是给每个 agent 建一个 machine user。Vitruvian 否掉了它,理由很硬:
- GitHub 条款允许每个自然人一个免费 machine account。十个 agent 就是十个条款不覆盖的账号,外加十套要轮换的凭据。
- GitHub App 没有这个限制:一个组织可以拥有任意多个 App,每个 App 都有真正的
name[bot]身份,能真的作为作者提交、开 PR、提交 review。 - App 用短期 installation token 认证,而不是密码或 PAT——这正是无人值守软件需要的东西。
命名也很直白:vitruvian-<agent>-agent,一个 agent 一个 App。
文档没回答的问题,只能自己测
动手之前有个关键未知:App 的批准,算不算数到"require 1 approving review"这条分支保护规则里? GitHub 关于受保护分支的文档只说批准来自"有写权限的人"或指定的 code owner,对 App 只字未提。
于是他们在一个 canary 仓库上测(required_approving_review_count: 1、enforce_admins: true):
before reviewDecision=REVIEW_REQUIRED mergeable_state=blocked
POST /pulls/1/reviews event=APPROVE (App installation token)
after reviewDecision=APPROVED mergeable_state=unstable
review <app-slug>[bot] type=Bot state=APPROVED
结论:算数。 批准要求从"阻塞"变成"满足"。(unstable 是另一个无关的 pending check。)
但测出一个限制:App 不能当 code owner。CODEOWNERS 只接受用户、团队、邮箱地址。所以他们的做法是——关掉"必须 code owner 审查",只用批准数量这一根杠杆。仓库配置里这两项是分开的,不是笔误。
唯一不能自动化的那一步
仓库的一切都当代码管,包括它自己的设置。创建 App 是例外:GitHub 没有创建 App 的 API。要么填网页表单,要么走 App manifest flow。
manifest flow 更好,因为它把私钥放在 API 响应里返回,而不是让你从浏览器下载一个文件。你 POST 一段 JSON,声明权限:
{
"name": "vitruvian-<agent>-agent",
"public": false,
"default_permissions": {
"contents": "write",
"pull_requests": "write",
"metadata": "read",
"checks": "read",
"workflows": "write"
},
"default_events": []
}
GitHub 回调时给一个一次性 code,你要在一小时内换成 App 的 id 和私钥。那是私钥的唯一一份拷贝:直接进 vault,响应随即删掉。
⚠️ 两个坑:① 安装页默认勾选 All repositories——接受它就等于给每个 agent 授了组织内所有仓库的写权限,一定要选 Only select repositories;② 权限里 workflows: write 是给 CI 用的,如果你的 agent 不该碰流水线,就别给。把"谁能写哪里"变成可 review 的 diff
repo_config:agentApps:
- name: beacon
installationId: "152031070"
# ...每个 agent 一条
repo_config:requiredApprovals: "1"
agentApps 把每个 App 的 installation 钉到这个仓库上。好处不是省事,而是:"哪个 agent 能写哪儿"从"某个设置页里的复选框"变成了"一个会被 review 的 diff"。
顺序错了会怎样
requiredApprovals: "1" 必须是最后一步。如果先开强制评审、agent 还没有各自身份,那么没有任何人能给出批准——每个 PR 永远卡在 REVIEW_REQUIRED。更糟的是 release-please 和 Dependabot 的自动合并也会死,因为规则集的 bypass 只覆盖"直接合并",从不覆盖"自动合并进 merge queue"。
安全顺序:
- 每个 agent 建并安装一个 App;
- 私钥进 vault,把每个 agent 的 harness 指向它;
- 把每个 agent 加进
agentApps; - 最后才提高
requiredApprovals。
日常使用时,agent 侧只需要一条命令:
eval "$(bazel run //tools/agent-app -- env wren)"
它查一张提交进仓库的注册表(App id、installation id、bot user id 都是公开标识符),用该 agent 的私钥签 JWT,换成 installation token,然后导出两样东西——也就是上面第二节的 GH_TOKEN 和 GIT_AUTHOR_*。
四、提交里那三行:谁来写,写什么
Git 的作者字段只有名字和邮箱,加不进"哪个 job、哪个工具、哪个模型"。Obi(一个 Rust 写的 agent 执行引擎)的做法是在提交信息末尾加三行 trailer:
Oby-Job: 3f2a9c1e
Oby-Agent: Codex
Oby-Model: gpt-6.1-sol
好处很直接:任何人 git log 或在 PR 里就能看出"这段代码是哪个 agent、用什么模型写的",不用去问 owner;owner 自己也能反查:
git log --grep "Oby-Model: gpt-6.1-sol"
这把"某个模型老犯某类错误"从传闻变成了可查询的事实。
为什么必须由引擎写,而不是让 agent 自己写
这是整个设计里最值得抄的一点。Obi 的实现是:
- 在
keep_commits阶段(job 结束、把 sandbox 里的提交打包回引擎的 clone 时),对新出现的那些提交逐个重写; - 读原始对象(
git cat-file commit),把父提交指向重写后的版本,然后把 trailer 追加到消息里,用git hash-object -t commit -w --stdin自己写新对象; - 刻意不用
git commit-tree或git interpret-trailers——它们会读commit.gpgSign、trailer.*之类设置,可能改变字节; - tree、author、committer 和两个时间戳保持原样;
- 如果 agent 自己写了
Oby-Job/Oby-Agent/Oby-Model,先删掉,只有引擎说的算; - 消息末尾已经是同样的三行时不动,所以重复写不改变提交 id(幂等)。
还有两个设计取舍值得记:
Oby-Agent记的是工具名(Claude Code、Codex、Cursor),不是账号;Oby-Model优先取工具自报的模型,其次取 job 指定的,最后才是default。- 绝不写账号名、邮箱、主机名:因为提交可能是公开的。
对比一下"让 agent 自己在 prompt 里加 co-author"的做法:agent 可以忘、可以写错、可以被注入的提示词骗去冒充别的 agent。归属信息由不参与推理的那一层写,才可信。
五、粒度:一个动作一个 commit
Orquesta 走的是另一条路:不重写 trailer,而是让每个动作本身成为一个提交。
他们的判断是:现在多数工具要么把改动直接落到工作区,要么在会话结束时来一个巨型提交(消息是"AI refactor"这种)——这两种都让中间决策不可见。那不是可追溯性,那是带收据的黑箱。
具体做法:
- agent 在真实分支上工作,从 base 拉出 feature 分支;
- 每次编辑文件、运行会改动代码的命令、跑测试并修失败,都自动提交,消息由 agent 自己写(
fix:/test:/refactor:这类 Conventional Commits 风格); - author 字段设置为 agent 的身份,而不是人类用户:
git config user.name "Orquesta Agent"
git config user.email "agent@orquesta.local"
- 只读命令(
git status、跑一次看结果的构建)不产生提交,因为仓库状态没变,但它们会记进活动流。
收益是外科手术式回滚:agent 在同一个会话里既加了一个 API 端点、又修了一个无关 bug,端点引入回归时,你可以只 git revert <那个提交>,bug 修复保留下来。
噪声问题他们给两个答案:① 让 agent 把逻辑上同一件事的改动合成一个提交;② 合并前提供 squash 步骤——执行期间保留每一步,最终历史的整洁度由你决定。而且中间提交都在 feature 分支上,噪声不会污染 main。
六、进程与凭据:把一把钥匙只交给一个 agent
送别最容易忽略的一层:身份不是"起个名字",而是"它能读到什么"。
Bill Mongan 把九种 agent 并排跑在一台小主机上(Ubuntu Server,约 14 GiB 可用内存):Claude Code、Codex、Antigravity、Grok Build、Hermes、GitHub Copilot、两份 OpenCode,还有 pi。用 Herdr(一个"认识 agent"的终端复用器)管,一个 agent 一个标签、一个目录。
目录分开这件事比他预想的重要:
多数 CLI 判断"恢复上次会话"是相对于当前目录的,而且几个会要求你先"信任这个工作区"。一个 agent 一个目录,能把这些历史和信任决定隔开,也顺手隔开彼此的草稿文件。
模型侧,大部分 agent 共用一个 mcpproxy 实例拿工具。但有一个服务需要个人 API key,而它的数据跟通用 agent 无关。于是:
- provider 同时声明"服务端环境变量"和"调用方请求头"两种取密钥方式;
- proxy 的
.env只放 base URL,不放 key; - key 只写在那一个 OpenCode 的私有环境文件里,由它自己的 MCP 配置以 header 转发:
"mcp": { "servers": { "mcpproxy": {
"type": "remote",
"url": "http://127.0.0.1:8888/mcp",
"oauth": false,
"headers": { "X-MCPProxy-Service-Key": "{env:SERVICE_API_KEY}" }
}}}
- 启动器用一个小 Bash 包装器:
set -a; source service.env; set +a; exec opencode。set -a保证每个赋值都被导出,exec保证不多留一个 shell 进程持有环境副本。于是这个变量只存在于这棵进程树里,其余 agent 的环境里根本没有它。
其余 agent 仍然能看到这个工具,只是调用时没有 key,服务端拒绝。需要这个能力时,它们把任务委派给那个 OpenCode(herdr agent prompt / herdr agent read),自己永远不持钥匙。
但作者把话说得很清楚:这不是硬边界
- 同一个 Unix 用户下,任何有 shell 的 agent 理论上都能直接读那个
service.env; - header 走的是明文 HTTP,只因为代理监听在 loopback 才可接受;
- 要真正隔离 agent 之间,得分成不同用户或不同容器。
所以他的定性是:这是单用户机器上的"进程级最小权限",不是安全边界,我也没有当它是。 它的实际价值是:别的 agent 不会"不小心"用上它,也不会通过某个 dump 环境变量的工具把它泄出去。
(顺带一个细节:那个服务返回个人信息,provider 在到模型之前先用只用 proxy 侧持有的稳定密钥做 HMAC 假名化。稳定很重要,否则跨会话假名会变。)
七、身份挡不住的事
这件事必须说清楚,否则你会以为"做完身份就安全了"。Vitruvian 自己写了两起事故:
| 事故 | 发生了什么 | 修法 |
|---|---|---|
| 绕过 merge queue | main 允许仓库管理员跳过 merge queue(给人留的应急口子)。跑在那个账号下的 agent 继承了这个 bypass:两天里 40 次合并有 5 次跳过了队列,其中一次是8 分钟前刚被队列拒绝的提交,main 直接变红 | pre-tool hook 拒绝 gh pr merge --admin、拒绝走 REST/GraphQL 直接合并、拒绝直接 push main,并提示 agent "去修队列拒绝的问题";人真要 bypass 就在自己终端里跑,那里没有 hook |
| worktree 里卸掉 Argo CD | homelab 的 Pulumi 配置是 gitignore 的,worktree 里不存在 → 所有开关都读成 false → 一次 pulumi up 把 Argo CD 连同 namespace 和它管的每个 Application 一起删了 |
程序改成"配置没显式声明就拒绝运行";并把"永远不要在 worktree 里 apply"写进 AGENTS.md,紧挨着事故日期 |
他们的结论值得整段抄下来:
按 agent 分身份是一个"我们遵守的约定",写在每个 coding 工具都会读的AGENTS.md里:用你自己的名字;冒充别的 agent 就是把你的活记到别人头上。它让诚实的活可归属、可 review;它并不能让不诚实的活变得不可能。
真正拦住 agent 偷工减料的是另一层:事故发生之后写下的护栏。而且每次修都必须带一个"能再抓到它"的检查——这是他们的家规。
同一件事在 Safeguard 2026-10-18 的文章里被归纳成五类风险:权限继承(你能 push 生产,它也能)、审计失真(日志只显示你)、提示注入、凭据可达(环境变量和 dotfile 里的 token 对"你跑的任何东西"都可读)、长会话(开几天会累积上下文与权限)。
他们的上线建议很实操:先从最高风险的账号与任务开始(生产访问、发布、云管理),日常编码先留在沙箱里但移走长期密钥;以及一条产品化经验——如果拿一个受限 token 要花一天和一张工单,开发者就会继续用自己的身份。工具好不好用,跟策略同样重要。
八、周末最小落地清单
按依赖顺序,而不是按重要性:
- 先定"谁记在历史上":给每个 agent 一套
GIT_AUTHOR_*/GIT_COMMITTER_*和一个可回指的邮箱;确认git log里能区分(这一步 10 分钟,收益立刻可见)。 - 再做"平台认不认":一个 agent 一个 GitHub App(manifest flow),私钥进 vault,只用 installation token,安装时选 Only select repositories。
- 把归属写进提交:要么像 Obi 那样由引擎在收尾时统一写 trailer,要么像 Orquesta 那样一个动作一个 commit + 独立 author。不要指望 agent 自己记得写。
- 收紧钥匙的可见范围:per-agent 配置目录 / 独立账号 / 独立环境文件;敏感凭据走 header 注入或代理,不落在共享
.env里。 - 最后才开强制评审:
requiredApprovals是最后一步;之前先确认 agent 之间有"不同身份"可互审。 - 写护栏,而不是写期望:把事故日期写进
AGENTS.md,每个修法配一个会失败的检查;把"身份当归属,不当强制"当作设计前提。
九、附:一人 + 三个 agent 的轻量版
上面四篇的来源都是“团队 + 十几个 agent”,直接搬会很重。如果只是你自己一台机器上加两三个 agent,可以按这个阶梯上:
| 你现在的状态 | 该做的最小动作 | 为什么够用 |
|---|---|---|
三个 agent 共用一个 user.name |
只改环境变量:每个 agent 启动时一套 GIT_AUTHOR_* / GIT_COMMITTER_* |
一小时就能换来可读的 git log,不改任何平台设置 |
| 想让它们互审 | 加 trailer+分支前缀(agent/<name>/<topic>) |
没有平台身份时,“互审”本质是人工 review;先让“谁写的”可辨 |
| 想让 CI / 保护的规则真正生效 | 才需要 GitHub App(或至少几个独立账号) | App 解决的是“平台承认两个身份不同”,这是唯一无法用环境变量替代的事 |
| 有敏感凭据 | 一 agent 一个配置目录 + 一个环境文件(0600) | 让 key 只进一棵进程树,剩下的 agent 连“误用”的机会都没有 |
最小可执行版本(bash 里包一层,不动全局配置):
# ~/bin/agent-run —— 用法:agent-run wren claude
agent-run() {
local name="$1"; shift
export GIT_AUTHOR_NAME="$name"
export GIT_AUTHOR_EMAIL="$name@agents.local"
export GIT_COMMITTER_NAME="$name"
export GIT_COMMITTER_EMAIL="$name@agents.local"
export PI_CODING_AGENT_DIR="$HOME/.pi/agents/$name" # 换成对应工具的配置目录变量
exec "$@"
}
再配一个会失败的检查(否则你不知道自己有没有设对):
git log -1 --format='%an <%ae> | %cn <%ce>' # 预期:agent 名,而不是你的名字
自检五问(任一条答不上,说明还差一层):
- 出事之后,能不能从
git log里指出具体是哪个 agent 写的这一行? - 两个 agent 能不能互相批准,而且平台承认它们是两个身份?
- 某个 agent 被关闭/换掉时,能不能只吊销它一个?
- 如果某个 agent 被提示注入骗了,它最多能推送到哪里?
- 你的护栏里,有几条是事故之后写的,并且各自带一个会失败的检查?
十、边界
- 本文是编译整理,未独立复现任何一家环境:Vitruvian 的 App 批准实测、Obi 的 trailer 重写、Orquesta 的 git-native 流程、Bill Mongan 的九 agent 单机配置,都是原文作者的实践。
- 各家的具体命名(
vitruvian-*、Oby-*)只是示例;能抄的是结构:谁写归属、归属写在哪、凭据放在哪一层。 - 平台的规则会变:GitHub App 是否计入批准、
CODEOWNERS是否接受 App,都属于"文档没写、需要自己测"的范畴——上线前在 canary 仓库上验一遍,别照抄结论。 - 与"身份"相邻但不等于的东西:沙箱隔离、网络出口限制、审批门。它们解决的是"能做什么",本文解决的是"做完算谁的"。
相关阅读:AI 编程 / Vibe Coding 完全指南 | Agent / MCP / 自动化完全指南 | 多机并行 coding agent 实战栈 | 九起事故与六项控制:谁拿着 Agent 的钥匙 | Agent 审计日志:让篡改留下形状 | 从 LLM 里捞出的真 API Key | 1,866 个项目的 Agent 说明书实测
Sources
- Vitruvian Software, Every AI agent gets its own GitHub identity, 2026-10-09:https://vitruviansoftware.dev/blog/2026/10/09/every-ai-agent-gets-its-own-github-identity/
- Obi(obi-ai-bot/oby)Issue #1068, obid: commits say which job, agent and model wrote them, 2026-10-22:https://github.com/obi-ai-bot/oby/issues/1068
- Orquesta, Git-Native AI Development: Every Action is a Commit, 2026-10-23:https://getorquesta.com/blog/git-native-ai-development-every-action-is-a-commit-2026-10-23
- William M. Mongan, Herding Agents: Multi-Agent Coordination with Herdr and Per-Agent MCP Credentials, 2026-10-04:https://www.billmongan.com/posts/2026/10/herdr-multiagent-mcpproxy/
- Safeguard Security Team, What are the identity risks when an AI coding agent runs as you, 2026-10-18:https://safeguard.sh/resources/blog/when-your-ai-coding-agent-runs-as-you-identity-risks-and-fixes
- 背景参考:Sumant Thakur, I Ran Multiple Coding Agents. The Code Was the Easy Part., 2026-09-30:https://sumantthakur.substack.com/p/multi-agent-coding-workflow("可用 / 已认证 / 已证明"三态与"证据包"六项,本文第七、八节受其启发)
本文由 hubwiz 编译整理,转载请注明出处。
这是「AI 编程 / Vibe Coding」专题的一部分。完整地图见:AI 编程 / Vibe Coding 完全指南