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 请求流
应用布局
┌─────────────────────────────────────────────────┐
│ 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-* 依赖 — 仅 serde、uuid、chrono、thiserror |
adk-awp | 路由、中间件、服务接口以及内存中的实现 | awp-types、adk-core、axum 0.8、tokio、dashmap |
拆分意味着任何 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/manifest | JSON-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 使用四个信任级别,访问权限逐级增加:
| 级别 | 判别值 | 如何分配 |
|---|---|---|
Anonymous | 0 | 无凭据 |
Known | 1 | 有效的 API 密钥或 JWT |
Partner | 2 | JWT 具有 partner 作用域 |
Internal | 3 | JWT 具有 internal 作用域 |
信任级别是有序的:Anonymous < Known < Partner < Internal。business.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
}
}
}
在分配 Partner 或 Internal 时,请使用与应用其余部分相同的已验证身份和作用域来源。
限流
内置的 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 代理:
X-AWP-Channel: agent头 → Agent(显式覆盖)Accept: application/json+ agent User-Agent 模式 → Agent- 否则 → Human
Agent User-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
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
该示例:
- 加载包含产品、策略和品牌语调的
business.toml - 使用来自业务上下文的指令创建一个 LLM agent
- 为该 agent 安装经过认证的 A2A 派发
- 在单独的演示凭证后挂载管理路由
- 运行每个端点并打印协议验证
最佳实践
- 从最小的
business.toml开始 — 只需要site_name、site_description、domain、capabilities 和 policies - 强制执行 capability 授权 —
access_level是清单元数据;应用处理器必须强制执行它 - 在生产环境中启用热重载 — 调用
loader.watch()以实现零停机配置更新 - 实现自定义
TrustLevelAssigner— 默认只会有意分配Anonymous - 对管理路由进行认证 — 绝不要从未受保护的路由器暴露订阅 CRUD
- 安装真实的 A2A 派发 — 失败即关闭的默认行为会返回
503 - 使用持久化事件投递 — 实现目标策略、队列和有界重试
- 验证 webhook 签名 — 对传入的 webhook 验证
X-AWP-Signature
相关内容
- A2A Protocol — Agent 到 Agent 的通信(与 AWP 互补)
- Server Deployment — 将 agents 作为 HTTP 服务器运行
- Access Control — 基于角色的权限
上一页: ← A2A Protocol | 下一页: Evaluation →