샌드박스: seatbelt에서 실행 세계로
ctx.fs와 ctx.subprocess는 하나의 경로 네임스페이스를 공유하므로 실행 환경을 통째로 바꿀 수 있어요. 핵심 소스: packages/sandbox/, packages/e2b/, native/landlock-run/.
ctx.sandbox는 호스트와 커널을 공유하는 자식 프로세스만 다루고 컨테이너·원격 실행은 다른 길인지, 그리고 실행 환경 전체를 로컬에서 원격 E2B 샌드박스로 바꿀 때 코드를 얼마나 고쳐야 하는지(스포iler: 소비자 쪽 변경 0).
먼저 해보고 설명해요. 시나리오 A는 세계 바꾸기예요: 위쪽은 bash, PTY, LSP 같은 일하는 컴포넌트이고 가운데 seam 두 개에만 연결돼요. 「E2B로 전환」을 눌러 seam 아래 세계가 통째로 바뀔 때 위쪽 연결선이 끊기는지 보세요. 「반면 교재」를 누르면 seam 없는 구조에서 세계를 바꿀 때 얼마나 끔찍한지 볼 수 있어요. 시나리오 B는 거부 디코더예요: 같은 stderr 오류로 샌드박스가 막은 건지, 샌드박스 자체가 고장 난 건지 구분하는 법을 봅니다.
packages/e2b/README.zh.md와 docs/subsystems/filesystem.zh.md의 실행 세계 약속에 대응하고, 시나리오 B는 packages/sandbox/sandbox/src/index.ts 95~116행의 denialSignatures와 runnerFailureRules 두 직교 분류기에 대응해요.먼저 결론부터: DSH는 샌드박스를 세 층으로 나누고 각자 맡게 해요.
ctx.sandbox.confine(argv, policy)는 한 가지만 해요: spawn할 argv를 제한 runner로 한 번 감싸요. Linux는 bwrap 또는 Landlock, macOS는 Seatbelt(sandbox-exec), Windows는 ACL 제한 토큰이에요. 전제는 자식 프로세스가 호스트와 파일 시스템·커널을 공유하는 거예요.
read / write / edit는 자식 프로세스를 spawn하지 않아 argv를 감쌀 의미가 없어요. fs-sandbox는 신뢰 코드 안에서 정책을 검사해요: 경로 정규화, 포함 관계 검증, 거부 시 구조화된 FS_SANDBOX_DENIED를 던져요. 무엇을 거부했는지 스스로 알아서 커널 stderr 텍스트를 추측할 필요가 없어요.
컨테이너, microVM, 원격 실행은 능력 seam 전체의 동급 교체예요. ctx.sandbox가 나설 차례가 아니에요(docs/subsystems/sandbox.zh.md 5행). 원격으로 가려면 ctx.fs와 ctx.subprocess 두 seam 구현만 바꾸면 돼요.
1층에서 흔히 헷갈리는 점: 샌드박스 정책은 매 호출마다 따라가는 매개변수예요. 제공자의 전역 상태로 영구 고정되지 않아요. ctx.sandboxPolicy.resolve()는 매 능력 호출마다 완전한 정책(모드 + 워크스페이스 루트 + 세션 ID)을 풀어요. 명시 승인된 권한 상승 모드가 세션 설정보다, 세션 설정이 배포 기본값보다 우선해요. 그래서 한 프로세스 안의 두 세션—read-only와 workspace-write—가 같은 제공자에게 서로 다른 경계를 요청해도 간섭하지 않아요. 승인된 권한 상승 재시도도 더 넓은 정책을 가진 새 호출일 뿐, 제공자 상태는 그대로예요.
Linux Landlock 백엔드는 따로 짚을 만해요. DSH는 기성 래퍼를 쓰지 않고 landlock-run을 직접 썼어요: C11 약 300행, musl 정적 링크, 자신에게 Landlock 규칙 집합을 먼저 설치한 뒤 대상 명령을 exec해요. 규칙 집합은 execve를 넘어 상속되므로 명령이 띄운 모든 자손이 제한 아래에 있어요. 커널이 지원하지 않으면 명령 없이 바로 종료해요. 바이너리 약속(argv 문법, 종료 코드, 리포트 줄)은 native/landlock-run/docs/cli-contract.md에 고정돼 있고, probe()는 full, partial, unusable 세 상태를 돌려줘요. 구 커널 ABI는 partial만 보고해요. 디테일 하나: 런처 실패의 관례 종료 코드는 125인데, exec에 성공한 자식도 스스로 125로 끝낼 수 있어요. 그래서 종료 코드만으로는 결론을 내릴 수 없고 치명적 진단 줄을 함께 봐야 해요. 아래 두 분류기로 이어집니다.
denial ≠ runner failureconfine()가 돌려주는 래핑 argv에는 stderr 분류기 두 세트가 붙어 있어요. 먼저 runnerFailureRules를 봐요: 치명적 시그니처가 매칭되면 runner 자체가 죽은 거고 명령은 아예 실행되지 않았으니 샌드박스 인프라 장애로 보고해요. 그다음 denialSignatures: 매칭되면 샌드박스가 정상 작동해 경계 밖 동작을 막은 거예요. 순서를 바꾸면 안 돼요.
거부어는 백엔드 방언으로 매칭bwrap 읽기 전용 마운트는 EROFS 텍스트, Landlock은 EACCES, Seatbelt는 EPERM이에요. 소비자는 현재 백엔드가 선언한 몇 개의 말만 매칭해요. 백엔드 간 합집합을 쓰지 않아요—합집합은 어떤 백엔드도 만들지 않는 거부를 주장하게 돼요.
partial을 full로 쓰면 안 됨강제 실행 무결성은 백엔드가 보고하는 사실이에요. 구 Landlock ABI, Windows ACL Everyone·하드 링크 구멍은 partial만 보고할 수 있어요. 절대 경계가 필요한 소비자는 이 차이를 명시적으로 처리해야 해요. 못 본 척하면 안 돼요.
제한 정책 아래 조용한 무격리 통과는 영원히 불법이에요. 샌드박스가 필요한데 쓸 백엔드가 하나도 없으면 confine()이 SandboxUnavailableError를 던지고 호출 전체가 실패해 명령 한 줄도 실행되지 않아요. 이 오류 타입 자체가 입장을 박아 둬요: 첫 문장은 「refusing to run the command unconfined」이고, 이어서 플랫폼별 수정 힌트—Linux는 bubblewrap 설치 또는 Landlock 강제 커널, macOS는 sandbox-exec 확인, Windows는 ACL 제한 토큰 runner 기동 확인; 정말 고치기 싫으면 소비자를 명시적으로 danger-full-access로 두고 무방비를 흑백으로 적어요.
이 예외는 HarnessError를 상속하고 SANDBOX_UNAVAILABLE 오류 코드를 구조화된 오류 채널로 실어 tool/result까지 갑니다. 호출자는 오류 코드만으로 격리 부재와 명령 실패 두 정산을 구분할 수 있어 stderr 텍스트를 추측할 필요가 없어요. fail-closed의 뜻은 단순해요: 환경이 준비되지 않았으면 답은 실행하지 않는 것, 누구도 몰래 무방비로 강등할 수 없어요.
출처: packages/sandbox/sandbox/src/index.ts 131~144행(SandboxUnavailableError), 124행(SANDBOX_UNAVAILABLE 오류 코드), 확인일 2026-08-13.
2층 구현은 생각보다 짧아요. fs-sandbox는 로컬 파일 시스템 백엔드를 상속하고 두 가지 변경 연산 앞에만 호출마다 풀리는 울타리를 더해요:
private async checkedTarget(target: FsTarget, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsTarget> {
const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()
const { mode } = policy
if (mode === 'danger-full-access') return target
if (mode === 'read-only') {
throw new FsError(`cannot write "${target.displayPath}": file access denied under read-only mode`, 'FS_SANDBOX_DENIED')
}
packages/fs/fs-sandbox/src/index.ts, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.아래 workspace-write 분기(같은 파일 133~147행)는 포함성 검사 전에 경로를 한 번 더 정규화해요. 검사 때는 A, 쓸 때 심볼릭 링크로 B로 바뀌는 바꿔치기를 막기 위해서예요. 그다음 검증된 새 대상으로 변경해요. 쓰기 가능 루트 집합은 writableRoots() 한 함수에서 오고 Seatbelt가 bash에 주는 권한 범위와 같은 출처이라 양쪽이 어긋나지 않아요.
이제 3층이에요. 가변 상태를 다루는 모든 컴포넌트—bash 실행기, 지속 PTY, LSP 호스트, 파일 도구—는 OS를 직접 건드리지 않고 ctx.fs와 ctx.subprocess 두 seam에 일을 위임해요. 두 seam은 하나의 경로 네임스페이스를 공유하기로 약속해요: ctx.fs.processPath(target)가 돌려주는 절대 경로를 ctx.subprocess가 띄운 자식이 바로 열 수 있어요(docs/subsystems/filesystem.zh.md 「대상 식별과 메타데이터」 절). read가 보는 파일과 bash가 만지는 파일은 같은 세계의 같은 파일이에요.
그래서 세계 바꾸기는 seam 바꾸기가 돼요. packages/e2b/는 fs-e2b와 subprocess-e2b 어댑터를 제공하고, E2B Filesystem API와 Commands/PTY API로 같은 seam을 구현해요. 공식 README는 소비자 변경 0을 아주 직설적으로 말해요:
「기존dsh-bash-local,dsh-terminal-bash,dsh-lsp-stdio에는 E2B 전용 fork가 필요 없어요. 실행 환경의 모든 동작을ctx.fs와ctx.subprocess에 위임하므로, 이 두 E2B 어댑터를 마운트하면 가변 상태를 다루는 모든 작업이 같은 샌드박스 안에서 일어나요.」
경계도 분명해요: 옮기는 건 파일과 프로세스뿐이고, harness 프로세스 자체, 모델 호출, agent·세션 상태, 세션 영속화는 로컬에 남아요(README 15행). Agent가 못 느끼는 이유가 여기 있어요: 여전히 같은 도구를 부르고, 도구는 같은 두 seam에 연결되며, seam 뒤의 세계만 바뀌었어요.
Grok Build 샌드박스는 독립 Rust crate xai-grok-sandbox이고, 프리셋 profile로 제한 강도를 나눠요. 사이트에서 이미 줄 단위로 확인했어요: 다섯 가지 샌드박스 Profile은 profile 단계 설계, 도구 요청부터 제한 실행까지의 전체 승인 체인은 한 번의 도구 호출이 층층이 어떻게 통과하는지 다루어요. DSH와 비교하면 Grok의 강점은 로컬 제한의 공학적 완성도예요. fs와 subprocess가 네임스페이스를 공유하고 두 seam을 통째로 원격 세계로 바꾸는 추상은 확인한 Grok Build 자료에서는 동급 항목을 못 봤어요. 공개 증거 기준이며 미확인 항목은 남겨 둬요.
Claude Code 방어선은 주로 실행 전에 깔려요: bash 명령은 세 층 권한 체인(모드 검사, 규칙 매칭, 필요 시 AI 분류기—원고 7장의 bashPermissions.ts 해설)을 거치고, macOS에서는 sandbox-exec로 보조 제한을 할 수 있어요. 공식 엔지니어링 블로그는 agent를 「sandboxed environments」에서 가드레일과 함께 돌리라고 조언해요(원고 7장 350행 인용)—환경 수준 격리는 배포자에게 더 맡긴다는 뜻이에요. 복원 소스라는 공개 증거 기준으로 CC에는 DSH처럼 호출마다 정책을 실는 샌드박스 seam도, 통째로 교체 가능한 실행 세계 추상도 없어요. 제품으로서의 선택은 충분히 말이 되고, 판정이 충분히 세밀하면 환경은 사용자에게 맡길 수 있어요.
경계 시나리오 두 가지 추론
시나리오 1: 같은 DSH 프로세스에서 세션 두 개—세션 A는 read-only, 세션 B는 workspace-write—가 동시에 파일 쓰기 bash 명령을 각각 실행해요. 각 명령이 ctx.sandboxPolicy.resolve()에서 ctx.sandbox.confine()까지 받는 정책 내용을 적고, 왜 제공자가 상태 전환 동작이 필요 없는지 설명하세요. 시나리오 2: landlock-run으로 감싼 명령이 125로 종료해요. 결론 내리기 전에 확인해야 할 증거(LAUNCHER_FAILURE_EXIT 관례, 치명적 진단 줄, 무해 알림 줄 제거 순서)를 나열하고, runner 장애와 명령 자체가 125로 끝난 두 판정 각각에서 도구 계층이 모델에 무엇을 보고해야 하는지 적으세요.