Protocolo Web Agentic (AWP)

ADK-Rust fornece tipos do Agentic Web Protocol (AWP) e integração com Axum para tornar sites e serviços acessíveis a agentes de IA. A implementação abrange dois crates: awp-types (tipos de protocolo puros) e adk-awp (rotas, middleware e interfaces de serviço). As aplicações fornecem despacho de agentes, autenticação, autorização e entrega durável de webhooks.

Visão geral

AWP permite que qualquer site declare suas capacidades, políticas e contexto de negócios em um formato legível por máquina. Agentes de IA podem descobrir essas capacidades, negociar versões do protocolo, assinar eventos e interagir por meio de mensagens tipadas de A2A. adk-awp impõe limites de corpo e de taxa em sua fronteira de HTTP; os manipuladores da aplicação impõem autorização de identidade e de capacidade.

Use AWP quando:

  • Você quiser que agentes de IA descubram e interajam com seu serviço programaticamente
  • Você precisar de metadados de nível de confiança e de um ponto de integração para controle de acesso imposto pela aplicação
  • Você quiser atender visitantes humanos e agentes de IA pelos mesmos endpoints
  • Você precisar de assinaturas de eventos e primitivas de assinatura HMAC-SHA256
  • Você quiser uma máquina de estados de saúde para monitoramento de serviço

Arquitetura

Fluxo de requisição de AWP

Rendering architecture…

Layout da aplicação

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

CrateFinalidadeDependências
awp-typesTipos de protocolo (enums, structs, erros)Zero dependências de adk-* — apenas serde, uuid, chrono, thiserror
adk-awpRotas, middleware, interfaces de serviço e implementações em memóriaawp-types, adk-core, axum 0.8, tokio, dashmap

A divisão significa que qualquer projeto Rust pode depender de awp-types sem puxar a árvore ADK.

Início rápido

1. Crie um 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. Carregue e sirva as rotas 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?;

Isso registra os quatro endpoints públicos AWP com negociação de versão, limitação de taxa e um limite de corpo de 64 KiB A2A. Sem um AwpA2aHandler, POST /awp/a2a retorna 503 e nunca reconhece trabalho que não foi disparado. ConnectInfo fornece o endereço do par usado para isolar buckets anônimos de limitação de taxa; sem ele, chamadores desconhecidos compartilham intencionalmente um único bucket.

Endpoints públicos AWP

MétodoCaminhoDescrição
GET/.well-known/awp.jsonDocumento de descoberta — ponto de entrada para agentes
GET/awp/manifestmanifesto de capacidade JSON-LD
GET/awp/healthEstado de saúde (Saudável/Em degradação/Degradado)
POST/awp/a2aDispatch fornecido pela aplicação de A2A

Endpoints de gerenciamento autenticados

awp_management_routes() retorna o gerenciamento de assinatura separadamente e sem uma camada de autenticação. Aplique o middleware de autenticação do aplicativo antes de mesclá-lo:

MétodoCaminhoDescrição
POST/awp/events/subscribeCriar uma assinatura de webhook
GET/awp/events/subscriptionsListar todas as assinaturas
DELETE/awp/events/subscriptions/{id}Excluir uma assinatura

Documento de descoberta

O documento de descoberta em /.well-known/awp.json é gerado automaticamente a partir do seu 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"]
}

Manifesto de capacidades

O manifesto em /awp/manifest usa o 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"
    }
  ]
}

Níveis de confiança

AWP usa quatro níveis de confiança com acesso crescente:

NívelDiscriminanteComo é atribuído
Anonymous0Sem credenciais
Known1Chave API válida ou JWT
Partner2JWT com escopo partner
Internal3JWT com escopo internal

Os níveis de confiança são ordenados: Anonymous < Known < Partner < Internal. Cada capability em business.toml declara seu access_level mínimo.

DefaultTrustAssigner classifica cada request como Anonymous. Um cabeçalho de portador ou API key não é confiável até que um verificador da aplicação o valide. Níveis de confiança mais altos, portanto, exigem um atribuidor personalizado.

Configure .supported_trust_levels(...) junto com esse atribuidor para que a descoberta anuncie apenas os níveis que a implantação pode verificar.

Atribuição Personalizada de Confiança

Implemente a 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 a mesma identidade verificada e a mesma fonte de escopo que o restante da aplicação ao atribuir Partner ou Internal.

Limitação de Taxa

O InMemoryRateLimiter integrado usa um algoritmo de janela deslizante com limites por nível de confiança:

Nível de ConfiançaLimite Padrão
Anônimo30 requisições/minuto
Conhecido120 requisições/minuto
Parceiro600 solicitações/minuto
InternoIlimitado

Requests rejeitadas recebem HTTP 429 com um cabeçalho Retry-After.

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

Negociação de Versão

Todos os caminhos AWP incluem middleware de negociação de versão:

  • Os clientes enviam o cabeçalho AWP-Version: 1.1 (opcional — por padrão, usa a versão atual)
  • O servidor verifica a compatibilidade da versão principal
  • Requests compatíveis prosseguem; requests incompatíveis recebem HTTP 406
  • Valores de versão malformados recebem HTTP 400
  • A resposta inclui o cabeçalho AWP-Version: 1.0

