theAuth 가이드: 에이전트 지출 제한 및 인간 승인 요구
요약
본 가이드는 오픈 소스 인증 시스템인 theAuth를 사용하여 AI 에이전트의 안전성과 통제력을 높이는 방법을 다룹니다. 특히, 에이전트가 무분별하게 자원을 사용하거나 위험한 행동을 하는 것을 막기 위해 '지출 한도', '만료되는 권한', 그리고 '인간 승인'이라는 세 가지 핵심 제어 장치를 구축하는 방법을 설명합니다.
핵심 포인트
- theAuth는 AI 에이전트와 인간을 위한 오픈 소스 인증 시스템입니다.
- 에이전트 군단에는 신원 확인 외 지출 한도, 만료 권한, 인간 검토가 필요합니다.
- theAuth를 사용해 제한된 접근 권한 위임 및 예산 보호 장치를 구현할 수 있습니다.
theAuth는 AI 에이전트와 인간을 위한 오픈 소스 인증(auth) 시스템입니다. GitHub에서 리포지토리 스타하기 · 문서 읽기 · 빠른 시작하기 · theauth.dev
어느 날 제 플래너 에이전트가 금요일 밤에 여섯 개의 워커로 분산되었습니다. 토요일 아침까지 한 워커가 실패한 요약 작업을 계속 재시도했고, 제 제공업체 대시보드에는 마음에 들지 않는 줄이 나타났습니다. 아무도 침입하지 않았고, 모든 자격 증명(credential)은 유효했습니다. 시스템은 단순히 '유효하다'와 '감당할 수 있다'가 다른 질문이라는 것을 알지 못했던 것입니다.
그 주말을 통해 에이전트 군단(agent fleets)에는 신원 확인 외에 세 가지 통제가 필요하다는 것을 배웠습니다. 만료되는 제한적인 권한 부여(Narrow grants that expire). 실제 호출을 막아주는 지출 한도(A spend cap that actually stops calls). 그리고 되돌릴 수 없는 행동 앞에 놓이는 인간의 검토 단계(human checkpoint)입니다.
이 가이드에서는 오픈 소스 인증 라이브러리인 theAuth를 사용하여 이 세 가지를 모두 구축합니다. (npm에서 @glinr/theauth). 본 가이드를 마치면, 작업자에게 범위가 지정되고 수명이 짧은 접근 권한을 위임하는 플래너, 한 임계값에서 경고하고 다른 임계값에서 차단하는 예산 보호 장치(budget guard), 파괴적인 행동에 대한 승인 흐름(approval flow), 그리고 각 위임 체인별로 비용을 집계하는 비용 보고서까지 갖게 될 것입니다.
저는 이미 이전 포스트에서 서브 에이전트 위임의 기본 사항들을 다루었습니다. 이번 글은 그곳에서 멈춘 지점, 즉 체인이 존재하는 경우 돈과 위험한 행동에는 무슨 일이 발생하는지에 대해 시작합니다.
이 가이드는 theAuth 가이드 중 8개 중 6번째입니다. 독립적으로 작동하므로 여기서 바로 시작할 수 있습니다. 가이드 5에서 에이전트 신원을 사용합니다. 이미 에이전트를 생성하는 방법을 알고 있다면, 여기부터 시작하세요.
| 가이드 | 제목 | 언제 읽어야 하나 |
|---|---|---|
| 1 | 기존 Next.js 앱에 로그인 기능 추가하기 | 아직 인증(auth) 기능이 없는 앱을 가지고 있을 때 |
| ... | ||
| 사람들을 위해 구축하나요? 가이드 1부터 시작하세요. AI 에이전트를 위해 구축하나요? 가이드 5부터 시작하세요. 모든 가이드는 해당 개념과 관련된 문서 페이지로 연결됩니다. |
요약 (TL;DR)
| 제어 | 호출하는 이름 | 기능 | 누가 강제하는가 |
|---|---|---|---|
| 위임(Delegation) | theauth.delegate() | 만료 기한과 깊이 제한을 가진 권한의 부분 집합을 부여합니다 | theAuth, authorize() 내부 |
| ... | |||
마지막 열에 주목하세요. 위임과 권한 제약만 authorize() 내부에 있습니다. 예산(Budgets) 및 승인(approvals)은 호출하는 모듈입니다. 이 점을 반복하겠습니다. 왜냐하면 잘못된 가정 때문에 대부분의 문제가 발생하기 때문입니다. |
전제 조건 (Prerequisites)
Node 20 이상, 패키지 관리자, 그리고 TypeScript 프로젝트가 필요합니다. 또한 theAuth에서 에이전트 식별자(agent identity)가 무엇인지 알아야 합니다. 모른다면 에이전트 식별자 페이지를 먼저 읽으세요. 5분이 걸립니다.
패키지 설치:
pnpm add @glinr/theauth
빈 프로젝트 대신 실행 가능한 앱을 원한다면, 빠른 시작(quickstart)에 한 번의 명령어로 설정하는 도구(scaffolder)가 있습니다. 아래 예제들은 SQLite를 사용하므로 어디서든 실행할 수 있습니다.
시작하기 전에 솔직한 범위 설명이 하나 있습니다. theAuth는 식별자, 제한된 권한, 그리고 감사 추적(audit trail)을 한 곳에서, 자체 프로세스와 데이터베이스 내에 원할 때 적합합니다. theAuth는 결제 시스템이 아닙니다. 제공업체가 사용자에게 요금을 청구하는 것을 막지 못하며, 네트워크 수준에서 지출액을 제한하지도 않습니다. 여기에 있는 모든 것은 코드가 라이브러리에 먼저 요청하고 그 답변을 따르기 때문에 작동합니다.
1단계: 플릿(fleet) 생성하기
하나의 인스턴스와 세 개의 에이전트로 시작하세요. 플래너(planner)는 광범위한 권한을 보유합니다. 요약기(summarizer)는 아무것도 없는 상태로 시작하여 위임을 통해 접근 권한을 받게 됩니다. 클리너(cleaner)는 승인이 필요한 삭제 기능을 포함하여 자체의 좁은 권한을 가지고 있습니다.
import { createTheAuth } from '@glinr/theauth';
const theauth = await createTheAuth({
...
ownerId는 실제 사용자 행과 일치해야 하므로, 먼저 해당 사용자를 생성하거나 조회해야 합니다. agent identity docs에는 토큰 로테이션을 포함한 전체 수명 주기가 나와 있습니다. 각 에이전트는 또한 베어러 토큰(bearer token)을 받는데, 이 토큰은 한 번만 나타나고 휴지 상태에서는 해시되어 저장됩니다. 보이는 즉시 비밀 스토리지에 보관하세요.
요약기(summarizer)에게 delegated 타입을 사용하는 이유는 의도를 알리기 위함입니다. 이 에이전트는 빌려온 접근 권한으로 하나의 작업을 완료하는 것이 목적이며, 그 후 사라집니다.
2단계: 가장 작은 유용한 권한을 위임하기 (Delegate the smallest useful grant)
이제 플래너(planner)는 요약기에게 GitHub 이슈에 대한 읽기 접근 권한을 30분 동안, 더 이상의 연결 지점 없이 위임합니다.
const chain = await theauth.delegate({
fromAgent: planner.id,
toAgent: summarizer.id,
...
delegation docs에 있는 세 가지 세부 사항은 실제 디버깅 시간을 절약해 줍니다.
첫째, 서브셋 규칙(subset rule)입니다. 플래너는 위임하는 모든 권한을 보유해야 합니다. 더 넓은 리소스나 자신이 갖지 않은 액션을 요청하면 오류가 발생합니다. 이것이 올바른 실패 방식입니다. 손상된 플래너는 자신이 결코 가질 수 없었던 힘을 발행할 수 없습니다.
둘째, 각 호출(call)은 자체적으로 maxDepth를 확인합니다. 후속 연결 지점(Later hops)은 이를 상한선으로 상속받지 않습니다. 재위임(re-delegation)을 막으려면 코드의 모든 delegate() 호출에 작은 maxDepth를 전달하세요. 만약 어딘가에서 기본값 3에 의존한다면, 체인이 계획했던 것보다 더 깊어질 수 있습니다.
셋째, 서브셋 검사(subset check)는 위임하는 에이전트 자체의 권한을 사용합니다. 위임된 접근만 가진 에이전트는 이를 전달할 수 없습니다. 중간 에이전트가 재위임하기를 원한다면, 그 에이전트에게 자체 복사본의 권한을 부여하세요. 저는 이것이 놀랍다고 생각하지만, 안전한 쪽으로 가는 것이 좋습니다.
요약기가 실제로 무엇을 할 수 있는지 확인해 보세요:
const allowed = await theauth.authorize(summarizer.id, {
action: 'read',
resource: 'mcp:github:issues',
...
authorize()는 먼저 에이전트 자체의 권한을 확인하고, 그 다음 위임된 권한으로 폴백(fallback)합니다. 위임된 세트를 직접 나열할 수도 있습니다:
const effective = await theauth.delegation.getEffectivePermissions(summarizer.id);
console.log(effective);
체인 취소 (Revoking a chain)
작업이 잘못되었을 때, 전체 서브트리(subtree)를 종료하는 단일 호출을 원합니다.
await theauth.delegation.revoke(chain.id);
링크를 취소하면 수신 에이전트(receiving agent)에서 시작되는 활성 체인도 함께 취소됩니다. 만약 요약기(summarizer)가 다음 단계로 위임했다면, 해당 링크들도 종료됩니다. 다음 authorize() 호출은 allowed: false를 반환합니다. 기억해야 할 한 가지 제한 사항이 있습니다. 즉, 취소는 이미 진행 중인 작업(operation already in flight)을 중단시키지 않습니다. 긴 HTTP 요청은 완료될 때까지 계속 실행됩니다.
3단계: 모든 LLM 호출 앞에 예산 책정 (Put a budget in front of every LLM call)
위임(Delegation)은 에이전트가 무엇에 접근할 수 있는지 제어합니다. 하지만 얼마나 많은 비용을 지출할 수 있는지는 아무것도 말해주지 않습니다. 이를 위해 theAuth는 theauth.policies에서 예산 정책(budget policies)을 제공합니다. 모델마다 구체적이기 때문에 예산 정책 페이지를 한 번 읽어보세요.
사람들이 놓치는 부분이 여기 있습니다. authorize()는 예산 정책을 참조하지 않습니다. 코드가 LLM 호출 전에 checkBudget()를 호출하고 후에 recordUsage()를 호출하지 않으면 아무 일도 일어나지 않습니다.
요약기를 위해 두 가지 정책, 즉 소프트(soft)한 정책과 하드(hard)한 정책을 생성합니다:
await theauth.policies.create({
agentId: summarizer.id,
limits: { maxTokensCostPerDay: 800 },
...
800 유닛에서는 warn 정책이 발동하고 checkBudget()는 여전히 allowed: true를 반환하며, 알림을 보낼 수 있도록 해당 정책이 첨부됩니다. 1000에서는 block 정책이 allowed: false를 반환합니다.
이제 확인 과정을 잊어버릴 수 없도록 모델 호출을 감싸세요 (wrap):
async function guardedCall<T>(
agentId: string,
estimatedCost: number,
...
모든 모델 호출 주위에 사용하세요:
const summary = await guardedCall(summarizer.id, 50, async () => {
const result = await llm.complete('Summarize the open issues.');
return { value: result.text, actualCost: result.usage.totalTokens };
...
llm 객체는 사용자의 자체 클라이언트 역할을 대신합니다. 래퍼(wrapper)는 숫자만 반환하면 됩니다.
주의할 점 (Things that bite)
action 필드는 네 가지 값(warn, throttle, block, revoke)을 가집니다. 현재 코드에서는 throttle, block, revoke가 모두 동일하게 작동합니다. 이들은 모두 allowed: false를 반환합니다. revoke는 에이전트 토큰을 취소하지 않습니다. 만약 에이전트를 비활성화하고 싶다면, 직접 theauth.agent.revoke()를 호출해야 합니다.
체크는 오직 agentId로만 정책과 일치시킵니다. 단순히 userId나 tenantId만 가진 정책은 에이전트가 없으므로 모든 에이전트에 적용됩니다. 이 두 필드는 list() 필터로만 작동하며, 체크 범위를 제한하지는 않습니다. 테넌트별 한도를 계획한다면, 매칭에 의존하기보다는 에이전트당 하나의 정책을 생성하고 해당 필드를 설정하여 필터링하는 것이 좋습니다. multi-tenant 페이지에서 테넌트가 어떻게 함께 작동하는지 다룹니다.
또한, 카운터를 자동으로 초기화하는 것은 없습니다. 이는 스케줄링해야 합니다.
// UTC 자정 크론
const { reset } = await theauth.policies.resetDaily();
console.log(`Reset ${reset} policies`);
...
두 호출 모두 모든 정책에 적용됩니다. triggered되었던 정책은 사용량이 다시 한도 미만으로 떨어지면 active 상태로 돌아갑니다.
만약 예산 로직 없이 에이전트당 시간별 하드 호출 횟수 제한을 원한다면, rate limiting 페이지가 더 간단한 도구입니다.
단계 4: 되돌릴 수 없는 작업에 인간 승인 요구하기
예산은 양을 제한합니다. 승인 게이트는 무엇이 필요한지 결정합니다. 일부 작업(프로덕션 파일 삭제, 자금 이동, 권한 확대 등)은 사람이 먼저 '예'라고 말해야 합니다.
theAuth는 이를 requireApproval이라는 권한 제약 조건으로 모델링합니다. 더 깔끔한 에이전트에게 읽기 권한과 인간의 승인이 필요한 삭제 권한을 부여해 보세요:
const cleaner = await theauth.agent.create({
ownerId: 'user-123',
name: 'file-cleaner',
...
클리너가 삭제를 시도할 때, authorize()는 이를 거부하고 그 이유를 알려줍니다. 사용자의 애플리케이션은 그 이유를 감지하여 승인 요청을 열게 됩니다. approval docs에서 동일한 흐름에 대해 설명합니다.
에이전트는 차단되지 않습니다. 대신 approvalId를 반환하고 다음 단계로 넘어갑니다. 이후 사람이 설정한 TTL(Time To Live) 내에서 몇 분 또는 몇 시간 후에 응답합니다 (기본값은 5분이며, 위의 설정에서는 이를 600초로 늘립니다).
사람에게 알림하기
theAuth는 요청을 저장할 뿐, 메시지를 전달하지는 않습니다. onApprovalNeeded를 사용하거나 webhookUrl을 사용하여 사람이 어떻게 이 사실을 알게 할지 선택합니다 (1단계에서 표시됨).
const theauth = await createTheAuth({
database: { provider: 'sqlite', url: 'theauth.db' },
approval: {
...
웹훅은 approval_needed 이벤트와 요청 본문을 포함하는 일반적인 POST 요청입니다. 서명 헤더나 재시도가 없습니다. 이를 보장이라기보다는 알림(nudge)으로 취급하십시오. 실패하더라도 요청은 여전히 저장되며, 주기적인 listPending() 스윕이 이를 찾아냅니다. 자체 검증 계층을 앞에 두지 않은 핸들러에 비밀 정보를 넣지 마십시오.
승인하고, 거부하고, 그리고 실제로 행동하기
사용자의 백엔드는 두 개의 경로를 노출해야 합니다. 기록상 누가 결정했는지 알 수 있도록 검토자(reviewer)의 신원을 전달하십시오.
app.post('/approvals/:id/approve', async (req, res) => {
const updated = await theauth.approval.approve(req.params.id, req.user.email);
res.json({ status: updated.status });
...
여기서 사람들이 실수하는 부분이 있습니다. 승인은 우회 수단이 아니라 기록입니다. 사람이 승인하더라도, 해당 삭제에 대한 authorize()는 여전히 allowed: false를 반환합니다. 왜냐하면 권한이 여전히 requireApproval을 가지고 있기 때문입니다. 이는 승인 행(rows)을 조회하지 않습니다.
코드가 상태가 변경된 후에 스스로 작업을 수행해야 합니다:
async function finishDelete(approvalId: string, path: string) {
const request = await theauth.approval.get(approvalId);
...
이 함수를 삭제가 일어나는 유일한 장소로 만드십시오. 두 번째 코드 경로에서 승인 상태 확인 없이 삭제할 수 있다면, 당신은 게이트(gate)가 아니라 체크박스를 만든 것입니다.
두 가지 작은 사실이 더 있습니다. approve()와 deny()는 요청 상태가 pending이 아닌 경우 예외를 발생시킵니다. 또한, approve()는 만료 시간(expiresAt)을 확인하지 않습니다. TTL(Time To Live)이 지난 요청은 클린업 작업을 실행할 때까지 계속 pending 상태로 유지되므로, 스케줄링하세요:
const { expired } = await theauth.approval.cleanup();
console.log(`Expired ${expired} stale approval requests`);
이를 listPending(userId)를 호출하여 목록을 보여주는 UI와 결합하면, 검토자는 에이전트가 어떤 인자들과 함께 승인을 요청했는지 정확히 보고 '예'라고 말할 수 있습니다.
5단계: 모든 달러에 속성을 부여하기
예산 정책은 사용자가 제공하는 모든 숫자를 계산합니다. 이 돈을 어느 도구가 소모했는지, 또는 특정 작업이 어떤 위임 체인(delegation chain)에 속했는지는 알려주지 않습니다. 이를 위해서는 비용 귀속(cost attribution) 기능이 필요합니다. 비용 귀속 페이지를 참조하세요.
이 모듈은 독립적입니다. 데이터베이스 핸들로부터 구축할 수 있습니다:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기