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 전용
관련
- channel-system — Telegram/Discord 등 플랫폼 채널 (Gateway와 별개)
- security-policy — SecurityPolicy, AutonomyLevel
- memory-system — auto_save 메모리 저장
- agent-loop — run_gateway_chat_with_tools → process_message
- config-schema — [gateway] 설정 섹션
- overview — ZeroClaw 학습 지도