Assinaturas de Eventos

O gerenciamento de assinaturas é uma superfície privilegiada. Monte awp_management_routes() atrás de autenticação antes de aceitar esses requests. O callback URLs deve ser um HTTPS URLs absoluto e os segredos de assinatura devem conter pelo 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 assina e registra as entregas correspondentes, mas não realiza I/O de rede. Aplicações em produção implementam EventSubscriptionService com validação de destino, uma fila durável, tentativas limitadas e seu cliente HTTP.

Uma implementação de entrega HTTP pode carregar um cabeçalho X-AWP-Signature com a assinatura HMAC-SHA256:

X-AWP-Signature: sha256=<hex_digest>

Verifique as assinaturas com adk_awp::verify_signature(payload, secret, signature).

Máquina de Estados de Saúde

O endpoint de saúde acompanha o estado do serviço com transições estritamente validadas:

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

As mudanças de estado emitem eventos health.changed para todos os assinantes correspondentes.

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

Transições inválidas (por exemplo, Healthy → Degraded) retornam um erro.

AWP inclui uma interface de armazenamento de consentimento. A conformidade regulatória também exige aviso específico da aplicação, base legal, retenção, controles de acesso e política de exclusão; selecionar uma implementação de armazenamento não estabelece conformidade:

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

Detecção do Tipo de Solicitante

AWP detecta se uma request vem de um humano ou de um agente de IA:

  1. Cabeçalho X-AWP-Channel: agent → Agent (substituição explícita)
  2. Accept: application/json + padrão de User-Agent de agent → Agent
  3. Caso contrário → Human

Padrões de User-Agent de 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

Tipos de Mensagem AWP

Além das mensagens genéricas A2A, AWP define categorias de mensagens tipadas para roteamento de agent:

TipoDescrição
VisitorIntentSignalIntenção de compra ou serviço
ContentGapSignalConteúdo ausente ou desatualizado detectado
PaymentIntentMensagem do ciclo de pagamento
SupportEscalationEscalonamento para suporte humano
ReviewSignalAvaliação ou feedback de uma plataforma
OperationsProposalInventário, proposta de agendamento
InvokeCapabilityInvocar uma capacidade declarada
RenderUiSolicitar renderização de UI
OutboundTriggerMensagem de saída proativa
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}),
};

Intenções de Pagamento

AWP define um ciclo de vida simplificado de pagamento para pagamentos orientados por política do proprietário:

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

A PaymentPolicy avalia se deve aprovar automaticamente ou exigir aprovação do proprietário:

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

O esquema completo oferece suporte a uma rica configuração de negócios:

SeçãoCamposObrigatório
(raiz)site_name, site_description, domain, contactSim (exceto contato)
[business]name, country, languages, currency, timezoneNão
[brand_voice]tone, greeting, escalation_messageNão
[[products]]sku, name, price, inventory, tags, descriptionNão
[[capabilities]]name, description, endpoint, method, access_levelSim
[[policies]]name, description, policy_typeSim
[channels]whatsapp, email, website, smsNão
[payments]providers, auto_approve_threshold, require_approval_thresholdNão
[support]escalation_contacts, hours, slaNão
[content]topics, auto_draft, publish_delayNão
[reviews]platforms, auto_respond_thresholdNão
[outreach]follow_up_delay, require_consentNão

Todas as seções estendidas são opcionais — arquivos business.toml mínimos existentes continuam funcionando.

Recarregamento a quente

O BusinessContextLoader suporta recarregamento a quente 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

Executando o Exemplo

Um exemplo completo de agente AWP está incluído:

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

O exemplo:

  1. Carrega business.toml com produtos, políticas e voz da marca
  2. Cria um agente LLM com instruções derivadas do contexto de negócios
  3. Instala despacho autenticado A2A para esse agente
  4. Monta rotas de gerenciamento atrás de uma credencial de demonstração separada
  5. Exercita cada endpoint e imprime a verificação do protocolo

Boas Práticas

  1. Comece com um business.toml mínimo — apenas site_name, site_description, domain, capacidades e políticas são necessários
  2. Imponha autorização de capacidadeaccess_level é metadado do manifesto; o manipulador da aplicação deve impô-la
  3. Habilite o recarregamento a quente em produção — chame loader.watch() para atualizações de configuração sem tempo de inatividade
  4. Implemente TrustLevelAssigner personalizado — o padrão, intencionalmente, atribui apenas Anonymous
  5. Autentique rotas de gerenciamento — nunca exponha assinaturas CRUD a partir de um router desprotegido
  6. Instale despacho A2A real — o padrão fail-closed retorna 503
  7. Use entrega durável de eventos — implemente política de destino, enfileiramento e tentativas limitadas
  8. Verifique assinaturas de webhook — valide X-AWP-Signature em webhooks de entrada

Anterior: ← Protocolo A2A | Próximo: Avaliação →