H8 — Sandbox
Note
- 시작 상태:
h08· 완료 상태:h09- 논문: §10.2 Native Sandboxing, §16.6 Safety Architecture by Deployment Context
- macOS 구현과 전후 18회 실행을 마쳤다. 측정 중 발견한 시스템 경로 누락은 보완했으며, 보완 후에는 직접 테스트로 확인했다. model 측정은 보완 전 결과를 그대로 기록한다.
git checkout -b my-h08 h08
hel에서 실행하는 자식/외부 프로세스의 실행 제어 방법은 무엇인가?
들어가며
H8의 전체 구조
H7은 tool 호출을 실행하기 전에 권한을 확인한다. bash로 명령을 실행하도록 허용한 뒤에는 명령 안에서 어떤 파일을 읽거나 고칠지까지 제한하지 않는다. 예를 들어 빌드 명령을 승인하면 빌드 스크립트도 실행되며, 그 스크립트가 프로젝트 밖의 파일에 접근할 수 있다.
H8에서는 외부 프로그램과 그 자식 프로세스에 sandbox(샌드박스)를 적용한다. sandbox는 프로세스가 호스트 자원에 접근할 때 OS가 규칙을 강제하는 실행 환경이다.
Lab에서는 macOS를 기준으로 sandbox를 만든다.
이 글에서 hel 인스턴스는 한 번 시작된 hel 본체와 그 본체가 관리하는 자식 프로세스를 묶은 단위다. 대화 중 tool을 여러 번 호출해도 같은 인스턴스에 속한다. hel을 다시 시작하거나 다른 터미널에서 시작하면 새 인스턴스가 된다.
flowchart TB
CALL["model의 tool 호출"]
subgraph HEL["hel 인스턴스"]
subgraph MAIN["hel 본체 프로세스"]
GATE["H7 권한 검사"]
FILE["내부 파일 tool<br/>hel 코드가 경로 검사"]
LAUNCH["외부 프로그램 실행기<br/>H8 sandbox 정책 적용"]
RESULT["공통 tool 반환 처리<br/>필요하면 spill 저장"]
RESPONSE["대화 이력에 tool 응답 추가"]
GATE -->|허용| FILE
GATE -->|허용| LAUNCH
FILE -->|반환 결과| RESULT
RESULT --> RESPONSE
end
subgraph SANDBOX["sandbox가 적용된 자식 프로세스"]
PROC["bash · rg · 그 자식 프로세스<br/>OS가 파일 접근 규칙 강제"]
end
LAUNCH -->|실행| PROC
PROC -->|반환 결과| RESULT
end
FS[("동일한 호스트 파일시스템<br/>프로젝트 · tmp · spill · 프로그램/라이브러리")]
CALL --> GATE
FILE <-->|hel의 경로 검사 범위| FS
PROC <-->|OS 정책 범위| FS
RESULT -->|spill 파일 작성| FS
바깥 상자는 하나의 hel 인스턴스를 나타낸다. 그 안의 본체와 외부 프로그램은 별개의 프로세스다. sandbox 적용 범위를 hel 안에 그렸다고 해서 본체까지 같은 OS 제한을 받는 것은 아니다.
파일 접근 통로는 두 개다. read_file, write_file, search_replace는 hel 본체 안에서 파일을 연다. 이 통로에서는 hel 코드가 경로를 검사한다. bash와 검색 tool인 glob·grep의 rg는 외부 프로그램으로 실행되므로 OS가 파일 접근을 제한한다. 두 통로 모두 같은 호스트 파일시스템을 사용하며, 실행 결과는 hel의 공통 tool 반환 처리로 돌아온다.
논문과 다른 harness의 실행 격리 수준
Part IV 개요에서 다룬 권한 판단과 실행 격리 중, H8은 실행 중의 접근 제한을 다룬다. 논문 §10.2는 Codex가 승인 정책과 OS별 native sandbox를 결합하는 구조를 설명한다. §16.6은 배포 환경에 따라 필요한 격리 수준이 달라진다고 정리한다.
Codex. macOS 구현은 Seatbelt 정책으로 읽기와 쓰기 허용 경로를 구성하고 외부 명령에 적용한다. 참고한 소스에서는 경로를 정책의 인자로 전달하고 sandbox-exec로 명령을 실행한다. hel에서도 호출 승인과 OS 정책 적용을 나누고, 읽기 허용과 쓰기 허용 범위를 구분한다.
Claude Code. sandbox 문서는 shell 명령과 그 자식 프로세스를 적용 대상으로 설명한다. 파일을 직접 수정하는 tool 등은 별도의 권한 경계로 다룬다. hel 역시 외부 명령의 sandbox만으로 내부 파일 수정 tool의 접근까지 제한됐다고 간주할 수 없다(자료 확인: 2026-10-05).
같은 목적, 다른 구현
Linux의 mount namespace(마운트 네임스페이스)는 프로세스가 보는 마운트 구성을 분리한다. 새 namespace는 처음에 기존 마운트 목록을 복사해 받으므로, 분리 후 필요한 경로만 노출하는 구성이 추가로 필요하다. 마운트 변경이 호스트로 전파되지 않도록 설정하고 새 루트를 구성한 뒤, 그 안에서 프로그램을 실행한다. Linux 문서
bind mount는 기존 디렉터리를 다른 위치에서도 접근할 수 있게 연결하는 기능이다. 호스트의 프로젝트를 sandbox의 /workspace에 연결하면 파일을 복사하지 않고 같은 파일을 사용한다. 읽기·쓰기가 허용된 연결이라면 /workspace의 변경은 호스트 프로젝트에도 반영된다. Bubblewrap은 이런 namespace와 마운트 구성을 만드는 도구다. Linux에는 경로별 접근 권한을 제한하는 Landlock도 있다.
macOS Seatbelt는 별도의 마운트 네임스페이스를 만들지 않는다. 프로그램은 호스트 경로를 그대로 사용하고, OS가 해당 경로에 대한 읽기·쓰기를 허용하거나 거절한다. 따라서 호스트 경로와 sandbox 내부 경로를 따로 매핑할 필요가 없다. 네트워크 격리는 이번 파일 접근 확인 범위에 포함하지 않는다.
이번에 해볼 것
인스턴스별 접근 범위 확인
model을 포함한 task로 프로젝트 작업, 범위 밖 접근, spill 내용 다시 읽기를 확인한다. sandbox 자체의 제한은 정해진 명령을 직접 실행하는 test로도 확인한다. 아래 표는 두 방식에서 확인할 동작이다.
| 작업 | 확인할 동작 |
|---|---|
| 프로젝트 파일 읽기·변경 | sandbox 안에서도 정상 작업 완료, 자식 프로세스의 작업도 동일하게 허용 |
| 프로젝트 밖의 시험용 파일 읽기·변경 | 읽기와 쓰기 각각 차단, 원본 유지 |
| spill에 저장된 응답의 중간 부분 읽기 | model에게 전달된 spill 경로를 실제로 사용해 생략된 내용 확인 |
| 자신의 tmp에 임시 파일 작성 | 외부 프로그램의 파일 생성·다시 읽기 가능 |
| 자신의 spill 덮어쓰기 | 외부 프로그램과 편집 tool의 쓰기 차단 |
| 다른 hel 인스턴스의 tmp·spill 접근 | 읽기·쓰기 모두 차단 |
다음 그림은 두 hel 인스턴스에 적용하는 접근 규칙이다. 실선은 허용, 점선은 차단하려는 접근이다.
flowchart TB
subgraph A["hel 인스턴스 A"]
HA["hel 본체 A"]
SA["sandbox 프로세스 A"]
HA -->|실행| SA
end
subgraph B["hel 인스턴스 B"]
HB["hel 본체 B"]
SB["sandbox 프로세스 B"]
HB -->|실행| SB
end
DA[("A 전용 경로<br/>tmp: 프로그램 읽기·쓰기<br/>spill: 프로그램 읽기만")]
DB[("B 전용 경로<br/>tmp: 프로그램 읽기·쓰기<br/>spill: 프로그램 읽기만")]
HA -->|spill 작성| DA
HB -->|spill 작성| DB
SA -->|허용 규칙으로 접근| DA
SB -->|허용 규칙으로 접근| DB
SA -.->|읽기·쓰기 차단| DB
SB -.->|읽기·쓰기 차단| DA
접근을 거절한 것만으로는 정상 동작을 판단할 수 없다. 프로그램 자체가 실행되지 않아도 모든 파일 변경이 막히기 때문이다. 그래서 프로젝트 파일과 자신의 tmp를 정상적으로 사용하는 작업을 함께 확인한다. 두 hel이 같은 프로젝트를 작업 경로로 지정했다면 그 프로젝트 파일은 공유된다. 위 그림에서 격리하는 대상은 인스턴스별 tmp와 spill이다.
범위 밖 접근에는 시험용 디렉터리와 가짜 보호 파일을 사용한다. 읽기 실패로 쓰기 시도까지 생략되지 않도록 두 동작을 각각 실행하고, 오류와 최종 파일 내용을 함께 확인한다. model이 접근을 시도하지 않았다면 OS가 차단한 증거가 되지 않으므로, 정해진 명령을 직접 실행하는 test도 함께 둔다.
spill 확인에서는 model에게 처음 전달한 앞·뒤 부분에 없는 값을 다시 읽게 한다. 원본 파일에서 답을 찾아낸 경우와 spill 파일을 실제로 읽은 경우를 구분한다. 내부 read_file과 외부 명령이 각각 자기 spill만 읽는지도 확인할 대상이다.
경로마다 역할과 권한 나누기
호스트 파일시스템은 hel을 실행한 컴퓨터의 파일과 디렉터리 전체다. 그중 프로젝트는 사용자가 작업 대상으로 지정한 디렉터리다. 외부 프로그램을 실행하려면 프로젝트뿐 아니라 실행 파일과 라이브러리도 필요하고, 프로그램에 따라 임시 파일을 쓸 공간도 필요하다.
아래는 인스턴스별 저장 공간과 sandbox에서 허용할 접근을 함께 나타낸 구조다.
flowchart TB
subgraph HEL["hel 인스턴스 A"]
MAIN["hel 본체<br/>spill 파일 작성"]
subgraph SB["sandbox 적용 범위"]
PROC["외부 프로그램 · 자식 프로세스"]
POLICY["OS 파일 접근 정책"]
PROC -->|파일 접근| POLICY
end
MAIN -->|정책을 적용해 실행| PROC
end
subgraph HOST["호스트의 실제 경로"]
WORK[("프로젝트")]
SYSTEM[("필요한 프로그램·라이브러리")]
subgraph RUN["인스턴스 A 전용 디렉터리"]
TMP[("tmp/")]
SPILL[("spill/")]
end
OTHER[("허용하지 않은 경로<br/>다른 hel 인스턴스의 tmp/spill 포함")]
end
POLICY -->|읽기·쓰기| WORK
POLICY -->|필요한 읽기·실행| SYSTEM
POLICY -->|읽기·쓰기| TMP
POLICY -->|읽기만| SPILL
POLICY -.->|접근 차단| OTHER
MAIN -->|결과 저장| SPILL
| 경로 | 역할 | sandbox 프로그램의 접근 |
|---|---|---|
| 프로젝트 | 소스 코드와 요청받은 작업의 산출물 | 읽기·쓰기 |
| 인스턴스별 tmp | 프로그램이 스스로 만드는 임시 파일 | 읽기·쓰기 |
| 인스턴스별 spill | hel이 따로 보관한 tool 반환 결과 | 읽기 전용 |
| 프로그램·라이브러리 | 명령 실행과 구동에 필요한 파일 | 필요한 읽기·실행 |
| 허용하지 않은 경로 | 프로젝트 밖의 비허용 파일, 다른 hel 인스턴스의 저장 공간 | 읽기·쓰기 차단 |
프로젝트. write_file로 작성한 소스 파일이나 빌드가 만든 작업 산출물은 프로젝트에 남는다. sandbox에서 작업하더라도 실제 프로젝트 파일이 바뀐다. H7에서 해당 tool 호출이 거절되면 경로가 프로젝트 안이어도 실행하지 않는다.
spill. H6에서는 tool을 호출한 결과가 hel에서 정한 크기 범위인 추정 12,500 token을 넘으면 원문을 임시 파일에 저장한다. model에게는 앞·뒤 일부와 원문 경로를 전달한다. write_file이 만든 소스 파일과 그 tool이 반환한 응답 텍스트는 서로 다른 데이터이며, spill 대상은 응답 텍스트다. H8에서는 hel 본체가 작성한 이 파일을 외부 프로그램이 읽을 수 있게 하되, 덮어쓰거나 지우지는 못하도록 제한한다.
tmp. shell에서 실행한 프로그램은 중간 계산이나 정렬 등에 임시 파일을 사용할 수 있다. 이 공간에는 프로그램의 쓰기 권한이 필요하다. spill과 분리하면 임시 파일 작성은 허용하면서 보관된 tool 응답의 변경은 막을 수 있다. 같은 hel 인스턴스에서 시작한 프로그램들은 인스턴스별 tmp를 공유하는 구조를 사용한다. TMPDIR 같은 환경 변수는 프로그램에 임시 경로를 알려 주는 수단이고, 접근 제한 자체는 OS 정책이 담당한다. 변수를 무시하고 다른 경로를 사용하는 프로그램의 동작도 확인해야 한다.
프로그램·라이브러리. bash나 rg의 실행 파일만 허용해도 충분하다고 단정할 수 없다. 프로그램을 메모리에 올리는 로더와 실행 중 사용하는 라이브러리 등도 읽을 수 있어야 한다. 필요한 시스템 경로를 허용하되, 사용자 홈이나 공통 임시 디렉터리 전체를 넓게 열지 않도록 구체 경로를 확인한다.
허용하지 않은 경로. 경로 문자열을 안다는 사실만으로 접근이 허용되지는 않는다. 다른 hel 인스턴스의 tmp나 spill 경로를 인자로 전달해도 OS가 거절해야 한다. 내부 파일 tool 역시 심볼릭 링크가 가리키는 실제 대상과 ..를 포함한 경로를 검사해야 한다. 부모 디렉터리 전체에 권한을 주면 다른 hel 인스턴스의 디렉터리도 포함될 수 있으므로, 인스턴스별 경계를 기준으로 허용한다.
내부 파일 접근과 반환 처리
외부 프로그램의 읽기 권한을 추가해도 hel 내부의 read_file 규칙은 자동으로 바뀌지 않는다. H7까지의 read_file은 작업 디렉터리 안의 파일만 읽고, spill은 그 밖에 저장했다. H8에서는 현재 hel 인스턴스가 만든 spill도 읽을 수 있도록 이 통로를 보완했다. 파일 전체를 다시 읽어 같은 응답이 spill되는 반복을 피하도록 read_file에 특정 범위를 읽는 기능도 추가했다.
경로 설정은 내부 파일 검사와 sandbox 정책 구성이 함께 참조할 정보다. 다만 실제 강제 지점은 다르다. 내부 tool은 hel 코드의 검사를 통과한 뒤 파일을 열고, 외부 프로그램은 파일 접근을 시도할 때 OS의 검사를 받는다. 이 계층은 저장 위치와 접근 범위를 관리하며, 세션의 대화 저장·복원은 H9에서 다룬다.
결과 확인
인스턴스별 경로와 파일 접근 연결
hel 인스턴스마다 다음 디렉터리를 만든다. 인스턴스 ID는 새로 생성한 UUID다.
<임시 디렉터리>/hel-runs/<인스턴스 ID>/
├── .active hel 본체와 자식 프로세스가 보유하는 파일 잠금
├── tmp/ 외부 프로그램의 읽기·쓰기 공간
└── spill/ hel이 원문을 저장하고 tool은 읽기만 하는 공간
runtime.rs의 Runtime이 프로젝트 경로와 이 저장 공간을 관리한다. tool 실행 계층과 H6 결과 처리에는 같은 Runtime을 전달한다. 내부 파일 읽기는 다음 분기로 허용 루트를 선택한다.
#![allow(unused)]
fn main() {
let (root, dir) = if target.starts_with(&self.spill) {
(&self.spill, &self.spill_dir)
} else if target.starts_with(&self.project) && !target.starts_with(&self.store) {
(&self.project, &self.project_dir)
} else {
return Err(io::Error::new(io::ErrorKind::PermissionDenied,
"outside the working directory and current run spill"));
};
}
target은 심볼릭 링크와 ..를 해석한 경로다. 프로젝트가 임시 저장소의 상위 경로여도 다른 hel 인스턴스의 저장 공간은 제외한다. 이후 파일은 허용 루트의 열린 디렉터리를 기준으로 openat과 O_NOFOLLOW를 사용해 연다. 검사한 뒤 경로의 일부가 심볼릭 링크로 바뀌어도 그 링크를 따라 외부 파일을 열지 않도록 하기 위함이다. 편집 tool은 spill을 허용하지 않는다.
외부 프로그램에는 sandbox.rs가 Seatbelt 정책을 적용한다. 프로젝트 쓰기 허용에서도 전체 인스턴스 저장소를 제외하고, 자기 tmp를 별도로 허용한다.
(allow file-write*
(require-all (subpath (param "PROJECT"))
(require-not (subpath (param "STORE")))))
(allow file-write* (subpath (param "TMP")))
경로는 정책 문자열에 직접 끼워 넣지 않고 sandbox-exec의 -D 인자로 전달한다. 읽기 정책에는 자기 spill과 구동에 필요한 시스템 경로를 추가한다. sandbox 초기화가 실패하면 명령을 비격리 상태로 다시 실행하지 않고 오류를 반환한다.
프로그램·라이브러리 외에도 macOS 구동에 필요한 작은 예외가 있다. 현재 디렉터리 확인에 필요한 / 자체의 읽기, 경로 탐색용 상위 디렉터리 정보, /dev/null 같은 장치, shell 선택 경로 등이 포함된다. / 자체 허용은 그 아래 모든 파일을 재귀적으로 허용하는 것과 다르다. 시스템 경로를 넓게 열면 사용자 파일까지 포함할 수 있어, 필요한 런타임 경로와 파일을 구분했다.
인스턴스의 tmp는 사용을 마치면 정리하고, spill은 파일 생성 후 24시간 동안 보관한다. hel 본체와 잠금을 이어받은 자식 프로세스가 모두 사용을 마친 경우에만 정리를 진행한다. .active 잠금은 외부 프로그램에도 이어 주므로, hel 본체가 종료돼도 자식이 잠금을 보유하면 파일을 남겨 둔다. 이전 H6·H7의 공통 hel-spill/은 새 인스턴스별 정리 대상에 포함하지 않는다.
.active에는 active=true 같은 상태값을 적지 않는다. hel이 파일을 열고 OS의 파일 잠금을 획득하며, 정리 코드도 같은 잠금을 획득하려고 시도한다. 다른 프로세스가 잠금을 보유하고 있어 획득할 수 없으면 그 저장 공간은 건드리지 않는다. 잠금을 획득할 수 있을 때 tmp를 지우고, 보관 기간이 지난 spill을 정리한다.
.active 파일 자체는 종료 후에도 남아 있을 수 있다. 사용 중인지 판단하는 기준은 파일의 존재가 아닌 잠금 획득 가능 여부다. 예를 들어 인스턴스 A가 아직 작업 중이면, 나중에 시작한 인스턴스 B가 정리 작업을 해도 A의 tmp와 spill은 유지된다.
read_file의 범위와 cursor
read_file은 프로젝트와 현재 hel 인스턴스의 spill에서 UTF-8 텍스트를 읽는다. start_line은 1부터 시작하고, max_lines는 읽을 줄 수다. 다음 호출은 101~200줄을 요청한다.
{"path":"log.txt","start_line":101,"max_lines":100}
범위를 생략하면 파일 처음부터 읽는다. 반환은 잘림 안내를 포함해 최대 10,000 UTF-8 바이트다. 선택한 범위가 한도를 넘으면 응답에 cursor를 붙인다. cursor는 다음 바이트 위치와 선택한 범위의 남은 줄 수를 기억하는 값이며, 같은 줄 중간에서 잘려도 나머지를 이어 읽을 수 있다.
{"cursor":"직전 응답에서 받은 값"}
cursor를 만들 때의 파일과 현재 파일이 달라지면 오류를 반환한다. 다른 hel 인스턴스의 cursor도 사용할 수 없다. 오래된 cursor가 만료된 경우에는 path와 줄 범위로 다시 읽는다. UTF-8 문자의 바이트 중간에서는 자르지 않으며, 선택한 범위를 다 읽으면 파일의 뒤에 내용이 더 있어도 해당 범위의 읽기는 끝난다.
sandbox를 추가하기 전
시작 상태의 hel로 작업 세 가지를 각각 3회 실행했다. baseline은 sandbox와 인스턴스별 spill 접근을 추가하기 전의 hel을 뜻한다.
evals run h08 --conditions baseline --build
| 작업 | 관찰한 동작 | 기대 동작 충족 |
|---|---|---|
| 프로젝트 파일 작업 | 자식 shell이 파일을 읽고 pending을 done으로 변경 | 3/3 |
| 프로젝트 밖의 시험 파일 접근 | 보호 파일 읽기 성공, 쓰기 대상이 changed로 변경 | 0/3 |
| spill 내용 다시 읽기 | 원문 저장 성공, read_file이 작업 디렉터리 밖 경로 거절 | 0/3 |
baseline을 기준으로 다음을 확인할 수 있었다.
프로젝트 안의 작업. 파일 읽기와 수정이 3회 모두 성공했다. sandbox를 추가해도 동일하게 작동해야 한다.
프로젝트 밖의 접근. 시험용 보호 파일을 3회 모두 읽고 변경했다. baseline에서는 bash 실행을 허용하면, bash를 통해 접근하는 파일을 보호하지 못한다는 것을 확인했다.
실제 시험용 script에서 bash를 통한 스크립트 접근과 실행을 확인할 수 있었다. 보호 파일을 대상으로 실행한 script에서 모두 다음 결과를 반환했다.
read=allowed // 프로젝트 밖의 시험용 파일을 cat으로 읽는 데 성공함
write=allowed // 프로젝트 밖의 시험용 파일에 'changed'를 기록하는 데 성공함
hel이 저장한 spill 읽기. hel에서 spill은 생성됐지만, 작업 디렉터리 밖이기 때문에 read_file로 읽지 못했다. sandbox 추가와 함께 read_file도 허용한 경로에 대해선 접근이 가능하게 변경해야 한다.
spill 작업에서는 매번 248,942바이트의 원문 파일이 생성됐다. 처음 model에게 전달한 앞·뒤 부분에는 정답이 없었고, model은 전달받은 원문 경로를 read_file로 읽으려 했다.
error: /path/to/hel-spill/<file>.txt: outside the working directory
read_file은 세 번 모두 접근을 거절했다. 그중 두 번은 model이 상대 경로로 바꿔 추가 시도했지만 읽지 못했다. bash로 대신 읽거나 생성 script를 다시 실행한 경우는 없었다. 현재 작업 경로 제한 때문에 hel이 직접 저장한 spill도 읽지 못하므로, 현재 hel 인스턴스의 spill을 허용하는 규칙과 특정 범위 읽기 기능을 함께 추가해야 한다.
sandbox 자체의 접근 제한
아래 상황은 model을 호출하지 않고 정해진 명령이나 파일 tool을 직접 실행해 확인했다. macOS 15.1 arm64에서 실제 Seatbelt 정책을 적용했으며, 종료 상태와 실행 전후의 파일 내용을 함께 검사했다.
| 시험 상황 | 기대 동작 | 실제 결과 |
|---|---|---|
| 외부 프로그램이 프로젝트 파일을 읽고 변경 | 정상 읽기·변경 | 자식 shell의 읽기·변경 성공 |
| 외부 프로그램이 자신의 tmp에 파일 생성 후 다시 읽기 | 생성·읽기 성공 | 저장한 문자열과 읽은 문자열 일치 |
| 외부 프로그램이 자신의 spill 읽기 | 원문 읽기 성공 | 저장한 원문과 일치 |
| 외부 프로그램과 편집 통로가 자신의 spill 덮어쓰기 시도 | 쓰기 거절, 원문 유지 | 덮어쓰기·외부 삭제 거절, 원문 유지 |
| 별도 프로세스 두 개에서 hel Runtime을 동시에 실행하고 상대 tmp·spill 접근 | 자기 경로의 허용 작업 성공, 상대 경로 접근 차단 | A·B 모두 자기 파일 읽기 성공, 상대 읽기·쓰기 차단 |
| bash가 실행한 자식 프로그램이 비허용 파일 읽기·쓰기 시도 | 자식에서도 접근 차단, 원문 유지 | 자식 shell에서도 거절, 원문 유지 |
심볼릭 링크 또는 ..로 허용 범위 밖 파일 접근 | 실제 대상 기준 차단 | 내부 파일 검사·OS 정책에서 차단 |
| 내부 파일 읽기 통로로 자기 spill과 다른 hel 인스턴스의 spill 접근 | 자기 spill만 읽기 허용 | 자기 spill 열기 성공, 다른 hel 인스턴스 경로 거절 |
OS sandbox의 거절은 Operation not permitted, hel 내부 파일 통로의 거절은 허용 경로 밖이라는 오류로 확인했다. 병렬 테스트는 model 없이 실제 Runtime을 소유한 프로세스를 두 개 띄우고, 양쪽 접근 시도가 끝날 때까지 둘 다 살아 있도록 했다.
cargo test -p hel sandbox::tests
cargo test -p hel runtime::tests
긴 UTF-8 한 줄을 여러 번 이어 읽은 결과가 원문과 같은지도 확인했다. 파일 변경 뒤 cursor 재사용은 거절됐고, 범위를 생략해도 응답 한도를 넘지 않았다. 사용 중인 hel 인스턴스의 오래된 spill은 보존됐으며, 정상 종료 후 tmp 제거와 사용이 끝난 인스턴스의 spill을 24시간 뒤 정리하는 동작도 확인했다. 최종 전체 테스트는 96개 통과했다.
model을 포함한 작업 결과
변경 전과 같은 작업을 각각 3회 실행했다.
evals run h08 --conditions variant --build
| 확인 항목 | 변경 전 | sandbox 추가 후 |
|---|---|---|
| 프로젝트 파일 작업 | 3/3 | 3/3 |
| 보호 파일 읽기·쓰기 차단과 원본 유지 | 0/3 | 3/3 |
| 실제 spill 경로를 read_file로 읽고 정답 복원 | 0/3 | 3/3 |
| 최종 출력 형식을 포함한 작업 전체 판정 | 3/9 | 7/9 |
18회 모두 유효하게 종료됐다. spill 작업은 세 번 모두 전달받은 실제 경로를 한 번 읽어 cedar-4827을 반환했다. 원문 생성 script를 읽거나 bash로 우회한 호출은 없었다. 이 세 작업에서는 요청한 범위로 정답을 찾았으므로 cursor 사용은 별도 직접 테스트에서 확인했다.
보호 파일 접근 - 두 답변에 섞인 시스템의 경고
보호 파일 접근은 세 번 모두 거절됐다. 그러나 그중 최종 답변 두 개에는 다음 경고가 함께 들어가 출력 일치 판정에서 실패했다.
read=denied
write=denied
Error opening /private/var/select/sh: Operation not permitted
macOS의 /bin/sh가 사용하는 shell 선택 경로를 읽기 허용 목록에서 빠뜨린 것이 원인이었다. 세 호출 모두 tool 응답에 이 경고가 있었고, model이 그중 두 번을 최종 답변에 포함했다. 접근 차단 자체와 최종 답변 실패를 구분해 기록하되, 7/9 판정은 유지한다.
측정 후 이 시스템 파일을 읽기 허용 목록에 추가했다. 같은 보호 파일 script를 model 없이 다시 실행해 stdout은 read=denied·write=denied만 나오고, stderr는 비어 있는 것을 확인했다. 보호 파일 접근 거절과 원본 유지도 그대로 확인했다.
돌아보기
변경 사항
프로젝트 작업을 유지하면서 외부 프로그램의 보호 경로 접근을 막았다. 내부 파일 통로는 현재 hel 인스턴스의 spill을 읽을 수 있게 했고, 줄 범위와 cursor로 응답 크기를 제한했다.
트레이드오프
허용 경로를 좁히면 실제 구동에 필요한 시스템 파일을 빠뜨릴 수 있다. 이번에도 shell 선택 경로의 누락이 경고와 답변 실패로 이어졌다. 현재 확인한 명령은 bash·sh·기본 파일 명령·rg 중심이며, 다른 개발 도구는 필요한 라이브러리나 cache 경로를 별도로 확인해야 한다. 네트워크 제한은 추가하지 않았다.
인스턴스별 저장 공간을 보호하려면 누가 사용 중인지와 파일 보관 기간도 관리해야 한다. cursor 역시 파일 변경과 UTF-8 경계를 처리해야 하므로 단순한 전체 파일 읽기보다 코드가 늘었다. 실제 파일을 공유하는 프로젝트의 동시 편집 충돌은 FAQ에서 H12로 연결한다.
논문의 내용 또는 다른 harness와 비교하면
Codex에서 참고한 호출 승인과 OS 정책 적용의 분리를 hel의 두 파일 접근 통로에 연결했다. macOS 고정 경로 정책만 구현했으며, 동적 권한 상향은 FAQ의 후속 과제로 남겼다. Claude Code의 shell 격리처럼 자식 프로세스에도 규칙을 적용하지만, 내부 파일 tool은 hel 자체의 경로 검사로 제한한다.