Agentic Web Protocol (AWP)
ADK-Rust은 웹사이트와 서비스를 AI 에이전트가 접근할 수 있도록 만드는 Agentic Web Protocol (AWP) 타입과 Axum 통합을 제공합니다. 구현은 두 개의 crate에 걸쳐 있습니다: awp-types(순수 프로토콜 타입)과 adk-awp(라우트, 미들웨어, 서비스 인터페이스). 애플리케이션은 에이전트 디스패치, 인증, 권한 부여, 그리고 내구성 있는 웹훅 전달을 제공합니다.
개요
AWP는 모든 웹사이트가 자신의 기능, 정책, 비즈니스 컨텍스트를 기계가 읽을 수 있는 형식으로 선언할 수 있게 합니다. AI 에이전트는 이러한 기능을 발견하고, 프로토콜 버전을 협상하며, 이벤트를 구독하고, 타입이 지정된 A2A 메시지를 통해 상호작용할 수 있습니다. adk-awp는 HTTP 경계에서 본문 및 속도 제한을 강제하며, 애플리케이션 핸들러는 신원과 기능 권한 부여를 강제합니다.
다음 경우에 AWP을 사용하세요:
- AI 에이전트가 프로그램 방식으로 서비스와 상호작용하고 탐색하길 원할 때
- 신뢰 수준 메타데이터와 애플리케이션이 강제하는 접근 제어를 위한 훅이 필요할 때
- 동일한 엔드포인트에서 사람 방문자와 AI 에이전트를 모두 서비스하고 싶을 때
- 이벤트 구독과 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
| 크레이트 | 목적 | 의존성 |
|---|---|---|
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?;
이렇게 하면 버전 협상, 속도 제한, 그리고 64 KiB A2A 본문 제한이 적용된 네 개의 공개 AWP 엔드포인트가 등록됩니다. AwpA2aHandler이 없으면, POST /awp/a2a는 503를 반환하며 디스패치되지 않은 작업에 대해서는 절대 수신 확인을 하지 않습니다. ConnectInfo는 익명 속도 제한 버킷을 분리하는 데 사용되는 피어 주소를 제공합니다. 이것이 없으면 알 수 없는 호출자는 의도적으로 하나의 버킷을 공유합니다.
공개 AWP 엔드포인트
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /.well-known/awp.json | 디스커버리 문서 — 에이전트용 진입점 |
| GET | /awp/manifest | JSON-LD 기능 매니페스트 |
| GET | /awp/health | 건강 상태 (Healthy/Degrading/Degraded) |
| POST | /awp/a2a | 애플리케이션 제공 A2A 디스패치 |
인증된 관리 엔드포인트
awp_management_routes()는 구독 관리를 별도로 반환하며
인증 계층 없이 반환합니다. 이를 병합하기 전에 애플리케이션의 인증 미들웨어를 적용하세요:
| 메서드 | 경로 | 설명 |
|---|---|---|
| POST | /awp/events/subscribe | 웹훅 구독 생성 |
| 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은 접근 권한이 증가하는 네 가지 신뢰 수준을 사용합니다:
| 수준 | 식별자 | 배정 방법 |
|---|---|---|
Anonymous | 0 | 자격 증명 없음 |
Known | 1 | 유효한 API 키 또는 JWT |
Partner | 2 | JWT partner 범위 |
Internal | 3 | JWT internal 범위 |
신뢰 수준은 순서가 정해져 있습니다: Anonymous < Known < Partner < Internal. business.toml의 각 capability는 최소 access_level를 선언합니다.
DefaultTrustAssigner는 모든 요청을 Anonymous로 분류합니다. bearer 또는 API
키 헤더는 애플리케이션 verifier가 검증하기 전까지는 신뢰되지 않습니다. 따라서 더 높은
신뢰 수준은 사용자 지정 assigner를 필요로 합니다.
discovery가 배포에서 검증할 수 있는 수준만 광고하도록, 해당 assigner와 함께 .supported_trust_levels(...)를 구성하세요.
사용자 지정 Trust 할당
사용자 지정 로직을 위해 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를 할당할 때는 애플리케이션의 나머지 부분과 동일한 검증된 identity 및 scope 소스를 사용하세요.
Rate Limiting
내장된 InMemoryRateLimiter는 trust 수준별 제한을 사용하는 슬라이딩 윈도우 알고리즘을 사용합니다:
| 신뢰 수준 | 기본 제한 |
|---|---|
| 익명 | 30 requests/minute |
| 알려진 | 120 requests/minute |
| 파트너 | 분당 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 에이전트인지 감지합니다:
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는 에이전트 라우팅을 위한 유형화된 메시지 범주를 정의합니다:
| 유형 | 설명 |
|---|---|
VisitorIntentSignal | 구매 또는 서비스 의도 |
ContentGapSignal | 누락되었거나 오래된 콘텐츠 감지 |
PaymentIntent | 결제 수명 주기 메시지 |
SupportEscalation | 사람 지원으로의 에스컬레이션 |
ReviewSignal | 플랫폼의 리뷰 또는 피드백 |
OperationsProposal | 재고, 일정 제안 |
InvokeCapability | 선언된 capability 호출 |
RenderUi | UI 렌더링 요청 |
OutboundTrigger | 능동적 outbound 메시지 |
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 스키마
전체 스키마는 풍부한 비즈니스 구성을 지원합니다:
| 섹션 | 필드 | 필수 |
|---|---|---|
| (루트) | 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 파일은 계속 작동합니다.
핫 리로드
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 에이전트 예제가 포함되어 있습니다:
cd examples/awp_agent
cp .env.example .env # add your GOOGLE_API_KEY
cargo run
이 예제는 다음을 수행합니다:
- 제품, 정책, 브랜드 보이스가 포함된
business.toml를 로드합니다 - 비즈니스 컨텍스트에서 도출한 지침으로 LLM 에이전트를 생성합니다
- 해당 에이전트에 인증된 A2A 디스패치를 설치합니다
- 별도의 데모 자격 증명 뒤에 관리 라우트를 마운트합니다
- 모든 엔드포인트를 실행하고 프로토콜 검증을 출력합니다
모범 사례
- 최소한의
business.toml로 시작하세요 —site_name,site_description,domain, 기능, 정책만 필요합니다 - 기능 권한 부여를 강제하세요 —
access_level는 매니페스트 메타데이터이며, 애플리케이션 핸들러가 이를 강제해야 합니다 - 프로덕션에서 핫 리로드를 활성화하세요 — 무중단 구성 업데이트를 위해
loader.watch()를 호출하세요 - 사용자 지정
TrustLevelAssigner를 구현하세요 — 기본값은 의도적으로Anonymous만 할당합니다 - 관리 라우트를 인증하세요 — 보호되지 않은 라우터에서 구독 CRUD를 절대 노출하지 마세요
- 실제 A2A 디스패치를 설치하세요 — fail-closed 기본값은
503를 반환합니다 - 지속적인 이벤트 전달을 사용하세요 — 대상 정책, 큐잉, 제한된 재시도를 구현하세요
- 웹훅 서명을 검증하세요 — 들어오는 웹훅에서
X-AWP-Signature를 검증하세요
관련 항목
이전: ← A2A 프로토콜 | 다음: 평가 →