Files
2026-09-14 14:54:28 -03:00

28 KiB
Raw Permalink Blame History

AS-BUILT — Enterprise MCP Service Bus (RI)

Referência técnica do que existe hoje na Reference Implementation (RI): responsabilidade de cada componente, onde o código vive, contratos de entrada/saída em nível alto (o detalhe fino de cada contrato está em interfaces/), o que foi validado e contra qual dependência real, e as limitações arquiteturais reais encontradas durante a implementação.

Público-alvo: quem vai integrar com ou estender a RI. Para quem vai rodar a RI, ver How-To.md. Para quem vai avaliar/adaptar a RI para um cenário real, ver Assumptions.md e Production-Recommendations.md.

Decisões arquiteturais individuais estão registradas em ../../docs/adr/ (ADR-001 a ADR-025); as premissas e razões por trás delas estão sintetizadas em Assumptions.md.

Status

Todo o backlog planejado (marcos M0 a M4) está implementado, testado e validado. 261 testes automatizados (contagem verificada via pytest --collect-only nesta revisão): 254 passam com pytest --forked (unit + e2e + contract + adversarial), mais 7 testes de aceite de segurança (tests/e2e/test_security_acceptance.py) que exigem Docker real e são rodados separadamente pelo custo de build+Keycloak por execução. Lógica de produção sem mocks no núcleo de segurança e orquestração: OPA real (subprocess), Keycloak real (Docker), LangGraph real com checkpointer SQLite, um LLM real (endpoint OpenAI-compatible), JWKS HTTP real (stand-in leve para o resto da suíte e2e) com JWT RS256 reais, YAML reais de config/, SQLite real para o Registry.

Índice de épicos

Cada EP-XX-TXX que aparece em comentários/docstrings do código-fonte referencia um destes épicos. Esta tabela é a referência autoritativa para essas siglas — consulte-a sempre que um comentário/docstring citar um EP-XX-TXX sem contexto adicional.

