feat(autofix): run the verification gate in an ephemeral container - #9214
Closed
wenshao wants to merge 14 commits into
Closed
feat(autofix): run the verification gate in an ephemeral container#9214wenshao wants to merge 14 commits into
wenshao wants to merge 14 commits into
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
Runs the autofix verification gate inside an ephemeral container instead of on the host, and pins the resulting trust boundary with a structural test. This is Phase 1 + Phase 2 of the design agreed in #9089.
Verification gate,Repair verification gate) now invoke a staged, digest-verified wrapper that runsrun-autofix-review-verification.shin the sandbox image the agent already uses. The image name comes from the resolve step's output (expression context), not$GITHUB_ENV, so branch code cannot choose it.docker rundoes not inherit the host environment, so inside the container there is noCI_DEV_BOT_PAT, no$GITHUB_ENV, no real$GITHUB_OUTPUT,$HOMEis a throwaway, and only three paths are mounted: the workspace, the round's workdir, and a copy-staged container temp. The realRUNNER_TEMP— which holds the staged agent runner and the PAT steps' throwaway git/gh configs — is never mounted.failedregardless of the file, and any other code leaves the outcome unset for the existing gate-crash retry path.CI_DEV_BOT_PATinvokes branch-authored code (the gate script, the agent runner,npm/npx) outside a container.Why it's needed
The gate executes the branch's own
build/typecheck/lint/test. On the host that code shares the OS user,$HOME,$GITHUB_ENVand$GITHUB_OUTPUTwith the same job's later PAT-bearing steps, and #8961's review showed several channels that no in-step guard can reach:BASH_ENV/BASH_FUNC_*— applied at the step shell's startup, before line 1 (the runner's$GITHUB_ENVblocklist isNODE_OPTIONS-only).LD_PRELOAD/LD_AUDIT— mapped by the loader into the step's own bash at exec time, with the PAT already inenviron.$GITHUB_OUTPUT— a runner-provided file the gate-run branch code can appendoutcome=fixedto, regardless of any digest check on the gate script's bytes.The trust boundary cannot be a job boundary. A job that has executed attacker code cannot vouch for anything it emits (its outputs, its "gate passed" verdict, even a signature), and a second job that re-verified would have to run that same code — while holding the PAT. So the boundary has to be an execution-isolation boundary the PAT never crosses: the container wall.
Reviewer Test Plan
How to verify
npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js— 174/174. Two new tests: one pins the container invocation (wrapper + both digests, image fromsteps.sandbox.outputs.image, the exact--envallowlist and--volumeset,--user/--rm) and drives the verdict translation behaviourally; the other is the PAT/branch-code invariant.ghcr.io/qwenlm/qwen-code), not a mock:CI_DEV_BOT_PATvisible inside the container<absent>✅$GITHUB_ENVvisible inside<absent>✅RUNNER_TEMPreachable inside$GITHUB_OUTPUTinside$GITHUB_ENVafter the runoutcome=fixedin the verdictoutcome=failed+retryable=true✅Evidence (Before & After)
Before:
bash "${RUNNER_TEMP}/run-autofix-review-verification.sh"on the host — branch build/test in the same execution context as the PAT.After:
bash "${RUNNER_TEMP}/run-autofix-gate-container.sh"→docker runwith an explicit six-entry env allowlist and three mounts; the PAT/$GITHUB_ENV/host$GITHUB_OUTPUTare absent by construction.Tested on
Risk & Scope
docker infoand the agent already runs in this image against this same workspace with the samenpm run build/typecheck/lint+npx vitestcommands, so the toolchain is proven — but the first real round is where a mount/permission surprise would show.--user $(id -u):$(id -g)keeps container writes owned by the runner user (the ownership-restore step's failure mode). A docker failure is a gate crash (retry), never a silent pass.issue-autofixjob's inline gate (same class, but agent-authored branch rather than an external contributor's PR) and Phase 3 (per-job ephemeral runners, a pool-infra decision) — both stay tracked in autofix: PAT-bearing jobs share a host with untrusted branch code — needs runner-level isolation #9089.Linked Issues
中文说明
本 PR 做了什么
把 autofix 的验证门放进临时容器执行(不再在宿主上),并用结构性测试钉住由此建立的信任边界。这是 #9089 中已确认设计的阶段 1 + 阶段 2。
Verification gate、Repair verification gate)现在调用一个暂存且经 digest 校验的 wrapper,由它在 agent 已在使用的 sandbox 镜像中运行run-autofix-review-verification.sh。镜像名取自 resolve 步骤的输出(expression context)而非$GITHUB_ENV,因此分支代码无法选择它。docker run不继承宿主环境,故容器内没有CI_DEV_BOT_PAT、没有$GITHUB_ENV、没有真实的$GITHUB_OUTPUT,$HOME是一次性目录,且只挂载三个路径:workspace、本轮 workdir、以及拷贝暂存的容器 temp。真实的RUNNER_TEMP(存放暂存的 agent runner 与 PAT 步骤的一次性 git/gh 配置)从不挂载。failed(无视文件内容),其他退出码则不设置 outcome,走既有的门崩溃重试路径。CI_DEV_BOT_PAT的步骤在容器之外调用分支编写的代码(门脚本、agent runner、npm/npx)"时失败。为什么需要
门执行的是分支自己的
build/typecheck/lint/test。在宿主上,这些代码与同一 job 后续携带 PAT 的步骤共享 OS 用户、$HOME、$GITHUB_ENV与$GITHUB_OUTPUT,而 #8961 的评审已证明有几条通道任何步骤内的防御都够不到:BASH_ENV/BASH_FUNC_*—— 在步骤 shell 启动时即生效,早于第 1 行(runner 对$GITHUB_ENV的屏蔽名单只有NODE_OPTIONS)。LD_PRELOAD/LD_AUDIT—— 由加载器在 exec 步骤自身 bash 时映射进去,此时 PAT 已在environ中。$GITHUB_OUTPUT—— runner 提供的文件,门中运行的分支代码可直接追加outcome=fixed,无论 digest 如何校验门脚本的字节。信任边界不能是 job 边界。 执行过攻击者代码的 job 无法为其产出的任何东西背书(输出、"门已通过"的裁决、甚至签名),而负责重新校验的第二个 job 又必须运行同一批代码——同时还持有 PAT。因此边界必须是一道 PAT 永不跨越的执行隔离边界:容器墙。
审阅者测试计划
如何验证
npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js—— 174/174。两个新测试:其一钉住容器调用(wrapper + 两个 digest、镜像取自steps.sandbox.outputs.image、精确的--env白名单与--volume集合、--user/--rm)并对裁决翻译做行为性验证;其二是 PAT/分支代码不变量。ghcr.io/qwenlm/qwen-code)探针验证,非 mock:CI_DEV_BOT_PAT<absent>✅$GITHUB_ENV<absent>✅RUNNER_TEMP$GITHUB_OUTPUT$GITHUB_ENVoutcome=fixedoutcome=failed+retryable=true✅证据(Before & After)
Before:宿主上
bash "${RUNNER_TEMP}/run-autofix-review-verification.sh"—— 分支 build/test 与 PAT 处于同一执行上下文。After:
bash "${RUNNER_TEMP}/run-autofix-gate-container.sh"→docker run,带显式六项 env 白名单与三个挂载;PAT/$GITHUB_ENV/宿主$GITHUB_OUTPUT由构造上即不存在。测试平台
风险与范围
docker info预检,且 agent 本就在这个镜像中、对同一 workspace 运行同样的npm run build/typecheck/lint与npx vitest,工具链已被证明可用——但首个真实轮次才是挂载/权限意外会暴露的地方。--user $(id -u):$(id -g)确保容器写入仍归 runner 用户所有(即 ownership-restore 步骤所针对的故障模式)。docker 失败属于门崩溃(重试),绝不会成为静默通过。issue-autofixjob 的内联门(同类问题,但分支由 agent 编写而非外部贡献者 PR)与阶段 3(每 job 临时 runner,属池基础设施决策)——两者继续在 autofix: PAT-bearing jobs share a host with untrusted branch code — needs runner-level isolation #9089 跟踪。关联 Issue