
    `gjZ                       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
mZmZmZ  ej                  e      ZdZdZddZd	 Zdd
Z ed       G d d             Zd dZ ej0                  d      d d       Zd!dddd	 	 	 	 	 	 	 d"dZd!dddd	 	 	 	 	 	 	 	 	 d#dZd!dddd	 	 	 	 	 	 	 	 	 d$dZd%dZd&dZd'dZg dZ y)(u  Microsoft Entra ID adapter for Microsoft Foundry.

Provides keyless authentication for Microsoft Foundry deployments using the
`azure-identity` SDK's `DefaultAzureCredential` chain (env service principal
→ workload identity → managed identity → VS Code → Azure CLI → azd →
PowerShell → broker).

Architecture mirrors `agent/bedrock_adapter.py`:

* Lazy import. `azure-identity` is only loaded when ``model.auth_mode =
  entra_id`` is selected. Users who stick with `AZURE_FOUNDRY_API_KEY`
  never pay the import cost.
* SDK-callable contract. The public entry point ``build_token_provider``
  returns a zero-arg callable produced by ``get_bearer_token_provider`` —
  this is exactly the value Microsoft's documented sample plugs into
  ``OpenAI(api_key=token_provider, base_url=...)``. The OpenAI SDK calls
  it before every request, so token refresh is transparent.
* Three explicit consumer-side helpers (display / cache / http-bearer)
  rather than one generic "materialize" function — splitting them by
  purpose prevents accidental token-minting in logging paths or token
  leakage into cache keys / dashboard JSON.
* No persisted JWT. ``azure-identity`` caches in-process and (where
  available) in the OS keychain or ``~/.IdentityService``. Hermes does
  not duplicate that storage in ``auth.json``.

Reference: https://learn.microsoft.com/azure/ai-foundry/foundry-models/how-to/configure-entra-id

Requires: ``azure-identity`` (optional dependency — only needed when
``model.auth_mode = entra_id``).
    )annotationsN)	dataclass)AnyCallableDictOptionalzhttps://ai.azure.com/.defaultzprovider.azure_identityc                 ,    	 ddl } y# t        $ r Y yw xY w)ux   Return True if `azure-identity` can be imported right now.

    Cheap check — does not walk the credential chain.
    r   NTF)azure.identity	Exception)azures    O/root/.hermes/venv/lib/python3.12/site-packages/agent/azure_identity_adapter.pyhas_azure_identity_installedr   <   s    
 s    	c                     	 ddl m}  | S # t        $ rg 	 ddlm}m} n# t        $ r}t        d      |d}~ww xY w	  |t        d       n%# |$ r}t        dt        |      z         |d}~ww xY wddl m}  | cY S w xY w)zImport ``azure.identity``, lazy-installing it if allowed.

    Raises ``ImportError`` with a clear actionable message when the
    package is missing and lazy installs are disabled.
    r   N)ensureFeatureUnavailablezThe 'azure-identity' package is required for Azure AI Foundry Entra ID authentication. Install it with: pip install azure-identityF)promptzWThe 'azure-identity' package is required for Azure AI Foundry Entra ID authentication. )r
   identityImportErrortools.lazy_depsr   r   _AZURE_IDENTITY_FEATUREstr)_air   r   excs       r   _require_azure_identityr   H   s    $
 	BB 	- 			*59! 	469#h? 	 	%
)sL   
 	A:A:	727A:A
	A:
A,A''A,,A:9A:c                 L    t        t        dd      } t        |       r |         yy)u%  Clear the cached ``DefaultAzureCredential``. Used by tests and
    profile switches.

    Defensive against tests that ``monkeypatch.setattr`` over
    ``build_credential`` with a plain (non-lru-cached) function — those
    won't expose ``cache_clear()`` until pytest reverts the patch.
    cache_clearN)getattrbuild_credentialcallable)r   s    r   reset_credential_cacher    h   s%     *M4@K     T)frozenc                  \    e Zd ZU dZeZded<   dZded<   ddZddZ	e
d	d
	 	 	 dd       Zy	)EntraIdentityConfigu  Serializable Entra ID config.

    Captures the Hermes-managed Entra knobs we need outside Azure SDK
    environment configuration. Everything else
    (tenant ID, service principal secret, federated token file, sovereign
    cloud authority, etc.) flows through azure-identity's standard
    ``AZURE_*`` env vars — see the Bedrock pattern in
    ``hermes_cli/runtime_provider.py:1310-1377`` for the analogous
    "let the SDK read env" approach.

    ``scope`` is Microsoft's documented Foundry inference audience. Almost
    everyone uses the default; sovereign-cloud / non-standard tenants can
    override via ``model.entra.scope``. Identity selection (user-assigned
    managed identity, workload identity, service principal, tenant, authority)
    stays in the standard Azure SDK env vars such as ``AZURE_CLIENT_ID``.

    ``exclude_interactive_browser`` is kept as an internal constructor knob
    so probes stay non-interactive by default. It is not written by the setup
    wizard.

    The dataclass is frozen so it's hashable for ``functools.lru_cache``
    keying, and serializable across multiprocessing boundaries (workers
    rebuild the credential inside their own process).
    r   scopeTboolexclude_interactive_browserc                    t        | j                  xs d      j                         xs t        }t        j                  | d|       y )N r%   )r   r%   stripSCOPE_AI_AZURE_DEFAULTobject__setattr__)selfr%   s     r   __post_init__z!EntraIdentityConfig.__post_init__   s6    DJJ$"%++-G1G4%0r!   c                4    | j                   | j                  dS )Nr%   r'   r1   )r.   s    r   to_dictzEntraIdentityConfig.to_dict   s    ZZ+/+K+K
 	
r!   N)default_scopec                   |xs i }t        |j                  d      xs d      j                         xs
 |xs t        }t	        |j                  dd            } | ||      S )Nr%   r)   r'   Tr1   )r   getr*   r+   r&   )clsdatar3   r%   exclude_browsers        r   	from_dictzEntraIdentityConfig.from_dict   sb     zrDHHW%+,224__I_txx(EtLM(7
 	
r!   returnNone)r;   Dict[str, Any])r7   zOptional[Dict[str, Any]]r3   Optional[str]r;   z'EntraIdentityConfig')__name__
__module____qualname____doc__r+   r%   __annotations__r'   r/   r2   classmethodr9    r!   r   r$   r$   z   sO    2 (E3'(,,1
 48
$1
=R
 
r!   r$   c                `    t               }i }| j                  sd|d<    |j                  di |S )u  Construct a ``DefaultAzureCredential`` for ``config``.

    Only Hermes-selected knobs are passed as kwargs. Everything else
    (tenant, service principal secret, federated token file, sovereign
    cloud authority, etc.) is read by ``azure-identity`` from the
    standard ``AZURE_*`` environment variables — see Microsoft's
    documented credential resolution chain. Users configure those in
    ``~/.hermes/.env`` or the deployment environment.
    F&exclude_interactive_browser_credentialrE   )r   r'   DefaultAzureCredential)configaikwargss      r   _build_default_credentialrL      s=     
