4.6 KiB
Interface: downstream / outbound identity
Contrato de identidade de saída, para quem vai integrar um novo backend/domain server.
Implementação: src/emcp_bus/downstream/models.py,
downstream/identity.py. Ver
AS-BUILT.md.
Princípio: inbound token (MCP Client → Gateway) ≠ outbound token (Fabric → Backend). O Gateway
autentica o caller; este contrato resolve uma pergunta separada — qual credencial o Fabric
apresenta ao backend — e nunca a resolve repassando o token inbound sem modificação.
Registro de um backend (BackendConfig)
Um arquivo YAML por backend em config/backends/*.yaml:
# service_account (modo padrão dos backends seed)
backend: sales-domain
url: http://sales-domain:8100/mcp
audience: sales-domain-service # nunca igual ao OIDC_AUDIENCE inbound do Gateway
credential_mode: service_account
service_account_token_env_var: SALES_DOMAIN_SERVICE_TOKEN # nome da env var, nunca um valor literal
# token_exchange (alternativa disponível e verificada contra Keycloak real)
backend: finance-domain
url: http://finance-domain:8101/mcp
audience: finance-domain-service
credential_mode: token_exchange
token_exchange_endpoint: http://keycloak:8080/realms/emcp/protocol/openid-connect/token
token_exchange_scope: aud-finance-domain-service
Modo service_account
Credencial estática, lida de uma variável de ambiente (nome, nunca valor, declarado em
service_account_token_env_var) no momento de cada chamada. Ausência da variável em runtime →
BackendCredentialUnavailableError.
Modo token_exchange (RFC 8693, Keycloak Standard Token Exchange)
Dois round-trips HTTP por troca nova (TokenExchangeClient.exchange): (1) client_credentials
como o próprio client de exchange do Gateway, produzindo um subject_token; (2) troca desse
subject_token por um token com a audience do backend alvo. Cacheado por backend até pouco
antes do próprio expires_in da troca — um cache hit custa zero round-trips.
Configuração exigida no IdP (verificada empiricamente contra Keycloak 26.7.3 real):
- O client de exchange do Gateway (
GATEWAY_TOKEN_EXCHANGE_CLIENT_ID/_SECRET) precisa terattributes.standard.token.exchange.enabled = "true"eserviceAccountsEnabled. audienceprecisa ser o id de um client realmente registrado no realm — uma string livre falha com"Audience not found".- Pedir
audiencesem também pedir, viascope, um client scope que efetivamente adiciona essa audience falha com"Requested audience not available"— o parâmetroaudiencesó restringe, nunca adiciona. Por issotoken_exchange_scopeé obrigatório neste modo. - Esse client scope precisa carregar um
oidc-audience-mapperapontando para aaudiencedo backend, e ser um scope padrão (ou opcional solicitado) do client de exchange do Gateway.
Ver deploy/keycloak/realm-export.json para um exemplo real já configurado (gateway-token-exchange
- client scopes
aud-sales-domain-service/aud-finance-domain-service).
Contrato de saída (BackendCredential)
BackendCredential(
backend="finance-domain",
url="http://finance-domain:8101/mcp",
token="...",
audience="finance-domain-service",
)
Como integrar um novo backend
- Registrar o backend no IdP se usar
token_exchange(client scope +oidc-audience-mapperpara aaudienceescolhida) — ou apenas definir a env var do token estático se usarservice_account. - Criar
config/backends/<backend>.yaml(BackendConfig). - Referenciar
backend.service== esse mesmo id em cadaCapabilityManifestque rotear para ele (verregistry-capability-manifest.md). - Se o backend for MCP-nativo (
backend.type: mcp, o único com dispatch ao vivo nesta RI), implementar oBackendCredentialGatedo lado do backend (verservices/example_mcp_servers/sales_domain/server.pycomo referência) — uma requisição sem a credencial de saída do Gateway nunca deve alcançar um handler de tool.
Erros
| Situação | Exceção | Tratamento pelo caller (Gateway) |
|---|---|---|
backend sem BackendConfig registrado |
UnknownBackendError |
UnroutableCapabilityError no Fabric → UNROUTABLE_CAPABILITY. |
Env var de service_account ausente em runtime |
BackendCredentialUnavailableError |
backend_credential_unavailable (evento NOC) → chamada negada. |
| Troca RFC 8693 rejeitada pelo IdP | TokenExchangeError → relançada como BackendCredentialUnavailableError |
Idem acima. |