把开源编码 Agent opencode 挂到代码托管平台:权限与边界

2026-08-04

本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。

这两条集成线看起来对称,实际上只有 GitHub 那条自带”谁能指挥它”的闸门,GitLab 那条的闸门得你自己装。 你如果按同一套心智模型去配,很可能在 GitLab 上做出一个”任何能触发流水线的人都能让它改代码并推分支”的东西,而它跑起来一切正常,你不会收到任何警告。

先把名字说清楚:opencode 是小写起首的开源项目名,指的是这个跑在终端里的编码 Agent 仓库,不是泛指的开源代码,也不是任何一个同名相近的模型。它用 MIT 许可证(LICENSE,Copyright 2025 opencode)。

站内已有的 GitHub Copilot 使用教程 讲的是编辑器里的日常补全与对话,Copilot 编码 Agent 拆解Cursor 后台 Agent 拆解 讲的是各家托管形态的后台任务;这篇只管一件事——一个你自己装、跑在你自己 runner 上的开源 Agent,挂到托管平台上时,权限链条是怎么串起来的。


一、这两条线的形状根本不一样

GitHub 那条是项目自己维护的一个 Action。仓库根下有独立的 github/ 目录,里面 action.yml 是 Action 的声明。同一套流程在仓库里存着两份实现:github/index.ts 是一份可以用 bun 直接跑起来的独立入口,github/README.md 里那段用 MOCK_EVENT 本地试跑的说明用的就是它;而 action.yml 的最后一步只执行 opencode github run,落到的是 CLI 里的 packages/opencode/src/cli/cmd/github.handler.ts。两份实现的行为并不完全一致,第三节会看到一处要命的差异。触发方式是在 issue 或 PR 评论里提 /opencode/oc,它在你自己的 GitHub Actions runner 上跑完,再把结果写回评论、提交、或者开一个 PR。

GitLab 那条在文档里分成两支。一支是普通的 GitLab CI 流水线,文档指向的是一个社区做的 CI 组件 nagyv/gitlab-opencode,你在 .gitlab-ci.ymlinclude 它,通过 config_dirauth_jsoncommandmessage 这几个输入把配置目录、认证 JSON 和初始提示喂进去。另一支是接 GitLab Duo:在评论里提 @opencode(触发词可以改成别的),任务在 GitLab CI 流水线里跑。

