Skip to content

10 Gateway

Nikolay Vyahhi edited this page Feb 19, 2026 · 2 revisions

Gateway

Relevant source files

The following files were used as context for generating this wiki page:

Purpose and Scope

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.


Overview

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


Architecture

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
Loading

Gateway Component Hierarchy

Sources: src/gateway/mod.rs:260-505


Security Model

The Gateway enforces a five-layer security model to prevent unauthorized access, abuse, and data exfiltration:

Layer 1: Network Isolation

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
Loading

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 = true in config (NOT recommended)

Sources: src/gateway/mod.rs:284-292, src/security/pairing.rs


Layer 2: Pairing Authentication

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
Loading

PairingGuard implementation details:

  • Generates a 6-digit one-time pairing code on startup (if require_pairing is 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.toml for restart durability

Sources: src/gateway/mod.rs:393-412, 545-612, src/security/pairing.rs


Layer 3: Rate Limiting

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
Loading

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_keys is 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: Use X-Forwarded-For or X-Real-IP
  • Otherwise: Use peer socket IP
  • Security note: Only enable trust_forwarded_headers when behind a trusted reverse proxy

Sources: src/gateway/mod.rs:64-158, 220-257, 400-405, 547-559, 621-636


Layer 4: Idempotency

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


Layer 5: Webhook Signature Verification

Generic Webhook (/webhook):

  • Optional X-Webhook-Secret header authentication
  • SHA-256 hash of secret compared in constant-time
  • Configured via channels_config.webhook.secret in config

WhatsApp Webhook (/whatsapp):

  • Required HMAC-SHA256 signature verification via X-Hub-Signature-256 header
  • App secret configured via ZEROCLAW_WHATSAPP_APP_SECRET env var or channels_config.whatsapp.app_secret
  • Meta webhook verification challenge at GET /whatsapp uses constant-time token comparison

Sources: src/gateway/mod.rs:353-360, 375-390, 843-868, 883-904


API Endpoints

Summary Table

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


GET /health

Purpose: Always-public health check endpoint that never leaks secrets.

Request:

GET /health HTTP/1.1
Host: localhost:3000

Response:

{
  "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


GET /metrics

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


POST /pair

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: 123456

Success 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-For if trust_forwarded_headers = true)
  • Default limit: Unlimited (configurable via gateway.pair_rate_limit_per_minute)

Token Persistence:

  • Bearer tokens are persisted to config.tomlgateway.paired_tokens array
  • Survives gateway restarts

Implementation: src/gateway/mod.rs:545-604

Sources: src/gateway/mod.rs:545-612


POST /webhook

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 if require_pairing = true)
  • X-Webhook-Secret: <secret> (optional, required if channels_config.webhook.secret is 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


GET /whatsapp (Verification)

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.com

Query Parameters:

  • hub.mode: Must equal "subscribe"
  • hub.verify_token: Must match channels_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


POST /whatsapp (Message Webhook)

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_SECRET env var or channels_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_numbers allowlist
  • 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


Configuration Reference

[gateway] Section

[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] Section

[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] Section

[channels_config.webhook]
secret = "my-webhook-secret"        # Optional X-Webhook-Secret for /webhook endpoint

[channels_config.whatsapp] Section

[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


Request Lifecycle

Webhook Request Flow

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
Loading

Sources: src/gateway/mod.rs:620-804


WhatsApp Message Flow

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

Sources: src/gateway/mod.rs:870-968, src/channels/whatsapp.rs


Integration Points

Provider Integration

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


Memory Integration

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


Observability Integration

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


Deployment Considerations

Tunnel Configuration

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:

  1. Gateway calls tunnel.start(host, port) on startup
  2. Tunnel returns public URL (e.g., https://abc123.cloudflare.com)
  3. Gateway logs public URL for webhook configuration

Sources: src/gateway/mod.rs:416-431, src/tunnel/mod.rs


Production Security Checklist

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 Handling

Gateway-Level Errors

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


Provider Error Sanitization

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


Testing

Unit Tests

The gateway module includes comprehensive tests for security enforcement:

Test coverage:

Run tests:

cargo test gateway::tests --lib

Sources: src/gateway/mod.rs:970-1248


Summary

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


Clone this wiki locally