Pular para o conteúdo principal

ADR 0144 — A segunda raiz do broker, e o localizador discriminado que a escolhe

Context​

O broker (ADR 0130) é o único processo do produto que fala com um daemon Docker no servidor, e a coisa que o torna contido é uma só: ele não aceita especificação de container. Recebe um projectId e uma das cinco operações, vai à api LER a decisão do Arquiteto e COMPÕE imagem, rede, recursos e o único mount ele mesmo. Nenhum caminho atravessa a rede — a api mandava workspaceDirName e o broker o concatenava com a raiz DELE (PROJECT_WORKSPACES_HOST_ROOT).

Essa forma tinha uma raiz porque havia um lugar só onde uma pasta de projeto podia estar do ponto de vista do servidor: /data/project-workspaces, a pasta GERENCIADA pelo produto, do modo container. Projeto mounted e runner ficavam de fora, e o broker recusava os dois na fonte com ModoDeExecucaoNaoSuportadoError.

O ADR 0141 mudou o fato do mundo que sustentava metade dessa recusa. Todo projeto no modo Pasta montada passa a morar sob uma base (BRABO_PROJECTS_BASE), montada por identidade em api e engine, e portanto alcançável pelo daemon do servidor. A recusa do broker era sobre GEOMETRIA — "o código dele está numa máquina que eu não enxergo" —, e a geometria de mounted deixou de ser essa. A de runner não mudou nada.

Isso abre uma pergunta que a forma de uma raiz só não sabe responder: dado um segmento, contra qual raiz ele vale?

A tentação óbvia é a api mandar o caminho absoluto e o broker montá-lo. Isso desfaria o ADR 0130 inteiro: a contenção de um processo root-equivalente no host voltaria a depender de o CHAMADOR estar correto, que é a definição de não haver contenção. A segunda tentação é o broker adivinhar — usar a base quando o modo for mounted. Isso funciona até o dia em que existe um terceiro lugar, e até lá esconde a decisão num if sobre um enum que o broker não é dono.

Decision​

O broker ganha uma SEGUNDA raiz, BRABO_PROJECTS_HOST_BASE, e a api passa a mandar um localizador DISCRIMINADO que diz contra qual das duas o segmento vale.

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

O invariante fica exatamente onde estava. O que atravessa a rede continua sendo só a metade que a raiz do broker não cobre; a api NÃO sabe onde essas raízes ficam no host, e o broker não sabe nada sobre o projeto. Nenhum dos dois lados sozinho consegue escrever um caminho arbitrário — o que mudou é que a api passou a dizer QUAL raiz, em vez de haver uma só e a resposta ser implícita.

Duas variáveis e não uma, e sem herança entre elas. A raiz gerenciada é nomeada por workspace_dir_name (UNIQUE) e a base é nomeada pelo usuário. O mesmo nome aponta para pastas diferentes nas duas, e nada no schema impede a colisão — é o primeiro motivo pelo qual o ADR 0141 recusou conflar BRABO_PROJECTS_BASE com PROJECT_WORKSPACES_HOST_DIR, e ele vale idêntico aqui. Por isso BaseDeProjetosNaoConfiguradaError é classe PRÓPRIA, com o mesmo molde de RaizDeWorkspacesNaoConfiguradaError: origem infra, mensagem nomeando a variável, nenhum container tocado. Cair na outra raiz por omissão montaria a pasta de OUTRO projeto, e o container subiria — com o código errado dentro, sem nada indicando por quê.

Três variantes e não duas. indisponivel não é um null disfarçado: é o estado em que nenhuma raiz DESTE servidor alcança a pasta, e ele tem dois consertos diferentes. Projeto runner (a pasta está na máquina do usuário — o conserto é o brabo-runner, do lado de lá) e projeto mounted LEGADO criado antes da base (o conserto é mover a pasta). Um null mandaria quem opera para o lugar errado metade das vezes. A pasta que É a própria base cai aqui também, em vez de virar segmento vazio: <raiz>/ montaria a base inteira — a pasta de todos os projetos montados — dentro do container de um só.

