통합 입구: 명령 하나가 특징에 따라 갈라져요
모델은 exec_command와 write_stdin만 봐요. 문을 지나면 tty, 원격 환경, 150밀리초 창이 같은 명령을 PTY, pipe, exec-server, 또는 재시도 문으로 보냅니다.
- command에 cwd를 더한 요청을 만듦mod.rs L12
- 오케스트레이터 승인: bypass, 캐시, 또는 팝업mod.rs L14
- 프로필로 샌드박스를 고르고 transformmod.rs L15
- tty와 원격 환경으로 PTY, pipe, exec-server로 가름spawn.rs L97
- 150밀리초 안에 끝나면 샌드박스 거절을 검사process.rs L349
- 휴리스틱 또는 실행기 깃발이 서면 Denied로 표시process.rs L307
- 오케스트레이터가 escalate와 정책 문을 지남orchestrator.rs L356
- UnlessTrusted이고 이미 승인됐으면 다시 묻지 않음orchestrator.rs L411
- unsandboxed가 허용되면 두 번째는 Noneorchestrator.rs L459
- 창을 넘겨 살면 입고 후 yieldprocess_manager.rs L535
- 늦은 거절은 Ok 회신으로 거둠exec_command.rs L384
- 사용자 Esc는 turn만 취소protocol.rs L546
모델이 의존성을 깔려고 npm install을 보내요. 같은 턴에 vim으로 README도 고칩니다. 실행 종류마다 승인과 샌드박스를 따로 쓰면 Guardian, 네트워크 프록시, 승인 캐시가 세 벌이 되어, 한 곳을 고치면 두 곳을 빠뜨려요.
밖에는 exec_command와 write_stdin만 있어요. 앞은 프로세스를 열고, 뒤는 이미 있는 프로세스에 쓰며, 빈 쓰기는 poll 한 번입니다. 안에서는 process_id를 쫓고, 모델 쪽 인자 이름은 session_id예요. 매니저는 요청만 준비하고, 승인, 샌드박스 고르기, 재시도는 오케스트레이터에 맡깁니다.
출처:codex-rs/core/src/unified_exec/mod.rs 12–17행
로컬 띄우기는 스위치 둘로 셋 중 하나를 고릅니다. tty가 참이면 PTY. tty가 거짓이고 stdin이 열려 있으면 stdin을 살린 pipe. 아니면 stdin 없는 pipe예요. 원격 환경이나 shell snapshot이 있는 요청은 로컬 spawn을 건너뛰고 exec-server로 갑니다. Windows 제한 토큰은 백엔드가 따로예요.
출처:codex-rs/sandboxing/src/spawn.rs 97–127행
기본 도구 호출은 종종 pipe예요. 모듈 주석의 PTY 띄우기는 tty=true 갈래만 덮습니다. 터미널을 온전히 쓰려면 모델이 tty를 직접 켜야 해요. 환경 변수도 TERM=dumb, PAGER=cat 같은 값을 박아, 대화형 프로그램이 먼저 한 겹 깎입니다.
정책 논리는 한곳에 모이고, 프로세스 모양은 바꿀 수 있어요. 다른 언어로 다시 써도 입구는 하나, 띄우는 방법은 특징으로 가릅니다. PTY를 어떻게 구현할지는 자기 저장소에 남겨도 돼요.
샌드박스가 /etc/hosts 쓰기를 거절하고, stderr는 Operation not permitted예요. 런타임이 이걸 명령을 잘못 쓴 것으로 보면 모델은 소스를 고치고, 경로를 바꾸고, sudo를 붙입니다. 알아봐야 정책대로 한 번 더 돌리거나, 거절 본문을 모델에 먹여 권한을 신청할 기회가 생겨요.
로컬 프로세스를 받은 뒤, 종료 채널에 이미 코드가 있거나, 채널이 닫혔거나, 150밀리초 안에 끝났을 때만 거절 검사를 해요. 이 창을 넘기면 백그라운드 작업 하나만 걸어 종료를 기다리고, 아직 살아있는 프로세스를 돌려줍니다. 오케스트레이터 재시도는 여기서 돌아오는 SandboxDenied에 기대요. 프로세스가 150밀리초를 넘기면 오케스트레이터는 이미 Ok를 받았고, 그다음 죽어도 두 번째 spawn에 못 들어갑니다.
출처:codex-rs/core/src/unified_exec/process.rs 38행 · 349–367행
exec-server 경로에도 같은 타임아웃이 있어요. 검사 함수 스스로 먼저 20밀리초를 기다려, 출력 알림이 올 틈을 줍니다. 판정에는 숏컷 셋이 더 있어요. 프로세스가 아직 안 끝났으면 그냥 통과. 이미 SandboxType::None이고 실행기가 거절을 안 알렸어도 통과. 나머지만 공유 휴리스틱을 돌려요.
출처:codex-rs/core/src/unified_exec/process.rs 290–324행
창을 넘겨 산 프로세스는 먼저 입고하고, 그다음 yield를 시작해요. turn을 끊을 때 마지막 Arc가 떨어졌다고 백그라운드 프로세스를 죽이면 안 됩니다. yield가 프로세스가 이미 끝난 뒤에야 끝나면, 매니저가 거절 검사를 한 번 더 돌려요. 이때 오케스트레이터는 이미 Ok를 돌려준 뒤입니다. 이 오류는 handler로 바로 돌아가, 본문이 있는 도구 회신이 되고 process_id는 비워요.
출처:codex-rs/core/src/unified_exec/process_manager.rs 535–556행 · codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs 384–407행
한 번짜리 명령은 끝날 때까지 기다렸다가 판정해도 돼요. 지속 프로세스는 마감이 있어야 합니다. 마감을 빼먹으면 몇 초 뒤에 실패한 명령을 거절로 보고 다시 맨몸으로 돌려요. 부작용은 이미 디스크에 쓰였고, 두 번째는 다른 프로세스이며, 소스에는 롤백이 없습니다.
모듈 머리에 적혀 있어요. 거절된 뒤 정책대로 SandboxType::None으로 다시 시도하고, 캐시에 기대 한 번 더 묻지 않는다고요. Never, OnRequest, Guardian strict, deny-read가 있는 프로필은 “다시 묻지 않기”나 샌드박스 없는 재시도를 꺼 버립니다. 사용자가 Esc를 눌러 vim까지 데려가면, 다음 턴은 이 세션을 못 찾아요.
출처:codex-rs/core/src/unified_exec/mod.rs 7–8행
오케스트레이터는 오류를 하나만 알아봐요. SandboxErr::Denied. 알아본 뒤 문 다섯을 지납니다. unified_exec는 스스로 업그레이드하겠다고 밝혀요. Never와 OnRequest는 기본으로 샌드박스 없는 재시도를 하지 않고, 원문이 있는 거절을 호출 쪽에 드러냅니다. 프로필에 deny-read가 있으면 샌드박스를 우회할 때 그 거절 읽기가 조용히 통과되므로 unsandboxed를 꺼요. Guardian의 strict auto-review는 첫 승인을 샌드박스 안 시도에만 덮습니다. 두 번째가 unsandboxed를 허용하면 None에 착지해요.
출처:codex-rs/core/src/tools/runtimes/unified_exec.rs 159–161행 · codex-rs/core/src/tools/sandboxing.rs 330–337행 · 269–278행 · codex-rs/core/src/tools/orchestrator.rs 411–415행 · 444–460행
사용자가 TUI에서 Esc를 누르면 프로토콜 입구는 Interrupt예요. 지금 작업을 중단하고, 백그라운드 terminal은 안 죽입니다. 백그라운드를 전부 죽이려면 CleanBackgroundTerminals가 따로 있어요. 먼저 입고한 뒤 yield하면, turn 토큰이 취소돼도 프로세스 Arc는 남아 있습니다. pipe 세션은 보통 키를 못 받고, \u{3}만 interrupt로 가요.
출처:codex-rs/protocol/src/protocol.rs 546–552행
사용자가 고른 격리 수준을 런타임이 몰래 바꾸면 안 돼요. 생각을 멈추는 것과 터미널을 멈추는 것은 다른 일이에요. 취소는 기다림만 멈추고, 이미 등록된 프로세스는 안 죽입니다.
DeepSeek Harness: 터미널 도구 여섯, 거기다 jobs
DSH는 지속 PTY를 독립 도구 족으로 만들어요. 열기, 쓰기, 읽기, 시그널, 닫기, 목록. 백그라운드 전송은 ctx.jobs를 재사용하고, 수집은 job_output, 중지는 job_kill입니다. 시스템 프롬프트에 적혀 있어요. 호출을 건너뛰는 터미널 상태나 대화형 stdin이 필요할 때만 터미널을 쓰고, 한 번짜리 일은 shell이나 읽기/쓰기 도구를 우선하라고.
Codex는 열기와 쓰기를 도구 둘로 접고, 읽기는 다음 write_stdin이나 빈 poll에 합쳐요. DSH에는 샌드박스 거절 뒤 오케스트레이터가 자동으로 샌드박스 없이 재시도하는 짝이 없습니다. 실패한 뒤 누가 한 번 더 돌리느냐, 양쪽 답이 달라요.
Grok: 세션을 열 때 지속 여부를 고름
Grok에는 unified_exec 모듈이 없어요. 지속성을 세션 단위 백엔드 선택으로 둡니다. 부모 세션 재사용, ACP 클라이언트 터미널, 로컬 지속, 로컬 비지속. 백엔드를 고르면 그 모양으로 끝까지 달려요.
Codex는 같은 질문을 한 번의 exec_command에 올려요. 프로세스가 yield를 넘겨 살면 process_id를 돌려줍니다. Grok은 세션 전체가 백엔드 하나를 공유하고, 자식 에이전트는 부모 백엔드를 재사용해요. 양쪽 모두 한 번짜리 bash -c가 cwd와 대화 상태를 못 지킨다는 건 인정합니다. 착지점만 달라요.
출처:packages/terminal/tool-terminal/src/index.ts 156–160행 · crates/codegen/xai-grok-shell/src/session/acp_session_impl/spawn.rs 2048–2070행
어느 쪽이 두 번째 spawn을 하나
같은 npm install을 먼저 UnlessTrusted로 타고, 다시 Never로 돌려요. 그다음 명령을 sleep 2로 바꿉니다. 밀어 가며 보세요. 어느 쪽이 두 번째 spawn을 하는지, 어느 회신에 아직 process_id가 있는지, 램프가 꺼진 뒤 오케스트레이터가 거절을 못 보는 이유.