
    epjwj                       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Zddl	m
Z
 ddlmZ ddlmZmZ  ej        e          ZdZdZd	Zd
ZdZdZdNdZdOdZdPdZdQdZddddRdZi Zded <   d!Z d"Z!d#Z"d$Z#dZ$d%Z%d&Z&d'Z'i Z(d(ed)<   d*Z)d+Z* e+h d,          Z,dSd.Z-dTd2Z.dUd4Z/dVd6Z0dWd9Z1dXd=Z2d>d?dYdBZ3dZdCZ4d[dEZ5dFdGdHd\dMZ6dS )]u  GitHub Copilot authentication utilities.

Implements the OAuth device code flow used by the Copilot CLI and handles
token validation/exchange for the Copilot API.

Token type support (per GitHub docs):
  gho_          OAuth token           ✓  (default via copilot login)
  github_pat_   Fine-grained PAT      ✓  (needs Copilot Requests permission)
  ghu_          GitHub App token      ✓  (via environment variable)
  ghp_          Classic PAT           ✗  NOT SUPPORTED

Credential search order (matching Copilot CLI behaviour):
  1. COPILOT_GITHUB_TOKEN env var
  2. GH_TOKEN env var
  3. GITHUB_TOKEN env var
  4. gh auth token  CLI fallback
    )annotationsN)Path)Optional)
IS_WINDOWSwindows_hide_flagszIv1.b507a08c87ecfe98ghp_)gho_github_pat_ghu_)COPILOT_GITHUB_TOKENGH_TOKENGITHUB_TOKEN      tokenstrreturntuple[bool, str]c                n    |                                  } | sdS |                     t                    rdS dS )zYValidate that a token is usable with the Copilot API.

    Returns (valid, message).
    )FzEmpty token)Fu3  Classic Personal Access Tokens (ghp_*) are not supported by the Copilot API. Use one of:
  → `copilot login` or `hermes model` to authenticate via OAuth
  → A fine-grained PAT (github_pat_*) with Copilot Requests permission
  → `gh auth login` with the default device code flow (produces gho_* tokens))TOK)strip
startswith_CLASSIC_PAT_PREFIX)r   s    =/home/thesage/.hermes/hermes-agent/hermes_cli/copilot_auth.pyvalidate_copilot_tokenr   6   sG    
 KKMME $##+,, 

 
 :    tuple[str, str]c                    d} t           D ]d}t          j        |d                                          }|r9d} t	          |          \  }}|st
                              d||           ^||fc S e| rt
                              d           dS t                      }|r*t	          |          \  }}|st          d|           |dfS dS )	zResolve a GitHub token suitable for Copilot API use.

    Returns (token, source) where source describes where the token came from.
    Raises ValueError if only a classic PAT is available.
    F Tz"Token from %s is not supported: %szCopilot env var(s) set but none held a supported token; skipping `gh auth token` fallback to honor explicit env-var intent (and avoid the subprocess cost on cold start, #60800).)r   r   z5Token from `gh auth token` is a classic PAT (ghp_*). zgh auth token)
COPILOT_ENV_VARSosgetenvr   r   loggerwarningdebug_try_gh_cli_token
ValueError)any_env_var_setenv_varvalvalidmsgr   s         r   resolve_copilot_tokenr-   K   s    O# 
  
 i$$**,, 	 "O/44JE3 8'3   <	 &  L	
 	
 	

 vE &+E22
s 	MMM   o%%6r   	list[str]c                 n   g } t          j        d          }|r|                     |           ddt          t	          j                    dz  dz  dz            fD ]Z}|| v rt          j                            |          r4t          j	        |t          j
                  r|                     |           [| S )zIReturn candidate ``gh`` binary paths, including common Homebrew installs.ghz/opt/homebrew/bin/ghz/usr/local/bin/ghz.localbin)shutilwhichappendr   r   homer!   pathisfileaccessX_OK)
