로컬 AI 에이전트를 위한 아웃바운드 전용 WebSocket 브리지 구축
출처: Dev.to
책상을 떠나는 순간, 제어권을 잃게 됩니다. 휴대폰으로 에이전트 작업을 시작하거나, 커피숍에서 장시간 파이프라인을 모니터링하거나, 밤새 실행될 작업을 예약할 실질적인 방법이 없습니다.
내가 찾은 모든 해결책은 같은 트레이드오프를 가지고 있었습니다. 포트를 열거나, 터널 데몬을 설치하거나, 코드를 누군가의 클라우드에 업로드해야 했죠. 로컬 파일 시스템에 접근할 수 있는 인프라에 이런 방법들은 전혀 맞지 않았습니다.
그래서 저는 CTRL NODE를 만들었습니다 — 로컬 AI 에이전트를 위한 브라우저 기반 제어 평면입니다. 핵심은 Bridge라는 프로세스로, 가벼운 Node.js 데몬이 여러분의 머신에서 실행되면서 절대 인바운드 연결을 받지 않고 클라우드에 연결합니다.
이 글에서는 그 작동 방식, 설계 선택이 중요한 이유, 그리고 실제 코드가 어떻게 생겼는지를 다룹니다.
전통적인 접근 방식
가장 단순한 방법은 로컬 에이전트 런타임을 포트에 노출하고 클라우드가 그 안으로 들어오게 하는 것입니다. ngrok 같은 도구가 바로 이 방식을 사용합니다 — 로컬호스트에 대한 리버스 프록시를 만들죠. 동작은 하지만 다음과 같은 실질적인 비용이 있습니다:
- 포트 개방 = 공격 표면. 모든
ngrok터널은 공개적으로 접근 가능한 엔드포인트입니다. 인증이 뚝 끊기면 누군가가 여러분의 에이전트와 대화할 수 있습니다. - 제3자 트래픽 중계. 프롬프트, 파일 경로, 에이전트 응답이 모두
ngrok인프라를 통해 전달됩니다. - 데몬 복잡성. 여러분이 직접 작성하지 않았고 쉽게 감사할 수 없는 지속적인 인프라를 운영하게 됩니다.
대안: 연결 방향 뒤집기
Bridge는 클라우드로 나가는 연결을 엽니다. 클라우드는 그 연결을 통해 명령을 푸시하고, 로컬 머신은 절대 공개 포트를 리스닝하지 않습니다.
Your machine ctrlnode.ai cloud
──────────────────────────────────────────────────
Bridge ──── ws:// connect() ────▶ WebSocket server
◀─── {action: "run_task", ...} ────────────
───── stdout/stderr events ──────────────▶
이 패턴은 IoT 디바이스, CI 에이전트(예: GitHub Actions 러너), 원격 데스크톱 클라이언트에서도 동일하게 사용됩니다. 클라우드가 먼저 연결을 시작하는 것이 아니라 대기합니다.
websocket.ts 핵심 코드
export function connect(): void {
const url = buildWsUrl();
ws = new WebSocket(url, { headers: buildAuthHeaders() });
ws.on("open", () => {
logger.info("Bridge connected to SAAS");
flushPendingQueue();
startHeartbeat();
});
ws.on("message", (data: WebSocket.RawData) => {
const message = JSON.parse(data.toString()) as InboundMessage;
handleInboundMessage(message);
});
ws.on("close", (code: number, reason: Buffer) => {
stopHeartbeat();
if (isAuthError(code, reason.toString())) {
logger.warn(`Auth error (${code}), retrying in ${AUTH_RETRY_MS / 1000}s`);
setTimeout(connect, AUTH_RETRY_MS);
} else {
scheduleReconnect();
}
});
ws.on("error", (err: Error) => {
logger.error(`WebSocket error: ${err.message}`);
});
}
눈여겨볼 점 3가지
- 인증 오류는 더 긴 대기시간을 갖습니다. 서버가
1008(Policy Violation)이나1002를 반환하거나, 이유 문자열에"401"/"403"/"Unauthorized"가 포함되면 30초를 기다렸다가 재시도합니다. 인증이 거부된 엔드포인트를 계속 두드리는 것은 의미 없고 소음만 발생합니다. - 정상적인 종료는 지수 백오프를 트리거합니다.
scheduleReconnect()는 표준 백오프를 사용해 일시적인 네트워크 오류가 로그를 폭풍처럼 채우는 것을 방지합니다.