# 3대 엔진 합의도출 아키텍처 미팅 — Cycle 2-5

**일시**: 2026-03-18
**주관**: 아누 (개발실장)
**연속**: Cycle 1 완료 후 미해결 쟁점 A-F 해결
**참조 코드베이스**: `services/multimodel-bot/engine.py` (OAuth Secret 하드코딩 발견)

---

## Cycle 2
**쟁점 A + B**: 합의 게이트 인젝션 방어 + engine.py 리팩 vs 신규
**참여**: 불칸(API설계), 로키(DA)

### 쟁점
- A: 엔진 출력이 다음 단계 프롬프트로 전달될 때 인젝션 방어
- B: 기존 engine.py (REST API, 하드코딩) vs 신규 engine_v2/

### 합의 사항

**인젝션 방어 — 3레이어:**

| 레이어 | 구현 | 우선순위 |
|--------|------|---------|
| L1: 패턴 기반 Sanitizer | `content_sanitizer.py` (정규식) | 1차 방어 |
| L2: 구조적 격리 | `<UPSTREAM_DATA>` 태그 + 태그 이스케이프 | 2차 방어 |
| L3: 오케스트레이터 게이트 | `flagged_count >= 3` → 파이프라인 중단 | 3차 방어 |
| L4: 에러 자동 제외 | `error=True AND !fallback_used` → 중단 | 안전망 |

L2 이스케이프 필수:
```python
def _escape_envelope_tags(text: str) -> str:
    return text.replace("</UPSTREAM_DATA>", "&lt;/UPSTREAM_DATA&gt;")
```

**EngineResult 확정 스키마:**
```python
@dataclass
class EngineResult:
    engine:         EngineRole
    content:        str          # raw output
    clean:          str          # sanitized output
    task_id:        str
    step:           int
    timestamp:      datetime     = field(default_factory=datetime.utcnow)
    token_est:      int          = 0
    error:          bool         = False
    fallback_used:  bool         = False
    flagged_count:  int          = 0
```

**engine.py 처리:**
- 신규 `engine_v2/` 패키지 생성
- 기존 `engine.py` 유지 (봇 의존성 보호)
- `engine.py` 최상단에 `# DEPRECATED` 헤더 + 각 함수 docstring에 경고 추가 필수

### DA 반론 및 판정

| 로키 공격 | 판정 | 조치 |
|-----------|------|------|
| 유니코드 우회 (Ignοre) | 수용 — Sanitizer는 1차 방어선으로 격하 | Layer 분리 명시 |
| 태그 탈출 (`</UPSTREAM_DATA>`) | 수용 — 이스케이프 필수 | `_escape_envelope_tags()` 추가 |
| 드리프트 위험 (두 파일 공존) | 수용 | engine.py Deprecation 헤더 필수 |
| Codex free plan 제한 미처리 | 수용 | `fallback_used` 필드 + 중단 로직 |

### 미해결 → 다음 Cycle
- Circuit Breaker 수치 → Cycle 4
- OAuth 시크릿 패치 → Cycle 4

---

## Cycle 3
**쟁점 C + D**: 16건 Phase 분리 + 출판팀-범용 엔진 코드 공유 범위
**참여**: 토르(성능), 비너스(Gemini), 아틀라스(GPT), 로키(DA)

### 쟁점
- C: 구현 필요 컴포넌트를 Phase로 분리 + 팀 배정
- D: engine_v2(범용)와 publishing/(출판팀)의 코드 경계

### 합의 사항

**Phase 분리 확정:**

| Phase | 팀 | 주요 작업 | 산출물 |
|-------|----|-----------|----|
| Phase 1 | 개발3팀(라) | G01(Claude CLI 정리) + G02(Codex fallback 매핑) | CLI 테스트 통과 |
| Phase 2 | 개발1팀(헤르메스) | G03(Gemini CLI 전환) + G04(EngineResult) + G05(Sanitizer) + G06(CLIRunner) | engine_v2 기반 완성 |
| Phase 3 | 개발1팀(헤르메스) | G07(orchestrator SEQUENTIAL/PARALLEL/BROADCAST) + G08(publishing_adapter) | 오케스트레이터 동작 |
| Phase 4 | 개발2팀(오딘) | G09(CONSENSUS) + G10(Circuit Breaker) + G11(비용 JSONL) + G12(Semaphore) | 안정화 레이어 완성 |
| Phase 5 | 개발1팀(헤르메스) | G13(3대엔진 Step 1-5) + G14(Deprecation) + G15(QC훅) + G16(대시보드) + 봇 마이그레이션 계획서 | 출판팀 엔드투엔드 |

**코드 공유 경계 확정:**

