
    (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dS )"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_REASONvaluereturnfloatc                    | sdS t          |           }t          |           t          fd|                                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  R   K   | ]!}|z  t          j        |z            z  V  "d S N)mathlog2).0cns     K/home/rurouni/.hermes/hermes-agent/plugins/dashboard_auth/drain/__init__.py	<genexpr>z _shannon_bits.<locals>.<genexpr>d   s:      HH1QUdiA...HHHHHH    )r   lensumvalues)r   countsper_charr   s      @r   _shannon_bitsr"   Z   s_      sU^^FE

AHHHHHHHHHHHa<r   	min_charssecretr$   intOptional[str]c                  | sdS 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 dS )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)r%   r$   distinctbitss       r   assess_secret_strengthr.   h   s      !  
6{{Y3V 3 3i 3 3 3	

 3v;;H%%%Dx D D"D D D	
   DCt@ C C BC C C	
 4r   c                  `    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dS )'DrainSecretProviderz@Non-interactive shared-bearer-secret provider for drain control.zdrain-secretz"Drain Control (service credential)TFdrain)scoper%   r   r2   r   Nonec               l    t          |          }|t          d|           || _        |pd| _        d S )Nzdrain secret rejected: r1   )r.   
ValueError_secret_scope)selfr%   r2   reasons       r   __init__zDrainSecretProvider.__init__   sE     (//?v??@@@&wr   tokenOptional[TokenPrincipal]c                   |sdS t          j        |                    d          | j                            d                    rt	          d| j        | j        f          S dS )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encoder6   r	   namer7   )r8   r;   s     r   verify_tokenz DrainSecretProvider.verify_token   so      	4u||G44dl6I6I'6R6RSS 	!)~   
 tr   redirect_urir   c                    t          d          )NzTDrainSecretProvider is a non-interactive service credential; there is no login flow.NotImplementedError)r8   rF   s     r   start_loginzDrainSecretProvider.start_login   s    !&
 
 	
r   codestatecode_verifierr   c                    t          d          Nz<DrainSecretProvider is a non-interactive service credential.rH   )r8   rK   rL   rM   rF   s        r   complete_loginz"DrainSecretProvider.complete_login   s     "J
 
 	
r   access_tokenOptional[Session]c                   d S r    )r8   rQ   s     r   verify_sessionz"DrainSecretProvider.verify_session   s	     tr   refresh_tokenc                    t          d          rO   rH   r8   rV   s     r   refresh_sessionz#DrainSecretProvider.refresh_session   s    !J
 
 	
r   c                   d S r   rT   rX   s     r   revoke_sessionz"DrainSecretProvider.revoke_session   s    tr   N)r%   r   r2   r   r   r3   )r;   r   r   r<   )rF   r   r   r   )
rK   r   rL   r   rM   r   rF   r   r   r   )rQ   r   r   rR   )rV   r   r   r   )rV   r   r   r3   )__name__
__module____qualname____doc__rD   display_namesupports_tokensupports_sessionr:   rE   rJ   rP   rU   rY   r[   rT   r   r   r0   r0      s        JJD7LN4; ' ' ' ' ' '   &
 
 
 

 
 
 
   
 
 
 

     r   r0   dictc                     	 ddl m} m}  |            }n4# t          $ r'}t                              d|           i cY d}~S d}~ww xY w | |ddd          }t          |t                    r|ni S )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.configre   rf   	Exceptionloggerdebug
isinstancerc   )re   rf   cfgexcsections        r   _load_config_drain_auth_sectionrr      s    
::::::::kmm   5	
 	
 	

 						 gc;dCCCG $//777R7s    
AAAAr3   c                   da t          j                            dd                                          }|s$da t
                              dt                      dS t                      }t          |                    dd          pd                                          pd}	 t          |                    dt                              }n# t          t          f$ r
 t          }Y nw xY wt          ||	          }|(d
| da t
                              dt                      dS 	 t          ||          }n=# t          $ r0}d| a t
                              dt                      Y d}~dS d}~ww xY w|                     |           	 ddlm}  |t&                     n8# t(          $ r+}t
                              dt&          |           Y d}~nd}~ww xY wt
                              d|t&                     dS )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: %sNr2   r1   min_secret_charsr#   u+   HERMES_DASHBOARD_DRAIN_SECRET rejected — z2. The drain endpoint stays disabled (fail-closed).)r%   r2   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striprl   rm   rr   r   r&   _DEFAULT_MIN_SECRET_CHARS	TypeErrorr5   r.   warningr0    register_dashboard_auth_provider$hermes_cli.dashboard_auth.token_authrv   DRAIN_ROUTE_PATHrk   info)	ctxr%   rq   r2   r$   r9   r?   rp   rv   s	            r   registerr      se    Z^^;R@@FFHHF J 	 	/1ABBB-//GGW--899??AALWE.$68QRRSS		z" . . .-			. $Fi@@@F?& ? ? ? 	 	13CDDD&fEBBB   LsLL13CDDD
 ((222
MMMMMM-.... 
 
 
Ic	
 	
 	
 	
 	
 	
 	
 	

 KK	    sB   #(C C'&C'&D8 8
E2%E--E2F" "
G,!GG)r   r   r   r   )r%   r   r$   r&   r   r'   )r   rc   )r   r3   )r_   
__future__r   rA   loggingr   rw   collectionsr   typingr   hermes_cli.dashboard_authr   r   r   r	   	getLoggerr\   rl   r{   r*   r+   r   r   __annotations__r"   r.   r0   rr   r   rT   r   r   <module>r      s  4 4 4j # " " " " "    				                        
	8	$	$      
 (          &?! ! ! ! ! !H@ @ @ @ @/ @ @ @P8 8 8 8"> > > > > >r   