Files
multi-agent-mux/docs/contracts/issue-5/baseline.md
T

14 KiB

📑 Issue #5 Baseline Contract Specification


1. 개요 및 계약 정의 원칙

본 문서는 Issue #5(Python 모듈화 및 lib_py 승격)의 착수 기준선(Baseline)으로서, 현재 코드베이스(run_loop.sh, lib.sh, delegate-job 도구군)에 구현되어 동작 중인 CLI, 이벤트, 리포트, 락(Lock) 및 상태 전이 계약을 엄밀하게 분류하여 명문화합니다.

각 항목은 다음 세 범주로 구분하여 기록합니다:

  • [Observed]: 현재 소스 코드 및 단위 테스트에서 실제로 확인된 동작.
  • [Proposed]: 향후 Phase 1–4에서 구현하고자 하는 목표 동작.
  • [Gap]: 현행 동작과 목표 동작 사이의 불일치, 결함 또는 미구현 사항.

2. CLI 인터페이스 계약 (Orchestrator & Delegation)

2-1. run_loop.sh CLI 옵션 및 인수 파싱 계약

옵션 기본값 제약 및 검증 규칙 현행 소스 위치 대응 테스트 범주
--creator <session> 없음 (필수) 비어있지 않은 문자열 필수(L95). 세션 레지스트리(.mam/agent-sessions.yaml / .db) 등록 및 실행 중 검증은 L160–185에서 수행 run_loop.sh:79, 95-98, 160-185 test_loop_cli.py:test_creator_parses_and_reaches_session_check [Observed]
--task <goal-text> 없음 (필수) 작업 목표 문자열. 누락 시 fast-fail 및 usage 출력 후 exit 1 run_loop.sh:85, 95-98 test_loop_cli.py:test_missing_creator_and_task_fail_fast [Observed]
--target-agent <name> 제거됨 사용 즉시 에러 메시지 출력 후 exit 1 반환 (하위 호환 폐기) run_loop.sh:80-83 test_loop_cli.py:test_target_agent_is_rejected [Observed]
--plan false 플래너 개입 및 설계 단계 활성화 플래그 run_loop.sh:56 test_loop_cli.py:test_planner_without_plan_fail_fast [Observed]
--planner <session> 자동 조회 --plan 플래그 없이 지정 시 즉시 exit 1 반환. 미지정 시 등록된 planner 역할 세션 자동 선택 run_loop.sh:84, 100-105 test_loop_cli.py:test_explicit_planner_unregistered, test_explicit_planner_skips_auto_resolve [Observed]
--plan-talk <N> 1 숫자 regex(^[0-9]+$) 검증. 에러 메시지는 양수를 요구하지만 regex는 0을 허용함. 0 지정 시 토론 루프($turn -le $PLAN_TALK_TURNS)를 완전히 건너뜀 run_loop.sh:57-62, 442 test_loop_cli.py (regex 파싱) [Observed]
--reviewer <A,B> 없음 쉼표 구분 리뷰어 세션 목록 run_loop.sh:63 test_loop_cli.py [Observed]
--all-reviewer false 모든 활성 리뷰어 세션 강제. --reviewer와 동시 지정 시 --all-reviewer가 우선하며 --reviewer 목록은 무시되고 경고 로그 출력 run_loop.sh:64, 192-196 현행 전용 CLI 테스트 부재 ([Gap]) [Observed]
--max-loop <N> 3 양수 정수 검증(^[0-9]+$ and N > 0). 0 이하 시 에러 반환 run_loop.sh:65-70 test_loop_cli.py [Observed]
--max-rebut <N> 1 비음수 정수 검증(^[0-9]+$). 0 지정 시 반론(Rebuttal) 완전 비활성화 run_loop.sh:71-76 test_o1_rebuttal.py:test_v3_rebut_option_parsing_and_budget [Observed]
REBUT_TOTAL_BUDGET MAX_REBUT * MAX_LOOP 전체 실행 기간 동안의 총 반론 허용 상한(Run-wide budget). 조기 소진 시 추가 반론 금지 run_loop.sh:93, 762-766 test_o1_rebuttal.py:test_v3_rebut_option_parsing_and_budget [Observed]
--verbose false 상세 실행 타임라인 추적 출력 활성화 플래그 run_loop.sh:77 N/A [Observed]
--cleanup false 루프 성공 종료 시 생성된 임시 작업 디렉터리(.mam/jobs/<job>) 및 가입자 출력 파일 삭제 run_loop.sh:78, 921-930 N/A [Observed]
-h, --help N/A 도움말 출력 후 exit 1로 비정상 종료 (현재 코드의 관행적 동작) run_loop.sh:50, 86 test_loop_cli.py:test_help_lists_creator_not_target_agent [Observed]

⚠️ Help Exit Code Gap ([Gap]): 현재 run_loop.sh:50은 usage() 내부에서 exit 1을 호출하여 --help 요청 시에도 exit 1을 반환합니다. 표준 CLI 관례(exit 0)와 다르나, 기존 서브프로세스 테스트들이 exit 1에 의존하므로 Phase 0에서는 현행 동작을 유지하며 의도적 변경은 Phase 3 wrapper 승격 시 별도 Decision으로 다룹니다.

