From 3523b9b1ea461446923832d84d28fdfaf87dff43 Mon Sep 17 00:00:00 2001 From: Godopu Date: Sat, 22 Aug 2026 23:47:29 +0900 Subject: [PATCH] docs(broker): establish remote Docker deployment plan for nats-server with networking/security guides and add D-15~D-21 freshness guards --- .../plan-27236ab6.md | 641 ++++++++++++++++++ .../report-e1c4e9c3.md | 161 +++++ .agents/skills/lib.sh | 13 +- .mam.env.example | 4 + PRIVATE_SERVER.md | 274 +++++++- implementation_plan.md | 64 +- tests/test_deploy_freshness.py | 123 +++- 7 files changed, 1215 insertions(+), 65 deletions(-) create mode 100644 .agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-27236ab6.md create mode 100644 .agents/reports/canary-projects-multi-agent-mux-creator-cline/report-e1c4e9c3.md diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-27236ab6.md b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-27236ab6.md new file mode 100644 index 0000000..cecd797 --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-27236ab6.md @@ -0,0 +1,641 @@ +# ๐ŸŒ ์›๊ฒฉ ์„œ๋ฒ„ `nats-server` Docker ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๊ณ„ํš์„œ **Rev.2** (Job `b11d499d`) + +- **์—ญํ• **: Planner (`MULTI_AGENT_RULES.md` ยง1 โ€” ์ €์žฅ์†Œ ์ฝ”๋“œ/๋ฌธ์„œ ๋ฏธ๋ณ€๊ฒฝ, ๊ณ„ํš์„œ๋งŒ ์‚ฐ์ถœ) +- **์„ ํ–‰ ๋ฆฌ๋น„์ „**: Rev.1 = Job `27236ab6` +- **๋ฐ˜์˜ ์ฑŒ๋ฆฐ์ง€**: Job `9a5cb88f` (`agy`) โ€” `[VERDICT: PASS WITH CHALLENGE]` +- **๊ธฐ์ค€ ์ปค๋ฐ‹**: `c6b6c77` (Track 0 ์™„๋ฃŒ, **290 tests collected** ์‹ค์ธก) +- **๊ฒ€์ฆ ์›์น™**: ์ถ”๋ก ์ด ์•„๋‹Œ **์‹ค์ธก**. ์ฑŒ๋ฆฐ์ง€๋Š” ์ง€์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ **๊ฐ€์„ค**๋กœ ์ทจ๊ธ‰ํ•˜์—ฌ ์žฌํ˜„ยท๋ฐ˜์ฆํ–ˆ์Šต๋‹ˆ๋‹ค. + +--- + +## 0. Rev.2 ํŒ์ • ์š”์•ฝ (Adjudication) + +| ์ฑŒ๋ฆฐ์ง€ | ํŒ์ • | ์š”์ง€ | +|---|---|---| +| **C1** ๊ณ„์ • ๊ฒฉ๋ฆฌ๊ฐ€ ๊ต์ฐจ ๊ด€์ธก์„ ์ฐจ๋‹จ | ๐ŸŸก **๋ถ€๋ถ„ ์ธ์šฉ โ€” ์ง„๋‹จ ์œ ํšจ, ๊ท€์† ๋ถ€์ •ํ™•, ๋‘ ์˜ต์…˜ ๋ชจ๋‘ ๊ฒฐ์ •์  ํ•œ๊ณ„ ๋ˆ„๋ฝ** | ๊ณ„์ • ๊ฒฉ๋ฆฌ ์‚ฌ์‹ค์€ ๋งž์Œ. ๋‹ค๋งŒ "๊ณ„ํš์ด `HOME` ๊ณ„์ •์—์„œ MAM ์ด๋ฒคํŠธ ๊ด€์ธก์„ ์ฃผ์žฅํ•œ๋‹ค"๋Š” ๊ท€์†์€ ๋ถ€์ •ํ™• โ€” `PRIVATE_SERVER.md:230`์ด **์ •๋ฐ˜๋Œ€๋ฅผ ์ด๋ฏธ ์ฒ˜๋ฐฉ**. ๋ฐ˜๋ฉด ๋‚ด Rev.1 config์— ๊ด€์ธก์ž ์‚ฌ์šฉ์ž๊ฐ€ **์•„์˜ˆ ์—†์—ˆ๋˜ ๊ฒƒ**์€ ์‹ค์ œ ๊ฒฐํ•จ์ด๋ฏ€๋กœ ์ˆ˜์šฉ. **์‹ ๊ทœ ์‹ค์ธก**: retained ์ด๋ฒคํŠธ๋Š” **MQTT ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ** ์ „๋‹ฌ๋˜๋ฏ€๋กœ Option A/B ์–ด๋А ์ชฝ๋„ "์‚ฌํ›„ ์ ‘์† ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ณธ๋‹ค"๋ฅผ ๋งŒ๋“ค์ง€ ๋ชปํ•จ | +| **C2** `_load_dotenv` ์šฐ์„ ์ˆœ์œ„ | ๐ŸŸข **๋ฐฉํ–ฅ ์ˆ˜์šฉ + ๊ทผ๋ณธ ๊ฒฐํ•จ ์žฌ์ •์˜ + ์‹ ๊ทœ ๊ฒฐํ•จ 1๊ฑด ๋ฐœ๊ฒฌ** | ์ง„์งœ ๊ฒฐํ•จ์€ ํ›„๋ณด ๋ชฉ๋ก์ด ์•„๋‹ˆ๋ผ **๋‹จ์ผ ํ›„๋ณด ํ•ด์„**. ๊ทธ๋ฆฌ๊ณ  `MAM_ENV_FILE`์ด ์—†๋Š” ํŒŒ์ผ์„ ๊ฐ€๋ฆฌํ‚ค๋ฉด **๋‹ค๋ฅธ ํ›„๋ณด๋ฅผ ํ•˜๋‚˜๋„ ์‹œ๋„ํ•˜์ง€ ์•Š๊ณ  ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค๋กœ ํด๋ฐฑ**(์‹ค์ธก) โ€” ์ œ์•ˆ๋œ ์ˆœ์„œ๋กœ๋Š” ๊ณ ์ณ์ง€์ง€ ์•Š์Œ | +| **C3** `G-D5` ์Šค์ฝ”ํ•‘ | ๐ŸŸก **์ด๋ฏธ Rev.1์— ์กด์žฌ. ๋‹จ ์ž”์—ฌ ์ง€์ ์ด D-1์„ ๊ฐ•ํ™”** | ํŽœ์Šค ํ•œ์ •ยท์˜คํƒ ๋ถ€์žฌ ๋‹จ์–ธ์€ Rev.1 ยง6์— ์ด๋ฏธ ๋ช…์‹œ. **์‹ ๊ทœ ์‹ค์ธก**: NATS ๋ ‰์„œ๋Š” **์ธ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’์—์„œ๋งŒ** `$VAR`๋ฅผ ํ•ด์„ โ†’ `store_dir: "$HOME/..."`๋Š” ๋ฆฌํ„ฐ๋Ÿด์ด๋ฉฐ ์ธ์šฉ heredoc๊ณผ ๊ฒฐํ•ฉ ์‹œ **D-1๊ณผ ๋™์ผํ•˜๊ฒŒ ํŒŒ์†**. G-D5๋ฅผ 2ํ•ญ ๊ฒ€์‚ฌ๋กœ ๊ฐ•ํ™” | + +**Rev.2 ์‹ค์งˆ ๋ณ€๊ฒฝ 6๊ฑด** +1. ยงA-1์— **`mam_observer` ์ฝ๊ธฐ ์ „์šฉ ์‚ฌ์šฉ์ž**๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ถ”๊ฐ€(Option B ์ฑ„ํƒ โ€” ์ €์žฅ์†Œ ยง5.5 ์ฒ˜๋ฐฉ๊ณผ ์ผ์น˜). +2. **retained๋Š” MQTT ์ „์šฉ**์ด๋ผ๋Š” ์‹ ๊ทœ ์‹ค์ธก์„ ยง1.9๋กœ ์‹ ์„คํ•˜๊ณ  ยง5.2 ๋ธŒ๋ฆฌ์ง• ์ฃผ์žฅ์˜ ๊ฒฝ๊ณ„๋กœ ๋ช…๋ฌธํ™”. +3. Option A(export/import)๋ฅผ **์˜ˆ์™ธ ๊ฒฝ๋กœ**๋กœ ๋ฌธ๋ฒ• ๊ฒ€์ฆ๊นŒ์ง€ ๋งˆ์ณ ๋ถ€๋ก์— ๋ฐฐ์น˜(๋ฌด์กฐ๊ฑด ์ฑ„ํƒํ•˜์ง€ ์•Š๋Š” ๊ทผ๊ฑฐ 3๊ฑด ์ฒจ๋ถ€). +4. `B-17` ์ฒ˜๋ฐฉ์„ **first-hit-wins ํ›„๋ณด ๋ชฉ๋ก**์œผ๋กœ ์žฌ์ •์˜ํ•˜๊ณ  `MAM_ENV_FILE` ์กฐ๊ธฐ ํƒˆ์ถœ ๊ฒฐํ•จ์„ ์ถ”๊ฐ€. +5. **G-D5๋ฅผ 2ํ•ญ ๊ฒ€์‚ฌ๋กœ ๊ฐ•ํ™”**(๊ฐ’ + heredoc ๊ตฌ๋ถ„์ž), `$VAR` ์ธ์šฉ ๊ทœ์น™์„ ยง1.1 ๊ฐ์ฃผ์— ์ •๋ฐ€ํ™”. +6. **G-D9 ์‹ ์„ค** โ€” ๋ฌธ์„œ config ์˜ˆ์ œ์˜ subject ๋ฆฌํ„ฐ๋Ÿด๊ณผ `DEFAULT_TOPIC_ROOT` ์ผ์น˜ ๊ฐ•์ œ(M3 ํ† ํ”ฝ ์ „ํ™˜ ์‹œ ์กฐ์šฉํ•œ ํŒŒ์† ์ฐจ๋‹จ). ํ…Œ์ŠคํŠธ ์ „๋ง 290 โ†’ **297** โ†’ 298. + +--- + +## 1. ์‚ฌ์ „ ์‹ค์ธก ๊ฒฐ๊ณผ (Pre-Flight Measurements) + +> Rev.1์˜ D-1 ~ D-5, H-1 ~ H-3์€ ์ฑŒ๋ฆฐ์ €๊ฐ€ "Verified 100% accurate"๋กœ ์Šน์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ์š”์ง€ ์œ ์ง€ + **Rev.2 ์‹ ๊ทœ ์‹ค์ธก 2๊ฑด(ยง1.9, ยง1.10)** ๋ฐ ยง1.1 ๊ฐ์ฃผ ์ •๋ฐ€ํ™”์ž…๋‹ˆ๋‹ค. + +### 1.1 D-1 โ€” `store_dir`์˜ `~`๋Š” ํ™•์žฅ๋˜์ง€ ์•Š๋Š”๋‹ค (P1) + +`PRIVATE_SERVER.md:73`, `:146`์ด `store_dir: "~/.local/share/nats/data"`๋ฅผ ์ง€์‹œํ•ฉ๋‹ˆ๋‹ค. + +**์ฆ๊ฑฐ 1 โ€” NATS ์„ค์ • ํŒŒ์„œ์— ํ‹ธ๋“œ ํ™•์žฅ ์—†์Œ** (`server/opts.go`): +```go +case "store", "store_dir", "storedir": + opts.StoreDir = mv.(string) // ๋ฌธ์ž์—ด ๊ทธ๋Œ€๋กœ ๋Œ€์ž…. os.UserHomeDir ํ˜ธ์ถœ ์—†์Œ +``` +**์ฆ๊ฑฐ 2 โ€” ์ธ์šฉ heredoc์ด ์…ธ ํ™•์žฅ๊นŒ์ง€ ์ฐจ๋‹จ** (์‹ค์ธก): `<<'EOF'` โ†’ `store_dir: "~/.local/share/nats/data"` ๋ฆฌํ„ฐ๋Ÿด ์œ ์ง€ / `< [!IMPORTANT] +> **๊ฐ์ฃผ ์ •๋ฐ€ํ™” (Rev.2, C3 ํŒŒ์ƒ)**: NATS ์„ค์ •์˜ `$VAR` ์ฐธ์กฐ๋Š” **์ธ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’์—์„œ๋งŒ** ํ•ด์„๋ฉ๋‹ˆ๋‹ค. ๋ ‰์„œ ์›๋ฌธ โ€” *"Check if the **unquoted** string is a variable reference, starting with `$`."* ์ด๋ฉฐ `lexQuotedString`์€ *"It will not interpret any internal contents."* ์ž…๋‹ˆ๋‹ค. +> ๋”ฐ๋ผ์„œ **`store_dir: "$HOME/..."`๋Š” ๋ฆฌํ„ฐ๋Ÿด ๋ฌธ์ž์—ด**์ด๋ฉฐ, ์ธ์šฉ heredoc๊ณผ ๊ฒฐํ•ฉํ•˜๋ฉด `./$HOME/.local/share/nats/data`๊ฐ€ ๋งŒ๋“ค์–ด์ ธ D-1๊ณผ **๋™์ผํ•˜๊ฒŒ ํŒŒ์†**๋ฉ๋‹ˆ๋‹ค. +> ๊ทœ์น™: `$HOME`์€ **์…ธ์ด ์ „๊ฐœํ•  ๋•Œ๋งŒ**(= ๋น„์ธ์šฉ heredoc ์•ˆ์—์„œ๋งŒ) ํ—ˆ์šฉ. NATS๊ฐ€ ํ•ด์„ํ•ด์•ผ ํ•˜๋Š” ๋ณ€์ˆ˜(`password: $MAM_BROKER_PASS`)๋Š” **๋”ฐ์˜ดํ‘œ๋ฅผ ์”Œ์šฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** + +### 1.2 D-2 โ€” `nats:latest`๋Š” scratch ๋ณ€ํ˜•์ด๋ผ healthcheck๋ฅผ ๋„ฃ์„ ์ˆ˜ ์—†๋‹ค (P1) + +`docker-library/official-images`์˜ `library/nats`: `SharedTags: 2.14.5, 2.14, 2, latest` @ `Directory: 2.14.x/scratch`. ํ•ด๋‹น Dockerfile์€ `FROM scratch` + `ENTRYPOINT ["/nats-server"]`. โ†’ ์…ธยทwgetยทcurl ๋ถ€์žฌ๋กœ **healthcheck ๊ตฌํ˜„ ๋ถˆ๊ฐ€**, ๊ฒŒ๋‹ค๊ฐ€ ๋ฉ”์ด์ € ๊ฒฝ๊ณ„๋ฅผ ๋„˜๋‚˜๋“œ๋Š” ๋ถ€๋™ ํƒœ๊ทธ. + +**๊ต์ •**: `image: nats:2.12-alpine`. alpine ์—”ํŠธ๋ฆฌํฌ์ธํŠธ๊ฐ€ ์ฒซ ์ธ์ž `-` ๊ฐ์ง€ ์‹œ `nats-server`๋ฅผ ์ž๋™ prependํ•˜๋ฏ€๋กœ `command: ["-c", ...]` ๋ผ์ธ์€ **์–‘์ชฝ ๋ณ€ํ˜•์—์„œ ๋™์ผ ๋™์ž‘**(์‹ค์ธก): +```sh +if [ "$#" -eq 0 ] || [ "${1#-}" != "$1" ]; then set -- nats-server "$@"; fi +``` + +### 1.3 D-3 โ€” ๋ฌด์ธ์ฆ ๋ชจ๋‹ˆํ„ฐ๋ง ํฌํŠธ๋ฅผ ์ „ ์ธํ„ฐํŽ˜์ด์Šค์— ๊ฒŒ์‹œ (P1) + +ํ˜„ํ–‰ `PRIVATE_SERVER.md:120-124`๊ฐ€ `"8222:8222"`, `"8080:8080"`์„ 0.0.0.0์— ๊ฒŒ์‹œํ•ฉ๋‹ˆ๋‹ค. NATS ๊ณต์‹ ๋ฌธ์„œ: *"The monitoring port is unauthenticated by default."* โ†’ `/varz`ยท`/connz`ยท`/jsz`ยท`/routez` ๊ณต๊ฐœ. 8080์€ `no_tls: true` ํ‰๋ฌธ. + +### 1.4 D-4 โ€” TLS ์‚ฌ์šฉ ์‹œ ํ˜ธ์ŠคํŠธ๋ช… ๊ฒ€์ฆ์ด ๊ฐ•์ œ๋œ๋‹ค (P1) + +`make_client()`๋Š” `tls_set(...)`๋งŒ ํ˜ธ์ถœํ•˜๊ณ  `tls_insecure_set()`์„ ๋ถ€๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹ค์ธก(paho 2.1.0): `check_hostname=True`, `verify_mode=CERT_REQUIRED`, `_tls_insecure=False`, ์šฐํšŒ env **์—†์Œ**. + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ฒฐ๊ณผ | +|---|---| +| **A)** IP ํ˜ธ์ŠคํŠธ + ์ •ํ™•ํ•œ CA ๋ฒˆ๋“ค ํ•€ | `IP address mismatch, certificate is not valid for '127.0.0.1'` | +| **B)** ์‚ฌ์„ค CA + `MQTT_CA_CERTS` ๋ฏธ์„ค์ • | `self signed certificate` | +| **C)** `MQTT_TLS=0`์œผ๋กœ TLS ํฌํŠธ ์ ‘์† | **`CONNECTED (handshake ok)`** โ† ์†Œ์ผ“๋งŒ ์—ด๋ฆผ | + +โ†’ (1) TLS ์‹œ `MQTT_BROKER`๋Š” **์ธ์ฆ์„œ SAN์˜ DNS ์ด๋ฆ„** ํ•„์ˆ˜(IP ๊ธˆ์ง€). (2) ์‚ฌ์„ค CA๋ฉด `MQTT_CA_CERTS` ํ•„์ˆ˜, Let's Encrypt๋ฉด **๋น„์›Œ ๋‘˜ ๊ฒƒ**. (3) **์†Œ์ผ“ ์—ฐ๊ฒฐ ์„ฑ๊ณต์€ ๋ธŒ๋กœ์ปค ์ •์ƒ์˜ ์ฆ๊ฑฐ๊ฐ€ ์•„๋‹˜** โ€” ๊ฒ€์ฆ์€ CONNACK ๋˜๋Š” `/healthz`๊นŒ์ง€ ๋„๋‹ฌํ•ด์•ผ ํ•จ. + +### 1.5 D-5 โ€” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ…œํ”Œ๋ฆฟ ์–‘๋ฐฉํ–ฅ ๋“œ๋ฆฌํ”„ํŠธ (P2) + +`deploy/install.sh:521-522`๊ฐ€ `MQTT_RETRY_INTERVAL=2`, `MQTT_MAX_RETRIES=5`๋ฅผ `.mam.env`์— **ํ™œ์„ฑ ๊ธฐ๋ณธ๊ฐ’**์œผ๋กœ ๊ธฐ๋กํ•˜์ง€๋งŒ **์ฝ๋Š” ์ฝ”๋“œ 0๊ฑด**. ์‹ค์ œ ์žฌ์‹œ๋„๋Š” `--attempts`(๊ธฐ๋ณธ 3) + `with_retry(base_delay=0.5, factor=2.0, max_delay=8.0)`. ์—ญ์œผ๋กœ ์ฝ”๋“œ๊ฐ€ ์ฝ๋Š” `MQTT_KEEPALIVE`(๊ธฐ๋ณธ 60)๋Š” `.mam.env.example`์— **0๊ฑด**. + +### 1.6 H-1 โ€” freeze ๊ฒฝ๋กœ์˜ ์กฐ์šฉํ•œ ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ํšŒ๊ท€ (P1) + +`_load_dotenv()`๋Š” `__file__`์—์„œ ์œ„๋กœ ์˜ฌ๋ผ๊ฐ€๋‹ค `.agents` ๋˜๋Š” `.git`์—์„œ ๋ฉˆ์ถฅ๋‹ˆ๋‹ค. freeze ์Šค๋ƒ…์ƒท ๋ฃจํŠธ๋Š” `.agents`๋งŒ ๋‹ด๊ณ  `.mam.env`๋Š” ์—†์Šต๋‹ˆ๋‹ค(์‹ค์ธก: `ls` ๊ฒฐ๊ณผ `.agents` ๋‹จ ํ•˜๋‚˜). + +| # | ์กฐ๊ฑด | ํ•ด์„๋œ ๋ธŒ๋กœ์ปค | +|---|---|---| +| 1 | ์ €์žฅ์†Œ ๊ฒฝ๋กœ ์Šคํฌ๋ฆฝํŠธ, `MAM_ENV_FILE` ์—†์Œ | `nats.example.internal:8883 tls=True` โœ… | +| 2 | **freeze ๊ฒฝ๋กœ, `MAM_ENV_FILE` ์—†์Œ** | **`broker.hivemq.com:1883 tls=False`** โŒ | +| 3 | freeze ๊ฒฝ๋กœ + `MAM_ENV_FILE` | `nats.example.internal:8883 tls=True` โœ… | + +์ •์ƒ ๋ฃจํ”„๋Š” `run_loop.sh:106`์˜ `export MAM_ENV_FILE=...`์œผ๋กœ ๋ณดํ˜ธ๋˜๋‚˜, **์œ„์ž„ ๋ธŒ๋ฆฌํ”„๊ฐ€ ๋ฐฐํฌํ•˜๋Š” ๋ช…๋ น์ค„์€ freeze ๊ฒฝ๋กœ๋ฅผ ์ง์ ‘ ๊ฐ€๋ฆฌํ‚ต๋‹ˆ๋‹ค.** + +### 1.7 H-2 โ€” ์žก ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋ธŒ๋กœ์ปค๋ฅผ ํ•€ ๊ณ ์ • (P1) + +`registry.py:73-74`๊ฐ€ ๋“ฑ๋ก ์‹œ์  ๋ธŒ๋กœ์ปค ๋ธ”๋ก์„ ์Šค๋ƒ…์ƒทํ•˜๊ณ  `broker_config_from_job()`์ด env๋ณด๋‹ค **์šฐ์„ ** ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. โ†’ `.mam.env` ๊ต์ฒด๋งŒ์œผ๋กœ๋Š” ๊ธฐ์กด pending/running ์žก์ด ์ „ํ™˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +### 1.8 H-3 โ€” ๋น„๋ฐ€๋ฒˆํ˜ธ ํ‰๋ฌธ ๋ณด๊ด€ / ์ž๋™ ํ† ํฐ ๋ฐœ๊ธ‰ ์ด๋“ (P2) + +๋ ˆ์ฝ”๋“œ์— `"password": "SUPERSECRET123"` ํ‰๋ฌธ ํ™•์ธ(๋ชจ๋“œ 0600, `.gitignore:14`์— `.mam/`). ๋™์‹œ์— **`tls` ๋˜๋Š” `username` ๊ฐ์ง€ ์‹œ `auth_token` ์ž๋™ ๋ฐœ๊ธ‰** ํ™•์ธ โ†’ ์›๊ฒฉ ์ธ์ฆ ์ „ํ™˜์ด ๊ณง HMAC ์ž๋™ ํ™œ์„ฑํ™”์ด๋ฉฐ `B-16`/`G-11` ์œ„ํ—˜์„ ๋Œ€๋ถ€๋ถ„ ๋ถ€์ˆ˜ ํ•ด์†Œ. + +### 1.9 ๐Ÿ†• **N-1 โ€” retained ๋ฉ”์‹œ์ง€๋Š” MQTT ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ ์ „๋‹ฌ๋œ๋‹ค** (P1, C1 ํŒŒ์ƒ ์‹ ๊ทœ ์‹ค์ธก) + +์ฑŒ๋ฆฐ์ €์˜ C1์€ ๊ณ„์ • ๊ฒฝ๊ณ„๋งŒ ๋‹ค๋ค˜์œผ๋‚˜, **๊ณ„์ • ๋ฌธ์ œ๋ฅผ ์–ด๋–ป๊ฒŒ ํ’€๋“  ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ๋” ๊ทผ๋ณธ์ ์ธ ๊ฒฝ๊ณ„**๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. + +`server/mqtt.go` ์‹ค์ธก โ€” retained ์ „๋‹ฌ์€ **MQTT SUBSCRIBE ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ์—์„œ๋งŒ** ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค: +```go +case mqttPacketSub: // โ† MQTT SUBSCRIBE ํŒจํ‚ท ์ฒ˜๋ฆฌ + ... + c.mqttEnqueueSubAck(pi, filters) + c.mqttSendRetainedMsgsToNewSubs(subs) // โ† ์—ฌ๊ธฐ์„œ๋งŒ ํ˜ธ์ถœ + +func (c *client) mqttSendRetainedMsgsToNewSubs(subs []*subscription) { + for _, sub := range subs { + if sub.mqtt != nil && sub.mqtt.prm != nil { ... } // โ† MQTT ๊ตฌ๋…์—๋งŒ ์กด์žฌํ•˜๋Š” ํ•„๋“œ + } +} +``` + +**๊ฒฐ๋ก **: NATS ๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌ๋…์ž์™€ WebSocket(NATS) ๊ตฌ๋…์ž๋Š” **retained ๋ฉ”์‹œ์ง€๋ฅผ ์ ˆ๋Œ€ ๋ฐ›์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.** ๊ณ„์ •์„ ํ•ฉ์น˜๋“ (Option B), export/import๋ฅผ ๊ฑธ๋“ (Option A) ์ด ์‚ฌ์‹ค์€ ๋ณ€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +**MAM์— ์ฃผ๋Š” ๊ตฌ์ฒด์  ์˜๋ฏธ**: +- `publish_event.py`๋Š” `retain = args.retained or args.event in TERMINAL_EVENTS` โ€” ์ฆ‰ **์ข…๋ฃŒ ์ด๋ฒคํŠธ๊ฐ€ ์ •ํ™•ํžˆ retained ๋Œ€์ƒ**์ž…๋‹ˆ๋‹ค. +- ์žก์ด ๋๋‚œ **๋’ค์—** ์ ‘์†ํ•œ NATS/WebSocket ๋Œ€์‹œ๋ณด๋“œ๋Š” **๊ทธ ์žก์˜ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ณด์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.** ๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. +- `PRIVATE_SERVER.md` ยง5.2์˜ *"์ฆ‰์‹œ ์‹ค์‹œ๊ฐ„ ์ˆ˜์‹ "* ์ฃผ์žฅ์€ **๋ผ์ด๋ธŒ ๊ตฌ๊ฐ„์— ํ•œ์ •**ํ•ด์•ผ ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. + +**๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ์‚ฌํ›„ ์ƒํƒœ๊นŒ์ง€ ์•Œ์•„์•ผ ํ•œ๋‹ค๋ฉด ์„ ํƒ์ง€๋Š” 2๊ฐœ๋ฟ**: +1. ๋Œ€์‹œ๋ณด๋“œ๋ฅผ **MQTT๋กœ** ๋ถ™์ธ๋‹ค(๊ฐ™์€ `nats-server`์˜ 1883 ๋ฆฌ์Šค๋„ˆ ์‚ฌ์šฉ, retained ๊ทธ๋Œ€๋กœ ์ˆ˜์‹ ). +2. ยง5.3์˜ **JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ์„ ์˜ตํŠธ์ธ**ํ•œ๋‹ค(`python.mqtt.jobs.>` ๊ตฌ๋… ์ŠคํŠธ๋ฆผ + `max_age`/`max_bytes` ์ƒํ•œ ํ•„์ˆ˜). + +์ด ๋‘ ๊ฐˆ๋ž˜๋ฅผ ยง5.2 ๊ฐœ์ •์•ˆ๊ณผ ยงA-1 ์ฃผ์„์— ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค. + +### 1.10 ๐Ÿ†• **H-4 โ€” `MAM_ENV_FILE`์ด ์—†๋Š” ํŒŒ์ผ์„ ๊ฐ€๋ฆฌํ‚ค๋ฉด ๋ชจ๋“  ํด๋ฐฑ์ด ๋ฌด๋ ฅํ™”๋œ๋‹ค** (P1, C2 ํŒŒ์ƒ ์‹ ๊ทœ ์‹ค์ธก) + +`_load_dotenv()` ๋„์ž…๋ถ€: +```python +explicit_file = os.environ.get("MAM_ENV_FILE") +if explicit_file: + if os.path.isfile(explicit_file): + _parse_env_file(explicit_file) + return # โ† ํŒŒ์ผ์ด ์—†์–ด๋„ ์—ฌ๊ธฐ์„œ ์ข…๋ฃŒ. ๋‹ค๋ฅธ ํ›„๋ณด๋ฅผ ์‹œ๋„ํ•˜์ง€ ์•Š์Œ +``` +์‹ค์ธก: + +| ์กฐ๊ฑด | ํ•ด์„๋œ ๋ธŒ๋กœ์ปค | +|---|---| +| `MAM_ENV_FILE=<์˜คํƒ€/์ด๋™๋œ ๊ฒฝ๋กœ>`, ์ €์žฅ์†Œ ๊ฒฝ๋กœ ์Šคํฌ๋ฆฝํŠธ | **`broker.hivemq.com tls=False`** โŒ | +| `MAM_ENV_FILE=<์ •์ƒ ๊ฒฝ๋กœ>` (๋Œ€์กฐ๊ตฐ) | `nats.private.internal tls=True` โœ… | +| freeze ๊ฒฝ๋กœ ์Šคํฌ๋ฆฝํŠธ, cwd = ์‹ค์ œ ์ €์žฅ์†Œ, env ์—†์Œ | **`broker.hivemq.com tls=False`** โŒ | + +**์ค‘์š”**: ์ฑŒ๋ฆฐ์ €๊ฐ€ ์ œ์•ˆํ•œ ์šฐ์„ ์ˆœ์œ„ ์žฌ๋ฐฐ์—ด(`MAM_ENV_FILE โ†’ MAM_REAL_ROOT โ†’ โ€ฆ`)์€ **์ด ๊ฒฝ๋กœ๋ฅผ ๊ณ ์น˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.** 1์ˆœ์œ„์—์„œ ์ด๋ฏธ `return`์œผ๋กœ ํƒˆ์ถœํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทผ๋ณธ ๊ฒฐํ•จ์€ ์ˆœ์„œ๊ฐ€ ์•„๋‹ˆ๋ผ **๋‹จ์ผ ํ›„๋ณด ํ•ด์„**์ž…๋‹ˆ๋‹ค(ยง8 `B-17` ์žฌ์ •์˜). + +์„ธ ๋ฒˆ์งธ ํ–‰์€ ์ฑŒ๋ฆฐ์ €์˜ `os.getcwd()` ๋„์ž… ๊ทผ๊ฑฐ๊ฐ€ **์‹ค์ธก์œผ๋กœ ํƒ€๋‹นํ•จ**์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + +--- + +## 2. ยงA โ€” `PRIVATE_SERVER.md` ๊ฐœ์ •์•ˆ: ยง9 ใ€Œ์›๊ฒฉ ์„œ๋ฒ„ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌใ€ ์‹ ์„ค + +### A-1. ํ”„๋กœ๋•์…˜ `nats.conf` (**Rev.2 โ€” ๊ด€์ธก์ž ์‚ฌ์šฉ์ž ์ถ”๊ฐ€**) + +```conf +# nats.conf โ€” ์›๊ฒฉ ํ”„๋กœ๋•์…˜ (์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ์ ˆ๋Œ€๊ฒฝ๋กœ ๊ธฐ์ค€) +server_name: mam-hub + +# โ”€โ”€ JetStream: MQTT retained/QoS1 ์ €์žฅ์†Œ. MAM ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์žฌ์ˆ˜์‹ ์ด ์—ฌ๊ธฐ์— ์˜์กด โ”€โ”€ +jetstream { + store_dir: "/data" # D-1: ์ ˆ๋Œ€๊ฒฝ๋กœ. '~' ๋„, ์ธ์šฉ๋œ "$HOME" ๋„ ํ™•์žฅ๋˜์ง€ ์•Š์Œ + max_file: 10G + max_mem: 256M +} + +http_port: 8222 # D-3: ํ˜ธ์ŠคํŠธ ๊ฒŒ์‹œ๋Š” loopback ํ•œ์ • (ยงA-3) + +mqtt { + port: 1883 # ๊ณต๊ฐœ TLS ๋ชจ๋ธ์—์„œ๋Š” 8883 + tls ๋ธ”๋ก (ยงB-3) + ack_wait: 60s # WAN RTT ํก์ˆ˜ (๊ธฐ๋ณธ 30s) + max_ack_pending: 1024 # ๊ธฐ๋ณธ 100 โ†’ ๋‹ค์ค‘ ์—์ด์ „ํŠธ ๋™์‹œ ๋ฐœํ–‰ ์—ฌ์œ  +} + +websocket { + port: 8080 + no_tls: true # ์‚ฌ์„ค๋ง/tailnet ํ•œ์ • +} + +# โ”€โ”€ ์ธ์ฆ ๋ฐ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ (Rev.2: C1 ๋ฐ˜์˜) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +accounts { + MAM: { + jetstream: enabled # MQTT ๋‚ด๋ถ€ ์ŠคํŠธ๋ฆผ์ด ์ด ๊ณ„์ • ์•ˆ์— ์ƒ์„ฑ๋จ + users: [ + # ๋ฐœํ–‰์ž ๊ฒธ ๊ตฌ๋…์ž โ€” MAM ์—์ด์ „ํŠธ ๋ณธ์ฒด + { user: mam_agent, password: $MAM_BROKER_PASS } + + # ๊ด€์ธก์ž โ€” ๋Œ€์‹œ๋ณด๋“œ/๋ชจ๋‹ˆํ„ฐ๋ง. PRIVATE_SERVER.md ยง5.5 ์˜ '๋™์ผ ๊ณ„์ • ๋ฐฐ์น˜' ์ฒ˜๋ฐฉ + { 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๊ฑด**์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์ด๋Š” `PRIVATE_SERVER.md:230` ยง5.5๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฐฉํ•œ ๋ฐฐ์น˜("MAM๊ณผ ์ด๋ฅผ ๊ด€์ธกํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋™์ผํ•œ ๊ณ„์ •(`MAM`)์— ๋ฐฐ์น˜")์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋กœ ๋‹ค๋ฅธ ์‹ ๋ขฐ ๋„๋ฉ”์ธ์ด๋ผ ๊ณ„์ •์„ ๋ฐ˜๋“œ์‹œ ๊ฐˆ๋ผ์•ผ ํ•˜๋Š” ์˜ˆ์™ธ ์ƒํ™ฉ์€ **๋ถ€๋ก X(Option A)** ๋ฅผ ๋”ฐ๋ฅด์‹ญ์‹œ์˜ค. + +> [!WARNING] +> **๊ด€์ธก์ž๊ฐ€ ๋ฐ›๋Š” ๊ฒƒ๊ณผ ๋ฐ›์ง€ ๋ชปํ•˜๋Š” ๊ฒƒ (ยง1.9 N-1)** +> - โœ… ์žก ์‹คํ–‰ **์ค‘** ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ์ด๋ฒคํŠธ (๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ) +> - โŒ **์ด๋ฏธ ๋๋‚œ ์žก์˜ retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ** โ€” retained ๋Š” **MQTT ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ** ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. NATS/WebSocket ๋Œ€์‹œ๋ณด๋“œ๋Š” ์ ‘์† ์ด์ „ ์ƒํƒœ๋ฅผ ์žฌ๊ตฌ์„ฑํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. +> - ์‚ฌํ›„ ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•˜๋ฉด ๋Œ€์‹œ๋ณด๋“œ๋ฅผ **MQTT(1883)๋กœ** ๋ถ™์ด๊ฑฐ๋‚˜ ยง5.3์˜ **JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ์„ ์˜ตํŠธ์ธ**ํ•˜์‹ญ์‹œ์˜ค. + +> [!NOTE] +> **๊ณ„์ •๋ณ„ JetStream ํ™œ์„ฑํ™”๋Š” ํ•„์ˆ˜**์ž…๋‹ˆ๋‹ค. MQTT๋Š” ์ ‘์† ๊ณ„์ • ์•ˆ์— `$MQTT_sess`ยท`$MQTT_rmsgs`ยท`$MQTT_out` ์ŠคํŠธ๋ฆผ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๊ณ„์ •์— JetStream์ด ์—†์œผ๋ฉด `JetStream not enabled for account`(ErrCode 10039, HTTP 503)๋กœ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ์ „์—ญ ์š”๊ฑด๋„ ๋ณ„๋„๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค โ€” `mqtt requires JetStream to be enabled if running in standalone mode` (`mqtt.go:234`). +> *๋‹ค๊ณ„์ • ๊ตฌ์„ฑ์—์„œ์˜ ๊ณ„์ •๋ณ„ ์š”๊ตฌ๋Š” ์ŠคํŠธ๋ฆผ ์ƒ์„ฑ ๊ฒฝ๋กœ๋กœ๋ถ€ํ„ฐ์˜ ์ถ”๋ก ์ด๋ฉฐ, ์œ„ ์ฒ˜๋ฐฉ์€ fail-safe์ž…๋‹ˆ๋‹ค. **M2b ์ŠคํŒŒ์ดํฌ R-4์—์„œ ํ™•์ธ ํ•ญ๋ชฉ์œผ๋กœ ์ง€์ •**ํ•ฉ๋‹ˆ๋‹ค.* + +### A-2. ํ”„๋กœ๋•์…˜ `docker-compose.yml` + +```yaml +services: + nats: + image: nats:2.12-alpine # D-2: latest(=scratch)๋Š” healthcheck ๋ถˆ๊ฐ€ + container_name: mam-nats + restart: unless-stopped + command: ["-c", "/etc/nats/nats.conf"] + environment: + # ๋ฏธํ•ด๊ฒฐ $VAR ๋Š” ํŒŒ์‹ฑ ์—๋Ÿฌ โ†’ ์‹œํฌ๋ฆฟ ๋ˆ„๋ฝ ์‹œ '๊ธฐ๋™ ์‹คํŒจ'๋กœ fail-closed + MAM_BROKER_PASS: ${MAM_BROKER_PASS:?set in .env} + MAM_OBSERVER_PASS: ${MAM_OBSERVER_PASS:?set in .env} + HOME_BROKER_PASS: ${HOME_BROKER_PASS:?set in .env} + SYS_BROKER_PASS: ${SYS_BROKER_PASS:?set in .env} + ports: + - "${MQTT_BIND:-127.0.0.1}:1883:1883" + - "127.0.0.1:8222:8222" # D-3: ๋ฌด์ธ์ฆ ๋ชจ๋‹ˆํ„ฐ๋ง์€ loopback ํ•œ์ • + - "${WS_BIND:-127.0.0.1}:8080:8080" + volumes: + - ./nats.conf:/etc/nats/nats.conf:ro + - nats-data:/data + 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 ํฌํŠธ ์ž์ฒด์— ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๋ฅผ ๋ช…์‹œ**ํ•˜๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค. + +### A-3. ํฌํŠธ ๋…ธ์ถœ ๋งคํŠธ๋ฆญ์Šค + +| ํฌํŠธ | ์šฉ๋„ | Tailscale ๋ชจ๋ธ (๊ถŒ์žฅ) | ๊ณต๊ฐœ TLS ๋ชจ๋ธ | ์ ˆ๋Œ€ ๊ธˆ์ง€ | +|---|---|---|---|---| +| 1883 | MQTT ํ‰๋ฌธ | tailnet IP ๋ฐ”์ธ๋“œ | โœ– ๋ฏธ๊ฒŒ์‹œ | 0.0.0.0 ๊ฒŒ์‹œ | +| 8883 | MQTT TLS | (๋ถˆํ•„์š”) | `0.0.0.0` + LE ์ธ์ฆ์„œ | ์ธ์ฆ ์—†์ด ๊ฒŒ์‹œ | +| 4222 | NATS ๋„ค์ดํ‹ฐ๋ธŒ | tailnet IP ๋ฐ”์ธ๋“œ | ๋ฏธ๊ฒŒ์‹œ(๋˜๋Š” TLS+์ธ์ฆ) | 0.0.0.0 ํ‰๋ฌธ | +| 8222 | HTTP ๋ชจ๋‹ˆํ„ฐ๋ง | **`127.0.0.1` ํ•œ์ •** | **`127.0.0.1` ํ•œ์ •** | ์–ด๋–ค ๊ฒฝ์šฐ์—๋„ ๊ณต๊ฐœ | +| 8080 | WebSocket(`no_tls`) | tailnet IP ๋ฐ”์ธ๋“œ | ๋ฏธ๊ฒŒ์‹œ | ๊ณต๊ฐœ ์ธํ„ฐํŽ˜์ด์Šค ๊ฒŒ์‹œ | + +### A-4. ๐Ÿ†• `PRIVATE_SERVER.md` ยง5.2 ๊ฐœ์ • (N-1 ๋ฐ˜์˜) + +๊ธฐ์กด ยง5.2 ๋ฌธ์žฅ *"์›น ๋ธŒ๋ผ์šฐ์ €๋‚˜ ํƒ€ ํ”„๋กœ์ ํŠธ์˜ NATS ๊ตฌ๋…์ž๋Š” โ€ฆ ์ฆ‰์‹œ ์‹ค์‹œ๊ฐ„ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค"* ์— ๋‹ค์Œ ๊ฒฝ๊ณ„๋ฅผ ๋ณ‘๊ธฐํ•ฉ๋‹ˆ๋‹ค. + +```markdown +- **๊ฒฝ๊ณ„ (ํ•„์ˆ˜ ์ธ์ง€)**: ๊ต์ฐจ ํ”„๋กœํ† ์ฝœ ๋ธŒ๋ฆฌ์ง•์€ **๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ์— ํ•œ์ •**๋ฉ๋‹ˆ๋‹ค. MQTT์˜ + retained ๋ฉ”์‹œ์ง€๋Š” MQTT ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ ์ „๋‹ฌ๋˜๋ฏ€๋กœ(`mqttSendRetainedMsgsToNewSubs`๊ฐ€ + MQTT SUBSCRIBE ๊ฒฝ๋กœ ์ „์šฉ), ์žก์ด ๋๋‚œ ๋’ค ์ ‘์†ํ•œ NATS/WebSocket ๋Œ€์‹œ๋ณด๋“œ๋Š” ๊ทธ ์žก์˜ + **์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค**. ์‚ฌํ›„ ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•˜๋ฉด (a) ๋Œ€์‹œ๋ณด๋“œ๋ฅผ MQTT(1883)๋กœ + ์—ฐ๊ฒฐํ•˜๊ฑฐ๋‚˜ (b) ยง5.3 JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ์„ ์˜ตํŠธ์ธํ•˜์‹ญ์‹œ์˜ค. +- **๊ณ„์ • ๋ฐฐ์น˜**: ๊ด€์ธก ํด๋ผ์ด์–ธํŠธ๋Š” ยง5.5 ์ฒ˜๋ฐฉ๋Œ€๋กœ `MAM` ๊ณ„์ • ์•ˆ์˜ ์ฝ๊ธฐ ์ „์šฉ ์‚ฌ์šฉ์ž + (`mam_observer`)๋กœ ์ ‘์†ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ณ„์ •์—์„œ๋Š” subject ๊ฐ€ ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. +``` + +--- + +## 3. ยงB โ€” ์›๊ฒฉ ๋„คํŠธ์›Œํ‚น & ๋ณด์•ˆ ๊ฐ€์ด๋“œ + +### B-1. ๋…ธ์ถœ ๋ชจ๋ธ 3์•ˆ ๋น„๊ต ๋ฐ ๊ถŒ๊ณ  + +| ํ•ญ๋ชฉ | ๐Ÿ† **๋ชจ๋ธ T: Tailscale/WireGuard ์˜ค๋ฒ„๋ ˆ์ด** | ๋ชจ๋ธ P: ๊ณต๊ฐœ TLS (Let's Encrypt) | ๋ชจ๋ธ S: SSH ํ„ฐ๋„ | +|---|---|---|---| +| ์ธํ„ฐ๋„ท ๋…ธ์ถœ ๋ฉด์  | **0** | 8883 1๊ฐœ | 0 | +| ์ธ์ฆ์„œ ํ•„์š” | ๋ถˆํ•„์š” (`MQTT_TLS=0`) | ํ•„์ˆ˜ + 90์ผ ๊ฐฑ์‹  | ๋ถˆํ•„์š” | +| **D-4 ํ˜ธ์ŠคํŠธ๋ช… ์ œ์•ฝ** | **ํ•ด๋‹น ์—†์Œ** | ๋„๋ฉ”์ธ ํ•„์ˆ˜, IP ๋ถˆ๊ฐ€ | ํ•ด๋‹น ์—†์Œ | +| ๋„๋ฉ”์ธ ํ•„์š” | ๋ถˆํ•„์š” | **ํ•„์ˆ˜** | ๋ถˆํ•„์š” | +| ์ด๋™์„ฑ | ์ž๋™ | ์ž๋™ | ํ„ฐ๋„ ์ˆ˜๋™ ๊ด€๋ฆฌ | +| ์žฅ์•  ์ง€์  | tailnet ์ฝ”๋””๋„ค์ดํ„ฐ | certbot ๊ฐฑ์‹  ์‹คํŒจ | SSH ์„ธ์…˜ | + +**๊ถŒ๊ณ : ๋ชจ๋ธ T.** ๊ทผ๊ฑฐ โ€” (1) D-4์˜ ๋„๋ฉ”์ธยทSAN ์ œ์•ฝ์„ ์†Œ๊ฑฐ, (2) D-3์˜ ๋ชจ๋‹ˆํ„ฐ๋ง/WS ๋…ธ์ถœ์„ ๊ตฌ์กฐ์ ์œผ๋กœ ์ œ๊ฑฐ, (3) ๋กค๋ฐฑ์ด `.mam.env` ํ•œ ์ค„, (4) MAM์€ **๊ด€์ธก ์‚ฌ์ด๋“œ์นด**์ด๋ฏ€๋กœ(์ œ์–ด ํ‰๋ฉด์€ `wait_for_job` ํŒŒ์ผ ํด๋ง) ์˜ค๋ฒ„๋ ˆ์ด ์ง€์—ฐ์ด ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ์ •ํ™•์„ฑ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Œ. + +### B-2. ๋ฐฉํ™”๋ฒฝ (UFW) โ€” 2์ฐจ ๋ฐฉ์–ด์„  + +```bash +sudo ufw default deny incoming +sudo ufw default allow outgoing +sudo ufw allow 22/tcp + +# ๋ชจ๋ธ T: 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 + +# ๋ชจ๋ธ P: 8883๋งŒ ๊ณต๊ฐœ +# sudo ufw allow 8883/tcp +# sudo ufw allow 80/tcp # certbot HTTP-01 ์ฑŒ๋ฆฐ์ง€ ๊ธฐ๊ฐ„ ํ•œ์ • + +sudo ufw enable && sudo ufw status verbose +``` +**Docker ์šฐํšŒ ๋Œ€์‘(ํ•„์ˆ˜)** โ€” compose ์˜† `.env`์— ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๋ฅผ ์ฃผ์ž…ํ•˜๊ณ  ์‹ค์ œ ๋ฐ”์ธ๋”ฉ์„ ๋‹จ์–ธํ•ฉ๋‹ˆ๋‹ค: +```bash +MQTT_BIND=100.x.y.z # tailscale ip -4 +WS_BIND=100.x.y.z +``` +```bash +sudo ss -lntp | grep -E ':(1883|4222|8222|8080)\b' +# ๊ธฐ๋Œ€: 8222 ๋Š” 127.0.0.1 ์—๋งŒ, 1883/8080 ์€ tailnet IP ์—๋งŒ +``` + +### B-3. ๋ชจ๋ธ P ์ „์šฉ โ€” TLS / Certbot + +```bash +sudo certbot certonly --standalone -d mam-broker.example.com + +# nats.conf ์˜ mqtt ๋ธ”๋ก: +# mqtt { +# port: 8883 +# tls { +# cert_file: "/etc/letsencrypt/live/mam-broker.example.com/fullchain.pem" +# key_file: "/etc/letsencrypt/live/mam-broker.example.com/privkey.pem" +# } +# } +# compose ๋ณผ๋ฅจ: live/ ๋Š” archive/ ๋กœ์˜ ์‹ฌ๋ณผ๋ฆญ ๋งํฌ โ†’ /etc/letsencrypt ์ „์ฒด๋ฅผ ๋งˆ์šดํŠธ +# - /etc/letsencrypt:/etc/letsencrypt:ro + +sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nats.sh >/dev/null <<'SH' +#!/bin/sh +docker compose -f /srv/mam-nats/docker-compose.yml kill -s HUP nats +SH +sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nats.sh +sudo certbot renew --dry-run +``` +**ํด๋ผ์ด์–ธํŠธ ๊ทœ์น™ (D-4)**: `MQTT_BROKER`๋Š” **๋„๋ฉ”์ธ**(IP ๊ธˆ์ง€). Let's Encrypt ์‚ฌ์šฉ ์‹œ `MQTT_CA_CERTS`๋Š” **์„ค์ •ํ•˜์ง€ ์•Š์Œ**. ์‚ฌ์„ค CA์ผ ๋•Œ๋งŒ ์ง€์ •ํ•˜๊ณ  SAN์— ์ ‘์†๋ช…์„ ๋ฐ˜๋“œ์‹œ ํฌํ•จ. + +### B-4. ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ์‹œํฌ๋ฆฟ ์ฃผ์ž… + +```bash +openssl rand -base64 32 # ๊ณ„์ •/์‚ฌ์šฉ์ž๋ณ„๋กœ ๊ฐ๊ฐ ์ƒ์„ฑ (mam_agent, mam_observer, home, sys) +chmod 600 .env +``` +NATS ํŒŒ์„œ๋Š” ๋ฏธํ•ด๊ฒฐ `$VAR`๋ฅผ **์—๋Ÿฌ**๋กœ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์‹œํฌ๋ฆฟ์ด ๋น„๋ฉด ์„œ๋ฒ„๊ฐ€ ์กฐ์šฉํžˆ ์ต๋ช…์œผ๋กœ ๋œจ์ง€ ์•Š๊ณ  **๊ธฐ๋™์— ์‹คํŒจ**ํ•ฉ๋‹ˆ๋‹ค(fail-closed, ์˜๋„์  ์ฑ„ํƒ). + +**H-3 ์™„ํ™” ๊ทœ์น™**: (1) `MQTT_PASSWORD`๋Š” ์‚ฌ๋žŒ์ด ์žฌ์‚ฌ์šฉํ•˜๋Š” ์•”ํ˜ธ๊ฐ€ ์•„๋‹ˆ๋ผ **๊ธฐ๊ณ„ ์ƒ์„ฑ ํ† ํฐ**๋งŒ ์‚ฌ์šฉ(๋ ˆ์ฝ”๋“œ์— ํ‰๋ฌธ์œผ๋กœ ๋‚จ์Œ). (2) ํšŒ์ „ ์‹œ ์„œ๋ฒ„ `.env` โ†’ `docker compose up -d` โ†’ ํด๋ผ์ด์–ธํŠธ `.mam.env` โ†’ **์ž”์—ฌ ์žก ๋ ˆ์ฝ”๋“œ ์ •๋ฆฌ**(ยงC-3๊ณผ ๋™์ผ ์ ˆ์ฐจ). (3) ์žฅ๊ธฐ ๊ณผ์ œ๋Š” `B-18`. + +**๊ถŒํ•œ ๊ฒฉ๋ฆฌ**: `mam_agent`๋Š” ๋ฐœํ–‰/๊ตฌ๋…, `mam_observer`๋Š” `publish: { deny: [">"] }`๋กœ **๋ฐœํ–‰ ์ „๋ฉด ๊ธˆ์ง€**. ์ด๋Š” A-2(์™ธ๋ถ€ ์•…์˜์  ์ด๋ฒคํŠธ ์ฃผ์ž…) ๋Œ€์‘๊ณผ ๊ฐ™์€ ๋ฐฉํ–ฅ์ด๋ฉฐ, HMAC(`auth_token`)๊ณผ ์ด์ค‘ ๋ฐฉ์–ด๋ฅผ ์ด๋ฃน๋‹ˆ๋‹ค. + +--- + +## 4. ยงC โ€” ํด๋ผ์ด์–ธํŠธ ์„ค์ • ๋ฐ ์›๊ฒฉ ๊ฒ€์ฆ ํ”Œ๋ ˆ์ด๋ถ + +### C-1. `.mam.env` (๋ชจ๋ธ๋ณ„) + +```bash +# โ”€โ”€ ๋ชจ๋ธ 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=<๊ธฐ๊ณ„ ์ƒ์„ฑ ํ† ํฐ> +MQTT_KEEPALIVE=60 # D-5: ์ฝ”๋“œ๊ฐ€ ์‹ค์ œ๋กœ ์ฝ๋Š” ๊ฐ’. ํ…œํ”Œ๋ฆฟ์— ์ถ”๊ฐ€ ํ•„์š” + +# โ”€โ”€ ๋ชจ๋ธ P (๊ณต๊ฐœ TLS) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# MQTT_BROKER="mam-broker.example.com" # D-4: ์ธ์ฆ์„œ SAN ์˜ DNS ์ด๋ฆ„. IP ๊ธˆ์ง€ +# MQTT_PORT=8883 +# MQTT_TLS=1 +# MQTT_CA_CERTS ๋Š” Let's Encrypt ์‚ฌ์šฉ ์‹œ '์„ค์ •ํ•˜์ง€ ์•Š์Œ' +``` +> **D-5 ๊ต์ •**: `MQTT_RETRY_INTERVAL` / `MQTT_MAX_RETRIES`๋Š” ์–ด๋–ค ์ฝ”๋“œ๋„ ์ฝ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ…œํ”Œ๋ฆฟ์—์„œ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ "๋ฏธ์‚ฌ์šฉ(historical)"๋กœ ๊ฐ•๋“ฑํ•˜๊ณ , ์žฌ์‹œ๋„ ์กฐ์ •์€ `publish_event.py --attempts`์ž„์„ ๋ช…์‹œ. ์—ญ์œผ๋กœ `MQTT_KEEPALIVE`๋Š” ์ถ”๊ฐ€. + +### C-2. ์›๊ฒฉ ๊ฒ€์ฆ ํ”Œ๋ ˆ์ด๋ถ (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` ์—†์ด ์‹คํ–‰ / ๊ทธ๋ฆฌ๊ณ  ์˜คํƒ€ ๊ฒฝ๋กœ๋กœ ์‹คํ–‰ | ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค๋กœ ๋‚˜๊ฐ€์ง€ ์•Š์Œ โ€” **ํ˜„์žฌ๋Š” ์–‘์ชฝ ๋‹ค ์‹คํŒจ๊ฐ€ ๊ธฐ๋Œ€๊ฐ’**์ด๋ฉฐ `B-17`์˜ ๊ทผ๊ฑฐ | +| **R-8** | ์ „์ฒด ํšŒ๊ท€ ์Šค์œ„ํŠธ | `.venv/bin/python -m pytest tests/ -q` | ํ˜„์žฌ ๊ธฐ์ค€ **290 passed** | +| ๐Ÿ†• **R-9** | **๊ด€์ธก์ž ๊ณ„์ • ๊ฒฝ๊ณ„** | `mam_observer`๋กœ NATS ๊ตฌ๋… โ†’ ์ด๋ฒคํŠธ ์ˆ˜์‹  ํ™•์ธ. ์ด์–ด์„œ `home`(๊ณ„์ • HOME)์œผ๋กœ ๋™์ผ ๊ตฌ๋… | `mam_observer` **์ˆ˜์‹ **, `home` **0๊ฑด** (= ๊ฒฉ๋ฆฌ ์ •์ƒ) | +| ๐Ÿ†• **R-10** | **N-1 retained ๊ฒฝ๊ณ„ ํ™•์ธ** | ์žก ์ข…๋ฃŒ **ํ›„** NATS ๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌ๋…์ž๋ฅผ ์ƒˆ๋กœ ๋ถ™์ž„ | **0๊ฑด ์ˆ˜์‹ **์ด ์ •์ƒ. ์ˆ˜์‹ ๋˜๋ฉด N-1 ์ „์ œ๊ฐ€ ํ‹€๋ฆฐ ๊ฒƒ์ด๋ฏ€๋กœ ยงA-4 ๋ฌธ๊ตฌ ์žฌ์ž‘์„ฑ | + +> R-9/R-10์€ ์ฑŒ๋ฆฐ์ง€ C1์ด ์ œ๊ธฐํ•œ ๊ณ„์ • ๋ฌธ์ œ์™€, ๊ทธ๋ณด๋‹ค ๊ทผ๋ณธ์ ์ธ retained ๊ฒฝ๊ณ„๋ฅผ **๊ฐ๊ฐ ์‹ค์ฆ**ํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ R-10์€ **๋ฐ˜์ฆ ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ**๋กœ ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. + +**R-4 ๊ตฌ์ฒด ์ ˆ์ฐจ**: +```bash +JID=$(.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \ + --registry-dir .mam/jobs \ + register --prompt "remote broker connectivity test" --agent-session "herdr:test") + +.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py \ + --registry-dir .mam/jobs --job "$JID" --event progress --detail "remote broker verified" -v + +.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \ + --registry-dir .mam/jobs status --job "$JID" --set completed # ์œ ๋ น ์žก ๋ฐฉ์ง€ +``` +> `--registry-dir`๋Š” **๋ถ€๋ชจ ํŒŒ์„œ ์ธ์ž**์ด๋ฏ€๋กœ ์„œ๋ธŒ์ปค๋งจ๋“œ ์•ž์— ์˜ต๋‹ˆ๋‹ค. `register`์—๋Š” `--job-id`๊ฐ€ ์—†์–ด ID๋Š” stdout์—์„œ ์บก์ฒ˜ํ•ฉ๋‹ˆ๋‹ค(์‹ค์ธก ํ™•์ธ). + +**์ง€์—ฐ ์ธก์ •**: +```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 +``` +**ํŒ์ •**: QoS1 RTT p50 > 200ms๋ฉด `mqtt.ack_wait` ์ƒํ–ฅ, > 1s๋ฉด ์˜ค๋ฒ„๋ ˆ์ด ๊ฒฝ๋กœ(๋ฆด๋ ˆ์ด ํด๋ฐฑ) ์ ๊ฒ€. `with_retry` ๋ฐฑ์˜คํ”„๊ฐ€ 0.5sโ†’1sโ†’2s์ด๋ฏ€๋กœ ์ˆ˜๋ฐฑ ms RTT์—์„œ๋Š” ์žฌ์‹œ๋„ ์—†์ด ํ†ต๊ณผํ•ด์•ผ ์ •์ƒ์ž…๋‹ˆ๋‹ค. + +### C-3. ์ „ํ™˜(Cutover) ์ ˆ์ฐจ โ€” H-2 ๋Œ€์‘ + +``` +[1 ๋“œ๋ ˆ์ธ] โ”€โ”€> [2 ์ž”์—ฌ ์Šค์บ”] โ”€โ”€> [3 .mam.env ๊ต์ฒด] โ”€โ”€> [4 R-1~R-10] โ”€โ”€> [5 ๋ ˆ๊ฑฐ์‹œ ์ฐจ๋‹จ] +``` +1. **๋“œ๋ ˆ์ธ**: ์‹ ๊ทœ ์œ„์ž„ ์ค‘๋‹จ, ์ง„ํ–‰ ์ค‘ ์žก์ด ๋ชจ๋‘ terminal ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ. +2. **์ž”์—ฌ ์Šค์บ”** โ€” ์˜› ๋ธŒ๋กœ์ปค์— ํ•€ ๊ณ ์ •๋œ ๋ ˆ์ฝ”๋“œ ํ™•์ธ: + ```bash + .venv/bin/python - <<'PY' + import json, glob + for p in sorted(glob.glob('.mam/jobs/*.json')): + d = json.load(open(p)) + if d.get('status') not in ('completed', 'error', 'cancelled'): + b = d.get('broker') or {} + print(f"{d.get('job_id')} status={d.get('status'):<9} broker={b.get('host')}:{b.get('port')}") + PY + ``` + ์ถœ๋ ฅ์ด ๋น„์–ด์•ผ 3๋‹จ๊ณ„ ์ง„์ž…. ๋‚จ์œผ๋ฉด ์ข…๊ฒฐ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ `broker` ๋ธ”๋ก์„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜. +3. `.mam.env` ๊ต์ฒด โ€” **์ฝ”๋“œ ๋ณ€๊ฒฝ 0์ค„.** +4. R-1 ~ R-10 ์ „๊ฑด ํ†ต๊ณผ. +5. ๋ ˆ๊ฑฐ์‹œ ์ฐจ๋‹จ: ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ์ฃผ์†Œ๊ฐ€ ํ™œ์„ฑ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋‚จ์ง€ ์•Š๋„๋ก `.mam.env` / `deploy/install.sh` ์ ๊ฒ€. + +--- + +## 5. ยงD โ€” `implementation_plan.md` ๊ฐœ์ •์•ˆ + +### D-a. M2 ๋ถ„ํ•  + +| ๋งˆ์ผ์Šคํ†ค | ์ด๋ฆ„ | DoD | ๊ฒŒ์ดํŠธ | +|---|---|---|---| +| M0 | ๋ฌธ์„œ ์ •ํ•ฉ์„ฑ | (์™„๋ฃŒ) | (ํ†ต๊ณผ, 290 ์‹ค์ธก) | +| M1 | Track 0 ๋‚ด๊ฒฐํ•จ์„ฑ | (์™„๋ฃŒ โ€” `c6b6c77`) | (ํ†ต๊ณผ) | +| **M2a** | ๋กœ์ปฌ ์ŠคํŒŒ์ดํฌ (Track 1) | ๊ฒฉ๋ฆฌ ํด๋ก ์—์„œ S-1 ~ S-9 ์™„์ˆ˜ | S-3 retained ํ†ต๊ณผ | +| **M2b** | ์›๊ฒฉ ํ”„๋กœ๋•์…˜ ์ „ํ™˜ (Track 1R) | D-1~D-5 ๊ต์ • + ยงA/ยงB ๋ฐฐํฌ + ยงC-3 ์ „ํ™˜ | **R-3 ยท R-5 ยท R-6 ยท R-9 ๋™์‹œ ํ†ต๊ณผ**, R-7ยทR-10 ๊ฒฐ๊ณผ ๊ธฐ๋ก | +| M3 | Track 2 ๋ณด์•ˆ | A-2 ์ง€๋ฌธ ํ† ํ”ฝ, G-11 | ์ง€๋ฌธ ํ† ํ”ฝ ํ™•์ธ ํ›„ ๋ ˆ๊ฑฐ์‹œ ๊ตฌ๋… ์ œ๊ฑฐ **+ ๊ด€์ธก์ž ๊ถŒํ•œ/Export subject ๋™์‹œ ๊ฐฑ์‹ ** | +| M4 | Track 3 ๋™๊ธฐํ™” | ๋ฌธ์„œ/๋ฐฐํฌ ์ •ํ•ฉ | ์ „์ฒด ์Šค์œ„ํŠธ Green | + +> **M2b ์ง„์ž… ์„ ํ–‰ ์กฐ๊ฑด**: D-1 ~ D-5 ๊ต์ •์ด `PRIVATE_SERVER.md`์— ๋ฐ˜์˜๋˜๊ณ  ์‹ ๊ทœ ๊ฐ€๋“œ๊ฐ€ ํ†ต๊ณผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ต์ • ์ „ ๋ฐฐํฌ๋Š” D-1(์Šคํ† ๋ฆฌ์ง€ ์œ ์‹ค)ยทD-2(healthcheck ๋ถˆ๊ฐ€)ยทD-3(๋ชจ๋‹ˆํ„ฐ๋ง ๊ณต๊ฐœ)ยทD-4(TLS ์ ‘์† ๋ถˆ๊ฐ€)๋กœ **๋ฐ˜๋“œ์‹œ ์‹คํŒจ**ํ•ฉ๋‹ˆ๋‹ค. + +> ๐Ÿ†• **M3์— ์ถ”๊ฐ€๋œ ๊ฒฐํ•ฉ ํ•ญ๋ชฉ**: ํ† ํ”ฝ ๋ฃจํŠธ๊ฐ€ `python/mqtt/jobs/โ€ฆ` โ†’ `mam//jobs/โ€ฆ`๋กœ ๋ฐ”๋€Œ๋ฉด ยงA-1์˜ `mam_observer.subscribe.allow`์™€ (Option A ์ฑ„ํƒ ์‹œ) export subject๊ฐ€ **์กฐ์šฉํžˆ ๋งค์นญ ์‹คํŒจ**ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋“œ `G-D9`๊ฐ€ ์ด๋ฅผ ๊ธฐ๊ณ„์ ์œผ๋กœ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. + +### D-b. Track 1R (์‹ ์„ค) + +| ํŠธ๋ž™ | ๋Œ€์ƒ | ๋ชฉํ‘œ | ๋ณ€๊ฒฝ ์ง€์  | +|---|---|---|---| +| **Track 1R** | ์›๊ฒฉ ๋ฐฐํฌ | VPS/ํ™ˆ๋žฉ `nats-server` ์ƒ์‹œ ๊ฐ€๋™ ๋ฐ MAM ์ „ํ™˜ | `PRIVATE_SERVER.md` ยง9/ยง5.2, ์„œ๋ฒ„์ธก `nats.conf`ยท`docker-compose.yml`, `.mam.env` | + +### D-c. M2b ๋‹จ๊ณ„๋ณ„ ๋กœ๋“œ๋งต + +``` +[P0 ๊ต์ •] D-1~D-5 ๋ฌธ์„œ ๊ต์ • + ยง5.2 ๊ฒฝ๊ณ„ ๋ช…๋ฌธํ™” + ์‹ ๊ทœ ๊ฐ€๋“œ 7์ข… + โ–ผ +[P1 ์„œ๋ฒ„ ์ค€๋น„] VPS/ํ™ˆ๋žฉ ํ”„๋กœ๋น„์ €๋‹, Docker, Tailscale ๊ฐ€์ž… + โ–ผ +[P2 ๋ฐฐํฌ] nats.conf + compose ๊ธฐ๋™, healthcheck healthy ํ™•์ธ + โ–ผ +[P3 ์ž ๊ธˆ] ๋ฐ”์ธ๋“œ ์ฃผ์†Œ ํ•œ์ • + UFW + ss/nmap ๋กœ ๋…ธ์ถœ ๋ฉด์  0 ๋‹จ์–ธ (R-3) + โ–ผ +[P4 ์ „ํ™˜] ๋“œ๋ ˆ์ธ โ†’ ์ž”์—ฌ ์Šค์บ” โ†’ .mam.env ๊ต์ฒด (ยงC-3) + โ–ผ +[P5 ๊ฒ€์ฆ] R-1 ~ R-10. R-5(retained) / R-9(๊ณ„์ • ๊ฒฝ๊ณ„) ๋ฅผ ์ตœ์ข… ๊ด€๋ฌธ์œผ๋กœ + โ–ผ +[P6 ์ƒ์‹œํ™”] ๋กœ๊ทธ ๋กœํ…Œ์ด์…˜, JetStream ๋ณผ๋ฅจ ๋ฐฑ์—…, healthcheck ์•Œ๋ฆผ +``` +**P6 ์ตœ์†Œ ์š”๊ฑด**: `nats-data` ๋ณผ๋ฅจ ์ฃผ๊ธฐ ์Šค๋ƒ…์ƒท(**retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๊ฐ€ ์—ฌ๊ธฐ ์žˆ์Œ โ€” ๋ณผ๋ฅจ ์œ ์‹ค = ์ด๋ฒคํŠธ ์œ ์‹ค**), healthcheck ์ƒํƒœ ๊ฐ์‹œ, JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ ์‚ฌ์šฉ ์‹œ `max_age`/`max_bytes` ์ƒํ•œ ํ•„์ˆ˜. + +### D-d. ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +```markdown +### M2b: ์›๊ฒฉ ํ”„๋กœ๋•์…˜ ์ „ํ™˜ (Track 1R) +- [ ] D-1 store_dir ์ ˆ๋Œ€๊ฒฝ๋กœ ๊ต์ • + ๋น„์ธ์šฉ heredoc (PRIVATE_SERVER.md:73, :146) +- [ ] D-2 ์ด๋ฏธ์ง€ ํ•€ nats:2.12-alpine + /healthz healthcheck +- [ ] D-3 8222/8080 ๋ฐ”์ธ๋“œ ์ฃผ์†Œ ํ•œ์ • +- [ ] D-4 TLS ์ ˆ์˜ IP ์˜ˆ์‹œ ์ œ๊ฑฐ ๋ฐ DNS/SAN ์š”๊ฑด ๋ช…์‹œ +- [ ] D-5 .mam.env.example ์ •ํ•ฉ (MQTT_KEEPALIVE ์ถ”๊ฐ€ / RETRYยทMAX_RETRIES ๊ฐ•๋“ฑ) +- [ ] N-1 ยง5.2 ์— retained=MQTT ์ „์šฉ ๊ฒฝ๊ณ„ ๋ช…๋ฌธํ™” + ยงA-1 mam_observer ์ถ”๊ฐ€ +- [ ] ์‹ ๊ทœ ๊ฐ€๋“œ G-D5(๊ฐ•ํ™”) ~ G-D9, G-R1, G-R2 ๊ตฌํ˜„ ๋ฐ mutation ํ™•์ธ (290 โ†’ 297) +- [ ] ์„œ๋ฒ„ ๋ฐฐํฌ ๋ฐ R-3 ๋…ธ์ถœ ๋ฉด์  0 ๋‹จ์–ธ +- [ ] ยงC-3 ๋“œ๋ ˆ์ธยท์ž”์—ฌ ์Šค์บ” ํ›„ .mam.env ์ „ํ™˜ +- [ ] R-1 ~ R-10 ์ „๊ฑด ํ†ต๊ณผ (R-5 / R-9 ์ตœ์ข… ๊ด€๋ฌธ) +``` + +--- + +## 6. ํšŒ๊ท€ ๊ฐ€๋“œ (7์ข…) โ€” `tests/test_deploy_freshness.py::test_d7` ๊ณ„๋ณด + +| ID | ๊ฐ€๋“œ ๋‚ด์šฉ | ๋ณ€์ด ๊ฒ€์ถœ ๊ธฐ์ค€ (Mutation) | +|---|---|---| +| **G-D5** ๐Ÿ”บ๊ฐ•ํ™” | **2ํ•ญ ๊ฒ€์‚ฌ**: (i) ํŽœ์Šค ๋ธ”๋ก ๋‚ด ๋ชจ๋“  `store_dir:` ๊ฐ’์ด `/`๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ ˆ๋Œ€๊ฒฝ๋กœ, (ii) ํ•ด๋‹น ๊ฐ’์„ ๊ธฐ๋กํ•˜๋Š” heredoc ๊ตฌ๋ถ„์ž๊ฐ€ **๋น„์ธ์šฉ**(`</jobs`๋กœ ๋ฐ”๊พธ๊ณ  ๋ฌธ์„œ๋ฅผ ๊ฐฑ์‹ ํ•˜์ง€ ์•Š์œผ๋ฉด FAIL | +| **G-R1** | `run_loop.sh`์— `export MAM_ENV_FILE=` ๋ผ์ธ ์กด์žฌ ๋‹จ์–ธ + `.agents`๋งŒ ์žˆ๊ณ  `.mam.env`๊ฐ€ ์—†๋Š” ์ž„์‹œ ๋ฃจํŠธ์—์„œ์˜ ํšŒ๊ท€ ๋™์ž‘์„ ๋ช…์‹œ์ ์œผ๋กœ ๊ณ ์ • | `run_loop.sh:106` export ์ œ๊ฑฐ ์‹œ FAIL | +| **G-R2** | ์ฝ”๋“œ๊ฐ€ ์ฝ๋Š” ๋ชจ๋“  `MQTT_*` ์ด๋ฆ„์ด `.mam.env.example`์— ์กด์žฌ(ํŠนํžˆ `MQTT_KEEPALIVE`)ํ•˜๊ณ , ์ฝ”๋“œ๊ฐ€ ์ฝ์ง€ ์•Š๋Š” ์ด๋ฆ„์€ ํ™œ์„ฑ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๊ธฐ๋ก๋˜์ง€ ์•Š์Œ | `MQTT_KEEPALIVE` ์ œ๊ฑฐ ๋˜๋Š” `MQTT_RETRY_INTERVAL=2` ํ™œ์„ฑ ๋ณต์› ์‹œ FAIL | + +**๊ตฌํ˜„ ๊ทœ์น™(Rev.1 ์Šน๊ณ„ + C3 ๋ฐ˜์˜ ํ™•์ธ)**: +- ๋ฌธ์„œ ๊ฐ€๋“œ๋Š” **ํŽœ์Šค ์ฝ”๋“œ ๋ธ”๋ก์— ํ•œ์ •**ํ•ด ์Šค์บ”ํ•˜๊ณ , ์‚ฐ๋ฌธ errata(์˜ˆ: *"๊ณผ๊ฑฐ์—๋Š” `nats:latest`๋ฅผ ๊ถŒ์žฅํ–ˆ์œผ๋‚˜โ€ฆ"*)๊ฐ€ ์˜คํƒ๋˜์ง€ ์•Š์Œ์„ **๋™๋ฐ˜ ๋‹จ์–ธ**ํ•ฉ๋‹ˆ๋‹ค. โ€” *์ด ๊ทœ์ •์€ Rev.1 ยง6์— ์ด๋ฏธ ์žˆ์—ˆ์œผ๋ฉฐ C3์˜ ์š”์ฒญ๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ ์—†์ด ์žฌํ™•์ธํ•ฉ๋‹ˆ๋‹ค.* +- G-D9์™€ G-R2๋Š” ์†Œ์Šค์—์„œ ๊ฐ’์„ **์ •์ ์œผ๋กœ ์ˆ˜์ง‘**ํ•ด ๋ฌธ์„œ์™€ ๋Œ€์กฐํ•ฉ๋‹ˆ๋‹ค(๋ธŒ๋กœ์ปค ์ ‘์† ์—†์Œ). + +**ํ…Œ์ŠคํŠธ ์ˆ˜ ์ „๋ง**: 290 (ํ˜„์žฌ ์‹ค์ธก) โ†’ **297** (G-D5 ๊ฐ•ํ™” + G-D6~G-D9, G-R1, G-R2) โ†’ **298** (M3์˜ G-11). + +--- + +## 7. ๋กค๋ฐฑ ์ „๋žต + +| ์‹คํŒจ ์ง€์  | ๋กค๋ฐฑ | ๋น„์šฉ | +|---|---|---| +| R-5 retained ์‹คํŒจ | `.mam.env`์˜ `MQTT_BROKER`๋งŒ `eclipse-mosquitto`๋กœ ๊ต์ฒด | ์ฝ”๋“œ 0์ค„, ์ฆ‰์‹œ | +| R-9 ๊ณ„์ • ๊ฒฝ๊ณ„ ์‹คํŒจ | `mam_observer` ๊ถŒํ•œ ๋ธ”๋ก๋งŒ ์ˆ˜์ •, MAM ๋ณธ์ฒด ๋ฌด์˜ํ–ฅ | ์„œ๋ฒ„ ์„ค์ • 1๊ณณ | +| ์›๊ฒฉ ๋งํฌ ๋ถˆ์•ˆ์ • | `.mam.env`๋ฅผ ์ง์ „ ๊ฐ’์œผ๋กœ ์›๋ณต | ์ฝ”๋“œ 0์ค„ | +| ์„œ๋ฒ„ ์ „์†Œ | Track 0 ๋•๋ถ„์— **๋ฃจํ”„๋Š” ๊ณ„์† ์™„์ฃผ**(๋””์Šคํฌ ํด๋ฐฑ). ๊ด€์ธก๋งŒ ์ผ์‹œ ์ƒ์‹ค | 0 | +| JetStream ๋ณผ๋ฅจ ์œ ์‹ค | retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์œ ์‹ค โ†’ ๋ฐฑ์—… ์Šค๋ƒ…์ƒท ๋ณต๊ตฌ | P6 ๋ฐฑ์—… ํ•„์š” | + +Track 0(`B-14`/`B-15`)์€ ๋ธŒ๋กœ์ปค ์ œํ’ˆยท์œ„์น˜์™€ ๋ฌด๊ด€ํ•œ ์ˆœ์ด๋“์ด๋ฏ€๋กœ ๋กค๋ฐฑํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. **์›๊ฒฉ ์ „ํ™˜ ์ „์ฒด๊ฐ€ ๊ฐ€์—ญ์ ์ธ ์ด์œ ๊ฐ€ M1 ์™„๋ฃŒ ๋•๋ถ„์ž…๋‹ˆ๋‹ค.** + +--- + +## 8. ํ›„์† ์ฝ”๋“œ ๊ณผ์ œ (Rev.2 ์žฌ์ •์˜) + +### B-17 ๐Ÿ”บ์žฌ์ •์˜ โ€” `_load_dotenv` ๋‹จ์ผ ํ›„๋ณด ํ•ด์„ (P1) + +> **C2 ํŒ์ •**: ์ฑŒ๋ฆฐ์ €์˜ ์šฐ์„ ์ˆœ์œ„ ๋ชฉ๋ก์€ ๋ฐฉํ–ฅ์ด ์˜ณ์œผ๋‚˜, **๊ทผ๋ณธ ๊ฒฐํ•จ์€ ์ˆœ์„œ๊ฐ€ ์•„๋‹ˆ๋ผ "๋ฃจํŠธ๋ฅผ ํ•˜๋‚˜๋งŒ ์ •ํ•˜๊ณ  ๋๋‚ธ๋‹ค"๋Š” ๊ตฌ์กฐ**์ž…๋‹ˆ๋‹ค(ยง1.10 H-4). ์ˆœ์„œ๋งŒ ๋ฐ”๊พธ๋ฉด `MAM_ENV_FILE` ์˜คํƒ€ ๊ฒฝ๋กœ์—์„œ ์—ฌ์ „ํžˆ ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค๋กœ ํด๋ฐฑํ•ฉ๋‹ˆ๋‹ค. + +**์ฒ˜๋ฐฉ โ€” ์ˆœ์„œ ์žˆ๋Š” ํ›„๋ณด ๋ชฉ๋ก + ์ฒซ ์ ์ค‘ ์šฐ์„ (first-hit-wins)**: +``` +1) $MAM_ENV_FILE (ํŒŒ์ผ์ด ์‹ค์ œ๋กœ ์กด์žฌํ•  ๋•Œ๋งŒ ์ฑ„ํƒ. ๋ถ€์žฌ ์‹œ return ํ•˜์ง€ ๋ง๊ณ  ๊ณ„์† ์ง„ํ–‰) โ† H-4 ๊ต์ • +2) $MAM_REAL_ROOT (run_loop.sh:104 ๊ฐ€ export) +3) $WORKSPACE_ROOT (run_loop.sh:105 ๊ฐ€ export) +4) walk_up(__file__) (์ €์žฅ์†Œ ์ง์ ‘ ์‹คํ–‰์—์„œ ์ด๋ฏธ ์ •์ƒ ๋™์ž‘ํ•จ์ด ์‹ค์ธก๋จ) +5) walk_up(os.getcwd()) (freeze ๊ฒฝ๋กœ๋ฅผ ์ˆ˜๋™ ์‹คํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๊ตฌ์ œ โ€” ์ฑŒ๋ฆฐ์ € ๊ทผ๊ฑฐ๊ฐ€ ์‹ค์ธก์œผ๋กœ ํƒ€๋‹น) +โ†’ ๊ฐ ํ›„๋ณด์—์„œ .mam.env / .env ๋ฅผ ์ฐพ๊ณ , ์ฒซ ์ ์ค‘์„ ์ฑ„ํƒ +โ†’ ์ „๋ถ€ ์‹คํŒจํ•˜๋ฉด ๋ฐ˜๋“œ์‹œ ๊ฒฝ๊ณ  ๋กœ๊ทธ: "no env file found; falling back to public default broker" +``` +**Rev.1 ๋Œ€๋น„ / ์ฑŒ๋ฆฐ์ € ์ œ์•ˆ ๋Œ€๋น„ ์ฐจ์ด 3๊ฐ€์ง€**: +1. `MAM_ENV_FILE` ๋ถ€์žฌ ์‹œ **์กฐ๊ธฐ ํƒˆ์ถœ ์ œ๊ฑฐ**(H-4). ์ฑŒ๋ฆฐ์ € ์ œ์•ˆ์œผ๋กœ๋Š” ๊ณ ์ณ์ง€์ง€ ์•Š๋Š” ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค. +2. `walk_up(__file__)`์„ cwd๋ณด๋‹ค **์•ž**์— ๋‘ก๋‹ˆ๋‹ค. ์ €์žฅ์†Œ ์ง์ ‘ ์‹คํ–‰์€ ์ด๋ฏธ ์ •ํ™•ํžˆ ๋™์ž‘ํ•จ์ด ์‹ค์ธก๋˜์—ˆ๊ณ , cwd๋ฅผ ์•ž์„ธ์šฐ๋ฉด **๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ์˜ `.mam.env`๋ฅผ ์ฝ๋Š” ๊ต์ฐจ ์˜ค์—ผ** ์œ„ํ—˜๋งŒ ์ปค์ง‘๋‹ˆ๋‹ค(MAM์€ ๋‹ค์ค‘ ํ”„๋กœ์ ํŠธ ์‚ฌ์šฉ์„ ์ „์ œ). +3. cwd๋Š” **`walk_up(os.getcwd())`** ๋กœ ๋‘ก๋‹ˆ๋‹ค. ์ €์žฅ์†Œ ํ•˜์œ„ ๋””๋ ‰ํ„ฐ๋ฆฌ์—์„œ ์‹คํ–‰ํ•ด๋„ ๋™์ž‘ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. +4. **์ตœ์ข… ํด๋ฐฑ ๊ฒฝ๊ณ ๋Š” ์ˆœ์„œ์™€ ๋ฌด๊ด€ํ•œ ์•ˆ์ „๋ง**์ด๋ฏ€๋กœ ํ•„์ˆ˜ ์š”๊ฑด์œผ๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์–ด๋–ค ์ˆœ์„œ๋“  ๋†“์น  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +### B-18 โ€” ์žก ๋ ˆ์ฝ”๋“œ ์ž๊ฒฉ์ฆ๋ช… ํ‰๋ฌธ ๋ณด๊ด€ (P2) + +`to_registry_block()`์—์„œ `password`๋ฅผ ๋งˆ์Šคํ‚นํ•˜๊ฑฐ๋‚˜ ๋ ˆ์ฝ”๋“œ ๋Œ€์‹  ์‹คํ–‰ ์‹œ์  env์—์„œ๋งŒ ํ•ด์„. `broker_config_from_job()`์˜ override ์šฐ์„ ์ˆœ์œ„ ๊ณ„์•ฝ(H-2์™€ ๋™์ผ ์ง€์ )๊ณผ ํ•จ๊ป˜ ์žฌ๊ฒ€ํ† . + +--- + +## 9. ๋ถ€๋ก X โ€” Option A (๊ณ„์ • ๊ฐ„ Export / Import): **์˜ˆ์™ธ ๊ฒฝ๋กœ** + +์ฑŒ๋ฆฐ์ €๊ฐ€ 1์ˆœ์œ„๋กœ ์ œ์•ˆํ•œ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. **๊ธฐ๋ณธ ์ฑ„ํƒํ•˜์ง€ ์•Š์œผ๋ฉฐ**, ๊ด€์ธก์ž๊ฐ€ ์‹ค์ œ๋กœ **๋‹ค๋ฅธ ์‹ ๋ขฐ ๋„๋ฉ”์ธ**์— ์†ํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + +**๋ฌธ๋ฒ• ๊ฒ€์ฆ ์™„๋ฃŒ** (์†Œ์Šค ๋Œ€์กฐ): +```conf +accounts { + MAM: { + jetstream: enabled + users: [ { user: mam_agent, password: $MAM_BROKER_PASS } ] + exports: [ { stream: "python.mqtt.jobs.>", accounts: [HOME] } ] # accounts ๋กœ ์ˆ˜์ž…์ž ์ œํ•œ ๊ถŒ์žฅ + } + HOME: { + users: [ { user: home, password: $HOME_BROKER_PASS } ] + imports: [ { stream: { account: MAM, subject: "python.mqtt.jobs.>" } } ] + } +} +``` +- `exports: [ { stream: "..." } ]` / `imports: [ { stream: { account: X, subject: "..." } } ]` ๋ฌธ๋ฒ•์€ ๊ณต์‹ ๋ฌธ์„œ์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. +- **`>` ์™€์ผ๋“œ์นด๋“œ๋Š” ์œ ํšจ**ํ•ฉ๋‹ˆ๋‹ค โ€” export subject๋Š” `IsValidSubject`๋กœ ๊ฒ€์ฆ๋˜๋ฉฐ(`opts.go:3517`) ์ด ํ•จ์ˆ˜๋Š” ๋งˆ์ง€๋ง‰ ํ† ํฐ์˜ `>`๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค(์™€์ผ๋“œ์นด๋“œ ๊ธˆ์ง€์šฉ `IsValidLiteralSubject`๋Š” ๋ณ„๋„ ํ•จ์ˆ˜์ด๋ฉฐ export์— ์“ฐ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค). +- ์ˆ˜์ž… ์ธก์€ `prefix:` / `to:`๊ฐ€ ์—†์œผ๋ฉด **๋™์ผ subject**๋กœ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค. + +**๊ธฐ๋ณธ ์ฑ„ํƒํ•˜์ง€ ์•Š๋Š” ๊ทผ๊ฑฐ 3๊ฐ€์ง€**: +1. **A-2 ๋ชฉ์ ๊ณผ ์ƒ์ถฉ** โ€” ๋ฒ”์šฉ ํ™ˆ๋žฉ ๊ณ„์ •์— **๋ชจ๋“  ์žก์˜ ํŽ˜์ด๋กœ๋“œ**(`detail` ๋ณธ๋ฌธ ํฌํ•จ)๋ฅผ ์—ด์–ด ์ค๋‹ˆ๋‹ค. ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ฒฉ๋ฆฌ๋ฅผ ๊ฐ•ํ™”ํ•˜๋ ค๋Š” Track 2์™€ ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์ž…๋‹ˆ๋‹ค. +2. **M3 ์กฐ์šฉํ•œ ํŒŒ์†** โ€” export subject๊ฐ€ ํ† ํ”ฝ ๋ฃจํŠธ๋ฅผ **๋‘ ๋ฒˆ์งธ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ**ํ•˜๋Š” ์ง€์ ์ด ๋ฉ๋‹ˆ๋‹ค. ์ง€๋ฌธ ํ† ํ”ฝ ์ „ํ™˜ ์‹œ ๋งค์นญ์ด ์กฐ์šฉํžˆ ๋Š๊น๋‹ˆ๋‹ค(โ†’ `G-D9`๊ฐ€ ๊ฐ•์ œ). +3. **N-1์„ ํ•ด๊ฒฐํ•˜์ง€ ๋ชปํ•จ** โ€” import๋Š” ๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆผ์ด๋ฉฐ **retained๋ฅผ ์˜ฎ๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** ์ฆ‰ Option A๋ฅผ ์จ๋„ ์‚ฌํ›„ ์ ‘์† ๋Œ€์‹œ๋ณด๋“œ๋Š” ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ณด์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. Option B์™€ ๋™์ผํ•œ ํ•œ๊ณ„์ž…๋‹ˆ๋‹ค. + +โ†’ ๊ฒฐ๋ก : ๋‹จ์ผ ์‚ฌ์šฉ์ž ํ™ˆ๋žฉ์—์„œ๋Š” **Option B(ยงA-1์˜ `mam_observer`)** ๊ฐ€ ์ €์žฅ์†Œ ยง5.5 ์ฒ˜๋ฐฉ๊ณผ ์ผ์น˜ํ•˜๊ณ  ๋…ธ์ถœ๋„ ์ตœ์†Œ์ž…๋‹ˆ๋‹ค. Option A๋Š” "๋‹ค๋ฅธ ์‚ฌ๋žŒ/๋‹ค๋ฅธ ์‹ ๋ขฐ ๋„๋ฉ”์ธ์ด ๊ด€์ธกํ•œ๋‹ค"๋Š” ์š”๊ตฌ๊ฐ€ ์‹ค์ œ๋กœ ์ƒ๊ฒผ์„ ๋•Œ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 10. ๋ฏธ๊ฒฐ ์งˆ๋ฌธ (์‚ฌ์šฉ์ž/Creator ํŒ๋‹จ ์‚ฌํ•ญ) + +1. **๋…ธ์ถœ ๋ชจ๋ธ**: ๋ชจ๋ธ T(Tailscale) ๊ถŒ๊ณ . ๋ณด์œ  ๋„๋ฉ”์ธ์ด ์žˆ๊ณ  ์™ธ๋ถ€ ํ˜‘์—…์ž๊ฐ€ ๋ถ™๋Š”๋‹ค๋ฉด ๋ชจ๋ธ P + ยงB-3. +2. **๊ด€์ธก ํด๋ผ์ด์–ธํŠธ์˜ ํ”„๋กœํ† ์ฝœ**: N-1 ๋•Œ๋ฌธ์— **์‚ฌํ›„ ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•˜๋ฉด MQTT๋กœ ๋ถ™๋Š” ๊ฒƒ์ด ์ •๋‹ต**์ž…๋‹ˆ๋‹ค. WebSocket/NATS ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ณ ์ง‘ํ•œ๋‹ค๋ฉด ยง5.3 JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ ์˜ตํŠธ์ธ์ด ํ•„์š”ํ•˜๋ฉฐ, ์ด๋Š” ์ถ”๊ฐ€ ๋””์Šคํฌ ๊ด€๋ฆฌ ๋ถ€๋‹ด์„ ๋™๋ฐ˜ํ•ฉ๋‹ˆ๋‹ค. ์–ด๋А ์ชฝ์„ ํƒํ• ์ง€ ๊ฒฐ์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. +3. **์„œ๋ฒ„์ธก ์ž์‚ฐ์˜ ์œ„์น˜**: `nats.conf`/`docker-compose.yml`์„ `deploy/nats/`๋กœ ์ปค๋ฐ‹ํ• ์ง€, ์„œ๋ฒ„ ๋กœ์ปฌ์—๋งŒ ๋‘˜์ง€. ์ปค๋ฐ‹ํ•˜๋ฉด G-D5~G-D7ยทG-D9๋ฅผ **์‹ค์ œ ํŒŒ์ผ์— ์ง์ ‘** ๊ฑธ ์ˆ˜ ์žˆ์–ด ๋ฌธ์„œ ๊ฐ€๋“œ๋ณด๋‹ค ๊ฐ•ํ•ด์ง‘๋‹ˆ๋‹ค. ์‹œํฌ๋ฆฟ์€ ์–ด๋А ์ชฝ์ด๋“  `.env`๋กœ ๋ถ„๋ฆฌ. +4. **`B-17` ์šฐ์„ ์ˆœ์œ„**: H-1ยทH-4๋Š” ์›๊ฒฉ ์ „ํ™˜์˜ ๋ณด์•ˆ ๋ชฉ์ ์„ ์ง์ ‘ ํ›ผ์†ํ•ฉ๋‹ˆ๋‹ค. Planner ๊ถŒ๊ณ ๋Š” **M2b ์ง„์ž… ์ „ ์ฒ˜๋ฆฌ**์ž…๋‹ˆ๋‹ค. +5. **`implementation_plan.md` ํŒŒ์ผ๋ช…**: ์ €์žฅ์†Œ์˜ ๋Œ€๋ฌธ์ž ๊ด€๋ก€์™€ ์–ด๊ธ‹๋‚ฉ๋‹ˆ๋‹ค(์ด์ „ ๋ฆฌ๋น„์ „์—์„œ๋„ ์ œ๊ธฐ). + +--- + +### ๋ถ€๋ก Y โ€” Rev.2์—์„œ ์ƒˆ๋กœ ์ˆ˜ํ–‰ํ•œ ์‹ค์ธก + +| ๋Œ€์ƒ | ํ™•์ธ ๊ฒฐ๊ณผ | +|---|---| +| retained ์ „๋‹ฌ ๊ฒฝ๋กœ | `mqttSendRetainedMsgsToNewSubs`๋Š” `mqttPacketSub` ์ฒ˜๋ฆฌ์—์„œ๋งŒ ํ˜ธ์ถœ, `sub.mqtt.prm` ์ˆœํšŒ โ†’ **MQTT ๊ตฌ๋…์ž ์ „์šฉ** | +| NATS ๋ณ€์ˆ˜ ํ•ด์„ ๋ฒ”์œ„ | `isVariable()`์€ **๋น„์ธ์šฉ ๋ฌธ์ž์—ด**์—์„œ๋งŒ ๋„๋‹ฌ. `lexQuotedString`์€ *"will not interpret any internal contents"* | +| export subject ์™€์ผ๋“œ์นด๋“œ | `IsValidSubject`๊ฐ€ ๋งˆ์ง€๋ง‰ ํ† ํฐ `>`๋ฅผ ํ—ˆ์šฉ(`isValidSubject`์˜ `fwc` ๋ถ„๊ธฐ). export๋Š” `IsValidLiteralSubject`๋ฅผ ์“ฐ์ง€ ์•Š์Œ | +| export/import ๋ฌธ๋ฒ• | `exports: [{stream: "..."}]`, `imports: [{stream: {account: X, subject: "..."}}]`, `prefix`/`to` ์ง€์› | +| ๊ณ„์ • ๊ฒฉ๋ฆฌ | *"Accounts create isolated tenant subject spaces"*, `jetstream: enabled`๋Š” ๊ณ„์ • ๋‹จ์œ„ | +| `MAM_ENV_FILE` ์˜คํƒ€ ๊ฒฝ๋กœ | **`broker.hivemq.com tls=False`** (๋‹ค๋ฅธ ํ›„๋ณด ๋ฏธ์‹œ๋„) | +| ๋Œ€์กฐ๊ตฐ(์ •์ƒ ๊ฒฝ๋กœ) | `nats.private.internal tls=True` | +| freeze ์Šคํฌ๋ฆฝํŠธ + cwd=์ €์žฅ์†Œ | **`broker.hivemq.com tls=False`** (์ฑŒ๋ฆฐ์ €์˜ cwd ๋„์ž… ๊ทผ๊ฑฐ ์„ฑ๋ฆฝ) | +| `PRIVATE_SERVER.md:230` | *"MAM๊ณผ ์ด๋ฅผ ๊ด€์ธกํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋™์ผํ•œ ๊ณ„์ •(`MAM`)์— ๋ฐฐ์น˜"* โ€” Option B๋Š” ๊ธฐ์กด ์ฒ˜๋ฐฉ | diff --git a/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-e1c4e9c3.md b/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-e1c4e9c3.md new file mode 100644 index 0000000..c6d7302 --- /dev/null +++ b/.agents/reports/canary-projects-multi-agent-mux-creator-cline/report-e1c4e9c3.md @@ -0,0 +1,161 @@ +# Cross-Code Review Report - Job e1c4e9c3 + +**Review Target**: Remote Docker deployment plan for `nats-server` (Track 1R) +**Scope**: `PRIVATE_SERVER.md`, `implementation_plan.md`, `.agents/skills/lib.sh`, `.mam.env.example`, `tests/test_deploy_freshness.py` +**Reviewer**: cline (herdr:canary-projects-multi-agent-mux-creator-cline) +**Date**: 2026-08-22 +**Commit base**: c6b6c77 (HEAD) + +--- + +## 1. Executive Summary + +The changeset establishes a comprehensive remote `nats-server` Docker production deployment plan (Track 1R / M2b) across 5 files (+413 / -66 lines). It delivers all four task deliverables: production Docker Compose & nats.conf, networking/security guide, client config & verification playbooks, and a phased rollout roadmap. Seven new regression guards (D-15~D-21) lock the documentation invariants. + +**Test results**: 297 tests collected (290 -> 297); deploy_freshness 20/20 pass; tier1+o2 67/67 pass; sanity 2/2 pass. No regressions detected in the fast subset. + +**Verdict**: PASS. One Medium documentation inconsistency (section 9.2 production compose omits NATS 4222 while section 9.3 UFW and R-3 reference it) and several Low/Very Low findings - none require re-planning. + +--- + +## 2. Changed Files Overview + +| File | Delta | Purpose | +|---|---|---| +| `.agents/skills/lib.sh` | +8 / -5 | Claude startup dialog handling robustness in `wait_for_tui_ready` / `handle_startup_dialogs` | +| `.mam.env.example` | +4 | Document `MQTT_KEEPALIVE` env var | +| `PRIVATE_SERVER.md` | +239 / -36 | D-1~D-5 corrections, N-1 boundary, section 9 remote production guide, Appendix X | +| `implementation_plan.md` | +44 / -20 | Split M2 -> M2a/M2b, add Track 1R roadmap section 5, renumber sections | +| `tests/test_deploy_freshness.py` | +112 / -11 | D-11 cleanup (remove dead `recognized` set, add `MQTT_BIND`); add D-15~D-21 guards | + +--- + +## 3. Detailed Review by File + +### 3.1 `.agents/skills/lib.sh` + +**Changes**: +1. `wait_for_tui_ready()` (line 1519): adds `handle_startup_dialogs "$sess" 1 || true` inside the 30-iteration loop for `$agent = "claude"` only. +2. `handle_startup_dialogs()` (line 1725): broadens trust-dialog regex to `'Do you trust the files|Yes, I trust this folder|Quick safety check'`. +3. (lines 1733-1735): adds a new `'Press Enter to continue'` branch; adds `${_MAM_READY_TOKENS_CLAUDE:-Anthropic|Assistant|Chat|Welcome}` fallback default. +4. (lines 1738-1739): reduces sleep from 2s->1s and `waited` increment from 2->1. + +**Verification**: +- `bash -n` syntax check: PASS +- `_MAM_READY_TOKENS_CLAUDE` is defined at line 63 -> the `:-` fallback is defensive but harmless (consistent with prior N5 observation). +- The regex broadening correctly handles newer Claude dialog variants ("Quick safety check" appeared in recent Claude Code versions). + +**Findings**: + +**L-1 (Low) - Latency overhead in `wait_for_tui_ready`**: The new `handle_startup_dialogs "$sess" 1` call adds ~1s (one loop iteration with `sleep 1`) per `wait_for_tui_ready` iteration even when no dialog is present. Combined with the existing `sleep 1`, each of the 30 iterations now takes ~2s (max ~60s vs previous ~30s). Acceptable for TUI readiness but doubles worst-case latency. Not a blocker - the function returns early when ready tokens appear. + +**L-2 (Low) - `handle_startup_dialogs` default timeout halved**: Changing `sleep 2; waited+=2` -> `sleep 1; waited+=1` halves the default timeout from ~40s to ~20s. When called with the default `timeout=20`, the function now runs at most ~20s instead of ~40s. This is reasonable for Claude dialogs (which appear within seconds) but reduces the safety margin for slow environments. The `wait_for_tui_ready` call uses `timeout=1` (1s), so it is unaffected by this change. + +### 3.2 `.mam.env.example` + +Adds `MQTT_KEEPALIVE=60` with a descriptive comment. Verified `mqtt_common.py:234` reads it via `_env_int("MQTT_KEEPALIVE", 60)` and the dataclass default is `keepalive: int = 60` (line 179). Consistent. PASS + +### 3.3 `PRIVATE_SERVER.md` + +**D-1 store_dir correction**: Changed from literal `"~/.local/share/nats/data"` (which does not expand in nats.conf) to `"/data"` (Docker) and `"$HOME/..."` (native, via unquoted `< `nats:2.12-alpine`. The comment correctly notes `latest` is scratch-based (no `wget` for healthcheck). Alpine includes busybox `wget`. Verified by D-16 guard. + +**D-3 port binding**: All ports now bind to `127.0.0.1` or `${*_BIND:-127.0.0.1}`. Port 8222 (unauthenticated monitoring) is hardcoded to `127.0.0.1`. Verified by D-17 guard. + +**D-4 TLS examples**: TLS blocks use DNS domain names (`mam-broker.example.com`), not IP literals. Verified by D-18 guard. + +**N-1 retained boundary**: Section 5.2 now explicitly documents that NATS/WebSocket subscribers joining after job termination will not receive retained MQTT terminal events, with two remediation paths (MQTT reconnect or JetStream opt-in). + +**Section 9 Remote production guide**: Well-structured with: +- 9.1: Production nats.conf with multi-tenant accounts, `mam_observer` read-only user, JetStream, `ack_wait: 60s` for WAN, `max_ack_pending: 1024`. +- 9.2: Production compose with fail-closed env (`${VAR:?set in .env}`), healthcheck, log rotation. +- 9.3: Tailscale vs TLS comparison table, UFW rules, secret generation. +- 9.4: R-1~R-10 verification playbook + WAN latency probe. +- 9.5: 5-step cutover procedure. +- Appendix X: Account export/import for cross-trust-domain scenarios. + +**Findings**: + +**M-1 (Medium) - Section 9.2 production compose omits NATS 4222 port**: The section 9.2 `docker-compose.yml` (lines 405-408) publishes only ports 1883, 8222, 8080 - **missing `${NATS_BIND:-127.0.0.1}:4222:4222`**. This contradicts: +- The task brief which explicitly requires "NATS 4222" in the production compose. +- Section 9.3 UFW rule `sudo ufw allow in on tailscale0 to any port 4222 proto tcp` (line 447) - a dead rule since the container does not publish 4222 to the host. +- R-3 verification playbook (line 473) which nmap-tests 4222. + +The section 4.1 *dev* compose (lines 101, 122) correctly includes 4222. The section 9.1 nats.conf enables NATS default port 4222 inside the container (nats-server listens on 4222 by default), but without the compose port mapping it is unreachable from the tailnet. For MAM-only deployments (MQTT 1883 only), 4222 is optional - but the UFW rule and R-3 test should then be updated to match, or the port should be added to section 9.2. **Fix**: Add `- "${NATS_BIND:-127.0.0.1}:4222:4222"` to section 9.2 ports, OR remove 4222 from section 9.3 UFW and R-3. + +**V-1 (Very Low) - Misleading `store_dir` comment (line 73)**: `store_dir: "/data"` is annotated `# Docker ... (native execution $HOME expansion)` - but `/data` is a fixed absolute path that does NOT expand to `$HOME`. Native execution uses a separate config block (line 145, `"$HOME/.local/share/nats/data"`). The parenthetical comment is slightly misleading; a reader might expect `/data` to auto-expand. Cosmetic only. + +### 3.4 `implementation_plan.md` + +Splits M2 -> M2a (local spike) + M2b (remote production), adds Track 1R roadmap (new section 5), renumbers sections 5->6, 6->7, and removes the old section 7 dependency graph (content folded into the milestone flow diagram at line 33). The M2b gate condition correctly cites R-3/R-5/R-6/R-9 as the final gates. + +**Findings**: + +**V-2 (Very Low) - Unchecked guard implementation checkbox**: The M2b checklist item `- [ ] new guards G-D5 ~ G-D9, G-R1, G-R2 implementation and verification (290 -> 297)` is marked `[ ]` (incomplete), but the guards (D-15~D-21) are implemented in `test_deploy_freshness.py` and verified passing (297 collected, 7 new pass). This is a tracking discrepancy - the work is done but the checkbox is not toggled. Recommend `- [x]`. + +### 3.5 `tests/test_deploy_freshness.py` + +**D-11 cleanup**: Removed the unused `recognized` set (which contained `MAM_MQTT_HOST` for exclusion-checking that was never exercised) and added `MQTT_BIND` to `valid_mqtt_vars`. Verified `MAM_MQTT_HOST` appears nowhere in the codebase. The test only checks `MQTT_*`-prefixed vars (regex `\b(MQTT_[A-Z0-9_]+)\b`), so `NATS_BIND`/`WS_BIND` are correctly excluded from validation. PASS + +**D-15~D-21 new guards**: All 7 guards pass. Verified: +- D-15: store_dir absolute path + unquoted heredoc PASS +- D-16: nats image alpine-pinned (no `latest`) PASS +- D-17: port 8222 bound to 127.0.0.1 PASS +- D-18: TLS blocks use DNS names, not IP literals PASS +- D-19: subject literals match `DEFAULT_TOPIC_ROOT` (`python.mqtt.jobs`) PASS +- D-20: `run_loop.sh` exports `MAM_ENV_FILE` (verified line 106) PASS +- D-21: `.mam.env.example` documents `MQTT_KEEPALIVE`; no uncommented `MQTT_RETRY_INTERVAL`/`MQTT_MAX_RETRIES` PASS + +**Finding**: + +**L-3 (Low) - D-19 regex is brittle**: `re.findall(r'["\'](python\.mqtt\.jobs\.[>*\w.]+)["\']', content)` scans the entire markdown (not just code blocks) and matches subject literals in quoted strings. If a future prose sentence contains a quoted subject like `"python.mqtt.jobs.test"` without a wildcard, it would be validated. Currently passes but the scope is broader than "config examples". Non-blocking. + +--- + +## 4. Task Deliverable Coverage + +| Requirement | Status | Location | +|---|---|---| +| Production Docker Compose (MQTT 1883, NATS 4222, WS 8080, HTTP 8222, JetStream volume, healthchecks) | Partial | section 9.2 compose has 1883/8222/8080 + healthcheck + nats-data volume; **missing 4222** (M-1) | +| Production nats.conf (ports, JetStream, healthcheck endpoint) | PASS | section 9.1 nats.conf | +| Remote networking & security (UFW, TLS/Certbot vs Tailscale, user auth) | PASS | section 9.3 comparison table + UFW rules + section 9.1 accounts/permissions | +| Client configuration (.mam.env) | PASS | section 6 `.mam.env` template + `.mam.env.example` MQTT_KEEPALIVE | +| Remote verification playbooks (ping, latency, pub/sub) | PASS | section 9.4 R-1~R-10 + WAN latency probe | +| Phased rollout roadmap (M2 local spike + remote switchover) | PASS | implementation_plan.md M2a/M2b + section 5 Track 1R roadmap | + +--- + +## 5. Test Validation Summary + +| Suite | Tests | Result | +|---|---|---| +| `tests/test_deploy_freshness.py` (full) | 20 | PASS 20 passed (13.55s) | +| D-15~D-21 (new guards) | 7 | PASS 7 passed (0.02s) | +| `tests/test_tier1_unit.py` + `test_o2_race_free_lock.py` | 67 | PASS 67 passed (19.08s) | +| `tests/test_sanity.py` | 2 | PASS 2 passed (9.73s) | +| `--collect-only` (full suite) | 297 | PASS 297 collected | +| `bash -n .agents/skills/lib.sh` | - | PASS syntax OK | + +Full suite (297 tests) not executed end-to-end due to 30s tool timeout; tier2/3/4 tests require a live broker. The fast subset (89 tests across deploy, tier1, o2, sanity) passes cleanly with no regressions. + +--- + +## 6. Findings Summary + +| ID | Severity | File | Description | Fix | +|---|---|---|---|---| +| **M-1** | Medium | PRIVATE_SERVER.md section 9.2 | Production compose omits NATS 4222; contradicts section 9.3 UFW rule and R-3 test | Add `4222:4222` port mapping to section 9.2, or remove 4222 from section 9.3/R-3 | +| L-1 | Low | lib.sh:1519 | `handle_startup_dialogs` call adds ~1s/iteration to `wait_for_tui_ready` | Acceptable; consider gating on `_pane_dialog_open` first | +| L-2 | Low | lib.sh:1738 | Default timeout halved (40s->20s) via sleep 2->1 | Acceptable for Claude; verify slow-env tolerance | +| L-3 | Low | test_deploy_freshness.py D-19 | Regex scans full markdown, not just code blocks | Narrow to code_blocks scope if desired | +| V-1 | Very Low | PRIVATE_SERVER.md:73 | Misleading store_dir comment ("$HOME expansion") | Clarify comment | +| V-2 | Very Low | implementation_plan.md | Guard checkbox unchecked despite work done | Toggle to `[x]` | + +--- + +## 7. Recommendation + +The changeset is production-ready for the M2b documentation milestone. The only Medium finding (M-1: section 9.2 missing 4222) is a documentation inconsistency resolvable by a one-line compose edit or removing the corresponding UFW/R-3 reference - no re-planning required. All 7 new guards pass; no test regressions; bash syntax valid; codebase cross-references (`mqtt_common.py` DEFAULT_TOPIC_ROOT, MQTT_KEEPALIVE; `run_loop.sh` MAM_ENV_FILE) all verified. + +[VERDICT: PASS] diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index dd316dd..c210053 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -1516,6 +1516,9 @@ wait_for_tui_ready() { fi local i for i in {1..30}; do + if [ "$agent" = "claude" ]; then + handle_startup_dialogs "$sess" 1 || true + fi if _pane_dialog_open "$sess"; then if printf '%s\n' "$(_pane_tail "$sess" 5)" | grep -q 'Press Enter to continue'; then _sks_herdr send-keys -t "$sess" Enter || true @@ -1719,7 +1722,7 @@ handle_startup_dialogs() { local sess="$1" timeout="${2:-20}" waited=0 pane while [ "$waited" -lt "$timeout" ]; do pane=$(_pane_tail "$sess" 20) - if printf '%s\n' "$pane" | grep -q 'Do you trust the files'; then + if printf '%s\n' "$pane" | grep -Eq 'Do you trust the files|Yes, I trust this folder|Quick safety check'; then _sks_herdr send-keys -t "$sess" Enter elif printf '%s\n' "$pane" | grep -q 'Yes, proceed'; then _sks_herdr send-keys -t "$sess" Down @@ -1727,11 +1730,13 @@ handle_startup_dialogs() { _sks_herdr send-keys -t "$sess" Enter elif printf '%s\n' "$pane" | grep -q 'Resuming the full session'; then _sks_herdr send-keys -t "$sess" Enter - elif printf '%s\n' "$pane" | grep -Eq "$_MAM_READY_TOKENS_CLAUDE"; then + elif printf '%s\n' "$pane" | grep -q 'Press Enter to continue'; then + _sks_herdr send-keys -t "$sess" Enter + elif printf '%s\n' "$pane" | grep -Eq "${_MAM_READY_TOKENS_CLAUDE:-Anthropic|Assistant|Chat|Welcome}"; then return 0 fi - sleep 2 - waited=$((waited + 2)) + sleep 1 + waited=$((waited + 1)) done return 0 } diff --git a/.mam.env.example b/.mam.env.example index 543ccdb..8463b1c 100644 --- a/.mam.env.example +++ b/.mam.env.example @@ -80,6 +80,10 @@ #default: hermes # MQTT_CLIENT_ID_PREFIX=hermes +# MQTT keepalive interval (seconds). Used by paho-mqtt client connections. +#default: 60 +# MQTT_KEEPALIVE=60 + # Log level for MAM runtime components (DEBUG, INFO, WARN, ERROR). #default: INFO # MAM_LOG_LEVEL=INFO diff --git a/PRIVATE_SERVER.md b/PRIVATE_SERVER.md index 56e7fa8..650a151 100644 --- a/PRIVATE_SERVER.md +++ b/PRIVATE_SERVER.md @@ -70,7 +70,7 @@ server_name: mam-hub # JetStream ์˜์† ์Šคํ† ๋ฆฌ์ง€ (MQTT QoS 1 ๋ฐ Retained ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ์— ํ•„์ˆ˜) jetstream { - store_dir: "~/.local/share/nats/data" # Docker ํ™˜๊ฒฝ์—์„œ๋Š” "/data"๋กœ ๋งคํ•‘ + store_dir: "/data" # Docker ํ™˜๊ฒฝ ๊ธฐ๋ณธ ์Šคํ† ๋ฆฌ์ง€ (๋„ค์ดํ‹ฐ๋ธŒ ์‹คํ–‰ ์‹œ $HOME ์ „๊ฐœ) max_file: 10G # ํ™ˆ๋žฉ ๋””์Šคํฌ ์ƒํ•œ ์„ค์ • } @@ -93,17 +93,17 @@ websocket { **๋‹จ์ผ Docker ์‹คํ–‰:** ```bash -# ํ˜ธ์ŠคํŠธ์— nats.conf ์ƒ์„ฑ ํ›„ ์‹คํ–‰ +# ํ˜ธ์ŠคํŠธ์— nats.conf ์ƒ์„ฑ ํ›„ ์‹คํ–‰ (D-2: alpine ๊ณ ์ • ํ•€, D-3: ๋ฃจํ”„๋ฐฑ/๋ฐ”์ธ๋“œ ํ•œ์ •) docker run -d \ --name mam-nats \ --restart unless-stopped \ - -p 1883:1883 \ - -p 4222:4222 \ - -p 8222:8222 \ - -p 8080:8080 \ + -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:latest \ + nats:2.12-alpine \ -c /etc/nats/nats.conf ``` @@ -113,15 +113,15 @@ version: '3.8' services: nats: - image: nats:latest + image: nats:2.12-alpine container_name: mam-nats restart: unless-stopped command: ["-c", "/etc/nats/nats.conf"] ports: - - "1883:1883" # MQTT 3.1.1 ํฌํŠธ (ํ‰๋ฉด A: MAM) - - "4222:4222" # NATS ๊ธฐ๋ณธ ํฌํŠธ (ํ‰๋ฉด B) - - "8222:8222" # HTTP ๋ชจ๋‹ˆํ„ฐ๋ง (/varz, /jsz) - - "8080:8080" # WebSocket (ํ‰๋ฉด B) + - "${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 @@ -138,11 +138,11 @@ macOS์˜ sealed APFS ๋ฃจํŠธ ๋ณผ๋ฅจ(`/data`) ๊ถŒํ•œ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด # ์„ค์ • ๋ฐ ๋ฐ์ดํ„ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ ์ƒ์„ฑ (sudo ๋ถˆํ•„์š”) mkdir -p ~/.config/nats ~/.local/share/nats/data -# ์„ค์ • ํŒŒ์ผ ์ž‘์„ฑ -cat <<'EOF' > ~/.config/nats/nats.conf +# ์„ค์ • ํŒŒ์ผ ์ž‘์„ฑ (D-1: ๋น„์ธ์šฉ heredoc < ~/.config/nats/nats.conf server_name: mam-hub jetstream { - store_dir: "~/.local/share/nats/data" + store_dir: "$HOME/.local/share/nats/data" max_file: 10G } http_port: 8222 @@ -215,6 +215,8 @@ persistence_location /mosquitto/data/ - `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)**: ๊ต์ฐจ ํ”„๋กœํ† ์ฝœ ๋ธŒ๋ฆฌ์ง•์€ **๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ์— ํ•œ์ •**๋ฉ๋‹ˆ๋‹ค. MQTT์˜ retained ๋ฉ”์‹œ์ง€๋Š” MQTT ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ ์ „๋‹ฌ๋˜๋ฏ€๋กœ(`mqttSendRetainedMsgsToNewSubs`๊ฐ€ MQTT SUBSCRIBE ๊ฒฝ๋กœ ์ „์šฉ), ์žก์ด ๋๋‚œ ๋’ค ์ ‘์†ํ•œ NATS/WebSocket ๋Œ€์‹œ๋ณด๋“œ๋Š” ๊ทธ ์žก์˜ **์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค**. ์‚ฌํ›„ ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•˜๋ฉด (a) ๋Œ€์‹œ๋ณด๋“œ๋ฅผ MQTT(1883)๋กœ ์—ฐ๊ฒฐํ•˜๊ฑฐ๋‚˜ (b) ยง5.3 JetStream ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ์„ ์˜ตํŠธ์ธํ•˜์‹ญ์‹œ์˜ค. +- **๊ณ„์ • ๋ฐฐ์น˜**: ๊ด€์ธก ํด๋ผ์ด์–ธํŠธ๋Š” ยง5.5 ์ฒ˜๋ฐฉ๋Œ€๋กœ `MAM` ๊ณ„์ • ์•ˆ์˜ ์ฝ๊ธฐ ์ „์šฉ ์‚ฌ์šฉ์ž(`mam_observer`)๋กœ ์ ‘์†ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ณ„์ •์—์„œ๋Š” subject๊ฐ€ ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. - **์ฃผ์˜ ์‚ฌํ•ญ**: ํ† ํ”ฝ ๋ ˆ๋ฒจ ๋‚ด์— ๋งˆ์นจํ‘œ(`.`)๊ฐ€ ํฌํ•จ๋˜๋ฉด NATS ๊ณ„์ธต์—์„œ ํ† ํฐ์ด ๋ถ„๋ฆฌ๋  ์ˆ˜ ์žˆ์œผ๋‚˜, MAM์˜ `job_id`๋Š” 8์ž๋ฆฌ hex, ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ง€๋ฌธ์€ 12์ž๋ฆฌ hex์ด๋ฏ€๋กœ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ### 5.3 JetStream ์ด๋ฒคํŠธ ๋ฆฌํ”Œ๋ ˆ์ด (Event Replay) @@ -227,7 +229,7 @@ persistence_location /mosquitto/data/ ### 5.5 ๋ฉ€ํ‹ฐํ…Œ๋„ŒํŠธ ๊ณ„์ • ๋ถ„๋ฆฌ ๋ฐ ๋ณด์•ˆ - ๋‹จ์ผ ์„œ๋ฒ„ ๋‚ด์—์„œ `MAM` ์ „์šฉ ๊ณ„์ •๊ณผ `HOME` ๊ฐœ์ธ ๊ณ„์ •์„ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ฆฌ์†Œ์Šค ์ฟผํ„ฐ์™€ ๊ถŒํ•œ์„ ์™„๋ฒฝํžˆ ๊ฒฉ๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -- **๊ถŒ๊ณ  ๋ฐฐ์น˜**: MAM๊ณผ ์ด๋ฅผ ๊ด€์ธกํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋™์ผํ•œ ๊ณ„์ •(`MAM`)์— ๋ฐฐ์น˜ํ•˜๊ณ , ๋ฌด๊ด€ํ•œ ํ™ˆ๋žฉ ์„œ๋น„์Šค๋Š” ๋ณ„๋„ ๊ณ„์ •(`HOME`)์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค. +- **๊ถŒ๊ณ  ๋ฐฐ์น˜**: MAM๊ณผ ์ด๋ฅผ ๊ด€์ธกํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋™์ผํ•œ ๊ณ„์ •(`MAM`)์— ๋ฐฐ์น˜ํ•˜๊ณ (๊ด€์ธก์ž๋Š” ์ฝ๊ธฐ ์ „์šฉ `mam_observer` ์—ญํ•  ๋ถ€์—ฌ), ๋ฌด๊ด€ํ•œ ํ™ˆ๋žฉ ์„œ๋น„์Šค๋Š” ๋ณ„๋„ ๊ณ„์ •(`HOME`)์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค. --- @@ -243,23 +245,21 @@ persistence_location /mosquitto/data/ # MAM Private MQTT Broker Configuration (.mam.env) # ============================================================================== -# ๊ฐœ์ธ ์„œ๋ฒ„ IP ๋˜๋Š” ๋„๋ฉ”์ธ -MQTT_BROKER="192.168.1.100" # ์˜ˆ: 10.0.0.5, mqtt.my-domain.com ๋“ฑ - -# MQTT ๊ธฐ๋ณธ ํฌํŠธ (ํ‰๋ฌธ TCP: 1883, TLS ์•”ํ˜ธํ™”: 8883) +# โ”€โ”€ ๋ชจ๋ธ T (Tailscale ์‚ฌ์„ค ์˜ค๋ฒ„๋ ˆ์ด๋ง ๊ถŒ์žฅ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +MQTT_BROKER="mam-hub.tailXXXX.ts.net" # ๋˜๋Š” 100.x.y.z (ํ‰๋ฌธ์ด๋ฏ€๋กœ IP ์‚ฌ์šฉ ๊ฐ€๋Šฅ) MQTT_PORT=1883 - -# TLS ์•”ํ˜ธํ™” ํ™œ์„ฑํ™” ์—ฌ๋ถ€ (0: ํ‰๋ฌธ TCP, 1: TLS ์•”ํ˜ธํ™”) MQTT_TLS=0 +MQTT_USERNAME=mam_agent +MQTT_PASSWORD=replace_me_with_token +MQTT_KEEPALIVE=60 # WAN ๊ตฌ๊ฐ„ ๊ถŒ์žฅ ์—ฐ๊ฒฐ ์œ ์ง€ ์‹œ๊ฐ„ -# ์ธ์ฆ ์„ค์ • (์ต๋ช… ๋ธŒ๋กœ์ปค๋Š” ์ฃผ์„ ์ฒ˜๋ฆฌ ๋˜๋Š” ๋นˆ ๋ฌธ์ž์—ด ์œ ์ง€) -# MQTT_USERNAME=my_agent_user -# MQTT_PASSWORD=my_secure_password - -# TLS ์ธ์ฆ์„œ ๊ฒฝ๋กœ (MQTT_TLS=1 ์„ค์ • ์‹œ ์‚ฌ์šฉ) -# MQTT_CA_CERTS=/path/to/ca.crt -# MQTT_CERTFILE=/path/to/client.crt -# MQTT_KEYFILE=/path/to/client.key +# โ”€โ”€ ๋ชจ๋ธ 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` ํŒŒ์ผ ์„ค์ •๋ณด๋‹ค ์šฐ์„ ํ•ฉ๋‹ˆ๋‹ค.* @@ -273,10 +273,10 @@ MQTT_TLS=0 ### Step 1. ๋ธŒ๋กœ์ปค ๋ฆฌ์Šค๋„ˆ ๋ฐ JetStream ์ƒํƒœ ํ™•์ธ ```bash # MQTT ๋ฆฌ์Šค๋„ˆ ํ™œ์„ฑํ™” ํ™•์ธ -curl -s http://192.168.1.100:8222/varz | grep -i mqtt +curl -s http://127.0.0.1:8222/varz | grep -i mqtt # JetStream ์—”์ง„ ์ •์ƒ ๊ตฌ๋™ ํ™•์ธ -curl -s http://192.168.1.100:8222/jsz +curl -s http://127.0.0.1:8222/jsz ``` ### Step 2. ์ž„์‹œ ์žก ๋“ฑ๋ก ๋ฐ ์—ฐ๊ฒฐ ๊ฒ€์ฆ ์ด๋ฒคํŠธ ๋ฐœํ–‰ @@ -325,3 +325,213 @@ Step 2์˜ `-v` ์ถœ๋ ฅ ๋กœ๊ทธ ๋˜๋Š” `.mam/delegate_job_logs/$JID/events.ndjson` 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๊ณผ ์—ฐ๋™ํ•˜๋Š” ํ‘œ์ค€ ์ ˆ์ฐจ์ž…๋‹ˆ๋‹ค. + +### 9.1 ํ”„๋กœ๋•์…˜ `nats.conf` + +```conf +# nats.conf โ€” ์›๊ฒฉ ํ”„๋กœ๋•์…˜ (์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ์ ˆ๋Œ€๊ฒฝ๋กœ ๊ธฐ์ค€) +server_name: mam-hub + +# โ”€โ”€ JetStream: MQTT retained/QoS1 ์ €์žฅ์†Œ. MAM ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์žฌ์ˆ˜์‹ ์ด ์—ฌ๊ธฐ์— ์˜์กด โ”€โ”€ +jetstream { + store_dir: "/data" # D-1: ์ ˆ๋Œ€๊ฒฝ๋กœ ๊ณ ์ •. '~' ๋„, ์ธ์šฉ๋œ "$HOME" ๋„ ํ™•์žฅ๋˜์ง€ ์•Š์Œ + max_file: 10G + max_mem: 256M +} + +http_port: 8222 # D-3: ํ˜ธ์ŠคํŠธ ๊ฒŒ์‹œ๋Š” loopback ํ•œ์ • (ยง9.3) + +mqtt { + port: 1883 # ๊ณต๊ฐœ TLS ๋ชจ๋ธ์—์„œ๋Š” 8883 + tls ๋ธ”๋ก (ยง9.3) + ack_wait: 60s # WAN RTT ํก์ˆ˜ (๊ธฐ๋ณธ 30s) + max_ack_pending: 1024 # ๊ธฐ๋ณธ 100 โ†’ ๋‹ค์ค‘ ์—์ด์ „ํŠธ ๋™์‹œ ๋ฐœํ–‰ ์—ฌ์œ  +} + +websocket { + port: 8080 + no_tls: true # ์‚ฌ์„ค๋ง/tailnet ํ•œ์ • +} + +# โ”€โ”€ ์ธ์ฆ ๋ฐ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ (Rev.2: C1/C2 ๋ฐ˜์˜) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +accounts { + MAM: { + jetstream: enabled # MQTT ๋‚ด๋ถ€ ์ŠคํŠธ๋ฆผ์ด ์ด ๊ณ„์ • ์•ˆ์— ์ƒ์„ฑ๋จ + users: [ + # ๋ฐœํ–‰์ž ๊ฒธ ๊ตฌ๋…์ž โ€” MAM ์—์ด์ „ํŠธ ๋ณธ์ฒด + { user: mam_agent, password: $MAM_BROKER_PASS } + + # ๊ด€์ธก์ž โ€” ๋Œ€์‹œ๋ณด๋“œ/๋ชจ๋‹ˆํ„ฐ๋ง. PRIVATE_SERVER.md ยง5.5 ์˜ '๋™์ผ ๊ณ„์ • ๋ฐฐ์น˜' ์ฒ˜๋ฐฉ + { 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-compose.yml` + +```yaml +version: '3.8' + +services: + nats: + image: nats:2.12-alpine # D-2: latest(=scratch)๋Š” healthcheck ๋ถˆ๊ฐ€ + container_name: mam-nats + restart: unless-stopped + command: ["-c", "/etc/nats/nats.conf"] + environment: + # ๋ฏธํ•ด๊ฒฐ $VAR ๋Š” ํŒŒ์‹ฑ ์—๋Ÿฌ โ†’ ์‹œํฌ๋ฆฟ ๋ˆ„๋ฝ ์‹œ '๊ธฐ๋™ ์‹คํŒจ'๋กœ fail-closed + MAM_BROKER_PASS: ${MAM_BROKER_PASS:?set in .env} + MAM_OBSERVER_PASS: ${MAM_OBSERVER_PASS:?set in .env} + HOME_BROKER_PASS: ${HOME_BROKER_PASS:?set in .env} + SYS_BROKER_PASS: ${SYS_BROKER_PASS:?set in .env} + ports: + - "${MQTT_BIND:-127.0.0.1}:1883:1883" + - "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS ๋„ค์ดํ‹ฐ๋ธŒ ํ”„๋กœํ† ์ฝœ (Plane B) + - "127.0.0.1:8222:8222" # D-3: ๋ฌด์ธ์ฆ ๋ชจ๋‹ˆํ„ฐ๋ง์€ loopback ํ•œ์ • + - "${WS_BIND:-127.0.0.1}:8080:8080" + volumes: + - ./nats.conf:/etc/nats/nats.conf:ro + - nats-data:/data + 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 +``` + +**์‹œํฌ๋ฆฟ ์ƒ์„ฑ ๋ฐ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ ์ฃผ์ž… (`.env`):** +```bash +# ์„œ๋ฒ„ ์ธก compose ์˜† .env +cat < .env +MQTT_BIND=$(tailscale ip -4) +WS_BIND=$(tailscale ip -4) +MAM_BROKER_PASS=$(openssl rand -base64 32) +MAM_OBSERVER_PASS=$(openssl rand -base64 32) +HOME_BROKER_PASS=$(openssl rand -base64 32) +SYS_BROKER_PASS=$(openssl rand -base64 32) +EOF +chmod 600 .env +``` + +### 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/implementation_plan.md b/implementation_plan.md index 7070e92..ce8f288 100644 --- a/implementation_plan.md +++ b/implementation_plan.md @@ -20,6 +20,7 @@ |---|---|---|---| | **Track 0** | `B-14`, `B-15` (P1) | ๋ธŒ๋กœ์ปค ๋‹ค์šด ์‹œ 65๋ถ„ ์ •์ง€(Hang) ๋ฐ ์˜คํŒ์ • ๋ฐฉ์ง€ (๋กœ์ปฌ ๋””์Šคํฌ ๋‚ด๊ฒฐํ•จ์„ฑ) | `publish_event.py`, `job_subscriber.py`, `multi-agent-mux-delegate-job` | | **Track 1** | `O-5` (P2) | `nats-server` MQTT 3.1.1 ์–ด๋Œ‘ํ„ฐ ํ˜ธํ™˜์„ฑ ๋ฐ Retained ๋ฉ”์‹œ์ง€ ์‹ค์ธก ๊ฒ€์ฆ | ๊ฒฉ๋ฆฌ ํด๋ก  (`$SCRATCH/nats-spike`) | +| **Track 1R** | ์›๊ฒฉ ํ”„๋กœ๋•์…˜ (P1) | VPS/ํ™ˆ๋žฉ `nats-server` Docker ์ƒ์‹œ ๊ฐ€๋™ ๋ฐ MAM ์›๊ฒฉ ๋ฐฑํ”Œ๋ ˆ์ธ ์ „ํ™˜ | `PRIVATE_SERVER.md` ยง9, ์„œ๋ฒ„์ธก `docker-compose.yml`/`nats.conf`, `.mam.env` | | **Track 2** | `A-2`, `B-16` (P2) | ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ง€๋ฌธ ํ† ํ”ฝ ๊ฒฉ๋ฆฌ ๋ฐ `auth_token` ๋ฌด์กฐ๊ฑด ๋ฐœ๊ธ‰ ๊ฐ•์ œ | `mqtt_common.py`, `registry.py`, `reconcile.sh` | | **Track 3** | ๋ฌธ์„œ/์„ค์ • ๋™๊ธฐํ™” | ๊ณต์‹ ๊ฐ€์ด๋“œ, ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ, ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ…œํ”Œ๋ฆฟ ์ผ์›ํ™” | `MESSAGING.md`, `IMPROVEMENTS.md`, `VERSIONS.md`, `deploy/*` | @@ -30,15 +31,16 @@ ๊ฐ ๋งˆ์ผ์Šคํ†ค์€ ์™„๋ฃŒ ์ •์˜(DoD)์™€ ์—„๊ฒฉํ•œ ๊ฒŒ์ดํŠธ(Gate)๋ฅผ ๊ฐ€์ง€๋ฉฐ, ๊ฒŒ์ดํŠธ ์กฐ๊ฑด์„ ์ถฉ์กฑํ•˜์ง€ ๋ชปํ•˜๋ฉด ๋‹ค์Œ ๋งˆ์ผ์Šคํ†ค์œผ๋กœ ์ง„์ž…ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ``` -M0 (๋ฌธ์„œ ์ •ํ•ฉ์„ฑ) โ”€โ”€> M1 (Track 0 ๋‚ด๊ฒฐํ•จ์„ฑ) โ”€โ”€> M2 (Track 1 ์‹ค์ฆ) โ”€โ”€> M3 (Track 2 ๋ณด์•ˆ) โ”€โ”€> M4 (Track 3 ์™„๊ฒฐ) +M0 (๋ฌธ์„œ ์ •ํ•ฉ์„ฑ) โ”€โ”€> M1 (Track 0 ๋‚ด๊ฒฐํ•จ์„ฑ) โ”€โ”€> M2a (๋กœ์ปฌ ์ŠคํŒŒ์ดํฌ) โ”€โ”€> M2b (์›๊ฒฉ ๋ฐฐํฌ) โ”€โ”€> M3 (Track 2 ๋ณด์•ˆ) โ”€โ”€> M4 (Track 3 ์™„๊ฒฐ) ``` | ๋งˆ์ผ์Šคํ†ค | ์ด๋ฆ„ | ์™„๋ฃŒ ์ •์˜ (Definition of Done) | ํ†ต๊ณผ ๊ฒŒ์ดํŠธ (Gate Condition) | |---|---|---|---| | **M0** | ๋ฌธ์„œ ์ •ํ•ฉ์„ฑ ํ™•๋ณด | `PRIVATE_SERVER.md` E-1~E-4 ๊ต์ •, ๋‹ค๋Šฅ์„ฑ ์ ˆ ์ถ”๊ฐ€, ๋ณธ ๋กœ๋“œ๋งต ์ž‘์„ฑ | **G-D1 ~ G-D4 ๊ฐ€๋“œ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ** (276 -> 280) | | **M1** | ๋‚ด๊ฒฐํ•จ์„ฑ ํ™•๋ณด (Track 0) | `B-14`, `B-15` ์ฝ”๋“œ ํŒจ์น˜ ์™„๋ฃŒ | **G-1 ~ G-10 ๊ฐ€๋“œ ํ†ต๊ณผ + mutation ์ „๊ฑด FAIL ํ™•์ธ** (280 -> 290) | -| **M2** | ๋ธŒ๋กœ์ปค ์‹ค์ฆ (Track 1) | ๊ฒฉ๋ฆฌ ํด๋ก ์—์„œ S-1 ~ S-9 ์ŠคํŒŒ์ดํฌ ์™„์ˆ˜ | **S-3(Retained Terminal Event) ํ†ต๊ณผ** (์‹คํŒจ ์‹œ mosquitto๋กœ ๋ถ„๊ธฐ) | -| **M3** | ๋ณด์•ˆ ์ข…๊ฒฐ (Track 2) | A-2 ์ง€๋ฌธ ํ† ํ”ฝ ์ „ํ™˜, G-11 ๋ฌด์กฐ๊ฑด ํ† ํฐ ๋ฐœ๊ธ‰ | ์ง€๋ฌธ ํ† ํ”ฝ ๋™์ž‘ ํ™•์ธ **ํ›„** legacy ๊ตฌ๋… ์ œ๊ฑฐ (290 -> 291) | +| **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(๊ณ„์ •๊ฒฉ๋ฆฌ) ๋™์‹œ ํ†ต๊ณผ** | +| **M3** | ๋ณด์•ˆ ์ข…๊ฒฐ (Track 2) | A-2 ์ง€๋ฌธ ํ† ํ”ฝ ์ „ํ™˜, G-11 ๋ฌด์กฐ๊ฑด ํ† ํฐ ๋ฐœ๊ธ‰ | ์ง€๋ฌธ ํ† ํ”ฝ ๋™์ž‘ ํ™•์ธ **ํ›„** legacy ๊ตฌ๋… ์ œ๊ฑฐ | | **M4** | ๋™๊ธฐํ™” ์™„๋ฃŒ (Track 3) | `MESSAGING.md`, `IMPROVEMENTS.md`, `VERSIONS.md`, `deploy/*` ์ •ํ•ฉ | ์ „์ฒด ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ 100% Green | --- @@ -102,7 +104,27 @@ M0 (๋ฌธ์„œ ์ •ํ•ฉ์„ฑ) โ”€โ”€> M1 (Track 0 ๋‚ด๊ฒฐํ•จ์„ฑ) โ”€โ”€> M2 (Track 1 ์‹ค --- -## 5. Track 2: A-2 ๋ณด์•ˆ ๊ฒฐํ•จ ๋ฐ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ฒฉ๋ฆฌ ํ•ด์†Œ (`A-2`, `B-16`) +## 5. Track 1R: ์›๊ฒฉ ํ”„๋กœ๋•์…˜ ์ „ํ™˜ ๋กœ๋“œ๋งต (M2b ์ƒ์„ธ) + +``` +[P0 ๊ต์ •] D-1~D-5 ๋ฌธ์„œ ๊ต์ • + ยง5.2 ๊ฒฝ๊ณ„ ๋ช…๋ฌธํ™” + ์‹ ๊ทœ ๊ฐ€๋“œ 7์ข… + โ–ผ +[P1 ์„œ๋ฒ„ ์ค€๋น„] VPS/ํ™ˆ๋žฉ ํ”„๋กœ๋น„์ €๋‹, Docker, Tailscale ๊ฐ€์ž… + โ–ผ +[P2 ๋ฐฐํฌ] nats.conf + compose ๊ธฐ๋™, healthcheck healthy ํ™•์ธ + โ–ผ +[P3 ์ž ๊ธˆ] ๋ฐ”์ธ๋“œ ์ฃผ์†Œ ํ•œ์ • + UFW + ss/nmap ๋กœ ๋…ธ์ถœ ๋ฉด์  0 ๋‹จ์–ธ (R-3) + โ–ผ +[P4 ์ „ํ™˜] ๋“œ๋ ˆ์ธ โ†’ ์ž”์—ฌ ์Šค์บ” โ†’ .mam.env ๊ต์ฒด (ยง9.5) + โ–ผ +[P5 ๊ฒ€์ฆ] R-1 ~ R-10. R-5(retained) / R-9(๊ณ„์ • ๊ฒฝ๊ณ„) ๋ฅผ ์ตœ์ข… ๊ด€๋ฌธ์œผ๋กœ + โ–ผ +[P6 ์ƒ์‹œํ™”] ๋กœ๊ทธ ๋กœํ…Œ์ด์…˜, JetStream ๋ณผ๋ฅจ ๋ฐฑ์—…, healthcheck ์•Œ๋ฆผ +``` + +--- + +## 6. Track 2: A-2 ๋ณด์•ˆ ๊ฒฐํ•จ ๋ฐ ์›Œํฌ์ŠคํŽ˜์ด์Šค ๊ฒฉ๋ฆฌ ํ•ด์†Œ (`A-2`, `B-16`) 1. **`auth_token` ๋ฌด์กฐ๊ฑด ๋ฐœ๊ธ‰ (`F-3` / `G-11`)**: `registry.register_job()`์—์„œ ๋ธŒ๋กœ์ปค ์„ค์ •๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ํ•ญ์ƒ `secrets.token_urlsafe(32)` ๊ธฐ๋ฐ˜ ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜์—ฌ ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ํ™˜๊ฒฝ์—์„œ๋„ HMAC ๊ฒ€์ฆ์ด ๋ฌด๋ ฅํ™”๋˜์ง€ ์•Š๋„๋ก ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. @@ -113,12 +135,12 @@ M0 (๋ฌธ์„œ ์ •ํ•ฉ์„ฑ) โ”€โ”€> M1 (Track 0 ๋‚ด๊ฒฐํ•จ์„ฑ) โ”€โ”€> M2 (Track 1 ์‹ค --- -## 6. Track 3: ๋ฌธ์„œ ๋ฐ ๋ฐฐํฌ ์„ค์ • ๋™๊ธฐํ™” +## 7. Track 3: ๋ฌธ์„œ ๋ฐ ๋ฐฐํฌ ์„ค์ • ๋™๊ธฐํ™” | ๋Œ€์ƒ ํŒŒ์ผ | ๊ฐฑ์‹  ๋‚ด์šฉ | |---|---| | [`MESSAGING.md`](MESSAGING.md) | ๋ธŒ๋กœ์ปค ํ‘œ์ค€์„ `nats-server`๋กœ ๊ฐฑ์‹ , F-1/C1 ํ•ด์†Œ ๊ธฐ๋ก, F-5 ์˜์† ์„ธ์…˜ ์„œ์ˆ  ์ •์ • | -| [`IMPROVEMENTS.md`](IMPROVEMENTS.md) | A-2 ์™„๋ฃŒ ์ „ํ™˜, B-14/B-15/B-16/O-5 ํ•ด๊ฒฐ ์ƒํƒœ ๊ฐฑ์‹  | +| [`IMPROVEMENTS.md`](IMPROVEMENTS.md) | A-2 ์™„๋ฃŒ ์ „ํ™˜, B-14/B-15/B-16/O-5 ํ•ด๊ฒฐ ์ƒํƒœ ๊ฐฑ์‹ , B-17/B-18 ์‹ ์„ค ๋“ฑ๋ก | | [`VERSIONS.md`](VERSIONS.md) | `v2.0.0` ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ์— ๋ฉ”์‹œ์ง• ๋ฐฑํ”Œ๋ ˆ์ธ ๊ณ ๋„ํ™” ๋ฐ ๋‚ด๊ฒฐํ•จ์„ฑ ํŒจ์น˜ ๊ธฐ๋ก | | [`PRIVATE_SERVER.md`](PRIVATE_SERVER.md) | ์ŠคํŒŒ์ดํฌ ๊ฒฐ๊ณผ ๋ฐ˜์˜ ๋ฐ ์ตœ์ข… ๊ฐ€์ด๋“œ ํ™•์ • | | [`deploy/install.sh`](deploy/install.sh) | `requirements.txt` ํ™•์ธ (paho ์œ ์ง€) ๋ฐ ๊ฐœ์ธ ๋ธŒ๋กœ์ปค ์•ˆ๋‚ด ์ถ”๊ฐ€ | @@ -126,21 +148,6 @@ M0 (๋ฌธ์„œ ์ •ํ•ฉ์„ฑ) โ”€โ”€> M1 (Track 0 ๋‚ด๊ฒฐํ•จ์„ฑ) โ”€โ”€> M2 (Track 1 ์‹ค --- -## 7. ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„ ๋ฐ ๋กค๋ฐฑ ์ „๋žต - -``` -[M0: ๋ฌธ์„œ/๊ฐ€๋“œ] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ โ”‚ (M0 A-1 ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ •๋ ฌ ์„ ํ–‰) - โ–ผ โ–ผ -[M1: Track 0 ๋‚ด๊ฒฐํ•จ์„ฑ] โ”€โ”€> [M2: Track 1 ์ŠคํŒŒ์ดํฌ] โ”€โ”€> [M3: Track 2 ๋ณด์•ˆ] โ”€โ”€> [M4: ๋™๊ธฐํ™”] -``` - -- **๋กค๋ฐฑ ์ „๋žต**: - - `nats-server` ์ŠคํŒŒ์ดํฌ(S-3) ์‹คํŒจ ์‹œ: ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด `.mam.env`์˜ ๋ธŒ๋กœ์ปค ์ฃผ์†Œ๋งŒ `eclipse-mosquitto`๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค (๊ฐ€์—ญ์„ฑ 100%). - - Track 0 ๋‚ด๊ฒฐํ•จ์„ฑ ํŒจ์น˜๋Š” ๋ธŒ๋กœ์ปค ์ œํ’ˆ๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ์ˆœ์ˆ˜ ์ด๋“์ด๋ฏ€๋กœ ๋กค๋ฐฑํ•˜์ง€ ์•Š๊ณ  ์˜๊ตฌ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. - ---- - ## 8. ์ง„ํ–‰ ์ถ”์  ์ฒดํฌ๋ฆฌ์ŠคํŠธ ### M0: ๋ฌธ์„œ ์ •ํ•ฉ์„ฑ ํ™•๋ณด @@ -154,14 +161,27 @@ M0 (๋ฌธ์„œ ์ •ํ•ฉ์„ฑ) โ”€โ”€> M1 (Track 0 ๋‚ด๊ฒฐํ•จ์„ฑ) โ”€โ”€> M2 (Track 1 ์‹ค - [x] Step 3: `multi-agent-mux-delegate-job` ์ธํ”„๋ผ `rc=3` ์—๋Ÿฌ ๋ถ„๋ฆฌ (`F-4` / G-9~G-10) - [x] M1 ํ†ตํ•ฉ ๊ฒ€์ฆ (๋ธŒ๋กœ์ปค ๋‹ค์šด ์ƒํƒœ ์œ„์ž„ 3์ดˆ ์™„์ฃผ) -### M2: Track 1 `nats-server` ์‹ค์ฆ (`O-5`) +### M2a: Track 1 `nats-server` ๋กœ์ปฌ ์‹ค์ฆ (`O-5`) - [ ] ๊ฒฉ๋ฆฌ ํด๋ก  ์ƒ์„ฑ (`$SCRATCH/nats-spike`) - [ ] S-1 ~ S-9 ์ŠคํŒŒ์ดํฌ ๋งคํŠธ๋ฆญ์Šค ๊ฒ€์ฆ ์ˆ˜ํ–‰ - [ ] S-3 Retained ๋ฉ”์‹œ์ง€ ๊ฒŒ์ดํŠธ ํ†ต๊ณผ ํ™•์ธ +### M2b: Track 1R ์›๊ฒฉ ํ”„๋กœ๋•์…˜ ์ „ํ™˜ +- [x] D-1 `store_dir` ์ ˆ๋Œ€๊ฒฝ๋กœ ๊ต์ • + ๋น„์ธ์šฉ heredoc (`PRIVATE_SERVER.md:73`, `:146`) +- [x] D-2 ์ด๋ฏธ์ง€ ํ•€ `nats:2.12-alpine` + `/healthz` healthcheck +- [x] D-3 8222/8080 ๋ฐ”์ธ๋“œ ์ฃผ์†Œ ํ•œ์ • +- [x] D-4 TLS ์ ˆ์˜ IP ์˜ˆ์‹œ ์ œ๊ฑฐ ๋ฐ DNS/SAN ์š”๊ฑด ๋ช…์‹œ +- [x] D-5 `.mam.env.example` ์ •ํ•ฉ (`MQTT_KEEPALIVE` ์ถ”๊ฐ€) +- [x] N-1 ยง5.2 ์— retained=MQTT ์ „์šฉ ๊ฒฝ๊ณ„ ๋ช…๋ฌธํ™” + ยง9.1 `mam_observer` ์ถ”๊ฐ€ +- [ ] ์‹ ๊ทœ ๊ฐ€๋“œ G-D5 ~ G-D9, G-R1, G-R2 ๊ตฌํ˜„ ๋ฐ ๊ฒ€์ฆ (290 -> 297) +- [ ] ์„œ๋ฒ„ ๋ฐฐํฌ ๋ฐ R-3 ๋…ธ์ถœ ๋ฉด์  0 ๋‹จ์–ธ +- [ ] ยง9.5 ๋“œ๋ ˆ์ธยท์ž”์—ฌ ์Šค์บ” ํ›„ `.mam.env` ์ „ํ™˜ +- [ ] R-1 ~ R-10 ์ „๊ฑด ํ†ต๊ณผ (R-5 / R-9 ์ตœ์ข… ๊ด€๋ฌธ) + ### M3: Track 2 ๋ณด์•ˆ ๋ฐ ํ† ํ”ฝ ๊ฒฉ๋ฆฌ (`A-2`, `B-16`) - [ ] G-11 ๋ฌด์กฐ๊ฑด `auth_token` ๋ฐœ๊ธ‰ ์ ์šฉ - [ ] ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ง€๋ฌธ ํ† ํ”ฝ ๋ฐœํ–‰ ์ „ํ™˜ ๋ฐ ๋ ˆ๊ฑฐ์‹œ ๊ตฌ๋… ์ œ๊ฑฐ ### M4: Track 3 ๋ฌธ์„œ ๋ฐ ๋ฐฐํฌ ๋™๊ธฐํ™” - [ ] `MESSAGING.md`, `IMPROVEMENTS.md`, `VERSIONS.md`, `deploy/*` ์ตœ์ข… ๊ฐฑ์‹  + diff --git a/tests/test_deploy_freshness.py b/tests/test_deploy_freshness.py index a365f19..19f189f 100644 --- a/tests/test_deploy_freshness.py +++ b/tests/test_deploy_freshness.py @@ -283,16 +283,11 @@ def test_d11_private_server_env_names_valid(): code_blocks = re.findall(r"```(?:bash|conf|yaml|)(.*?)```", content, re.DOTALL) assert code_blocks, "No code blocks found in PRIVATE_SERVER.md" - # Known recognized MQTT env vars from broker_config_from_env() - recognized = { - "MQTT_BROKER", "MQTT_PORT", "MQTT_TLS", "MQTT_USERNAME", "MQTT_PASSWORD", - "MQTT_CLIENT_ID_PREFIX", "MQTT_CA_CERTS", "MQTT_CERTFILE", "MQTT_KEYFILE", - "MQTT_KEEPALIVE", "MAM_MQTT_HOST" # checked for exclusion - } + # Known recognized MQTT env vars from broker_config_from_env() and deployment valid_mqtt_vars = { "MQTT_BROKER", "MQTT_PORT", "MQTT_TLS", "MQTT_USERNAME", "MQTT_PASSWORD", "MQTT_CLIENT_ID_PREFIX", "MQTT_CA_CERTS", "MQTT_CERTFILE", "MQTT_KEYFILE", - "MQTT_KEEPALIVE" + "MQTT_KEEPALIVE", "MQTT_BIND" } for block in code_blocks: @@ -352,3 +347,117 @@ def test_d14_private_server_cli_args_valid(): ) assert "status --job " in code_blocks, "PRIVATE_SERVER.md must include cleanup step with status --job" + +# -------------------------------------------------------------------------- +# D-15 โ€” (G-D5) store_dir in code blocks must be absolute path (/ or $HOME) +# and heredocs writing it must be unquoted (<*\w.]+)["\']', content) + assert matches, "Expected subject literals matching DEFAULT_TOPIC_ROOT in PRIVATE_SERVER.md" + for sub in matches: + assert sub.startswith(expected_prefix), f"Subject '{sub}' does not start with expected prefix '{expected_prefix}'" + + +# -------------------------------------------------------------------------- +# D-20 โ€” (G-R1) run_loop.sh exports MAM_ENV_FILE +# -------------------------------------------------------------------------- +def test_d20_run_loop_exports_mam_env_file(): + run_loop_path = os.path.join(REPO_ROOT, ".agents", "skills", "multi-agent-mux-loop", "scripts", "run_loop.sh") + with open(run_loop_path, "r", encoding="utf-8") as f: + content = f.read() + assert 'export MAM_ENV_FILE=' in content, "run_loop.sh must export MAM_ENV_FILE" + + +# -------------------------------------------------------------------------- +# D-21 โ€” (G-R2) MQTT_KEEPALIVE documented and no un-commented retry vars +# -------------------------------------------------------------------------- +def test_d21_env_template_mqtt_var_coverage(): + template_path = os.path.join(REPO_ROOT, ".mam.env.example") + with open(template_path, "r", encoding="utf-8") as f: + template = f.read() + assert "MQTT_KEEPALIVE" in template, ".mam.env.example must document MQTT_KEEPALIVE" + for line in template.splitlines(): + line = line.strip() + if not line.startswith("#"): + assert "MQTT_RETRY_INTERVAL" not in line + assert "MQTT_MAX_RETRIES" not in line + +