差别在于中间那层。GitHub 线有一个官方 GitHub App(https://github.com/apps/opencode-agent),Action 用 Actions 的 OIDC 令牌去换这个 App 的安装访问令牌,评论、提交、开 PR 都以 App 身份出现。GitLab 线没有这样一个中间件——文档给的流程配置样例是从零拼的:拉 node:22-slim 镜像、npm install --global opencode-ai、装 glab 命令行、把模型服务商的密钥写进 ~/.local/share/opencode/auth.json、配 git 身份、然后 opencode run 跑一段提示词,最后自己判断有没有改动、自己 git addgit push

这不是谁做得好谁做得差的问题,是两条线所处的位置不同:一条是产品化的集成,一条是把 CLI 塞进流水线的模式示范。你要为此付的代价,后面第四节会算。


二、GitHub 线:一次 /oc 背后跑了哪几步

github/action.ymlruns 部分是 composite,步骤很朴素:先 curl 取仓库最新 release 的 tag_name 作为版本,用 actions/cache@v4 缓存 ~/.opencode/bin,缓存没命中就跑安装脚本,把 $HOME/.opencode/bin 加进 $GITHUB_PATH,最后执行 opencode github run。你在 with: 里填的每一个输入,都被映射成大写环境变量传给这一步:

      env:
        MODEL: ${{ inputs.model }}
        AGENT: ${{ inputs.agent }}
        SHARE: ${{ inputs.share }}
        PROMPT: ${{ inputs.prompt }}
        USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
        MENTIONS: ${{ inputs.mentions }}
        VARIANT: ${{ inputs.variant }}
        OIDC_BASE_URL: ${{ inputs.oidc_base_url }}

真正干活的逻辑在运行侧。按 github.handler.ts 里的顺序,它会先判断事件类型受不受支持,然后换令牌、解析用户提示词、改 git 配置、校验权限、给评论加一个 eyes 反应,接着建会话跑模型,最后按事件类型分三种走法:issue 走”新建分支 → 有改动就提交 → 开 PR”,同仓库的 PR 走”checkout 那个分支 → 直接提交上去”,fork 来的 PR 则会先 git remote add fork,把改动推回对方的分支。

有几个细节值得你在排查时记住。分支名是有规律的:issue 和 fork PR 这类有 issue 号的,格式是 opencode/ 加事件类型、issue 号和一个时间戳;scheduleworkflow_dispatch 没有 issue 号可用,就换成一段随机短串加时间戳。提交身份不用你在 workflow 里预置——CLI 实现在配置 git 那步会把全局 user.nameuser.email 直接写成 opencode-agent[bot] 和它对应的 noreply 邮箱;请注意那两条 git config 带的是 --global,同一个 runner 上后续步骤的提交署名也会跟着变。提交信息里会带一行 Co-authored-by,署的是触发这次运行的那个人(定时任务那条路径没有发起人,就不带这一行)。还有一个容易被忽略的判断:如果模型自己在会话里切走了分支(比如它自作主张开了新分支和 PR),外层会识别出这种情况并跳过自己那套推送逻辑,只把回复贴出来——日志里会打印一句它”自己管了分支”。

组成部分它负责什么对应仓库位置你什么时候会碰到它
Action 声明定义 modelagentsharepromptuse_github_tokenmentionsvariantoidc_base_url 这些输入,装二进制并调起 opencode github rungithub/action.yml写 workflow、想确认某个 with: 参数到底存不存在
独立入口实现同一套流程的另一份实现:解析事件负载、换令牌、校验权限、建分支、推提交、回写评论;action.yml 并不调用它,github/README.md 的本地 MOCK_EVENT 调试走的是它github/index.ts想在本地把流程跑一遍,或者对照两份实现的差异
CLI 子命令入口opencode github installopencode github run 两个命令的定义packages/opencode/src/cli/cmd/github.ts本地跑安装向导、或者想带 --event/--token 本地试跑
CLI 侧实现生成默认 workflow 文件、事件路由、权限断言、会话配置、令牌回收packages/opencode/src/cli/cmd/github.handler.ts想知道默认 workflow 长什么样、权限门槛卡在哪一行
GitHub 集成文档支持的事件对照表、权限位写法、schedule/pull_request/issues 三类示例packages/web/src/content/docs/github.mdx第一次配非评论触发的自动化
GitLab 集成文档CI 组件的输入说明与 Duo 流程配置样例packages/web/src/content/docs/gitlab.mdx在 GitLab 上从零搭这条线

事件分两类,这个分类直接决定了安全行为。issue_commentpull_request_review_commentissuespull_request 属于有真人发起的一类,有 actor、有 issue 号,会加表情反应、会回评论、会做权限校验scheduleworkflow_dispatch 属于另一类,没有 actor 可查,因此不做权限校验也不加反应,输出只进日志和 PR。文档里对这一点说得很直白:定时任务跑起来时没有用户上下文可以做权限检查,所以如果你希望它建分支开 PR,就得在 workflow 里显式给 contents: writepull-requests: write

另外,非评论类的事件需要你自己给 prompt——issuesscheduleworkflow_dispatch 都没有评论正文可以提取指令。pull_request 是个例外:不给 prompt 时它默认去评审这个 PR。


三、令牌怎么来,决定了谁能指挥它

这是整条链上最该看懂的一段。

默认路线是 OIDC。Action 用 core.getIDToken("opencode-github-action") 取一个 Actions 的 OIDC 令牌,拿去换 GitHub App 的安装访问令牌,换取地址默认指向项目的 API 服务,oidc_base_url 这个输入就是给自建 App 安装场景留的覆盖点。走这条路,workflow 里必须有 id-token: write,否则拿不到 OIDC 令牌——代码里对应的报错原文就是提醒你去加这一行。

拿到令牌之后,紧接着是权限断言:

        const response = await octoRest.repos.getCollaboratorPermissionLevel({
          owner,
          repo,
          username: actor!,
        })

        permission = response.data.permission
      ...
      if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)