A lista de modos que o broker atende é de PERMITIDOS, não de recusados (container, mounted). Um modo novo no enum da api nasce recusado, com mensagem, em vez de nascer silenciosamente aceito e cair na composição sem raiz que o resolva.

Três barreiras sobre a concatenação, não uma. A api recusa o que não está sob a base (segmentoSobABaseDeProjetos); packages/docker-port recusa o segmento que não é relativo (segmentoDeProjetoValidado: .., absoluto, vazio, barra dupla, NUL); e o resultado de raiz + segmento ainda passa por raizDeProjetoValidada antes de virar -v, como sempre passou. Validar o segmento e não validar a concatenação seria confiar na aritmética de strings — e a validação do lado do broker existe justamente porque ele não pode pressupor que a api esteja correta.

Consequência do lado da api: a ramificação de container_start passa a ser por DESTINO. container e mounted vão ao broker; só runner vai ao runner. E container_stop/container_remove mudam junto, obrigatoriamente: subir no servidor e pedir para parar na máquina do usuário deixaria de pé, sem forma de parar, o que está de pé.

mounted ELEGE a imagem, como container. Não é simetria: é o único desenho que funciona deste lado. O broker compõe a partir de artifact.project_image, indo BUSCÁ-LO na api — uma eleição da Infra que não fosse gravada nesse artefato seria inerte, e o container subiria com a imagem do Arquiteto enquanto o payload aprovado pelo humano diria outra coisa. É literalmente o argumento do ADR 0133, aplicado a um segundo modo. O caminho do runner pode ler a vigente porque lá a api MANDA os campos da spec pelo canal; o artefato não é a fonte que o outro lado consulta.

Consequences​

O modo Pasta montada deixa de exigir um brabo-runner conectado para ter container. Era essa exigência que tornava incoerente a sequência que o plano do dono do produto pede — escolher mounted na criação, e o container subir antes dos dev agents.

runner fica sozinho no caminho do runner, e isso é o desenho, não uma sobra: o broker (e o servidor) continuam sem enxergar a pasta dele. container continua sendo o único modo que sobe container no servidor sem depender de uma base configurada pelo operador; mounted sobe no servidor quando ela está.

Custo declarado 1: uma instalação pode ter uma raiz e não a outra. Isso é estado legítimo, e cada operação diz qual falta em vez de as duas caírem numa recusa genérica. O preço é duas classes de erro onde poderia haver uma com parâmetro — aceito, porque quem lê o erro é quem precisa saber QUAL variável definir.

Custo declarado 2: o broker agora conhece um segundo formato de segmento. gerenciada é um nome sem /; montada é um caminho relativo, que pode ter níveis. nomeDeWorkspaceValidado (que serve ao NOME do container e recusa / de propósito) deixa de servir para validar caminho, e nasce segmentoDeProjetoValidado ao lado dela. São duas perguntas que passaram a ter respostas diferentes — "como se chama o container" e "onde fica a pasta" — e tê-las com uma função só era o que funcionava enquanto elas coincidiam.

O que este ADR NÃO decide. Não materializa pasta nenhuma (é a validação diferida, PR vizinha). Não muda o portão da imagem — RN-105 já vale para os três modos desde a RN-494/ADR 0135. Não muda quem PROPÕE container_start: o Infra Lead segue podendo propor para um projeto sem runner conectado e sem imagem decidida, a lacuna declarada desde a RN-494. Não altera a contagem de operações do broker: continuam cinco, e uma sexta segue sendo decisão de produto com ADR. E não publica a imagem do broker no GHCR — as quatro do ADR 0119 seguem sendo quatro.

Referencia 0130 (o broker compõe a spec), 0133 (a eleição precisa ser gravada para ter efeito), 0137 (o outro caminho de execução) e 0141 (a base que tornou isto possível). Nenhum deles é editado.