
    3jH                       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 ddlmZmZmZmZmZ dZ ed      Zed	z  d
z  Z ej,                  d      Z ej,                  d      Zeedf   Z G d de      Z ed       G d d             ZdddZddZd dZ dd	 	 	 	 	 	 	 d!dZ!d"dZ"ddd	 	 	 	 	 	 	 	 	 d#dZ#g dZ$y)$u  anu_v3.cli_output_path_guard — CLI output-path hardening guard (task-2617).

회장 직접 승인 (2026-05-19) BOUNDED REMEDIATION. task-2616 scan 이 확정한
Critical7 "arbitrary fs write" 3건(``batch_hold_adjudicator --output`` ·
``batch_dependency_classifier --out`` · ``pre_authorized_evidence_bundle_builder
--out``)의 출력 경로를 정책 허용 경로로만 제한하기 위한 **import-only 순수
모듈**.

설계 불변식 (회장 verbatim · task-2617 §4):

  * 본 모듈 자체에 argparse / main / CLI / ``__main__`` **절대 금지**
    (자기참조 결함 회피 — guard 가 또 다른 CLI sink 이 되면 안 됨).
  * 기본 출력은 stdout 유지 — 호출부의 ``else: stdout`` 분기는 불변.
  * 파일 출력은 policy 허용 경로로만 가능 (fail-closed).
  * CANONICAL_WS_ROOT(``/home/jay/workspace``) 기준 ``memory/events`` 또는
    ``memory/reports`` 직하위만 허용.
  * 파일명 task-id prefix 강제.
  * absolute path · ``../`` traversal · workspace 이탈 · symlink component ·
    hardlink · 기존파일 overwrite = **fail-closed**.
  * ``Path.resolve()`` strict-prefix 단독으로 충분하다고 가정하지 않는다.
    경로 component 별 ``O_NOFOLLOW`` open + ``lstat`` 으로 symlink component
    를 직접 거부한다.
  * ``O_NOFOLLOW``/``O_EXCL`` temp 생성 + ``os.link`` 기반 atomic no-overwrite
    + dir-fd 상대연산 으로 **TOCTOU 경계 차단**.
  * 검증 실패 시 **write 전 차단** (어떤 바이트도 디스크에 닿지 않는다).

NO-CRON / Layer A: 본 모듈은 cron register/remove · dispatch · merge ·
subprocess · credential 접근 ZERO. 경로 검증과 정책 허용 경로 내 atomic
파일 생성만 수행한다.
    )annotationsN)	dataclassfield)PathPurePosixPath)	FrozenSetOptionalSequenceTupleUnionzanu_v3.cli_output_path_guard.v1z/home/jay/workspaceconfigzcli_output_path_policy.yamlz^task-\d+(?:\+\d+)?$z^(task-\d+(?:\+\d+)?)[._-]zos.PathLike[str]c                      e Zd ZdZy)
GuardErroruL   fail-closed 위반. 어떤 write 도 수행되지 않았음을 의미한다.N)__name__
__module____qualname____doc__     3/home/jay/workspace/anu_v3/cli_output_path_guard.pyr   r   8   s    Vr   r   T)frozenc                  p    e Zd ZU dZeZded<   dZded<   dZded	<    e	d
       Z