```
services/multimodel-bot/
├── engine.py                        # DEPRECATED (기존 봇 보호)
├── engine_v2/                       # 범용 (출판팀 미인지)
│   ├── __init__.py
│   ├── engine_result.py
│   ├── content_sanitizer.py
│   ├── cli_runner.py
│   └── engine_orchestrator.py
└── publishing/                      # 출판팀 전용 (engine_v2만 의존)
    ├── publishing_adapter.py
    ├── step_templates.py
    ├── consensus_pipeline.py
    └── chapter_runner.py
```

- `engine_v2/`는 출판팀을 전혀 알지 못한다 (순수 범용)
- `CONSENSUS` 모드: `publishing/consensus_pipeline.py` (범용 오케스트레이터 외부)
- `Step_templates.py` 출처 주석 필수: `memory/specs/three-engine-consensus.md`
- Gemini CLI 버전 체크 (v0.31.0 기준) Phase 2에서 구현

**Codex fallback 매핑:**
```python
_CODEX_MODEL_FALLBACK: dict[str, str] = {
    "gpt-5.2-codex":  "gpt-5.1-codex-mini",  # free plan fallback
    "gpt-5.1-codex":  "gpt-5.1-codex-mini",
}
```

### DA 반론 및 판정

| 로키 공격 | 판정 | 조치 |
|-----------|------|------|
| G03(Gemini CLI 전환) 의존 체인 위험 | 수용 | G03 → Phase 1에서 Phase 2로 이동 |
| publishing/ 위치 미정 | 수용 | `services/multimodel-bot/publishing/` 확정 |
| 16건 출처 불명확 (추론 기반) | 수용 | 위임 시 pm-skills 문서 확인 절차 추가 |

### 미해결 → 다음 Cycle
- Circuit Breaker 구체 수치 → Cycle 4
- OAuth 패치 → Cycle 4

---

## Cycle 4
**쟁점 E + F**: Circuit Breaker 수치 + OAuth 시크릿 즉시 패치
**참여**: 로키(보안 주도), 불칸(안정성)

### 쟁점
- E: Circuit Breaker 임계값 / 일일 예산 상한 수치
- F: `engine.py` OAuth Client Secret 하드코딩 즉시 패치 범위

### 합의 사항

**쟁점 F — OAuth 패치:**

실제 코드 (`engine.py` 10-11번 라인):
```python
_OAUTH_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
_OAUTH_CLIENT_SECRET = "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"
```

| 우선순위 | 조치 | 타이밍 |
|----------|------|--------|
| P0 (즉시) | `_require_env()` 헬퍼로 환경변수 전환 | Phase 0 (오늘) |
| P0 (즉시) | `.env.keys`에 `GEMINI_OAUTH_CLIENT_ID/SECRET` 추가 | Phase 0 (오늘) |
| P1 | Git history 노출 여부 확인 → 제이회장님 보고 → 시크릿 로테이션 결정 | 별도 보고 |
| P2 | Phase 2 완료 시 engine_v2 CLI 전환으로 OAuth 코드 자체 제거 | Phase 2 후 |

환경변수 패턴:
```python
def _require_env(key: str) -> str:
    val = os.environ.get(key)
    if not val:
        raise RuntimeError(
            f"필수 환경변수 '{key}' 미설정. "
            f"source /home/jay/workspace/.env.keys 실행 후 재시작하세요."
        )
    return val

_OAUTH_CLIENT_ID     = _require_env("GEMINI_OAUTH_CLIENT_ID")
_OAUTH_CLIENT_SECRET = _require_env("GEMINI_OAUTH_CLIENT_SECRET")
```

**쟁점 E — Circuit Breaker 수치:**

| 파라미터 | 수치 | 근거 |
|----------|------|------|
| `fail_threshold` | 3회 | fluke 2회 허용 |
| `recovery_sec` | 120초 | 600초 타임아웃의 1/5 (로키 자기 공격 반영) |
| `half_max_calls` | 1회 | 복구 확인 최소 |
| `MAX_CONSENSUS_ROUNDS` | 3회 하드캡 | 무한루프 방지 (비즈니스 룰) |
| `asyncio.Semaphore` | 3 | Cycle 1 합의 유지 |
| `DAILY_LIMITS` 저장 | `config/engine_budget.yaml` | 하드코딩 금지 |

비용 추적 JSONL 스키마 (`memory/engine_usage_YYYY-MM.jsonl`):
```json
{
    "ts":           "2026-03-18T12:00:00Z",
    "task_id":      "task-200",
    "step":         2,
    "engine":       "gemini",
    "prompt_chars": 4500,
    "output_chars": 8200,
    "token_est":    3200,
    "fallback_used": false,
    "flagged_count": 0,
    "duration_sec": 45.2,
    "error":        false
}
```

### DA 반론 및 판정

