엔지니어링 노트보안과 감사

확인하라고 일러도 모델은 메일을 보낸다, 실행 직전에 승인을 받는다

시스템 프롬프트에 「메일을 보내기 전에 사용자에게 확인하라」고 적어 두어도, 모델은 확인 없이 메일 도구를 부를 수 있습니다(「권한은 코드로 막고, 판단은 모델에 맡긴다」). 그래서 OwlAI에서는 승인이 필요한 도구를 모델이 부르면, 그 도구가 실행되기 직전에 서버가 루프를 세우고 사람의 답을 기다립니다. 그런데 승인 창을 띄우는 것만으로는 부족합니다. 창을 띄운 뒤에 답을 받을 자리를 만들면 아주 빨리 누른 답이 사라지고, 화면이 서버가 받지 않는 버튼을 그리면 그 버튼을 누른 턴이 멈춥니다. 같은 요청을 거절해도 모델이 다시 부르면 창이 끝없이 뜨고, 관리자가 규칙을 조여도 예전에 누른 「항상 허용」이 그대로 통과합니다.

이 글은 실행 직전 승인의 세부를 다룹니다. 사람이 승인 창에서 무엇을 보는지, 답을 기다리는 동안 무엇이 어디에 있는지, 거절과 만료 뒤에 모델이 무엇을 받는지, 「항상 허용」이 어디까지 미치고 어떻게 거두는지, 승인할 사람이 없는 예약 실행은 어떻게 하는지입니다. 어떤 도구에 승인을 거는지는 「권한은 코드로 막고, 판단은 모델에 맡긴다」과 「커넥터 도구의 위험은 고객사가 안다, 도구 정책은 설치마다 둔다」에서 다룹니다.

승인을 받을지는 실행 직전의 확인이 정한다

확인을 모델에게 맡기는 방법도 있습니다. 모델에게 되묻기 도구를 주고, 메일을 보내기 전에는 그 도구로 물으라고 적는 것입니다. 모델이 문맥을 보고 위험한 호출에서만 묻는다는 장점이 있지만, 물을지 정하는 쪽이 여전히 모델이라 묻지 않은 한 번이 그대로 사고가 됩니다. 화면에서만 확인 창을 띄우는 방법은 반대로 서버가 그 확인을 모릅니다. 화면을 거치지 않은 호출은 확인 없이 실행됩니다.

그래서 판정은 서버가 도구를 실행하기 전에 권한을 확인하는 단계에서 합니다. 이 단계는 호출마다 허용, 차단, 승인 필요 가운데 하나를 냅니다. 승인 필요가 나오면 그 자리에서 승인 절차가 돌고, 승인을 받은 호출만 실행됩니다. 모델이 직접 부른 호출, 스크립트 안에서 부른 호출(「스크립트 안에서 부른 도구도 호출마다 권한과 승인을 확인한다」), 데스크톱 앱이 사용자 PC에서 실행하는 파일 도구가 모두 같은 단계와 같은 승인 절차를 지납니다.

판정 자체가 실패하면 그 호출은 실행하지 않고, 모델에게 policy_unavailable 사유를 돌려줍니다. 판정하지 못한 호출을 승인 절차로 넘기면 관리자가 차단한 도구도 사용자의 승인 한 번으로 실행되기 때문입니다. 실패는 기억하지 않으므로 다음 판정이 성공하면 그 도구는 원래 권한대로 돌아옵니다.

승인 창은 도구가 받을 인자를 키마다 보여 준다

승인 창에는 도구 이름과 커넥터 이름, 그리고 모델이 그 도구에 넘긴 인자가 키마다 한 줄씩 뜹니다. 화면은 모델이 보낸 키를 빠짐없이 보여 줍니다. 커넥터 도구는 서버마다 인자 이름이 달라서, 키를 목록으로 정하면 처음 보는 서버의 도구는 인자가 한 줄도 보이지 않습니다. 그러면 메일 세 통을 읽음으로 바꾸는 호출과 메일 백 통을 지우는 호출이 같은 창으로 보입니다.

