승인 정책: 같은 명령, 물을지 말지는 노브 두 개
기본으로 물을지는 샌드박스 종류에 묶여 있어요. 한 번 Yes를 누른 뒤, 기억하는 것은 전체 명령일까요, 접두일까요.
- 이번 turn의 AskForApproval을 읽습니다protocol.rs L924
- FileSystemSandboxKind가 Restricted인지 봅니다permissions.rs L227
- Never는 Skip, UnlessTrusted는 NeedsApprovalsandboxing.rs L198
- OnRequest나 Granular는 Restricted일 때만 묻습니다sandboxing.rs L200
- Granular가 물어야 하는데 샌드박스 승인을 끄면 Forbiddensandboxing.rs L209
- execpolicy Prompt는 두 번째 문을 한 번 더 지납니다exec_policy.rs L214
- 세션 캐시는 ApprovedForSession만 받고, 정확한 key를 봅니다sandboxing.rs L108
- 정책이 바뀌면 모델 설명서도 같이 바뀝니다permissions_instructions.rs L271
승인은 on-request에 두고, 샌드박스를 workspace-write에서 danger-full-access로 비틉니다. 십 분 전 그 샌드박스 밖 명령은 창이 떴어요. 지금은 안 뜹니다. 승인 노브는 안 건드렸습니다.
승인 정책 AskForApproval은 이번 turn이 쓰는 사람 질문 규칙이고, 타입에는 variant가 넷입니다. 파일 시스템 샌드박스 종류는 셋이고 기본은 Restricted, 읽기 전용이거나 작업 공간만 쓰는 뜻이에요. 노브 둘은 같은 판정 함수 안에 있습니다. 하나를 비틀면 다른 쪽 행동도 따라갑니다.출처:codex-rs/protocol/src/protocol.rs 924–947행, codex-rs/protocol/src/permissions.rs 227–232행
명령은 먼저 승인 정책을 읽고, 파일 시스템이 Restricted인지 본 뒤, execpolicy가 Allow·Prompt·Forbidden을 겹칩니다. execpolicy는 명령 텍스트에 세 상태를 주는 규칙 파일이에요.
기본으로 물을지는 default_exec_approval_requirement에 있습니다. Never 층은 Skip, UnlessTrusted 층은 NeedsApproval. OnRequest와 Granular는 Restricted일 때만 묻습니다. read-only와 workspace-write는 둘 다 Restricted라 두 칸의 기본 결말이 같아요. danger-full-access는 kind를 Unrestricted로 바꾸고, 기본 함수는 Skip을 탑니다.출처:codex-rs/core/src/tools/sandboxing.rs 198–230행
Granular에는 문이 하나 더 있어요. 승인이 필요하고 sandbox_approval이 꺼져 있으면 바로 Forbidden. 파일 시스템이 이미 Unrestricted면 needs_approval이 먼저 거짓이 되어 Forbidden 분기에 들어가지 못합니다. 샌드박스 승인을 끄는 것은, 원래 뜨려던 창만 막는 일입니다.
Never가 execpolicy Prompt를 만나면 질문은 거절로 승급합니다. 무엇이든 달려도 된다는 뜻이 아니에요.출처:codex-rs/core/src/exec_policy.rs 214–236행
untrusted는 설정에 쓸 수 없어요. 타입은 남아 있고, 공개 문자열은 은퇴했습니다. 사용자가 명시적으로 쓰면 로드 전체가 실패해요. 안 써야 프로젝트 신뢰를 봅니다. 신뢰면 OnRequest, 명시적으로 미신뢰면 UnlessTrusted.출처:codex-rs/core/src/config/mod.rs 3607–3625행
로드 시점의 하드 거절이 하나 더 있습니다. requirements가 danger-full-access를 허용하지 않는데 설정이 approval_policy = "never"이면, 로더는 권한 프로필을 먼저 읽기 전용으로 떨어뜨립니다. 읽기 전용에 질문 안 함은, 모델이 좁은 샌드박스에 있는데 물을 사람이 없다는 뜻이에요. 그 조합은 바로 불법입니다.출처:codex-rs/core/src/config/mod.rs 3969–3979행
전체 디스크가 쓰일 때 샌드박스 밖을 한 번 더 묻는 것은 의미가 없어요. 노브 둘을 묶으면 팝업이 줄어듭니다. 대가는 샌드박스를 바꾸면 승인 행동이 조용히 따라간다는 점이에요. 다른 언어로 다시 써도 먼저 답해야 합니다. 전권 모드에서도 사람을 물을까요.
정책 enum은 분기를 남김없이 써서 규칙을 말합니다. variant를 하나 더하면 컴파일러가 모든 판정 함수에 입장을 강요해요. 타입이 런타임의 문을 지키는 겁니다.
팝업의 기억 두 줄은 닮아 보여요. 하나는 이 세션에서 이 명령을 기억하는 것. 다른 하나는 접두를 execpolicy에 써서 세션을 넘기는 것. 잘못 누르면 뒤의 경계 문제가 반대로 나옵니다.
일반 exec 팝업 기본 메뉴에는 ApprovedForSession조차 없어요. 네트워크 컨텍스트가 있을 때만 붙습니다. 보통 명령은 한 번 승인, 접두 수정 제안이 있으면 접두 기억을 더하고, 마지막은 취소. 사용자가 가장 자주 만나는 기억은 접두 쪽입니다.출처:codex-rs/protocol/src/approvals.rs 314–347행
세션 캐시는 ApprovalStore에 살고, 세션과 수명이 같습니다. key는 정규화된 전체 명령에 cwd, 환경, execpolicy 지문을 더한 것이에요. 모든 key가 이미 ApprovedForSession일 때만 맞습니다. 한 번 통과인 Approved는 map에 들어가지 않아요.출처:codex-rs/core/src/tools/sandboxing.rs 64–116행
npm run test를 세션에 승인하면 npm run lint는 다른 key입니다. /bin/bash -lc와 bash -lc는 단일 평문 명령을 쪼갤 수 있을 때 같은 token 묶음으로 캐시돼요. 세션 캐시는 전체 명령을 보고, npm *는 보지 않습니다. 접두 확장은 execpolicy로만 갑니다.
승인 정책을 바꿔도 이 map은 비우지 않아요. key에 AskForApproval이 없습니다. 도중에 OnRequest에서 UnlessTrusted로 바꿔도 이미 캐시된 승인은 유효해요. execpolicy를 바꿔야 지문이 바뀌고, 그때야 캐시가 죽습니다.
기억 방식 둘은 위협 둘에 대응합니다. 정확한 key는 인자 교체를 막지 못해요. 접두는 막지만, 포장 명령의 범위를 너무 넓히기도 쉽습니다. 층을 나눠야 각자 금지 조언 명단을 달 수 있어요. 하루 팝업이 많지 않으면, 한 번만 통과시켜도 성립합니다.
정책만 바꾸고 런타임 분기만 고치면 부족해요. 모델이 보는 설명도 같이 바뀌어야 합니다. 안 그러면 옛 규칙으로 require_escalated를 또 달라고 해요. Never 아래에서 설명서가 여전히 승격을 가르치면, 런타임은 질문을 거절로 승급하고 모델은 벽만 반복해서 부딪힙니다.
권한 설명은 developer 메시지 한 토막입니다. 샌드박스 템플릿을 고르고, 승인 템플릿을 고릅니다. Never, UnlessTrusted, OnRequest는 각각 준비된 마크다운이 있어요. Never 파일은 한 줄입니다. sandbox_permissions를 다시 주지 마세요, 명령은 거절됩니다. Granular에는 다섯 번째 파일이 없고, 스위치 다섯으로 허용·거절 목록을 그 자리에서 붙입니다. 설명 두 벌을 한 토막에 붙여, 모델이 현재 조합을 한 번에 봅니다.출처:codex-rs/prompts/src/permissions_instructions.rs 271–291행
훅이 사람보다 앞에 있어요. 훅이 Allow나 Deny를 주면 팝업은 안 뜹니다. 훅의 Allow는 한 번의 Approved이고, 세션 캐시에는 쓰지 않아요.
런타임과 모델 설명서는 같은 계약의 양면입니다. 판정 함수를 바꾸면 사전 그 줄도 같이 바꿔야 해요. 둘을 같은 모듈에 두고 같은 테스트로 양쪽에 먹이면, 다른 언어로 옮겨도 됩니다.
DSH: 노브 둘, 한 번 승인은 넓히지 않음
DSH 승인 정책은 ask와 never뿐입니다. never는 답하는 쪽에 넘기기 전에 rejected를 돌려주고, 뒤에 걸린 리스너는 그 약속을 못 바꿉니다. 승인 결과는 allowed-once뿐이에요. 세션 캐시도, 접두 수정도 없습니다. 한 번 물어도 다음에는 또 물어요.출처:packages/interaction/user-approval/src/index.ts 84–94행, 304–312행
샌드박스와 승인은 독립 노브 둘입니다. 사용자가 보는 드롭다운은 프리셋 표예요. workspace-write는 ask에, danger-full-access는 never에 묶입니다. 다음 단을 누르면 setter 둘을 따로 호출하고, 표에 안 맞으면 custom을 보여 줍니다. 그래서 DSH는 승인을 ask에 두고 샌드박스만 전체 디스크 쓰기로 비틀 수 있어요. Codex의 OnRequest는 Unrestricted에서 기본이 Skip이라, 그 조합은 기본 함수 안에서 따로 존재할 수 없습니다.출처:packages/interaction/permission-presets/src/index.ts 167–176행
Claude Code: 기억을 규칙 표에 씁니다
Claude Code의 대외 권한 모드는 다섯 단이고, 내부에 auto와 bubble이 더 있어요. 규칙 출처는 userSettings, projectSettings, session, cliArg. 판정은 도구 전체 deny를 먼저 보고 아래로 내려갑니다.출처:restored-src/src/types/permissions.ts 16–29행, restored-src/src/utils/permissions/permissions.ts 1169–1181행
alwaysAllowRules는 출처별로 허용을 적을 수 있고, session은 그중 한 단입니다. 다음에는 규칙으로 맞추며, 전체 argv가 같을 필요는 없어요. 대가는 매칭 함수가 포장을 스스로 막아야 한다는 점입니다. Codex는 범위 확장을 execpolicy 접두와 금지 조언 명단에 맡깁니다. dontAsk는 Never에 가깝습니다. Codex에는 공개된 전면 허용 승인 정책이 없고, Never도 execpolicy에 막혀요.출처:restored-src/src/types/permissions.ts 54–62행, 433행
샌드박스를 비틀면, 팝업은 남아 있을까요
승인은 on-request에 두고, 샌드박스를 workspace-write에서 danger-full-access로 비틉니다. 작업 공간 밖 경로를 쓰는 명령을 다시 돌리세요. 팝업은 남아 있을까요? 왜요?
이어서 같은 npm run test를 세션으로 승인한 뒤 npm run lint를 제출합니다. 캐시는 맞아야 할까요? 접두로 기억을 눌렀다면 답이 바뀔까요?