From 0e74aa1e4ec21edf24c817665cf0577d9caafe53 Mon Sep 17 00:00:00 2001 From: Godopu Date: Sun, 6 Sep 2026 20:11:40 +0900 Subject: [PATCH] docs(issue-5): record the evidence requirement, the audit lock and the ack rule Three contract points the round 2 fixes established. An idempotent re-receipt is an ACK in the table, so a caller must treat it as success. Only a genuine refusal is non-zero, and an invalid status stays the usage error it always was. That distinction is what keeps the wrapper's approved path from aborting under set -e. Completion attribution now requires evidence to be present, not merely non-contradictory, and names where it may be read from: a collector envelope beside the payload, or a non-native source's own payload. Never from inside a native Herdr payload, because MAM fields are forbidden there and one appearing anyway is not evidence of anything. The audit mirror's ordering guard is documented as a locked read-compare-write, with the reason a bare sequence number was not enough, and the note that the lock is deliberately taken outside the registry transaction. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd --- docs/contracts/issue-5/phase2a-shadow.md | 15 +++++++++++++-- docs/contracts/issue-5/state-transitions.md | 4 +++- docs/contracts/issue-5/task.md | 2 ++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/contracts/issue-5/phase2a-shadow.md b/docs/contracts/issue-5/phase2a-shadow.md index 88a354a..ae0649f 100644 --- a/docs/contracts/issue-5/phase2a-shadow.md +++ b/docs/contracts/issue-5/phase2a-shadow.md @@ -31,7 +31,7 @@ Shadow 러너는 애초에 쓰기 경로가 없으므로 **프로세스 종료 | `publish_event` (에이전트) | 자신의 작업을 `running`/`completed`/`error`/`cancelled`로 전이 | **terminal을 되돌리는 것** (`redispatch`/`finalize` 거부) | | `registry_cli` (래퍼·운영자) | 위 전부 + `redispatch`(terminal→`pending`, iteration 증가 필수) + `finalize`(terminal→`error`) | iteration을 되돌리는 재생 | -**CLI 종료 코드 변경**: `registry.py status`/`update`가 CAS에 의해 거부되면 이제 **stderr에 사유를 내고 non-zero(4)** 로 종료한다. Phase 2a 이전에는 거부가 불가능해 항상 0이었고, 그 침묵이 반복 위임이 조용히 깨진 것을 감췄다. +**CLI 종료 코드**: `registry.py status`/`update`는 **진짜 거부**에만 non-zero(4)를 낸다. 잘못된 status 값은 rc 1(2a 이전 계약 유지), **멱등 ACK(이미 그 상태임)는 rc 0**이다 — 래퍼가 `set -e` 아래에서 승인 직후 `--set completed`를 실행하므로 이를 실패로 보고하면 성공 경로가 중단된다(교차 리뷰 F-C/F-D). Phase 2a 이전에는 거부가 불가능해 항상 0이었고, 그 침묵이 반복 위임이 조용히 깨진 것을 감췄다. ## 3. 락 규율 (P2A-01) @@ -40,7 +40,7 @@ with registry_lock: 판정 + 레코드 1회 쓰기 + 알림 큐잉 ← 락 해제 후: 감사 로그 flush, 발행 등 느린 작업 ``` -감사 mirror가 락 밖으로 나가면서 순서 보장을 잃을 수 있으므로, 전이마다 단조 증가하는 `transition_seq`를 레코드와 알림에 함께 싣는다. **`update_logged_status`는 기존 `status.json`의 `transition_seq`가 더 크거나 같으면 쓰기를 건너뛴다** — 지연된 flush가 최신 상태를 되돌리지 못하게 하는 것이며, 락이 공짜로 주던 순차 일관성을 복원한다(교차 리뷰 F-B). 이 형태는 새로 만든 것이 아니라 `pick_pending`이 이미 쓰던 패턴을 모든 전이로 일반화한 것이다. +감사 mirror가 락 밖으로 나가면서 순서 보장을 잃을 수 있으므로, 전이마다 단조 증가하는 `transition_seq`를 레코드와 알림에 함께 싣는다. **`update_logged_status`는 전용 per-job 감사 락 아래에서 읽기-비교-쓰기를 수행하고, 기존 `status.json`의 `transition_seq`가 더 크거나 같으면 쓰기를 건너뛴다**(교차 리뷰 R4: 락 없는 비교-후-쓰기는 두 flush가 모두 옛 값을 읽고 느린 쪽이 최신을 덮어쓸 수 있어, 시퀀스 번호만으로는 순서가 강제되지 않는다). 이 락은 **레지스트리 트랜잭션 밖**에서 잡으므로 느린 감사 I/O가 트랜잭션을 늘리지 않는다. 각 쓰기는 고유 임시 파일을 쓴다 — 지연된 flush가 최신 상태를 되돌리지 못하게 하는 것이며, 락이 공짜로 주던 순차 일관성을 복원한다(교차 리뷰 F-B). 이 형태는 새로 만든 것이 아니라 `pick_pending`이 이미 쓰던 패턴을 모든 전이로 일반화한 것이다. ## 4. `herdr agent wait` 사용 규칙 @@ -50,6 +50,17 @@ with registry_lock: 판정 + 레코드 1회 쓰기 + 알림 큐잉 ← - **타임아웃은 건강한 응답이다.** 소켓이 답했고 상태가 안 바뀐 것뿐이다. 전송 실패와 구분해야 한다. - 전송 실패 시 백오프는 1→2→4→8→16초 상한이며, **캐시된 스냅샷을 폐기**한다. 단절 구간 이전의 상태로 추론하면 단절이 완료처럼 보인다. +## 4-1. 완료 귀속에 요구되는 증거 (교차 리뷰 R1) + +`candidate_approved`는 **증거의 존재**를 요구한다. 값이 있을 때만 비교하는 방식은, 아무 식별자도 없는 증거를 dispatch 레코드의 자기 선언만으로 승인해 버린다 — 이 검사가 대체하려던 바로 그 자기 선언이다. + +| 요구 증거 | 위치 | 없으면 | +|---|---|---| +| `job_id`, `attempt_id`, `agent_session_uuid`, `pane_process_generation` | 타임라인 항목의 **`collector_attribution` 봉투** (또는 event/filesystem 등 **비-herdr** 소스의 payload) | `hold` | +| 리포트 매니페스트의 `attempt_id` | filesystem 소스 payload의 `report_manifest` | `hold` | + +**네이티브 경계 유지**: 식별자는 **네이티브 Herdr payload 안에서 읽지 않는다.** Phase 0이 네이티브 페이로드에 MAM 필드 주입을 금지하므로, 거기 나타난 식별자는 조작이거나 계약 위반이며 어느 쪽이든 증거가 아니다. 수집기의 연관 정보는 payload **옆**의 봉투에 둔다. + ## 5. 판정 순서 (fail-closed) 1. 전송 이상 → `hold_reconnect` (증거 자체가 무효) diff --git a/docs/contracts/issue-5/state-transitions.md b/docs/contracts/issue-5/state-transitions.md index 5f9cd08..87e5256 100644 --- a/docs/contracts/issue-5/state-transitions.md +++ b/docs/contracts/issue-5/state-transitions.md @@ -1,6 +1,6 @@ # 📑 Target State Transitions and CAS Specification -- **Document Version**: Rev.4 (운영자 권한 전이 2행 및 권한 축 추가 — 교차 리뷰 반영) +- **Document Version**: Rev.5 (멱등 ACK 구분 및 관측 증거 필수 요건 명문화) - **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5) - **Scope**: Target Compare-And-Swap (CAS) State Machine & Attribution Contract @@ -54,6 +54,8 @@ $$\text{Attribution Tuple} = (\text{job\_id}, \text{attempt\_id}, \text{agent\_s > 🔑 **권한 축(Authority)**: `redispatch`·`finalize`는 `source ∈ {registry_cli, operator}`에서만 허용된다. 에이전트(`publish_event`)는 **자신의 terminal을 되돌릴 수 없다** — 이것이 CAS 표의 존재 이유이며, 위 두 행은 그 원칙의 예외가 아니라 래퍼가 수행하는 별개 전이를 이름 붙인 것이다. > +> 🔑 **멱등 ACK는 거부가 아니다**: `terminal` 상태에서 같은 terminal을 재수신하면 표는 이를 **No-op ACK**로 정의한다. 호출자(운영자 CLI)는 이를 성공(rc 0)으로 취급해야 한다. 실제 거부(상충 신호·권한 부족)만 non-zero다. — 교차 리뷰 `cb792e91` F-C +> > 🔑 **status는 요청으로만 설정된다**: 임의 필드로 실린 `status`는 경계 내부에서 거부된다(`progress`가 terminal을 커밋하는 우회 차단). 알 수 없는 요청명도 거부된다. `pending`도 소유 요청(`redispatch`)을 갖는다. --- diff --git a/docs/contracts/issue-5/task.md b/docs/contracts/issue-5/task.md index d69e80f..e9050f6 100644 --- a/docs/contracts/issue-5/task.md +++ b/docs/contracts/issue-5/task.md @@ -87,6 +87,8 @@ - *증거*: 커밋 `964abc6`. `ShadowRunner`는 engine 참조를 **갖지 않는다**(구조적 차단, AST 단정). 측정: 전체 시나리오 실행 전후 **레지스트리 디렉터리 전 파일 SHA-256 동일**. - [x] **P2A-05** 교차 리뷰(`68abd0ff` Claude / `cae12436` Codex) 지적 5건 전건 수용·수정. - *증거*: 커밋 `56d08ef`. F-A(래퍼 loop/discuss 전이 2건 누락 → 운영자 권한 요청 `redispatch`/`finalize` 신설 + CLI 거부 시 non-zero), F-B(감사 mirror 순서 회귀 → `transition_seq` 비교), R1(관측 식별자 미대조 → `collect_observed_identity` 전량 대조 + 매니페스트 attempt 검증), R2(`expected_attempt`가 레코드에 없으면 무시 → fail-closed), R3(raw `status` 필드 우회 → 요청만이 status를 설정). 신규 회귀 11종 전부 수정 전 코드에서 실패함을 역검증. +- [x] **P2A-06** 교차 리뷰 Round 2(`cb792e91` Claude / `0026165b` Codex) 지적 4건 전건 수용·수정. + - *증거*: 커밋 `cb575fa`. F-C(멱등 ACK를 거부로 분류 → 래퍼 성공 경로가 `set -e`로 중단; 멱등은 rc 0, 진짜 거부만 rc 4), F-D(`VALID_STATUSES` 검증 소실 → 두 모드 공통 rc 1·레코드 불변), R1(증거 부재 시에도 승인 → 4식별자와 매니페스트 attempt **필수**, 식별자는 네이티브 payload가 아닌 `collector_attribution` 봉투에서 수집), R4(비원자적 비교-후-쓰기 → per-job 감사 락 + 고유 임시 파일). 신규 회귀 8종 전부 수정 전 코드에서 실패함을 역검증. - [x] **P2A-04** race/attempt 재사용/부분 report/도구 실행/blocked/unknown/재시작 시나리오. - *증거*: 신규 5종(`race_double_dispatch`, `attempt_reuse`, `partial_report`, `unknown_status`, `restart_midflight`)을 Phase 0 스키마로 작성해 **Phase 0의 `validate_watcher_scenario`를 통과**시켰고, 기존 7종과 합쳐 **12종 전부 `expected_policy.decision`과 일치**함을 단정한다.