# 🚀 MAM 메시징 백플레인 전환 실행 로드맵 (`implementation_plan.md`)

- **문서 버전**: v1.2.0
- **작성/관리 주체**: Multi-Agent Orchestration Team (`claude`, `agy`, `hermes`, `grok`)
- **기준 커밋**: `916185c` (306/306 baseline tests passing)
- **문서 목적**: MAM의 메시징 인프라를 공개 HiveMQ 브로커에서 `nats-server` 전용 사설 브로커로 무중단 전환하기 위한 5개 트랙(Track 0~3, Track 1R)과 6단계 마일스톤(M0~M4, M2b)의 구체적 실행 지침 및 진행 상황 추적.
- **연계 문서**: [`NATS_REPORT.md`](nats-docker/NATS_REPORT.md), [`PRIVATE_SERVER.md`](nats-docker/PRIVATE_SERVER.md), [`IMPROVEMENTS.md`](IMPROVEMENTS.md)

---

## 1. 개요 및 5개 트랙 구조

```
[M0: 문서 정합성] ──> [M1: 내결함성 확보] ──> [M2a: 로컬스파이크 / M2b: 원격배포] ──> [M3: 보안 종결] ──> [M4: 동기화 완료]
 (E-1~E-4 교정,       (Track 0: B-14,B-15,     (Track 1: O-5 스파이크 /          (Track 2: A-2,      (Track 3: 문서,
  G-D1~G-D4 가드)      G-1~G-10 가드)           Track 1R: 원격 자산·서브모듈)      지문 토픽, G-11)    배포 스크립트)
```

| 트랙 | 대상 과제 | 핵심 목표 | 코드 변경 지점 |
|---|---|---|---|
| **Track 0** | `B-14`, `B-15` (P1) | 브로커 다운 시 65분 정지(Hang) 및 오판정 방지 (로컬 디스크 내결함성) | `publish_event.py`, `job_subscriber.py`, `multi-agent-mux-delegate-job` |
| **Track 1** | `O-5` (P2) | `nats-server` MQTT 3.1.1 어댑터 호환성 및 Retained 메시지 실측 검증 | 격리 클론 (`$SCRATCH/nats-spike`) |
| **Track 1R** | 원격 프로덕션 (P1) | VPS/홈랩 `nats-server` Docker 상시 가동, 서브모듈 분리 및 MAM 원격 백플레인 전환 | `nats-docker/PRIVATE_SERVER.md` §9, `nats-docker/docker/docker-compose.yaml`, `nats.conf`, `.mam.env` |
| **Track 2** | `A-2`, `B-16` (P2) | 워크스페이스 지문 토픽 격리 및 `auth_token` 무조건 발급 강제 | `mqtt_common.py`, `registry.py`, `reconcile.sh` |
| **Track 3** | 문서/설정 동기화 | 공식 가이드, 배포 스크립트, 환경변수 템플릿 일원화 | `MESSAGING.md`, `IMPROVEMENTS.md`, `VERSIONS.md`, `deploy/*` |

---

## 2. 단계별 마일스톤 (Milestones M0 ~ M4)

각 마일스톤은 완료 정의(DoD)와 엄격한 게이트(Gate)를 가지며, 게이트 조건을 충족하지 못하면 다음 마일스톤으로 진입할 수 없습니다.

```
M0 (문서 정합성) ──> M1 (Track 0 내결함성) ──> M2a (로컬 스파이크) ──> M2b (원격 배포) ──> M3 (Track 2 보안) ──> M4 (Track 3 완결)
```

| 마일스톤 | 이름 | 완료 정의 (Definition of Done) | 통과 게이트 (Gate Condition) |
|---|---|---|---|
| **M0** | 문서 정합성 확보 | `PRIVATE_SERVER.md` E-1~E-4 교정, 다능성 절 추가, 본 로드맵 작성 | **G-D1 ~ G-D4 가드 테스트 통과** (276 -> 280) |
| **M1** | 내결함성 확보 (Track 0) | `B-14`, `B-15` 코드 패치 완료 | **G-1 ~ G-10 가드 통과 + mutation 전건 FAIL 확인** (280 -> 290) |
| **M2a** | 로컬 스파이크 (Track 1) | 격리 클론에서 S-1 ~ S-9 스파이크 완수 | **S-3(Retained Terminal Event) 통과** (실패 시 mosquitto로 분기) |
| **M2b** | 원격 프로덕션 전환 (Track 1R) | D-1~D-5 교정 + §9 원격 배포 + 서브모듈 분리 + §9.5 전환 | **R-3(노출0) · R-5(retained) · R-6(신원) · R-9(계정격리) 동시 통과** (290 -> 297 -> 306) |
| **M3** | 보안 종결 (Track 2) | A-2 지문 토픽 전환, G-11 무조건 토큰 발급 | 지문 토픽 동작 확인 **후** legacy 구독 제거 |
| **M4** | 동기화 완료 (Track 3) | `MESSAGING.md`, `IMPROVEMENTS.md`, `VERSIONS.md`, `deploy/*` 정합 | 전체 테스트 스위트 100% Green |

