
    jD                       d Z ddlmZ ddlZddlZddlZddlZddlmZm	Z	 ddl
mZmZmZmZmZ dZdZ eh d      Z eh d	      Zd
ZdZdZdZdZe G d d             ZddZddZddZ	 	 	 	 	 	 d dZ	 	 	 	 	 	 	 	 d!dZ	 	 	 	 	 	 	 	 d"dZ deedddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d#dZ!d$d%dZ"e#dk(  r e$ e"             y)&uo  ANU normal callback 4-source validator.

회장 verbatim (2026-05-27, task-2694+1):
    "actual cron 등록 + schedule_history + owner key + ANU inbound
     또는 authoritative collector receipt 가 있어야 PASS."

본 모듈은 finish-task.sh 등에서 envelope 작성만으로 .done 이 생성되던
우회 패턴을 차단하는 단일 진입점 검증기다.

검증 4-source (AND):
    1) schedule_id            — envelope 회수, placeholder/blocked schedule_type 거부
    2) schedule_history       — /home/jay/.cokacdir/schedule_history/<sid>.log status=ok
    3) owner_key              — envelope owner_key == ANU_KEY (executor self-key → NON_AUTHORITATIVE)
    4) inbound/receipt        — ANU inbound 파일 OR authoritative collector receipt 1건 이상

추가 방어 (Codex suggestion 1/3 반영):
    - envelope task_id ↔ 호출 task_id 결속 (stale receipt 재사용 차단)
    - chair_facing_sid 결속 (옵션)

verdict 정책:
    PASS              모든 4-source PASS
    FAIL              1개 이상 FAIL
    NON_AUTHORITATIVE owner_key 가 executor self-key → downstream 은 FAIL 로 처리
    )annotationsN)	dataclassfield)AnyDictListOptionalSequencec119085addb0f8b7z$/home/jay/.cokacdir/schedule_history>   pendingdeferred"to_be_registered_by_finish_task_sh>    nonenullr   r   to_be_registeredr   )z./home/jay/workspace/memory/events/anu_callbackz!/home/jay/workspace/memory/eventsz/utils.normal_callback_registration_validator.v1PASSFAILNON_AUTHORITATIVEc                      e Zd ZU ded<   ded<   ded<   ded<    ee      Zded	<    ee      Zd
ed<    ee      Z	ded<   e
dd       ZddZy)ValidationResultstrschemaverdicttask_idOptional[str]schedule_id)default_factoryzDict[str, str]sources_checkedz	List[str]reasonszDict[str, Any]evidencec                (    | j                   t        k(  S N)r   r   selfs    ]/home/jay/workspace/.worktrees/task-2721-dev1/utils/normal_callback_registration_validator.pyokzValidationResult.okV   s    ||t##    c           	         | j                   | j                  | j                  | j                  t	        | j
                        t        | j                        t	        | j                        dS )Nr   r   r   r   r   r    r!   )	r   r   r   r   dictr   listr    r!   r$   s    r&   to_jsonzValidationResult.to_jsonZ   sP    kk||||++#D$8$89DLL)T]]+
 	
r(   N)returnbool)r.   r+   )__name__
__module____qualname____annotations__r   r+   r   r,   r    r!   propertyr'   r-    r(   r&   r   r   L   s[    KLL&+D&AO^At4GY4$T:Hn:$ $	
r(   r   c                   | rt         j                  j                  |       sy	 t        | dd      5 }t	        j
                  |      cddd       S # 1 sw Y   yxY w# t        t        t        j                  f$ r Y yw xY w)u-   envelope JSON 로드. 실패 시 None 반환.Nrutf-8)encoding)	ospathisfileopenjsonloadOSError
ValueErrorJSONDecodeError)envelope_pathfhs     r&   _load_enveloperE   j   sh    } =-w7 	!299R=	! 	! 	!Z!5!56 s.   A A	A AA A A>=A>c                :    | yt        |       j                         S )Nr   )r   strip)vals    r&   
_normalizerI   u   s    
{s8>>r(   c                x   g }| j                  d      }t        |      }t        | j                  d            }|t        v r"|j                  d| d       t        |xs d|fS |s|j                  d       t        d|fS |j                         t        v r|j                  d| d       t        ||fS t        ||fS )u5   Source 1: schedule_id + blocked schedule_type 검증.r   schedule_typez/blocked schedule_type detected: schedule_type=''Nz(schedule_id missing or empty in envelopez)schedule_id is placeholder: schedule_id=')getrI   BLOCKED_SCHEDULE_TYPESappendr   lower_PLACEHOLDER_SCHEDULE_IDSr   )enveloper    schedule_id_rawr   rK   s        r&   _check_schedule_idrT   {   s    Gll=1O_-Kx||O<=M ..=m_AN	
 [(D'11ABT7""777}AF	
 ['))g%%r(   c                   g }i }| s|j                  d       t        ||fS t        j                  j	                  |       | k7  sd| v sd| v s| dv r|j                  d|        t        ||fS t        j                  j                  ||  d      }||d<   t        j                  j                  |      s|j                  d|        t        ||fS d	}	 t        |d
dd      5 }|D ]\  }|j                         }|s	 t        j                  |      }	t        |	t              r!t        |	j                  d            dk(  rd} n^ d	d	d	       ||j                  d|        t        ||fS ||d<   t$        ||fS # t        t        j                   f$ r d|v sd|v rd}Y  [Y w xY w# 1 sw Y   bxY w# t"        $ r'}
|j                  d|
        t        ||fcY d	}
