Agentic Web Protocol (AWP)

ADK-Rust 提供 Agentic Web Protocol (AWP) 类型以及 Axum 集成,用于让网站和服务对 AI agent 可访问。该实现分布在两个 crate 中:awp-types(纯协议类型)和 adk-awp(路由、中间件和服务接口)。应用负责提供 agent 分发、认证、授权以及持久化 webhook 投递。

概览

AWP 使任何网站都能以机器可读的格式声明其能力、策略和业务上下文。AI agent 可以发现这些能力、协商协议版本、订阅事件,并通过类型化的 A2A 消息进行交互。adk-awp 在其 HTTP 边界强制执行请求体和速率限制;应用处理器则强制执行身份和能力授权。

在以下情况下使用 AWP:

  • 你希望 AI agent 以编程方式发现并与你的服务交互
  • 你需要信任级别元数据以及应用强制访问控制的挂钩
  • 你希望从相同的端点同时为人类访客和 AI agent 提供服务
  • 你需要事件订阅和 HMAC-SHA256 签名原语
  • 你希望为服务监控提供健康状态机

架构

AWP 请求流

Rendering architecture…

应用布局

┌─────────────────────────────────────────────────┐
│                  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作用依赖
awp-types协议类型(枚举、结构体、错误)零个 adk-* 依赖 — 仅 serdeuuidchronothiserror
adk-awp路由、中间件、服务接口以及内存中的实现awp-typesadk-coreaxum 0.8、tokiodashmap

拆分意味着任何 Rust 项目都可以依赖 awp-types,而无需拉入 ADK 树。

快速开始

1. 创建一个 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. 加载并提供 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?;

这会为四个公开的 AWP 端点注册版本协商、速率限制,以及 64 KiB A2A 的主体限制。没有 AwpA2aHandler 时,POST /awp/a2a 会返回 503,并且绝不会确认未被派发的工作。ConnectInfo 提供用于隔离匿名限流桶的对端地址;如果没有它,未知调用方会有意共享同一个桶。

公开的 AWP 端点

方法路径描述
GET/.well-known/awp.json发现文档 — agent 的入口点
GET/awp/manifestJSON-LD 能力清单
GET/awp/health健康状态(Healthy/Degrading/Degraded)
POST/awp/a2a应用提供的 A2A 分发

认证管理端点

awp_management_routes() 会单独返回订阅管理内容,且不包含认证层。请在合并之前先应用应用程序的认证中间件:

方法路径描述
POST/awp/events/subscribe创建一个 webhook 订阅
GET/awp/events/subscriptions列出所有订阅
删除/awp/events/subscriptions/{id}删除一个订阅

发现文档

位于 /.well-known/awp.json 的发现文档是从你的 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"]
}

能力清单

位于 /awp/manifest 的清单使用 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"
    }
  ]
}

信任级别

AWP 使用四个信任级别,访问权限逐级增加:

级别判别值如何分配
Anonymous0无凭据
Known1有效的 API 密钥或 JWT
Partner2JWT 具有 partner 作用域
Internal3JWT 具有 internal 作用域

信任级别是有序的:Anonymous < Known < Partner < Internalbusiness.toml 中的每个能力都会声明其最低 access_level

DefaultTrustAssigner 会将每个请求分类为 Anonymous。Bearer 或 API 密钥头在应用验证器验证之前都不被信任。因此,更高的 信任级别需要自定义分配器。

请将 .supported_trust_levels(...) 与该分配器一同配置,以便发现机制 仅公布部署能够验证的级别。

自定义信任分配

为自定义逻辑实现 TrustLevelAssigner trait:

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

在分配 PartnerInternal 时,请使用与应用其余部分相同的已验证身份和作用域来源。

限流

内置的 InMemoryRateLimiter 使用带有按信任级别限制的滑动窗口算法:

信任级别默认限制
匿名30 请求/分钟
已知120 请求/分钟
合作伙伴每分钟 600 个请求
内部无限制

被拒绝的请求返回 HTTP 429,并带有一个 Retry-After 头。

自定义限制

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

版本协商

所有 AWP 路由都包含版本协商中间件:

  • 客户端发送 AWP-Version: 1.1 头(可选——默认为当前版本)
  • 服务器检查主版本兼容性
  • 兼容的请求继续;不兼容的请求返回 HTTP 406
  • 格式错误的版本值返回 HTTP 400
  • 响应包含 AWP-Version: 1.0