candidatesresolved	candidates      r   _gh_cli_candidatesr=   |   s    J|D!!H $(### 	DIKK("U*T122 ) )	
 
""7>>)$$ 	)9bg)F)F 	)i(((r   Optional[str]c                 F   t          j        dd                                          } d t           j                                        D             }t
          rdt                      ini }t                      D ]}|ddg}| r|d| gz  }	 t          j	        |fddd	d
d|d|}n?# t          t          j        f$ r&}t                              d||           Y d}~bd}~ww xY w|j        dk    r4|j                                        r|j                                        c S dS )aa  Return a token from ``gh auth token`` when the GitHub CLI is available.

    When COPILOT_GH_HOST is set, passes ``--hostname`` so gh returns the
    correct host's token.  Also strips GITHUB_TOKEN / GH_TOKEN from the
    subprocess environment so ``gh`` reads from its own credential store
    (hosts.yml) instead of just echoing the env var back.
    COPILOT_GH_HOSTr   c                "    i | ]\  }}|d v	||S )>   r   r    ).0kvs      r   
<dictcomp>z%_try_gh_cli_token.<locals>.<dictcomp>   s3     ; ; ;$!Q999 A999r   creationflagsauthr   z
--hostnameTutf-8replacer   )capture_outputtextencodingerrorstimeoutenvz#gh CLI token lookup failed (%s): %sNr   )r!   r"   r   environitemsr   r   r=   
subprocessrunFileNotFoundErrorTimeoutExpiredr#   r%   
returncodestdout)hostname	clean_env_popen_kwargsgh_pathcmdresultexcs          r   r&   r&      sq    y*B//5577H; ;"*"2"2"4"4 ; ; ;I @JQ_&8&:&:;;rM%'' ) )( 	,L(++C	^#GI    FF ":#<= 	 	 	LL>MMMHHHH	 !!fm&9&9&;&;!=&&(((((4s   B""C8CCz
github.comi,  )hosttimeout_secondsr`   ra   floatc                8	   ddl }ddl}|                     d          }d| d}d| d}|j                            t
          dd                                          }|j                            ||d	d
dd          }	 |j        	                    |d          5 }t          j        |                                                                          }	ddd           n# 1 swxY w Y   nE# t          $ r8}
t                              d|
           t#          d|
            Y d}
~
dS d}
~
ww xY w|	                    dd          }|	                    dd          }|	                    dd          }t'          |	                    dt(                    d          }|r|st#          d           dS t#                       t#          d|            t#          d|            t#                       t#          ddd           t+          j                    |z   }t+          j                    |k     rKt+          j        |t0          z              |j                            t
          |dd                                           }|j                            ||d	d
dd          }	 |j        	                    |d!          5 }t          j        |                                                                          }ddd           n# 1 swxY w Y   n## t          $ r t#          d"dd           Y w xY w|                    d#          rt#          d$           |d#         S |                    d%d          }|d&k    rt#          d"dd           w|d'k    r`|                    d          }t3          |t4          t6          f          r|dk    rt5          |          }n|d(z  }t#          d"dd           |d)k    rt#                       t#          d*           dS |d+k    rt#                       t#          d,           dS |r"t#                       t#          d-|            dS t+          j                    |k     Kt#                       t#          d.           dS )/a  Run the GitHub OAuth device code flow for Copilot.

    Prints instructions for the user, polls for completion, and returns
    the OAuth access token on success, or None on failure/cancellation.

    This replicates the flow used by opencode and the Copilot CLI.
    r   N/https://z/login/device/codez/login/oauth/access_tokenz	read:user)	client_idscopeapplication/jsonz!application/x-www-form-urlencodedHermesAgent/1.0)AcceptzContent-Type
