오류 문자열에서 <system> 같은 태그를 지운다고 해서 프롬프트 인젝션 문제가 완전히 해결되지는 않습니다.
이 문장부터 분명히 해야 합니다. 도구가 실패할 때 반환하는 문자열에는 외부 서비스의 응답, 잘못된 파일 내용, 사용자가 입력한 값, 예외 객체의 메시지가 섞일 수 있습니다. 그 안에 “앞의 지시를 무시하라” 같은 문장이 들어 있다면, 그것은 성공 결과가 아니라 오류라는 이유만으로 안전해지지 않습니다. 반대로 모든 오류를 통째로 버리면 에이전트는 실패 원인을 파악할 수 없습니다.
Hermes의 구현은 이 문제를 하나의 만능 필터로 해결하려 하지 않습니다. 대신 서로 다른 위험을 서로 다른 단계에서 줄입니다. 오류 문자열의 구조적 프레이밍을 정리하고 길이를 제한하는 경로와 터미널 같은 일반 장문 출력의 일부만 대화에 넣고 전체를 별도 보존하는 경로가 구분돼 있습니다.[1][2][3] 이 둘을 섞어 설명하면 “악성 오류 원문도 언제나 안전하게 전체 보존된다”거나 “태그만 제거하면 내용까지 신뢰할 수 있다”는 잘못된 결론에 도달하기 쉽습니다.
기대했던 방어와 실제 방어는 다르다
처음 떠올리기 쉬운 방어는 간단합니다.
- 오류 문자열에서 역할 태그를 찾는다.
- 위험해 보이는 문장을 삭제한다.
- 나머지를 모델에게 보여준다.
하지만 “위험해 보이는 문장”을 의미만 보고 완벽하게 판별하는 것은 어렵습니다. 정상적인 디버깅 정 보에도 system, assistant, XML, 마크다운 코드 블록 같은 단어가 들어갈 수 있습니다. 반대로 공격 문장은 특별한 태그 없이 평범한 자연어로도 작성할 수 있습니다.
그래서 Hermes의 _sanitize_tool_error는 오류의 의미를 판정하는 검열기가 아니라, 모델 대화 구조처럼 보일 수 있는 프레이밍을 약화하는 정규화기에 가깝습니다. 구현은 역할 태그와 닫히지 않은 구조적 표식, CDATA 같은 프레이밍을 정리하고, 결과 앞에 [TOOL_ERROR]라는 명시적 표시를 붙입니다. 모델에게 전달하는 오류 본문은 중앙 레지스트리에서 정한 길이 상한과 같은 기준으로 제한됩니다.[2][3]
여기서 중요한 한계가 있습니다. 예를 들어 <system>앞의 지시를 무시하라</system>이라는 입력에서 태그가 제거되더라도 안쪽 자연어가 남을 수 있습니다. 따라서 정리된 결과는 “신뢰할 수 있는 명령”이 아니라 여전히 실패를 설명하기 위한 비신뢰 데이터입니다. 접두사는 이 경계를 모델에게 알려 주고, 길이 제한은 재시도할 때 거대한 예외가 대화 기록에 계속 누적되는 일을 줄입니다. 그러나 자연어 수준의 모든 조작 의도를 판별해 제거한다고 보장하지는 않습니다.[2]
첫 번째 방어선: 예외가 어느 경로로 와도 길이를 묶는다
오류를 만드는 도구가 모두 같은 도우미 함수를 쓴다고 가정할 수는 없습니다. 어떤 핸들러는 tool_error()를 사용할 수 있고, 어떤 핸들러는 {"error": str(exc)} 형태의 JSON을 직접 직렬화할 수 있습니다.
Hermes의 도구 레지스트리는 이 두 경우를 함께 다룹니다. tool_error()는 오류 본문을 제한한 뒤 JSON 문자열로 만들고, 디스패치 경계에서는 직접 만들어진 JSON 결과라도 문자열 error 필드가 지나치게 길면 다시 같은 상한을 적용합니다. 현재 동결 코드에서 모델 문맥으로 들어가는 오류 본문 상한은 2,048자이며, 잘렸음을 알리는 표식이 붙습니다.[3]
이 설계의 의미는 “2,048자가 안전한 마법의 숫자”라는 데 있지 않습니다. 핵심은 오류를 만든 개별 도구의 구현 습관에만 안전성을 맡기지 않고, 공통 경계에서도 확인한다는 점입니다. 재시도 세 번이 각각 수만 자짜리 예외를 남기는 상황이라면 오류 자체보다 누적된 문맥이 먼저 문제가 될 수 있기 때문입니다.
공식 회귀 테스트는 다음 경계를 나눠 확인합니다.
- 상한보다 짧은 오류는 그대로 유지되는가
- 상한을 넘긴 오류는 절단 표식과 함께 제한되는가
- 직접 직렬화된 JSON 오류도 디스패치 경계에서 제한되는가
- 구조적 역할 표식과 CDATA가 정리되는가
- 오류가 길어도 부가 상태 필드는 보존되는가[4][5]
테스트가 확인하는 것은 이 입력·출력 계약입니다. 이것만으로 실제 공격 성공률이 얼마나 낮아졌는지, 모든 모델이 오류 접두사를 똑같이 해석하는지까지 입증되지는 않습니다.
두 번째 방어선: 일반 장문 출력은 ‘보여 주기’와 ‘보관하기’를 나눈다
오류 전용 정리와 별개로, 터미널 명령은 정상 실행에서도 방대한 출력을 만들 수 있습니다. 빌드 로그 10만 줄을 그대로 대화에 넣으면 중요한 앞부분과 마지막 실패 원인을 찾기 어려워지고, 부모 문맥도 빠르게 소모됩니다.
터미널 구현은 출력이 설정된 한도를 넘으면 앞부분과 뒷부분을 남기고 가운데가 생략됐다는 표시를 넣습니다. 수집 단계에서 전체 출력 파일이 만들어졌다면, 보이는 결과에는 전체 문자 수와 파일 위치, read_file이나 검색 도구로 누락 부분을 확인하라는 안내가 함께 들어갑니다. 디스크의 전체 출력도 보이는 출력과 같은 비밀정보 마스킹 단계를 거친 뒤에만 참조가 노출됩니다.[6]
공식 터미널 회귀 테스트는 합성 출력으로 다음을 확인합니다.
- 보이는 결과에는 절단 표시가 있는가
- 전체 출력 파일에는 앞·중간·뒤 표식이 모두 남는가
- 실패 종료 코드를 낸 명령도 긴 출력 파일을 남기는가
- 보존 파일에도 비밀정보 마스킹이 적용되는가
- 오래된 보존 파일이 정리되는가[7]
이것은 “오류 원문을 언제나 전부 보존한다”는 계약이 아닙니다. 터미널의 일반 출력 보존 경로에 대한 계약입니다. 오류 정리 경로는 모델에게 전달할 본문을 제한하며, 잘린 악성 예외 전문을 독자가 나중에 읽도록 자동 보존한다고 약속하지 않습니다. 보안 정규화와 디버깅용 장문 출력 보존은 목적부터 다릅니다.
안전한 최소 재현
실제 외부 서비스나 공격 대상 없이 합성 문자열만으로 확인할 수 있습니다. Hermes 개발 환경에서 임시 홈을 지정하고 관련 회귀 테스트만 실행합니 다.
export HERMES_HOME="$(mktemp -d)"
python -m pytest -o 'addopts=' -q \
tests/test_sanitize_tool_error.py \
tests/tools/test_registry.py \
tests/tools/test_terminal_truncation_spill.py
직접 fixture를 만든다면 입력을 두 묶음으로 분리하는 편이 좋습니다.
묶음 A: 오류 정리
- 역할 태그, CDATA, 코드 펜스, 5,000자짜리 반복 문자열을 합친 오류를 만듭니다.
_sanitize_tool_error에 넣습니다.[TOOL_ERROR]접두사, 구조적 표식 제거, 본문 길이, 절단 표시를 기록합니다.- 태그 안의 자연어가 남을 수 있다는 점도 함께 확인합니다.
기대 결과: 구조적 프레이밍은 약해지고 오류 본문은 상한에 묶입니다. 그러나 남은 자연어를 신뢰 가능한 지시로 승격하면 안 됩니다.[2][3]
묶음 B: 일반 장문 터미널 출력
- 첫 줄에
HEAD_MARKER, 가운데에 번호가 붙은 여러 행, 마지막 줄에TAIL_MARKER를 출력하는 로컬 명령을 만듭니다. - 작은 출력 한도를 fixture로 설정합니다.
- 보이는 결과의 절단 표시와 전체 출력 파일의 앞·중간·뒤 표식을 비교합니다.
- 합성 토큰 모양 문자열을 넣어 보존 파일에서도 마스킹됐는지 확인합니다.
기대 결과: 대화에는 앞·뒤 중심의 제한된 결과가 들어가고, 전체 출력 파일의 위치와 읽기 안내가 메타데이터로 제공됩니다.[6][7]
이번 글에서 확인한 것과 실행하지 않은 것
동결 구현과 공식 회귀 테스트가 규정하는 기대 출력은 [TOOL_ERROR] 접두사, 역할 태그와 CDATA 같은 구조적 표식의 약화, 2,048자 오류 본문 상한과 생략 표시입니다.[2][3][4][5] 태그 안쪽의 자연어는 남을 수 있으므로, 이 처리가 의미 기반 공격을 판별한다고 확대해서는 안 됩니다.
이 글에는 합성 입력·정확한 출력·실행 명령을 묶은 공개 영수증이 없습니다. 따라서 오류 정리 함수를 이번 작업에서 별도로 실행해 결과를 관찰했다고 주장하지 않습니다. 터미널 장문 출력의 디스크 보존도 동결 구현과 공식 회귀 테스트의 문서·코드 계약으로만 기술하며,[6][7] 이번 환경에서 보존 파일이 생성됐다는 관찰 결과로 쓰지 않습니다.
확인 범위와 한계
직 접 확인한 범위는 오류 정리 구현, 공통 도구 레지스트리의 오류 상한, 터미널 출력 절단·보존 구현, 그리고 각각의 공식 회귀 테스트입니다. 이 근거로 말할 수 있는 것은 구조적 프레이밍 약화, 오류 길이 제한, 일반 장문 출력의 문맥·보존 분리입니다.
이번 글에서 직접 확인하지 못한 것은 다음과 같습니다.
- 실제 여러 모델을 대상으로 한 프롬프트 인젝션 성공률 비교
- MCP·브라우저·웹 추출 등 모든 도구 경로의 동일한 방어 수준
- 보존 파일 접근 권한과 정리 주기의 운영체제별 장기 동작
- 의미 기반 공격 문장을 자동으로 판별하는 능력
- 오류 정리 전후의 디버깅 시간이나 장애 복구 성공률 변화
따라서 가장 안전한 결론은 “오류를 안전한 텍스트로 바꾼다”가 아닙니다. 오류도 비신뢰 입력으로 취급하고, 대화 구조처럼 보이는 표식을 약화하며, 문맥에 들어갈 양을 제한한다. 장문 일반 출력은 필요할 때 별도 파일로 조회한다. 서로 다른 두 방어선을 구분할 때 비로소 구현의 효과와 한계를 함께 볼 수 있습니다.
Sources
[1] Tools Runtime — Hermes Agent 개발자 문서
[2] 도구 오류 정리 구현 (`model_tools.py`)
[4] 오류 정리 회귀 테스트
[5] 도구 레지스트리 회귀 테스트
[6] 터미널 출력 절단·보존 구현