Files
multi-agent-mux/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-1fa7183a.md
T

32 KiB

🔎 문서 정합성 검증 및 동기화 계획서 Rev.2 (Job eb04e918)

  • 작성일: 2026-08-23
  • 역할: Planner (.agents/MULTI_AGENT_RULES.md §1 — Planner 는 저장소 코드/문서를 수정하지 않으며, 산출물은 본 보고서입니다)
  • 기준 커밋: 916185c, 작업 트리 clean, mainorigin/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-remoteFailed to authenticate user). 그러나 제안된 url = ../nats-dockertmpl/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-11D-19, D-22D-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개 · withNone 이며, 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_ROOTWORKSPACE_ROOTwalk_up(__file__)walk_up(cwd)
접속 지점 (make_client() / 브로커 설정 확정) _env_file_missing 이면 명시적 예외로 거부(fail-closed). ② 해석된 호스트가 내장 공개 기본값(broker.hivemq.com)과 같으면 눈에 띄는 보안 경고 출력

이 배치가 두 요구를 모두 만족시킵니다: 명시적 설정이 깨졌을 때 조용히 다른 환경으로 새지 않고(리뷰어 C3-1), 자동 탐색이 아무것도 못 찾아 공개 브로커로 떨어질 때 반드시 경고가 나오며(리뷰어 C3-2), 그러면서도 읽기 전용 소비자의 import 를 깨뜨리지 않습니다.

보조 실측_parse_env_fileif 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-1V-15), 발견 S-1S-12, 작업 T-2T-8·T-10T-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.txtPyYAML>=6.0 추가 / CI 의 PyYAML 은 스킬 requirements.txtpyyaml 로 확보되어 결함 아님 / 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-dockertmpl/nats-docker / ../../laa/nats-dockerlaa/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개 · withNone
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.py3개
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-11D-19, D-22D-30 전멸)를 실측했습니다.

Rev.2 갱신: submodules: recursive 추가만으로는 부족합니다. 서브모듈이 비공개(V-16)이고 상위 저장소는 공개(V-17)이며 서로 다른 조직이므로, 러너에 laa/nats-docker 읽기 권한이 별도로 공급되어야 합니다. §4 T-1a/T-1b/T-1c 참조.

🔴 S-13 (P1, 타이밍) — 신설: 아직 푸시되지 않았을 뿐이다

mainorigin/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.shdocker/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.mdbroker_config_from_env 파싱 10종 중 8종만 문서화 — MQTT_CLIENT_ID_PREFIX, MQTT_KEEPALIVE 누락. .mam.env 해석 순서(_load_dotenv) 절 부재
🔴 S-5 (P1) IMPROVEMENTS.md:3-6276/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.mdD-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) :172PRIVATE_SERVER.md:73, :146 행 번호 인용이 낡음 → 절 번호로 교체

4. 동기화 작업 명세 (Creator 범위)

T-1 계열은 CI 를 되살리는 작업이며 S-13 때문에 다음 푸시 이전에 완료되어야 합니다.

T-1a — .gitmodules 상대 URL 전환 (선택지 C 를 택할 경우 필수, 그 외에는 권고)

[submodule "nats-docker"]
	path = nats-docker
	url = ../../laa/nats-docker

⚠️ ../nats-docker 를 쓰지 마십시오. 상위 origin 이 tmpl/multi-agent-mux 이므로 tmpl/nats-docker 로 해석되어 존재하지 않는 저장소를 가리킵니다(V-18). 변경 후 반드시 검증:

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).

      - 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 failed0 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 반영 재설계)

# 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
# 접속 지점(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/ -qbash 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]