# Hermes 고난이도 항목 7개 상세 설계서

**작성자**: 토르 (dev2-team 백엔드)
**작성일**: 2026-03-23
**기준**: Hermes 소스 직접 분석 + dev2-team 시스템 구조 적합화

---

## 목차

1. [5단계 컨텍스트 압축 (context_compressor.py)](#1-5단계-컨텍스트-압축)
2. [FTS5 세션 검색 (session_search.py)](#2-fts5-세션-검색)
3. [SQLite WAL 세션 저장소 (session_store.py)](#3-sqlite-wal-세션-저장소)
4. [스킬 보안 스캔 AST (skill_guard.py)](#4-스킬-보안-스캔-ast)
5. [서브에이전트 격리 위임 (delegate_controller.py)](#5-서브에이전트-격리-위임)
6. [사전 실행 보안 스캔 (pre_exec_scan.py)](#6-사전-실행-보안-스캔)
7. [이벤트 훅 시스템 (event_hooks.py)](#7-이벤트-훅-시스템)

---

## 1. 5단계 컨텍스트 압축

### [context_compressor.py]

**Hermes 구현 분석**:
- 파일: `/tmp/hermes-agent/agent/context_compressor.py` (659줄)
- 핵심 클래스: `ContextCompressor`
- 핵심 알고리즘 (5단계):
  1. **Phase 1 — 툴 결과 사전 정리**: `_prune_old_tool_results()` — LLM 호출 없이 200자 이상 오래된 툴 출력을 플레이스홀더로 교체
  2. **Phase 2 — 헤드 보호**: `protect_first_n` (기본 3개) 메시지를 항상 유지
  3. **Phase 3 — 테일 보호 (토큰 예산)**: `_find_tail_cut_by_tokens()` — 20K 토큰을 역방향 누적하여 최근 컨텍스트 보호 (고정 카운트 대신 토큰 예산 기반)
  4. **Phase 4 — 구조화 요약 생성**: `_generate_summary()` — Goal/Progress/Decisions/Files/NextSteps 구조, 반복 압축 시 이전 요약을 업데이트하는 iterative 모드 지원
  5. **Phase 5 — 툴 쌍 무결성 복원**: `_sanitize_tool_pairs()` — 고아 tool_result 제거 + 고아 tool_call에 스텁 결과 삽입
- 특이점: `_align_boundary_forward/backward()`로 툴콜/결과 그룹이 경계에서 분할되지 않도록 보정; 요약 메시지의 role이 헤드/테일과 충돌 시 병합 처리; `_previous_summary` 상태로 iterative 누적

**우리 시스템 적용 설계**:

- **파일 구조**:
  - `utils/context_compressor.py` — 메인 압축 엔진 (~180줄)
  - `utils/context_summarizer.py` — 요약 LLM 호출 분리 (~60줄)
  - **총 예상 240줄** (200줄 원칙상 2파일 분리)

- **핵심 클래스/함수**:
  ```python
  # utils/context_compressor.py
  class ContextCompressor:
      def __init__(self, model: str, threshold_percent: float = 0.50,
                   protect_first_n: int = 3, tail_token_budget: int = 20_000):
          ...
      def should_compress(self, messages: list[dict]) -> bool: ...
      def compress(self, messages: list[dict]) -> list[dict]: ...
      def _prune_old_tool_results(self, messages, protect_tail_count) -> tuple[list, int]: ...
      def _find_tail_cut_by_tokens(self, messages, head_end) -> int: ...
      def _sanitize_tool_pairs(self, messages) -> list[dict]: ...
      def _align_boundary(self, messages, idx, direction) -> int: ...

  # utils/context_summarizer.py
  async def generate_summary(turns: list[dict], prev_summary: str | None,
                              budget_tokens: int) -> str | None:
      """LLM 호출로 구조화 요약 생성. 실패 시 None 반환."""
      ...

  def serialize_for_summary(turns: list[dict]) -> str:
      """툴콜 인자·결과를 레이블 텍스트로 직렬화."""
      ...
  ```

- **통합 포인트**:
  - `dispatch.py` — 팀장에게 작업 위임 전, 메시지 목록이 임계값 초과 시 자동 호출
  - `orchestrator.py` — 장기 오케스트레이션 중 메시지 누적 감지 후 훅
  - 향후 세션 저장소(session_store.py) 연계: 압축 후 새 세션 분기 기록 (parent_session_id 체인)

- **의존성**:
  - `utils/logger.py` (기존) — 로깅
  - 외부 LLM 클라이언트: Hermes `auxiliary_client.call_llm` 패턴 → 우리 시스템의 AI 모델 호출 방식으로 대체 (GLM/Claude API)
  - `utils/injection_guard.py` (기존) — 요약 결과 인젝션 검증 추가 고려

**난이도 상세**:
- **왜 난이도 '상'인가**:
  1. **툴 쌍 무결성**: tool_call ID와 tool_result가 1:1 매핑되지 않으면 LLM API가 오류를 반환함. 압축 후 이 invariant를 보장하는 `_sanitize_tool_pairs` 로직이 비직관적으로 복잡
  2. **경계 정렬**: 헤드/테일 경계가 툴콜 그룹 중간에 걸리면 데이터 소실 → `_align_boundary_forward/backward` 필요
  3. **Role 연속성**: 요약 메시지 삽입 시 연속 동일 role 금지 조건 → 헤드 마지막 role과 테일 첫 role을 모두 고려해야 함
  4. **Iterative 누적**: 재압축 시 이전 요약을 보존하면서 새 정보를 병합하는 프롬프트 설계 (단순 재요약 시 누적 정보 소실)
  5. **토큰 추정 정확도**: 정확한 토크나이저 없이 chars/4 추정 → 보수적 여유값 설정 필요

**구현 시 주의사항**:
1. `_sanitize_tool_pairs` 는 orphaned result 제거 후 orphaned call에 stub result 삽입 — 순서 바꾸면 새로 삽입한 stub이 다시 제거될 수 있음
2. `protect_first_n` 계산 시 system 메시지 포함 여부를 일관되게 처리 (0-indexed)
3. `_find_tail_cut_by_tokens` 가 `head_end + 1` 이상을 보장해야 중간 구역이 비지 않음
4. 요약 LLM 호출이 실패(RuntimeError/timeout)해도 압축 자체는 진행 — 빈 요약으로 중간 턴 드롭
5. 시스템 프롬프트(index 0)에 "[압축 노트]" 추가는 첫 번째 압축 시 한 번만 적용

**예상 소요**: **4일**
- Day 1: 5단계 압축 파이프라인 + 툴 쌍 무결성 (핵심 난점)
- Day 2: 테일 토큰 예산 + 경계 정렬 알고리즘
- Day 3: Iterative 요약 프롬프트 + LLM 호출 모듈 분리
- Day 4: 단위 테스트 (경계 케이스 — 빈 중간 구역, 모든 메시지 보호, 반복 압축)

---

## 2. FTS5 세션 검색

### [session_search.py]

**Hermes 구현 분석**:
- 파일: `/tmp/hermes-agent/tools/session_search_tool.py` (421줄)
- 핵심 함수: `session_search()`, `_summarize_session()`, `_truncate_around_matches()`
- 플로우:
  1. `db.search_messages()` (FTS5 MATCH) → 최대 50개 raw 결과
  2. 세션 ID를 부모 세션 lineage로 resolve (`_resolve_to_parent()`) — 위임 자식 세션 병합
  3. 현재 세션 lineage 제외 후 최대 N개 유니크 세션 선택
  4. 각 세션 대화 로드 → 쿼리 매칭 위치 중심으로 100K자 트런케이션
  5. 병렬 async LLM 요약 (`asyncio.gather`) — 세션당 별도 Gemini Flash 호출
  6. JSON 응답 반환 (query, results[], count, sessions_searched)
- 특이점: 동기 컨텍스트에서 비동기 요약을 `ThreadPoolExecutor + asyncio.run()` 패턴으로 처리; 위임 세션의 상세 내용이 자식에 있어 부모 lineage 해결 필수

**우리 시스템 적용 설계**:

- **파일 구조**:
  - `utils/session_search.py` — 검색 오케스트레이션 (session_store.py 필요, ~150줄)
  - **총 예상 150줄** (session_store.py 완성 후 의존)

- **핵심 클래스/함수**:
  ```python
  # utils/session_search.py

  def search_sessions(
      query: str,
      db: "SessionStore",
      role_filter: list[str] | None = None,
      limit: int = 3,
      current_session_id: str | None = None,
  ) -> dict:
      """FTS5 검색 → 요약 → JSON 결과 반환."""
      ...

  def _resolve_lineage_root(session_id: str, db: "SessionStore") -> str:
      """위임 체인을 역방향으로 추적하여 루트 세션 ID 반환."""
      ...

  def _truncate_around_matches(full_text: str, query: str,
                                max_chars: int = 100_000) -> str:
      """쿼리 매칭 위치 중심으로 텍스트 창 추출."""
      ...

  def _format_conversation(messages: list[dict]) -> str:
      """세션 메시지를 요약용 텍스트로 직렬화."""
      ...

  async def _summarize_session(text: str, query: str,
                                meta: dict) -> str | None:
      """단일 세션에 대해 LLM 요약 생성."""
      ...
  ```

- **통합 포인트**:
  - `session_store.py` (신규 구현) — `search_messages()`, `get_messages_as_conversation()`, `get_session()` 메서드 의존
  - `dispatch.py` — 아누가 "이전에 이 작업 했었나?" 쿼리 시 호출 가능
  - `orchestrator.py` — 작업 시작 전 유사 과거 결과 검색 (선택적)

- **의존성**:
  - `utils/session_store.py` (신규, 항목 3) — FTS5 검색 엔진
  - `utils/logger.py` (기존)
  - async LLM 클라이언트 (context_compressor와 동일 패턴)

**난이도 상세**:
- **왜 난이도 '상'인가**:
  1. **FTS5 쿼리 sanitization**: 사용자 입력의 특수문자 (`"`, `(`, `)`, `+`, `*`, boolean 연산자) 처리 — 잘못된 쿼리가 `sqlite3.OperationalError` 유발
  2. **세션 lineage 해결**: 위임으로 생성된 자식 세션들이 별도 session_id를 가지므로 부모 추적 없이는 중복 결과 또는 현재 세션 제외 실패
  3. **동기-비동기 경계**: 동기 컨텍스트(`dispatch.py`)에서 병렬 async LLM 호출 → event loop 충돌 방지 패턴 필요 (`ThreadPoolExecutor + asyncio.run`)
  4. **컨텍스트 창 관리**: 세션 전체가 수십만 자일 수 있어 LLM 호출 전 쿼리 매칭 위치 중심 트런케이션 필수
  5. **세션 저장소 의존**: 이 모듈 단독 테스트 불가 — session_store.py 완성 필요

**구현 시 주의사항**:
1. FTS5 sanitization에서 하이픈 포함 단어(`chat-send`)는 FTS5가 `chat AND send`로 분리 → 인용부호로 감싸야 함
2. 위임 자식 세션의 `parent_session_id`가 null일 수 있음 — 루프 탈출 조건 반드시 포함 (visited set)
3. 현재 세션 제외는 exact ID + lineage root 모두 확인 — 부모/자식 모두 제외 대상
4. 요약 LLM 타임아웃을 60초로 제한 (병렬이라도 네트워크 지연 가능)
5. `asyncio.gather(return_exceptions=True)` — 한 세션 요약 실패가 전체를 막지 않도록

**예상 소요**: **3일**
- Day 1: FTS5 sanitization + lineage 해결 로직
- Day 2: 비동기 요약 오케스트레이션 + 동기-비동기 경계 처리
- Day 3: 단위 테스트 + session_store.py 통합 검증

---

## 3. SQLite WAL 세션 저장소

### [session_store.py]

**Hermes 구현 분석**:
- 파일: `/tmp/hermes-agent/hermes_state.py` (955줄)
- 핵심 클래스: `SessionDB`
- 핵심 설계:
  - `PRAGMA journal_mode=WAL` + `threading.Lock` → 다중 리더/단일 라이터 안전
  - FTS5 가상 테이블 + INSERT/DELETE/UPDATE 트리거로 메시지 인덱스 자동 유지
  - `SCHEMA_VERSION = 5` + `ALTER TABLE` 마이그레이션 (버전별 순차 적용)
  - `parent_session_id` 외래키로 압축/위임 세션 체인 표현
  - `sanitize_title()` — ASCII 제어문자, 유니코드 zero-width 문자, RTL/LTR override 제거
  - `_sanitize_fts5_query()` — 인용구 보호, FTS5 특수문자 제거, 하이픈 단어 인용 처리
  - 세션 제목 유니크 인덱스 (WHERE title IS NOT NULL) + lineage 번호화 (`base #2`, `base #3`)

**우리 시스템 적용 설계**:

- **파일 구조**:
  - `utils/session_store.py` — SessionStore 클래스 + 스키마 (~200줄)
  - `utils/session_store_search.py` — FTS5 검색 메서드 분리 (~80줄)
  - **총 예상 280줄** (200줄 원칙, 검색 로직 분리)

- **핵심 클래스/함수**:
  ```python
  # utils/session_store.py

  SCHEMA_VERSION = 1

  class SessionStore:
      """SQLite WAL 기반 세션 영구 저장소."""

      def __init__(self, db_path: Path | None = None):
          self.db_path = db_path or _default_db_path()
          self._lock = threading.Lock()
          self._conn: sqlite3.Connection
          self._init_db()

      # 세션 라이프사이클
      def create_session(self, session_id: str, source: str,
                         model: str | None = None,
                         parent_session_id: str | None = None) -> str: ...
      def end_session(self, session_id: str, end_reason: str) -> None: ...
      def get_session(self, session_id: str) -> dict | None: ...
      def list_sessions(self, source: str | None = None,
                        limit: int = 20) -> list[dict]: ...

      # 메시지 CRUD
      def append_message(self, session_id: str, role: str,
                         content: str | None = None,
                         tool_calls: list | None = None,
                         tool_call_id: str | None = None) -> int: ...
      def get_messages(self, session_id: str) -> list[dict]: ...
      def get_messages_as_conversation(self, session_id: str) -> list[dict]: ...

      # 유지보수
      def prune_sessions(self, older_than_days: int = 90) -> int: ...
      def close(self) -> None: ...

  # utils/session_store_search.py
  def search_messages(db: SessionStore, query: str,
                      role_filter: list[str] | None = None,
                      limit: int = 20) -> list[dict]:
      """FTS5 MATCH로 메시지 검색. sanitize 후 실행."""
      ...

  def sanitize_fts5_query(query: str) -> str:
      """FTS5 특수문자 처리 — 인용구 보호, 하이픈 단어 인용."""
      ...
  ```

- **통합 포인트**:
  - `session_search.py` (항목 2) — FTS5 검색의 기반
  - `context_compressor.py` (항목 1) — 압축 트리거 시 새 세션 분기 (parent_session_id)
  - `dispatch.py` — 팀장 세션 메시지 영구 기록 (현재 JSONL → SQLite 이전)
  - `orchestrator.py` — 오케스트레이션 이벤트 세션별 저장
  - `event_hooks.py` (항목 7) — `session:start`, `session:end` 이벤트 훅 연결

- **의존성**:
  - Python 표준 라이브러리: `sqlite3`, `threading`, `json`, `time`, `re`, `pathlib`
  - `utils/logger.py` (기존)
  - SQLite 3.x (FTS5 활성화 빌드 필요 — 대부분 시스템 기본 포함)

**난이도 상세**:
- **왜 난이도 '상'인가**:
  1. **WAL 동시성**: `check_same_thread=False` + `threading.Lock` 조합으로 게이트웨이 다중 스레드 안전 보장 — lock 범위를 너무 좁히면 race condition, 너무 넓히면 성능 저하
  2. **FTS5 트리거**: `messages_fts` 가상 테이블과 `messages` 실테이블 동기화를 트리거로 관리 — 테이블 재생성(마이그레이션) 시 트리거가 소실되지 않도록 주의
  3. **스키마 마이그레이션**: `ALTER TABLE ADD COLUMN`은 중복 실행 시 `OperationalError` → try/except로 멱등성 보장; 버전 순서 중요
  4. **FTS5 쿼리 sanitization**: 사용자 입력의 특수문자가 `sqlite3.OperationalError` 유발 — 인용구 보호 순서가 중요 (보호 → 제거 → 복원)
  5. **부모-자식 세션 체인**: 외래키 + 순환 참조 방지 + lineage 번호화 (`#2`, `#3`) 모두 고려

**구현 시 주의사항**:
1. FTS5 가상 테이블은 `executescript()`로 생성 불가 (IF NOT EXISTS 신뢰도 낮음) → `SELECT`로 존재 확인 후 별도 생성
2. WAL 모드에서 `PRAGMA wal_checkpoint(PASSIVE)` 주기적 실행 필요 — WAL 파일이 무한 증가 방지
3. 마이그레이션에서 `ALTER TABLE`은 `WITH LOCK` 내에서만 실행 — 동시 마이그레이션 시 lock 경합 발생 가능
4. `sanitize_fts5_query`에서 `AND/OR/NOT` 제거는 단어 경계 확인 (`\b`) — "ANDROID" 같은 단어 오파싱 방지
5. `prune_sessions`는 `ended_at IS NOT NULL` 조건 필수 — 활성 세션 삭제 방지

**예상 소요**: **4일**
- Day 1: 스키마 설계 + WAL 초기화 + 세션 CRUD
- Day 2: FTS5 가상 테이블 + 트리거 + 메시지 저장
- Day 3: 마이그레이션 + sanitize_fts5_query + 검색 모듈 분리
- Day 4: 단위 테스트 (동시성, 마이그레이션 멱등성, FTS5 sanitization 엣지케이스)

---

## 4. 스킬 보안 스캔 AST

### [skill_guard.py]

**Hermes 구현 분석**:
- 파일: `/tmp/hermes-agent/tools/skills_guard.py` (1085줄)
- 핵심 함수: `scan_file()`, `scan_skill()`, `should_allow_install()`, `llm_audit_skill()`
- 위협 패턴 카테고리 (총 ~80개 패턴):
  - exfiltration (curl/wget/fetch + 환경변수, SSH/AWS/k8s 자격증명 파일)
  - injection (프롬프트 인젝션 20+ 패턴, HTML 숨김 주석, RTL override)
  - destructive (rm -rf /, mkfs, dd 디스크 덮어쓰기)
  - persistence (crontab, authorized_keys, .bashrc/.zshrc, systemd)
  - network (역쉘 nc/socat, ngrok, 하드코딩 IP:포트)
  - obfuscation (base64 decode pipe, eval/exec, chr() 체인)
  - supply_chain (curl|bash, 버전 미고정 pip install)
  - privilege_escalation (sudo, setuid, NOPASSWD, allowed-tools 필드)
  - credential_exposure (하드코딩 API 키, 개인키 PEM, GitHub/OpenAI/Anthropic/AWS 토큰 패턴)
- 신뢰 레벨 (`builtin/trusted/community/agent-created`) × 판정(`safe/caution/dangerous`) → 설치 정책 매트릭스
- 구조 검사: 파일 수, 총 크기, 바이너리 파일, 심링크 탈출
- LLM 감사: `llm_audit_skill()` — 정적 판정이 "safe/caution"이면 추가 LLM 검토 (판정은 오직 상향만 가능)

**우리 시스템 적용 설계**:

- **파일 구조**:
  - `utils/skill_guard.py` — `scan_skill()`, `should_allow_install()`, `SkillScanResult` (~150줄)
  - `utils/skill_guard_patterns.py` — 위협 패턴 정의 분리 (~100줄)
  - `utils/skill_guard_structural.py` — 구조 검사 + 보조 함수 (~60줄)
  - **총 예상 310줄** (3파일 분리)

- **핵심 클래스/함수**:
  ```python
  # utils/skill_guard.py
  from dataclasses import dataclass, field
  from pathlib import Path

  @dataclass
  class SkillFinding:
      pattern_id: str
      severity: str     # "critical" | "high" | "medium" | "low"
      category: str
      file: str
      line: int
      match: str
      description: str

  @dataclass
  class SkillScanResult:
      skill_name: str
      source: str       # "official" | "community" | "agent-created"
      trust_level: str
      verdict: str      # "safe" | "caution" | "dangerous"
      findings: list[SkillFinding] = field(default_factory=list)
      scanned_at: str = ""

  def scan_skill(skill_path: Path, source: str = "community") -> SkillScanResult:
      """스킬 디렉터리/파일 전체 보안 스캔."""
      ...

  def should_allow_install(result: SkillScanResult,
                            force: bool = False) -> tuple[bool | None, str]:
      """(allowed, reason) — None은 사용자 확인 필요."""
      ...

  def format_scan_report(result: SkillScanResult) -> str:
      """CLI/챗 출력용 보고서 문자열."""
      ...

  # utils/skill_guard_patterns.py
  THREAT_PATTERNS: list[tuple[str, str, str, str, str]] = [
      # (regex, pattern_id, severity, category, description)
      ...  # Hermes의 ~80개 패턴 이식 + 우리 시스템 특화 패턴 추가
  ]

  INVISIBLE_CHARS: set[str] = { '\u200b', '\u200c', ... }

  # utils/skill_guard_structural.py
  def check_structure(skill_dir: Path) -> list[SkillFinding]:
      """파일 수, 크기, 바이너리, 심링크 탈출 검사."""
      ...
  ```

- **통합 포인트**:
  - `memory/skill-router.py` (기존) — 스킬 로드 전 스캔 훅
  - `dispatch.py` — `--skill` 옵션 추가 시 스킬 파일 사전 검증
  - `event_hooks.py` (항목 7) — `skill:install` 이벤트 발생 시 자동 스캔
  - `utils/injection_guard.py` (기존) — Hermes의 프롬프트 인젝션 패턴 15개와 우리 injection_guard의 12개 패턴 통합 검토

- **의존성**:
  - `utils/injection_guard.py` (기존) — 프롬프트 인젝션 패턴 재활용 가능
  - Python 표준 라이브러리: `re`, `hashlib`, `pathlib`, `dataclasses`, `datetime`

**난이도 상세**:
- **왜 난이도 '상'인가**:
  1. **패턴 정확도**: ~80개 정규식 패턴의 false positive/negative 균형 — 지나치게 엄격하면 정상 스킬 차단, 너무 느슨하면 위협 통과
  2. **다언어 스캔**: `.py`, `.sh`, `.js`, `.ts`, `.rb`, `.yaml` 등 확장자별 언어 특성 고려 (Python의 `__import__`, JS의 `atob`, Shell의 backtick 등)
  3. **보이지 않는 유니코드 탐지**: RTL override, ZWSP 등 눈에 보이지 않는 주입 문자 — 텍스트 에디터에서 표시 안 됨
  4. **신뢰-판정 매트릭스**: `builtin/trusted/community/agent-created` × `safe/caution/dangerous` 4×3 매트릭스로 정책 관리
  5. **구조 검사 + 심링크 탈출**: `Path.resolve()`로 심링크가 스킬 디렉터리 외부를 가리키는지 검증 — symlink attack 방지

**구현 시 주의사항**:
1. 패턴 컴파일은 모듈 로드 시 한 번만 (`re.compile`) — 스캔 호출마다 재컴파일 방지
2. `scan_file`에서 binary 파일 `UnicodeDecodeError` 예외 처리 필수 (`.so`, `.pyc` 등)
3. 심링크 검사는 `f.resolve().is_relative_to(skill_dir.resolve())` — `skill_dir`도 resolve 후 비교
4. `llm_audit_skill`은 정적 판정이 "dangerous"이면 스킵 (이미 최고 위험도)
5. 기존 `utils/injection_guard.py`의 프롬프트 인젝션 패턴 12개와 Hermes의 injection 카테고리 20개를 병합 검토하여 중복 제거

**예상 소요**: **3일**
- Day 1: 데이터 클래스 + 신뢰-판정 매트릭스 + 구조 검사
- Day 2: 위협 패턴 이식 (Hermes 80개 → 우리 시스템 맞춤 정제 + injection_guard 통합)
- Day 3: 유니코드 탐지 + 단위 테스트 (패턴별 케이스, 심링크 탈출, 바이너리 파일 처리)

---

## 5. 서브에이전트 격리 위임

### [delegate_controller.py]

**Hermes 구현 분석**:
- 파일: `/tmp/hermes-agent/tools/delegate_tool.py` (790줄)
- 핵심 함수: `delegate_task()`, `_build_child_agent()`, `_run_single_child()`, `_resolve_delegation_credentials()`
- 격리 설계:
  - `DELEGATE_BLOCKED_TOOLS`: 자식이 가질 수 없는 툴셋 고정 목록 (`delegate_task`, `clarify`, `memory`, `send_message`, `execute_code`)
  - `MAX_DEPTH = 2`: 부모(0) → 자식(1) → 손자 거부(2) 재귀 차단
  - `MAX_CONCURRENT_CHILDREN = 3`: 병렬 실행 상한
  - 자식 에이전트는 `skip_context_files=True`, `skip_memory=True`, `ephemeral_system_prompt` 사용 — 부모 히스토리 없는 격리 컨텍스트
  - `_delegate_depth` 속성으로 깊이 추적
  - 부모의 `_active_children` 리스트 + `threading.Lock`으로 인터럽트 전파
  - 진행 상황 콜백: CLI spinner 및 게이트웨이 배치 진행 표시
  - `model_tools._last_resolved_tool_names` 전역 오염 방지: 빌드 전 저장 → 빌드 후 복원
  - 위임 크레덴셜: `delegation.provider` 설정 시 다른 provider:model 사용 가능

**우리 시스템 적용 설계**:

- **파일 구조**:
  - `utils/delegate_controller.py` — `DelegateController`, `SubAgentTask`, `SubAgentResult` (~180줄)
  - `utils/delegate_runner.py` — 개별 서브에이전트 실행 로직 분리 (~80줄)
  - **총 예상 260줄** (200줄 원칙, 실행 로직 분리)

- **핵심 클래스/함수**:
  ```python
  # utils/delegate_controller.py
  from dataclasses import dataclass
  from typing import Any

  BLOCKED_TOOLS: frozenset[str] = frozenset([
      "delegate",   # 재귀 위임 차단
      "clarify",    # 사용자 상호작용 차단
      "memory",     # 공유 MEMORY.md 쓰기 차단
      "send_message",  # 크로스 채널 메시지 차단
  ])

  MAX_DEPTH = 2
  MAX_CONCURRENT = 3

  @dataclass
  class SubAgentTask:
      goal: str
      context: str | None = None
      toolsets: list[str] | None = None

  @dataclass
  class SubAgentResult:
      task_index: int
      status: str           # "completed" | "failed" | "error" | "interrupted"
      summary: str | None
      duration_seconds: float
      api_calls: int
      error: str | None = None

  class DelegateController:
      def __init__(self, parent_depth: int = 0):
          self._depth = parent_depth
          self._active_children: list = []
          self._children_lock = threading.Lock()

      def can_delegate(self) -> tuple[bool, str]:
          """위임 가능 여부 + 거부 이유 반환."""
          ...

      def run(self, tasks: list[SubAgentTask],
              max_iterations: int = 50) -> list[SubAgentResult]:
          """단일/배치 서브에이전트 실행. 배치는 ThreadPoolExecutor 병렬."""
          ...

      def interrupt_children(self) -> None:
          """모든 활성 자식에게 인터럽트 전파."""
          ...

  # utils/delegate_runner.py
  def run_subagent(task: SubAgentTask, depth: int,
                   config: dict) -> SubAgentResult:
      """격리된 단일 서브에이전트 실행. 스레드 내에서 호출."""
      ...
  ```

- **통합 포인트**:
  - `dispatch.py` — `--delegate` 플래그 추가: 복수 팀 동시 위임 시 DelegateController 사용
  - `orchestrator.py` — Phase별 병렬 작업 시 `DelegateController.run(tasks)` 호출
  - `event_hooks.py` (항목 7) — `agent:start`, `agent:end` 이벤트 자식 에이전트도 발생
  - `session_store.py` (항목 3) — 자식 세션을 부모의 `child_of` 관계로 기록

- **의존성**:
  - Python 표준 라이브러리: `threading`, `concurrent.futures`, `json`
  - `utils/logger.py` (기존)
  - `utils/approval.py` (기존) — 자식이 실행할 명령어 사전 검증 (위임 전 목표 텍스트 스캔 가능)
  - `utils/injection_guard.py` (기존) — 위임 목표(goal) 텍스트 프롬프트 인젝션 스캔

**난이도 상세**:
- **왜 난이도 '상'인가**:
  1. **전역 상태 오염**: Hermes의 `model_tools._last_resolved_tool_names` 문제 — 우리 시스템도 툴 목록 전역 상태가 있으면 동일 문제 발생. 빌드 전 저장 → 빌드 후 복원 패턴 필수
  2. **인터럽트 전파**: 부모가 인터럽트 수신 시 모든 자식에게 전파 — `threading.Lock` 보호 하에 자식 목록 관리
  3. **깊이 제한**: `_delegate_depth` 속성이 전달되지 않으면 재귀 무한 루프 가능 — 생성 시 반드시 `parent_depth + 1` 설정
  4. **병렬 실행 결과 수집**: `as_completed()` 패턴으로 완료 순서 상관없이 수집, 최종 결과는 task_index 기준 정렬
  5. **격리 검증**: 자식이 실제로 차단된 툴을 호출하지 못하는지 — 툴셋 필터링이 실행 엔진 레벨에서 이루어져야 함

**구현 시 주의사항**:
1. `BLOCKED_TOOLS` 필터링은 툴셋 이름 레벨에서 적용 — 개별 툴 이름과 툴셋 이름이 다를 수 있어 매핑 명확화 필요
2. 자식 에이전트 생성은 메인 스레드에서만 (thread-safe 초기화), 실행만 스레드 풀에서
3. `ThreadPoolExecutor` context manager 사용 — 예외 발생 시에도 스레드 정리 보장
4. 배치 결과 출력 시 `task_index` 기준 정렬 필수 — 완료 순서는 비결정적
5. 자식의 `max_iterations`는 부모의 잔여 iteration budget에서 차감 — 무한 자원 소모 방지

**예상 소요**: **4일**
- Day 1: DelegateController + SubAgentTask/Result 데이터 클래스 + 깊이 제한
- Day 2: 격리 컨텍스트 구성 + 블록된 툴 필터링 + 인터럽트 전파
- Day 3: 병렬 실행 (ThreadPoolExecutor) + 결과 수집 + 전역 상태 보호
- Day 4: 단위 테스트 (깊이 초과, 인터럽트, 병렬 결과 정렬, 툴 차단 검증)

---

## 6. 사전 실행 보안 스캔

### [pre_exec_scan.py]

**Hermes 구현 분석**:
- 파일: `/tmp/hermes-agent/tools/tirith_security.py` (675줄)
- 핵심 함수: `check_command_security()`, `ensure_installed()`, `_install_tirith()`
- 설계 철학:
  - **외부 바이너리(tirith)** 사용 — 시스템 명령어 content-level 위협 분석 (homograph URL, pipe-to-interpreter, terminal injection 등)
  - **종료 코드가 판정의 source of truth**: 0=allow, 1=block, 2=warn (JSON stdout은 풍부화용, 판정 변경 불가)
  - **fail_open 설정**: 바이너리 실패/타임아웃 시 차단 or 허용 선택 가능
  - **자동 설치**: SHA-256 + cosign 서명 검증으로 GitHub Releases에서 자동 다운로드
  - 캐시: `_resolved_path` 프로세스 수명 + 디스크 실패 마커(24시간) — 반복 네트워크 시도 방지
  - 백그라운드 스레드 설치 — 첫 호출 시 블록 없음, 설치 완료 전에는 fail_open

**우리 시스템 적용 설계**:

- **파일 구조**:
  - `utils/pre_exec_scan.py` — 메인 스캔 API (~120줄)
  - `utils/pre_exec_patterns.py` — 패턴 기반 정적 스캔 (tirith 없을 때 폴백) (~80줄)
  - **총 예상 200줄**

- **핵심 클래스/함수**:
  ```python
  # utils/pre_exec_scan.py

  @dataclass
  class ScanVerdict:
      action: str         # "allow" | "warn" | "block"
      findings: list[dict]
      summary: str
      scanner: str        # "tirith" | "static" | "disabled"

  def scan_command(command: str) -> ScanVerdict:
      """명령어 사전 실행 보안 스캔. tirith 우선, 폴백은 정적 패턴.

      우리 시스템에서는 tirith 외에 기존 approval.py도 통합 활용.
      """
      ...

  def _scan_with_tirith(command: str, timeout: int = 5,
                         fail_open: bool = True) -> ScanVerdict | None:
      """tirith 바이너리 호출. 없으면 None 반환."""
      ...

  def _scan_static(command: str) -> ScanVerdict:
      """approval.py + pre_exec_patterns.py 기반 정적 스캔 폴백."""
      ...

  # utils/pre_exec_patterns.py
  # Hermes tirith가 탐지하는 content-level 패턴을 정적으로 재현:
  # homograph URL, pipe-to-interpreter, terminal escape injection,
  # LD_PRELOAD/LD_LIBRARY_PATH 환경변수 주입 등
  EXEC_THREAT_PATTERNS: list[tuple[str, str, str]] = [...]
  ```

- **통합 포인트**:
  - `utils/approval.py` (기존) — `check_command()` 결과를 `pre_exec_scan`이 통합 (approval이 "high/critical"이면 즉시 block, tirith 호출 스킵 가능)
  - `dispatch.py` — `--task` 또는 `--task-file` 실행 전 명령어 스캔 호출
  - `delegate_controller.py` (항목 5) — 자식 에이전트의 툴 실행 전 인터셉트 훅
  - `event_hooks.py` (항목 7) — `agent:step` 이벤트에서 명령어 추출 후 스캔

- **의존성**:
  - `utils/approval.py` (기존) — 1차 정적 패턴 스캔
  - `utils/logger.py` (기존)
  - Python 표준 라이브러리: `subprocess`, `json`, `os`, `shutil`
  - 외부: tirith 바이너리 (선택적, 자동 설치 또는 PATH에 존재 시)

**난이도 상세**:
- **왜 난이도 '상'인가**:
  1. **외부 바이너리 의존성**: tirith가 없을 때의 폴백 전략 + 자동 설치 로직 (SHA-256 검증, 플랫폼별 바이너리 선택, 백그라운드 스레드)
  2. **fail_open vs fail_closed**: 보안-가용성 트레이드오프를 설정으로 제어하면서 타임아웃/오류 각각의 경우를 별도 처리
  3. **종료 코드 신뢰 계층**: JSON 파싱 실패가 판정을 바꾸면 안 됨 — 종료 코드가 우선, JSON은 풍부화만
  4. **기존 approval.py 통합**: 두 스캐너의 결과를 어떻게 병합할지 — 더 엄격한 판정 채택 원칙
  5. **캐싱과 프로세스 간 상태**: 디스크 마커 기반 24시간 캐시 — 설치 실패 시 반복 네트워크 시도 방지, 성공 시 마커 삭제

**구현 시 주의사항**:
1. tirith 종료 코드 0/1/2 외의 코드는 `fail_open` 설정에 따라 처리 — 예상치 못한 종료 코드 처리 필수
2. `subprocess.run` 타임아웃은 `subprocess.TimeoutExpired` → 반드시 catch
3. `OSError` (FileNotFoundError 포함)와 `TimeoutExpired`를 각각 별도 처리 — fail_open 적용
4. approval.py 통합 시 "critical/high" 결과는 tirith 호출 없이 즉시 block 가능 (성능 최적화)
5. 우리 시스템은 tirith 자동 설치보다 `PATH에 없으면 정적 폴백` 정책이 더 적합할 수 있음 — 자동 설치는 선택 기능으로

**예상 소요**: **3일**
- Day 1: `ScanVerdict` 데이터 클래스 + tirith 호출 + fail_open 처리
- Day 2: 정적 폴백 패턴 (`pre_exec_patterns.py`) + approval.py 통합 로직
- Day 3: 단위 테스트 (tirith 없음, 타임아웃, JSON 파싱 실패, fail_open/closed 양쪽)

---

## 7. 이벤트 훅 시스템

### [event_hooks.py]

**Hermes 구현 분석**:
- 파일: `/tmp/hermes-agent/gateway/hooks.py` (154줄)
- 핵심 클래스: `HookRegistry`
- 설계:
  - 훅 디렉터리 탐색: `~/.hermes/hooks/` 내 각 서브디렉터리에서 `HOOK.yaml` + `handler.py` 발견
  - `HOOK.yaml`: `name`, `description`, `events` 필드
  - `handler.py`: `async def handle(event_type, context)` 또는 동기 함수 모두 지원
  - `importlib.util.spec_from_file_location`으로 동적 모듈 로드
  - 와일드카드 매칭: `command:*`가 `command:reset`, `command:new` 등 모두 매칭
  - 동기/비동기 핸들러 자동 감지 (`asyncio.iscoroutine`)
  - 훅 오류는 catch + log — 메인 파이프라인 블록 안 함
  - 이벤트 목록: `gateway:startup`, `session:start`, `session:end`, `session:reset`, `agent:start`, `agent:step`, `agent:end`, `command:*`

**우리 시스템 적용 설계**:

- **파일 구조**:
  - `utils/event_hooks.py` — `HookRegistry`, 내장 훅 등록 (~150줄)
  - `hooks/` 디렉터리 — 커스텀 훅 저장소 (`/home/jay/workspace/hooks/`)
  - **총 예상 150줄** (+ 훅 파일들은 별도 디렉터리)

- **핵심 클래스/함수**:
  ```python
  # utils/event_hooks.py
  import asyncio
  import importlib.util
  from pathlib import Path
  from typing import Any, Callable

  HOOKS_DIR = Path("/home/jay/workspace/hooks")

  # 시스템 이벤트 타입 정의
  class Event:
      DISPATCH_START = "dispatch:start"
      DISPATCH_END = "dispatch:end"
      DISPATCH_ERROR = "dispatch:error"
      ORCHESTRATOR_PHASE_START = "orchestrator:phase_start"
      ORCHESTRATOR_PHASE_END = "orchestrator:phase_end"
      SESSION_START = "session:start"
      SESSION_END = "session:end"
      AGENT_STEP = "agent:step"
      SKILL_INSTALL = "skill:install"
      SECURITY_BLOCK = "security:block"
      SECURITY_WARN = "security:warn"

  class HookRegistry:
      def __init__(self):
          self._handlers: dict[str, list[Callable]] = {}
          self._loaded_hooks: list[dict] = []

      def discover_and_load(self, hooks_dir: Path | None = None) -> None:
          """훅 디렉터리 탐색 + 동적 로드."""
          ...

      def register(self, event_type: str, handler: Callable,
                   name: str = "") -> None:
          """코드에서 직접 훅 등록 (내장 훅용)."""
          ...

      async def emit(self, event_type: str,
                     context: dict[str, Any] | None = None) -> None:
          """이벤트 발생 — 모든 매칭 핸들러 순차 실행."""
          ...

      def emit_sync(self, event_type: str,
                    context: dict[str, Any] | None = None) -> None:
          """동기 컨텍스트에서 이벤트 발생 (asyncio.run 또는 loop.run_until_complete)."""
          ...

  # 싱글턴 레지스트리
  _registry: HookRegistry | None = None

  def get_registry() -> HookRegistry:
      """싱글턴 레지스트리 반환 (없으면 생성)."""
      ...

  def emit(event_type: str, context: dict | None = None) -> None:
      """전역 편의 함수 — dispatch.py 등에서 간편 호출."""
      ...
  ```

- **통합 포인트**:
  - `dispatch.py` — `emit(Event.DISPATCH_START, {...})`, `emit(Event.DISPATCH_END, {...})` 삽입
  - `orchestrator.py` — Phase 전환 시 `ORCHESTRATOR_PHASE_START/END` 이벤트
  - `session_store.py` (항목 3) — 세션 생성/종료 시 `SESSION_START/END` 이벤트
  - `pre_exec_scan.py` (항목 6) — 보안 차단/경고 시 `SECURITY_BLOCK/WARN` 이벤트
  - `skill_guard.py` (항목 4) — 스킬 설치 시 `SKILL_INSTALL` 이벤트
  - 내장 훅 예시: audit-trail 훅 (모든 이벤트 → `/home/jay/workspace/memory/logs/audit.jsonl` 기록)

- **의존성**:
  - Python 표준 라이브러리: `asyncio`, `importlib.util`, `pathlib`, `json`
  - `utils/logger.py` (기존)
  - 선택적: `pyyaml` (HOOK.yaml 파싱용) — 없으면 JSON fallback

**난이도 상세**:
- **왜 난이도 '상'인가**:
  1. **동기/비동기 경계**: `dispatch.py`와 `orchestrator.py`는 동기 코드인데, 훅 핸들러가 async일 수 있음 → `emit_sync`가 event loop 존재 여부를 감지하여 적절히 처리
  2. **동적 모듈 로드**: `importlib.util.spec_from_file_location`으로 런타임 파일 로드 — 모듈 이름 충돌, 재로드 문제, sandboxing 없음
  3. **오류 격리**: 훅 핸들러 하나가 예외를 던져도 나머지 훅과 메인 파이프라인이 계속 실행되어야 함 — 개별 try/except 필수
  4. **와일드카드 매칭**: `command:*` 패턴이 `command:reset`에 매칭되지만 `command` (콜론 없음)에는 매칭 안 됨 — 매칭 로직 명확화
  5. **싱글턴과 재로드**: 장기 실행 프로세스에서 훅 디렉터리 변경 감지 + 재로드 전략 (선택적 구현)

**구현 시 주의사항**:
1. `emit_sync`에서 event loop 감지: `asyncio.get_event_loop().is_running()` → True면 `loop.run_until_complete` 불가 → `threading.Thread + asyncio.run()` 패턴
2. 훅 디렉터리 없으면 조용히 무시 (`discover_and_load`는 디렉터리 없어도 오류 아님)
3. 훅 핸들러 중 하나가 오래 걸리면 다음 핸들러 지연 → 타임아웃 설정 고려 (핸들러당 5초)
4. 동적 로드 모듈 이름에 훅 디렉터리 이름 포함 (`hook_{dir_name}`) — `sys.modules` 충돌 방지
5. `HOOK.yaml`의 `events` 목록이 정의된 이벤트 타입과 불일치 시 경고 로그 출력 (오타 방지)

**예상 소요**: **3일**
- Day 1: `HookRegistry` + 동적 모듈 로드 + 와일드카드 매칭
- Day 2: 동기-비동기 경계 처리 (`emit_sync`) + 오류 격리 + 싱글턴
- Day 3: 내장 audit-trail 훅 구현 + 단위 테스트 (동기/async 핸들러, 오류 격리, 와일드카드)

---

## 전체 요약

### 구현 우선순위 및 의존성 그래프

```
session_store.py (4일)
    └──> session_search.py (3일)
    └──> context_compressor.py (4일)  [압축 후 세션 분기 기록]
    └──> event_hooks.py (3일)          [session:start/end 이벤트]

event_hooks.py (3일)
    └──> pre_exec_scan.py (3일)        [security:block 이벤트]
    └──> skill_guard.py (3일)          [skill:install 이벤트]
    └──> delegate_controller.py (4일)  [agent:start/end 이벤트]

approval.py (기존) ──> pre_exec_scan.py
injection_guard.py (기존) ──> skill_guard.py
injection_guard.py (기존) ──> delegate_controller.py (goal 스캔)
```

### 권장 구현 순서

1. **session_store.py** — 모든 영구 저장 기반
2. **event_hooks.py** — 파이프라인 관찰 인프라
3. **pre_exec_scan.py** — 기존 approval.py 통합 (의존성 최소)
4. **skill_guard.py** — 기존 injection_guard.py 통합
5. **context_compressor.py** — LLM 연동 필요, session_store 연계
6. **delegate_controller.py** — 실행 엔진 통합 필요, 복잡도 최고
7. **session_search.py** — session_store + context_compressor 완성 후

### 예상 총 소요

| 항목 | 예상 일수 |
|------|---------|
| session_store.py | 4일 |
| event_hooks.py | 3일 |
| pre_exec_scan.py | 3일 |
| skill_guard.py | 3일 |
| context_compressor.py | 4일 |
| delegate_controller.py | 4일 |
| session_search.py | 3일 |
| **합계** | **24일** |

병렬 구현 가능 그룹 (의존성 독립):
- **그룹 A**: session_store.py + event_hooks.py + pre_exec_scan.py → 동시 착수 가능
- **그룹 B**: skill_guard.py (그룹 A 완료 후 착수 권장이나 독립 가능)
- **그룹 C**: context_compressor.py + delegate_controller.py (session_store 완료 후)
- **그룹 D**: session_search.py (그룹 C 완료 후)

병렬 진행 시 **실질 소요**: 약 11-13일 (3명 병렬)

---

*이 설계서는 Hermes 소스코드 직접 분석 기반. 구현 착수 전 팀 리뷰 권장.*
