본문으로 건너뛰기

© 2026 Molayo

HN분석2026. 05. 07. 09:34

에이전트 해니스는 샌드박스 외부에 있어야 함

요약

본 기사는 LLM 기반 에이전트의 핵심 구성 요소인 '에이전트 해니스(Agent Harness)'가 어디에서 실행되어야 하는지에 대한 아키텍처적 논쟁을 다룹니다. 전통적인 방식은 루프를 작업 중인 코드와 같은 컨테이너 내부에 두는 것이지만, 필자는 여러 사용자 환경과 장기 실행 워크플로우의 요구사항을 충족하기 위해 해니스를 샌드박스 외부(백엔드)에 배치해야 한다고 주장합니다. 이는 인증 정보 보호, 자원 관리 용이성, 그리고 특히 다중 사용자 및 내구성(durability) 측면에서 큰 이점을 제공합니다.

핵심 포인트

  • **해니스 위치의 중요성:** 에이전트 해니스는 LLM 호출 루프를 구성하며, 그 실행 위치는 보안 속성, 실패 모드, 확장성에 결정적인 영향을 미칩니다.
  • **외부 배치(Out-of-Sandbox)의 이점:** 해니스를 샌드박스 외부에 두면 인증 정보(API 키, 사용자 토큰 등)가 안전하게 유지되고, 에이전트가 명령을 실행할 때만 자원을 프로비저닝/비활성화하여 효율적입니다.
  • **내구성 및 공유성 확보:** 외부 배치는 장시간 실행되는 세션의 내구성을 보장하며, 여러 사용자가 동일한 에이전트를 공유하는 다중 사용자 환경에서 분산 파일 시스템 문제를 해결합니다.
  • **기술 스택 제안:** 장기 실행 워크플로우를 위해 Inngest와 같은 이벤트 기반 플랫폼을 사용하여 루프를 구현하고, 빠른 콜드 스타트가 필요한 샌드박스 관리를 위해 Blaxel과 같은 기술을 활용할 것을 제안합니다.

에이전트 해니스 (Agent Harness) 는 LLM 을 구동하는 루프입니다. 프롬프트를 보내고 응답을 받으며, 모델이 요청한 도구 호출을 실행하고 결과를 다시 입력하여 모델을 종료할 때까지 반복합니다. 모든 프로덕션 에이전트는 하나씩 가집니다. 문제는 어디에서 실행되는지입니다.

두 가지 답변이 있습니다. 서로 다른 보안 속성, 실패 모드, 그리고 에이전트가 할 수 있는 것에 대한 영향을 가집니다. 단일 사용자 에이전트 (노트북에 있는 한 엔지니어) 나 다중 사용자 에이전트 (동일한 조직의 수십 명 엔지니어가 같은 에이전트를 공유) 를 구축하는지에 따라 트레이드오프는 다르게 보입니다. 우리는 다중 사용자 캠프에 속하며, 이는 단일 사용자 빌더들이 겪지 않는 문제를 표면화합니다.

루프는 작업 중인 코드와 동일한 컨테이너에 lives 합니다. LLM 호출은 컨테이너 내부에서 나갑니다. 도구 호출 (bash, read, write) 은 로컬로 실행됩니다. 해니스가 추적하는 스킬, 메모리, 그리고 모든 다른 것은 컨테이너의 파일 시스템에 있는 파일입니다.

이것이 claude 를 노트북에서 실행할 때 수행하는 것과 원격 컨테이너에서 Claude Code 를 스피닝 업할 때 보입니다. 단일 사용자 에이전트를 구축한다면 Claude Code SDK 를 가져와 작동하는 것을 발송할 수 있습니다.

루프는 백엔드에서 실행됩니다. 도구를 실행해야 할 때 샌드박스로 API 를 호출합니다. 샌드박스는 도구를 실행하고 결과를 반환합니다. 루프는 결코 샌드박스에 들어가지 않습니다.

샌드박스 내부에 해니스를 실행하는 것은 몇 가지 장점이 있습니다. 실행 모델은 간단합니다: 하나의 컨테이너, 하나의 프로세스 트리, 하나의 파일 시스템, 하나의 라이프사이클입니다. 오프더샤프 해니스를 그대로 재사용할 수 있습니다. 스킬과 메모리는 로컬 파일 시스템을 가정하고 있으므로 변경 없이 작동하며 하나를 받습니다.

