Files
multi-agent-mux/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-6481e5b4.md
T

16 KiB
Raw Blame History

lib.sh 인라인 파이썬 분리 여부 — 손익 분석 및 최종 결정 (Rev.2)

  • job_id: 98393a97 (Rev.1 = 6481e5b4)
  • 역할: Planner
  • 반영한 이의제기: f3a0adf1 (agy, herdr:agy-creator-01) — [VERDICT: PASS WITH CHALLENGE]
  • 실측 하네스:
    • .mam/jobs/6481e5b4/claude-reports/proposed/probe_inline_vs_module.sh (Rev.1, 유효)
    • .mam/jobs/98393a97/claude-reports/proposed/probe_verify_py_coupling.sh (Rev.2 신규, 실행·검증 완료)
  • 저장소 변경: 없음

0. Rev.1 대비 변경 요약

agy 의 3개 주장이 전부 사실로 확인되었다. 반박할 것이 없다.

Rev.1 의 §5 D2 는 VERIFY_SESSION_PYTHON 을 두고 "verify_session_uuid 라는 단일 진입점만 갖는다" 고 썼다. 이것이 틀렸다. 소비자는 3곳이며, 그중 하나는 lib.sh 밖의 장기 실행 모니터다. 나는 lib.sh 안에서만 소비자를 찾았고 저장소 전체를 검색하지 않았다.

이의제기 항목 판정 Rev.2 반영
§1 reconcile.shMAM_VERIFY_PY 런타임 붕괴 [ADJUDICATION: SUSTAINED] D2 를 D2a/D2b 로 분할, 파사드 착지 방식 도입
§2.1 _validate_env_keyPYTHONPATH 차단 [ADJUDICATION: SUSTAINED] D0 을 내부 결합으로 한정 + 선행조건에서 심층방어로 강등
§2.2 find_workspace_uuidMAM_VERIFY_PY 결합 [ADJUDICATION: SUSTAINED] D4 에 명시적 의존 해소 단계 추가

결정 자체(부분 분리, 대형 블록 3개)는 바뀌지 않는다. 바뀌는 것은 어떻게 착지시키는가 이며, 그것이 이 이의제기의 기여다.

부수적으로 agy 의 서술 1건을 정정한다(§1.2) — 결론에는 영향이 없다.


1. 이의제기 판정

1.1 Primary — reconcile.shMAM_VERIFY_PY 결합: SUSTAINED

저장소 전체를 검색해 소비자를 전수 확인했다:

reconcile.sh:323   exec(os.environ['MAM_VERIFY_PY'])
reconcile.sh:862   … MAM_VERIFY_PY="$VERIFY_SESSION_PYTHON" env_python "$AGENT_SESSIONS_YAML"
reconcile.sh:864   … MAM_VERIFY_PY="$VERIFY_SESSION_PYTHON" atomic_dump_yaml "$AGENT_SESSIONS_YAML"
lib.sh:1329        VERIFY_SESSION_PYTHON='
lib.sh:1546/1548   verify_session_uuid   — 주입 + exec
lib.sh:1618/1627   find_workspace_uuid   — 주입 + exec

소비자는 3곳이다: verify_session_uuid(lib.sh), find_workspace_uuid(lib.sh), 그리고 reconcile.sh. Rev.1 이 "단일 진입점"이라고 한 것은 세 번째를 놓친 결과다.

reconcile.shlib.sh 를 source 한 뒤 셸 변수 $VERIFY_SESSION_PYTHON 을 자기 파이썬 heredoc 에 환경변수로 실어 보내고, 그 안에서 exec() 로 네임스페이스에 푼다. 셸 변수를 없애면 MAM_VERIFY_PY 가 빈 문자열이 되고 exec("") 는 아무것도 정의하지 않으며, 이후 첫 호출에서 NameError 가 난다. agy 가 서술한 메커니즘 그대로다.

이것이 특히 위험한 이유는 reconcile.sh백그라운드 모니터 루프라는 점이다. 대화형 명령이 아니라 watchdog 으로 돌기 때문에, 실패가 사용자 눈앞이 아니라 로그 안에서 조용히 일어난다.

1.2 정정: agy 가 사용 함수를 하나 더 셌다

agy 는 reconcile.shverify_session_uuid(), workspace_key(), mam_orchestrator_uuids() 를 쓴다고 했다. 실측:

VERIFY_SESSION_PYTHON 본문: 213 줄, 정의 함수 4개
   mam_orchestrator_uuids   reconcile.sh 참조 0회
   mam_row_own_uuid         reconcile.sh 참조 0회
   workspace_key            reconcile.sh 참조 1회  <- 사용
   verify_session_uuid      reconcile.sh 참조 6회  <- 사용

