본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 15. 10:59

무모하지 않게 원격으로 전환하기: Multi-LLM 오케스트레이션(Orchestration)과 llm-cli-gateway 2.9.0의 새로운

요약

llm-cli-gateway 2.9.0 업데이트를 통해 Multi-LLM 오케스트레이션의 원격 실행 환경을 지원합니다. MCP 서버를 HTTP 기반으로 전환하여 인증 및 보안 경계를 강화했으며, 여러 모델을 활용한 레드팀 테스트와 합의 체크가 가능합니다.

핵심 포인트

  • llm-cli-gateway 2.9.0에서 HTTP 기반 원격 실행 지원
  • OAuth 2.0 및 베어러 토큰을 통한 보안 및 인증 강화
  • 다양한 LLM 벤더를 단일 MCP 서버로 통합 관리
  • Multi-LLM 오케스트레이션을 통한 레드팀 및 합의 체크 기능

이 시리즈의 이전 포스트들은 게이트웨이가 호출할 수 있는 것들(5개 제공업체에 걸친 캐시 인식 스포닝 (cache-aware spawning), Codex 리뷰 게이트, CLI 대 API 논쟁)에 관한 것이었으며, 바로 전 포스트는 도구로 나타나지 않는 부분들, 즉 업스트림 추적 (upstream-tracking)과 프로젝트의 프런트 도어 (front door)가 된 웹사이트에 관한 것이었습니다. 이번 포스트는 다른 종류의 프런트 도어에 관한 것입니다. 이제 게이트웨이는 실제 인증 (authentication) 뒤에서 HTTP를 통해 리스닝할 수 있으며, 호출자들이 서로의 작업 내용을 읽을 수 없도록 하면서도 하나 이상의 호출자에게 서비스를 제공할 수 있습니다. 이것은 단순한 토글 (toggle) 설정처럼 들릴 수도 있지만, 그렇지 않습니다. MCP 서버를 로컬 파이프 (local pipe)에서 네트워크 포트 (network port)로 옮기는 것은 신뢰 경계 (trust boundary)를 완전히 변화시키며, 2.9.0 버전은 제가 원격 경로가 준비되었다고 누구에게 알리기 전에, 바로 그 표면 (surface)에 대해 정확히 Multi-LLM 레드팀 (red-team) 테스트를 수행하여 발견된 17가지의 모든 취약점을 해결한 릴리스입니다.

요약 버전: llm-cli-gateway는 5개의 벤더 CLI (Claude Code 2.1.177, Codex 0.139.0, Google의 Antigravity agy 1.0.8, Grok 0.2.51, Mistral Vibe 2.14.1)를 단일하고 통일된 도구 표면 (tool surface) 뒤로 감싸는 하나의 Model Context Protocol (MCP) 서버입니다. 이를 통해 하나의 오케스트레이팅 에이전트 (orchestrating agent)가 작업을 여러 모델로 분산시키고, 독립적인 의견을 수집하며, 레드팀 (red-team) 또는 합의 체크 (consensus check)를 실행하고, 이 모든 과정에서 내구성이 있는 세션 (session) 및 작업 상태 (job state)를 유지할 수 있습니다. 최근까지 이러한 방식은 localhost 상의 stdio를 통해서만 의미가 있었습니다. 2.9.0 버전부터는 동일한 서버가 정적 베어러 토큰 (static bearer token) 또는 내장된 OAuth 2.0 인증 서버 (기본적으로 PKCE 활성화, 선택 사항인 사용자 동의 게이트, 그리고 자체 ID 인식 프록시 (identity-aware proxy)를 앞에 둘 때를 위한 신뢰할 수 있는 주체 헤더 (trusted-principal-header) 접합부 포함)를 통해 HTTP 상에서 실행됩니다. 모든 세션, 작업 및 저장된 요청에는 소유자 주체 (owner principal)가 찍히며 액세스는 주체별로 강제됩니다. 워크스페이스 (workspace)가 등록되지 않으면 원격 제공업체 호출은 거부되며, 구성이 위험할 경우 전체 시스템은 개방형 (open)이 아닌 폐쇄형 (fail closed)으로 작동합니다.