ded<   dZded<   ddZy)GuardPolicyuV   CLI output-path 정책. yaml/json 에서 로드되거나 기본값으로 생성된다.r   canonical_ws_root)memory/eventsmemory/reportszTuple[str, ...]allowed_rootsTbooltask_id_prefix_requiredc                     t        h d      S )N>   hardlink	ws_escapedotdot_traversalsymlink_componentoverwrite_existingabsolute_outside_ws)	frozensetr   r   r   <lambda>zGuardPolicy.<lambda>D   s    		!
 r   )default_factoryzFrozenSet[str]deny!single_segment_under_allowed_rootc                :    t        d | j                  D              S )Nc              3  2   K   | ]  }t        |        y wN)r   ).0rs     r   	<genexpr>z/GuardPolicy.normalized_roots.<locals>.<genexpr>R   s     B!]1%B   )tupler   )selfs    r   normalized_rootszGuardPolicy.normalized_rootsQ   s    Bt/A/ABBBr   N)returnzTuple[PurePosixPath, ...])r   r   r   r   CANONICAL_WS_ROOTr   __annotations__r   r   r   r*   r+   r5   r   r   r   r   r   <   sN    `/t/%HM?H$(T( 	
D.  /3%t2Cr   r   c           
        | t        |       nt        }	 |j                  d      }d}	 ddl}|j                  |      }t        |t              st	        d|       t        t        |j                  d	t        t                                }|j                  d
      xs ddg}t        |t        t         f      r|st	        d      |j                  d      xs g }|j                  d      xs i }	d}
t        |	t              rt#        |	j                  dd            }
t%        |t!        d |D              t#        |j                  dd            t'        d |D              xs t%               j(                  |
      S # t        $ r}t	        d| d|       |d}~ww xY w# t        $ r> 	 t        j                  |      }n## t        $ r}t	        d| d|       |d}~ww xY wY w xY w)u   yaml/json 정책 파일을 로드한다. 부재/파싱불가 = fail-closed (GuardError).

    인자 없으면 ``DEFAULT_POLICY_PATH`` 사용.
    Nutf-8)encodingu#   policy 로드 실패(fail-closed): : r   u#   policy 파싱 실패(fail-closed): u#   policy 형식 오류(fail-closed): r   r   r   r   u5   policy.allowed_roots 누락/형식오류(fail-closed)r*   filenameTr+   c              3  2   K   | ]  }t        |        y wr.   strr/   xs     r   r1   zload_policy.<locals>.<genexpr>{   s     4qCF4r2   r   c              3  2   K   | ]  }t        |        y wr.   r?   rA   s     r   r1   zload_policy.<locals>.<genexpr>}   s     ,!s1v,r2   )r   r   r   r*   r+   )r   DEFAULT_POLICY_PATH	read_textOSErrorr   yaml	safe_load	Exceptionjsonloads
isinstancedictr@   getr7   listr3   r   r   r'   r*   )pathprawedatarG   ws_rootallowedr*   filename_cfgsingles              r   load_policyrY   V   s   
 &T
,?APkk7k+ DT~~c" dD!>qcBCC3txx 3S9J5KLMNGhh'NO=M+NGge}-WPQQ88F!rD88J'-2LF,%@$G
 !4G44 $TXX.G%N O,t,,B0B0B*0 7  P>qcA3GHaOP  T	T::c?D 	TB1#RsKLRSS	T TsL   E> F! >	FFF!	G(+G G(	G!
GG!!G('G(c                    t        |       S r.   )r   )msgs    r   _rejectr\      s    c?r   c                V    t         j                  |       }|r|j                  d      S d S )N   )_FILENAME_TASK_PREFIX_REmatchgroup)r=   ms     r   _derive_task_idrc      s'     &&x0A1771:$$r   )task_idc               J   	 t        j                  |       }t	        |t
              r|j                         dk(  rt        d      t        |      }|j                         rt        d|       |j                  }|st        d      |D ]5  }|dv rt        d|       |d	v rt        d
|       d|v s,t        d       t        | }d}	|j                         D ]#  }
|
j                  }|dt        |       |k(  s!|
}	 n |	$t        dt        |j                         d|       |t        |	j                        d }|j                  rt        |      dk7  rt        d|       |st        d|       |d   }|j                  rHt!        |      }|t"        j%                  |      st        d|       |||k7  rt        d| d|       |j&                  |z  }t         j(                  j+                  t        |            }t         j(                  j+                  t        |j&                              t         j,                  z   }|t         j,                  z   j/                  |      st        d|       t1        |      S # t        $ r}t        d|      |d}~ww xY w)uc  요청 출력경로를 정책으로 검증한다. **write 는 수행하지 않는다.**

    PASS 시 검증된 절대 target 경로를 반환. 위반 시 ``GuardError``
    (fail-closed — 어떤 바이트도 디스크에 닿지 않음).

    Path.resolve() strict-prefix 만으로 충분하다고 가정하지 않는다:
    여기서는 *문법적* 거부(absolute/.. /ws-escape/task-id)만 수행하고,
    symlink-component/overwrite/hardlink 같은 *물리적* TOCTOU 거부는
    실제 open 시점(:func:`atomic_guarded_write`)에 ``O_NOFOLLOW``/
    ``O_EXCL``/``lstat`` 으로 재확인한다.
    u)   output 경로 형식오류(fail-closed): N u&   output 경로 공백/None(fail-closed)u#   absolute path 금지(fail-closed): u   빈 경로(fail-closed))..u$   '..' traversal 금지(fail-closed): )rf   .u)   비정상 component 금지(fail-closed):  u   NUL byte 금지(fail-closed)zallowed_roots u!    밖 출력 금지(fail-closed): r^   u<   allowed_root 직하위 단일파일만 허용(fail-closed): u   파일명 누락(fail-closed): u;   파일명 task-id prefix 누락/형식오류(fail-closed): u'   task-id prefix 불일치(fail-closed): z != u   workspace 이탈(fail-closed): )osfspath	TypeErrorr\   rL   r@   stripr   is_absolutepartsr5   lenrO   r   r+   r   rc   _TASK_ID_REr`   r   rP   normpathsep
