Enterprise MCP Service Bus — Reference Implementation
Implementação executável da arquitetura descrita em ../README.md, planejada em ../docs/RI-PLANNING.md.
Status: M0, M1, M2, M3 e M4 concluídos — backlog completo, nenhuma tarefa aberta (M0-M2 implementados 2026-09-11 e revalidados 2026-09-14; M3 implementado 2026-09-14; M4 implementado e concluído 2026-09-14, incluindo as duas tarefas
couldEP-13-T05/T06). 260 testes automatizados (253 passando com--forked— unit + e2e + contract + adversarial, sem flakiness — mais 7 testes de aceite de segurança contra o docker-compose real, EP-15-T02, rodados separadamente pelo custo de build+Keycloak por execução). M3 (Profile Intelligence, Offering Filter, Service Orchestrator com LangGraph real, Agent Runtime, agente de exemplo com LLM real + Langfuse self-hosted real) está descrito na seção "M3 — Personalização e orquestração"; M4 (testes de contrato, suíte de aceite de segurança, testes adversariais, testes de fail-closed, os dois modos multiagente do ADR-024, ADRs formalizados) na seção "M4 — Hardening e entrega", ambas abaixo. Detalhes completos e critérios de aceite verificados emdocs/RI-PLANNING.mdseção 8.7.
Quickstart (sem Docker)
make install # cria .venv e instala o pacote em modo editável (dev extras inclusos)
make lint # ruff check + ruff format --check
make typecheck # mypy --strict
make validate-schemas # valida todo YAML de config/ contra seu modelo Pydantic
make export-schemas # regenera schemas/*.schema.json a partir dos modelos
make test # pytest --forked (unit + e2e) -- ver nota abaixo
make test precisa do binário opa no PATH (ou OPA_BINARY apontando para ele) para os testes que exercitam o PDP de verdade — ver "Como instalar o OPA" abaixo. Sem ele, esses testes são pulados (pytest.skip), não falham. Os testes de test_real_keycloak.py precisam de docker no PATH pela mesma razão.
Flakiness da suíte e2e: resolvida com --forked
Sessões anteriores documentaram falhas intermitentes (mcp.shared.exceptions.MCPError: SSE stream ended without a response) ao rodar pytest tests/e2e/ sem isolamento de processo — a hipótese era limpeza assíncrona (anyio/httpx/uvicorn) compartilhada entre pares de servidor ASGI em processo, na mesma sessão pytest, e várias mitigações (mais retries, pausas, isolar por arquivo) não resolviam. O follow-up então recomendado — isolar cada teste em subprocesso próprio via pytest-forked — foi implementado e confirma a hipótese: com --forked (um processo fork() por função de teste), a suíte inteira passa de forma consistente. Verificado nesta sessão: 119/119 testes (97 unit + 22 e2e) passando em múltiplas execuções seguidas, contra a mesma taxa de 1-9 falhas por execução sem --forked. make test e .github/workflows/ci.yml já usam --forked por padrão; o custo é só tempo de execução (~+1min nesta máquina).
Como instalar o OPA (sem Docker)
OPA é um binário Go estático — não precisa de Docker nem de build:
curl -sL -o /tmp/opa "https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static"
chmod +x /tmp/opa
cp /tmp/opa .venv/bin/opa # coloca no PATH do venv
opa version # confirma
Versão usada durante o desenvolvimento desta RI: OPA v1.20.2 (Rego v1 é o padrão — as policies em config/policies/*.rego usam import rego.v1).
Subir a stack completa localmente (sem Docker)
source .venv/bin/activate
opa run --server --addr 127.0.0.1:8181 config/policies/ &
export SALES_DOMAIN_SERVICE_TOKEN=dev-sales-domain-service-token # EP-07: credencial de saída, nunca o token do client
export FINANCE_DOMAIN_SERVICE_TOKEN=dev-finance-domain-service-token
python3 -m example_mcp_servers.sales_domain.server &
python3 -m example_mcp_servers.finance_domain.server &
# Sem OIDC_JWKS_URL/OIDC_ISSUER, o Gateway roda em modo "sem IdP configurado":
# nega tudo por padrão (ver "Dois modos de deployment" abaixo).
python3 -m emcp_bus.gateway.server
O Fabric router (EP-06-T01) resolve cada capability para o backend correto via config/backends/*.yaml + o Registry (seedado de config/capabilities/ no startup do Gateway) — não há mais uma única SALES_DOMAIN_URL estática cobrindo tudo.
Para autenticação real, aponte OIDC_JWKS_URL/OIDC_ISSUER para um Keycloak real (EP-01-T01, ✅ implementado e validado — ver deploy/keycloak/realm-export.json e tests/e2e/test_real_keycloak.py) ou qualquer IdP OIDC compliant (TokenValidator é agnóstico de fornecedor). A maior parte da suíte (tests/e2e/conftest.py) continua usando um servidor JWKS local mais leve como stand-in — válido para a lógica de validação, mas test_real_keycloak.py é quem prova a integração com Keycloak de verdade (client_credentials grant, claims reais, iss/aud reais).
Quickstart (Docker)
make up # == docker compose --profile core up --build (sem IdP)
make down # == docker compose --profile core down
make logs # == docker compose --profile core logs -f
# Com Keycloak real (EP-01-T01):
cp .env.example .env # descomente as 3 linhas OIDC_*
docker compose --profile core --profile identity up --build
✅ Validado com um daemon Docker real (Docker 29.8.0 / Compose v5.5.1) —
gateway+sales-domain+opa(+keycloak, opcional) sobem, o Gateway sincroniza entitlements reais com o OPA real do compose, e um cliente MCP real do host completatools/list/tools/callatravés do Gateway containerizado, com ou sem Keycloak. Ver "O que foi validado com Docker" abaixo para o que exatamente foi exercitado (incluindo três bugs reais encontrados e corrigidos ao longo dessas rodadas). Sem.env/perfilidentity, o Gateway do compose roda em modo "sem IdP" (toda requisição não autenticada, deny-by-default) — os dois modos são deliberados, não um "modo incompleto" esperando o outro.
O que foi validado com Docker
Build + up + smoke test end-to-end, mais dois testes adicionais montando um segundo container gateway manualmente (docker run --network ri_default ...) para exercitar caminhos que o compose padrão (sem Keycloak) não cobre, e (nesta rodada) uma segunda validação completa após o Fabric router multi-backend (EP-06-T01), o domínio Finance real (EP-06-T03), a segmentação de rede (EP-05-T07) e a stack de observabilidade (EP-08-T04):
docker compose --profile core up --buildsobe os 4 serviços (opa,sales-domain,finance-domain,gateway); ambos os domain servers ficamhealthy;gatewaysincroniza comopae seeda o Registry a partir deconfig/capabilities/no startup, permanecendo de pé.- Modo sem OIDC (compose padrão):
tools/listvazio,tools/callnegado comUNAUTHENTICATED— confirmado com um cliente MCP real do host contrahttp://127.0.0.1:8000/mcp. - Sync real de entitlement:
curl http://127.0.0.1:8181/v1/data/emcp/client_profilesno OPA do compose mostra os profiles reais (Sales.Read,Finance.Payments, ...) que o Gateway empurrou no startup — não é um mock. - Segmentação de rede real (EP-05-T07):
opa,sales-domainefinance-domainnão publicam porta de host no compose — confirmado comcurl http://127.0.0.1:8100/mcpdo host retornandoConnection refused, algo que só um daemon Docker real prova (Docker só expõe uma porta de container para o host viaports:, então a ausência dela já é a segmentação, sem precisar de uma rede Docker customizada adicional). - Caminho autenticado completo, cross-container, incluindo Finance: com
--profile identity(Keycloak real) e um tokenclient_credentialsreal parafinance-payments-agent,tools/listretorna exatamente os 3 tools deFinance.Payments(roteados pelo Fabric router para o containerfinance-domain, nãosales-domain) etools/call finance.payment.createexecuta de verdade contra o backend Finance real, retornando umpayment_idgenuíno. - Correlação de trace entre containers de verdade (não em-processo, como nos testes e2e): o mesmo
trace_idaparece nos logs degatewaye do domain server correspondente para uma chamada bem-sucedida — a forma mais forte de validar o critério de aceite do Marco M0. - Fail-closed real: com o container
opaparado, um novo containergatewayfalha ao subir (Application startup failed. Exiting.,PDPUnavailableErrorno traceback) — confirma README.md §38 sob Docker de verdade, não só em teste unitário. - Keycloak real, via
docker compose --profile core --profile identity up(EP-01-T01): realmemcpimportado dedeploy/keycloak/realm-export.json(3 clients —sales-read-agent,sales-write-agent,finance-payments-agent— cada um comclient_credentialsgrant habilitado e um mapper de audience paraemcp-gateway); token obtido viaclient_credentialsreal, validado pelo Gateway contra o JWKS real do Keycloak,tools/list/tools/callcorretos paraSales.Read, negação correta cross-domain (Sales.Read→finance.payment.execute) eREQUIRE_APPROVALcorreto paraFinance.Paymentsacima do threshold — tudo com identidades realmente distintas emitidas pelo mesmo IdP. tests/e2e/test_real_keycloak.pyautomatiza o item 8 (sobe seu próprio container Keycloak viadocker run, pula sedockernão estiver no PATH) — 4 testes, agora estáveis com--forked(ver nota de flakiness acima).
O perfil observability (otel-collector + jaeger, EP-08-T04) não foi exercitado com tráfego real nesta rodada (apenas docker compose config validado) — os dois serviços usam imagens oficiais padrão (otel/opentelemetry-collector-contrib, jaegertracing/all-in-one) com uma config mínima (deploy/otel/collector-config.yaml), sem lógica própria da RI além da configuração em si.
Três bugs reais encontrados e corrigidos ao longo da validação original (nenhum seria pego sem um daemon Docker/Keycloak real):
deploy/docker/Dockerfilenão copiavaconfig/para a imagem — o Gateway falhava ao subir (ConfigError: diretórioconfig/clientsnão encontrado) porque/app/configsimplesmente não existia no container. Corrigido comCOPY config ./config.- O healthcheck de
opanodocker-compose.ymlusavawget, que não existe na imagem oficialopenpolicyagent/opa(é uma imagem estilo distroless, só o binárioopa, sem shell). Isso deixavagateway(que dependia deopa: condition: service_healthy) preso para sempre emCreated. Corrigido: sem healthcheck emopa(não há ferramenta na imagem para escrevê-lo),gatewaydepende deopa: condition: service_startede ganhourestart: on-failure:5— o próprio fail-closed do Gateway (item 6 acima) faz o papel de "esperar o OPA ficar pronto de verdade", com um pequeno número de retries do Compose cobrindo a janela entre o processo do OPA iniciar e aceitar conexões. - Keycloak deriva o claim
issdoHostheader da requisição usada para obter o token, a menos queKC_HOSTNAMEseja fixado — um token obtido via127.0.0.1:8080(do host, para teste manual) tinhaissdiferente do que o Gateway (que valida viakeycloak:8080, nome interno do compose) esperava, e era rejeitado por "wrong issuer" mesmo sendo um token 100% válido. Corrigido fixandoKC_HOSTNAME(keycloakno compose; a URL completahttp://127.0.0.1:<porta>no teste automatizado, já queKC_HOSTNAMEnão aceitahost:portasem esquema).
Dois modos de deployment (importante)
O Gateway (src/emcp_bus/gateway/server.py) suporta dois modos, sempre deny by default:
- Com OIDC configurado (
OIDC_JWKS_URL+OIDC_ISSUERdefinidos): o próprio middleware de auth do SDKmcp(RequireAuthMiddleware) rejeita qualquer conexão sem bearer token válido antes mesmo do handshakeinitialize— mais estrito que qualquer checagem no nível do handler. - Sem OIDC configurado (padrão se as env vars não estiverem definidas): nenhum middleware de auth é instalado; toda requisição chega aos handlers com
get_access_token() is None, e é a própria lógica do handler (on_list_tools/on_call_tool) que nega por padrão —tools/listvazio,tools/callsempre negado comAccess denied: UNAUTHENTICATED.
Os dois modos são testados explicitamente em tests/e2e/test_governed_gateway.py.
M3 — Personalização e orquestração
Implementado e validado em 2026-09-14 (mesma sessão). Perfil → entitlement → ofertas filtradas → NBA → dispatch, ponta a ponta:
- Profile Intelligence (
src/emcp_bus/profile_intelligence/):ProfileViewversionado, provider rule-based determinístico (mesmosubject_ref+janela ISO → mesmoprofileVersion), guardrails de governança (pseudonimização, lineage log, stub de drift). - Offering Filter (
src/emcp_bus/offering_filter/):FilteredOfferings = ActiveCatalog ∩ MaximumEntitlement ∩ ConsentContext— invariante de subconjunto provada com Hypothesis (2000 casos gerados,tests/unit/test_offering_filter_invariants.py). - Service Orchestrator (
src/emcp_bus/orchestrator/):StateGraphreal do LangGraph 0.6 com checkpointer SQLite real — nósresolve_profile → resolve_entitlement → filter_offerings → decide_nba → approval_gate, mais um caminho de reentrada para recálculo após DENY (handle_denial, nunca chama o Fabric diretamente). O nóapproval_gateusalanggraph.types.interrupt/Command(resume=...)— pausa de verdade, sem timeout que vire ALLOW implícito, integrado aoApprovalServicedo EP-14. - Agent Runtime (
src/emcp_bus/agent_runtime/):AgentDispatchernunca gera/armazena credencial própria — usa exclusivamente o MCP Client já registrado, viaclient_credentialsreal contra o mesmo IdP de qualquer outro client. Channel adapter conversacional + Identity Resolver (config/orchestrator/identity.yaml, nunca consultado pelo PDP — checado via AST em teste). - Agente de exemplo (
agents/example_agent/): tool-calling real contra um endpoint OpenAI-compatible (verificado commeta.llama-3.3-70b-instructvia OCI Generative AI), catálogo restrito aotools/listdo próprio client. O cenário de prompt injection do §29 (pedirfinance.payment.executefora do entitlement) é reproduzido de verdade emtests/e2e/test_example_agent.py— a chamada nunca é executada, seja porque o modelo nunca viu a tool no seu próprio catálogo, seja porque o Gateway/PDP nega. - Langfuse self-hosted (
deploy/langfuse/docker-compose.override.yml, perfilllm-observability): adaptado dodocker-compose.ymloficial do Langfuse (upstream, 2026-09-14) — 6 containers (web/worker/postgres/clickhouse/redis/minio). Validado de verdade nesta sessão: stack subida viadocker compose ... --profile llm-observability up, um turno real do agente de exemplo (LLM real + Gateway real) comOTEL_EXPORTER_OTLP_ENDPOINT/OTEL_EXPORTER_OTLP_HEADERSapontando para o endpoint OTLP nativo do Langfuse (/api/public/otel/v1/traces, sem nenhuma mudança de código — só variáveis de ambiente padrão do OTel SDK), e os spans (incluindo um span de "generation" comgen_ai.request.model/gen_ai.usage.{input,output}_token_countreais, viaagents/example_agent/agent.py) confirmados fisicamente na tabelaevents_coredo ClickHouse. Essa validação foi manual (não virou teste e2e automatizado, dado o custo de subir 6 containers a cada execução da suíte) — os comandos exatos estão documentados no histórico desta sessão; reproduzir localmente:docker compose -f docker-compose.yml -f deploy/langfuse/docker-compose.override.yml --profile llm-observability up -d, aguardarcurl http://localhost:3000/api/public/health, criar um projeto/API key (UI ouLANGFUSE_INIT_*), apontar as duas env vars OTel acima. - Prompt versioning (
agents/example_agent/prompts/store.py,could): versionamento local (nunca sobrescreve, sempre cria versão nova) como núcleo testável — sync real para a API de Prompt Management do Langfuse é um adapter fino documentado, não implementado nesta sessão.
Dependências novas: langgraph, langgraph-checkpoint-sqlite, hypothesis (dev), openai, python-dotenv (dev). Nenhuma delas é necessária para M0-M2 (Gateway/PDP/Registry continuam funcionando sem elas).
M4 — Hardening e entrega
Implementado e concluído em 2026-09-14 (mesma sessão do M3). Cobre EP-15 (testes de segurança/adversariais) completo, EP-16 (deploy final/documentação) completo, e os dois modos de integração multiagente do ADR-024 (EP-13-T05/T06, ambos should/could mas implementados):
- EP-15-T01 (testes de contrato) (
tests/contract/): conectam direto a cada domain server, nunca pelo Gateway — o contrato input/output de uma tool é propriedade do domain server (ADR-023), independente de quem está ou não entitled a chamá-la. Achado durante a implementação:CapabilityManifest(EP-02-T01) não carregainput_schema/output_schema— nunca fez parte do desenho do EP-02 (o manifesto é metadado de entitlement/risco, não formato). Resolvido sem reinterpretar silenciosamente o critério de aceite: (a) oinput_schemareal, auto-gerado pelo MCP SDK a partir da assinatura Python de cada tool, já é validado pelo próprio servidor (campo obrigatório ausente/tipo errado →is_error: Truevia Pydantic, antes de qualquer código da RI rodar) — exercitado diretamente, não reimplementado; (b) ooutput_schemaauto-gerado é genérico demais (additionalProperties: true, já que toda tool retornadict[str, object]) — resolvido com JSON Schema escrito à mão por tool (jsonschema, já dependência transitiva), validado contra a resposta real das 9 tools (6 Sales + 3 Finance). 20 testes. - EP-15-T02 (suíte de aceite de segurança, §45) (
tests/e2e/test_security_acceptance.py): roda contra odocker-compose.ymlreal (--profile core --profile identity, Keycloak incluído), não os fixtures in-process do resto da suíte e2e. 7 dos 10 testes formais automatizados diretamente: Teste 1 (unauthorized discovery), Teste 2 (direct unauthorized call), Teste 4 (role spoofing via payload forjado), Teste 5 (cache isolation, incluindocache_scope="private"), Teste 7 (gateway bypass —sales-domain/finance-domain/opanão publicam porta de host), Teste 9 (backend/transaction-policy isolation,TRANSACTION_POLICY_DENIEDreal), Teste 10 (client compromise containment). Os outros 3 já têm cobertura real documentada no docstring do arquivo em vez de duplicada: Teste 3 (prompt injection, LLM real) emtest_example_agent.py; Teste 6 (policy revocation, sem endpoint HTTP de revogação nesta RI) emtest_entitlement_manager.py; Teste 8 (token audience) emtest_real_keycloak.py, mesmo realm Keycloak real. - EP-15-T03 (testes adversariais de prompt/tool injection) (
tests/adversarial/{test_prompt_injection,test_tool_poisoning,test_payload_identity_spoofing}.py, 12 testes): contra o Gateway real. Injeção direta/indireta de conteúdo em argumentos nunca altera autorização nem o catálogo; hidden tool invocation e spoofing de nome (case/whitespace/typo) sempre negados por comparação exata; tool description poisoning provado com umMCPServermalicioso real montado em runtime (descrição hostil pedindo "acesso admin") — a Gateway repassa a descrição verbatim (nunca sanitiza) mas ela não tem efeito algum na decisão PDP; variação explícita do achado ADR-023 (agent_id/tenant_id/business_contextforjados nos argumentos) provada por negação de entitlement e por auditoria (tool_call_allowedsempre registra oclient_idreal, nunca o forjado). - EP-15-T04 (testes adversariais de protocolo/identidade) (
tests/adversarial/test_protocol_identity.py, 6 testes + referências cruzadas para o que já era coberto alhures): header/body mismatch rejeitado pré-autenticação; replay de requisição idêntica (gap real e documentado — sem nonce nesta RI, mitigado por token de vida curta) mas sempre auditado comdecision_iddistinto; revogação de política ao vivo contra um Gateway rodando (fecha a lacuna do Teste 6 que EP-15-T02 havia documentado); condição de corrida de entitlement sob concorrência real sem crash; approval replay sob concorrência real (ThreadPoolExecutor) — achado real:ApprovalService.check_and_consumenão era thread-safe, corrigido comthreading.Lock; bulk exfiltration (gap real e documentado — sem rate limiting) sempre com trilha de auditoria completa. - EP-15-T05 (testes de política de falha, §38) (
tests/e2e/test_fail_closed.py, 7 testes, um por linha da tabela): IdP inalcançável nega novas autenticações; PDP morrendo em runtime (não só no boot) falha fechado na próxima chamada; Registry semcapabilities/falha o boot do Gateway; achado real corrigido: nenhuma das 8 chamadasaudit_sink.emitdo Gateway estava protegida — uma falha no sink teria virado erro 500 numa operação já ALLOWed, corrigido com o helper_emit_audit; approval service falho nega (nunca crasha) uma operação de alto risco; backend morto falha só aquela requisição, sem derrubar o Gateway. - EP-13-T05 (Modo A — multiagente governado, ADR-024) (
src/emcp_bus/agent_runtime/global_supervisor.py+agents/example_agent/backends/{sales_agent,finance_agent}/):GlobalSupervisorcom roteamento por palavra-chave simples (deliberadamente não é a fronteira de segurança — documentado), nunca importaemcp_bus.orchestrator(checado via AST). Validado com LLM real + Gateway real (tests/e2e/test_multi_agent_mode_a.py, 2 testes): handoff desales_agentparafinance_agentresolve entitlement do zero com token completamente distinto; injeção de prompt via handoff tentandofinance.payment.executecontinua negada pelo Gateway/PDP reais. - EP-13-T06 (Modo B — Orchestrator como único cérebro de NBA/NBO,
could) (agents/example_agent/mode_b_orchestrator_driven/):ModeBOrchestratorinvoca o journeyStateGraphreal do EP-11-T02 (não modificado) uma vez por client relevante para o mesmo subject, combina osNBADecisions (Finance sempre vence Sales na regra de referência) e despacha viaAgentDispatcherreal — nunca cria autorização própria. 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, dispatchALLOWde ponta a ponta. - EP-16 (deploy final e documentação):
docker-compose.ymlvalidado com--profile core --profile identity --profile observability up -d --buildreal (7 serviços saudáveis); 25 ADRs formalizados emdocs/adr/(ADR-001 a ADR-025, gerados a partir da tabela §8.4 do planning); versões confirmadas consolidadas emdocs/adr/ADR-018-stack-de-implementacao.md.
Duas correções de código reais (não apenas testes) surgiram escrevendo os testes adversariais/fail-closed: ApprovalService ganhou um threading.Lock (race condition real de double-spend sob concorrência), e o Gateway ganhou _emit_audit (falha de audit sink podia virar erro 500 numa operação já decidida, violando §38).
O que existe hoje
| Tarefa | Componente | Status |
|---|---|---|
| EP-00 (completo) | Scaffold, config loader, docker-compose base, CI | ✅ .github/workflows/ci.yml (EP-00-T03) |
| EP-01 (completo) | Registro de MCP Client, middleware OIDC/JWT, Keycloak, lint de shared client, spike mTLS | ✅ validado com JWKS real + tokens RS256 reais + Keycloak real (26.7.3, tests/e2e/test_real_keycloak.py); lint em src/emcp_bus/identity/shared_client_lint.py (ligado a make validate-schemas); spike em docs/adr/ADR-019-identity-extension.md |
| EP-02-T01..T05 (completo) | Registry: schema, pipeline, storage, lifecycle, manifests de exemplo | ✅ src/emcp_bus/registry/*, testado (inclui seed.py, usado pelo Gateway em runtime desde EP-06-T01) |
| EP-03 (completo) | PDP/OPA: entitlement.rego, transaction.rego, cliente, contrato de decisão | ✅ validado com OPA real — 9 testes Rego + 6 Python |
| EP-04 (completo) | Entitlement Manager: resolve, revoke, versionamento | ✅ validado com OPA real + YAML reais |
| EP-05 (completo) | Gateway: discovery filtrado, execution enforcement, fail-closed, cache hint, anti-bypass, consistência header/body | ✅ src/emcp_bus/gateway/{server,cache,canonical_request}.py — ver limitação de wire-mode do cache hint abaixo |
| EP-06-T01 | Fabric core (roteador por capability via Registry) | ✅ src/emcp_bus/fabric/router.py — multi-backend real, testado (Sales→sales-domain, Finance→finance-domain, nunca cruzado) |
| EP-06-T02 | MCP Server de exemplo — Sales | ✅ 6 tools (R1 + R2), agora com BackendCredentialGate (EP-05-T07) |
| EP-06-T03 | MCP Server de exemplo — Finance | ✅ services/example_mcp_servers/finance_domain/server.py — invoice.get (R1), payment.create (R2), payment.execute (R3, ciclo de aprovação completo testado e2e) |
| EP-06-T04 | Adapter REST genérico | ✅ src/emcp_bus/fabric/adapters/rest_adapter.py — implementado e testado standalone (httpx.MockTransport); nenhum manifest seed usa backend.type: "rest" ainda (os dois domínios de exemplo são MCP-nativos), então não há caminho ao vivo através do Gateway exercitando-o — ver módulo docstring |
| EP-07 (completo) | Outbound/downstream identity: credencial de backend distinta da do client, token exchange RFC 8693 | ✅ src/emcp_bus/downstream/{models,identity}.py — service_account (padrão dos backends seed) e token_exchange (Keycloak Standard Token Exchange, validado contra Keycloak real 2026-09-14) |
| EP-08-T01 | Tracing OTel | ✅ (M0) |
| EP-08-T02 | Eventos de auditoria (taxonomia IC/NOC/GRL) | ✅ src/emcp_bus/audit/{models,events,sink}.py — nunca registra valor de token/secret (hash apenas), sink JSONL append-only |
| EP-08-T03 | Correlação ponta a ponta (decisionId/policyDecisionId/entitlementVersion/mcpRequestId) | ✅ src/emcp_bus/audit/correlation.py — exposto no trace (span attributes) e em todo evento de auditoria |
| EP-08-T04 | Stack local de observabilidade (OTel Collector + Jaeger) | ✅ docker-compose.yml perfil observability + deploy/otel/collector-config.yaml — config validada (docker compose config), não exercitada com tráfego real nesta rodada |
| EP-14 (completo) | Approval workflow: contrato, serviço, fail-closed, ligado ao Gateway | ✅ src/emcp_bus/approval/{models,service}.py, testado (ciclo completo e2e para Sales e Finance: nega → aprova externamente → executa → replay nega) |
| EP-09 (completo) | Profile Intelligence: contrato, provider rule-based, governança | ✅ src/emcp_bus/profile_intelligence/*.py |
| EP-10 (completo) | Offering Filter: serviço + invariante de subconjunto | ✅ src/emcp_bus/offering_filter/service.py — invariante provada com Hypothesis (2000 casos) |
| EP-11 (completo) | Service Orchestrator: DecisionContext, StateGraph LangGraph, decisioning pluggable, recálculo, HITL | ✅ src/emcp_bus/orchestrator/*.py — LangGraph + checkpointer SQLite reais, checkpoint-resume e pausa/retomada de aprovação validados |
| EP-12 (completo) | Agent Runtime: dispatch, channel adapter, handoff, identity resolver | ✅ src/emcp_bus/agent_runtime/*.py — validado contra Gateway + Keycloak reais |
| EP-13-T01/T03/T04 | Agente de exemplo (LLM real), redação de PII, versionamento de prompt | ✅ agents/example_agent/*.py — tool-calling real, cenário de prompt injection do §29 reproduzido |
| EP-13-T02 | Langfuse self-hosted | ✅ deploy/langfuse/docker-compose.override.yml — subido e validado de verdade (ver seção M3 acima); validação manual, não automatizada em CI |
| EP-15-T01 | Testes de contrato dos MCP Servers | ✅ tests/contract/*.py — input schema real do SDK exercitado + output schema escrito à mão, validado contra os 2 domain servers reais (ver seção M4 acima) |
| EP-15-T02 | Suíte de aceite de segurança (§45) | ✅ tests/e2e/test_security_acceptance.py — 7/10 testes formais automatizados contra o docker-compose real; os outros 3 já cobertos em outros arquivos (ver seção M4 acima) |
| EP-15-T03 | Testes adversariais de prompt/tool injection | ✅ tests/adversarial/{test_prompt_injection,test_tool_poisoning,test_payload_identity_spoofing}.py — 12 testes, incluindo um MCP server malicioso real (ver seção M4 acima) |
| EP-15-T04 | Testes adversariais de protocolo/identidade | ✅ tests/adversarial/test_protocol_identity.py — 6 testes novos + threading.Lock real corrigido em ApprovalService (ver seção M4 acima) |
| EP-15-T05 | Testes de política de falha (§38) | ✅ tests/e2e/test_fail_closed.py — 7 testes, um por linha da tabela + _emit_audit real corrigido no Gateway (ver seção M4 acima) |
EP-13-T05 (should) |
Modo A — multiagente governado (ADR-024) | ✅ src/emcp_bus/agent_runtime/global_supervisor.py + agents/example_agent/backends/* — validado com LLM real + Gateway real (ver seção M4 acima) |
EP-13-T06 (could) |
Modo B — Orchestrator como único cérebro de NBA/NBO | ✅ agents/example_agent/mode_b_orchestrator_driven/* — validado com Keycloak real + OPA real + Sales/Finance reais (ver seção M4 acima) |
| EP-16 (completo) | Deploy final, ADRs formalizados, README | ✅ docker-compose.yml validado com todos os perfis; 25 ADRs em docs/adr/; versões confirmadas em docs/adr/ADR-018-stack-de-implementacao.md |
260 testes automatizados no total: 253 passam com --forked (unit + e2e + contract + adversarial — todo tests/ exceto test_security_acceptance.py, confirmado em execução completa 2026-09-14), mais 7 testes de aceite de segurança (EP-15-T02) contra o docker-compose.yml real (build + Keycloak, ~72s, rodados separadamente do --forked padrão pelo custo 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 real, LLM real (endpoint OpenAI-compatible), JWKS HTTP real (stand-in leve para o resto da suíte) + JWT RS256 reais, YAML reais de config/, SQLite real para o Registry. 100% determinísticos com --forked (verificado em múltiplas execuções seguidas) — a flakiness antes documentada para a suíte e2e está resolvida, ver seção dedicada acima.
Limitações conhecidas
- SEP-2549 (
ttlMsdo cache hint detools/list, EP-05-T03): implementado corretamente (src/emcp_bus/gateway/cache.py), mas o campo é vocabulário exclusivo do protocolo 2026-07-28, que só existe no modo stateless per-request do SDK (sem handshakeinitialize) — esta RI usa o handshake de sessão clássico em toda parte (Gateway↔Client e Gateway↔backends), entãottl_msnunca chega ao wire nesta arquitetura, não importa o que o Gateway compute (o SDK descarta o campo na serialização para versões de protocolo anteriores a 2026-07-28 — verificado empiricamente).cache_scopenão sofre essa limitação de wire, mas por não ter dependência de wire-mode também não é algo que um teste e2e consiga verificar de forma discriminante (o cliente sempre usa o default do próprio campo). A propriedade de segurança real (Teste 5, §45 — nunca vazar catálogo entre clientes) é garantida de forma independente pelo Gateway nunca cachear o catálogo filtrado no servidor. - EP-05-T08 (consistência header/body): implementado como um gate próprio (
canonical_request.py) reaproveitando as constantes/nomes de campo do SDK, e não como o ladder de validação nativo do SDK (mcp.shared.inbound.classify_inbound_request), que só se aplica ao mesmo modo stateless per-request de 2026-07-28 mencionado acima — pela mesma razão, este RI não o alcança pelo caminho nativo. - EP-06-T04 (adapter REST): implementado e testado de forma standalone; 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"não foi exercitado end-to-end. - Stack de observabilidade (EP-08-T04): config validada, não exercitada com tráfego real (ver "O que foi validado com Docker").
Token exchange RFC 8693 ( — resolvido em 2026-09-14 (início do M3): NotImplementedError)TokenExchangeClient implementa o Standard Token Exchange do Keycloak (GA desde 26.2) de ponta a ponta, validado contra Keycloak real, incluindo um tools/call completo pelo Gateway usando uma credencial obtida por exchange. Ver docs/RI-PLANNING.md §8.7 para os detalhes verificados (exigências reais do IdP: audience precisa ser um client id registrado; scope precisa acompanhar audience ou o exchange falha).
M3, ressalvas (2026-09-14):
- EP-13-T02 (Langfuse): validado manualmente de verdade (ver seção M3 acima — spans reais confirmados no ClickHouse), mas não é um teste e2e automatizado por causa do custo de subir 6 containers a cada execução da suíte.
- EP-13-T04 (prompt versioning,
could): o núcleo de versionamento é real e testado; o sync com a API de Prompt Management do Langfuse (langfuse.api.prompts.create) é um adapter fino documentado, não implementado. - Credencial de LLM:
LLM_API_KEY/LLM_BASE_URL/LLM_MODEL(ver.env.example) são necessárias paratests/e2e/test_example_agent.pyetests/e2e/test_multi_agent_mode_a.py— sem elas, esses testes são pulados (pytest.skip), não falham, igual ao padrão já usado paraopa/docker.
M4, ressalvas (2026-09-14):
- EP-12-T03 (
execute_handoff/build_handoff_payload,could): continua implementado e testado (tests/unit/test_agent_runtime_handoff.py), mas achado real durante o M4: nem o Modo A (EP-13-T05) nem o Modo B (EP-13-T06) acabaram usando esse primitivo. Modo A precisa de roteamento conversacional (qual backend responde à próxima mensagem), não de despacho de umaNBADecisionespecífica —GlobalSupervisorchamarun_turn()diretamente nos backends. Modo B já combinaNBADecisions de múltiplos clients num único passo (ModeBOrchestrator._combine) e despacha o vencedor viaAgentDispatcher.dispatchpuro — não há um agente "de origem" cedendo controle a outro paraexecute_handoffinterceptar.execute_handoffcontinua correto e reutilizável para um cenário futuro que precise especificamente de handoff dentro de uma jornada Orchestrator-driven (ex.: EP-11 decidindo, no meio de uma jornada, redirecionar para outro agente mantendo o mesmoNBADecision), mas essa forma específica de uso não existe nesta RI. - Replay de requisição (
tests/adversarial/test_protocol_identity.py): 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_livedem todoClientProfileseed) e por auditoria completa (cada tentativa, mesmo idêntica, geradecision_idpróprio). - Rate limiting / bulk exfiltration (
tests/adversarial/test_protocol_identity.py): 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. GlobalSupervisor(Modo A, EP-13-T05): roteamento por palavra-chave simples, deliberadamente não é a fronteira de segurança (documentado no módulo) — não é uma reivindicação de qualidade de roteamento, apenas um stand-in honesto para o que uma plataforma externa real faria.ModeBOrchestrator(Modo B, EP-13-T06): a regra de combinação entre domínios (Finance sempre vence Sales) é uma regra de referência simples, não uma reivindicação de otimalidade — mesma postura já adotada porRuleBasedNBADecisionModel(EP-11-T03). Não estendeJourneyState/build_journey_graphpara múltiplosclient_ids nativamente; a combinação vive numa camada acima do grafo (decisão de design documentada no módulo).- EP-06-T04 (adapter REST), stack de observabilidade (EP-08-T04, tráfego real ainda não exercitado por um teste automatizado — mas a subida completa com o perfil
observabilityfoi validada manualmente no M4) e o restante das ressalvas de M0-M3 acima continuam válidas sem mudança.
Versões fixadas
Ver pyproject.toml. Confirmadas via pip index versions/download direto em 2026-09-11 (não citadas de memória — ver docs/RI-PLANNING.md, EP-16-T04):
mcp2.2.0 (SDK v2 — API mudou significativamente da v1; ver comentários emsrc/emcp_bus/gateway/server.py)pydantic2.13.x,opentelemetry-sdk1.44.x,uvicorn0.52.x,pyjwt[crypto]2.14.x,ruff0.16.x,mypy2.3.x,pytest9.1.xOPA1.20.2 (binário estático, Rego v1 padrão)pytest-forked1.7.x (resolve a flakiness da suíte e2e, ver acima)langgraph0.6.x +langgraph-checkpoint-sqlite2.x (M3, EP-11),hypothesis6.1xx.x (dev, EP-10-T02),openai1.109.x (M3, EP-13-T01),python-dotenv1.2.x (dev) — confirmadas via instalação direta em 2026-09-14- Langfuse self-hosted 4.35.0 (
docker.langfuse.com/langfuse/langfuse:4, imagem oficial, EP-13-T02) types-jsonschema4.26.x (dev, stubs paramypy --strict;jsonschemaem si já era dependência transitiva domcp[cli], EP-15-T01) — confirmada via instalação direta em 2026-09-14- Keycloak 26.7.3 (
quay.io/keycloak/keycloak:26.7.3, Quarkus),opentelemetry-collector-contrib0.114.0,jaeger(all-in-one) 1.62.0 — imagens fixadas emdocker-compose.yml; registro completo emdocs/adr/ADR-018-stack-de-implementacao.md(EP-16-T04)
Estrutura
Ver ../docs/RI-PLANNING.md, seção 8.5, para a árvore completa planejada. O que existe até agora:
RI/
├── pyproject.toml
├── Makefile
├── docker-compose.yml # perfil "core": gateway+sales-domain+finance-domain+opa;
│ # "identity": +keycloak; "observability": +otel-collector+jaeger
├── .env.example
├── deploy/docker/Dockerfile
├── deploy/keycloak/realm-export.json # EP-01-T01: realm "emcp", 3 clients
├── deploy/otel/collector-config.yaml # EP-08-T04
├── scripts/{export_schemas,validate_config}.py
├── schemas/*.schema.json # gerados por export_schemas.py
├── config/
│ ├── policies/ # EP-03: entitlement.rego, transaction.rego (+ opa test)
│ ├── clients/{*.yaml,profiles/*.yaml} # EP-01/EP-04: registrations + profiles
│ ├── backends/*.yaml # EP-06-T01/EP-07: endpoint + outbound identity por backend
│ └── capabilities/{sales,finance}/*.yaml # EP-02-T05: manifests de exemplo
├── src/emcp_bus/
│ ├── common/{config,otel,models}.py # EP-00-T02, EP-08-T01, risk tier compartilhado
│ ├── identity/{models,authn,token_verifier,shared_client_lint}.py # EP-01
│ ├── entitlement/{models,manager}.py # EP-04
│ ├── registry/{models,store,pipeline,lifecycle,seed}.py # EP-02
│ ├── pdp/{client,models}.py # EP-03
│ ├── fabric/router.py # EP-06-T01
│ ├── fabric/adapters/rest_adapter.py # EP-06-T04 (standalone, ver limitações)
│ ├── downstream/{models,identity}.py # EP-07
│ ├── audit/{models,events,sink,correlation,bypass_detection}.py # EP-08-T02/T03, EP-05-T07
│ ├── approval/{models,service}.py # EP-14
│ ├── gateway/{server,cache,canonical_request}.py # EP-05, integra tudo acima
│ ├── profile_intelligence/{models,rule_based_provider,governance}.py # EP-09
│ ├── offering_filter/service.py # EP-10
│ ├── orchestrator/{decision_context,state,graph,nba_model,decisioning,fallback,hitl_node}.py # EP-11
│ └── agent_runtime/{dispatcher,identity_resolver,handoff_graph,global_supervisor,channels/*}.py # EP-12, EP-13-T05
├── services/example_mcp_servers/
│ ├── sales_domain/server.py # EP-06-T02 (6 tools: R1 + R2)
│ └── finance_domain/server.py # EP-06-T03 (invoice.get R1, payment.create R2, payment.execute R3)
├── agents/example_agent/
│ ├── agent.py, llm_client.py # EP-13-T01
│ ├── prompts/{store.py,v1.txt} # EP-13-T04
│ ├── backends/{sales_agent,finance_agent}/ # EP-13-T05, Modo A (ADR-024)
│ └── mode_b_orchestrator_driven/{decision_model,orchestrator}.py # EP-13-T06, Modo B (ADR-024)
├── deploy/langfuse/docker-compose.override.yml # EP-13-T02
├── docs/adr/ # ADR-001..025 formalizados (EP-16-T03), um arquivo por decisão
└── tests/
├── unit/ # schemas, PDP, entitlement, authn, registry, downstream identity, approval,
│ # fabric router, audit, canonical request, rest adapter, bypass detection,
│ # gateway cache, shared client lint, profile intelligence, offering filter
│ # (+ invariants), orchestrator (decision_context/decisioning/fallback/hitl/graph),
│ # agent_runtime (channels/identity_resolver/handoff/global_supervisor),
│ # pii_redaction, prompt store
├── e2e/ # conftest.py (fixtures compartilhados), walking skeleton, governed gateway,
│ # test_real_keycloak.py (EP-01-T01/T12, token exchange, Modo B, Docker real),
│ # test_example_agent.py (EP-13-T01, LLM real),
│ # test_security_acceptance.py (EP-15-T02, docker-compose real),
│ # test_multi_agent_mode_a.py (EP-13-T05, LLM real),
│ # test_fail_closed.py (EP-15-T05)
├── contract/ # conftest.py + test_sales_domain.py + test_finance_domain.py (EP-15-T01)
└── adversarial/ # test_prompt_injection.py, test_tool_poisoning.py,
# test_payload_identity_spoofing.py (EP-15-T03),
# test_protocol_identity.py (EP-15-T04)