Long version은 아래에 있으며, 항상 그렇듯 다음과 같은 구성을 따릅니다: 기능 설명, 실제 원격 옵션, 실행된 시나리오 스택, 그리고 하단에 숨겨두는 대신 미리 명시한 주의 사항입니다.

게이트웨이가 실제로 가능하게 하는 것

원격 기능에 대해 이야기하기 전에, 이 도구가 무엇을 하는지 정확히 짚고 넘어갈 가치가 있습니다. 왜냐하면 "Multi-LLM 오케스트레이션 (Multi-LLM orchestration)"이라는 문구는 도구의 이름을 명시하기 전까지는 아무런 의미가 없는 종류의 표현이기 때문입니다.

각 제공업체(provider)는 동기(synchronous) 방식과 비동기(asynchronous) 방식의 두 가지 도구를 갖게 됩니다: claude_requestclaude_request_async이며, codex, gemini, grok, mistral에 대해서도 이와 일치하는 쌍이 제공됩니다. Codex의 경우 추가적으로 codex_fork_session을 노출합니다. 추론 세션(reasoning session)을 브랜치로 포크(forking)하는 것은 숨기기보다는 드러낼 가치가 있는 Codex만의 고유한 기능이기 때문입니다. 오케스트레이션 에이전트(예: 노트북의 Claude Code)는 다른 MCP 도구를 호출하는 것과 동일한 방식으로 이들을 호출하며, 게이트웨이는 프로세스 생성(spawn), 타임아웃(timeout), 출력 크기 제한(output-size cap), 서킷 브레이커를 포함한 재시도(retry-with-circuit-breaker), 토큰 계산(token accounting), 그리고 구조화된 에러(structured error)를 처리합니다.

가공되지 않은 제공자 호출(raw provider calls) 위에는 검증 레이어(validation layer)가 존재하며, 이는 제가 가장 자주 사용하는 부분입니다: validate_with_models는 동일한 주장을 여러 모델에 독립적으로 보내어 모델들이 서로 동의하는 부분과 동의하지 않는 부분을 보고합니다; second_opinion은 단일 모델 버전입니다; red_team_review는 적대적 검토(adversarial pass)를 실행합니다; consensus_check는 모델 세트가 단순히 모호하게 고개를 끄덕이는 것이 아니라, 특정 주장에 대해 실제로 합의하는지 확인합니다; compare_answers는 제공자 호출을 전혀 사용하지 않고 로컬 차이(local diff)를 수행합니다; synthesize_validation은 수집된 결과에 대해 판사(judge)를 실행합니다; 그리고 ask_model은 의도적으로 단순하게 설계된 "그저 모델 하나에게 이것을 물어보세요" 방식의 진입점입니다. 이것이 중요한 이유는 단일 모델의 확신에 찬 답변은 단 하나의 샘플(sample of one)일 뿐이기 때문입니다. 위험을 수반하는 어떤 작업(마이그레이션, 보안 주장, 아키텍처 결정 등)에 대해서라면, 저는 잘 작성된 문단 하나를 신뢰하기보다 토큰을 3~4배 더 지불하더라도, 그중 두 개는 완전히 다른 벤더 제품군(vendor family)에서 가져온 세 개의 독립적인 읽기 결과를 얻는 쪽을 택하겠습니다.