2-2. 환경, 경로 및 수명주기 계약

  1. 파이썬 인터프리터 탐색 (_delegate_py_bin):
    • AGENT_PYTHON_BIN ➔ VIRTUAL_ENV/bin/python ➔ 상위 디렉터리 .venv/bin/python 탐색 ➔ 시스템 python3 순으로 캐싱 및 결정 (lib.sh:1841-1861). [Observed]
  2. 런타임 고정 복사본 (B-13 Freeze Snapshot):
    • 루프 기동 시 .agents/skills/ 트리를 /tmp/mam-loop-freeze.XXXXXX로 복사 후 해당 위치의 run_loop.sh로 exec 재실행 (run_loop.sh:112-127). [Observed]
    • 단, multi-agent-mux-create, resume, stop 등 1회성 스크립트는 Live Tree를 참조 ([Observed]).
  3. 루프 가드 및 시그널 정리:
    • MAM_LOOP_MARKER(.mam/loop-guard-active)에 pid, lstart, started 타임스탬프를 원자적 하드링크로 기록 (loop_lock.sh:69-75). [Observed]
    • trap _mam_release_guard EXIT INT TERM HUP을 통해 비정상 종료 시에도 안전하게 가드 파일 및 Freeze 임시 디렉터리 정리 (run_loop.sh:189). [Observed]
  4. 작업 위임 함수 계약:
    • delegate_submit_job: registry.py register를 실행하고 생성된 32-bit hex JID를 stdout으로만 출력 (lib.sh:1876-1889). [Observed]
    • delegate_publish_event: Non-fatal 계약 (|| true). 누락된 JID, 스크립트 부재, 브로커 전송 실패 시에도 0을 반환하여 호출자를 중단시키지 않음 (lib.sh:1897-1905). [Observed]
    • start_watchdog: 백그라운드에서 reconcile.sh --subscribe --idle-timeout 0를 실행하고 해당 데몬 PID를 stdout으로 반환 (lib.sh:1908-1938). [Observed]

3. 이벤트 스키마 및 수명주기 계약 (MQTT & Registry)

3-1. 이벤트 스키마 v1 명세

  • 필수 필드: schema_version (1), seq (단조 증가 양의 정수), job_id (JID), event (이벤트명), timestamp (ISO-8601 UTC YYYY-MM-DDTHH:MM:SSZ), detail (문자열 설명).
  • 확장 필드: data (딕셔너리), data.hmac_sig (HMAC-SHA256 16진수 서명).

3-2. 이벤트와 레지스트리 상태(Status) 매핑 현황

stateDiagram-v2
    [*] --> pending: register_job
    pending --> running: pick_pending / event 'started'
    running --> running: event 'progress' (상태 변경 없음)
    running --> running: event 'permission_required' (상태 변경 없음)
    running --> completed: event 'completed' (Terminal)
    running --> error: event 'error' (Terminal)
    running --> cancelled: event 'cancelled' / stop_session (Terminal)
이벤트 이름 레지스트리 상태 전이 지속성(Retain) 설명 범주
started running false 작업자가 작업을 수락하고 실행을 시작함 [Observed]
progress 전이 없음 (running 유지) false 중간 진행 경과 보고. 레지스트리 status는 미변경 [Observed]
permission_required 전이 없음 (running 유지) false 사용자 권한 승인 대기. 레지스트리 status는 미변경 [Observed]
completed completed true 작업 성공 완료. Terminal 상태 [Observed]
error error true 작업 실패 종료. Terminal 상태 [Observed]
cancelled cancelled true 세션 중단 등에 의한 취소. Terminal 상태 [Observed]

⚠️ 이벤트-상태 간극 Gap ([Gap]):

  1. progress와 permission_required는 이벤트로 발행되지만 레지스트리 status enum(VALID_STATUSES = ('pending', 'running', 'completed', 'error', 'cancelled'))에 포함되지 않아, 레지스트리 파일만으로는 권한 승인 대기 여부를 직접 알 수 없습니다 (mqtt_common.py:44-48).
  2. publish_event.py의 Terminal 이벤트는 ("completed", "error", "cancelled")이지만, job_subscriber.py:46의 Terminal 이벤트는 ("completed", "error")로 정의되어 있습니다. 가입자가 디스크 폴백(_check_disk_fallback)을 수행할 때 비로소 cancelled 상태를 감지하여 가상 error 이벤트(synth_event="error")로 변환하여 처리합니다 (job_subscriber.py:80).
  3. Terminal Non-reentry ([Proposed] vs [Gap]): 한 번 Terminal 상태에 도달한 후 중복/상충 이벤트에 의해 상태가 역행하지 않는 불변식은 Phase 2a의 목표 계약([Proposed])이며, 현행 update_job_status는 락 내에서 단순 필드 덮어쓰기(record.update(fields))를 수행하므로 완벽한 비역행이 보장되지 않는 간극([Gap])이 있습니다.

