- Fix Codex [P1]: add SUPPORTED_SOURCE_KINDS dispatch and enforce fail-closed rejection for non-herdr observed items without independent schema - Add strict correspondence checks for native Herdr observed items (agent_status, native schema, session UUID) - Add negative test reproducing Codex in-memory mutation of finalized_candidate - Fix Claude Finding 2: update VALID_STATUSES citation in baseline.md to registry.py:38 - Update Phase 0 collected test counts to 499 in contract docs
14 KiB
14 KiB
📑 Issue #5 Baseline Contract Specification
- Document Version: Rev.3 (Post Cross-Review Corrections)
- Issue: Gitea Issue #5 —
[Refactor] lib.sh 작업 위임 로직 및 run_loop.sh 오케스트레이터의 Python 모듈화 (lib_py 승격) - Baseline Commit:
47640e2fd7b108be29446a9384a343727e87edf8 - Environment Baseline: Python 3.9.6, Herdr 0.8.2, macOS Darwin 24.3.0
- Test Suite Baseline: 469 tests collected initially; 499 tests collected with Phase 0 contract suite (+30 contract & mutation tests)
- Status: Phase 0 Baseline Contract (Observed vs. Proposed vs. Gap)
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. 환경, 경로 및 수명주기 계약
- 파이썬 인터프리터 탐색 (
_delegate_py_bin):AGENT_PYTHON_BIN➔VIRTUAL_ENV/bin/python➔ 상위 디렉터리.venv/bin/python탐색 ➔ 시스템python3순으로 캐싱 및 결정 (lib.sh:1841-1861).[Observed]
- 런타임 고정 복사본 (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]).
- 루프 기동 시
- 루프 가드 및 시그널 정리:
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]
- 작업 위임 함수 계약:
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 UTCYYYY-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]):
progress와permission_required는 이벤트로 발행되지만 레지스트리statusenum(VALID_STATUSES = ('pending', 'running', 'completed', 'error', 'cancelled'))에 포함되지 않아, 레지스트리 파일만으로는 권한 승인 대기 여부를 직접 알 수 없습니다 (registry.py:38).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).- Terminal Non-reentry (
[Proposed]vs[Gap]): 한 번 Terminal 상태에 도달한 후 중복/상충 이벤트에 의해 상태가 역행하지 않는 불변식은 Phase 2a의 목표 계약([Proposed])이며, 현행update_job_status는 락 내에서 단순 필드 덮어쓰기(record.update(fields))를 수행하므로 완벽한 비역행이 보장되지 않는 간극([Gap])이 있습니다.
3-3. 보안 및 재생 공격 방어
- 인증 모드 분리:
auth_token == null일 때는 HMAC 검증 통과 (PoC 모드).auth_token != null일 때는data.hmac_sig검증 실패 시 즉시 폐기 (job_subscriber.py:122-126).[Observed] - 단조 시퀀스 검증: 가입자는 수신된 페이로드의
seq가 이전last_seq보다 큰 경우에만 처리하고, 같거나 작은 패킷은 replay 공격으로 간주하여 drop (job_subscriber.py:127-136).[Observed] - 로컬 감사 추적(Audit Log): MQTT 전송 성공/실패와 무관하게
append_event를 통해.mam/delegate_job_logs/<job_id>/{meta,events,status}에 모든 시도 기록 (mqtt_common.py:122, publish_event.py:208-219).[Observed] - 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) 계약
- 계획서 승격:
CURRENT_PLAN을.agents/reports/$PLANNER_SESSION/plan-${PLAN_JOB_ID}.md.tmp에 기록 후mv -f로 원자적 교체 (run_loop.sh:895-901).[Observed] - 리뷰 리포트 승격: 각 리뷰어의
report-final.md를.agents/reports/$rev/report-${job_id}.md로 복사 및 이동 (run_loop.sh:903-916).[Observed] - 배치 비원자성 (
[Gap]): 다수의 리뷰 리포트를 승격할 때 개별 파일 단위로cp && mv를 수행하므로, 중간 크래시 발생 시 일부 리포트만 승격되는 부분 성공 위험이 존재합니다.
5. 잠금(Lock) 및 동시성 제어 계약
5-1. 락 체계 및 획득 순서
- Workspace Loop Guard:
.mam/loop-guard-active(원자적 하드링크 +pid/lstart스틸 재시도). - Session Registry Lock:
.mam/agent-sessions.db(SQLite WAL 동시성 및 트랜잭션). - 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 기반 락킹 |