
     jk              
      ^   U 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Zddlm	Z	m
Z
 ddlmZmZ ddlmZ ddlmZ ddlmZmZmZ g d	Zd
ZdZdZdZ G d dee      ZdZded<   dZded<   dZded<   dZ ded<   dZ!ded<   dZ"ded<    e	d       G d d              Z#d<d!Z$d=d"Z%dd#	 	 	 	 	 d>d$Z&d?d@d%Z' eejP                  jS                  d& e ee*      jW                         jX                  jX                  d'z  d(z  d)z                    Z- e	d       G d* d+             Z.dAd,Z/	 	 	 	 	 	 	 	 dBd-Z0d.d.ddddd/	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dCd0Z1dd1	 	 	 	 	 dDd2Z2d3Z3ded4<   dEd5Z4 e	d       G d6 d7             Z5	 	 	 	 	 	 dFd8Z6dGd9Z7d?dHd:Z8e9d;k(  r ejt                   e8              yy)IuL  utils/bot_merge_identity.py — task-2522 §본질.

회장 §명시 본질:
  task-2522의 목표는 "머지가 되게 하는 것"이 **아니다.** 이미 머지는 된다.
  목표는 **"누가 머지했는가"**를 자동화 기준에 맞게 고치는 것.
  owner_pat로 머지되면 기능적으로는 성공이어도 **autonomy success로 인정하지 않는다.**

본 모듈은 4가지 책임을 박제한다:
  1. **token_source 4 enum 분류** — env var 존재 여부 + token prefix 휴리스틱.
     절대로 raw token 값을 출력/리턴하지 않는다 (회장 §보안 7번).
  2. **identity audit JSONL** — pre/post merge token_source + mergedBy 비교.
  3. **autonomy_capability_gap 박제** — owner_pat fallback이 1건이라도 감지되면
     기능적 머지 성공 여부와 무관하게 autonomy success로 인정하지 않는다.
  4. **autonomy_score delta 산출** — owner_pat→stuck 또는 하락, bot/app→상승.

회장 §금지 행위:
  - ❌ token raw value 출력 (값/hex/마스킹된 값 X) — env var 존재 여부와 prefix만 사용.
  - ❌ owner_pat을 bot identity로 위장.
  - ❌ admin override 호출.
  - ❌ branch protection 우회.
  - ❌ GitHub App private key 파일 공유/노출.
  - ❌ AUTOMATION_CAPABILITY_GAP을 CriticalEscalationType에 추가 (Critical 7종 외 보고).
  - ❌ 5 모듈 본체 신규 abstraction 생성.

CLI:
  python3 utils/bot_merge_identity.py --classify-token-source
  python3 utils/bot_merge_identity.py --audit --pr 72 --task-id task-2521
  python3 utils/bot_merge_identity.py --score --identity-from-stdin
    )annotationsN)	dataclassasdict)datetimetimezone)Enum)Path)IterableMappingOptional)TokenSourceTokenSourceProbeMergeIdentityAuditRecordAutonomyScoreDeltaclassify_token_sourceprobe_token_source_from_envfingerprint_token_for_auditbuild_audit_recordappend_audit_jsonldecide_autonomy_capability_gapcompute_autonomy_score_deltaexpected_bot_identity_for_actor'verify_branch_cleanup_token_inheritanceREQUIRED_AUDIT_FIELDS_2523TOKEN_SOURCE_OWNER_PATTOKEN_SOURCE_GITHUB_APPTOKEN_SOURCE_GITHUB_ACTIONSTOKEN_SOURCE_UNKNOWNDEFAULT_AUDIT_JSONL_PATH	OWNER_PATGITHUB_APP_INSTALLATION_TOKENGITHUB_ACTIONS_TOKENUNKNOWNc                       e Zd ZdZeZeZeZ	e