startswithr   )	requestedrd   policyreqrS   pprp   segrelmatched_rootrootrp	remainderr=   derivedtargetnormws_norms                     r   validate_output_pathr      s   "Pii	" c3399;"#4>??	s	B 
~~;C5ABB HHE/00 :'>@FGG)EcUKLLS=899: 
C,0L'') ZZ3r7r!L	
 T&"6"678 9!U$
 	
 c,,,-./I//C	Na4GJ3%P
 	
 7u=>>}H %%!(+?+"3"3G"<MhZX  7g#59(4yQ 
 &&,F77CK(Dggs6#;#;<=FG266M%%g.7u=>>:A  PA!GHaOPs   J 	J"JJ"c                Z   t         j                  t         j                  z  }t        t         d      r|t         j                  z  }t        j
                  t        |       |      }	 |D ]  }t        j                  ||      }ddl}|j                  |j                        rt        d|       |j                  |j                        st        d|       t        j
                  |||      }t        j                  |       |} |S # t        $ r t        j                  |        w xY w)u   ws_root 부터 component 별 O_NOFOLLOW open 으로 부모 dir fd 를 얻는다.

    중간 component 중 하나라도 symlink 면 ELOOP 로 open 실패 →
    symlink-component fail-closed. ws_root 자체는 신뢰 절대경로(고정 anchor).
    
O_NOFOLLOWdir_fdr   Nu'   symlink component 금지(fail-closed): u)   비-디렉터리 component(fail-closed): )rk   O_RDONLYO_DIRECTORYhasattrr   openr@   lstatstatS_ISLNKst_moder\   S_ISDIRcloseBaseException)rU   
components
base_flagsdfdcompst_statnfds           r   _open_dir_nofollowr      s     r~~-Jr< bmm#

''#g,

+C 	D$s+B }}RZZ( GvNOO==, I$PQQ''$
37CHHSMC	 
 
s   %B$D
 
 D*rd   rw   c          
     f   ||n	t               }t        | ||      }t        |t              r|j	                  d      n|}t        t        j                  j                  t        |      t        |j                                    }|j                  ^ }}	d|v rt        d      t        |j                  |      }
d|	 dt        j                          }d}d	}	 t        j                  j                  d
|
       }t        j                  j                  t        |j                              |k(  s0|j                  t        j                   z         st        d|       	 t        j"                  |	|
      }t        d| dt%        |dd       d      # t&        $ r Y nw xY wt        j(                  t        j*                  z  t        j,                  z  }t/        t        d      r|t        j0                  z  }	 t        j2                  ||d|
      }n## t4        $ r}t        d| d|       |d}~ww xY wd}t        j6                  |dd      5 }d}|j9                  |       |j;                          t        j<                  |j?                                t        j@                  |j?                               }ddd       n# 1 sw Y   nxY wjB                  |jD                  f}	 t        jF                  ||	|
|
       nY# t4        $ r}t        d|       |d}~wtH        $ r2}|jJ                  tJ        jL                  k(  rt        d|       | d}~ww xY wd}	 t        jN                  }t/        t        d      r|t        j0                  z  }t        j2                  |	||
      }t        j@                  |      }t        j                  j                  d
|       |dk7  rV	 t        jP                  |       n?# tH        $ r Y n4w xY w# |dk7  r&	 t        jP                  |       w # tH        $ r Y w w xY ww xY w|jB                  |jD                  f|k(  }tS        fd|jU                         D              }tW        fd|D              }|r|s<	 t        jX                  |	|
       n# tH        $ r Y nw xY wt[        d| d| d       t        jX                  ||
       d	}	 t        j<                  |
       n# tH        $ r Y nw xY w||dk7  r&	 t        jP                  |       n# tH        $ r Y nw xY w|r(	 t        jX                  ||
       n# tH        $ r Y nw xY w	 t        jP                  |
       S # tH        $ r Y S w xY w# t\        $ r  t^        $ r}t]        d |       |d}~ww xY w# |dk7  r&	 t        jP                  |       n# tH        $ r Y nw xY w|r(	 t        jX                  ||
       n# tH        $ r Y nw xY w	 t        jP                  |
       w # tH        $ r Y w w xY wxY w)!u  정책 검증 후 TOCTOU-safe 하게 파일을 생성한다 (overwrite 금지).

    절차:
      1. :func:`validate_output_path` 로 문법 검증 (write 전 차단).
      2. ws_root 부터 부모 dir 까지 component 별 ``O_NOFOLLOW`` open
         (+lstat) — symlink component 거부.
      3. 부모 dir fd 상대로 ``O_CREAT|O_EXCL|O_NOFOLLOW`` temp 생성,
         write+fsync.
      4. ``os.link`` (src/dst dir_fd 상대) 로 final 생성 — final 이 이미
         존재하면 ``FileExistsError`` → overwrite/hardlink fail-closed.
      5. temp unlink + dir fsync. 모든 단계 dir-fd 상대 → 경로 재해석
         (TOCTOU) 차단.

    실패 시 부분 파일을 남기지 않고 ``GuardError`` 를 던진다.
    Nr   r:   rg   ztraversal(fail-closed)rh   z.task-2617.tmp.rj   Fz/proc/self/fd/u,   post-resolve workspace 이탈(fail-closed): r   u-   기존 파일 overwrite 금지(fail-closed): z (nlink=st_nlink?)r   i  u   temp 충돌(fail-closed): r<   Twb)closefd)