发起评论的人必须对这个仓库有 adminwrite 权限,否则直接抛错。这道闸门挡的是最要命的一类攻击面:一个路人在你的公开仓库 issue 里发一句 /oc 把 CI 里的密钥打印出来

第二条路线是 use_github_token: true,直接用 runner 内置的 GITHUB_TOKEN(这条路要求你在 env 里把 GITHUB_TOKEN 传进去,没传会直接报错),跳过 OIDC 换令牌,也就不需要装那个 GitHub App。

这里就是前面说的那处要命差异,值得你自己回仓库看一眼再决定。 独立入口 github/index.ts 的权限断言函数一进来就有一段提前返回:判断出走的是环境里的 GitHub 令牌,就打印一句 skipped (using github token) 然后直接 return,后面的协作者权限查询根本不执行。而 action.yml 实际调起的那份 CLI 实现里没有这个提前返回——它的判断条件只有事件类型:只要落在有真人发起的那一类,就先 assertPermissions() 再加表情反应,令牌是从 App 换来的还是 runner 内置的,并不影响这一步跑不跑。

对你的意义是:既不要把”用了 GITHUB_TOKEN 就没人管我”当成结论,也不要反过来把”它一定会替我校验发起人”当成保障——两份代码给的答案不一样,而你装的是哪个版本、跑到的是哪条路径,得自己确认。稳妥的做法是不依赖它:把触发限制在特定事件、用 if: 表达式过滤、或者在 Action 之前自己加一步身份检查。文档里那个 issue 自动分诊的示例就示范了一种做法——先用一步脚本查发起人账号注册时长是否达到阈值,不达标就把后面几步整个跳过。

文档的配置说明里还列了第三条路:给一个 token(PAT 或别的访问令牌),让评论、提交、开 PR 都以这个令牌的身份进行。但这一条要多留个心眼——github/action.yml 的输入清单里只有 modelagentsharepromptuse_github_tokenmentionsvariantoidc_base_url 这八项,并没有 tokengithub.mdx 那段手动 workflow 里相关的那行也是注释掉的。文档说明和 Action 声明在这一项上没有对齐,配之前请以 action.yml 为准,别照着文档写完了纳闷为什么不生效。

走 OIDC 那条路时还有两个收尾动作容易被忽视:Action 会把换来的令牌写进本地 git 配置的 http.https://github.com/.extraheader 里好让 push 能过,跑完在 finally 里把原来的配置还回去,并且发一个 DELETE 请求去吊销这个安装令牌。用 GITHUB_TOKEN 的路线不做这两步——因为那个令牌不归它管。这也是为什么 checkout 那步文档一律写 persist-credentials: false:别让 checkout 留下的凭据和它自己写的凭据打架。

权限该怎么切,可以对照 Agent 最小权限设计 里的思路来做,密钥这块则参考 API 密钥安全管理:这些密钥是以仓库或组织 secret 的形式放进 runner 环境的,一旦触发门槛没设好,等于把密钥暴露给了能触发它的所有人。


四、GitLab 线:能力相近,托底的东西要你自己补

先说 CI 组件那支。你把认证用的 JSON 存成 File 类型的 CI 变量,文档明确要求勾上”Masked and hidden”,然后在 .gitlab-ci.ymlinclude 那个组件,用 config_dir 指向一个配置目录。这个 config_dir 是有实际价值的:不同的 job 可以指向不同的配置目录,从而按调用场景开关功能——评审用的那个 job 完全可以配成不允许写文件。