!	"BF --;@78$2$$.v..r!      )maxsizec                    t        |       S )u#  Return the cached ``DefaultAzureCredential`` for ``config``.

    Hermes processes use exactly one Entra config at a time (the
    ``model.entra.*`` block in config.yaml drives every aux task,
    subagent, and credential probe in the session). ``maxsize=1`` is
    intentional: it reflects the actual usage pattern and keeps the
    cache trivially small.

    ``EntraIdentityConfig`` is a frozen dataclass, so it's hashable and
    safe as an LRU-cache key. ``functools.lru_cache`` is thread-safe in
    CPython.

    If two distinct configs are ever passed (tests do this; production
    rarely), the LRU eviction handles it correctly — each call still
    returns a credential matching its config; only one is cached at a
    time. Use :func:`reset_credential_cache` to clear (e.g. in tests).
    )rL   )rI   s    r   r   r      s    & %V,,r!   )rI   base_urlr'   c                   t               }|t        | xs t        |      }t        |      }|j	                  ||j
                        S )u  Return a zero-arg callable that mints a fresh Entra bearer JWT.

    The returned callable is exactly what Microsoft's documented Foundry
    sample expects::

        from openai import OpenAI
        client = OpenAI(
            base_url="https://my-resource.openai.azure.com/openai/v1/",
            api_key=build_token_provider(),
        )

    Scope resolution order:
      1. ``config.scope`` when a config object is supplied
      2. explicit ``scope`` kwarg
      3. ``SCOPE_AI_AZURE_DEFAULT`` (Microsoft's documented Foundry scope)

    ``base_url`` is unused today and kept for back-compat. Tenant /
    service-principal / sovereign-cloud configuration flows through
    ``azure-identity``'s standard ``AZURE_*`` environment variables —
    see :func:`_build_default_credential` for the rationale.

    NOT serializable across process boundaries. For multiprocessing
    workers, serialize the ``EntraIdentityConfig`` and rebuild the
    provider inside the worker.
    r1   )r   r$   r+   r   get_bearer_token_providerr%   )r%   rI   rP   r'   rJ   
