
    Sjf)                       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 ddl	m
Z
 dZ ej                  d       ej                  dej                        gZd	Z ej                  d
      Z ej                  d      Z ej                  d      ZdZ ej                  d       ej                  d      gZdZ ej                  dej                        ZdZ ej                  dej                         ej                  d       ej                  d       ej                  dej                         ej                  dej                         ej                  dej                         ej                  dej                         ej                  dej                        gZdZg dZd(dZd)dZd)dZd)d Zd)d!Zd*d+d"Z d,d#Z!d-d$Z"d.d%Z#d/d0d&Z$e%d'k(  r e& e$             y)1u  ANU PreToolUse Guard hook — task-2643 Track A (staged · live 미적용).

목적: ANU 본체가 위험 tool call (특히 Bash CI/Gemini polling) 을 실행하기 *전에*
deny 해서 session-bound polling 위반 (PR #145 bzaona6au 사건) 재발을 막는다.

본 스크립트는 **staged draft** 다:
- `~/.claude/settings.json` 의 `PreToolUse` 섹션에 등록되어야 활성된다.
- 본 task-2643 범위에서는 live 적용 0 (회장 verbatim).
- 활성 절차: `memory/specs/task_2643_final_activation_packet_template_260523.json`.

fail-closed 원칙:
- 입력 JSON parse 실패, config 누락, internal exception → **deny** (allow fallback 금지).
- 어떤 정상 tool call 도 차단되어선 안 되지만, 위반 시 차단이 더 안전한 trade-off.

핵심 검사:
- tool_name == "Bash" 인 경우만 검사한다 (다른 tool 은 allow).
- forbidden pattern 5 그룹 (spec §2.2) 정적 매칭.
- match 발생 시 deny + reason + allowed_alternative + next_steps 반환.

stdin/stdout 규약:
- claude-code `PreToolUse` hook 규약: stdin 으로 `{tool_name, tool_input, ...}` JSON 수신.
- stdout 으로 `{decision, reason, allowed_alternative?, next_steps?}` JSON 반환.
- exit code 0 = decision 적용, 비0 = error (claude-code 가 fail-closed 처리).

CLI 모드 (dry-run):
- `--mode=dry-run --fixture=<path>` 형태로 fixture JSON 을 입력받아 결과를 stdout 으로 dump.
- `--mode=stdin` (default) 으로 claude-code hook 처럼 stdin 처리.
    )annotationsN)Path)AnyBACKGROUND_GH_PR_POLLz\bgh\s+pr\s+(view|checks)\bstatusCheckRollupLOOP_SLEEP_GH_PR_POLLz\b(while|until)\bz\bsleep\s+\d+z\bgh\s+(pr|run|api)\bGH_RUN_WATCH_POLLz\bgh\s+run\s+watch\bz\bgh\s+run\s+list\bSEMANTIC_CI_GEMINI_WAITz_\b(wait\w*|until|poll\w*|monitor\w*|nudge_wait|gemini[\w\s.]*review\w*|ci[\w\s.]*complete\w*)\bEXPLICIT_FORBIDDEN_ACTIONzadmin[\s_-]*overrideBOT_GITHUB_TOKENBOT_APP_TOKENchair_authorizationzauto[\s_-]*merge.*activatz\bPR\s*#?141\b.*pilotz\bgh\b[^|;&]*--admin\bz\bpr\s+merge\b[^|;&]*--admin\bdelegated_watcher_contract)uM   1. watcher contract 9 필드 생성 (schemas/watcher_contract_v1.json 참조)u1   2. watcher dispatch (dev bot 또는 cron-watcher)u:   3. ANU normal callback 대기 (owner_key=c119085addb0f8b7)c                :     |syt         fdt        D              S )zFGroup 1: run_in_background=True + gh pr view/checks/statusCheckRollup.Fc              3  @   K   | ]  }|j                          y wNsearch.0pcommands     M/home/jay/workspace/.worktrees/task-2643-dev6/hooks/pre_tool_use_anu_guard.py	<genexpr>z*_has_background_gh_poll.<locals>.<genexpr>g        ;Qqxx ;   )anyGROUP_1_PATTERNS)r   run_in_backgrounds   ` r   _has_background_gh_pollr    c   s    ;*:;;;    c                    t        t        j                  |             }t        t        j                  |             }t        t        j                  |             }|xr |xr |S )u4   Group 2: while/until + sleep + gh pr/run/api 결합.)boolGROUP_2_LOOP_KEYWORDSr   GROUP_2_SLEEPGROUP_2_GH_POLL)r   has_loop	has_sleephas_ghs       r   _has_loop_sleep_gh_pollr*   j   sR    )009:H]))'23I/((12F,	,f,r!   c                4     t         fdt        D              S )u+   Group 3: gh run watch / gh run list 반복.c              3  @   K   | ]  }|j                          y wr   r   r   s     r   r   z$_has_gh_run_watch.<locals>.<genexpr>u   r   r   )r   GROUP_3_PATTERNSr   s   `r   _has_gh_run_watchr/   s       ;*:;;;r!   c                    t         j                  |       syt        t        j                  d|       xs
 d| v xs d| v       S )u6   Group 4: CI/Gemini wait 의도 + GH API 호출 결합.Fz\bgh\s+zapi.github.comr   )GROUP_4_INTENT_KEYWORDSr   r#   rer.   s    r   _has_semantic_waitr4   x   sH    "))'2
		*g& 	*w&	*') r!   c                4     t         fdt        D              S )u$   Group 5: 명시적 forbidden action.c              3  @   K   | ]  }|j                          y wr   r   r   s     r   r   z*_has_explicit_forbidden.<locals>.<genexpr>   r   r   )r   GROUP_5_PATTERNSr.   s   `r   _has_explicit_forbiddenr8      r0   r!   c                   t        | t              r| j                         sddiS g }t        | |      r|j	                  t
               t        |       r|j	                  t               t        |       r|j	                  t               t        |       r|j	                  t               t        |       r|j	                  t               |sddiS |d   }dd||t        t        dS )u  Bash command 를 5 그룹 정적 검사한 뒤 decision dict 를 반환한다.

    Args:
        command: Bash tool 의 `command` 인자.
        run_in_background: Bash tool 의 `run_in_background` 인자 (기본 False).

    Returns:
        {"decision": "allow"} 혹은
        {"decision": "deny", "reason": "...", "allowed_alternative": "...", "next_steps": [...], "match_group": "..."}
    decisionallowr   denyANU_DIRECT_CI_POLLING_FORBIDDEN)r:   reasonmatch_groupall_match_groupsallowed_alternative
