多 Agent 同机:Git 作者、凭据与提交归属怎么分

十个 agent 共用一个 GitHub 账号,你得到的是署名,失去的是归属、互审与审计。四个层面拆开:Git 作者、平台身份、提交里的三行、谁手里有钥匙。

分享
多 Agent 同机: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"。

安全顺序:

  1. 每个 agent 建并安装一个 App;
  2. 私钥进 vault,把每个 agent 的 harness 指向它;
  3. 把每个 agent 加进 agentApps;
  4. 最后才提高 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 要花一天和一张工单,开发者就会继续用自己的身份。工具好不好用,跟策略同样重要。

八、周末最小落地清单

按依赖顺序,而不是按重要性:

  1. 先定"谁记在历史上":给每个 agent 一套 GIT_AUTHOR_* / GIT_COMMITTER_* 和一个可回指的邮箱;确认 git log 里能区分(这一步 10 分钟,收益立刻可见)。
  2. 再做"平台认不认":一个 agent 一个 GitHub App(manifest flow),私钥进 vault,只用 installation token,安装时选 Only select repositories。
  3. 把归属写进提交:要么像 Obi 那样由引擎在收尾时统一写 trailer,要么像 Orquesta 那样一个动作一个 commit + 独立 author。不要指望 agent 自己记得写。
  4. 收紧钥匙的可见范围:per-agent 配置目录 / 独立账号 / 独立环境文件;敏感凭据走 header 注入或代理,不落在共享 .env 里。
  5. 最后才开强制评审:requiredApprovals 是最后一步;之前先确认 agent 之间有"不同身份"可互审。
  6. 写护栏,而不是写期望:把事故日期写进 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 名,而不是你的名字

自检五问(任一条答不上,说明还差一层):

  1. 出事之后,能不能从 git log 里指出具体是哪个 agent 写的这一行?
  2. 两个 agent 能不能互相批准,而且平台承认它们是两个身份?
  3. 某个 agent 被关闭/换掉时,能不能只吊销它一个?
  4. 如果某个 agent 被提示注入骗了,它最多能推送到哪里?
  5. 你的护栏里,有几条是事故之后写的,并且各自带一个会失败的检查?

十、边界

  • 本文是编译整理,未独立复现任何一家环境: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

本文由 hubwiz 编译整理,转载请注明出处。

这是「AI 编程 / Vibe Coding」专题的一部分。完整地图见:AI 编程 / Vibe Coding 完全指南