| 로키/불칸 자기 공격 | 판정 | 조치 |
|--------------------|------|------|
| `os.environ[]` → KeyError | 수용 | `_require_env()` 헬퍼로 명시적 에러 |
| DAILY_LIMITS 근거 없음 | 수용 | `config/engine_budget.yaml`로 분리 |
| CB recovery_sec 60초 짧음 | 수용 | 120초로 상향 |

---

## Cycle 5 (Final)
**전체 통합 검토 + Temporal Interrogation**
**참여**: 전원 (불칸, 토르, 비너스, 아틀라스, 로키)

### Temporal Interrogation 결과

| 결정 시점 | 쟁점 | 최종 판정 |
|-----------|------|---------|
| Cycle 1→2 | engine.py 발견 후 처리 | 신규 engine_v2/ + 기존 유지. Deprecation 헤더 + 함수별 docstring 경고 필수 |
| Cycle 2→3 | Sanitizer 없는 Phase 1 | Phase 1은 독립 테스트만. 파이프라인 연결은 Phase 2 완료 후 |
| Cycle 3→4 | Phase 0 추가 | Phase 0 = 보안 패치 (긴급). Phase 1-5 번호 유지. Phase 0 완료가 Phase 1 선행 조건 |

### 추가 합의 (Cycle 5)

**engine_orchestrator.py 공개 인터페이스 확정:**
```python
class EngineOrchestrator:
    async def run(
        self,
        mode:    Literal["SEQUENTIAL", "PARALLEL", "BROADCAST"],
        prompts: list[str],
        engines: list[EngineRole],
        task_id: str,
        step:    int,
        timeout: int = 600,    # 출판팀은 900 권장
    ) -> list[EngineResult]: ...
```

- `SEQUENTIAL`: asyncio.Semaphore 불필요 (1개씩 직렬)
- `PARALLEL`/`BROADCAST`: asyncio.Semaphore(3) 적용

**로키 최종 DA 반론 수용:**

| 로키 최종 공격 | 판정 | 조치 |
|---------------|------|------|
| Token refresh race condition (engine.py + engine_v2 동시 호출) | 수용 | Phase 5에 봇 마이그레이션 계획서 산출물 추가 |
| chapter_runner.py 진입점 미정 | 수용 | CLI 인터페이스 Phase 5에서 명시 필수 (`python3 chapter_runner.py --chapter N --task-id task-XXX`) |
| engine_usage.jsonl 무한 성장 | 수용 | 월별 파일 분리 (`engine_usage_YYYY-MM.jsonl`) |

### DA 반론 및 판정

모든 반론 수용. 추가 쟁점 없음.

---

## 전체 최종 합의 요약

### 아키텍처 확정

```
services/multimodel-bot/
├── engine.py                        # DEPRECATED 헤더 필수
├── engine_v2/                       # 범용 레이어
│   ├── engine_result.py
│   ├── content_sanitizer.py
│   ├── cli_runner.py
│   └── engine_orchestrator.py
└── publishing/                      # 출판팀 전용
    ├── publishing_adapter.py
    ├── step_templates.py
    ├── consensus_pipeline.py
    └── chapter_runner.py

config/
└── engine_budget.yaml

memory/
└── engine_usage_YYYY-MM.jsonl
```

### Phase 실행 계획 (최종)

| Phase | 팀 | 내용 | 선행 조건 |
|-------|----|-----------|----|
| **Phase 0** | 긴급 처리 | OAuth 환경변수 전환 + engine.py 보안 패치 | 없음 (오늘) |
| **Phase 1** | 개발3팀(라) | G01+G02 (CLI 정리 + Codex fallback) | Phase 0 완료 |
| **Phase 2** | 개발1팀(헤르메스) | G03+G04+G05+G06 (Gemini CLI + engine_v2 기반) | Phase 1 완료 |
| **Phase 3** | 개발1팀(헤르메스) | G07+G08 (orchestrator + adapter) | Phase 2 완료 |
| **Phase 4** | 개발2팀(오딘) | G09+G10+G11+G12 (CONSENSUS + CB + 비용 + Semaphore) | Phase 3 완료 |
| **Phase 5** | 개발1팀(헤르메스) | G13+G14+G15+G16 + 마이그레이션 계획서 | Phase 4 완료 |

### 보안 체크리스트

- [ ] Phase 0: `engine.py` OAuth 하드코딩 → `_require_env()` 전환
- [ ] Phase 0: `.env.keys` 업데이트
- [ ] 별도 보고: Git history 시크릿 노출 + 로테이션 결정
- [ ] Phase 2: CLI 전환으로 OAuth 코드 자체 제거

### Circuit Breaker 최종 수치

- fail_threshold: 3회
- recovery_sec: 120초
- half_max_calls: 1회
- MAX_CONSENSUS_ROUNDS: 3회 (하드캡)
- Semaphore: 3
- DAILY_LIMITS: config/engine_budget.yaml (하드코딩 금지)

---

*미팅 종료. 아누 기록.*