그 모든 것의 밑바탕에는 상태(state)가 자리 잡고 있습니다. 세션(Sessions)은 CLI별로 대화의 연속성을 유지하며(Claude의 --continue, Codex의 exec resume, Grok의 --resume 등), 대화 내용 없이 최소한의 정보만을 디스크에 저장합니다. 비동기 요청(Async requests)은 내구성을 갖습니다. 동기식 호출(synchronous call)은 45초(SYNC_DEADLINE_MS를 통해 설정 가능)가 지나면 자동으로 지연(defer) 처리되어 작업 ID(job id)를 반환하며, 작업은 백그라운드에서 완료될 때까지 실행됩니다. 이후 사용자는 llm_job_statusllm_job_result로 결과를 수집하거나, llm_job_cancel로 작업을 취소할 수 있습니다. 작업 저장소(job store)는 기본적으로 SQLite를 사용하며(Node의 내장 node:sqlite를 사용하여 컴파일할 네이티브 모듈이 필요 없음), 결과는 30일 동안 보관됩니다. 또한 1시간 이내에 동일한 호출을 다시 발행하면 두 번째 작업을 시작하는 대신 실행 중인 작업에 다시 연결됩니다. 솔직히 말해서, 이러한 내구성(durability)은 가장 화려하지는 않지만 가장 많은 골칫거리를 해결해 주는 기능입니다. 예를 들어 12분 동안 실행되는 긴 레드팀(red-team) 스윕(sweep) 작업이 폴링 래퍼(polling wrapper)의 타임아웃이나 오케스트레이터(orchestrator)의 재시작 때문에 사라지는 일은 발생하지 않습니다. 모든 요청은 동기식(sync)이든 비동기식(async)이든 블랙박스(flight recorder, logs.db)에 기록되며, 여기에는 타이밍, 토큰 사용량, 캐시 통계, 승인 결정(approval decision), 종료 코드(exit code)가 포함됩니다. 이때 비밀 정보(secrets)는 기록되기 전에 마스킹(redacted) 처리됩니다.

토큰 최적화(선택 사항인 프롬프트 및 응답 압축)와 승인 게이트(approvalStrategy:"mcp_managed", 각 요청을 점수화하여 거부할 수 있음)가 이를 완성합니다. 이 중 어느 것도 생소한 것이 아닙니다. 이는 다섯 개의 CLI를 진지하게 오케스트레이션하고자 한다면 어차피 구축해야 할 배관(plumbing) 작업이며, 다섯 번 다시 만들지 않도록 한 번에 한 곳에서 작성된 것입니다.

원격(remote) 방식이 진정으로 어려운 이유

stdio를 통한 localhost 환경에서는 신뢰 모델(trust model)이 단순합니다. 사용자는 단 한 명이며, 바로 당신이고, 파이프(pipe)는 당신의 프로세스 트리 내에서 사적이며, 존재하는 유일한 주체(principal)는 "이 기기 앞에 앉아 있는 사람"뿐입니다. 게이트웨이는 항상 그 주체를 local이라고 불러왔으며, local 호출자는 모든 것이 자신의 것이기 때문에 모든 것을 볼 수 있습니다.

서버가 TCP 포트를 바인딩(bind)하는 순간, 그 모든 가정은 증발합니다. 이제 호출자가 한 명 이상일 수 있고, 호출자들은 서로를 신뢰하지 않을 수 있으며, 소켓에 도착하는 바이트(bytes)는 별도의 증명이 있기 전까지는 신뢰할 수 없는 데이터가 됩니다. 또한 "이 호출자가 이 세션을 읽을 수 있는가?"라는 질문은 더 이상 명확한 답이 있는 질문이 아니게 됩니다. 유혹을 느끼기 쉽고, 실제로 많은 프로젝트가 그 유혹에 굴복하는 것을 보았습니다. 그것은 바로 HTTP 전송(transport)을 구현하면서 "원한다면 토큰을 설정하세요"라는 메모만 남긴 채, 권한 부여(authorisation)는 다른 사람의 문제로 치부해 버리는 것입니다. 저는 그렇게 하고 싶지 않았습니다. 왜냐하면 게이트웨이는 실제 파일을 읽고 쓰는 실제 CLI(Command Line Interface)를 생성하기 때문입니다. 한 테넌트(tenant)의 작업을 다른 테넌트의 뷰(view)로 유출하거나, 인증되지 않은 호출자가 귀하의 리포지토리(repository)에 대해 프로바이더 생성(provider spawn)을 트리거할 수 있는 원격 접점(remote surface)은 편의 기능이 아니라, 친절한 README가 달린 부채(liability)일 뿐입니다.

