
    `gjh-                        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m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	Zd
Zdaded<   ddZed	 	 	 	 	 ddZ G d de      ZddZddZy)uQ
  DrainSecretProvider — shared-bearer-secret auth for the drain-control endpoint.

Task 2.0b of the safe-shutdown plan, and the FIRST consumer of the generic
non-interactive token-auth capability added in Task 2.0a
(``supports_token`` / ``verify_token`` on the ``DashboardAuthProvider`` ABC +
the route-agnostic ``token_auth`` middleware seam).

What it is
----------
A service-to-service auth provider. ``nous-account-service`` (NAS) provisions a
**per-agent unique** shared secret into each deployed agent's environment; this
provider verifies an inbound ``Authorization`` bearer token against that secret
with a constant-time compare and, on a match, vouches for the caller as the
``drain-control`` principal. It is NOT an interactive identity provider — there
is no login, cookie, session, or refresh. It implements ONLY the token
capability (``supports_token = True`` + ``verify_token``); the five interactive
ABC methods raise ``NotImplementedError``.

Why a plugin (not an ad-hoc header check on the drain route)
------------------------------------------------------------
Decisions.md Q-A: the drain credential MUST be a real auth plugin in the
dashboard auth framework, not a bolt-on. Q-C: the framework widening that
hosts it is generic (Task 2.0a) and this plugin is merely its first consumer.

Security properties (decisions.md Q-A)
--------------------------------------
* **Per-agent unique secret** — each agent gets a distinct secret; a leak's
  blast radius is one agent.
* **Entropy gate at registration** — a weak/short/low-entropy secret fails
  CLOSED at load (the plugin declines to register and records a skip reason);
  it is never silently accepted. Bar: >= 256 bits of entropy / >= 43
  url-safe-base64 chars, and the value must not be obviously structured
  (all-one-character, too few distinct characters).
* **Constant-time compare** — ``hmac.compare_digest`` on the request path, so
  the endpoint is not a timing oracle.

Configuration
-------------
The secret is a CREDENTIAL, so it is carried via an env var (the ``.env``-is-
for-secrets-only rule), provisioned by NAS at deploy time (Phase 3):

    HERMES_DASHBOARD_DRAIN_SECRET   # the per-agent shared secret (>=43 url-safe-b64 chars)

Behavioural knobs live in config.yaml (canonical surface):

    dashboard:
      drain_auth:
        scope: drain            # capability label attached to the principal
        min_secret_chars: 43    # entropy bar (optional; default 43 ~= 256 bits)

When ``HERMES_DASHBOARD_DRAIN_SECRET`` is unset, the plugin is a no-op (records
a skip reason) — agents that don't want NAS-driven drain just don't set it.
    )annotationsN)Counter)Optional)DashboardAuthProvider
LoginStartSessionTokenPrincipal+      g      `@z/api/gateway/drain strLAST_SKIP_REASONc                    | syt        |       }t        |       t        fd|j                         D               }|z  S )zTotal Shannon entropy (bits) of ``value`` over its character distribution.

    H = len * sum(-p_i * log2(p_i)). A long string drawn from a wide alphabet
    scores high; a long run of one character scores ~0.
    g        c              3  Z   K   | ]"  }|z  t        j                  |z        z   $ y wN)mathlog2).0cns     X/root/.hermes/venv/lib/python3.12/site-packages/plugins/dashboard_auth/drain/__init__.py	<genexpr>z _shannon_bits.<locals>.<genexpr>d   s'     H1QUdiiA..Hs   (+)r   lensumvalues)valuecountsper_charr   s      @r   _shannon_bitsr   Z   s@     U^FE
AHHHHHa<    	min_charsc                   | syt        |       |k  rdt        |        d| dS t        t        |             }|t        k  rd| dt         dS t        |       }|t        k  rd|d	d
t        d	dS y)a  Return a rejection reason if ``secret`` is too weak, else ``None``.

    Fail-closed entropy gate (decisions.md Q-A). Checks, in order:
      * length >= ``min_chars`` (default 43 url-safe-b64 chars ~= 256 bits),
      * at least ``_MIN_DISTINCT_CHARS`` distinct characters,
      * Shannon entropy >= ``_MIN_SHANNON_BITS`` bits.

    A ``None`` return means the secret passes. Any string return is a
    human-readable reason the caller logs + records as the skip reason.
    zsecret is emptyzsecret too short: z chars (need >= z]; use a >=256-bit value, e.g. `python -c "import secrets; print(secrets.token_urlsafe(32))"`)zsecret has only z distinct characters (need >= z); looks structured/low-entropyzsecret entropy too low: z.0fz bits (need >= z); looks structured/repeatedN)r   set_MIN_DISTINCT_CHARSr   _MIN_SHANNON_BITS)secretr"   distinctbitss       r   assess_secret_strengthr*   h   s      
