에이전트가 승인한 후 이메일을 변경했다: TypeScript로 페이로드 바운드 게이트 구축하기
요약
본 글은 에이전트가 승인한 작업명만으로 이메일의 목적지나 내용 같은 외부 행동(outbound action)까지 변경될 위험성을 지적합니다. Google Gemini 에이전트 발표를 언급하며, 애플리케이션 경계에서 신원, 권한 부여, 샌드박싱을 강화하는 것이 중요함을 강조합니다.
핵심 포인트
- 에이전트는 작업명만 승인해도 목적지(수신자)가 변경될 위험이 있습니다.
- 승인은 '작업'뿐 아니라 '특정 외부 행동의 모든 필드'를 포함해야 합니다.
- TypeScript 등을 사용하여 액션 스키마에서 외부 효과를 바운드하는 게이트 구축이 필요합니다.
당신은 내부 팀으로 보내는 이메일에 대해 승인을 합니다. 일상적인 업무입니다. 특별히 흥미로운 일이 일어나지 않아야 합니다.
발송 전에, 에이전트가 수신자를 회사 외부의 누군가로 변경합니다. 작업명(task)은 여전히 weekly-summary로 되어 있어, 당신의 승인 검사는 이를 통과시킵니다.
축하드립니다. 당신은 작업 이름만 승인했습니다. 이메일이 목적지에서 벗어났습니다.
2026년 10월 8일, Google은 Gemini 에이전트를 발표했습니다. 이는 지속적인 클라우드 실행, 도구 및 서브에이전트를 갖춘 애플리케이션 전반의 범용 작업 에이전트입니다. 이 발표는 또한 신원(identity), 권한 부여(authorization), 샌드박싱(sandboxing) 및 네트워크 제어에 대해서도 설명합니다.
이것이 출시입니다. 위에서 언급된 작은 이메일 사고는 애플리케이션 경계의 합성 예시이며, 나는 앱 전반에 걸쳐 행동할 수 있는 모든 에이전트 하에 구축할 것입니다.
승인은 이 정확한 작업을, 이 수신자들과 이 내용으로 보내야 함을 의미해야 합니다. 작업이 변경되었다면, 다시 요청해야 합니다.
TypeScript로 그 경계를 만들어 봅시다. API 키는 필요 없습니다. 모델 호출도 필요 없고, 실제 이메일도 없으므로(우리 가상의 법무팀에게는 아주 좋은 소식입니다).
이는 발표에서 영감을 받은 애플리케이션 측 패턴입니다. 저는 Google의 승인 구현을 테스트하지 않았습니다.
너무 많은 것을 승인한 체크박스
당신의 하네스가 이것을 저장한다고 상상해 보세요:
approvedTasks.add('weekly-summary');
그러면 나중에 그 작업명을 담는 모든 행동이 통과할 수 있습니다. 수신자를 수정하는 것은 조회(lookup)를 변경하지 않습니다. 본문(body)을 다르게 추가하는 것도 마찬가지입니다. 이 조회는 하나의 임무만 가지고 있으며, 그 임무는 폴더의 라벨을 기억하는 것입니다.
이것은 검토와 사용 사이의 간극입니다. 에이전트는 해당 작업에 대해 작동할 권한을 가질 수 있지만, 특정 외부 행동(outbound action)은 여전히 승인이 필요합니다.
수정은 좁은 액션 스키마(action schema)에서 시작합니다. 외부 효과를 변경할 수 있는 모든 필드는 승인된 표현(approved representation)에 포함되어야 합니다.
이 가상의 이메일 액션의 경우, 이는 테넌트(tenant), 태스크(task), 액터(actor), 툴 버전(tool version), 수신자(recipient), 제목(subject), 본문(body)을 의미합니다. 수신자는 툴의 주요 기능이 사람들에게 무언가를 보내는 것이기 때문에 상당히 중요합니다. 실제 이메일 스키마에는 첨부 파일(attachments), 참조(CC), 숨은 참조(BCC) 및 어댑터가 보낼 수 있는 기타 모든 필드가 필요할 것입니다. 이 데모는 BCC를 포함하여 선언되지 않은 필드는 거부합니다.
빌드 실행하기
소스 코드, 테스트 데이터(fixtures), 정확한 출력 및 다이어그램은 bobbyhalljr/payload-bound-approval에 있습니다.
사전 요구 사항: Node.js 22.18 이상 및 Git. 여기서는 Node.js 22.20.0으로 테스트되었습니다. 패키지나 API 키는 필요하지 않습니다.
git clone https://github.com/bobbyhalljr/payload-bound-approval.git
cd payload-bound-approval
node demo.ts
node demo.ts는 합성 케이스(synthetic cases)를 실행합니다. approval.ts에는 게이트(gate)가 포함되어 있습니다. Node는 네이티브 타입 스트리핑(native type stripping)을 통해 TypeScript를 실행하므로, 이 명령어는 TypeScript 타입 검사기(type checker)를 실행하지 않습니다.
'예'에 정확한 주소 부여하기
저는 호출자가 건네주는 어떤 객체를 직렬화하는 대신 버전이 지정된 튜플(versioned tuple)을 사용합니다. 필드 순서가 명시적이므로, 속성 삽입 순서가 그렇지 않으면 동일한 요청을 실수로 무효화할 수 없습니다.
해시에는 Node의 내장 crypto 모듈을 사용합니다. 이는 승인 서비스에 이 정확한 표현의 간결한 지문(fingerprint)을 제공합니다.
이 해시는 누가 승인했는지를 확립하지 않습니다. 에이전트가 자신의 나쁜 아이디어를 완벽하게 해시할 수 있습니다. 그 권한은 부여를 저장하는 신뢰할 수 있는 서비스에서 나옵니다.
여기에 전체 게이트가 있습니다:
import { createHash } from 'node:crypto';
export type Action = Readonly<{...}
흥미로운 부분은 해시(hash) 이후에 무슨 일이 일어나는가입니다. 세 가지 세부 사항이 승인(approval)을 액션(action)에 연결해 둡니다:
**알 수 없는 필드는 실패합니다.** TypeScript 타입은 런타임(runtime)에서 사라집니다. 이메일 제공업체는 편집기(editor)가 얼마나 안심하게 보였는지 신경 쓰지 않습니다. 게이트(gate)는 복사하기 전에 선언된 필드를 확인합니다. 추가적인 `bcc` 필드를 전달한다고 해서 게이트를 빠져나가는 액션이 조용히 확장될 수는 없습니다.
**반환되는 객체는 디스패치 객체입니다.** 게이트는 해싱하기 전에 필드들을 복사하고 동결(freeze)합니다. 유효성 검사 후 원래 객체를 편집하는 호출자(caller)가 그 반환된 액션을 변경할 수 없습니다. 툴 어댑터(tool adapter)는 `decision.action`을 직접 사용해야 하며, 가변적인 입력으로부터 페이로드(payload)를 재구성해서는 안 됩니다.
**승인은 권한을 소모합니다.** 유효성 검사와 사용 표시 사이에 `await`가 없습니다. 이로 인해 이 확인 및 소모 시퀀스(check-and-consume sequence)는 단일 JavaScript 프로세스 내에서 원자적(atomic)입니다. 이는 워커(workers) 간 또는 프로세스 재시작에 걸친 원자성을 제공하지 않습니다.
수신자나 본문이 변경되면, 에이전트(agent)는 새로운 정확한 액션에 대한 추가 검토가 필요합니다. 이전의 '예'는 오래된 페이로드에 붙어 있습니다.
승인 UI, 인증된 사용자, 영속적인 권한 부여 데이터베이스(durable grant database), 취소 서비스(revocation service), 이메일 제공업체 또는 모델은 여기에 없습니다. Fixture에서 사용되는 권한 ID는 예측 가능합니다. 프로덕션 환경의 ID는 추측할 수 없어야 하며, 에이전트는 스스로 권한을 발급해서는 안 됩니다. 에이전트가 자신을 승인하도록 허용하면 클릭 횟수를 많이 절약할 수 있습니다. 브레이크를 제거하는 것도 마찬가지입니다.
애플리케이션은 인증된 실행 컨텍스트(authenticated execution context)에서 테넌트와 액터(actor)를 파생하고, 도구의 전체 런타임 스키마를 검증하며, 검토자에게 정확한 필드를 보여주어야 합니다. 신뢰할 수 없는 JSON을 데이터로 취급하세요. 이 좁은 검증기(narrow validator)는 임의의 실행 가능한 JavaScript 객체, 게터(getters) 또는 프록시(proxies)를 위해 설계되지 않았습니다.
시간은 fixture에서 호출자로부터 옵니다. 프로덕션 환경에서는 신뢰할 수 있는 서버 클럭과 짧게 정책으로 정의된 생명주기가 필요합니다. 또한 일회성 클레임에 대한 영속적인 트랜잭션 또는 조건부 업데이트, 그리고 감사 기록(audit records) 및 취소 확인이 필요합니다.
권한을 소비하는 것이 원격 쓰기 작업(remote write)이 정확히 한 번 발생하도록 보장하지는 않습니다. 소비 후 충돌이 발생하면 작업이 손실될 수 있습니다. 제공업체의 응답이 손실되면 효과가 불분명할 수 있습니다. 승인 클레임에 **멱등성 키(idempotency key)**, 영속적인 디스패치 상태(durable dispatch state), 그리고 조정(reconciliation)을 페어링하세요. 재시도 시에는 새로운 의도를 만들어내기보다는 동일하게 승인된 의도가 복구되어야 합니다.
마지막으로, 해시 비교는 내용이 안전하거나 정확한지에 대해서는 아무것도 말해주지 않습니다. SHA-256은 형편없는 이메일을 충실하게 식별할 수 있습니다. 검토와 정책이 그것을 건물 밖으로 내보내야 할지 결정합니다. 지문(fingerprint)은 그들의 결정에 평가된 바이트를 첨부합니다.
## 다음으로 구현하고 싶은 것들
앱 전반에서 작동하는 에이전트는 여정 동안 살아남는 승인 경계가 필요합니다.
행동을 동결시키세요. 권한을 바인딩하세요. 디스패치 시점에 소비하세요. 네트워크가 불확실해질 때 영수증(receipt)을 보존하세요.
제가 Roster에 대해 생각하는 방식도 이와 같습니다: 책임, 도구, 그리고 실제 작업 주변의 명확한 경계를 가진 AI 직원들입니다. 유용한 단위는 검사할 수 있는 결과와 증거가 있는 할당(assignment)입니다.
[Roster 사용해 보기](https://get-roster.com).
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
