接入 GitHub,我们为什么选择给 CLI 注入凭据

Flashduty AI SRE 技术揭秘第 10 篇:通过 CLI 注入凭据接入 GitHub 和 GitLab,处理多组织 token 路由、凭据过期和长任务认证。

作者 快猫星云

暖色手绘封面:工匠的扳手上系着一把钥匙,表达为现有工具注入凭据

不给新工具,给旧工具系上一把钥匙。

Flashduty AI SRE是一个专注 SRE 领域、帮助团队分析故障根因和完成日常运维工作的 AI Agent 产品,注册即可免费试用。这是系列文章《Flashduty AI SRE 技术揭秘》第 10 篇,本系列会持续分享我们在 agent 工程上的真实设计、踩过的坑和做过的取舍。

一次故障排查可能需要查最近合入的代码,或看看某个仓库的提交记录。Agent 要完成这些操作,首先得能访问客户的代码仓库。

我们对接 GitHub 和 GitLab 时,没有为这些操作再加一组 MCP 工具,而是把凭据交给执行命令的环境,让 agent 使用 gh、glab 和 git 等已有的命令行工具。这样做省去了定义和维护一批工具接口,但认证问题仍然要解决:同一客户有多个 GitHub 组织时该用哪个 token?一条命令运行到一半,token 过期了怎么办?

这篇文章讲我们怎么处理这些问题,也讲一次凭据在长命令中途过期的事故。

接入外部系统,不一定要增加工具

先说一下这里的两种接入方式。我们的 Connector 通过 MCP 给 agent 提供新的工具:每个工具有自己的名称、参数和说明,模型按定义调用。另一种方式我们称为 App:不增加工具,而是让已有的 shell 环境拿到访问外部系统所需的凭据。GitHub 和 GitLab 采用的是后一种方式。

原因之一是命令行工具已经覆盖了不少常见操作。模型可以用 gh pr view 查看 Pull Request,用 git log --oneline 查看提交记录;不必为了每个操作再定义一个我们自己的接口。这并不意味着 CLI 永远比 MCP 好用,但对于 GitHub、GitLab 这样的系统,我们可以直接利用模型对常见命令的已有认识。

命令行还有组合的优势。例如,gh api 取回数据后,可以接上 jq 筛选 JSON,再用其他文本工具缩小范围。第 8 篇讲过,我们会限制工具返回给模型的输出,把超长内容保存下来供后续处理;CLI 的文本结果也可以沿着这条路径处理,不需要把一大份结果全部塞进模型上下文。

少加工具还有一笔上下文账。工具定义要随模型请求发送,定义越多,请求中的工具前缀就越大,也会影响第 5 篇讨论过的缓存和成本。复用已有的 bash 工具,不需要再为每项仓库操作增加一份 schema。

凭据怎样交给命令

接入的第一步是让命令有身份。每次派发 bash 命令时,云端会把所需凭据放进这次执行的环境变量里。GitHub 主组织的 token 使用 GH_TOKEN;如果客户还连接了其他组织,则使用 GH_TOKEN_1、GH_TOKEN_2 等槽位。

gh 可以读取环境变量,但 git clone、git push 等 Git 操作还需要另一条认证路径。Git 支持通过 credential helper 获取凭据:访问远端仓库时,Git 向 helper 询问用户名和密码,helper 再把相应凭据交给 Git。我们的 helper 是一段内联 shell 脚本,通过环境变量注入 Git 配置,不写入 .git/config。脚本只包含 token 环境变量的名称,不包含 token 的值,以免有人查看 git config -l 时直接看到凭据。

这段 helper 使用 POSIX sh,不依赖 gh 二进制。因此,即使执行环境里只有 Git 和 /bin/sh,也可以完成 HTTPS 认证。

还有一个容易忽略的细节:安装自己的 helper 前,我们会先清除宿主环境中可能残留的 credential helper。否则,宿主上已有的 helper,例如 gh auth git-credential,可能先用宿主的凭据应答。命令表面上仍在访问 GitHub,实际使用的身份却不是我们为当前客户选择的身份,组织路由也就失效了。

多个组织,怎么选 token

一个客户可能在多个 GitHub 组织中安装我们的 GitHub App。每次安装对应一个 installation,由它签发的 token 只能访问这次安装获授权的资源。比如客户在组织 A 和组织 B 都安装了 App,访问 A 的仓库时,不能拿 B 的 installation token 代替。

这个问题的症状不一定直观。用错 token 访问仓库时,GitHub 可能返回 404,而不是 403:对调用方来说,它看到的是「找不到仓库」,不一定能分辨仓库不存在还是当前身份无权访问。

我们在 2026 年遇到过一次这样的事故。旧实现只取最新安装的 token;客户在第二个组织安装 App 后,系统开始使用第二个组织的 token,结果第一个组织的仓库全部返回 404。token 没有过期,问题出在它属于另一个 installation。

