dev.to

The Agent Said It Worked. I Asked the Kernel.

에이전트가 작동한다고 했습니다. 커널에 물어봤습니다

AI 에이전트가 작성한 코드의 “성공” 보고를 종료 상태나 테스트 결과만으로 믿지 않고, 저장된 파일·시스템 콜·eBPF·패킷 캡처·CPU 샘플로 교차 검증하는 실험입니다. 8가지 결함을 의도적으로 심은 백업 클라이언트를 통해 결과 상태와 실행 과정을 독립적으로 관찰해야 하는 이유를 보여줍니다.

AI 요약

AI 에이전트가 “작동합니다”라고 보고한 코드를 어떻게 검증할 것인지 다루는 실험입니다. 글쓴이는 에이전트의 로그나 테스트 결과를 그대로 신뢰하는 대신, 프로그램 바깥에서 관찰할 수 있는 독립적인 증거를 수집했습니다. 실험의 출발점은 Hemapriya Kanagala의 글 “What Happens When AI Outgrows the Tests We Use to Measure It?”입니다. 벤치마크가 포화되고 참조 답안과 테스트 조건이 복잡해질수록 “92%라는 수치가 무엇을 기준으로 한 것인가”라는 질문이 중요해진다는 문제의식을, 작은 백업 프로그램에 적용했습니다.

■ 먼저 검증한 것은 계측기입니다

에이전트가 만든 코드를 평가하기 전에, 글쓴이는 8가지 동작을 의도적으로 심은 native C 백업 클라이언트를 만들었습니다. Python receiver는 loopback TCP로 연결을 받고, 클라이언트는 결정적인 512 KiB 파일을 읽어 SHA-256 digest를 계산합니다. 업로드 전에 receiver가 같은 콘텐츠를 보유했는지 확인하며, receiver는 들어온 데이터를 검증한 뒤에만 저장 파일을 교체합니다. 별도의 verifier는 저장된 파일의 digest를 원본과 비교합니다.

각 시나리오는 동일한 receiver 상태에서 두 번 실행합니다. 정상 동작에서는 첫 실행이 업로드를 수행하고, 두 번째 실행에서는 콘텐츠가 바뀌지 않았음을 인식해 업로드 payload를 보내지 않아야 합니다. 실험에서 확인하려는 계약은 저장된 콘텐츠가 원본과 일치해야 하고, 변경되지 않은 두 번째 실행에서는 업로드 payload가 없어야 하며, 설정된 receiver에만 연결해야 하고, 첫 전송이 중단되더라도 3회 업로드 시도 안에서 복구해야 한다는 것입니다. 광고한 digest와 실제 payload가 다르면 receiver는 저장 파일을 교체하기 전에 거부해야 합니다.

관찰에는 여러 계층을 사용합니다. strace는 파일 읽기와 연결 같은 system call, 반환값, 파일 디스크립터, 호출 시간과 duration을 기록합니다. 여기서 세는 파일 읽기 바이트는 물리 디스크 접근량이 아니라 논리적인 read 결과이므로, 운영체제가 메모리에서 처리했을 가능성은 별도로 남습니다. eBPF와 bpftrace는 system call, tracepoint, 함수 진입 같은 지점의 이벤트를 셉니다. uprobe는 실행 파일 안의 특정 함수 진입을 관찰하며, 실험에서는 hash_file 호출 횟수를 세었습니다. tcpdump는 두 fixture 포트 사이의 loopback 패킷을 저장하고, receiver payload count는 애플리케이션이 실제로 소비한 바이트를 기록합니다.

이 수치들은 서로 대체할 수 없습니다. send가 성공해도 데이터가 receiver 애플리케이션에 소비되기 전에 local kernel에 머물 수 있고, 패킷에는 header와 retry, buffering의 영향이 포함될 수 있습니다. 함수 진입 횟수 역시 함수가 올바른 값을 반환했다는 뜻이 아닙니다. 따라서 실행 과정, 네트워크 활동, 최종 저장 상태를 각각의 사각지점과 함께 요구사항에 대조해야 합니다.

■ 8가지 결함으로 확인한 독립 증거

false-success 변형은 완료 이벤트를 출력하고 종료 상태 0으로 끝나지만 파일을 전혀 업로드하지 않습니다. 독립 verifier는 백업 파일이 없음을 확인하고, eBPF에서는 client connection이 없으며, 필터링한 packet capture에도 패킷이 0개로 나타납니다. 종료 상태만 검사하는 테스트라면 이 구현을 통과시킬 수 있지만, 결과 파일을 검사하면 즉시 실패로 판정할 수 있습니다. 이 사례에서는 CPU probe까지 사용할 필요 없이 파일 비교가 가장 단순하고 강한 검사가 됩니다.

hash-storm 변형은 최종 백업의 digest 자체는 올바르게 만들지만, 실행할 때마다 원본 전체를 256번 해시합니다. 두 번째 실행에서 524,288바이트 파일을 읽은 논리적 원본 바이트는 134,217,728바이트였고, 이는 정확히 256배입니다. 별도의 eBPF 실행에서는 두 번째 호출에 hash_file 진입 256회, 연결 1회, read 바이트 134,233,569, send 바이트 75, 스케줄링 전환 4회가 기록됐습니다. eBPF read 수치는 모든 client descriptor의 성공한 read를 포함하고, system-call 요약은 원본 파일 read만 선택하므로 두 read 수치가 다릅니다. send 75바이트 역시 업로드 payload가 아니라 digest 확인 요청을 포함합니다. AMD Instruction-Based Sampling(IBS)에서는 첫 실행에 233개의 CPU sample이 수집됐고 손실 sample은 0개였으며, 대부분의 inclusive sample weight가 hash_file 아래에 놓였습니다. 이 결과는 반복 작업의 위치를 보여주지만, 수정된 구현이 얼마나 빨라지는지를 측정한 benchmark는 아닙니다.

