Pular para o conteúdo principal

ADR 0081 — Ciclo de vida do container: tabela de estado, sem orquestrador

Status​

Aceito, com o mesmo tipo de corte que o ADR 0065 já declarou para esta fatia: a tabela e a máquina de estados existem; o que comanda um container Docker de verdade não existe, e não é este documento que decide como ele vai existir.

Este ADR revisa o ADR 0065, fechando a metade que a seção "O que este ADR NÃO faz" dele declarava — "estado de container precisa de tabela… o slot único de migration desta onda pertence a outra fase" — e toca o terreno do ADR 0055, que permanece aceito e não é editado por nenhum dos dois.

Contexto​

O ADR 0065 entregou a metade que não precisava de banco: a decisão de imagem do Arquiteto (artifact.project_image, no event log) e o portão que ela abre para a aba Code (RN-105). Ele foi explícito sobre o que faltava:

O ciclo de vida do container (25b). Provisionar, parar, reciclar, limpar; o que acontece quando a imagem muda; o que sobrevive a restart; o worktree do agente passando a viver dentro do container.

O motivo é concreto e não é falta de desenho: estado de container precisa de tabela. Id do container, status, imagem em uso, quando subiu, a qual versão do artefato corresponde — nada disso é evento, é estado mutável, e forçá-lo no event log seria usar a ferramenta errada porque a certa estava ocupada. O slot único de migration desta onda pertence a outra fase.

Entregar meio provisionamento seria pior que não entregar: um container que sobe e não recicla é pior que nenhum. — ADR 0065

O PROGRAMA 28 chegou à onda com o slot de migration livre para esta tabela. Antes de escrever uma linha de código, uma pergunta teve de ser respondida com uma investigação, não com suposição: algum serviço do produto já consegue comandar um container Docker?

A resposta, verificada linha por linha em docker/docker-compose.yml: não. Nenhum serviço (api, engine, nem os de desenvolvimento) monta /var/run/docker.sock, e nenhum roda privileged: true. Isso não é uma omissão a corrigir de passagem — é a ausência de uma decisão de segurança real, e decisão de segurança real não nasce como efeito colateral de uma tabela. Montar o socket do Docker num container é um vetor conhecido de escalação para root no host; conceder isso sem que o usuário o autorize explicitamente, com as consequências na mesa, seria o mesmo erro que o ADR 0065 já registrou não cometer com rede e recursos: "decidido UMA vez, não comando a comando" — e "decidido uma vez" pressupõe alguém decidindo, não um ADR de infraestrutura escolhendo por conveniência.

Decisão​

A tabela é o CONTRATO de um orquestrador que ainda não existe​

project_containers (migration 0046) grava o ESTADO mutável do container de um projeto: status (máquina de estados explícita, abaixo), image_version (a versão de artifact.project_image que esta linha corresponde — nunca uma cópia de image/rationale/network, que continuam vivendo só no event log), container_id (o id real no daemon Docker, NULL sempre até um orquestrador existir), o teto de recursos DECLARADO (cpus/memory_mb/pids_limit, espelhando RecursosDoContainer do artefato vigente no momento em que a linha nasceu) e failure_reason.

Uma linha por PROJETO (project_id único) — o mesmo desenho de dev_agent_states no engine (ADR 0045): só existe um container vigente por vez, e reprovisionar depois de remover reusa a mesma linha em vez de acumular histórico nela. Histórico imutável já tem lugar — o event log —, e esta tabela não tenta ser as duas coisas.

A máquina de estados​

provisioning → running ⇄ stopped, com failed alcançável de provisioning/running/stopped e removed como o único estado do qual só se sai reprovisionando (removed → provisioning). Nenhum estado é terminal de verdade: mesmo removed permite subir de novo, porque um projeto pode reprovisionar com uma imagem revisada pelo Arquiteto. Validada em apps/api/src/domain/containers/container-lifecycle.ts, no MESMO formato de session-state-machine.ts e pr-gate-state-machine.ts — tabela de transições permitidas, função pura, erro tipado (InvalidContainerTransitionError) que o caso de uso traduz para 409.

A PRIMEIRA transição é especial: não existe linha até a primeira chamada com to: 'provisioning', e ela só é aceita se o Arquiteto já tiver decidido a imagem (RN-105) — o mesmo portão que já protege a aba Code, aplicado aqui na origem em vez de duplicado. A versão e os recursos da decisão vigente naquele instante são CONGELADOS na linha nova; uma revisão posterior do artefato não muda retroativamente o que uma instância já provisionada promete — reprovisionar é que lê o artefato de novo.

Nenhuma chamada a Docker — em nenhum dos dois casos de uso​

RegistrarTransicaoDeContainerUseCase e ObterCicloDeVidaDoContainerUseCase fazem exatamente o que os nomes dizem: gravam e leem. Nenhum dos dois invoca docker run, docker stop, a Docker Engine API ou qualquer client Docker — não existe um client Docker no código do produto. Um orquestrador real, quando existir, é quem CONSOME esta tabela: age primeiro contra o daemon, e só então chama RegistrarTransicaoDeContainerUseCase para registrar o que aconteceu — nunca o contrário, pela mesma razão que TransitionSessionUseCase.activate chama o engine ANTES de escrever active: a tabela não deve dizer que algo está rodando quando não está.