credentials         r   build_token_providerrT      sJ    > 
!	"B~$11(C
 "&)J''
FLLAAr!   g      $@)rI   timeout_secondsallow_installc                 	 t               s|sy	 t                )| xs dj                         xs t        }t        dd|i|ddi	d	fd}t        j                  |d	      }|j                          |j                  t        d
|             |j                         rt        j	                  d|       yt        	j                  d            S # t        $ r }t        j	                  d|       Y d}~yd}~ww xY w)u!  Best-effort probe: can `DefaultAzureCredential` mint a token now?

    Runs ``credential.get_token(scope)`` under a thread-based timeout so
    a slow token service can't hang the caller. Returns False on any
    error — never raises. Use for ``hermes doctor`` /
    ``hermes auth status`` / wizard preflight.

    ``allow_install``: when True (default) and ``azure-identity`` is not
    importable, the adapter triggers the standard lazy-install path
    (subject to ``security.allow_lazy_installs``) before probing. Set
    False to make this strictly an "is installed?" check — used on hot
    paths like CLI startup where we never want pip to run.

    NOT used by ``is_provider_configured()`` — that path is structural
    only (no token mint), so CLI startup doesn't pay this latency.
    Fz+azure-identity lazy install unavailable: %sNr)   r%   okc                     	 t              } | j                  j                        }t        t	        |dd             d<   y # t
        $ r%}t        j                  d|       dd<   Y d }~y d }~ww xY w)NtokenrX   z!Entra credential probe failed: %sF)r   	get_tokenr%   r&   r   r   loggerdebugrS   tokr   rI   results      r   _probez.has_azure_identity_credentials.<locals>._probe)  se    	!)&1J&&v||4CWd ;<F4L 	!LL<cB F4L	!s   ?A 	A1A,,A1Ttargetdaemon{Gz?timeoutz-Entra token service probe timed out after %ssrE   r:   )r   r   r   r\   r]   r*   r+   r$   	threadingThreadstartjoinmaxis_aliver&   r5   )
r%   rI   rU   rV   	overridesr   effective_scopera   threadr`   s
    `       @r   has_azure_identity_credentialsrq     s    , ()	#% ~ ;B--/I3I$H?HiHE]F! VD9F
LLN
KKD/2K3DoV

4 !!1  	LLFL	s   
C 	C>C99C>)r%   rU   rV   c                   ddi}t               s|sd|d<   d|d<   |S 	 t                 )|xs d	j	                         xs t
        }t        d"d
