Protocole Web Agentique (AWP)
ADK-Rust fournit les types Agentic Web Protocol (AWP) et l’intégration Axum pour rendre les sites web et les services accessibles aux agents IA. L’implémentation couvre deux crates : awp-types (types de protocole purs) et adk-awp (routes, middleware et interfaces de service). Les applications fournissent la distribution des agents, l’authentification, l’autorisation et la livraison durable des webhooks.
Vue d’ensemble
AWP permet à n’importe quel site web de déclarer ses capacités, ses politiques et son contexte métier dans un format lisible par machine. Les agents IA peuvent découvrir ces capacités, négocier les versions du protocole, s’abonner aux événements et interagir via des messages typés A2A. adk-awp applique les limites de corps et de débit à sa frontière HTTP ; les gestionnaires d’application appliquent l’identité et l’autorisation des capacités.
Utilisez AWP lorsque :
- Vous voulez que des agents IA découvrent votre service et interagissent avec lui de manière programmatique
- Vous avez besoin de métadonnées de niveau de confiance et d’un point d’accroche pour le contrôle d’accès appliqué par l’application
- Vous voulez servir à la fois des visiteurs humains et des agents IA depuis les mêmes points de terminaison
- Vous avez besoin d’abonnements aux événements et de primitives de signature HMAC-SHA256
- Vous voulez une machine d’état de santé pour la supervision du service
Architecture
Flux des requêtes AWP
Disposition de l’application
┌─────────────────────────────────────────────────┐
│ Your Application │
│ │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ LLM Agent │ │ awp_routes(state) │ │
│ │ (adk-agent) │ │ ├ /.well-known/awp.json │ │
│ │ │ │ ├ /awp/manifest │ │
│ │ Instructions│ │ ├ /awp/health │ │
│ │ derived from│ │ └ /awp/a2a │ │
│ │ business. │ │ auth + management routes│ │
│ │ toml │ │ │ │
│ └──────────────┘ └──────────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌────┴──────────────────────┴────┐ │
│ │ BusinessContextLoader │ │
│ │ (business.toml + ArcSwap) │ │
│ └────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Crates
| Crate | Objectif | Dépendances |
|---|---|---|
awp-types | Types de protocole (énums, structs, erreurs) | Zéro dépendance adk-* — seulement serde, uuid, chrono, thiserror |
adk-awp | Routes, middleware, interfaces de service et implémentations en mémoire | awp-types, adk-core, axum 0.8, tokio, dashmap |
La séparation signifie que tout projet Rust peut dépendre de awp-types sans introduire l’arbre ADK.
Démarrage rapide
1. Créez un business.toml
site_name = "My Shop"
site_description = "An online store powered by AWP"
domain = "myshop.example.com"
contact = "hello@myshop.example.com"
[business]
country = "US"
currency = "USD"
languages = ["en"]
[brand_voice]
tone = "friendly and helpful"
greeting = "Welcome! How can I help?"
[[capabilities]]
name = "browse_products"
description = "Browse the product catalog"
endpoint = "/api/products"
method = "GET"
access_level = "anonymous"
[[capabilities]]
name = "place_order"
description = "Place an order"
endpoint = "/api/orders"
method = "POST"
access_level = "known"
[[products]]
sku = "WIDGET-001"
name = "Standard Widget"
price = 1999
inventory = 500
tags = ["widget"]
[[policies]]
name = "privacy"
description = "Minimal data collection, no tracking."
policy_type = "privacy"
[payments]
providers = ["stripe"]
auto_approve_threshold = 5000
[support]
escalation_contacts = ["support@myshop.example.com"]
hours = "Mon-Fri 9-5 EST"
2. Chargez et servez les routes AWP
use std::sync::Arc;
use adk_awp::{AwpA2aHandler, AwpState, BusinessContextLoader, awp_routes};
use async_trait::async_trait;
use awp_types::AwpError;
use axum::http::{HeaderMap, header};
use serde_json::{Value, json};
struct ApplicationA2a {
bearer_token: Arc<str>,
}
#[async_trait]
impl AwpA2aHandler for ApplicationA2a {
async fn handle(&self, headers: HeaderMap, message: Value) -> Result<Value, AwpError> {
let expected = format!("Bearer {}", self.bearer_token);
let authorized = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value == expected);
if !authorized {
return Err(AwpError::Unauthorized("invalid A2A credential".to_string()));
}
// Authorize the requested capability and dispatch to the application agent.
Ok(json!({ "status": "processed", "messageId": message["id"] }))
}
}
let loader = BusinessContextLoader::from_file("business.toml".as_ref())?;
let a2a_token: Arc<str> = std::env::var("AWP_A2A_TOKEN")?.into();
let state = AwpState::builder(loader.context_ref())
.a2a_handler(Arc::new(ApplicationA2a { bearer_token: a2a_token }))
.build();
let app = axum::Router::new()
.merge(awp_routes(state))
.merge(your_custom_routes);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3456").await?;
axum::serve(
listener,
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.await?;
Cela enregistre les quatre points de terminaison publics AWP avec négociation de version, limitation du débit et une limite de corps de 64 KiB A2A. Sans un AwpA2aHandler,
POST /awp/a2a renvoie 503 et ne reconnaît jamais un travail qui n’a pas été
dispatché. ConnectInfo fournit l’adresse du pair utilisée pour isoler les compartiments anonymes
de limitation du débit ; sans elle, les appelants inconnus partagent volontairement un compartiment unique.
Points de terminaison publics AWP
| Méthode | Chemin | Description |
|---|---|---|
| GET | /.well-known/awp.json | Document de découverte — point d’entrée pour les agents |
| GET | /awp/manifest | JSON-LD manifeste de capacité |
| GET | /awp/health | État de santé (Sain/Se dégradant/Dégradé) |
| POST | /awp/a2a | Dispatch A2A fourni par l'application |
Points de terminaison de gestion authentifiés
awp_management_routes() renvoie la gestion des abonnements séparément et
sans couche d’authentification. Appliquez le middleware d’authentification de l’application
avant de le fusionner :
| Méthode | Chemin | Description |
|---|---|---|
| POST | /awp/events/subscribe | Créer un abonnement de webhook |
| GET | /awp/events/subscriptions | Lister tous les abonnements |
| SUPPRIMER | /awp/events/subscriptions/{id} | Supprimer un abonnement |
Document de découverte
Le document de découverte à /.well-known/awp.json est généré automatiquement à partir de votre business.toml :
{
"version": { "major": 1, "minor": 0 },
"siteName": "My Shop",
"siteDescription": "An online store powered by AWP",
"capabilityManifestUrl": "https://myshop.example.com/awp/manifest",
"a2aEndpointUrl": "https://myshop.example.com/awp/a2a",
"eventsEndpointUrl": "https://myshop.example.com/awp/events/subscribe",
"healthEndpointUrl": "https://myshop.example.com/awp/health",
"supportedTrustLevels": ["anonymous"]
}
Manifeste de capacités
Le manifeste à /awp/manifest utilise le format JSON-LD :
{
"@context": "https://schema.org",
"@type": "WebAPI",
"name": "My Shop",
"description": "An online store powered by AWP",
"capabilities": [
{
"name": "browse_products",
"description": "Browse the product catalog",
"endpoint": "/api/products",
"method": "GET"
}
]
}
Niveaux de confiance
AWP utilise quatre niveaux de confiance avec un accès croissant :
| Niveau | Discriminant | Mode d’attribution |
|---|---|---|
Anonymous | 0 | Aucune information d'identification |
Known | 1 | Clé API valide ou JWT |
Partner | 2 | JWT avec portée partner |
Internal | 3 | JWT avec portée internal |
Les niveaux de confiance sont ordonnés : Anonymous < Known < Partner < Internal. Chaque capacité dans business.toml déclare son access_level minimum.
DefaultTrustAssigner classe chaque requête comme Anonymous. Un en-tête bearer ou API
key n’est pas considéré comme fiable tant qu’un vérificateur d’application ne l’a pas validé. Des niveaux de confiance plus élevés exigent donc un assignateur personnalisé.
Configurez .supported_trust_levels(...) en même temps que cet assignateur afin que la découverte
n’annonce que les niveaux que le déploiement peut vérifier.
Attribution personnalisée de la confiance
Implémentez le trait TrustLevelAssigner pour une logique personnalisée :
use std::sync::Arc;
use adk_awp::TrustLevelAssigner;
use async_trait::async_trait;
use awp_types::TrustLevel;
use axum::http::{HeaderMap, header};
struct MyTrustAssigner {
bearer_token: Arc<str>,
}
#[async_trait]
impl TrustLevelAssigner for MyTrustAssigner {
async fn assign(&self, headers: &HeaderMap) -> TrustLevel {
let expected = format!("Bearer {}", self.bearer_token);
if headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value == expected)
{
TrustLevel::Known
} else {
TrustLevel::Anonymous
}
}
}
Utilisez la même identité vérifiée et la même source de portée que le reste de l’application
lors de l’attribution de Partner ou de Internal.
Limitation de débit
Le InMemoryRateLimiter intégré utilise un algorithme de fenêtre glissante avec des limites par niveau de confiance :
| Niveau de confiance | Limite par défaut |
|---|---|
| Anonyme | 30 requêtes/minute |
| Connu | 120 requêtes/minute |
| Partenaire | 600 requêtes/minute |
| Interne | Illimité |
Les requêtes rejetées reçoivent HTTP 429 avec un en-tête Retry-After.
Limites personnalisées
use std::collections::HashMap;
use awp_types::TrustLevel;
use adk_awp::{InMemoryRateLimiter, RateLimitConfig};
let mut limits = HashMap::new();
limits.insert(TrustLevel::Anonymous, RateLimitConfig {
max_requests: 10,
window_secs: 60,
});
limits.insert(TrustLevel::Known, RateLimitConfig {
max_requests: 100,
window_secs: 60,
});
let limiter = InMemoryRateLimiter::with_config(limits);
Négociation de version
Toutes les routes AWP incluent un middleware de négociation de version :
- Les clients envoient l’en-tête
AWP-Version: 1.1(facultatif — par défaut, la version actuelle) - Le serveur vérifie la compatibilité de la version majeure
- Les requêtes compatibles se poursuivent ; les requêtes incompatibles reçoivent HTTP 406
- Les valeurs de version malformées reçoivent HTTP 400
- La réponse inclut l’en-tête
AWP-Version: 1.0
Abonnements aux événements
La gestion des abonnements est une surface privilégiée. Montez
awp_management_routes() derrière une authentification avant d’accepter ces
requêtes. Le URLs de rappel doit être un HTTPS URLs absolu et les secrets de signature doivent
contenir au moins 32 octets :
# Subscribe
curl -X POST http://localhost:3456/awp/events/subscribe \
-H "Authorization: Bearer $AWP_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subscriber": "my-agent",
"callbackUrl": "https://my-agent.example/webhook",
"eventTypes": ["health.changed"],
"secret": "replace-with-at-least-32-random-bytes"
}'
# List subscriptions
curl -H "Authorization: Bearer $AWP_ADMIN_TOKEN" \
http://localhost:3456/awp/events/subscriptions
InMemoryEventSubscriptionService signe et journalise les livraisons correspondantes mais
n’effectue aucune E/S réseau. Les applications de production implémentent
EventSubscriptionService avec validation de la destination, une file durable,
des tentatives bornées et leur client HTTP.
Une implémentation de livraison HTTP peut porter un en-tête X-AWP-Signature avec la
signature HMAC-SHA256 :
X-AWP-Signature: sha256=<hex_digest>
Vérifiez les signatures avec adk_awp::verify_signature(payload, secret, signature).
Machine d’état de santé
Le point de terminaison de santé suit l’état du service avec des transitions strictement validées :
Healthy → Degrading → Degraded
↑ │ │
└─────────┘ │
└─────────────────────┘
Les changements d’état émettent des événements health.changed à tous les abonnés correspondants.
use adk_awp::HealthStateMachine;
// Transition to degrading
health.report_degrading("database latency high").await?;
// Transition to degraded
health.report_degraded("database unreachable").await?;
// Recover
health.report_healthy().await?;
Les transitions invalides (par ex., Healthy → Degraded) renvoient une erreur.
Stockage du consentement
AWP inclut une interface de stockage du consentement. La conformité réglementaire exige également une notification spécifique à l’application, une base légale, une rétention, des contrôles d’accès et une politique de suppression ; le choix d’une implémentation de stockage n’établit pas la conformité :
use adk_awp::InMemoryConsentService;
let consent = InMemoryConsentService::new();
// Capture consent
consent.capture_consent("visitor-123", "analytics").await?;
// Check consent
let has_consent = consent.check_consent("visitor-123", "analytics").await?;
// Revoke consent
consent.revoke_consent("visitor-123", "analytics").await?;
Détection du type de demandeur
AWP détecte si une requête provient d’un humain ou d’un agent IA :
- En-tête
X-AWP-Channel: agent→ Agent (remplacement explicite) Accept: application/json+ modèle User-Agent d’agent → Agent- Sinon → Humain
Modèles User-Agent d’agent : bot, crawler, spider, agent, gpt, claude, gemini, perplexity, anthropic, openai.
use adk_awp::detect_requester_type;
use axum::http::HeaderMap;
let mut headers = HeaderMap::new();
headers.insert("X-AWP-Channel", "agent".parse().unwrap());
let requester = detect_requester_type(&headers);
// RequesterType::Agent
Types de messages AWP
Au-delà des messages génériques A2A, AWP définit des catégories de messages typées pour le routage des agents :
| Catégorie | Fonctionnement |
|---|---|
VisitorIntentSignal | Intention d’achat ou de service |
ContentGapSignal | Contenu manquant ou obsolète détecté |
PaymentIntent | Message du cycle de vie du paiement |
SupportEscalation | Escalade vers le support humain |
ReviewSignal | Revue ou retour d’une plateforme |
OperationsProposal | Inventaire, proposition de planification |
InvokeCapability | Invoquer une capacité déclarée |
RenderUi | Demander le rendu de l’interface utilisateur |
OutboundTrigger | Message sortant proactif |
use awp_types::{AwpMessageType, AwpTypedMessage};
let msg = AwpTypedMessage {
id: uuid::Uuid::now_v7(),
sender: "visitor-agent".to_string(),
recipient: "payment-agent".to_string(),
awp_type: AwpMessageType::PaymentIntent,
timestamp: chrono::Utc::now(),
payload: serde_json::json!({"sku": "WIDGET-001", "amount": 2500}),
};
Intentions de paiement
AWP définit un cycle de vie de paiement simplifié pour les paiements pilotés par une politique de propriétaire :
Draft → PendingApproval → Approved → Executing → Settled
→ Rejected
→ Cancelled
Le PaymentPolicy évalue s’il faut approuver automatiquement ou exiger l’approbation du propriétaire :
use awp_types::{PaymentPolicy, TrustLevel};
let policy = PaymentPolicy::default(); // $50 auto-approve, $500 require approval
let decision = policy.evaluate(2500, TrustLevel::Known);
// PaymentPolicyDecision::AutoApprove (amount $25 <= $50 threshold)
let decision = policy.evaluate(60_000, TrustLevel::Partner);
// PaymentPolicyDecision::RequireApproval (amount $600 > $500 threshold)
Schéma business.toml
Le schéma complet prend en charge une configuration d’entreprise riche :
| Section | Champs | Requis |
|---|---|---|
| (racine) | site_name, site_description, domain, contact | Oui (sauf contact) |
[business] | name, country, languages, currency, timezone | Non |
[brand_voice] | tone, greeting, escalation_message | No |
[[products]] | sku, name, price, inventory, tags, description | No |
[[capabilities]] | name, description, endpoint, method, access_level | Yes |
[[policies]] | name, description, policy_type | Yes |
[channels] | whatsapp, email, website, sms | Non |
[payments] | providers, auto_approve_threshold, require_approval_threshold | Non |
[support] | escalation_contacts, hours, sla | Non |
[content] | topics, auto_draft, publish_delay | Non |
[reviews] | platforms, auto_respond_threshold | Non |
[outreach] | follow_up_delay, require_consent | Non |
Toutes les sections étendues sont facultatives — les fichiers business.toml minimaux existants continuent de fonctionner.
Rechargement à chaud
Le BusinessContextLoader prend en charge le rechargement à chaud via ArcSwap :
let loader = BusinessContextLoader::from_file("business.toml".as_ref())?;
loader.watch("business.toml".into()).await?;
// Changes to business.toml are picked up automatically every 5 seconds
Exécution de l’exemple
Un exemple complet d’agent AWP est inclus :
cd examples/awp_agent
cp .env.example .env # add your GOOGLE_API_KEY
cargo run
L’exemple :
- Charge
business.tomlavec les produits, les politiques et la voix de la marque - Crée un agent LLM avec des instructions dérivées du contexte métier
- Installe une diffusion A2A authentifiée vers cet agent
- Monte les routes de gestion derrière un identifiant de démonstration séparé
- Parcourt chaque point de terminaison et affiche la vérification du protocole
Bonnes pratiques
- Commencez avec un
business.tomlminimal — seulssite_name,site_description,domain, les capacités et les politiques sont requis - Appliquez l’autorisation des capacités —
access_levelest une métadonnée de manifeste ; le gestionnaire d’application doit l’appliquer - Activez le rechargement à chaud en production — appelez
loader.watch()pour des mises à jour de configuration sans interruption - Implémentez un
TrustLevelAssignerpersonnalisé — la valeur par défaut n’attribue intentionnellement queAnonymous - Authentifiez les routes de gestion — n’exposez jamais l’abonnement CRUD depuis un routeur non protégé
- Installez une diffusion A2A réelle — le comportement par défaut fail-closed renvoie
503 - Utilisez une livraison d’événements durable — implémentez la politique de destination, la mise en file d’attente et des tentatives bornées
- Vérifiez les signatures webhook — validez
X-AWP-Signaturesur les webhooks entrants
Liés
- A2A Protocol — Communication agent-à-agent (complémentaire à AWP)
- Déploiement serveur — Exécution d’agents en tant que serveurs HTTP
- Contrôle d’accès — Permissions basées sur les rôles
Précédent : ← A2A Protocol | Suivant : Évaluation →