事件订阅

订阅管理是一个特权区域。请在接受这些请求之前,将 awp_management_routes() 挂载在身份验证之后。回调 URLs 必须是绝对的 HTTPS URLs,并且签名密钥必须至少包含 32 字节:

# 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 会对匹配的投递进行签名并记录日志,但不会执行任何网络 I/O。生产应用应使用 EventSubscriptionService,并配备目标验证、持久化队列、受限重试以及其 HTTP 客户端。

一种 HTTP 投递实现可以携带一个 X-AWP-Signature 头,其中包含 HMAC-SHA256 签名:

X-AWP-Signature: sha256=<hex_digest>

使用 adk_awp::verify_signature(payload, secret, signature) 验证签名。

健康状态机

健康端点通过严格验证的转换跟踪服务状态:

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

状态更改会向所有匹配的订阅者发送 health.changed 事件。

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

无效转换(例如,Healthy → Degraded)会返回错误。

AWP 包含一个同意存储接口。法规合规还要求应用特定的通知、合法依据、保留、访问控制和删除策略;选择某种存储实现并不构成合规:

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

请求方类型检测

AWP 会检测请求来自人类还是 AI 代理:

  1. X-AWP-Channel: agent 头 → Agent(显式覆盖)
  2. Accept: application/json + agent User-Agent 模式 → Agent
  3. 否则 → Human

Agent User-Agent 模式:botcrawlerspideragentgptclaudegeminiperplexityanthropicopenai

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

AWP 消息类型

除了通用的 A2A 消息之外,AWP 还为 agent 路由定义了带类型的消息类别:

类型描述
VisitorIntentSignal购买或服务意图
ContentGapSignal检测到内容缺失或过时
PaymentIntent付款生命周期消息
SupportEscalation升级至人工支持
ReviewSignal来自平台的审核或反馈
OperationsProposal库存、排期提案
InvokeCapability调用已声明的能力
RenderUi请求 UI 渲染
OutboundTrigger主动外发消息
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}),
};

付款意图

AWP 为基于所有者策略驱动的付款定义了一个简化的付款生命周期:

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

PaymentPolicy 会评估是自动批准还是需要所有者批准:

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)

business.toml 架构

完整的架构支持丰富的业务配置:

部分字段必需
(root)site_name, site_description, domain, contact是(contact 除外)
[business]name, country, languages, currency, timezone
[brand_voice]tone, greeting, escalation_message
[[products]]sku, name, price, inventory, tags, description
[[capabilities]]name, description, endpoint, method, access_level
[[policies]]name, description, policy_type
[channels]whatsapp, email, website, sms
[payments]providers, auto_approve_threshold, require_approval_threshold
[support]escalation_contacts, hours, sla
[content]topics, auto_draft, publish_delay
[reviews]platforms, auto_respond_threshold
[outreach]follow_up_delay, require_consent

所有扩展部分都是可选的——现有的最小 business.toml 文件仍然可以工作。

热重载

BusinessContextLoader 通过 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

运行示例

包含一个完整的 AWP agent 示例:

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

该示例:

  1. 加载包含产品、策略和品牌语调的 business.toml
  2. 使用来自业务上下文的指令创建一个 LLM agent
  3. 为该 agent 安装经过认证的 A2A 派发
  4. 在单独的演示凭证后挂载管理路由
  5. 运行每个端点并打印协议验证

最佳实践

  1. 从最小的 business.toml 开始 — 只需要 site_namesite_descriptiondomain、capabilities 和 policies
  2. 强制执行 capability 授权access_level 是清单元数据;应用处理器必须强制执行它
  3. 在生产环境中启用热重载 — 调用 loader.watch() 以实现零停机配置更新
  4. 实现自定义 TrustLevelAssigner — 默认只会有意分配 Anonymous
  5. 对管理路由进行认证 — 绝不要从未受保护的路由器暴露订阅 CRUD
  6. 安装真实的 A2A 派发 — 失败即关闭的默认行为会返回 503
  7. 使用持久化事件投递 — 实现目标策略、队列和有界重试
  8. 验证 webhook 签名 — 对传入的 webhook 验证 X-AWP-Signature

上一页: ← A2A Protocol | 下一页: Evaluation →