8.7 KiB
How-To — rodando a RI
Guia prático orientado a tarefas. Para entender o que cada componente faz e por que, ver
AS-BUILT.md. Público-alvo: quem vai rodar esta RI.
Pré-requisitos
- Python 3.12+ e
pip/uv. - Opcional: binário
opano PATH (ouOPA_BINARYapontando para ele) — sem ele, os testes que exercitam o PDP real são pulados (pytest.skip), não falham. - Opcional:
dockerno PATH — sem ele,tests/e2e/test_real_keycloak.pyetests/e2e/test_security_acceptance.pysão pulados pela mesma razão. - Opcional: credenciais de um LLM OpenAI-compatible (
LLM_BASE_URL/LLM_API_KEY/LLM_MODEL, ver.env.example) — sem elas,tests/e2e/test_example_agent.pyetests/e2e/test_multi_agent_mode_a.pysão pulados.
Instalação
make install # cria .venv e instala o pacote em modo editável (dev extras inclusos)
Instalar o OPA (sem Docker)
OPA é um binário Go estático — não precisa de Docker nem de build:
curl -sL -o /tmp/opa "https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static"
chmod +x /tmp/opa
cp /tmp/opa .venv/bin/opa
opa version
Versão de referência usada nesta RI: OPA v1.20.2 (Rego v1 é o padrão — config/policies/*.rego
usa import rego.v1).
Lint, typecheck e validação de schema
make lint # ruff check + ruff format --check
make typecheck # mypy --strict
make validate-schemas # valida todo YAML de config/ contra seu modelo Pydantic
make export-schemas # regenera schemas/*.schema.json a partir dos modelos
Rodando os testes
make test # == pytest --forked
--forked (pytest-forked) isola cada função de teste em seu próprio processo — necessário
porque vários pares de servidor ASGI em processo, compartilhando um único event loop entre
muitos testes sequenciais na mesma sessão pytest, produziam falhas intermitentes de limpeza
assíncrona. Com --forked, a suíte é 100% determinística em execuções repetidas. O custo é só
tempo de execução (cerca de +1min nesta máquina de referência). make test/CI usam --forked
por padrão.
Por suíte, o que cada uma prova:
pytest tests/unit --forked— comportamento isolado de cada componente (schemas, PDP, entitlement, authn, registry, downstream identity, approval, fabric router, audit, canonical request, rest adapter, bypass detection, gateway cache, shared client lint, profile intelligence, offering filter + invariantes de propriedade, orchestrator, agent runtime, PII redaction, prompt store).pytest tests/e2e --forked— fluxos ponta a ponta contra um Gateway real em processo (test_walking_skeleton.py,test_governed_gateway.py), Keycloak/Docker real (test_real_keycloak.py), LLM real (test_example_agent.py,test_multi_agent_mode_a.py), política de falha (test_fail_closed.py).pytest tests/contract --forked— o contrato input/output de cada tool, conectando direto a cada domain server (nunca pelo Gateway) — o contrato de uma tool é propriedade do domain server, independente de quem está ou não entitled a chamá-la.pytest tests/adversarial --forked— cenários adversariais de prompt/tool injection (test_prompt_injection.py,test_tool_poisoning.py,test_payload_identity_spoofing.py) e de protocolo/identidade (test_protocol_identity.py).pytest tests/e2e/test_security_acceptance.py -v— a suíte de aceite de segurança formal, contra odocker-compose.ymlreal (--profile core --profile identity, Keycloak incluído). Não roda com--forked/make test— exige Docker e paga o custo de build+Keycloak por execução, por isso é rodada separadamente:pytest tests/e2e/test_security_acceptance.py -v
Subindo a stack local
Sem Docker
source .venv/bin/activate
opa run --server --addr 127.0.0.1:8181 config/policies/ &
export SALES_DOMAIN_SERVICE_TOKEN=dev-sales-domain-service-token
export FINANCE_DOMAIN_SERVICE_TOKEN=dev-finance-domain-service-token
python3 -m example_mcp_servers.sales_domain.server &
python3 -m example_mcp_servers.finance_domain.server &
python3 -m emcp_bus.gateway.server
Sem OIDC_JWKS_URL/OIDC_ISSUER definidos, o Gateway roda em modo "sem IdP configurado": nega
tudo por padrão (tools/list vazio, tools/call sempre UNAUTHENTICATED) — ver
interfaces/mcp-gateway.md para os dois
modos.
Com Docker Compose
make up # == docker compose --profile core up --build (sem IdP)
make down
make logs
Perfis disponíveis (docker-compose.yml):
| Perfil | Serviços | O que adiciona |
|---|---|---|
core |
opa, sales-domain, finance-domain, gateway |
PEP/PDP + os dois domínios de exemplo — mínimo funcional |
identity |
+ keycloak |
autenticação OIDC real |
observability |
+ otel-collector, jaeger |
tracing OTLP (UI Jaeger em http://localhost:16686) |
Com Keycloak real:
cp .env.example .env # descomente as 3 linhas OIDC_*
docker compose --profile core --profile identity up --build
Todos os perfis simultaneamente:
docker compose --profile core --profile identity --profile observability up -d --build
Rodando o agente de exemplo e os modos multiagente
Requer LLM_BASE_URL/LLM_API_KEY/LLM_MODEL em .env (qualquer endpoint OpenAI-compatible;
validado contra meta.llama-3.3-70b-instruct via OCI Generative AI).
pytest tests/e2e/test_example_agent.py -v # agente único, tool-calling real
pytest tests/e2e/test_multi_agent_mode_a.py -v # Modo A (Global Supervisor externo, ADR-024)
pytest tests/e2e/test_real_keycloak.py -k mode_b -v # Modo B (Orchestrator como único cérebro)
Modo B exige Docker (sobe seu próprio Keycloak real).
Publicando uma nova capability
- Escreva o manifesto YAML em
config/capabilities/<domain>/<capability>.yaml, seguindo o schemaCapabilityManifest(verinterfaces/registry-capability-manifest.mdpara o schema completo, e qualquer arquivo emconfig/capabilities/*/como exemplo real — nome canônico<domain>.<capability>,risk_tier,owner,entitlements,backend.serviceapontando para um backend já registrado emconfig/backends/). - Se a capability precisar de um backend novo, registre-o em
config/backends/<backend>.yaml(verinterfaces/downstream-identity.md). - Associe a capability a um
ClientProfileexistente ou novo emconfig/clients/profiles/<profile>.yaml, adicionando o nome canônico aallowed_tools. - Rode
make validate-schemas— valida o novo YAML contra seu modelo Pydantic antes de qualquer coisa. - Rode
make export-schemasse o modelo Pydantic em si mudou (não necessário para apenas adicionar um YAML de exemplo a um schema já existente). - Reinicie o Gateway (ou a stack Docker) — o Registry é semeado a partir de
config/capabilities/no boot (registry/seed.py); não há hot-reload nesta RI.
Registrando um novo MCP Client
- Registre o client no realm Keycloak (ou equivalente OIDC) com grant
client_credentialshabilitado e um mapper de audience para oOIDC_AUDIENCEdo Gateway (emcp-gatewaypor padrão) — verdeploy/keycloak/realm-export.jsonpara os 3 clients de exemplo já configurados dessa forma. - Crie
config/clients/<client-id>.yaml(ClientRegistration:client_id,profile,environment) apontando para umClientProfileexistente ou novo emconfig/clients/profiles/. - Rode
make validate-schemas. - Reinicie o Gateway — registros de client também são carregados no boot
(
load_yaml_models(config.clients_dir, ClientRegistration)).
Observabilidade
Jaeger (tracing)
docker compose --profile core --profile observability up -d --build
# UI: http://localhost:16686
Langfuse self-hosted (observabilidade de LLM)
docker compose -f docker-compose.yml -f deploy/langfuse/docker-compose.override.yml \
--profile llm-observability up -d
# Aguardar:
curl http://localhost:3000/api/public/health
# Criar um projeto/API key na UI (http://localhost:3000) ou pré-semear via
# LANGFUSE_INIT_PROJECT_PUBLIC_KEY/LANGFUSE_INIT_PROJECT_SECRET_KEY
# (ver deploy/langfuse/docker-compose.override.yml).
Depois, aponte as variáveis padrão do OTel SDK para o endpoint OTLP nativo do Langfuse (nenhuma mudança de código):
export OTEL_EXPORTER_OTLP_ENDPOINT=http://langfuse-web:3000/api/public/otel
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64(public_key:secret_key)>"
Este perfil é pesado (6 containers: web/worker/postgres/clickhouse/redis/minio) e opt-in,
desacoplado dos perfis core/identity/observability.