본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 24. 03:32

프롬프트와 RBAC도 에이전트가 누군가를 두 번 환불하는 것을 막지 못한다

요약

에이전트가 환불이나 이메일 발송 등 실제 부작용을 일으키는 도구를 사용할 때, 프롬프트나 RBAC만으로는 중복 실행을 막을 수 없음을 설명합니다. 에이전트의 컨텍스트는 휘발적이기 때문에, 모델 외부의 상태를 기반으로 한 '상태 유지(Stateful) 검사'와 '게이트' 설계가 필수적임을 강조합니다.

핵심 포인트

  • 프롬프트와 RBAC는 에이전트의 중복 동작을 방지하는 데 한계가 있음
  • 에이전트 컨텍스트는 요약되거나 삭제될 수 있어 상태 저장소로 부적합함
  • 무상태(Stateless) 검사와 상태 유지(Stateful) 검사를 구분해야 함
  • 도구 호출을 감싸는 모델 외부의 독립적인 '게이트' 설계가 필요함

만약 여러분이 에이전트가 실제 부작용(side effects)을 가진 도구—환불, 이메일 발송, 데이터 내보내기(exports), 프로덕션 환경에 쓰기(writes to prod)—를 호출하도록 허용한다면, 일반적인 안전 계층 중 어느 것도 실제로 포착하지 못하는 실패의 유형이 존재합니다. 저는 이 분야에서 구축하면서 계속해서 이것을 겪고 있기 때문에, 문제를 명확하게 설명하고 제가 도달한 접근 방식을 보여주고자 합니다. 이는 저의 의견이 담긴 것이며, 제가 틀린 부분이 있다면 진심으로 듣고 싶습니다.

문제점

환불을 처리할 수 있는 에이전트를 상상해 보세요. 어떤 상위 시스템(upstream)이 단계를 재시도합니다—시간 초과(timeout), 일시적 오류(transient error), 노드를 다시 실행하는 오케스트레이터(orchestrator)가 작동할 때—에이전트는 환불 도구를 두 번째로 호출하게 됩니다. 같은 고객, 같은 인보이스입니다. 중복 환불이 나갑니다.

이제 이것을 막아야 할 것이 무엇이었는지 살펴보고, 아무것도 기술적으로 '고장나지' 않았다는 점에 주목하세요:

  • OAuth는 에이전트가 결제 서버에 접근할 수 있음을 증명했습니다.
  • RBAC(Role-Based Access Control)는 에이전트의 신원이 환불 도구를 호출하도록 허용되었음을 확인했습니다.
  • MCP/도구 스키마는 페이로드(payload)가 잘 구성되어 있음을 말해주었습니다.
  • 프롬프트는 에이전트에게 중복 환불을 하지 않도록 지시했습니다.

모든 검사가 통과되었음에도 불구하고, 두 번째 환불은 여전히 실행되었습니다. 이들은 모두

그리고 제가 이해하는 데 시간이 좀 걸렸던 부분은 이것입니다. 기본적으로 "이미 일어난 일"에 대한 상태가 존재하는 유일한 장소는 에이전트 자신의 컨텍스트 (context)뿐이라는 점입니다. 그런데 컨텍스트는 바로 요약되거나, 잘려 나가거나, 다시 쓰여지는 대상입니다. 특히 중복 작업이 발생하는 지점인 재시도 경로 (retry path)에서는 더욱 그렇습니다. 에이전트는 에이전트가 이미 수행한 일에 대한 기억을 저장하기에 최악의 장소입니다.

무상태 (Stateless) vs 상태 유지 (Stateful) 검사
두 종류의 검사를 구분할 가치가 있습니다. 왜냐하면 쉬운 검사들이 당신으로 하여금 모든 준비가 되었다고 착각하게 만들기 때문입니다.

무상태 (Stateless) (쉬움, 프로세스 내 수행)상태 유지 (Stateful) (문제를 일으키는 것들)
이 도구 (tool)가 차단되었는가?이 정확한 동작이 이미 실행되었는가?
금액이 한도를 초과했는가?이 루프 (loop)가 이미 N번 실행되었는가?
이것이 개인정보 (PII) 필드인가?작업이 토큰/비용 예산을 초과했는가?
목적지가 허용되었는가?이 승인이 이 동작에 국한된 것인가, 아니면 재사용되었는가?

무상태 검사는 진정으로 유용하며 반드시 수행해야 합니다. 하지만 이는 기본 중의 기본 (table stakes)일 뿐이며, 막대한 비용이 드는 사고가 발생하는 지점은 아닙니다.

접근 방식: 루프 외부의 게이트 (gate)
제가 결국 선택한 방향은 각 도구 호출 (tool call)을 감싸는 작은 게이트를 만드는 것이었습니다. 이 게이트는 호출이 실행되기 전에 에이전트의 컨텍스트 외부에 존재하는 상태 (state)를 바탕으로 허용 (allow) / 거부 (deny) / 도전 (challenge) 여부를 결정합니다.

에이전트가 도구 호출을 제안함 -> 게이트가 정책 + 상태를 확인함 -> 허용 / 거부 / 도전

저에게 있어 타협할 수 없는 설계 제약 조건은, 게이트가 모델 컨텍스트 (model context) 외부와 에이전트가 수정 가능한 메모리 외부에서 실행되어야 한다는 것입니다. 만약 에이전트가 규칙을 다시 쓰거나, 스스로에게 승인을 부여하거나, 자신이 이미 수행한 기록을 지울 수 있다면, 그것은 가드레일 (guardrail)이 아니라 포스트잇 (sticky note)에 불과합니다.

