
    JjB                       U d Z ddlmZ ddlZddlZddlmZ ddlmZ ddl	m
Z
mZ ddlmZmZmZmZmZmZmZmZ ddlmZmZmZmZ d	Zd
ed<   dZd
ed<    ej:                  d      Zded<    ej:                  d       ej:                  d       ej:                  d       ej:                  d       ej:                  d       ej:                  d       ej:                  d       ej:                  d       ej:                  d       ej:                  d      f
Zded<   dZ ded<    ed !       G d" d#             Z! G d$ d%e"      Z# G d& d'e"      Z$ G d( d)e%      Z& G d* d+e"      Z'd1d,Z(d2d-Z)d3d4d.Z* G d/ d0      Z+y)5u9  anu_v2.owner_trigger_only — OWNER_TRIGGER_ONLY_CAPABILITY core module.

회장 §명시 14장 1:1 박제 (2026-05-11 KST, OWNER_TRIGGER_ONLY_CAPABILITY).
task-2554+1 회장 §명시 (2026-05-12 KST) HIGH race condition fix 반영.
task-2554+2 회장 §명시 (2026-05-12 KST) §1 1:1 완성:
  - ``RESULT_PENDING`` import + sentinel 활용.
  - ``http_post`` 호출 직전 ``txn.record(PENDING)`` 으로 영구 기록 — process crash 후
    다음 runner 가 DEDUPED 판정 (crash-safety fail-closed).

본 모듈 범위:
  - public action **1 종**: ``POST_GEMINI_REVIEW_TRIGGER_COMMENT``
  - comment body 상수 (외부 입력 X): ``COMMENT_BODY = "/gemini review"``
  - 단일 허용 endpoint: ``POST /repos/{owner}/{repo}/issues/{pr_number}/comments``
  - 금지 11 endpoint hard-block (``PermissionError`` raise)
  - 전용 token 1 종: ``OWNER_GEMINI_TRIGGER_TOKEN`` (다른 token fallback 0)
  - merge path 와 완전 분리: ``BOT_GITHUB_TOKEN`` 본 모듈 어디서도 사용 0
  - dedupe atomic (audit JSONL + fcntl flock + sidecar lock transaction)
  - token redaction guard (audit 에 raw value 미출력)
  - decision JSON v1 검증 fail-closed
  - result enum: POSTED / DEDUPED / FAILED / PENDING

result 흐름 (task-2554+2 §1):
  1. txn.check_dedupe (POSTED + PENDING)
  2. (DEDUPED 인 경우 audit DEDUPED + return)
  3. token presence + hash_prefix 계산
  4. **txn.record(PENDING)** — http_post 직전 영구 기록 (crash-safety)
  5. http_post 호출
  6. 성공: txn.record(POSTED) / 실패: txn.record(FAILED)

one-way isolation: anu_v2/ 외부 import 금지.
    )annotationsN)	dataclass)Path)CallableFinal)AuditRedactionErrorDedupeViolationOwnerTriggerAuditRESULT_DEDUPEDRESULT_FAILEDRESULT_PENDINGRESULT_POSTEDtoken_hash_prefix)ALLOWED_ACTIONALLOWED_COMMENT_BODYDecisionInvalidErrorvalidate_decisionz/gemini reviewz