User-Agent)dataheaders   rO   z+Failed to initiate device authorization: %su,     ✗ Failed to start device authorization: verification_urizhttps://github.com/login/device	user_coder   device_codeinterval   u*     ✗ GitHub did not return a device code.z!  Open this URL in your browser: z  Enter this code: z  Waiting for authorization...T)endflushz,urn:ietf:params:oauth:grant-type:device_code)rf   rr   
grant_type
   .access_tokenu    ✓errorauthorization_pending	slow_downr   expired_tokenu,     ✗ Device code expired. Please try again.access_deniedu     ✗ Authorization was denied.u     ✗ Authorization failed: u*     ✗ Timed out waiting for authorization.)urllib.requesturllib.parserstripparse	urlencodeCOPILOT_OAUTH_CLIENT_IDencoderequestRequesturlopenjsonloadsreaddecode	Exceptionr#   r{   printgetmax_DEVICE_CODE_POLL_INTERVALtime	monotonicsleep_DEVICE_CODE_POLL_SAFETY_MARGIN
isinstanceintrb   )r`   ra   urllibdomaindevice_code_urlaccess_token_urlrl   reqrespdevice_datar_   rp   rq   rr   rs   deadline	poll_datapoll_reqr^   r{   server_intervals                        r   copilot_device_code_loginr      s{    [[F;;;;OC&CCC <!!,# #   vxx 	
 .
 
 (?+
 
 !  C^##C#44 	;*TYY[[%7%7%9%9::K	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	;   BCHHHBSBBCCCttttt
 #'9;\]]R00I//-44K;??:/IJJANNH i :;;;t 
GGG	
@.>
@
@AAA	
+	
+
+,,,	GGG	
*$???? ~/1H
.

X
%
%
8==>>>L**0&H,
 ,
   688	 	 >)), C/  * 
 
	''"'== :DIIKK$6$6$8$899: : : : : : : : : : : : : : : 	 	 	#2T****H	 ::n%% 	*&MMM.))

7B''+++#2T****k!!$jj44O/C<88 _q=P=P//A#2T****o%%GGG@AAA4o%%GGG34444 	GGG8889994m .

X
%
%p 
GGG	
67774sr    C- 9C!C- !C%%C- (C%)C- -
D/7-D**D/%L 9L:L L

L L
L L21L2z+dict[str, tuple[str, float, Optional[str]]]
_jwt_cachex   z0https://api.github.com/copilot_internal/v2/tokenvscode/1.104.1zGitHubCopilotChat/0.26.7g      ?z.copilot_jwt.jsoni   zdict[str, float]_exchange_failure_cacheg      N@g      @>         	raw_tokenc                    ddl }|                    |                                                                           dd         S )zNShort fingerprint of a raw token for cache keying (avoids storing full token).r   N   )hashlibsha256r   	hexdigest)r   r   s     r   _token_fingerprintr   \  s>    NNN>>)**,,--7799#2#>>r   r6   r   Optional[dict]c                t   	 |                                  j        t          k    r"t                              dt                     dS t          j        |                     d                    }t          |t                    r|ndS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)ug  Bounded read of the on-disk JWT store → dict, or None if unusable.

    Single chokepoint for every read of the persisted store (load, eviction,
    save-merge). A well-formed store is a few KB; a file over the 1 MiB cap or
    with non-dict content is treated as unusable so a corrupt/oversized file
    can't balloon memory or get rewritten back out.
    z6Persisted Copilot JWT store exceeds %d bytes; ignoringNrI   rM   z.Failed to read persisted Copilot JWT store: %s)statst_size_JWT_DISK_MAX_BYTESr#   r%   r   r   	read_textr   dictr   )r6   loadedr_   s      r   _read_jwt_storer   b  s    
99;;!444LLHJ]   4DNNGN<<==#FD11;vvt;   EsKKKttttts   AB A B 
B7B22B7Nonec                   | sdS t          |           }t                              |d           t                              |d           t	                      }|r|                                sdS 	 t          |          }|||v r||= |                    |j        dz             }|	                    t          j        |          d           	 t          j        |d           n# t          $ r Y nw xY wt          j        ||           dS dS dS # t          $ r&}t                               d|           Y d}~dS d}~ww xY w)a  Drop any cached exchanged JWT for ``raw_token`` (in-process + on-disk).

    Used by the runtime stale-credential recovery path: when a live request
    starts failing with a Copilot ``model_not_available_for_integrator`` /
    ``model_not_supported`` 400, the cached exchanged token (or a degraded raw
    fallback that was cached in its place) is stale. Evicting both cache tiers
    forces the next ``exchange_copilot_token`` call to hit the network and mint
    a fresh token instead of returning the poisoned cache entry.
    N.tmprI   r     z&Failed to evict cached Copilot JWT: %s)r   r   popr   _jwt_disk_pathexistsr   with_suffixsuffix
write_textr   dumpsr!   chmodr   rJ   r#   r%   )r   fpr6   storetmpr_   s         r   evict_cached_exchanged_tokenr   w  s}     	I	&	&BNN2t D)))D t{{}} D%%ub	""4;#788CNN4:e,,wN???e$$$$   JsD!!!!!   D D D=sCCCCCCCCCDs=   1AD C& %D &
C30D 2C33D 
ED<<EOptional[Path]c                 p    	 ddl m}  t           |                       t          z  S # t          $ r Y dS w xY w)zAPath to the on-disk exchanged-JWT cache (profile-aware), or None.r   get_hermes_homeN)hermes_constantsr   r   _JWT_DISK_FILENAMEr   r   s    r   r   r     sW    444444OO%%&&);;;   tts   $' 
55r   *Optional[tuple[str, float, Optional[str]]]c                   t                      }|r|                                sdS 	 t          |          }||                    |           nd}t	          |t
                    sdS |                    dd          }t          |                    dd          pd          }|                    d          }|r|r|||fS n2# t          $ r%}t          	                    d|           Y d}~nd}~ww xY wdS )uP   Load a persisted exchanged JWT for ``fp`` → (api_token, expires_at, base_url).N	api_tokenr   
expires_atr   base_urlz(Failed to load persisted Copilot JWT: %s)
r   r   r   r   r   r   rb   r   r#   r%   )r   r6   r   entryr   r   r   r_   s           r   _load_jwt_from_diskr     s   D t{{}} tF  %%!&!2		"%&& 	4IIk2..	599\155:;;
99Z(( 	3 	3j(22 F F F?EEEEEEEEF4s   =C 'AC 
C0C++C0r   r   r   c                   t                      }|sdS 	 i }|                                rt          |          pi }t          j                    fd|                                D             }|||d|| <   |                    |j        dz             }|                    t          j	        |          d           	 t          j        |d           n# t          $ r Y nw xY wt          j        ||           	 t          j        |d           dS # t          $ r Y dS w xY w# t          $ r&}t                              d|           Y d}~dS d}~ww xY w)	z:Persist an exchanged JWT (0o600), pruning expired entries.Nc                    i | ]F\  }}t          |t                    r,t          |                    d d          pd          k    C||GS )r   r   )r   r   rb   r   )rC   rD   rE   nows      r   rF   z%_save_jwt_to_disk.<locals>.<dictcomp>  sc     
 
 
1!T""
 (-QUU<-C-C-Hq'I'IC'O'O q'O'O'Or   )r   r   r   r   rI   r   r   z!Failed to persist Copilot JWT: %s)r   r   r   r   rR   r   r   r   r   r   r!   r   r   rJ   r#   r%   )	r   r   r   r   r6   r   r   r_   r   s	           @r   _save_jwt_to_diskr     s    D ?;;== 	0#D))/REikk
 
 
 

 
 
 #$ 
 
b	
 t{V344tz%((7;;;	HS%     	 	 	D	

3	HT5!!!!! 	 	 	DD	 ? ? ?8#>>>>>>>>>?sZ   B)D# ?C D# 
C"D# !C""D# ;D 
D D# D  D# #
E-EEg      $@ro   rO    tuple[str, float, Optional[str]]c          	     2   ddl }t          |           }t                              |          }|r*|\  }}}t	          j                    |t
          z
  k     r|||fS t          |          }|r7|\  }}}t	          j                    |t
          z
  k     r|||ft          |<   |||fS t                              |d          }	t	          j                    |	k     r4t          dt          |	t	          j                    z
             d          |j
                            t          dd|  t          dt          d	
          }
d}d}d}t          t                     D ]-}	 |j
                            |
|          5 }t%          j        |                                                                          }ddd           n# 1 swxY w Y    n# t,          $ r}|}t/          |dd          pt/          |dd          }|t0          v r#d}t2                              d|           Y d}~ nc|t           dz
  k     rGt6          |dz   z  }t2                              d|dz   t           ||           t	          j        |           Y d}~'d}~ww xY w|I|rt:          nt<          }t	          j                    |z   t          |<   t          dt            d|           |t                              |d           |                    dd          }|                    dd          }|st          d          |rtA          |          nt	          j                    dz   }d}|                    d          }tC          |tD                    rMtG          |                    d          pd          $                                %                    d          }|r|}|stM          |          }|||ft          |<   tO          ||||           t2                              d||           |||fS )a  Exchange a raw GitHub token for a short-lived Copilot API token.

    Calls ``GET https://api.github.com/copilot_internal/v2/token`` with
    the raw GitHub token and returns ``(api_token, expires_at, base_url)``.

    The returned token is a semicolon-separated string (not a standard JWT)
    used as ``Authorization: Bearer <token>`` for Copilot API requests.
    ``base_url`` is the account-specific API host: the authoritative
    ``endpoints.api`` advertised by the exchange (enterprise/proxied
    accounts), falling back to a host derived from the token's ``proxy-ep``
    field. Individual accounts have neither, so ``base_url`` is None.

    Results are cached in-process and reused until close to expiry.
    Raises ``ValueError`` on failure.
    r   Ng        zHCopilot token exchange recently failed; skipping re-attempt for another sGETztoken rh   )Authorizationrk   rj   Editor-Version)methodrm   Fro   codestatusTz7Copilot token exchange rejected (HTTP %s); not retryingrt   zCCopilot token exchange attempt %d/%d failed (%s); retrying in %.1fsz$Copilot token exchange failed after z attempts: r   r   r   z+Copilot token exchange returned empty tokeni  	endpointsapird   z3Copilot token exchanged, expires_at=%s, base_url=%s)(r   r   r   r   r   _JWT_REFRESH_MARGIN_SECONDSr   r   r'   r   r   r   _TOKEN_EXCHANGE_URL_EXCHANGE_USER_AGENT_EDITOR_VERSIONrange_EXCHANGE_MAX_ATTEMPTSr   r   r   r   r   r   getattr!_EXCHANGE_PERMANENT_HTTP_STATUSESr#   r%   _EXCHANGE_BACKOFF_BASE_SECONDSr   '_EXCHANGE_FAILURE_TTL_PERMANENT_SECONDS'_EXCHANGE_FAILURE_TTL_TRANSIENT_SECONDSr   rb   r   r   r   r   r   _derive_base_url_from_proxy_epr   )r   rO   r   r   cachedr   r   r   disk_cached_fail_untilr   rl   last_excpermanent_failureattemptr   r_   r   sleep_sttlr   api_endpoints                         r   exchange_copilot_tokenr     s     	I	&	&B ^^BF 3*0'	:x9;;&AAAAj(22 &b))K 3*5'	:x9;;&AAAA'X>JrNj(22 *--b#66Ky{{[  ={TY[[899= = =
 
 	

 .
 
 1i11.(-	
 
 ! 	 	C$ D$(H/00 $ $	$''W'== 8z$))++"4"4"6"6778 8 8 8 8 8 8 8 8 8 8 8 8 8 8E 	$ 	$ 	$HS&$//O73$3O3OF:::$(!M   /!3338GaKHYaK!7g   
7###!	$" | !9338 	
 '+ikkC&7#`3I``V^``
 
	 D)))"%%I,**J HFGGG '1Hz"""dikkD6HJ #H%%I)T"" $9==//5266<<>>EEcJJ 	$#H =1)<<X6JrNb)Z:::
