OpenAI Codex · 두 가지 보안 시선

두 가지 보안 시선: 같은 명령, 판결 둘

일이 나기 전에 거절할 문이 있는지, 난 뒤에 모델이 그때 본 세계를 복원할 수 있는지. 같은 위험한 명령에서 이 두 시선은 일치하기도, 반대 결론을 내기도 해요.

강의 목표읽고 나면 두 가지를 말할 수 있어요. 재생 가능이 원하는 건 모델이 그때 본 세계를 로그에서 다시 맞출 수 있는 일입니다. 거부 가능이 원하는 건 명령이 돌기 전에 정책, 승인, 샌드박스, 프록시 가운데 적어도 한 문이 “아니요”라고 말할 수 있는 일이에요. 같은 명령에서 둘이 어디서 일치하고, 어디서 서로 길을 양보하는지.
먼저 해보기 · 같은 명령, 판사 둘
같은 위험한 조작을 두 보안 시선이 각각 어떻게 판결하는지
사고
정책에 이미 Forbidden이 있어요. 양쪽이 반대 결론을 내는지 보세요.
git reset --hard HEAD~3
재생 가능심리 중
묻는 것: 일이 난 뒤, 현장을 다시 맞출 수 있는가.
거부 가능심리 중
묻는 것: 일이 나기 전에, “아니요”라고 말할 문이 있는가.
두 판결이 아직 맞닿지 않았어요.
논리 궤적 · 애니메이션 각 단계가 소스의 어느 구간에 대응하는지
  1. 같은 argv를 두 시선에 동시에 넘깁니다L장면
  2. 거부 가능은 먼저 Decision 세 상태를 보고 가장 엄한 칸을 씁니다decision.rs L9
  3. 승인 캐시 key는 완전한 argv를 들고, 접두사는 인정하지 않아요unified_exec.rs L92
  4. Guardian이 시간 초과이거나 나쁜 출력이면 게이트를 닫아요guardian/mod.rs L11
  5. 플랫폼 샌드박스 백엔드 셋. Windows가 꺼져 있으면 Nonemanager.rs L37
  6. 재생 가능은 JSONL 원문을 믿고, SQLite는 거울일 뿐이에요README.md L22
  7. fork는 진짜 TurnStarted를 만나야 해요thread_rollout_truncation.rs L187
  8. 실패는 관찰로 쓰고, success는 여전히 truecontext.rs L351
  9. 프록시 403은 명령 프로세스로 돌아가고, 루프는 계속돼요responses.rs L80
  10. 두 판결을 대조: 일치인가 반대인가L검수
재생을 눌러, 같은 명령이 두 시선에서 어떻게 판결되는지 보세요.
판결 둘
틈은 어디인가
명령을 바꾸기나머지 사고 둘로 잘라, 일치와 반대가 자리를 바꾸는지 보세요.
수업용 스케치: 명령 텍스트는 수업용 보기이고, 데모는 실제 셸이나 실제 프록시를 부르지 않아요. 논리 궤적 오른쪽 행 번호는 openai/codex 저장소 commit 4f39251a01에 대응합니다.
아이디어 1 · 일이 난 뒤, 현장이 아직 있는가
어떤 문제를 푸는가

월요일 아침, 보안 동료가 채팅 기록을 그룹에 던집니다. 모델이 어젯밤 외부 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만이 말해 줍니다.

DSH: 먼저 이벤트로 남기고, 그다음 모델에 투영 모델 가시 입력 세션 로그에 쓰기 deriveMessages 모델에 보내기 Codex: 먼저 모델에 보내고, 확정 뒤에 원문을 남김 컨텍스트 조립 모델에 보내기 JSONL 원문 SQLite 거울 승인 과정은 정책에 버려질 수 있어요
수업용 구조도: 둘 다 원문과 투영이 있고, DSH는 이벤트를 남기는 일을 모델에 보내기 앞에 둡니다.
왜 오래가는가

원문과 투영을 갈라 두면, 투영이 깨져도 다시 만들 수 있고, 원문이 깨지면 현장이 사라져요. 저장소를 바꿔도 물을 것은 누가 원문인가입니다. 이 계약은 언어가 바뀌어도 남아요.

아이디어 2 · 일이 나기 전에, 어떤 문이든 “아니요”라고 말할 수 있어요
어떤 문제를 푸는가

수요일 오후, 다른 기계의 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 testnpm 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행

모델이 argv를 제안 execpolicy 승인과 캐시 샌드박스 출장 프록시 Forbidden deny 또는 Abort 시작 실패 프로세스가 403을 받음 어떤 문이든 거절할 수 있어요. 프록시가 막는 건 출장이지 argv가 아닙니다. DNS rebinding은 이 층이 스스로 못 막는다고 인정해요.
수업용 구조도: 문 넷에 출장 깔때기 하나. 각 문은 위험 한 종류만 잠급니다.

출처:codex-rs/network-proxy/README.md 234–238행

왜 오래가는가

각 층이 보는 것이 다릅니다. 정책은 argv를, 승인은 사람을, 커널은 경로와 syscall을, 프록시는 호스트를 봐요. 한 층이 못 보면 거기서 멈춥니다. 규칙이 사람이 읽는 파일에만 있고 로드 시점 예가 없으면 소스와 함께 떠내려가요. AGENTS.md 35행은 아직 mcp_connection_manager.rs를 가리키는데, 저장소에 그 파일이 없습니다.

출처:AGENTS.md 35행

아이디어 3 · 거절은 끝났어도, 관찰은 남아야 해요
어떤 문제를 푸는가

명령 종료 코드가 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는 그것을 가리키는 심볼릭 링크)와 아키텍처 문서에 씁니다. 모델 요청에 들어가는 입력은 모두 세션 로그에서 다시 만들 수 있어야 해요. 이어 붙이기만 하는 로그가 진실이고, 모델이 보는 것은 투영입니다. 승인 정책은 asknever뿐이고, Guardian도 프로세스 안 출장 프록시도 없어요.

Codex의 재생 가능은 확정된 역사에서 멈춥니다. DSH는 한 걸음 앞으로 가요. 새로운 모델 가시 입력은 먼저 세션 이벤트가 되어야 합니다. 재생 쪽 계약은 더 단단하고, 거부 쪽은 더 얇아요.

출처:AGENTS.md 107행

출처:docs/architecture.md 92–96행

출처:packages/interaction/user-approval/src/index.ts 84–94행

양쪽 모두 소스 대조 완료 · 2026-08-22 · DSH · Model-visible ⟺ logged

거부: 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행

양쪽 모두 소스 대조 완료 · 2026-08-22
수업 실습
01

두 판결은 언제 반대가 되나요

위 데모를 열고 토큰을 들고 출장으로 자르세요. 거부 가능이 문 넷을 지나고도 왜 통과하는지, 재생 가능이 JSONL이 있어도 왜 환경 변수를 맞추지 못하는지 밀어 보세요.

그다음 샌드박스가 위험한 쓰기를 막음으로 잘라, 같은 장치가 이번엔 왜 일치하는 판결을 내는지 보세요.

Takeaway: 재생 가능은 일이 난 뒤 복원할 수 있는지를 묻습니다. 거부 가능은 일이 나기 전에 문이 있었는지를 묻고요. 어느 쪽을 베낄지 정하기 전에 셋만 물으세요. 누구의 기계에서 도는지, 누구의 데이터를 다루는지, 실패 값이 비밀 유출인지, 저장소가 바뀌는 일인지, 재현 못 하는 평가인지.