
Un protocole filaire unifié, axé sur la sécurité, pour l'accès aux outils et la coordination des agents. UAP élimine la CVE-2025-49596 et les vulnérabilités d'empoisonnement des outils MCP grâce à des CapabilityCards signées Ed25519, au mTLS obligatoire, à l'authentification IdP Keycloak et à un sandboxing Docker éphémère par appel.
██╗ ██╗ █████╗ ██████╗
██║ ██║██╔══██╗██╔══██╗
██║ ██║███████║██████╔╝
██║ ██║██╔══██║██╔═══╝
╚██████╔╝██║ ██║██║
╚═════╝ ╚═╝ ╚═╝╚═╝
Universal Agent Protocol
Un protocole de transmission unifié, orienté sécurité, pour l'accès aux outils, la coordination des agents et les RPC structurés. Conçu pour combler CVE-2025-49596 et la classe de vulnérabilités d'empoisonnement d'outils et d'évasion de sandbox que MCP a laissées structurellement ouvertes.
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"}
Cela démarre Keycloak sur :8080 et la passerelle UAP sur :3000. La passerelle applique le mTLS, vérifie les CapabilityCards signées en Ed25519 et exécute chaque appel d'outil dans un conteneur Docker éphémère. Aucune configuration requise au-delà de la paire de clés générée.
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)]
Couches hexagonales — le domaine n'a aucune dépendance d'E/S. Chaque préoccupation d'infrastructure se trouve derrière une interface de port typée. Remplacez Docker par WASM, Keycloak par Auth0, Pino par un exportateur SIEM — aucun impact sur le domaine.
CVE-2025-49596 (empoisonnement d'outils via des métadonnées non signées). Les descriptions d'outils MCP sont modifiables après publication. Un attaquant peut injecter des instructions malveillantes dans les noms ou descriptions d'outils après le déploiement — le client n'a aucun moyen de détecter la falsification. UAP place toutes les métadonnées d'outils dans la charge utile de la CapabilityCard signée en Ed25519. Toute mutation après signature invalide la signature et la passerelle rejette la carte avant d'exécuter quoi que ce soit.
CWE-284 / classe des évasions de sandbox. MCP exécute les outils au niveau du processus serveur. Une traversée de chemin ou une injection de processus dans un outil atteint le système de fichiers et le réseau de l'hôte. UAP crée un conteneur Docker neuf par appel — rootfs en lecture seule, CapDrop: ALL, plafond de 128 Mo de RAM, réseau désactivé par défaut. Le conteneur est détruit après la réponse. Aucune surface d'attaque persistante entre les appels.
CWE-287 / authentification par député confus. MCP agit comme son propre fournisseur OAuth, ce qui en fait à la fois le serveur de ressources et le serveur d'autorisation. C'est le schéma classique du député confus. UAP sépare ces rôles : la passerelle est un pur serveur de ressources. Keycloak (ou tout IdP externe) est l'unique autorité. Les JWT sont vérifiés auprès d'un point de terminaison JWKS distant et strictement plafonnés à 15 minutes.
Chaque message UAP utilise la même enveloppe. Le bloc auth transporte un JWT à portée limitée et une référence à la CapabilityCard signée de l'agent. La passerelle vérifie les deux avant que la requête n'atteigne le moindre code applicatif.
{
"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 — Fastify-based UAP Gateway
│ ├── sdk-ts/ @uap/sdk-ts — TypeScript client + server SDK
│ └── sdk-py/ uap-sdk — Python async SDK (httpx + FastAPI)
├── scripts/
│ ├── generate-keypair.ts — Ed25519 keypair for local dev
│ └── keycloak-bootstrap.ts — Idempotent Keycloak realm setup
└── docker/
└── sandbox/ — Base image for zero-trust tool execution
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 encapsule n'importe quel serveur MCP en une CapabilityCard UAP signée en moins de 5 minutes. Idempotente — relancer sur un serveur déjà migré est sans effet.
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
Les noms d'outils, les descriptions et les schémas de paramètres font partie de la charge utile signée en Ed25519. Toute mutation d'un champ après signature invalide la signature — ce qui neutralise structurellement l'empoisonnement d'outils.
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..."
Nommage des branches : feat/<sprint>-<task>-<slug> — p. ex. feat/s2-2.1-mtls-keycloak
Chaque modification suit : branche → npx tsc --noEmit + npx vitest run en local → commit → PR → squash merge → suppression de la branche. Aucune dépendance à la CI pour le développement 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
| Couche | Contenu | Dépendances |
|---|
| Domaine | UapEnvelope, CapabilityCard, AuditEvent, Task | Aucune — stdlib uniquement |
| Application | InvokeToolUseCase, DelegateTaskUseCase, ValidateCardUseCase | Ports du domaine uniquement |
| Infrastructure | KeycloakAuthAdapter, DockerSandboxAdapter, Ed25519SignerAdapter, PinoAuditAdapter | Ports applicatifs + bibliothèques externes |
| Transport | Analyseur de trames UAP-RPC, passerelle Fastify, gestionnaires SSE/WebSocket | Infrastructure + sérialiseurs du domaine |
| Fonctionnalité | UAP | MCP | A2A | JSON-RPC 2.0 |
|---|
| Authentification obligatoire | ✅ mTLS + JWT toujours | ❌ Optionnel | ⚠️ Clé API uniquement | ❌ Aucune |
| Intégrité des métadonnées d'outil | ✅ Charge utile signée Ed25519 | ❌ Mutation possible après publication | ❌ Aucune signature | ❌ Aucune signature |
| Modèle de sandbox | ✅ Docker éphémère par appel | ⚠️ Niveau serveur uniquement | ❌ Aucun | ❌ Aucun |
| Découverte d'agents | ✅ Hub-and-spoke · N connexions | ❌ HTTP direct en N² | ⚠️ Basée sur DNS | ❌ Aucune |
| Piste d'audit | ✅ Journal d'ajout natif au protocole | ❌ Aucune | ❌ Aucune | ❌ Aucune |
| Traçage distribué | ✅ W3C TraceContext obligatoire | ❌ Aucun | ❌ Aucun | ❌ Aucun |
| Validation de schéma | ✅ AJV au niveau transport | ⚠️ Zod optionnel | ❌ Aucune | ❌ Aucune |
| Plafond de durée de vie du jeton | ✅ 15 min imposées | ❌ Non imposé | ❌ Non imposé | ❌ N/A |
| Sémantique de lot | ✅ séquentiel · parallèle · transactionnel | ❌ Non défini | ❌ Aucune | ⚠️ Ambiguë |
| Charges utiles binaires | ✅ Trames natives | ❌ Base64 uniquement | ❌ Base64 uniquement | ❌ Base64 uniquement |
| Modèle IdP | ✅ IdP externe uniquement | ❌ Le serveur est son propre fournisseur OAuth | ⚠️ Variable | ❌ Aucun |
| Adaptateurs de pont | ✅ Ponts MCP + A2A livrés en phase 2 | ❌ Aucun pont | ❌ Aucun pont | ❌ Aucun pont |
| Mise à l'échelle horizontale | ✅ Sans état · n'importe quel répartiteur de charge | ❌ Sessions sticky | ⚠️ Variable | ❌ Aucune |
| Variable | Description | Défaut |
|---|
KEYCLOAK_URL | URL de base de Keycloak | http://localhost:8080 |
KEYCLOAK_REALM | Nom du realm | uap |
UAP_AUDIENCE | Revendication d'audience du JWT | uap-gateway |
UAP_SIGNING_KEY_PATH | Chemin vers la clé privée Ed25519 (hex) | config/dev.privkey.hex |
UAP_DOCKER_SOCKET | Chemin du socket Docker | /var/run/docker.sock |
UAP_AUDIT_LOG_DIR | Répertoire du journal d'audit NDJSON en ajout seul | /var/log/uap |
UAP_GATEWAY_PORT | Port d'écoute de la passerelle | 3000 |
UAP_MTLS_CERT | Certificat TLS serveur (PEM) | certs/server.crt |
UAP_MTLS_KEY | Clé privée TLS serveur (PEM) | certs/server.key |
UAP_MTLS_CA | Certificat CA pour la vérification des certificats clients | certs/ca.crt |
OTEL_EXPORTER_OTLP_ENDPOINT | Point de terminaison du collecteur OpenTelemetry | http://localhost:4318 |
REDIS_URL | Redis pour le registre multi-instances (optionnel) | — |
| Paquet | Version | Rôle |
|---|
fastify | ^4.27 | Serveur HTTP de la passerelle |
zod | ^3.23 | Schéma d'enveloppe et inférence de types |
@noble/curves | ^1.4 | Signature Ed25519 |
jose | ^5.4 | Vérification JWT, client JWKS |
ajv | ^8.16 | Validation des paramètres au niveau transport |
dockerode | ^4.0 | Adaptateur de sandbox zero-trust |
pino | ^9.2 | Journal d'audit structuré en ajout seul |
ulid | ^2.3 | Identifiants uniques triables |
@opentelemetry/sdk-node | ^0.52 | Traçage distribué |
ioredis | ^5.3 | Registre de services multi-instances |
@modelcontextprotocol/sdk | ^1.0 | Adaptateur de pont MCP |
httpx (Python) | ^0.27 | Client HTTP asynchrone pour le SDK Python |
pydantic (Python) | ^2.7 | Validation de types Python |