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
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
| Crate | Propósito | Dependencias |
|---|---|---|
awp-types | Tipos de protocolo (enum, structs, errores) | Sin dependencias de adk-* — solo serde, uuid, chrono, thiserror |
adk-awp | Rutas, middleware, interfaces de servicio e implementaciones en memoria | awp-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étodo | Ruta | Descripción |
|---|---|---|
| GET | /.well-known/awp.json | Documento de descubrimiento — punto de entrada para agentes |
| GET | /awp/manifest | JSON-LD manifiesto de capacidades |
| GET | /awp/health | Estado de salud (Healthy/Degrading/Degraded) |
| POST | /awp/a2a | Dispatch 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étodo | Ruta | Descripción |
|---|---|---|
| POST | /awp/events/subscribe | Crear 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:
| Nivel | Discriminante | Cómo se asigna |
|---|---|---|
Anonymous | 0 | Sin credenciales |
Known | 1 | Clave válida API o JWT |
Partner | 2 | JWT con ámbito partner |
Internal | 3 | JWT 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 confianza | Límite predeterminado |
|---|---|
| Anónimo | 30 solicitudes/minuto |
| Conocido | 120 solicitudes/minuto |
| Socio | 600 solicitudes/minuto |
| Interno | Ilimitado |
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.
Almacenamiento de consentimiento
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:
- Cabecera
X-AWP-Channel: agent→ Agente (anulación explícita) Accept: application/json+ patrón User-Agent de agente → Agente- 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:
| Tipo | Descripción |
|---|---|
VisitorIntentSignal | Intención de compra o de servicio |
ContentGapSignal | Se detectó contenido faltante o desactualizado |
PaymentIntent | Mensaje del ciclo de vida del pago |
SupportEscalation | Escalación al soporte humano |
ReviewSignal | Revisión o comentarios de una plataforma |
OperationsProposal | Inventario, propuesta de programación |
InvokeCapability | Invocar una capacidad declarada |
RenderUi | Solicitar renderizado de la UI |
OutboundTrigger | Mensaje 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ón | Campos | Requerido |
|---|---|---|
| (raíz) | site_name, site_description, domain, contact | Sí (excepto contacto) |
[business] | name, country, languages, currency, timezone | No |
[brand_voice] | tone, greeting, escalation_message | No |
[[products]] | sku, name, price, inventory, tags, description | No |
[[capabilities]] | name, description, endpoint, method, access_level | Sí |
[[policies]] | name, description, policy_type | Sí |
[channels] | whatsapp, email, website, sms | No |
[payments] | providers, auto_approve_threshold, require_approval_threshold | No |
[support] | escalation_contacts, hours, sla | No |
[content] | topics, auto_draft, publish_delay | No |
[reviews] | platforms, auto_respond_threshold | No |
[outreach] | follow_up_delay, require_consent | No |
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:
- Carga
business.tomlcon productos, políticas y voz de marca - Crea un agente LLM con instrucciones derivadas del contexto empresarial
- Instala envío autenticado de A2A a ese agente
- Monta rutas de gestión detrás de una credencial de demostración separada
- Ejecuta cada endpoint e imprime la verificación del protocolo
Mejores prácticas
- Empieza con un
business.tomlminimal — solo se requierensite_name,site_description,domain, capacidades y políticas - Impone la autorización de capacidades —
access_leveles metadatos del manifiesto; el controlador de la aplicación debe imponerla - Activa la recarga en caliente en producción — llama a
loader.watch()para actualizaciones de configuración sin tiempo de inactividad - Implementa
TrustLevelAssignerpersonalizado — el valor predeterminado asigna intencionalmente soloAnonymous - Autentica las rutas de gestión — nunca expongas la suscripción CRUD desde un enrutador sin protección
- Instala un envío real de A2A — el valor predeterminado fail-closed devuelve
503 - Usa entrega duradera de eventos — implementa política de destino, encolado y reintentos acotados
- Verifica las firmas de webhook — valida
X-AWP-Signatureen los webhooks entrantes
Relacionado
- Protocolo A2A — Comunicación de agente a agente (complementaria a AWP)
- Despliegue del servidor — Ejecutar agentes como servidores HTTP
- Control de acceso — Permisos basados en roles
Anterior: ← Protocolo A2A | Siguiente: Evaluación →