에이전트 도구 호출을 위한 Capability Token: 게이트(Gate)와 감사(Audit) 역할을 동시에 수행하는 하나의 서명된 객체
요약
LLM 에이전트의 도구 호출 시 보안을 강화하기 위해 'Capability Token'이라는 서명된 객체를 제안합니다. 이 토큰은 도구 실행 전의 권한 확인(Gate)과 실행 후의 감사(Audit) 역할을 동시에 수행하며, 인자 바인딩 및 컨텍스트 검증을 통해 보안 취약점을 해결합니다.
핵심 포인트
- Capability Token은 실행 전 권한 확인과 실행 후 감사 기능을 통합함
- args_hash를 통해 승인된 인자와 실제 호출 인자의 불일치를 방지함
- caller_context_hash로 토큰의 세션 및 사용자 간 재사용을 차단함
- 단순 불리언이나 이벤트 로그 방식의 보안 취약점을 보완함
면책 조항: 이 기사는 AI의 도움을 받아 초안을 작성하였으며, 저자가 검토 및 편집하였습니다. 기술적 설계와 의견은 저자 개인의 것입니다.
LLM 에이전트가 도구(tool)를 호출하기로 결정할 때, 누군가는 "예"라고 말해야 합니다. 대부분의 코드베이스에서 그 "예"는 두 가지 중 하나입니다. 정책 확인(policy check)에서 반환되는 불리언(boolean) 값이거나, 사후에 이벤트 로그(event log)에 추가되는 행(row)입니다. 둘 다 취약합니다. 불리언은 증거를 담고 있지 않습니다. 분기(branch)가 실행되는 즉시 사라지기 때문입니다. 이벤트 로그는 권한(authority)을 담고 있지 않습니다. 실행자가 이미 도구를 실행하기로 결정한 "후"에 기록되므로, 아무것도 차단(gate)할 수 없으며, 나중에 아무도 모르게 수정될 수도 있습니다.
이 글은 저의 이전 글인 에이전트를 신뢰하지 마세요: 도구 호출 승인을 정확한 호출에 바인딩하세요의 후속편입니다. 이전 글에서는 하나의 페이로드(payload)에 대한 승인이 다른 페이로드에 재사용될 수 없도록 승인을 정확한 호출의 인자(arguments)에 바인딩해야 한다고 주장했습니다. 그것은 한 가지 공격을 해결했습니다. 하지만 세 가지 공격은 여전히 남아 있었습니다. 여기에서 저는 완전한 객체인 **Capability Token (역량 토큰)**을 정의하고, 동일한 객체가 동시에 (a) 실행자가 도구를 실행하기 "전"에 확인하는 대상이자 (b) 실행 "후"의 감사(audit) 기록임을 보여주고자 합니다. 강제 집행(Enforcement)과 증거(evidence)가 하나의 서명된 값으로 통합됩니다. 이전 글이 하나의 승인을 하나의 호출에 바인딩했다면, 이 글은 전체 토큰을 명시하고 세 가지 에이전트 프레임워크(agent frameworks)에 걸쳐 변경 없이 이를 검증하는 방법을 다룹니다.
토큰 (The token)
CapabilityToken = {
"tool": "transfer",
"args_hash": "sha256(canonical(args))",
...
각 필드는 불리언이나 일반적인 이벤트 로그가 잡아낼 수 없는 특정 실패 사례를 제거하기 위해 존재합니다.
1. args_hash — 정확한 인자(argument)에 바인딩합니다. 이것이 이전 항목의 핵심입니다. 간단히 말해, transfer(amount=10)에 대한 승인이 transfer(amount=10000)에 재사용되는 것을 방지해야 합니다. 표준화된 인자를 서명된 토큰으로 해시하고, 실행기(executor)는 실제 호출로부터 해시를 다시 계산하여 불일치 시 거부합니다. 끝났습니다. 다음으로 넘어갑니다.
다음 세 가지는 호출별 인자 바인딩만으로는 포착할 수 없는 실패 클래스입니다. 여기서 이 글은 이전 항목보다 진전됩니다.
2. caller_context_hash — 누가 호출하는지에 바인딩합니다. 인자 바인딩은 페이로드 스와프(payload swaps)를 막지만, _컨텍스트(context)_에 대해서는 아무것도 말해주지 않습니다. 세션 S에서 에이전트 A를 위해 발행된 토큰은 여전히 transfer(amount=10)에 대한 완벽하게 유효한 서명입니다. 이 토큰을 에이전트 B의 실행이나 다른 사용자의 세션으로 가져가도 인자는 여전히 일치합니다. 호출자 식별자(agent id, session id, user id)의 해시를 토큰에 바인딩하고, 실행기는 라이브 컨텍스트가 해당 해시를 재현하지 못하는 모든 호출을 거부합니다. 이로써 토큰은 컨텍스트 간에 전송 불가능해집니다.
3. approved_for {step_index, attempt} — 두 개의 시계, 하나가 아닙니다. 벽시계(exp) 만료 시간은 필요하지만 충분하지 않습니다. 재시도 큐를 고려해 봅시다. 승인이 발행되고, 시도가 실패하고, 페이로드가 큐에 머무르다가 나중에 재시도가 이를 가져와
4. policy_version — 떠다니는 포인터가 아닌, 재구성 가능한 권한 (reconstructable authority). 예를 들어, "규칙 R42가 실행됨"이라고 로그를 남겼다고 가정해 봅시다. R42는 변경 가능한 정책 저장소 (mutable policy store)에 존재합니다. 6주 후, 사고 검토 (incident review) 중에 R42를 찾아보니 누군가 정책을 수정하여 내용이 달라져 있습니다. 귀하의 로그는 어떤 규칙이 실행되었는지는 알려주지만, _결정 시점에 그 규칙이 무엇이었는지_는 알려주지 않습니다. 정책 버전(또는 더 나아가, 정확한 규칙 세트의 콘텐츠 해시 (content-hash))을 토큰에 결합하십시오. 이제 해당 엔트리는 호출이 실행된 _권한 (authority)_을 재구성할 수 있습니다. 즉, 결정 사항이 단순히 움직이는 목표를 가리키는 텔레메트리 (telemetry)가 아니라, 재현 가능한 (reproducible) 정보가 됩니다.
5. prev_entry_hash + 주기적인 외부 앵커 (external anchor) — 체인 무결성 (chain integrity). 개별적으로 완벽한 토큰들이 여전히 놓치는 실패 사례는 바로 **부재 (absence)**입니다. 모든 엔트리가 형식이 올바르고, 올바르게 서명되었으며, 인자(args)와 컨텍스트(context)가 결합되어 있더라도, 마지막 부분(tail)이 조용히 사라질 수 있습니다. 쓰기 도중의 충돌 (crash), 공격적인 로그 로테이션 (log rotation), 또는 의도적인 변조로 인해 마지막 N개의 엔트리가 삭제되면, 잘려 나간 꼬리 부분은 "해당 호출이 발생하지 않은 것"과 구별할 수 없습니다. 누락된 것과 삭제된 것을 구분할 수 없게 됩니다. 따라서 엔트리들을 해시 체인 (hash-chain)으로 연결하십시오. 즉, 각 토큰이 이전 토큰의 해시를 포함하도록 합니다. 그리고 주기적으로 원장(ledger) 자체의 신뢰 도메인 외부 (투명성 로그 (transparency log), 별도의 계정, 또는 원장 작성자가 제어할 수 없는 모든 것)에 체크포인트 해시를 게시하십시오. 이제 체인이 끊어지면 가시화되며, 마지막 앵커 이후의 절단 (truncation)도 탐지할 수 있습니다. 엔트리의 부재를 엔트리의 삭제와 구분할 수 있게 됩니다.
6. sig — 감사의 절반 (the audit half). 비대칭 키 (asymmetric key)로 서명하여 토큰의 부인 방지 (non-repudiable)를 보장하고, 토큰을 발행할 수 없는 당사자들도 검증할 수 있도록 합니다. 이것이 _동일한 객체_가 증거가 될 수 있게 하는 핵심입니다. 공개 키 (public key)를 가진 사람이라면 누구나 나중에 오프라인에서도 이를 확인할 수 있습니다.
동일한 토큰, 세 가지 프레임워크
토큰은 불변량 (invariant)입니다. 프레임워크는 단지 그것을 어디서 확인하느냐의 차이일 뿐입니다.
Google ADK (adk-python). BaseTool은 before_tool_callback과 after_tool_callback을 제공합니다. before-callback은 게이트 (gate) 역할을 하며, after-callback은 증거 (evidence)를 기록합니다 — 즉, 동일한 객체를 사용합니다.
def before_tool_callback(tool, args, ctx):
tok = ctx.state["pending_token"]
if not verify(tok, tool.name, args, ctx): # args_hash, caller_context, approved_for, policy, sig
...
Microsoft Semantic Kernel. 자연스러운 훅 (hook)은 자동 함수 호출 (auto-function-invocation) 필터입니다. DENY는 next()를 호출하지 않거나 거절을 반환하는 것을 의미하며, REDACT는 next()를 호출하기 전에 context.arguments를 변형 (mutate)하는 것을 의미합니다. 진정으로 누락된 프리미티브 (primitive)는 REQUIRE_APPROVAL이며, Semantic Kernel (SK)의 구조는 그 의미를 강제합니다. 자동 호출 루프는 **동기적 (synchronous)**이므로, "승인"이 루프 내의 await가 될 수는 없습니다. 그렇게 되면 사람이 결정하는 동안 채팅 완료 (chat-completion) 연결을 계속 열어두어야 하기 때문입니다. 이는 반드시 **종료 후 재개 (terminate-and-resume)**를 의미해야 합니다:
async def on_auto_invoke(context, next):
tok = context.arguments.get("_cap_token")
verdict = verify(tok, context.function, context.arguments)
...
pydantic-ai. 본문이 실행되기 전에 도구 래퍼 (tool wrapper) 또는 RunContext에서 토큰을 확인합니다.
def guarded(fn):
def wrapper(ctx: RunContext, **kwargs):
if not verify(ctx.deps.token, fn.__name__, kwargs, ctx):
...
세 곳의 호출 지점, 하나의 객체. 최신성 버그(freshness bug, 필드 3), 컨텍스트 리프트 버그(context-lift bug, 필드 2), 그리고 이동 정책 버그(moving-policy bug, 필드 4)는 세 곳 모두에서 동일하게 포착됩니다. 왜냐하면 이 버그들은 프레임워크가 아닌 토큰 내에 존재하기 때문입니다. 이는 제가 참여하고 있는 업스트림(upstream) 논의에서 현재 다루어지고 있는 문제들과 동일합니다. 즉, ADK decision-ledger 이슈, Semantic Kernel의 자동 함수 호출 승인 격차(auto-function-invocation approval gap), 그리고 단순한 tool_call_approved 불리언(bool) 값을 (run_id, tool_call_id, expiry)를 포함하며 HMAC으로 결합된 승인 토큰(approval token)으로 교체하려는 pydantic-ai의 제안이 그것입니다. 여기서 반복되는 질문은 불변량(invariant)이 하나의 결합된 객체라는 점을 받아들인 후, 검증이 '어디에' 속해야 하는가 하는 점입니다.
텔레메트리(telemetry)가 아닌 증거(evidence)
이것이 핵심적인 보상입니다. 단순한 이벤트 로그는 텔레메트리(telemetry)입니다. 이는 이야기꾼을 신뢰해야만 알 수 있는 과거의 이야기를 들려줍니다. 반면, Capability Token으로 구성된 결정 원장(decision ledger)은 **증거(evidence)**입니다. 각 항목은 변조 방지 기능(tamper-evident; 서명 + 체이닝 + 앵커링)을 갖추고 있으며, 나중에 다시 재생(replay)했을 때도 여전히 의미가 있습니다(인자(args), 호출자(caller), 시퀀스 위치, 그리고 정확한 정책 텍스트가 모두 결합되어 있기 때문입니다). 사건 발생 몇 주 후, 현장에 없었으며 토큰을 생성할 수 없는 사람에게 이를 전달하더라도 그들은 검증할 수 있습니다. 불리언(boolean) 값은 그렇게 할 수 없습니다. 로그 한 줄도 그렇게 할 수 없습니다. 하나의 서명된 객체가 두 가지 역할을 모두 수행합니다.
토큰 자체는 아마 여러분이 작성할 코드 중 100줄 정도일 것입니다. 중요한 규율은 이것을 무언가 잘못되었을 때 사후에 덧붙이는 부가 요소가 아니라, 여러분의 에이전트 내에서 일급 객체(first-class object)로 결정하는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기