LL=  
 j(**sD   G69F;/G;F?	?GF?	G
JA
I="AI==Jc                f   ddl }|                    d|           }|sdS |                    d          }dD ]0}|                    |          r|t	          |          d         } n1|                    d          }|                    d          rd|t	          d          d         z   }n|}d	| S )
a  Derive the Copilot API base URL from a proxy-ep field in the token.

    The exchanged Copilot token is a semicolon-separated string like
    ``tid=xxx;exp=xxx;proxy-ep=proxy.enterprise.githubcopilot.com;...``.
    This extracts ``proxy-ep`` and converts it to an API base URL by
    replacing the leading ``proxy.`` with ``api.``.

    Returns ``https://{api_hostname}`` or None if proxy-ep is absent.
    r   Nz(?:^|;)\s*proxy-ep=([^;\s]+)rt   )re   zhttp://rd   zproxy.zapi.re   )researchgroupr   lenr   )r   r   mproxy_epprefixapi_hosts         r   r   r   j  s     III
		1599A twwqzzH)  v&& 	F-HE	 s##H 8$$ HS]]^^44 h   r   tuple[str, Optional[str]]c                    | s| dfS 	 t          |           \  }}}||fS # t          $ r)}t                              d|           | dfcY d}~S d}~ww xY w)a2  Exchange a raw GitHub token for a Copilot API token, with fallback.

    Convenience wrapper: returns ``(api_token, base_url)`` on success, or
    ``(raw_token, None)`` if the exchange fails (e.g. network error, unsupported
    account type). This preserves existing behaviour for accounts that don't
    need exchange while enabling access to internal-only models for those that do.

    ``base_url`` is the account-specific API endpoint advertised by the
    exchange (``endpoints.api``, with a ``proxy-ep`` fallback), or None for
    individual accounts.
    Nz2Copilot token exchange failed, using raw token: %s)r   r   r#   r%   )r   r   _r   r_   s        r   get_copilot_api_tokenr    s      $!7	!B!B	1h(""   I3OOO$s    
