메뉴
HN
Hacker News 28일 전

에이전트 제어부는 샌드박스 밖에 두어야 한다

IMP
8/10
핵심 요약

AI 에이전트를 구동하는 핵심 루프인 '하니스(harness)'를 샌드박스 내부가 아닌 외부(백엔드)에 배치해야 보안성과 안정성을 확보할 수 있다는 주장입니다. 특히 다수의 사용자가 동시에 에이전트를 사용하는 환경에서는 자격 증명(Credential) 유출 방지, 리소스 효율화, 장애 복구 등의 이점이 있어 외부 배치가 필수적입니다. 이 글은 실제 프로덕션 환경에서 하니스를 외부에 두기 위해 해결해야 했던 기술적 과제와 그 이유를 설명합니다.

번역된 본문

에이전트 하니스(harness)는 LLM을 구동하는 루프(loop)입니다. 이 루프는 프롬프트를 보내고 응답을 받은 뒤, 모델이 요청한 도구 호출(툴 콜)을 실행하고 그 결과를 다시 피드백하며, 모델이 작업이 완료되었다고 할 때까지 이 과정을 반복합니다. 모든 프로덕션 에이전트에는 이러한 하니스가 존재합니다. 중요한 것은 이 하니스가 '어디서' 실행되느냐입니다. 정답은 두 가지로 나뉩니다. 두 방식은 보안 속성, 장애 발생 모드(Failure mode), 그리고 에이전트가 수행할 수 있는 작업의 범위에서 서로 다른 함의를 가집다. 또한 단일 사용자용 에이전트(노트북을 사용하는 한 명의 엔지니어)를 구축하는지, 아니면 다중 사용자용 에이전트(같은 조직의 수십 명의 엔지니어가 동일한 에이전트를 공유)를 구축하는지에 따라 트레이드오프가 다르게 보입니다. 저희는 다중 사용자 환경에 해당하며, 이는 단일 사용자 환경을 구축하는 개발자들은 겪지 못하는 문제들을 드러냅니다.

두 가지 아키텍처

  1. 샌드박스 내부의 하니스 루프가 작업 중인 코드와 동일한 컨테이너 내에 존재합니다. LLM 호출은 컨테이너 내부에서 외부로 나가며, 도구 호출(bash, read, write)은 로컬에서 실행됩니다. 하니스가 추적하는 스킬(Skills), 메모리 등은 모두 컨테이너의 파일 시스템에 파일 형태로 저장됩니다. 이는 노트북에서 Claude를 실행할 때, 또는 원격 컨테이너에서 Claude Code를 돌릴 때 작동하는 방식입니다. 단일 사용자 에이전트를 구축 중이라면 Claude Code SDK를 사용해 그대로 작동하는 것을 출시(Ship)할 수 있습니다.

  2. 샌드박스 외부의 하니스 루프는 자체 백엔드에서 실행됩니다. 도구를 실행해야 할 때 API를 통해 샌드박스를 호출합니다. 샌드박스는 도구를 실행하고 결과를 반환하며, 루프 자체는 절대 샌드박스 내부로 들어가지 않습니다.

트레이드오프

샌드박스 내부에서 하니스를 실행하는 방식에는 몇 가지 장점이 있습니다. 실행 모델이 단순합니다. 즉, 하나의 컨테이너, 하나의 프로세스 트리, 하나의 파일 시스템, 하나의 수명 주기(Lifetime)를 가집니다. 기존에 잘 만들어진 오프 셸프(Off-the-shelf) 하니스를 있는 그대로 재사용할 수 있습니다. 스킬과 메모리 역시 로컬 파일 시스템을 기반으로 작동하도록 작성되어 있으므로 수정 없이 그대로 사용할 수 있습니다.

반면 샌드박스 외부에서 하니스를 실행하면 내부 모델에서는 얻을 수 없는 이점들이 있습니다. 먼저 인증 정보(Credentials)를 샌드박스 밖에 안전하게 보관할 수 있습니다. 루프에는 LLM API 키, 사용자 토큰, 데이터베이스 접근 권한이 유지되며, 샌드박스에는 에이전트가 작업을 수행하는 데 필요한 환경만 존재합니다. 샌드박스 내부에는 에이전트가 빠져나가려 해야 할 대상(권한, 토큰 등)이 없으므로, 권한 모델을 강제하거나 자격 증명 유출을 막기 위한 조치를 할 필요가 없습니다.

