Arquiteturafonte: AGENTS.md · docs/adr

Arquitetura

O DOP são três repositórios que entregam e um que os fixa. Os componentes são repositórios git independentes, agregados como submódulos pelo repositório guarda-chuva, que carrega a documentação do produto: o PRD, as ADRs, as specs e as coleções da API.

Os componentes

  • dop-coreGo
    O core: domínio, estado, transações e o log de eventos.Um binário, quatro modos (serve, worker, sched, launcher). É dono do schema e do vault; é a única fonte da verdade, e verifica a assinatura de todo chamador.
    dop-core ↗
  • dop-apiPython
    O BFF: REST e SSE para o cockpit, gRPC para a CLI.Não tem banco nem segredo. Traduz e encaminha; toda escrita passa pelo core.
    dop-api ↗
  • dop-appReact · Vite
    O cockpit onde o desenvolvedor trabalha.A caixa de atenção, a árvore de workspaces e projetos, o cockpit da demanda. Consome um contrato commitado e gera dele os seus hooks e schemas.
    dop-app ↗
  • dop-infraTerraform · k3d
    A infraestrutura da plataforma.Terraform para o Google Cloud (Cloud Run, Secret Manager, Identity Platform) e um ambiente local em k3d com Postgres, NATS JetStream e os emuladores do Firebase.
    dop-infra ↗
  • sandboxmicroVM
    Uma por demanda, com um único worktree compartilhado.A fronteira dura é entre contas e entre demandas. O conhecimento do projeto é clonado lá dentro como um repositório git; a verificação roda da fonte num runner efêmero.

Os diagramas

Seis figuras, da plataforma inteira até cada componente. Todo nome nelas é um pacote, um port ou um adaptador que existe nos repositórios; o que está planejado e não construído aparece tracejado.

A plataforma

A arquitetura da plataforma DOPO cockpit fala REST e SSE com o BFF, e a CLI fala gRPC com ele; o BFF fala gRPC com o core; o core é dono do Postgres para o estado e do NATS JetStream para os eventos, e provisiona uma sandbox por demanda. O Identity Platform autentica o usuário na borda.Identity Platformlogin · tokensSandboxuma microVM por demandaCockpitdop-app · ReactCLIdop-cliBFFdop-api · PythonCoredop-core · Goa única fonte da verdadeschema · vault · eventosPostgresestado · outboxNATSJetStreamREST · SSEgRPCgRPCestadoeventosprovisionaverifica
O cockpit e a CLI chegam ao BFF; o BFF chega ao core; o core é dono do estado, dos eventos e das sandboxes. O Identity Platform autentica a pessoa na borda.

O core por dentro — o hexágono

O core por dentro: ports e adaptadoresUm binário com quatro modos aciona o domínio pela esquerda; o domínio só alcança a infraestrutura por ports; cada port tem dois ou mais adaptadores à direita, verificados por uma única suíte de contrato.acionaservegRPC · app/grpcworkerconsumidoresschedtarefas periódicaslaunchersandboxestoda chamada verificada: o token da pessoa,ou uma asserção da plataforma (ADR-0022)domínioidentity · hierarchy · resourcedemand · workflow · executiondelivery · attention · notificationreaction · event · knowledgecost · agent · secondfactorserviços, entidades, regras —sem SQL, sem HTTP, sem SDKtoda escrita emite um eventoportsacionados — um port, seus adaptadoresRepositoriesPostgresEventBusNATS JetStream · memoryIdentityProviderFirebase · OIDCSecretStoreSecret Manager · k8s · memoryObjectStoreGCS · fsSandboxLauncherDocker · KubernetesVerificationRunnerDocker · Kubernetes · scriptProjectRepositorygit server · local · remoteMailerOneSignal · SendGrid · SMTPSMSerTwilio · ZenviaAgentProviderAnthropic · OpenAIGitProviderGitHub · GitLabuma suíte de contrato por port; todo adaptadorpassa nos mesmos testes (ADR-0001)
Um binário, quatro modos, um domínio. O domínio nunca importa um driver: declara um port, e o adaptador à direita o implementa. Todo port tem pelo menos dois adaptadores e uma suíte de contrato que ambos precisam passar — é isso que faz do ambiente local e do Google Cloud a mesma plataforma.

O BFF por dentro — dois transportes, uma regra