---

## 3. Track 0: 가용성 및 로컬 내결함성 교정 (`B-14`, `B-15`)

> **핵심 원칙**: 브로커 선택과 완전히 독립적인 선행 과제이며, **Step 1 -> Step 2 -> Step 3의 엄격한 순서 의존성**을 갖습니다. Step 2를 먼저 구현하면 디스크에 터미널 상태가 기록되지 않아 폴백 효과가 0이 됩니다.

```
[Step 1: publish_event.py] ──> [Step 2: job_subscriber.py] ──> [Step 3: delegate-job rc 매핑]
  디스크 상태 동기화 선행         로컬 디스크 폴백 감지           인프라 에러(rc=3) 분리
```

### 3.1 Step 1 — `publish_event.py` 실패 처리 순서 재구성 (`B-14` / `F-1`)
1. `publish(...)` 함수를 `try-except`로 감싸되, 네트워크 실패 시 즉시 `return 2` 하지 않고 `publish_ok = False`로 표시합니다.
2. `mqtt_common.append_event` 감사 로그 작성 및 `mqtt_common.update_job_status(status=new_status)` 레지스트리 상태 동기화를 **발행 성공 여부와 무관하게 항상 수행**합니다.
3. 감사 로그 레코드에 `"published": publish_ok` 및 `"publish_error": str(exc)` 필드를 기록합니다.
4. 모든 로컬 디스크 동기화가 완료된 후, 네트워크 발행이 실패했다면 기존 호출부 계약 유지를 위해 `return 2`를 반환합니다.

### 3.2 Step 2 — `job_subscriber.py` 로컬 디스크 폴백 도입 (`B-15` / `C1`)
1. 대기 루프의 `queue.Empty` 분기(`job_subscriber.py:233`)에서, 3초 간격 스로틀로 감시 중인 잡의 디스크 터미널 상태를 확인합니다 (`registry.load_job` -> `mqtt_common.read_logged_status`).
2. 디스크에서 터미널 상태(`completed` 또는 `error`)가 감지되면, `source: disk-fallback` 합성 이벤트를 표준 출력에 기록하고 즉시 정상 종료합니다.
3. 종료 코드 매핑: 디스크 상태가 `completed`이면 `return 0`, `error`이면 `return 1`을 반환합니다.

### 3.3 Step 3 — `multi-agent-mux-delegate-job` 인프라 예외 분리 (`B-15` / `F-4`)
1. `job_subscriber.py`의 미포착 브로커 접속 예외에 전용 종료 코드 `rc=3`을 부여합니다.
2. `multi-agent-mux-delegate-job:331-341`의 `sub_rc` 매핑에 `rc=3` 분기를 추가하여 `job_status="broker_unavailable"`로 분류하고, `wait_for_job`과 동일하게 디스크 상태를 재확인합니다.

### 3.4 Track 0 회귀 가드 매트릭스 (10종 신설 — G-1 ~ G-10)

| ID | 가드 내용 | 변이 검출 기준 (Mutation) |
|---|---|---|
| **G-1** | 브로커 도달 불가 시 `publish_event.py`가 `rc=2`이면서 레지스트리 `status=completed` 기록 | `return 2`를 상태 동기화 앞으로 이동 시 FAIL |
| **G-2** | 동일 상황 감사 로그에 `published: false` 및 `publish_error` 레코드 존재 | `append_event`를 성공 경로로만 한정 시 FAIL |
| **G-3** | 브로커 정상 시 `rc=0` + `status=completed` + `published: true` 무회귀 검증 | — |
| **G-4** | 발행 실패 후 `last_seq`가 1 증가하고 후속 발행이 더 큰 seq 사용 | seq 롤백 도입 시 FAIL |
| **G-5** | 디스크 `status=completed` 선작성 시 브로커 다운 상태에서도 `job_subscriber.py`가 3초 내 `rc=0` 종료 | 디스크 폴백 제거 시 FAIL |
| **G-6** | 동일 조건에서 stdout 합성 라인에 `disk-fallback` 표기 확인 | 표기 누락 시 FAIL |
| **G-7** | 디스크 `status=error` 시 폴백 `rc=1` 반환 확인 | 매핑 반전 시 FAIL |
| **G-8** | 다중 잡 감시 시 전체 완료 전까지 조기 종료 방지 | 부분 종료 도입 시 FAIL |
| **G-9** | 디스크 터미널 부재 + 브로커 실패 시 `rc=3` 반환 확인 | `rc=1`로 되돌릴 시 FAIL |
| **G-10** | `loop` 위임 경로에서 `rc=3` 수신 시 `job_status`가 `"error"`로 오판되지 않음 확인 | 3분기 매핑 복원 시 FAIL |

