docs(broker): expand PRIVATE_SERVER.md with versatility guide and create implementation_plan.md

This commit is contained in:
2026-08-20 11:59:53 +09:00
parent a9934ad104
commit 4025623958
6 changed files with 1170 additions and 45 deletions
@@ -0,0 +1,277 @@
# 📐 구현 계획서 Rev.2 — `PRIVATE_SERVER.md` 확장 및 `implementation_plan.md` 신설
- **Job ID**: `8c651798` (Rev.1 = `d42004ee`)
- **Planner**: claude (session: `herdr:canary-projects-multi-agent-mux-creator-claude`)
- **Role**: Planner (`MULTI_AGENT_RULES.md` §1 — 본 작업에서 저장소 코드 **0건 수정**)
- **반영 대상 Challenge**: `019495f4` (agy, Worker / Plan Reviewer) — `[VERDICT: PASS WITH CHALLENGE]`
- **기준 커밋**: `a9934ad` — 테스트 베이스라인 **276**
---
## 0. Challenge 판정 요약
Challenge 3건을 **실측으로 판정**했습니다. 3건 모두 **지적은 타당**하나, 그중 1건은 **제시된 해법 자체가 동작하지 않고**, 1건은 **지적보다 심각**하며, 1건은 **Rev.1 에 이미 있던 조항의 구체화**입니다.
| # | 지적 | 판정 | 실측 근거 |
|---|---|---|---|
| **C1-a** | E-3 수정에 `registry.py register` 정확한 인자가 필요 | ✅ **채택**`--prompt` 는 실제로 required | `registry.py:240` `p_reg.add_argument("--prompt", required=True)` |
| **C1-b** | "`--job-id <id>` 를 쓴다 (not `--job`)" + 복사·붙여넣기 명령 제시 | ❌ **실측 반증 — 해법이 동작하지 않음** | `--job-id``register` 서브파서에 **존재하지 않음**. 실행 시 `error: unrecognized arguments: --job-id test-ping-01` |
| **C1-c** | (제시 명령의 나머지 부분) | ⚠️ **추가 결함 2건 발견** | ① `--registry-dir`**부모 파서** 인자라 서브커맨드 **앞**에 와야 함(실측 오류) ② 테스트 잡이 `pending` 으로 **영구 잔존**`--wait-any` 가 수집 |
| **C2** | `/etc/nats/nats.conf` · `/data` 는 비루트 환경에서 `Permission denied` | ✅ **채택 — 심각도 상향** | macOS 는 `Permission denied` 가 아니라 **`Read-only file system`**. `/` 가 sealed APFS 라 **sudo 로도 생성 불가** |
| **C3** | G-D2 를 코드 펜스 범위로 한정할 것 | ✅ **채택 — 단, Rev.1 §5.5 에 이미 명시된 조항** | Rev.1 원문: *"정규식이 코드 블록 밖의 산문까지 잡으면 오탐이 납니다. 펜스(```) 안 블록으로 스코프를 한정하고…"* — 다만 **구체적 충돌 사례를 특정한 것은 유효한 기여** |
**메타 관찰**: C1-b 는 이 리뷰가 교정하려는 결함(E-1·E-2·E-3 = *검증되지 않은 복사·붙여넣기 명령*)과 **정확히 같은 유형**을 재생산했습니다. 이는 §5.5 문서 드리프트 가드의 필요성을 역설적으로 입증하므로, **Rev.2 는 가드 범위를 문서 내 실행 명령 전반으로 확대**합니다(G-D4 신설).
---
## 1. C1 정밀 판정 — E-3 수정의 정확한 명령
### 1.1 반증 — `--job-id` 는 존재하지 않습니다
Challenge 가 "copy-pasteable" 로 제시한 명령을 그대로 실행한 결과:
```
$ registry.py --registry-dir <dir> register --job-id test-ping-01 \
--prompt "Private broker connectivity test" --agent-session "herdr:test"
registry.py: error: unrecognized arguments: --job-id test-ping-01
```
`register` 서브파서(`registry.py:239-252`)의 인자는 다음이 전부입니다:
```
--prompt (required) --agent --agent-session --role --timeout --idle-timeout
--bits --artifact --auth-token --job-type --reviewer --reviewer-session --max-iterations
```
**`--job-id``--job` 도 없습니다.** 혼동의 원인은 함수 시그니처입니다 — `register_job()` **함수**에는 `job_id` 파라미터가 있고(`registry.py:72` `job_id = job_id or generate_job_id(bits)`), CLI 의 `main()` 은 이를 **전달하지 않습니다**(`:304-318``register_job(...)` 호출에 `job_id=` 인자 부재). 즉 **CLI 로는 잡 ID 를 지정할 수 없고, 항상 새로 채번됩니다.**
### 1.2 추가 결함 — `--registry-dir` 위치
```
$ registry.py register --registry-dir <dir> --prompt "x"
registry.py: error: unrecognized arguments: --registry-dir <dir>
```
`--registry-dir``registry.py:236` 에서 **부모 파서**에 등록되므로 **서브커맨드 앞**에 와야 합니다. 문서에 실릴 명령이라면 이 순서를 틀리게 적을 여지를 없애야 합니다.
### 1.3 추가 결함 — 테스트 잡의 영구 잔존
`register_job()``status: "pending"`(`registry.py:84`)으로 레코드를 만듭니다. 그리고 `job_subscriber.py::_collect_jobs()``--wait-any`**`status in ("pending","running")` 인 모든 잡을 수집**합니다. 따라서 정리하지 않은 연결 테스트 잡은:
- `job_subscriber.py --wait-any` 가 **영원히 기다리는 유령 잡**이 되고,
- `pick_pending` 의 후보로 남습니다(`agent_session` 일치 시).
**`registry.py` 에는 delete/remove 서브커맨드가 없습니다**(`register/list/get/status/update/get-feedback/pick/logs` 가 전부). 따라서 정리는 `status` 서브커맨드로 종결 처리하는 것이 정석입니다.
### 1.4 채택 — `PRIVATE_SERVER.md` §6 에 실릴 최종 명령
```bash
# 1) 임시 잡 등록 — ID 는 지정할 수 없고 자동 채번되므로 stdout 을 반드시 캡처한다
JID=$(.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \
--registry-dir .mam/jobs \
register \
--prompt "Private broker connectivity test" \
--agent-session "herdr:test")
echo "registered job: $JID"
# 2) 이벤트 발행 (rc=0 단언)
.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py \
--registry-dir .mam/jobs \
--job "$JID" \
--event progress \
--detail "Private broker connection verified" -v
# 3) 접속 대상 단언 — 개인 서버 IP 가 보이고 broker.hivemq.com 이 없어야 한다
# (-v 로그 또는 감사 로그에서 확인)
# 4) 정리 — 미정리 시 --wait-any 가 수집하는 유령 잡으로 남는다
.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \
--registry-dir .mam/jobs status --job "$JID" --set completed
```
> 주의 3가지를 문서에 각주로 명시: ① **`--registry-dir` 은 서브커맨드 앞** ② **잡 ID 는 지정 불가, 캡처 필수** ③ **4)번 정리 생략 금지**.
---
## 2. C2 판정 — 심각도 상향 (Permission denied 가 아니라 생성 불가)
Challenge 는 비루트 환경의 `Permission denied` 를 지적했습니다. **실측 결과 macOS 에서는 그보다 강한 제약입니다**:
```
$ mkdir -p /data
mkdir: /data: Read-only file system
$ mount | grep 'on / '
/dev/disk3s1s1 on / (apfs, sealed, local, read-only, journaled)
```
macOS 의 루트 볼륨은 **sealed read-only APFS** 이므로 `store_dir: "/data"`**`sudo` 로도 생성할 수 없습니다**(`/etc/synthetic.conf` 편집 + 재부팅이 필요). 그리고 **본 프로젝트의 개발 플랫폼이 darwin** 이므로, Rev.1 §3 A-2 의 네이티브 스니펫은 **주 사용 환경에서 곧바로 실패**합니다.
따라서 C2 는 "실용성 개선"이 아니라 **E-2 교정안 자체의 결함**으로 분류하고, 기본값을 사용자 공간으로 전환합니다.
### 2.1 채택 — 사용자 공간 기본값
**네이티브 (기본 경로 — sudo 불필요)**
```conf
# ~/.config/nats/nats.conf
server_name: mam-hub
jetstream {
store_dir: "~/.local/share/nats/data" # 홈 디렉터리. 루트 볼륨 접근 없음
max_file: 10G
}
http_port: 8222
mqtt { port: 1883 }
websocket { port: 8080, no_tls: true } # 내부망 한정
```
```bash
mkdir -p ~/.config/nats ~/.local/share/nats/data
nats-server -c ~/.config/nats/nats.conf
```
**Docker Compose (상대 경로 + 네임드 볼륨)**
```yaml
services:
nats:
image: nats:latest
container_name: mam-nats
restart: unless-stopped
command: ["-c", "/etc/nats/nats.conf"]
volumes:
- ./nats.conf:/etc/nats/nats.conf:ro # 호스트 상대 경로
- nats-data:/data # 네임드 볼륨
ports:
- "1883:1883" # MQTT 3.1.1 (평면 A: MAM)
- "4222:4222" # NATS
- "8222:8222" # HTTP 모니터링
- "8080:8080" # WebSocket (평면 B)
volumes:
nats-data:
```
컨테이너 내부 `nats.conf``store_dir: "/data"` 를 씁니다(**컨테이너 안에서는 유효** — 호스트 루트와 무관).
> ⚠️ 문서에 명시할 검증 포인트: 네임드 볼륨의 소유권이 컨테이너 실행 사용자와 맞지 않으면 JetStream 이 기동에 실패할 수 있습니다. **기동 직후 `curl -s localhost:8222/jsz` 로 JetStream 활성 여부를 반드시 확인**하도록 절차에 넣습니다. (이 확인은 §3 A-3 Step 1 과 자연스럽게 합쳐집니다.)
---
## 3. C3 판정 — 기존 조항의 구체화 (채택)
Rev.1 §5.5 는 이미 다음을 명시했습니다:
> **가드 구현 주의**: 정규식이 코드 블록 밖의 산문까지 잡으면 오탐이 납니다. **펜스(```) 안 블록으로 스코프를 한정**하고, G-D1 은 `mqtt_common` 을 import 해 실제 집합과 대조해야 합니다.
따라서 C3 은 신규 발견이 아니라 **동일 조항의 재확인**입니다. 다만 Challenge 가 특정한 **구체적 충돌 사례는 유효한 기여**입니다 — Rev.1 §6 은 `-m 1883` 에 대해 *"기존 안내는 오류였다"는 정정 각주*를 권고했고, Creator 가 `MAM_MQTT_*` 에 대해서도 같은 각주를 쓰면 **G-D2 가 자기 문서의 정정 설명에 걸립니다**. 이 상호작용을 Rev.1 은 짚지 않았습니다.
### 3.1 채택 — G-D2 스펙 확정
- **판정 대상**: ` ```bash `, ` ```conf `, ` ```yaml ``.mam.env` 블록 **안쪽만**.
- **판정 제외**: 산문, `> [!NOTE]` 인용, 표, 각주 — 즉 **정정 각주는 자유롭게 작성 가능**.
- **구현**: 파일 전체 `re.search` 금지. 펜스 파싱 후 블록 본문에 대해서만 `MAM_MQTT_` 부재를 단언.
- **자기검증**: 가드 자체가 스코핑을 지키는지 확인하기 위해, **테스트가 "산문에 `MAM_MQTT_` 를 포함한 임시 문서"를 만들어 통과함을 함께 단언**합니다(오탐 방지 회귀).
---
## 4. 신설 — G-D4 (C1-b 가 드러낸 구조적 결함)
E-1·E-2·E-3 와 C1-b 는 모두 **"문서에 실린 명령이 실행되지 않는다"** 는 단일 원인을 공유합니다. G-D1~G-D3 는 *특정 문자열*을 감시할 뿐 이 원인을 막지 못합니다.
| ID | 가드 | 검증 방식 |
|---|---|---|
| **G-D4** | `PRIVATE_SERVER.md` §6 의 검증 절차에 등장하는 `registry.py` / `publish_event.py` 호출의 **인자 이름이 실제 argparse 파서에 존재**할 것 | 문서에서 명령을 추출 → 해당 스크립트의 `_build_parser()` 를 import → 각 플래그가 파서에 등록되어 있는지 대조. **`--job-id` 같은 유령 인자를 즉시 검출** |
**Mutation**: 문서의 `--job``--job-id` 로 되돌리면 FAIL 해야 합니다.
> 구현 주의: 실제로 명령을 **실행하지 않습니다**(브로커·네트워크 의존). 파서 대조만으로 C1-b 유형은 전부 잡힙니다.
**테스트 증분 전망 갱신**: 276 → **286**(Track 0 G-1~G-10) → **290**(G-D1~G-D4) → **291**(Track 2 G-11).
---
## 5. Phase A — `PRIVATE_SERVER.md` 교정 (Rev.2 확정본)
Rev.1 에서 발견한 E-1~E-4 는 판정 변경 없이 유지되며, C1·C2 를 반영해 A-2·A-3 을 갱신합니다.
| 항목 | 내용 | Rev.2 변경 |
|---|---|---|
| **A-1** (E-1) | §5 의 `MAM_MQTT_*``MQTT_BROKER`/`MQTT_PORT`/`MQTT_TLS`/`MQTT_USERNAME`/`MQTT_PASSWORD` + `MQTT_CA_CERTS`/`MQTT_CERTFILE`/`MQTT_KEYFILE` 추가. `.mam.env:39-64` 템플릿과 1:1 정렬. OS 환경변수 우선순위 1줄 명시 | 불변 |
| **A-2** (E-2) | `-m 1883` **3개소 전량 제거**(§4.1 방법 A·B, §7 Phase 2), `mqtt { port: 1883 }` 설정 블록 + `-c` 도입, `8080` 노출, Compose 포트 주석 정정, `max_file` 상한 | 🔄 **경로를 사용자 공간으로 전환**(§2.1). 네이티브 `~/.config/nats/nats.conf` + `~/.local/share/nats/data`, Docker `./nats.conf` + 네임드 볼륨 |
| **A-3** (E-3·E-4) | §6 을 4단계 검증으로 재작성 | 🔄 **Step 2 명령을 §1.4 확정본으로 교체**(ID 캡처·`--registry-dir` 위치·정리 단계). Step 1 에 **`/jsz` JetStream 확인** 추가(§2.1 단서) |
| **A-4** | §6 Step 2 의 "개인 브로커 환경에서도 100% 통과" → "브로커와 무관하게 통과, 연동 검증은 Step 1~3 담당". 테스트 건수 고정 표기 회피 | 불변 |
**§6 최종 4단계**
| Step | 내용 | 통과 기준 | 검출 대상 |
|---|---|---|---|
| 1 | `curl -s http://<host>:8222/varz` (MQTT 리스너) + `/jsz` (JetStream) | 둘 다 활성 보고 | **E-2**, 볼륨 소유권 문제 |
| 2 | §1.4 의 잡 등록 → 발행 | **rc=0** | **E-3**, C1 |
| 3 | 접속 대상 단언 — 로그에 개인 서버 IP, `broker.hivemq.com` **부재** | 단언 성립 | **E-1** |
| 4 | `pytest tests/ -q` + "브로커 무관 검증" 명시 | 베이스라인 통과 | (E-4 오해 방지) |
---
## 6. Phase B — 다능성 절 (Rev.1 대비 불변)
§4 와 §5 사이에 신설. **설계 결정 "하나의 서버, 두 개의 소비 평면"**(Rev.1 §2)은 Challenge 가 전면 승인했으므로 그대로 유지합니다.
| 소절 | 내용 | 필수 제약 |
|---|---|---|
| 5.1 두 소비 평면 | 평면 A(MAM/MQTT, 변경 없음) vs 평면 B(NATS·WS·KV·Object). **"다능성은 이관할 이유가 아니라 이관하지 않고도 얻는 이득"** 을 첫 문장으로 | `NATS_REPORT.md` 정합성 자기선언 |
| 5.2 교차 프로토콜 브리징 | MQTT `python/mqtt/jobs/<id>/events` ↔ NATS `python.mqtt.jobs.<id>.events`. MAM 코드 0줄로 대시보드 부착 | ① **동일 계정 내에서만** ② 토픽 레벨에 `.` 금지(MAM은 hex라 안전) |
| 5.3 JetStream 리플레이 | `python.mqtt.jobs.>` 캡처 스트림으로 사후 재생 | ① 옵트인 ② `$MQTT_*` 내부 스트림과 별개 ③ **`max_age`/`max_bytes` 필수** |
| 5.4 KV / Object Store | 홈랩 설정·피처플래그·산출물 저장 | **MAM 레지스트리를 KV로 대체 금지**(`wait_for_job` 폴링 계약) |
| 5.5 멀티테넌트 계정 | `MAM`/`HOME` 계정 분리, 계정별 쿼터·subject 권한 → A-2 ACL 충족 | ① **MQTT 접속 계정은 JetStream 활성 필수** ② 격리↔관측 상충과 권고 배치(Rev.1 §2.1) |
| 5.6 운영 이점 | 단일 정적 바이너리, `/varz`·`/jsz`, 컨테이너 1개 | — |
**서술 원칙 3가지 유지**: ① 기능마다 "MAM에 쓰는가" 명시 ② Track 1 이전이므로 **미검증 항목은 확정형 금지**(특히 S-3 retained) ③ 제약을 장점과 같은 비중으로 기술.
---
## 7. Phase C — `implementation_plan.md` (Rev.1 구조 유지 + 갱신)
**파일명**: 브리핑대로 `implementation_plan.md` 로 진행하되, 저장소 대문자 규약(`README.md`·`NATS_REPORT.md`·`PRIVATE_SERVER.md` 등)과의 불일치를 Creator 가 1줄 확인받습니다. Challenge 도 이 항목은 이의 없이 통과했습니다.
**마일스톤 (M0 게이트만 갱신)**
| M | 이름 | DoD | 게이트 |
|---|---|---|---|
| **M0** | 문서 정합성 | E-1~E-4 교정 + 다능성 절 + 로드맵 | 🔄 **G-D1~G-D4** green (G-D4 신설) |
| **M1** | 내결함성 (Track 0) | B-14·B-15, **286 passed** | G-1~G-10 + mutation 전건 FAIL 확인 |
| **M2** | 브로커 실증 (Track 1) | 격리 클론 S-1~S-9 | **S-3(retained) 통과** ← 미통과 시 mosquitto 분기 |
| **M3** | 보안 종결 (Track 2) | A-2 해소, B-16 완결 | 지문 토픽 전환 확인 **후** legacy 구독 제거 |
| **M4** | 동기화 (Track 3) | 문서·`.mam.env`·`deploy/*` 정합 | 전체 스위트 green |
**의존성**: `M0 → M1 → M2 → M3 → M4` (직렬). **M0 의 A-1 은 M2 의 선행조건이기도 합니다** — 환경변수 이름이 틀린 채 스파이크를 돌리면 **공개 브로커에 붙은 결과를 개인 브로커 성공으로 오독**합니다. 이 함정을 로드맵에 경고로 명시.
**본문 구성** (Rev.1 §5.2 유지): 개요 / 마일스톤 / Track 0(3-Step 순서 의존성 + G-1~G-10 + 통합 검증) / Track 1(S-1~S-9, 격리 클론 원칙) / Track 2(무조건 토큰 발급 G-11, 지문 토픽 3단계 순서) / Track 3(문서 동기화표 — **`PRIVATE_SERVER.md` 자신도 대상**) / 의존성·롤백 / 진행 추적표.
**역할 분리 명시**: `IMPROVEMENTS.md` = 과제 백로그(무엇을/왜), `implementation_plan.md` = 실행 로드맵(언제/어떤 순서로/완료 판정). 상호 링크하되 사실을 복제하지 않습니다.
---
## 8. 위험 · 비-목표 (Rev.2 갱신분)
| 위험 | 완화 | 비고 |
|---|---|---|
| 문서에 실린 명령이 또 검증 없이 들어감 | **G-D4** 가 파서 대조로 차단 | 🆕 C1-b 대응 |
| macOS 사용자가 §4.1 를 따라가다 실패 | 사용자 공간 기본값 + `/jsz` 확인 절차 | 🆕 C2 대응 |
| 정정 각주가 G-D2 에 걸림 | 펜스 스코핑 확정 + 오탐 방지 회귀 단언 | 🆕 C3 대응 |
| 다능성 절이 `NATS_REPORT.md` 와 모순되게 읽힘 | 평면 분리를 절 도입부 첫 문장으로 고정 | 불변 |
| Track 1 이전 확정형 서술 | 미검증 "검증 대상" 표기, 특히 S-3 | 불변 |
| 테스트 잡 잔존으로 `--wait-any` 오염 | §1.4 Step 4 정리 명령 필수화 | 🆕 C1-c |
**비-목표** (불변): 저장소 코드 수정 / Track 0~3 실제 구현 / `nats-py` 도입 / 레지스트리 KV 대체 / client_id 안정화 / 실제 브로커 기동 및 S-1~S-9 실행.
---
## 9. 산출물 및 Reviewer 확인 요청
**Creator 산출물 2종**
1. `PRIVATE_SERVER.md` — Phase A 교정(§1.4 명령·§2.1 경로 포함) + Phase B 신설 §5 + §7 Phase 2 명령 동시 교정
2. `implementation_plan.md` — M0~M4, 4트랙 본문, 의존성/롤백, 진행 추적표
3. (M0 게이트) `tests/test_deploy_freshness.py`**G-D1~G-D4** — 단, 이는 **Creator 의 구현 범위**이며 본 계획서는 스펙만 제공합니다
**Reviewer 재현 검증 요청 4건**
1. **C1-b 반증**: `registry.py … register --job-id X --prompt Y``error: unrecognized arguments: --job-id X` 인가
2. **C1-c**: `--registry-dir``register` **뒤**에 두면 오류인가 / `register``status:"pending"` 을 만들고 `--wait-any` 가 이를 수집하는가
3. **C2**: `mkdir -p /data``Read-only file system` 이며 `/``sealed … read-only` 인가
4. **C3**: Rev.1 §5.5 에 펜스 스코핑 조항이 이미 있었는가 (기여의 범위 확인)
**미해결 확인 요청 1건**: `implementation_plan.md` vs `IMPLEMENTATION_PLAN.md` 파일명 — 기본은 브리핑대로 소문자.