샌드박스 외부에 해니스를 실행하면 내부 모델이 할 수 없는 것을 얻습니다.

인증 정보가 샌드박스 밖으로 나갑니다. 루프는 LLM API 키, 사용자 토큰, 데이터베이스 접근을 유지합니다. 샌드박스는 에이전트가 작업을 수행하기 위해 필요한 환경만 보유합니다. 에이전트가 탈출할 것이 아무것도 없으므로 권한 모델을 강제할 필요도 없고 자격 증명 유출을 제어할 필요도 없습니다.

에이전트가 사용하지 않을 때 샌드박스를 일시 정지할 수 있습니다. 에이전트가 하는 많은 것은 샌드박스가 전혀 필요하지 않습니다: 사고, API 호출, 요약, CI 대기 등. 일부 세션은 샌드박스를 전혀 만지지 않습니다. 해니스가 외부라면 에이전트가 명령을 실행해야 할 때만 하나를 프로비저닝하고 비활성화할 수 있습니다. 해니스가 샌드박스 내부에 lives 하면 루프가 실행 중인 것을 일시 정지할 수 없습니다.

샌드박스는 소총 (Cattle) 이 됩니다. 한 세션 중간에 죽으면 루프는 새로운 하나를 프로비저닝하고 계속합니다. 해니스가 내부에 lives 할 때 샌드박스 세션이며, 잃으면 세션을 잃습니다.

다중 사용자는 분산 파일 시스템 문제가不再是됩니다. 동일한 조직의 여러 엔지니어가 같은 에이전트를 실행합니다. 스킬을 공유하며, 메모리를 공유하며, 때로는 병렬로 동일한 사건을 조사합니다. 해니스가 샌드박스 외부에 lives 할 때 이것은 공유 데이터베이스입니다. 내부에 lives 할 때는 돌아올 분산 파일 시스템 문제입니다.

오프더샤프 로컬 해니스는 루프를 밖으로 옮기면 작동하지 않습니다. 모두 로컬 파일 시스템을 가정하기 때문입니다. 내구성 실행이 당신의 문제가 됩니다. 에이전트 세션은 수 시간 동안 실행될 수 있으며 배포를 견뎌야 합니다. 그리고 해니스와 샌드박스가 서로 다른 머신에 lives 할 때 "파일 시스템" 은 지목할 수 있는 것이不再是됩니다.

우리는 외부 모델을 선택했습니다. 이 포스트의 나머지 부분은 이를 작동시키기 위해 해결해야 했던 세 가지 사항에 대해 다룹니다.

Agent loop 은 장시간 실행되는 함수입니다. 최소 몇 분, 우리 경우에는 수 시간입니다. 롤링 배포 (rolling deploys), 스케일 이벤트 (scale events), 인스턴스 장애를 견뉜어야 합니다. API 서버에서 루프를 메모리에 유지하면 첫 번째 버전 릴리스 시 죽습니다.

우리는 이미 Inngest 에서 CI ingestion pipeline 을 실행하고 있습니다 (이전에 다룬 바 있습니다). 이를 agent loop 으로 확장하는 것은 동일한 이유로 같은 결정입니다: 좋은 DX, 자체 클러스터 운영 불필요, 그리고 Temporal 의 완전한 일반성 필요 없음. 루프는 Inngest 함수입니다. 각 턴은 단계이며, Inngest 는 각각 체크포인트 (checkpoint) 합니다. 서버가 재시작되면 루프는 이전 위치에서 계속합니다.

루프는 대부분의 시간이 중단된 상태 (suspended) 입니다: LLM 호출 중, 툴 호출 사이, CI 와 같은 장시간 워크플로우를 기다리는 동안. 우리는 샌드박스를 또한 중단하고 싶으며, 에이전트가 명령을 실행할 때만 활성화합니다. 문제는 cold starts 입니다. Cold sandbox 는 몇 초가 걸려 시작되는데, 이는 인터랙티브 턴 내부에서 영원한 (forever) 것입니다.

우리는 Blaxel 을 사용합니다. Blaxel 은 대기 중 (standby) 에서 25ms 를 재개 (resume) 합니다. 에이전트가 명령을 실행하지 않을 때 샌드박스를 중단하고, 명령이 실행되는 순간 재개합니다. 25ms 는 에이전트가 샌드박스가 사라졌음을 알 수 없을 정도로 낮습니다.