---

## 4. Track 1: `nats-server` 스파이크 검증 (`O-5`)

> **실행 원칙**: 메인 저장소 작업 트리를 오염시키지 않기 위해 격리 클론(`git clone --local --no-hardlinks . "$SCRATCH/nats-spike"`)에서 수행하고 종료 후 삭제합니다.

| ID | 검증 항목 | 검증 방법 | 통과 기준 |
|---|---|---|---|
| **S-1** | `nats-server` MQTT 리스너 기본 수용 | `nats-server -c nats.conf` 기동 후 `started/progress/completed` 3연속 발행 | `rc=0`, 레지스트리 `status=completed` |
| **S-2** | paho 2.x `CallbackAPIVersion.VERSION2` 호환 | `on_connect` CONNACK reason code 수신 확인 | `reason_code == 0` |
| **S-3** | **Retained Terminal Event 전달** (핵심 관문) | `--event completed` 발행 후 신규 `job_subscriber.py` 기동 | **즉시 최종 이벤트 수신** (*실패 시 mosquitto로 회귀*) |
| **S-4** | QoS 1 발행 ACK | `info.wait_for_publish()` 대기 | `is_published() == True` |
| **S-5** | 와일드카드 토픽 라우팅 | `mam/<fp>/jobs/+/events` 구독 후 이벤트 수신 | `SUBSCRIBED` 출력 및 페이로드 수신 |
| **S-6** | 인증 및 TLS 암호화 | user/pass 및 TLS 구성 후 접속 테스트 | 자격증명 누락 시 거부, 유효 시 성공 |
| **S-7** | Subject 단위 권한 격리 | Publisher write-only / Subscriber read-only 설정 | 비인가 작업 시 연결 거부 |
| **S-8** | 전체 회귀 테스트 | `pytest tests/ -q` | **전건 PASS (0 failed)** |
| **S-9** | Track 0 내결함성 통합 검증 | `nats-server` 강제 종료 상태에서 위임 잡 완주 테스트 | `wait_for_job` 3초 내 반환 |

---

## 5. Track 1R: 원격 프로덕션 전환 로드맵 (M2b 상세)

```
[P0 교정]        D-1~D-5 문서 교정 + §5.2 경계 명문화 + 신규 가드 7종
   ▼
[P0.5 자산화]    docker/ 4대 자산 정본화 + D-22~D-30 회귀 가드 (297 -> 306)
   ▼
[P0.6 서브모듈]  nats-docker 서브모듈 분리 + 동적 경로 해석기 + CI checkout 동기화
   ▼
[P1 서버 준비]   VPS/홈랩 프로비저닝, Docker, Tailscale 가입
   ▼
[P2 배포]        nats.conf + compose 기동, healthcheck healthy 확인
   ▼
[P3 잠금]        바인드 주소 한정 + UFW + ss/nmap 로 노출 면적 0 단언 (R-3)
   ▼
[P4 전환]        드레인 → 잔여 스캔 → .mam.env 교체 (§9.5)
   ▼
[P5 검증]        R-1 ~ R-13. R-5(retained) / R-9(계정 경계) / R-13(MQTT-over-WS)
   ▼
[P6 상시화]      로그 로테이션, JetStream 볼륨 백업, healthcheck 알림
```

---

## 6. Track 2: A-2 보안 결함 및 워크스페이스 격리 해소 (`A-2`, `B-16`)

1. **`auth_token` 무조건 발급 (`F-3` / `G-11`)**:
   `registry.register_job()`에서 브로커 설정과 무관하게 항상 `secrets.token_urlsafe(32)` 기반 토큰을 발급하여 공개 브로커 환경에서도 HMAC 검증이 무력화되지 않도록 강제합니다.
2. **워크스페이스 지문 토픽 3단계 전환 (`F-2`)**:
   - Step 1: 발행자 기본 토픽을 `mam/<sha256[:12]>/jobs/<id>/events`로 전환합니다.
   - Step 2: 실환경 및 통합 테스트에서 이벤트 수신을 확인합니다.
   - Step 3: `reconcile.sh:237`의 레거시 전역 토픽(`python/mqtt/jobs/...`) 구독을 제거합니다.

---

## 7. Track 3: 문서 및 배포 설정 동기화

