RicoCheesethe studio log · v2.0
Live · KRRead posts
목록으로
뉴스PUBLISHED · 2026년 8월 13일·19 MIN READ

AI 에이전트에게 도구를 맡기지 않기로 했다 — 그래서 문지기를 만들었다

허용/거부 두 가지로만 나뉘던 AI 에이전트 권한 체계를 4단계 판정 엔진으로 바꾼 오픈소스 프로젝트. 목(mock) 에이전트 없이 10개 프레임워크의 실제 에이전트 83개로 검증했다.

#ai#agents#security#opensource
I Stopped Trusting AI Agents With Tools. So I Built a Gatekeeper.

개요 #

개발자 Debashish Ghosal이 AI 에이전트의 도구 호출을 가로채 판정하는 권한 엔진 Agent ToolTrust를 공개했다. pip install agent-tooltrust로 설치할 수 있고, 저장소는 github.com/deghosal-2026/agent-tooltrust에 공개돼 있다.

핵심은 판정 결과를 넷으로 쪼갠 것이다. 허용(allow), 감사(audit), 승인 요청(escalate), 거부(deny). 지금까지 에이전트 권한은 사실상 허용 아니면 거부, 둘뿐이었다.

검증 방식도 눈에 띈다. 목 에이전트를 단 하나도 쓰지 않고 GitHub에서 긁어온 실제 에이전트 83개, 프레임워크 10종으로 필드 테스트를 돌렸다. 결정론적 테스트는 2,490건 전부 통과했고, 필드 테스트 Plan A는 83개 에이전트 전원 통과했다.

정지 표지판 Photo by Francesco Ungaro on Pexels

허용 목록만으로는 부족하다 #

저자가 문제 삼은 건 지금의 권한 모델이 "도달 가능성"이지 "권한 부여"가 아니라는 점이다. 같은 도구라도 스테이징에서는 무해하고 프로덕션에서는 위험하다. 공개 문서를 읽는 것과 고객 데이터를 읽는 것은 다른 행위다. CI 샌드박스의 delete와 프로덕션의 delete도 마찬가지다. 그런데 허용 목록은 이 차이를 표현할 방법이 없다.

숫자도 인용했다. MCP 서버 배포 중 접근 범위 제한을 구현한 곳은 약 18%. 조직의 80%는 에이전트가 의도한 범위를 넘는 행동을 한 적 있다고 인정했다. OWASP는 에이전트 도구 오용을 1급 위험으로 분류한다.

저자는 엔진 코드를 건드리기 전에 PRD와 아키텍처 명세부터 썼다고 밝혔다. 스스로를 붙잡아 두려는 목적도 있었고, 설계를 건너뛰면 엉뚱한 걸 만들어 내놓게 된다는 걸 여러 번 겪었기 때문이라고 했다.

엔진은 이렇게 동작한다 #

에이전트가 도구를 호출하면 실행 전에 엔진이 정규화 → 점수화 → 판정 → 설명 → 감사, 다섯 단계 파이프라인을 돌린다. 통합 지점은 데코레이터다.

untitled
python
from agent_tooltrust.engine.engine import Engine
from agent_tooltrust.policy.models import default_policy
from agent_tooltrust.adapters.raw import RawAdapter

engine = Engine(default_policy("balanced"))
adapter = RawAdapter(engine)

@adapter.guard(
    tool_name="deploy_service",
    action="deploy",
    environment="production",
    data_class="restricted",
)
def deploy_service(service: str) -> str:
    return f"deployed {service}"

# 에이전트가 도구를 호출한다. 엔진이 먼저 평가한다.
# 제한 데이터에 대한 프로덕션 배포 → escalate
deploy_service("payment-api")
# ToolTrustDecisionError: escalate — "Write action (deploy) in production
# on restricted data requires approval..."

에이전트는 정책의 존재를 모른다. LLM도 규칙이 있다는 걸 알지 못한다. 저자의 표현을 빌리면 "LLM은 제안하고 정책은 처분한다". 엔진이 프롬프트 안이 아니라 모델 바깥에 있으니 프롬프트 엔지니어링으로 거부를 뒤집을 수 없다.

audit은 "허용하되 전부 기록한다"는 뜻으로, 민감 데이터를 읽을 때 쓴다. escalate는 "멈추고 사람을 부른다"는 뜻이다. 허용/거부만 있으면 권한을 과하게 열어 두거나 승인 요청에 지쳐 버리거나 둘 중 하나를 골라야 하는데, 4단계는 그 사이에 여지를 만든다.