Duo 那支的流程配置样例值得逐行读一遍,因为它把所有”平时被封装起来的东西”都摊开了。它把上下文通过三个变量传进提示词:

        opencode run "
        You are an AI assistant helping with GitLab operations.

        Context: $AI_FLOW_CONTEXT
        Task: $AI_FLOW_INPUT
        Event: $AI_FLOW_EVENT

也就是说,Agent 拿到的”我该干什么”完全来自平台注入的这几个变量,而它读写 GitLab 数据靠的是 glab 命令行——样例里先 export GITLAB_TOKEN=$GITLAB_TOKEN_OPENCODE,把这个令牌配好之后跑一句 glab issue list 做连通性验证,再在提示词里明确告诉模型”用 glab 访问 GitLab 数据,它已经认证过了”。

这里就是权限的真正落点:GITLAB_TOKEN_OPENCODE 这个令牌能干什么,Agent 就能干什么。文档的准备清单里有一步是”创建一个服务账号”,那一步不是走过场——服务账号的角色和它在哪些项目上有权限,是这条线上唯一的边界。GitHub 那条线上由 App 令牌 + 发起人权限断言两道来卡的事,在这里全压在这一个令牌上。

推送逻辑也是明写在样例里的:跑完之后 checkout 到 $CI_WORKLOAD_REF,检查工作区和暂存区有没有变化、有没有未跟踪文件,有就 git add .、提交、推送。提示词里那段 <important> 还特意跟模型说”你不需要自己 commit 和 push,那些会根据你的文件改动自动完成”。这个设计的意思是:模型只管改文件,落盘动作交给流水线。好处是行为可预期;代价是 git add . 是无差别的——模型在工作区里生成的任何临时文件、调试输出、缓存目录,只要没被 .gitignore 挡住,都会一起进提交。


五、边界与代价:它明确不管的事

它不替你判断这次改动该不该合。 两条线的终点都是”提一个 PR/MR”或者”往分支上推一次提交”,评审和合并仍然是你的事。GitHub 线上那个自动评审的示例甚至刻意把权限写成只读(contents: readpull-requests: readissues: read),说明”评审”这个用法本来就不该带写权限。

它不做交互式确认。 CLI 侧建会话时显式塞了一条权限规则:question 这类权限,匹配模式写成 *,动作是 deny——在无人值守的 runner 里没人能回答问题,与其挂在那儿等,不如一律拒绝。这意味着模型遇到模糊需求时不会停下来问你,它会按自己的判断往下做:提示词写得含糊,产出就会漂。

会话分享的默认值要看仓库可见性。 代码里的判断是:显式设成 false 就不分享;没显式设置且仓库是私有的,也不分享;剩下的情况(公开仓库、没设置)会创建分享链接。你如果在公开仓库上跑,默认会生成一个可以外部访问的会话记录页。这不是漏洞,是默认值,但你得知道它是这样。

它在 runner 上是有实权的。 它会执行 shell 命令、直接改工作区里的文件、把仓库内容和 issue/PR 上下文发给你配置的模型服务商。三个后果都是真实存在的:命令写错可能删掉不该删的东西;git add . 之后一次提交可能带上不该进仓库的文件;私有代码的外泄面从”你的机器”扩大到了”你的 runner + 模型服务商”。各家模型服务商对数据留存的规则不同且会调整,以官方最新说明为准。

触发面比你想的宽。 GitHub 线默认的触发词是 /opencode/oc,可以用 mentions 输入换成自定义的、逗号分隔的一组词。/oc 只有三个字符,普通评论里意外撞上并不难。安装向导生成的 workflow 用的是”以它开头或者前面有空格”这种更严的匹配条件,而文档手动配置那段用的是宽松的 contains——这两者不等价。

跨仓库场景要额外小心。 fork 来的 PR 会走”加 fork remote 然后推回对方分支”的路径。这条路径本身是对的,但它意味着你的 workflow 会向一个你不控制的仓库写入内容,触发条件宽松时后果比同仓库的情况更难收拾。


六、上手与避坑清单