O BFF por dentro: dois transportes, uma regraREST com SSE e gRPC são adaptadores que chamam o mesmo caso de uso; o caso de uso fala com o core por gRPC com uma asserção da plataforma, e verifica o token da pessoa no Identity Platform. Não tem banco nem segredo.acionaREST · SSEFastAPI · app/routersgRPCapp/grpcapia regra vive uma vez, em usecases;os dois transportes chamam a mesma função,e um teste de paridade prova issousecasesidentity · hierarchy · resourcedemand · workflow · executiondelivery · attention · costknowledge · runtime · streamsecondfactortraduz, autoriza, encaminha —nada persiste aquiplatformsecurity · context · errors · loggingacionadoscoreclient → dop-coregRPC · TLS · asserção (ADR-0022)Identity Platformverifica o token da pessoa · resolve a contaFirebase Adminemite links de ação (verificação de e-mail)sem banco · sem segredo · toda escrita vai para o core
REST com SSE para o cockpit, gRPC para a CLI, e o mesmo caso de uso por baixo. A autorização está presa ao caso de uso, não ao router, então uma regra não pode existir num transporte e não no outro; um teste de paridade mantém isso.

Eventos e o motor de fluxos

Eventos e o motor de fluxosUma escrita commita o seu evento com o estado; um relay publica no NATS JetStream; todo consumidor o recebe. O motor de fluxos e o caminho da falha são dois subprocessos, cada um desenhado na sua própria figura: o motor decide o que um evento dispara; o caminho da falha cobre a entrega de todo consumidor.escritaPostgres · txoutboxmesma txrelayat-least-onceNATS JetStreamdop.> · dlq.>Envelopeaggregate · actor · requestdispatcherplanejadotimelineprojeçãoattentionprojeçãonotificationMailer · SMSerem falha, qualquer consumidoro motor de fluxosADR-0010 · Decide → PlannedAction → Executesubprocesso ↓+o caminho da falhaADR-0014 · NAK ×5 → dlq.event → event_errorssubprocesso ↓+
O pipeline: uma escrita commita o seu evento na mesma transação; um relay publica; todo consumidor recebe. Os dois blocos marcados com um mais são subprocessos desenhados nas duas figuras seguintes — o motor de fluxos, que decide o que um evento dispara, e o caminho da falha, que cobre a entrega de todo consumidor, não de um só.

O motor de fluxos

O motor de fluxosUm fluxo é uma composição versionada de estágios tipados; cada estágio tem um portão, artefatos e ações na entrada e na saída. O fluxo efetivo é resolvido pela cadeia plataforma, conta, workspace, projeto, demanda, e congelado quando a demanda começa. Um evento de avanço de estágio é decidido a partir das ações do estágio e da tabela de regras acumulada em ações planejadas, que o executor roda por um registro de quatro ações, registrando cada uma como aplicada por evento, regra e ação.Flowname · versioncontextspecplanimplementationtesthuman_validationfinalizationStagetypegate · human|noneartifactsactions · enter|exitdemand/‹id›/‹kind›.md · ADR-0021cadeia de resoluçãoplatformaccountworkspaceprojectdemando mais próximo vencecongelado ao iniciar a demandareaction_rulesacumulam pela cadeiatrigger · actions · disablesstage-advanced · from → toDecideStagedas ações do estágioDecideda tabela de regrasPlannedActionrule · name · paramsExecuteRegistry · Appliedopen_attentionclose_attentionsend_emailprovision_benchaplicada — idempotente(event, rule, action)
Um fluxo é dado: uma lista versionada de estágios tipados, cada um com um portão, os seus artefatos e as suas ações na entrada e na saída. O fluxo efetivo desce por uma cadeia — plataforma, conta, workspace, projeto, demanda — em que o nível declarado mais próximo vence, e uma demanda congela a versão com que começou. Quando um estágio avança, as ações do estágio e a tabela de regras acumulada são decididas em ações planejadas; o executor as roda por um registro de quatro e registra cada uma como aplicada por evento, regra e ação, para uma retentativa nunca repetir uma.

O caminho da falha

