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
15 KiB
PR #43 review — job 3001f1d3
판정: NOT PASS. 로컬 MQTT 전환의 검증은 강화됐지만, 설정 파일 읽기 실패 처리와 두 정수 정규화 구현의 동작에 재현 가능한 차이가 남아 있다. 아래 수정은 현재 설계 안에서 가능하므로 재계획 에스컬레이션은 요청하지 않는다.
검증 identity와 범위
- loop:
loop-20261001T131705Z-a60092 - base:
e1eb22bd59ce825e3a22e86433c30f7ffd833810 - head:
b58edace115205500c1197d9ce59cb5c6d724893 - tree:
14b670fbfc82dc9179cab43cf53eef920a3d6ffa - 검증 cwd:
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261001T131705Z-a60092/1 - 인터프리터:
/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-43/.venv/bin/python
검증 트리의 HEAD/tree와 clean 상태를 확인했다. 프로젝트 코드, 브랜치, 실제 세션은 변경하지 않았다. 실제 .mam.env를 읽거나 export하지 않았다. 테스트 및 탐침의 설정·레지스트리·HOME·산출물은 임시 디렉터리를 사용했다. 기준 비교는 git archive <base>로 만든 /tmp/3001f1d3-base 사본의 purge 테스트만 같은 인터프리터와 검증 cwd에서 실행했다.
수정 필요 사항
R1 — [P2] 설정 파일을 읽을 수 없는데 래퍼가 성공으로 계속 진행한다
근거: .agents/skills/lib_py/delegation/envfile.py:52는 OSError와 UnicodeDecodeError를 빈 설정과 경고로 바꾸고, :94에서 이를 받은 CLI는 :99에서 항상 0을 반환한다. 래퍼 .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:42의 실패 검사는 따라서 이 오류를 잡지 못한다.
재현: 임시 파일에 PROBE_A=one을 쓰고 chmod 000한다. 해당 파일을 MAM_ENV_FILE로 지정하고 실제 래퍼의 --help를 실행한다. HOME/WORKSPACE_ROOT와 로그·레지스트리도 임시 경로로 지정한다. 확인 후 파일 권한을 복원한다.
기대: 파일 전체 읽기 실패는 명시적 오류와 nonzero 종료. 잘못된 개별 줄을 경고 후 건너뛰는 규칙과 구분해야 한다.
실제: head는 cannot read (PermissionError) 경고와 함께 rc 0, usage 출력을 반환했다. 같은 파일과 환경의 base 래퍼는 rc 1 / Permission denied였다. 잘못된 UTF-8 파일도 head에서 경고와 rc 0이었다. 실제 submit에서는 필요한 설정 없이 기본 설정으로 진행할 수 있다. 기존 test_a1_an_unreadable_parser_fails_loudly는 이름과 달리 파일 읽기 실패가 아니라 Python 실행파일 부재만 시험한다(tests/test_issue26_envfile.py:138).
수정 방향: 공통 파서에서 전체 읽기/디코딩 실패를 개별 줄 경고와 별도로 표현하고 CLI 및 Python 진입점으로 실패를 전달한다. 기존의 선택적 파일 부재 처리와 개별 잘못된 줄 건너뛰기는 유지한다. 실제 권한 오류 및 invalid UTF-8에 대한 wrapper rc 회귀 검증을 추가한다.
R2 — [P2] 로케일에 따라 Bash와 Python이 같은 정수 입력을 다르게 해석한다
근거: .agents/skills/lib.sh:2346은 로케일 의존 [[:space:]]를 사용한다. .agents/skills/lib_py/loop/runner.py:74는 ASCII 공백 6개만 제거하고 :75에서 ASCII 숫자만 허용한다. runner.py:483은 이 결과를 명시적인 --timeout으로 전달한다.
재현: LC_ALL=en_US.UTF-8, MAM_JOB_TIMEOUT_S='\u200330\u2003'를 실제 U+2003 문자로 설정한다. 임시 HOME/WORKSPACE_ROOT, 없는 MAM_ENV_FILE로 (a) 실제 wrapper submit --agent human --dry-run의 registry timeout_sec, (b) 실제 LoopRunner(...).parse_args() 후 job_budget/contract_args()를 비교한다. 재현 코드: /tmp/3001f1d3-probe2.py.
기대: 동일 입력은 두 경로에서 같은 예산 또는 같은 기본값으로 정규화된다.
실제: 직접 wrapper는 30초로 등록했다. runner는 invalid 경고 후 3600초, ['--timeout', '3600']을 선택했다. U+00A0 및 U+3000도 helper 비교에서 같은 불일치를 보였다. LC_ALL=C에서는 Bash도 거부했다. 정상 ASCII 공백과 0700은 C/UTF-8 모두 정상이다. 현재 표 기반 테스트는 유니코드 공백과 로케일을 포함하지 않는다.
수정 방향: 문자 집합을 명시하고 로케일에 독립적인 한 규칙으로 통일한다. 브리프의 Python 판정 원칙을 따르면 공통 Python 정규화기를 Bash가 호출하는 방식이 적합하다. Bash 구현을 유지해야 한다면 최소한 함수 내부 C 로케일/명시적 ASCII 공백 등으로 범위를 고정하고 두 로케일에서 실제 wrapper와 runner를 비교한다. C 로케일에서 위 불일치가 없어지는 것은 별도 탐침으로 확인했다.
R3 — [P2] 명시적으로 전달한 빈 정수 플래그가 검증을 우회한다
근거: .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:197의 [ -n "${!_var}" ] || continue는 미지정 optional 값과 사용자가 전달한 빈 값을 구분하지 않는다. :201은 빈 idle timeout을 기본값으로 다시 만든다.
재현: 격리된 임시 환경에서 실제 래퍼를 submit --agent human --dry-run --prompt probe --registry-dir <tmp> --idle-timeout ''로 실행한다. 다른 네 플래그도 MAM_DELEGATE_JOB_NO_MAIN=1로 실제 래퍼를 source한 뒤 parse_opts <flag> ''로 확인했다.
기대: 명시적 빈 문자열은 양의 10진수 플래그가 아니므로 등록 전에 rc 1과 해당 플래그 오류 메시지.
실제: --idle-timeout ''은 rc 0으로 잡을 등록하고 idle_timeout_sec=120을 기록했다. 다섯 플래그 모두 parse_opts에서 수락된다. --heartbeat '' 또는 --heartbeat-misses ''는 그 단계에서 파생 idle timeout을 0으로 만든다. 후속 argparse가 일부 빈 값을 거부하더라도 새 검증의 일관된 rc 1 계약을 충족하지 못한다.
수정 방향: 명시적 플래그 값은 소비 시 검증하거나 presence를 기록하여 빈 값도 검증한다. 미지정 --idle-timeout에만 파생 기본값을 적용한다. 다섯 플래그의 빈 문자열을 현재 invalid-value 표에 추가하고 잡 미등록까지 검증한다.
R4 — [P2] NUL이 들어간 값은 Python 로더를 종료시키고 Bash에서는 다른 값이 된다
근거: .agents/skills/lib_py/delegation/envfile.py:69–:72는 NUL을 값으로 수락한다. :82의 실제 os.environ 대입은 ValueError를 발생시키지만 :94의 일반 dict는 수락하며 :98에서 그대로 출력한다. mqtt_common.py:100에는 이 예외 처리가 없다.
재현: 임시 파일을 정확히 b'GOOD=ok\nVALUE=left\x00right\nAFTER=yes\n' 바이트로 쓰고 같은 파일을 두 진입점에 지정한다. 실제 wrapper를 source한 후 세 환경값을 출력하고, 별도 Python 프로세스에서 mqtt_common.ensure_env_loaded()를 호출한다. 재현 코드: /tmp/3001f1d3-probe.py.
기대: OS 환경변수로 표현할 수 없는 값은 공통 파서에서 값 없는 줄 번호 경고와 함께 거부되거나, 두 진입점 모두 같은 명시적 실패를 반환한다.
실제: wrapper는 rc 0, VALUE=leftright, AFTER=yes. Python은 rc 1, ValueError: embedded null byte로 종료하여 뒤의 설정을 적용하지 못한다. 현재 “하나의 입력 → 같은 값/경고” 계약 밖의 차이다.
수정 방향: 공통 파서에서 NUL을 거부한다. 값이나 전체 원문을 경고에 포함하지 않는다. 정상 앞뒤 줄 처리와 두 실제 진입점의 동일 동작을 회귀 검증한다.
브리프의 7개 검토 항목
-
파서 규칙:
envfile.py:50–:83이 양쪽의 공통 구현이다. 키/값 바깥 공백 제거, 한 쌍의 따옴표 제거, 값 내부=, 빈 값,export KEY=거부, 잘못된 키/줄 경고, 중복 키의 첫 값 우선은 코드와 기존 테스트에서 일치한다. CRLF는 두 줄을 정상 파싱했고 BOM은 첫 키를 invalid key로 건너뛰었다(탐침 확인). BOM 지원을 약속한 문서는 없으므로 이것 자체를 결함으로 판정하지 않았다. R4의 표현 불가능한 값은 불일치한다..mam.env.example:32,:36,:49,:110의 경로 예시는 확장 없는 절대경로로 바뀌어 파서와 맞는다. 다만 템플릿 헤더:10에는 env 우선, shell expansion 없음,export/inline comment/중복 키/BOM 규칙 설명이 없다. 사용자 문서에 문법과 이전 source 방식에서의 이행 예를 추가하는 것을 권고한다. -
eval 경로:
envfile.py:21,:63,:66,:98의 키 fullmatch/예약 변수 차단/shlex.quote가 일반 문자열의 명령 치환과 따옴표 탈출을 막는다. wrapper:42는 자신의 세 경로 변수를 parser env에 넣어 파일로 교체되지 않게 한다. 일반 값과 예약 변수 공격 테스트에 우회는 관찰하지 않았다.-I는 stdlib-only 파일을 절대 경로로 실행하므로 이 파서에는 PYTHONPATH 불필요하며 격리가 적절하다. Python 실행 불가 시 wrapper rc 1+ERROR는 테스트로 검증된다. 파일 읽기 실패는 R1, NUL은 R4와 같이 별도 수정이 필요하다. -
우선순위 변경:
envfile.py:81의 key 존재 검사로 기존 env가 빈 문자열이어도 파일을 이긴다. 이전 wrapper source는 파일이 덮어썼으므로 의도적인 호환성 변경이다. runner의 env 계약(runner.py:477) 및 creator에 전달되는 control-plane 네 env(lib_py/control_plane.py:24,:70; create script:230,:242)를 작업 트리 파일이 덮어쓰지 않게 된다는 이점이 있다. 반대로 기존 pane의 broker/budget env가 오래됐으면 파일 수정만으로 바뀌지 않으므로 unset 또는 새 환경이 필요하다. Python remote 진입점(remote.py:463)은 이전에도 env 우선이었다. 테스트 env/MAM_ENV_FILE 격리와도 맞는다. 호환성 변경은 코드 주석과 테스트(test_issue26_envfile.py:184)에는 있지만 사용자용 템플릿/README에는 설명이 추가되지 않았다. -
정수 노브:
tests/test_issue26_ints.py:35의 표에 해당하는 shell arithmetic 입력은 공통 Bash helper를 통과한다. 예산/heartbeat/misses/ack 및 명시적 timeout 플래그의 min=1, inject/onboard/SKS 대기의 min=0은 기존 의미와 맞는다. 유효 9자리 경계와 선행 0 제거는 큰 입력을 int/arithmetic 전에 제한한다. 다만 R2/R3 때문에 “같은 규칙”은 아직 성립하지 않는다. PR 범위 밖의 기존 Python 노브(runner.py:580retry,:628wait grace,:749child wait,:752tree max)와 loop-count CLI(:208,:223,:235)는 이 helper를 사용하지 않는다. 따라서 모든 정수 설정이 통일됐다고 문서화하면 안 된다. 이들은 기존 코드이며 이번 변경의 신규 회귀로 별도 반려하지 않는다. -
로컬 MQTT:
tests/_local_mqtt.py:90의 localhost port 0 bind가 포트 배정을 OS에 맡긴다.:180이후 실제 CONNECT/CONNACK, QoS 0/1,+/#, SUBACK, retained 저장/재전달/빈 payload 삭제, UNSUBACK, PING을 구현한다. PUBACK 전에 route하는 순서와 실제 paho 응답 대기 테스트(test_local_mqtt.py:100,:140,:173)가 wire 동작을 검증한다. 변경된 기존 테스트에서 의미 있는 단언 삭제는 찾지 못했다. 외부 브로커 실패 skip을 실패로 바꾸고 remote/heartbeat 수신 감사 로그 단언을 추가했다(test_issue18_liveness_contract.py:1098,:1228;test_issue19_subscriber_daemon_lifecycle.py:422). static scan은tests/*.py범위의 재유입 방지이지 모든 가능한 외부 네트워크를 차단하는 sandbox는 아니다. session fixture는 env를 복원하고 broker를 stop한다(conftest.py:1569); 이 호스트의 별도 start/stop 탐침에서 0.4초 후 broker thread 잔류는 없었다. 구현은 daemon thread를 join하지 않으므로 모든 플랫폼/종료 경합에 대한 누수 부재까지 증명한 것은 아니다. TLS/auth/persistent session/QoS2 등은 명시적 비지원 범위다. -
기준 실패 주장: 이 호스트의 head와 base에서
tests/test_issue10_13_purge.py는 각각 23 passed, 실패 0건이었다. base 파일과 테스트 fixture를 archive 사본에서 실행했다. UUID4 및 임시 HOME/registry 격리를 먼저 확인했다(test_issue10_13_purge.py:51,:138;conftest.py:193). 따라서 “13개 실패 중 12개가 이 호스트의 기준 purge 실패”는 이번 검증으로 뒷받침되지 않는다. 과거 실행의 환경 차이/로그 없이는 그 당시 결과를 부정하거나 나머지 1건 원인을 단정할 수도 없다. PR 본문에 현재 재현 결과 또는 당시 명령·환경 차이를 명시해야 한다. -
설계 경계 #17/#42: 파일 문법 판정의 Python 단일화는 목표와 맞는다. 정수 부분은
lib.sh:2345/:2359에 새로운 수락/거부/기본값/경고 판정을 두고runner.py:71에 별도 구현을 복제했다. 따라서 브리프의 “판정은 Python” 경계에 완전히 부합하지 않으며 R2가 실제 drift 증거다. 공통 Python 규칙으로 정리하거나 제한적 Bash 예외를 명시적으로 합의해야 한다. 이 평가는 브리프가 제시한 합의와 저장소 설계 문서(docs/design/worktree-creator-model.md:116)를 기준으로 하며, 원격 이슈 댓글 원문까지 독립 확인했다고 주장하지 않는다.
검증 결과와 재현 산출물
- 브리프 지정 7개 모듈: 228 passed, 1 skipped in 527.11s, rc 0. 유일한 skip은 macOS에서
/proc기반 process-tree 테스트를 제외하는tests/test_issue30_stall_watch.py:40조건이다. - 추가 변경 모듈(
test_issue19_subscriber_daemon_lifecycle.py,test_issue6_fail_closed.py,test_b19_headless_reconcile_fixes.py): 61 passed in 173.09s, rc 0. - purge 비교: head 23 passed in 11.24s, base 23 passed in 9.96s, 모두 rc 0.
git diff --check <base> HEAD, 두 변경 shell 파일의bash -n, 변경 Python 파일의 AST syntax 검사: 모두 통과.- 테스트 종료 후 검증 트리
git status --short와git diff --quiet: clean.
지정 테스트 명령(위 검증 cwd):
AGENT_PYTHON_BIN=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-43/.venv/bin/python \
PYTHONDONTWRITEBYTECODE=1 \
/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-43/.venv/bin/python \
-m pytest -p no:cacheprovider -q --basetemp=/tmp/3001f1d3-selected \
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
로그: /tmp/3001f1d3-selected.log, /tmp/3001f1d3-migrated.log. 추가 모듈도 같은 환경/옵션에 별도 --basetemp=/tmp/3001f1d3-migrated를 사용했다. 실제 .mam.env는 export하지 않았다.
추가 탐침 로그:
/tmp/3001f1d3-probe.log: 빈 플래그, NUL, CRLF/BOM, invalid UTF-8, Unicode whitespace, broker stop./tmp/3001f1d3-probe2.log: 실제 dry-run wrapper와 runner의 예산 차이, 실제 빈 idle-timeout 등록, PermissionError./tmp/3001f1d3-purge-head.log: 23 passed in 11.24s./tmp/3001f1d3-purge-base.log: 23 passed in 9.96s.
탐침 재실행은 위 검증 cwd에서 PYTHONDONTWRITEBYTECODE=1 <위 인터프리터> /tmp/3001f1d3-probe.py "$PWD" 및 probe2.py로 가능하다. 모두 자체 임시 디렉터리와 합성 설정만 사용한다. 전체 약 75분 스위트는 지시대로 실행하지 않았다.
[VERDICT: NOT PASS]