값은 서버가 줄여서 보냅니다. 긴 값은 앞부분만 싣고, 화면은 그 줄에 「(원래 N자 중 일부만 표시)」를 붙여 숨은 부분이 있다는 것을 알립니다. 값 전체가 주소면 호스트와 경로는 자르지 않고 쿼리 뒤쪽만 줄입니다. 파일을 올리거나 페이지를 여는 도구를 승인할 때 봐야 하는 것은 「어디로」이기 때문입니다. 사용자 PC의 파일을 지우거나 옮기는 도구에는 되돌릴 수 없다는 경고가 붙습니다.

버튼도 서버가 정합니다. 서버가 그 호출에 받아 줄 선택지(승인, 「항상 허용」, 거부)를 보내고, 화면은 그대로 버튼으로 그립니다. 화면이 세 버튼을 늘 그리면 「항상 허용」을 받지 않는 도구에서 그 버튼을 누른 턴이 멈춥니다.

답을 받을 자리를 먼저 만들고, 그다음 창을 띄운다

승인 한 번은 다음 순서로 돕니다.

  1. 서버가 이 호출의 대기 항목을 Postgres에 만듭니다.
  2. 화면에 승인 요청을 보냅니다. 요청에는 도구 이름, 커넥터 이름, 인자 미리보기, 받아 줄 선택지가 실립니다.
  3. 턴을 돌리는 루프가 그 자리에서 대기 항목의 상태를 주기적으로 읽으며 기다립니다.
  4. 답이 오면 승인이나 거부로 바꿔 실행 단계에 돌려줍니다. 대기 상한이 지나면 만료로 처리합니다.
승인 한 번의 흐름: 실행 전 확인에서 실행 또는 사유 반환까지
그림 1. 승인 한 번의 흐름: 실행 전 확인에서 실행 또는 사유 반환까지

대기 항목을 창보다 먼저 만드는 것은 경합 때문입니다. 순서가 반대면 사용자가 창을 보자마자 눌렀을 때 그 답을 받을 항목이 아직 없습니다. 대기 항목을 프로세스 메모리가 아닌 Postgres에 두는 것은 답이 들어오는 웹 프로세스와 턴을 돌리는 프로세스가 다르기 때문입니다. 답이 어느 프로세스로 들어와도 행 하나를 바꾸고, 기다리는 루프는 그 행을 읽습니다. 처리가 끝난 대기 항목은 지웁니다.

대기 상한은 기본 600초이고 timeouts.userInteractionParkSeconds로 바꿉니다. 기다리는 동안 루프와 대화 맥락은 메모리에 그대로 있고, 스레드 하나가 대기에 묶입니다.

답을 받는 API는 세 가지를 확인합니다. 대기 항목의 주인이 요청한 계정인지, 대화가 맞는지, 이미 답했거나 취소된 항목이 아닌지입니다. 다른 계정의 항목에 보낸 답은 거부되고, 이미 끝난 항목에 보낸 답은 충돌로 돌아옵니다. 이 확인과 실제 기록 사이에 다른 답이나 취소가 끼어들 수 있으므로, 마지막 기록은 「아직 대기 중인 항목만 바꾼다」는 조건을 건 UPDATE 하나로 합니다. 두 요청이 동시에 와도 한쪽만 이기고, 두 요청이 서로 다른 프로세스로 들어와도 같습니다. 사용자가 턴을 중지하면 그 사람의 대화에 걸린 대기 항목만 취소되고, 기다리던 루프는 턴을 끝냅니다.

거절과 만료는 실행하지 않은 이유로 모델에게 돌아간다

거절하거나 상한까지 답이 없으면 그 호출은 실행되지 않고, 모델은 도구 결과로 사유 코드를 받습니다. 거절은 approval_denied, 만료는 approval_timeout입니다. 코드에는 다음에 할 일이 함께 붙습니다. 같은 인자로 다시 부르지 말 것, 그리고 실행하지 않은 이유를 사용자에게 알리고 그 도구 없이 할 수 있는 일을 이어 갈 것입니다.

