ToolReplay의 기능
ToolReplay는 실제 도구를 호출하지 않고 기록된 AI 에이전트 도구 호출을 감사합니다. 트랜스크립트를 검사하여 세 가지 문제를 식별합니다. 비결정적 응답, 중복 호출, 에이전트가 선언한 권한 범위를 벗어난 호출입니다. 또한 트랜스크립트를 SHA-256 해시 체인으로 봉인하고 봉인된 파일이 수정되지 않았는지 확인할 수 있습니다.
ToolReplay는 실시간 호출을 수행하는 대신 기록된 응답을 재생하므로 감사가 안전하고, 부작용이 없으며, 재현 가능합니다. 원래 도구를 사용할 수 없는 경우에도 사용할 수 있습니다.
ToolReplay 설치 및 실행
ToolReplay에는 Python 3.11 이상이 필요하며 타사 런타임 종속성이 없습니다. 네트워크에 액세스하지 않습니다.
소스 체크아웃에서 다음과 같이 모듈을 직접 실행합니다.
python -m toolreplay version
콘솔 스크립트를 설치할 수도 있습니다.
pip install .
toolreplay version
두 방법 모두 설치된 버전을 출력합니다. 예를 들면 다음과 같습니다.
toolreplay 0.6.0
트랜스크립트 형식 이해
트랜스크립트는 JSON Lines 파일입니다. 비어 있지 않은 각 줄은 하나의 도구 호출을 나타내며 정확히 네 개의 필드를 포함해야 합니다.
index: 0에서 시작하여 정확히 1씩 증가하는 정수입니다.tool: 호출된 도구의 이름입니다.args: 도구 인수를 포함하는 객체입니다.response: 기록된 도구 응답을 포함하는 객체입니다.
유효한 레코드는 다음과 같습니다.
{"index": 4, "tool": "write_file", "args": {"path": "docs/intro.md", "text": "edited"}, "response": {"ok": true}}
파싱은 의도적으로 엄격합니다. 알 수 없는 필드, 누락된 필드, 잘못된 객체 유형, 정수가 아닌 인덱스, 순서가 잘못된 인덱스가 있으면 치명적 오류가 발생합니다. Python에서 bool이 int의 하위 클래스임에도 불구하고 불리언 값은 정수로 허용되지 않습니다.
ToolReplay는 표준 JSON을 사용하여 호출을 비교합니다. 도구 이름과 인수는 키를 정렬하고 불필요한 공백을 제거한 형태로 인코딩됩니다. 따라서 키 순서가 다른 인수 객체도 동일한 호출을 나타냅니다. ToolReplay가 결정성을 확인할 때 응답에도 동일한 표준 인코딩을 사용합니다.
재생 감사 실행
replay 명령은 트랜스크립트를 자체 기록 응답과 비교하여 검사합니다. 저장소에 포함된 문제가 있는 샘플을 실행해 보세요.
python -m toolreplay replay samples/session-dirty.jsonl
이 명령은 결정론적인 줄 단위 보고서를 생성합니다.
calls: 6
divergence: index 5
findings: 2
index 2: redundant-call: tool 'read_file' repeats the identical call at index 1 with no state change between them
index 5: non-determinism: tool 'search' returned a different response than the identical call at index 3
헤더는 위에서 아래 순서로 읽습니다. calls: 6은 유효한 레코드 6개가 파싱되었음을 의미합니다. divergence: index 5는 결정론적으로 다시 실행할 경우 기록 내용과 달라지는 최초 지점을 나타냅니다. findings: 2는 뒤따르는 발견 항목의 수를 셉니다.
발견 항목은 인덱스순으로 정렬한 다음 종류순으로 정렬되므로 동일한 입력을 반복 감사하면 바이트 단위로 동일한 출력이 생성됩니다.
비결정성
ToolReplay는 각 표준 호출의 첫 번째 응답을 기억합니다. 이후 동일한 호출에서 기록된 응답이 다르면 비결정성으로 보고합니다. 샘플에서 install에 대한 search 호출은 인덱스 3과 5에 나타나지만 응답에 기록된 검색 결과 수가 다릅니다. 따라서 인덱스 5가 분기 지점이 됩니다.
이는 에이전트에 중요합니다. 첫 번째 응답을 바탕으로 내린 결정이 두 번째 응답이 반환되었다면 달라질 수 있기 때문입니다. 이 발견 항목은 세션이 기록 내에서 안정적이지 않은 정보에 의존한다는 것을 나타냅니다.
중복 호출
두 호출 사이에 기록된 이벤트 중 상태를 변경할 수 있는 이벤트가 없을 때만 반복 호출을 중복 호출로 간주합니다. 기본 상태 변경 도구는 write_file, delete_file, create_file, move_file, run_command입니다. 동일한 호출 사이에 다른 호출이 하나라도 있으면 가능한 상태 변경으로 보수적으로 처리합니다.
샘플에서는 read_file이 인덱스 1과 2에서 docs/intro.md를 읽고 그 사이에 상태 변경이 없으므로 인덱스 2가 표시됩니다. write_file 호출 이후의 읽기는 중복으로 간주되지 않습니다.
에이전트 권한 확인
에이전트에 대한 권한 선언이 있다면 scope를 사용합니다.
python -m toolreplay scope samples/session-dirty.jsonl samples/scope.json
샘플 결과는 다음과 같습니다.
calls: 6
findings: 1
index 4: permission-overreach: tool 'write_file' is not in the declared scope for agent 'docs-reader'
재생과 범위 확인은 설계상 별도의 명령입니다. 재생은 세션을 세션 자체와 비교하며 트랜스크립트만 필요합니다. 범위 확인은 각 호출을 별도의 권한 파일과 비교합니다.
범위 파일은 에이전트 이름과 허용된 도구의 정확한 목록을 포함하는 하나의 JSON 객체입니다.
{"agent": "docs-reader", "allowed_tools": ["read_file", "list_dir", "search"]}
도구 이름은 대소문자를 구분하여 정확히 일치해야 합니다. 필드가 누락되었거나, 유형이 잘못되었거나, 문자열이 아닌 항목이 있으면 거부됩니다. 범위 확인은 이름을 기준으로 하며 인수를 검사하거나 허용된 도구가 허용된 경로를 대상으로 하는지 판단하지 않습니다.
트랜스크립트 봉인
seal 명령은 트랜스크립트를 해시 체인이 적용된 JSONL 파일로 변환합니다.
python -m toolreplay seal samples/session-dirty.jsonl > sealed.jsonl
각 봉인된 링크에는 원본 레코드 필드와 prev, digest가 포함됩니다. 첫 번째 링크는 이전 다이제스트로 64개의 0으로 이루어진 16진수 문자를 사용합니다. 이후의 모든 SHA-256 다이제스트에는 이전 다이제스트와 현재 레코드의 정규 바이트가 포함되며, 여기에는 인덱스, 도구, 인수, 응답이 포함됩니다.
각 링크는 앞선 링크에 의존하므로 이전 레코드를 변경하면 해당 레코드의 다이제스트와 이후 모든 다이제스트가 변경됩니다. 따라서 체인을 사용하면 편집 여부를 확인하고 처음 손상된 링크가 어디인지 식별할 수 있습니다.
봉인된 파일 검증
손상되지 않은 체인은 다음 명령으로 검증합니다.
python -m toolreplay verify sealed.jsonl
변경되지 않은 파일은 다음과 같이 보고합니다.
chain: intact
기록된 응답이나 체인에 포함된 다른 필드를 수정하면 검증 결과에 처음 손상된 링크와 예상 다이제스트 및 발견된 다이제스트가 표시됩니다.
chain: broken
first broken link: index 3
expected: 34d19a9dfb112baa07afa37994ddcbbebe2973674385f3942340653d08d205bd
found: 40aadf3b8156217f0b5b1d95010b6b79f82a26ccf5e780aed0f5f311f865028a
봉인은 봉인 후 발생한 편집을 감지하며, 순서, 인덱스, 도구, 인수, 응답의 변경도 포함됩니다. 이는 디지털 서명이 아니므로 파일을 편집할 수 있는 사람은 누구나 파일을 다시 봉인할 수 있습니다. 출처를 입증하려면 ToolReplay 외부에서 관리되는 키로 최종 다이제스트에 서명해야 합니다.
CI에서 ToolReplay 사용
ToolReplay의 종료 코드는 빌드 게이트에 적합합니다.
0: 감사 결과 문제가 없거나 체인이 손상되지 않았습니다.1: 발견 항목이 있거나 체인이 손상되었습니다.2: 잘못된 입력 또는 파일 누락과 같은 사용 오류가 발생했습니다.
CI 작업에서 재생 및 범위 검사를 직접 실행할 수 있습니다.
python -m toolreplay replay session.jsonl
python -m toolreplay scope session.jsonl scope.json
종료 코드 1은 도구가 감사를 완료했으며 보고할 항목을 찾았다는 의미입니다. 종료 코드 2는 감사를 실행할 수 없었다는 의미이므로, 자동화에서 잘못된 입력과 실제 발견 항목을 다르게 처리할 수 있습니다.
재현 가능한 비교를 위해 각 실행을 봉인하고 결과를 비교합니다.
python -m toolreplay seal run-a.jsonl > run-a.sealed.jsonl
python -m toolreplay seal run-b.jsonl > run-b.sealed.jsonl
git --no-pager diff --no-index run-a.sealed.jsonl run-b.sealed.jsonl
보고서에는 타임스탬프나 무작위 값이 포함되지 않으므로 동일한 입력은 동일한 출력을 생성합니다. 봉인된 두 실행의 첫 번째 차이점이 서로 일치하지 않기 시작한 위치를 보여 주며, 변경된 다이제스트는 이후 링크가 해당 차이에 의존함을 나타냅니다.
고급 사용법 및 실제 한계
- 트랜스크립트를 추가에 적합하게 유지하세요. JSON Lines를 사용하면 생성자가 호출마다 하나의 레코드를 추가할 수 있고 버전 관리에서 변경 사항을 쉽게 검토할 수 있습니다.
- 기록된 응답을 보존하세요. 재생은 실제 도구를 호출하지 않으므로 트랜스크립트에 표현된 문제만 감지할 수 있습니다.
- 중복을 보수적으로 해석하세요. 기본 변이 도구 목록은 고정되어 있으며, 해당 목록에 없는 사용자 지정 변이 도구는 중복 분석에서 읽기 전용으로 처리됩니다.
- 비교 경계를 기억하세요. 비결정성은 동일한 정규 호출이 반복될 때만 감지됩니다. 의미상 관련된 응답을 반환하는 서로 다른 호출은 동등성 평가 대상이 아닙니다.
- 권한과 대상 지정을 분리하세요. 범위 검사는 도구 이름만 확인합니다. 허용된 도구가 금지된 리소스에 사용되는 경우는 감지할 수 없습니다.
- 봉인은 무결성을 위해 사용하고 신원 확인에는 사용하지 마세요. 체인은 봉인 후 파일이 변경되었는지 보여 주지만 누가 체인을 생성했는지는 증명하지 않습니다.
테스트 모음 실행
이 프로젝트는 Python 표준 라이브러리의 unittest 프레임워크를 사용하며 서드 파티 테스트 의존성이 없습니다. 저장소 루트에서 다음을 실행하세요.
python -m unittest discover -s tests
테스트에서는 엄격한 트랜스크립트 구문 분석, 정규 인코딩, 해시 체인 생성 및 변조 감지, 재생 발견 항목, 범위 적용, CLI 동작, 종료 코드를 다룹니다.
결론
ToolReplay는 AI 에이전트 세션을 위한 집중적인 감사 작업 흐름을 제공합니다. 먼저 replay로 일관되지 않거나 낭비적인 동작을 찾고, scope로 선언된 도구 권한을 확인한 다음, seal과 verify를 함께 사용해 기록 후 변경 사항을 감지하세요. 엄격한 입력 검증과 결정론적 보고서를 제공하므로 디버깅, 검토, 버전 관리, CI 적용에 실용적입니다.
