-
Notifications
You must be signed in to change notification settings - Fork 4.4k
10 Gateway
Relevant source files
The following files were used as context for generating this wiki page:
The Gateway is ZeroClaw's HTTP/WebSocket server that exposes the agent runtime over the network. It provides a RESTful API for external clients to interact with the AI agent, handles webhook integrations (including WhatsApp), and enforces authentication, rate limiting, and request validation.
For information about starting the full autonomous runtime (gateway + channels + heartbeat + scheduler), see the Daemon documentation. For details on the agent's tool execution loop that the gateway invokes, see Agent Core.
The Gateway is implemented as an Axum-based HTTP server (src/gateway/mod.rs), replacing earlier raw TCP implementations to ensure proper HTTP/1.1 compliance, request timeouts, and body size validation.
Key responsibilities:
- Expose HTTP endpoints for message submission and health checks
- Authenticate clients via pairing codes and bearer tokens
- Rate-limit requests per client IP to prevent abuse
- Support idempotent webhook delivery via
X-Idempotency-Key - Handle WhatsApp webhook verification and message ingestion
- Optionally bind to public addresses when used with tunnels
- Export Prometheus metrics for observability
Default binding: The gateway binds to 127.0.0.1:3000 by default and refuses public binds (0.0.0.0) unless a tunnel is configured or allow_public_bind = true is explicitly set.
Sources: src/gateway/mod.rs:1-505, README.md:218-226
graph TB
subgraph "Entry Layer"
Listener["TcpListener<br/>(127.0.0.1:3000)"]
Tunnel["Tunnel<br/>(Cloudflare/Tailscale/ngrok)"]
end
subgraph "HTTP Middleware (Axum)"
BodyLimit["RequestBodyLimitLayer<br/>(64KB max)"]
Timeout["TimeoutLayer<br/>(30s max)"]
Router["Router<br/>(/health, /pair, /webhook, /whatsapp, /metrics)"]
end
subgraph "Handler Layer"
HealthHandler["handle_health()"]
PairHandler["handle_pair()"]
WebhookHandler["handle_webhook()"]
WhatsAppVerifyHandler["handle_whatsapp_verify()"]
WhatsAppMessageHandler["handle_whatsapp_message()"]
MetricsHandler["handle_metrics()"]
end
subgraph "Security Components"
PairingGuard["PairingGuard<br/>(6-digit code + bearer tokens)"]
RateLimiter["GatewayRateLimiter<br/>(pair & webhook limits)"]
IdempotencyStore["IdempotencyStore<br/>(TTL-based dedup)"]
WebhookSecretHash["webhook_secret_hash<br/>(SHA-256)"]
WhatsAppSigVerify["verify_whatsapp_signature()<br/>(HMAC-SHA256)"]
end
subgraph "Shared State (AppState)"
Config["Config (Arc<Mutex>)"]
Provider["Provider (Arc<dyn>)"]
Memory["Memory (Arc<dyn>)"]
WhatsAppChannel["WhatsAppChannel (Option<Arc>)"]
Observer["Observer (Arc<dyn>)"]
end
subgraph "Backend Integration"
AgentCore["Agent Core<br/>(provider.simple_chat)"]
MemoryStore["Memory Backend<br/>(store conversation)"]
PrometheusBackend["PrometheusObserver<br/>(metrics export)"]
end
Listener --> Tunnel
Tunnel --> BodyLimit
BodyLimit --> Timeout
Timeout --> Router
Router --> HealthHandler
Router --> PairHandler
Router --> WebhookHandler
Router --> WhatsAppVerifyHandler
Router --> WhatsAppMessageHandler
Router --> MetricsHandler
PairHandler --> PairingGuard
PairHandler --> RateLimiter
WebhookHandler --> PairingGuard
WebhookHandler --> RateLimiter
WebhookHandler --> IdempotencyStore
WebhookHandler --> WebhookSecretHash
WhatsAppMessageHandler --> WhatsAppSigVerify
HealthHandler --> Config
PairHandler --> Config
WebhookHandler --> Provider
WebhookHandler --> Memory
WhatsAppMessageHandler --> WhatsAppChannel
MetricsHandler --> Observer
Provider --> AgentCore
Memory --> MemoryStore
Observer --> PrometheusBackend
Gateway Component Hierarchy
Sources: src/gateway/mod.rs:260-505
The Gateway enforces a five-layer security model to prevent unauthorized access, abuse, and data exfiltration:
graph LR
subgraph "Secure Default"
BindCheck["is_public_bind(host)"]
TunnelCheck["tunnel.provider == 'none'"]
AllowPublicCheck["gateway.allow_public_bind"]
end
subgraph "Decision"
Refuse["Refuse startup<br/>(bail with error)"]
Allow["Allow bind"]
end
BindCheck -->|"true (0.0.0.0)"| TunnelCheck
TunnelCheck -->|"true"| AllowPublicCheck
AllowPublicCheck -->|"false"| Refuse
AllowPublicCheck -->|"true"| Allow
BindCheck -->|"false (127.0.0.1)"| Allow
TunnelCheck -->|"false (tunnel active)"| Allow
Security enforcement:
- Default bind:
127.0.0.1:3000(localhost-only) - Public binds (
0.0.0.0) require either:- Active tunnel (Cloudflare/Tailscale/ngrok), OR
- Explicit
allow_public_bind = truein config (NOT recommended)
Sources: src/gateway/mod.rs:284-292, src/security/pairing.rs
sequenceDiagram
participant Client
participant Gateway
participant PairingGuard
participant Config
Note over Gateway: Startup
Gateway->>PairingGuard: new(require_pairing, paired_tokens)
alt require_pairing && no existing tokens
PairingGuard->>PairingGuard: generate_pairing_code() → 6-digit code
end
Gateway->>Gateway: Display pairing code in console
Note over Client: Pairing
Client->>Gateway: POST /pair<br/>X-Pairing-Code: 123456
Gateway->>PairingGuard: try_pair(code)
PairingGuard->>PairingGuard: Constant-time comparison
alt Code matches
PairingGuard->>PairingGuard: generate_bearer_token() → SHA-256 hashed
PairingGuard-->>Gateway: Ok(Some(token))
Gateway->>Config: Save token to config.toml
Gateway-->>Client: 200 OK {"token": "..."}
else Code invalid
PairingGuard->>PairingGuard: Increment failure count
alt Too many failures
PairingGuard-->>Gateway: Err(lockout_secs)
Gateway-->>Client: 429 Too Many Requests
else
PairingGuard-->>Gateway: Ok(None)
Gateway-->>Client: 403 Forbidden
end
end
Note over Client: Subsequent Requests
Client->>Gateway: POST /webhook<br/>Authorization: Bearer <token>
Gateway->>PairingGuard: is_authenticated(token)
alt Token valid
PairingGuard-->>Gateway: true
Gateway->>Gateway: Process request
else Token invalid
PairingGuard-->>Gateway: false
Gateway-->>Client: 401 Unauthorized
end
PairingGuard implementation details:
- Generates a 6-digit one-time pairing code on startup (if
require_pairingis true and no existing tokens) - Bearer tokens are 32-byte random values, SHA-256 hashed before storage
- Constant-time comparison prevents timing attacks
- Lockout after 3 failed pairing attempts (5-minute cooldown)
- Persists paired tokens to
config.tomlfor restart durability
Sources: src/gateway/mod.rs:393-412, 545-612, src/security/pairing.rs
The gateway enforces per-client rate limits using a sliding window algorithm with cardinality bounds.
graph TB
subgraph "GatewayRateLimiter"
PairLimiter["pair: SlidingWindowRateLimiter<br/>(default: unlimited per minute)"]
WebhookLimiter["webhook: SlidingWindowRateLimiter<br/>(default: unlimited per minute)"]
end
subgraph "SlidingWindowRateLimiter State"
RequestsMap["requests: HashMap<ClientKey, Vec<Instant>>"]
LastSweep["last_sweep: Instant"]
Window["window: Duration (60s)"]
LimitPerWindow["limit_per_window: u32"]
MaxKeys["max_keys: usize (10,000 default)"]
end
subgraph "Client Key Derivation"
TrustForwarded{"trust_forwarded_headers?"}
XFF["X-Forwarded-For header"]
XRealIP["X-Real-IP header"]
PeerAddr["Peer socket IP"]
end
TrustForwarded -->|"true"| XFF
TrustForwarded -->|"true"| XRealIP
TrustForwarded -->|"false"| PeerAddr
XFF --> RequestsMap
XRealIP --> RequestsMap
PeerAddr --> RequestsMap
PairLimiter --> RequestsMap
WebhookLimiter --> RequestsMap
RequestsMap --> Window
RequestsMap --> LimitPerWindow
RequestsMap --> MaxKeys
Rate limiter behavior:
- Sliding window: Tracks timestamps per client IP, retains only requests within the last 60 seconds
-
Cardinality bound: Limits memory usage by evicting least-recently-used IPs when
max_keysis exceeded - Periodic sweep: Clears stale entries every 5 minutes (src/gateway/mod.rs:64)
-
Configurable limits:
-
gateway.pair_rate_limit_per_minute(default: 0 = unlimited) -
gateway.webhook_rate_limit_per_minute(default: 0 = unlimited) -
gateway.rate_limit_max_keys(default: 10,000)
-
Client key derivation:
- If
trust_forwarded_headers = true: UseX-Forwarded-FororX-Real-IP - Otherwise: Use peer socket IP
-
Security note: Only enable
trust_forwarded_headerswhen behind a trusted reverse proxy
Sources: src/gateway/mod.rs:64-158, 220-257, 400-405, 547-559, 621-636
The gateway supports idempotent webhook delivery via the X-Idempotency-Key header, preventing duplicate message processing.
| Component | Type | Behavior |
|---|---|---|
IdempotencyStore |
In-memory TTL cache | Tracks seen keys with expiration |
| Default TTL | gateway.idempotency_ttl_secs |
300 seconds (5 minutes) |
| Max Keys | gateway.idempotency_max_keys |
10,000 (LRU eviction) |
| Response | HTTP 200 | Returns {"status": "duplicate", "idempotent": true} for duplicates |
Sources: src/gateway/mod.rs:161-200, 409-413, 685-700
Generic Webhook (/webhook):
- Optional
X-Webhook-Secretheader authentication - SHA-256 hash of secret compared in constant-time
- Configured via
channels_config.webhook.secretin config
WhatsApp Webhook (/whatsapp):
-
Required HMAC-SHA256 signature verification via
X-Hub-Signature-256header - App secret configured via
ZEROCLAW_WHATSAPP_APP_SECRETenv var orchannels_config.whatsapp.app_secret - Meta webhook verification challenge at
GET /whatsappuses constant-time token comparison
Sources: src/gateway/mod.rs:353-360, 375-390, 843-868, 883-904
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/health |
GET | None | Health check + runtime status |
/metrics |
GET | None | Prometheus text exposition format |
/pair |
POST | X-Pairing-Code |
Exchange one-time code for bearer token |
/webhook |
POST | Authorization: Bearer <token> |
Submit message to agent |
/whatsapp |
GET | Query params | Meta webhook verification |
/whatsapp |
POST | X-Hub-Signature-256 |
Receive WhatsApp messages |
Sources: src/gateway/mod.rs:483-489, README.md:749-758
Purpose: Always-public health check endpoint that never leaks secrets.
Request:
GET /health HTTP/1.1
Host: localhost:3000Response:
{
"status": "ok",
"paired": true,
"runtime": {
"channels": {"status": "ok", "last_update": "2026-02-18T12:00:00Z"},
"gateway": {"status": "ok", "last_update": "2026-02-18T12:00:00Z"},
"heartbeat": {"status": "ok", "last_update": "2026-02-18T11:59:00Z"},
"scheduler": {"status": "ok", "last_update": "2026-02-18T11:58:00Z"}
}
}Implementation: src/gateway/mod.rs:511-519
Sources: src/gateway/mod.rs:511-519, src/health.rs
Purpose: Prometheus text exposition format for observability backends.
Response when Prometheus backend is enabled:
# HELP zeroclaw_heartbeat_ticks_total Total number of heartbeat ticks
# TYPE zeroclaw_heartbeat_ticks_total counter
zeroclaw_heartbeat_ticks_total 42
# HELP zeroclaw_llm_requests_total Total LLM API requests
# TYPE zeroclaw_llm_requests_total counter
zeroclaw_llm_requests_total{provider="openrouter",model="anthropic/claude-sonnet-4"} 123
Response when Prometheus backend is disabled:
# Prometheus backend not enabled. Set [observability] backend = "prometheus" in config.
Implementation: src/gateway/mod.rs:524-542
Content-Type: text/plain; version=0.0.4; charset=utf-8
Sources: src/gateway/mod.rs:522-542, src/observability/prometheus.rs
Purpose: Exchange the one-time pairing code for a long-lived bearer token.
Request:
POST /pair HTTP/1.1
Host: localhost:3000
X-Pairing-Code: 123456Success Response (200 OK):
{
"paired": true,
"persisted": true,
"token": "a1b2c3d4e5f6...",
"message": "Save this token — use it as Authorization: Bearer <token>"
}Failure Response (403 Forbidden):
{
"error": "Invalid pairing code"
}Lockout Response (429 Too Many Requests):
{
"error": "Too many failed attempts. Try again in 300s.",
"retry_after": 300
}Rate Limiting:
- Rate limit key: Client IP (or
X-Forwarded-Foriftrust_forwarded_headers = true) - Default limit: Unlimited (configurable via
gateway.pair_rate_limit_per_minute)
Token Persistence:
- Bearer tokens are persisted to
config.toml→gateway.paired_tokensarray - Survives gateway restarts
Implementation: src/gateway/mod.rs:545-604
Sources: src/gateway/mod.rs:545-612
Purpose: Submit a message to the AI agent and receive a synchronous response.
Request:
POST /webhook HTTP/1.1
Host: localhost:3000
Authorization: Bearer a1b2c3d4e5f6...
X-Webhook-Secret: my-secret-value
X-Idempotency-Key: unique-request-id-123
Content-Type: application/json
{
"message": "What is the capital of France?"
}Headers:
-
Authorization: Bearer <token>(required ifrequire_pairing = true) -
X-Webhook-Secret: <secret>(optional, required ifchannels_config.webhook.secretis set) -
X-Idempotency-Key: <key>(optional, enables idempotent delivery)
Success Response (200 OK):
{
"response": "The capital of France is Paris.",
"model": "anthropic/claude-sonnet-4"
}Duplicate Request Response (200 OK):
{
"status": "duplicate",
"idempotent": true,
"message": "Request already processed for this idempotency key"
}Error Responses:
| Status | Condition |
|---|---|
| 400 Bad Request | Invalid JSON body |
| 401 Unauthorized | Missing/invalid bearer token or webhook secret |
| 429 Too Many Requests | Rate limit exceeded |
| 500 Internal Server Error | LLM request failed |
Auto-save behavior:
- If
memory.auto_save = true, the incoming message is stored to memory before LLM invocation - Memory key format:
webhook_msg_<UUID> - Category:
Conversation
Implementation: src/gateway/mod.rs:620-804
Sources: src/gateway/mod.rs:614-804
Purpose: Meta webhook verification challenge for WhatsApp integration.
Request:
GET /whatsapp?hub.mode=subscribe&hub.verify_token=my-verify-token&hub.challenge=challenge-string HTTP/1.1
Host: my-tunnel.example.comQuery Parameters:
-
hub.mode: Must equal"subscribe" -
hub.verify_token: Must matchchannels_config.whatsapp.verify_token(constant-time comparison) -
hub.challenge: Random string provided by Meta
Success Response (200 OK):
challenge-string
Failure Response (403 Forbidden):
Forbidden
Implementation: src/gateway/mod.rs:817-841
Sources: src/gateway/mod.rs:807-841
Purpose: Receive incoming WhatsApp messages from Meta's Cloud API.
Request:
POST /whatsapp HTTP/1.1
Host: my-tunnel.example.com
X-Hub-Signature-256: sha256=abcdef1234567890...
Content-Type: application/json
{
"object": "whatsapp_business_account",
"entry": [
{
"changes": [
{
"value": {
"messages": [
{
"from": "1234567890",
"id": "wamid.xyz",
"text": {
"body": "Hello"
}
}
]
}
}
]
}
]
}Signature Verification:
- Header:
X-Hub-Signature-256: sha256=<hex_hmac> - Algorithm: HMAC-SHA256
- Secret:
ZEROCLAW_WHATSAPP_APP_SECRETenv var orchannels_config.whatsapp.app_secret - Constant-time comparison via
hmac::verify_slice
Success Response (200 OK):
{
"status": "ok"
}Error Responses:
| Status | Condition |
|---|---|
| 400 Bad Request | Invalid JSON payload |
| 401 Unauthorized | Signature verification failed |
| 404 Not Found | WhatsApp channel not configured |
Message Processing:
- Parses Meta webhook payload via
WhatsAppChannel::parse_webhook_payload - Checks sender against
allowed_numbersallowlist - Auto-saves message to memory if
memory.auto_save = true - Invokes LLM via
provider.simple_chat - Sends reply via
WhatsAppChannel::send
Implementation: src/gateway/mod.rs:870-968
Sources: src/gateway/mod.rs:843-968, src/channels/whatsapp.rs
[gateway]
port = 3000 # Listening port (0 = random available port)
host = "127.0.0.1" # Bind address (localhost by default)
require_pairing = true # Enforce pairing authentication
allow_public_bind = false # Allow 0.0.0.0 without tunnel (NOT recommended)
paired_tokens = [] # Array of SHA-256 hashed bearer tokens (auto-managed)
trust_forwarded_headers = false # Trust X-Forwarded-For / X-Real-IP headers (only enable behind trusted proxy)
pair_rate_limit_per_minute = 0 # Rate limit for /pair (0 = unlimited)
webhook_rate_limit_per_minute = 0 # Rate limit for /webhook (0 = unlimited)
rate_limit_max_keys = 10000 # Max distinct IPs tracked by rate limiter
idempotency_ttl_secs = 300 # Idempotency key expiration (5 minutes)
idempotency_max_keys = 10000 # Max distinct idempotency keys tracked[tunnel]
provider = "none" # "none", "cloudflare", "tailscale", "ngrok", "custom"
# Cloudflare example
# provider = "cloudflare"
# Tailscale Funnel example
# provider = "tailscale"
# ngrok example
# provider = "ngrok"
# [tunnel.ngrok]
# auth_token = "your-ngrok-token"[channels_config.webhook]
secret = "my-webhook-secret" # Optional X-Webhook-Secret for /webhook endpoint[channels_config.whatsapp]
access_token = "EAABx..." # Meta access token (from WhatsApp Business API setup)
phone_number_id = "123456789012345" # Phone number ID (from WhatsApp Business API setup)
verify_token = "my-verify-token" # Webhook verification token (you define this)
app_secret = "abc123..." # Optional app secret for signature verification (or use ZEROCLAW_WHATSAPP_APP_SECRET env var)
allowed_numbers = ["+1234567890"] # E.164 format phone numbers allowlist (or ["*"] for all)Sources: src/gateway/mod.rs:283-432, src/config.rs, README.md:525-590
sequenceDiagram
participant Client
participant Axum as "Axum Middleware"
participant Handler as "handle_webhook()"
participant RateLimiter
participant PairingGuard
participant IdempotencyStore
participant Provider
participant Memory
participant Observer
Client->>Axum: POST /webhook
Axum->>Axum: RequestBodyLimitLayer (64KB max)
Axum->>Axum: TimeoutLayer (30s max)
Axum->>Handler: Extract State + Headers + Body
Handler->>Handler: client_key_from_request()
Handler->>RateLimiter: allow_webhook(client_key)
alt Rate limit exceeded
RateLimiter-->>Handler: false
Handler-->>Client: 429 Too Many Requests
end
Handler->>PairingGuard: require_pairing()?
alt Pairing required
Handler->>PairingGuard: is_authenticated(bearer_token)
alt Not authenticated
PairingGuard-->>Handler: false
Handler-->>Client: 401 Unauthorized
end
end
Handler->>Handler: Verify X-Webhook-Secret (if configured)
alt Secret mismatch
Handler-->>Client: 401 Unauthorized
end
Handler->>Handler: Parse JSON body
alt Invalid JSON
Handler-->>Client: 400 Bad Request
end
Handler->>IdempotencyStore: Check X-Idempotency-Key
alt Duplicate key
IdempotencyStore-->>Handler: false (already seen)
Handler-->>Client: 200 OK (duplicate response)
end
IdempotencyStore->>IdempotencyStore: record_if_new(key)
Handler->>Memory: auto_save? store(webhook_memory_key(), message)
Handler->>Observer: record_event(AgentStart)
Handler->>Observer: record_event(LlmRequest)
Handler->>Provider: simple_chat(message, model, temperature)
Provider-->>Handler: Result<String>
Handler->>Observer: record_event(LlmResponse)
Handler->>Observer: record_metric(RequestLatency)
Handler->>Observer: record_event(AgentEnd)
alt LLM success
Handler-->>Client: 200 OK {"response": "...", "model": "..."}
else LLM error
Handler->>Provider: sanitize_api_error()
Handler->>Observer: record_event(Error)
Handler-->>Client: 500 Internal Server Error
end
Sources: src/gateway/mod.rs:620-804
sequenceDiagram
participant Meta as "Meta Webhook"
participant Axum
participant Handler as "handle_whatsapp_message()"
participant Verifier as "verify_whatsapp_signature()"
participant WhatsAppChannel
participant Provider
participant Memory
Meta->>Axum: POST /whatsapp<br/>X-Hub-Signature-256: sha256=...
Axum->>Handler: Extract State + Headers + Body
alt WhatsApp not configured
Handler-->>Meta: 404 Not Found
end
Handler->>Verifier: verify_whatsapp_signature(app_secret, body, signature)
Verifier->>Verifier: HMAC-SHA256 computation
Verifier->>Verifier: Constant-time comparison
alt Signature invalid
Verifier-->>Handler: false
Handler-->>Meta: 401 Unauthorized
end
Handler->>Handler: Parse JSON payload
alt Invalid JSON
Handler-->>Meta: 400 Bad Request
end
Handler->>WhatsAppChannel: parse_webhook_payload(json)
WhatsAppChannel->>WhatsAppChannel: Extract messages from nested structure
WhatsAppChannel->>WhatsAppChannel: Check allowed_numbers allowlist
WhatsAppChannel-->>Handler: Vec<ChannelMessage>
alt No messages
Handler-->>Meta: 200 OK (acknowledge)
end
loop For each message
Handler->>Memory: auto_save? store(whatsapp_memory_key(msg))
Handler->>Provider: simple_chat(msg.content, model, temperature)
Provider-->>Handler: Result<String>
alt LLM success
Handler->>WhatsAppChannel: send(SendMessage::new(response, reply_target))
WhatsAppChannel->>Meta: POST to Meta API (send_message endpoint)
else LLM error
Handler->>WhatsAppChannel: send(error_message)
end
end
Handler-->>Meta: 200 OK {"status": "ok"}
Sources: src/gateway/mod.rs:870-968, src/channels/whatsapp.rs
The gateway invokes the LLM via provider.simple_chat():
// From AppState
let provider: Arc<dyn Provider> = ...;
let model: String = config.default_model.clone().unwrap_or(...);
let temperature: f64 = config.default_temperature;
// In handle_webhook
let response = provider.simple_chat(message, &model, temperature).await?;Error handling:
- Provider errors are sanitized via
providers::sanitize_api_error()to prevent API key leakage - Sanitized errors are logged and returned as generic error messages to clients
Sources: src/gateway/mod.rs:300-314, 735-803, src/providers/mod.rs
The gateway auto-saves messages and responses when memory.auto_save = true:
Webhook messages:
let key = webhook_memory_key(); // Format: webhook_msg_<UUID>
mem.store(&key, message, MemoryCategory::Conversation, None).await?;WhatsApp messages:
let key = whatsapp_memory_key(&msg); // Format: whatsapp_<sender>_<msg_id>
mem.store(&key, &msg.content, MemoryCategory::Conversation, None).await?;Memory key functions: src/gateway/mod.rs:48-54
Sources: src/gateway/mod.rs:48-54, 704-710, 931-937, src/memory/mod.rs
The gateway emits observability events for request tracking, error monitoring, and latency metrics:
Event types:
-
AgentStart- When request processing begins -
LlmRequest- Before calling the provider -
LlmResponse- After receiving provider response (includes duration and success flag) -
AgentEnd- When request processing completes (includes total duration and cost) -
Error- When errors occur (includes component and sanitized message)
Metrics:
-
RequestLatency- Histogram of request processing times
Implementation:
// From AppState
let observer: Arc<dyn Observer> = ...;
// Record events
observer.record_event(&ObserverEvent::AgentStart { provider, model });
observer.record_event(&ObserverEvent::LlmRequest { provider, model, messages_count });
observer.record_event(&ObserverEvent::LlmResponse { provider, model, duration, success, error_message });
observer.record_metric(&ObserverMetric::RequestLatency(duration));
observer.record_event(&ObserverEvent::AgentEnd { provider, model, duration, tokens_used, cost_usd });Prometheus export: GET /metrics endpoint encodes all counters/histograms in text exposition format.
Sources: src/gateway/mod.rs:462-479, 721-797, src/observability/mod.rs
The gateway requires a tunnel for remote access. Supported tunnel providers:
| Provider | Config | Use Case |
|---|---|---|
| Cloudflare Tunnel | tunnel.provider = "cloudflare" |
Zero-trust access, automatic HTTPS |
| Tailscale Funnel | tunnel.provider = "tailscale" |
Secure peer-to-peer mesh, automatic HTTPS |
| ngrok | tunnel.provider = "ngrok" |
Development/testing, ephemeral URLs |
| Custom | tunnel.provider = "custom" |
Bring your own tunnel binary |
Tunnel startup:
- Gateway calls
tunnel.start(host, port)on startup - Tunnel returns public URL (e.g.,
https://abc123.cloudflare.com) - Gateway logs public URL for webhook configuration
Sources: src/gateway/mod.rs:416-431, src/tunnel/mod.rs
| Item | Recommendation | Config Key |
|---|---|---|
| Pairing | Always enable | gateway.require_pairing = true |
| Public bind | Refuse unless tunnel active | gateway.allow_public_bind = false |
| Rate limiting | Enable for production | gateway.webhook_rate_limit_per_minute = 100 |
| Webhook secret | Set for generic webhook | channels_config.webhook.secret |
| WhatsApp signature | Always verify | ZEROCLAW_WHATSAPP_APP_SECRET |
| Tunnel | Use Cloudflare or Tailscale | tunnel.provider = "cloudflare" |
| Forwarded headers | Only behind trusted proxy | gateway.trust_forwarded_headers = false |
Sources: README.md:380-433, src/gateway/mod.rs:284-292
| Error | Cause | Mitigation |
|---|---|---|
| Refuses to start | Public bind without tunnel | Configure tunnel or set allow_public_bind = true
|
| Pairing lockout | 3+ failed pairing attempts | Wait 5 minutes, then retry |
| Rate limit exceeded | Too many requests from same IP | Wait 60 seconds (window duration) |
| Body too large | Request exceeds 64KB | Split message into smaller chunks |
| Request timeout | Processing exceeds 30s | Check LLM provider latency |
Sources: src/gateway/mod.rs:37-40, 284-292
LLM API errors are sanitized before returning to clients to prevent credential leakage:
Example sanitization:
Original: "OpenAI API error: 401 Unauthorized (invalid API key: sk-abc123...)"
Sanitized: "OpenAI API error: 401 Unauthorized"
Implementation: src/providers/mod.rs:sanitize_api_error
Sources: src/gateway/mod.rs:769, 799, src/providers/mod.rs
The gateway module includes comprehensive tests for security enforcement:
Test coverage:
- Body size limit enforcement (src/gateway/mod.rs:984-986)
- Request timeout enforcement (src/gateway/mod.rs:989-991)
- Webhook body schema validation (src/gateway/mod.rs:994-1003)
- Rate limiter behavior (src/gateway/mod.rs:1090-1095)
- Rate limiter sweep mechanism (src/gateway/mod.rs:1098-1129)
- Zero-limit bypass (src/gateway/mod.rs:1132-1137)
- Idempotency store deduplication (src/gateway/mod.rs:1140-1145)
- Cardinality bounds and LRU eviction (src/gateway/mod.rs:1148-1165)
Run tests:
cargo test gateway::tests --libSources: src/gateway/mod.rs:970-1248
The Gateway is ZeroClaw's HTTP interface, providing:
- Secure defaults: Localhost-only binding, mandatory pairing, rate limiting
- Multiple endpoints: Health checks, pairing, webhooks, WhatsApp, metrics
- Defense in depth: Network isolation → Authentication → Authorization → Rate limiting → Signature verification
- Production-ready: Tunnel support, idempotency, observability, error sanitization
- Lightweight: Axum-based, minimal dependencies, fast cold starts
Related documentation:
- For full autonomous runtime (gateway + channels + scheduler), see the Daemon documentation
- For agent turn cycle details, see Agent Core
- For security model deep dive, see Security Model
- For provider integration, see Providers
Sources: src/gateway/mod.rs:1-1248, README.md:218-590