* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
(폐쇄망 사내 에이전트 ⑤) 도구
에이전트의 능력은 곧 도구 목록이다. 에어의 Tool은 35종 — 파일(읽기·쓰기·검색·이동), 검색, Git, 엑셀, 문서(PDF·Word), 코드 분석, 셸, SSH, HTTP까지. 폐쇄망 사무 업무에서 실제로 반복되는 일을 기준으로 골랐다. 이번 편은 그중 제일 위험한 셸과, 도구 설계에서 배운 것들을 커밋 순서대로 푼다.
셸은 ‘셸 없이’ 실행한다
shell.execute의 첫 번째 안전장치는 역설적이게도 셸을 거치지 않는 것이다. 명령을 /bin/sh -c로 넘기면 파이프·리다이렉트·명령 치환($(...)) 같은 조합 공격면이 통째로 열린다. 대신 명령을 직접 프로세스로 실행하니 파이프도 글롭도 루프도 안 된다 — 불편하지만, 4B 모델이 만들어낸 문자열을 그대로 셸에 꽂는 것보다 백 배 낫다. 두 번째 장치가 화이트리스트다. 허용 명령만 실행되고, 목록 추가는 커밋으로 남는다.
commit fc2f0ea (2026-08-03)
config: shell 화이트리스트에 curl 추가 (18 → 19)
명령 하나 추가가 커밋 하나다. 과해 보여도, “왜 이 명령이 허용돼 있나”에 git blame으로 답할 수 있다는 뜻이다.
승인을 묻기 전에, 실행 가능부터 판정한다
실사용에서 이런 일이 있었다. 사용자가 승인 프롬프트에 ‘예’를 눌렀는데 그제서야 “shell 도구는 비활성화되어 있습니다”가 나온 것. 승인이라는 절차가 형식적 의례가 되는 순간이다. 그래서 나온 커밋이 이거다.
commit 85984e2 (2026-08-02)
승인을 묻기 전에 실행 불가를 판정한다 (AirTool.precheck)
실행될 수 없는 것이 확정된 호출에 승인을 묻는 것은
승인 절차를 형식적 의례로 만들고, 승인 화면을
"실행 직전의 마지막 관문"으로 신뢰할 수 없게 한다.
인자만 보고 알 수 있는 거부(비활성화·화이트리스트 위반·허용 밖 호스트)는 precheck가 승인 앞에서 걸러낸다. 디테일 두 개가 좋다. ① execute 안의 같은 검사는 그대로 남겨뒀다 — precheck는 ‘승인 앞당김’이지 유일한 방어선이 아니다. ② precheck가 예외를 던지면 삼키고 진행한다 — 앞당김 로직이 깨졌다고 정상 호출까지 잃으면 안 되니까. 안전장치를 이중으로 두되, 안전장치 자체의 고장이 서비스를 죽이지 않게 하는 설계다.
실측이 잡아낸 도구 버그 둘
도구는 머리로 설계하고 실데이터로 검증해야 한다. 다운로드 폴더 사본 214개로 ‘전부 정리해줘’를 시켜본 실측에서 두 개가 잡혔다.
- 빈 패턴이 전멸을 부른다 — 모델이
namePattern: ""를 넣자new Glob("")이 아무것도 매치하지 않아, 실재하는 xlsx 7개가 “일치하는 파일이 없습니다”로 나왔다. 빈 문자열을 ‘패턴 없음’으로 접어서 해결. - 파일 158개 = 승인 158번 — 셸 루프가 없으니 모델은 파일당 move 한 건씩, 왕복 158번을 해야 했다. 도구가 글롭을 직접 전개하게 바꿔
*→ 158개 이동이 승인 1회가 됐다. 승인 화면에 파일 목록 전체가 떠서, 셸 글롭과 달리 사용자가 결과를 예측할 필요도 없다.
tools/는 동결한다
코드로 — 다섯 겹의 실제 모습
말로 한 다섯 겹을 코드에서 확인하자. 먼저 precheck의 실제 시그니처 — 주석이 설계 의도를 그대로 말한다.
// packages/tool-api/src/AirTool.ts
/**
* 여기서 판정하지 않는다. execute의 검사는 그대로 남겨 둔다 —
* 이 훅은 사용자 경험을 위한 앞당김이지 유일한 방어선이 아니다.
*/
precheck?(args: ToolArguments, context: ToolExecutionContext): string | null;
그리고 셸 실행기의 보안 원칙. 파일 상단 주석에 박혀 있는 것을 요약하면 —
- 셸 인터폴레이션 금지 — 어떤 경우에도
sh -c·powershell -Command로 감싸지 않는다. command와 args가 그대로 argv가 된다. - command는 화이트리스트에 정확히 일치해야 하고, 경로 구분자(
/·\)가 섞이면 무조건 거부 —./mal류 우회 차단. - 환경변수도 화이트리스트만 전달(POSIX: PATH·HOME·LANG…) — 토큰이 든 env가 자식에게 새지 않는다.
거부문이 길을 가리켜야 한다
이 파일에서 제일 좋아하는 건 보안 코드가 아니라 거부 메시지 상수다. 사용성 검증에서, 화이트리스트 거부를 받은 4B 모델이 대안을 못 찾고 “df는 허용 안 되니 어떻게 할까요”라며 사용자에게 되묻는 게 관찰됐다. 정답 처리된 문항은 전부 파일 도구로 우회한 경우였고 — 그래서 거부문 자체가 그 길을 가리키게 했다.
const ALTERNATIVE_TOOLS =
"파일 찾기는 filesystem.search, 내용 검색은 search.text, "
+ "폴더 목록은 filesystem.list, 파일 읽기는 filesystem.read를 대신 쓰세요.";
6편의 plan 모드 거부문과 같은 원리다 — 거부는 벽이 아니라 표지판이어야 한다. 소형 모델일수록 오류·거부 메시지가 사실상의 프롬프트다.
✅ 확인
- 내 도구들의 거부·오류 메시지가 다음 행동을 가리키는지 읽어봤다.
- 셸류 도구가 인터폴레이션 없이 실행되는지, env가 새지 않는지 확인했다.
- 실행 불가가 확정된 호출이 승인 화면까지 오지 않는지(precheck) 테스트했다.
마지막으로 이 프로젝트의 별난 규칙 하나 — 도구 코드는 동결이고, 고치려면 건별로 사용자 승인을 받는다. 도구는 위험 등급·승인 화면·감사와 맞물린 안전 표면이라, ‘살짝 고친’ 것이 안전 속성을 바꿀 수 있어서다. 위의 두 수정도 전부 명시적으로 동결을 해제받고 들어갔다. 귀찮음이 곧 감사 가능성이다. 도구 설명을 어떻게 쓰느냐가 모델 성능을 가른다는 얘기는 도구 정의 편과 그대로 이어진다.
자주 묻는 질문 (FAQ)
셸 명령을 화이트리스트로만 막으면 충분한가요?
화이트리스트는 첫 겹입니다. 이 프로젝트는 ① 셸을 거치지 않는 직접 실행(파이프·치환 차단) ② 화이트리스트 ③ 실행 전 precheck ④ 사용자 승인 ⑤ 감사 기록의 다섯 겹으로 방어합니다. 한 겹이 뚫려도 다음 겹이 받칩니다.
도구를 많이 만들수록 좋은 것 아닌가요?
도구 정의는 매 요청 프롬프트에 실려 컨텍스트를 먹고(8편), 위험 표면도 함께 늘어납니다. 실제 업무에서 반복되는 작업 기준으로 고르고, 위험한 것은 승인·감사로 감싸는 편이 낫습니다.

답글 남기기