Final[str]COMMENT_BODYOWNER_GEMINI_TRIGGER_TOKENTOKEN_ENV_NAMEz(^/repos/[^/]+/[^/]+/issues/\d+/comments$zFinal[re.Pattern[str]]_ALLOWED_PATH_REz/pulls/\d+/merge\bz/pulls/\d+/reviews\bz/git/refs\bz/contents\bz/issues/\d+(?:\?|$)z	/labels\bz/branches\bz$/actions/(?:runs|workflows)/.*/rerunz3/check-runs/.*/rerequest|/check-suites/.*/rerequestz/pulls/\d+\?|/pulls/\d+$z"Final[tuple[re.Pattern[str], ...]]FORBIDDEN_ENDPOINT_PATTERNS)BOT_GITHUB_TOKENGH_TOKENGITHUB_TOKEN	OWNER_PAT	PAT_TOKENzFinal[tuple[str, ...]]FORBIDDEN_TOKEN_ENV_NAMEST)frozenc                  p    e Zd ZU dZded<   ded<   ded<   ded<   ded<   ded	<   d
ed<   ded<   dZded<   y)TriggerResultuP   ``trigger_gemini_review`` 결과 객체. token / authorization header 미포함.strstatusintprheadactioncomment_bodyendpointbooltoken_presentr   N
str | None
error_code)__name__
__module____qualname____doc____annotations__r-        anu_v2/owner_trigger_only.pyr!   r!   _   s:    ZKG
IKM!J
!r4   r!   c                      e Zd ZdZy)ForbiddenEndpointErroru9   금지 11 endpoint 호출 시도 (회장 §7 hard-block).Nr.   r/   r0   r1   r3   r4   r5   r7   r7   q   s    Cr4   r7   c                      e Zd ZdZy)TokenBoundaryViolationu-   다른 token 사용 시도 (회장 §6 fail).Nr8   r3   r4   r5   r:   r:   u   s    7r4   r:   c                      e Zd ZdZy)CommentBodyViolationuK   COMMENT_BODY 외부 변조 / 외부 입력 사용 시도 (회장 §4 fail).Nr8   r3   r4   r5   r<   r<   y   s    Ur4   r<   c                      e Zd ZdZy)MergePathViolationuG   merge_queue_executor merge path 와의 분리 위반 (회장 §9 fail).Nr8   r3   r4   r5   r>   r>   }   s    Qr4   r>   c                    t        | t              r| rd| v rt        d      t        |t              r|rd|v rt        d      t        |t              rt        |t              s|dk  rt        d      d|  d| d| dS )	N/z,owner must be a non-empty string without '/'z+repo must be a non-empty string without '/'r   z pr_number must be a positive intz/repos/z/issues/z	/comments)
isinstancer"   
ValueErrorr$   r*   )ownerrepo	pr_numbers      r5   _build_post_comment_pathrF      s    eS!#,GHHdC tFGGi%It)D	UV;<<UG1TF(9+Y??r4   c                6   t        | t              r| j                         dk7  rt        d|       t        |t              st        d      t        D ]!  }|j                  |      st        d|       t        j                  |      st        d|      y)z+static + dynamic endpoint hard-block guard.POSTzforbidden method: zpath must be stringzforbidden endpoint path: z8path not in allow-list (POST issues/{n}/comments only): N)rA   r"   upperr7   r   searchr   match)methodpathpats      r5   assert_endpoint_allowedrO      s    fc"flln&>$'9&%DEEdC $%:;;* O::d(+DTH)MNNO !!$'$HQ
 	
 (r4   c                T    | yt         D ]  }|| v st        d| dt         d       y)u   token 경계 검증 (회장 §6).

    명시적으로 ``env`` dict 에 다른 token env 이름이 키로 전달되면 fail.
    Nzforbidden token env injected: u!    — owner trigger path must use z only)r   r:   r   )env	forbiddens     r5   assert_token_boundaryrS      sI    
 {. 	(0 <*+52 r4   c                  z    e Zd ZdZdd	 	 	 	 	 	 	 	 	 ddZddZddd	 	 	 	 	 	 	 	 	 	 	 	 	 ddZd Zd	 Zd
 Z	d Z
y)OwnerTriggerOnlyu3  OWNER_TRIGGER_ONLY_CAPABILITY 단일 진입점.

    public action: ``trigger_gemini_review(decision_path, owner, repo, current_head_actual)``.

    side-effect 추상화 (test injection):
      - ``http_post``: ``Callable[[method:str, path:str, body:dict, headers:dict], dict]``
      - ``token_provider``: ``Callable[[], str]`` — OWNER_GEMINI_TRIGGER_TOKEN value 반환.

    task-2554+2 §1 흐름:
      1. token boundary 검증
      2. decision JSON 검증
      3. comment_body 검증
      4. endpoint hard-block
      5. ── ATOMIC TRANSACTION ────────────────────────────────
         5a. audit.transaction() 진입 (sidecar lock LOCK_EX)
         5b. txn.check_dedupe (POSTED + PENDING)
         5c. DEDUPED 시 audit record + return
         5d. token presence + hash_prefix 계산
         5e. **txn.record(PENDING)** — http_post 호출 직전 영구 기록 (crash-safety)
         5f. http_post 호출
         5g. 성공 시 audit POSTED / 실패 시 audit FAILED
      6. lock 해제
    N)auditc                   |t        d      |t        d      t        |      j                         | _        || _        || _        ||| _        y t        | j                        | _        y )Nz#http_post callable must be injectedz(token_provider callable must be injected)NotImplementedErrorr   resolve_workspace_root
_http_post_token_providerr
   _audit)selfworkspace_root	http_posttoken_providerrV   s        r5   __init__zOwnerTriggerOnly.__init__   sk     %&KLL!%&PQQ#N3;;=#-$0e6GH\H\6]r4   c                    t        |      }|j                         s| j                  |z  }|j                         st	        dd|       t        j                  |j                  d            }|S )NE_FILE_MISSINGzdecision file not found: zutf-8)encoding)r   is_absoluterZ   existsr   jsonloads	read_text)r^   decision_pathrM   datas       r5   _read_decisionzOwnerTriggerOnly._read_decision   sg    M"!''$.D{{}&'7;TUYTZ9[\\zz$..'.:;r4   )env_overrider(   c                  t        |       | j                  |      }t        ||       |t        n|}|t        k7  rt        dt        d|      t        |d         }	|d   }
t        |||	      }t        d|       t        |      }|j                  dd	      }| j                  j                         5 }	 |j                  |	|

       | j'                         }t)        |t              r|st+        t,         d      t/        |      }|j                  ||	|
