エージェント型 Web プロトコル (AWP)

ADK-Rust は、Web サイトやサービスを AI エージェントから利用可能にするための Agentic Web Protocol (AWP) の型と Axum 統合を提供します。実装は 2 つのクレートにまたがっています: awp-types(純粋なプロトコル型)と adk-awp(ルート、ミドルウェア、サービスインターフェース)です。アプリケーションは、エージェントのディスパッチ、認証、認可、そして永続的な webhook 配信を提供します。

概要

AWP は、任意の Web サイトがその機能、ポリシー、ビジネスコンテキストを機械可読な形式で宣言できるようにします。AI エージェントはこれらの機能を発見し、プロトコルバージョンを交渉し、イベントを購読し、型付きの A2A メッセージを通じてやり取りできます。adk-awp は、その HTTP 境界でボディ制限とレート制限を強制します。アプリケーションハンドラーは、ID と機能の認可を強制します。

AWP を使う場合:

  • AI エージェントにサービスをプログラム的に発見・操作してほしい
  • 信頼レベルのメタデータと、アプリケーションによるアクセス制御のためのフックが必要
  • 同じエンドポイントで人間の訪問者と AI エージェントの両方にサービス प्रदानしたい
  • イベント購読と 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)  │            │
│    └────────────────────────────────┘            │
└─────────────────────────────────────────────────┘

クレート

クレート目的依存関係
awp-typesプロトコル型(enum、struct、error)依存関係はゼロ、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?;

これにより、バージョンネゴシエーション、レート 制限、および 64 KiB A2A の本文制限付きで、4 つの公開 AWP エンドポイントが登録されます。AwpA2aHandler がない場合、 POST /awp/a2a503 を返し、ディスパッチされていない作業を決して受理しません。ConnectInfo は、匿名のレート制限バケットを分離するために使用されるピアアドレスを提供します。これがない場合、未知の呼び出し元は意図的に 1 つのバケットを共有します。

公開 AWP エンドポイント

メソッドパス説明
GET/.well-known/awp.jsonディスカバリードキュメント — エージェントのエントリーポイント
GET/awp/manifestJSON-LD 機能マニフェスト
GET/awp/healthヘルス状態(Healthy/Degrading/Degraded)
POST/awp/a2aアプリケーション提供のA2Aディスパッチ

認証済み管理エンドポイント

awp_management_routes() は、サブスクリプション管理を個別に、かつ 認証レイヤーなしで返します。これをマージする前に、アプリケーションの認証ミドルウェアを適用してください:

メソッドパス説明
POST/awp/events/subscribewebhookサブスクリプションを作成する
GET/awp/events/subscriptionsすべてのサブスクリプションを一覧表示する
DELETE/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 は、アクセス権が段階的に増える 4 つの信頼レベルを使用します。

レベル判別子割り当て方法
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
        }
    }
}

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 は行いません。本番アプリケーションでは、宛先検証、永続的なキュー、上限付き再試行、および独自の HTTP クライアントを用いて EventSubscriptionService を実装します。

HTTP の配信実装は、HMAC-SHA256 署名を含む X-AWP-Signature ヘッダーを運ぶことができます。

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 + エージェントの User-Agent パターン → Agent
  3. それ以外 → Human

エージェントの 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 はエージェントルーティングのための型付きメッセージカテゴリを定義します。

種類説明
VisitorIntentSignal購入またはサービスの意図
ContentGapSignal欠落または古いコンテンツが検出されました
PaymentIntent支払いライフサイクルメッセージ
SupportEscalation人間のサポートへのエスカレーション
ReviewSignalプラットフォームからのレビューまたはフィードバック
OperationsProposal在庫、スケジューリングの提案
InvokeCapability宣言された機能を呼び出す
RenderUiUI レンダリングを要求する
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}),
};

Payment Intents

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 Schema

完全なスキーマは、豊富なビジネス設定をサポートします:

セクションフィールド必須
(root)site_name, site_description, domain, 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ファイルは引き続き動作します。

ホットリロード

BusinessContextLoaderArcSwap を介したホットリロードをサポートしています:

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 エージェントの例が含まれています:

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

この例では:

  1. 製品、ポリシー、ブランドボイスを含む business.toml を読み込みます
  2. ビジネスコンテキストから導出された指示を持つ LLM エージェントを作成します
  3. そのエージェントに認証済みの A2A ディスパッチをインストールします
  4. 別のデモ用認証情報の背後に管理ルートをマウントします
  5. すべてのエンドポイントを実行し、プロトコル検証を出力します

ベストプラクティス

  1. 最小限の business.toml から始める — 必要なのは site_namesite_descriptiondomain、機能、ポリシーのみです
  2. 機能の認可を強制するaccess_level はマニフェストのメタデータです。アプリケーションハンドラーがこれを強制する必要があります
  3. 本番環境でホットリロードを有効にする — ゼロダウンタイムの設定更新のために loader.watch() を呼び出します
  4. カスタムの TrustLevelAssigner を実装する — デフォルトは意図的に Anonymous のみを割り当てます
  5. 管理ルートを認証する — 保護されていないルーターからサブスクリプション CRUD を決して公開しないでください
  6. 実際の A2A ディスパッチをインストールする — フェイルクローズのデフォルトは 503 を返します
  7. 耐久性のあるイベント配信を使用する — 宛先ポリシー、キューイング、制限付き再試行を実装します
  8. Webhook署名を検証する — 受信Webhookで X-AWP-Signature を検証します

前へ: ← A2A Protocol | 次へ: Evaluation →