O caminho da falhaUm consumidor trata uma entrega; em sucesso a mensagem é confirmada e o sucesso registrado; em falha é reentregue com backoff até cinco vezes. Uma entrega esgotada vira uma dead letter que carrega o envelope inteiro, o consumidor e as tentativas, publicada em dlq.event com id de mensagem de evento e consumidor. O consumidor da DLQ reexecuta o mesmo handler até três vezes mais; um sucesso registra; uma falha encerra a mensagem. Toda falha é classificada pela sua assinatura, aprendida depois de dois esgotamentos, e escrita no ledger de erros.NATS JetStreamdop.>consumidorHandle(event)ok?simAckRecordSuccessnão · NAKreentrega1s · 5s · 15s · 1min×5 · MaxDeliveresgotadaDeadLetterevent · consumer · attemptsdlq.eventMsgId event:consumerDLQConsumermesmo handler · ×3ok?simnão · TermClassifySignature(consumer, code|kind)recoverableirrecoverableunknownerror_signaturesaprendida · ×2event_errors(event_id, consumer) · attempts · broker_attempts · classification · last_success_at
O mesmo caminho cobre todo consumidor. Uma entrega que falha é reentregue com backoff até cinco vezes; uma esgotada vira uma dead letter com o envelope inteiro e as suas tentativas, publicada num subject próprio. O consumidor da DLQ reexecuta o mesmo handler mais três vezes, depois encerra a mensagem — mas toda tentativa, a sua classificação e o último sucesso ficam no ledger de erros, e uma assinatura que esgota duas vezes é aprendida como irrecuperável até um sucesso rebaixá-la.

O cockpit por dentro

O cockpit por dentroUm roteador monta as páginas; as páginas são construídas de famílias de componentes; os componentes leem e escrevem por hooks; os hooks chamam um cliente gerado — hooks do react-query e schemas Zod produzidos pelo Orval a partir de um documento OpenAPI commitado — cuja camada de fetch carrega o token da pessoa e a conta. O Firebase Auth emite o token; o BFF responde REST e empurra SSE.roteadorwouter · App.tsxpagessign-insign-uponboardinghomeworkspaceprojectdemandcard-executionaccountinvitecomponentsplatformlayout · sidebarattention boxlivesecuritygate · step-upstage viewsplan · exec · testdoc viewermarkdownuishadcnuiStorezustandi18npt-BR · enhooksuse-apireact-queryuse-attention-streamSSE · EventSourceo contrato geradoapi-client-reacthooksapi-zodschemascustom-fetchAuthorization · x-account-idopenapi.jsonlib/api-spec · commitadoOrvalIdentity Platformauth.qa.dop-t.comFirebase Auth SDKlogin · id tokenid tokenRESTSSEBFFapi.qa.dop-t.com · dop-api
Um roteador, dez páginas, seis famílias de componentes, dois hooks e um cliente gerado por baixo. O contrato é um arquivo OpenAPI commitado; o Orval produz dele os hooks do react-query e os schemas Zod; uma camada de fetch leva o token da pessoa e a conta em toda chamada. O Firebase Auth emite o token; o BFF responde REST e empurra o stream de atenção por SSE. Nada que o backend já decidiu é decidido de novo aqui.

Como o core verifica seus chamadores

Como o core verifica seus chamadores (ADR-0022)A pessoa entra pelo Identity Platform e o cockpit guarda o token. O cockpit o envia ao BFF como bearer. O BFF o encaminha inteiro ao core e acrescenta uma asserção assinada nomeando chamador, ator, tipo, conta, sessão e validade; no Cloud Run também leva o token de invocador. Chamadores sem pessoa enviam só a asserção. O interceptor do core verifica o token pelo port de identidade, verifica o HMAC da asserção com a chave do chamador, recusa quando token e asserção nomeiam atores diferentes, aplica o modo e deixa a decisão para a autorização. A autenticação de transporte — IAM do Cloud Run ou uma network policy — é adicional.transporteCloud Run IAM · NetworkPolicyIdentity Platformloginid tokencockpitdop-appBearerBFFdop-apitoken + assertionworker · schedlauncherassertioncoreinterceptorverifica tokentoken · IdPverifica HMACHMAC · 2 minmesmo ator?senão: recusadamodostrict · permissive · offautorização
Toda chamada ao core carrega uma assinatura que o core consegue checar: o token da pessoa, encaminhado inteiro pelo BFF, ou uma asserção da plataforma assinada com a chave do próprio chamador quando não há pessoa. O interceptor resolve o ator pelo token e a conta pela asserção, recusa quando os dois discordam, e deixa a autorização dizer não com uma mensagem que significa algo. O IAM do Cloud Run na nuvem e uma network policy num cluster restringem quem consegue sequer alcançar o serviço; nenhum dos dois substitui a assinatura.

A plataforma e com quem ela conversa