mam_orchestrator_uuidsreconcile.sh 에서 한 번도 참조되지 않는다. 실제 결합은 4개 중 2개다. 이는 결론을 바꾸지 않지만(2개든 3개든 제거하면 깨진다), D2b 에서 import 로 전환할 때 필요한 이름이 2개뿐이라는 사실은 작업량 산정에 쓸모가 있다.

1.3 _validate_env_keyPYTHONPATH 차단: SUSTAINED — 그리고 이것이 D0 을 재검토하게 만들었다

가드는 실재하고 실제로 동작한다:

lib.sh:  LD_PRELOAD|LD_LIBRARY_PATH|PYTHONPATH|PYTHONHOME|PYTHONINSPECT|PYTHONSTARTUP)
           echo "ERROR: Blocked environment variable: $key" >&2; return 1

실측:  env_python /dev/null PYTHONPATH=/tmp/x  →  ERROR: Blocked environment variable: PYTHONPATH

agy 가 지적한 대로 D0 을 "인자 전달"로 구현하면 즉시 막힌다. 그리고 agy 가 권고한 "내부 결합" 방식은 성립한다 — env_python 의 초기 envs 배열은 직접 구성되며 _validate_env_key 를 거치지 않기 때문이다. 이 방식은 가드를 우회하지도 약화시키지도 않는다: 가드의 목적은 외부 호출자가 인터프리터 탐색 경로를 조작하는 것을 막는 것이고, 프레임워크가 자기 자신의 패키지 경로를 넣는 것은 그 위협 모델 밖이다.

그런데 이 지적을 확인하다가 Rev.1 의 더 큰 오류를 발견했다. Rev.1 §3.2 는 D0 을 "분리의 필수 동반 조건" 이라고 단정했다. 검증해 보니 사실이 아니다:

child PYTHONPATH = /Users/…/multi-agent-mux/.agents/skills…
atomic_dump_yaml 의 env 호출에 -i 가 있는가: 없음 → 상속됨

env-i 없이 쓰므로 부모 환경이 그대로 상속되고, lib.sh:25export PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}" 가 이미 자식에 도달한다. Rev.1 이 근거로 든 실패 사례 (b)("호출자가 PYTHONPATH 를 지운 뒤")는 내가 만든 인위적 조건이었고, 실제 코드 경로에는 그런 호출자가 없다.

따라서 D0 은 선행 조건이 아니라 심층 방어다. 등급을 낮추고 순서에서 앞으로 끌어낼 이유도 없앤다. 이 정정은 agy 의 §2.1 이 아니었으면 하지 못했을 것이다.

1.4 find_workspace_uuid 의 결합: SUSTAINED

lib.sh:1618/1627 에서 동일 패턴이 확인된다. D4 가 find_workspace_uuid 본문을 옮길 때 exec(os.environ['MAM_VERIFY_PY'])from lib_py.verify_session import verify_session_uuid, workspace_key 로 바꾸지 않으면 같은 NameError 가 난다. D2 와 D4 는 독립 단계가 아니라 같은 결합을 공유한다.


2. 해법: 파사드로 착지시킨다 (실측 검증됨)

agy 는 두 선택지를 제시했다 — (1) lib.sh 에 하위 호환 파사드를 남기거나, (2) reconcile.sh 를 import 방식으로 고치거나. 두 개를 순서대로 다 한다. 그래야 이동과 소비자 수정이 분리된다.

2.1 파사드안 A: VERIFY_SESSION_PYTHON="$(cat lib_py/verify_session.py)"

셸 변수를 없애지 않고, 그 내용을 파일에서 읽어 채운다. 현재 213줄을 그대로 .py 로 추출해 실측했다:

(a) exec 계약 보존       : OK — 필요한 이름 전부 로드
(b) workspace_key 동작   : workspace_key(/a/b_c) = -a-b-c
(c) 작은따옴표 허용      : apostrophes fine

세 결과가 뜻하는 바:

  • (a)(b): 세 소비자 전부 코드 한 줄도 고치지 않는다. reconcile.sh 는 자기가 파사드를 보고 있다는 사실조차 모른다. NameError 위험이 원천적으로 없다.
  • (c) 가 핵심이다. 현행 VERIFY_SESSION_PYTHON='…' 는 홑따옴표 문자열이라 본문에 ' 를 넣을 수 없다(Rev.1 §2.3). 파사드는 $(cat …) 명령치환이므로 그 제약이 사라진다. 실험에서 # it's a comment — don't break 를 파일에 넣고도 exec 가 정상 동작했다.

파사드만으로도 분리 이득의 상당 부분이 즉시 실현된다: 정적 검사 대상이 되고(D5), 따옴표 제약이 없어지고, 파일로서 편집·리뷰·diff 가 가능해진다. 잃는 것은 exec 를 거치므로 트레이스백이 여전히 <string> 이라는 점뿐이다.