6{Y V-=i[ I3 3	

 3v;H%%xj(F"##BD	
  D&tCj %%AC	
 r    c                  r    e Zd ZdZdZdZdZdZddddZdd	Z	dd
Z
	 	 	 	 	 	 	 	 	 	 ddZddZddZddZy)DrainSecretProviderz@Non-interactive shared-bearer-secret provider for drain control.zdrain-secretz"Drain Control (service credential)TFdrain)scopec               ^    t        |      }|t        d|       || _        |xs d| _        y )Nzdrain secret rejected: r-   )r*   
ValueError_secret_scope)selfr'   r.   reasons       r   __init__zDrainSecretProvider.__init__   s:     (/6vh?@@&wr    c                   |syt        j                  |j                  d      | j                  j                  d            r#t	        d| j
                  | j                  f      S y)a   Constant-time compare against the per-agent shared secret.

        Returns a ``drain-control`` principal on an exact match, else ``None``
        (the generic seam falls through / fails closed). Uses
        ``hmac.compare_digest`` so a wrong token can't be recovered by timing.
        Nzutf-8zdrain-control)	principalproviderscopes)hmaccompare_digestencoder1   r	   namer2   )r3   tokens     r   verify_tokenz DrainSecretProvider.verify_token   sW     u||G4dll6I6I'6RS!)~ 
 r    c                   t        d      )NzTDrainSecretProvider is a non-interactive service credential; there is no login flow.NotImplementedError)r3   redirect_uris     r   start_loginzDrainSecretProvider.start_login   s    !&
 	
r    c                   t        d      Nz<DrainSecretProvider is a non-interactive service credential.rA   )r3   codestatecode_verifierrC   s        r   complete_loginz"DrainSecretProvider.complete_login   s     "J
 	
r    c                    y r    )r3   access_tokens     r   verify_sessionz"DrainSecretProvider.verify_session   s     r    c                   t        d      rF   rA   r3   refresh_tokens     r   refresh_sessionz#DrainSecretProvider.refresh_session   s    !J
 	
r    c                    y r   rL   rP   s     r   revoke_sessionz"DrainSecretProvider.revoke_session   s    r    N)r'   r   r.   r   returnNone)r>   r   rU   zOptional[TokenPrincipal])rC   r   rU   r   )
rG   r   rH   r   rI   r   rC   r   rU   r   )rM   r   rU   zOptional[Session])rQ   r   rU   r   )rQ   r   rU   rV   )__name__
__module____qualname____doc__r=   display_namesupports_tokensupports_sessionr5   r?   rD   rJ   rN   rR   rT   rL   r    r   r,   r,      sh    JD7LN4; '&


#&
7:
JM
	


r    r,   c                     	 ddl m} m}  |       } | |ddd      }t        |t              r|S i S # t        $ r"}t        j                  d|       i cY d}~S d}~ww xY w)z<Return ``dashboard.drain_auth`` from config.yaml, or ``{}``.r   )cfg_getload_configzUdashboard-auth-drain: load_config() raised %s; falling back to env-only configurationN	dashboard
drain_auth)default)hermes_cli.configr_   r`   	Exceptionloggerdebug
isinstancedict)r_   r`   cfgexcsections        r   _load_config_drain_auth_sectionrm      sd    
:m c;dCG $/77R7  5	

 	s   1 	AAAAc                T   da t        j                  j                  dd      j	                         }|sda t
        j                  dt                yt               }t        |j                  dd      xs d      j	                         xs d}	 t        |j                  dt                    }t        ||	      }|!d
| da t
        j                  dt                y	 t        ||      }| j!                  |       	 ddlm}  |t&               t
        j+                  d|t&               y# t        t        f$ r	 t        }Y w xY w# t        $ r)}d| a t
        j                  dt                Y d}~yd}~ww xY w# t(        $ r%}t
        j                  dt&        |       Y d}~d}~ww xY w)u&  Plugin entry — registers DrainSecretProvider when a strong secret is set.

    No-op (records a skip reason) when ``HERMES_DASHBOARD_DRAIN_SECRET`` is
    unset or fails the entropy gate. On success, also registers the
    begin/cancel-drain route as token-authable via the generic seam.
    r   HERMES_DASHBOARD_DRAIN_SECRETzHERMES_DASHBOARD_DRAIN_SECRET is not set. Set a per-agent >=256-bit secret (e.g. `python -c "import secrets; print(secrets.token_urlsafe(32))"`) to enable NAS-driven drain coordination; leave it unset to disable the drain endpoint.zdashboard-auth-drain: %sNr.   r-   min_secret_charsr!   u+   HERMES_DASHBOARD_DRAIN_SECRET rejected — z2. The drain endpoint stays disabled (fail-closed).)r'   r.   z)DrainSecretProvider construction failed: r   )register_token_routez;dashboard-auth-drain: could not register token route %s: %szWdashboard-auth-drain: registered drain service-credential provider (scope=%s, route=%s))r   osenvirongetstriprf   rg   rm   r   int_DEFAULT_MIN_SECRET_CHARS	TypeErrorr0   r*   warningr,    register_dashboard_auth_provider$hermes_cli.dashboard_auth.token_authrq   DRAIN_ROUTE_PATHre   info)	ctxr'   rl   r.   r"   r4   r8   rk   rq   s	            r   registerr      s    ZZ^^;R@FFHFJ 	 	/1AB-/GGW-89??ALWE.$68QRS	 $Fi@F9& B? ? 	 	13CD&fEB ((2
M-. KK	C z" .-	.  FseL13CD  
Ic	
 	

sB   D) E ;E9 )E E	E6E11E69	F'F""F')r   r   rU   float)r'   r   r"   rv   rU   zOptional[str])rU   ri   )rU   rV   )rZ   
__future__r   r:   loggingr   rr   collectionsr   typingr   hermes_cli.dashboard_authr   r   r   r	   	getLoggerrW   rf   rw   r%   r&   r|   r   __annotations__r   r*   r,   rm   r   rL   r    r   <module>r      s   4j #    	    
		8	$      
 (  #  &?!!"!!H@/ @P8">r    