research

ZeroClaw — Gateway 시스템

ZeroClaw — Gateway 시스템

Layer 3 학습. 실제 소스 코드 기반. 파일: src/gateway/mod.rs, src/gateway/api.rs, src/gateway/ws.rs, src/gateway/sse.rs


한 줄 정의

Gateway = ZeroClaw를 HTTP로 노출하는 Axum 웹 서버.

Channel 시스템(Telegram, Discord)이 플랫폼 채널로 붙는다면, Gateway는 HTTP/WebSocket으로 외부 시스템이 ZeroClaw에 접근하는 창구.

외부 세계                       ZeroClaw
────────────────────────────────────────────────────
POST /webhook          →   LLM 응답 (단순 채팅)
POST /whatsapp         →   WhatsApp 메시지 처리
POST /linq             →   iMessage/RCS/SMS 처리
WebSocket /ws/chat     →   대화형 에이전트 채팅
GET /api/*             →   대시보드 REST API
GET /                  →   Web UI (SPA)
GET /health            →   헬스 체크
GET /metrics           →   Prometheus 메트릭

전체 구조

run_gateway(host, port, config)
    │
    ├─ 보안 검사: public bind 거부 (127.0.0.1 기본)
    ├─ Provider, Memory, SecurityPolicy, Tools 초기화
    ├─ Tunnel 시작 (ngrok 등)
    ├─ AppState 구성
    │
    ├─ Axum Router 구성:
    │   /health            GET
    │   /metrics           GET  (Prometheus)
    │   /pair              POST (페어링)
    │   /webhook           POST (메인 엔드포인트)
    │   /whatsapp          GET/POST
    │   /linq              POST
    │   /wati              GET/POST
    │   /nextcloud-talk    POST
    │   /webhook/gmail     POST
    │   /api/*             REST API
    │   /ws/chat           WebSocket
    │   /ws/canvas/{id}    WebSocket (Live Canvas)
    │   /ws/nodes          WebSocket (Node 디스커버리)
    │   /admin/*           관리자 엔드포인트 (localhost 전용)
    │   /                  Web Dashboard SPA
    │
    ├─ Middleware:
    │   RequestBodyLimitLayer (64KB)
    │   TimeoutLayer (30초, ZEROCLAW_GATEWAY_TIMEOUT_SECS로 오버라이드)
    │
    └─ axum::serve() + graceful shutdown

AppState — 공유 상태

pub struct AppState {
    config: Arc<Mutex<Config>>,
    provider: Arc<dyn Provider>,       // LLM provider
    model: String,
    temperature: f64,
    mem: Arc<dyn Memory>,              // 메모리 백엔드
    auto_save: bool,
    webhook_secret_hash: Option<Arc<str>>, // SHA-256 해시 (평문 아님)
    pairing: Arc<PairingGuard>,        // 페어링 인증
    trust_forwarded_headers: bool,     // 리버스 프록시 설정
    rate_limiter: Arc<GatewayRateLimiter>,
    idempotency_store: Arc<IdempotencyStore>,
    whatsapp: Option<Arc<WhatsAppChannel>>,
    linq: Option<Arc<LinqChannel>>,
    nextcloud_talk: Option<Arc<NextcloudTalkChannel>>,
    wati: Option<Arc<WatiChannel>>,
    gmail_push: Option<Arc<GmailPushChannel>>,
    observer: Arc<dyn Observer>,
    tools_registry: Arc<Vec<ToolSpec>>,
    cost_tracker: Option<Arc<CostTracker>>,
    event_tx: broadcast::Sender<serde_json::Value>, // SSE 이벤트 브로드캐스트
    shutdown_tx: watch::Sender<bool>,               // Graceful shutdown
    node_registry: Arc<NodeRegistry>,
    session_backend: Option<Arc<dyn SessionBackend>>, // WS 채팅 세션 퍼시스턴스
    device_registry: Option<Arc<DeviceRegistry>>,
    canvas_store: CanvasStore,
    path_prefix: String,               // 리버스 프록시 경로 접두사
    // ...
}

인증 시스템 — PairingGuard

페어링 흐름

1. 서버 시작 시 일회성 페어링 코드 생성:
   "🔐 PAIRING REQUIRED — use this one-time code:"
   "    ┌──────────────┐"
   "    │  ABC-123456  │"
   "    └──────────────┘"

2. 클라이언트: POST /pair
   Header: X-Pairing-Code: ABC-123456

3. 서버 응답:
   {"paired": true, "token": "<64자 hex 토큰>"}
   → config.toml에 암호화 저장 (재시작 후에도 유효)

4. 이후 요청:
   Header: Authorization: Bearer <token>

인증 계층 (중첩 가능)

require_pairing = true이면:
  Authorization: Bearer <token> 필수

webhook_secret 설정 시 (추가 레이어):
  X-Webhook-Secret: <원문 시크릿>
  → SHA-256 해시로 비교 (timing-safe)

각 채널별 서명 검증:
  WhatsApp  → X-Hub-Signature-256 (HMAC-SHA256)
  Linq      → X-Webhook-Signature + X-Webhook-Timestamp
  Nextcloud → X-Nextcloud-Talk-Signature + X-Nextcloud-Talk-Random
  Gmail     → Authorization: Bearer <webhook_secret>

public bind 보호

// localhost가 아니면 시작 거부
if is_public_bind(host) && tunnel == "none" && !allow_public_bind {
    anyhow::bail!("🛑 Refusing to bind to {host} — exposed to the internet");
}

기본적으로 127.0.0.1만 허용. 외부 접근은 Tunnel을 통해야 함.


Rate Limiting — SlidingWindowRateLimiter

// 슬라이딩 윈도우 방식 (1분 기본)
struct SlidingWindowRateLimiter {
    limit_per_window: u32,
    window: Duration,          // 기본 60초
    max_keys: usize,           // 최대 추적 IP 수 (기본 10,000)
    requests: Mutex<(HashMap<String, Vec<Instant>>, Instant)>,
}

// GatewayRateLimiter = pair용 + webhook용 독립 limiter
struct GatewayRateLimiter {
    pair: SlidingWindowRateLimiter,     // POST /pair 전용
    webhook: SlidingWindowRateLimiter,  // POST /webhook 전용
}

보안 설계:

  • IP별 독립 추적
  • 5분마다 stale 항목 sweep (메모리 누수 방지)
  • max_keys 초과 시 가장 오래된 IP 퇴거
  • 0으로 설정 시 무제한 허용

클라이언트 IP 결정:

trust_forwarded_headers = false (기본):
    TCP 연결 peer 주소 사용 (스푸핑 불가)

trust_forwarded_headers = true (리버스 프록시 뒤에 있을 때):
    X-Forwarded-For 또는 X-Real-IP 헤더 사용

Idempotency Store

struct IdempotencyStore {
    ttl: Duration,      // 기본 설정 가능
    max_keys: usize,    // 기본 10,000
    keys: Mutex<HashMap<String, Instant>>,
}

클라이언트가 X-Idempotency-Key 헤더 포함 시:

  • 처음 요청: 처리 → 저장
  • 중복 요청: {"status": "duplicate", "idempotent": true} 즉시 반환
  • TTL 초과 후: 새 요청으로 처리

중복 webhook 재전송(네트워크 오류 등)으로 LLM이 이중 호출되는 것 방지.


POST /webhook — 메인 엔드포인트

요청 흐름:
  1. Rate limit 체크 (IP 기반)
  2. Bearer token 인증 (require_pairing = true 시)
  3. X-Webhook-Secret 인증 (설정 시)
  4. JSON 파싱 {"message": "..."}
  5. Idempotency 체크 (X-Idempotency-Key 헤더 있으면)
  6. Memory 자동 저장 (auto_save = true 시)
  7. run_gateway_chat_simple() → LLM 호출 (도구 없음)
  8. 응답: {"response": "...", "model": "..."}

Body 제한: 64KB
Timeout: 30초 (ZEROCLAW_GATEWAY_TIMEOUT_SECS로 변경 가능)

두 가지 chat 함수:

// 단순 채팅 (도구 없음) — POST /webhook용
run_gateway_chat_simple(state, message) → LLM 단순 호출

// 도구 포함 채팅 — WhatsApp/Linq/Nextcloud 채널용
run_gateway_chat_with_tools(state, message, session_id)
    → crate::agent::process_message()  ← agent loop 전체

WebSocket /ws/chat

연결 → upgrade HTTP → WS
    │
    ├─ 인증: Bearer token 쿼리 파라미터 또는 헤더
    │
    ├─ 세션 퍼시스턴스:
    │   session_persistence = true이면
    │   SQLite에 대화 히스토리 저장
    │   재연결 시 이전 대화 복원
    │
    ├─ 메시지 수신 → run_gateway_chat_with_tools()
    │   → agent loop (LLM + 도구 호출)
    │
    └─ 응답 스트리밍 (delta 방식 지원)

SSE /api/events

GET /api/events → Server-Sent Events 스트림
    │
    BroadcastObserver가 감싸서:
    tool_call_start, llm_request, llm_response, heartbeat_tick 등
    → JSON 이벤트로 SSE 클라이언트에 브로드캐스트
    → Web Dashboard 실시간 업데이트에 사용

REST API /api/* (Web Dashboard용)

| 경로 | 메서드 | 설명 | |------|--------|------| | /api/status | GET | 서버 상태 | | /api/config | GET/PUT | 설정 조회/변경 | | /api/tools | GET | 등록된 도구 목록 | | /api/cron | GET/POST | 예약 작업 | | /api/cron/{id} | DELETE/PATCH | 작업 관리 | | /api/memory | GET/POST | 메모리 조회/저장 | | /api/memory/{key} | DELETE | 메모리 삭제 | | /api/cost | GET | 비용 추적 | | /api/health | GET | 컴포넌트 헬스 | | /api/sessions | GET | WS 세션 목록 | | /api/sessions/{id} | DELETE | 세션 삭제 | | /api/pairing/initiate | POST | 페어링 시작 | | /api/devices | GET | 페어링된 디바이스 | | /api/devices/{id} | DELETE | 디바이스 해제 | | /api/canvas/{id} | GET/POST/DELETE | Live Canvas | | /api/events | GET | SSE 스트림 |

모든 /api/* 요청: Bearer token 필수 (require_pairing = true 시)


Admin 엔드포인트 (localhost 전용)

POST /admin/shutdown      → graceful shutdown
GET  /admin/paircode      → 현재 페어링 코드 조회
POST /admin/paircode/new  → 새 페어링 코드 생성
fn require_localhost(peer: &SocketAddr) -> Result<()> {
    if peer.ip().is_loopback() { Ok(()) }
    else { Err((403, "Admin restricted to localhost")) }
}

zeroclaw gateway --stop 같은 CLI 명령이 이 엔드포인트를 사용.


Tunnel 연동

[tunnel]
provider = "ngrok"  # none / ngrok / cloudflare / bore / localtunnel

실행 시:
  🔗 Starting ngrok tunnel...
  🌐 Tunnel active: https://abcd1234.ngrok.io

allow_public_bind = false (기본)면 tunnel이 없는 한 공개 바인드 불가. tunnel이 설정되면 인터넷에서 https://xxx.ngrok.io/webhook으로 접근 가능.


config.toml 설정

[gateway]
host = "127.0.0.1"          # 기본값 (로컬만)
port = 3000
require_pairing = true       # 페어링 강제
allow_public_bind = false    # 인터넷 노출 차단
session_persistence = true   # WS 채팅 세션 SQLite 저장
session_ttl_hours = 24       # 세션 유지 시간
path_prefix = ""             # 리버스 프록시 경로 (/zeroclaw 등)
trust_forwarded_headers = false  # nginx 뒤에 있으면 true
pair_rate_limit_per_minute = 5   # /pair 요청 제한
webhook_rate_limit_per_minute = 60
rate_limit_max_keys = 10000
idempotency_ttl_secs = 300
idempotency_max_keys = 10000
paired_tokens = ["sha256-hex-of-token", ...]  # 자동 저장됨

[channels_config.webhook]
secret = "my-webhook-secret"  # X-Webhook-Secret 인증

[tunnel]
provider = "ngrok"

보안 요약

Layer 1: public bind 거부 (127.0.0.1 기본)
Layer 2: Tunnel (외부 노출 시 HTTPS 강제)
Layer 3: PairingGuard (Bearer token 인증)
Layer 4: Webhook secret (X-Webhook-Secret, SHA-256 비교)
Layer 5: HMAC 서명 검증 (WhatsApp/Linq/Nextcloud 각자)
Layer 6: Rate limiting (IP 기반, 슬라이딩 윈도우)
Layer 7: Body size limit (64KB)
Layer 8: Request timeout (30초, slow-loris 방지)
Layer 9: Admin 엔드포인트 localhost 전용

관련