src_dir_fd
dst_dir_fdu.   link 시점 기존파일 발견(fail-closed): c              3     K   | ]K  }t         j                  j                  t        j                  j                  g|j                          M y wr.   )rk   rP   rs   joinrp   )r/   r0   ws_reals     r   r1   z'atomic_guarded_write.<locals>.<genexpr>  s>      
 GGRWW\\'<AGG<=
s   AAc              3  p   K   | ]-  }|k(  xs" j                  |t        j                  z          / y wr.   )ru   rk   rt   )r/   ar
final_reals     r   r1   z'atomic_guarded_write.<locals>.<genexpr>  s9      
 "B
 5 5b266k BB
s   36ul   post-link final-inode bound 이탈(fail-closed·reopen-by-name substitution/dir-rename TOCTOU): inode_bound=z containment=z real=u#   guarded write 실패(fail-closed): )0rY   r   rL   r@   encoder   rk   rP   relpathr   rp   r\   r   getpidrealpathru   rt   r   getattrFileNotFoundErrorO_WRONLYO_CREATO_EXCLr   r   r   FileExistsErrorfdopenwriteflushfsyncfilenofstatst_devst_inolinkrF   errnoEEXISTr   r   r3   r5   anyunlink
SystemExitr   rI   )rv   rT   rd   rw   polr   payloadr{   	dir_partsfnamer   tmp_nametmp_fdcreated_tmpreal_direstoflagsrS   fh_tmp_sttrusted_dev_inoffdf_oflagsfinal_stinode_bound_okallowed_abscontainment_okr   r   s                              @@r   atomic_guarded_writer     s   , &&KMC!)WSIF&0s&;dkk'"G

FS)>)>%?@C 		Yy.//
S22I
>C5'6HFKK77##nSE$:;''""3s'<'<#=>G#x':':7RVV;K'L>xjI 
	((5-C?x H!#z378;  ! 		 rzz)BII52|$bmm#F	WWXvuSAF 	,XJb<	 YYvtT2 	,bFHHWHHJHHRYY[! hhryy{+G	, 	, 	, #>>7>>:	GGHeD 	@I  	ww%,,&DVHM 	. 	{{Hr<(BMM)''%#6Cxx}H))N3%*@AJbyHHSM  byHHSM   __hoo./A 	  
))+
 
  
!
 
 >		%, ((6'7 8-.fZLB  			(3'	HHSM 		 R<  		(3/ 	HHSM 		#   K>qcBCJK R<  		(3/ 	HHSM 		s  B	V $4F 	F$!V #F$$AV =H V 	H7 H22H77V A*K>	V KV ,L V 	MLM*-MMV "BP $V *P  ?V  	P	V PV P<P,+P<,	P85P<7P88P<<AV R- ,V -	R96V 8R990V *T  ?V  	T	V TV T,,	T87T8>U	U"!U"&U<<	VVV1V,,V11V4 4X0;WX0	WX0WX0#W;:X0;	XX0XX0X! X0!	X-*X0,X--X0)GUARD_SCHEMAr7   rD   r   r   rY   r   r   r.   )rP   zOptional[PathLike]r6   r   )r[   r@   r6   z'GuardError')r=   r@   r6   Optional[str])rv   PathLikerd   r   rw   r   r6   r   )rU   r   r   zSequence[str]r6   int)
rv   r   rT   zUnion[str, bytes]rd   r   rw   zOptional[GuardPolicy]r6   r   )%r   
__future__r   r   rJ   rk   redataclassesr   r   pathlibr   r   typingr   r	   r
   r   r   r   r7   rD   compilerr   r_   r@   r   rI   r   r   rY   r\   rc   r   r   r   __all__r   r   r   <module>r      sD  < #   	 	 ( ' > >0 ./ '(25RR  bjj01%2::&CD (()W W $C C C2)Z% "SS S 	S
 
SnD "$(qq
q 	q
 "q 
qh	r   