현재 소프트웨어 엔지니어링 분야에서 우리는 제품에 많은 AI 에이전트를 구축하고 있습니다. 그리고 제품에 AI 에이전트가 있다는 것은 제품을 살아있게 유지하는 방법입니다. 그렇죠? 세상은 그렇게 움직이고 있습니다. 모두가 AI 에이전트를 만드는 데 바쁘고 — 프롬프트를 조정하거나, 도구 호출을 제공하거나, 모델 선택과 매개변수에 집중하고 있지만 — 대부분의 개발자가 간과하는 한 가지 중요한 영역이 있습니다. 바로 도구 하네스(Tool harness)와 보안입니다. 프롬프트가 아닙니다. 모델도 아닙니다. 당신의 도주 주변을 감싸는 하네스 — 그것을 어떻게 설계하고, 제약하며, 에이전트가 실제로 무엇을 할 수 있는지 통제하는 방식입니다. 그리고 이것을 건너뛰면 보안과 신뢰성 측면에서 많은 비용을 치르게 될 것입니다.
도구 하네스란 무엇인가요?
AI 에이전트에게 도구를 제공할 때, 단순히 함수를 주는 것이 아닙니다. 경계를 주는 것입니다. 그것이 무엇에 접근할 수 있는지, 무엇에 접근할 수 없는지, 그리고 행동할 때 어떻게 행동해야 하는지에 대한 일련의 규칙을 주는 것입니다. 우리 대부분은 그렇게 생각하지 않습니다. 우리는 도구를 작성하고, 에이전트에 연결한 다음, 넘어갑니다. 하네스 — 즉 제약 조건, 접근 통제, 행동 가드레일(behavioral guardrails) — 은 프롬프트에 맡겨집니다. 그것이 실수입니다. 프롬프트는 무시될 수 있습니다. 조작될 수 있습니다. 무시될 수도 있습니다. 하네스는 코드 레벨, 실행 레벨, 아키텍처 레벨에서 존재해야 합니다. 그리고 올바르게 구축하는 방법은 다음과 같습니다. 세 가지 계층이 있습니다.
계층 1: 식별자 매개변수 제거 — 서버 측에 주입하기
첫 번째 계층은 접근 제어에 관한 것입니다. 그리고 그것은 도구 스키마(tool schema)에서 시작됩니다. 예를 들어, 당신이 투두 리스트를 만들고 있다고 가정해 봅시다.
AI 에이전트와 연동된 프롬프트(prompt)입니다. 이에게 list_tasks라는 도구(tool)를 제공한다고 가정해 봅시다. 스키마는 다음과 같습니다: { "name" : "list_tasks" , "parameters" : { "user_id" : "string" , "filters" : { "status" : "string" , "due_before" : "string" } } }. 괜찮아 보이죠? 하지만 그렇지 않습니다. user_id가 스키마에 포함되어 있기 때문에, 에이전트는 원하는 어떤 사용자 ID든 전달할 수 있습니다. 악의적인 프롬프트(malicious prompt), 혼란스러운 모델(confused model), 또는 프롬프트 주입(prompt injection) — 이 중 어느 것이든 여러분의 에이전트가 절대 접근해서는 안 되는 데이터를 가져오게 할 수 있습니다. 인증(authentication)이 없습니다. 권한 부여(authorization)도 없습니다. 해결책은 스키마에서 모든 식별자 매개변수(identity params)를 제거하는 것입니다. user_id, account_id, workspace_id, knowledge_base_id와 같은 것들은 누가 무엇을 볼지 범위를 정의합니다. 에이전트가 범위를 결정하게 두어서는 안 됩니다. 여러분이 결정해야 합니다. { "name" : "list_tasks" , "parameters" : { "filters" : { "status" : "string" , "due_before" : "string" } } }. 그리고 도구가 실행될 때, 인증된 세션(authenticated session)으로부터 직접 식별자를 주입하세요: async function list_tasks ( params : { filters : Filters }, session : Session ) { const userId = session . userId ; // 이 값은 에이전트가 아니라 여러분이 제어합니다 return db . tasks . findMany ({ where : { userId , ... params . filters , }, }); } 에이전트는 자신이 무엇을 필요로 하는지만 말합니다. 어떤 데이터가 건드려질지 결정하는 것은 여러분입니다. 이것이 바로 안전장치(harness)입니다. 💡 레이어 2: 코드 수준에서 행동 제약 조건 강제하기 (Enforce Behavioral Constraints at the Code Level) 두 번째 계층은 도구가 무엇에 접근할 수 있는지뿐만 아니라, 어떻게 작동해야 하는지에 관한 것입니다. 만약 Claude Code를 사용해 보셨다면, 다음과 같은 오류 메시지를 본 적이 있을 겁니다: "A file cannot be written before it has been read." (파일은 읽히기 전에 쓰여질 수 없습니다.)
이는 프롬프트 지시사항이 아닙니다. 이는 도구 자체에 내재된 하드 제약 조건입니다. Claude Code의 개발자들은 매우 인간적인 행동, 즉 파일을 열고, 읽고, 이해한 다음 편집하는 과정을 가져와 실행 수준에서 강제했습니다. 이것이 바로 우리가 우리만의 도구로 해야 할 일입니다. 예를 들어, update_task라는 도구가 있다면, 에이전트가 이를 임의로 호출하도록 두지 마십시오. 코드 수준에서 '읽기 우선(read-first)' 제약 조건을 강제하십시오: async function update_task ( params : UpdateTaskParams , session : Session ) { const lastRead = await cache . get ( task_read: ${ params . task_id } : ${ session . userId } ); if ( ! lastRead || Date . now () - lastRead > 60 _000 ) { throw new Error ( " Task must be read before it can be updated. Call get_task first. " ); } return db . tasks . update ({ where : { id : params . task_id }, data : params . updates , }); } 프롬프트에서도 언급할 수는 있지만, 이 검사는 코드에 존재해야 합니다. 모델이 놓치거나 무시할 수 있는 시스템 프롬프트가 아니라요. 실행 계층(execution layer)에 장치가 있어야 합니다. 🔒 레이어 3: 추론 에이전트를 사용한 사전 비행 검증 (Pre-flight Validation with a Reasoning Agent) 이것은 더 진보된 방식입니다. 아직 제 자체 제품에는 구현하지 못했지만, 작동할 것이라는 것을 압니다. 아이디어는 다음과 같습니다. 어떤 도구 호출이 실행되기 전에, 에이전트가 그 이유—왜 해당 도구를 호출하는지에 대한 짧은 설명—를 제시하도록 요구하는 것입니다. { "name" : "list_tasks" , "parameters" : { "reason" : "string" , "filters" : { "status" : "string" , "due_before" : "string" } } } 이는 에이전트가 행동하기 전에 생각하도록 강제합니다. 그것은 어쩌면
실제로 그 이유가 유효하지 않다는 것을 깨닫고 도구를 전혀 호출하지 않기로 결정합니다. 더 나아가서, 도구 이름(tool name), 이유(reason), 그리고 대화 컨텍스트(conversation context)를 받아 해당 호출이 실제로 정당한지 판단하는 가벼운 검증 에이전트(validation agent)를 실행할 수 있습니다: async function validateToolCall ( toolName : string , reason : string , context : string ) { const response = await llm . complete ({ model : " fast-small-model " , prompt : Tool requested: ${ toolName } Reason given: ${ reason } Conversation context: ${ context } Is this tool call justified? Reply YES or NO with a brief explanation. , }); return response . text . startsWith ( " YES " ); } 만약 검증 에이전트가 '아니요(no)'라고 말하면 — 도구는 실행되지 않습니다. 이는 환각된 도구 호출(hallucinated tool calls), 프롬프트 주입 시도(prompt injection attempts), 그리고 에이전트가 필요성보다는 습관적으로 도구를 호출하는 경우를 포착합니다. 🛡️ 마무리하며 우리는 오늘날 너무 많은 에이전트를 설계하고 있습니다. 그리고 빠르게 진행하고 있습니다. 하지만 하네스(harness) — 즉 보안, 제약 조건, 접근 통제(access controls) — 가 뒤처지고 있습니다. 최소한, 우리가 에이전트에게 주면 안 되는 것에 대한 접근 권한을 주고 있지 않은지 확실해야 합니다. 우리가 구축하는 도구들이 어떻게 사용되어야 하는지에 대한 의견을 갖도록 해야 합니다. 프롬프트에만 존재하는 것이 아니라 아키텍처 수준에서 존재하는 가드레일(guardrails)이 있어야 합니다. 식별자 매개변수(identity params)를 제거하세요. 코드에서 행동 제약 조건(behavioral constraints)을 강제하세요. 실행 전에 추론 검토 지점(reasoning checkpoint)을 추가하세요. 이 세 가지 계층은 단순히 에이전트를 더 안전하게 만드는 것 이상을 할 것입니다.
이것은 더 신뢰할 수 있고, 예측 가능하며, 문제가 발생했을 때 디버깅하기 훨씬 쉬워질 것입니다. 그리고 저를 믿으세요. — 무언가 잘못될 겁니다. 중요한 것은 여러분의 테스트 환경(harness)이 그것을 감당할 준비가 되어 있었는지 여부입니다. 이 글이 유익하셨기를 바라며, 더 많은 기술 콘텐츠는 제 소셜 미디어를 팔로우해주세요. 다음 블로그에서 뵙겠습니다 👋🏻