AAAATF)is_agent_turn	is_visionr  boolr  dict[str, str]c                ,    dddd| rdndd}|rd|d	<   |S )
z~Build the standard headers for Copilot API requests.

    Replicates the header set used by opencode and the Copilot CLI.
    r   ri   zvscode-chatzconversation-editsagentuser)r   rk   zCopilot-Integration-IdzOpenai-Intentzx-initiatortruezCopilot-Vision-RequestrB   )r  r  rm   s      r   copilot_request_headersr    sB     +'"/-"/;wwV G  3,2()Nr   )r   r   r   r   )r   r   )r   r.   )r   r>   )r`   r   ra   rb   r   r>   )r   r   r   r   )r6   r   r   r   )r   r   r   r   )r   r   )r   r   r   r   )
r   r   r   r   r   rb   r   r>   r   r   )r   r   rO   rb   r   r   )r   r   r   r>   )r   r   r   r  )r  r  r  r  r   r  )7__doc__
__future__r   r   loggingr!   r2   rS   r   pathlibr   typingr   hermes_cli._subprocess_compatr   r   	getLogger__name__r#   r   r   _SUPPORTED_PREFIXESr    r   r   r   r-   r=   r&   r   r   __annotations__r   r   r   r   r   r   r   r   r   r   r   	frozensetr   r   r   r   r   r   r   r   r   r  r  rB   r   r   <module>r      s    $ # " " " " "   				                   H H H H H H H H		8	$	$ 1  5  H   "#    *. . . .b   *! ! ! !P  x x x x x x~ ;=
 < < < <!  I "1   !$ (   -/  . . . .*. '*0 ' %.Iooo$>$> !? ? ? ?   *!D !D !D !DH      ."? "? "? "?J @D G+ G+ G+ G+ G+ G+T! ! ! !@   4        r   