修复后,Git credential helper 根据仓库 URL 的 owner 选择 token:访问哪个组织的仓库,就返回对应 installation 的凭据;没有匹配到时,才回退到主槽位。gh CLI 通常读取一个 GH_TOKEN,所以我们在命令前注入一个 shell 包装函数,根据目标仓库参数或当前 checkout 的 origin 选择 token,再调用真正的 gh。

默认使用哪个 token 也需要稳定的规则。我们按首次连接时间排序,选择最早连接的组织作为主槽位,而不是每次都用最新安装的组织。这样,新接入一个组织不会悄悄改变默认身份;主组织卸载重装后,只要系统保留了用于排序的首次连接时间,也不会因此改变默认身份。

一小时的 token,为什么会在 20 分钟后过期

选对组织还不够,token 必须在命令需要它的时候仍然有效。

GitHub installation token 的有效期是一小时。最初,我们在一轮对话开始时解析一次 token,之后这轮里的命令都复用它。这个办法省去了每次派发时重新解析的工作,却没有考虑一轮开始时拿到的 token 可能已经用了很久。

事故发生时,某一轮拿到的 token 只剩 18.9 分钟有效期。一条 bash 命令运行到第 20.3 分钟时,GitHub 开始返回 401:命令还在执行,凭据已经过期。我们的 bash 命令最长可以运行 30 分钟,所以只看「签发后有效一小时」并不足够,得看派发时还剩多少时间。

我们随后调整了三个地方。

每次派发时重新解析

现在,每次派发 bash 命令都会重新解析 token,不再把一轮开始时的结果固定下来。这里的「重新解析」不等于每次都向 GitHub 签发新 token:系统可以复用符合条件的缓存,必要时才重新签发。

解析失败时,系统记录日志,并尝试使用手头已有的 token 继续派发。这是一个可用性上的取舍:GitHub API 暂时出问题时,不让签发环节直接堵住整条命令派发链路;代价是已有 token 可能过期,命令仍可能因为认证失败。签发请求本身设置了 15 秒超时。

给缓存设一个剩余寿命下限

GitHub token 按 installation 缓存在 Redis 中。只有剩余寿命达到 35 分钟的 token 才能作为缓存命中使用;低于这个下限,就需要获取新的 token。

35 分钟来自一笔时间账:单条 bash 命令最多运行 30 分钟,再留 5 分钟余量。这样,在正常解析并命中缓存的路径上,派发时拿到的 token 应当能覆盖整条前台命令的运行时间。它不是所有情况下的保证:上面说的解析失败、继续使用已有 token,就是一条例外路径。

后台任务单独处理

后台任务的生命周期可以超过普通前台命令。即使缓存 token 还剩 35 分钟,对一个长时间运行的后台进程也可能不够。因此,使用 run_in_background 派发时,我们绕过缓存,直接签发一个新的 GitHub token。

新 token 能提供完整的一小时有效期,但这并不等于后台进程可以无限期使用它。任务如果超过 token 的寿命,或者在更晚的时候才访问 GitHub,仍需要面对凭据过期的问题。

固定 token 与每次派发时检查剩余有效期的时间线对比

上:一轮开始时固定 token,长命令可能跨过有效期;下:每次派发重新解析,并检查缓存 token 的剩余寿命。

为什么 GitLab 没有照搬这套机制

同一个系统中,GitLab bot token 仍然是在一轮对话开始时解析,然后在这一轮里复用。它的有效期是 90 天,系统会在到期前 14 天自动轮换。与 GitHub 的一小时 token 相比,它的生命周期长得多,不需要仅仅为了覆盖一条最长 30 分钟的命令,就在每次派发时重新解析。

两种处理方式的差别来自凭据寿命、轮换时机和命令执行时长。选方案时,需要把这些时间放在一起算;只看 token 的标称有效期,或者只看命令通常运行多久,都可能漏掉临近过期时的情况。

凭据还留下哪些边界

向 GitHub 签发 installation token 时,我们没有另外请求缩小权限或仓库范围,而是以 GitHub App 的配置为授权范围的依据。这样平台不需要再维护一份独立的权限配置,但也意味着 App 获得了哪些权限,必须在 App 的授权配置中管好。

installation token 不写入数据库;需要缓存时放在 Redis 里。前面说的 Git helper 也不把 token 值写进 Git 配置。不过,token 既然通过环境变量交给了 bash 进程,就需要继续处理另一个问题:agent 能不能运行 echo $GH_TOKEN,把凭据读出来?

可以。这也是下一篇要讨论的内容:当 agent 能执行 shell 命令时,bash guard 如何限制凭据泄露,以及这道防线有哪些边界。


这是「Flashduty AI SRE技术揭秘」系列的第十篇。我们在做一个专注 SRE 领域、帮助团队分析故障根因的 AI Agent 产品,本系列会持续分享我们在 agent 工程上的真实设计、踩过的坑和做过的取舍。

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云