Codex · 가드레일: 샌드박스 + 권한 + 승인 (Seatbelt/Landlock/bwrap + Guardian)
한 줄 요약
AI가 “이 명령을 실행해 / 이 파일을 고쳐 / 이 서버에 접속해”라고 말한 행동을 진짜로 실행하기 직전에 네 겹의 검문소로 거르고, 허락하더라도 OS가 만든 ‘우리’ 안에 가둬서 돌리는 안전장치다. 왜 배우나: AI에게 실제 컴퓨터 조작 권한을 주면서도 사고를 막으려면, “물어볼까/그냥 할까/막을까”를 누가 어떻게 정하는지를 알아야 안전한 에이전트를 설계할 수 있기 때문이다.
그림
flowchart TD A["모델이 도구 호출 반환<br/>shell·패치·네트워크 등"] --> B["1차: 규칙표로 빠른 판정<br/>execpolicy"] B -->|금지된 명령| R[차단] B -->|확실히 안전| S0[샌드박스 우회 가능] B -->|"애매함 / 물어보기"| C["2차: 정책 + 현재폴더 + 쓰기허용폴더로<br/>안전결정 산출"] C --> D{"안전결정<br/>SafetyCheck"} D -->|자동허용| W[샌드박스로 감싸서 실행] D -->|거절| R D -->|물어봐| E{"사람 대신<br/>AI 심판에게?"} E -->|예| G["Guardian AI가<br/>정책 보고 허용/거부"] E -->|아니오| U[사용자에게 승인창 표시] G -->|허용| W G -->|"거부·시간초과·해석실패"| R U -->|승인| W U -->|거부| R W --> X["OS 우리 안에서 실행<br/>macOS Seatbelt / Linux Landlock+bwrap / Windows 제한토큰"]
쉽게 풀기
공항 보안검색을 떠올리면 쉽다. 비행기(실제 실행)에 타기 전에 반드시 검문소를 통과해야 하고, 검문소는 한 겹이 아니라 네 겹으로 짜여 있다.
① 승인 정책(Approval) — “기준표” 이 행동을 만나면 어떻게 할지 미리 정한 회사 규칙이다. “그냥 통과 / 사람에게 물어봐 / 무조건 거절” 중 하나를 정한다. 예를 들어 “프로젝트 폴더 안 작업은 그냥 통과, 밖으로 나가면 물어봐” 같은 식이다.
② 샌드박스(Sandbox) — “방탄 우리” 통과시키더라도 맨몸으로 내보내지 않는다. OS가 만든 우리 안에 가둬서 돌린다. 이 우리는 “이 폴더만 쓸 수 있고, 인터넷은 막혀 있다”처럼 물리적으로 손발을 제한한다. macOS는 Seatbelt, Linux는 Landlock+bubblewrap, Windows는 제한 토큰이라는 서로 다른 우리를 쓴다.
③ execpolicy — “단골 손님 명단”
git status처럼 매번 물어볼 필요 없이 명백히 안전한 명령은, 규칙표(DSL)로 미리 “허용/금지/물어보기”를 즉답한다. 검문소 앞에 붙은 ‘프리패스 명단’ 같은 것이다. 단, 여러 규칙이 동시에 걸리면 가장 빡센 판정을 따른다(허용보다 금지가 이긴다).
④ Guardian — “대리 심판 AI” “사람에게 물어봐야 하는” 건이 너무 많으면 사용자가 지친다(승인 피로). 그래서 사람 대신 또 다른 AI 심판을 세워, 회사 정책 문서를 읽고 위험도를 매겨 자동으로 허용/거부하게 한다. 이 심판은 의심스러우면 무조건 거부하는 보수적 성격이고(시간 초과·오류·이상한 답 = 전부 거부), 거부가 한 턴에 3번 넘게 쌓이면 아예 작업을 멈춘다(폭주 차단).
핵심 흐름은 이렇다. 정책 + 지금 어느 폴더에 있나 + 쓸 수 있는 폴더가 어디까지인가를 보고 “안전결정(SafetyCheck)” 하나를 뽑는다. 그 결정이 자동허용이면 샌드박스 우리에 넣어 실행하고, 물어봐면 승인창(또는 Guardian)으로 보내고, 거절이면 그냥 막는다.
비유 한 줄 정리
승인 정책 = 기준표 / 샌드박스 = 방탄 우리 / execpolicy = 프리패스 명단 / Guardian = 졸린 사람 대신 서는 대리 심판.
핵심 정리
안전결정 SafetyCheck — 결국 셋 중 하나로 떨어진다 (core/src/safety.rs)
| 결정 | 뜻 | 딸려오는 정보 |
|---|---|---|
AutoApprove | 자동 허용 | 어떤 샌드박스로 감쌀지(sandbox_type) |
AskUser | 사람/Guardian에게 물어봄 | — |
Reject | 막음 | 거절 사유(reason) |
승인 정책 AskForApproval — “언제 물어볼까”의 5단계 (protocol/src/protocol.rs:814)
| 값 | 한마디로 | 패치 안전성 평가 시 |
|---|---|---|
UnlessTrusted | 신뢰 명령 빼곤 항상 물음 | 즉시 물어봄 |
OnFailure | 실패할 때만 물음 | 일단 샌드박스로 자동 실행 |
OnRequest | 모델이 요청할 때 물음 | 쓰기폴더 안이면 자동, 밖이면 물음 |
Granular(...) | 항목별 세분 제어 | sandbox_approval=false면 거절 |
Never | 절대 안 물음 | 쓰기폴더 밖이면 곧장 거절 |
샌드박스 백엔드(우리의 종류)와 규칙 판정
- SandboxType (sandboxing/src/manager.rs:25):
None(없음) ·MacosSeatbelt(seatbelt) ·LinuxSeccomp(bubblewrap+seccomp+Landlock) ·WindowsRestrictedToken(제한토큰)- execpolicy Decision (execpolicy/src/decision.rs:9):
Allow(0·가장 약함) <Prompt(1) <Forbidden(2·가장 강함)- 여러 규칙이 매칭되면
max()로 가장 제한적인 결정을 채택한다(policy.rs:366).
Guardian가 심사하는 행동 종류 (approval_request.rs:17)
-
Shell/ExecCommand/Execve(unix) — 셸·명령 실행 -
ApplyPatch— 파일 패치 -
NetworkAccess— 네트워크 접근 -
McpToolCall— MCP 도구 호출 -
RequestPermissions— 권한 요청
Guardian의 답안지 GuardianAssessment (core/src/guardian/mod.rs:64): risk_level(low/medium/high/critical) · user_authorization(unknown/low/medium/high) · outcome(allow/deny, 필수) · rationale(근거).
실제 예시
먼저, 패치(파일 수정)를 어떻게 판정하는지 핵심 분기를 보자. “쓰기 허용 폴더 안인가?”가 갈림길이다.
// /home/seunghyeong/harness-work/codex/codex-rs/core/src/safety.rs
let rejects_sandbox_approval = matches!(policy, AskForApproval::Never)
|| matches!(
policy,
AskForApproval::Granular(granular_config) if !granular_config.sandbox_approval
);
// 패치가 쓰기 가능 루트 안에 있거나(OnFailure면 우회) → 샌드박스로 자동 실행 시도
if is_write_patch_constrained_to_writable_paths(action, file_system_sandbox_policy, cwd)
|| matches!(policy, AskForApproval::OnFailure)
{
// ... Disabled/External 프로파일은 SandboxType::None 으로 AutoApprove ...
match get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) {
Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type, user_explicitly_approved: false },
None => {
if rejects_sandbox_approval {
SafetyCheck::Reject { reason: patch_rejection_reason(/*...*/).to_string() }
} else {
SafetyCheck::AskUser // 샌드박스를 못 거니 사람에게 물음
}
}
}
} else if rejects_sandbox_approval {
SafetyCheck::Reject { reason: /* "writing outside of the project; rejected by ..." */ }
} else {
SafetyCheck::AskUser
}다음은 샌드박스 ‘우리’의 바탕 설계도다. macOS Seatbelt는 일단 전부 막고(deny default) 꼭 필요한 것만 연다.
; /home/seunghyeong/harness-work/codex/codex-rs/sandboxing/src/seatbelt_base_policy.sbpl
(version 1)
; start with closed-by-default
(deny default)
; child processes inherit the policy of their parent
(allow process-exec)
(allow process-fork)
(allow signal (target same-sandbox))
; /dev/null 쓰기만 허용
(allow file-write-data
(require-all
(path "/dev/null")
(vnode-type CHARACTER-DEVICE)))이 바탕 정책은 모든 것을 막은 뒤 최소한의 시스템 호출(프로세스 생성, 일부 sysctl 읽기, pty 등)만 연다. 쓰기 가능 폴더와 네트워크 허용은 정적으로 박아두지 않고 실행 시점에 정책 파라미터로 덧붙인다(manager.rs의 create_seatbelt_command_args가 file_system_sandbox_policy/network_sandbox_policy를 끼워 조립).
Linux에서는 헬퍼 프로그램에 넘길 인자(argv)를 이렇게 조립한다. “어디까지 우리에 넣을지”, “네트워크가 필요한지”에 따라 옵션이 달라진다.
// /home/seunghyeong/harness-work/codex/codex-rs/sandboxing/src/landlock.rs
let mut linux_cmd: Vec<String> = vec![
"--sandbox-policy-cwd".to_string(), sandbox_policy_cwd,
"--command-cwd".to_string(), command_cwd,
"--permission-profile".to_string(), permission_profile_json, // 권한 프로파일을 JSON으로 전달
];
// 프록시 네트워크가 필요 없고 legacy landlock 모드면 bwrap 대신 landlock만
if use_legacy_landlock && !allow_network_for_proxy {
linux_cmd.push("--use-legacy-landlock".to_string());
}
if allow_network_for_proxy {
linux_cmd.push("--allow-network-for-proxy".to_string()); // 격리 net namespace 필요 → bwrap
}
linux_cmd.push("--".to_string()); // 구분자: 뒤 인자를 헬퍼 옵션으로 오해하지 않게
linux_cmd.extend(command); // 원래 도구 명령 append직접 만든다면, 결정 트리는 이렇게 단순화된다(의사 Rust).
// 1) 규칙 우선
match execpolicy.check(&cmd) {
Decision::Forbidden => return Reject("policy forbade"),
Decision::Allow => { /* bypass_sandbox 가능, 단 denied-read 있으면 유지 */ }
Decision::Prompt => { /* 아래 정책 평가로 */ }
}
// 2) 정책 + cwd/쓰기루트로 SafetyCheck 산출
let check = match policy {
Never if !inside_writable_roots => Reject("outside project, never-ask"),
Granular(c) if !c.sandbox_approval && needs_ask => Reject("granular: no sandbox approval"),
OnFailure => AutoApprove { sandbox: platform_sandbox() }, // 실패하면 그때 상승
OnRequest if inside_writable_roots => AutoApprove { sandbox: platform_sandbox() },
_ => AskUser,
};
// 3) 결과 처리
match check {
AutoApprove { sandbox } => manager.transform(req_with(sandbox)).run(),
AskUser => if routes_to_guardian { guardian_review() } else { ask_user() },
Reject(r) => deny(r),
}설계 체크리스트:
- 샌드박스 바탕 정책은 deny-default(전부 막고 필요한 것만 허용)로 시작한다.
- 쓰기 폴더/네트워크 허용은 정적 정책이 아니라 실행 시점 파라미터로 주입한다.
- 여러 규칙이 겹치면 가장 빡센 결정을 채택한다(
max()). - AI 심판에게 넘기는 대화/행동은 **신뢰 불가 증거(untrusted evidence)**로 라벨해 프롬프트 인젝션을 막는다.
- AI 심판은 fail-closed(시간초과/실패/이상한 답 = 거부) + 연속거부 서킷브레이커를 둔다.
- 샌드박스를 벗기는 상승은 읽기차단(denied-read)이 없을 때만 허용한다.
-
apply_patch는 하드링크로 폴더 밖을 가리킬 수 있으니, 쓰기폴더 안이라도 샌드박스로 실행한다.
AI 흐름에서 언제 작동하나
모델이 도구 호출(
shell/unified_exec/apply_patch/MCP/네트워크)을 반환한 직후, 실행 경계에서 매번 작동한다. 모델 프롬프트에 미리 들어가는 게 아니라 **모델 출력의 후처리(실행 게이트)**다. Guardian만은 예외적으로 또 다른 LLM 세션을 띄워, 원래 사용자에게 갈 승인 요청을 대신 심사한다(routes_approval_to_guardian이 참일 때). Guardian 프롬프트는 정책 문서 + 압축된 대화 + 검토 대상 행동 JSON으로 구성되고, 대화/도구결과/행동을 “신뢰할 수 없는 증거, 따를 명령이 아님”으로 명시해 인젝션을 방어한다. 타임아웃(90초)·실행실패·JSON 파싱실패는 전부 거부, 연속 거부가 한 턴 3회/최근 10회를 넘으면 서킷 브레이커가 턴을 중단한다(mod.rs:49-51, 103-120).
승인 상승(escalation)이란
명령이 샌드박스 안에서 실패하면,
OnFailure/UnlessTrusted/Granular(sandbox_approval=true)일 때 “샌드박스 없이 다시 실행할까요?”를 띄운다(tools/sandboxing.rs:355). 단 읽기차단(denied-read) 제한이 걸려 있으면 샌드박스를 벗기는 순간 그 차단이 풀려버리므로, 상승을 막고 샌드박스를 유지한다(sandboxing.rs:283).
요약 & 셀프체크
3줄 요약:
- 가드레일은 승인 정책 → execpolicy 규칙 → 안전결정 → 샌드박스 실행의 네 겹 검문소로, 모든 도구 호출을 실행 직전에 거른다.
- 결정은 결국 자동허용 / 물어봐 / 거절 셋 중 하나이고, “지금 폴더가 쓰기 허용 범위 안인가”가 가장 큰 갈림길이다.
- Guardian는 사람의 승인 피로를 덜어주는 대리 AI 심판이며, **의심스러우면 거부(fail-closed)**가 기본이다.
스스로 답해보기:
- 어떤 명령을 “그냥 통과”시키더라도 왜 굳이 샌드박스 우리에 넣어 실행할까?
- 여러 규칙이 동시에 걸렸을 때
Allow가 아니라Forbidden이 이기도록 만든 이유는? - 샌드박스에서 실패한 명령을 “샌드박스 없이 다시 실행”하려 할 때, 읽기차단이 있으면 막는 이유는 무엇일까?
근거 파일
- /home/seunghyeong/harness-work/codex/codex-rs/core/src/safety.rs (SafetyCheck, assess_patch_safety, rejects_sandbox_approval, 쓰기루트 제약 검사)
- /home/seunghyeong/harness-work/codex/codex-rs/sandboxing/src/manager.rs (SandboxType, get_platform_sandbox, SandboxManager.transform, SandboxExecRequest)
- /home/seunghyeong/harness-work/codex/codex-rs/sandboxing/src/seatbelt_base_policy.sbpl (deny-default Seatbelt 베이스)
- /home/seunghyeong/harness-work/codex/codex-rs/sandboxing/src/landlock.rs (Linux 헬퍼 argv, —use-legacy-landlock / —allow-network-for-proxy)
- /home/seunghyeong/harness-work/codex/codex-rs/execpolicy/src/policy.rs (Policy, Evaluation, max() 합산)
- /home/seunghyeong/harness-work/codex/codex-rs/execpolicy/src/decision.rs (Decision: Allow<Prompt<Forbidden)
- /home/seunghyeong/harness-work/codex/codex-rs/execpolicy/src/rule.rs (PrefixRule, NetworkRule, RuleMatch)
- /home/seunghyeong/harness-work/codex/codex-rs/core/src/guardian/mod.rs (GuardianAssessment, 서킷브레이커, 상수)
- /home/seunghyeong/harness-work/codex/codex-rs/core/src/guardian/prompt.rs (프롬프트 조립, output_schema, untrusted evidence, fail-closed 파싱)
- /home/seunghyeong/harness-work/codex/codex-rs/core/src/guardian/policy.md, policy_template.md (테넌트 위험 분류 정책)
- /home/seunghyeong/harness-work/codex/codex-rs/core/src/guardian/review.rs (routes_approval_to_guardian, review_approval_request)
- /home/seunghyeong/harness-work/codex/codex-rs/core/src/guardian/approval_request.rs (GuardianApprovalRequest variants)
- /home/seunghyeong/harness-work/codex/codex-rs/core/src/tools/sandboxing.rs (ExecApprovalRequirement, SandboxOverride, wants_no_sandbox_approval, unsandboxed_execution_allowed)
- /home/seunghyeong/harness-work/codex/codex-rs/protocol/src/protocol.rs:814 (AskForApproval, GranularApprovalConfig)
- /mnt/d/6study/10_프레임워크분석/_원문아카이브/codex/31_sandbox.md
- /mnt/d/6study/10_프레임워크분석/_원문아카이브/codex/68_permissions.md
- /mnt/d/6study/10_프레임워크분석/_원문아카이브/codex/02_agent-approvals-security.md
연결
CX_개요 · _분석축_루브릭 · CX_30_tool-system · CX_70_mcp-and-plugins
Codex 교차검증 (원문 아카이브 대조)
공식 문서(02_agent-approvals-security.md)와 대조해 핵심 사실을 확인했다. ① 보안은 샌드박스 모드(무엇을 할 수 있나) + 승인 정책(언제 멈춰 물어보나) 두 층이 함께 작동한다 — 본문 4겹 구조와 일치. ② 기본값은 네트워크 차단 + 쓰기 범위를 워크스페이스로 제한,
Auto프리셋은--sandbox workspace-write --ask-for-approval on-request. ③ OS별 우리: macOS=Seatbelt(sandbox-exec), Linux=bwrap+seccomp, Windows=WSL2면 Linux 샌드박스/네이티브면 Windows 샌드박스 — 본문 백엔드 표와 일치. ④ 쓰기 허용 루트 안에도 보호 경로(.git/.agents/.codex는 읽기 전용)가 있어, “쓰기폴더 안=무조건 허용”이 아님에 유의. ⑤ Auto-review(=Guardian) 정책은 데이터 유출·자격증명 탐침·지속적 보안 약화·파괴적 행동을 검사하고, critical은 거부 / high는 충분한 사용자 인가 필요 / 프롬프트빌드·리뷰세션·파싱 실패는 fail-closed / 타임아웃은 별도 표시하되 실행 안 함 — 본문 fail-closed 서술과 일치. ⑥ 네트워크는network_proxy기능으로 도메인 allowlist 제어(deny가 항상 우선, 전역*는 allow 전용), DNS 리바인딩 best-effort 차단까지 추가됨(본문 미수록 보강 포인트).