diff --git a/NATS_REPORT.md b/NATS_REPORT.md deleted file mode 100644 index b2f28dc..0000000 --- a/NATS_REPORT.md +++ /dev/null @@ -1,176 +0,0 @@ -# ๐Ÿ“Š MAM ๋ฉ”์‹œ์ง• ๋ฐฑํ”Œ๋ ˆ์ธ ์•„ํ‚คํ…์ฒ˜ ์‹ฌ์ธต ๋ถ„์„ ๋ณด๊ณ ์„œ: MQTT vs NATS - -- **๋ฌธ์„œ ๋ฒ„์ „**: Rev.2 Final Synthesis (`f1956d2e` / `5ac88ca0`) -- **์ž‘์„ฑ/๊ฒ€ํ†  ์ฃผ์ฒด**: MAM Multi-Agent Orchestration Team (`claude`, `agy`) -- **๊ธฐ์ค€ ์ปค๋ฐ‹**: `ac82f9b` (`refactor`, 276/276 tests passing) -- **๋ฌธ์„œ ๋ชฉ์ **: MAM ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๋ฉ”์‹œ์ง• ์ธํ”„๋ผ(MQTT)๋ฅผ NATS๋กœ ์ „๋ฉด ์ „ํ™˜ํ•  ๊ฒƒ์ธ์ง€ ์—ฌ๋ถ€์— ๋Œ€ํ•œ ์ข…ํ•ฉ์ ์ธ ๊ธฐ์ˆ ยท์šด์˜ยท๋ณด์•ˆ ํƒ€๋‹น์„ฑ ๋ถ„์„ ๋ฐ ์‹คํ–‰ ๋กœ๋“œ๋งต ํ™•์ •. - ---- - -## 0. ์ตœ์ข… ํŒ์ • (Executive Verdict) - -> ### ๐ŸŽฏ **[VERDICT: DO NOT MIGRATE CLIENT PROTOCOL โ€” ADOPT `nats-server` AS DEDICATED BROKER]** -> -> **ํด๋ผ์ด์–ธํŠธ ์ „์†ก ํ”„๋กœํ† ์ฝœ(MQTT)์€ ์œ ์ง€ํ•˜๊ณ , ์ „์šฉ ๋ธŒ๋กœ์ปค๋กœ์„œ `nats-server`์˜ ๋‚ด์žฅ MQTT 3.1.1 ์–ด๋Œ‘ํ„ฐ๋ฅผ ์ฑ„ํƒํ•ฉ๋‹ˆ๋‹ค.** - -### 0.1 3๋Œ€ ํ•ต์‹ฌ ๊ทผ๊ฑฐ ์š”์•ฝ - -``` -[MAM Control Plane] โ”€โ”€โ”€โ”€> run_loop.sh (wait_for_job: 3s Local Disk Polling) โ”€โ”€> 100% Broker-Independent -[Observability Plane] โ”€โ”€โ”€โ”€> publish_event.py โ”€โ”€(MQTT 3.1.1)โ”€โ”€> nats-server (JetStream + nkeys) -``` - -1. **์ œ์–ด ํ‰๋ฉด๊ณผ ๊ด€์ธก ํ‰๋ฉด์˜ ๋ถ„๋ฆฌ**: - MAM์˜ ํ•ต์‹ฌ ๋ฃจํ”„(`run_loop.sh`)๋Š” MQTT ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ๋…ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ๋กœ์ปฌ ํŒŒ์ผ์‹œ์Šคํ…œ(`.mam/jobs/.json`)์„ 3์ดˆ ์ฃผ๊ธฐ๋กœ ํด๋ง(`wait_for_job`)ํ•˜์—ฌ ์ž‘์—… ์™„๋ฃŒ๋ฅผ ํŒ์ •ํ•ฉ๋‹ˆ๋‹ค. ๋ธŒ๋กœ์ปค๋Š” **๋น„๋™๊ธฐ ๊ด€์ธก(observability) ์‚ฌ์ด๋“œ์นด**์ด๋ฉฐ ์ œ์–ด ํ‰๋ฉด์„ ์ฐจ๋‹จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. -2. **NATS์˜ ์‹ค์งˆ์  ์ด์ ์€ '์„œ๋ฒ„'์— ์กด์žฌ**: - NATS์˜ ํ•ต์‹ฌ ๊ฐ•์ (๋‹จ์ผ ๋ฌด์˜์กด Go ๋ฐ”์ด๋„ˆ๋ฆฌ, JetStream ์˜์†์„ฑ, nkeys/JWT ๊ณ„์ •ยทSubject๋ณ„ ACL)์€ ์„œ๋ฒ„ ๊ณ„์ธต์˜ ์†์„ฑ์ž…๋‹ˆ๋‹ค. `nats-server`๋Š” **MQTT 3.1.1 ํ”„๋กœํ† ์ฝœ์„ ๋„ค์ดํ‹ฐ๋ธŒ๋กœ ์ˆ˜์šฉ**ํ•˜๋ฏ€๋กœ, ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋ฅผ ํ•œ ์ค„๋„ ๋ฐ”๊พธ์ง€ ์•Š๊ณ  ์„œ๋ฒ„์˜ ๋ชจ๋“  ์šด์˜ยท๋ณด์•ˆ ์ด์ ์„ 100% ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -3. **๋„ค์ดํ‹ฐ๋ธŒ NATS(`nats-py`) ์ „ํ™˜์˜ ๋น„์šฉ ๋Œ€๋น„ ๋ฌด์ตํ•จ**: - `nats-py`๋Š” asyncio ์ „์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ, bash ๊ธฐ๋ฐ˜์˜ ๋‹จ๋ช…(short-lived) ๋™๊ธฐ CLI ๋„๊ตฌ๋“ค(`publish_event.py` ๋“ฑ)๊ณผ ์‹ฌ๊ฐํ•œ ๊ตฌ์กฐ์  ๋งˆ์ฐฐ์„ ์ผ์œผํ‚ค๋ฉฐ, ์ตœ์†Œ 46๊ฑด์˜ ํ…Œ์ŠคํŠธ ์žฌ์ž‘์„ฑ ๋ฐ 276๊ฑด green ๋ฒ ์ด์Šค๋ผ์ธ ํ›ผ์† ์œ„ํ—˜์„ ์ดˆ๋ž˜ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด NATS ๊ณ ์œ  ๊ธฐ๋Šฅ(Req/Reply, ์ดˆ๋‹น ์ˆ˜๋ฐฑ๋งŒ ์ฒ˜๋ฆฌ๋Ÿ‰, ํด๋Ÿฌ์Šคํ„ฐ๋ง)์€ MAM ์›Œํฌ๋กœ๋“œ(๋‹จ์ผ ์›Œํฌ์ŠคํŽ˜์ด์Šค, ์žก๋‹น ์ˆ˜ ๊ฐœ ์ด๋ฒคํŠธ)์—์„œ ์ „ํ˜€ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. - ---- - -## 1. 3๋Œ€ ์ „๋žต์  ์„ ํƒ์ง€ ๋น„๊ต ๋ถ„์„ - -| ํ‰๊ฐ€ ํ•ญ๋ชฉ | (A) ํ˜„ํ–‰ ์œ ์ง€ (๊ณต๊ฐœ HiveMQ) | (B) ๋„ค์ดํ‹ฐ๋ธŒ NATS ์ „ํ™˜ (`nats-py`) | (C) `nats-server` + MQTT ํ”„๋กœํ† ์ฝœ ์œ ์ง€ (์ฑ„ํƒ์•ˆ) | -|---|---|---|---| -| **ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ ๋ณ€๊ฒฝ** | 0์ค„ | 4๊ฐœ ํ˜ธ์ถœ๋ถ€ ์ „๋ฉด ๋น„๋™๊ธฐ ์žฌ์ž‘์„ฑ | **0์ค„** (ํ™˜๊ฒฝ๋ณ€์ˆ˜๋งŒ ๊ตฌ์„ฑ) | -| **ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์žฌ์ž‘์„ฑ** | 0๊ฑด | ์ตœ์†Œ 46๊ฑด ์žฌ์ž‘์„ฑ (276๊ฑด ๋ฒ ์ด์Šค๋ผ์ธ ์œ„ํ—˜) | **0๊ฑด** (๊ธฐ์กด 276๊ฑด 100% ๋ณด์กด) | -| **A-2 ๋ณด์•ˆ ๊ฒฐํ•จ ํ•ด์†Œ** | โŒ ๋ถˆ๊ฐ€ (๊ณต๊ฐœ ๋ธŒ๋กœ์ปค) | โœ… ์™„์ „ ํ•ด์†Œ | โœ… **์™„์ „ ํ•ด์†Œ** (nkeys/JWT subject ACL) | -| **๋‹จ์ผ ์ •์  ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐฐํฌ** | โŒ ๋ถˆ๊ฐ€ | โœ… ์ง€์› | โœ… **์ง€์›** (`nats-server` ๋ฐ”์ด๋„ˆ๋ฆฌ 1๊ฐœ) | -| **์ด๋ฒคํŠธ ์˜์†์„ฑ (JetStream)** | โŒ ๋ฏธ์ง€์› | โœ… ์ง€์› | โœ… **์ง€์›** (๋‚ด์žฅ JetStream ์—”์ง„) | -| **๋™๊ธฐ CLI ํ˜ธํ™˜์„ฑ** | โœ… ์šฐ์ˆ˜ (paho-mqtt) | โŒ ์‹ฌ๊ฐ (asyncio ๊ฐ•์ œ) | โœ… **์šฐ์ˆ˜** (๊ธฐ์กด ๋™๊ธฐ ํ•ซํŒจ์Šค ์œ ์ง€) | -| **๋˜๋Œ๋ฆฌ๊ธฐ(Rollback) ๋น„์šฉ** | โ€” | ๐Ÿ”ด ๋†’์Œ (๋น„๊ฐ€์—ญ ์ฝ”๋“œ ์žฌ์ž‘์„ฑ) | ๐ŸŸข **0 (๊ฐ€์—ญ์  ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์Šค์œ„์น˜)** | -| **์ตœ์ข… ํ‰๊ฐ€** | **๊ธฐ๊ฐ (๋ณด์•ˆ ์œ„ํ—˜)** | **๊ธฐ๊ฐ (๋น„์šฉ ๋Œ€๋น„ ์‹ค์ต ์ „๋ฌด)** | ๐Ÿ† **์ตœ์ข… ์ฑ„ํƒ** | - ---- - -## 2. ํ˜„ํ–‰ ์•„ํ‚คํ…์ฒ˜ ์‹ค์ธก ๋ฐ ๊ธฐ์ˆ ์  ์ง„๋‹จ (Ground Truth) - -### 2.1 ์ œ์–ด ๊ฒฝ๋กœ ์ƒ์˜ MQTT ์˜์กด๋„ ์‹ค์ธก -- `run_loop.sh` (899์ค„, ๋ฉ”์ธ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ) ๋‚ด MQTT ์ง์ ‘ ์ฐธ์กฐ๋Š” `:889`์˜ ์ž„์‹œ ๊ตฌ๋…์ž ๋กœ๊ทธ ํŒŒ์ผ ์‚ญ์ œ 1๊ฑด๋ฟ์ž…๋‹ˆ๋‹ค. -- ์ž‘์—… ์™„๋ฃŒ ๊ฐ์ง€๋Š” 11๊ฐœ ํ˜ธ์ถœ ์ง€์ (์ „์ฒด 12๊ฐœ ์ฐธ์กฐ) ์ „์ฒด๊ฐ€ `wait_for_job()` ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด `.mam/jobs/.json` ํŒŒ์ผ์˜ `status` ํ•„๋“œ๋ฅผ 3์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. -- ๋”ฐ๋ผ์„œ ๋ธŒ๋กœ์ปค๊ฐ€ ๋‹ค์šด๋˜์–ด๋„ ์ œ์–ด ํ‰๋ฉด ์ž์ฒด๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์™„์ฃผํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. - -### 2.2 paho-mqtt ๊ฒฐํ•ฉ๋„ (Blast Radius) -- `import paho`๋Š” `mqtt_common.py:32` ๋‹จ 1๊ณณ์— ์บก์Аํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. -- ๊ทธ๋Ÿฌ๋‚˜ `make_client()`๊ฐ€ raw `mqtt.Client` ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ๋‹ค์Œ 4๊ฐœ ์ง€์ ์—์„œ ๊ตฌ๋™๋ฉ๋‹ˆ๋‹ค: - 1. `mqtt_common.py:250-276` (`make_client`) - 2. `publish_event.py:102-122` (๋ฐœํ–‰ ๋ฐ ACK ๋Œ€๊ธฐ) - 3. `job_subscriber.py:172-251` (์ด๋ฒคํŠธ ํ์ž‰ ๋ฐ ๊ตฌ๋…) - 4. `reconcile.sh:245-292` (๋‚ด์žฅ python ์ด๋ฒคํŠธ ์ˆ˜์‹ ) - ---- - -## 3. ์ฝ”๋“œ๋ฒ ์ด์Šค ์ž ๋ณต ๊ฒฐํ•จ ๋ถ„์„ (F-1 ~ F-5) - -๋ธŒ๋กœ์ปค ์ œํ’ˆ ์„ ํƒ๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ํ˜„ํ–‰ ์ฝ”๋“œ์— ์ž ๋ณตํ•ด ์žˆ๋Š” 5๊ฐ€์ง€ ๊ตฌ์กฐ์  ๊ฒฐํ•จ์ด ๋ฐœ๊ตด๋˜์—ˆ์Šต๋‹ˆ๋‹ค. - -``` -[๋ฐœ๊ตด๋œ ๊ฒฐํ•จ ์ฒด์ธ] -F-1: publish ์‹คํŒจ ์‹œ return 2 โ”€โ”€> ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ƒํƒœ ๋™๊ธฐํ™” ๋ˆ„๋ฝ โ”€โ”€> run_loop 3900์ดˆ(65๋ถ„) ์ •์ง€ -F-4: subscriber ๋ฏธํฌ์ฐฉ ์˜ˆ์™ธ rc=1 โ”€โ”€> loop/discuss ๊ฒฝ๋กœ์—์„œ job_status="error" ์˜คํŒ์ • -F-2/F-3: ์ „์—ญ ํ† ํ”ฝ + auth_token ์กฐ๊ฑด๋ถ€ โ”€โ”€> ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ฒฉ๋ฆฌ ๋ฐ HMAC ๊ฒ€์ฆ ์‚ฌ๊ฐ์ง€๋Œ€ (A-2) -F-5: ๋งค ์‹คํ–‰ ๋žœ๋ค client_id โ”€โ”€> ๋ฌธ์„œ๊ฐ€ ์ฃผ์žฅํ•˜๋Š” durable session ๊ตฌ์„ฑ ๋ถˆ๊ฐ€ -``` - -### 3.1 F-1 (Critical): ๋ฐœํ–‰ ์‹คํŒจ ์‹œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ฐฑ์‹  ๋ˆ„๋ฝ (65๋ถ„ ๋ฃจํ”„ ์ •์ง€) -- `publish_event.py:195-199`์—์„œ ๋ธŒ๋กœ์ปค ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ `return 2`๋กœ ์กฐ๊ธฐ ์ข…๋ฃŒ๋ฉ๋‹ˆ๋‹ค. -- ์ด๋กœ ์ธํ•ด ๋’ค๋”ฐ๋ฅด๋Š” `append_event`, `registry.append_event`, `update_job_status(status=completed)`๊ฐ€ ์‹คํ–‰๋˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. -- `wait_for_job`์€ `status=running` ์ƒํƒœ์—์„œ `max_wait=3900s`๋ฅผ ์†Œ์ง„ํ•  ๋•Œ๊นŒ์ง€ **65๋ถ„๊ฐ„ ์ •์ง€**ํ•ฉ๋‹ˆ๋‹ค. - -### 3.2 F-2 (High): ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ง€๋ฌธ ํ† ํ”ฝ ๋ฏธ๋ฐœํ–‰ (A-2) -- `reconcile.sh:236`์€ ์ง€๋ฌธ ํ† ํ”ฝ(`mam//jobs/+/events`)์„ ๊ตฌ๋…ํ•˜์ง€๋งŒ, `mqtt_common.py:119`๋Š” ์ „์—ญ ํ† ํ”ฝ(`python/mqtt/jobs`)์œผ๋กœ๋งŒ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค. -- ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ฐ„ ๋ฉ”์‹œ์ง€ ๊ฒฉ๋ฆฌ๊ฐ€ ์‹ค์งˆ์ ์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. - -### 3.3 F-3 (High): HMAC ์ธ์ฆ ์กฐ๊ฑด๋ถ€ ๊ณตํ—ˆํ™” (A-2) -- `registry.py:75-79`๋Š” TLS๋‚˜ ์‚ฌ์šฉ์ž ์ธ์ฆ์ด ์ผœ์ง„ ๋ณด์•ˆ ๋ธŒ๋กœ์ปค ๊ฐ์ง€ ์‹œ `secrets.token_urlsafe(32)`๋ฅผ ์ž๋™ ์ƒ์„ฑํ•˜๋‚˜, ๊ธฐ๋ณธ ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค(๋˜๋Š” ํ‰๋ฌธ TCP ๋ธŒ๋กœ์ปค) ํ™˜๊ฒฝ์—์„œ๋Š” ํ† ํฐ์ด ๋ฐœ๊ธ‰๋˜์ง€ ์•Š์•„ `auth_token=None`์œผ๋กœ ๋‚จ์Šต๋‹ˆ๋‹ค. -- ์ด๋กœ ์ธํ•ด `verify_hmac`์˜ `if not auth_token: return True` ๋ถ„๊ธฐ๊ฐ€ ๋ฌด์กฐ๊ฑด ์ฐธ์ด ๋˜์–ด ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ํ™˜๊ฒฝ์—์„œ HMAC ๊ฒ€์ฆ์ด ๋ฌด๋ ฅํ™”๋ฉ๋‹ˆ๋‹ค (Track 2์—์„œ ์ „ ๋ธŒ๋กœ์ปค ๋Œ€์ƒ ๋ฌด์กฐ๊ฑด ๋ฐœ๊ธ‰์œผ๋กœ ์‹ฌ์ธต ๋ฐฉ์–ด ์ ์šฉ ํ•„์š”). - -### 3.4 F-4 (Critical): `loop`/`discuss` ์œ„์ž„ ๊ฒฝ๋กœ์˜ ์˜คํŒ์ • ๊ฒฐํ•จ -- `multi-agent-mux-delegate-job:331-341`์—์„œ `wait "$sub_pid"`์˜ `sub_rc`๋ฅผ ์ง์ ‘ `job_status`๋กœ ๋งคํ•‘(`rc=1` -> `job_status="error"`). -- ๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ ์‹คํŒจ ์‹œ `job_subscriber.py`๊ฐ€ ๋ฏธํฌ์ฐฉ ์˜ˆ์™ธ๋กœ `rc=1`์„ ๋‚ด๋ฏ€๋กœ, **๋ธŒ๋กœ์ปค ์ ‘์† ์‹คํŒจ๊ฐ€ ์ž‘์—… ์—๋Ÿฌ๋กœ ๋‘”๊ฐ‘**ํ•ฉ๋‹ˆ๋‹ค. - -### 3.5 F-5 (Medium): ์˜์† ์„ธ์…˜(Durable Session) ๊ตฌ์„ฑ ๋ถˆ๊ฐ€ -- `make_client()`๊ฐ€ ๋งค ์‹คํ–‰๋งˆ๋‹ค `uuid.uuid4().hex[:8]`๋กœ ๋žœ๋ค `client_id`๋ฅผ ์ƒ์„ฑํ•˜๋ฏ€๋กœ, ๋ธŒ๋กœ์ปค๊ฐ€ ์žฌ์—ฐ๊ฒฐ ์„ธ์…˜์„ ์‹๋ณ„ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - ---- - -## 4. ๋ฆฌ๋ทฐ ๋ฐ ์ฑŒ๋ฆฐ์ง€ ๊ฒ€์ฆ (Challenge Resolution) - -### 4.1 C1 ์ฑŒ๋ฆฐ์ง€ ๋ถ„์„ ๋ฐ ํŒ์ • -Reviewer (`agy`)๊ฐ€ ์ œ๊ธฐํ•œ `job_subscriber.py`์˜ ์ œ์–ด ๊ฒฝ๋กœ ๋ธ”๋กœํ‚น ๋ฐ ๋””์Šคํฌ ํด๋ฐฑ ๋ˆ„๋ฝ ์ง€์ ์„ ์‹ค์ธก ๊ฒ€์ฆํ•˜์˜€์Šต๋‹ˆ๋‹ค. - -1. **C1-a (์œ„์ž„ ๋Œ€๊ธฐ ๊ฒฝ๋กœ ์‹ค์žฌ)**: `multi-agent-mux-delegate-job:227`์— `wait "$sub_pid"`๊ฐ€ ์กด์žฌํ•˜๋ฉฐ, `run_loop.sh`์˜ ๋ชจ๋“  ํ˜ธ์ถœ๋ถ€๊ฐ€ `--type direct`๋กœ ์ด ๊ฒฝ๋กœ๋ฅผ ํ†ต๊ณผํ•จ์„ ํ™•์ธ (์ˆ˜์šฉ). -2. **C1-b (๋””์Šคํฌ ํด๋ฐฑ ๋ถ€์žฌ)**: `job_subscriber.py`๋Š” ์˜ค์ง `watcher.events.get()`๋งŒ ๋Œ€๊ธฐํ•˜๋ฏ€๋กœ ๋ธŒ๋กœ์ปค ๋‹จ์ ˆ ์‹œ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์ง€ ๋ชปํ•จ (์ˆ˜์šฉ). -3. **C1-c (๋ฉ”์ปค๋‹ˆ์ฆ˜ ์„ ํ›„๊ด€๊ณ„)**: C1์€ F-1์ด ํ•ด๊ฒฐ๋˜์–ด ๋””์Šคํฌ์— ์™„๋ฃŒ ์ƒํƒœ๊ฐ€ ์“ฐ์—ฌ์ง„ ์ดํ›„์— ๋“œ๋Ÿฌ๋‚˜๋Š” ์—ฐ์‡„ ๊ฒฐํ•จ์ž„ (์ •์ • ๋ฐ ๋ฐ˜์˜). -4. **C1-d (์ง€์—ฐ ์‹œ๊ฐ„ ์‹ค์ธก)**: ๋ธŒ๋กœ์ปค ๋„๋‹ฌ ๋ถˆ๊ฐ€ ์‹œ ๊ตฌ๋…์ž๋Š” 15~40์ดˆ ๋‚ด `rc=1`๋กœ ์กฐ๊ธฐ ์ข…๋ฃŒ๋˜์–ด ์‹ค์ œ ์ถ”๊ฐ€ ๋ธ”๋กœํ‚น์€ 0์ดˆ์ž„ (์ง€์—ฐ ์˜ํ–ฅ ๊ธฐ๊ฐ, ๊ทธ๋Ÿฌ๋‚˜ ๊ฐ์‚ฌ ๋กœ๊ทธ ์˜ค์—ผ ๋ฐ ๊ฑฐ์ง“ ์‹คํŒจ ํŒ์ •์˜ ์‹ฌ๊ฐ์„ฑ์œผ๋กœ ์ฑ„ํƒ). -5. **C1-e (ํ•ด๊ฒฐ์ฑ… ์ฑ„ํƒ)**: `job_subscriber.py`์˜ ๋Œ€๊ธฐ ๋ฃจํ”„์— ๋กœ์ปฌ ๋””์Šคํฌ(`load_job` / `read_logged_status`) ํด๋ฐฑ์„ ๋„์ž…ํ•˜์—ฌ ๋ธŒ๋กœ์ปค ๋‹จ์ ˆ ์‹œ์—๋„ ์ฆ‰์‹œ ์ •์ƒ ์ข…๋ฃŒํ•˜๋„๋ก ๋ณด๊ฐ•. - ---- - -## 5. ๋‹จ๊ณ„๋ณ„ ์‹คํ–‰ ๊ณ„ํš (Actionable Roadmap) - -``` -[Track 0: ๊ฒฐํ•จ ๊ต์ •] โ”€โ”€> [Track 1: nats-server ์ŠคํŒŒ์ดํฌ] โ”€โ”€> [Track 2: A-2 ๋ณด์•ˆ/๊ฒฉ๋ฆฌ ํ•ด์†Œ] โ”€โ”€> [Track 3: ๋ฌธ์„œํ™”] -(F-1, C1, F-4 ํ•ด๊ฒฐ) (S-1 ~ S-9 ๋งคํŠธ๋ฆญ์Šค ๊ฒ€์ฆ) (F-2, F-3, Token, ACL) (MESSAGING, VERSIONS) -``` - -### 5.1 Track 0 โ€” ๊ฐ€์šฉ์„ฑ ๋ฐ ๊ฒฐํ•จ ๊ต์ • (์ตœ์šฐ์„  ๊ณผ์ œ, ๋ธŒ๋กœ์ปค ๋ฌด๊ด€) - -#### Step ์ˆœ์„œ ์˜์กด์„ฑ (Strict Ordering) -1. **Step 1 (`publish_event.py`)**: ๋ฐœํ–‰ ์‹คํŒจ ์‹œ์—๋„ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ƒํƒœ ๋™๊ธฐํ™” ๋ฐ ๊ฐ์‚ฌ ๋กœ๊ทธ ์ž‘์„ฑ์„ ์™„์ˆ˜ํ•˜๊ณ  `return 2` ๋ฐ˜ํ™˜. -2. **Step 2 (`job_subscriber.py`)**: `queue.Empty` ์‹œ 3์ดˆ ์Šค๋กœํ‹€๋กœ ๋””์Šคํฌ ํ„ฐ๋ฏธ๋„ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์—ฌ `source: disk-fallback` ํ•ฉ์„ฑ ์ด๋ฒคํŠธ ์ถœ๋ ฅ ํ›„ `rc=0` ์กฐ๊ธฐ ์ข…๋ฃŒ. -3. **Step 3 (`multi-agent-mux-delegate-job`)**: ์ธํ”„๋ผ ์˜ˆ์™ธ์— ์ „์šฉ `rc=3`์„ ๋ถ€์—ฌํ•˜๊ณ  `job_status="broker_unavailable"` ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ. - -#### ํšŒ๊ท€ ๊ฐ€๋“œ ๋งคํŠธ๋ฆญ์Šค (11์ข… ์‹ ์„ค โ€” G-1 ~ G-11, ๋ชฉํ‘œ 287/287 PASS) -- **G-1**: ๋ธŒ๋กœ์ปค ๋„๋‹ฌ ๋ถˆ๊ฐ€ ๋ฐœํ–‰ ์‹œ `rc=2`์ด๋ฉด์„œ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ `status=completed` ํ™•์ธ. -- **G-2**: ๊ฐ์‚ฌ ๋กœ๊ทธ์— `published: false` ๋ฐ `publish_error` ํ•„๋“œ ๊ธฐ๋ก ํ™•์ธ. -- **G-3**: ์ •์ƒ ๋ธŒ๋กœ์ปค ๋ฐœํ–‰ ์‹œ `rc=0` ๋ฐ `published: true` ๋ฌดํšŒ๊ท€ ํ™•์ธ. -- **G-4**: ๋ฐœํ–‰ ์‹คํŒจ ์‹œ ๋‹จ์กฐ `last_seq` ์ฆ๊ฐ€ ๋ฐ ํ›„์† ๋ฐœํ–‰ seq ๋ณด์กด ํ™•์ธ. -- **G-5**: ๋””์Šคํฌ `status=completed` ์„ ์ž‘์„ฑ ์‹œ ๋ธŒ๋กœ์ปค ๋‹ค์šด ์ƒํƒœ์—์„œ๋„ `job_subscriber.py`๊ฐ€ 3์ดˆ ๋‚ด `rc=0` ์ข…๋ฃŒ. -- **G-6**: ๋””์Šคํฌ ํด๋ฐฑ ์ข…๋ฃŒ ์‹œ stdout์— `disk-fallback` ๋ช…์‹œ ํ™•์ธ. -- **G-7**: ๋””์Šคํฌ `status=error` ์‹œ ํด๋ฐฑ `rc=1` ๋ฐ˜ํ™˜ ํ™•์ธ. -- **G-8**: ๋‹ค์ค‘ ์žก ๊ฐ์‹œ ์‹œ ์ „์ฒด ์™„๋ฃŒ ์ „๊นŒ์ง€ ์กฐ๊ธฐ ์ข…๋ฃŒ ๋ฐฉ์ง€. -- **G-9**: ๋””์Šคํฌ ํ„ฐ๋ฏธ๋„ ๋ถ€์žฌ + ๋ธŒ๋กœ์ปค ์‹คํŒจ ์‹œ `rc=3` ๋ฐ˜ํ™˜ ํ™•์ธ (F-4 ๋ฐฉ์–ด). -- **G-10**: `loop` ์œ„์ž„ ๊ฒฝ๋กœ์—์„œ `rc=3` ์ˆ˜์‹  ์‹œ `job_status`๊ฐ€ `"error"`๋กœ ์˜คํŒ๋˜์ง€ ์•Š์Œ์„ ํ™•์ธ. -- **G-11**: `registry.register_job()` ํ˜ธ์ถœ ์‹œ `auth_token`์ด ํ•ญ์ƒ ๋น„์–ด์žˆ์ง€ ์•Š๊ฒŒ ์ƒ์„ฑ๋จ์„ ๋‹จ์–ธ (`secrets.token_urlsafe(32)` ์œ ์ง€). - -### 5.2 Track 1 โ€” `nats-server` ์ŠคํŒŒ์ดํฌ ๊ฒ€์ฆ ๋งคํŠธ๋ฆญ์Šค (S-1 ~ S-9) - -๊ฒฉ๋ฆฌ ํด๋ก (`$SCRATCH/nats-spike`)์—์„œ ๊ฒ€์ฆ ์ˆ˜ํ–‰: -- **S-1**: `nats-server -js` MQTT ๋ฆฌ์Šค๋„ˆ ๊ธฐ๋ณธ ๊ตฌ๋™ ๋ฐ `started/progress/completed` ๋ฐœํ–‰ ์ˆ˜์šฉ (`rc=0`). -- **S-2**: paho-mqtt 2.x `CallbackAPIVersion.VERSION2` CONNACK ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ. -- **S-3 (ํ•ต์‹ฌ ๊ด€๋ฌธ)**: **Retained terminal event ์ •์ƒ ์ „๋‹ฌ ๊ฒ€์ฆ** (๋Šฆ์€ ๊ตฌ๋…์ž์˜ ์ฆ‰์‹œ ์ตœ์ข… ์ƒํƒœ ์ˆ˜์‹ ). *์‹คํŒจ ์‹œ mosquitto๋กœ ํšŒ๊ท€*. -- **S-4**: QoS 1 `wait_for_publish` ACK ๋™์ž‘ ๊ฒ€์ฆ. -- **S-5**: ์™€์ผ๋“œ์นด๋“œ ํ† ํ”ฝ(`mam//jobs/+/events`) ๊ตฌ๋… ๋ฐ ๋ผ์šฐํŒ… ๊ฒ€์ฆ. -- **S-6**: TLS ์•”ํ˜ธํ™” ๋ฐ ์œ ์ € ์ธ์ฆ ์ ‘๊ทผ ์ œ์–ด ๊ฒ€์ฆ. -- **S-7**: Subject/Topic ๋ ˆ๋ฒจ ๊ถŒํ•œ ๋ถ„๋ฆฌ(Publisher write-only / Subscriber read-only) ๊ฒ€์ฆ. -- **S-8**: ์ „์ฒด 287๊ฑด ํšŒ๊ท€ ํ…Œ์ŠคํŠธ 100% PASS ๊ฒ€์ฆ. -- **S-9**: Track 0 ๋””์Šคํฌ ํด๋ฐฑ์ด `nats-server` ์žฅ์•  ์ƒํ™ฉ์—์„œ๋„ ์ •์ƒ ๋™์ž‘ํ•จ์„ ํ†ตํ•ฉ ๊ฒ€์ฆ. - -### 5.3 Track 2 โ€” A-2 ๋ณด์•ˆ ๋ฐ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ฒฉ๋ฆฌ ํ•ด์†Œ -1. **F-3 ํ•ด์†Œ**: `registry.register_job()`์—์„œ ๋ธŒ๋กœ์ปค ์„ค์ •(TLS/์ธ์ฆ ์œ ๋ฌด)๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ `secrets.token_urlsafe(32)` ๊ธฐ๋ฐ˜ `auth_token`์„ **๋ฌด์กฐ๊ฑด ํ•ญ์ƒ ๋ฐœ๊ธ‰**. -2. **F-2 ํ•ด์†Œ**: `DEFAULT_TOPIC_ROOT`๋ฅผ `mam//jobs`๋กœ ์ „ํ™˜. ๋ฐœํ–‰์ธก ์ „ํ™˜ ํ›„ `reconcile.sh`์˜ ๋ ˆ๊ฑฐ์‹œ ๊ตฌ๋… ๋‹จ๊ณ„์  ์ œ๊ฑฐ. -3. ๋ฐฐํฌ ์„ค์ •์— nkeys ๊ธฐ๋ฐ˜ ๊ณ„์ • ๋ถ„๋ฆฌ ์ ์šฉ. - -### 5.4 Track 3 โ€” ๋ฌธ์„œ ๋ฐ ์„ค์ • ๋™๊ธฐํ™” -- `MESSAGING.md`: ๋ธŒ๋กœ์ปค ์‚ฌ์–‘์„ `nats-server`๋กœ ๊ฐฑ์‹ , F-1/C1 ํ•ด์†Œ ๊ธฐ๋ก, F-5 ์‹ค์ธก์— ๋งž์ถ˜ ์˜์† ์„ธ์…˜ ์„ค๋ช… ์ •์ •. -- `IMPROVEMENTS.md` & `VERSIONS.md`: A-2 ์™„๋ฃŒ ์ „ํ™˜, F-1/F-4/F-5 ๋ฐฑ๋กœ๊ทธ ์ด๋ ฅ ๋ฐ˜์˜. -- `.mam.env`: `nats-server` ํฌํŠธ(1883/8883) ๋ฐ ์ธ์ฆ ํ…œํ”Œ๋ฆฟ ๊ฐฑ์‹ . - ---- - -## 6. ๋น„-๋ชฉํ‘œ (Explicit Non-Goals) - -1. โŒ **`nats-py` ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋„์ž… ๋ฐ ํด๋ผ์ด์–ธํŠธ ๋น„๋™๊ธฐ ์žฌ์ž‘์„ฑ**: ๋ถˆํ•„์š”ํ•œ ๋ณต์žก๋„ ๋ฐ ์žฅ์•  ์œ ๋ฐœ. -2. โŒ **`.mam/jobs/*.json`์˜ JetStream KV ๋Œ€์ฒด**: ํŒŒ์ผ์‹œ์Šคํ…œ ํด๋ง ์ œ์–ด ๊ณ„์•ฝ์„ ํ›ผ์†ํ•˜๋ฏ€๋กœ ์ƒํƒœ ๊ณ„์ธต ๋ณ€๊ฒฝ ์ œ์™ธ. -3. โŒ **Durable Session ๊ฐ•์ œ ๋„์ž…์„ ์œ„ํ•œ `client_id` ๊ณ ์ •**: ๋™์‹œ์„ฑ ์ถฉ๋Œ ์œ„ํ—˜์ด ํฌ๋ฉฐ, Track 0 ๋””์Šคํฌ ํด๋ฐฑ์ด ๋™์ผ ๋ณต์›๋ ฅ์„ ๋ฌด๋น„์šฉ์œผ๋กœ ์ œ๊ณตํ•จ. -4. โŒ **`requirements.txt` ๋‚ด `paho-mqtt>=2.0.0` ์ œ๊ฑฐ**: ํ˜„ํ–‰ ์ข…์†์„ฑ ์œ ์ง€. - ---- - -## 7. ๊ฒฐ๋ก  - -MAM ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๋ฉ”์‹œ์ง• ๋ฐฑํ”Œ๋ ˆ์ธ์€ **ํด๋ผ์ด์–ธํŠธ ํ”„๋กœํ† ์ฝœ(MQTT)์„ 100% ๋ณด์กดํ•œ ์ƒํƒœ์—์„œ `nats-server`๋ฅผ ์ „์šฉ ๋ธŒ๋กœ์ปค๋กœ ์ฑ„ํƒ(Option C)**ํ•˜๋Š” ๊ฒƒ์ด ๊ธฐ์ˆ ์ ยท์šด์˜์ ยท๋ณด์•ˆ์  ์ตœ์ ํ•ด์ž…๋‹ˆ๋‹ค. - -์„ ํ–‰ ํ•„์ˆ˜ ๊ณผ์ œ์ธ **Track 0(F-1 + C1 + F-4 ๊ฐ€์šฉ์„ฑ ๊ฒฐํ•จ ๊ต์ •)**์„ ์šฐ์„  ์™„์ˆ˜ํ•œ ํ›„, ์ŠคํŒŒ์ดํฌ ๊ฒ€์ฆ(Track 1) ๋ฐ A-2 ๋ณด์•ˆ ๊ฐ•ํ™”(Track 2)๋ฅผ ์ˆœ์ฐจ ์ „๊ฐœํ•ฉ๋‹ˆ๋‹ค. diff --git a/PRIVATE_SERVER.md b/PRIVATE_SERVER.md deleted file mode 100644 index 492d75f..0000000 --- a/PRIVATE_SERVER.md +++ /dev/null @@ -1,584 +0,0 @@ -# ๐Ÿ”’ MAM ๊ฐœ์ธ ์ „์šฉ ๋ธŒ๋กœ์ปค(Private Broker) ๊ตฌ์ถ• ๋ฐ ์—ฐ๋™ ๊ฐ€์ด๋“œ (`PRIVATE_SERVER.md`) - -- **์ž‘์„ฑ์ผ**: 2026-08-20 (Rev.2) -- **๋ฌธ์„œ ๋ชฉ์ **: MAM(Multi-Agent Mux)์˜ ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ์˜์กด์„ฑ ๋ฐ ๋ณด์•ˆ ๊ฒฐํ•จ(A-2)์„ ํ•ด์†Œํ•˜๊ธฐ ์œ„ํ•œ ๊ฐœ์ธ ์ „์šฉ ๋ธŒ๋กœ์ปค(NATS / Mosquitto) ๊ตฌ์ถ•, ์šด์˜, ๋‹ค๋Šฅ์„ฑ ํ™œ์šฉ ๋ฐ MAM ํด๋ผ์ด์–ธํŠธ ์—ฐ๋™ ๊ฐ€์ด๋“œ. -- **์—ฐ๊ณ„ ๋ฌธ์„œ**: [`NATS_REPORT.md`](NATS_REPORT.md), [`implementation_plan.md`](implementation_plan.md), [`IMPROVEMENTS.md`](IMPROVEMENTS.md) - ---- - -## 1. ๊ฐœ์š” ๋ฐ ๋„์ž… ๋ฐฐ๊ฒฝ - -ํ˜„์žฌ MAM ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง• ๋ธŒ๋กœ์ปค๋Š” ๊ณต๊ฐœ ์„œ๋ฒ„(`broker.hivemq.com:1883`)๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ์ธ ์ „์šฉ ๋ธŒ๋กœ์ปค(Private Broker)๋ฅผ ๊ตฌ์ถ•ํ•˜์—ฌ ์—ฐ๊ฒฐํ•˜๋ฉด **ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด(0์ค„ ๋ณ€๊ฒฝ)** ๋ณด์•ˆ ์œ„ํ—˜์„ ์›์ฒœ ์ฐจ๋‹จํ•˜๊ณ  ๋„คํŠธ์›Œํฌ ์•ˆ์ •์„ฑ์„ ๋Œ€ํญ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - -``` -[MAM Orchestrator / Agents] - โ”‚ - โ–ผ (MQTT 3.1.1 / TLS) -[Private Dedicated Broker] โ”€โ”€โ”€> ์‚ฌ์„ค๋ง/๊ฐœ์ธ ์„œ๋ฒ„ (NATS Server / Mosquitto) - โ€ข ์™ธ๋ถ€ ๋ถˆ๋ฒ• ํŠธ๋ž˜ํ”ฝ 100% ์ฐจ๋‹จ (A-2 ๋ณด์•ˆ ํ•ด์†Œ) - โ€ข JetStream ์˜์†์„ฑ ๋ฐ NKey/JWT ACL ์ง€์› - โ€ข ์ดˆ์ €์ง€์—ฐ (<1ms) ๋ฐ ๋ฌด์ œํ•œ ๋Œ€์—ญํญ -``` - -MAM์˜ ์ œ์–ด ํ‰๋ฉด(`run_loop.sh`์˜ `wait_for_job` ํŒŒ์ผ์‹œ์Šคํ…œ ํด๋ง)์€ ๋ธŒ๋กœ์ปค์™€ 100% ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋ฏ€๋กœ, ๋ธŒ๋กœ์ปค๋Š” **๋น„๋™๊ธฐ ๊ด€์ธก(observability) ์‚ฌ์ด๋“œ์นด** ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. - ---- - -## 2. ํ•ด๊ฒฐ ์˜์—ญ ๋งคํŠธ๋ฆญ์Šค (๋ธŒ๋กœ์ปค ์ „ํ™˜ vs ์ฝ”๋“œ ํŒจ์น˜) - -์ „์šฉ ๋ธŒ๋กœ์ปค ๊ตฌ์ถ•์œผ๋กœ ์ฆ‰์‹œ ํ•ด๊ฒฐ๋˜๋Š” ์˜์—ญ๊ณผ, ๋กœ์ปฌ ์ฝ”๋“œ ํŒจ์น˜(Track 0)๊ฐ€ ๋ณ‘ํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ์˜์—ญ์˜ ๋ช…ํ™•ํ•œ ๊ตฌ๋ถ„์ž…๋‹ˆ๋‹ค. - -| ๊ตฌ๋ถ„ | ๋‹น๋ฉด ๊ณผ์ œ | ๊ฐœ์ธ ๋ธŒ๋กœ์ปค ๊ตฌ์ถ• ์‹œ | ๋กœ์ปฌ ์ฝ”๋“œ ํŒจ์น˜ ํ•„์š” ์—ฌ๋ถ€ (Track 0) | -|---|---|:---:|:---:| -| **๋ณด์•ˆ (A-2)** | ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ๋…ธ์ถœ ๋ฐ ์™ธ๋ถ€ ์•…์˜์  ์ด๋ฒคํŠธ ์ˆ˜์‹  ์œ„ํ˜‘ | ๐ŸŸข **100% ์ฆ‰์‹œ ํ•ด์†Œ** (์‚ฌ์„ค๋ง/ACL ๊ฒฉ๋ฆฌ) | Track 2์—์„œ ํ† ํฐ ๋ฐœ๊ธ‰ ๊ฐ•์ œ | -| **์•ˆ์ •์„ฑ** | ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค์˜ ์˜ˆ๊ณ  ์—†๋Š” ์ˆœ๋‹จ ๋ฐ ์†๋„ ์ œํ•œ(Rate-limit) | ๐ŸŸข **100% ์ฆ‰์‹œ ํ•ด์†Œ** (์ „์šฉ ๋ฆฌ์†Œ์Šค) | โ€” | -| **๋‚ด๊ฒฐํ•จ์„ฑ (B-14)** | ๋ธŒ๋กœ์ปค ์ผ์‹œ ์žฅ์•  ์‹œ 65๋ถ„ ๋ฃจํ”„ ์ •์ง€(Hang) ๊ฒฐํ•จ | โš ๏ธ ๋ธŒ๋กœ์ปค ์ ๊ฒ€/์ˆœ๋‹จ ์‹œ ์—ฌ์ „ํžˆ ์œ„ํ—˜ | ๐Ÿ”ด **ํ•„์ˆ˜ (Track 0 Step 1 ์„ ํ–‰ ํŒจ์น˜)** | -| **์ง€์—ฐ/์˜คํŒ (B-15)** | ๋ธŒ๋กœ์ปค ๋‹ค์šด ์‹œ 120์ดˆ ์ง€์—ฐ ๋ฐ ์ •์ƒ ์ž‘์—…์˜ ์—๋Ÿฌ ์˜คํŒ์ • | โš ๏ธ ๋ธŒ๋กœ์ปค ์ ๊ฒ€/์ˆœ๋‹จ ์‹œ ์—ฌ์ „ํžˆ ์œ„ํ—˜ | ๐Ÿ”ด **ํ•„์ˆ˜ (Track 0 Step 2 & 3 ์„ ํ–‰ ํŒจ์น˜)** | - -> [!IMPORTANT] -> **๋ฐฉํƒ„ ์•„ํ‚คํ…์ฒ˜ ์›์น™:** -> "Track 0(`B-14`, `B-15`) ํŒจ์น˜๋ฅผ ํ†ตํ•ด ๋ธŒ๋กœ์ปค๊ฐ€ ๋‹ค์šด๋˜์–ด๋„ ๋ฃจํ”„๊ฐ€ 100% ์ •์ƒ ์™„์ฃผํ•˜๋„๋ก ๋กœ์ปฌ ๋””์Šคํฌ ๋‚ด๊ฒฐํ•จ์„ฑ์„ ๋จผ์ € ํ™•๋ณดํ•˜๊ณ , ๊ฐœ์ธ ๋ธŒ๋กœ์ปค๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ A-2 ๋ณด์•ˆ๊ณผ ์„ฑ๋Šฅ์„ ์™„๊ฒฐํ•ฉ๋‹ˆ๋‹ค." - ---- - -## 3. ์ „์šฉ ๋ธŒ๋กœ์ปค ์ถ”์ฒœ ๋ฐ ๋น„๊ต - -MAM ํด๋ผ์ด์–ธํŠธ๋Š” ํ‘œ์ค€ `paho-mqtt`๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, MQTT 3.1.1์„ ์ง€์›ํ•˜๋Š” ๋ชจ๋“  ๋ธŒ๋กœ์ปค์™€ 100% ํ˜ธํ™˜๋ฉ๋‹ˆ๋‹ค. - -| ๋น„๊ต ํ•ญ๋ชฉ | ๐Ÿ† `nats-server` (๊ฐ•๋ ฅ ๊ถŒ์žฅ) | `eclipse-mosquitto` (๋Œ€์•ˆ) | -|---|---|---| -| **์•„ํ‚คํ…์ฒ˜** | Go ๋‹จ์ผ ์ •์  ๋ฐ”์ด๋„ˆ๋ฆฌ (Zero Dependency) | C ๊ธฐ๋ฐ˜ ๊ฒฝ๋Ÿ‰ ์˜คํ”ˆ์†Œ์Šค ๋ธŒ๋กœ์ปค | -| **์ฃผ์š” ํŠน์ง•** | โ€ข ๋‚ด์žฅ MQTT 3.1.1 ๋ฆฌ์Šค๋„ˆ (`mqtt { port: 1883 }`)
โ€ข JetStream ์—”์ง„ ๋‚ด์žฅ (์ด๋ฒคํŠธ ์˜์†ํ™” ๋ฐ ๋ณต๊ตฌ)
โ€ข NKey/JWT ๊ธฐ๋ฐ˜ ๊ณ„์ • ๋ฐ Subject๋ณ„ ์„ธ๋ถ„ํ™”๋œ ACL
โ€ข WebSocket ๋ฐ NATS ๋„ค์ดํ‹ฐ๋ธŒ ํ”„๋กœํ† ์ฝœ ๋™์‹œ ์„œ๋น™ | โ€ข ๊ฐ€์žฅ ๋„๋ฆฌ ์“ฐ์ด๋Š” ํ‘œ์ค€ ๊ฒฝ๋Ÿ‰ MQTT ๋ธŒ๋กœ์ปค
โ€ข ๋‚ฎ์€ ๋ฉ”๋ชจ๋ฆฌ ์ ์œ ์œจ (~10MB) | -| **์ถ”์ฒœ ์šฉ๋„** | ๋ชจ๋˜ ์ธํ”„๋ผ, ํ™•์žฅ์„ฑ, ๊ฐ์‚ฌ ๋กœ๊ทธ ์˜์†ํ™”, ํ™ˆ๋žฉ ํ†ตํ•ฉ | ์ •ํ†ต ์ดˆ๊ฒฝ๋Ÿ‰ ์ž„๋ฒ ๋””๋“œ/๋‹จ์ผ ๋ชฉ์  ํ™˜๊ฒฝ | -| **๋ฐฐํฌ ๋‚œ์ด๋„** | ๐ŸŸข ๋ฐ”์ด๋„ˆ๋ฆฌ 1๊ฐœ ์‹คํ–‰ ๋˜๋Š” Docker 1์ค„ | ๐ŸŸข ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ € (`apt`, `brew`) ๋˜๋Š” Docker | - ---- - -## 4. ๊ฐœ์ธ ์„œ๋ฒ„ ๋ธŒ๋กœ์ปค ๋ฐฐํฌ ๊ฐ€์ด๋“œ - -### 4.1 `nats-server` ๋ฐฐํฌ (๊ถŒ์žฅ) - -`nats-server`์—์„œ MQTT๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด ์„ค์ • ํŒŒ์ผ(`nats.conf`)์— `mqtt { port: 1883 }` ๋ธ”๋ก๊ณผ `jetstream { }` ๋ธ”๋ก์ด ๋ฐ˜๋“œ์‹œ ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - -> [!NOTE] -> `nats-server`์˜ `-m` ํ”Œ๋ž˜๊ทธ๋Š” HTTP ๋ชจ๋‹ˆํ„ฐ๋ง ํฌํŠธ(`--http_port`)๋ฅผ ์ง€์ •ํ•˜๋Š” ์˜ต์…˜์ด๋ฉฐ, MQTT๋ฅผ ์ผœ๋Š” ํ”Œ๋ž˜๊ทธ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. MQTT ํ™œ์„ฑํ™”๋Š” ๋ฐ˜๋“œ์‹œ `-c nats.conf` ์„ค์ • ํŒŒ์ผ์„ ํ†ตํ•ด ๊ตฌ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - -#### 1) ๊ณตํ†ต ์„ค์ • ํŒŒ์ผ (`nats.conf`) -```conf -# nats.conf -server_name: mam-hub - -# JetStream ์˜์† ์Šคํ† ๋ฆฌ์ง€ (MQTT QoS 1 ๋ฐ Retained ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ์— ํ•„์ˆ˜) -jetstream { - store_dir: "/data" # Docker ํ™˜๊ฒฝ ๊ธฐ๋ณธ ์Šคํ† ๋ฆฌ์ง€ (๋„ค์ดํ‹ฐ๋ธŒ ์‹คํ–‰ ์‹œ $HOME ์ „๊ฐœ) - max_file: 10G # ํ™ˆ๋žฉ ๋””์Šคํฌ ์ƒํ•œ ์„ค์ • -} - -# HTTP ๋ชจ๋‹ˆํ„ฐ๋ง ์—”๋“œํฌ์ธํŠธ (/varz, /jsz ๋Œ€์‹œ๋ณด๋“œ) -http_port: 8222 - -# ํ‰๋ฉด A: MAM MQTT 3.1.1 ํ”„๋กœํ† ์ฝœ ๋ฆฌ์Šค๋„ˆ -mqtt { - port: 1883 -} - -# ํ‰๋ฉด B: ํ™ˆ๋žฉ/์›น ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ์šฉ WebSocket ๋ฆฌ์Šค๋„ˆ (์„ ํƒ ์‚ฌํ•ญ) -websocket { - port: 8080 - no_tls: true # ๋‚ด๋ถ€ ์‚ฌ์„ค๋ง ํ•œ์ • -} -``` - -#### 2) ๋ฐฐํฌ ๋ฐฉ๋ฒ• A. Docker / Docker Compose (๊ถŒ์žฅ) - -**๋‹จ์ผ Docker ์‹คํ–‰:** -```bash -# ํ˜ธ์ŠคํŠธ์— nats.conf ์ƒ์„ฑ ํ›„ ์‹คํ–‰ (D-2: alpine ๊ณ ์ • ํ•€, D-3: ๋ฃจํ”„๋ฐฑ/๋ฐ”์ธ๋“œ ํ•œ์ •) -docker run -d \ - --name mam-nats \ - --restart unless-stopped \ - -p "${MQTT_BIND:-127.0.0.1}:1883:1883" \ - -p "${NATS_BIND:-127.0.0.1}:4222:4222" \ - -p "127.0.0.1:8222:8222" \ - -p "${WS_BIND:-127.0.0.1}:8080:8080" \ - -v ./nats.conf:/etc/nats/nats.conf:ro \ - -v nats-data:/data \ - nats:2.12-alpine \ - -c /etc/nats/nats.conf -``` - -**Docker Compose (`docker-compose.yml`):** -```yaml -version: '3.8' - -services: - nats: - image: nats:2.12-alpine - container_name: mam-nats - restart: unless-stopped - command: ["-c", "/etc/nats/nats.conf"] - ports: - - "${MQTT_BIND:-127.0.0.1}:1883:1883" # MQTT 3.1.1 ํฌํŠธ (ํ‰๋ฉด A: MAM) - - "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS ๊ธฐ๋ณธ ํฌํŠธ (ํ‰๋ฉด B) - - "127.0.0.1:8222:8222" # HTTP ๋ชจ๋‹ˆํ„ฐ๋ง (/varz, /jsz, /healthz) - - "${WS_BIND:-127.0.0.1}:8080:8080" # WebSocket (ํ‰๋ฉด B) - volumes: - - ./nats.conf:/etc/nats/nats.conf:ro - - nats-data:/data - -volumes: - nats-data: -``` - -#### 3) ๋ฐฐํฌ ๋ฐฉ๋ฒ• B. ๋„ค์ดํ‹ฐ๋ธŒ ๋ฐ”์ด๋„ˆ๋ฆฌ ์„ค์น˜ (macOS / Linux โ€” ๋น„๋ฃจํŠธ ์‚ฌ์šฉ์ž ๊ณต๊ฐ„) - -macOS์˜ sealed APFS ๋ฃจํŠธ ๋ณผ๋ฅจ(`/data`) ๊ถŒํ•œ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ์ž ํ™ˆ ๋””๋ ‰ํ„ฐ๋ฆฌ(`~/.config/nats/`, `~/.local/share/nats/data`)๋ฅผ ๊ธฐ๋ณธ ์Šคํ† ๋ฆฌ์ง€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. - -```bash -# ์„ค์ • ๋ฐ ๋ฐ์ดํ„ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ ์ƒ์„ฑ (sudo ๋ถˆํ•„์š”) -mkdir -p ~/.config/nats ~/.local/share/nats/data - -# ์„ค์ • ํŒŒ์ผ ์ž‘์„ฑ (D-1: ๋น„์ธ์šฉ heredoc < ~/.config/nats/nats.conf -server_name: mam-hub -jetstream { - store_dir: "$HOME/.local/share/nats/data" - max_file: 10G -} -http_port: 8222 -mqtt { - port: 1883 -} -websocket { - port: 8080 - no_tls: true -} -EOF - -# macOS (Homebrew ์„ค์น˜ ๋ฐ ์‹คํ–‰) -brew install nats-server -nats-server -c ~/.config/nats/nats.conf & - -# Linux (x86_64 ๋‹จ์ผ ๋ฐ”์ด๋„ˆ๋ฆฌ ์„ค์น˜ ๋ฐ ์‹คํ–‰) -curl -L https://github.com/nats-io/nats-server/releases/download/v2.10.20/nats-server-v2.10.20-linux-amd64.tar.gz | tar xz -sudo mv nats-server-v2.10.20-linux-amd64/nats-server /usr/local/bin/ -nats-server -c ~/.config/nats/nats.conf & -``` - ---- - -### 4.2 `mosquitto` ๋ฐฐํฌ (๋Œ€์•ˆ) - -#### Docker ์‹คํ–‰: -```bash -docker run -d \ - --name mam-mosquitto \ - --restart unless-stopped \ - -p 1883:1883 \ - -v ./mosquitto.conf:/mosquitto/config/mosquitto.conf \ - eclipse-mosquitto:latest -``` - -**๊ธฐ๋ณธ `mosquitto.conf` ์„ค์ • ํŒŒ์ผ ์˜ˆ์‹œ:** -```conf -listener 1883 -allow_anonymous true -persistence true -persistence_location /mosquitto/data/ -``` - ---- - -## 5. ํ•˜๋‚˜์˜ ์„œ๋ฒ„๋กœ ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ โ€” `nats-server` ๋‹ค๋Šฅ์„ฑ (Versatility) - -`nats-server`์˜ ๋‹ค๋Šฅ์„ฑ์€ **MAM์„ ๋„ค์ดํ‹ฐ๋ธŒ NATS๋กœ ์ด๊ด€ํ•  ์ด์œ ๊ฐ€ ์•„๋‹ˆ๋ผ, MAM ์ฝ”๋“œ๋ฅผ ํ•œ ์ค„๋„ ๋ฐ”๊พธ์ง€ ์•Š๊ณ ๋„ ์–ป์„ ์ˆ˜ ์žˆ๋Š” ๋ถ€๊ฐ€์  ์ด๋“**์ž…๋‹ˆ๋‹ค. - -### 5.1 ๋‘ ๊ฐœ์˜ ์†Œ๋น„ ํ‰๋ฉด ๋ฐ ์„ธ ๊ฐ€์ง€ ์ ‘์† ๊ฒฝ๋กœ (Consumption Planes & Transport Paths) - -`nats-server`๋Š” ๋‹จ์ผ ํ”„๋กœ์„ธ์Šค ๋‚ด์—์„œ ์—ฌ๋Ÿฌ ํ”„๋กœํ† ์ฝœ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋™์‹œ์— ๊ตฌ๋™ํ•˜๋ฏ€๋กœ, MAM์˜ ๋‹จ์ˆœ์„ฑ๊ณผ ๊ฐœ์ธ ํ™ˆ๋žฉ์˜ ํ™•์žฅ์„ฑ์„ ์™„๋ฒฝํžˆ ์–‘๋ฆฝ์‹œํ‚ต๋‹ˆ๋‹ค. - -``` - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ nats-server (๋‹จ์ผ ์ธ์Šคํ„ด์Šค) โ”‚ - โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค - โ”‚ ํ‰๋ฉด A: MAM ์›Œํฌ๋กœ๋“œ โ”‚ ํ‰๋ฉด B: ํ™ˆ๋žฉ/๊ฐœ์ธ ํ”„๋กœ์ ํŠธ โ”‚ - โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค - ํ”„๋กœํ† ์ฝœ โ”‚ MQTT 3.1.1 (ํฌํŠธ 1883) โ”‚ NATS(4222), WebSocket(8080) โ”‚ - ํด๋ผ์ด์–ธํŠธ โ”‚ paho-mqtt (์ฝ”๋“œ ๋ณ€๊ฒฝ 0์ค„) โ”‚ nats-py, nats.js, MQTT.js ๋“ฑโ”‚ - ์‚ฌ์šฉ ๊ธฐ๋Šฅ โ”‚ QoS 1, Retain, ์™€์ผ๋“œ์นด๋“œ, TLSโ”‚ JetStream ๋ฆฌํ”Œ๋ ˆ์ด, KV, Objectโ”‚ - ์„ค๊ณ„ ์›์น™ โ”‚ ์ดˆ๊ฒฝ๋Ÿ‰ ๋™๊ธฐ CLI ํ•ซํŒจ์Šค ๋ณด์กด โ”‚ ๊ณ ๊ธ‰ ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ โ”‚ - ๊ณต์œ  ์ž์› โ”‚ โ””โ”€โ”€โ”€โ”€โ”€ ๋‹จ์ผ ์ •์  ๋ฐ”์ด๋„ˆ๋ฆฌ / JetStream ์Šคํ† ๋ฆฌ์ง€ / ACL โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -ํŠนํžˆ **WebSocket ํฌํŠธ(8080)**๋Š” ์ ‘์† ๊ฒฝ๋กœ(URL Path)์— ๋”ฐ๋ผ ๋‘ ๊ฐ€์ง€ ํ”„๋กœํ† ์ฝœ์„ ๋™์‹œ์— ์„œ๋น™ํ•ฉ๋‹ˆ๋‹ค: -1. **MQTT 1883**: TCP ๋„ค์ดํ‹ฐ๋ธŒ MQTT 3.1.1 (MAM CLI ์—์ด์ „ํŠธ ํ‘œ์ค€ ๊ฒฝ๋กœ) -2. **WebSocket 8080 (`/mqtt` ๊ฒฝ๋กœ)**: **MQTT-over-WebSocket** (`MQTT.js` ๋“ฑ์œผ๋กœ ์ ‘์†). ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ 1883 ๋ฆฌ์Šค๋„ˆ์™€ ๋™์ผํ•˜๊ฒŒ ์ทจ๊ธ‰๋˜์–ด **retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ์™„์ „ํ•˜๊ฒŒ ์ˆ˜์‹ **ํ•ฉ๋‹ˆ๋‹ค (N-7). -3. **WebSocket 8080 (`/` ๋˜๋Š” ๊ฒฝ๋กœ ์—†์Œ)**: **NATS ๋„ค์ดํ‹ฐ๋ธŒ WebSocket** (`nats.ws` ๋“ฑ์œผ๋กœ ์ ‘์†). ๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆผ๋งŒ ์ˆ˜์‹ ํ•˜๋ฉฐ retained ๋ฉ”์‹œ์ง€๋Š” ์ˆ˜์‹ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (N-1). - -### 5.2 ๊ต์ฐจ ํ”„๋กœํ† ์ฝœ ๋ธŒ๋ฆฌ์ง• (Cross-Protocol Bridging) -- `nats-server`๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ MQTT ํ† ํ”ฝ(`/`)์„ NATS Subject(`.`)๋กœ ์‹ค์‹œ๊ฐ„ ์ž๋™ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. -- MAM ์—์ด์ „ํŠธ๊ฐ€ MQTT ํ† ํ”ฝ `python/mqtt/jobs//events`๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉด, ์›น ๋ธŒ๋ผ์šฐ์ €๋‚˜ ํƒ€ ํ”„๋กœ์ ํŠธ์˜ NATS ๊ตฌ๋…์ž๋Š” NATS Subject `python.mqtt.jobs..events` ๋˜๋Š” `python.mqtt.jobs.*.events`๋กœ ์ฆ‰์‹œ ์‹ค์‹œ๊ฐ„ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -- **์‹ค์šฉ์  ์ด์ **: MAM ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๋‹จ 1์ค„๋„ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ ๋„ React/Vue ์›น ๋Œ€์‹œ๋ณด๋“œ๋‚˜ ํ„ฐ๋ฏธ๋„ ๋ชจ๋‹ˆํ„ฐ๋ง ํˆด์„ ์ฆ‰์‹œ ๋ถ€์ฐฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -- **๊ฒฝ๊ณ„ (ํ•„์ˆ˜ ์ธ์ง€ โ€” N-1 & N-7)**: - - NATS ๋„ค์ดํ‹ฐ๋ธŒ(๊ฒฝ๋กœ ์—†๋Š” WebSocket ๋˜๋Š” 4222)๋กœ ์ ‘์†ํ•˜๋Š” ๊ฒฝ์šฐ: ๊ต์ฐจ ํ”„๋กœํ† ์ฝœ ๋ธŒ๋ฆฌ์ง•์€ **๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ์— ํ•œ์ •**๋ฉ๋‹ˆ๋‹ค. ์žก์ด ๋๋‚œ ๋’ค ์ ‘์†ํ•œ ๋„ค์ดํ‹ฐ๋ธŒ NATS ๋Œ€์‹œ๋ณด๋“œ๋Š” ๊ทธ ์žก์˜ **์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค** (retained ๋ฉ”์‹œ์ง€๋Š” MQTT SUBSCRIBE ๊ฒฝ๋กœ ์ „์šฉ). - - ์›น ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์‚ฌํ›„ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๊นŒ์ง€ ๋ฌด์ƒ์œผ๋กœ ์ˆ˜์‹ ํ•˜๋ ค๋ฉด **`ws://:8080/mqtt` ๊ฒฝ๋กœ๋กœ `MQTT.js` ํด๋ผ์ด์–ธํŠธ๋ฅผ ์—ฐ๊ฒฐ**ํ•˜์‹ญ์‹œ์˜ค (N-7). ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ ๊ตฌ์ถ• ๋ฐ ๋””์Šคํฌ ๊ด€๋ฆฌ ๋ถ€๋‹ด ์—†์ด ์™„์ „ํ•œ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ์ฆ‰์‹œ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -- **๊ณ„์ • ๋ฐฐ์น˜**: ๊ด€์ธก ํด๋ผ์ด์–ธํŠธ๋Š” ยง5.5 ์ฒ˜๋ฐฉ๋Œ€๋กœ `MAM` ๊ณ„์ • ์•ˆ์˜ ์ฝ๊ธฐ ์ „์šฉ ์‚ฌ์šฉ์ž(`mam_observer`)๋กœ ์ ‘์†ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ณ„์ •์—์„œ๋Š” subject๊ฐ€ ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. -- **์ฃผ์˜ ์‚ฌํ•ญ**: ํ† ํ”ฝ ๋ ˆ๋ฒจ ๋‚ด์— ๋งˆ์นจํ‘œ(`.`)๊ฐ€ ํฌํ•จ๋˜๋ฉด NATS ๊ณ„์ธต์—์„œ ํ† ํฐ์ด ๋ถ„๋ฆฌ๋  ์ˆ˜ ์žˆ์œผ๋‚˜, MAM์˜ `job_id`๋Š” 8์ž๋ฆฌ hex, ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ง€๋ฌธ์€ 12์ž๋ฆฌ hex์ด๋ฏ€๋กœ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. - -### 5.3 JetStream ์ด๋ฒคํŠธ ๋ฆฌํ”Œ๋ ˆ์ด (Event Replay) -- `python.mqtt.jobs.>` Subject๋ฅผ ๊ตฌ๋…ํ•˜๋Š” JetStream ์ŠคํŠธ๋ฆผ์„ ์ƒ์„ฑํ•˜๋ฉด, ์ง€๋‚œ ์ž‘์—…์˜ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ ์ „์ฒด๋ฅผ ์‹œ์  ์ง€์ •(Time-based) ๋˜๋Š” ์‹œํ€€์Šค ์ง€์ •(Sequence-based)์œผ๋กœ ์‚ฌํ›„ ๋ฆฌํ”Œ๋ ˆ์ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -- **์ฃผ์˜ ์‚ฌํ•ญ**: ์ด ๊ธฐ๋Šฅ์€ ์˜ตํŠธ์ธ(Opt-in)์ด๋ฉฐ, MQTT QoS 1 ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋‚ด๋ถ€ ์‹œ์Šคํ…œ ์ŠคํŠธ๋ฆผ(`$MQTT_*`)๊ณผ ๋ณ„๊ฐœ๋กœ ๊ด€๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ๋””์Šคํฌ ์šฉ๋Ÿ‰ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด `max_age`๋‚˜ `max_bytes` ์ƒํ•œ์„ ๋ฐ˜๋“œ์‹œ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - -### 5.4 ๋‚ด์žฅ Key-Value (KV) ๋ฐ Object Store -- ํ™ˆ๋žฉ ๋ฐ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์—์„œ Redis๋‚˜ MinIO ๊ฐ™์€ ๋ณ„๋„ ์ธํ”„๋ผ๋ฅผ ๋„์šฐ์ง€ ์•Š๊ณ ๋„ `nats-server` ๋‚ด์žฅ KV ๋ฐ Object Store๋ฅผ ์ฆ‰์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -- **๊ธˆ์ง€ ์‚ฌํ•ญ (Non-Goal)**: MAM์˜ ๋กœ์ปฌ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ(`.mam/jobs/*.json`)๋ฅผ JetStream KV๋กœ ๋Œ€์ฒดํ•ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค (`wait_for_job`์˜ fcntl ๋ฐ ํŒŒ์ผ์‹œ์Šคํ…œ ํด๋ง ๊ณ„์•ฝ ์œ ์ง€). - -### 5.5 ๋ฉ€ํ‹ฐํ…Œ๋„ŒํŠธ ๊ณ„์ • ๋ถ„๋ฆฌ ๋ฐ ๋ณด์•ˆ -- ๋‹จ์ผ ์„œ๋ฒ„ ๋‚ด์—์„œ `MAM` ์ „์šฉ ๊ณ„์ •๊ณผ `HOME` ๊ฐœ์ธ ๊ณ„์ •์„ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ฆฌ์†Œ์Šค ์ฟผํ„ฐ์™€ ๊ถŒํ•œ์„ ์™„๋ฒฝํžˆ ๊ฒฉ๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -- **๊ถŒ๊ณ  ๋ฐฐ์น˜**: MAM๊ณผ ์ด๋ฅผ ๊ด€์ธกํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋™์ผํ•œ ๊ณ„์ •(`MAM`)์— ๋ฐฐ์น˜ํ•˜๊ณ (๊ด€์ธก์ž๋Š” ์ฝ๊ธฐ ์ „์šฉ `mam_observer` ์—ญํ•  ๋ถ€์—ฌ), ๋ฌด๊ด€ํ•œ ํ™ˆ๋žฉ ์„œ๋น„์Šค๋Š” ๋ณ„๋„ ๊ณ„์ •(`HOME`)์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค. - ---- - -## 6. MAM ํด๋ผ์ด์–ธํŠธ ์—ฐ๋™ ์„ค์ • (`.mam.env`) - -๊ฐœ์ธ ์„œ๋ฒ„ ๋ธŒ๋กœ์ปค๊ฐ€ ๊ตฌ๋™๋˜๋ฉด, MAM ์ €์žฅ์†Œ ๋ฃจํŠธ์˜ [`.mam.env`](file:///.mam.env) ํŒŒ์ผ์— ๊ฐœ์ธ ์„œ๋ฒ„ ์ฃผ์†Œ๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. - -> [!NOTE] -> MAM ์ฝ”๋“œ(`mqtt_common.py`)๋Š” `MQTT_*` ์ ‘๋‘์‚ฌ์˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค. ์ด์ „ ๋น„๊ณต์‹ ๋ฌธ์„œ์˜ `MAM_MQTT_*` ๋ณ€์ˆ˜๋Š” ๋ฌดํšจํ•˜๋ฏ€๋กœ ๋ฐ˜๋“œ์‹œ ์•„๋ž˜์˜ ํ‘œ์ค€ ๋ณ€์ˆ˜๋ช…์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - -```bash -# ============================================================================== -# MAM Private MQTT Broker Configuration (.mam.env) -# ============================================================================== - -# โ”€โ”€ ๋ชจ๋ธ T (Tailscale ์‚ฌ์„ค ์˜ค๋ฒ„๋ ˆ์ด๋ง ๊ถŒ์žฅ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -MQTT_BROKER="mam-hub.tailXXXX.ts.net" # ๋˜๋Š” 100.x.y.z (ํ‰๋ฌธ์ด๋ฏ€๋กœ IP ์‚ฌ์šฉ ๊ฐ€๋Šฅ) -MQTT_PORT=1883 -MQTT_TLS=0 -MQTT_USERNAME=mam_agent -MQTT_PASSWORD=replace_me_with_token -MQTT_KEEPALIVE=60 # WAN ๊ตฌ๊ฐ„ ๊ถŒ์žฅ ์—ฐ๊ฒฐ ์œ ์ง€ ์‹œ๊ฐ„ - -# โ”€โ”€ ๋ชจ๋ธ P (๊ณต๊ฐœ TLS / Let's Encrypt ๋ชจ๋ธ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# MQTT_BROKER="mam-broker.example.com" # D-4: ๋ฐ˜๋“œ์‹œ ์ธ์ฆ์„œ SAN ์˜ DNS ์ด๋ฆ„ (IP ๊ธˆ์ง€) -# MQTT_PORT=8883 -# MQTT_TLS=1 -# MQTT_USERNAME=mam_agent -# MQTT_PASSWORD=replace_me_with_token -# MQTT_CA_CERTS ๋Š” Let's Encrypt ์‚ฌ์šฉ ์‹œ '์„ค์ •ํ•˜์ง€ ์•Š์Œ' (์‹œ์Šคํ…œ ์‹ ๋ขฐ ์ €์žฅ์†Œ ์‚ฌ์šฉ) -``` - -*์ฐธ๊ณ : OS ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ๋™์ผํ•œ ์ด๋ฆ„์ด ์ด๋ฏธ `export`๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ OS ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ `.mam.env` ํŒŒ์ผ ์„ค์ •๋ณด๋‹ค ์šฐ์„ ํ•ฉ๋‹ˆ๋‹ค.* - ---- - -## 7. ์—ฐ๋™ ๋ฐ ๋™์ž‘ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ (4-Step Verification) - -๊ฐœ์ธ ์„œ๋ฒ„ ๋ธŒ๋กœ์ปค์™€์˜ ์—ฐ๋™ ์ƒํƒœ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ๊ฒ€์ฆํ•˜๋Š” 4๋‹จ๊ณ„ ์ ˆ์ฐจ์ž…๋‹ˆ๋‹ค. - -### Step 1. ๋ธŒ๋กœ์ปค ๋ฆฌ์Šค๋„ˆ ๋ฐ JetStream ์ƒํƒœ ํ™•์ธ -```bash -# MQTT ๋ฆฌ์Šค๋„ˆ ํ™œ์„ฑํ™” ํ™•์ธ -curl -s http://127.0.0.1:8222/varz | grep -i mqtt - -# JetStream ์—”์ง„ ์ •์ƒ ๊ตฌ๋™ ํ™•์ธ -curl -s http://127.0.0.1:8222/jsz -``` - -### Step 2. ์ž„์‹œ ์žก ๋“ฑ๋ก ๋ฐ ์—ฐ๊ฒฐ ๊ฒ€์ฆ ์ด๋ฒคํŠธ ๋ฐœํ–‰ -`publish_event.py`๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋“ฑ๋ก๋œ ์žก์— ๋Œ€ํ•ด์„œ๋งŒ ๋ฐœํ–‰์„ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ, ์ž„์‹œ ์žก์„ ๋“ฑ๋กํ•˜๊ณ  ๋ฐœํ–‰ํ•œ ํ›„ ์™„๋ฃŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. - -```bash -# 1) ์ž„์‹œ ์žก ๋“ฑ๋ก (์ž๋™ ์ฑ„๋ฒˆ๋œ JID ์บก์ฒ˜) -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 test 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) ํ…Œ์ŠคํŠธ ์žก ์ข…๊ฒฐ ์ฒ˜๋ฆฌ (๋ฏธ์ข…๊ฒฐ ์‹œ --wait-any ์œ ๋ น ์žก ์ž”์กด ๋ฐฉ์ง€) -.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \ - --registry-dir .mam/jobs status --job "$JID" --set completed -``` - -### Step 3. ์ ‘์† ๋Œ€์ƒ ๋ธŒ๋กœ์ปค IP ๋‹จ์–ธ -Step 2์˜ `-v` ์ถœ๋ ฅ ๋กœ๊ทธ ๋˜๋Š” `.mam/delegate_job_logs/$JID/events.ndjson` ํŒŒ์ผ์—์„œ ์‹ค์ œ ์ ‘์† ํ˜ธ์ŠคํŠธ๊ฐ€ ๊ฐœ์ธ ๋ธŒ๋กœ์ปค IP๋กœ ๋‚˜ํƒ€๋‚˜๊ณ  `broker.hivemq.com`์ด ํฌํ•จ๋˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. - -### Step 4. ๋‹จ์œ„ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ๊ฒ€์ฆ -```bash -.venv/bin/python -m pytest tests/ -q -``` -*์ฐธ๊ณ : MAM์˜ ๊ธฐ๋ณธ ๋‹จ์œ„/์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋Š” ๋ชจ์˜(Mock) ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ ์—ฌ๋ถ€์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ 100% ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ๋„คํŠธ์›Œํฌ ์—ฐ๋™ ๊ฒ€์ฆ์€ Step 1~3์ด ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.* - ---- - -## 8. ๊ถŒ์žฅ ์‹คํ–‰ ์ˆœ์„œ - -``` -[Phase 1: ๋‚ด๊ฒฐํ•จ์„ฑ ํ™•๋ณด] โ”€โ”€> [Phase 2: ๊ฐœ์ธ ๋ธŒ๋กœ์ปค ๊ฐ€๋™] โ”€โ”€> [Phase 3: A-2 ๋ณด์•ˆ ์™„์ „ ์ข…๊ฒฐ] - Track 0 (B-14, B-15) nats-server (nats.conf) ์ง€๋ฌธ ํ† ํ”ฝ ๋ฐ ์ธ์ฆ ํ† ํฐ ๋ฐœ๊ธ‰ - ๋กœ์ปฌ ๋””์Šคํฌ ํด๋ฐฑ ํŒจ์น˜ .mam.env ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์—ฐ๋™ ์™ธ๋ถ€ ๊ฐ„์„ญ 100% ์ฐจ๋‹จ -``` - -1. **Phase 1 (Track 0 ์„ ํ–‰ ํŒจ์น˜)**: `publish_event.py`์™€ `job_subscriber.py`์˜ ๋กœ์ปฌ ๋””์Šคํฌ ํด๋ฐฑ(`B-14`, `B-15`)์„ ๋จผ์ € ์ ์šฉํ•˜์—ฌ ๋ธŒ๋กœ์ปค ๋‹ค์šด ์‹œ์—๋„ ๋ฃจํ”„๊ฐ€ ๋ฉˆ์ถ”์ง€ ์•Š๋Š” ๋ฐฉํƒ„ ๊ตฌ์กฐ๋ฅผ ํ™•๋ฆฝํ•ฉ๋‹ˆ๋‹ค. -2. **Phase 2 (๊ฐœ์ธ ๋ธŒ๋กœ์ปค ๊ฐ€๋™)**: ๊ฐœ์ธ ์„œ๋ฒ„์— `nats-server -c nats.conf`๋ฅผ ๊ตฌ๋™ํ•˜๊ณ  `.mam.env`์— `MQTT_BROKER`๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. -3. **Phase 3 (A-2 ๋ณด์•ˆ ์™„์ „ ์ข…๊ฒฐ)**: ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ง€๋ฌธ ํ† ํ”ฝ(`mam//jobs/...`) ๋ฐ ๋ฌด์กฐ๊ฑด ํ† ํฐ ๋ฐœ๊ธ‰์„ ์ ์šฉํ•˜์—ฌ ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ์œ„ํ—˜์„ ์™„์ „ํžˆ ์˜๊ตฌ ํ๊ธฐํ•ฉ๋‹ˆ๋‹ค. - ---- - -## 9. ์›๊ฒฉ ์„œ๋ฒ„ `nats-server` Docker ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๊ฐ€์ด๋“œ (Track 1R) - -์›๊ฒฉ VPS ๋˜๋Š” ์ƒ์‹œ ๊ฐ€๋™ ํ™ˆ๋žฉ ์„œ๋ฒ„์— ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€์˜ `nats-server`๋ฅผ Docker ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์ถ•ํ•˜๊ณ  MAM๊ณผ ์—ฐ๋™ํ•˜๋Š” ํ‘œ์ค€ ์ ˆ์ฐจ์ž…๋‹ˆ๋‹ค. - -> [!NOTE] -> **์ •๋ณธ ์ž์‚ฐ ์•ˆ๋‚ด**: ๋ณธ ์ ˆ์˜ ์„ค์ •์€ [`docker/`](docker/) ๋””๋ ‰ํ„ฐ๋ฆฌ์— ์ •๋ณธ(canonical) ํŒŒ์ผ(`docker/docker-compose.yaml`, `docker/nats.conf`, `docker/.env.example`, `docker/README.md`)๋กœ ๊ด€๋ฆฌ๋˜๋ฉฐ, ์•„๋ž˜ ์ฝ”๋“œ ํŽœ์Šค๋Š” ๊ทธ ์‚ฌ๋ณธ์ž…๋‹ˆ๋‹ค. ๋‘ ๊ณณ์ด ์–ด๊ธ‹๋‚˜๋ฉด `tests/test_deploy_freshness.py`์˜ D-22 ~ D-30 ํšŒ๊ท€ ๊ฐ€๋“œ๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. - -### 9.1 ํ”„๋กœ๋•์…˜ `nats.conf` - -```conf -# ============================================================================== -# docker/nats.conf โ€” MAM ์›๊ฒฉ ํ”„๋กœ๋•์…˜ ๋ธŒ๋กœ์ปค (Track 1R) -# -# ์ •๋ณธ ๋ฌธ์„œ: PRIVATE_SERVER.md ยง9.1 -# ์‹œํฌ๋ฆฟ: ์ด ํŒŒ์ผ์—๋Š” ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  password ๋Š” docker/.env โ†’ compose -# environment โ†’ ์ปจํ…Œ์ด๋„ˆ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ฃผ์ž…๋˜๋Š” $VAR ์ฐธ์กฐ์ž…๋‹ˆ๋‹ค. -# -# โš  ์•”ํ˜ธ ๋ฌธ์ž ์ œ์•ฝ: NATS ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ’์„ ์ž์ฒด ์„ค์ • ๋ ‰์„œ๋กœ ์žฌํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค. -# ์•”ํ˜ธ์— [๊ณต๋ฐฑ ; , ] } # ' " $] ๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ์„ค์ •์ด ๊นจ์ง€๊ฑฐ๋‚˜ ๋‹ค๋ฅด๊ฒŒ ํ•ด์„๋ฉ๋‹ˆ๋‹ค. -# ๋ฐ˜๋“œ์‹œ `openssl rand -base64 32` (์•ŒํŒŒ๋ฒณ A-Za-z0-9+/=) ๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. -# ============================================================================== -server_name: mam-hub - -# โ”€โ”€ JetStream: MQTT retained/QoS1 ์ €์žฅ์†Œ. ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์žฌ์ˆ˜์‹ ์ด ์—ฌ๊ธฐ์— ์˜์กด โ”€โ”€ -jetstream { - store_dir: "/data" # ์ ˆ๋Œ€๊ฒฝ๋กœ ๊ณ ์ •. '~' ๋„ ์ธ์šฉ๋œ "$HOME" ๋„ ํ™•์žฅ๋˜์ง€ ์•Š์Œ - max_file: 10G # ์ ‘๋ฏธ์‚ฌ๋Š” ๋Œ€๋ฌธ์ž๋งŒ ์œ ํšจ (K/M/G/T) - max_mem: 256M -} - -http_port: 8222 # ๋ฌด์ธ์ฆ ๋ชจ๋‹ˆํ„ฐ๋ง โ†’ ํ˜ธ์ŠคํŠธ ๊ฒŒ์‹œ๋Š” loopback ํ•œ์ • (compose) - -mqtt { - port: 1883 - ack_wait: 60s # WAN RTT ํก์ˆ˜ (๊ธฐ๋ณธ 30s) - max_ack_pending: 1024 # ๋‹ค์ค‘ ์—์ด์ „ํŠธ ๋™์‹œ ๋ฐœํ–‰ ์—ฌ์œ  (์ƒํ•œ 65535) -} - -websocket { - port: 8080 - no_tls: true # ์‚ฌ์„ค๋ง/tailnet ํ•œ์ •. ์ƒ๋žตํ•˜๋ฉด TLS ์„ค์ • ํ•„์ˆ˜๋ผ ๊ธฐ๋™ ์‹คํŒจ - - # โ”€โ”€ ์›์ (Origin) ์ •์ฑ… โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # ๊ธฐ๋ณธ๊ฐ’์€ ์ด๋ฏธ '๋ชจ๋“  ์ถœ์ฒ˜ ํ—ˆ์šฉ'์ž…๋‹ˆ๋‹ค. same_origin ์˜ ๊ธฐ๋ณธ๊ฐ’์€ false ์ด๊ณ  - # allowed_origins ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด checkOrigin() ์ด Origin ํ—ค๋”๋ฅผ ์ฝ์ง€๋„ ์•Š๊ณ  - # ์ฆ‰์‹œ nil ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค (server/websocket.go:1039). - # โ†’ http://localhost:3000 ์˜ ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋ณ„๋„ ์„ค์ • ์—†์ด ์ ‘์†๋ฉ๋‹ˆ๋‹ค. - # โ†’ `same_origin: false` ๋ฅผ ์ ๋Š” ๊ฒƒ์€ no-op ์ž…๋‹ˆ๋‹ค. - # - # 8080 ์„ tailnet ๋ฐ–์œผ๋กœ ๋…ธ์ถœํ•œ๋‹ค๋ฉด ์•„๋ž˜๋ฅผ ์ผœ์„œ ์ถœ์ฒ˜๋ฅผ ์ขํžˆ์‹ญ์‹œ์˜ค. - # ์ฃผ์˜: allowed_origins ๋ฅผ ๋น„์šฐ์ง€ ์•Š๋Š” ์ˆœ๊ฐ„ ์›์  ๊ฒ€์‚ฌ๊ฐ€ '์ผœ์ง‘๋‹ˆ๋‹ค'. - # "*" ๋Š” ์ ˆ๋Œ€ ์“ฐ์ง€ ๋งˆ์‹ญ์‹œ์˜ค โ€” ์˜ต์…˜ ๊ฒ€์ฆ ์‹คํŒจ๋กœ ์„œ๋ฒ„๊ฐ€ ๊ธฐ๋™ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค - # (websocket.go:1142 "must be absolute URLs with http or https scheme"). - # allowed_origins: ["https://dashboard.example", "http://localhost:3000"] - - # โ”€โ”€ ์ด ํฌํŠธ๋Š” MQTT-over-WebSocket ๋„ ์„œ๋น™ํ•ฉ๋‹ˆ๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - # ๊ฒฝ๋กœ /mqtt ๋กœ ๋ถ™์œผ๋ฉด ์™„์ „ํ•œ MQTT ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค (mqtt.go:193, - # websocket.go:1335 โ†’ createMQTTClient, 1883 ๋ฆฌ์Šค๋„ˆ์™€ ๋™์ผ ํ•จ์ˆ˜). - # ws://:8080/mqtt โ†’ MQTT. retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค. - # ws://:8080/ โ†’ NATS ๋„ค์ดํ‹ฐ๋ธŒ. retained ๋ฅผ ๋ฐ›์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค (N-1). - # ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ๋Š” MQTT.js ๋กœ /mqtt ์— ๋ถ™์ด๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. -} - -# โ”€โ”€ ์ธ์ฆ ๋ฐ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -accounts { - MAM: { - jetstream: enabled # MQTT ๋‚ด๋ถ€ ์ŠคํŠธ๋ฆผ์ด ์ด ๊ณ„์ • ์•ˆ์— ์ƒ์„ฑ๋จ - users: [ - # ๋ฐœํ–‰์ž ๊ฒธ ๊ตฌ๋…์ž โ€” MAM ์—์ด์ „ํŠธ ๋ณธ์ฒด (.mam.env ์˜ MQTT_USERNAME) - { user: mam_agent, password: $MAM_BROKER_PASS } - - # ๊ด€์ธก์ž โ€” ๋Œ€์‹œ๋ณด๋“œ/๋ชจ๋‹ˆํ„ฐ๋ง. ๋ฐ˜๋“œ์‹œ MAM ๊ณ„์ • ์•ˆ์— ์œ„์น˜ - { user: mam_observer, password: $MAM_OBSERVER_PASS, - permissions: { - subscribe: { allow: ["python.mqtt.jobs.>"] } # M3 ์ดํ›„: "mam..jobs.>" - publish: { deny: [">"] } - } - } - ] - } - # MAM ๊ณผ ๋ฌด๊ด€ํ•œ ํ™ˆ๋žฉ ์„œ๋น„์Šค ์ „์šฉ. MAM subject ๋Š” ๋ณด์ด์ง€ ์•Š์Œ(์˜๋„๋œ ๊ฒฉ๋ฆฌ) - HOME: { jetstream: enabled, users: [ { user: home, password: $HOME_BROKER_PASS } ] } - SYS: { users: [ { user: sys, password: $SYS_BROKER_PASS } ] } -} -system_account: SYS -``` - -> [!IMPORTANT] -> **๊ด€์ธก์ž๋Š” ๋ฐ˜๋“œ์‹œ `MAM` ๊ณ„์ • ์•ˆ์— ๋‘ก๋‹ˆ๋‹ค.** NATS ๊ณ„์ •์€ ํ•˜๋“œ ๊ฒฉ๋ฆฌ ๊ฒฝ๊ณ„์ด๋ฏ€๋กœ `user: home`(๊ณ„์ • `HOME`)์œผ๋กœ ์ ‘์†ํ•œ ํด๋ผ์ด์–ธํŠธ๋Š” `python.mqtt.jobs.>`๋ฅผ ๊ตฌ๋…ํ•ด๋„ **0๊ฑด**์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์„œ๋กœ ๋‹ค๋ฅธ ์‹ ๋ขฐ ๋„๋ฉ”์ธ์ด๋ผ ๊ณ„์ •์„ ๋ฐ˜๋“œ์‹œ ๊ฐˆ๋ผ์•ผ ํ•˜๋Š” ์˜ˆ์™ธ ์ƒํ™ฉ์€ **๋ถ€๋ก X(Option A)** ๋ฅผ ๋”ฐ๋ฅด์‹ญ์‹œ์˜ค. - -### 9.2 ํ”„๋กœ๋•์…˜ `docker/docker-compose.yaml` - -```yaml -# ============================================================================== -# docker/docker-compose.yaml โ€” MAM ์›๊ฒฉ ํ”„๋กœ๋•์…˜ ๋ธŒ๋กœ์ปค (Track 1R) -# ์ •๋ณธ ๋ฌธ์„œ: PRIVATE_SERVER.md ยง9.2 -# -# ์‚ฌ์šฉ๋ฒ•: cd docker && cp .env.example .env && <์‹œํฌ๋ฆฟ ์ฑ„์šฐ๊ธฐ> && docker compose up -d -# ============================================================================== -services: - nats: - image: nats:2.12-alpine # alpine ํ•„์ˆ˜: healthcheck ์˜ wget ์ด ์—ฌ๊ธฐ์—๋งŒ ์žˆ์Œ - container_name: mam-nats - restart: unless-stopped - command: ["-c", "/etc/nats/nats.conf"] - environment: - # ๋ฏธ์„ค์ •/๋นˆ ๊ฐ’์ด๋ฉด ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ ์ „์— compose ๊ฐ€ ์ค‘๋‹จ โ†’ fail-closed - MAM_BROKER_PASS: ${MAM_BROKER_PASS:?set MAM_BROKER_PASS in docker/.env} - MAM_OBSERVER_PASS: ${MAM_OBSERVER_PASS:?set MAM_OBSERVER_PASS in docker/.env} - HOME_BROKER_PASS: ${HOME_BROKER_PASS:?set HOME_BROKER_PASS in docker/.env} - SYS_BROKER_PASS: ${SYS_BROKER_PASS:?set SYS_BROKER_PASS in docker/.env} - ports: - # โš  Docker ์˜ published ํฌํŠธ๋Š” UFW ๋ฅผ ์šฐํšŒํ•ฉ๋‹ˆ๋‹ค. ๋…ธ์ถœ ํ†ต์ œ๋Š” ๋ฐฉํ™”๋ฒฝ์ด ์•„๋‹ˆ๋ผ - # ์—ฌ๊ธฐ์˜ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๊ฐ€ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ ์ „๋ถ€ loopback. - - "${MQTT_BIND:-127.0.0.1}:1883:1883" # MQTT 3.1.1 (ํ‰๋ฉด A: MAM) - - "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS ๋„ค์ดํ‹ฐ๋ธŒ (ํ‰๋ฉด B) - - "127.0.0.1:8222:8222" # ๋ฌด์ธ์ฆ ๋ชจ๋‹ˆํ„ฐ๋ง โ€” loopback ๊ณ ์ • - - "${WS_BIND:-127.0.0.1}:8080:8080" # WebSocket (NATS + /mqtt ๊ฒฝ๋กœ์˜ MQTT) - volumes: - - ./nats.conf:/etc/nats/nats.conf:ro - - nats-data:/data # nats.conf ์˜ store_dir ์™€ ์ผ์น˜ - healthcheck: - test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8222/healthz"] - interval: 30s - timeout: 5s - retries: 3 - start_period: 20s - logging: - driver: json-file - options: { max-size: "10m", max-file: "3" } - -volumes: - nats-data: -``` - -> [!WARNING] -> **Docker์˜ published ํฌํŠธ๋Š” UFW๋ฅผ ์šฐํšŒํ•ฉ๋‹ˆ๋‹ค.** Docker๊ฐ€ ์‚ฝ์ž…ํ•˜๋Š” NAT/FORWARD ๊ทœ์น™์ด `ufw`์˜ INPUT ์ฒด์ธ๋ณด๋‹ค ๋จผ์ € ํ‰๊ฐ€๋˜๋ฏ€๋กœ, `ufw deny 1883`์„ ๊ฑธ์–ด๋„ `-p 1883:1883`์œผ๋กœ ๊ฒŒ์‹œํ•œ ํฌํŠธ๋Š” ์ธํ„ฐ๋„ท์— ์—ด๋ฆฝ๋‹ˆ๋‹ค. ๋ณธ ๊ตฌ์„ฑ์ด ๋ฐฉํ™”๋ฒฝ์—๋งŒ ์˜์กดํ•˜์ง€ ์•Š๊ณ  **published ํฌํŠธ ์ž์ฒด์— ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๋ฅผ ๋ช…์‹œ**(`${MQTT_BIND}:1883:1883`)ํ•˜๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค. - -### 9.3 ์›๊ฒฉ ๋„คํŠธ์›Œํ‚น & ๋ณด์•ˆ ๊ฐ€์ด๋“œ - -| ํ•ญ๋ชฉ | ๐Ÿ† **๋ชจ๋ธ T: Tailscale/WireGuard ์˜ค๋ฒ„๋ ˆ์ด (๊ถŒ์žฅ)** | ๋ชจ๋ธ P: ๊ณต๊ฐœ TLS (Let's Encrypt) | -|---|---|---| -| ์ธํ„ฐ๋„ท ๋…ธ์ถœ ๋ฉด์  | **0** (๊ณต๊ฐœ ๋ฆฌ์Šค๋„ˆ ์—†์Œ) | 8883 1๊ฐœ | -| ์ธ์ฆ์„œ ํ•„์š” | ๋ถˆํ•„์š” (`MQTT_TLS=0`) | ํ•„์ˆ˜ + 90์ผ ๊ฐฑ์‹  | -| **D-4 ํ˜ธ์ŠคํŠธ๋ช… ์ œ์•ฝ** | **ํ•ด๋‹น ์—†์Œ** | ๋„๋ฉ”์ธ ํ•„์ˆ˜, IP ๋ถˆ๊ฐ€ | -| ๋„๋ฉ”์ธ ํ•„์š” | ๋ถˆํ•„์š” | **ํ•„์ˆ˜** | -| ์ด๋™์„ฑ | ์ž๋™ (Tailscale mesh) | ์ž๋™ | - -**๋ฐฉํ™”๋ฒฝ (UFW) ์„ค์ • (๋ชจ๋ธ T):** -```bash -sudo ufw default deny incoming -sudo ufw default allow outgoing -sudo ufw allow 22/tcp - -# tailnet ์ธํ„ฐํŽ˜์ด์Šค๋งŒ ํ—ˆ์šฉ -sudo ufw allow in on tailscale0 to any port 1883 proto tcp -sudo ufw allow in on tailscale0 to any port 4222 proto tcp -sudo ufw allow in on tailscale0 to any port 8080 proto tcp - -sudo ufw enable && sudo ufw status verbose -``` - -**์‹œํฌ๋ฆฟ ์ƒ์„ฑ ๋ฐ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ฃผ์ž… (`docker/.env`):** -```bash -# docker/.env.example ๋ณต์‚ฌ ๋ฐ ๊ถŒํ•œ ์ œํ•œ -cd docker && cp .env.example .env && chmod 600 .env - -# ์‹œํฌ๋ฆฟ ์•”ํ˜ธ ์ƒ์„ฑ (openssl rand -base64 32 ์‚ฌ์šฉ) -# .env ํŒŒ์ผ์„ ์—ด๊ณ  ์ƒ์„ฑ๋œ ์‹œํฌ๋ฆฟ ๋ฐ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ ์ž…๋ ฅ: -# MAM_BROKER_PASS=<์ƒ์„ฑ๋œ_ํ† ํฐ> -# MAM_OBSERVER_PASS=<์ƒ์„ฑ๋œ_ํ† ํฐ> -# HOME_BROKER_PASS=<์ƒ์„ฑ๋œ_ํ† ํฐ> -# SYS_BROKER_PASS=<์ƒ์„ฑ๋œ_ํ† ํฐ> -# MQTT_BIND=$(tailscale ip -4) -# WS_BIND=$(tailscale ip -4) -``` - -### 9.4 ์›๊ฒฉ ๊ฒ€์ฆ ํ”Œ๋ ˆ์ด๋ถ (R-1 ~ R-10) - -| ID | ๊ฒ€์ฆ ํ•ญ๋ชฉ | ๋ฐฉ๋ฒ• | ํ†ต๊ณผ ๊ธฐ์ค€ | -|---|---|---|---| -| **R-1** | ๋ธŒ๋กœ์ปค ํ—ฌ์Šค | SSH ํ„ฐ๋„ ํ›„ `curl -sf http://127.0.0.1:8222/healthz` | **HTTP 200** | -| **R-2** | ๋ฆฌ์Šค๋„ˆ + TLS ์‹ ์› | `curl -s .../varz \| grep -i mqtt`; ๋ชจ๋ธ P๋Š” `openssl s_client -connect H:8883 -servername H` | MQTT ๋ฆฌ์Šค๋„ˆ ๋…ธ์ถœ, SAN์— `MQTT_BROKER` ํฌํ•จ | -| **R-3** | ๋…ธ์ถœ ๋ฉด์  ๋‹จ์–ธ | ์™ธ๋ถ€ ๋ง์—์„œ `nmap -Pn -p 1883,4222,8222,8080 <๊ณต๊ฐœIP>` | ์ „๋ถ€ closed/filtered | -| **R-4** | ์™•๋ณต pub/sub + ๊ณ„์ • JetStream | ์ž„์‹œ ์žก ๋“ฑ๋ก โ†’ ๊ตฌ๋…์ž ๊ธฐ๋™ โ†’ `progress` ๋ฐœํ–‰ โ†’ ์ˆ˜์‹  โ†’ ์ข…๊ฒฐ | ์ˆ˜์‹  ์„ฑ๊ณต, `JetStream not enabled for account` **๋ฏธ๋ฐœ์ƒ** | -| **R-5** | **retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ (MQTT)** | `--event completed` ๋ฐœํ–‰ **ํ›„** ์‹ ๊ทœ `job_subscriber.py` ๊ธฐ๋™ | ์ฆ‰์‹œ ์ตœ์ข… ์ด๋ฒคํŠธ ์ˆ˜์‹  | -| **R-6** | ๋ธŒ๋กœ์ปค ์‹ ์› ๋‹จ์–ธ | `grep -c 'broker.hivemq.com' .mam/delegate_job_logs/$JID/events.ndjson` | **0**, ์›๊ฒฉ ํ˜ธ์ŠคํŠธ ๋“ฑ์žฅ | -| **R-7** | freeze ํšŒ๊ท€ (H-1/H-4) | freeze ๊ฒฝ๋กœ `publish_event.py`๋ฅผ `MAM_ENV_FILE` ์—†์ด ์‹คํ–‰ / ์˜คํƒ€ ๊ฒฝ๋กœ ์‹คํ–‰ | ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค๋กœ ๋‚˜๊ฐ€์ง€ ์•Š์Œ | -| **R-8** | ์ „์ฒด ํšŒ๊ท€ ์Šค์œ„ํŠธ | `.venv/bin/python -m pytest tests/ -q` | **์ „๊ฑด ํ†ต๊ณผ** | -| **R-9** | **๊ด€์ธก์ž ๊ณ„์ • ๊ฒฝ๊ณ„** | `mam_observer`๋กœ NATS ๊ตฌ๋… โ†’ ์ˆ˜์‹  ํ™•์ธ. ์ด์–ด์„œ `home`์œผ๋กœ ๋™์ผ ๊ตฌ๋… | `mam_observer` **์ˆ˜์‹ **, `home` **0๊ฑด** | -| **R-10** | **retained ๊ฒฝ๊ณ„ ํ™•์ธ (N-1)** | ์žก ์ข…๋ฃŒ **ํ›„** NATS ๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌ๋…์ž๋ฅผ ์ƒˆ๋กœ ๋ถ™์ž„ | **0๊ฑด ์ˆ˜์‹ ** (retained๋Š” MQTT ์ „์šฉ) | - -**WAN ์ง€์—ฐ ์‹œ๊ฐ„ ์ธก์ • (Latency Probe):** -```bash -.venv/bin/python - <<'PY' -import sys, time, statistics -sys.path.insert(0, '.agents/skills/multi-agent-mux-delegate-job/scripts') -import mqtt_common as m -cfg = m.broker_config_from_env() -print(f"target: {cfg.host}:{cfg.port} tls={cfg.tls}") -conn, rtt = [], [] -for _ in range(5): - c = m.make_client("latency", cfg) - t0 = time.perf_counter(); c.connect(cfg.host, cfg.port, 10); c.loop_start() - conn.append((time.perf_counter() - t0) * 1000) - t1 = time.perf_counter() - info = c.publish("mam/latency/probe", b"x", qos=1); info.wait_for_publish(10) - rtt.append((time.perf_counter() - t1) * 1000) - c.loop_stop(); c.disconnect() -print(f"connect p50={statistics.median(conn):.1f}ms max={max(conn):.1f}ms") -print(f"qos1 rtt p50={statistics.median(rtt):.1f}ms max={max(rtt):.1f}ms") -PY -``` - -### 9.5 ์ „ํ™˜(Cutover) ์ ˆ์ฐจ (H-2 ๋Œ€์‘) - -``` -[1 ๋“œ๋ ˆ์ธ] โ”€โ”€> [2 ์ž”์—ฌ ์Šค์บ”] โ”€โ”€> [3 .mam.env ๊ต์ฒด] โ”€โ”€> [4 R-1~R-10] โ”€โ”€> [5 ๋ ˆ๊ฑฐ์‹œ ์ฐจ๋‹จ] -``` -1. **๋“œ๋ ˆ์ธ**: ์‹ ๊ทœ ์œ„์ž„ ์ค‘๋‹จ, ์ง„ํ–‰ ์ค‘ ์žก์ด ๋ชจ๋‘ terminal ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ. -2. **์ž”์—ฌ ์Šค์บ”**: ์˜› ๋ธŒ๋กœ์ปค์— ํ•€ ๊ณ ์ •๋œ ๋ ˆ์ฝ”๋“œ ์Šค์บ” ํ›„ ์ •๋ฆฌ. -3. **`.mam.env` ๊ต์ฒด**: ์›๊ฒฉ ๋ธŒ๋กœ์ปค ์ฃผ์†Œ ๋ฐ ํ† ํฐ ์ ์šฉ. -4. **๊ฒ€์ฆ**: R-1 ~ R-10 ์ „๊ฑด ํ†ต๊ณผ ํ™•์ธ. -5. **๋ ˆ๊ฑฐ์‹œ ์ฐจ๋‹จ**: ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ์„ค์ • ์™„์ „ ์ œ๊ฑฐ. - ---- - -### ๋ถ€๋ก X. Option A (๊ณ„์ • ๊ฐ„ Export / Import) ์˜ˆ์™ธ ๊ฒฝ๋กœ - -๊ด€์ธก์ž๊ฐ€ **์„œ๋กœ ๋‹ค๋ฅธ ์‹ ๋ขฐ ๋„๋ฉ”์ธ**์— ์†ํ•ด ๊ณ„์ •์„ ์—„๊ฒฉํžˆ ๋ถ„๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ: - -```conf -accounts { - MAM: { - jetstream: enabled - users: [ { user: mam_agent, password: $MAM_BROKER_PASS } ] - exports: [ { stream: "python.mqtt.jobs.>", accounts: [HOME] } ] - } - HOME: { - users: [ { user: home, password: $HOME_BROKER_PASS } ] - imports: [ { stream: { account: MAM, subject: "python.mqtt.jobs.>" } } ] - } - SYS: { users: [ { user: sys, password: $SYS_BROKER_PASS } ] } -} -system_account: SYS -``` - diff --git a/nats-docker b/nats-docker index c86cc98..a4b6e49 160000 --- a/nats-docker +++ b/nats-docker @@ -1 +1 @@ -Subproject commit c86cc982beb12b92bed136376bebe9e9fa4b9842 +Subproject commit a4b6e49a1f01dac4974fcd3c7e4e9382be665e33 diff --git a/tests/test_deploy_freshness.py b/tests/test_deploy_freshness.py index a776eec..c5aceee 100644 --- a/tests/test_deploy_freshness.py +++ b/tests/test_deploy_freshness.py @@ -266,6 +266,20 @@ def test_d10_customization_survives_repeated_refresh(src_and_target): "user gets no signal that their edit is diverging" % n) +def _resolve_private_server_doc() -> str: + for candidate in [ + os.path.join(REPO_ROOT, "nats-docker", "PRIVATE_SERVER.md"), + os.path.join(REPO_ROOT, "nats-docker", "docs", "PRIVATE_SERVER.md"), + os.path.join(REPO_ROOT, "PRIVATE_SERVER.md"), + ]: + if os.path.exists(candidate): + return candidate + return os.path.join(REPO_ROOT, "nats-docker", "PRIVATE_SERVER.md") + + +PRIVATE_SERVER_DOC_PATH = _resolve_private_server_doc() + + # -------------------------------------------------------------------------- # D-11 โ€” (G-D1) PRIVATE_SERVER.md must only document MQTT_* environment # variables that broker_config_from_env() actually parses. @@ -274,8 +288,8 @@ def test_d11_private_server_env_names_valid(): sys.path.insert(0, os.path.join(REPO_ROOT, ".agents", "skills", "multi-agent-mux-delegate-job", "scripts")) import mqtt_common - doc_path = os.path.join(REPO_ROOT, "PRIVATE_SERVER.md") - assert os.path.exists(doc_path), "PRIVATE_SERVER.md missing" + doc_path = PRIVATE_SERVER_DOC_PATH + assert os.path.exists(doc_path), f"PRIVATE_SERVER.md missing at {doc_path}" with open(doc_path, "r", encoding="utf-8") as f: content = f.read() @@ -301,7 +315,7 @@ def test_d11_private_server_env_names_valid(): # active configuration code blocks. # -------------------------------------------------------------------------- def test_d12_private_server_no_mam_mqtt_in_code_fences(): - doc_path = os.path.join(REPO_ROOT, "PRIVATE_SERVER.md") + doc_path = PRIVATE_SERVER_DOC_PATH with open(doc_path, "r", encoding="utf-8") as f: content = f.read() @@ -317,7 +331,7 @@ def test_d12_private_server_no_mam_mqtt_in_code_fences(): # use valid config blocks (mqtt {) and not HTTP port flag (-m 1883). # -------------------------------------------------------------------------- def test_d13_private_server_nats_config_valid(): - doc_path = os.path.join(REPO_ROOT, "PRIVATE_SERVER.md") + doc_path = PRIVATE_SERVER_DOC_PATH with open(doc_path, "r", encoding="utf-8") as f: content = f.read() @@ -333,7 +347,7 @@ def test_d13_private_server_nats_config_valid(): # CLI flags matching the actual scripts' argparse parsers. # -------------------------------------------------------------------------- def test_d14_private_server_cli_args_valid(): - doc_path = os.path.join(REPO_ROOT, "PRIVATE_SERVER.md") + doc_path = PRIVATE_SERVER_DOC_PATH with open(doc_path, "r", encoding="utf-8") as f: content = f.read() @@ -353,7 +367,7 @@ def test_d14_private_server_cli_args_valid(): # and heredocs writing it must be unquoted (<