Zy)r   uB   Token 출처 4 enum. 회장 §1 — 정확히 4종, 추가 금지.N)__name__
__module____qualname____doc__r   r    r   r!   r   r"   r   r#        //home/jay/workspace/utils/bot_merge_identity.pyr   r   O   s    L&I$;!6"Gr*   r   )ghs_ztuple[str, ...]_INSTALLATION_TOKEN_PREFIXES)ghp_github_pat_gho__OWNER_PAT_PREFIXES)GITHUB_ACTIONSRUNNER_NAMEGITHUB_WORKFLOWACTIONS_ID_TOKEN_REQUEST_TOKEN_ACTIONS_RUNNER_ENV_NAMES)
GITHUB_PATGH_PATOWNER_GITHUB_TOKEN_TOKEN_ENV_NAMES_OWNER_PAT)INSTALLATION_TOKENr!   APP_INSTALLATION_TOKEN_TOKEN_ENV_NAMES_INSTALLATION)GITHUB_TOKENGH_TOKEN_TOKEN_ENV_NAMES_GENERICT)frozenc                  x    e Zd ZU dZded<   dZded<   dZded<   dZd	ed
<   dZd	ed<   dZ	d	ed<   dZ
ded<   ddZy)r   u  token_source 분류 결과. raw value는 절대 보유하지 않는다.

    Fields:
      token_source: 분류 결과 (TokenSource enum value).
      env_var_name_observed: 어느 env var에서 token이 발견됐는지 (값 X — 이름만).
      token_prefix_observed: 5자 prefix만 (e.g. "ghs_", "ghp_", "githu"). 값 자체 X.
      actions_runner_signal: GITHUB_ACTIONS=true 또는 RUNNER_NAME 존재 시 True.
      installation_signal: ghs_ prefix 또는 INSTALLATION_TOKEN 계열 env 존재 시 True.
      owner_pat_signal: ghp_/github_pat_/gho_ prefix 또는 GH_PAT 계열 env 존재 시 True.
      token_fingerprint_sha256_8: token raw가 있을 경우 sha256(token)의 첫 8 hex 문자만.
                                  raw value는 절대 보존되지 않으며, 동일 token
                                  identity 추적용으로만 사용.

    회장 §보안: raw token value는 어떤 형태로도 보유 X.
    strtoken_sourceNOptional[str]env_var_name_observedtoken_prefix_observedFboolactions_runner_signalinstallation_signalowner_pat_signaltoken_fingerprint_sha256_8c                    t        |       S Nr   selfs    r+   to_dictzTokenSourceProbe.to_dict       d|r*   returndict)r%   r&   r'   r(   __annotations__rF   rG   rI   rJ   rK   rL   rR   r)   r*   r+   r   r      sV      +/=/+/=/"'4' %%"d"044r*   r   c                v    | syt        j                  | j                  d            j                         }|dd S )u  token raw value의 sha256 첫 8 hex 문자만 반환 (회장 §보안).

    동일 token identity 추적이 필요할 때만 사용. raw 값은 어디에도 저장하지 않는다.
    None / 빈 문자열 → None 반환 (audit에 fingerprint=null 로 기록).
    Nutf-8   )hashlibsha256encode	hexdigest)token_valuedigests     r+   r   r      s8     ^^K..w78BBDF"1:r*   c                    | sy| dd S )u   token raw → 첫 5자 prefix만 추출. 5자 미만이면 그대로 (X 마스크).

    회장 §보안: 5자 이상 노출 금지. 첫 5자도 prefix 식별용으로만 사용.
     N   r)   )r_   s    r+   _detect_prefixrd      s    
 r?r*   )envc                2   ||nt         j                  d}j                  dd      j                         dk(  rd}n&t        D ]  }|dk(  r	j                  |      sd} n t        fdt        D              }t        fdt        D              }d}d}d}| rKt        |       }t        D ]  }	| j                  |	      sd} n t        D ]  }	| j                  |	      sd} n d}
t        t              t        t              z   t        t              z   D ]  }j                  |      s|}
 n |r	|st        }n5|rt        }n,|r	|rt         }n!|rt        }n|rt        }n|rt         }nt"        }t%        ||
