Issue #26: one .mam.env parser for the wrapper and the Python tools, decimal integer knobs, tests on a local MQTT broker. Merge-workspace review: round 1 (@b58edac) claude agy PASS, codex NOT PASS (R1-R4), grok no verdict (its late report, archived as report-c9ed5b14.md, found R1 too); round 2 (@0eec795) 4/4 PASS. Reports and round-2 identity sidecars go to .agents/reports/<session>/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
14 KiB
🔍 Review Report — PR #43 fix/issue-26 리뷰 (job 645adb2a)
- Reviewer:
planner-reviewer-claude-01(role: planner,reviewer) · 2026-10-01 - 대상: PR #43 @
b58edac, basemain e1eb22b(v4.8.0). 6커밋, 17파일+1590/−94,git diff --check클린. - 검증 경로:
/var/folders/.../T/mam-review/loop-20261001T131705Z-a60092/1. identity 확인:HEAD = b58edace…4893,HEAD^{tree} = 14b670fb…6ffa— 브리프와 일치. - 안전 규칙 준수: 검증 경로·소스 워크트리·본 체크아웃(
main e1eb22b) 모두 수정·커밋·checkout 없음. 실제.mam.env는 값을 출력하지 않고 형태만 셌다($·export·인라인#·따옴표 줄 수 = 모두 0). 파서 실험은 scratchpad 의 합성 파일로만. 테스트는 브리프대로.mam.env를 export 하지 않고(MQTT_*미설정 확인) 돌렸다. 실제.mam·데몬·세션·herdr 조작 없음, push·merge·PR 상태 변경 없음. - 참고(이 잡의 liveness): 이 잡의
started발행 때 브로커100.69.147.175:1883이 연결을 거부했다(Connection refused, 지금도 포트 닫힘).publish_event는 rc 2 였지만 레코드는running·events.log에started(토큰 포함)가 기록됐고 heartbeat 타이머도 떠 있다 — #18 계약상 로컬 증거로 충분하다. 브로커 쪽은 오케스트레이터 확인이 필요하다(아래 판정과 무관).
결론 요약
병합을 막을 결함은 없다. 두 로더는 같은 입력에서 같은 결과를 내고(합성 파일 15줄 실측), 래퍼의 eval 경로는 어떤 값도 셸 해석하지 않으며(명령 치환 주입 실험: 실행 0), bash mam_int 와 Python _positive_int 는 15가지 입력에서 결과가 같다. 로컬 브로커로 옮긴 시험들은 skip 을 fail 로 바꾸고 양쪽 로그·감사 기록까지 단언해 오히려 강해졌다. 비차단 관찰 5건(R1–R5).
1. 파서 규칙 (검토 항목 1)
envfile.py:45-73 parse, mqtt_common.py:98-101 은 envfile.apply 에 위임 — Python 쪽 로더는 하나다. 래퍼는 같은 파일을 --shell 로 실행한다. 같은 합성 파일을 두 경로에 넣은 결과:
| 입력 줄 | Python 로더 | 래퍼 eval 후 |
|---|---|---|
BOMKEY=1 (UTF-8 BOM) |
건너뜀(invalid key, 1행 경고) | 미설정 |
A = spaced \r (CRLF) |
'spaced' |
spaced |
B="q's" |
"q's" |
동일 |
C='x'' |
"x'"(바깥 한 쌍만) |
동일 |
D=a=b=c |
'a=b=c' |
동일 |
E= |
'' |
'' |
export F=exp |
건너뜀(invalid key) | 미설정 |
G=val # trailing |
'val # trailing' |
동일 |
H=$(touch …/PWNED) \id` ${HOME}` |
글자 그대로 | 글자 그대로, PWNED 파일 생성 안 됨 |
9BAD=x / noequals |
건너뜀 + 경고 | 미설정 |
IFS=x / BASH_ENV=x |
건너뜀(reserved) | 미설정 |
I="unbalanced |
'"unbalanced' |
동일 |
J= "inner" |
'inner' |
동일 |
두 경로가 완전히 일치한다. 경고에는 값이 찍히지 않는다(줄 번호·사유만). 이전 Python 로더(strip('"').strip("'"))는 따옴표를 여러 겹 벗기고 키 검사가 없었는데, 이제 한 쌍만 벗기고 형식 오류를 경고한다 — 문서화된 규칙과 맞다.
R1 (비차단, 문서): .mam.env.example 은 $HOME/... 예시를 /home/you/... 로 바꿔 "셸 해석 없음" 과 일관되게 했지만, 머리말에 규칙 자체(셸 확장 없음, 인라인 # 은 값의 일부, export 접두어 불가, BOM 저장 시 첫 줄 무시, 이미 있는 환경변수가 우선)를 적지 않았다. 특히 래퍼 사용자에게는 세 가지가 동작 변경이다 — 전에는 bash source 가 export KEY=·$HOME 확장·인라인 주석을 받아들였다. 실제 .mam.env 에는 해당 줄이 없음을 확인했다(형태만 셈).
2. 래퍼의 eval 경로 (검토 항목 2)
multi-agent-mux-delegate-job:41-46: python -I envfile.py --shell 의 출력을 eval. 출력은 envfile.py:97-98 의 print(f"export {key}={shlex.quote(val)}") 뿐이고, key 는 _KEY.fullmatch 를 통과한 것만, val 은 splitlines 로 개행이 없고 shlex.quote 로 감싸진다 — 주입 실험에서 $()·백틱·${} 모두 글자 그대로 남았다.
- 래퍼가 그 전에 세운 비-export 변수는
SCRIPT_DIR·REPO_ROOT·TARGET_ENV셋뿐이고, 셋 다 환경으로 넘겨 파일이 덮어쓸 수 없다(:42). env-wins 실측:D=fromenv가 파일의D=a=b=c를 이긴다. - 실패 경로: 파일이 없으면 rc 0·출력 없음, 읽을 수 없으면 stderr
WARNING: … cannot read (PermissionError)후 rc 0 — 래퍼는 설정 없이 계속한다. 래퍼의|| { echo "ERROR: cannot read $TARGET_ENV"; exit 1; }(:44)는 Python 실행 자체가 실패할 때(인터프리터 없음 등)만 걸려, 메시지와 실제 원인이 어긋난다(R2, 사소). -I:PYTHONPATH·사용자 site 를 무시하지만envfile.py는 표준 라이브러리만 쓰고 파일 경로로 실행되므로 문제없다. 오히려.mam.env를 읽기 전 환경의PYTHON*이 파서에 끼어들 여지를 없앤다.
3. 우선순위 변경의 영향 (검토 항목 3)
전에는 래퍼의 source 가 환경을 덮어썼고 Python 로더는 환경을 지켰다 — 같은 파일이 두 경로에서 다른 결과를 냈다. 이제 둘 다 env-wins.
- 루프 러너가 래퍼에 넘기는 env(
DELEGATE_JOB_PYTHON,--registry-dir대신 쓰는MAM_JOBS_DIR등): 전에는 파일 값에 덮일 수 있었다 → 이제 러너 값이 이긴다. 개선. - 워크트리 creator: create/resume 이 넘기는 절대경로 바인딩(
AGENT_SESSIONS_YAML·MAM_JOBS_DIR·…)이 이제 파일 값에 덮이지 않는다 — #21 의 의도와 맞는 개선. - 원격 러너(
remote.py)는 원래 Python 로더라 변화 없음. 테스트는conftest가MAM_ENV_FILE을 없는 파일로 고정해 무관. - 동작이 나빠지는 경로: 셸에 오래된
MQTT_*를 export 해 둔 채 파일만 고친 사용자는 이제 파일 값이 적용되지 않는다. 이 부분이 R1 의 문서 누락과 겹친다.
4. 정수 정규화 (검토 항목 4)
bash mam_int(lib.sh:2345-2356)와 Python _positive_int(runner.py:71-82)를 같은 15개 입력에 돌린 결과 전부 일치: ' 30 '→30, 0700→700(8진 아님), 0900→900, 0→거부(min 1), -5·+5·1e3·30s·전각 30·빈 값 → 거부, 999999999→수용, 1000000000→거부(9자리 상한), 0000000000000000001→1(앞 0 제거 후 길이 판정).
- 래퍼: 환경 노브는
mam_int_knob(잘못되면 경고 + 기본값,:154-156), 플래그는parse_opts끝의 루프(잘못되면ERROR … not a positive decimal integerrc 1,:196-200).MAM_INJECT_ACK_GRACE_S는 min 0(0 = 대기 없음 허용, 의미상 맞다),ACK_TIMEOUT·heartbeat·budget 은 min 1. lib.shSKS_*대기 노브는 min 0(대기 생략 허용),delegate_wait_started도 0 허용(:2379). - 빠진 노브:
$(( ))안에 환경 기본값을 직접 쓰는 자리는 남아 있지 않다(B5 정적 검사가 고정).[ -ge ]/[ -le ]/sleep에만 쓰이는 노브(MAM_READY_EMPTY_GIVEUP·SKS_EMPTY_GIVEUP·SKS_IDLE_RETRIES·…)는test의 비교가 원래 10진수라 8진 문제가 없고, PR 이 표로 분류해 "모든 정수 노브가 분류됨" 을 시험한다(test_b5_every_integer_env_default_is_classified). 그 노브들에 숫자가 아닌 값이 오면[가 오류(rc 2)를 내는 기존 동작은 남는다(R3, 범위 밖).
5. 로컬 브로커와 이전된 시험 (검토 항목 5)
tests/_local_mqtt.py 는 MQTT 3.1.1 의 필요한 부분을 클라이언트가 기대하는 순서로 처리한다: CONNECT(레벨 3/4만, 그 외 CONNACK rc 1)→CONNACK, SUBSCRIBE→SUBACK(QoS 최대 1 부여) 뒤 매칭되는 retained 전달, PUBLISH QoS1 은 라우팅·retained 저장 뒤에 PUBACK(b58edac — 발행자가 ack 를 받은 시점에 브로커 상태가 이미 반영됨), 빈 payload retained 는 삭제, UNSUBSCRIBE→UNSUBACK, PINGREQ→PINGRESP, +/# 와일드카드와 $ 토픽 예외(topic_matches). 미지원(QoS2·will·지속 세션)은 이 저장소가 쓰지 않는 기능이다. 127.0.0.1:0 에 바인딩해 포트 충돌이 없고, 스레드는 daemon 이며 stop() 이 리스너·연결을 닫는다.
이전된 시험의 단언은 약해지지 않았고 강해졌다:
test_acceptance5: 공개 브로커용_skip_if_broker_did_not_deliver·no CONNACKskip 이 삭제되고, 대신 러너 로그의DISPATCH·published started/completed·DONE rc=0과 오케스트레이터 감사 기록의dispatch_verified·dispatch_fingerprint == sha256(token)을 양쪽에서 단언한다.test_issue19: "브로커 도달 불가면 skip" 이pytest.fail로 바뀌었다(로컬 브로커는 항상 떠 있으므로).- S9/S10 의
_live_broker()(2·3회차 리뷰에서 내가 P2 로 지적한 공개 브로커 의존)가local_broker_env()로 대체됐다. test_b19는send_keys_safe본문을 앞 2500자가 아니라 함수 끝까지 검사하게 고쳐졌다(정확성 개선).- 정적 검사(
test_local_mqtt.py:215)가 공개 브로커 주소의 재유입을 막는다. 시험 로그에hivemq·실제 브로커 IP 흔적 0건.
6. 테스트 실행 (검증 방법·검토 항목 6)
cd <검증 경로> && AGENT_PYTHON_BIN=<pr-43 .venv> PYTHONDONTWRITEBYTECODE=1 <pr-43 .venv> -m pytest -p no:cacheprovider -q -rfEs \
--basetemp=<밖> tests/test_issue26_envfile.py tests/test_issue26_ints.py tests/test_local_mqtt.py \
tests/test_issue18_liveness_contract.py tests/test_issue33_budget.py tests/test_issue30_stall_watch.py tests/test_tier1_unit.py
(.mam.env 미export, MQTT_* 미설정)
→ 228 passed, 1 skipped in 525.44s (0:08:45) failed 0 (skip = S6, /proc)
- PR #41 리뷰 내내 공개 브로커 때문에 흔들리던 S9·S10·R2·B3·acceptance 계열이 외부 브로커 없이 전부 통과했다 — PR 의 주장 확인.
- 기준 실패 주장(검토 항목 6):
tests/test_issue10_13_purge.py를 이 PR 트리와 base(e1eb22b를git archive로 풀어 놓은 사본)에서 각각 단독 실행한 결과 둘 다 23 passed, 실패 0. 즉 이 호스트에서 이 리뷰어의 조건으로는 "12건 기준 실패" 가 재현되지 않는다. 양쪽 결과가 같으므로 어느 경우든 PR 회귀는 아니다. 오케스트레이터의 전체 스위트에서만 나타난다면 시험 간 순서·상태(예: 실제 홈의 대화 파일, 이전 시험이 남긴 환경) 의존을 의심할 만하다(R4). - 격리: 실제
.mam/jobs는 이 잡 자신의 파일 외 추가 0. 검증 경로 tracked 파일 변화 없음(HEADb58edac), 본 체크아웃main e1eb22b. 실행 뒤 호스트에 남은stall_watch(이 잡을 감시하는 오케스트레이터의 것)·sleep 300(다른 세션의 프로세스 그룹) 은 내 실행과 무관함을 부모/프로세스 그룹으로 확인했다.
7. 설계 경계 (검토 항목 7)
- 판정이 Python 으로 이동:
.mam.env해석(키 검증·예약 변수 거부·우선순위)이 bashsource에서envfile.py로 옮겨졌다 — #17 항목 4 의 ".mam.env파싱(#26) — bashsource제거" 그대로다. 래퍼에는 실행과eval만 남는다. - bash 에 새로 생긴 로직:
mam_int/mam_int_knob과parse_opts의 플래그 검증. 이는 bash 산술로 들어가는 값을 그 자리에서 정규화하는 입력 정제이고, Python 쌍둥이(_positive_int)와 한 규칙으로 고정됐다(B1 시험). 판정(무엇을 할지)이 아니라 형식 변환이라 경계 위반으로 보지 않지만, 1회차 PR #41 리뷰의 M4(MAM_JOB_TIMEOUT_S검증이 bash 에 있음)와 같은 부류이므로 항목 4 에서 래퍼 인자 처리가 Python 으로 갈 때 함께 옮길 목록에 둘 것을 권한다(R5).
8. 판정
버전 락스텝 bump 부재는 판정에서 제외한다. 병합을 막을 결함은 없다.
| # | 비차단 관찰 | 근거 |
|---|---|---|
| R1 | .mam.env.example 머리말에 새 파싱 규칙과 env-wins 가 적혀 있지 않다 — 래퍼 사용자에게 export 접두어·$ 확장·인라인 주석·우선순위 4가지가 동작 변경 |
.mam.env.example:1-20; §1·§3 |
| R2 | 래퍼의 ERROR: cannot read $TARGET_ENV 는 Python 실행 실패에만 걸리고, 실제 읽기 실패는 경고 후 설정 없이 진행 — 메시지가 원인과 어긋난다 |
multi-agent-mux-delegate-job:44, envfile.py:52-53 |
| R3 | [ -ge ]/sleep 전용 정수 노브에 숫자가 아닌 값이 오면 [ 오류(rc 2)를 내는 기존 동작은 남는다 |
PR 이 분류표로 의도적으로 범위 밖 처리 |
| R4 | "test_issue10_13_purge.py 12건 기준 실패" 가 이 리뷰어 조건(단독, PR·base 양쪽)에서는 재현되지 않음 — 전체 스위트 순서 의존 가능성 | §6 |
| R5 | mam_int/플래그 검증은 bash 의 입력 정제 — 항목 4 이전 목록에 추가 권고 |
lib.sh:2345-2364, 래퍼 :196-200 |
9. 교차검증 가이드
RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261001T131705Z-a60092/1
PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-43/.venv/bin/python
git -C "$RP" rev-parse HEAD 'HEAD^{tree}' # b58edac… / 14b670f…
# 합성 .env 15줄을 envfile.apply 와 `-I envfile.py --shell` + eval 양쪽에 → 결과 동일, $(…)/`…` 미실행 (§1 표)
# mam_int vs _positive_int 15개 입력 → 전부 동일 (§4)
# base: git -C "$RP" archive e1eb22b | tar -x -C <tmp> → tests/test_issue10_13_purge.py → 23 passed (PR 도 23 passed)
[VERDICT: PASS]