두 가지 보안 시선: 같은 명령, 판결 둘
일이 나기 전에 거절할 문이 있는지, 난 뒤에 모델이 그때 본 세계를 복원할 수 있는지. 같은 위험한 명령에서 이 두 시선은 일치하기도, 반대 결론을 내기도 해요.
- 같은 argv를 두 시선에 동시에 넘깁니다L장면
- 거부 가능은 먼저 Decision 세 상태를 보고 가장 엄한 칸을 씁니다decision.rs L9
- 승인 캐시 key는 완전한 argv를 들고, 접두사는 인정하지 않아요unified_exec.rs L92
- Guardian이 시간 초과이거나 나쁜 출력이면 게이트를 닫아요guardian/mod.rs L11
- 플랫폼 샌드박스 백엔드 셋. Windows가 꺼져 있으면 Nonemanager.rs L37
- 재생 가능은 JSONL 원문을 믿고, SQLite는 거울일 뿐이에요README.md L22
- fork는 진짜 TurnStarted를 만나야 해요thread_rollout_truncation.rs L187
- 실패는 관찰로 쓰고, success는 여전히 truecontext.rs L351
- 프록시 403은 명령 프로세스로 돌아가고, 루프는 계속돼요responses.rs L80
- 두 판결을 대조: 일치인가 반대인가L검수
월요일 아침, 보안 동료가 채팅 기록을 그룹에 던집니다. 모델이 어젯밤 외부 API에 붙었고, 요청 헤더에 저장소 배포 토큰이 있었어요. 그가 묻습니다: 그때 모델이 도대체 어떤 환경 변수를 봤는가. 세션을 열면 제목은 있는데, 들어가 보면 맞지가 않아요. SQLite에는 메타데이터 한 줄이 있고, JSONL은 반이 빠졌습니다.
재생 가능이 답해야 하는 게 바로 이거예요. 일이 난 뒤, 모델이 그때 본 세계를 정확히 다시 만들 수 있는가.
Codex는 역사를 두 벌로 씁니다. JSONL이 원문이에요. 이미 확정된 항목만 이어 붙이고, 내용에서 메타데이터를 추론하지 않아요. SQLite는 목록과 검색용 거울입니다. 메타데이터를 잃으면 다시 뽑을 수 있어요. JSONL을 잃으면 복구는 파일을 읽어야 합니다.
출처:codex-rs/thread-store/README.md 22–28행
그다음 거름망이 하나 더 있어요. 지속 정책은 스트리밍 증분, 승인 팝업, 경고, MCP 시작 진행을 버립니다. TurnStarted는 남아요. turn 경계와 완료 상태는 재생할 수 있고, 그때 화면에 스친 승인 문장은 재생할 수 없어요.
출처:codex-rs/rollout/src/policy.rs 86–105행
fork가 믿는 것도 이 물리 경계예요. 목표 turn은 유효 역사 안에 있어야 하고, 파일에 진짜 TurnStarted가 있어야 하며, 진행 중인 turn은 바로 거절합니다. 투영된 합성 ID는 자름점이 될 수 없어요.
출처:codex-rs/core/src/thread_rollout_truncation.rs 187–191행
그래서 진실도 거릅니다. 목록은 이 스레드가 있었다고 말해 줄 수 있어요. 모델이 그때 어떤 메시지를 봤는지는 JSONL만이 말해 줍니다.
원문과 투영을 갈라 두면, 투영이 깨져도 다시 만들 수 있고, 원문이 깨지면 현장이 사라져요. 저장소를 바꿔도 물을 것은 누가 원문인가입니다. 이 계약은 언어가 바뀌어도 남아요.
수요일 오후, 다른 기계의 agent가 git reset --hard를 끝냈습니다. 정책 파일에는 금지라고 쓰여 있었어요. 그날 승인 팝업이 너무 많아 누군가 기억을 눌렀습니다. 샌드박스는 켜져 있었고, 그 명령은 보호 경로를 건드리지 않아 커널이 막지 않았어요. 나중에 rollout을 통째로 재생할 수 있습니다. 재생은 그가 한 일을 알려 주고, 하기 전에 길을 막지는 않았어요.
거부 가능이 답해야 하는 것: 일이 나기 전에, 거절할 문이 있었는가.
명령이 모델 제안에서 프로세스 시작까지 가려면, Codex는 거절할 수 있는 문을 적어도 넷 지납니다. execpolicy의 Decision은 Allow, Prompt, Forbidden뿐이고, 순서로 가장 엄한 칸을 취해요. 규칙 파일의 not_match 로더는 진짜 한 번 달립니다. 반례가 맞으면 세션이 열리지 않아요.
출처:codex-rs/execpolicy/src/decision.rs 9–16행
출처:codex-rs/execpolicy/src/rule.rs 281–306행
둘째 문은 승인입니다. 세션 캐시는 정규화된 완전한 argv, 작업 디렉터리, 권한을 든 정확한 key를 믿어요. 초기에 “이 종류를 기억”을 접두 캐시로 읽는 일이 많았습니다. 지금 소스에서는 npm run test와 npm run lint가 열쇠 둘이에요. 이 문은 같은 정확한 명령의 반복 팝업을 막습니다.
출처:codex-rs/core/src/tools/runtimes/unified_exec.rs 86–97행
셋째 문은 팝업을 모델로 바꿉니다. Guardian은 시간 초과나 나쁜 출력이면 게이트를 닫고, 분명한 allow나 deny만 믿어요. 승인 피로를 막습니다. 사용자 본인이 승인을 누르면 이 문은 길을 양보해요.
출처:codex-rs/core/src/guardian/mod.rs 1–12행
넷째 문이 운영체제예요. macOS는 SBPL을 맞추고, Linux는 기본으로 bubblewrap에 seccomp를 더하며, 실패해도 남은 Landlock으로 후퇴하지 않아요. Windows는 제한 토큰을 쓰고, 스위치가 꺼져 있으면 None을 반환합니다. 프로세스가 뜬 뒤 출장은 다시 프록시를 지나요. 프록시가 거절하면 x-proxy-error 헤더를 단 403을 명령 프로세스에 돌려주고, 루프는 계속됩니다.
출처:codex-rs/sandboxing/src/manager.rs 36–42행
출처:codex-rs/network-proxy/src/responses.rs 76–83행
출처:codex-rs/network-proxy/README.md 234–238행
각 층이 보는 것이 다릅니다. 정책은 argv를, 승인은 사람을, 커널은 경로와 syscall을, 프록시는 호스트를 봐요. 한 층이 못 보면 거기서 멈춥니다. 규칙이 사람이 읽는 파일에만 있고 로드 시점 예가 없으면 소스와 함께 떠내려가요. AGENTS.md 35행은 아직 mcp_connection_manager.rs를 가리키는데, 저장소에 그 파일이 없습니다.
출처:AGENTS.md 35행
명령 종료 코드가 0이 아니면 직감은 오류예요. 샌드박스 거절을 엔진 오류로 올리면 모델은 종료 코드를 못 보고, 더 돌아가는 명령만 고릅니다. 프록시 403이 엔진에 꽂히면 그 turn이 멈추고, 모델은 호스트를 바꿔 다시 시도할 수 없어요.
도구 층이 허용하는 실패는 둘뿐입니다. 모델에 다시 먹이거나, 엔진을 끊거나. 샌드박스 거절은 성공한 도구 출력으로 가요. process_id는 지워지고, exit_code는 본문에 남아요. 로그를 쓸 때 성공 비트는 언제나 true입니다. 실패는 Exit code 그 줄에 씁니다.
출처:codex-rs/tools/src/function_call_error.rs 1–10행
출처:codex-rs/core/src/tools/context.rs 340–353행
재생 가능이 원하는 건 관찰이 남는 일이에요. 거부 가능은 시작 전이나 커널에서 이미 일을 끝냈습니다. 여기서는 오류로 turn을 끊지 않아요. 프록시 403은 같은 계약의 네트워크 판입니다. 명령 프로세스가 쉬운 말을 읽고, 출력이 JSONL에 들어가며, 모델이 다음을 정해요.
출처:codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs 383–411행
도구 오류와 엔진 오류를 가르는 일은 어떤 agent 루프에도 쓸 수 있는 모양이에요. 종료 코드, 시간 초과, 정책 거절은 기본으로 첫째 칸을 갑니다. 오케스트레이션 자체가 깨졌을 때만 그 turn을 멈춰요.
재생: DSH는 “본 것은 다시 만들 수 있어야 한다”를 빨간 줄로 씁니다
DSH 저장소는 같은 문장을 AGENTS.md(CLAUDE.md는 그것을 가리키는 심볼릭 링크)와 아키텍처 문서에 씁니다. 모델 요청에 들어가는 입력은 모두 세션 로그에서 다시 만들 수 있어야 해요. 이어 붙이기만 하는 로그가 진실이고, 모델이 보는 것은 투영입니다. 승인 정책은 ask와 never뿐이고, Guardian도 프로세스 안 출장 프록시도 없어요.
Codex의 재생 가능은 확정된 역사에서 멈춥니다. DSH는 한 걸음 앞으로 가요. 새로운 모델 가시 입력은 먼저 세션 이벤트가 되어야 합니다. 재생 쪽 계약은 더 단단하고, 거부 쪽은 더 얇아요.
출처:AGENTS.md 107행
출처:docs/architecture.md 92–96행
출처:packages/interaction/user-approval/src/index.ts 84–94행
거부: Grok은 시작할 때 격리를 한 번 설치합니다
Grok은 nono로 프로세스 시작 때 Landlock이나 Seatbelt를 한 번 설치합니다. 네트워크는 프로세스 급에서 열려 있고, 자식은 seccomp로 망을 막아요. 빈 web_fetch 허용 목록은 전부 막고, loopback은 기본 통과입니다. Codex가 겁내는 건 도구가 로컬 관리 포트에 닿는 일이에요. Grok이 겁내는 건 모델이 바깥망을 헤매는 일이고, 로컬 개발에는 문을 남겨 둡니다.
출처:crates/codegen/xai-grok-sandbox/src/lib.rs 8–12행
출처:crates/codegen/xai-grok-tools/src/implementations/grok_build/web_fetch/ssrf.rs 14–18행
두 판결은 언제 반대가 되나요
위 데모를 열고 토큰을 들고 출장으로 자르세요. 거부 가능이 문 넷을 지나고도 왜 통과하는지, 재생 가능이 JSONL이 있어도 왜 환경 변수를 맞추지 못하는지 밀어 보세요.
그다음 샌드박스가 위험한 쓰기를 막음으로 잘라, 같은 장치가 이번엔 왜 일치하는 판결을 내는지 보세요.