别照抄文档里那段手动 workflow 的缩进。 github.mdx 手动设置那段 YAML 里,uses: 和它上面的 - name: Run OpenCode 缩进对不齐,直接复制会在 YAML 解析阶段就失败。第一次配优先跑 opencode github install,让它写文件;要手写就以生成出来的那份为模板。

先想清楚走不走 App 那条路,再决定权限位怎么写。 走 OIDC 换 App 令牌,workflow 里只需要 id-token: write,其余的写权限由 App 的安装权限提供;走 use_github_token: true,就得自己把 contentspull-requestsissues 的写权限按需补齐,同时把”谁能触发”这道闸门自己补上,别依赖某一份实现替你做了发起人校验。两套配置混着抄,症状是”要么拿不到令牌,要么明明能跑却推不上去”。顺带一提,安装向导生成出来的那份 workflow,权限位给的是 id-token: writecontentspull-requestsissues 三个 read——默认就是只读的,你要它真的改代码开 PR,得自己把对应的位改成 write,这一步是有意让你手动过一遍的。

定时任务和手动触发一定要单独审一遍。 这两类事件没有 actor 可以校验,等于把闸门整个拿掉了。给它们的 prompt 要写死范围,权限位能给读就别给写;确实要它开 PR,就接受”这个 workflow 有能力自动往仓库里写东西”这个事实并据此评估。相关的判断标准可以参照 Agent 权限给太大会怎样

GitLab 侧别把令牌当配置项随手填。 那个 GITLAB_TOKEN_OPENCODE 就是整条线的权限上限。用服务账号、按项目授权、CI 变量记得设成受保护和掩码——文档对认证 JSON 的要求写的是”Masked and hidden”,令牌同理。

照抄 GitLab 样例前,把里面的示例值全换掉。 那段流程配置的 push 目标写的是一个演示项目的路径,提交信息写的是 Codex changes——那是示例作者留下的痕迹。不改就上,轻则提交历史莫名其妙,重则推到一个根本不存在的地址上,任务看着”跑完了”其实什么都没落。

先弄清提交身份由谁来定。 这三处的做法各不相同:CLI 实现在配置 git 那步自己 git config --global 把身份写成 opencode-agent[bot];独立入口实现反过来,推送前先查 user.nameuser.email 有没有配,缺了就抛一句”Git author identity is missing”直接终止;GitLab 的样例则是在流程里自己 git config --global 写死一个身份。后一种情况的表现是”模型明明改完了,结果什么都没提交”,看日志才知道卡在身份检查上;前一种情况的副作用是全局身份被改,同一 runner 上后续步骤的提交署名也会变。

别把 git add . 当成安全操作。 两条线在有改动时都会把工作区整体加进暂存区。跑之前确认 .gitignore 覆盖了构建产物、依赖目录、临时文件;否则模型顺手生成的中间文件会跟着进仓库,而这类改动在 PR 里最不容易被看出来。

在公开仓库上先确认分享行为符合预期。 不想生成外部可访问的会话记录,就把 share 显式设成 false,别依赖默认值。


opencode 这两条集成线,本质上是同一个终端 Agent 的两种外包装:GitHub 那条把身份、令牌、权限校验、令牌回收都封进了 Action,你要做的是把权限位写对;GitLab 那条把这些摊在你面前,你要做的是自己补上那道”谁能指挥它”的闸门。

上线前过一遍这四问:触发它的人是谁、这个人有没有被校验过;它手上那个令牌能写哪些仓库;改动落盘的路径是什么、git add 会扫到哪些文件;密钥在 runner 里以什么形式存在、谁能让 runner 跑起来。四个问题都能答出来,再打开自动触发。

想继续往下读,顺序建议是:先 packages/web/src/content/docs/github.mdx 里那张事件对照表,把哪些事件会做权限校验搞清楚;再 github/action.yml 确认输入项;最后 packages/opencode/src/cli/cmd/github.handler.ts,那里是权限断言、事件路由和令牌回收的原文。

本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 终端编码 Agent opencode 的 skills 用法与分工opencode 的诊断与格式化两条线:语言服务器怎么报错、格式化器何时跑

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。