|xs d||xs ||xs |t'        |       	      S )
u  token 값 (선택) + env mapping → TokenSource 분류.

    분류 우선순위 (회장 §1 deterministic):
      1. token prefix 검사 (값이 있는 경우 — 5자만 비교, raw 노출 X):
         - "ghs_"            → GITHUB_APP_INSTALLATION_TOKEN
         - "ghp_" / "githu" (github_pat_) / "gho_" → OWNER_PAT
      2. env-only fallback (값이 없거나 prefix 매칭 실패):
         - GITHUB_APP_* env 존재 → GITHUB_APP_INSTALLATION_TOKEN
         - GH_PAT / GITHUB_PAT / OWNER_GITHUB_TOKEN env 존재 → OWNER_PAT
         - GITHUB_ACTIONS=true 또는 RUNNER_NAME 존재 → GITHUB_ACTIONS_TOKEN
         - 그 외 → UNKNOWN
      3. **GitHub Actions runner 신호가 있는데 token prefix가 ghs_ 인 경우는
         GITHUB_ACTIONS_TOKEN으로 분류** (Actions runner의 GITHUB_TOKEN은
         실제로는 installation token이지만, 식별 가능한 출처는 Actions runner이므로
         "Actions runner의 자동 token" 으로 박제).

    회장 §보안 준수:
      - raw token value 절대 출력/저장 X.
      - prefix는 5자만 보존 (e.g. "ghs_", "ghp_").
      - env_var_name_observed는 이름만 (값 X).
    NFr2   rb   trueTc              3  @   K   | ]  }j                  |        y wrN   get.0nenv_maps     r+   	<genexpr>z(classify_token_source.<locals>.<genexpr>   s     !XQ'++a.!X   c              3  @   K   | ]  }j                  |        y wrN   ri   rk   s     r+   ro   z(classify_token_source.<locals>.<genexpr>   s     R!w{{1~Rrp   )rD   rF   rG   rI   rJ   rK   rL   )osenvironrj   lowerr6   anyr=   r:   rd   r-   
startswithr1   listr@   r   r   r   r   r   r   )r_   re   actions_signalnameinstallation_env_signalowner_pat_env_signalprefixinstallation_prefix_signalowner_pat_prefix_signalprF   final_sourcern   s               @r+   r   r      s   4 ),RZZG N{{#R(..0F:- 	D''{{4 !%	 "!X:W!XXR7QRR F!&#,- 	A%%a(-1*	 % 	A%%a(*.'	 ,0*+
)
*	+
'
(	)
 ;;t$(! "..	 -	#2	 .	-	2+!3$n,6Q:Q0H4H#>{#K r*   c                    | | nt         j                  }d}t        t              t        t              z   t        t
              z   D ]  }|j                  |      }|s|} n t        ||      S )u   env-only 분류. env 우선순위:
       INSTALLATION_TOKEN 계열 > GITHUB_PAT 계열 > GITHUB_TOKEN/GH_TOKEN > 없음.

    raw token 값은 함수 내부에서만 검사하고 외부로 노출하지 않는다.
    N)r_   re   )rr   rs   rw   r=   r:   r@   rj   r   )re   rn   candidate_valuery   vs        r+   r   r     ss     ),RZZG%)O*+
)
*	+
'
(	)
 KKO !_'JJr*   BOT_MERGE_IDENTITY_AUDIT_JSONLmemoryzorchestration-auditzbot-merge-identity.jsonlc                      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Zded<   dZded<   dZded<   dZded<   dZ	ded<   dZ