판정에는 항상 설명이 따라붙는다. 이유 코드, 사람이 읽는 문장, 어느 요소가 판정을 갈랐는지 보여 주는 분해 결과가 함께 나온다. LLM이 문장을 다듬는 옵션도 있지만 기본은 꺼져 있고, LLM은 판정 자체를 바꾸지 못한다. 모든 판정은 정책 버전·타임스탬프·세션 ID와 함께 JSONL, SQLite, Postgres 중 한 곳에 남는다.

기본 제공 프리셋은 strict, balanced, permissive 세 가지다. 사람이 읽고 쓰는 YAML 백엔드와, 이미 Rego 정책을 굴리는 팀을 위한 OPA/Rego 백엔드를 함께 지원한다. 섀도 모드를 쓰면 에이전트 코드를 손대지 않고 배포한 뒤 "거부됐을 호출"을 관찰하고 튜닝한 다음 실제 차단으로 넘어갈 수 있다.

실패 시에는 무조건 닫는다. 모르는 도구도 거부, 잘못된 입력도 거부, 엔진이 죽어도 거부다. 반대로 열어 두면 엔진을 죽일 수 있는 공격자가 도구를 무제한으로 쓰게 된다. 이건 첫 줄을 쓰기 전에 정한 설계 결정(DD-14)이지 사고 후 덧붙인 조치가 아니라고 저자는 강조했다.

목 에이전트 없이, 실제 에이전트 83개로 #

저자는 앞선 세 프로젝트에서 같은 교훈을 반복해 얻었다고 썼다. 목 에이전트는 거짓말을 하고, 단위 테스트는 통과하고, 데모는 깔끔해 보인다. 그러다 실제 에이전트를 붙이면 다 무너진다. 이전 프로젝트에서는 필드 테스트를 뒤늦게 붙였다가 통과율 9%를 확인한 적도 있었다.

이번에는 순서를 뒤집었다. 어댑터 코드를 쓰기 전에 명세에 못을 박았다. "릴리스 전에 필드 테스트를 통과해야 한다. 목이 아니라 실제 에이전트로 돌린다"(DD-11), "주요 플랫폼에 걸쳐 실제 에이전트 8~10개"(DD-12). 실제로는 그보다 훨씬 나아가 프레임워크 10종, 에이전트 83개를 돌렸다.

지원 프레임워크는 LangGraph, PydanticAI, CrewAI, OpenAI Agents SDK, Google ADK, AutoGen/AG2, LlamaIndex, smolagents에 자체 테스트용 SWE-bench와 ToolTrust MCP를 더한 10종이다. 어댑터는 모두 같은 계약을 따른다. CallContext를 뽑아 Engine.evaluate()에 넘기고 판정을 돌려주는 것이다.

untitled
python
@dataclass(frozen=True)
class CallContext:
    tool_name: str
    action: str
    environment: str
    data_class: str
    agent_id: str
    session_id: str | None = None
    arguments: dict[str, Any] | None = None

프레임워크마다 걸린 함정들 #

계약은 깔끔하지만 거기 도달하는 과정은 그렇지 않았다. 저자가 기록한 프레임워크별 함정 12가지 중 일부는 이렇다.

LangGraph의 ToolTrustToolNodeToolNode를 상속해 _run_one()을 오버라이드하는 구조인데, langgraph v1.x에서는 노드가 호출 가능하지 않았다. 결국 그래프에 넣기 전에 도구를 감싸는 쪽으로 돌아갔다.

untitled
python
# LangGraph — 도구를 먼저 감싸고, 그다음 그래프에 넘긴다
adapter = RawAdapter(engine)
guarded_tool = adapter.guard(
    tool_name="query_logs",
    action="read",
    environment="staging",
    data_class="internal",
)(query_logs_fn)

# 이제 guarded_tool을 create_react_agent(llm, tools=[guarded_tool])에 넘긴다

Google ADK의 LLM 레지스트리는 Gemini만 안다. 로컬 모델을 쓰려면 LiteLlm(model=f"openai/{MODEL}", api_base=ENDPOINT)를 넘겨야 하고, InMemorySessionService.create_session()은 코루틴이라 동기 호출이 아니라 await가 필요하다. 문서에는 없는 내용이다.