3-3. 보안 및 재생 공격 방어

  1. 인증 모드 분리: auth_token == null일 때는 HMAC 검증 통과 (PoC 모드). auth_token != null일 때는 data.hmac_sig 검증 실패 시 즉시 폐기 (job_subscriber.py:122-126). [Observed]
  2. 단조 시퀀스 검증: 가입자는 수신된 페이로드의 seq가 이전 last_seq보다 큰 경우에만 처리하고, 같거나 작은 패킷은 replay 공격으로 간주하여 drop (job_subscriber.py:127-136). [Observed]
  3. 로컬 감사 추적(Audit Log): MQTT 전송 성공/실패와 무관하게 append_event를 통해 .mam/delegate_job_logs/<job_id>/{meta,events,status}에 모든 시도 기록 (mqtt_common.py:122, publish_event.py:208-219). [Observed]
  4. MQTT 전송 실패 시 로컬 상태 기록: 브로커 접속 불가 시에도 publish_event.py는 로컬 레지스트리의 상태를 new_status로 동기화한 뒤 exit code 2로 종료 (publish_event.py:223-233). [Observed]

4. 리포트 파싱 및 승격 계약 (Report & Promotion)

4-1. 판정 토큰(Verdict) 추출 로직

  • run_loop.sh:204-210: has_verdict "$file" "$verdict"는 파일의 **마지막 비공백 행(last non-blank line)**을 추출하여 ^\[VERDICT: ${verdict}\][[:space:]]*\r?$ 패턴과 일치하는지 검사합니다.
  • [Gap]: 마지막 행이 마크다운 코드 블록(```) 내부에 있는 예제 문자열인 경우에도 현재 셸 정규식은 이를 유효한 판정 토큰으로 오인할 수 있습니다. Phase 2의 엄격한 파서(Strict Parser)는 코드 펜스 내부를 제외해야 합니다.
  • 리포트 파일 위치 탐색(find_report): find ".mam/jobs/$1" -maxdepth 2 -name "report-final.md" | head -n 1로 첫 번째 일치 파일을 취하며, 정확한 단일 경로 강제가 아닙니다 (run_loop.sh:234). [Observed]

4-2. 승격(Promotion) 및 정리(Cleanup) 계약

  1. 계획서 승격: CURRENT_PLAN을 .agents/reports/$PLANNER_SESSION/plan-${PLAN_JOB_ID}.md.tmp에 기록 후 mv -f로 원자적 교체 (run_loop.sh:895-901). [Observed]
  2. 리뷰 리포트 승격: 각 리뷰어의 report-final.md를 .agents/reports/$rev/report-${job_id}.md로 복사 및 이동 (run_loop.sh:903-916). [Observed]
  3. 배치 비원자성 ([Gap]): 다수의 리뷰 리포트를 승격할 때 개별 파일 단위로 cp && mv를 수행하므로, 중간 크래시 발생 시 일부 리포트만 승격되는 부분 성공 위험이 존재합니다.

5. 잠금(Lock) 및 동시성 제어 계약

5-1. 락 체계 및 획득 순서

  1. Workspace Loop Guard: .mam/loop-guard-active (원자적 하드링크 + pid/lstart 스틸 재시도).
  2. Session Registry Lock: .mam/agent-sessions.db (SQLite WAL 동시성 및 트랜잭션).
  3. Job Registry Lock: .mam/jobs/<id>.lock 또는 레지스트리 디렉터리 락 (fcntl.flock(LOCK_EX)).

5-2. update_job_status의 비-CAS 특성 ([Gap])

  • 현재 mqtt_common.py:377-403의 update_job_status는 락을 획득한 후 record.update(fields)로 단순히 필드를 덮어쓰며, 호출자가 기대한 이전 상태(expected_status)나 attempt 버전을 비교하지 않습니다.
  • 또한 registry.py:129-131의 pick_pending은 update_job_status를 거치지 않고 직접 파일에 chosen["status"] = "running"을 씁니다.
  • [Proposed]: Phase 2a에서는 모든 상태 변경이 단일 엔진의 원자적 CAS(Compare-And-Swap) 인터페이스를 통해서만 수행되도록 통일해야 합니다.

6. 소스 및 테스트 매핑 요약

구성요소 핵심 소스 파일 핵심 테스트 파일 기준선 상태
오케스트레이터 CLI run_loop.sh tests/test_loop_cli.py 9개 테스트 통과
루프 가드 락 loop_lock.sh tests/test_o2_race_free_lock.py 22개 테스트 통과
반론 상한선 run_loop.sh tests/test_o1_rebuttal.py 10개 테스트 통과
위임 헬퍼 lib.sh tests/test_tier1_unit.py 단위 테스트 통과
작업 레지스트리 registry.py, mqtt_common.py tests/test_tier2_component.py fcntl 기반 락킹