Coerência com o modo do workspace (ADR 0072)​

Um projeto em workspace_mode: 'local' não sobe container — ele roda no container do AGENTE de sempre, só a pasta mudou (RN-169). Pedir uma transição para um projeto local é recusado com 400 antes de tocar a tabela: a coerência não é "a tabela permite qualquer estado para qualquer projeto e a UI filtra depois", é o caso de uso recusando na origem.

Nenhuma rota HTTP nova​

Nada na Onda 4 consome esta tabela por HTTP ainda — o terminal interativo (25b/Onda 5) é o candidato óbvio, e decidir a forma da rota antes de saber exatamente o que ele precisa ler seria adivinhar um contrato. Os dois casos de uso ficam expostos ao módulo de containers, prontos para uma rota quando houver consumidor real.

Consequências​

O que passa a existir. Um lugar único e testado para responder "em que estado está o container deste projeto" e para registrar uma transição validada — pré-requisito de QUALQUER orquestrador futuro, sem o qual ele teria que inventar o próprio armazenamento de estado ou reabrir esta decisão.

O que continua exatamente como estava — sem atenuar. A metade "dentro o agente é livre" da política de terminal, que o ADR 0065 já dizia não ter mudado, CONTINUA não tendo mudado. O ADR 0055 (escopo de caminho, allowlist estreito) segue valendo palavra por palavra. Um container running nesta tabela não muda o que o terminal permite executar, porque nenhum comando de terminal hoje é roteado para dentro de um container gerido por ela — a tabela e a execução de comandos são dois sistemas que ainda não se tocam. Os achados Z e AD (allowlist de verbo não converge) continuam abertos pelo motivo de sempre: fechar exige a parede física (um orquestrador real isolando execução), não uma tabela que descreve a intenção de uma parede.

O que fica declarado, não escondido. Não existe hoje nenhum processo que transicione esta tabela sozinho — toda transição é, por enquanto, externa (teste, ou uma chamada manual). Isso é o esperado: a tabela nasce antes do consumidor, não depois. O dia em que um orquestrador real for desenhado — sidecar com privilégio restrito, daemon separado, ou outro formato — é uma decisão de segurança própria, com o usuário informado do que está sendo concedido, exatamente como o ADR 0065 já exige para rede e recursos. Este documento não a antecipa nem a atalha.

Alternativas consideradas​

Montar /var/run/docker.sock na api ou no engine para "fazer funcionar de verdade" já nesta fatia. Recusada com o argumento mais direto que existe: é uma decisão de segurança (vetor conhecido de escalação a root no host) que ninguém pediu nesta fatia, e tomá-la de passagem para fazer uma tabela "parecer completa" é exatamente o erro que a FASE 13 já nomeou — não é afrouxar allowlist de verbo, mas é a mesma classe de atalho: ganhar funcionalidade trocando garantia por conveniência.

Não criar a tabela agora, esperar o orquestrador estar desenhado. Recusada pelo motivo que o próprio ADR 0065 já deu: o slot de migration é escasso (uma por onda) e a tabela é pré-requisito, não acessório, de qualquer desenho de orquestrador — desenhá-lo sem ter onde gravar estado produziria a mesma tabela depois, sob pressão de um consumidor real esperando.

Guardar o estado como um segundo tipo de evento no event log (container.status_changed), em vez de tabela. Recusada pelo mesmo argumento que a distingue de artifact.project_image: isto é ESTADO (um valor por vez), não FATO histórico. Projetar "o estado atual" a partir de eventos a cada leitura reimplementaria uma tabela em cima do event log, pagando o custo de indireção sem ganhar nada — a mesma conclusão que "Alternativas consideradas" do ADR 0065 já registrou ao adiar esta tabela.

Referências​

  • ADR 0065 — a decisão de imagem e o portão que este documento não repete, só referencia.
  • ADR 0055 — a política de terminal que CONTINUA valendo como está; nenhuma linha muda aqui.
  • ADR 0072 — workspace_mode, que decide se um projeto tem ciclo de vida de container ou não.
  • ADR 0045 — a máquina de estados persistida (dev_agent_states) cujo desenho (uma linha por agente, status validado fora da coluna) esta tabela espelha para container.
  • RN-105, RN-169, RN-243–RN-248.
  • docs/explanation/achados-execucao-real.md — os achados Z e AD, que este documento explicitamente NÃO fecha.
  • apps/api/src/domain/containers/container-lifecycle.ts, apps/api/src/application/use-cases/containers/registrar-transicao-de-container.use-case.ts, apps/api/src/application/use-cases/containers/obter-ciclo-de-vida-do-container.use-case.ts, apps/api/src/db/migrations/0046_chilly_forgotten_one.sql.