2.2 그다음에 소비자를 하나씩 import 로 전환

파사드가 자리잡은 뒤, 소비자 3곳을 각각 독립적으로 from lib_py.verify_session import verify_session_uuid, workspace_key 로 바꾼다. reconcile.sh 는 이름 2개만 필요하다(§1.2). 마지막 소비자가 전환되면 파사드를 제거한다.

이 순서의 이점은 어느 단계에서 멈춰도 시스템이 동작한다는 것이다. 파사드까지만 하고 멈춰도 되고, 소비자 1개만 전환하고 멈춰도 된다.


3. 결정 (Rev.1 에서 변경 없음)

부분 분리한다. 전면 분리도, 현행 유지도 아니다.

736줄의 이동 가능한 파이썬 중 대형 블록 3개(635줄)만 lib_py/ 로 분리한다. 소형 4개(101줄)와 shim 679줄 + 인라인 12개는 그대로 둔다.

이동 줄수 이유
atomic_dump_yaml 본문 O 213 최대 블록, 동시성·원자성 로직
VERIFY_SESSION_PYTHON O 213 따옴표 제약의 유일한 피해자 — 단 소비자 3곳(§1.1)
find_workspace_uuid 본문 O 209 P0-C 불변식 구현부 — 같은 결합 공유(§1.4)
소형 PYEOF 4개 X 101 파일 4개를 늘려도 얻는 것이 없음
shim + 인라인 12개 X 679+ PYTHONPATH 없이 도는 독립 스크립트 (측정된 제약)

근거(Rev.1 §1–§4)는 이의제기의 영향을 받지 않았다. 요약하면:

  • 현행 비용: CI 의 flake8/py_compile 이 lib.sh 를 제외하므로 736줄이 정적 검사 사각지대다. 주입한 SyntaxErrorbash -nsource 도 통과하고, 해당 함수 호출 시점에야 File "<stdin>", line 3 으로 나타난다. 213줄이 작은따옴표를 쓸 수 없다.
  • 분리 비용: 배포는 install.shfind . -type f + is_framework_owned 글롭이 자동 처리하므로 사실상 0(remove.sh 1줄). 신규 실패 모드는 PYTHONPATH 의존 1건이며, §1.3 에서 확인했듯 기본 경로에서는 이미 상속으로 해결되어 있다.

4. 실행 계획 (Rev.2)

D0. env_pythonPYTHONPATH 내부 결합심층 방어, 선행 조건 아님 ← §1.3 반영 envs 초기 배열에 "PYTHONPATH=$SKILL_DIR:${PYTHONPATH:-}"덧붙이는 형태로 추가한다. 인자로 전달하지 않는다_validate_env_key 가 막는다. 기존 상속 경로가 이미 동작하므로 이 단계는 생략해도 D1–D6 이 성립한다. 우선순위 최하.

D1. lib_py/ 패키지 골격 + remove.sh fallback 1줄 추가

D2a. VERIFY_SESSION_PYTHONlib_py/verify_session.py + 파사드 착지 ← §2.1 lib.sh 에서 213줄을 파일로 옮기고, 셸 변수는 VERIFY_SESSION_PYTHON="$(cat "$SKILL_DIR/lib_py/verify_session.py")" 로 대체한다. 소비자 3곳을 전혀 건드리지 않는다. 이 시점에 따옴표 제약이 사라지고 정적 검사가 가능해진다. 검증: reconcile.sh --once 를 실제로 돌려 NameError 가 없고 drift 판정 결과가 이전과 동일한지 확인한다. 이 단계의 회귀 테스트는 reconcile.sh 실행이지 lib.sh 단위 테스트가 아니다.

D2b. 소비자 3곳을 import 로 전환 (각각 독립) ← §2.2 ① verify_session_uuid(lib.sh:1546) → ② find_workspace_uuid(lib.sh:1618) → ③ reconcile.sh(862/864/323). ③ 은 이름 2개(verify_session_uuid, workspace_key)만 필요하다. 세 번째가 끝나면 파사드를 제거한다. 각 전환은 독립 커밋으로.

D3. atomic_dump_yaml 본문 → lib_py/atomic_yaml.py-m 실행으로 stdin 을 해방하고 AGENT_SESSIONS_MUTATION exec 계약을 보존한다(7132d954 스파이크 (f) 검증됨). 주의: reconcile.sh:864atomic_dump_yamlMAM_VERIFY_PY 와 함께 호출하므로, D3 은 D2b-③ 이후에 하거나 파사드가 살아 있는 동안 해야 한다.

D4. find_workspace_uuid 본문 → lib_py/workspace_uuid.py — D2b-② 를 포함한다(같은 결합, §1.4).