~
S d	}
~
ww xY w)z*Source 2: schedule_history status=ok grep.z3schedule_history check skipped: schedule_id missing/\).z..z7invalid schedule_id (path traversal attempt detected): z.logschedule_history_pathzschedule_history file missing: Nr7   r8   replace)r9   errorsstatusr'   jsonlz"status": "ok"z	status=ok
plain_grepzschedule_history read error: z+schedule_history status=ok line missing in schedule_history_match)rO   r   r:   r;   basenamejoinr<   r=   rG   r>   loads
isinstancer+   rI   rM   rA   rB   r@   r   )r   schedule_history_dirr    r!   r;   	found_viarD   rawlineobjexcs              r&   _check_schedule_historyrj      s   
 G!HLMWh&&
 	%4+;+%Ek_U	
 Wh&&77<<,T.BCD(,H$%77>>$8?@Wh&& I'$gi@ 	B yy{**T*C!#t,CGGH<M1NRV1V$+		$ 9$@	
 Wh&&)2H%&("" #D$8$89 '4/;$3F$0	 4G	 	  '6se<=Wh&&'s[   F1 "F%<AE9 F%F1 9#F"F%!F""F%%F.*F1 1	G!:GG!G!c                   g }i }t        | j                  d            j                         }t        |      |d<   |s|j	                  d       t
        ||fS t        |      j                         }t        |      j                         }|r$||k(  r||k7  r|j	                  d       t        ||fS ||k7  r!|j	                  d| d| d       t
        ||fS t        ||fS )u   Source 3: owner_key 검증.	owner_keyowner_key_presentzowner_key missing in envelopezHself-key channel hit: owner_key matches executor_key (non-authoritative)z&owner_key mismatch: expected ANU key 'z', got 'rL   )rI   rM   rP   r/   rO   r   r   r   )rR   anu_keyexecutor_keyr    r!   rl   anuexec_ks           r&   _check_owner_keyrr      s     G!H 8<<45;;=I$(OH !67Wh&&
W

#
#
%C%++-F )v%)s*:V	
 !'833C4SE)AN	
 Wh&&(""r(   c                D   g }i }|D cg c]D  }|st         j                  j                  |      s&t         j                  j                  |      F }}dD ]  }t	        |j                  |            }|s t         j                  j                  |      t        fd|D              }	|	s|j                  d| d|        mt         j                  j                        r|i|d<   t        ||fc S |j                  d| d|         g }
| j                  t         j                  d      }t        j                  |      }|D ]  }|rt         j                  j                  |      s%t         j                  j                  || d      }t        j                  |      D ]3  }t         j                  j                  |      s#|
j                  |       5  |
r|
d	d
 |d<   t        |
      |d<   t        ||fS |j                  d|  d|        t         ||fS c c}w )u@   Source 4: ANU inbound 파일 OR authoritative collector receipt.)inbound_evidencecollector_receiptc              3  p   K   | ]-  }|k(  xs" j                  |t        j                  z          / y wr#   )
startswithr:   sep).0rootabs_paths     r&   	<genexpr>z)_check_inbound_receipt.<locals>.<genexpr>  s9      
 B 3 3D266M BB
s   36z	envelope.z' path is outside allowed inbound dirs: receipt_via_envelope_fieldz path does not exist on disk: r   *N   inbound_hitsinbound_hit_countzpinbound/receipt evidence missing: no envelope.inbound_evidence / collector_receipt and no glob hit for task_id='z' in )r:   r;   isdirabspathrI   rM   anyrO   r<   r   rZ   rx   globescapera   lenr   )r   rR   inbound_search_dirsr    r!   dallowed_rootskeyr;   	containedhitssafe_task_idescaped_task_idpatternpr{   s                  @r&   _check_inbound_receiptr      s    G!H %8 1qAQM  9 
(,,s+,77??4( 
%
 
	 NNC5 GvN 77>>(#698_H12(**u:4&A	
#
. D??2662.Lkk,/O  a('',,q_$5Q"787# 	Aww~~a A		 #'8 (+D	$%Wh&&NN	::A%
	!
 (""_s   HH!Hr   F)ro   rn   rd   r   require_chair_facing_sid_matchexpected_chair_facing_sidc                   i }g }	d|i}
t        |      }|!t        t        t        | ddt        idg|
      S t	        |j                  d            }|t	        |       k7  rDt        t        t        | t	        |j                  d            xs ddt        id	| d
|  dg|
      S |rpt	        |j                  d            }t	        |      }|r||k7  rDt        t        t        | t	        |j                  d            xs ddt        id| d| dg|
      S t        |      \  }}}||d<   |	j                  |       t        ||      \  }}}||d<   |	j                  |       |
