mirror of
https://github.com/mcloh/Enterprise-MCP-Service-Bus.git
synced 2026-11-05 22:14:11 +00:00
revisão e complementação da documentação do projeto
This commit is contained in:
93
RI/docs/interfaces/downstream-identity.md
Normal file
93
RI/docs/interfaces/downstream-identity.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 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`](../../src/emcp_bus/downstream/models.py),
|
||||
[`downstream/identity.py`](../../src/emcp_bus/downstream/identity.py). Ver
|
||||
[`AS-BUILT.md`](../AS-BUILT.md#downstream--outbound-identity-ep-07).
|
||||
|
||||
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`:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
```yaml
|
||||
# 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 ter
|
||||
`attributes.standard.token.exchange.enabled = "true"` e `serviceAccountsEnabled`.
|
||||
- `audience` precisa ser o id de um client **realmente registrado** no realm — uma string livre
|
||||
falha com `"Audience not found"`.
|
||||
- Pedir `audience` sem também pedir, via `scope`, um client scope que efetivamente adiciona essa
|
||||
audience falha com `"Requested audience not available"` — o parâmetro `audience` só
|
||||
**restringe**, nunca **adiciona**. Por isso `token_exchange_scope` é obrigatório neste modo.
|
||||
- Esse client scope precisa carregar um `oidc-audience-mapper` apontando para a `audience` do
|
||||
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`)
|
||||
|
||||
```python
|
||||
BackendCredential(
|
||||
backend="finance-domain",
|
||||
url="http://finance-domain:8101/mcp",
|
||||
token="...",
|
||||
audience="finance-domain-service",
|
||||
)
|
||||
```
|
||||
|
||||
## Como integrar um novo backend
|
||||
|
||||
1. Registrar o backend no IdP se usar `token_exchange` (client scope + `oidc-audience-mapper`
|
||||
para a `audience` escolhida) — ou apenas definir a env var do token estático se usar
|
||||
`service_account`.
|
||||
2. Criar `config/backends/<backend>.yaml` (`BackendConfig`).
|
||||
3. Referenciar `backend.service` == esse mesmo id em cada `CapabilityManifest` que rotear para
|
||||
ele (ver [`registry-capability-manifest.md`](registry-capability-manifest.md)).
|
||||
4. Se o backend for MCP-nativo (`backend.type: mcp`, o único com dispatch ao vivo nesta RI),
|
||||
implementar o `BackendCredentialGate` do lado do backend (ver
|
||||
`services/example_mcp_servers/sales_domain/server.py` como 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. |
|
||||
Reference in New Issue
Block a user