M3 implementada e backlog atualizado

This commit is contained in:
Marcos Lohmann
2026-09-14 10:11:29 -03:00
parent f3efcc3414
commit c894c1604b
65 changed files with 4261 additions and 74 deletions

View File

@@ -16,6 +16,7 @@ import shutil
import subprocess
import time
from collections.abc import AsyncIterator, Iterator
from datetime import UTC, datetime, timedelta
from pathlib import Path
import httpx
@@ -247,3 +248,332 @@ async def test_cross_domain_denial_with_real_keycloak_token(
token=token,
)
assert result.is_error is True
# --- RFC 8693 token exchange against real Keycloak (EP-07) -----------------
#
# `deploy/keycloak/realm-export.json` registers a `gateway-token-exchange`
# client (`standard.token.exchange.enabled: true`, Keycloak 26.2+ Standard
# Token Exchange -- GA, no preview feature flag) plus two audience-only
# placeholder clients (`sales-domain-service`/`finance-domain-service`,
# never used to log in) and two client scopes (`aud-sales-domain-service`/
# `aud-finance-domain-service`, each an `oidc-audience-mapper` targeting one
# placeholder client) as `gateway-token-exchange`'s default scopes. Verified
# empirically while building this: Keycloak's `audience` exchange parameter
# only accepts a *registered client id* (a free-form/"custom" audience
# string fails with "Audience not found"), and requesting `audience` alone
# without a `scope` that actually adds it fails with "Requested audience not
# available" -- both real IdP behaviors, not something this RI's code layer
# could have gotten right by API contract alone.
async def test_real_token_exchange_produces_a_token_scoped_to_sales_domain(
keycloak_url: str,
) -> None:
import base64
import json
from emcp_bus.downstream.identity import BackendCredentialProvider, TokenExchangeClient
from emcp_bus.downstream.models import BackendConfig
exchange_client = TokenExchangeClient(
"gateway-token-exchange", "dev-gateway-token-exchange-secret"
)
provider = BackendCredentialProvider(
[
BackendConfig(
backend="sales-domain",
url="http://127.0.0.1:8100/mcp",
audience="sales-domain-service",
credential_mode="token_exchange",
token_exchange_endpoint=(
f"{keycloak_url}/realms/{REALM_NAME}/protocol/openid-connect/token"
),
token_exchange_scope="aud-sales-domain-service",
)
],
token_exchange_client=exchange_client,
)
try:
credential = await provider.credential_for("sales-domain")
finally:
await exchange_client.aclose()
assert credential.audience == "sales-domain-service"
payload = credential.token.split(".")[1]
payload += "=" * (-len(payload) % 4)
claims = json.loads(base64.urlsafe_b64decode(payload))
assert claims["aud"] == "sales-domain-service"
assert claims["azp"] == "gateway-token-exchange"
async def test_real_token_exchange_never_reuses_the_same_token_across_different_backends(
keycloak_url: str,
) -> None:
"""The exchanged credential for one backend must never satisfy another --
each backend gets its own `aud` claim, exchanged independently."""
from emcp_bus.downstream.identity import BackendCredentialProvider, TokenExchangeClient
from emcp_bus.downstream.models import BackendConfig
endpoint = f"{keycloak_url}/realms/{REALM_NAME}/protocol/openid-connect/token"
exchange_client = TokenExchangeClient(
"gateway-token-exchange", "dev-gateway-token-exchange-secret"
)
provider = BackendCredentialProvider(
[
BackendConfig(
backend="sales-domain",
url="http://127.0.0.1:8100/mcp",
audience="sales-domain-service",
credential_mode="token_exchange",
token_exchange_endpoint=endpoint,
token_exchange_scope="aud-sales-domain-service",
),
BackendConfig(
backend="finance-domain",
url="http://127.0.0.1:8101/mcp",
audience="finance-domain-service",
credential_mode="token_exchange",
token_exchange_endpoint=endpoint,
token_exchange_scope="aud-finance-domain-service",
),
],
token_exchange_client=exchange_client,
)
try:
sales_credential = await provider.credential_for("sales-domain")
finance_credential = await provider.credential_for("finance-domain")
finally:
await exchange_client.aclose()
assert sales_credential.token != finance_credential.token
assert sales_credential.audience == "sales-domain-service"
assert finance_credential.audience == "finance-domain-service"
async def test_full_gateway_chain_with_token_exchange_backend_credential(
monkeypatch: pytest.MonkeyPatch, opa_url: str, keycloak_url: str, tmp_path: Path
) -> None:
"""EP-07 end to end through the real Gateway: `sales-domain`'s backend
credential comes from a real RFC 8693 exchange (not
`SALES_DOMAIN_SERVICE_TOKEN`), and the resulting `tools/call` still
reaches the real Sales backend -- proving the whole path, not just the
identity layer in isolation (the two tests above)."""
import shutil as shutil_module
import uvicorn
from tests.e2e.conftest import (
RunningServer,
free_port,
run_against_gateway,
set_current_gateway_port,
)
# A private copy of config/ with sales-domain switched to token_exchange
# mode -- never mutate the real config/backends/sales-domain.yaml, which
# every other e2e test relies on being in `service_account` mode.
config_copy = tmp_path / "config"
shutil_module.copytree(REPO_ROOT / "config", config_copy)
token_endpoint = f"{keycloak_url}/realms/{REALM_NAME}/protocol/openid-connect/token"
(config_copy / "backends" / "sales-domain.yaml").write_text(
"backend: sales-domain\n"
"url: ${SALES_DOMAIN_URL:-http://127.0.0.1:8100/mcp}\n"
"audience: sales-domain-service\n"
"credential_mode: token_exchange\n"
f"token_exchange_endpoint: {token_endpoint}\n"
"token_exchange_scope: aud-sales-domain-service\n",
encoding="utf-8",
)
gateway_port = free_port()
sales_port = free_port()
monkeypatch.setenv("EMCP_CONFIG_DIR", str(config_copy))
monkeypatch.setenv("SALES_DOMAIN_URL", f"http://127.0.0.1:{sales_port}/mcp")
monkeypatch.setenv("OPA_URL", opa_url)
monkeypatch.setenv(
"OIDC_JWKS_URL", f"{keycloak_url}/realms/{REALM_NAME}/protocol/openid-connect/certs"
)
monkeypatch.setenv("OIDC_ISSUER", f"{keycloak_url}/realms/{REALM_NAME}")
monkeypatch.setenv("OIDC_AUDIENCE", "emcp-gateway")
monkeypatch.setenv("GATEWAY_TOKEN_EXCHANGE_CLIENT_ID", "gateway-token-exchange")
monkeypatch.setenv("GATEWAY_TOKEN_EXCHANGE_CLIENT_SECRET", "dev-gateway-token-exchange-secret")
monkeypatch.delenv("SALES_DOMAIN_SERVICE_TOKEN", raising=False) # must not be needed anymore
import example_mcp_servers.sales_domain.server as sales_module
sales_server = RunningServer(
uvicorn.Config(
sales_module.server.streamable_http_app(host="127.0.0.1"),
host="127.0.0.1",
port=sales_port,
log_level="warning",
)
)
await sales_server.start()
import emcp_bus.gateway.server as gateway_module
auth_settings, token_verifier = gateway_module.build_auth()
gateway_server = RunningServer(
uvicorn.Config(
gateway_module.build_app(auth_settings, token_verifier, host="127.0.0.1"),
host="127.0.0.1",
port=gateway_port,
log_level="warning",
)
)
await gateway_server.start()
set_current_gateway_port(gateway_port)
try:
token = _fetch_token(keycloak_url, "sales-read-agent")
result = await run_against_gateway(
lambda session: session.call_tool("sales.customer.get", {"customer_id": "cust-001"}),
token=token,
)
assert result.is_error is not True
assert result.structured_content == {
"customer_id": "cust-001",
"name": "Aurora Ltda",
"region": "BR-SP",
}
finally:
set_current_gateway_port(None)
await gateway_server.stop()
await sales_server.stop()
# --- EP-12-T01: AgentDispatcher against a real Gateway + real Keycloak -----
async def test_agent_dispatcher_reaches_the_real_backend_through_the_real_gateway(
governed_stack_with_real_keycloak: None, keycloak_url: str
) -> None:
"""The Agent Runtime never generates or stores its own credential
(EP-12-T01 acceptance criterion) -- `OIDCAgentTokenProvider` obtains a
real token for `sales-read-agent`'s already-registered MCP Client via a
real `client_credentials` grant, and `AgentDispatcher` uses only that to
reach the real Sales backend through the real Gateway/PDP chain."""
from emcp_bus.agent_runtime.dispatcher import (
AgentClientCredentials,
AgentDispatcher,
OIDCAgentTokenProvider,
)
from emcp_bus.orchestrator.nba_model import NBADecision
from tests.e2e.conftest import _gateway_url
token_endpoint = f"{keycloak_url}/realms/{REALM_NAME}/protocol/openid-connect/token"
token_provider = OIDCAgentTokenProvider(
{
"sales-read-agent": AgentClientCredentials(
client_id="sales-read-agent",
client_secret=CLIENT_SECRETS["sales-read-agent"],
token_endpoint=token_endpoint,
)
}
)
dispatcher = AgentDispatcher(gateway_url=_gateway_url(), token_provider=token_provider)
nba = NBADecision(
decision_id="dec-e2e-1",
action="sales.customer.get",
capability_version="1.0.0",
subject_ref="subject:abc123",
channel="app",
agent="sales-read-agent",
reason_codes=["ELIGIBLE"],
policy_context={},
expires_at=datetime.now(UTC) + timedelta(hours=1),
)
try:
result = await dispatcher.dispatch(nba, arguments={"customer_id": "cust-001"})
finally:
await token_provider.aclose()
assert result.status == "allowed"
assert result.structured_content == {
"customer_id": "cust-001",
"name": "Aurora Ltda",
"region": "BR-SP",
}
async def test_agent_dispatcher_denies_a_client_reaching_outside_its_entitlement(
governed_stack_with_real_keycloak: None, keycloak_url: str
) -> None:
"""The exact same Gateway/PDP chain -- `sales-read-agent`'s own real
token still cannot reach `finance.payment.execute`."""
from emcp_bus.agent_runtime.dispatcher import (
AgentClientCredentials,
AgentDispatcher,
OIDCAgentTokenProvider,
)
from emcp_bus.orchestrator.nba_model import NBADecision
from tests.e2e.conftest import _gateway_url
token_endpoint = f"{keycloak_url}/realms/{REALM_NAME}/protocol/openid-connect/token"
token_provider = OIDCAgentTokenProvider(
{
"sales-read-agent": AgentClientCredentials(
client_id="sales-read-agent",
client_secret=CLIENT_SECRETS["sales-read-agent"],
token_endpoint=token_endpoint,
)
}
)
dispatcher = AgentDispatcher(gateway_url=_gateway_url(), token_provider=token_provider)
nba = NBADecision(
decision_id="dec-e2e-2",
action="finance.payment.execute",
capability_version="1.0.0",
subject_ref="subject:abc123",
channel="app",
agent="sales-read-agent",
reason_codes=["ELIGIBLE"],
policy_context={},
expires_at=datetime.now(UTC) + timedelta(hours=1),
)
try:
result = await dispatcher.dispatch(nba, arguments={"amount": 999})
finally:
await token_provider.aclose()
assert result.status == "denied"
async def test_two_agents_get_independently_resolved_tokens(
governed_stack_with_real_keycloak: None, keycloak_url: str
) -> None:
"""EP-12-T03's core guarantee, proven with two real, distinct MCP
Clients: `OIDCAgentTokenProvider` never reuses one agent's token for
another -- each `client_credentials` grant is independent."""
from emcp_bus.agent_runtime.dispatcher import AgentClientCredentials, OIDCAgentTokenProvider
token_endpoint = f"{keycloak_url}/realms/{REALM_NAME}/protocol/openid-connect/token"
token_provider = OIDCAgentTokenProvider(
{
"sales-read-agent": AgentClientCredentials(
client_id="sales-read-agent",
client_secret=CLIENT_SECRETS["sales-read-agent"],
token_endpoint=token_endpoint,
),
"finance-payments-agent": AgentClientCredentials(
client_id="finance-payments-agent",
client_secret=CLIENT_SECRETS["finance-payments-agent"],
token_endpoint=token_endpoint,
),
}
)
try:
sales_token = await token_provider.token_for("sales-read-agent")
finance_token = await token_provider.token_for("finance-payments-agent")
finally:
await token_provider.aclose()
assert sales_token != finance_token