Protocolo Web Agéntico (AWP)

ADK-Rust proporciona Agentic Web Protocol (AWP) tipos e integración con Axum para hacer que los sitios web y servicios sean accesibles para agentes de IA. La implementación abarca dos crates: awp-types (tipos de protocolo puros) y adk-awp (rutas, middleware e interfaces de servicio). Las aplicaciones proporcionan despacho de agentes, autenticación, autorización y entrega duradera de webhooks.

Resumen

AWP permite que cualquier sitio web declare sus capacidades, políticas y contexto de negocio en un formato legible por máquinas. Los agentes de IA pueden descubrir estas capacidades, negociar versiones del protocolo, suscribirse a eventos e interactuar mediante mensajes tipados de A2A. adk-awp aplica límites de cuerpo y de tasa en su límite de HTTP; los controladores de la aplicación aplican la identidad y la autorización de capacidades.

Usa AWP cuando:

  • Quieres que los agentes de IA descubran e interactúen con tu servicio de forma programática
  • Necesitas metadatos de nivel de confianza y un gancho para el control de acceso aplicado por la aplicación
  • Quieres servir tanto a visitantes humanos como a agentes de IA desde los mismos endpoints
  • Necesitas suscripciones a eventos y primitivas de firma HMAC-SHA256
  • Quieres una máquina de estados de salud para la supervisión del servicio

Arquitectura

Flujo de solicitudes de AWP

Rendering architecture…

Diseño de la aplicación

┌─────────────────────────────────────────────────┐
│                  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

CratePropósitoDependencias
awp-typesTipos de protocolo (enum, structs, errores)Sin dependencias de adk-* — solo serde, uuid, chrono, thiserror
adk-awpRutas, middleware, interfaces de servicio e implementaciones en memoriaawp-types, adk-core, axum 0.8, tokio, dashmap

La división significa que cualquier proyecto Rust puede depender de awp-types sin incorporar el árbol de ADK.

Inicio rápido

1. Crear 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. Cargar y servir rutas 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?;

Esto registra los cuatro puntos finales públicos de AWP con negociación de versión, limitación de tasa y un límite de cuerpo de 64 KiB A2A. Sin un AwpA2aHandler, POST /awp/a2a devuelve 503 y nunca confirma trabajo que no fue enviado. ConnectInfo proporciona la dirección del par usada para aislar cubos anónimos de limitación de tasa; sin ella, los llamadores desconocidos comparten intencionalmente un solo cubo.

Puntos finales públicos de AWP

MétodoRutaDescripción
GET/.well-known/awp.jsonDocumento de descubrimiento — punto de entrada para agentes
GET/awp/manifestJSON-LD manifiesto de capacidades
GET/awp/healthEstado de salud (Healthy/Degrading/Degraded)
POST/awp/a2aDispatch proporcionado por la aplicación de A2A

Endpoints de gestión autenticados

awp_management_routes() devuelve la gestión de suscripciones por separado y sin una capa de autenticación. Aplique el middleware de autenticación de la aplicación antes de fusionarlo:

MétodoRutaDescripción
POST/awp/events/subscribeCrear una suscripción de webhook
GET/awp/events/subscriptionsसूची todas las suscripciones
DELETE/awp/events/subscriptions/{id}Eliminar una suscripción

Documento de descubrimiento

El documento de descubrimiento en /.well-known/awp.json se genera automáticamente a partir de tu 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"]
}

Manifiesto de capacidades

El manifiesto en /awp/manifest usa el formato 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"
    }
  ]
}

Niveles de confianza

AWP utiliza cuatro niveles de confianza con acceso creciente:

NivelDiscriminanteCómo se asigna
Anonymous0Sin credenciales
Known1Clave válida API o JWT
Partner2JWT con ámbito partner
Internal3JWT con ámbito internal

Los niveles de confianza están ordenados: Anonymous < Known < Partner < Internal. Cada capacidad en business.toml declara su access_level mínimo.

DefaultTrustAssigner clasifica cada solicitud como Anonymous. Un encabezado bearer o clave de API no se considera confiable hasta que un verificador de la aplicación lo valida. Por lo tanto, niveles de confianza más altos requieren un asignador personalizado.

Configure .supported_trust_levels(...) junto con ese asignador para que el descubrimiento anuncie solo los niveles que la implementación pueda verificar.

Asignación personalizada de confianza

Implemente el trait TrustLevelAssigner para lógica personalizada:

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
        }
    }
}

Use la misma identidad verificada y la misma fuente de alcance que el resto de la aplicación al asignar Partner o Internal.

Limitación de tasa

El InMemoryRateLimiter integrado usa un algoritmo de ventana deslizante con límites por nivel de confianza:

Nivel de confianzaLímite predeterminado
Anónimo30 solicitudes/minuto
Conocido120 solicitudes/minuto
Socio600 solicitudes/minuto
InternoIlimitado

Las solicitudes rechazadas reciben HTTP 429 con una cabecera Retry-After.

Límites personalizados

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);

Negociación de versiones

Todas las rutas AWP incluyen middleware de negociación de versiones:

  • Los clientes envían la cabecera AWP-Version: 1.1 (opcional — por defecto usa la versión actual)
  • El servidor comprueba la compatibilidad de la versión mayor
  • Las solicitudes compatibles continúan; las incompatibles reciben HTTP 406
  • Los valores de versión mal formados reciben HTTP 400
  • La respuesta incluye la cabecera AWP-Version: 1.0