따라서 원격 경로가 준비되었다고 문서화하기 전에, 저는 4개의 모델을 사용하여 외부 및 내부 MCP 접점에 대해 광범위한 레드팀(red-team) 테스트를 수행했습니다 (Claude가 오케스트레이션(orchestrating)을 담당하고, Codex, Grok, Mistral이 독립적으로 검토하며, 각 모델은 요약본을 수용하는 대신 코드를 바탕으로 주장을 검증함). 그 결과 17개의 확정된 발견 사항을 정리하였고, 그 모든 사항을 해결했습니다. 2.9.0 버전은 바로 그 해결책입니다. 아래의 번호가 매겨진 발견 사항들(F1, F3, F14 등)은 내부 레이블이며, 기능들과 깔끔하게 매핑되기 때문에 여기에만 남겨두었습니다.

구체적인 새로운 원격 옵션들

전송 방식 (Transports). 기본값은 여전히 stdio이며, 아무것도 설정하지 않으면 이전에 가졌던 로컬(local) 동작과 정확히 일치합니다. 전송 방식을 HTTP로 설정하면 게이트웨이는 기본적으로 127.0.0.13333 포트, /mcp 경로에 바인딩되는 Streamable-HTTP MCP 엔드포인트(endpoint)를 구축합니다 (LLM_GATEWAY_HTTP_HOST, LLM_GATEWAY_HTTP_PORT, LLM_GATEWAY_HTTP_PATH). 또한 2.8.0에서 기반 기술이 출시된 Agent Client Protocol 전송 방식도 있으나, 기본적으로는 꺼져 있으며, 추후 단계가 완료되면 별도로 작성할 예정입니다.

정적 베어러 인증 (Static bearer authentication). 가장 간단한 원격 인증 방식입니다. LLM_GATEWAY_AUTH_TOKEN을 설정하면 모든 요청은 이를 베어러 토큰 (bearer token)으로 제시해야 하며, 인증 과정에서 토큰이 타이밍 공격을 통해 유출되지 않도록 상수 시간 (constant time) 비교를 수행합니다. 이는 단일 사용자 원격 설정(본인만 접근 가능한 본인의 서버)에 적합한 선택입니다.

내장된 OAuth 2.0 인가 서버 (A built-in OAuth 2.0 authorisation server). 멀티 클라이언트 환경을 위해, 게이트웨이는 ~/.llm-cli-gateway/config.toml[http.oauth] 섹션에서 설정할 수 있는 자체 OAuth 서버 역할을 할 수 있습니다. 이 서버는 /oauth/authorize, /oauth/token, /oauth/register 엔드포인트를 노출하며, MCP 클라이언트가 스스로를 구성하기 위해 읽는 /.well-known/oauth-protected-resource 메타데이터를 제공합니다. PKCE는 기본적으로 필수 사항이며 (require_pkce = true), 일반 PKCE와 퍼블릭 클라이언트 (public clients) 기능은 꺼져 있습니다. 클라이언트 등록 정책은 보수적인 static_clients가 기본값입니다 (사용자가 선택할 경우 shared_secret 및 개발 전용인 open_dev 사용 가능). 토큰의 기본 유효 기간은 1시간입니다. 내장 서버의 핵심은 OAuth를 지원하는 MCP 클라이언트가 메타데이터를 탐색하고, 등록하거나 사전 구성된 클라이언트를 사용하며, 인가 코드 흐름 (authorisation-code flow)을 실행하여 도구(tools)를 호출하기 시작할 수 있다는 점입니다. 이를 통해 원격 경로를 테스트하기 위해 별도의 ID 관리 제품을 추가로 구축할 필요가 없습니다.