구체적으로 게이트는 다음을 처리합니다:

부작용이 발생하는 도구(side-effectful tools)를 위한 멱등성 (Idempotency). 환불/이메일/내보내기 작업은 게이트가 루프 외부에서 기억하는 호출 지문(call fingerprint) 또는 멱등성 키 (idempotency key)를 수반하므로, 에이전트가 이미 실행했다는 사실을 "잊어버리더라도" 중복 요청은 거부됩니다.

회로 차단기 (Circuit breakers). 도구 호출 횟수, 재시도 (retries), 토큰 (tokens), 비용 (cost), 그리고 실행 시간 (runtime)에 대한 상한선을 설정하여, 제어 불능의 루프가 발생하더라도 예산을 모두 소진하는 대신 차단기가 작동하도록 합니다.

단일 작업 승인 (Single-action approvals). 도구 + 리소스 + 페이로드 해시 (payload hash) + 금액 + 만료 시간을 범위로 하는 승인 방식입니다. 이 중 하나라도 변경되면 재승인이 필요하며, 세션 전체에 대해 하나의 "예"를 재사용할 수 없습니다.

개인정보 (PII) / 데이터 흐름 규칙. 특정 필드와 목적지(예: customer_records -> external_email)를 차단합니다.

감사 (Audit). 모든 허용/거부/챌린지(challenge) 이벤트에 대한 기록을 남기므로, 게이트 로그는 무엇이 제안되었고 어떤 일이 일어났는지에 대한 기록이 됩니다.

코드에서의 모습
저는 이것을 AgentPass (Apache-2.0)라는 오픈 소스 프로젝트로 패키징했습니다. 주의할 점은, 이것은 제가 만든 것이므로 그 관점을 적절한 비판적 시각으로 받아들여 달라는 것입니다. 로컬 TypeScript 가드 (guard)는 npm의 @dinpd/ai-agent-guard에 있습니다. 도구 호출을 래핑(wrapping)하는 모습은 다음과 같습니다:

import { createToolGate } from "[@dinpd](https://dev.to/dinpd)/ai-agent-guard";

const gate = createToolGate({ policy });

const execution = await gate.run(  
{
  agentId: "support-agent",  
  jobId: "case-1042",  
  tool: "stripe.refund",  
  action: "pay",  
  resource: "payment/pi_123",  
  amountUsd: 49,  
  idempotencyKey: "refund-case-1042-pi_123",  
},
() => stripe.refunds.create({ payment_intent: "pi_123", amount: 4900 })  
);
if (!execution.executed) {  
return execution.decision; // 거부되었거나 승인이 필요함 — 환불이 실행되지 않음
}

동일한 idempotencyKey가 두 번째로 나타나면, 게이트(gate)가 이를 거부하며 하위의 stripe.refunds.create는 절대 실행되지 않습니다. 루프(breaker가 작동함), 승인(해당 페이로드 해시에 범위가 지정됨), 그리고 PII(목적지가 차단됨)의 경우도 동일한 개념입니다.

정책/매니페스트(policy/manifests)를 위한 Python CLI도 있으며, 제 말을 그대로 믿기보다 직접 확인해보고 싶다면 리포지토리(repo)에 환불 중복 제거(refund-dedup), 서킷 브레이커(circuit-breaker), PII 유출 방지(PII-egress), 그리고 MCP 도구/호출(tools/call) 사례에 대한 실행 가능한 데모가 포함되어 있습니다.

이것이 아닌 것
범위를 명확히 하자면, 이 부분은 자주 혼동되곤 합니다. 이것은 IAM, OAuth, OPA, Cedar 또는 여러분의 MCP 게이트웨이(gateway)를 대체하는 것이 아닙니다. 그것들은 당신이 누구인지, 그리고 당신이 광범위하게 무엇을 만질 수 있는지를 결정합니다. 이것은 그것들이 실제로 다루지 않는, 실행 직전의 작은 체크포인트(checkpoint)입니다. 그것들은 이와 나란히 존재합니다.

확신할 수 없는 부분
저는 제 모델이 완전히 옳다고 생각하지 않기 때문에, 홍보보다는 미결된 질문들로 글을 마치고 싶습니다.

  • 프로세스 내부(in-process)에서 수행하는 검사와 영구적인 경계(durable boundary)가 필요한 상태 사이의 올바른 경계는 어디인가?
  • 오늘날 실제 시스템에 에이전트(agents)를 실행하고 있다면, 중복된 부작용(side effects) 문제를 해결하고 있는가? 만약 그렇다면 중복 제거(dedup)는 어디에서 이루어지는가? API 계층의 멱등성 키(Idempotency keys), 래퍼(wrapper), 오케스트레이터(orchestrator), 아니면 신중한 재시도 로직(retry logic)인가?
  • 호출당 게이트(per-call gate)가 이 중 일부에 대해 잘못된 도구인가? 예를 들어, 모든 도구가 소스 자체에서 멱등성(idempotent)을 가져야 하는가?

만약 운영 시스템(production systems)을 다루는 에이전트를 출시했다면, 이를 어떻게 처리하고 계신지 정말 듣고 싶습니다. 이 접근 방식을 파헤쳐 보고 싶다면 리포지토리는 여기에 있습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0