그래도 모델은 같은 호출을 다시 낼 수 있습니다. 그래서 한 턴 안에서 같은 도구, 같은 인자 조합이 명시적으로 두 번 거절되면, 다음 같은 요청은 다시 묻지 않고 막고 approval_denied_repeated를 돌려줍니다. 횟수는 toolPolicy.approvalDeniedBlockThreshold(기본 2)로 바꿉니다. 같은 인자로 연달아 부르는 호출을 막는 검사가 따로 있지만, 승인이 필요한 호출은 승인 절차로 먼저 넘어가 그 검사에 닿지 않습니다. 거절 횟수를 따로 세지 않으면 거절과 재요청 창이 끝없이 이어집니다. 만료는 사용자가 거절한 것이 아니므로 이 횟수에 넣지 않고, 승인을 받으면 그 조합의 횟수를 지웁니다.

「항상 허용」은 그 사람의 그 도구에만 걸리고, 규칙이 바뀌면 다시 묻는다

사용자가 「항상 허용」을 누르면 이번 호출을 승인하고, 그 사람과 그 도구의 짝을 기록합니다. 그 뒤로 그 사람이 어느 대화에서 그 도구를 부르든 창 없이 승인됩니다. 기록은 도구 단위라 인자가 달라도 같은 도구면 묻지 않습니다. 다른 사용자에게는 걸리지 않습니다.

승인이 필요한 내장 도구와 「항상 허용」을 받는지는 다음과 같습니다.

도구 「항상 허용」
사용자 PC 파일 읽기(데스크톱 앱) 받음
사용자 PC 파일 쓰기·편집·삭제·이동 받음
외부 주소에서 파일 가져오기 받음
PC 파일을 샌드박스로 올리기 받지 않음
샌드박스 파일을 외부 주소로 올리기 받지 않음

받지 않는 두 도구는 파일을 지금 있는 곳 밖으로 내보냅니다. 이 둘에는 선택지에서 「항상 허용」을 빼고, 이미 기록된 신뢰가 있어도 무시하고 매번 묻습니다. 선택지만 빼면 그 전에 기록된 신뢰가 그대로 남아 묻지 않고 통과합니다.

「항상 허용」은 기록한 때의 규칙 위에서 한 동의입니다. 그래서 승인을 요구하는 관리자 규칙이 신뢰를 기록한 뒤에 바뀌었으면, 기록이 있어도 다시 묻습니다. 관리자가 허용이던 도구를 승인 필요로 조이면 이미 「항상 허용」을 누른 사람에게도 바로 창이 뜹니다.

거두는 곳은 커넥터 화면의 자동 승인 탭입니다. 「항상 허용」을 누른 도구가 커넥터별로 모이고, 내장 도구는 시스템 도구로, 지워진 커넥터의 도구는 따로 묶여 남습니다. 해제하면 다음 호출부터 다시 묻습니다. 채팅에서 누른 「항상 허용」은 플레이북 실행에 쓰이지 않습니다. 사람이 없는 실행에는 아래의 사전 승인이 따로 있습니다.

사람이 없는 실행은 소유자가 미리 승인한 도구만 쓴다

플레이북 예약 실행에는 승인 창에 답할 사람이 없습니다. 승인이 필요한 도구를 막으면 예약이 매 주기 조용히 실패하고, 통과시키면 관리자가 건 승인 요구가 무의미해집니다.

그래서 소유자가 실행 전에 한 번 승인합니다. 플레이북을 처음 실행하거나 켜거나 예약할 때, 승인이 필요한 커넥터 도구가 있으면 도구 사용 승인 창이 뜹니다. 승인이 끝나기 전에는 켜기와 예약이 PLAYBOOK_TOOL_APPROVAL_REQUIRED로 거절됩니다. 승인은 플레이북 하나와 (커넥터, 도구)의 짝 단위입니다. 같은 도구를 여러 단계가 써도 한 번 승인하면 됩니다. 실행이 시작되면 아직 유효한 승인만 펼쳐 두고, 그 짝의 호출만 통과시킵니다. 그 밖의 승인이 필요한 호출은 묻지 않고 거절되어 approval_unavailable이 돌아갑니다.

