
Un protocolo de red unificado y centrado en la seguridad para el acceso a herramientas y la coordinación de agentes. UAP elimina las vulnerabilidades CVE-2025-49596 y de envenenamiento de herramientas MCP mediante CapabilityCards firmadas con Ed25519, mTLS obligatorio, autenticación con Keycloak IdP y sandboxing efímero de Docker por llamada.
██╗ ██╗ █████╗ ██████╗
██║ ██║██╔══██╗██╔══██╗
██║ ██║███████║██████╔╝
██║ ██║██╔══██║██╔═══╝
╚██████╔╝██║ ██║██║
╚═════╝ ╚═╝ ╚═╝╚═╝
Universal Agent Protocol
Un protocolo de comunicación unificado y centrado en la seguridad para el acceso a herramientas, la coordinación de agentes y RPC estructurado. Creado para cerrar CVE-2025-49596 y la clase de vulnerabilidades de envenenamiento de herramientas y escape de sandbox que MCP dejó estructuralmente abiertas.
git clone https://github.com/RajSidwadkar/UAP-protocol
cd UAP-protocol
npm install
npx tsx scripts/generate-keypair.ts
docker compose -f docker-compose.dev.yml up -d
curl http://localhost:3000/health
# {"status":"ok","version":"1.0.0"}
Eso inicia Keycloak en :8080 y la Puerta de Enlace UAP en :3000. La puerta de enlace aplica mTLS, verifica las CapabilityCards firmadas con Ed25519 y ejecuta cada llamada de herramienta en un contenedor Docker efímero. No se requiere configuración más allá del par de claves generado.
graph TD
A[Client / Agent SDK] -->|mTLS + JWT + card_sig| B[UAP Gateway<br/>Fastify · port 3000]
B --> C{8-Stage Pipeline}
C --> C1[1 · Frame parse<br/>UapMessageFactory]
C1 --> C2[2 · Schema validate<br/>AJV against schema_ref]
C2 --> C3[3 · Token verify<br/>Keycloak JWKS · max 15 min]
C3 --> C4[4 · Scope enforce<br/>PermissionEnforcer]
C4 --> C5[5 · Card verify<br/>Ed25519 signature check]
C5 --> C6[6 · Sandbox execute<br/>Docker · CapDrop ALL · 128 MB]
C6 --> C7[7 · Audit emit<br/>AuditEventBus · non-blocking]
C7 --> C8[8 · Response<br/>UapResponseEnvelope]
B --> R[(Service Registry<br/>InMemory · Redis)]
B --> KC[(Keycloak IdP<br/>OAuth 2.1 · PKCE)]
B --> OT[(OpenTelemetry<br/>W3C TraceContext)]
B --> AU[(Audit Log<br/>append-only NDJSON)]Capas hexagonales — el dominio tiene cero dependencias de E/S. Cada preocupación de infraestructura se sitúa detrás de una interfaz de puerto tipada. Cambia Docker por WASM, Keycloak por Auth0, Pino por un exportador SIEM — impacto nulo en el dominio.
| Capa | Contenido | Dependencias |
|---|---|---|
| Dominio | UapEnvelope, CapabilityCard, AuditEvent, Task | Ninguna — solo stdlib |
| Aplicación | InvokeToolUseCase, DelegateTaskUseCase, ValidateCardUseCase | Solo puertos de dominio |
| Infraestructura | KeycloakAuthAdapter, DockerSandboxAdapter, Ed25519SignerAdapter, PinoAuditAdapter | Puertos de la aplicación + librerías externas |
| Transporte | Analizador de tramas UAP-RPC, puerta de enlace Fastify, manejadores SSE/WebSocket | Serializadores de infraestructura + dominio |
| Característica | UAP | MCP | A2A | JSON-RPC 2.0 |
|---|---|---|---|---|
| Autenticación obligatoria | ✅ mTLS + JWT siempre | ❌ Opcional | ⚠️ Solo clave API | ❌ Ninguna |
| Integridad de metadatos de herramientas | ✅ Carga útil firmada con Ed25519 | ❌ Mutación posterior a la publicación posible | ❌ Sin firma | ❌ Sin firma |
| Modelo de sandbox | ✅ Docker efímero por llamada | ⚠️ Solo a nivel de servidor | ❌ Ninguno | ❌ Ninguno |
| Descubrimiento de agentes | ✅ Hub-and-spoke · N conexiones | ❌ HTTP directo N² | ⚠️ Basado en DNS | ❌ Ninguno |
| Registro de auditoría | ✅ Registro de solo-apéndice nativo del protocolo | ❌ Ninguno | ❌ Ninguno | ❌ Ninguno |
| Trazado distribuido | ✅ W3C TraceContext obligatorio | ❌ Ninguno | ❌ Ninguno | ❌ Ninguno |
| Validación de esquema | ✅ AJV en la capa de transporte | ⚠️ Zod opcional | ❌ Ninguna | ❌ Ninguna |
| Límite de vida del token | ✅ 15 min aplicado | ❌ No aplicado | ❌ No aplicado | ❌ N/A |
| Semántica de lotes | ✅ serial · paralelo · transaccional | ❌ Indefinida | ❌ Ninguna | ⚠️ Ambigua |
| Cargas útiles binarias | ✅ Tramas nativas | ❌ Solo Base64 | ❌ Solo Base64 | ❌ Solo Base64 |
| Modelo de IdP | ✅ Solo IdP externo | ❌ El servidor es su propio proveedor OAuth | ⚠️ Varía | ❌ Ninguno |
| Adaptadores de puente | ✅ Los puentes MCP + A2A se incluyen en la Fase 2 | ❌ Sin puente | ❌ Sin puente | ❌ Sin puente |
| Escalado horizontal | ✅ Sin estado · cualquier balanceador de carga | ❌ Sticky sessions | ⚠️ Varía | ❌ Ninguno |
CVE-2025-49596 (envenenamiento de herramientas mediante metadatos sin firmar). Las descripciones de herramientas de MCP son mutables después de su publicación. Un atacante puede inyectar instrucciones maliciosas en los nombres o descripciones de las herramientas después del despliegue: el cliente no tiene forma de detectar la manipulación. UAP coloca todos los metadatos de las herramientas dentro de la carga útil de la CapabilityCard firmada con Ed25519. Cualquier mutación posterior a la firma invalida la firma y la puerta de enlace rechaza la tarjeta antes de ejecutar nada.
CWE-284 / clase de escape de sandbox. MCP ejecuta las herramientas a nivel del proceso del servidor. Un path traversal o una inyección de procesos en cualquier herramienta alcanza el sistema de archivos y la red del host. UAP crea un contenedor Docker nuevo por llamada — rootfs de solo lectura, CapDrop: ALL, límite de 128 MB de RAM, red deshabilitada por defecto. El contenedor se destruye después de la respuesta. No hay superficie de ataque persistente entre llamadas.
CWE-287 / autenticación de diputado confundido. MCP actúa como su propio proveedor OAuth, convirtiéndose a la vez en servidor de recursos y servidor de autorización. Este es el patrón clásico de diputado confundido. UAP separa estos roles: la puerta de enlace es un servidor de recursos puro. Keycloak (o cualquier IdP externo) es la única autoridad. Los JWT se verifican contra un endpoint JWKS remoto y tienen un tope máximo de 15 minutos.
Cada mensaje UAP usa el mismo envelope. El bloque auth transporta un JWT con alcance y una referencia a la CapabilityCard firmada del agente. La puerta de enlace verifica ambos antes de que la solicitud llegue a cualquier código de aplicación.
{
"uap": {
"version": "1.0",
"type": "tool_call",
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"trace": {
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
},
"auth": {
"token": "eyJhbGciOiJSUzI1NiJ9...",
"scope": ["tool:read"],
"card_sig": "ed25519:a1b2c3d4..."
}
},
"method": "tools/invoke",
"schema_ref": "uap:tool.invoke/v1",
"params": {
"tool_id": "db:query",
"input": { "sql": "SELECT 1" }
},
"ack": true
}
UAP-protocol/
├── packages/
│ ├── gateway/ @uap/gateway — Puerta de Enlace UAP basada en Fastify
│ ├── sdk-ts/ @uap/sdk-ts — SDK de cliente + servidor TypeScript
│ └── sdk-py/ uap-sdk — SDK asíncrono de Python (httpx + FastAPI)
├── scripts/
│ ├── generate-keypair.ts — Par de claves Ed25519 para desarrollo local
│ └── keycloak-bootstrap.ts — Configuración idempotente del realm de Keycloak
└── docker/
└── sandbox/ — Imagen base para ejecución de herramientas de confianza cero
import { UapClient, ClientCredentialsTokenProvider } from "@uap/sdk-ts";
const client = new UapClient({
gatewayUrl: "https://gateway.example.com",
tokenProvider: new ClientCredentialsTokenProvider({
tokenUrl: process.env.TOKEN_URL!,
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
}),
});
const result = await client.invokeTool(
"db:query",
{ sql: "SELECT * FROM users LIMIT 10" },
["tool:read"]
);
const task = await client.delegateTask(
"summarizer",
{ text: "..." },
["task:submit"]
);
from uap_sdk.client import UapClient, UapClientOptions
from uap_sdk.token import ClientCredentialsTokenProvider
async with UapClient(UapClientOptions(
gateway_url="https://gateway.example.com",
token_provider=ClientCredentialsTokenProvider(
token_url=os.environ["TOKEN_URL"],
client_id=os.environ["CLIENT_ID"],
client_secret=os.environ["CLIENT_SECRET"],
)
)) as client:
result = await client.invoke_tool("db:query", {"sql": "SELECT 1"}, ["tool:read"])
Middleware FastAPI
from uap_sdk.middleware import uap_auth
@app.post("/summarize")
@uap_auth(scope=["task:submit"])
async def summarize(request: Request):
claims = request.state.uap_claims # typed AuthClaims
...
La CLI uap-migrate envuelve cualquier servidor MCP como una CapabilityCard UAP firmada en menos de 5 minutos. Idempotente: volver a ejecutarla sobre un servidor ya migrado no produce ningún efecto.
node dist/cli/migrate.js mcp http://localhost:3001 \
--issuer did:uap:my-org \
--key config/dev.privkey.hex \
--out ./uap-cards
# → ./uap-cards/did:uap:my-org.card.json
Los nombres de herramientas, descripciones y esquemas de parámetros forman parte de la carga útil firmada con Ed25519. Mutar cualquier campo después de la firma invalida la firma — derrotando estructuralmente el envenenamiento de herramientas.
const signed = await signer.sign({
issuer: "did:uap:my-agent",
version: "1.0.0",
tools: [{
id: "db:query",
description: "Run a read-only SQL query",
inputSchema: { type: "object", required: ["sql"] },
scopes: ["tool:read"]
}],
scopes: ["tool:read"],
issuedAt: Date.now(),
expiresAt: Date.now() + 86_400_000 * 30
});
// signed.signature === "ed25519:a1b2c3..."
| Variable | Descripción | Valor por defecto |
|---|---|---|
KEYCLOAK_URL | URL base de Keycloak | http://localhost:8080 |
KEYCLOAK_REALM | Nombre del realm | uap |
UAP_AUDIENCE | Reclamo de audiencia del JWT | uap-gateway |
UAP_SIGNING_KEY_PATH | Ruta al hex de la clave privada Ed25519 | config/dev.privkey.hex |
UAP_DOCKER_SOCKET | Ruta del socket de Docker | /var/run/docker.sock |
UAP_AUDIT_LOG_DIR | Directorio del registro de auditoría NDJSON de solo-apéndice | /var/log/uap |
UAP_GATEWAY_PORT | Puerto de escucha de la puerta de enlace | 3000 |
UAP_MTLS_CERT | Certificado TLS del servidor (PEM) | certs/server.crt |
UAP_MTLS_KEY | Clave privada TLS del servidor (PEM) | certs/server.key |
UAP_MTLS_CA | Certificado CA para la verificación de certificados de cliente | certs/ca.crt |
OTEL_EXPORTER_OTLP_ENDPOINT | Endpoint del colector OpenTelemetry | http://localhost:4318 |
REDIS_URL | Redis para el registro multi-instancia (opcional) | — |
| Paquete | Versión | Propósito |
|---|---|---|
fastify | ^4.27 | Servidor HTTP de la puerta de enlace |
zod | ^3.23 | Esquema del envelope e inferencia de tipos |
@noble/curves | ^1.4 | Firma Ed25519 |
jose | ^5.4 | Verificación de JWT, cliente JWKS |
ajv | ^8.16 | Validación de parámetros a nivel de transporte |
dockerode | ^4.0 | Adaptador de sandbox de confianza cero |
pino | ^9.2 | Registro de auditoría estructurado de solo-apéndice |
ulid | ^2.3 | IDs únicos ordenables |
@opentelemetry/sdk-node | ^0.52 | Trazado distribuido |
ioredis | ^5.3 | Registro de servicios multi-instancia |
@modelcontextprotocol/sdk | ^1.0 | Adaptador de puente MCP |
httpx (Python) | ^0.27 | Cliente HTTP asíncrono para el SDK de Python |
pydantic (Python) | ^2.7 | Validación de tipos en Python |
Nomenclatura de ramas: feat/<sprint>-<task>-<slug> — p. ej. feat/s2-2.1-mtls-keycloak
Cada cambio sigue: rama → npx tsc --noEmit + npx vitest run localmente → commit → PR → squash merge → eliminación de la rama. Sin dependencia de CI para el desarrollo local.
# Run all tests before committing
cd packages/gateway && npx vitest run
cd packages/sdk-ts && npx vitest run
cd packages/sdk-py && python -m pytest tests/ -v
UAP · Universal Agent Protocol · 2026
SPDX-License-Identifier: Apache-2.0