Skip to main content

ADR 0135 — The Arquiteto's image gate (RN-105) now applies in all three execution modes

Context​

Since PHASE 25 (ADR 0065/RN-105), a project's Code tab answers 409 while nobody has decided which container image it runs — artifact.project_image, emitted by the Arquiteto (Architect) (choose_project_image) and, since ADR 0133 (RN-491), also by Infra electing among candidates from the Arquiteto's own routing. The order is the user's: the container is what gives meaning to reading the code there, and releasing the read before there is somewhere to execute would teach that the gate is decorative.

ADR 0072 (RN-169), revised by ADR 0104 (RN-421), exempted mounted/runner projects from that gate: since those two modes do not bring up their OWN container — the code lives in a user folder, mounted into the containers that already exist (mounted) or confirmed by a CLI running outside them (runner) — the original rule answered 409 forever in a project where the Arquiteto's decision was never going to happen. The exemption avoided closing the tab as a side effect.

The original plan of this delivery ("Execution in a real container", Part 1) declared that exemption as decision #5 to reopen, already accepted before this PR existed: "RN-105 gate now applies in all THREE modes, container and Code tab. Accepted cost: exp001/exp002 lose the Code tab until the Arquiteto decides." This ADR executes that decision and records what it costs and what it does NOT change.

The original exemption confused two questions that ADR 0130 (container broker) and ADR 0133 (Infra's election) separated, without the api having kept up: "does this project bring up a container on the SERVER?" (no, in mounted/runner — a question about MECHANISM) and "does it make sense to require that someone has decided the project's image before opening the code read?" (yes, in all three modes — a question about POLICY). The exemption answered the second question using the answer to the first.

Decision​

All THREE execution modes (container/mounted/runner) now require artifact.project_image decided before the Code tab opens. ReadProjectCodeUseCase.portaoDoContainer stops checking executionMode — the gate becomes unconditional, just as it already was unconditional within the container mode. ProjectCodeTab (web) stops treating mounted/runner as a special case: the three modes follow the same useQuery loading/error/sem_decisao/decided, which SIMPLIFIES the component — the modoLocal branch and the mode-conditional enabled disappear.

Accepted cost, in so many words: every EXISTING mounted/runner project without a decided artifact.project_image — which includes real dogfooding projects of this repository, exp001 and exp002 — LOSES access to the Code tab the instant this PR is deployed, until the Arquiteto (or Infra) decides an image for it. That is an OPERATOR ACTION required AFTER the deploy, not a transparent fix users do not even notice — which is why the branch is born breaking/, not bugfix/, even though the content is a gap fix: the repository convention (CLAUDE.md) is explicit that content does not decide the prefix when the change requires operator action. The version goes up MAJOR as a mechanical consequence of scripts/ci/version.ts, not by manual decision — the number is not corrected afterwards.

The container lifecycle (project_containers) also stops refusing by mode, but for a narrower reason. RegistrarTransicaoDeContainerUseCase answered 400 for any project outside the container mode — a check that actually belonged to the broker (which already refuses for its OWN, more specific reason), not to this use case. The 400 goes away; the table can now record a row for all three modes, through the SAME provisioning funnel that already read the image decision (now also required for mounted/runner).

What remains IMPOSSIBLE, by deliberate decision, is mounted/runner actually reaching running. That is applied in a single place — ContainerBrokerPort.start() refuses with ModoDeExecucaoNaoSuportadoError (apps/broker/src/operacoes.ts): the broker runs on the SERVER and does not see the user folder where the code of mounted/runner lives, so composing a container there would be composing from an empty folder. That policy does NOT change in this PR — the error class's comment already pointed to this ADR ("o PR do portão nos três modos (1.7) revisita as duas ao mesmo tempo") and the review confirms it is right as it is.

Investigated and decided, without a code change: ExecuteContainerStartUseCase calls ContainerBrokerPort.start() BEFORE any RegistrarTransicaoDeContainerUseCase.execute(..., 'provisioning') — if the broker refuses (the normal case for mounted/runner), the failure becomes container.start_failed with the reason named (BrokerRecusouError, NEVER a crash or a silent fallback) and the lifecycle transition is never called. That is: through the real approval path (container_start), no project_containers row is born for mounted/runner even with the per-mode 400 removed — the broker is already the effective gate, more precise than the old 400 (it knows WHETHER IT CAN compose the container; the use case only knew the MODE). That is why this PR does NOT add a second mode check in propose-action.use-case.ts nor in ExecuteContainerStartUseCase: duplicating it would create a second place to diverge from the first — the same lesson the broker's own comment already recorded.

Also investigated: neither propose_container_start (the Infra Lead's tool, apps/engine/lib/engine/infra/tools/propose_container_start.ex) nor GetInfraContextUseCase restrict by executionMode today — the Infra Lead CAN propose container_start for a mounted/runner project, and the proposal only fails when someone approves and the broker refuses. That is a PRE-EXISTING gap (not introduced by this PR) that spends a human approval cycle on an action bound to fail — but it fails clearly, named, never silently. Fixing it would require touching the Infra Lead's prompt/instruction in the engine, outside the scope of this slice (API/domain); it is declared below, not fixed in passing.

Consequences​

exp001/exp002 and any other mounted/runner project without a decided image lose the Code tab until someone runs the Arquiteto (or Infra's election) for them. This is the accepted cost of decision #5 of the original plan, not a side effect discovered later.

The real containment against mounted/runner bringing up a container on the server did not change. It remains the broker (ModoDeExecucaoNaoSuportadoError), not a 400 from the api — in fact it BECAME more precise, because now a single place decides it instead of two.

project_containers can, in theory, record a provisioning row for mounted/runner — but no production path today calls RegistrarTransicaoDeContainerUseCase.execute(..., 'provisioning') for those modes without first going through the broker, which refuses. If a new path some day bypasses ExecuteContainerStartUseCase (calling the use case directly), it inherits the same protection: obterImagem.execute still requires a decision, and nothing else changes here.

The Infra Lead can propose container_start for a mounted/runner project knowing it will fail — a pre-existing gap, neither introduced nor closed by this PR, declared in CLAUDE.md ("Estado atual e aberto") so it is not lost.

Alternative considered and discarded: adding the mode check in propose-action.use-case.ts or in ExecuteContainerStartUseCase, to refuse container_start for mounted/runner BEFORE calling the broker. It would avoid the wasted approval cycle cited above — but it would move knowledge about WHO can compose a container (today only the broker knows, via PROJECT_WORKSPACES_HOST_ROOT) into the api, which lacks that context and would replicate a rule that already exists elsewhere. Discarded for now: the cost is an avoidable approval cycle, not incorrectness — and the failure message already names the cause. It stays recorded as a UX candidate, not as a bug fix, if the volume of futile proposals justifies it.