Épico Título Módulo/teste principal
EP-00 Bootstrap e tooling da RI pyproject.toml, Makefile, .github/workflows/ci.yml, src/emcp_bus/common/config.py
EP-01 Identidade do MCP Client e autenticação src/emcp_bus/identity/*.py, deploy/keycloak/realm-export.json
EP-02 Global Capability Registry e publishing pipeline src/emcp_bus/registry/*.py
EP-03 PDP / Policy Engine (OPA) src/emcp_bus/pdp/*.py, config/policies/*.rego
EP-04 Entitlement Manager src/emcp_bus/entitlement/*.py
EP-05 MCP Gateway / PEP src/emcp_bus/gateway/*.py
EP-06 Enterprise MCP Fabric e servidores de domínio de exemplo src/emcp_bus/fabric/*.py, services/example_mcp_servers/*
EP-07 Downstream identity (inbound vs outbound) src/emcp_bus/downstream/*.py
EP-08 Auditoria, correlação e observabilidade src/emcp_bus/audit/*.py, src/emcp_bus/common/otel.py
EP-09 Profile Intelligence (contrato genérico) src/emcp_bus/profile_intelligence/*.py
EP-10 Offering Filter src/emcp_bus/offering_filter/service.py
EP-11 Service Orchestrator (NBA/NBO) src/emcp_bus/orchestrator/*.py
EP-12 Agent Runtime e channel adapters src/emcp_bus/agent_runtime/*.py
EP-13 Agente de exemplo e instrumentação Langfuse agents/example_agent/*
EP-14 Human-in-the-loop / Approval workflow src/emcp_bus/approval/*.py
EP-15 Testes de segurança e adversariais tests/contract/, tests/e2e/test_security_acceptance.py, tests/adversarial/, tests/e2e/test_fail_closed.py
EP-16 Deploy final e documentação docker-compose.yml, docs/adr/

Dentro de cada épico, TXX numera as tarefas (ex.: EP-05-T03 = terceira tarefa do épico EP-05/Gateway — o cache hint de tools/list). Os ADRs referenciados por número (ADR-XXX) são arquivos individuais em ../../docs/adr/.

Componentes

Identidade do MCP Client (EP-01)

Responsabilidade: autenticar a identidade de workload que faz a chamada, nunca a identidade autodeclarada em clientInfo ou em qualquer campo do payload. src/emcp_bus/identity/authn.py (TokenValidator) valida um bearer token OAuth2/OIDC contra o JWKS do IdP (assinatura RS256, exp, iss, aud); o client_id confiável vem exclusivamente da claim azp/client_id do token verificado. src/emcp_bus/identity/models.py (ClientRegistration) mapeia esse client_id a exatamente um ClientProfile (EP-04) via YAML em config/clients/*.yaml. shared_client_lint.py (EP-01-T05) valida em CI que dois agentes distintos nunca compartilham o mesmo client_id sob profiles diferentes.

Validado contra: Keycloak 26.7.3 real via Docker (tests/e2e/test_real_keycloak.py) — realm emcp importado de deploy/keycloak/realm-export.json, 3 clients (sales-read-agent, sales-write-agent, finance-payments-agent) com grant client_credentials; e um servidor JWKS HTTP local mais leve como stand-in para o resto da suíte e2e (válido para a lógica de validação, não para a integração de protocolo real com o IdP).

Extensão de produção documentada, não implementada: mTLS/workload identity — docs/adr/ADR-019-extensao-de-identidade.md.

PDP / Policy Engine (EP-03)

Responsabilidade: decidir ALLOW/DENY/REQUIRE_APPROVAL de forma determinística e auditável. Modelo em dois passos (src/emcp_bus/pdp/client.py, PDPClient.evaluate): (1) entitlement.rego decide se a tool está no ClientProfile.allowed_tools — DENY aqui é definitivo, nenhum argumento é avaliado; (2) transaction.rego restringe por argumento/recurso (limite de valor, região, classificação do cliente) e pode rebaixar um ALLOW para REQUIRE_APPROVAL acima de approval_threshold. Toda resposta é um PDPDecision (outcome, policy_version, reason_code) — nunca um booleano. policy_version é um hash SHA-256 (12 chars) do conteúdo dos arquivos .rego em disco, computado uma vez no boot do Gateway.

Validado contra: OPA real via subprocess (binário opa run --server), 9 testes Rego (opa test) + 6 testes Python de integração.

Global Capability Registry (EP-02)

Responsabilidade: control-plane asset com autoridade e ciclo de vida próprios sobre o catálogo de capabilities, separado da visão de execução que o Gateway expõe a cada client. registry/models.py define CapabilityManifest (nome canônico <domínio>.<capability>, risk_tier R0–R4, data_classification, side_effects, idempotent, entitlements, backend, approval, observability, lifecycle). registry/pipeline.py (PublishingPipeline) é o único caminho de entrada: valida gates de negócio (owner obrigatório, backend declarado, tiers R3/R4 exigem approval.required=true) e avança NOMINATE → REVIEW → REGISTER; um publish() separado avança REGISTER → ACTIVE, quando a capability se torna resolvível pelo Fabric. registry/lifecycle.py garante mecanicamente que nada alcança ACTIVE sem passar por REVIEW. registry/store.py persiste em SQLite; registry/seed.py popula o Registry a partir de config/capabilities/*.yaml no boot do Gateway (um arquivo temporário por processo, não compartilhado entre execuções).

Validado contra: SQLite real, 9 manifestos de exemplo (6 Sales + 3 Finance) passando pelo pipeline completo sem intervenção manual.

PEP / MCP Gateway (EP-05)

Responsabilidade: único Policy Enforcement Point. src/emcp_bus/gateway/server.py implementa dois invariantes: tools/list retorna a interseção do catálogo dos backends entitled com o MaximumEntitlement do client autenticado (discovery filtrado); tools/call é reautorizado de forma totalmente independente do que tools/list retornou — o PDP é consultado em toda chamada. Sem token válido: tools/list vazio, tools/call sempre Access denied: UNAUTHENTICATED. PDP inalcançável é DENY (PDP_UNAVAILABLE), nunca ALLOW implícito. Falha do AuditSink nunca vira erro 500 numa operação já decidida (_emit_audit, engole exceção em todos os 8 pontos de emissão). Roteamento por capability é resolvido a cada chamada via CapabilityRouter (EP-06) contra o Registry — um client pode ver tools de mais de um backend (ex.: Sales + Finance). Ver interfaces/mcp-gateway.md para o contrato completo (headers, códigos de negação, cache hint).

Validado contra: OPA real, Keycloak real (Docker), JWT RS256 reais, e um cliente MCP real do host contra o Gateway containerizado (docker compose --profile core [--profile identity] up).

Enterprise MCP Fabric e servidores de domínio de exemplo (EP-06)

Responsabilidade: src/emcp_bus/fabric/router.py (CapabilityRouter) resolve um nome de tool <domínio>.<capability> para o backend que a serve, usando apenas manifestos ACTIVE do Registry — nunca um mapeamento estático no Gateway. services/example_mcp_servers/sales_domain/ (6 tools, R1+R2) e finance_domain/ (3 tools: invoice.get R1, payment.create R2, payment.execute R3 com ciclo de aprovação completo) são os dois domínios de exemplo, protegidos por BackendCredentialGate (EP-05-T07) — uma requisição sem a credencial de saída do Gateway nunca alcança um handler de tool. fabric/adapters/rest_adapter.py (EP-06-T04) implementa um adapter REST genérico, testado de forma standalone (httpx.MockTransport); nenhum manifesto seed usa backend.type: "rest", então não há caminho ao vivo através do Gateway exercitando-o.

Validado contra: roteamento real Sales→sales-domain / Finance→finance-domain nunca cruzado, inclusive cross-container via Docker Compose real (segmentação de rede confirmada: nenhum dos domain servers ou o OPA publica porta de host).

Downstream / outbound identity (EP-07)

Responsabilidade: a credencial que o Fabric apresenta ao backend nunca é o token inbound do MCP Client (src/emcp_bus/downstream/identity.py, BackendCredentialProvider). Dois modos declarados por backend em config/backends/*.yaml (BackendConfig.credential_mode): service_account (credencial estática lida de uma env var no momento da chamada — o modo padrão dos backends seed) e token_exchange (RFC 8693 via Keycloak Standard Token Exchange, TokenExchangeClient — dois round-trips HTTP por troca nova, com cache por backend até pouco antes do expires_in). Ver interfaces/downstream-identity.md.

Validado contra: Keycloak 26.7.3 real — audience do exchange precisa ser um client id realmente registrado (uma string livre falha com "Audience not found"); pedir audience sem um scope que efetivamente adiciona essa audience falha com "Requested audience not available". Realm ganhou o client gateway-token-exchange e client scopes com oidc-audience-mapper por backend. 7 testes (4 unit com httpx.MockTransport, 3 e2e contra Keycloak real, incluindo um tools/call completo pelo Gateway usando uma credencial obtida por token exchange).

Auditoria, correlação e observabilidade (EP-08)

Responsabilidade: audit/models.py define EventEnvelope com uma taxonomia de 3 categorias adotada da referência Agent Platform OCI (ADR-022): IC (Indicador de Controle/negócio-jornada, ex. client_authenticated, tool_call_allowed), NOC (operacional/erro de disponibilidade, ex. pdp_unavailable), GRL (guardrail/governança, ex. tool_call_denied, tools_list_filtered, bypass_attempt_detected). Todo evento sanitiza recursivamente qualquer chave cujo nome contenha token/secret/password/authorization/credential, substituindo o valor por sha256:<hex> — nunca um valor de segredo chega a um sink. audit/correlation.py propaga decision_id/policy_decision_id/entitlement_version/policy_version/ mcp_request_id como campos de primeira classe em todo evento e como atributos de span OTel (ADR-017). audit/sink.py (JSONLFileAuditSink) grava em arquivo JSONL append-only. common/otel.py instala tracing automático (OpenTelemetryMiddleware) sem código manual de span no Gateway. Ver interfaces/audit-events.md.

Validado contra: correlação de trace_id real entre containers Docker distintos (Gateway ↔ domain server) para uma chamada bem-sucedida. Stack local otel-collector + Jaeger (EP-08-T04, perfil observability): configuração validada (docker compose config), não exercitada com tráfego OTLP real por um teste automatizado — Langfuse (EP-13-T02) é quem recebeu tráfego OTLP real de fato, validado manualmente (ver componente "Agente de exemplo" abaixo).

Entitlement Manager (EP-04)

Responsabilidade: entitlement/manager.py (EntitlementManager) resolve o MaximumEntitlement combinando o ClientProfile (YAML validado, entitlement/models.py) com a decisão de entitlement do PDP, produzindo um ResolvedEntitlement versionado (entitlement_version) com allowed_tools/risk_ceiling/profile_name. sync_profiles_to_pdp empurra os profiles validados para o data document do OPA no boot — falha aqui impede o Gateway de subir "saudável" (fail-closed, README.md §38). Revogação invalida o cache de entitlement e reavalia a partir da próxima chamada.

Validado contra: OPA real + YAML reais; revogação de política ao vivo contra um Gateway rodando (tests/adversarial/test_protocol_identity.py).

Approval workflow (EP-14)

Responsabilidade: approval/service.py (ApprovalService) gate para decisões REQUIRE_APPROVAL: check_and_consume (chamado pelo Gateway a cada tentativa) só retorna verdadeiro para uma aprovação concedida, não expirada e ainda não consumida — single-use, nunca "enfileira e libera". Um ApprovalRequest é vinculado a um hash SHA-256 do triplo exato (client_profile, tool, arguments) (compute_operation_hash), não apenas ao nome da tool: uma aprovação para uma chamada nunca satisfaz uma chamada diferente com os mesmos tool/profile. check_and_consume é protegido por threading.Lock (achado real de concorrência, ver "Limitações" abaixo). Ver interfaces/approval-workflow.md.

Validado contra: ciclo completo e2e para Sales e Finance (nega → aprova externamente → executa → replay nega); replay sob concorrência real (ThreadPoolExecutor, 20 threads).

Profile Intelligence (EP-09)

Responsabilidade: profile_intelligence/models.py define ProfileView (subject_ref pseudonimizado, profile_version, segments, attributes, reason_codes, expires_at) como um contrato genérico e substituível — qualquer ProfileIntelligenceProvider (o stub determinístico rule-based em rule_based_provider.py, ou um modelo de clustering real numa implantação de produção) produz o mesmo contrato. Um ProfileView nunca é consultado pelo PDP e nunca pode conceder uma capability fora do MaximumEntitlement — nada aqui está conectado ao caminho de autorização. governance.py implementa os guardrails de governança: pseudonimização, log de linhagem, stub de monitor de drift.

Validado contra: determinismo (mesmo subject_ref + janela ISO → mesmo profileVersion).

Offering Filter (EP-10)

Responsabilidade: offering_filter/service.py implementa FilteredOfferings = ActiveCatalog ∩ MaximumEntitlement ∩ ConsentContext. A ordem da interseção é deliberada: allowed_tools (MaximumEntitlement, já resolvido pelo EP-04) é o único termo tratado como relevante para segurança e é aplicado primeiro; um ConsentContext (aqui, apenas allow_restricted_data sobre CapabilityManifest.data_classification) só pode reduzir o que já sobreviveu a essa interseção, nunca reintroduzir uma capability fora do entitlement.

Validado contra: invariante de subconjunto provada com Hypothesis, 2000 casos gerados (tests/unit/test_offering_filter_invariants.py).

Service Orchestrator (EP-11)

Responsabilidade: orchestrator/graph.py (build_journey_graph) é um StateGraph real do LangGraph com nós fixos resolve_profile → resolve_entitlement → filter_offerings → decide_nba → approval_gate, mais uma reentrada dedicada para o loop de recálculo após DENY (handle_denial_node, que nunca chama o Fabric diretamente — apenas recalcula a NBA sobre as offerings restantes). orchestrator/nba_model.py define NBADecision (decision_id, action, capability_version, subject_ref, channel, agent, reason_codes, policy_context, expires_at) e o protocolo NBADecisionModel — trocar a implementação de referência (decisioning.RuleBasedNBADecisionModel) por um modelo de ranking/IA não muda nada no contrato de entrada/saída nem no que EP-03/04/05 autorizam, porque o modelo só ordena/seleciona entre DecisionContext.filtered_offerings, já intersectado pelo EP-10 antes de chegar aqui. orchestrator/hitl_node.py usa langgraph.types.interrupt/ Command(resume=...) para pausar de verdade o grafo em aprovação — sem timeout que vire ALLOW implícito.

Validado contra: OPA real + LangGraph real + checkpointer SQLite real, incluindo prova de que retomar de um checkpoint pausado não reprocessa nós já executados.

Agent Runtime (EP-12)

Responsabilidade: agent_runtime/dispatcher.py (AgentDispatcher) consome uma NBADecision e a despacha contra o Gateway real, autenticado como o MCP Client já registrado para o agente alvo — nunca gera ou armazena credencial própria (AgentTokenProvider, tipicamente um client_credentials real contra o mesmo IdP de qualquer outro MCP Client). handoff_graph.py (EP-12-T03, execute_handoff/build_handoff_payload) implementa handoff entre agentes com minimização de contexto e resolução de entitlement sempre independente por agente — ver "Limitações" abaixo para o achado de que nem o Modo A nem o Modo B (EP-13-T05/T06) acabaram usando esse primitivo. identity_resolver.py (EP-12-T04, IdentityResolver) normaliza nomes de campo entre canais (config/orchestrator/identity.yaml) e nunca é consultado pelo PDP — checado mecanicamente via análise de AST no teste unitário correspondente, a mesma disciplina que global_supervisor.py usa para provar que nunca importa emcp_bus.orchestrator.

Validado contra: Gateway + Keycloak reais — AgentDispatcher alcançando o backend Sales real e sendo negado no cross-domain; dois agentes reais obtendo tokens client_credentials independentes.

Agente de exemplo (EP-13)

Responsabilidade: agents/example_agent/agent.py é um agente conversacional com tool-calling real contra um endpoint OpenAI-compatible, cujo catálogo é restrito ao próprio tools/list do client. common/pii_redaction.py redige PII/segredos antes de qualquer export. prompts/store.py implementa versionamento local de prompt (nunca sobrescreve uma versão anterior) como núcleo testável — sincronização com a API de Prompt Management do Langfuse é um adapter fino documentado, não implementado.

Validado contra: um LLM real (meta.llama-3.3-70b-instruct via endpoint OpenAI-compatible da OCI Generative AI), com tool-calling confirmado empiricamente; o cenário de prompt injection do README.md §29 (pedir uma tool fora do entitlement) reproduzido de verdade — a chamada nunca executa, seja porque o modelo nunca viu a tool no próprio catálogo, seja porque o Gateway/PDP nega. Langfuse self-hosted real (6 containers: web/worker/postgres/clickhouse/redis/minio, deploy/langfuse/docker-compose.override.yml) recebendo tráfego OTLP real do agente — spans de MCP e de "generation" (com gen_ai.request.model/tokens reais) confirmados fisicamente na tabela events_core do ClickHouse. Essa validação é manual, não um teste e2e automatizado (custo de subir 6 containers a cada execução da suíte).

Modo A — Global Supervisor externo (EP-13-T05, ADR-024)

Responsabilidade: agent_runtime/global_supervisor.py (GlobalSupervisor) é o stand-in deliberadamente simples (roteamento por palavra-chave) para uma plataforma de orquestração externa que decide qual agente de backend atende uma conversa — nunca é, ou pretende ser, a fronteira de segurança; nunca importa emcp_bus.orchestrator (checado via AST). A propriedade de segurança real não está neste módulo: está no fato de que cada BackendAgent (agents/example_agent/backends/{sales_agent,finance_agent}/) resolve seu próprio MCP Client/entitlement de forma completamente independente — token, tools/list e reautorização próprios — e o GlobalSupervisor nunca lê, guarda ou repassa a credencial de um backend para outro.

Validado contra: LLM real + Gateway real (tests/e2e/test_multi_agent_mode_a.py, 2 testes): handoff de sales_agent para finance_agent com token completamente distinto; injeção de prompt via handoff tentando finance.payment.execute continua negada pelo Gateway/PDP reais.

Modo B — Orchestrator como único cérebro de NBA/NBO (EP-13-T06, could, ADR-024)

Responsabilidade: agents/example_agent/mode_b_orchestrator_driven/orchestrator.py (ModeBOrchestrator) invoca o journey StateGraph real do EP-11-T02 (não modificado) uma vez por client_id relevante para o mesmo subject, combina os NBADecisions resultantes (regra de referência: pendência de Finance sempre vence oferta discricionária de Sales) e despacha o vencedor via AgentDispatcher real — nunca cria um caminho de autorização próprio. Deliberadamente não estende JourneyState/build_journey_graph para múltiplos client_ids nativamente; a combinação vive uma camada acima do grafo (decisão de design registrada no próprio módulo).

Validado contra: Keycloak real + OPA real + Sales/Finance reais (tests/e2e/test_real_keycloak.py::test_mode_b_orchestrator_combines_offerings_and_dispatches_through_the_real_chain): candidatos de ambos os domínios realmente combinados, decisão final do nosso Orchestrator, dispatch ALLOW de ponta a ponta.

O que foi validado, por dependência real

Dependência real O que exercita Onde
OPA (subprocess) entitlement.rego/transaction.rego, PDPClient tests/unit/policies/, toda a suíte e2e
Keycloak (Docker, 26.7.3) client_credentials, JWKS real, token exchange RFC 8693, Modo B tests/e2e/test_real_keycloak.py
LangGraph 0.6 + checkpointer SQLite journey graph, pausa/retomada de aprovação tests/unit/test_orchestrator_graph.py, e2e do M3
LLM real (OpenAI-compatible) tool-calling do agente de exemplo, prompt injection tests/e2e/test_example_agent.py, test_multi_agent_mode_a.py
Docker Compose completo build+up+fail-closed+trace cross-container+segmentação de rede tests/e2e/test_security_acceptance.py, validação manual documentada
Langfuse self-hosted (6 containers) export OTLP real, spans de generation Validação manual (não automatizada)

Limitações e achados arquiteturais reais

  • ttl_ms do cache hint de tools/list (SEP-2549, EP-05-T03): implementado corretamente em gateway/cache.py, mas o campo pertence ao protocolo 2026-07-28, que só existe no modo stateless per-request do SDK mcp (sem handshake initialize). Esta RI usa o handshake de sessão clássico em toda parte (Gateway↔Client e Gateway↔backends), então ttl_ms nunca chega ao wire nesta arquitetura — o SDK descarta o campo na serialização para versões de protocolo anteriores a 2026-07-28 (verificado empiricamente). cache_scope="private" é sempre definido, mas por não depender do wire-mode também não é algo que um teste e2e consiga verificar de forma discriminante. A propriedade de segurança real (nunca vazar catálogo entre clientes) é garantida de forma independente: o Gateway nunca cacheia o catálogo filtrado no servidor.
  • Consistência header/body (EP-05-T08): implementada como um gate próprio (gateway/canonical_request.py) reaproveitando as constantes de campo do SDK, não como o ladder de validação nativo do SDK (que só se aplica ao mesmo modo stateless per-request de 2026-07-28 acima) — pela mesma razão, esta RI não o alcança pelo caminho nativo. Cabeçalhos ausentes (um client que não implementa esse SEP) não são um erro.
  • Adapter REST (EP-06-T04): implementado e testado de forma standalone (fabric/adapters/rest_adapter.py); nenhum backend de exemplo desta RI é REST (ambos são MCP-nativos), então o roteamento ao vivo através do Gateway para um backend type: "rest" nunca foi exercitado end-to-end.
  • Handoff entre agentes (EP-12-T03, execute_handoff/build_handoff_payload): continua implementado e testado (tests/unit/test_agent_runtime_handoff.py), mas nem o Modo A (EP-13-T05) nem o Modo B (EP-13-T06) acabaram usando esse primitivo. O Modo A precisa de roteamento conversacional (qual backend responde à próxima mensagem), não de despacho de uma NBADecision específica — GlobalSupervisor chama run_turn() diretamente nos backends. O Modo B já combina NBADecisions de múltiplos clients num único passo e despacha o vencedor via AgentDispatcher.dispatch puro — não há um agente "de origem" cedendo controle a outro para execute_handoff interceptar. O primitivo permanece correto e reutilizável para um cenário futuro de handoff dentro de uma jornada Orchestrator-driven, mas essa forma específica de uso não existe nesta RI.
  • Replay de requisição: sem nonce/anti-replay nesta RI — uma requisição capturada com um token ainda válido pode ser reenviada com sucesso. Mitigado por tokens de vida curta (credential_policy.short_lived em todo ClientProfile seed) e por auditoria completa (cada tentativa, mesmo idêntica, gera decision_id próprio). Ver tests/adversarial/test_protocol_identity.py.
  • Rate limiting / bulk exfiltration: sem limitação de taxa de requisições no Gateway — N leituras legítimas consecutivas são todas permitidas. Mitigado apenas por detectabilidade a posteriori via auditoria (cada chamada gera evento correlacionado), nunca por prevenção em tempo real.
  • Race condition real corrigida em ApprovalService: check_and_consume fazia check-then-set em duas operações não atômicas — sob concorrência real de threads, duas tentativas podiam ambas observar "granted" antes de qualquer uma escrever "consumed" (double-spend de um grant single-use). Corrigido com threading.Lock dedicado, provado com ThreadPoolExecutor (20 threads) em tests/adversarial/test_protocol_identity.py.
  • Falha de audit sink podia virar erro 500 real, corrigida: nenhuma das 8 chamadas audit_sink.emit(...) no Gateway estava protegida — uma falha no sink (disco cheio, fila indisponível) podia transformar uma operação já ALLOWed num erro 500 para o caller, violando a política de não bloquear operações de baixo risco. Corrigido com o helper _emit_audit (gateway/server.py), que engole qualquer exceção do sink nos 8 pontos de emissão, para ALLOW e para DENY. Uma falha do sink em produção precisa de observabilidade própria (ver Production-Recommendations.md) — este comportamento é correto por design, mas silencioso.
  • Roteamento do GlobalSupervisor (Modo A): por palavra-chave simples, deliberadamente não é a fronteira de segurança nem uma reivindicação de qualidade de roteamento — apenas um stand-in honesto para o que uma plataforma externa real faria.
  • Regra de combinação do ModeBOrchestrator (Modo B): "Finance sempre vence Sales" é uma regra de referência simples, não uma reivindicação de otimalidade — mesma postura de RuleBasedNBADecisionModel (EP-11-T03).
  • Stack de observabilidade (EP-08-T04): configuração validada (docker compose config), não exercitada com tráfego real por um teste automatizado (a subida completa com o perfil observability foi validada manualmente).