LlamaIndex의 구형 ReActAgent에는 .query().chat()도 없다. llama_index.core.agent.workflow의 워크플로 에이전트를 써야 하고, 실행은 async for event in handler.stream_events()로 굴러간다. await handler만 해서는 아무 일도 일어나지 않는다. 저자는 여기서 한 시간을 썼다고 적었다.

나머지도 비슷한 결이다. AutoGen은 에이전트 ID의 하이픈을 밑줄로 바꿔야 하고(ag-01ag_01), 로컬 Qwen 모델은 "반드시 scn_<id> 도구를 호출하라"고 명시하지 않으면 그냥 글로 답한다. smolagents는 @tool 마다 인자별 설명이 들어간 완전한 독스트링을 요구하고, 없으면 DocstringParsingException을 던진다. CrewAI는 litellm 폴백이 필요하고, OpenAI Agents SDK는 pydantic 충돌 때문에 function_tool(..., strict_mode=False)를 써야 한다.

이런 건 목 에이전트로는 하나도 안 나온다.

12일치 테스트를 하루로 줄인 방법 #

에이전트 83개 × 시나리오 30개면 2,490회다. 한 번 돌 때마다 Apple Silicon에서 OMLX로 돌리는 로컬 Qwen3.5-4B-4bit를 호출하는데, 호출당 3080초가 걸린다. 워커 10개로도 약 2.7시간. 게다가 2,490은 이론상 최솟값일 뿐, 실제로는 디버깅하며 재실행하느라 LLM 호출이 45배로 불어나 1만 건을 넘겼다.

저자는 여기서 무차별 대입을 멈췄다. 엔진의 정확성은 LLM 호출 없는 결정론적 테스트 2,490건으로 이미 검증됐고, Engine.evaluate()는 호출자가 LangGraph든 CrewAI든 신경 쓰지 않는다. 필드 테스트가 진짜로 증명해야 할 건 어댑터였다. 각 프레임워크가 실제 에이전트 루프 안에서 네 가지 판정을 제대로 드러내는가. 그건 교차곱 문제가 아니라 커버링 문제다.

그래서 계획을 둘로 나눴다.

  • Plan A — 에이전트당 시나리오 하나(83회). 배정을 짜서 시나리오 30개, 프레임워크 10개, 에이전트 클래스 5종을 모두 덮었다. 결과는 83/83, 100%.
  • Plan B — 프레임워크별 판정 유형 증명(123회). 프레임워크마다 tier-1 에이전트 하나를 골라 네 가지 판정 유형과 적대적 시나리오를 돌렸다. 결과는 116/123, 94%.

합치면 2,490회 대신 206회다. 커버리지는 시나리오 30/30, 에이전트 83/83, 프레임워크 10/10, 클래스 5/5로 같다. 약 12분의 1이다. 전체 교차곱은 12일쯤 걸렸을 일이 한나절로 끝났다.

저자가 가장 뿌듯해하는 부분도 엔진이 아니라 이 커버링 설계다. 비싼 LLM 호출은 실제 에이전트만이 증명할 수 있는 곳에 쓰고, 결정론적 테스트가 이미 덮은 걸 다시 증명하는 데는 쓰지 않는다는 것.

실패 7건이 알려 준 것 #

Plan B에서 실패한 7건은 전부 같은 패턴이었다. not-available — 가드가 아예 발동하지 않았다. LLM이 도구를 호출하지 않았기 때문이다. 로컬 Qwen 모델에 도구 5개를 한꺼번에 쥐여 주면 가드가 걸린 도구를 부르는 대신 그냥 글로 답해 버리는 경우가 있었다. 엔진은 판정할 기회조차 얻지 못했다.

목 에이전트는 항상 도구를 호출한다. 실제 4B 모델은 가끔 호출하지 않는다.

저자는 not-available을 정책 실패로 해석하지 말라고 못 박았다. 가드가 실행됐는데 엔진이 틀린 판정을 내린 unexpected-decision과는 다른 범주이고, 진짜 회귀는 후자뿐이다. Plan B에서 실제로 실행된 도구 호출은 전부 올바른 판정을 냈다. 실패 7건이 가리킨 건 엔진이 아니라 LLM이었다.

