ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • AI 에이전트가 뭘 했는지 추적하기 — 경량 감사 로그 구축기
    IT 2026. 4. 23. 21:00
    AI 에이전트가 뭘 했는지 추적하기 — 경량 감사 로그 구축기

    전권을 줬더니 생긴 문제

    이전 글에서 AI 코딩 에이전트의 권한을 화이트리스트에서 블랙리스트로 전환했습니다. 191개 허용 목록 대신 Bash(*) 하나로 전부 열고, 위험 명령만 차단하는 방식입니다.

    작업 흐름은 쾌적해졌지만, 하나 빠진 것이 있었습니다.

    "에이전트가 오늘 뭘 했지?"

    화이트리스트 시절에는 에이전트가 새로운 명령을 실행할 때마다 "허용하시겠습니까?" 프롬프트가 떴습니다. 불편했지만 무엇이 실행되는지 알고 있었습니다. 블랙리스트로 전환하니 에이전트가 조용히 수백 개의 명령을 실행하고, 사용자는 최종 결과만 봅니다.

    문제 없을 때는 괜찮지만, 뭔가 잘못됐을 때 원인을 추적할 방법이 없습니다. 파일이 사라졌는데 언제 삭제된 건지, 어떤 명령이 예상과 다르게 동작했는지. AI 에이전트 세션 로그가 있긴 하지만, 대화 전체가 기록된 거대한 JSONL 파일에서 특정 명령을 찾기란 건초 더미에서 바늘 찾기입니다.

    설계 원칙 — 가볍게, 비차단으로, 쿼리 가능하게

    팀 환경이라면 ELK 스택이나 Datadog 같은 중앙 로깅 시스템을 쓰겠지만, 개인 개발 환경에서는 과잉입니다. 세 가지 원칙을 세웠습니다.

    1. 가볍게 — 파일 하나에 append. 외부 의존성 없음. 로깅 모듈 50줄 이내.
    2. 비차단 — 로깅이 실패해도 에이전트 작업에 영향 없음. try/except로 조용히 무시.
    3. 쿼리 가능하게 — JSONL 형식이면 jq 한 줄로 원하는 이벤트를 필터링할 수 있음.

    구현 — PostToolUse Hook에 로깅 한 줄 추가

    Hook 시스템 복습

    AI 코딩 도구에는 에이전트가 도구를 실행할 때 외부 스크립트를 호출하는 Hook 시스템이 있습니다. 시점별로 나뉩니다:

    시점 역할 예시
    PreToolUse 실행 — 차단 가능 위험 명령 정규식 검사
    PostToolUse 실행 — 결과 관찰 파이프라인 상태 추적, 감사 로그
    Stop 세션 종료 시 미완료 작업 점검

    감사 로그는 PostToolUse에 넣는 것이 자연스럽습니다. 실행이 끝난 후 "실제로 일어난 사실"을 기록하기 때문입니다. PreToolUse에서 기록하면 "실행하려 했지만 실패한 것"과 "실제로 실행된 것"을 구분할 수 없습니다.

    단, 차단된 명령은 PreToolUse에서 기록합니다. 차단됐다는 것 자체가 감사에서 가장 가치 있는 정보이기 때문입니다.

    로그 스키마

    한 줄에 하나의 이벤트를 기록합니다.

    {
      "ts": "2026-04-11T14:30:00+09:00",
      "tool": "Bash",
      "hook": "PostToolUse",
      "session": 12345,
      "cmd": "git commit -m 'add feature'",
      "exit_code": 0,
      "tags": ["git", "commit"],
      "pipeline": "calendar_sync"
    }
    필드 의미
    ts 타임스탬프 (ISO 8601, 시간대 포함)
    tool 어떤 도구가 실행됐나 — Bash, Edit 등
    hook 어떤 시점에 기록됐나 — PostToolUse 또는 PreToolUse(차단)
    session 어떤 세션에서 실행됐나
    cmd 실행된 명령 또는 편집된 파일 경로
    exit_code 종료 코드 (0이면 성공)
    tags 자동 분류 태그
    pipeline 매칭된 자동화 파이프라인 이름 (없으면 null)

    태그 자동 분류

    모든 명령에 수동으로 태그를 달 수는 없습니다. 정규식 9개로 자동 분류합니다.

    TAG_RULES = [
        (r"git\s+commit",        ["git", "commit"]),
        (r"git\s+push",          ["git", "push"]),
        (r"gh\s+(pr|issue)",     ["github", "api"]),
        (r"(curl|wget)\s",       ["network"]),
        (r"(pip|npm)\s+install", ["install"]),
        (r"\brm\s",              ["delete"]),
        (r"\bsudo\s",            ["sudo"]),
        ...
    ]

    git commit -m "fix bug"이 실행되면 자동으로 ["git", "commit"] 태그가 붙습니다. 나중에 "오늘 git 관련 작업만 보고 싶다"면:

    jq 'select(.tags[] == "git")' audit.jsonl

    한 줄이면 됩니다.

    차단 이벤트 기록

    이전 글에서 구축한 PreToolUse safety Hook이 위험 명령을 차단하면, 그 사실도 감사 로그에 남깁니다.

    {
      "ts": "2026-04-11T15:00:00+09:00",
      "tool": "Bash",
      "hook": "PreToolUse",
      "cmd": "git push --force origin main",
      "tags": ["blocked"],
      "reason": "git push --force 차단"
    }

    "무엇을 막았는지"를 기록하는 이유는 두 가지입니다:

    1. 에이전트의 의도 파악 — 에이전트가 위험 명령을 시도했다면, 그 컨텍스트에서 왜 그런 판단을 했는지 분석할 수 있습니다.
    2. 차단 규칙 검증 — 정당한 명령이 차단됐다면 (false positive), 차단 패턴을 수정해야 합니다.

    파이프라인 매칭

    여러 자동화 파이프라인을 운영하고 있다면, 각 명령이 어떤 파이프라인의 일부인지 아는 것이 중요합니다. 기존 PostToolUse 디스패처가 명령을 각 파이프라인 추적 함수에 넘기는 구조였으므로, 여기에 매칭된 파이프라인 이름만 캡처하면 됩니다.

    # 기존: 매칭만 하고 이름은 버림
    for tracker in [sync.track, review.track, ...]:
        if tracker(cmd): break
    
    # 변경: 이름도 캡처
    matched = None
    for name, tracker in [("sync", sync.track), ("review", review.track), ...]:
        if tracker(cmd):
            matched = name
            break
    
    audit.log_event(..., pipeline=matched)

    이렇게 하면 나중에 "배포 파이프라인이 오늘 몇 번 실행됐나?" 같은 질문에 답할 수 있습니다.

    jq -r '.pipeline // "none"' audit.jsonl | sort | uniq -c | sort -rn

    비차단 설계 — 로깅이 에이전트를 막으면 안 된다

    감사 로그에서 가장 중요한 설계 결정은 실패 시 조용히 무시하는 것이었습니다.

    try:
        with open(AUDIT_LOG, "a") as f:
            f.write(json.dumps(entry) + "\n")
    except Exception:
        pass  # 로깅 실패는 무시

    디스크가 꽉 차거나, 권한 문제가 생기거나, 파일 시스템에 이상이 생겨도 에이전트는 정상적으로 작업을 계속합니다. 로깅은 관찰(observability)이지 제어(control)가 아닙니다. 제어는 PreToolUse Hook과 deny 목록이 담당합니다.

    반면 PreToolUse의 safety 검증은 반대입니다 — 실패하면 차단합니다. 역할이 다르기 때문입니다:

    Hook 역할 실패 시 동작
    PreToolUse (safety) 제어 차단 (exit 2)
    PostToolUse (audit) 관찰 무시 (pass)

    실제 로그는 이렇게 쌓인다

    구축 후 몇 시간 사용한 결과, 이런 로그가 쌓였습니다.

    # 파일 편집 기록
    {"tool":"Edit","cmd":"/home/user/project/config.yaml","tags":[],"pipeline":null}
    
    # 패키지 설치
    {"tool":"Bash","cmd":"pip install requests","tags":["install"],"pipeline":null}
    
    # 자동화 파이프라인 내 git 작업
    {"tool":"Bash","cmd":"git commit -m 'sync data'","exit_code":0,"tags":["git","commit"],"pipeline":"data_sync"}
    
    # 차단된 위험 명령
    {"tool":"Bash","hook":"PreToolUse","cmd":"git push --force origin main","tags":["blocked"]}

    하루에 100~300줄 정도 쌓이고, 용량은 월 1.5MB 수준입니다. 로그 로테이션이 필요 없는 수준입니다.

    Harness Engineering 체계 완성

    이 시리즈에서 다룬 Harness Engineering의 구성 요소를 정리하면:

    영역 구성 요소 역할
    Data Plane Context Engineering 맥락 소스 우선순위, 적재량 관리
    Skill 절차 정의, 파이프라인 표준화
    Control Plane Hook / Guardrail PreToolUse 안전 검증, PostToolUse 파이프라인 추적
    HITL Policy 블랙리스트 방식 + 이중 방어
    Governance & Audit JSONL 감사 로그, 태그 자동 분류

    Governance & Audit는 Harness의 마지막 조각입니다. 다른 구성 요소들이 "에이전트가 올바르게 행동하도록" 만든다면, 감사 로그는 "에이전트가 올바르게 행동했는지 검증"합니다.

    이 체계에서 가장 중요한 것은 각 구성 요소의 역할이 분리되어 있다는 점입니다:

    • Context Engineering이 좋은 입력을 만들고
    • Skill이 일관된 절차를 보장하고
    • Hook이 위험을 차단하고
    • HITL Policy가 어디까지 자율인지 정하고
    • Audit이 모든 것을 기록합니다

    한 곳이 실패해도 다른 곳이 보완합니다. 이것이 사이버네틱 시스템으로서의 Harness Engineering의 본질입니다.


    참고 자료


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.