j                  |       t        |||      \  }}}||d<   |	j                  |       |
j                  |       |rt        |      nt        t              }t        | ||      \  }}}||d<   |	j                  |       |
j                  |       t        d |j                         D              rt         }n-t#        d |j                         D              rt$        }nt        }t        t        || |||	|
      S )u  4-source validator. envelope 파일을 읽어 task_id 결속 후 4-source AND 검증.

    Args:
        task_id: 검증 대상 task_id (envelope.task_id 와 결속 확인)
        envelope_path: ANU callback envelope JSON 파일 경로
        executor_key: 실행 주체 key (self-key channel hit 탐지용, 선택)
        anu_key: 권위 authoritative key (기본 ANU_KEY)
        schedule_history_dir: schedule_history 디렉토리
        inbound_search_dirs: ANU inbound 파일 검색 디렉토리
        require_chair_facing_sid_match: chair_facing_sid 결속 강제 여부
        expected_chair_facing_sid: 결속 강제 시 기대값

    Returns:
        ValidationResult — verdict PASS / FAIL / NON_AUTHORITATIVE
    rC   Nenvelope_loadz%envelope file missing or invalid JSONr*   r   r   task_id_bindingzBtask_id mismatch (stale receipt reuse blocked): envelope.task_id='z' vs caller='rL   chair_facing_sidchair_facing_sid_bindingz%chair_facing_sid mismatch: envelope='z' vs expected='schedule_history)rn   ro   rl   )r   rR   r   inbound_receiptc              3  .   K   | ]  }|t         k(    y wr#   )r   ry   vs     r&   r|   z1validate_callback_registration.<locals>.<genexpr>  s     
Da1!!
D   c              3  .   K   | ]  }|t         k(    y wr#   )r   r   s     r&   r|   z1validate_callback_registration.<locals>.<genexpr>  s     91Q$Y9r   )rE   r   VALIDATOR_SCHEMAr   rI   rM   rT   extendrj   updaterr   r,   _DEFAULT_INBOUND_DIRSr   r   valuesr   allr   )r   rC   ro   rn   rd   r   r   r   r   r    r!   rR   envelope_task_idenv_cfexpected_cfsid_verdictr   sid_reasonshist_verdicthist_reasonshist_evidenceown_verdictown_reasonsown_evidencedirsinb_verdictinb_reasonsinb_evidencer   s                                r&   validate_callback_registrationr   =  s   4 ')OG /?H
 m,H#,d3<=
 	
 "(,,y"9::g..#"8<<#>?G4.5%%5$6mG9AO 
 	
 &HLL);<= !:;f3#'&x||M'BCKt!;T B!!'}AG "   -?x,H)Kk%0OM"NN;
 1H)1-L, +7O&'NN< OOM"
 .>'.*Kl $/OK NN;OOL!
 )<4#$F[A\D-C(.*Kl *5O%&NN;OOL!
 
D?+A+A+C
DD#	9 6 6 89	9' r(   c                T   t        j                  d      }|j                  dd       |j                  dd       |j                  dd	       |j                  d
t        	       |j                  dd	       |j                  dd       |j	                  |       }t        |j                  |j                  |j                  |j                  |j                  |j                        }t        t        j                  |j                         dd             |j                   rdS dS )u=   CLI: 정상 PASS면 exit 0, FAIL/NON_AUTHORITATIVE면 exit 2.&normal_callback_registration_validator)progz	--task-idT)requiredz--envelope-pathz--executor-keyr   )defaultz	--anu-keyz--expected-chair-facing-sidNz --require-chair-facing-sid-match
store_true)action)r   rC   ro   rn   r   r   F   )ensure_asciiindentr   )argparseArgumentParseradd_argumentANU_KEY
parse_argsr   r   rC   ro   rn   r   r   printr>   dumpsr-   r'   )argvapargsresults       r&   mainr     s    		 	 &N	OBOOK$O/OO%O5OO$bO1OOKO1OO14O@OO6|OL==D+((&&"&"@"@'+'J'JF 
$**V^^%E!
DE		1 q r(   __main__)rC   r   r.   zOptional[dict])rH   r   r.   r   )rR   r+   r.   z$tuple[str, Optional[str], List[str]])r   r   rd   r   r.   %tuple[str, List[str], Dict[str, Any]])rR   r+   rn   r   ro   r   r.   r   )r   r   rR   r+   r   zSequence[str]r.   r   )r   r   rC   r   ro   r   rn   r   rd   r   r   Optional[List[str]]r   r/   r   r   r.   r   r#   )r   r   r.   int)%__doc__
__future__r   r   r   r>   r:   dataclassesr   r   typingr   r   r   r	   r
   r   SCHEDULE_HISTORY_DIR	frozensetrN   rQ   r   r   r   r   r   r   rE   rI   rT   rj   rr   r   r   r   r0   
SystemExitr5   r(   r&   <module>r      s  2 #    	 ( 6 6 = " $   & '   
 E '  
 
 
:&8<#<#<# +<#~!#!#!# !# +	!#H<#<#<# '<# +	<#N  4/3+0/3FF F 	F
 F F -F %)F  -F FZ!. z
TV
 r(   