선택적 인간 동의 게이트 (An opt-in human-consent gate (F14b)). 클라이언트가 요청하는 즉시 인간의 개입 없이 토큰을 발행하는 인가 코드 흐름은, 누군가 "클라이언트가 정중하게 요청했다"는 것이 리소스 소유자의 인증을 의미하지 않는다는 점을 지적하기 전까지는 괜찮아 보일 수 있습니다. 따라서 이제 선택적인 동의 게이트가 추가되었습니다. require_consent = true (또는 LLM_GATEWAY_OAUTH_REQUIRE_CONSENT=1)를 설정하고 scrypt로 해싱된 전용 동의 비밀번호를 지정하면, 코드가 발급되기 전에 운영자가 작은 동의 화면을 통해 승인해야 합니다. 이 화면은 더블 서브밋 쿠키 (double-submit cookie)를 통해 CSRF (cross-site request forgery)로부터 보호됩니다. 모든 배포 환경에서 인간의 개입을 원하는 것은 아니기에 기본적으로는 꺼져 있지만, 이를 필요로 하는 환경을 위해 준비되어 있습니다.

신뢰할 수 있는 주체 헤더(trusted-principal-header) 접점 (F14a). 이미 많은 사람들이 서비스 앞에 적절한 ID 인식 프록시(identity-aware proxy, 예: OAuth2 프록시, ID 인식 게이트웨이, 자체 프런트 도어)를 운영하고 있으며, 게이트웨이가 이를 재발명하는 대신 해당 지점에서 인증을 종료(terminate)하기를 원합니다. 이들을 위한 접점(seam)이 있습니다: LLM_GATEWAY_TRUSTED_PRINCIPAL_HEADER를 프록시가 주입하는 헤더 이름으로 설정하면, 게이트웨이는 해당 헤더의 값을 호출 주체(calling principal)로 채택합니다. 단, 요청이 정적 게이트웨이 베어러(static gateway bearer)로서 인증되었을 때만, 그리고 해당 값이 로그에 무언가를 숨겨 넣을 수 없도록 엄격한 문자 집합(character set)과 일치할 때만 작동합니다. 이는 의도적으로 IdP(Identity Provider, ID 제공자)에 구애받지 않도록 설계된 부분입니다. 게이트웨이는 접점(seam)을 제공하고, 사용자는 이미 신뢰하고 있는 어떤 ID 제공자든 가져오기만 하면 되며, 게이트웨이는 그것이 무엇인지 알 필요가 없습니다.

실패 시 차단(Fail-closed) 태세 (F17). 퍼블릭 클라이언트(public clients) 또는 개발 전용 공개 등록(open registration)과 함께 OAuth를 활성화한 상태에서 서버를 루프백(loopback)이 아닌 주소에 바인딩하려고 하면, 게이트웨이는 운영자의 명시적인 오버라이드(override) 없이는 시작을 거부합니다. 이는 실수로 공개 등록 엔드포인트를 인터넷에 노출하는 것을 방지하며, 게이트웨이는 더 이상 자신이

인증 (Authentication)은 누가 호출하는지를 알려줍니다. 격리 (Isolation)는 그들이 무엇을 볼 수 있는지를 결정하며, 이 둘은 동일한 것이 아닙니다. 2.9.0 버전에서는 모든 세션, 모든 비동기 작업 (async job), 그리고 모든 영속화된 요청 (persisted request)이 생성 시점에 소유자 주체 (owner principal)가 찍히며, 읽기 및 변경 (mutate) 경로에서 이를 강제합니다. 즉, 호출자는 자신의 행 (rows)만 볼 수 있으며, local 호출자는 격리 이전에 존재했던 레거시 행(소유자가 없어 그렇지 않으면 보이지 않게 될 행들)을 추가로 볼 수 있는 것이 전부입니다. session_list는 호출자에 맞춰 필터링되며, session_get, session_delete, 그리고 작업 및 요청 읽기 도구들은 소유자를 확인하여 타인의 데이터 대신

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0