사전 승인에는 유효기간이 없습니다. 만료가 있으면 어느 날 예약이 조용히 실패합니다. 대신 동의의 전제가 바뀌면 그 즉시 무효가 되고, 창에 그 이유가 뜹니다.

  • 승인을 요구한 관리자 규칙이 바뀌었을 때(「권한 설정이 바뀌어 이전 승인 만료」)
  • 플레이북 정의가 바뀌었을 때(「플레이북 내용이 바뀌어 이전 승인 만료」)
  • 소유자가 바뀌었을 때(「소유자가 바뀌어 이전 승인 만료」)

정의가 바뀌었는지는 정의 전체의 지문(fingerprint)으로 봅니다. 캔버스에서 단계를 옮긴 것만으로는 바뀌지 않고, 지시문·설정·단계 순서가 바뀌면 달라집니다. 관리자가 등록한 작업 유형을 고친 경우도 그 유형의 버전이 지문에 들어가 있어 무효가 됩니다. 승인은 소유자 본인만 할 수 있습니다. 실행이 소유자의 권한으로 돌므로 승인 책임도 같은 사람에게 둡니다.

승인 결정은 감사 기록에 한 줄씩 남습니다. 승인 창의 승인·「항상 허용」·거부·만료, 「항상 허용」으로 묻지 않고 승인한 호출, 반복 거부로 묻지 않고 막은 호출, 「항상 허용」 해제, 사전 승인의 부여와 해제가 관리자의 감사 이벤트 화면에 오릅니다. 줄에는 도구와 커넥터 이름, 대화와 턴(사전 승인이면 플레이북)만 실리고 인자 본문은 실리지 않습니다(「감사 기록에는 대화 대신 AI가 실행한 SQL과 도구 호출을 남긴다」).

딥 리서치의 조사원도 사람이 지켜보지 않는 실행입니다. 조사원에게는 승인이 필요한 도구를 처음부터 주지 않습니다(「서브에이전트 도구를 부모 모델이 고르면 승인이 빠진다, 코드가 정한다」).

설계에서 고른 것

승인 판정의 단위로 인자 내용 대신 도구를 골랐습니다. 인자로 위험을 가르면 같은 메일 도구라도 사내 수신자에게는 묻지 않는 식으로 더 세밀해집니다. 그러려면 고객사마다 다른 외부 서버의 인자 뜻을 제품 코드가 알아야 합니다. 그래서 판정은 도구 단위로 단순하게 두고, 인자는 승인 창에 펼쳐 사람이 보고 정하게 했습니다. 대가는 안전한 호출에도 창이 뜬다는 것이고, 자주 쓰는 도구는 「항상 허용」으로 그 횟수를 줄입니다.

인자 미리보기는 원문 대신 줄인 값을 보냅니다. 승인 요청에 자격 증명이나 개인정보 원문이 그대로 실리지 않게 하려는 것입니다. 대가는 긴 메일 본문을 창에서 끝까지 읽지 못한다는 점이고, 줄였다는 사실과 원래 길이는 화면에 보입니다.

승인을 기다리는 턴은 내려놓았다 다시 세우는 대신 그 자리에서 기다립니다. 내려놓으면 스레드가 풀리지만, 다시 세울 때 대화 맥락과 진행 중이던 라운드를 온전히 되살려야 합니다. 그 자리에서 기다리면 상태를 지키기 쉽고, 스레드를 쥐는 비용은 600초 상한이 묶습니다.

사전 승인의 지문은 단계별 대신 정의 전체로 잡았습니다. 단계별로 잡으면 고치지 않은 단계의 승인이 살아남아 다시 승인할 일이 줄어듭니다. 하지만 「3단계만 고쳤으니 1단계의 메일 발송 승인은 그대로 둬도 된다」는 판단은 사람이 해야 합니다. 그래서 무엇이든 바뀌면 소유자가 다시 봅니다.

외부 서비스에 쓰는 도구를 에이전트에 붙인다면, 승인 판정과 대기와 선택지를 모두 서버의 실행 직전 한 곳에 두는 것부터 권합니다. 그리고 「항상 허용」 같은 지속 동의에는 그 동의가 기댄 규칙이 바뀌면 다시 묻는 조건을 함께 붙입니다.

어떤 문제를 해결하나요?

보안과 감사의 다른 글