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

Rendering architecture…

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

CrateObjectifDépendances
awp-typesTypes de protocole (énums, structs, erreurs)Zéro dépendance adk-* — seulement serde, uuid, chrono, thiserror
adk-awpRoutes, middleware, interfaces de service et implémentations en mémoireawp-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éthodeCheminDescription
GET/.well-known/awp.jsonDocument de découverte — point d’entrée pour les agents
GET/awp/manifestJSON-LD manifeste de capacité
GET/awp/healthÉtat de santé (Sain/Se dégradant/Dégradé)
POST/awp/a2aDispatch 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éthodeCheminDescription
POST/awp/events/subscribeCréer un abonnement de webhook
GET/awp/events/subscriptionsLister 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 :

NiveauDiscriminantMode d’attribution
Anonymous0Aucune information d'identification
Known1Clé API valide ou JWT
Partner2JWT avec portée partner
Internal3JWT 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 confianceLimite par défaut
Anonyme30 requêtes/minute
Connu120 requêtes/minute
Partenaire600 requêtes/minute
InterneIllimité

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.

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 :

  1. En-tête X-AWP-Channel: agent → Agent (remplacement explicite)
  2. Accept: application/json + modèle User-Agent d’agent → Agent
  3. 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égorieFonctionnement
VisitorIntentSignalIntention d’achat ou de service
ContentGapSignalContenu manquant ou obsolète détecté
PaymentIntentMessage du cycle de vie du paiement
SupportEscalationEscalade vers le support humain
ReviewSignalRevue ou retour d’une plateforme
OperationsProposalInventaire, proposition de planification
InvokeCapabilityInvoquer une capacité déclarée
RenderUiDemander le rendu de l’interface utilisateur
OutboundTriggerMessage 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 :

SectionChampsRequis
(racine)site_name, site_description, domain, contactOui (sauf contact)
[business]name, country, languages, currency, timezoneNon
[brand_voice]tone, greeting, escalation_messageNo
[[products]]sku, name, price, inventory, tags, descriptionNo
[[capabilities]]name, description, endpoint, method, access_levelYes
[[policies]]name, description, policy_typeYes
[channels]whatsapp, email, website, smsNon
[payments]providers, auto_approve_threshold, require_approval_thresholdNon
[support]escalation_contacts, hours, slaNon
[content]topics, auto_draft, publish_delayNon
[reviews]platforms, auto_respond_thresholdNon
[outreach]follow_up_delay, require_consentNon

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 :

  1. Charge business.toml avec les produits, les politiques et la voix de la marque
  2. Crée un agent LLM avec des instructions dérivées du contexte métier
  3. Installe une diffusion A2A authentifiée vers cet agent
  4. Monte les routes de gestion derrière un identifiant de démonstration séparé
  5. Parcourt chaque point de terminaison et affiche la vérification du protocole

Bonnes pratiques

  1. Commencez avec un business.toml minimal — seuls site_name, site_description, domain, les capacités et les politiques sont requis
  2. Appliquez l’autorisation des capacitésaccess_level est une métadonnée de manifeste ; le gestionnaire d’application doit l’appliquer
  3. Activez le rechargement à chaud en production — appelez loader.watch() pour des mises à jour de configuration sans interruption
  4. Implémentez un TrustLevelAssigner personnalisé — la valeur par défaut n’attribue intentionnellement que Anonymous
  5. Authentifiez les routes de gestion — n’exposez jamais l’abonnement CRUD depuis un routeur non protégé
  6. Installez une diffusion A2A réelle — le comportement par défaut fail-closed renvoie 503
  7. Utilisez une livraison d’événements durable — implémentez la politique de destination, la mise en file d’attente et des tentatives bornées
  8. Vérifiez les signatures webhook — validez X-AWP-Signature sur les webhooks entrants

Précédent : ← A2A Protocol | Suivant : Évaluation →