[verified] Bind review publication authority to the current request

This commit is contained in:
2026-10-15 01:41:52 +00:00
parent abf4d07472
commit b6f87db623
2 changed files with 17 additions and 2 deletions
@@ -40,7 +40,7 @@ Bootstrap 이후 code review는 canonical [Repository cache 운영 지침](../..
## 진입 조건
1. 식별자와 review role의 인증 출처·stable account ID를 검증한다. Impl 및 spec role과 다른 identity여야 하며 다른 role credential로 fallback하지 않는다.
1. 식별자와 review role의 인증 출처·stable account ID를 검증한다. Impl 및 spec role과 다른 identity여야 하며 다른 role credential로 fallback하지 않는다. 현재 요청의 원문·대상·승인 범위와 확인 가능한 식별자(예: session/message ID)를 독립적으로 대조해 검증된 요청 참조를 기존 workflow artifact에 고정한다. workflow label이나 run-id suffix에서 요청 식별자를 추론하지 않는다. 이 label들은 게시 권한 근거가 아니다.
2. PR이 open이고 source Issue에 exact `Closes #<issue-number>` 하나로 연결되며 default branch를 base로 하는지 확인한다.
3. 현재 계약 digest와 PR full head SHA에 결합된 spec-agent의 `approve/APPROVED` native review를 확인한다. Author, body binding, stale·dismissed 상태를 검증한다. 이전 head의 pass나 구현자의 pass 주장은 인정하지 않는다.
4. Issue 계약, PR base·head, 전체 changed files·diff·reviews·review comments와 CI/status를 snapshot으로 보존한다. Pagination 누락, truncated diff, 계약 모순 또는 identity 불명은 차단한다.
@@ -133,7 +133,7 @@ Finding마다 `F-001` 같은 ID, 파일·행, 발생 조건, 영향·심각도,
게시 단계는 [공통 Native review adapter](references/native-review.md)와 canonical `scripts/native_review.py`의 검증된 API를 사용한다. 매 실행마다 gate·게시·검증 bootstrap을 다시 생성하거나 지원하지 않는 CLI·legacy JSON schema를 추정하지 않는다. 기존 role-authenticated transport와 현재 보유한 Issue lease를 연결하고, durable journal을 유지해 timeout·partial 이후에는 POST 재시도 없이 readback만 수행한다. 정상적인 같은 작성자의 자동 supersession/retouch는 별도 opt-in 없이 기본 인정하되, exact before/after/new-review 대조로 정해진 `official`·`dismissed` 전이와 새 리뷰의 시각만 허용한다. 이전 작성자·본문·commit·판정·댓글과 다른 필드는 보존해야 하며 다른 작성자의 변경은 거부한다. `supersede_same_author`와 `retouch_dismissed`는 v1 호환 필드이지 이 정상 전이의 허가 스위치가 아니다. Commit `files` 순서 정규화만 `unordered_commit_files`와 별도 근거가 필요한 예외로 유지한다. 기존 plan의 bytes/fingerprint와 journal을 변경하지 않는다. Snapshot/plan schema, 실제 지원 interface 및 read-only replay와 live 게시의 차이는 참조 문서대로 확인한다.
1. Exact PR 리뷰 요청은 진입 조건을 충족한 native review 하나의 게시만 승인한다. 단순 조사·역할 보고 요청은 write를 승인하지 않는다. `approve → APPROVED`, `request_changes → REQUEST_CHANGES`로 매핑하고 `blocked`는 게시 없이 보고한다.
1. Exact PR 리뷰 요청은 진입 조건을 충족한 native review 하나의 게시만 승인한다. 단순 조사·역할 보고 요청은 write를 승인하지 않는다. 게시 직전 진입 시 고정한 요청 참조를 재사용해 현재 요청·대상·승인 범위와의 일치를 재확인한다. 참조가 불명확하거나 불일치하면 게시하지 않는다. label이나 동일 문구의 다른 요청으로 대체하지 않는다. `approve → APPROVED`, `request_changes → REQUEST_CHANGES`로 매핑하고 `blocked`는 게시 없이 보고한다.
2. 게시 직전 Issue 계약, PR base·head, spec pass와 review baseline을 재확인한다. Drift면 중단한다. Body에는 결과 artifact의 요약과 아래 marker를 넣는다.
`<!-- gitea-agent:code-review/v1 owner=<owner> repository=<repository> issue=<issue-number> pr=<pull-number> head=<full-head-sha> contract-sha256=<digest> -->`
3. 같은 author·head·계약의 canonical review가 하나 있고 body·최종 state가 같으면 readback 후 write 없는 no-op으로 종료한다. 중복·상충 결과 또는 PENDING이면 추가 게시하지 않는다.
@@ -189,6 +189,21 @@ class NativeReviewTests(unittest.TestCase):
m.publish(p, before, api, lease, self.canonical(m, p), declaration={'mode': 'none'})
self.assertNotIn('secret transport detail', str(error.exception))
self.assertEqual(api.posts, 1)
def test_publication_request_binding_is_independent_of_workflow_labels(self):
text = (ROOT / 'harness/agents/review/skills/gitea-code-review/SKILL.md').read_text()
entry = text.split('## 진입 조건', 1)[1].split('## 리뷰 관점', 1)[0]
publication = text.split('## Native review 게시와 재검토', 1)[1].split('### 재리뷰 수렴', 1)[0]
for rule in ('현재 요청의 원문', 'session/message ID', '독립적으로 대조',
'기존 workflow artifact', 'workflow label', 'run-id suffix',
'추론하지 않는다', '권한 근거가 아니다'):
with self.subTest(entry_rule=rule):
self.assertIn(rule, entry)
for rule in ('진입 시 고정한 요청 참조', '재사용', '현재 요청',
'불명확하거나 불일치하면 게시하지 않는다',
'label이나 동일 문구의 다른 요청으로 대체하지 않는다'):
with self.subTest(publication_rule=rule):
self.assertIn(rule, publication)
def test_empty_review_comments_no_header_contract_is_explicit(self):
text = (ROOT / 'harness/agents/review/skills/gitea-code-review/references/native-review.md').read_text()
self.assertIn('### Empty review-comments readback without a count header', text)