unexpected-egress 변형은 정상 백업 전에 두 번째 local endpoint로 불필요한 연결을 만듭니다. 파일은 정상적으로 검증되지만, baseline의 연결 수가 첫 실행 2회와 두 번째 실행 1회인 반면 변형은 각각 3회와 2회입니다. 연결 횟수만으로는 문제가 확정되지 않으며, socket trace와 패킷 주소, 두 번째 endpoint의 기록을 함께 봐야 목적지 제한 위반을 판단할 수 있습니다.

나머지 사례에는 변경되지 않은 파일을 다시 업로드하는 redundant upload, 실제 작업 전에 의도적으로 200ms를 busy-wait하는 busy wait, 잘못된 payload를 보내 receiver가 실행마다 3회 거부하게 하는 corrupt upload, 첫 전송에서 65,536바이트를 받은 뒤 연결을 끊어도 처음부터 다시 전송해 성공하는 retry가 포함됩니다. retry 실행에서 receiver가 소비한 총 payload는 589,824바이트로, 완전한 524,288바이트와 중단된 첫 전송의 65,536바이트를 합한 값입니다. 이는 제한된 retry 복구를 보여주지만 partial-transfer resume을 의미하지는 않습니다.

■ 계측 결과의 한계와 재현 절차

실험은 AMD Threadripper PRO 5975WX 기반 Linux 환경에서 수행했으며, perf를 통해 AMD IBS를 수집했습니다. CPU call-stack profile과 sample weight가 반영된 assembly를 저장하지만 모든 instruction, register, 중간값을 기록하는 것은 아닙니다. tracing 자체가 실행에 영향을 주므로 수집된 CPU accounting은 collector overhead를 포함하며, 이 실행 결과를 통제된 성능 benchmark로 해석할 수 없습니다. 짧은 실행은 CPU sampling에서 누락될 수 있고, packet capture의 drop counter가 0이라는 사실도 모든 관찰 범위를 보장하지 않습니다. 서로 다른 clock을 사용하는 client claim, receiver event, verifier check를 하나의 timeline으로 맞출 때도 주의해야 합니다.

재현 가능한 소스와 저장된 증거는 GitHub의 pinned revision에서 가져옵니다. `git clone https://github.com/copyleftdev/execution-evidence-lab.git observability-lab` 뒤에 `git checkout --detach e61aadee4a68e6a1d5b54a00387dcd325b5d5e51`을 실행하고, `make`, `make test`, `python3 -m instrument run --all` 순서로 기본 실행을 수행합니다. 필요한 구성 요소는 Linux, Python 3.10 이상, C compiler, Make, OpenSSL development library이며, 선택한 collector에 따라 strace, bpftrace, perf 등의 도구와 권한이 필요합니다. `python3 -m instrument doctor`로 사용 가능한 collector를 확인할 수 있고, 예를 들어 `python3 -m instrument run hash-storm --collector strace`로 특정 사례를 실행할 수 있습니다. 권한이 필요한 실험은 신뢰할 수 있는 local lab에서 `sudo python3 -m instrument run --all --collector bpftrace --packets required`처럼 수행합니다. tracing 없이 실행하려면 `python3 -m instrument run --all --collector none --packets off`를 사용합니다.

runner가 성공했다는 메시지는 모든 백업이 올바르다는 뜻이 아닙니다. 의도적으로 실패하도록 만든 시나리오까지 실행되었다는 의미일 수 있으므로, 각 시나리오의 독립적인 verification 결과를 직접 확인해야 합니다. 개발 중에는 CPU 보고서 parser가 `Total Lost Samples` 안의 `Samples` 문자열을 잘못 읽어 실제 raw profile에 수백 개의 sample이 있는데도 0개로 표시한 문제도 발견했습니다. raw artifact를 확인한 뒤 parser를 수정했고, 회귀 테스트가 실제 sample 수와 lost sample 수를 구분하도록 했습니다.

■ 에이전트 평가로 확장할 때의 원칙

이 실험은 특정 AI model의 coding ability를 측정하는 benchmark가 아닙니다. 8가지 동작은 계측기가 알려진 문제를 감지하는지 확인하기 위해 심은 사례이며, AI 에이전트가 이런 결함을 얼마나 자주 만든다는 증거가 아닙니다. 또한 Linux local program만 관찰하므로 remote model provider 내부의 계산은 볼 수 없습니다. TLS, compression, multi-file snapshot, 전송 중 source mutation, 별도 restore 명령, power loss 이후의 생존성도 다루지 않습니다. 관찰자 역시 같은 host에서 같은 프로젝트의 일부로 만들어졌으므로 third-party audit도 아닙니다.

다음 실험에서는 에이전트에게 정상 구현을 제공하고, integrity·recovery behavior·destination restriction을 보존하면서 반복 백업을 빠르게 개선하도록 요청할 계획입니다. 그 전에 요구사항, fixture, resource limit, grading criteria를 고정하고, 독립 검사를 에이전트가 수정할 수 없는 공간에 둬야 합니다. 에이전트의 patch, executable identity, evaluation setup도 보존해야 합니다. 성능은 반복적인 비계측 실행으로 측정하고, tracing은 차이가 발생한 이유를 조사하는 데 사용해야 합니다. 이 방식에서 올바른 파일은 필요조건이며, 복구 동작과 자원 사용, 연결 대상 같은 추가 속성이 구현을 신뢰할 수 있는지 판단하는 증거가 됩니다.

원문: dev.to / 번역·요약: Trawling