ded<   dZded<   ddZy)r   u  Single PR merge audit record.

    회장 §3 audit 구조 7 field 정확 매칭:
      pr_number, token_source_used, mergedBy_login, mergedBy_is_bot,
      expected_bot_identity, autonomy_capability_gap, timestamp.

    추가 metadata는 분리된 sub-dict (raw value 절대 X).
    int	pr_numberrC   token_source_usedmergedBy_loginrH   mergedBy_is_botexpected_bot_identityautonomy_capability_gap	timestampNrE   rG   rF   rL   merge_commit_shatask_idnotesFowner_pat_usedc                    t        |       S rN   rO   rP   s    r+   rR   z MergeIdentityAuditRecord.to_dict\  rS   r*   rT   )r%   r&   r'   r(   rW   rG   rF   rL   r   r   r   r   rR   r)   r*   r+   r   r   ?  s     N!!N+/=/+/=/044&*m*!G]!E= !ND r*   r   c                z    | sy| j                         j                  d      ry|xs dj                         dk(  ryy)u   주어진 mergedBy actor가 bot/app identity로 인정 가능한지.

    - login이 "[bot]" 으로 끝나면 → True
    - actor_type이 "Bot" 이면 → True (대소문자 무시)
    - 그 외 → False
    Fz[bot]Trb   bot)rt   endswith)login