|i|  j                  |d
<   t        j                  j                  dd	      j	                         r$t        j                  d   j	                         |d<   g }t        j                  j                  dd	      j	                         r|j                  d       t        j                  j                  dd	      j	                         rmt        j                  j                  dd	      j	                         r?t        j                  j                  dd	      j	                         r|j                  d       t        j                  j                  dd	      j	                         s.t        j                  j                  dd	      j	                         r|j                  d       ||d<   i d# fd}	t        j                  |	d      }
|
j                          |
j                  t!        d|             |
j#                         rd|dd|d<   d|d<   |S dv r
d   |d<   |S j                  d      }|d |d<   |S d|d<   t%        |d!d      |d!<   |S # t        $ r#}t        |      xs d|d<   d|d<   |cY d}~S d}~ww xY w)$u  Return diagnostic info about the active credential chain.

    Best-effort: runs ``get_token()`` and inspects what came back.
    Designed for ``hermes doctor`` and the wizard preflight — never
    raises, returns ``{"ok": False, "error": ...}`` on failure.

    ``allow_install``: when True (default) and ``azure-identity`` is not
    importable, the adapter triggers the standard lazy-install path
    (subject to ``security.allow_lazy_installs``) before probing. The
    install failure is surfaced as the diagnostic error when it fails.
    Set False for hot CLI paths that should never trigger pip.

    ``azure-identity`` doesn't expose the winning inner credential as
    a public field, so we report a coarse picture (env vars present,
    token expiry, claims-derived tenant) rather than the credential
    class name. Users wanting the precise class can run with
    ``AZURE_LOG_LEVEL=DEBUG``.
    rX   Fzazure-identity not installederrorzApip install azure-identity (or rely on lazy install at first use)hintzqpip install azure-identity manually, or enable lazy installs (security.allow_lazy_installs: true in config.yaml).Nr)   r%   AZURE_TENANT_IDtenant_id_envAZURE_FEDERATED_TOKEN_FILEz7WorkloadIdentityCredential (AZURE_FEDERATED_TOKEN_FILE)AZURE_CLIENT_IDAZURE_CLIENT_SECRETz%EnvironmentCredential (client secret)IDENTITY_ENDPOINTMSI_ENDPOINTz-ManagedIdentityCredential (IDENTITY_ENDPOINT)env_sourcesc                     	 t              } | j                  j                        }|d<   y # t        $ r}t	        |      d<   Y d }~y d }~ww xY w)NrZ   rs   )r   r[   r%   r   r   r^   s      r   ra   z*describe_active_credential.<locals>._probe  sL    	')&1J&&v||4C!F7O 	'!#hF7O	's   +/ 	AAATrb   re   rf   zToken probe timed out after z.0fszDefaultAzureCredential can be slow when the token service is unreachable or when az login state is stale. Try `az login` or set AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_CLIENT_SECRET.rZ   zcredential chain exhausted
expires_onrE   r:   )r   r   r   r   r*   r+   r$   r%   osenvironr5   appendrh   ri   rj   rk   rl   rm   r   )rI   r%   rU   rV   rn   infor   ro   r|   ra   rp   rZ   r`   s   `           @r   describe_active_credentialr   ;  s   0 !%=D'):DM L K		#% ~ ;B--/I3I$H?HiHLLDM 
zz~~',224 "

+< = C C E_ K	zz~~2B7==?TU


("-335

4b9??A

0"5;;=BC	zz~~)2.446"**..Y[:\:b:b:dJK%D  F' VD9F
LLN
KKD/2K36s6K1MWG 	V
 &wWJJwE}4WDJ d;DKC  	HF(FDM  L
 K	s   
J0 0	K9KKKc                >    t        |       xr t        | t               S )zReturn True when ``value`` is a callable Entra token provider.

    Used at the seams where a consumer must decide between
    string-API-key semantics and bearer-callable semantics.
    )r   
isinstancer   )values    r   is_token_providerr     s     E?9:eS#999r!   c                    t        |       r& |        }t        |t              r|st        d      |S t        | t              r| r| S t        d      )uF  Return a fresh Bearer JWT for a manual HTTP request.

    Only call this at sites that must construct an ``Authorization``
    header outside the OpenAI SDK (e.g. ``hermes_cli/azure_detect.py``).
    Calls the callable exactly once and returns the resulting token.

    **Anthropic SDK integration:** the Anthropic Python SDK does not
    accept a ``Callable[[], str]`` for ``auth_token``. Instead,
    :func:`build_bearer_http_client` returns an ``httpx.Client`` whose
    request event hook calls this function and rewrites the
    ``Authorization`` header per request — and that client is passed to
    the Anthropic SDK via ``http_client=...``. See
    :func:`agent.anthropic_adapter.build_anthropic_client` for the
    consumer.

    Raises ``ValueError`` if ``value`` is not a callable token provider
    or non-empty string.
    z#token provider returned empty valuez"no usable api_key / token provider)r   r   r   