또한 에이전트가 샌드박스를 사용하지 않을 때 이를 일시 정지(Suspend)할 수 있습니다. 에이전트가 하는 많은 작업(생각하기, API 호출, 요약, CI 대기 등)은 사실 샌드박스가 전혀 필요하지 않습니다. 일부 세션은 샌드박스를 아예 터치하지 않기도 합니다. 하니스가 외부에 있으면 에이전트가 명령을 실행해야 할 때만 샌드박스를 프로비저닝(할당)하고, 유휴 상태가 되면 즉시 일시 정지할 수 있습니다. 하니스가 샌드박스 내부에 있으면 루프가 실행되는 공간 자체를 일시 정지할 수 없으므로 이러한 최적화가 불가능합니다.

샌드박스를 소모성 리소스(Cattle)처럼 다룰 수 있게 됩니다. 세션 도중 하나의 샌드박스가 죽더라도 루프는 새로운 샌드박스를 프로비저닝하고 계속해서 작업을 진행할 수 있습니다. 반면 하니스가 내부에서 실행될 경우 샌드박스가 곧 세션을 의미하므로, 샌드박스를 잃으면 세션도 잃게 됩니다.

결과적으로 다중 사용자 환경 지원이 단순한 '공유 데이터베이스' 문제로 변환됩니다. 같은 조직의 여러 엔지니어가 동일한 에이전트를 사용하며 스킬과 메모리를 공유하고, 때로는 동일한 장애 인시던트를 병렬로 조사하기도 합니다. 하니스가 샌드박스 외부에 있다면 이는 단순히 공유 데이터베이스를 사용하는 문제가 됩니다. 하지만 내부에 있다면 나중에 다룰 분산 파일 시스템 문제로 번지게 됩니다.

물론 루프를 외부로 옮기면 기존의 로컬용 오프 셸프 하니스들은 더 이상 작동하지 않습니다. 이들은 모두 로컬 파일 시스템이 있다고 가정하고 설계되었기 때문입니다. 에이전트 세션은 몇 시간 동안 실행될 수 있고 배포(Deploy) 과정에서도 살아남아야 하므로, 내구성 있는 실행(Durable execution)을 직접 구현해야 하는 과제가 생깁니다. 하니스와 샌드박스가 다른 머신에 존재하게 되므로, 더 이상 가리킬 수 있는 단일 "파일 시스템"이라는 개념은 사라집니다.

저희는 외부 모델을 선택했습니다. 이 글의 나머지 부분에서는 이 방식이 작동하게 만들기 위해 저희가 해결해야 했던 세 가지 과제에 대해 다룹니다.

내구성 있는 실행 (Durable execution) 에이전트 루프는 오래 실행되는(Long-running) 함수입니다. 최소 몇 분, 저희의 경우 몇 시간씩 실행됩니다. 이 과정은 롤링 배포(Rolling deploys), 스케일링 이벤트(Scale events), 인스턴스 장애 속에서도 살아남아야 합니다. API 서버의 메모리에 루프를 올려두는 방식은 새 버전을 배포하는 즉시 데이터가 날아가 버리는 치명적인 단점이 있습니다. 저희는 이미 CI 수집(Ingest) 과정에서...