Suscripciones a eventos

La gestión de suscripciones es una superficie privilegiada. Monta awp_management_routes() detrás de autenticación antes de aceptar estas solicitudes. La devolución de llamada URLs debe ser absoluta HTTPS URLs y los secretos de firma deben contener al menos 32 bytes:

# 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 firma y registra las entregas coincidentes pero no realiza ninguna E/S de red. Las aplicaciones de producción implementan EventSubscriptionService con validación de destino, una cola duradera, reintentos limitados y su cliente HTTP.

Una implementación de entrega HTTP puede llevar una cabecera X-AWP-Signature con la firma HMAC-SHA256:

X-AWP-Signature: sha256=<hex_digest>

Verifica las firmas con adk_awp::verify_signature(payload, secret, signature).

Máquina de estados de salud

El endpoint de salud rastrea el estado del servicio con transiciones estrictamente validadas:

Healthy → Degrading → Degraded
    ↑         │           │
    └─────────┘           │
    └─────────────────────┘

Los cambios de estado emiten eventos health.changed a todos los suscriptores coincidentes.

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?;

Las transiciones inválidas (p. ej., Healthy → Degraded) devuelven un error.

AWP incluye una interfaz de almacenamiento de consentimiento. El cumplimiento normativo también requiere aviso específico de la aplicación, base legal, retención, controles de acceso y política de eliminación; seleccionar una implementación de almacenamiento no establece el cumplimiento:

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?;

Detección del tipo de solicitante

AWP detecta si una solicitud proviene de un humano o de un agente de IA:

  1. Cabecera X-AWP-Channel: agent → Agente (anulación explícita)
  2. Accept: application/json + patrón User-Agent de agente → Agente
  3. En caso contrario → Humano

Patrones User-Agent de agente: 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

Tipos de mensaje AWP

Más allá de los mensajes genéricos A2A, AWP define categorías de mensajes tipadas para el enrutamiento de agentes:

TipoDescripción
VisitorIntentSignalIntención de compra o de servicio
ContentGapSignalSe detectó contenido faltante o desactualizado
PaymentIntentMensaje del ciclo de vida del pago
SupportEscalationEscalación al soporte humano
ReviewSignalRevisión o comentarios de una plataforma
OperationsProposalInventario, propuesta de programación
InvokeCapabilityInvocar una capacidad declarada
RenderUiSolicitar renderizado de la UI
OutboundTriggerMensaje saliente proactivo
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}),
};

Intenciones de pago

AWP define un ciclo de vida de pago simplificado para pagos impulsados por la política del propietario:

Draft → PendingApproval → Approved → Executing → Settled
                                                → Rejected
                                                → Cancelled

La PaymentPolicy evalúa si se debe aprobar automáticamente o requerir la aprobación del propietario:

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)

Esquema de business.toml

El esquema completo admite una configuración empresarial rica:

SecciónCamposRequerido
(raíz)site_name, site_description, domain, contactSí (excepto contacto)
[business]name, country, languages, currency, timezoneNo
[brand_voice]tone, greeting, escalation_messageNo
[[products]]sku, name, price, inventory, tags, descriptionNo
[[capabilities]]name, description, endpoint, method, access_level
[[policies]]name, description, policy_type
[channels]whatsapp, email, website, smsNo
[payments]providers, auto_approve_threshold, require_approval_thresholdNo
[support]escalation_contacts, hours, slaNo
[content]topics, auto_draft, publish_delayNo
[reviews]platforms, auto_respond_thresholdNo
[outreach]follow_up_delay, require_consentNo

Todas las secciones extendidas son opcionales — los archivos mínimos existentes business.toml siguen funcionando.

Recarga en caliente

El BusinessContextLoader admite recarga en caliente mediante 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

Ejecutar el ejemplo

Se incluye un ejemplo completo de agente AWP:

cd examples/awp_agent
cp .env.example .env   # add your GOOGLE_API_KEY
cargo run

El ejemplo:

  1. Carga business.toml con productos, políticas y voz de marca
  2. Crea un agente LLM con instrucciones derivadas del contexto empresarial
  3. Instala envío autenticado de A2A a ese agente
  4. Monta rutas de gestión detrás de una credencial de demostración separada
  5. Ejecuta cada endpoint e imprime la verificación del protocolo

Mejores prácticas

  1. Empieza con un business.toml minimal — solo se requieren site_name, site_description, domain, capacidades y políticas
  2. Impone la autorización de capacidadesaccess_level es metadatos del manifiesto; el controlador de la aplicación debe imponerla
  3. Activa la recarga en caliente en producción — llama a loader.watch() para actualizaciones de configuración sin tiempo de inactividad
  4. Implementa TrustLevelAssigner personalizado — el valor predeterminado asigna intencionalmente solo Anonymous
  5. Autentica las rutas de gestión — nunca expongas la suscripción CRUD desde un enrutador sin protección
  6. Instala un envío real de A2A — el valor predeterminado fail-closed devuelve 503
  7. Usa entrega duradera de eventos — implementa política de destino, encolado y reintentos acotados
  8. Verifica las firmas de webhook — valida X-AWP-Signature en los webhooks entrantes

Anterior: ← Protocolo A2A | Siguiente: Evaluación →

Protocolo Web Agéntico (AWP) - Documentación ADK-Rust | ADK-Rust