현대적인 agent harness 는 단순히 bash 와 LLM 만 아닙니다. 그들은 skills (에이전트가 필요시 읽는 프롬프트 조각), memories (에이전트나 사용자가 작성하는 메모), subagents, plans, todo lists 를 포함합니다. 모두 로컬 파일 시스템을 가정합니다. skill 은 .claude/skills/foo.md 의 파일입니다. memory 는 .claude/memory/MEMORY.md 의 파일입니다. harness 는 소스 코드와 동일한 readwrite 도구로 이를 읽습니다.

이것은 노트북에서는 작동합니다. harness 가 샌드박스 외부일 때는 작동하지 않습니다.

샌드박스는 disposable 입니다. 우리는 그것을 ephemeral 로 취급합니다: 중단, 재개, 종료, 재생성 (respawn). 그것이 죽고 새로운 것을 시작하면 에이전트가 .claude/memory/MEMORY.md 에 작성한 것은 사라집니다. 세션별로 장시간 샌드박스를 유지하여 상태를 보존할 수 있지만, 그러면 세션당 하나의 샌드박스를 돌보는 babysitting 으로 돌아갑니다. 그리고 원하는 모든 다른 속성을 잃습니다.

다른 문제는 multi-user 입니다. 사용자의 노트북은 한 사람을 위한 에이전트를 실행합니다. 우리의 에이전트는 조직 내 수십 명의 엔지니어를 위해 실행됩니다. skills 는 조직적입니다: 팀의 모든 사람이 동일한 triage playbook 을 공유합니다. memories 도 마찬가지입니다. 에이전트가 월요일에 팀 X 가 항상 release branch 에서 배포함을 학습하면, 같은 팀의 다른 엔지니어의 화요일 세션은 이를 알아야 합니다.

샌드박스가 로컬 파일 시스템을 가진 것처럼 pretend 하고, 작성하고, 나가는 길에 모든 것을 데이터베이스로 동기화할 수 있습니다. 이는 single-user 경우에서 작동합니다. multi-user 경우, 당신은 분산 파일 시스템을 구축했습니다. 동시에 실행되는 두 세션이 같은 memory 파일을 작성하고, 이를 조정 (reconcile) 해야 합니다. 세 명의 엔지니어가 동일한 incident 에서 에이전트를 트리거하면, 세션이 끝날 때까지 모두 오래된 상태 (stale state) 를 봅니다. Conflict resolution, eventual consistency, cache invalidation.

정답은 pretend 을 멈추는 것입니다. memories 와 skills 을 데이터베이스에 둡니다. harness 는 에이전트가 요청할 때 데이터베이스에서 읽으며, 에이전트가 업데이트할 때 다시 작성합니다.

하지만 우리는 여전히 에이전트가 파일을 생각하도록 원합니다.

harness 는 파일 시스템 접근을 virtualizes 합니다. 에이전트는 하나의 read 도구, 하나의 write 도구를 갖습니다.

도구 (tool). 에이전트가 이를 호출할 때, 허브는 경로를 확인하고 해당 경로가 의미하는 바에 따라 호출을 라우팅합니다.

워크스페이스 하위의 경로는 항상 그랬던 대로 샌드박스로 이동합니다. 스킬과 메모리 네임스페이스 하위의 경로는 데이터베이스로 이동합니다. 메모리 경로의 작성은 조직에 스코프된 데이터베이스 트랜잭션입니다. 메모리 경로의 읽기는 역시 데이터베이스에서 이루어지므로, 같은 조직 내의 두 개의 병렬 세션이 메모리가 작성되는 순간 동일한 메모리를 볼 수 있습니다.

에이전트는 그 차이를 알지 못합니다. 에이전트가 알 수 있는 범위에서는 파일 시스템이 존재하며 파일을 읽고 씁니다. 일부 파일은 Postgres 에 lives 합니다. 일부는 국가를 가로지르는 샌드박스에 lives 합니다.

명백한 대안으로는 에이전트에게 memory_readmemory_write 도구를 readwrite 도구와 함께 제공하는 것입니다. 이는 작동하며, 이것이 대부분의 사람들이 하는 방식입니다. 우리는 가상화 레이어를 갖기 전에 이것을我们自己 했습니다.

