'모두 승인(Approve All)'은 당신의 에이전트가 위험한 것을 배포하게 만드는 방식입니다
요약
코딩 에이전트의 권한 승인 방식에서 발생하는 '승인 피로'와 '보안 위험' 문제를 다룹니다. 단순히 도구의 종류나 가역성을 기준으로 삼는 대신, 시스템의 상태를 변경하는지 여부에 따라 승인 경계를 설정해야 한다고 제안합니다.
핵심 포인트
- 모든 명령을 승인하면 승인 피로로 인해 중요한 위험을 놓칠 수 있음
- 모든 승인을 건너뛰면 에이전트가 치명적인 명령을 실행할 위험이 있음
- 승인 기준은 도구의 종류가 아닌 '상태 변이(state mutation)' 여부여야 함
- 읽기 작업은 자동 승인하고, 쓰기 작업은 반드시 차단(Gate)할 것을 권장
미리 밝혀둡니다: 저는 이 시장의 도구 중 하나인 agentproto를 만들고 있습니다. 후반부의 원시 요소(primitives)들은 실제이며 명령어도 확인 가능합니다. 전반부의 문제는 그 자체로 성립하는 문제입니다. 수정 사항이 있다면 언제든 환영합니다 — 이슈를 남겨주세요.
당신은 코딩 에이전트(coding agent)에게 셸(shell)을 부여했습니다. 이제 당신은 두 가지 나쁜 집 중 하나에 살게 되었습니다.
첫 번째 집에서는, 모든 위험한 명령이 멈춰서 당신에게 묻습니다. git push를 실행해도 될까요? 이 파일을 삭제해도 될까요? 이걸 curl 해도 될까요? 당신은 승인하고, 승인하고, 또 승인합니다 — 그러다 약 200번째 정도의 반사적인 _'예'_를 할 때쯤, 실제로 중요했던 단 하나의 요청이 근육 기억(muscle memory)을 통해 슬쩍 통과되어 버립니다.
두 번째 집에서는, 당신이 그런 것에 지쳐서 에이전트를 --dangerously-skip-permissions 옵션으로 실행했습니다. 이제 에이전트는 절대 묻지 않습니다. 또한 당신이 점심을 먹는 동안, 잘못된 디렉토리에서의 rm -rf나 잘못된 DATABASE_URL에 대한 DROP TABLE 명령을 내리기 전에도 절대 묻지 않습니다.
다른 것은 다 잊더라도, 이 한 가지 아이디어만은 기억하세요:
읽기(reads)는 자동 승인(Auto-approve)하세요. 쓰기(writes)는 차단(Gate)하세요. 기준은 _위험함 vs 안전함_이 아니라, _이것이 상태(state)를 변경하는가_입니다.
두 집 모두 하나의 버그를 공유합니다: 그들은 "에이전트에게 승인이 필요한가?"를 _행동(action)_의 속성이 아니라, 당신의 주의력(attention) — 즉 당신이 얼마나 기꺼이 클릭할 용의가 있는가 — 의 속성으로 취급합니다. 그것을 해결하면 문제의 형태 자체가 바뀝니다.
모두가 잘못된 곳에 긋고 있는 경계선
열 개의 팀에게 승인 경계(approval boundary)를 어디에 두어야 하는지 물어보면, 세 가지의 틀린 답을 얻게 될 것입니다. 도구 카테고리(tool category) 기준으로는: "셸은 위험하지만, 파일 읽기는 괜찮다"라고 말합니다. 하지만 cat ~/.aws/credentials는 정보를 유출(exfiltrate)하는 읽기 작업이며, echo는 파일을 덮어쓸(clobber) 수 있습니다. 가역성(reversibility) 기준으로는: "되돌릴 수 없는 작업은 차단하라"라고 합니다. 하지만 호출 시점(call time)에 무엇이 가역적인지 확실히 알 수는 없습니다. 분위기(vibes) 기준으로는: 무언가 망가질 때까지 모두 건너뛰기(skip-all)입니다.
실제로 유효한 경계선은 더 단순하며, 도구에 관한 것이 전혀 아닙니다:
상태 변이(state mutation) 시점에 선을 그으세요. 코드 에이전트(code agents)의 인간 감독(human-oversight)에 대해 널리 인용되는 글(Niklas Heidloff)은 정확히 다음과 같은 규칙을 제시합니다: 상태를 변경하지 않는 명령만 자동 승인(auto-approve)하고, 상태를 변경하는 명령은 차단(gate)하십시오. 도구에 의해서도, 가역성(reversibility)에 의해서도 아닌 — 그 이후의 세상이 달라지는지 여부에 따라 결정하는 것입니다. 순수 읽기(pure read)는 자유롭게 실행될 수 있습니다. 쓰기(write)는 대기해야 합니다.
그리고 이를 연구한 사람들이 밝힌, 왜 "그냥 각각 승인하면 된다"는 방식이 결코 당신을 구원하지 못할 것인지에 대한 이유는 다음과 같습니다: 수동 승인(manual-approve), 모두 건너뛰기(skip-all), 그리고 정적 허용 목록(static allowlists)은 모두 동일한 방식으로 실패합니다. 모든 동작을 검토하다 보면 승인 피로(approval fatigue)에 직면하게 됩니다. 수백 번의 반사적인 "예"를 외친 뒤에는 위험한 요청에도 "예"를 누르게 됩니다. 모든 것을 건너뛰면 에이전트에게 무기를 쥐여주는 꼴이 됩니다. 정적 허용 목록은 에이전트가 새로운 무언가를 필요로 하는 순간 부패합니다. 실패의 원인은 당신의 절제력이 아니라, 인간에게 런타임 권한 확인(runtime permission check) 역할을 요구하는 데 있습니다.
이는 신뢰에 관한 글에서 다루었던 것과 동일한 벽이지만, 다른 모습으로 나타난 것입니다: 당신이 직렬 병목(serial bottleneck)이며, "각 동작을 승인한다"는 것은 단지 그 병목을 당신의 클릭하는 손가락으로 옮길 뿐입니다.
오늘날 위험한 호출을 어떻게 차단하나요?
구축하기 전에 스스로를 배치해 보십시오. 각각 한계점이 있는 네 가지 정직한 답변입니다.
당신이 각각 승인합니다. 모든 차단된 호출이 당신에게 알림을 보내고, 당신은 "예"를 클릭합니다. 어느 정도 안전하지만, 완전히 확장 불가능(unscalable)합니다. 이것이 바로 승인 피로의 함정이며, 당신이 전담해서 돌봐야 하는 에이전트가 단 한 명일 때 한계에 도달합니다. 한계: 다시, 당신 자신입니다.
당신은 모두 건너뜁니다. --dangerously-skip-permissions, YOLO 모드입니다. 빠르고 완전히 무인(unattended)으로 작동하지만, 환각(hallucination)으로 인한 경로 하나만으로도 매우 끔찍한 오후를 맞이할 수 있습니다. 한계: 아무도 포착하지 못한 첫 번째 파괴적인 명령입니다.
당신은 정적 허용 목록을 유지합니다. 미리 승인된 명령들이 담긴 파일입니다. 더 낫긴 하지만, 그것은 얼어붙어 있습니다. 에이전트가 새로운 것에 부딪히면 작동이 멈추거나, 목록이 *가 될 때까지 당신이 목록을 확장해야 합니다. 한계: 읽기와 쓰기를 구분할 수 없으므로, 과도하게 차단하거나 과도하게 허용하게 됩니다.
위험에 따라 분류하고 쓰기(writes) 작업만 전달(relay)하십시오. 읽기 전용(Read-only) 호출은 자동 승인(auto-approve)되며, 상태를 변경하는(state-changing) 호출은 일시 중단되어 결정(인간 또는 정책) 단계로 라우팅됩니다. 이를 통해 중요한 작업들이 다른 작업들에 묻히지 않고 표면으로 드러납니다. 이것만이 확장 가능한 유일한 방식이며, 당신이 직접 구축할 수 있는 방식입니다. 이 글의 나머지 부분은 이를 구현하는 방법을 다룹니다.
당신은 현재 어디에 있습니까? 만약 당신의 솔직한 답변이 _"모두 건너뛰고, 잘 되길 바랍니다"_라면, 당신은 자율성(autonomy) 설정을 갖춘 것이 아니라, 언제 터질지 모르는 폭탄을 안고 있는 것입니다. 아래의 모든 내용은 '예/아니오'의 결정을 당신의 주의력에서 분리하여 아키텍처(architecture) 내부로 옮기는 과정입니다.
1단계: 도구가 스스로 자신의 위험도를 선언하게 하라
전달(relay) 방식은 호출이 실행되기 전부터 자신이 상태를 변경(mutate)하는지 여부를 알아야만 작동합니다. 따라서 위험 등급을 인간의 머릿속이 아닌 도구 계약(tool contract)에 명시하십시오. agentproto에서 도구는 자신이 무엇을 변경하는지 선언합니다. 읽기 전용(read-only) 도구는 아무것도 변경하지 않으며 스스로를 승인합니다:
// 읽기 전용 도구 — 아무것도 변경되지 않으므로 자유롭게 실행됨
export const knowledgeSearch = defineTool({
id: "knowledge.search",
...
이것은 클릭을 통해 강제하는 정책 대신, 두 줄의 계약으로 구현된 "읽기는 자동 승인하고, 쓰기는 차단(gate)한다"는 원칙입니다. knowledge piece의 knowledge.search가 정확히 이 이유로 mutates: []로 설정되어 있는 것입니다. 이제 경계(boundary)는 데이터가 되었으므로 기계가 이를 강제할 수 있습니다. 이는 한 인간 참여형(human-in-the-loop) 빌드(Nylas)의 표현을 빌리자면, 승인을 도구마다 사후에 덧붙이는 것이 아니라, 일급 아키텍처 기능(first-class architectural feature)으로 만드는 것의 핵심입니다.
2단계: 권한 전달(permission relay) — 벽이 아닌 일시 중단
이제 차단된(gated) 호출들은 터미널을 막아버리는 모달(modal) 창이 아닌 다른 곳으로 보내져야 합니다. agentproto는 대기 중인 쓰기(write) 작업을 세션 간 인박스(cross-session inbox)의 항목으로 전환합니다. 에이전트는 일시 중단되고, 결정은 대기하며, 누구든 — 또는 무엇이든 — 이에 응답할 수 있습니다.
# 현재 결정이 대기 중인 항목은 무엇인가요?
permissions_list
# → [{ id: "perm_9f", sessionId: "sess_42",
...
터미널 프롬프트(terminal prompt)가 제공하지 못하는 두 가지 이점이 있습니다. 첫째, 이는 대역 외(out of band) 방식입니다. 승인 절차가 하나의 연결된 세션에 갇혀 있지 않기 때문에, 관리자(또는 당신이 휴대폰을 통해)가 에이전트 앞에 앉아 있지 않고도 승인을 처리할 수 있습니다. 둘째, 이는 **프로그래밍 가능(programmable)**합니다. decision은 단순히 approve(승인) 또는 deny(거부)일 뿐이므로, 판단이 필요한 호출에는 사람이 응답자가 될 수 있고, 그렇지 않은 호출에는 _정책(policy)_이 응답자가 될 수 있습니다.
릴레이(relay)는 구축된 L2 단계입니다. 감독 사다리(The supervision ladder)는 _당신이 관찰하는 단계_부터 _게이트가 결정하는 단계_까지의 전체 과정을 매핑합니다. 이것이 실제 배선으로서의 L2 "권한 릴레이(permission relay)" 단계입니다. 읽기 전용 자동 승인(Rung 1)이 있고, 위험한 쓰기(writes) 작업은 여기에 도달하며, 진정으로 판단이 필요한 작업만이 사람에게 도달합니다. 읽기 작업은 애초에 나타나지 않기 때문에 승인 피로(Approval fatigue)는 사라집니다.
DROP TABLE과 같은 쓰기 작업을 사람에게 전달하는 것은 괜찮습니다. 하지만 "이 diff가 실제로 버그를 수정했는가"와 같은 문제에 대해 사람이 승인을 클릭하는 것은, 에이전트가 스스로를 채점하는 것보다 단지 속도만 느린 버전일 뿐입니다. 어떤 쓰기 작업에는 _클릭(click)_이 아니라 _검사(check)_가 필요합니다.
Rung 3: 쓰기에 외부 검사를 적용하고, 그 뒤에 커밋을 스테이징(stage)하기
최상위 단계(top rung)는 인간의 판단을 확장 가능한 무언가로 대체합니다. 즉, 작동 중인 에이전트 _외부_에서 실행되며 쓰기 작업이 실행되기 전에 반드시 'yes'라고 말해야 하는 게이트(gate)입니다. 이를 세션에 연결하면 에이전트의 턴이 끝날 때 실행되며, 에이전트의 자기 보고가 아닌 게이트의 판결이 다음 동작을 결정합니다.
// 세션에 게이트를 연결합니다; 녹색 결과는 당신의 확인(ack) 뒤에 커밋을 스테이징합니다
attach_policy({
sessionId: "sess_42",
...
중요한 것은 라이프사이클(lifecycle)입니다. 게이트(gate)가 실행되고, 결과가 '그린(green, 성공)'이더라도 정책(policy)은 커밋되지 않습니다. 대신 awaiting-ack 상태로 이동하며 policy:commit-ready를 방출(emit)합니다. 커밋은 당신이 ack_policy({ policyId, approve: true })를 호출하기 전까지는 발생하지 않습니다. 이 호출은 나열된 경로들만 스테이징(git add -- <paths>, 절대 -A나 glob 사용 금지)하고, 커밋한 뒤, policy:committed를 방출합니다. 이는 절대 푸시(push)하지 않습니다. 에이전트는 게이트를 넘어 커밋에 도달할 수 없으며, 게이트는 당신의 승인(ack)을 넘어 푸시에 도달할 수 없습니다.
종료 코드(exit codes)로 판단할 수 없을 때는, 회의론자를 생성하세요. 쉘 게이트(shell gate)를
gate: { judge: { adapter, prompt } }로 교체하면, agentproto는VERDICT: PASS|FAIL로 끝나는 수명이 짧은 LLM 리뷰어를 실행합니다. 이는 페일 세이프(fail-safe) 방식으로 설계되어, 타임아웃이 발생하거나 파싱할 수 없는 답변이 나오면 FAIL로 간주합니다. LLM에게 찬양하지 말고 '반박(refute)'하도록 프롬프트를 작성하세요 ("이 diff가 버그를 수정하지 않았음을 증명해 보세요"). 이것이 kill-the-loop 레슨을 하나의 프리미티브(primitive)로 구현한 것입니다. 즉, 검증 작업은 검증 대상이 되는 루프(loop)의 외부에 위치해야 하며, 그렇지 않으면 단순히 거수기(rubber-stamps) 역할만 하게 됩니다.
이제 당신은 전체 평면(full plane)을 갖게 되었습니다. 읽기(reads)는 자유롭게 실행되고, 쓰기(writes)는 전달(relay)되며, 부하를 주는 쓰기(load-bearing writes)는 외부의 판결을 기다립니다. 그리고 되돌릴 수 없는 행위인 커밋(commit)은 단 한 번의 인간의 '예(yes)' 뒤에 스테이징됩니다.
솔직한 경계선 (나를 물었던 주의사항들)
이것은 실제 인프라(infrastructure)이며, 이는 날카로운 모서리가 있음을 의미합니다. 여러분은 겪지 않도록 제가 실전에서 맞닥뜨린 네 가지를 소개합니다:
- 게이트가 허용 목록(allowlist)을 기준으로 작동하며,
test는 목록에 없습니다. 셸 게이트(Shell gates)는 다른 명령어와 마찬가지로 기본 거부(default-deny) 허용 목록(.agentproto/allowed-commands.json)을 따릅니다.test -f x는 허용 목록에 없음(not in allowlist) 오류와 함께 실패하며, 정책은차단(blocked)상태가 됩니다. 허용 목록에 있는 바이너리를 사용하세요.test -f x대신ls x를 사용하거나, 테스트를 위해pnpm/node를 사용하십시오. - 게이트의 작업 디렉터리(working directory)는 워크스페이스(workspace)에 고정되어 있습니다. 현재 작업 디렉터리(cwd)가 워크스페이스 _외부_인 세션은
cwd escapes the workspace오류와 함께 게이트를 통과하지 못합니다. 만약 리포지토리 루트(repo root) 외부의 기능별 워크트리(per-feature worktrees)에서 에이전트를 실행한다면, 셸 게이트는 사실상 사용할 수 없습니다. 이 경우then: "emit"(마일스톤 전용)으로 전환하고,git/gh를 사용하여 결과를 직접 확인하십시오. - 턴(turn)이 끝나기 전에 연결(Attach)하십시오. 게이트는
turn-end시점에 실행됩니다. 세션이 턴을 마치기 _전_에(유휴 상태 생성(spawn idle) → 연결(attach) → 프롬프트(prompt)) 정책을 연결해야 합니다. 그렇지 않으면 이벤트와 경합(race)이 발생하여 정책 적용을 놓치게 됩니다. - 지속 가능한 자동 감독(durable auto-supervisor)은 아직 휘발성입니다. 여러 단계에 걸쳐 이를 관리할 RoutineRunner는 현재 인메모리(in-memory) MVP 단계입니다. 즉, 데몬(daemon)이 재시작되면 진행 중인 실행(in-flight runs)이 중단됩니다. 살아남는 구성 요소는 위에 언급된
attach_policy, 이벤트 버스(event bus), 승인(ack)뿐입니다. 아직 하중을 견딜 수 없는 부분 위에 관제탑을 구축하지 마십시오.
그리고 공정한 비교가 필요합니다. 신뢰성은 바로 그 비교에 달려 있기 때문입니다. Claude Code는 이미 1~2단계(Rung 1–2)의 훌륭한 버전을 제공하고 있습니다 — 별도의 설정 없이 내장된 명령별 권한 프롬프트(permission prompts)와 허용 목록(allowlist) 방식입니다. 만약 당신이 완전히 한 벤더(vendor)의 에이전트 내에서만 생활한다면, 이 모든 것을 연결할 필요가 없을지도 모릅니다. 하지만 그 편리함을 위해 당신이 포기해야 하는 두 가지가 있습니다: 승인 정보가 세션과 함께 소멸된다는 점(터미널을 닫으면 릴레이(relay)도 사라짐), 그리고 단일 벤더 전용이라는 점입니다. 즉, 당신의 Codex 에이전트나 저렴한 로컬 모델(local model)에 남아 있는 대기 중인 쓰기(write) 작업을 승인해주지 못합니다. 당신이 실행하는 모든 에이전트에 걸쳐 적용되고, 노트북이 절전 모드로 들어가도 유지되는 승인 평면(approval plane)은 도구별 프롬프트(per-tool prompt)가 제공할 수 없는 영역입니다. 이 모든 것의 밑바탕에 깔린 더 넓은 원칙은 OWASP의 **최소 에이전시(least agency)**입니다. 작업에 필요한 최소한의 자율성(autonomy), 도구, 그리고 자격 증명 범위(credential scope)만을 부여하십시오. 그리고 그 부여가 클릭을 통해 매번 다시 내리는 결정이 아니라, 아키텍처(architecture)가 강제하는 결정이 되도록 만드십시오.
열쇠는 주되, 점화 장치는 붙잡고 있으라
자율성(Autonomy)은 결코 무서운 부분이 아니었습니다. 당신의 전체 코드베이스를 읽고, 테스트를 실행하며, 수정안을 초안하는 에이전트는 정확히 당신이 원하는 모습입니다. 무서운 부분은 바로 쓰기(write) — 즉, 되돌릴 수 없는 무언가를 변경하는 단 하나의 명령 — 가 당신이 너무 지쳐서 잡아내지 못했거나, 질문을 받을 만큼 가까이 있지 않았을 때 실행되는 것입니다.
그러니 당신의 주의력(attention)으로 승인을 결정하는 것을 멈추고, 동작(action)으로 결정하기 시작하십시오. 읽기(reads)는 자동 승인하십시오; 그것들은 당신을 해칠 수 없습니다. 쓰기(writes)는 릴레이(relay)하십시오; 인간이나 정책(policy)이 이를 승인합니다. 중요한 쓰기에 대해서는 외부 체크(external check)를 배치하고, 단 하나의 의도적인 '예(yes)' 뒤에 되돌릴 수 없는 단 하나의 행위인 커밋(commit)을 단계별로 배치하십시오. 에이전트는 제안(propose)합니다. 에이전트 외부의 무언가가 결정(dispose)합니다. 그것이 승인 평면(plane)의 전부입니다.
에이전트에게 열쇠를 주십시오. 다만 점화 장치(ignition)는 에이전트가 닿을 수 없는 곳에 두십시오.
만약 당신의 설정이 상태 변이(state mutation)보다 더 스마트한 곳에 승인선을 긋고 있거나, '각각 승인(approve-each)' 방식을 실제로 확장 가능하게(scale) 만들었다면, 어디인지 알려주십시오. 제가 그 부분을 수정하겠습니다.
시리즈 — 솔직하게 말하는 오케스트레이션(Orchestration, Honestly)
열 개의 글, 하나의 논증. 어디서 시작해도 좋습니다; 각 글은 서로 연결되어 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기