Skip to main content

ADR 0144 — The broker's second root, and the discriminated locator that chooses it

Context​

The broker (ADR 0130) is the only product process that talks to a Docker daemon on the server, and the thing that makes it contained is a single one: it does not accept a container specification. It receives a projectId and one of the five operations, goes to the api to READ the Arquiteto's (Architect's) decision and COMPOSES the image, network, resources and the single mount itself. No path crosses the network — the api sent workspaceDirName and the broker concatenated it with ITS root (PROJECT_WORKSPACES_HOST_ROOT).

That shape had one root because there was only one place where a project folder could be from the server's point of view: /data/project-workspaces, the folder MANAGED by the product, of the container mode. mounted and runner projects were left out, and the broker refused both at the source with ModoDeExecucaoNaoSuportadoError.

ADR 0141 changed the fact of the world that sustained half of that refusal. Every project in the Mounted folder mode now lives under one base (BRABO_PROJECTS_BASE), mounted by identity in api and engine, and therefore reachable by the server's daemon. The broker's refusal was about GEOMETRY — "its code is on a machine I do not see" —, and the geometry of mounted stopped being that. That of runner did not change at all.

This opens a question the single-root shape cannot answer: given a segment, against which root does it apply?

The obvious temptation is for the api to send the absolute path and the broker to mount it. That would undo ADR 0130 entirely: the containment of a root-equivalent process on the host would again depend on the CALLER being correct, which is the definition of there being no containment. The second temptation is for the broker to guess — use the base when the mode is mounted. That works until the day there is a third place, and until then it hides the decision in an if over an enum the broker does not own.

Decision​

The broker gains a SECOND root, BRABO_PROJECTS_HOST_BASE, and the api now sends a DISCRIMINATED locator that says against which of the two the segment applies.

localizacao:
| { tipo: 'gerenciada'; segmento: string } // PROJECT_WORKSPACES_HOST_ROOT
| { tipo: 'montada'; segmento: string } // BRABO_PROJECTS_HOST_BASE
| { tipo: 'indisponivel'; motivo: string } // nenhuma raiz alcança

The invariant stays exactly where it was. What crosses the network is still only the half the broker's root does not cover; the api does NOT know where those roots are on the host, and the broker knows nothing about the project. Neither side alone can write an arbitrary path — what changed is that the api now says WHICH root, instead of there being only one and the answer being implicit.

Two variables and not one, and no inheritance between them. The managed root is named by workspace_dir_name (UNIQUE) and the base is named by the user. The same name points to different folders in the two, and nothing in the schema prevents the collision — it is the first reason ADR 0141 refused to conflate BRABO_PROJECTS_BASE with PROJECT_WORKSPACES_HOST_DIR, and it holds identically here. That is why BaseDeProjetosNaoConfiguradaError is its OWN class, in the same mold as RaizDeWorkspacesNaoConfiguradaError: origin infra, message naming the variable, no container touched. Falling back to the other root by omission would mount ANOTHER project's folder, and the container would come up — with the wrong code inside, with nothing indicating why.

Three variants and not two. indisponivel is not a disguised null: it is the state in which no root of THIS server reaches the folder, and it has two different fixes. A runner project (the folder is on the user's machine — the fix is the brabo-runner, on that side) and a LEGACY mounted project created before the base (the fix is to move the folder). A null would send whoever operates to the wrong place half the time. A folder that IS the base itself falls here too, instead of becoming an empty segment: <raiz>/ would mount the whole base — the folder of all mounted projects — inside a single one's container.

The list of modes the broker serves is an ALLOW list, not a deny list (container, mounted). A new mode in the api's enum is born refused, with a message, instead of being born silently accepted and falling into a composition with no root that resolves it.

Three barriers over the concatenation, not one. The api refuses what is not under the base (segmentoSobABaseDeProjetos); packages/docker-port refuses the segment that is not relative (segmentoDeProjetoValidado: .., absolute, empty, double slash, NUL); and the result of raiz + segmento still goes through raizDeProjetoValidada before becoming -v, as it always did. Validating the segment and not validating the concatenation would be trusting string arithmetic — and the validation on the broker side exists precisely because it cannot presuppose that the api is correct.

Consequence on the api side: the branching of container_start becomes by DESTINATION. container and mounted go to the broker; only runner goes to the runner. And container_stop/container_remove change together, necessarily: bringing up on the server and asking to stop on the user's machine would leave standing, with no way to stop it, what is standing.

mounted ELECTS the image, like container. It is not symmetry: it is the only design that works on this side. The broker composes from artifact.project_image, going to FETCH it from the api — an Infra election that was not recorded in that artifact would be inert, and the container would come up with the Arquiteto's image while the payload approved by the human would say something else. It is literally the argument of ADR 0133, applied to a second mode. The runner path can read the current one because there the api SENDS the spec fields through the channel; the artifact is not the source the other side queries.

Consequences​

The Mounted folder mode no longer requires a connected brabo-runner to have a container. It was that requirement that made incoherent the sequence the product owner's plan asks for — choosing mounted at creation, and the container coming up before the dev agents.

runner is left alone on the runner path, and that is the design, not a leftover: the broker (and the server) still do not see its folder. container remains the only mode that brings up a container on the server without depending on a base configured by the operator; mounted brings it up on the server when the base is there.

Declared cost 1: an installation can have one root and not the other. That is a legitimate state, and each operation says which one is missing instead of both falling into a generic refusal. The price is two error classes where there could be one with a parameter — accepted, because whoever reads the error is whoever needs to know WHICH variable to set.

Declared cost 2: the broker now knows a second segment format. gerenciada is a name without /; montada is a relative path, which may have levels. nomeDeWorkspaceValidado (which serves the container's NAME and refuses / on purpose) stops serving to validate a path, and segmentoDeProjetoValidado is born next to it. They are two questions that came to have different answers — "what is the container called" and "where is the folder" — and having them with a single function was what worked while they coincided.

What this ADR does NOT decide. It materializes no folder at all (that is the deferred validation, neighboring PR). It does not change the image gate — RN-105 already applies to all three modes since RN-494/ADR 0135. It does not change who PROPOSES container_start: the Infra Lead can still propose it for a project without a connected runner and without a decided image, the gap declared since RN-494. It does not change the count of broker operations: they remain five, and a sixth remains a product decision with an ADR. And it does not publish the broker image on GHCR — the four of ADR 0119 remain four.

References 0130 (the broker composes the spec), 0133 (the election must be recorded to have effect), 0137 (the other execution path) and 0141 (the base that made this possible). None of them is edited.