CI 설계에도 그대로 이어진다. not-available에서 실패시키면 모델 비결정성 때문에 게이트가 불안정해진다. unexpected-decision에서만 실패시키면 엄격하면서도 안정적인 게이트가 된다. 필드 테스트 보고서는 골든 not-available 허용치를 커밋해 두고, 모델 컨디션이 나쁜 날이 아니라 실제 회귀에서만 CI가 깨지게 하라고 권한다.

거부 → 재계획 → 허용 #

저자가 가장 만족스러워한 결과는 안전 루프다. 에이전트가 drop_database를 시도하면 엔진이 거부한다. 잘 만들어진 에이전트는 거기서 멈추지 않고 다시 계획을 세워 무해한 다른 도구를 고른다. 엔진은 그건 허용한다. 두 호출 모두 감사 기록에 남는다.

untitled
python
@adapter.guard(
    tool_name="drop_database",
    action="delete",
    environment="production",
    data_class="restricted",
)
def drop_database(db: str) -> str:
    return f"dropped {db}"

@adapter.guard(
    tool_name="query_audit_log",
    action="read",
    environment="production",
    data_class="internal",
)
def query_audit_log(query: str) -> str:
    return f"audit rows for {query}"

# 에이전트가 drop_database 시도 → 엔진 거부 (프로덕션 delete)
# 에이전트 재계획 → query_audit_log 호출 → 엔진 허용 (프로덕션 read)
# 두 판정 모두 감사 기록. 에이전트는 차단이 아니라 우회 유도.

LLM을 쓰는 프레임워크 8종에서 전부 확인했다. 라이브 8/8, 스크립트 8/8. 모든 프레임워크가 파괴적 호출을 거부하고, 무해한 읽기로 재계획한 뒤 허용을 받았다. 에이전트를 막는 게 아니라 방향을 트는 것이고, 모든 단계가 감사 기록에 남는다. 저자는 v0.2에서 사람이 승인 또는 거부하고 에이전트가 이어서 진행하는 승인 왕복 구조를 이 패턴 위에 올리겠다고 밝혔다.

지금 쓸 수 있는 것 #

현재 버전은 PyPI에 v0.1.1로 올라가 있다.

untitled
bash
pip install agent-tooltrust
tooltrust init --posture balanced

설치하고 프리셋을 고른 다음 도구에 데코레이터를 붙이면 된다. 별도 인프라는 필요 없다. 이미 OPA/Rego 정책이 있다면 듀얼 백엔드로 재사용할 수 있고, 차단 전에 관찰만 하고 싶으면 섀도 모드(dry_run=True)로 모든 판정을 기록만 할 수 있다. LangGraph, PydanticAI, CrewAI, OpenAI Agents SDK, Google ADK, AutoGen, LlamaIndex, smolagents를 쓴다면 실제 에이전트로 검증한 어댑터가 이미 들어 있다.

확장 지점도 열려 있다. @tooltrust.risk_function으로 자체 위험 함수를 등록해 가중합에 끼워 넣을 수 있고, AuditSink 인터페이스를 구현하면 Splunk든 Datadog든 원하는 SIEM으로 감사 로그를 보낼 수 있다. 새 프레임워크 어댑터는 BaseAdapter의 세 메서드를 채우는 일이라 50줄 남짓이면 된다. 프리셋 세 가지도 그냥 YAML 파일이라 포크해서 조직 기본값으로 쓸 수 있다.

출시 현황 #

v0.1.0 기준 성적표는 이렇다. 결정론적 테스트 2,490건, Ruff 0, Mypy strict 0, Docker 통과, SWE-bench 검증, OWASP 5/10, OpenSSF Silver. 프레임워크 10종, 실제 에이전트 83개, 시나리오 30개, 목 0개. Plan A 83/83, Plan B 116/123, 재계획 루프 8/8.

엔진은 출시됐고 검증도 끝났다. 승인 왕복, 정책 팩, 규칙 조합, HTTP /authorize, 리플레이 탐지, 자식 에이전트 위임 같은 플랫폼 기능은 v0.2.0을 향한 32개 이슈로 남아 있다. 저자는 필드 테스트 보고서에 무엇이 증명됐고 무엇이 아닌지 솔직하게 적었다며, 프로덕션에 올린 뒤보다 지금 깨뜨려 보는 편이 낫다고 덧붙였다.


이 글은 위 출처를 바탕으로 한국 독자를 위해 재작성한 기사입니다. 원문의 사실과 수치에 근거하며, 별도의 견해를 포함하지 않습니다.