A plataforma e com quem ela conversaO DOP roda no Google Cloud; mantém seu próprio Postgres e NATS; envia e-mail e SMS por OneSignal, SendGrid, SMTP, Twilio ou Zenvia; lê e escreve repositórios no GitHub e no GitLab; roda agentes na Anthropic ou na OpenAI; e provisiona sandboxes em Docker ou Kubernetes. O Jira está planejado.DOPcore · BFF · cockpitGoogle CloudCloud Run · Secret ManagerCloud Storage · Identity PlatformFirebase HostingcomunicaçãoOneSignal · SendGrid · SMTPTwilio · ZenviaMailer · SMSerdados, nossosPostgres · pgvectorNATS JetStreamruntime das sandboxesDocker · KubernetesSandboxLauncherVerificationRunnerhospedagem de códigoGitHub · GitLabGitProviderprovedores de modeloAnthropic · OpenAIAgentProviderJiraplanejado
Todo serviço externo fica atrás de um port, então trocar um é um adaptador e um valor de configuração, não uma reescrita. O que está sólido tem adaptador hoje; o que está tracejado está no roadmap.

As cinco invariantes

As paredes estruturais da arquitetura. Uma mudança que quebra uma delas está errada mesmo quando compila e os testes passam.

  1. O core é a única fonte da verdade.É dono do schema e do vault. Toda escrita passa por ele.
  2. O BFF não tem banco nem segredo.Traduz e encaminha; nunca persiste e nunca guarda uma credencial.
  3. Os arquivos .proto são o contrato.Código gerado é regenerado, nunca editado à mão. Uma mudança incompatível é recusada de propósito.
  4. O cockpit consome um contrato commitado.A spec OpenAPI é baixada e commitada; os hooks do react-query e os schemas Zod são gerados dela.
  5. O core verifica seus chamadores, e a verificação roda da fonte.Uma assinatura em toda chamada, nunca um header de confiança. Uma verificação constrói do repositório num runner, não de uma imagem.

A vida de um evento

Toda escrita no core emite um evento na mesma transação. O que acontece depois é infraestrutura, não problema do repositório.

  1. escritaUma transação no Postgres muda o estado e anexa o evento à outbox — um commit, ou nenhum.
  2. outboxUm relay lê a outbox e publica no NATS JetStream. O id da mensagem é o id do evento, então uma retentativa não duplica.
  3. envelopeO evento viaja com o seu contexto: chave do agregado, ator, id da requisição, id da sessão, chamador.
  4. consumidorUm consumidor o processa. Em falha, é reentregue com backoff, até cinco vezes.
  5. dead letterEsgotado, o evento vai para uma única fila de dead letters com o histórico de tentativas e a sua classificação: recuperável, irrecuperável, desconhecida.
  6. ledger de errosToda falha é registrada por (evento, consumidor); uma assinatura que queima todas as retentativas duas vezes é aprendida como irrecuperável, e um sucesso a rebaixa.
MaxDeliver 5 backoff 1s · 5s · 15s · 1min chave de dedup (event, consumer)

O registro de decisões

Toda decisão estruturante, no formato MADR: contexto, decisão, alternativas consideradas, consequências. Um assunto, uma ADR.

  1. 0001 Infraestrutura atrás de ports, com adaptadores plugáveis
  2. 0002 Tenancy: a conta é dona de tudo, e uma organização se prova pelo domínio
  3. 0003 Uma credencial da organização para agir, autoria humana no commit
  4. 0004 A demanda é um log de eventos; todo o resto é projeção
  5. 0005 Sem verde, sem PR: verificação nativa e uma fila de merge por repositório
  6. 0006 Contexto é um subsistema: uma base de conhecimento e um pacote por demanda
  7. 0007 Multiagente por demanda: threads endereçáveis e achados publicados
  8. 0008 O custo do LLM: medir, limitar, rotear e gastar menos
  9. 0009 O recurso como unidade de posse e compartilhamento da conta
  10. 0010 Um fluxo dinâmico, tipado e herdável
  11. 0011 O orquestrador do projeto: o agente techlead
  12. 0012 Um core em Go, um BFF em Python, e a fronteira entre eles
  13. 0013 O .proto é a fonte da verdade do contrato
  14. 0014 PostgreSQL como banco único, e a outbox que move seus eventos
  15. 0015 Emuladores do Firebase no ambiente local; Terraform como dono único
  16. 0016 O runtime do agente: um port por fornecedor, rodando dentro do core
  17. 0017 Uma microVM por demanda, com um único worktree compartilhado
  18. 0018 Comunicação: o gatilho e o canal nascem juntos
  19. 0019 O convite não tem segredo: identidade no lugar de um bearer
  20. 0020 O segundo fator é da plataforma, com três verificadores
  21. 0021 O conhecimento do projeto é um repositório git, hospedado pela plataforma
  22. 0022 O core verifica uma assinatura; não acredita num header
  23. 0023 A verificação constrói da fonte num runner, não de uma imagem