원문 보기
원문 보기 (영어)
An agent harness is the loop that drives an LLM. It sends a prompt, gets a response, executes the tool calls the model requested, feeds the results back, and repeats until the model says it's done. Every production agent has one. The question is where it runs. There are two answers. They have different security properties, different failure modes, and different implications for what the agent can do. The tradeoffs also look different depending on whether you're building a single-user agent (one engineer on a laptop) or a multi-user one (dozens of engineers in the same organization sharing the same agent). We're in the multi-user camp, which surfaces problems single-user builders don't hit. The two architectures Harness inside the sandbox The loop lives in the same container as the code it's working on. LLM calls go out from inside the container. Tool calls (bash, read, write) execute locally. Skills, memories, and anything else the harness tracks are files on the container's filesystem. This is what claude does when you run it on your laptop, and what it looks like when you spin up Claude Code in a remote container. If you're building a single-user agent, you can grab the Claude Code SDK and ship something that works. Harness outside the sandbox The loop runs on your backend. When it needs to execute a tool, it calls into a sandbox over an API. The sandbox runs the tool and returns the result. The loop never enters the sandbox. Tradeoffs Running the harness inside the sandbox has a few things going for it. The execution model is simple: one container, one process tree, one filesystem, one lifetime. You can reuse off-the-shelf harnesses as-is. Skills and memories work unchanged because they assume a local filesystem and they get one. Running the harness outside the sandbox gets you things the inside model can't. Your credentials stay out of the sandbox. The loop holds the LLM API keys, the user tokens, the database access. The sandbox holds only the environment the agent needs to do its work. There's nothing in there for the agent to escape to, so there's no permission model to enforce and no credential leak to contain. You can suspend the sandbox when the agent isn't using it. A lot of what an agent does doesn't need a sandbox at all: thinking, calling APIs, summarizing, waiting for CI. Some sessions never touch a sandbox. With the harness outside, you provision one only when the agent needs to run a command, and suspend it whenever it's idle. When the harness lives inside the sandbox you can't do any of this, because you can't suspend the thing the loop is running on. Sandboxes become cattle. If one dies mid-session, the loop provisions a new one and keeps going. When the harness runs inside, the sandbox is the session, and losing it loses the session. And multi-user stops being a distributed filesystem problem. Several engineers in the same organization run the same agent. They share skills, they share memories, they sometimes investigate the same incident in parallel. When the harness runs outside the sandbox, this is a shared database. When it runs inside, it's the distributed filesystem problem we'll come back to. Off-the-shelf local harnesses stop working once you move the loop out, because they all assume a local filesystem. Durable execution becomes your problem, because an agent session can run for hours and has to survive deploys. And once the harness and the sandbox live on different machines, "filesystem" stops being a thing you can point at. We picked the outside model. The rest of this post is about the three things we had to solve to make it work. Durable execution An agent loop is a long-running function. Minutes at a minimum, hours in our case. It has to survive rolling deploys, scale events, and instance failures. Keeping the loop in memory on an API server dies the first time you ship a new version. We already run our CI ingestion pipeline on Inngest , which we wrote about in a previous post . Extending it to the agent loop was the same decision for the same reasons: good DX, no cluster to run ourselves, and we didn't need the full generality of Temporal. The loop is an Inngest function. Each turn is a step, and Inngest checkpoints each one. If the server restarts, the loop picks up where it left off. Sandbox lifecycle The loop is suspended most of the time: during LLM calls, between tool calls, while waiting on a long-running workflow like CI. We want the sandbox to be suspended too, and only active when the agent is running a command. The problem is cold starts. A cold sandbox takes seconds to spin up, which is forever inside an interactive turn. We use Blaxel for this. Blaxel gives us 25ms resume from standby. We suspend the sandbox when the agent isn't running a command and resume it the instant it is. 25ms is low enough that the agent can't tell the sandbox was ever gone. The filesystem Modern agent harnesses aren't just bash and an LLM. They have skills (prompt fragments the agent reads on demand), memories (notes the agent writes for itself or the user), subagents, plans, todo lists. All of these assume a local filesystem. A skill is a file at .claude/skills/foo.md . A memory is a file at .claude/memory/MEMORY.md . The harness reads and writes them with the same read and write tools it uses for source code. That works on a laptop. It doesn't work when the harness is outside the sandbox. The sandbox is disposable. We treat it as ephemeral: suspended, resumed, killed, respawned. If it dies and we spin up a new one, whatever the agent wrote to .claude/memory/MEMORY.md is gone. You could keep a long-lived sandbox per session to preserve the state, but then you're back to babysitting one sandbox per session, and you lose every other property you wanted. The other problem is multi-user. A user's laptop runs an agent for one person. Our agent runs for dozens of engineers in the same organization. Skills are organizational: everyone on a team shares the same triage playbook. Memories are too. If the agent learns on Monday that team X always deploys from a release branch, Tuesday's session for a different engineer on the same team should know. You could pretend the sandbox has a local filesystem, write to it, and sync everything to a database on the way out. This works in the single-user case. In the multi-user case, you've just built a distributed filesystem. Two sessions running at the same time write to the same memory file, and you have to reconcile them. Three engineers trigger the agent on the same incident, and they all see stale state until their sessions end. Conflict resolution, eventual consistency, cache invalidation. The clean answer is to stop pretending. Put memories and skills in a database. The harness reads them from the database when the agent asks for them and writes them back when the agent updates them. But we still want the agent to think in terms of files. One interface, two backends The harness virtualizes filesystem access. The agent has one read tool, one write tool, one edit tool. When the agent calls them, the harness looks at the path and routes the call based on what the path means. Paths under the workspace go to the sandbox, the way they always did. Paths under the skill and memory namespaces go to the database. A write to a memory path is a database transaction, scoped to the organization. A read to a memory path comes from the database too, so two parallel sessions in the same org see the same memory the instant it's written. The agent doesn't know the difference. As far as it can tell, there's a filesystem and it reads and writes files. Some of those files live in Postgres. Some live in a sandbox running across the country. Why not just add tools The obvious alternative is to give the agent memory_read and memory_write tools alongside read and write . Th