t         t0        t        ||d|dd       d| ddd}dt        i}	 | j3                  d|||       	 i }d	}	 |j                  ||	|
t         t8        t        ||d|dd       t%        t8        |	|
t         t        |d|d	      cddd       S # t        $ rV |j                  ||	|
t         t"        t        ||dd	ddd       t%        t"        |	|
t         t        |dd	d	      cY cddd       S w xY w# t4        $ r?}	 |j                  ||	|
t         t6        t        ||d|ddd       i }d	}|# i }d	}w xY wd}~ww xY w# i }d	}w xY w# t        t:        f$ r  w xY w# 1 sw Y   yxY w)u  단일 public action. /gemini review 댓글 1회 작성.

        race condition: 동시 2 process 가 같은 (pr, head) 호출 시 lock 직렬화로 후속
        process 의 check_dedupe 가 항상 DedupeViolation 차단.

        crash-safety (task-2554+2 §1): http_post 호출 직전 ``txn.record(PENDING)`` 으로
        영구 기록. process crash 후 다음 runner 가 ``_has_active_trigger`` 에서 PENDING
        발견 → DedupeViolation → DEDUPED 판정 (fail-closed).
        )current_head_actualNzcomment_body must be z, got r%   current_headrH   task_id )r%   r&   FDEDUPE)rr   r%   r&   r'   resultr(   r)   rk   r+   r   token_value_loggedr-   )	r#   r%   r&   r'   r(   r)   r+   r   r-   u+    not provided — owner trigger fail-closedT)rr   r%   r&   r'   ru   r(   r)   rk   r+   r   rv   zBearer zapplication/vnd.github+jsonz
2022-11-28)AuthorizationAcceptzX-GitHub-Api-VersionbodyHTTP_POST_FAIL)rS   rm   r   r   r   r<   r$   rF   rO   r"   getr]   transactioncheck_deduper	   recordr   r   r!   r\   rA   r:   r   r   r   r[   	Exceptionr   r   r   )r^   rk   rC   rD   rp   rn   r(   decisionry   pr_numr&   rM   decision_path_strtask_id_valtxntokenhash_prefixheadersbody_payloadexcs                       r5   trigger_gemini_reviewz&OwnerTriggerOnly.trigger_gemini_review   s   ( 	l+ &&}5(8KL  ,3|''&'(<'?vdXN 
 Xd^$''tV<-.ll9b1 [[$$& @	#  F 6> ((*EeS),%&&QR  ,E2K
 JJ*  ,,$0 $%6%))4*/$ $+5'!27(4G
 #L1LlGD4 

#.$ $"0"/(4$():)--8.3$ !$%)""-
m@	 @	 # 

#.$ $"0"0(4$():).-/.3&.  %))!-!"'&('
 
+@	 @	D  JJ'2"($(&4&3,8(,->-11<27*:" !GE	 !GE-2 & $%89 g@	 @	s   5I:7F0
A9I:HI:)I&I:0AHI:HI:	I*IIIIIII##I:&I77I::Jc                    t        d      )NzZowner_trigger_only must not perform merge. Use merge_queue_executor with BOT_GITHUB_TOKEN.)r>   r^   argskwargss      r5   mergezOwnerTriggerOnly.merge  s     >
 	
r4   c                    t        d      )Nz)approve forbidden (review submit blocked)r7   r   s      r5   approvezOwnerTriggerOnly.approve  s    $%PQQr4   c                    t        d      )Nzclose forbiddenr   r   s      r5   closezOwnerTriggerOnly.close  s    $%677r4   c                    t        d      )Nzreopen forbiddenr   r   s      r5   reopenzOwnerTriggerOnly.reopen  s    $%788r4   )
r_   
str | Pathr`   z&Callable[[str, str, dict, dict], dict]ra   zCallable[[], str]rV   zOwnerTriggerAudit | NonereturnNone)rk   r   r   dict)rk   r   rC   r"   rD   r"   rp   r"   rn   dict | Noner(   r,   r   r!   )r.   r/   r0   r1   rb   rm   r   r   r   r   r   r3   r4   r5   rU   rU      s    < +/^ #^ :	^
 *^ (^ 
^&$ %)#'k "k 	k
 k !k "k !k 
k^
R89r4   rU   )rC   r"   rD   r"   rE   r$   r   r"   )rL   r"   rM   r"   r   r   )N)rQ   r   r   r   ),r1   
__future__r   rh   redataclassesr   pathlibr   typingr   r   anu_v2.owner_trigger_auditr   r	   r
   r   r   r   r   r   anu_v2.owner_trigger_decisionr   r   r   r   r   r2   r   compiler   r   r   r!   PermissionErrorr7   r:   rB   r<   r>   rF   rO   rS   rU   r3   r4   r5   <module>r      s  @ #  	 !  "	 	 	  ,j +9
 9+52::/, (  BJJ$%BJJ&'BJJ~BJJ~BJJ%&BJJ|BJJ~BJJ67BJJEFBJJ*+C ? 5 1  $" " ""D_ D8_ 8V: VR R@
$s9 s9r4   