next_steps)
isinstancestrstripr    appendGROUP_1_BACKGROUND_GH_POLLr*   GROUP_2_LOOP_POLLr/   GROUP_3_RUN_WATCHr4   GROUP_4_SEMANTIC_WAITr8   GROUP_5_EXPLICIT_FORBIDDENALLOWED_ALTERNATIVE
NEXT_STEPS)r   r   matched_groupsprimary_reasons       r   evaluate_bash_commandrP      s     gs#7==?G$$ "Nw(9:89w'/0!/0'"34w'89G$$#A&N3%*2  r!   c                X   | j                  d      xs | j                  d      }|dk7  rdddS | j                  d      xs | j                  d      xs i }t        |t              sd	d
t        dgdS |j                  dd      }t	        |j                  dd            }t        ||      S )u   전체 tool call payload 에서 decision 을 산출한다.

    Bash 가 아니면 allow.
    Bash 면 command/run_in_background 추출 후 evaluate_bash_command 위임.
    	tool_nametoolNameBashr;   NOT_BASH_TOOL)r:   r>   
tool_input	toolInputr<   INVALID_TOOL_INPUT_TYPEu1   Bash tool_input 형식 오류. payload 재검토.)r:   r>   rA   rB   r    r   F)getrC   dictrL   r#   rP   )payloadrR   rV   r   r   s        r   evaluate_tool_callr]      s     K(CGKK
,CIF#??\*Lgkk+.FL"Jj$' /#6NO	
 	
 nnY+GZ^^,?GH *;<<r!   c                     t         j                  j                         } | j                         st	        d      t        j                  |       S )Nzempty stdin payload)sysstdinreadrE   
ValueErrorjsonloads)raws    r   _read_stdin_payloadrf      s5    
))..
C99;.//::c?r!   c                ^    t        j                  t        |       j                  d            S )Nzutf-8)encoding)rc   rd   r   	read_text)paths    r   _load_fixturerk      s#    ::d4j**G*<==r!   c                   t        j                  d      }|j                  dddgd       |j                  dd	       |j                  |       }	 |j                  dk(  r-|j
                  st        d
      t        |j
                        }n
t               }t        |      }t        t        j                   |dd             y# t        $ r0}ddt        |      j                   d| t        ddgd}Y d }~Vd }~ww xY w)Nu/   ANU PreToolUse Guard (staged · live 미적용))descriptionz--moder`   zdry-run)choicesdefaultz	--fixtureu)   dry-run 모드일 때 fixture JSON 경로)helpu/   --fixture 가 dry-run 모드에 필수입니다r<   HOOK_FAIL_CLOSEDz: u/   hook 입력/내부 오류 → fail-closed deny.u:   정상 동작 확인 후 재시도 (rollback plan 참조).)r:   r>   errorrA   rB   F   )ensure_asciiindentr   )argparseArgumentParseradd_argument
parse_argsmodefixturerb   rk   rf   r]   	Exceptiontype__name__rL   printrc   dumps)argvparserargsr\   r:   excs         r   mainr      s    $$1bcF
7I*>P
*UVT"D
99	!<< !RSS#DLL1G)+G%g. 
$**XE!
<=  

(S	**+2cU3#6AL	


s   AC 	C>&C99C>__main__)r   rD   r   r#   returnr#   )r   rD   r   r#   )F)r   rD   r   r#   r   dict[str, Any])r\   r   r   r   )r   r   )rj   rD   r   r   r   )r   zlist[str] | Noner   int)'__doc__
__future__r   rv   rc   r3   r_   pathlibr   typingr   rG   compile
IGNORECASEr   rH   r$   r%   r&   rI   r-   rJ   r2   rK   r7   rL   rM   r    r*   r/   r4   r8   rP   r]   rf   rk   r   r~   
SystemExit r!   r   <module>r      s  : #   	 
   5 BJJ-.BJJ#R]]3  , "

#78 

+,"**56 ( BJJ&'BJJ%&  2 $"**fMM  9 BJJ&6BJJ"#BJJ BJJ%r}}5BJJ+R]];BJJ'7BJJ("--8BJJ0"--@
  3 
<-<
	<
'T=<>< z
TV
 r!   