M0 e M1 revisados e fixes implementados

This commit is contained in:
Marcos Lohmann
2026-09-14 08:18:53 -03:00
parent 439f60598e
commit f3efcc3414
72 changed files with 4831 additions and 128 deletions

View File

View File

@@ -0,0 +1,75 @@
"""Outbound (backend) identity, distinct from inbound (MCP Client) identity
(EP-07-T01, README.md §20, ADR-006).
inbound token (MCP Client -> Gateway) != outbound token (Fabric -> Backend)
The Gateway authenticates the *caller*; this module answers a completely
separate question -- which credential the Fabric presents *to the backend*
-- and never answers it by handing the inbound token through unmodified
(README.md §35.6, "Token passthrough indiscriminado"). `service_account`
mode resolves a static, backend-specific credential from an env var at call
time; `token_exchange` (RFC 8693) is a documented, unimplemented extension
(see `BackendConfig`).
"""
from __future__ import annotations
import os
from pydantic import BaseModel
from emcp_bus.downstream.models import BackendConfig
class UnknownBackendError(Exception):
pass
class BackendCredentialUnavailableError(Exception):
"""The configured env var for a backend's service-account token is unset.
Distinct from `UnknownBackendError`: the backend *is* configured, but
its credential is missing at runtime -- callers (the Gateway) must
treat this as DENY, not as "fall back to no credential" (README.md §38).
"""
class BackendCredential(BaseModel):
backend: str
url: str
token: str
audience: str
class BackendCredentialProvider:
def __init__(self, backend_configs: list[BackendConfig]) -> None:
self._configs = {config.backend: config for config in backend_configs}
def credential_for(self, backend: str) -> BackendCredential:
config = self._configs.get(backend)
if config is None:
raise UnknownBackendError(backend)
if config.credential_mode == "token_exchange":
raise NotImplementedError(
f"credential_mode='token_exchange' for backend {backend!r} is a documented "
"extension point (RFC 8693 via an OIDC IdP), not implemented in this RI -- "
"see BackendConfig docstring."
)
assert config.service_account_token_env_var is not None # enforced by BackendConfig
token = os.environ.get(config.service_account_token_env_var)
if not token:
raise BackendCredentialUnavailableError(
f"Environment variable {config.service_account_token_env_var!r} for backend "
f"{backend!r} is not set"
)
return BackendCredential(
backend=backend, url=config.url, token=token, audience=config.audience
)
def url_for(self, backend: str) -> str:
config = self._configs.get(backend)
if config is None:
raise UnknownBackendError(backend)
return config.url

View File

@@ -0,0 +1,69 @@
"""Backend credential config schema (EP-07-T02, README.md §20).
One entry per backend the Fabric calls, declaring *how* the Gateway obtains
an outbound credential for it -- never the credential's value. The RI
implements `service_account` (a static credential resolved from an env var
at call time); `token_exchange` (RFC 8693, e.g. via Keycloak) is modeled as
a distinct, documented mode but not implemented in this milestone -- see
`emcp_bus.downstream.identity.BackendCredentialProvider`.
"""
from __future__ import annotations
import re
from typing import Literal, Self
from pydantic import BaseModel, field_validator, model_validator
_ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
class BackendConfig(BaseModel):
backend: str
"""Adapter/service id, matching `CapabilityManifest.backend.service` (EP-02-T01)."""
url: str
"""Where the Fabric router (EP-06-T01) connects to reach this backend --
an MCP streamable-http endpoint for `backend.type == "mcp"`, or a REST
base URL for `"rest"` (EP-06-T04). Not a secret, so unlike the credential
fields below it may be a literal value, though it commonly uses
`${VAR}` too since it differs between `make dev` (127.0.0.1) and Docker
(the compose service DNS name)."""
audience: str
"""The outbound token's audience -- deliberately never equal to the Gateway's
own inbound `OIDC_AUDIENCE` (README.md §20: inbound and outbound authorization
are distinct relations)."""
credential_mode: Literal["service_account", "token_exchange"] = "service_account"
service_account_token_env_var: str | None = None
"""Name (not value) of the environment variable holding the backend's
static credential. Required when `credential_mode == "service_account"`."""
token_exchange_endpoint: str | None = None
"""RFC 8693 token endpoint URL. Documented extension point, not yet
implemented -- see module docstring."""
@field_validator("service_account_token_env_var")
@classmethod
def _looks_like_an_env_var_name_not_a_secret(cls, value: str | None) -> str | None:
if value is not None and not _ENV_VAR_NAME_PATTERN.match(value):
raise ValueError(
f"{value!r} does not look like an environment variable name "
"(expected e.g. 'SALES_DOMAIN_SERVICE_TOKEN') -- this field must "
"never hold a literal secret value (README.md §35.5)"
)
return value
@model_validator(mode="after")
def _required_field_matches_credential_mode(self) -> Self:
if self.credential_mode == "service_account" and not self.service_account_token_env_var:
raise ValueError(
"service_account_token_env_var is required for credential_mode='service_account'"
)
if self.credential_mode == "token_exchange" and not self.token_exchange_endpoint:
raise ValueError(
"token_exchange_endpoint is required when credential_mode == 'token_exchange'"
)
return self