actor_types     r+   r   r   `  s=     {{}g&b!U*r*   c                6    |r|sy| t         k(  ry| t        k(  ryy)u  token_source + mergedBy → autonomy_capability_gap 박제.

    회장 §본질:
      - mergedBy가 bot/app이고 token_source도 GITHUB_APP_*/GITHUB_ACTIONS_TOKEN
        → False (정상 자동화 success).
      - mergedBy가 owner (사람) 이면 → True (owner_pat fallback이 명시적).
      - token_source == OWNER_PAT 면 → True (mergedBy가 bot이라도 owner_pat 사용은
        autonomy success로 인정 X — task-2522 §본질).
      - token_source == UNKNOWN 면 → True (fail-closed, 분류 불가는 보수적으로 GAP).
      - mergedBy 데이터가 비어있고 token_source가 GITHUB_APP_*/GITHUB_ACTIONS_TOKEN
        → False (audit 데이터 부재 → GAP 분류 안 함; pre-merge 단계 record).
    TF)r   r   rD   r   r   s      r+   r   r   p  s'    & o--++r*   rb   )r   mergedBy_typer   r   r   r   c                   t        ||      }|j                  t        t        hv }	t	        |j                  ||      }
|xs2 t        j                  t        j                        j                  d      }|j                  t        k(  }t        t        |       |j                  |xs d||	|
||j                  |j                  |j                  ||||      S )u   TokenSourceProbe + mergedBy → MergeIdentityAuditRecord 생성.

    timestamp 미지정 시 datetime.now(timezone.utc) 사용.
    r   z%Y-%m-%dT%H:%M:%S.%fZrb   )r   r   r   r   r   r   r   rG   rF   rL   r   r   r   r   )r   rD   r   r   r   r   nowr   utcstrftimer   r   r   rG   rF   rL   )r   token_prober   r   r   r   r   r   is_botexpectedgaptsr   s                r+   r   r     s     -^]KF''#, H ) --%C
 
	Rhll8<<099:QRB --1GGN#i.%22%+& #)??)??#.#I#I)% r*   
audit_pathc                  |xs t         }|j                  j                  dd       t        j                  | j                         d      }t        |dd      5 }|j                  |dz          d	d	d	       |S # 1 sw Y   |S xY w)
u   audit record를 JSONL 형식으로 append.

    회장 §보안: record는 raw token 값을 보유하지 않으므로 그대로 직렬화 가능.
    경로 부재 시 default 경로 사용. 부모 디렉터리는 자동 생성.
    T)parentsexist_okF)ensure_asciiarY   )encoding
N)r   parentmkdirjsondumpsrR   openwrite)recordr   pathlinefhs        r+   r   r     su     11DKKdT2::fnn&U;D	dC'	* b
KKs   A::B)r   r   r   r   r   c                    t        | xs g       }t        |      dk\  xr |d   dk(  xr |d   dk(  xr |d   dk(  }d|v }|D cg c]  }|d	v s|j                  d
      r| }}|xr |xr | }|xr |||||dS c c}w )u  `gh pr merge --delete-branch ...` 호출이 process-local GH_TOKEN 주입을
    그대로 상속하는지 정적 검증 (task-2523 §검증 6).

    회장 §본질:
      branch cleanup도 같은 머지 호출 안에서 일어나야 한다 (별도 토큰 X).
      `gh pr merge --squash --delete-branch` 한 번 호출로 머지+삭제가
      동일 process 내 동일 GH_TOKEN으로 수행된다.

    검증 항목 (정적 — 실제 API 호출 X):
      - args에 `gh pr merge` 가 포함
      - `--delete-branch` 플래그 존재 (branch cleanup 동일 호출)
      - admin override / force / rebase 플래그 부재 (회장 §금지)

    반환:
      {
        "branch_cleanup_in_same_call": bool,
        "delete_branch_flag_present": bool,
        "merge_command_present": bool,
        "forbidden_flags_detected": list[str],
        "token_inherits_process_local_gh_token": bool,
      }
       r   gh   pr   mergez--delete-branch>   --admin--force--rebasez--admin=)branch_cleanup_in_same_calldelete_branch_flag_presentmerge_command_presentforbidden_flags_detected%token_inherits_process_local_gh_token)rw   lenrv   )
merge_argsargsmerge_presentdelete_presentr   	forbiddentoken_inheritss          r+   r   r     s    . 
 b!DD	QU47d?UtAw$U47gCU  '$.N22all:6N 	
I  #G~Gi-N'4'G&4!.$-1? s   A;c                  :    e Zd ZU dZded<   ded<   ded<   ded<   y)	r   u  이전 score → 다음 score 변화 박제.

    회장 §정책:
      - mergedBy=bot/app & token_source=GITHUB_APP_* → +1 (cap 10)
      - mergedBy=bot/app & token_source=GITHUB_ACTIONS_TOKEN → +1 (cap 10)
      - mergedBy=owner & token_source=OWNER_PAT → 유지 또는 -1 (회장 §본질 - stuck signal)
      - token_source=OWNER_PAT (mergedBy bot 포함) → -1 (위장 차단)
      - token_source=UNKNOWN → 유지 (분류 실패는 점수 변경 안 함, GAP만 박제)
    r   previous_score	new_scoredeltarC   reasonN)r%   r&   r'   r(   rW   r)   r*   r+   r   r     s     NJKr*   r   c           	        t        dt        dt        |                   }d}d}|j                  rH|j                  t
        k(  rd}d}n||j                  r|j                  sd}d|j                   d}nQd}d}nL|j                  t        t        fv r0|j                  r$d	}d
|j                   d|j                  xs d d}nd}d}t        dt        d||z               }t        ||||z
  |      S )uZ   audit record를 기반으로 autonomy_score 변화 산출.

    score 범위: 0 ~ 10.
    r   
   rb   u5   owner_pat token used (autonomy fallback) — score -1zowner direct merge (mergedBy=u   ) — score -1uA   capability_gap detected without explicit owner_pat — score heldr   zbot/app merge (z) by z(pre-merge)u    — score +1u,   no gap but identity ambiguous — score held)r   r   r   r   )maxminr   r   r   r   r   r   r   r   r   )r   r   prevr   r   r   s         r+   r   r     s    q#b#n-./DEF%%##'==ELF""6+A+AE4V5J5J4K>ZFEXF ###'(
 
 $$E!&":":!;5((9M:-I 
 ECFAs2te|,-I$	 r*   c                 0   t        j                  d      } | j                  ddd       | j                  ddd       | j                  d	t        d
d       | j                  dt        d d       | j                  dt        d d       | j                  dt        d d       | j                  ddd       | j                  ddd       | j                  dt        dd       | j                  dt        dd       | j                  dt        dd       | S )Nu}   bot_merge_identity — token source 4 enum 분류 + identity audit 박제. (회장 §보안: raw token value 절대 출력 X))descriptionz--classify-token-source
store_trueuI   env에서 token source 추출 후 JSON 출력 (값 X — source label만))actionhelpz--audituJ   --pr 와 함께 사용. mergedBy stdin JSON 입력 → audit record 출력z--prr   u	   PR 번호)typedefaultr   z	--task-idztask id (audit metadata)z--merge-commitzmerge commit shaz--audit-pathu6   audit JSONL 경로 (default: DEFAULT_AUDIT_JSONL_PATH)z
--no-writeu0   --audit 시 JSONL append 하지 않고 stdout만z--scoreu5   stdin record JSON + --previous-score 로 delta 계산z--previous-score   u%   --score 시 이전 점수 (default 7)z--mergedBy-loginrb   u8   --audit: mergedBy login (실호출 대신 직접 주입)z--mergedBy-typez!--audit: mergedBy type (User/Bot))argparseArgumentParseradd_argumentr   rC   )r   s    r+   _build_parserr   S  sJ   A	A NN!X  
 NNY  
 NN6Q[NANN;S$=WNXNN##tBTNUNNE	   NN?  
 NND  
 NN4	   NNG	   NN0	   Hr*   c           
        t               }|j                  | t        |       nd       }|j                  r:t	               }t        t        j                  |j                         dd             y|j                  r|j                  dk  r1t        t        j                  ddi      t        j                         yt	               }t        |j                  ||j                  |j                  |j                   |j"                        }|j$                  s0|j&                  rt)        |j&                        nd }t+        ||	       t        t        j                  |j                         dd             y|j,                  r	 t        j.                  t        j0                  j3                         xs d
      }	 t7        di |j9                         D 	ci c]  \  }}	|t6        j:                  v s||	 c}	}}t?        |j@                  |      }
t        t        j                  tC        |
      dd             y|jE                          y# t        j4                  $ r=}t        t        j                  dd| i      t        j                         Y d }~yd }~ww xY wc c}	}w # t<        $ r=}t        t        j                  dd| i      t        j                         Y d }~yd }~ww xY w)NFr   )r   indentr   errorz--pr is required)file)r   r   r   r   r   r   r   z{}zstdin not JSON: zrecord fields mismatch: )r   r   r)   )#r   
parse_argsrw   r   r   printr   r   rR   auditr   sysstderrr   r   r   merge_commitr   no_writer   r	   r   scoreloadsstdinreadJSONDecodeErrorr   items__dataclass_fields__	TypeErrorr   r   r   
print_help)argvparserr   prober   r   payloadexckr   r   s              r+   mainr     sE   _F4+;T$ZFD!!+-djjuQGHzz77a<$**g'9:;#**M+-#gg..,,!..LL
 }}26//doo.tJv*=djj)aHIzz	jj!1!9T:G
	-  J'--/  1I$!QUVZr  [H  [H  VHA  1I  JF -D<O<OX^_djjU1EF
 ## 	$**g)9#'?@A

S	
 1I 	$**g)A#'GHIPSPZPZ[	sH   5I
 7J# J,J1J# 
J3JJJ# #	K),3K$$K)__main__)r_   rE   rU   rE   )r_   rC   rU   rC   )r_   rE   re   Optional[Mapping[str, str]]rU   r   rN   )re   r  rU   r   )r   rC   r   rC   rU   rH   )rD   rC   r   rC   r   rH   rU   rH   )r   r   r   r   r   rC   r   rC   r   rE   r   rE   r   rE   r   rE   rU   r   )r   r   r   zOptional[Path]rU   r	   )r   z	list[str]rU   rV   )r   r   r   r   rU   r   )rU   zargparse.ArgumentParser)r   zOptional[Iterable[str]]rU   r   );r(   
__future__r   r   r[   r   rr   r   dataclassesr   r   r   r   enumr   pathlibr	   typingr
   r   r   __all__r   r   r   r   rC   r   r-   rW   r1   r6   r:   r=   r@   r   r   rd   r   r   rs   rj   __file__resolver   r   r   r   r   r   r   r   r   r   r   r   r   r%   exitr)   r*   r+   <module>r     s  : #    	 
 ) '   . .< % 9 4   ##t #( 1: o 9'F _ F. ? / O 
2  
- /  $  :	 (,cc 
%c 	cLK6  JJNN(DN""$++22X=@UUXrrs  $  @   	
 
F &*!#** "* 	*
 * $* * * * *` "&$  
	0/ O )b $  "-- %- 	-h6r.b zCHHTV r*   