로컬 CLI 세션을 양방향 클라우드 연결 환경으로 전환하는 방법.
브릿지 시스템은 로컬 CLI 세션을 클라우드 기반 인터페이스(예: claude.ai)와 양방향으로 연결합니다. 단순한 출력 스트리밍이 아니라 권한, 도구 호출, 파일 편집까지 원격에서 제어할 수 있는 완전한 양방향 파이프라인입니다.
bridge/server.ts → bridge/transport.ts → bridge/flushGate.ts → bridge/permissions.ts → remote-control/
핵심 구성 요소:
브릿지는 두 방향의 데이터 흐름을 서로 다른 전송 메커니즘으로 처리합니다.
CLI의 모든 상태 변경(도구 실행 결과, 어시스턴트 응답 스트리밍, 권한 요청 등)은 SSE(Server-Sent Events) 스트림을 통해 클라우드로 전송됩니다. 이 방향은 연속적이고 실시간입니다.
사용자 입력, 권한 승인/거부, 중단 요청 등은 HTTP POST 요청으로 CLI에 전달됩니다. 각 요청은 독립적이며 멱등성을 가집니다.
// bridge/server.ts — 인바운드 메시지 디스패치
switch (message.type) {
case 'user_input':
handleUserInput(message.text)
break
case 'permission_response':
resolvePermission(message.requestId, message.granted)
break
case 'abort':
abortCurrentTurn()
break
}
브릿지 시스템은 두 번의 주요 반복을 거쳤습니다. v1은 단방향 스트리밍에 초점을 맞췄고, v2는 완전한 양방향 제어를 실현했습니다.
| 특성 | Bridge v1 | Bridge v2 |
|---|---|---|
| 전송 | 순수 SSE | HybridTransport (SSE + HTTP POST) |
| 방향 | 단방향 (출력만) | 양방향 (입력 + 출력) |
| 권한 처리 | 로컬 전용 | 원격 승인/거부 지원 |
| 배압 제어 | 없음 | FlushGate |
| 도구 호출 | 로컬 실행만 | 원격 트리거 + 결과 스트리밍 |
v1과 v2는 동시에 활성화될 수 없습니다. 클라이언트가 v2 핸드셰이크를 보내면 서버는 v1 SSE 엔드포인트 응답을 거부합니다. 레거시 클라이언트가 v2 서버에 연결하면 업그레이드 필요 응답을 받습니다.
전송 레이어는 브릿지의 물리적 통신을 추상화합니다.
아웃바운드 전용 전송으로, 클라이언트가 /events 엔드포인트에 GET 요청을 보내면 서버는 SSE 스트림을 열어 상태 변경을 실시간으로 푸시합니다.
// bridge/transport.ts — SSE 이벤트 전송
class SSETransport {
send(event: BridgeEvent): void {
const data = JSON.stringify(event)
this.response.write(`data: ${data}\n\n`)
}
keepAlive(): void {
this.response.write(`: keepalive\n\n`) // SSE 주석으로 연결 유지
}
}
v2에서 도입된 복합 전송입니다. SSETransport를 래핑하면서 인바운드 HTTP POST 핸들러를 추가합니다. 양방향 통신의 핵심 추상화 레이어입니다.
// bridge/transport.ts — HybridTransport 구조
class HybridTransport {
private sse: SSETransport
private flushGate: FlushGate
async send(event: BridgeEvent): Promise<void> {
await this.flushGate.waitForFlush() // 배압 대기
this.sse.send(event)
}
handleInbound(req: IncomingMessage): void {
const message = parseBody(req)
this.dispatch(message)
}
}
FlushGate는 클라이언트가 이전 메시지를 소비했음을 확인할 때까지 다음 메시지 전송을 차단하는 배압(backpressure) 메커니즘입니다. 빠른 도구 실행이 느린 네트워크 연결을 압도하는 것을 방지합니다.
// bridge/flushGate.ts — 배압 제어
class FlushGate {
private pending: Promise<void> | null = null
private resolve: (() => void) | null = null
async waitForFlush(): Promise<void> {
if (this.pending) {
await this.pending // 이전 flush 완료 대기
}
this.pending = new Promise(r => this.resolve = r)
}
ack(): void {
this.resolve?.() // 클라이언트가 ACK를 보내면 게이트 해제
this.pending = null
}
}
도구 호출이 연속으로 빠르게 완료되는 경우(예: 여러 파일 읽기), CLI는 초당 수십 개의 이벤트를 생성할 수 있습니다. 클라우드 클라이언트가 이를 렌더링하는 속도는 네트워크 지연과 UI 렌더링 시간에 제약됩니다. FlushGate 없이는 메시지가 서버 측 버퍼에 쌓여 메모리 증가와 UI 끊김을 유발합니다.
ACK 메커니즘은 TCP의 흐름 제어와 유사한 애플리케이션 레벨 배압을 제공합니다. 클라이언트가 /ack 엔드포인트에 POST를 보내면 FlushGate가 해제되어 다음 이벤트 전송이 허용됩니다.
원격 인터페이스에서 도구 실행 권한을 승인/거부할 수 있어야 합니다. 권한 브릿지는 로컬 권한 시스템과 원격 클라이언트 사이의 프록시 역할을 합니다.
// bridge/permissions.ts — 원격 권한 요청 흐름
async function requestPermissionRemotely(
tool: ToolName,
args: ToolArgs,
): Promise<PermissionResult> {
const requestId = randomUUID()
// 1. 클라우드에 권한 요청 이벤트 전송
bridge.send({ type: 'permission_request', requestId, tool, args })
// 2. 사용자 응답 대기 (승인 또는 거부)
return await waitForPermissionResponse(requestId)
}
권한 요청은 도구 이름, 인수, 요청 ID를 포함합니다. 클라우드 UI는 이를 표시하고 사용자의 승인/거부를 permission_response 메시지로 반환합니다. 타임아웃 시 자동 거부됩니다.
원격 제어 모드는 보안에 민감하므로 5개의 자격 게이트를 모두 통과해야 활성화됩니다.
--remote-control 플래그로 시작되어야 함// remote-control/gates.ts — 5개 게이트 검증
async function validateRemoteControlEligibility(): Promise<GateResult> {
const gates = [
checkAuthGate(), // OAuth 토큰 + 범위
checkSubscriptionGate(), // 플랜 레벨
checkFeatureFlagGate(), // GrowthBook
checkSessionGate(), // --remote-control 플래그
checkNetworkGate(), // 브릿지 핸드셰이크
]
const results = await Promise.all(gates)
return results.every(r => r.passed)
? { eligible: true }
: { eligible: false, reason: results.find(r => !r.passed)!.reason }
}
게이트는 순서에 독립적이지만 Promise.all로 병렬 실행됩니다. 하나라도 실패하면 첫 번째 실패 이유가 사용자에게 표시됩니다. 네트워크 게이트는 다른 게이트보다 느릴 수 있으므로 전체 검증 시간은 네트워크 게이트의 지연에 의해 결정됩니다.
CCR(Claude Code Remote) 환경에서 브릿지는 컨테이너 내부에서 실행되며, 업스트림 프록시를 통해 외부와 통신합니다.
CCR 컨테이너 내부에서 브릿지 서버는 자동으로 시작됩니다. 업스트림 프록시가 WebSocket 연결을 중계하며, 브릿지는 프록시의 로컬 포트에 바인딩합니다. 환경 변수 CLAUDE_CODE_BRIDGE_PORT가 포트를 지정합니다.
로컬 개발 환경에서도 claude bridge-server 명령으로 독립적인 브릿지 서버를 실행할 수 있습니다. 이를 통해 커스텀 클라이언트(IDE 확장, 웹 UI 등)가 CLI 세션에 연결할 수 있습니다.
// 독립 브릿지 서버 시작
$ claude bridge-server --port 3456
// 클라이언트 연결
// SSE: GET http://localhost:3456/events
// 입력: POST http://localhost:3456/message
// ACK: POST http://localhost:3456/ack
독립 브릿지 서버는 localhost에만 바인딩되므로 외부 네트워크 접근이 차단됩니다. 추가로 세션별 토큰이 발급되어 모든 요청에 Authorization: Bearer <token> 헤더가 필요합니다. 토큰 없는 요청은 즉시 401 Unauthorized로 거부됩니다.
Q1. Bridge v2에서 아웃바운드(CLI → 클라우드)와 인바운드(클라우드 → CLI) 흐름에 각각 사용되는 전송 메커니즘은?
Q2. FlushGate의 주된 목적은 무엇인가요?
/ack 엔드포인트에 POST를 보내야 게이트가 해제되어 다음 이벤트 전송이 허용됩니다. 이를 통해 빠른 도구 실행이 느린 클라이언트를 압도하는 것을 방지합니다.Q3. 원격 제어 모드 활성화에 필요한 5개 자격 게이트에 포함되지 않는 것은?
--remote-control 플래그), 네트워크(핸드셰이크)입니다. 로컬 파일시스템 쓰기 권한은 별도의 권한 시스템에서 처리되며 원격 제어 자격 게이트에는 포함되지 않습니다.Q4. 독립 브릿지 서버의 보안 모델은 어떻게 구성되어 있나요?
Authorization: Bearer 헤더를 요구합니다.Q5. Bridge v1에서 v2로의 주요 변경점은 무엇인가요?