Files
Enterprise-MCP-Service-Bus/RI/docs/AS-BUILT.md
2026-09-14 14:54:28 -03:00

394 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/`](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`](How-To.md). Para quem vai **avaliar/adaptar** a RI para um cenário real, ver
[`Assumptions.md`](Assumptions.md) e [`Production-Recommendations.md`](Production-Recommendations.md).
Decisões arquiteturais individuais estão registradas em [`../../docs/adr/`](../../docs/adr/)
(ADR-001 a ADR-025); as premissas e razões por trás delas estão sintetizadas em
[`Assumptions.md`](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/`](../../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`](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`](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`](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`](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 `NBADecision`s 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_id`s 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 `NBADecision`s 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á `ALLOW`ed 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).