D5. CI 에 lib_py/ 를 flake8 / py_compile 경로로 추가 — §2.1 의 이득을 실현하는 지점. 빠뜨리면 분리의 최대 명분이 사라진다. D2a 직후에 해도 된다(파사드 상태에서도 .py 파일은 검사 가능).

D6. 소형 블록 4개는 명시적으로 유지하고 이유를 주석으로 남긴다.

검증 원칙: 각 단계마다 HEAD 구현과 새 구현에 동일 입력을 주고 stdout·종료코드·YAML/DB 산출물을 바이트 비교한다. 추가로, MAM_VERIFY_PY 를 건드리는 모든 단계(D2a/D2b/D3/D4)는 reconcile.sh --once 실행을 회귀 게이트에 포함한다 — 이의제기가 지적한 붕괴는 lib.sh 단위 테스트로는 잡히지 않는다.


5. 다른 작업과의 순서

  1. 3aee63cf Rev.2 Phase 1 (W1W5) — P0 2건. lib_py 에 의존하지 않으므로 가장 먼저.
  2. D1 → D2a → D5 — 파사드까지. 소비자 무수정.
  3. D2b → D3 → D4 — import 전환 및 나머지 블록.
  4. 3aee63cf Rev.2 Phase 2 이후 — 에이전트 레지스트리(W6a)가 lib_py 위에 올라간다.
  5. D0 — 아무 때나. 다른 무엇의 선행 조건도 아니다.

전 보고서 작성 시점에 Creator 가 이미 1번에 착수했고, 워킹트리의 lib.sh 차이에 W2(hermes SELECT cwd FROM sessions)가 들어 있었다. D2a 가 옮기려는 213줄이 그 편집 대상이므로, Phase 1 이 커밋으로 확정된 뒤 D1 을 시작해야 한다.


6. 리스크 및 미측정 항목

6.1 reconcile.sh 의 나머지 결합은 전수 확인하지 않았다. MAM_VERIFY_PY 는 저장소 전체 grep 으로 전수 확인했으나(§1.1), reconcile.shlib.sh다른 셸 변수나 함수에 유사하게 의존하는지는 조사하지 않았다. D2b-③ 착수 전 reconcile.sh 840줄의 lib.sh 의존 전수 조사를 별도로 수행할 것. 이번 이의제기가 정확히 그 종류의 누락에서 나왔다.

6.2 §2.1 의 파사드 실험은 현재 213줄 본문으로 했다. Creator 가 진행 중인 W1/W2 가 이 본문을 수정 중이므로, 확정 후 파사드 실험을 한 번 더 돌려야 한다. 하네스가 lib.sh 에서 본문을 직접 추출하므로 재실행만 하면 된다.

6.3 $(cat …) 파사드는 파일이 없을 때 조용히 빈 문자열이 된다. 이는 MAM_VERIFY_PY=""exec("")NameError 라는 이의제기가 지적한 바로 그 실패 모드와 같다. D2a 는 파사드에 존재 검사를 반드시 포함해야 한다 — 파일이 없으면 명시적으로 실패해야지, 빈 문자열로 넘어가면 안 된다.

6.4 lib_py 는 PyYAML 외의 서드파티 의존을 추가해서는 안 된다. 가용성은 시스템 python3(6.0.3)와 .venv(6.0.3)에서 확인했으나 이 머신 한정이다.

6.5 shellcheck 가 인용 heredoc 본문을 검사하지 않는다는 것은 직접 실행으로 확인하지 못했다 — 로컬 미설치. shellcheck 의 문서화된 동작에 근거한 판단이며, CI 경로 배제는 설정 파일에서 직접 확인한 사실이므로 결론은 유지된다.

6.6 Rev.1 의 측정 기준은 커밋되지 않은 워킹트리였다 (HEAD 2275 vs 워킹트리 2284, 6줄 차이). 대형 블록 3개의 식별과 200줄 임계 기준은 양쪽에서 동일하다.


7. 판정

이의제기 3건이 전부 사실로 확인되어 모두 수용했다. 반박한 항목은 없고, 서술 1건(mam_orchestrator_uuids 사용 여부)만 정정했다. 이의제기를 검증하는 과정에서 Rev.1 자신의 오류 2건을 함께 발견했다 — D2 의 "단일 진입점" 서술(§1.1)과 D0 의 "필수 선행 조건" 등급(§1.3).

[ADJUDICATION: SUSTAINED]f3a0adf1 §1 (reconcile.sh 결합), §2.1 (_validate_env_key), §2.2 (find_workspace_uuid 결합)

결정: 부분 분리 유지 — 대형 블록 3개(635줄) 이동, 단 파사드로 착지시킨 뒤 소비자를 하나씩 전환.

[VERDICT: PASS]

(이 토큰은 이 결정 보고서 산출물의 완성도를 뜻한다. 감사 대상 코드에 대한 판정이 아니다.)