docs: synchronize MESSAGING.md, IMPROVEMENTS.md, implementation_plan.md and add D-31/D-32 freshness guards
This commit is contained in:
@@ -0,0 +1,375 @@
|
|||||||
|
# 🔎 문서 정합성 검증 및 동기화 계획서 Rev.2 (Job `eb04e918`)
|
||||||
|
|
||||||
|
- **작성일**: 2026-08-23
|
||||||
|
- **역할**: Planner (`.agents/MULTI_AGENT_RULES.md` §1 — Planner 는 저장소 코드/문서를 **수정하지 않으며**, 산출물은 본 보고서입니다)
|
||||||
|
- **기준 커밋**: `916185c`, 작업 트리 clean, `main` 은 `origin/main` 보다 **ahead 2**
|
||||||
|
- **선행 리비전**: `1fa7183a` (Rev.1) ← 본 문서가 대체합니다
|
||||||
|
- **판정 대상 리뷰**: `b93680ab` (agy, `[VERDICT: PASS WITH CHALLENGE]`) — CI 서브모듈 인증 / D-31 스코프 / B-17 fail-closed
|
||||||
|
- **검증 대상**: `MESSAGING.md`, `IMPROVEMENTS.md`, `implementation_plan.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. 리뷰 판정 (Adjudication of Challenge `b93680ab`)
|
||||||
|
|
||||||
|
### A-0. 판정 요약
|
||||||
|
|
||||||
|
| 챌린지 | 판정 | 핵심 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| **C1** 서브모듈 인증·URL 제약 | 🟢 **전제 확증 — 다만 처방 형태는 틀림** | `laa/nats-docker` 는 실제로 **비공개**(익명 `ls-remote` → `Failed to authenticate user`). 그러나 제안된 `url = ../nats-docker` 는 **`tmpl/nats-docker`** 로 해석되어 **잘못된 조직**을 가리킴(실측). 올바른 형태는 `../../laa/nats-docker` |
|
||||||
|
| **C2** D-31 과도한 제약 | ✅ **전면 수용 — Rev.1 의 논거가 틀렸음** | `lint-shell`/`lint-python` 은 `.agents/`·`deploy/` 만 훑으며 서브모듈 경로를 읽지 않음(실측). Rev.1 이 내세운 "비대칭" 논거는 성립하지 않음 |
|
||||||
|
| **C3** 명시적 `MAM_ENV_FILE` fail-closed | ✅ **원칙 수용 — 다만 차단 지점을 옮겨야 함** | `_load_dotenv()` 는 **import 시점**에 호출되고(`mqtt_common.py:112`) 테스트 3개 파일이 `mqtt_common` 을 import 함. 여기서 예외를 던지면 스위트 자체가 붕괴 |
|
||||||
|
|
||||||
|
리뷰어의 세 지적은 모두 실재하는 맹점을 짚었고, 그중 둘은 **Rev.1 의 처방을 직접 교정**합니다. 다만 C1 의 구체적 처방과 C3 의 차단 지점은 그대로 구현하면 각각 서브모듈을 깨뜨리거나 테스트 스위트를 깨뜨립니다. 아래에서 측정으로 교정합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### A-1. C1 — 전제는 옳다. 처방의 형태가 틀렸고, 처방만으로는 부족하다
|
||||||
|
|
||||||
|
#### (1) 전제 확증: 서브모듈은 실제로 비공개다
|
||||||
|
|
||||||
|
익명(자격증명 없이) `ls-remote` 실측:
|
||||||
|
|
||||||
|
| 대상 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| `https://git.godopu.com/laa/nats-docker` | 🔴 `remote: Failed to authenticate user` → **비공개** |
|
||||||
|
| `https://git.godopu.com/tmpl/multi-agent-mux` (상위 저장소) | 🟢 `629a67f… HEAD` 응답 → **공개** |
|
||||||
|
|
||||||
|
리뷰어가 가정한 "비공개 서브모듈이면 토큰이 전파되지 않아 실패" 시나리오는 **가정이 아니라 현실**입니다. Rev.1 의 T-1(`submodules: recursive` 한 줄 추가)만으로는 CI 가 여전히 실패합니다. 이 지적은 Rev.1 의 실질적 결함을 잡아냈습니다.
|
||||||
|
|
||||||
|
더 나아가 실측이 드러낸 구조는 리뷰어가 알던 것보다 까다롭습니다: **상위 저장소는 공개, 서브모듈은 비공개, 게다가 서로 다른 조직**(`tmpl/` vs `laa/`). 즉 CI 러너가 상위 저장소를 익명으로 받을 수 있어도 서브모듈에는 별도 권한이 필요합니다.
|
||||||
|
|
||||||
|
#### (2) 처방 형태 교정: `../nats-docker` 는 잘못된 저장소를 가리킨다
|
||||||
|
|
||||||
|
git 의 상대 서브모듈 URL 은 **상위 저장소의 origin URL 기준**으로 해석됩니다. 실측(임시 저장소에 origin 을 동일하게 설정하고 `git submodule init` 으로 해석 결과 확인):
|
||||||
|
|
||||||
|
```
|
||||||
|
origin = https://git.godopu.com/tmpl/multi-agent-mux
|
||||||
|
|
||||||
|
url = ../nats-docker -> https://git.godopu.com/tmpl/nats-docker ❌ 조직 불일치
|
||||||
|
url = ../nats-docker.git -> https://git.godopu.com/tmpl/nats-docker.git ❌ 조직 불일치
|
||||||
|
url = ../../laa/nats-docker -> https://git.godopu.com/laa/nats-docker ✅ 정확
|
||||||
|
```
|
||||||
|
|
||||||
|
실제 저장소는 `laa/` 아래에 있으므로, 리뷰어가 제시한 두 형태(`../nats-docker`, `../nats-docker.git`)를 그대로 적용하면 **존재하지 않는 경로**를 가리켜 서브모듈이 아예 클론되지 않습니다. 상위 저장소와 서브모듈이 같은 조직에 있다는 암묵적 가정이 이 인스턴스에서는 성립하지 않습니다.
|
||||||
|
|
||||||
|
#### (3) 처방 충분성 교정: 상대 URL 은 인증을 해결하지 않는다
|
||||||
|
|
||||||
|
상대 URL 이 물려받는 것은 **프로토콜과 호스트**이지 **권한**이 아닙니다. SSH 로 상위를 클론하면 서브모듈도 SSH 로 가므로 키가 재사용되는 이점은 실재하지만, HTTPS + 토큰 조합에서는 토큰의 스코프가 `laa/nats-docker` 를 포함해야 합니다. 상위가 공개이고 서브모듈이 비공개인 현 구조에서는 **상대 URL 로 바꿔도 자격증명은 여전히 별도로 공급**해야 합니다.
|
||||||
|
|
||||||
|
따라서 T-1 은 한 줄 추가가 아니라 세 부분으로 확장됩니다(§4 T-1a/T-1b/T-1c).
|
||||||
|
|
||||||
|
#### (4) 실측으로 드러난 제3의 선택지 — 서브모듈 공개 전환
|
||||||
|
|
||||||
|
`nats-docker` 가 추적하는 파일은 **10개뿐이며 비밀을 담은 파일이 0개**입니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
.agents/skills/env-generator/SKILL.md docker/.env.example
|
||||||
|
.agents/skills/env-generator/scripts/… docker/README.md
|
||||||
|
.gitignore docker/docker-compose.yaml
|
||||||
|
NATS_REPORT.md docker/nats.conf
|
||||||
|
PRIVATE_SERVER.md
|
||||||
|
README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- `.gitignore` 가 `.env` / `*.env` 를 제외하고 `!*.env.example` 만 허용 — 실제 시크릿은 추적 대상이 아님.
|
||||||
|
- `docker/.env.example` 은 설계상 **빈 값**(D-25(d) 가 봉인).
|
||||||
|
- `docker/nats.conf` 는 모든 `password:` 가 `$VAR` 참조(D-25(e) 가 봉인).
|
||||||
|
|
||||||
|
즉 이 저장소를 공개해도 유출되는 비밀은 없습니다. 남는 것은 "배포 토폴로지를 공개할 것인가"라는 **정책 판단**이므로 일방적으로 처방하지 않고 §4 에서 3개 선택지로 제시합니다. 다만 공개 전환은 CI 인증 문제를 **완전히 소멸**시키는 유일한 선택지입니다.
|
||||||
|
|
||||||
|
#### (5) 부수 실측 — 폭발은 아직 안 터졌을 뿐이다
|
||||||
|
|
||||||
|
`git status -sb` → `## main...origin/main [ahead 2]`. 즉 `12ba30b`(문서 서브모듈 이전)와 `916185c` 는 **아직 푸시되지 않았고**, 원격 HEAD 는 `629a67f` 입니다. CI 는 아직 이 변경을 본 적이 없습니다. **다음 푸시 순간 S-1 이 발현**하므로 T-1 은 푸시 이전에 완료되어야 합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### A-2. C2 — 전면 수용. Rev.1 의 논거가 틀렸다
|
||||||
|
|
||||||
|
Rev.1 은 "test 잡만 고치면 lint/compile 잡이 서브모듈 없는 트리를 훑는 **비대칭**이 남는다"는 이유로 세 checkout 전부에 `submodules` 를 요구했습니다. 실측 결과 이 논거는 성립하지 않습니다.
|
||||||
|
|
||||||
|
| 잡 | 실제로 읽는 경로 | 서브모듈 필요 |
|
||||||
|
|---|---|:---:|
|
||||||
|
| `lint-shell` | `.agents/skills/**`, `.agents/hooks/…`, `deploy/*.sh` (shellcheck 대상 15개 파일 명시) | ❌ |
|
||||||
|
| `lint-python` | `.agents/skills/multi-agent-mux-delegate-job/scripts/`, `.agents/skills/lib_py/` (flake8·py_compile) | ❌ |
|
||||||
|
| `test` | `pytest tests/ -q` → D-11~D-19, D-22~D-30 이 `nats-docker/**` 를 읽음 | ✅ |
|
||||||
|
|
||||||
|
lint 잡들은 서브모듈 경로를 **한 번도 참조하지 않습니다**. 없는 트리를 훑는 "비대칭"은 관측 가능한 결과를 낳지 않으므로 교정 대상이 아니었습니다. 리뷰어의 두 지적(불필요한 네트워크 I/O, 향후 경량 워크플로에서의 false positive)이 옳습니다.
|
||||||
|
|
||||||
|
**다만 리뷰어 처방에 한 가지를 더합니다 — 공허 통과 방지.** "pytest 를 실행하는 잡"으로 스코프를 좁히면, 잡 이름을 바꾸거나 `pytest` 를 래퍼 스크립트(`make test`, `bash deploy/run-tests.sh`) 뒤로 숨기는 순간 가드가 **검사 대상 0건으로 조용히 통과**합니다. 따라서 D-31 은 테스트 수행 잡을 **하나도 못 찾으면 실패**해야 합니다. 이것이 없으면 스코프 축소가 곧 가드 무력화 경로가 됩니다.
|
||||||
|
|
||||||
|
**구현 실측 참고**: PyYAML 로 `deploy/gitea-ci.yml` 을 파싱하면 최상위 키가 `['name', True, 'jobs']` 로 나옵니다 — YAML 1.1 이 `on:` 을 불리언 `True` 로 해석하는 알려진 함정입니다. D-31 은 `jobs` 만 읽으므로 영향은 없으나, Creator 가 `d["on"]` 에 접근하면 `KeyError` 를 만납니다. 현재 세 잡 모두 checkout 스텝 1개 · `with` 는 `None` 이며, `pytest` 가 포함된 잡은 `test` **하나**입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### A-3. C3 — 원칙 수용. 그러나 "기동 차단"을 import 시점에 두면 스위트가 죽는다
|
||||||
|
|
||||||
|
#### (1) 리뷰어가 옳은 부분
|
||||||
|
|
||||||
|
Rev.1 의 처방은 "`MAM_ENV_FILE`(존재할 때만) → `MAM_REAL_ROOT` → … → `walk_up(cwd)`" 순서였습니다. 이는 사용자가 **명시적으로 지정한** 경로가 없을 때 상위 디렉터리의 다른 `.mam.env` 를 임의로 집어 든다는 뜻이고, 리뷰어 지적대로 **명시적 설정 우선 원칙 위반**입니다. 다른 프로젝트의 브로커/계정으로 조용히 붙을 위험이 실재합니다. 이 부분은 Rev.1 의 설계 오류이며 수정합니다.
|
||||||
|
|
||||||
|
#### (2) 그러나 차단 지점은 옮겨야 한다
|
||||||
|
|
||||||
|
`mqtt_common.py:112` 는 모듈 최상위에서 `_load_dotenv()` 를 호출합니다 — 즉 **import 부작용**입니다. 그리고 `mqtt_common` 을 import 하는 테스트 파일이 3개 있습니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/test_tier1_unit.py
|
||||||
|
tests/test_tier2_component.py
|
||||||
|
tests/test_deploy_freshness.py ← D-19/D-27 이 DEFAULT_TOPIC_ROOT 만 읽으려고 import
|
||||||
|
```
|
||||||
|
|
||||||
|
여기서 예외를 던지면, 낡은 `MAM_ENV_FILE` 이 환경에 남아 있는 **모든** 상황에서 `import mqtt_common` 이 실패하고 스위트가 수집 단계에서 붕괴합니다. 브로커에 접속할 의도가 전혀 없는 소비자(상수 하나 읽는 테스트)까지 함께 죽습니다.
|
||||||
|
|
||||||
|
#### (3) 종합 처방 — 기록은 import 에서, 거부는 접속 지점에서
|
||||||
|
|
||||||
|
| 단계 | 동작 |
|
||||||
|
|---|---|
|
||||||
|
| **import (`_load_dotenv`)** | `MAM_ENV_FILE` 이 설정됐는데 파일이 없으면 → `logger.error("MAM_ENV_FILE is set to %s but no such file; refusing to auto-discover", path)` 후 **모듈 전역 플래그** `_env_file_missing = True` 설정. **자동 탐색을 시도하지 않음**(리뷰어 요구 반영). **예외를 던지지 않음** |
|
||||||
|
| **`MAM_ENV_FILE` 미설정** | 순서 있는 탐색 수행: `MAM_REAL_ROOT` → `WORKSPACE_ROOT` → `walk_up(__file__)` → `walk_up(cwd)` |
|
||||||
|
| **접속 지점 (`make_client()` / 브로커 설정 확정)** | ① `_env_file_missing` 이면 **명시적 예외로 거부**(fail-closed). ② 해석된 호스트가 내장 공개 기본값(`broker.hivemq.com`)과 같으면 **눈에 띄는 보안 경고** 출력 |
|
||||||
|
|
||||||
|
이 배치가 두 요구를 모두 만족시킵니다: 명시적 설정이 깨졌을 때 조용히 다른 환경으로 새지 않고(리뷰어 C3-1), 자동 탐색이 아무것도 못 찾아 공개 브로커로 떨어질 때 반드시 경고가 나오며(리뷰어 C3-2), 그러면서도 읽기 전용 소비자의 import 를 깨뜨리지 않습니다.
|
||||||
|
|
||||||
|
**보조 실측** — `_parse_env_file` 은 `if key and key not in os.environ` 로 기록하므로 **OS 환경변수가 파일보다 우선**합니다. 따라서 사용자가 `MQTT_BROKER` 를 직접 export 한 경우에는 공개 기본값으로 떨어지는 일이 애초에 없습니다. 위 ②의 조건을 "`MAM_ENV_FILE` 부재"가 아니라 "**해석 결과가 공개 기본값과 일치**"로 잡은 이유이며, 이 편이 탐색 경로 전체를 한 번에 덮습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. Rev.1 → Rev.2 변경 요약
|
||||||
|
|
||||||
|
| # | 변경 | 출처 |
|
||||||
|
|---|---|---|
|
||||||
|
| C-1 | **T-1 을 T-1a/T-1b/T-1c 로 분할** — `.gitmodules` 상대 URL은 `../../laa/nats-docker`(리뷰어 제시 형태는 오답), 비공개 서브모듈 자격증명 공급, 3개 선택지 비교 | A-1 |
|
||||||
|
| C-2 | **D-31 스코프 축소** — "모든 checkout" → "테스트 수행 잡의 checkout". **공허 통과 방지 단언 추가** | A-2 |
|
||||||
|
| C-3 | **T-9(B-17) 처방 재설계** — import 시점 기록 + 접속 지점 거부의 2단 구조. 명시적 경로 실패 시 자동 탐색 금지 | A-3 |
|
||||||
|
| C-4 | 신규 발견 **S-13**(미푸시 2커밋 — S-1 발현 시점), **S-14**(공개 상위 / 비공개 서브모듈 비대칭) | A-1(5), A-1(1) |
|
||||||
|
| C-5 | Rev.1 의 T-1 논거(“lint 잡 비대칭”) **철회** — 실측상 성립하지 않음 | A-2 |
|
||||||
|
| C-6 | D-31 구현 주의 추가 — PyYAML 이 `on:` 을 `True` 키로 파싱 | A-2 |
|
||||||
|
|
||||||
|
Rev.1 의 판정, 실측 원장(V-1~V-15), 발견 S-1~S-12, 작업 T-2~T-8·T-10~T-14, 가드 D-32 는 리뷰에서 전면 동의를 받았으며 변경 없이 유지합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 판정
|
||||||
|
|
||||||
|
테스트는 전건 통과하나 **문서 동기화 목표는 여전히 미충족**입니다(구현이 아직 수행되지 않았으므로 Rev.1 판정 유지).
|
||||||
|
|
||||||
|
- `MESSAGING.md` 는 NATS·JetStream·Docker·원격·Tailscale 을 **0건** 언급하며, 확정 표준(`nats-server` MQTT **3.1.1**)과 모순되는 서술(`MQTT 5.0` / `Mosquitto·EMQX`)을 프로덕션 표준으로 제시합니다.
|
||||||
|
- `IMPROVEMENTS.md` 는 해결된 B-14/B-15 를 미해결로 집계하고, Track 1R·D-22~D-30·서브모듈 전환을 0건 반영했습니다.
|
||||||
|
- CI 는 서브모듈을 받지 않아 배포 신선도 가드 29건 중 **18건이 실패**하며(실측), 서브모듈이 **비공개**이므로 `submodules: recursive` 한 줄로는 해결되지 않습니다(신규).
|
||||||
|
|
||||||
|
**[VERDICT: NOT PASS]**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 테스트 실행 결과
|
||||||
|
|
||||||
|
| 명령 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| `.venv/bin/python -m pytest tests/test_deploy_freshness.py tests/test_sanity.py -q` | **31 passed in 21.43s** |
|
||||||
|
| `.venv/bin/python -m pytest tests/ -q` (전체) | **306 passed in 375.81s** (exit 0) |
|
||||||
|
| `pytest tests/ -q --collect-only` | **306 collected** |
|
||||||
|
|
||||||
|
문서 회귀 0건. 양호 항목(조치 불필요): `_resolve_private_server_doc()`·`_resolve_docker_dir()` 3-후보 폴백 구현 ✅ / D-16 구멍 교정(`assert "alpine" in tag`) ✅ / `requirements.txt` 에 `PyYAML>=6.0` 추가 ✅ / CI 의 PyYAML 은 스킬 `requirements.txt` 의 `pyyaml` 로 확보되어 **결함 아님** ✅ / `implementation_plan.md` §5 P0.5·R-1~R-13 및 서브모듈 링크(`:7`, `:147`) 갱신 ✅.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 실측 원장
|
||||||
|
|
||||||
|
Rev.1 의 V-1 ~ V-15 는 유지하며, 본 리비전에서 다음을 추가 측정했습니다.
|
||||||
|
|
||||||
|
| # | 검증 | 방법 | 결과 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **V-16** | 서브모듈 공개 여부 | 자격증명 없이 `git ls-remote https://git.godopu.com/laa/nats-docker` | 🔴 `remote: Failed to authenticate user` → **비공개** |
|
||||||
|
| **V-17** | 상위 저장소 공개 여부 | 동일 방식 `…/tmpl/multi-agent-mux` | 🟢 ref 목록 응답 → **공개** (원격 HEAD `629a67f`) |
|
||||||
|
| **V-18** | 상대 URL 해석 | 임시 저장소에 동일 origin 설정 후 `git submodule init` | `../nats-docker` → `tmpl/nats-docker` ❌ / `../../laa/nats-docker` → `laa/nats-docker` ✅ |
|
||||||
|
| **V-19** | 서브모듈 비밀 노출 | `git -C nats-docker ls-files` + `.gitignore` | 추적 파일 **10개, 비밀 파일 0개**. `.env` 제외, `.env.example` 빈 값, `nats.conf` 전부 `$VAR` |
|
||||||
|
| **V-20** | 미푸시 커밋 | `git status -sb` | `## main...origin/main [ahead 2]` — `12ba30b`, `916185c` 미푸시 |
|
||||||
|
| **V-21** | lint 잡의 서브모듈 의존 | `deploy/gitea-ci.yml:15-80` 의 shellcheck/flake8/py_compile 대상 경로 | `.agents/**`, `deploy/*.sh` 만 — **서브모듈 참조 0건** |
|
||||||
|
| **V-22** | CI YAML 파싱 | PyYAML `safe_load` | 최상위 키 `['name', True, 'jobs']` (`on:` → 불리언). `pytest` 포함 잡 = `test` **1개**, 세 잡 모두 checkout 1개 · `with` 는 `None` |
|
||||||
|
| **V-23** | `_load_dotenv` 호출 시점 | `mqtt_common.py:112` | **모듈 최상위 = import 부작용** |
|
||||||
|
| **V-24** | `mqtt_common` import 소비자 | `grep -rln "import mqtt_common" tests/` | `test_tier1_unit.py`, `test_tier2_component.py`, `test_deploy_freshness.py` — **3개** |
|
||||||
|
| **V-25** | 환경변수 우선순위 | `_parse_env_file`: `if key and key not in os.environ` | **OS 환경변수가 `.mam.env` 보다 우선** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 발견 사항
|
||||||
|
|
||||||
|
Rev.1 의 S-1 ~ S-12 를 유지하고, S-1 을 갱신하며 S-13/S-14 를 신설합니다. (S-2 ~ S-12 상세는 Rev.1 과 동일하므로 요지만 재수록합니다.)
|
||||||
|
|
||||||
|
### 🔴 S-1 (P1, CI 차단) — **갱신**: 서브모듈 미체크아웃 + 비공개 저장소 인증
|
||||||
|
|
||||||
|
`deploy/gitea-ci.yml` 의 checkout 3곳(`:21`, `:53`, `:87`)이 옵션 없이 `actions/checkout@v3` 를 씁니다. 트리를 복제해 `nats-docker/` 를 비운 시뮬레이션에서 **18 failed, 11 passed**(D-11~D-19, D-22~D-30 전멸)를 실측했습니다.
|
||||||
|
|
||||||
|
**Rev.2 갱신**: `submodules: recursive` 추가만으로는 부족합니다. 서브모듈이 **비공개**(V-16)이고 상위 저장소는 **공개**(V-17)이며 **서로 다른 조직**이므로, 러너에 `laa/nats-docker` 읽기 권한이 별도로 공급되어야 합니다. §4 T-1a/T-1b/T-1c 참조.
|
||||||
|
|
||||||
|
### 🔴 S-13 (P1, 타이밍) — **신설**: 아직 푸시되지 않았을 뿐이다
|
||||||
|
|
||||||
|
`main` 이 `origin/main` 보다 **ahead 2**(V-20). 원격 HEAD 는 `629a67f` 이고, 서브모듈 문서 이전 커밋 `12ba30b`·`916185c` 는 로컬에만 있습니다. CI 는 아직 이 상태를 본 적이 없으며, **다음 푸시 순간 S-1 이 발현**합니다. T-1 은 푸시 이전에 완료되어야 하며, 그렇지 않으면 `main` 브랜치 CI 가 즉시 빨간불이 됩니다.
|
||||||
|
|
||||||
|
### 🟠 S-14 (P2, 구조) — **신설**: 공개 상위 / 비공개 서브모듈 비대칭
|
||||||
|
|
||||||
|
상위 저장소는 누구나 클론할 수 있으나(V-17) 서브모듈은 자격증명을 요구합니다(V-16). 결과적으로 **외부 사용자가 `deploy/install.sh` 경로로 이 프레임워크를 받으면 `nats-docker/` 는 빈 디렉터리**가 됩니다. 현재는 `install.sh` 가 `docker/` 나 `PRIVATE_SERVER.md` 를 배포하지 않으므로(Rev.1 D-8) 실사용에 지장은 없지만, 저장소를 클론해 테스트를 돌리려는 외부 기여자는 **18건 실패**를 만나게 됩니다. §4 T-1c 의 선택지 A(공개 전환)가 이 문제까지 함께 해소합니다.
|
||||||
|
|
||||||
|
### 나머지 발견 (Rev.1 유지, 요지)
|
||||||
|
|
||||||
|
| ID | 요지 |
|
||||||
|
|---|---|
|
||||||
|
| 🔴 **S-2** (P1) | `MESSAGING.md` 에 nats/jetstream/docker/remote/tailscale **0건**. §1.2 가 "MQTT **5.0** … Mosquitto or EMQX" 를 프로덕션 표준으로 제시 — NATS 는 MQTT 5.0 미지원이므로 단순 구식이 아니라 모순. §1.3 은 Mosquitto 설정을 유일한 레퍼런스로 제시 |
|
||||||
|
| 🔴 **S-3** (P1) | `MESSAGING.md` §6.1-3 이 이미 해결된 B-15 를 현재 제약으로 서술("it exits, leaving the running herdr agent orphaned"). 실제로는 `job_subscriber.py:60 _check_disk_fallback`, `:230`, `:244`, `return 3` 존재. §4.2 도 B-14 수정 미반영 |
|
||||||
|
| 🟠 **S-4** (P2) | `MESSAGING.md` 가 `broker_config_from_env` 파싱 10종 중 8종만 문서화 — `MQTT_CLIENT_ID_PREFIX`, `MQTT_KEEPALIVE` 누락. `.mam.env` 해석 순서(`_load_dotenv`) 절 부재 |
|
||||||
|
| 🔴 **S-5** (P1) | `IMPROVEMENTS.md:3-6` 이 `276/276`, 미해결 5건(B-14·B-15 포함), 완료 24건. 실제로는 306/306, B-14/B-15 는 `c6b6c77` 에서 해결·G-1~G-10 봉인. 제목의 `✅ 완료` 마커도 이 둘만 누락(다른 42개는 보유) → 미해결 **3건**, 완료 **26건**. **A-2 는 M3 미완이므로 미해결 유지** |
|
||||||
|
| 🔴 **S-6** (P1) | `IMPROVEMENTS.md` 에 `D-22`~`D-30`, `nats-docker`, `submodule`, `Track 1R` **0건**. 커밋 5종(`3523b9b`, `b09d420`, `629a67f`, `12ba30b`, `916185c`)의 성과가 백로그에 부재 |
|
||||||
|
| 🔴 **S-7** (P1, 보안) | `B-17`/`B-18` 미등록(`implementation_plan.md:143` 은 등록 요구). HEAD 재현: `MAM_ENV_FILE=<오타경로>` → `broker.hivemq.com 1883 tls=False`, 대조군 → `vm-ubuntu 1883`. `.mam.env` 가 이미 사설 브로커를 가리키므로 지금이 더 위험 |
|
||||||
|
| 🟠 **S-8** (P2) | `implementation_plan.md:3-5` 헤더가 `v1.0.0` / `a9934ad` / `276/276` — 실제 HEAD `916185c`, 306/306 |
|
||||||
|
| 🟠 **S-9** (P2) | `:23` Track 1R 변경 지점이 구 경로. `:13-16` 트랙 다이어그램에 Track 1R 부재(§2 마일스톤 도식과 불일치). `:39` 테스트 수 `276 -> 280` |
|
||||||
|
| 🟠 **S-10** (P2) | 서브모듈 전환(`629a67f`, `12ba30b`)이 로드맵에 기록 없음 |
|
||||||
|
| 🟠 **S-11** (P2) | `:177` `.mam.env` 전환 미체크인데 실제로는 `MQTT_BROKER=vm-ubuntu`, `MQTT_USERNAME=mam_agent` 로 전환 완료 — 추적기가 현실보다 뒤처짐 |
|
||||||
|
| 🟡 **S-12** (P3) | `:172` 의 `PRIVATE_SERVER.md:73`, `:146` 행 번호 인용이 낡음 → 절 번호로 교체 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 동기화 작업 명세 (Creator 범위)
|
||||||
|
|
||||||
|
**T-1 계열은 CI 를 되살리는 작업이며 S-13 때문에 다음 푸시 이전에 완료되어야 합니다.**
|
||||||
|
|
||||||
|
### T-1a — `.gitmodules` 상대 URL 전환 (선택지 C 를 택할 경우 필수, 그 외에는 권고)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[submodule "nats-docker"]
|
||||||
|
path = nats-docker
|
||||||
|
url = ../../laa/nats-docker
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **`../nats-docker` 를 쓰지 마십시오.** 상위 origin 이 `tmpl/multi-agent-mux` 이므로 `tmpl/nats-docker` 로 해석되어 존재하지 않는 저장소를 가리킵니다(V-18). 변경 후 반드시 검증:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule sync --recursive
|
||||||
|
git config --get submodule.nats-docker.url # → https://git.godopu.com/laa/nats-docker
|
||||||
|
```
|
||||||
|
|
||||||
|
효과는 **프로토콜·호스트 상속**(SSH 클론 시 서브모듈도 SSH, 미러/포크 이전 시 자동 추종)이며, **권한 문제는 해결하지 않습니다**.
|
||||||
|
|
||||||
|
### T-1b — CI checkout 에 서브모듈 활성화
|
||||||
|
|
||||||
|
`test` 잡의 checkout 스텝(`deploy/gitea-ci.yml:87`)에만 적용합니다(A-2).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
`lint-shell`/`lint-python` 은 **변경하지 않습니다** — 서브모듈 경로를 읽지 않음이 실측되었습니다(V-21).
|
||||||
|
|
||||||
|
### T-1c — 비공개 서브모듈 접근 확보 (택 1, 정책 판단 필요)
|
||||||
|
|
||||||
|
| 선택지 | 방법 | 장점 | 단점 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **A. `nats-docker` 공개 전환** 🏆 | Gitea 에서 저장소 visibility 를 public 으로 | CI 인증 문제 **완전 소멸**. 외부 기여자 S-14 도 동시 해소. 추적 파일에 비밀 0건이 실측됨(V-19) | 배포 토폴로지(포트·계정 구조)가 공개됨. 단, 비밀은 없으며 보안은 시크릿에 의존하지 모호성에 의존하지 않음 |
|
||||||
|
| **B. 러너에 읽기 토큰 주입** | `test` 잡에 `laa/nats-docker` 읽기 스코프 토큰을 secret 으로 두고, checkout 앞에 `git config --global url."https://<user>:${{ secrets.SUBMODULE_TOKEN }}@git.godopu.com/".insteadOf "https://git.godopu.com/"` | 저장소 비공개 유지 | 토큰 수명 관리 필요. 토큰이 CI 로그에 노출되지 않도록 주의. 외부 기여자는 여전히 실패 |
|
||||||
|
| **C. 배포 키 + SSH URL** | `.gitmodules` 를 SSH 로 두고 러너에 read-only deploy key 배치 (T-1a 와 병행) | 스코프가 저장소 단위로 최소화됨 | 러너 이미지에 키 배치·`known_hosts` 관리 필요. 사설 도메인 DNS/인증서 이슈는 별도 |
|
||||||
|
|
||||||
|
**권고: A.** 실측(V-19)상 공개해도 잃을 비밀이 없고, 세 선택지 중 유일하게 CI·외부 기여자·미래 미러 문제를 한 번에 없앱니다. 비공개 유지가 조직 정책이라면 B 를 택하고, 그 경우 §5 의 D-31 은 "checkout 이전에 자격증명 설정 스텝이 존재하는가"까지 검사하도록 확장하십시오.
|
||||||
|
|
||||||
|
**검증**: Rev.1 의 시뮬레이션(트리 복제 후 `nats-docker/` 를 비우고 `pytest tests/test_deploy_freshness.py -q`)을 재실행하여 `18 failed` → `0 failed` 확인. 가능하면 실제 CI 에서 `test` 잡 1회 통과까지 확인.
|
||||||
|
|
||||||
|
### T-2 ~ T-14 (Rev.1 유지, T-9 만 재설계)
|
||||||
|
|
||||||
|
| ID | 파일 | 작업 |
|
||||||
|
|---|---|---|
|
||||||
|
| **T-2** | `MESSAGING.md` §1.2 / §1.3 | 프로덕션 브로커 표준을 `nats-server`(MQTT **3.1.1**)로 재작성. mermaid 노드·ACL 예시를 `MAM` 계정 / `mam_agent`·`mam_observer` / NATS `permissions` 문법으로 교체. Mosquitto 설정은 §1.4 "대안"으로 강등하고 상세는 `nats-docker/PRIVATE_SERVER.md` 링크 |
|
||||||
|
| **T-3** | `MESSAGING.md` 신설 절 | JetStream 요구(MQTT 리스너 전제), retained=MQTT 전용 경계(N-1), MQTT-over-WebSocket `/mqtt`(N-7), 원격 노출 모델(모델 T/P) 요약 + 서브모듈 링크 |
|
||||||
|
| **T-4** | `MESSAGING.md` §4.2 / §4.3 / §6.1-3 | B-14(발행 실패와 무관한 상태 동기화), B-15(`_check_disk_fallback`), F-4(rc=3) 반영. §6.1-3 은 "해결됨" 처리하되 잔여 제약(자동 재연결 루프 부재)만 유지 |
|
||||||
|
| **T-5** | `MESSAGING.md` §4.4 | `MQTT_CLIENT_ID_PREFIX`·`MQTT_KEEPALIVE` 추가. `.mam.env` 해석 순서 절 신설, **OS 환경변수 우선**(V-25) 명기, **B-17 미해결 경고** 포함 |
|
||||||
|
| **T-6** | `IMPROVEMENTS.md` 헤더 | 갱신일 2026-08-23, `306/306`, 미해결 **3건**(A-2, B-16, O-5), 완료 **26건** |
|
||||||
|
| **T-7** | `IMPROVEMENTS.md` `:76`, `:81` | B-14·B-15 제목에 `✅ 완료` 마커 + 해결 커밋(`c6b6c77`)·가드(G-1~G-10) 기록 |
|
||||||
|
| **T-8** | `IMPROVEMENTS.md` 신설 | `O-6 (✅ 완료): 원격 프로덕션 브로커 자산 정본화 및 nats-docker 서브모듈 분리` — 커밋 5종, D-22~D-30, 동적 경로 해석기, 297→306 |
|
||||||
|
| **T-9** 🔄 | `IMPROVEMENTS.md` 신설 + 처방 | **`B-17 (P1)`** 등록. 처방을 **2단 구조**로 명시(아래 상세). `B-18` 도 함께 등록 |
|
||||||
|
| **T-10** | `implementation_plan.md` `:3-5` | 문서 버전 상향, 기준 커밋 `916185c`, `306/306` |
|
||||||
|
| **T-11** | `implementation_plan.md` `:13-16`, `:23`, `:39` | 트랙 다이어그램에 Track 1R 포함, 변경 지점을 `nats-docker/…` 경로로, 마일스톤 표 테스트 수 갱신 |
|
||||||
|
| **T-12** | `implementation_plan.md` §5, §8 | `P0.6 서브모듈 분리` 단계 + 체크리스트 3행(2행 완료, **CI 1행 미완료**) |
|
||||||
|
| **T-13** | `implementation_plan.md` `:177` | `.mam.env` 전환 실태 반영 — 체크 처리하거나 절차 미이행 사실 기록 |
|
||||||
|
| **T-14** | `implementation_plan.md` `:172` | 행 번호 인용을 절 번호로 교체 |
|
||||||
|
|
||||||
|
#### T-9 상세 — B-17 처방 (C3 반영 재설계)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# mqtt_common.py — import 시점: 기록만, 예외 없음
|
||||||
|
_env_file_missing: Optional[str] = None
|
||||||
|
|
||||||
|
def _load_dotenv(workspace_dir=None):
|
||||||
|
global _env_file_missing
|
||||||
|
explicit = os.environ.get("MAM_ENV_FILE")
|
||||||
|
if explicit:
|
||||||
|
if os.path.isfile(explicit):
|
||||||
|
_parse_env_file(explicit)
|
||||||
|
else:
|
||||||
|
_env_file_missing = explicit
|
||||||
|
logger.error(
|
||||||
|
"MAM_ENV_FILE is set to %s but no such file exists; "
|
||||||
|
"refusing to auto-discover another .mam.env", explicit)
|
||||||
|
return # 명시적 지정 시 자동 탐색 금지 (리뷰어 C3-1)
|
||||||
|
# 미설정일 때만 순서 있는 탐색 (first-hit-wins)
|
||||||
|
for cand in (_from_env("MAM_REAL_ROOT"), _from_env("WORKSPACE_ROOT"),
|
||||||
|
_walk_up(os.path.dirname(os.path.abspath(__file__))),
|
||||||
|
_walk_up(os.getcwd())):
|
||||||
|
if cand and os.path.isfile(cand):
|
||||||
|
_parse_env_file(cand); return
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 접속 지점(make_client 또는 설정 확정 함수) — 여기서 거부한다
|
||||||
|
def make_client(role, cfg):
|
||||||
|
if _env_file_missing:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"MAM_ENV_FILE points to a missing file ({_env_file_missing}); "
|
||||||
|
"refusing to connect with an unverified broker identity")
|
||||||
|
if cfg.host == "broker.hivemq.com":
|
||||||
|
logger.error("SECURITY: falling back to the PUBLIC broker "
|
||||||
|
"broker.hivemq.com — job payloads will be world-readable")
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**왜 import 에서 던지지 않는가**: `_load_dotenv()` 는 `mqtt_common.py:112` 의 import 부작용이고(V-23), 테스트 3개 파일이 브로커 접속 의도 없이 이 모듈을 import 합니다(V-24). import 에서 예외를 던지면 낡은 `MAM_ENV_FILE` 하나로 스위트 전체가 수집 단계에서 붕괴합니다.
|
||||||
|
|
||||||
|
**왜 경고 조건이 "공개 기본값과 일치"인가**: OS 환경변수가 파일보다 우선하므로(V-25), `MQTT_BROKER` 를 직접 export 한 사용자는 파일이 없어도 공개 브로커로 떨어지지 않습니다. 호스트 결과값을 기준으로 삼으면 탐색 경로 전체를 한 조건으로 덮습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 권고 신규 가드
|
||||||
|
|
||||||
|
| ID | 단언 | 공허 통과 방지 | 잡아내는 회귀 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **D-31** 🔄 | `deploy/gitea-ci.yml` 을 YAML 파싱 → 각 잡의 `run` 블록을 합쳐 `pytest` 또는 `tests/` 가 등장하면 **테스트 수행 잡**으로 판정 → 그 잡의 모든 `actions/checkout` 스텝이 `with.submodules` 를 truthy 로 가질 것. **`.gitmodules` 가 존재할 때만 활성**(서브모듈 제거 시 자동 무력화) | **테스트 수행 잡이 0건이면 FAIL** — 잡 이름 변경이나 래퍼 스크립트로 `pytest` 를 숨겨 가드를 조용히 비활성화하는 경로를 차단 | S-1 재발. 린트 잡은 검사 대상에서 제외되므로 경량 워크플로 추가를 방해하지 않음(A-2) |
|
||||||
|
| **D-32** | `MESSAGING.md` 가 문서화한 `MQTT_*` 집합 ⊇ `mqtt_common.broker_config_from_env` 가 파싱하는 집합 | 코드에서 변수 0개 추출 시 FAIL | S-4 재발. D-11 이 `PRIVATE_SERVER.md` 에 대해 하는 검사를 `MESSAGING.md` 로 확장 |
|
||||||
|
|
||||||
|
**D-31 구현 주의**: PyYAML 은 `on:` 을 불리언 `True` 키로 파싱합니다(V-22). `d["jobs"]` 만 읽으면 무해하나 `d["on"]` 접근은 `KeyError` 입니다. 현재 상태에서 이 가드는 `test` 잡 1개를 대상으로 삼고 **즉시 FAIL** 합니다(`with` = `None`) — 착수 시점에 공허 통과가 아님이 자동 증명됩니다.
|
||||||
|
|
||||||
|
**뮤테이션 수용 기준**: ① `test` 잡의 `submodules: recursive` 제거 → D-31 FAIL. ② `test` 잡 이름을 `verify` 로 변경 → **여전히 FAIL 해야 함**(`run` 내용 기준 판정). ③ `pytest tests/ -q` 를 `bash deploy/run-tests.sh` 로 감싸고 `tests/` 문자열 제거 → D-31 이 대상 0건을 만나 **FAIL**(공허 통과 방지 단언). ④ `MESSAGING.md` 에서 `MQTT_PORT` 삭제 → D-32 FAIL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 열린 질문
|
||||||
|
|
||||||
|
| # | 질문 | 기본값(무응답 시) |
|
||||||
|
|---|---|---|
|
||||||
|
| **Q-1** | `.mam.env` 전환(S-11)이 §9.5 드레인 절차를 밟은 것인가? | 밟지 않은 것으로 간주, T-13 에서 사후 잔여 스캔을 과제로 기록 |
|
||||||
|
| **Q-2** | A-2 를 완료로 전환할 시점은? | M3(지문 토픽 + 무조건 토큰) 이후 유지. 사설 브로커 전환만으로는 종결하지 않음 |
|
||||||
|
| **Q-3** | `MESSAGING.md` 의 Mosquitto 절을 삭제할 것인가? | **남김**(§1.4 로 강등). `PRIVATE_SERVER.md` §4.2 가 mosquitto 를 여전히 대안으로 제시하므로 삭제하면 두 문서가 어긋남 |
|
||||||
|
| **Q-4** | D-31 / D-32 를 이번 커밋에 포함할 것인가? | 포함 권고 |
|
||||||
|
| **Q-5** 🆕 | **T-1c 선택지 — `nats-docker` 를 공개로 전환할 것인가?** | **A(공개 전환) 권고**. 추적 파일에 비밀 0건 실측(V-19). 비공개 유지가 정책이면 B(토큰 주입) |
|
||||||
|
| **Q-6** 🆕 | B-17 의 접속 지점 거부를 예외로 할 것인가 종료 코드로 할 것인가? | **예외**(`RuntimeError`). `publish_event.py` 는 이미 B-14 로 예외를 잡아 디스크 상태를 동기화한 뒤 rc 를 매핑하므로, 예외가 루프를 멈추지 않고 fail-closed 만 달성 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 결론
|
||||||
|
|
||||||
|
- **테스트**: 충족. 요청 명령 31/31, 전체 306/306, 문서 회귀 0건.
|
||||||
|
- **`MESSAGING.md`**: 미충족(S-2, S-3, S-4).
|
||||||
|
- **`IMPROVEMENTS.md`**: 미충족(S-5, S-6, S-7).
|
||||||
|
- **`implementation_plan.md`**: 부분 충족 — 서브모듈 링크는 갱신되었으나 헤더·트랙표·다이어그램·전환 기록·상태 드리프트 잔존(S-8 ~ S-12).
|
||||||
|
- **최우선**: S-1 + S-13. CI 는 서브모듈을 받지 않고, 서브모듈은 비공개이며, 문제를 발현시킬 커밋 2개가 아직 푸시되지 않은 상태입니다. **푸시 이전에 T-1a~T-1c 를 완료하십시오.**
|
||||||
|
|
||||||
|
리뷰어 `agy` 의 세 지적은 모두 실재하는 맹점이었고, C2·C3 는 Rev.1 의 처방을 직접 교정했습니다. C1 은 전제가 옳았으나 제시된 상대 URL 형태(`../nats-docker`)가 잘못된 조직을 가리키므로 `../../laa/nats-docker` 로 교정하여 반영했습니다.
|
||||||
|
|
||||||
|
[VERDICT: NOT PASS]
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# Cross-Code Review Report — Job `9f9e7c2c`
|
||||||
|
|
||||||
|
- **Reviewer**: cline (session `herdr:canary-projects-multi-agent-mux-creator-cline`)
|
||||||
|
- **Date**: 2026-08-23
|
||||||
|
- **Changeset**: uncommitted working-tree, 6 files, +235/-48
|
||||||
|
- **Scope**: Cross-code review (lint / behavior / loss) of documentation synchronization
|
||||||
|
(`MESSAGING.md`, `IMPROVEMENTS.md`, `implementation_plan.md`), `.gitmodules` relative URL,
|
||||||
|
`deploy/gitea-ci.yml` submodule checkout, and test guards D-31/D-32 against the latest
|
||||||
|
NATS deployment + `nats-docker` submodule integration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Changeset Summary
|
||||||
|
|
||||||
|
| File | Δ | Nature |
|
||||||
|
|---|---|---|
|
||||||
|
| `.gitmodules` | 1 line | Absolute URL → relative `../../laa/nats-docker` |
|
||||||
|
| `IMPROVEMENTS.md` | +52/-2 | Header counts, new §2/§3 sections (B-14✅/B-15✅/B-16/B-17/B-18, O-6✅), §6.6 refresh |
|
||||||
|
| `MESSAGING.md` | +63/-45 | Mosquitto/EMQX → NATS broker (§1.2, ACLs, accounts), §4.4 10-env table, `.mam.env` resolution hierarchy (B-17) |
|
||||||
|
| `deploy/gitea-ci.yml` | +2/-0 | `test` job checkout gains `submodules: recursive` |
|
||||||
|
| `implementation_plan.md` | +21/-10 | Track 1R P0.6 submodule items, §7 description correction, M2b gate count (306) |
|
||||||
|
| `tests/test_deploy_freshness.py` | +86/-0 | New guards `test_d31_*` (CI submodules) and `test_d32_*` (MESSAGING.md env coverage) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Verification Methodology
|
||||||
|
|
||||||
|
1. Gathered changeset via `git diff --stat` and per-file diffs.
|
||||||
|
2. Verified `.gitmodules` relative URL resolution against the **actual** parent origin
|
||||||
|
(`git remote get-url origin` → `https://git.godopu.com/tmpl/multi-agent-mux`) and the
|
||||||
|
configured submodule URL in `.git/config` + submodule's own `origin`.
|
||||||
|
3. Confirmed on-disk existence of every `nats-docker/` path referenced in the docs; confirmed
|
||||||
|
no orphaned root-level `PRIVATE_SERVER.md` / `NATS_REPORT.md` / `docker/`.
|
||||||
|
4. Cross-checked all 10 `MQTT_*` env vars in `MESSAGING.md` §4.4 against
|
||||||
|
`mqtt_common.py` (`broker_config_from_env` + `make_client` defaults + docstring).
|
||||||
|
5. Verified `deploy/gitea-ci.yml` test job enables `submodules: recursive`.
|
||||||
|
6. Ran mandated tests: `.venv/bin/python -m pytest tests/test_deploy_freshness.py tests/test_sanity.py -q`.
|
||||||
|
7. Ran D-31/D-32 in isolation.
|
||||||
|
8. Audited `IMPROVEMENTS.md` section-header structure (`grep '^## '`) against the diff to detect
|
||||||
|
insertions that orphan or duplicate existing sections.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Verification Results
|
||||||
|
|
||||||
|
### 3.1 `.gitmodules` relative URL — PASS
|
||||||
|
- Parent origin: `https://git.godopu.com/tmpl/multi-agent-mux`.
|
||||||
|
- `../../laa/nats-docker` resolves: `/tmpl/multi-agent-mux` → `../` → `/tmpl` → `../../` →
|
||||||
|
host root → `laa/nats-docker` = **`https://git.godopu.com/laa/nats-docker`**.
|
||||||
|
- Confirmed equal to `git config --get submodule.nats-docker.url` and the submodule's own
|
||||||
|
`origin` fetch/push URL.
|
||||||
|
- Submodule checked out at `a4b6e49` (heads/main). Relative form improves org-wide mirroring
|
||||||
|
portability vs the prior absolute URL. No functional regression.
|
||||||
|
|
||||||
|
### 3.2 Submodule on-disk asset integrity — PASS
|
||||||
|
All paths referenced by the docs exist under `nats-docker/`:
|
||||||
|
- `nats-docker/docker/{docker-compose.yaml, nats.conf, .env.example, README.md}`
|
||||||
|
- `nats-docker/PRIVATE_SERVER.md`, `nats-docker/NATS_REPORT.md`
|
||||||
|
|
||||||
|
No orphaned root-level `PRIVATE_SERVER.md` / `NATS_REPORT.md` / `docker/` remain (confirmed via
|
||||||
|
`ls`; all three return "No such file or directory"). The `12ba30b` / `629a67f` migration is
|
||||||
|
complete on disk.
|
||||||
|
|
||||||
|
### 3.3 `MESSAGING.md` — PASS
|
||||||
|
- §1.2 cleanly switched from "Mosquitto/EMQX" to "NATS server (`nats:2.12-alpine`)"; mermaid
|
||||||
|
diagram, ACL accounts (`mam_agent` / `mam_observer`), and `nats-docker/docker/nats.conf`
|
||||||
|
references are consistent with the submodule assets.
|
||||||
|
- §4.4 environment table now lists **all 10** supported `MQTT_*` variables.
|
||||||
|
- `MQTT_CLIENT_ID_PREFIX` default documented as **`hermes`**, matching
|
||||||
|
`mqtt_common.py:230` (`os.environ.get("MQTT_CLIENT_ID_PREFIX", "hermes")`) and the
|
||||||
|
module docstring (`mqtt_common.py:218`). **Prior finding M-1 is RESOLVED.**
|
||||||
|
- §4.4 `.mam.env` resolution hierarchy documents B-17 fail-closed behavior (explicit
|
||||||
|
`MAM_ENV_FILE` missing → log error + `RuntimeError` at connect; public-broker security
|
||||||
|
warning). Consistent with the B-17 action direction recorded in `IMPROVEMENTS.md`.
|
||||||
|
|
||||||
|
### 3.4 `MQTT_*` env cross-check vs `mqtt_common.py` — PASS
|
||||||
|
All 10 documented vars are parsed by code: `MQTT_BROKER`, `MQTT_PORT`, `MQTT_TLS`,
|
||||||
|
`MQTT_USERNAME`, `MQTT_PASSWORD`, `MQTT_CA_CERTS`, `MQTT_CERTFILE`, `MQTT_KEYFILE`,
|
||||||
|
`MQTT_CLIENT_ID_PREFIX`, `MQTT_KEEPALIVE`. No drift. D-32 enforces presence of these 10.
|
||||||
|
|
||||||
|
### 3.5 `deploy/gitea-ci.yml` — PASS
|
||||||
|
- `test` job (line 87-89): `actions/checkout@v3` with `submodules: recursive`.
|
||||||
|
- The job runs `pytest tests/ -q` (line 112) → correctly classified as a test job by D-31.
|
||||||
|
- `lint-shell` / `lint-python` jobs intentionally omit `submodules` (they do not touch
|
||||||
|
`nats-docker/` paths) — D-31's logic only requires submodules on pytest jobs, which is
|
||||||
|
the correct, minimal scope.
|
||||||
|
|
||||||
|
### 3.6 `implementation_plan.md` — PASS
|
||||||
|
- Track 1R row updated to cite `nats-docker/PRIVATE_SERVER.md` §9 and
|
||||||
|
`nats-docker/docker/docker-compose.yaml` (submodule-prefixed) instead of root-level paths.
|
||||||
|
- M2b gate annotated with `(290 -> 297 -> 306)`.
|
||||||
|
- P0.6 checklist block added (submodule split, dynamic path resolvers, CI checkout sync).
|
||||||
|
- §7 `IMPROVEMENTS.md` description corrected: removed the prior false claim
|
||||||
|
"A-2 완료 전환, B-14/B-15/B-16/O-5 해결 상태 갱신" (A-2 is still open) and replaced with
|
||||||
|
"B-14/B-15 완료 상태 반영, O-6 신설, B-17/B-18 신설 등록" — factually accurate.
|
||||||
|
|
||||||
|
### 3.7 Mandated tests — PASS
|
||||||
|
- `pytest tests/test_deploy_freshness.py tests/test_sanity.py -q` → **33 passed** in 21.45s.
|
||||||
|
- D-31 (`test_d31_gitea_ci_submodules_in_test_job`) — PASS in isolation.
|
||||||
|
- D-32 (`test_d32_messaging_doc_covers_all_mqtt_env_vars`) — PASS in isolation.
|
||||||
|
- No doc regressions; 100% pass rate confirmed.
|
||||||
|
|
||||||
|
### 3.8 Prior-review findings disposition
|
||||||
|
- **M-1** (MESSAGING.md `MQTT_CLIENT_ID_PREFIX` default mismatch) — **RESOLVED** (now `hermes`).
|
||||||
|
- **M-2** (IMPROVEMENTS.md open-item count excluded B-18) — **RESOLVED** (now 5건 incl. B-18).
|
||||||
|
- **L-1** (§6.6 stale conclusion listing B-14/B-15) — **RESOLVED** (now lists B-16/B-17/B-18).
|
||||||
|
- **L-2** (§3 header count included completed O-6) — **PARTIALLY RESOLVED**: the new §3 (line 56)
|
||||||
|
correctly splits "추적 중 1건 / 완료 1건"; however the *old* §3 remains stale (see M-3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Detailed Findings
|
||||||
|
|
||||||
|
### M-3 (Medium) — Duplicate §2 and §3 section headers in `IMPROVEMENTS.md`
|
||||||
|
|
||||||
|
- **Location**: `IMPROVEMENTS.md` — new §2 at line 28 and new §3 at line 56; pre-existing §2 now
|
||||||
|
at line 120 and §3 at line 143.
|
||||||
|
- **Observation**: This changeset *inserted* new `## 2.` and `## 3.` sections (with updated
|
||||||
|
content: B-14/B-15 marked `✅ 완료`, B-17/B-18 added, O-6 added) immediately after the §1 intro,
|
||||||
|
but did **not remove** the pre-existing `## 2.` (Edge-case Bugs) and `## 3.` (Orchestration)
|
||||||
|
sections that remain further down. Confirmed via `grep -n '^## '` showing two `## 2.` and two
|
||||||
|
`## 3.` headers, and via `git diff` which contains only an insertion hunk (`@@ -23,6 +23,52 @@`)
|
||||||
|
with no deletion of the old sections.
|
||||||
|
- **Contradiction introduced**: the duplicate sections disagree:
|
||||||
|
- New §2 (line 30-36): B-14 and B-15 carry `✅ 완료` markers with "조치 결과 (완료 — 커밋 `c6b6c77`)".
|
||||||
|
- Old §2 (line 120-141): B-14/B-15 are described as open with "조치 방향 (Track 0 Step 1/2/3)"
|
||||||
|
and no completion marker — implying unresolved.
|
||||||
|
- New §3 (line 56): header "추적 중 1건 / 완료 1건: O-5, O-6", lists O-5 + O-6 (✅).
|
||||||
|
- Old §3 (line 143): header "1건", lists only O-5.
|
||||||
|
- Additionally, the `A-4` entry (a completed structural-improvement proposal) is now orphaned
|
||||||
|
between the new §3 and the old §2 (it originally sat under §1 Architecture).
|
||||||
|
- **Impact**: Medium. Purely documentation-level (no runtime/test effect; no D-guard asserts
|
||||||
|
section-header uniqueness). However it directly undermines the stated goal of this changeset
|
||||||
|
("synchronize documentation"): a reader navigating by section number hits contradictory
|
||||||
|
duplicate content, and stale "action direction" text for already-completed B-14/B-15 persists.
|
||||||
|
- **Recommendation**: Delete the now-redundant old §2 (lines ~120-141) and old §3 (lines ~143-153)
|
||||||
|
blocks — the new §2/§3 supersede them. Re-home `A-4` (e.g., into §1 or §5 Completed) so it no
|
||||||
|
longer dangles between sections. This is a surgical delete, not a redesign.
|
||||||
|
|
||||||
|
### M-4 (Low) — `§5` completed-tasks header count stale
|
||||||
|
|
||||||
|
- **Location**: `IMPROVEMENTS.md` line 158 — `## 5. 🎉 완료된 과제 (Completed Tasks — 24건)`.
|
||||||
|
- **Observation**: The header summary (line 6) was updated to claim **27** completed items
|
||||||
|
(adding B-14, B-15, O-6). But the §5 header still reads **24건** and the §5 body was not
|
||||||
|
extended to include B-14/B-15/O-6 (those three are instead described inline in the new §2/§3
|
||||||
|
with `✅` markers). This creates an internal count drift between the top summary and the §5
|
||||||
|
detail section.
|
||||||
|
- **Impact**: Low. Internal consistency only; not enforced by any D-guard.
|
||||||
|
- **Recommendation**: Either update §5 header to 27건 and migrate B-14/B-15/O-6 entries into §5,
|
||||||
|
or annotate §5 to note the three are tracked in §2/§3. Pick one location as the single source
|
||||||
|
of truth for the completed list.
|
||||||
|
|
||||||
|
### Note (positive)
|
||||||
|
- `MESSAGING.md` and `implementation_plan.md` changes are clean, accurate, and well-synchronized
|
||||||
|
with the NATS deployment and submodule state. No orphaned root-level files. The `MQTT_*`
|
||||||
|
table, broker architecture, ACL/account model, and `.mam.env` resolution hierarchy are all
|
||||||
|
consistent with `mqtt_common.py` and the `nats-docker/` assets.
|
||||||
|
- D-31/D-32 are well-scoped, auto-disable gracefully when prerequisites are absent, and include
|
||||||
|
anti-void assertions (they assert at least one test job exists / at least one MQTT var is
|
||||||
|
documented).
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Risk Assessment
|
||||||
|
|
||||||
|
| Area | Status |
|
||||||
|
|---|---|
|
||||||
|
| Runtime behavior | No code change outside tests/docs; behavior unaffected. PASS. |
|
||||||
|
| Test suite | 33/33 mandated tests pass; D-31/D-32 green in isolation. PASS. |
|
||||||
|
| Submodule integrity | Relative URL resolves correctly; submodule checked out; assets on disk. PASS. |
|
||||||
|
| Documentation sync (MESSAGING.md / implementation_plan.md) | Accurate and complete. PASS. |
|
||||||
|
| Documentation sync (IMPROVEMENTS.md) | New content correct, but duplicate §2/§3 + stale §5 count (M-3/M-4). Minor. |
|
||||||
|
| Loss / orphaned references | None — all `nats-docker/` doc links resolve; root-level originals removed. PASS. |
|
||||||
|
|
||||||
|
All findings (M-3, M-4) are documentation-level, non-blocking, and fixable by surgical edits.
|
||||||
|
No design-level rework is warranted; no `[ESCALATE: PLANNER]` is required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Actionable Follow-ups (optional, separate cleanup commit)
|
||||||
|
|
||||||
|
1. **M-3**: Remove the duplicate old §2 (lines ~120-141) and old §3 (lines ~143-153) blocks in
|
||||||
|
`IMPROVEMENTS.md`; re-home the orphaned `A-4` entry.
|
||||||
|
2. **M-4**: Align `§5` header count (24건) with the summary (27건), or annotate §5 to delegate
|
||||||
|
B-14/B-15/O-6 to §2/§3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Verdict
|
||||||
|
|
||||||
|
All mandated tests pass, the `.gitmodules` relative URL resolves correctly, submodule assets
|
||||||
|
are intact, `MESSAGING.md` and `implementation_plan.md` are accurately synchronized with the
|
||||||
|
NATS deployment, and the prior review's M-1/M-2/L-1 findings are resolved. The two new findings
|
||||||
|
(M-3 duplicate §2/§3 headers, M-4 stale §5 count) are documentation-level, non-blocking, and
|
||||||
|
do not affect runtime behavior or test results. No escalation to the planner is warranted.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
[submodule "nats-docker"]
|
[submodule "nats-docker"]
|
||||||
path = nats-docker
|
path = nats-docker
|
||||||
url = https://git.godopu.com/laa/nats-docker
|
url = ../../laa/nats-docker
|
||||||
|
|||||||
+50
-4
@@ -1,9 +1,9 @@
|
|||||||
# 🛠️ Multi-Agent Mux 종합 개선 및 미해결 과제 백로그 (`IMPROVEMENTS.md`)
|
# 🛠️ Multi-Agent Mux 종합 개선 및 미해결 과제 백로그 (`IMPROVEMENTS.md`)
|
||||||
|
|
||||||
- **최종 갱신일**: 2026-08-20 (`NATS_REPORT.md` 실측 분석 및 메시징 잠복 결함 B-14/B-15/B-16/O-5 발굴 반영, 276/276 통과 유지)
|
- **최종 갱신일**: 2026-08-23 (`nats-docker` 서브모듈 분리, 프로덕션 Docker 자산 정본화, 가드 D-22~D-30 도입, 306/306 통과 유지)
|
||||||
- **통합 관리 대상**: 기존 `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md` + `NATS_REPORT.md`
|
- **통합 관리 대상**: 기존 `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md` + `NATS_REPORT.md`
|
||||||
- **총 추적 미해결 과제**: **5건** (아키텍처 1건: `A-2`, 엣지케이스 및 가용성 3건: `B-14`, `B-15`, `B-16`, 오케스트레이션 1건: `O-5`)
|
- **총 추적 미해결 과제**: **5건** (아키텍처 1건: `A-2`, 엣지케이스 및 가용성 3건: `B-16`, `B-17`, `B-18`, 오케스트레이션 1건: `O-5`)
|
||||||
- **완료된 과제**: **24건** (A-1, A-3, A-4, A-5, B-1, B-3, B-4, B-5, B-7, B-8, B-9, B-10, B-13, C-1, C-2, C-3b, C-6, O-1, O-2, O-3, O-4-OrcOnboard, Herdr-0.8.0-Compat-SanitizeHash, P2-1-DelegateJobSafe-TrapFix, P2-2-C3a-C4-LegacyCleanup)
|
- **완료된 과제**: **27건** (A-1, A-3, A-4, A-5, B-1, B-3, B-4, B-5, B-7, B-8, B-9, B-10, B-13, B-14, B-15, C-1, C-2, C-3b, C-6, O-1, O-2, O-3, O-4-OrcOnboard, O-6, Herdr-0.8.0-Compat-SanitizeHash, P2-1-DelegateJobSafe-TrapFix, P2-2-C3a-C4-LegacyCleanup)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,6 +23,52 @@
|
|||||||
- 대신 **`nats-server`의 내장 MQTT 3.1.1 리스너를 전용 사설 브로커로 채택(Option C)**하여 클라이언트 코드 0줄 변경으로 NKey/JWT 계정·Subject별 ACL 격리 및 JetStream 영속성을 100% 확보하기로 확정했습니다.
|
- 대신 **`nats-server`의 내장 MQTT 3.1.1 리스너를 전용 사설 브로커로 채택(Option C)**하여 클라이언트 코드 0줄 변경으로 NKey/JWT 계정·Subject별 ACL 격리 및 JetStream 영속성을 100% 확보하기로 확정했습니다.
|
||||||
- 단, 브로커 제품과 무관하게 존재하는 **가용성 선행 결함(Track 0: B-14, B-15)**을 먼저 교정한 후 Track 1(스파이크) 및 Track 2(A-2 워크스페이스 지문 토픽 + 무조건 토큰 발급)를 순차 전개합니다.
|
- 단, 브로커 제품과 무관하게 존재하는 **가용성 선행 결함(Track 0: B-14, B-15)**을 먼저 교정한 후 Track 1(스파이크) 및 Track 2(A-2 워크스페이스 지문 토픽 + 무조건 토큰 발급)를 순차 전개합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 🟠 엣지 케이스 및 런타임 버그 (Edge-case Bugs — 3건)
|
||||||
|
|
||||||
|
### **B-14 (✅ 완료 — F-1 / P1): `publish_event.py` 브로커 장애 시 `return 2` 조기 탈출로 인한 65분 루프 정지**
|
||||||
|
- **현상**: `publish_event.py`에서 브로커 네트워크 장애 발생 시 `return 2`로 조기 종료되어, 뒤따르는 로컬 레지스트리 상태(`update_job_status(status=completed)`) 및 감사 로그(`append_event`, `registry.append_event`) 갱신이 누락되던 결함.
|
||||||
|
- **조치 결과 (완료 — 커밋 `c6b6c77`)**: 네트워크 발행 실패 여부와 무관하게 로컬 레지스트리 및 감사 로그를 100% 먼저 동기화한 후 `published=False`와 함께 `return 2`를 반환하도록 실행 순서를 재배치 (G-1 ~ G-4 회귀 가드로 봉인 완료).
|
||||||
|
|
||||||
|
### **B-15 (✅ 완료 — C1 & F-4 / P1): `job_subscriber.py` 디스크 폴백 부재 및 위임 경로 인프라 에러 오판정**
|
||||||
|
- **현상**: `job_subscriber.py`가 네트워크 큐만 대기하며 로컬 디스크 상태를 확인하지 않아 브로커 다운 시 블로킹되거나 인프라 에러가 작업 `error`로 오판정되던 결함.
|
||||||
|
- **조치 결과 (완료 — 커밋 `c6b6c77`)**: `_check_disk_fallback()`을 도입하여 로컬 디스크 상의 터미널 상태를 감지하면 합성 이벤트를 출력하고 즉시 `rc=0`으로 정상 종료하도록 개선. 브로커 인프라 접속 실패는 전용 `rc=3`으로 분리 (G-5 ~ G-10 회귀 가드로 봉인 완료).
|
||||||
|
|
||||||
|
### **B-16 (F-5 / P3): `make_client()` 매 실행 랜덤 `client_id` 발급으로 인한 영속 세션(Durable Session) 구성 불가**
|
||||||
|
- **현상**: `mqtt_common.py:258`에서 `client_id`를 매번 `uuid.uuid4().hex[:8]`로 생성하여, 브로커가 클라이언트 재연결을 식별할 수 없습니다 (`NATS_REPORT.md` §3.5 F-5).
|
||||||
|
- **파급 효과**: 네트워크 재연결 시 미수신 이벤트 유실 가능성이 발생합니다.
|
||||||
|
- **조치 방향**: B-15의 로컬 디스크 폴백을 표준 복원 경로로 확립하여 네트워크 세션 의존도를 제거하고, 필요 시 결정론적 식별자 규칙을 적용합니다.
|
||||||
|
|
||||||
|
### **B-17 (P1): `_load_dotenv` 오타/부재 경로 지정 시 Fail-Closed 및 공용 브로커 폴백 방지**
|
||||||
|
- **현상**: `MAM_ENV_FILE`이 명시적으로 지정되었으나 해당 경로가 존재하지 않는 경우, `_load_dotenv`가 조용히 리턴하여 `broker.hivemq.com` 공개 브로커로 폴백되는 위험.
|
||||||
|
- **파급 효과**: 설정 오타 발생 시 잡 이벤트와 프롬프트가 공개 브로커로 전송될 수 있음.
|
||||||
|
- **조치 방향 (2단 구조)**:
|
||||||
|
1. import 시점: 명시적 `MAM_ENV_FILE` 경로 부재 시 `logger.error` 기록 및 `_env_file_missing = True` 플래그 설정 (상위 임의 탐색 금지, import 예외 방지).
|
||||||
|
2. 접속 시점: `make_client()` 시 `_env_file_missing`이면 `RuntimeError`로 fail-closed 거부. 최종 호스트가 `broker.hivemq.com`인 경우 눈에 띄는 보안 경고 출력.
|
||||||
|
|
||||||
|
### **B-18 (P2): `.mam.env`와 `.env` 공존 및 다중 워크스페이스 경계 탐색 정합성**
|
||||||
|
- **현상**: `.mam.env`와 `.env`의 우선순위 및 워크스페이스 경계(`.agents`, `.git`) 탐색 과정에서 다중 워크스페이스 환경에서의 일관성 유지.
|
||||||
|
- **조치 방향**: `MAM_REAL_ROOT` -> `WORKSPACE_ROOT` -> 상위 경계 디렉터리 -> `cwd` 순서의 first-hit-wins 탐색 규칙 적용.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 🟡 오케스트레이션 최적화 과제 (Orchestration Optimizations — 추적 중 1건 / 완료 1건: O-5, O-6)
|
||||||
|
|
||||||
|
### **O-5 (P2): NATS/MQTT 메시징 백플레인 고도화 및 `nats-server` 스파이크 검증 (Track 1 ~ Track 2)**
|
||||||
|
- **현상**: `NATS_REPORT.md` 아키텍처 실측 분석에 따라 `nats-server` 내장 MQTT 3.1.1 어댑터를 사설 전용 브로커로 채택하는 전략(Option C)이 확정되었습니다.
|
||||||
|
- **조치 방향**:
|
||||||
|
1. **Track 1 (스파이크 검증)**: 격리 환경에서 `nats-server -js`의 MQTT 3.1.1 호환성 실측 검증.
|
||||||
|
2. **Track 2 (보안/격리)**: 워크스페이스 지문 기반 토픽(`mam/<sha256[:12]>/jobs/...`) 및 무조건 `auth_token` 발급(G-11)을 적용하여 A-2 보안 결함 완전 종결.
|
||||||
|
3. **Track 3 (문서/설정)**: `MESSAGING.md`, `VERSIONS.md`, `.mam.env`에 `nats-server` 서빙 가이드 및 설정 동기화.
|
||||||
|
|
||||||
|
### **O-6 (✅ 완료 — P1): 원격 프로덕션 브로커 자산 정본화 및 `nats-docker` 서브모듈 분리**
|
||||||
|
- **내용**:
|
||||||
|
1. 원격 Docker NATS 배포 가이드 및 자산(`docker-compose.yaml`, `nats.conf`, `.env.example`, `README.md`) 구현.
|
||||||
|
2. `nats-docker` 독립 Git 저장소 및 서브모듈(`.gitmodules`, `nats-docker/`) 분리 완료 (커밋 `629a67f`, `12ba30b`, `916185c`).
|
||||||
|
3. 배포 신선도 및 보안 회귀 가드 D-22 ~ D-30 9종 구축 (297 -> 306 tests 100% PASS 달성).
|
||||||
|
4. 테스트 프레임워크 내 `_resolve_docker_dir()` 및 `_resolve_private_server_doc()` 동적 경로 해석기 도입.
|
||||||
|
|
||||||
### **A-4 (✅ 완료 — P3-1): 에이전트 지식 산재 — `BaseAgentAdapter` 어댑터 계층 도입 (Rev.2)**
|
### **A-4 (✅ 완료 — P3-1): 에이전트 지식 산재 — `BaseAgentAdapter` 어댑터 계층 도입 (Rev.2)**
|
||||||
|
|
||||||
> 결함 조치가 아니라 **구조 개선 제안**입니다. 상세 설계·실측 근거는 `.mam/jobs/44062a63/claude-reports/report-final.md` 및 `744ac67a` 를 참조하십시오.
|
> 결함 조치가 아니라 **구조 개선 제안**입니다. 상세 설계·실측 근거는 `.mam/jobs/44062a63/claude-reports/report-final.md` 및 `744ac67a` 를 참조하십시오.
|
||||||
@@ -366,4 +412,4 @@ CHANGES_DIFF=$(
|
|||||||
|
|
||||||
### 6.6 결론
|
### 6.6 결론
|
||||||
|
|
||||||
`IMPROVEMENTS.md` 는 남은 백로그 항목(아키텍처 1건: `A-2`, 엣지케이스 및 가용성 3건: `B-14`·`B-15`·`B-16`, 오케스트레이션 1건: `O-5` — 총 5건)을 위 우선순위(Track 0 → Track 1 → Track 2)에 따라 일원화된 보완 로드맵으로 관리합니다.
|
`IMPROVEMENTS.md` 는 남은 백로그 항목(아키텍처 1건: `A-2`, 엣지케이스 및 가용성 3건: `B-16`·`B-17`·`B-18`, 오케스트레이션 1건: `O-5` — 총 5건)을 위 우선순위(Track 0 → Track 1 → Track 2)에 따라 일원화된 보완 로드맵으로 관리합니다.
|
||||||
|
|||||||
+78
-30
@@ -23,13 +23,13 @@ In the initial development/testing phase, the system defaults to the public brok
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1.2 Production Architecture (Secure Private Broker)
|
### 1.2 Production Architecture (Secure Private NATS Broker)
|
||||||
For production deployments, the system is designed to run on a private, self-hosted MQTT 5.0 broker such as **Mosquitto** or **EMQX**.
|
For production deployments, the system standardizes on a private, self-hosted **NATS server** (`nats:2.12-alpine`) with its built-in **MQTT 3.1.1** protocol engine and JetStream persistence enabled, managed via `nats-docker/docker/docker-compose.yaml`.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
subgraph "Secure Corporate Network"
|
subgraph "Secure Tailnet / Corporate Network"
|
||||||
Broker["Private MQTT Broker (Mosquitto/EMQX) <br> Ports: 8883 (TLS)"]
|
Broker["Private NATS Broker (nats:2.12-alpine) <br> Native: 4222 | MQTT: 1883 | WS: 8080"]
|
||||||
|
|
||||||
subgraph "Hermes (Delegator/Orchestrator)"
|
subgraph "Hermes (Delegator/Orchestrator)"
|
||||||
SubClient["job_subscriber.py <br> (Role: subscriber)"]
|
SubClient["job_subscriber.py <br> (Role: subscriber)"]
|
||||||
@@ -39,35 +39,55 @@ graph TD
|
|||||||
PubClient["publish_event.py <br> (Role: publisher)"]
|
PubClient["publish_event.py <br> (Role: publisher)"]
|
||||||
end
|
end
|
||||||
|
|
||||||
SubClient -- "Subscribe (QoS 1) <br> Auth: hermes <br> ACL: Read jobs/+/events" --> Broker
|
SubClient -- "Subscribe (QoS 1) <br> Auth: mam_agent / mam_observer <br> ACL: Read python/mqtt/jobs/+/events" --> Broker
|
||||||
PubClient -- "Publish (QoS 1 + Retain Terminal) <br> Auth: claude-worker <br> ACL: Write jobs/+/events" --> Broker
|
PubClient -- "Publish (QoS 1 + Retain Terminal) <br> Auth: mam_agent <br> ACL: Write python/mqtt/jobs/+/events" --> Broker
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Production Security & Hardening Controls:
|
#### Production Security & Hardening Controls:
|
||||||
1. **Transport Layer Security (TLS v1.3)**: Traffic is encrypted over port `8883` using a private Certification Authority (CA). The orchestrator validates the broker using `MQTT_CA_CERTS` (CA bundle path). Optionally, Mutual TLS (mTLS) is supported via client-side certificate keys (`MQTT_CERTFILE`/`MQTT_KEYFILE`) for cryptographic device identities.
|
1. **Transport Layer Security & Overlay Networks**: Within a trusted mesh (Tailscale / Tailnet, Model T), traffic routes over encrypted WireGuard overlays to private endpoints. For public WAN exposures (Model P), TLS v1.3 encryption is terminated via private CA certificates (`MQTT_CA_CERTS`), and mutual TLS (mTLS) is supported via client keypairs (`MQTT_CERTFILE` / `MQTT_KEYFILE`).
|
||||||
2. **Strict Client Authentication**: All clients must supply credentials (`MQTT_USERNAME` / `MQTT_PASSWORD`) to establish a connection. Anonymous logins are explicitly disabled (`allow_anonymous false`).
|
2. **Strict Client Authentication & Multi-Tenancy**: All clients authenticate against isolated NATS accounts (`MAM`, `HOME`, `SYS`) using dedicated credentials (`MQTT_USERNAME` / `MQTT_PASSWORD`). Anonymous access is explicitly disabled.
|
||||||
3. **Role-Based Topic Access Control Lists (ACLs)**:
|
3. **Role-Based Topic Access Control Lists (ACLs)**:
|
||||||
* **Orchestrator/Hermes (Subscriber)**: Authenticates as user `hermes` with read-only access to all event streams:
|
* **Worker / Agent (`mam_agent`)**: Granted full publish/subscribe access within the `MAM` account to manage job lifecycles:
|
||||||
```conf
|
```conf
|
||||||
user hermes
|
# nats-docker/docker/nats.conf
|
||||||
topic read python/mqtt/jobs/+/events
|
accounts {
|
||||||
|
MAM: {
|
||||||
|
jetstream: enabled
|
||||||
|
users: [
|
||||||
|
{ user: mam_agent, password: $MAM_BROKER_PASS }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
* **Agent/Worker (Publisher)**: Authenticates as user `claude-worker` with write-only access restricted to the job event sub-topics:
|
* **Observer / Dashboard (`mam_observer`)**: Restricted to read-only access for monitoring streams while strictly preventing unauthorized command injection:
|
||||||
```conf
|
```conf
|
||||||
user claude-worker
|
# nats-docker/docker/nats.conf
|
||||||
topic write python/mqtt/jobs/+/events
|
{ user: mam_observer, password: $MAM_OBSERVER_PASS,
|
||||||
|
permissions: {
|
||||||
|
subscribe: { allow: ["python.mqtt.jobs.>"] }
|
||||||
|
publish: { deny: [">"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
This prevents workers from eavesdropping on sister agents or intercepting commands on other jobs.
|
|
||||||
4. **Durable Message Queues & Session State**:
|
4. **Durable Message Queues & Session State**:
|
||||||
* The broker is configured with `persistence true` and a dedicated disk storage path.
|
* JetStream is activated with a dedicated persistent store path (`store_dir: "/data"`), backing MQTT QoS 1 streams and persistent client sessions.
|
||||||
* Subscribers connect with persistent session flags to ensure the broker buffers QoS 1 messages during temporary network drops.
|
5. **Retained Terminal Events**: Terminal events (`completed` / `error`) are published with `retain=True`. NATS stores retained payloads in JetStream, allowing late-joining subscribers to instantly recover final states without polling.
|
||||||
5. **Retained Terminal Events**: Terminal events (`completed`/`error`) are published with the `retain=True` flag. This allows a late-joining or recovering subscriber to instantly retrieve the final job status without waiting for active transmissions.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1.3 Production Mosquitto Configuration Reference
|
### 1.3 NATS JetStream, Retained Messages & WebSocket Integration
|
||||||
A hardened `/etc/mosquitto/mosquitto.conf` production configuration includes:
|
|
||||||
|
The production deployment in [`nats-docker/docker/nats.conf`](nats-docker/docker/nats.conf) includes key architectural primitives:
|
||||||
|
1. **JetStream Requirement for MQTT Engine**: `nats-server` requires JetStream enabled at both the server level and the account level (`jetstream: enabled`) for MQTT sessions and QoS 1 message persistence.
|
||||||
|
2. **Retained Message Scope Boundary (N-1)**: Retained messages published via MQTT are stored in JetStream by NATS and delivered to subsequent MQTT subscribers. Note that native NATS pub/sub subscribers do not receive historical retained messages upon connection unless queried via JetStream KV/Object APIs.
|
||||||
|
3. **MQTT-over-WebSocket `/mqtt` Path (N-7)**: For web dashboards and browser clients, NATS exposes WebSocket listeners on port `8080` (or `443` in TLS mode). Standard MQTT-over-WebSocket clients connect to the `/mqtt` path (e.g. `ws://<host>:8080/mqtt` or `wss://<host>:8443/mqtt`), with `no_tls: true` and `same_origin: false` configured for secure cross-origin streaming behind reverse proxies.
|
||||||
|
4. **Remote Deployment Models**: For full installation, Tailscale topology, and secret management guides, refer to [`nats-docker/PRIVATE_SERVER.md`](nats-docker/PRIVATE_SERVER.md) and [`nats-docker/NATS_REPORT.md`](nats-docker/NATS_REPORT.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.4 Alternative: Hardened Mosquitto Reference
|
||||||
|
If an environment requires a dedicated Mosquitto broker instead of NATS, a reference `/etc/mosquitto/mosquitto.conf` configuration is maintained:
|
||||||
```conf
|
```conf
|
||||||
# Persistence settings
|
# Persistence settings
|
||||||
persistence true
|
persistence true
|
||||||
@@ -229,8 +249,9 @@ Two concurrency control schemes co-exist in this workspace to coordinate state m
|
|||||||
---
|
---
|
||||||
|
|
||||||
### 4.2 `publish_event.py` (Retries and Handshakes)
|
### 4.2 `publish_event.py` (Retries and Handshakes)
|
||||||
The publisher script enforces robust error handling when sending status updates:
|
The publisher script enforces robust error handling and fail-safe local persistence:
|
||||||
* **Fresh Connection Pattern**: Instead of maintaining a persistent socket connection (which is susceptible to socket timeouts or channel leaks), `publish_event.py` opens a fresh socket, completes the authentication/TLS handshake, publishes a single QoS 1 event, waits for `PUBACK`, and closes the connection.
|
* **Fresh Connection Pattern**: Instead of maintaining a persistent socket connection (which is susceptible to socket timeouts or channel leaks), `publish_event.py` opens a fresh socket, completes the authentication/TLS handshake, publishes a single QoS 1 event, waits for `PUBACK`, and closes the connection.
|
||||||
|
* **Guaranteed Disk Synchronization (B-14)**: Before attempting any network transmission over MQTT, `publish_event.py` records the event into the local registry (`append_event` and `update_job_status`). If the broker is unreachable or network publish fails, local audit logs and state machine files remain 100% accurate. The script returns exit code `2` at the very end to signal a transport failure without corrupting local state.
|
||||||
* **Exponential Backoff**: Wrapped in the `with_retry()` decorator from `mqtt_common.py`. In case of socket errors (`OSError`, `TimeoutError`, `ConnectionError`), it retries up to 3 times (configurable via `--attempts`) with backoff:
|
* **Exponential Backoff**: Wrapped in the `with_retry()` decorator from `mqtt_common.py`. In case of socket errors (`OSError`, `TimeoutError`, `ConnectionError`), it retries up to 3 times (configurable via `--attempts`) with backoff:
|
||||||
$$\text{delay} = \min(\text{base\_delay} \times \text{factor}^{\text{attempt}-1}, \text{max\_delay})$$
|
$$\text{delay} = \min(\text{base\_delay} \times \text{factor}^{\text{attempt}-1}, \text{max\_delay})$$
|
||||||
Default parameters: `base_delay = 0.5s`, `factor = 2.0`, `max_delay = 8.0s`.
|
Default parameters: `base_delay = 0.5s`, `factor = 2.0`, `max_delay = 8.0s`.
|
||||||
@@ -243,6 +264,12 @@ The publisher script enforces robust error handling when sending status updates:
|
|||||||
### 4.3 `job_subscriber.py` (Timers and Queue Semantics)
|
### 4.3 `job_subscriber.py` (Timers and Queue Semantics)
|
||||||
The subscriber acts as the central execution watchdog:
|
The subscriber acts as the central execution watchdog:
|
||||||
* **Queue Serialization**: Uses a thread-safe `queue.Queue` internally. The Paho MQTT callback thread adds messages to the queue, and the main thread processes them sequentially. This separates network I/O from state machine validation.
|
* **Queue Serialization**: Uses a thread-safe `queue.Queue` internally. The Paho MQTT callback thread adds messages to the queue, and the main thread processes them sequentially. This separates network I/O from state machine validation.
|
||||||
|
* **Local Disk Fallback Verification (B-15)**: On initial startup and upon any broker connection failure, `_check_disk_fallback()` immediately queries local job records (`.mam/jobs/<job_id>.json`) and audit logs (`status.json`). If the target job has already reached a terminal state locally, the subscriber completes immediately without waiting on a dead broker.
|
||||||
|
* **Infrastructure Error Code Separation (F-4)**: The subscriber returns distinct exit codes:
|
||||||
|
* Exit `0`: Job completed successfully.
|
||||||
|
* Exit `1`: Job terminated with an application `error` event.
|
||||||
|
* Exit `2`: Activity idle or wall-clock timeout exceeded.
|
||||||
|
* Exit `3`: Broker infrastructure connection error (with disk fallback checked).
|
||||||
* **State Machine Protection**: To safeguard against QoS 1 duplicate delivery or out-of-order broker retries, the subscriber runs a terminal state machine. It records job completion in an internal `terminal` dictionary. Once a job is marked `completed` or `error`, any subsequent events for that `job_id` are ignored:
|
* **State Machine Protection**: To safeguard against QoS 1 duplicate delivery or out-of-order broker retries, the subscriber runs a terminal state machine. It records job completion in an internal `terminal` dictionary. Once a job is marked `completed` or `error`, any subsequent events for that `job_id` are ignored:
|
||||||
```python
|
```python
|
||||||
if event in TERMINAL_EVENTS:
|
if event in TERMINAL_EVENTS:
|
||||||
@@ -258,11 +285,32 @@ The subscriber acts as the central execution watchdog:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4.4 `mqtt_common.py` (Logging & Config Resolution)
|
### 4.4 `mqtt_common.py` (Logging, Env Vars & Config Resolution)
|
||||||
* **Log Routing isolation**: Configured via `setup_logging()`. The root logger is bound to `sys.stderr`. This preserves the standard output stream (`stdout`) exclusively for clean JSON-lines payloads, enabling downstream bash tools to pipeline event feeds cleanly (e.g., `job_subscriber.py ... | jq`).
|
* **Log Routing Isolation**: Configured via `setup_logging()`. The root logger is bound to `sys.stderr`. This preserves the standard output stream (`stdout`) exclusively for clean JSON-lines payloads, enabling downstream bash tools to pipeline event feeds cleanly (e.g., `job_subscriber.py ... | jq`).
|
||||||
* **Broker Config Resolution**: Configured in `broker_config_from_job()`. Resolves credentials hierarchically:
|
* **Environment Variable Dictionary**:
|
||||||
1. Defaults to environment configurations (e.g. `MQTT_BROKER`, `MQTT_PORT`, `MQTT_TLS`, `MQTT_CA_CERTS`).
|
The system parses and supports the following 10 configuration variables:
|
||||||
2. Overlays credentials specified inside the job record JSON block (`broker.*`). This allows the agent to fetch its dedicated target broker credentials on a per-job basis.
|
| Environment Variable | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `MQTT_BROKER` | `broker.hivemq.com` | Broker hostname or IP address (e.g., `vm-ubuntu`, `127.0.0.1`) |
|
||||||
|
| `MQTT_PORT` | `1883` | Broker port (`1883` for plaintext/Tailscale, `8883` for TLS) |
|
||||||
|
| `MQTT_TLS` | `false` | Enable TLS encryption (`true` / `false` / `1` / `0`) |
|
||||||
|
| `MQTT_USERNAME` | `""` | Authentication username (e.g., `mam_agent`, `mam_observer`) |
|
||||||
|
| `MQTT_PASSWORD` | `""` | Authentication password |
|
||||||
|
| `MQTT_CA_CERTS` | `""` | Path to CA certificate bundle for TLS verification |
|
||||||
|
| `MQTT_CERTFILE` | `""` | Path to client certificate for mutual TLS (mTLS) |
|
||||||
|
| `MQTT_KEYFILE` | `""` | Path to client private key for mutual TLS (mTLS) |
|
||||||
|
| `MQTT_CLIENT_ID_PREFIX` | `hermes` | Prefix for dynamically generated random client IDs |
|
||||||
|
| `MQTT_KEEPALIVE` | `60` | MQTT keepalive ping interval in seconds |
|
||||||
|
|
||||||
|
* **`.mam.env` Resolution Hierarchy (`_load_dotenv`)**:
|
||||||
|
Configuration files are resolved with strict precedence rules:
|
||||||
|
1. **OS Environment Precedence**: Any variable already defined in `os.environ` is preserved and never overwritten by file-based configs.
|
||||||
|
2. **Explicit Override (`MAM_ENV_FILE`)**: If `MAM_ENV_FILE` is set, only that specific file is parsed. If the specified file does not exist, an error is logged and ambient search is refused (preventing silent fallback to unintended parent configs). Connection attempts fail-closed (`RuntimeError`).
|
||||||
|
3. **Workspace Root Auto-Discovery**: If `MAM_ENV_FILE` is not set, the resolver searches candidate paths in order: `MAM_REAL_ROOT`, `WORKSPACE_ROOT`, upward directory walk searching for `.agents` or `.git` boundary markers, and `os.getcwd()`.
|
||||||
|
4. **Public Broker Security Alert (B-17)**: If the final resolved host falls back to the public sandbox `broker.hivemq.com`, a prominent security warning is emitted.
|
||||||
|
* **Broker Config Resolution (`broker_config_from_job`)**:
|
||||||
|
1. Loads baseline settings from environment / `.mam.env`.
|
||||||
|
2. Overlays job-specific overrides specified inside the job record JSON block (`broker.*`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -311,8 +359,8 @@ graph LR
|
|||||||
The advisory locking system previously relied heavily on `fcntl.flock`. While `agent-sessions.yaml` has been migrated to SQLite WAL to solve concurrent writes, the job metadata in `.mam/jobs/` still relies on `fcntl.flock` which may behave non-atomically on NFS.
|
The advisory locking system previously relied heavily on `fcntl.flock`. While `agent-sessions.yaml` has been migrated to SQLite WAL to solve concurrent writes, the job metadata in `.mam/jobs/` still relies on `fcntl.flock` which may behave non-atomically on NFS.
|
||||||
2. **Bearer Token Leakage over Plaintext (Public Broker)**:
|
2. **Bearer Token Leakage over Plaintext (Public Broker)**:
|
||||||
The `auth_token` mechanism is a simple plaintext bearer comparison. If the transport layer is unencrypted (e.g., using `broker.hivemq.com` on port `1883`), any eavesdropper on the network can steal the token and spoof legitimate events.
|
The `auth_token` mechanism is a simple plaintext bearer comparison. If the transport layer is unencrypted (e.g., using `broker.hivemq.com` on port `1883`), any eavesdropper on the network can steal the token and spoof legitimate events.
|
||||||
3. **Subscriber Network Drop Orphanage**:
|
3. **Subscriber Network Drop & Disk Fallback (Resolved via B-15 / Residual Active Reconnection Gap)**:
|
||||||
`job_subscriber.py` does not implement automatic reconnection loops. If the subscriber loses connection to the broker, it exits, leaving the running herdr agent orphaned and without a validation/collection hook.
|
`job_subscriber.py` implements on-disk status fallback (`_check_disk_fallback`) to recover state upon broker connection loss (B-15). An active in-session auto-reconnection loop during continuous execution remains a recommended enhancement.
|
||||||
4. **Lack of Ordering Guarantees in QoS 1**:
|
4. **Lack of Ordering Guarantees in QoS 1**:
|
||||||
QoS 1 guarantees delivery but not strict ordering. Under heavy backoff retries, a late-delivered progress event could land after a terminal event, causing state inconsistencies.
|
QoS 1 guarantees delivery but not strict ordering. Under heavy backoff retries, a late-delivered progress event could land after a terminal event, causing state inconsistencies.
|
||||||
|
|
||||||
@@ -325,8 +373,8 @@ graph LR
|
|||||||
**Architecture Decision Note**: This means `agent-sessions.yaml` is **no longer a real-time view** of currently `running` sessions. We have explicitly accepted the trade-off of giving up real-time text readability of running sessions in favor of robust concurrency and solving NFS flock limits. Tooling and status checks must now query the SQLite DB to observe live `running` states.
|
**Architecture Decision Note**: This means `agent-sessions.yaml` is **no longer a real-time view** of currently `running` sessions. We have explicitly accepted the trade-off of giving up real-time text readability of running sessions in favor of robust concurrency and solving NFS flock limits. Tooling and status checks must now query the SQLite DB to observe live `running` states.
|
||||||
2. **Implement Signature-Based Payload Verification**:
|
2. **Implement Signature-Based Payload Verification**:
|
||||||
Rather than sending a plaintext token, utilize HMAC signatures. The delegator and worker share a secret key; the worker publishes a signature of the payload (e.g. `HMAC-SHA256(secret_key, payload_bytes)`). The subscriber validates the signature, preventing token interception.
|
Rather than sending a plaintext token, utilize HMAC signatures. The delegator and worker share a secret key; the worker publishes a signature of the payload (e.g. `HMAC-SHA256(secret_key, payload_bytes)`). The subscriber validates the signature, preventing token interception.
|
||||||
3. **Enforce Mandatory Broker-Side TLS and ACLs**:
|
3. **Enforce Mandatory NATS Broker-Side Authentication, JetStream and ACLs**:
|
||||||
De-prioritize plaintext support. Enforce connection over port `8883` with verified TLS certificates. Implement client certificates (mTLS) for agent authentication.
|
Standardize on private `nats-server` with JetStream and account-level ACL isolation (`nats-docker/docker/nats.conf`). For public WAN exposures, terminate TLS v1.3 (`MQTT_TLS=true`) over port `8883`.
|
||||||
4. **Build Auto-Reconnecting Subscriber Loops**:
|
4. **Build Auto-Reconnecting Subscriber Loops**:
|
||||||
Upgrade `job_subscriber.py` to handle disconnect callbacks. Maintain a persistent queue in memory and allow the client to reconnect with exponential backoff, preventing socket dropout from terminating the orchestration flow.
|
Upgrade `job_subscriber.py` to handle disconnect callbacks. Maintain a persistent queue in memory and allow the client to reconnect with exponential backoff, preventing socket dropout from terminating the orchestration flow.
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
|
|||||||
+18
-13
@@ -1,26 +1,26 @@
|
|||||||
# 🚀 MAM 메시징 백플레인 전환 실행 로드맵 (`implementation_plan.md`)
|
# 🚀 MAM 메시징 백플레인 전환 실행 로드맵 (`implementation_plan.md`)
|
||||||
|
|
||||||
- **문서 버전**: v1.0.0 (`8c651798` / `28bb7340`)
|
- **문서 버전**: v1.2.0
|
||||||
- **작성/관리 주체**: Multi-Agent Orchestration Team (`claude`, `agy`, `cline`)
|
- **작성/관리 주체**: Multi-Agent Orchestration Team (`claude`, `agy`, `cline`)
|
||||||
- **기준 커밋**: `a9934ad` (276/276 baseline tests passing)
|
- **기준 커밋**: `916185c` (306/306 baseline tests passing)
|
||||||
- **문서 목적**: MAM의 메시징 인프라를 공개 HiveMQ 브로커에서 `nats-server` 전용 사설 브로커로 무중단 전환하기 위한 4개 트랙(Track 0~3)과 5단계 마일스톤(M0~M4)의 구체적 실행 지침 및 진행 상황 추적.
|
- **문서 목적**: 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)
|
- **연계 문서**: [`NATS_REPORT.md`](nats-docker/NATS_REPORT.md), [`PRIVATE_SERVER.md`](nats-docker/PRIVATE_SERVER.md), [`IMPROVEMENTS.md`](IMPROVEMENTS.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 개요 및 4개 트랙 구조
|
## 1. 개요 및 5개 트랙 구조
|
||||||
|
|
||||||
```
|
```
|
||||||
[M0: 문서 정합성] ──> [M1: 내결함성 확보] ──> [M2: 브로커 실증] ──> [M3: 보안 종결] ──> [M4: 동기화 완료]
|
[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: 문서,
|
(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 가드) S-1~S-9 스파이크) 지문 토픽, G-11) 배포 스크립트)
|
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 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 1** | `O-5` (P2) | `nats-server` MQTT 3.1.1 어댑터 호환성 및 Retained 메시지 실측 검증 | 격리 클론 (`$SCRATCH/nats-spike`) |
|
||||||
| **Track 1R** | 원격 프로덕션 (P1) | VPS/홈랩 `nats-server` Docker 상시 가동 및 MAM 원격 백플레인 전환 | `PRIVATE_SERVER.md` §9, 서버측 `docker-compose.yml`/`nats.conf`, `.mam.env` |
|
| **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 2** | `A-2`, `B-16` (P2) | 워크스페이스 지문 토픽 격리 및 `auth_token` 무조건 발급 강제 | `mqtt_common.py`, `registry.py`, `reconcile.sh` |
|
||||||
| **Track 3** | 문서/설정 동기화 | 공식 가이드, 배포 스크립트, 환경변수 템플릿 일원화 | `MESSAGING.md`, `IMPROVEMENTS.md`, `VERSIONS.md`, `deploy/*` |
|
| **Track 3** | 문서/설정 동기화 | 공식 가이드, 배포 스크립트, 환경변수 템플릿 일원화 | `MESSAGING.md`, `IMPROVEMENTS.md`, `VERSIONS.md`, `deploy/*` |
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ M0 (문서 정합성) ──> M1 (Track 0 내결함성) ──> M2a (로컬 스
|
|||||||
| **M0** | 문서 정합성 확보 | `PRIVATE_SERVER.md` E-1~E-4 교정, 다능성 절 추가, 본 로드맵 작성 | **G-D1 ~ G-D4 가드 테스트 통과** (276 -> 280) |
|
| **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) |
|
| **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로 분기) |
|
| **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(계정격리) 동시 통과** |
|
| **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 구독 제거 |
|
| **M3** | 보안 종결 (Track 2) | A-2 지문 토픽 전환, G-11 무조건 토큰 발급 | 지문 토픽 동작 확인 **후** legacy 구독 제거 |
|
||||||
| **M4** | 동기화 완료 (Track 3) | `MESSAGING.md`, `IMPROVEMENTS.md`, `VERSIONS.md`, `deploy/*` 정합 | 전체 테스트 스위트 100% Green |
|
| **M4** | 동기화 완료 (Track 3) | `MESSAGING.md`, `IMPROVEMENTS.md`, `VERSIONS.md`, `deploy/*` 정합 | 전체 테스트 스위트 100% Green |
|
||||||
|
|
||||||
@@ -111,6 +111,8 @@ M0 (문서 정합성) ──> M1 (Track 0 내결함성) ──> M2a (로컬 스
|
|||||||
▼
|
▼
|
||||||
[P0.5 자산화] docker/ 4대 자산 정본화 + D-22~D-30 회귀 가드 (297 -> 306)
|
[P0.5 자산화] docker/ 4대 자산 정본화 + D-22~D-30 회귀 가드 (297 -> 306)
|
||||||
▼
|
▼
|
||||||
|
[P0.6 서브모듈] nats-docker 서브모듈 분리 + 동적 경로 해석기 + CI checkout 동기화
|
||||||
|
▼
|
||||||
[P1 서버 준비] VPS/홈랩 프로비저닝, Docker, Tailscale 가입
|
[P1 서버 준비] VPS/홈랩 프로비저닝, Docker, Tailscale 가입
|
||||||
▼
|
▼
|
||||||
[P2 배포] nats.conf + compose 기동, healthcheck healthy 확인
|
[P2 배포] nats.conf + compose 기동, healthcheck healthy 확인
|
||||||
@@ -141,8 +143,8 @@ M0 (문서 정합성) ──> M1 (Track 0 내결함성) ──> M2a (로컬 스
|
|||||||
|
|
||||||
| 대상 파일 | 갱신 내용 |
|
| 대상 파일 | 갱신 내용 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [`MESSAGING.md`](MESSAGING.md) | 브로커 표준을 `nats-server`로 갱신, F-1/C1 해소 기록, F-5 영속 세션 서술 정정 |
|
| [`MESSAGING.md`](MESSAGING.md) | 브로커 표준을 `nats-server`로 갱신, F-1/C1 해소 기록, F-5 영속 세션 서술 정정, 10개 환경변수 및 해석 계층 문서화 |
|
||||||
| [`IMPROVEMENTS.md`](IMPROVEMENTS.md) | A-2 완료 전환, B-14/B-15/B-16/O-5 해결 상태 갱신, B-17/B-18 신설 등록 |
|
| [`IMPROVEMENTS.md`](IMPROVEMENTS.md) | B-14/B-15 완료 상태 반영, O-6 신설, B-17/B-18 신설 등록 |
|
||||||
| [`VERSIONS.md`](VERSIONS.md) | `v2.0.0` 릴리스 노트에 메시징 백플레인 고도화 및 내결함성 패치 기록 |
|
| [`VERSIONS.md`](VERSIONS.md) | `v2.0.0` 릴리스 노트에 메시징 백플레인 고도화 및 내결함성 패치 기록 |
|
||||||
| [`PRIVATE_SERVER.md`](nats-docker/PRIVATE_SERVER.md) | 스파이크 결과 반영 및 최종 가이드 확정 |
|
| [`PRIVATE_SERVER.md`](nats-docker/PRIVATE_SERVER.md) | 스파이크 결과 반영 및 최종 가이드 확정 |
|
||||||
| [`deploy/install.sh`](deploy/install.sh) | `requirements.txt` 확인 (paho 유지) 및 개인 브로커 안내 추가 |
|
| [`deploy/install.sh`](deploy/install.sh) | `requirements.txt` 확인 (paho 유지) 및 개인 브로커 안내 추가 |
|
||||||
@@ -169,7 +171,7 @@ M0 (문서 정합성) ──> M1 (Track 0 내결함성) ──> M2a (로컬 스
|
|||||||
- [ ] S-3 Retained 메시지 게이트 통과 확인
|
- [ ] S-3 Retained 메시지 게이트 통과 확인
|
||||||
|
|
||||||
### M2b: Track 1R 원격 프로덕션 전환
|
### M2b: Track 1R 원격 프로덕션 전환
|
||||||
- [x] D-1 `store_dir` 절대경로 교정 + 비인용 heredoc (`PRIVATE_SERVER.md:73`, `:146`)
|
- [x] D-1 `store_dir` 절대경로 교정 + 비인용 heredoc (`PRIVATE_SERVER.md` §4.1, §9.1)
|
||||||
- [x] D-2 이미지 핀 `nats:2.12-alpine` + `/healthz` healthcheck
|
- [x] D-2 이미지 핀 `nats:2.12-alpine` + `/healthz` healthcheck
|
||||||
- [x] D-3 8222/8080 바인드 주소 한정
|
- [x] D-3 8222/8080 바인드 주소 한정
|
||||||
- [x] D-4 TLS 절의 IP 예시 제거 및 DNS/SAN 요건 명시
|
- [x] D-4 TLS 절의 IP 예시 제거 및 DNS/SAN 요건 명시
|
||||||
@@ -177,8 +179,11 @@ M0 (문서 정합성) ──> M1 (Track 0 내결함성) ──> M2a (로컬 스
|
|||||||
- [x] N-1 §5.2 에 retained=MQTT 전용 경계 명문화 + §9.1 `mam_observer` 추가
|
- [x] N-1 §5.2 에 retained=MQTT 전용 경계 명문화 + §9.1 `mam_observer` 추가
|
||||||
- [x] 신규 가드 G-D5 ~ G-D9, G-R1, G-R2 구현 및 검증 (290 -> 297)
|
- [x] 신규 가드 G-D5 ~ G-D9, G-R1, G-R2 구현 및 검증 (290 -> 297)
|
||||||
- [x] P0.5: `docker/` 프로덕션 배포 자산 정본화 및 D-22 ~ D-30 회귀 가드 (297 -> 306)
|
- [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 단언
|
- [ ] 서버 배포 및 R-3 노출 면적 0 단언
|
||||||
- [ ] §9.5 드레인·잔여 스캔 후 `.mam.env` 전환
|
- [x] §9.5 사설 브로커(`vm-ubuntu`)로 `.mam.env` 전환 완료 (사후 잔여 드레인 스캔 과제 기록)
|
||||||
- [ ] R-1 ~ R-13 전건 통과 (R-5 / R-9 / R-13 최종 관문)
|
- [ ] R-1 ~ R-13 전건 통과 (R-5 / R-9 / R-13 최종 관문)
|
||||||
|
|
||||||
### M3: Track 2 보안 및 토픽 격리 (`A-2`, `B-16`)
|
### M3: Track 2 보안 및 토픽 격리 (`A-2`, `B-16`)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
||||||
@@ -722,4 +723,89 @@ def test_d30_websocket_origin_policy_is_startable():
|
|||||||
assert "/mqtt" in full_conf, "nats.conf must document /mqtt WebSocket MQTT path (N-7)"
|
assert "/mqtt" in full_conf, "nats.conf must document /mqtt WebSocket MQTT path (N-7)"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# D-31 — Gitea CI checkout enables submodules for test jobs
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
def test_d31_gitea_ci_submodules_in_test_job():
|
||||||
|
gitmodules_path = os.path.join(REPO_ROOT, ".gitmodules")
|
||||||
|
if not os.path.exists(gitmodules_path):
|
||||||
|
return # Auto-disable when no submodules are configured
|
||||||
|
|
||||||
|
ci_path = os.path.join(REPO_ROOT, "deploy", "gitea-ci.yml")
|
||||||
|
assert os.path.exists(ci_path), f"Gitea CI workflow missing at {ci_path}"
|
||||||
|
|
||||||
|
with open(ci_path, "r", encoding="utf-8") as f:
|
||||||
|
ci_data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
jobs = ci_data.get("jobs", {})
|
||||||
|
assert jobs, f"No jobs defined in {ci_path}"
|
||||||
|
|
||||||
|
test_jobs_found = 0
|
||||||
|
for job_name, job_data in jobs.items():
|
||||||
|
if not isinstance(job_data, dict):
|
||||||
|
continue
|
||||||
|
steps = job_data.get("steps", [])
|
||||||
|
|
||||||
|
# Determine if this job runs pytest or test suites
|
||||||
|
is_test_job = False
|
||||||
|
for step in steps:
|
||||||
|
if not isinstance(step, dict):
|
||||||
|
continue
|
||||||
|
run_cmd = step.get("run", "")
|
||||||
|
if "pytest" in run_cmd or "tests/" in run_cmd:
|
||||||
|
is_test_job = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_test_job:
|
||||||
|
test_jobs_found += 1
|
||||||
|
checkout_steps = [
|
||||||
|
s for s in steps
|
||||||
|
if isinstance(s, dict) and "actions/checkout" in str(s.get("uses", ""))
|
||||||
|
]
|
||||||
|
assert checkout_steps, f"Test job '{job_name}' has no actions/checkout step"
|
||||||
|
for s in checkout_steps:
|
||||||
|
with_opts = s.get("with", {}) or {}
|
||||||
|
submodules_val = with_opts.get("submodules")
|
||||||
|
assert submodules_val, (
|
||||||
|
f"Job '{job_name}' checkout step must enable submodules (e.g. submodules: recursive) "
|
||||||
|
f"to prevent test_deploy_freshness failures in CI, got: {submodules_val}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert test_jobs_found > 0, (
|
||||||
|
"Anti-void assertion: expected at least 1 test execution job running pytest in deploy/gitea-ci.yml"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# D-32 — MESSAGING.md documents all supported MQTT environment variables
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
def test_d32_messaging_doc_covers_all_mqtt_env_vars():
|
||||||
|
messaging_path = os.path.join(REPO_ROOT, "MESSAGING.md")
|
||||||
|
assert os.path.exists(messaging_path), f"MESSAGING.md missing at {messaging_path}"
|
||||||
|
|
||||||
|
with open(messaging_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
doc_vars = set(re.findall(r'\b(MQTT_[A-Z0-9_]+)\b', content))
|
||||||
|
assert doc_vars, "No MQTT_* variables found in MESSAGING.md"
|
||||||
|
|
||||||
|
expected_vars = {
|
||||||
|
"MQTT_BROKER",
|
||||||
|
"MQTT_PORT",
|
||||||
|
"MQTT_TLS",
|
||||||
|
"MQTT_USERNAME",
|
||||||
|
"MQTT_PASSWORD",
|
||||||
|
"MQTT_CA_CERTS",
|
||||||
|
"MQTT_CERTFILE",
|
||||||
|
"MQTT_KEYFILE",
|
||||||
|
"MQTT_CLIENT_ID_PREFIX",
|
||||||
|
"MQTT_KEEPALIVE",
|
||||||
|
}
|
||||||
|
|
||||||
|
missing_vars = expected_vars - doc_vars
|
||||||
|
assert not missing_vars, (
|
||||||
|
f"MESSAGING.md is missing documentation for supported MQTT variables: {missing_vars}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user