| 대상 파일 | 갱신 내용 |
|---|---|
| [`MESSAGING.md`](MESSAGING.md) | 브로커 표준을 `nats-server`로 갱신, F-1/C1 해소 기록, F-5 영속 세션 서술 정정, 10개 환경변수 및 해석 계층 문서화 |
| [`IMPROVEMENTS.md`](IMPROVEMENTS.md) | B-14/B-15 완료 상태 반영, O-6 신설, B-17/B-18 신설 등록 |
| [`VERSIONS.md`](VERSIONS.md) | `v2.0.0` 릴리스 노트에 메시징 백플레인 고도화 및 내결함성 패치 기록 |
| [`PRIVATE_SERVER.md`](nats-docker/PRIVATE_SERVER.md) | 스파이크 결과 반영 및 최종 가이드 확정 |
| [`deploy/install.sh`](deploy/install.sh) | `requirements.txt` 확인 (paho 유지) 및 개인 브로커 안내 추가 |
| [`.mam.env`](.mam.env) | `MQTT_BROKER`, `MQTT_PORT`, `MQTT_TLS` 기본 템플릿 확정 |

---

## 8. 진행 추적 체크리스트

### M0: 문서 정합성 확보
- [x] `PRIVATE_SERVER.md` E-1~E-4 교정 및 다능성 절(§5) 추가
- [x] `implementation_plan.md` 4개 트랙 및 마일스톤 수립
- [x] `tests/test_deploy_freshness.py` 내 G-D1 ~ G-D4 문서 드리프트 가드 구현

### M1: Track 0 내결함성 확보 (`B-14`, `B-15`)
- [x] Step 1: `publish_event.py` 상태 동기화 선행 처리 (`B-14` / G-1~G-4)
- [x] Step 2: `job_subscriber.py` 로컬 디스크 폴백 도입 (`B-15` / G-5~G-8)
- [x] Step 3: `multi-agent-mux-delegate-job` 인프라 `rc=3` 에러 분리 (`F-4` / G-9~G-10)
- [x] M1 통합 검증 (브로커 다운 상태 위임 3초 완주)

### M2a: Track 1 `nats-server` 로컬 실증 (`O-5`)
- [ ] 격리 클론 생성 (`$SCRATCH/nats-spike`)
- [ ] S-1 ~ S-9 스파이크 매트릭스 검증 수행
- [ ] S-3 Retained 메시지 게이트 통과 확인

### M2b: Track 1R 원격 프로덕션 전환
- [x] D-1 `store_dir` 절대경로 교정 + 비인용 heredoc (`PRIVATE_SERVER.md` §4.1, §9.1)
- [x] D-2 이미지 핀 `nats:2.12-alpine` + `/healthz` healthcheck
- [x] D-3 8222/8080 바인드 주소 한정
- [x] D-4 TLS 절의 IP 예시 제거 및 DNS/SAN 요건 명시
- [x] D-5 `.mam.env.example` 정합 (`MQTT_KEEPALIVE` 추가)
- [x] N-1 §5.2 에 retained=MQTT 전용 경계 명문화 + §9.1 `mam_observer` 추가
- [x] 신규 가드 G-D5 ~ G-D9, G-R1, G-R2 구현 및 검증 (290 -> 297)
- [x] P0.5: `docker/` 프로덕션 배포 자산 정본화 및 D-22 ~ D-30 회귀 가드 (297 -> 306)
- [x] P0.6: `docker/` 자산의 `nats-docker` 서브모듈 분리 및 `PRIVATE_SERVER.md`/`NATS_REPORT.md` 이전 (`629a67f`, `12ba30b`, `916185c`)
- [x] P0.6: 테스트 동적 경로 해석기(`_resolve_private_server_doc` / `_resolve_docker_dir`) 도입
- [x] P0.6: CI checkout 에 `submodules: recursive` 적용 (`deploy/gitea-ci.yml`)
- [ ] 서버 배포 및 R-3 노출 면적 0 단언
- [x] §9.5 사설 브로커(`vm-ubuntu`)로 `.mam.env` 전환 완료 (사후 잔여 드레인 스캔 과제 기록)
- [ ] R-1 ~ R-13 전건 통과 (R-5 / R-9 / R-13 최종 관문)

### M3: Track 2 보안 및 토픽 격리 (`A-2`, `B-16`)
- [ ] G-11 무조건 `auth_token` 발급 적용
- [ ] 워크스페이스 지문 토픽 발행 전환 및 레거시 구독 제거

### M4: Track 3 문서 및 배포 동기화
- [ ] `MESSAGING.md`, `IMPROVEMENTS.md`, `VERSIONS.md`, `deploy/*` 최종 갱신