문제는 더 많은 도구가 에이전트를 나쁘게 만든다는 점입니다. 각 도구는 모델이 다른 모든 도구에 기울이는 주의를 희석시키고 프롬프트를 길어지며, 모델이 각 턴에 대해 결정해야 할 또 다른 것을 추가합니다. readmemory_read와 같이 거의 같은 일을 하는 두 도구는 특히 나쁘며, 왜냐하면 모델은 컨텍스트에서 이를 구분해야 하며 때로는 잘못 선택할 수 있기 때문입니다.

다른 이유는 더 중요합니다. Anthropic 과 모든 프론티어 모델을 훈련하는 사람들은 Claude Code와 유사한 허브에서 강화학습 (RL) 을 거의 확실하게 수행합니다. 이 훈련은 모델을 특정 API 표면 (API surface) 에 대해 좋은 성능을 갖도록 형성합니다: read(path), write(path, content), edit(path, old, new). 만약 memory_read 를 발명한다면, 당신은 훈련된 경로에서 벗어나게 됩니다. 모델이 일반적으로 학습한 것에서 훈련된 정확한 관습에 대해 학습한 것을 뺀 것이 나옵니다.

가상화 인터페이스는 모델이 훈련된 API 표면과 같게 유지하고 데이터베이스의 의미를 우리가 필요로 하는 곳에 백엔드에 배치합니다.

SOTA 는 빠르게 움직입니다. 몇 주마다 새로운 패턴 (서브 에이전트, 계획, 배경 작업) 이 Claude Code 나 유사한 곳에서 도착하며, 그것은 거의 항상 로컬 파일 시스템을 가정합니다. 우리는 대부분의 것을 인터셉트할 수 있지만, 새로운 기능의 출시와 가상화 레이어가 이를 올바르게 처리하는 것 사이에는 항상 간격이 있습니다. Claude Code 를 실행하지 않는 것은 실제 비용입니다.

우리는 Claude Code 의 로컬 레이아웃과 거울을 이루는 경로 접두사 (/skills/, /memory/) 를 선택했으며, 이는 아마도 우리를 찌를 것입니다. Claude Code 의 레이아웃은 여전히 움직이고 있으며, 우리는 모든 것을 마이그레이션해야 할 컨벤션 변경 하나에서 멀어집니다. 올바른 답은 완전히 다른 인터페이스를 노출하는 것일 수 있습니다. 그러나 위의 내용을 보십시오: 전체 목적은 모델이 훈련된 것과 동일한 인터페이스를 유지하는 것이었습니다.

Bash 는 누출입니다. 허브는 read('/skills/foo.md')와 같은 구조화된 도구 호출을 인터셉트할 수 있습니다. 하지만 에이전트는 또한 bash 도구를 가지고 있으며, 그것은 bash 세션에서 grep -r 'foo' /skills/를 실행하는 것을 막지 못합니다. Bash 는 가상화 레이어를 우회하고 /skills/가 존재하지 않는 샌드박스의 실제 파일 시스템에 도달합니다. 우리는 두 가지 최선의 노력 보호로 이를 처리합니다: 시스템 프롬프트는 에이전트가 가상화 네임스페이스를 사용하지 않도록 지시하며, 우리는 tree-sitter 를 사용하여 해당 경로에 도달하는 호출을 포착하기 위해 bash 호출을 파싱합니다. 어느 것도 완벽하지 않습니다. 지금은 충분합니다.

일관성 (Consistency) 은 우리가 아직 답변하지 못한 부분입니다. 같은 조직 내의 두 세션이 모두 메모리를 업데이트할 때, 각 세션은 무엇을 볼까요? 엄격한 직렬화 가능성 (Strict serializability) 이 매력적일 수 있지만, 아마도 틀린 것일 것입니다. 에이전트는 데이터베이스가 아니기 때문입니다. 한 세션을 다른 세션의 쓰기 작업에 대기하게 하면 우리가 답변을 아직 구하지 못한 데드락 패턴 (deadlock patterns) 을 열리게 됩니다. 우리는 키당 마지막 작성자 승법 (last-writer-wins) 을 실행하고 있으며, 이는 우리가 겪은 경우에는 괜찮지만, 거의 확실하게 예측 가능한 방식으로 깨질 것입니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
2

댓글

0