ValueError)r   rZ   s     r   materialize_bearer_for_httpr     sM    & %%UBCC%%
9
::r!   c                     t               st        d      	 ddl}d fd} |j                  d	dd|gii|S # t        $ r}t        d      |d}~ww xY w)
uy  Return an ``httpx.Client`` that mints a fresh Entra bearer JWT
    per outbound request.

    The Anthropic SDK (≤ 0.86.0 at the time of writing) stores
    ``api_key`` / ``auth_token`` as static strings and computes the
    ``Authorization`` header at construction time. To get per-request
    token refresh (the Microsoft-recommended Foundry pattern for
    callable bearer providers), we install an httpx ``request`` event
    hook on a custom client and pass that client to the SDK via
    ``http_client=...``. The hook:

      1. Calls :func:`materialize_bearer_for_http` to mint a fresh JWT
         (azure-identity caches internally — this is cheap when the
         cached token is still valid).
      2. Strips any pre-set ``Authorization`` / ``api-key`` /
         ``x-api-key`` headers the SDK may have added (avoids
         conflicting auth values).
      3. Sets ``Authorization: Bearer <fresh-jwt>``.

    ``token_provider`` must be a zero-arg callable returning a string —
    typically the result of :func:`build_token_provider`.

    ``httpx_kwargs`` are forwarded verbatim to ``httpx.Client(...)`` so
    callers can attach a ``timeout``, ``transport``, ``proxy``, etc.

    Raises ``ImportError`` if ``httpx`` is not installed (it is a
    transitive dependency of both ``openai`` and ``anthropic`` SDKs, so
    in practice always available when this helper is reached).
    zDbuild_bearer_http_client requires a zero-arg callable token providerr   Nzhttpx is required for Entra ID bearer auth on Microsoft Foundry Anthropic-style endpoints. It is normally a transitive dependency of the openai/anthropic SDKs.requestc                &   	 t              }dD ]  }| j                  j                  |d          d| | j                  d<   y # t        $ rC}t        j                  d|       dD ]  }| j                  j                  |d          Y d }~y d }~ww xY w)Nu   Bearer hook: Entra ID token provider returned empty (%s) — stripping Authorization headers. Azure will respond 401. Run `hermes doctor` or `az login` to recover.)AuthorizationauthorizationzApi-Keyzapi-keyz	X-Api-Keyz	x-api-keyzBearer r   )r   r   r\   warningheaderspop)r   rZ   r   header_nametoken_providers       r   _inject_bearerz0build_bearer_http_client.<locals>._inject_bearer  s    	/?E0 n 	3KOOT2	3-4UG+<(3  	 NN@ 	  r 7##K67-	s   A 	B9BBevent_hooks)r   z'httpx.Request'r;   r<   rE   )r   r   httpxr   Client)r   httpx_kwargsr   r   r   s   `    r   build_bearer_http_clientr     s    < ^,
 	

=< 5<<  01
 K  7
 		s   : 	AAA)r$   r+   r   r   rT   r   rq   r   r   r   r    )r;   r&   r:   )rI   r$   r;   r   )N)
r%   r>   rI   Optional[EntraIdentityConfig]rP   r>   r'   r&   r;   Callable[[], str])r%   r>   rI   r   rU   floatrV   r&   rn   r   r;   r&   )rI   r   r%   r>   rU   r   rV   r&   rn   r   r;   r=   )r   r   r;   r&   )r   r   r;   r   )r   r   r   r   r;   r   )!rB   
__future__r   	functoolsloggingr   rh   dataclassesr   typingr   r   r   r   	getLoggerr?   r\   r+   r   r   r   r    r$   rL   	lru_cacher   rT   rq   r   r   r   r   __all__rE   r!   r   <module>r      s  > #   	  ! 0 0			8	$ 9  4 	@
$ $0
 0
 0
f/& Q-  -*&BAE37=A	&B!>&B $1&B 7;	&B
 0&B\3"KO<@9=	3"+H3" 5:3" 37	3"
 143"
 9=3"ld6:8<59	d&3d 16d /3	d
 -0d
 5CdZ:;:Nbr!   