ADR 0145 — Docker 成为 runner 模式的真正前提
Context
runner 模式(ADR 0104)为住在用户文件夹中、没有到服务器的 bind-mount、并由在其机器上运行的 CLI(brabo-runner)确认的代码而存在。自 ADR 0137(RN-497)起,同一个 CLI 也会启动用户机器上的 Docker 容器,而 Engine.Actions.TerminalExecutor 只要 workspace 已验证且 runner 已连接,就会把每一个已批准的终端命令路由到 runner——而不检查是否有任何 running 容器。宿主机还是容器的选择完全在 runner 内部(EstadoDoRunner.containerAtivo):有容器运行时,它通过 docker exec 运行;没有时,它静默地落到纯宿主机上,在任何隔离之外执行命令。
这让 runner 与 container/mounted 不一致,后两者自 ADR 0143(RN-502)起就已拒绝在没有已登记 running 容器的情况下运行终端——绝不降级为本地 System.cmd。对 runner 而言,“本地”就是用户自己的宿主机:ADR 0143 为另外两种模式关闭的同一静默回退,在这里——服务器之外——仍然开着。
第二个缺陷,自 RN-478 起就已声明却从未修正:dev agent 工作树的实体化
(Engine.Actions.Workspace.ensure!/4)试图在 engine 进程内本地执行 File.mkdir_p!/
System.cmd("git", ...),针对的是一个只在用户机器上物理存在的路径——engine 进程没有任何通往那里的 bind-mount。之前的修正只是改进了一个不可避免的失败的消息;dev agent 仍然没有地方工作。
第三点,在调查中发现:container_start(真正启动容器的动作,ADR 0130/0133)也服务于 runner,但它的 payload(imagem/network/resources/rationale——Infra 从 Arquiteto 的路由中选择一个候选项)对这条路径从来没有意义:broker 永远到达不了 runner 项目的文件夹,因此没有可以据以选择的路由,启动的镜像就是早已决定的那个。
Decision
Docker 成为 runner 模式的真正前提,没有回退到宿主机。 runner 要让任何命令到达用户机器所需的三个前置条件——workspace 已确认、runner 已连接、容器已登记为 running(ProjectContainerLifecycle.running?/1,与 container/mounted 相同的谓词)——住在一个函数中,
Engine.Runners.RunnerReadiness,有两个消费者:TerminalExecutor(它早已检查前两个)以及下面的新模块。
工作树的实体化改为在项目真实的容器内部**、在用户机器上进行——经由早已执行终端的同一个通道。**
Engine.Actions.Workspace.ensure!/4 按 execution_mode 分叉:对 container/mounted 本地进行(不变),对 runner 经由 Engine.Actions.Workspace.RunnerGit(新增)。RunnerGit 执行 git init/remote add/fetch/
checkout,以及 Engine.Dev.WorktreeManager 所需的 File.dir?/ls/rm_rf 的等价操作,作为普通 shell 命令,经由 Engine.Runners.RunnerRouter.exec/5 传递——绝不是新机制,总是在 RunnerReadiness.verificar/1 确认三个前置条件之后。WorktreeManager(create/3、remove/2、list/1、
cleanup_orphans/2)按同一标准分叉,周期任务
Engine.Dev.WorktreeCleanup 开始跳过(而不是失败)此刻没有就绪 runner 的 runner 项目,对其他项目照常继续。
exec/exec_result 协议增加一个可选的 env 字段。 带认证的 git fetch 需要 provider 的凭据(ADR 0056),而它只能在子进程的环境中传递——绝不在 argv 中,绝不在文件中。由于这个子进程现在诞生在用户机器上,env(一个
Record<string,string>)进入 engine 分派的 payload;runner 在 spawn 之前把这个 env 合并到 process.env 之上(绝不替换——会丢失 PATH),并且绝不把它传给 docker exec 路径(packages/docker-port 中刻意不支持 env——见 Consequences),也不在任何日志中打印它。
container_start 整体迁移为 container/mounted 专属(两者都经由 broker,RN-503)。诞生 container_start_via_runner,一种新的动作类型,runner 专属。 与 container_start 同一量级(maintainer,默认 require_approval,可在 permissions.json 中配置)——但 schema 只有可选的 rationale:它不选择任何候选项,启动的是早已决定的镜像
(ObterSpecDeContainerUseCase)。Engine.Infra.Tools. ProposeContainerStartViaRunner,Infra Lead 的新工具,在调用 propose_action 之前被 InfraLeadServer 拦截——它本地查询(Project.get/1 + Engine.Runners.Registry.connected?/1,没有 HTTP,在同一个 BEAM 进程中)execution_mode 以及是否有已连接的 runner,以有名称的原因拒绝,而不是盲目提出。
Consequences
- 没有
running容器的runner项目会失去终端和 dev agent 的工作树,直到有人提出并批准container_start_via_runner。 被接受的代价,并对照真实先例测量过:container/mounted的姊妹改动(ADR 0143,RN-502)早已具有同样的形式——“开始要求以前不要求的东西”——并以feature/、MINOR 发布,没有breaking/分支。在本次交付之前,没有任何现有的runner项目有保证的隔离(静默回退到宿主机恰恰是被关闭的东西),因此没有任何先前正确的行为被拿走——只是一个不安全的行为不再存在。 - 只有当 runner 选择宿主机路径时,
env才会到达git fetch。 如果 runner 已经有活跃容器(EstadoDoRunner.containerAtivo),tratarExec会把每个exec——包括RunnerGit的——经由docker exec路由,而它没有env字段(ADR 0130 经测量的决定:保持 Docker port 为封闭类型,没有自由的-e)。在这种状态下分派的git fetch对私有仓库会静默丢失凭据。本次交付中未解决——扩展packages/docker-port以接受env会触及它的两个消费者(runner 和 broker),不在范围内;声明在此,待带私有仓库的runner项目数量证明有必要时处理。 RunnerReadiness.verificar/1在每次RunnerGit调用时都会再运行一次(每次 git/worktree 操作多一次查询)——可以接受,因为这个模块中没有任何函数位于热路径上(与本次交付之前对Engine.Actions.Workspace.ensure!/4早已适用的同一把尺子)。- RN-494 为
propose_container_start声明的缺口(它和GetInfraContextUseCase都不按executionMode限制——Infra Lead 可以盲目提出)对那个工具保持原样。 新工具(container_start_via_runner)诞生时没有这个缺口,因为它诞生时就知道拒绝——但这不会追溯:propose_container_start仍然不会区分模式,这是单独的决定,不在 API/领域的范围内。