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
113 lines
15 KiB
Markdown
113 lines
15 KiB
Markdown
# 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개 검토 항목
|
||
|
||
1. **파서 규칙:** `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 방식에서의 이행 예를 추가하는 것을 권고한다.
|
||
|
||
2. **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와 같이 별도 수정이 필요하다.
|
||
|
||
3. **우선순위 변경:** `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에는 설명이 추가되지 않았다.
|
||
|
||
4. **정수 노브:** `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:580` retry, `:628` wait grace, `:749` child wait, `:752` tree max)와 loop-count CLI(`:208`, `:223`, `:235`)는 이 helper를 사용하지 않는다. 따라서 모든 정수 설정이 통일됐다고 문서화하면 안 된다. 이들은 기존 코드이며 이번 변경의 신규 회귀로 별도 반려하지 않는다.
|
||
|
||
5. **로컬 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 등은 명시적 비지원 범위다.
|
||
|
||
6. **기준 실패 주장:** 이 호스트의 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 본문에 현재 재현 결과 또는 당시 명령·환경 차이를 명시해야 한다.
|
||
|
||
7. **설계 경계 #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):
|
||
|
||
```bash
|
||
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]
|