
    Fj                    *   U d 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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ZddlmZ ddlmZmZmZmZmZ ddlZ ee          j        j                                        Z ee          ej        vr$ej                             d ee                     ddl!m"Z"m#Z# ddl$m%Z%m&Z&m'Z'm(Z(m)Z)m*Z*m+Z+m,Z,m-Z-m.Z.m/Z/m0Z0m1Z1 ddl2m3Z3m4Z4 ddl5m6Z6 	 dd	l7m8Z8m9Z9m:Z:m;Z;m<Z< dd
l=m>Z> ddl?m@Z@mAZAmBZBmCZC ddlDmEZE ddlFmGZG ni# eH$ ra 	 ddlImJZK  eKdd           dd	l7m8Z8m9Z9m:Z:m;Z;m<Z< dd
l=m>Z> ddl?m@Z@mAZAmBZBmCZC ddlDmEZE ddlFmGZG n# eL$ r  eMdejN         d          w xY wY nw xY wdejO        v r eejO        d                   n ee          j        dz  ZP ejQ        eR          ZS e8de"          ZT ejU        d          ZVdZWdaXg ZYeeZ         e[d<   dZ\dZ]eT^                    e>ddgdg           dd l_m`Za d!e:d"ebfd#Zcd!e:d"dfd$Zd eeh d%          Zfeee[d&<   d'ed(ebd"ebfd)Zgd*ed+ed"ebfd,ZheTi                    d-          d!e:fd.            ZjeTi                    d-          d!e:fd/            ZkeTi                    d-          d!e:fd0            Zli d1d2d3d4d5d6d7d8d4d5d9d:d;g d<d=d>d:d?d@dAgd=dBd:dCg dDd=dEd:dFdGdHgd=dId:dJg dKd=dLd:dMg dNd=dOd:dPg dQd=dRd:dSg dTd=dUd:dVdWdXgd=dYd:dZg d[d=d\d:d]d^d_gd=d`d:dag dbd=dcd:ddg ded=dfd:dgg dhd=did:djg dkd=Zmeeeeef         f         e[dl<   dmdndndndndndmdododndndndpdqZneeef         e[dr<   g dsZodted"efduZp	 ddweeef         dxed"eeeeef         f         fdyZq eqe&          Zremd6         Zsi Zteeeeef         f         e[dz<   eru                                D ]\  ZvZwewetev<   evd1k    resetd6<   etZr G d{ d|eG          Zx G d} d~eG          Zy G d deG          Zz G d deG          Z{ G d deG          Z| ej}        d          Z~	  eZ ej}        dd                    Zn3# eef$ r) eS                    d ej}        d                     dZY nw xY wd"eebedz  f         fdZeT                    d          d             Z e*            dz  Zee[d<   dddZeeef         e[d<   i Zeee
j        f         e[d<   dee         ded"e
j        fdZdeded"ee         fdZeT                    d          d             ZeT                    d          d             ZeT                    d          ddedefd            ZeT                    d          ddedefd            ZeT                    d          ddedefd            Zdweeef         d"eeef         fdZeT                    d          d             ZeT                    d          d             ZeT                    d          d             Zdvdvdddi dZee[d<   eT                    d          d             ZdZeedf         e[d<   eT                    d          d             ZeT                    d          d             ZeT                    d          de|fd            Zdweeef         d"eeef         fdZeT                    d          dexfd            ZeT                    d          d             ZeT                    d          deyfdÄ            ZeT                    d          dezfdĄ            ZeT                    dŦ          de{d!e:fdƄ            Zddtee         ded"efdɄZd"eeef         fdʄZd"eeef         fd˄ZdddddedќdddddedќdddddddќdddddddќdddddddќdddddddќfZeeeef         df         e[d<   ded"eeef         fdZeT                    d          d             ZeT                    d          ded!e:fd            ZdZi Zeeeeef         f         e[d<    ej                    Z	 ddlmZmZmZmZmZ dZn# eH$ r dZY nw xY wdZddZdeded"eeeeef         f         fdZdededed"dfdZd"eeef         fdZdeded"eeef         fdZded"eeef         fdZded"dfd Zded"dfdZded"dfdZeT                    d          ded!e:fd            Z G d deG          ZeT                    d          deded!e:fd            ZeT                    d	          dedefd
            ZeT                    d          ded!e:fd            ZdefdZeT                    d          defd            ZeT                    d          defd            ZeT                    d          defd            ZeT                    d          defd            ZeT                    d          	 	 	 	 	 ddededee         dee         dee         f
d            Z G d deG          Z G d deG          Z ej                    Zd"eeeef                  fd Zאd!ee         d"eeef         fd"Zؐd#eeef         d!ed$ed"eeef         fd%Zِd!ee         d&efd'Zڐd(ed"ee         fd)ZeT                    d*          dd!efd,            ZeT                    d-          dd(ed!ee         fd.            ZeT                    d*          ddeӐd!efd/            ZeT                    d-          dd(edeԐd!ee         fd0            ZeT                    d1          dd(ed!ee         fd2            ZeT                    d3          dd(ed!ee         fd4            ZeT                    d5          dd(ed!ee         fd6            ZeT                    d-          dd(ed!ee         fd7            Z G d8 d9eG          Z G d: d;eG          Z G d< d=eG          Zdded^ed"efd>Zd"eeef         fd?Zd"eeeef                  fd@Zded"efdAZded"efdBZeT                    dC          dD             ZeT                    dC          defdE            ZeT                    dF          defdG            ZeT                    dH          defdI            ZeT                    dJ          dedefdK            ZeT                    dJ          defdL            ZeT                    dM          defdN            ZeT                    dM          dedefdO            Z G dP dQeG          ZeT                    dR          dS             ZeT                    dT          defdU            ZeT                    dV          dW             Z G dX dYeG          ZeT                    dZ          d[             ZeT                    dZ          defd\            ZeT                    d]          dd^efd_            ZeT                    d`          dd^efda            ZddlZ	 ddblm Z mZ dZn/# eH$ r'ZdZ dZ G dc dde          ZY dZ[n
dZ[ww xY w ej        de          ZdfZ ej        dg          Z eeh dh          Z	didjd"ebfdkZ
didjd"ebfdlZdidjd"ebfdmZdidjd"ebfdnZi Zeeef         e[do<    ej                    Z	 	 ddpee         dqee         d"eee         ee         ee         f         fdrZdsed"ee         fdtZdsedued"dfdvZdie;d"ee         fdwZeT                    dx          die;d"dfdy            ZeT                    dz          die;d"dfd{            ZeT                    d|          die;d"dfd}            ZeT                    d~          die;d"dfd            Zdee         d"efdZde8fdZd^dddddddddddddddddddddddddddgZddtededeZd"eeeef                  fdZddddddZeeef         e[d<   dddZ eeef         e[d<   h dZ!h dZ"h dZ#h dZ$dZ%deeef         d"eeeef                  fdZ&d"efdZ'eT                    d          d             Z( G d deG          Z)eT                    d          de)fd            Z*deded"ee         fdZ+d"efdZ,da-ee         e[d<   ddebd"efdZ.eT                    d          d             Z/eT                    d          d             Z0 G d deG          Z1deeef         d"eeef         fdĄZ2d"eeef         fdńZ3eT                    dƦ          d!e:fdǄ            Z4eT                    dȦ          d!e:de1fdɄ            Z5ded"efdʄZ6eT                    d˦          d!e:defd̄            Z7eT                    dͦ          d!e:defd΄            Z8eT                    dϦ          d!e:defdЄ            Z9eT                    dѦ          d!e:defd҄            Z: G dӄ deG          Z;eT                    dզ          d!e:de;fdք            Z< G dׄ deG          Z=eT                    d٦          d!e:dede=fdڄ            Z>eT                    dۦ          dedefdބ            Z?d߄ Z@ e@             ddlAmBZC eTD                    eC            eeT           	 	 	 	 dddd'ededebd(ebdebf
dZEdS (  u7  
Hermes Agent — Web UI server.

Provides a FastAPI backend serving the Vite/React frontend and REST API
endpoints for managing configuration, environment variables, and sessions.

Usage:
    python -m hermes_cli.main web          # Start on http://127.0.0.1:9119
    python -m hermes_cli.main web --port 8080
    N)Path)AnyDictListOptionalTuple)__version____release_date__)cfg_getDEFAULT_CONFIGOPTIONAL_ENV_VARSget_config_pathget_env_pathget_hermes_homeload_configload_envsave_configsave_env_valueremove_env_valuecheck_config_version
redact_key)get_running_pidread_runtime_status)env_var_enabled)FastAPIHTTPExceptionRequest	WebSocketWebSocketDisconnect)CORSMiddleware)FileResponseHTMLResponseJSONResponseResponse)StaticFiles)	BaseModel)ensureztool.dashboardF)promptz3Web UI requires fastapi and uvicorn.
Install with: z- -m pip install 'fastapi' 'uvicorn[standard]'HERMES_WEB_DISTweb_distzHermes Agent)titleversion    zX-Hermes-Session-Token_reveal_timestamps      z*^https?://(localhost|127\.0\.0\.1)(:\d+)?$*)allow_origin_regexallow_methodsallow_headers)PUBLIC_API_PATHSrequestreturnc                    | j                             t          d          }|r@t          j        |                                t                                                    rdS | j                             dd          }dt           }t          j        |                                |                                          S )a=  True if the request carries a valid dashboard session token.

    The dedicated session header avoids collisions with reverse proxies that
    already use ``Authorization`` (for example Caddy ``basic_auth``). We still
    accept the legacy Bearer path for backward compatibility with older
    dashboard bundles.
     TauthorizationzBearer )headersget_SESSION_HEADER_NAMEhmaccompare_digestencode_SESSION_TOKEN)r6   session_headerauthexpecteds       4/usr/local/lib/hermes-agent/hermes_cli/web_server.py_has_valid_session_tokenrF      s     _(()=rBBN $-   t?33D)))Ht{{}}hoo.?.?@@@    c                 F    t          |           st          dd          dS )z>Validate the ephemeral session token.  Raises 401 on mismatch.  Unauthorizedstatus_codedetailN)rF   r   )r6   s    rE   _require_tokenrN      s2    #G,, DNCCCCD DrG   >   	localhost::1	127.0.0.1_LOOPBACK_HOST_VALUEShostallow_publicc                     | t           vo| S )u\  Return True iff the dashboard OAuth auth gate must be active.

    Truth table:
      host == loopback                              → False (no auth)
      host != loopback AND allow_public (--insecure)→ False (legacy escape hatch)
      host != loopback AND NOT allow_public         → True  (gate engages)

    "Loopback" matches the same set used by ``--insecure`` enforcement in
    ``start_server``: 127.0.0.1, localhost, ::1. RFC1918 / CGNAT / link-local
    are deliberately treated as PUBLIC — a hostile device on the same LAN is
    exactly the threat model the gate is designed for.
    )rR   )rS   rT   s     rE   should_require_authrV      s     --E4DErG   host_header
bound_hostc                    | sdS |                                  }|                    d          r<|                    d          }|dk    r|d|         }n8|                     d          }n"d|v r|                    dd          d         n|}|                                }|d	v rd
S |                                }|t
          v r	|t
          v S ||k    S )a#  True if the Host header targets the interface we bound to.

    Accepts:
    - Exact bound host (with or without port suffix)
    - Loopback aliases when bound to loopback
    - Any host when bound to 0.0.0.0 (explicit opt-in to non-loopback,
      no protection possible at this layer)
    F[]   z[]:r   >   ::0.0.0.0T)strip
startswithfindrsplitlowerrR   )rW   rX   hclose	host_onlybound_lcs         rE   _is_accepted_hostrj      s      u 	A||C ;sB;;!E'
IIII+.!88AHHS!$$Q''	!!I
 &&&t !!H(((111   rG   httpc                    K   t          t          j        dd          }|r>| j                            dd          }t          ||          st          dddi          S  ||            d{V S )	u  Reject requests whose Host header doesn't match the bound interface.

    Defends against DNS rebinding: a victim browser on a localhost
    dashboard is tricked into fetching from an attacker hostname that
    TTL-flips to 127.0.0.1. CORS and same-origin checks don't help —
    the browser now treats the attacker origin as same-origin with the
    dashboard. Host-header validation at the app layer catches it.

    See GHSA-ppp5-vxwm-4cf7.
    rX   NrS   r9     rM   zVInvalid Host header. Dashboard requests must use the hostname the server was bound to.rL   content)getattrappstater;   r<   rj   r#   )r6   	call_nextrX   rW   s       rE   host_header_middlewarert      s       L$77J o))&"55 j99 		@    7#########rG   c                 6   K   ddl m}  || |           d {V S )Nr   )gated_auth_middleware)$hermes_cli.dashboard_auth.middlewarerv   )r6   rs   rv   s      rE   _dashboard_auth_gaterx   	  s=      JJJJJJ&&w	:::::::::rG   c                   K   t          | j        j        dd          r ||            d{V S | j        j        }|                    d          r+|t          vr"t          |           st          dddi          S  ||            d{V S )	zERequire the session token on all /api/ routes except the public list.auth_requiredFNz/api/rI   rM   rJ   rn   )	rp   rq   rr   urlpathrb   _PUBLIC_API_PATHSrF   r#   )r6   rs   r|   s      rE   auth_middlewarer~     s       w{ /599 (Yw''''''''';Dw D0A$A$A'00 	!>2    7#########rG   modelstringz0Default model (e.g. anthropic/claude-sonnet-4.6)generaltypedescriptioncategorymodel_context_lengthnumberz=Context window override (0 = auto-detect from model metadata)zterminal.backendselectzTerminal execution backend)localdockersshmodaldaytonasingularity)r   r   optionszterminal.modal_modezModal sandbox modesandboxfunctionztts.providerzText-to-speech provider)edge
elevenlabsopenaineuttszstt.providerzSpeech-to-text providerr   r   zdisplay.skinzCLI visual theme)defaultaresmonoslatezdashboard.themezWeb dashboard visual theme)r   midnightemberr   	cyberpunkrosezdisplay.resume_displayz$How resumed sessions display history)minimalfulloffzdisplay.busy_input_modez%Input behavior while agent is running)	interruptqueuesteerzmemory.providerzMemory provider pluginbuiltinhonchozapprovals.modezDangerous command approval mode)askyolodenyzcontext.enginezContext management enginer   customzhuman_delay.modezSimulated typing delay mode)r   typingfixedzlogging.levelzLog level for agent.log)DEBUGINFOWARNINGERRORzagent.service_tierz#API service tier (OpenAI/Anthropic))r9   autor   flexzdelegation.reasoning_effortz(Reasoning effort for delegated subagents)r9   lowmediumhigh_SCHEMA_OVERRIDESsecurityagentdisplaydiscord)privacycontextskillscronnetworkcheckpoints	approvalshuman_delay	dashboardcode_executionprompt_cachinggoalstelegram_CATEGORY_MERGE)r   r   terminalr   
delegationmemorycompressionr   browservoicettssttloggingr   	auxiliaryvaluec                     t          | t                    rdS t          | t                    rdS t          | t                    rdS t          | t                    rdS t          | t
                    rdS dS )z*Infer a UI field type from a Python value.booleanr   listobjectr   )
isinstanceboolintfloatr   dictr   s    rE   _infer_typer     sz    % y% x% x% v% x8rG   r9   configprefixc                    i }|                                  D ],\  }}|r| d| n|}|dv r|r|                    d          d         }nt          |t                    r|}nd}t          |t                    r$|                    t          ||                     t          |          |                    dd                              dd                                          |d}|t          v r |                    t          |                    t                              |d	         |d	                   |d	<   |||<   .|S )
uF   Walk DEFAULT_CONFIG and produce a flat dot-path → field schema dict..>   _config_versionr   r   u    → _ r   r   )itemssplitr   r   update_build_schema_from_configr   replacer+   r   r   r<   )r   r   schemakeyr   full_keyr   entrys           rE   r   r     sk   
 )+Fllnn % %
U(.7f$$s$$$C +++  	!||C((+HHt$$ 	!HH HeT"" 	%MM3E8DDEEEE $E**'//W==EEc3OOUUWW$% %E ,,,.x8999 / 3 3E*4EuZGX Y YE*$F8MrG   _ordered_schemac                       e Zd ZU eed<   dS )ConfigUpdater   N__name__
__module____qualname__r   __annotations__ rG   rE   r   r              LLLLLrG   r   c                   $    e Zd ZU eed<   eed<   dS )EnvVarUpdater   r   Nr   r   r   strr   r   rG   rE   r   r     s"         	HHHJJJJJrG   r   c                       e Zd ZU eed<   dS )EnvVarDeleter   Nr   r   rG   rE   r   r              	HHHHHrG   r   c                       e Zd ZU eed<   dS )EnvVarRevealr   Nr   r   rG   rE   r   r     r   rG   r   c                   @    e Zd ZU dZeed<   eed<   eed<   dZeed<   dS )ModelAssignmentu  Payload for POST /api/model/set — assign a provider/model to a slot.

    scope="main"        → writes model.provider + model.default
    scope="auxiliary"   → writes auxiliary.<task>.provider + auxiliary.<task>.model
    scope="auxiliary" with task=""  → applied to every auxiliary.* slot
    scope="auxiliary" with task="__reset__"  → resets every slot to provider="auto"
    scopeproviderr   r9   taskN)r   r   r   __doc__r   r   r  r   rG   rE   r  r    sD           JJJMMMJJJD#NNNNNrG   r  GATEWAY_HEALTH_URLGATEWAY_HEALTH_TIMEOUT3u>   Invalid GATEWAY_HEALTH_TIMEOUT value %r — using default 3.0sg      @c                     t           sdS t                               d          } |                     d          r| dt          d                    } n-|                     d          r| dt          d                    } |  d|  dfD ]}	 t          j                            |d          }t          j                            |t                    5 }|j	        d	k    r8t          j        |                                          }d
|fcddd           c S 	 ddd           n# 1 swxY w Y   # t          $ r Y w xY wdS )u>  Probe the gateway via its HTTP health endpoint (cross-container).

    .. deprecated::
        Driven by the deprecated ``GATEWAY_HEALTH_URL`` /
        ``GATEWAY_HEALTH_TIMEOUT`` env vars.  Scheduled for removal alongside
        a move to a first-class dashboard config key.  See
        :data:`_GATEWAY_HEALTH_URL` for context.

    Uses ``/health/detailed`` first (returns full state), falling back to
    the simpler ``/health`` endpoint.  Returns ``(is_alive, body_dict)``.

    Accepts any of these as ``GATEWAY_HEALTH_URL``:
    - ``http://gateway:8642``                (base URL — recommended)
    - ``http://gateway:8642/health``         (explicit health path)
    - ``http://gateway:8642/health/detailed`` (explicit detailed path)

    This is a **blocking** call — run via ``run_in_executor`` from async code.
    )FN/z/health/detailedNz/healthGET)methodtimeout   T)_GATEWAY_HEALTH_URLrstripendswithlenurllibr6   r   urlopen_GATEWAY_HEALTH_TIMEOUTstatusjsonloadsread	Exception)baser|   reqrespbodys        rE   _probe_gateway_healthr!    s   &  { %%c**D}}'(( '.s-..../	y	!	! '%s9~~o%&***t,<,<,<=  	.((e(<<C''5L'MM &QU;#%%:diikk22D:& & & & & & & & & &%& & & & & & & & & & & & & & &  	 	 	H	;s=   AD05D$D0D0$D(	(D0+D(	,D00
D=<D=z/api/statusc                    K   t                      \  } }t                      }|d u}d }|sYt          rRt          j                    }|                    d t                     d {V \  }}|rd}|r|                    d          }d }i }d }	d }
d 	 ddlm	}  |            }d |
                                D             n# t          $ r d Y nw xY wt                      }||r|                    d          r|}|r|                    d          }|                    d          pi } fd|                                D             }|                    d	          }	|                    d
          }
|s|dv r|nd}i }n
|r||dv rd}|r||d}d}	 ddlm}  |            }	 |                    d          }t#          j                    t%          fd|D                       }|                                 n# |                                 w xY wn# t          $ r Y nw xY wt)          t+          t,          j        dd                    }g }	 ddlm} d  |            D             }n# t          $ r Y nw xY wi dt4          dt6          dt9          t;                                dt9          t=                                dt9          t?                                d| d|d|d|d t          d|d!|d"|	d#|
d$|d|d%|S )&NTpidr   )load_gateway_configc                     h | ]	}|j         
S r   r   ).0platforms     rE   	<setcomp>zget_status.<locals>.<setcomp>\  s'     (
 (
 (
'HN(
 (
 (
rG   gateway_state	platformsc                 $    i | ]\  }}|v 	||S r   r   )r&  r   r   configured_gateway_platformss      rE   
<dictcomp>zget_status.<locals>.<dictcomp>l  s5     ! ! !C666 U666rG   exit_reason
updated_at>   stoppedstartup_failedr0  >   Nr0  running	SessionDB2   )limitc           	   3      K   | ]L}|                     d           5|                     d|                     dd                    z
  dk     HdV  MdS )ended_atNlast_active
started_atr   ,  r]   r<   )r&  snows     rE   	<genexpr>zget_status.<locals>.<genexpr>  sm       " "55$$,155lA0F0FGGG3NN NNNN" "rG   rz   Flist_providersc                     g | ]	}|j         
S r   namer&  ps     rE   
<listcomp>zget_status.<locals>.<listcomp>  s    <<<Q!&<<<rG   r,   release_datehermes_homeconfig_pathenv_pathconfig_versionlatest_config_versiongateway_runninggateway_pidgateway_health_urlgateway_platformsgateway_exit_reasongateway_updated_atactive_sessionsauth_providers) r   r   r  asyncioget_running_looprun_in_executorr!  r<   gateway.configr$  get_connected_platformsr  r   r   hermes_stater4  list_sessions_richtimesumrg   r   rp   rq   rr   hermes_cli.dashboard_authrA  r	   r
   r   r   r   r   )current_ver
latest_verrO  rN  remote_health_bodyloopaliver)  rQ  rR  rS  r$  gateway_configruntimerT  r4  dbsessionsrz   rU  _list_providersr,  r>  s                        @@rE   
get_statusrj  <  s     244K "##K!-O&* 	<2 	<'))*.*>*>'+
 +
 %
 %
 %
 %
 %
 %
!!  	<"O! <044U;;M 48 ,666666,,..(
 (
+9+Q+Q+S+S(
 (
 (
$$  , , ,'+$$$,
 "##G-2D2H2H2Y2Y$ *O44#KK44:'3! ! ! !"3"9"9";";! ! !
 &kk-88$[[66 	*-:>[-[-[MMajM " 	*!3!?  111 )  "=05G5S!O******Y[[		,,2,66H)++C! " " " "#" " "  O HHJJJJBHHJJJJJ    OUCCDDM "NOOOOOO<<//*;*;<<<   ;( 	s?,,-- 	s?,,--	
 	C'' 	+ 	  	? 	{ 	1 	 	. 	2 	0 	?  	!" 	.# sO   .B? ?CCH 'AH  +H  HH 
H'&H'I0 0
I=<I=logs_ACTION_LOG_DIRzgateway-restart.logzhermes-update.log)gateway-restarthermes-update_ACTION_LOG_FILES_ACTION_PROCS
subcommandrD  c                 ^   t           |         }t                              dd           t          |z  }t          |dd          }|                    d| dt          j        d           d	                                           t          j	        d
dg| }t          t                    t          j        |t          j        i t          j        ddid}t          j        dk    r't          j        t'          t          dd          z  |d<   nd|d<   t          j        |fi |}|t*          |<   |S )zSpawn ``hermes <subcommand>`` detached and record the Popen handle.

    Uses the running interpreter's ``hermes_cli.main`` module so the action
    inherits the same venv/PYTHONPATH the web server is using.
    Tparentsexist_okabr   )	bufferingz
=== z	 started z%Y-%m-%d %H:%M:%Sz ===
z-mzhermes_cli.mainHERMES_NONINTERACTIVE1)cwdstdinstdoutstderrenvwin32DETACHED_PROCESScreationflagsstart_new_session)ro  rl  mkdiropenwriter]  strftimer@   sys
executabler   PROJECT_ROOT
subprocessDEVNULLSTDOUTosenvironr'  CREATE_NEW_PROCESS_GROUPrp   Popenrp  )rq  rD  log_file_namelog_pathlog_filecmdpopen_kwargsprocs           rE   _spawn_hermes_actionr    sF    &d+M$666.HHda000HNNJJJ.A B BJJJQQSS   >4!2
@Z
@C <  ##;"*;5s;;$ $L |w/j"4a889 	_%%
 -1()C00<00DM$KrG   r|   nc                     |                                  sg S 	 |                     dd          }n# t          $ r g cY S w xY w|                                }|dk    r|| d         n|S )u   Return the last ``n`` lines of ``path``.  Reads the whole file — fine
    for our small per-action logs.  Binary-decoded with ``errors='replace'``
    so log corruption doesn't 500 the endpoint.utf-8r   )encodingerrorsr   N)exists	read_textOSError
splitlines)r|   r  textliness       rE   _tail_linesr    s     ;;== 	~~wy~AA   			OOEQ5!::E)s   0 ??z/api/gateway/restartc                     K   	 t          ddgd          } n@# t          $ r3}t                              d           t	          dd|           d}~ww xY wd	| j        dd
S )z8Kick off a ``hermes gateway restart`` in the background.gatewayrestartrm  zFailed to spawn gateway restart  zFailed to restart gateway: rK   NTokr#  rD  r  r  _log	exceptionr   r#  r  excs     rE   restart_gatewayr    s      Y#Y	$:<MNN Y Y Y89994WRU4W4WXXXXY x!  s    
A.AAz/api/hermes/updatec                     K   	 t          dgd          } n@# t          $ r3}t                              d           t	          dd|           d}~ww xY wd| j        dd	S )
z-Kick off ``hermes update`` in the background.r   rn  zFailed to spawn hermes updater  zFailed to start update: rK   NTr  r  r  s     rE   update_hermesr    s      V#XJ@@ V V V67774Ts4T4TUUUUV x  s    
A.AAz/api/actions/{name}/statusr  r  c           	      f  K   t                               |           }|t          dd|            t          |z  }t	          |t          t          |d          d                    }t                              |           }|d}d}d}n|                                }|du }|j	        }| ||||dS )	zCTail an action log and report whether the process is still running.N  zUnknown action: rK   r]     F)rD  r2  	exit_coder#  r  )
ro  r<   r   rl  r  minmaxrp  pollr#  )	rD  r  r  r  tailr  r2  r  r#  s	            rE   get_action_statusr    s       &))$//M4Mt4M4MNNNN.HxS]]D!9!9::DT""D|#'	!IIKK	t#h   rG   z/api/sessions   r6  offsetc           	         K   	 ddl m}  |            }	 |                    | |          }|                                }t	          j                    }|D ]M}|                    d          d u o0||                    d|                    dd                    z
  dk     |d<   N||| |d	|                                 S # |                                 w xY w# t          $ r, t          	                    d
           t          dd          w xY w)Nr   r3  r6  r  r8  r9  r:  r;  	is_active)rh  totalr6  r  zGET /api/sessions failedr  Internal server errorrK   )r[  r4  r\  session_countr]  r<   rg   r  r  r  r   )r6  r  r4  rg  rh  r  r>  r=  s           rE   get_sessionsr  9  s8     M******Y[[	,,5,HHH$$&&E)++C  EE*%%- Squu]AEE,4J4JKKKsR + !)55TZ[[HHJJJJBHHJJJJ M M M12224KLLLLMs#   C BB> )C >CC 6Dz/api/sessions/searchqc           	        K   | r|                                  sdg iS 	 ddlm}  |            }	 ddl}g } |j        d|                                            D ]Z}|                    d          s|                    d          r|                    |           B|                    |dz              [d                    |          }|	                    ||	          }i }	|D ]z}
|
d
         }||	vrl||

                    dd          |

                    d          |

                    d          |

                    d          |

                    d          d|	|<   {dt          |	                                          i|                                 S # |                                 w xY w# t          $ r, t                              d           t#          dd          w xY w)z;Full-text search across session message content using FTS5.resultsr   r3  Nz"[^"]*"|\S+"r1   r   )queryr6  
session_idsnippetr9   rolesourcer   session_started)r  r  r  r  r   r  zGET /api/sessions/search failedr  zSearch failedrK   )ra   r[  r4  refindallrb   r  appendjoinsearch_messagesr<   r   valuesrg   r  r  r  r   )r  r6  r4  rg  r  termstokenprefix_querymatchesseenmsids               rE   search_sessionsr  O  s       AGGII 2"E******Y[[	 IIIE#NAGGII>> . .##C(( .ENN3,?,? .LL''''LL----88E??L((|5(IIGD 
 
od??&)#$55B#7#7 !f"#%%//!"w+,551B+C+C! !DI tDKKMM223HHJJJJBHHJJJJ E E E8999ODDDDEs#   F. EF  F. F++F. .6G$c                 B   t          |           } |                     d          }t          |t                     r`|                    dd          }|                    d|                    dd                    | d<   t          |t                    r|nd| d<   nd| d<   | S )a  Normalize config for the web UI.

    Hermes supports ``model`` as either a bare string (``"anthropic/claude-sonnet-4"``)
    or a dict (``{default: ..., provider: ..., base_url: ...}``).  The schema is built
    from DEFAULT_CONFIG where ``model`` is a string, but user configs often have the
    dict form.  Normalize to the string form so the frontend schema matches.

    Also surfaces ``model_context_length`` as a top-level field so the web UI can
    display and edit it.  A value of 0 means "auto-detect".
    r   context_lengthr   r   rD  r9   r   )r   r<   r   r   )r   	model_valctx_lens      rE   _normalize_config_for_webr  y  s     &\\F

7##I)T"" +-- 0!44#--	9==3L3LMMw4>w4L4L)SRS%&&)*%&MrG   z/api/configc                  x   K   t          t                                } d |                                 D             S )Nc                 D    i | ]\  }}|                     d           ||S r   rb   r&  kvs      rE   r-  zget_config.<locals>.<dictcomp>  s/    EEETQ1<<3D3DEAqEEErG   )r  r   r   )r   s    rE   
get_configr    s3      &{}}55FEEV\\^^EEEErG   z/api/config/defaultsc                     K   t           S N)r   r   rG   rE   get_defaultsr    s      rG   z/api/config/schemac                  $   K   t           t          dS )N)fieldscategory_order)CONFIG_SCHEMA_CATEGORY_ORDERr   rG   rE   
get_schemar    s      #GGGrG   r   r  auto_context_lengthconfig_context_lengtheffective_context_lengthcapabilities_EMPTY_MODEL_INFOz/api/model/infoc                     	 t                      } |                     dd          }t          |t                    rl|                    d|                    dd                    }|                    dd          }|                    dd          }|                    d          }n|rt	          |          nd}d}d}d}|st          t
          |	          S 	 d
dlm}  ||||d          }n# t          $ r d
}Y nw xY wd
}t          |t                    r|d
k    r|}|d
k    r|n|}	i }
	 d
dl
m}  |||          }|'|j        |j        |j        |j        |j        |j        d}
n# t          $ r Y nw xY w|||||	|
dS # t          $ r1 t$                              d           t          t
                    cY S w xY w)a.  Return resolved model metadata for the currently configured model.

    Calls the same context-length resolution chain the agent uses, so the
    frontend can display "Auto-detected: 200K" alongside the override field.
    Also returns model capabilities (vision, reasoning, tools) when available.
    r   r9   r   rD  r  base_urlr  N)r  r   )get_model_context_length)r   r  r  r  get_model_capabilitiesr  r   supports_toolssupports_visionsupports_reasoningcontext_windowmax_output_tokensmodel_familyr  zGET /api/model/info failed)r   r<   r   r   r   r  agent.model_metadatar  r  r   agent.models_devr  r  r   r  r  r  r  r  r  )cfg	model_cfg
model_namer  r  
config_ctxr  auto_ctxconfig_ctx_inteffective_ctxcapsr  mcs                rE   get_model_infor    sa   B'mmGGGR((	 i&& 		"y)--2K2KLLJ }}Z44H }}Z44H"'788JJ+4<Y"JHHJ 	>)H====		EEEEEE// !!&*	  HH  	 	 	HHH	 j#&& 	(:>>'N +91*<*<( 	??????''LLLB~&(&7')'9*,*?&(&7)+)=$&O   	 	 	D	   #+%3(5 
 
 	
  ' ' '3444%&&&&&'sZ   CF C/ .F /C>;F =C>>.F -<E* )F *
E74F 6E77F 8F>=F>)visionweb_extractr   
skills_hubapprovalmcptitle_generationtriage_specifierkanban_decomposerprofile_describercurator._AUX_TASK_SLOTSz/api/model/optionsc                      	 ddl m} m}  |  |            d          S # t          $ r, t                              d           t          dd          w xY w)	aG  Return authenticated providers + their curated model lists.

    REST equivalent of the ``model.options`` JSON-RPC on tui_gateway, so the
    dashboard Models page can render the picker without a live chat session.
    The response shape matches ``model.options`` 1:1 so ``ModelPickerDialog``
    can share the same types.
    r   build_models_payloadload_picker_contextr5  )
max_modelszGET /api/model/options failedr  zFailed to list model optionsrK   )hermes_cli.inventoryr  r  r  r  r  r   r  s     rE   get_model_optionsr"    s    TRRRRRRRR##$7$7$9$9bIIII T T T67774RSSSSTs	    6Az/api/model/auxiliaryc                     	 t                      } |                     di           }t          |t                    si }g }t          D ]}t          |                    |          t                    r|                    |i           ni }|                    |t          |                    dd          pd          t          |                    dd          pd          t          |                    dd          pd          d           |                     di           }t          |t                    r`t          |                    dd          pd          t          |                    d|                    d	d                    pd          d
}nd|rt          |          ndd
}||dS # t          $ r, t          	                    d           t          dd          w xY w)a  Return current auxiliary task assignments.

    Shape:
      {
        "tasks": [
          {"task": "vision", "provider": "auto", "model": "", "base_url": ""},
          ...
        ],
        "main": {"provider": "openrouter", "model": "anthropic/claude-opus-4.7"},
      }
    r   r  r   r   r9   r  )r  r  r   r  r   rD  r  )tasksmainzGET /api/model/auxiliary failedr  zFailed to read auxiliary configrK   )r   r<   r   r   r  r  r   r  r  r  r   )r  aux_cfgr$  slotslot_cfgr  r%  s          rE   get_auxiliary_modelsr)  !  s   Wmm''+r**'4(( 	G# 	 	D0:7;;t;L;Ld0S0S[w{{4,,,Y[HLLZ @ @ JFKKX\\'266<"==Z < < BCC	      GGGR((	i&& 	R	j" = = CDDY]]9immFB6O6OPPVTVWW DD
 !#y-PS^^^bQQD--- W W W89994UVVVVWs   F/F2 26G(z/api/model/setr   c                   K   | j         pd                                                                }| j        pd                                }| j        pd                                }| j        pd                                                                }|dvrt          dd          	 t                      }|dk    r|r|st          dd          |                    di           }t          |t                    si }||d	<   ||d
<   d|v r|                    d          rd|d<   d|v r|                    dd           ||d<   t          |           dd||dS |                    d          }t          |t                    si }|dk    r_t          D ]=}|                    |          }	t          |	t                    si }	d|	d	<   d|	d<   |	||<   >||d<   t          |           ddddS |st          dd          |r|gnt          t                    }
|
D ]Z}|t          vrt          dd|           |                    |          }	t          |	t                    si }	||	d	<   ||	d<   |	||<   [||d<   t          |           dd|
||dS # t          $ r  t          $ r, t                               d           t          dd          w xY w)u!  Assign a model to the main slot or an auxiliary task slot.

    Writes to ``~/.hermes/config.yaml`` — applies to **new** sessions only.
    The currently running chat PTY (if any) is not affected; use the
    ``/model`` slash command inside a chat to hot-swap that specific session.
    r9   >   r%  r   rm   z#scope must be 'main' or 'auxiliary'rK   r%  z$provider and model required for mainr   r  r   r  r  NT)r  r  r  r   r   	__reset__r   )r  r  resetzprovider required for auxiliaryzunknown auxiliary task: )r  r  r$  r  r   zPOST /api/model/set failedr  zFailed to save model assignment)r  ra   re   r  r   r  r   r   r<   r   r   popr   r  r   r  r  r  )r   r  r  r   r  r  r  auxr'  r(  targetss              rE   set_model_assignmentr0  M  s      Z2$$&&,,..E#**,,HZ2$$&&EIO""$$**,,D)))4YZZZZCWmmF?? d5 d#<bcccc,,Ii.. 	$,Ij!#(Ii Y&&9==+D+D&(*	*%  9,,.555$CLXPUVVV ggk""#t$$ 	C;' % %774==!(D11 "!H'-$$&!$D		"CtDDD 	[C8YZZZZ ;4&&d?&;&; 	! 	!D?**#<]W[<]<]^^^^wwt}}Hh-- #+HZ  %HW CIIKC  
 
 	
     W W W34444UVVVVWs   )B2J BJ -B$J A Kc                 \   t          |           } |                     dd           |                     dd          }t          |t                    s*	 t          |          }n# t          t
          f$ r d}Y nw xY w|                     d          }t          |t                    r|r	 t                      }|                    d          }t          |t                     r-||d<   |dk    r||d<   n|                    dd           || d<   n|dk    r||d| d<   n# t          $ r Y nw xY w| S )	u9  Reverse _normalize_config_for_web before saving.

    Reconstructs ``model`` as a dict by reading the current on-disk config
    to recover model subkeys (provider, base_url, api_mode, etc.) that were
    stripped from the GET response.  The frontend only sees model as a flat
    string; the rest is preserved transparently.

    Also handles ``model_context_length`` — writes it back into the model dict
    as ``context_length``.  A value of 0 or absent means "auto-detect" (omitted
    from the dict so get_model_context_length() uses its normal resolution).
    _model_metaNr   r   r   r   r  )r   r  )
r   r-  r   r   	TypeError
ValueErrorr<   r   r   r  )r   ctx_overrider  disk_config
disk_models        rE   _denormalize_config_from_webr8    su    &\\F JJ}d### ::4a88LlC(( 	|,,LL:& 	 	 	LLL	 

7##I)S!! i 	%--K$11J*d++ (1
9%!##3?J/00NN#3T:::",w !!(&2# #w  	 	 	D	Ms%   A" "A87A8(A3D 
D)(D)c                    K   	 t          t          | j                             ddiS # t          $ r, t                              d           t          dd          w xY w)Nr  TzPUT /api/config failedr  r  rK   )r   r8  r   r  r  r  r   )r   s    rE   update_configr:    su      M0==>>>d| M M M/0004KLLLLMs	   $) 6Az/api/envc                    K   t                      } i }t          j                    D ]\  }}|                     |          }t	          |          |rt          |          nd |                    dd          |                    d          |                    dd          |                    dd          |                    dg           |                    dd          d	||<   |S )
Nr   r9   r{   r   passwordFtoolsadvanced)is_setredacted_valuer   r{   r   is_passwordr=  r>  )r   r   r   r<   r   r   )env_on_diskresultvar_nameinfor   s        rE   get_env_varsrF    s      **KF+133 
 
$))5kk38Bj///d88M26688E??R0088J66XXgr**U33	
 	
x MrG   c                   K   	 t          | j        | j                   d| j        dS # t          $ r$}t	          dt          |                    |d }~wt          $ r, t                              d           t	          dd          w xY w)NTr  r   rm   rK   zPUT /api/env failedr  r  )	r   r   r   r4  r   r   r  r  r  )r   r  s     rE   set_env_varrI    s      Mtx,,,48,,, G G G
 CHH===3F M M M,---4KLLLLMs   #( 
B
A9B
c                    K   	 t          | j                  }|st          d| j         d          d| j        dS # t          $ r  t          $ r, t                              d           t          dd          w xY w)	Nr   not found in .envrK   TrH  zDELETE /api/env failedr  r  )r   r   r   r  r  r  )r   removeds     rE   remove_env_varrM    s      	M"48,, 	YC488W8W8WXXXX48,,,    M M M/0004KLLLLMs
   8= A A=z/api/env/revealc                   K   t          |           t          j                    }|t          z
  fdt          D             t          dd<   t	          t                    t
          k    rt          dd          t                              |           t                      }|	                    | j
                  }|t          d| j
         d          t                              d| j
                   | j
        |d	S )
zReturn the real (unredacted) value of a single env var.

    Protected by:
    - Ephemeral session token (generated per server start, injected into SPA)
    - Rate limiting (max 5 reveals per 30s window)
    - Audit logging
    c                      g | ]
}|k    |S r   r   )r&  tcutoffs     rE   rG  z"reveal_env_var.<locals>.<listcomp>#  s    III1a&jjQjjjrG   Ni  z,Too many reveal requests. Try again shortly.rK   r  rK  zenv/reveal: %s)r   r   )rN   r]  _REVEAL_WINDOW_SECONDSr.   r  _REVEAL_MAX_PER_WINDOWr   r  r   r<   r   r  rE  )r   r6   r>  rB  r   rQ  s        @rE   reveal_env_varrT    s       7 )++C))FIIII(:IIIqqq
"8884bccccc""" **KOODH%%E}tx4S4S4STTTTII)))8e,,,rG      visiblec                 ,   | sdS t          |           rt          | t                    sdS t          |           }d|v r5|                    d          dk    r|                    dd          d         }t          |          |k    r|S d|| d          S )	u  Return ``...XXXXXX`` (last N chars) for safe display in the UI.

    We never expose more than the trailing ``visible`` characters of an
    OAuth access token. JWT prefixes (the part before the first dot) are
    stripped first when present so the visible suffix is always part of
    the signing region rather than a meaningless header chunk.

    Returns the Entra-ID placeholder when handed a callable (Azure Foundry
    bearer provider) — the callable is NEVER invoked here.
    r9   z<entra-id-bearer>r      r]   r\   u   …N)callabler   r   countrd   r  )r   rV  r=  s      rE   _truncate_tokenr[  >  s      r #z%55 #""E

A
axxAGGCLLA%%HHS!R 
1vvG899rG   c            
      ,   	 ddl m} m}m} n# t          $ r	 d}d} d}Y nw xY wd}| r	  |             }n# t
          $ r d}Y nw xY w|ru|                    d          r`ddd| dt          |                    d                    |                    d	          t          |                    d
                    dS d}|r	  |            }n# t
          $ r d}Y nw xY w|rq|                    d          r\dddt          |                    d                    |                    d	          t          |                    d
                    dS t          j
        d          pt          j
        d          }|rdddt          |          dddS dddS )u  Combined status across the three Anthropic credential sources we read.

    Hermes resolves Anthropic creds in this order at runtime:
    1. ``~/.hermes/.anthropic_oauth.json`` — Hermes-managed PKCE flow
    2. ``~/.claude/.credentials.json`` — Claude Code CLI credentials (auto)
    3. ``ANTHROPIC_TOKEN`` / ``ANTHROPIC_API_KEY`` env vars
    The dashboard reports the highest-priority source that's actually present.
    r   )read_hermes_oauth_credentialsread_claude_code_credentials_HERMES_OAUTH_FILENaccessTokenThermes_pkcezHermes PKCE ()	expiresAtrefreshToken	logged_inr  source_labeltoken_preview
expires_athas_refresh_tokenclaude_codez)Claude Code (~/.claude/.credentials.json)ANTHROPIC_TOKENCLAUDE_CODE_OAUTH_TOKENenv_varz$ANTHROPIC_TOKEN environment variableFrf  r  )agent.anthropic_adapterr]  r^  r_  ImportErrorr  r<   r[  r   r  getenv)r]  r^  r_  hermes_credscc_creds	env_tokens         rE   _anthropic_oauth_statusrv  W  sZ   	"	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	

  " " "'+$(,%!"
 L$  	 88::LL 	  	  	 LLL	  
((77 
#A,>AAA,\-=-=m-L-LMM&**;77!%l&6&6~&F&F!G!G
 
 	
 H# 	3355HH 	 	 	HHH	 
HLL// 
#G,X\\--H-HII",,{33!%hll>&B&B!C!C
 
 	
 	+,,T	:S0T0TI 
B,Y77!&
 
 	
 $///s-   
   
3 AA
C CCc            
      :   	 ddl m}   |             }n# t          $ r d}Y nw xY w|rq|                    d          r\dddt	          |                    d                    |                    d          t          |                    d	                    d
S dddS )a  Surface Claude Code CLI credentials as their own provider entry.

    Independent of the Anthropic entry above so users can see whether their
    Claude Code subscription tokens are actively flowing into Hermes even
    when they also have a separate Hermes-managed PKCE login.
    r   )r^  Nr`  Tclaude_code_cliz~/.claude/.credentials.jsonrc  rd  re  Fro  )rp  r^  r  r<   r[  r   )r^  credss     rE   _claude_code_only_statusrz    s    HHHHHH,,..    
=)) 
'9,UYY}-E-EFF))K00!%eii&?&?!@!@
 
 	
 $///s    ""	anthropiczAnthropic (Claude API)pkcezhermes auth add anthropicz.https://docs.claude.com/en/api/getting-started)idrD  flowcli_commanddocs_url	status_fnclaude-codezClaude Code (subscription)externalzclaude setup-tokenz+https://docs.claude.com/en/docs/claude-codenousNous Portaldevice_codezhermes auth add nouszhttps://portal.nousresearch.comopenai-codexzOpenAI Codex (ChatGPT)zhermes auth add openai-codexz https://platform.openai.com/docs
qwen-oauthzQwen (via Qwen CLI)zhermes auth add qwen-oauthz#https://github.com/QwenLM/qwen-codeminimax-oauthzMiniMax (OAuth)zhermes auth add minimax-oauthzhttps://www.minimax.io_OAUTH_PROVIDER_CATALOGprovider_idc           
         |4	  |            S # t           $ r}dt          |          dcY d}~S d}~ww xY w	 ddlm} | dk    r|                                }t          |                    d                    d|                    d	          pd
t          |                    d                    |                    d          t          |                    d                    dS | dk    r|                                }t          |                    d                    |                    d          pd|                    d          pdt          |                    d                    dd|                    d          dS | dk    r|	                                }t          |                    d                    d|                    d          pdt          |                    d                    |                    d          t          |                    d                    dS | dk    rh|
                                }t          |                    d                    dd|                    dd            d!d|                    d          d"dS n)# t           $ r}dt          |          dcY d}~S d}~ww xY wddiS )#z@Dispatch to the right status helper for an OAuth provider entry.NF)rf  errorr   )rC   r  rf  nous_portalportal_base_urlr  access_tokenaccess_expires_atrj  re  r  r  openai_codex	auth_modezOpenAI Codexapi_keylast_refresh)rf  r  rg  rh  ri  rj  r  r  qwen_cliauth_store_pathzQwen CLIri  r  minimax_oauthz	MiniMax (regionglobalrb  T)r  r   
hermes_clirC   get_nous_auth_statusr   r<   r[  get_codex_auth_statusget_qwen_auth_statusget_minimax_oauth_auth_status)r  r  ehauthraws        rE   _resolve_provider_statusr    s	   	99;; 	9 	9 	9!&Q88888888	9,5,,,,,,&  ,,..C!#''+"6"677' #(9 : : Km!01H1H!I!I!gg&9::%)#''2E*F*F%G%G   .((--//C!#''+"6"677''(++=~ # 4 4 F!01C1C!D!D"%* # 7 7   ,&&,,..C!#''+"6"677$ #(9 : : Hj!01H1H!I!I!ggl33%)#''2E*F*F%G%G   /))5577C!#''+"6"677) JCGGHh,G,G J J J!%!ggl33%)   *  5 5 5"SVV444444445sG   	 
4/44B0J$ )B J$ 
B*J$ 5A-J$ $
K
.K?K
K
z/api/providers/oauthc            
         K   g } t           D ]e}t          |d         |                    d                    }|                     |d         |d         |d         |d         |d         |d           fd| iS )	u#  Enumerate every OAuth-capable LLM provider with current status.

    Response shape (per provider):
        id              stable identifier (used in DELETE path)
        name            human label
        flow            "pkce" | "device_code" | "external"
        cli_command     fallback CLI command for users to run manually
        docs_url        external docs/portal link for the "Learn more" link
        status:
          logged_in        bool — currently has usable creds
          source           short slug ("hermes_pkce", "claude_code", ...)
          source_label     human-readable origin (file path, env var name)
          token_preview    last N chars of the token, never the full token
          expires_at       ISO timestamp string or null
          has_refresh_token bool
    r}  r  rD  r~  r  r  )r}  rD  r~  r  r  r  	providers)r  r  r<   r  )r  rF  r  s      rE   list_oauth_providersr  '  s      $ I$ 	 	)!D'1553E3EFFD'fIfI]+*
 
 	 	 	 	 ##rG   z"/api/providers/oauth/{provider_id}c                   K   t          |           d t          D             }| |vr7t          dd|  dd                    t	          |                               | dv r	 dd	lm} |                                r|                                 n# t          $ r Y nw xY w	 dd
l
m}  |d           n# t          $ r Y nw xY wt                              d|            d| dS 	 dd
l
m}  ||           }t                              d| |           t          |          | dS # t          $ r>}t                              d|            t          dt!          |                    d}~ww xY w)zDDisconnect an OAuth provider. Token-protected (matches /env/reveal).c                     h | ]
}|d          S r}  r   rE  s     rE   r(  z,disconnect_oauth_provider.<locals>.<setcomp>L  s    :::Q4:::rG   rm   zUnknown provider: . Available: , rK   >   r{  r  r   r_  )clear_provider_authr{  zoauth/disconnect: %sT)r  r  z!oauth/disconnect: %s (cleared=%s)zdisconnect %s failedr  N)rN   r  r   r  sortedrp  r_  r  unlinkr  hermes_cli.authr  r  rE  r   r  r   )r  r6   	valid_idsr_  r  clearedr  s          rE   disconnect_oauth_providerr  G  s      7::"9:::I)##@ @ @!%6)+<+<!=!=@ @
 
 
 	
 222	BBBBBB!((** ,"))+++ 	 	 	D		;;;;;;,,,, 	 	 	D			(+666444<777777%%k22		5{GLLL7mm=== < < <-{;;;CFF;;;;<s<   #.B 
BB#B5 5
CC&>D% %
E-/9E((E-  _oauth_sessions)_OAUTH_CLIENT_ID_OAUTH_TOKEN_URL_OAUTH_REDIRECT_URI_OAUTH_SCOPES_generate_pkceTz!https://claude.ai/oauth/authorizec                  
   t          j                     t          z
  t          5  fdt                                          D             } | D ]}t                              |d           	 ddd           dS # 1 swxY w Y   dS )z:Drop expired sessions. Called opportunistically on /start.c                 2    g | ]\  }}|d          k     |S )
created_atr   )r&  r  sessrQ  s      rE   rG  z&_gc_oauth_sessions.<locals>.<listcomp>  s-    ]]]dlASV\A\A\A\A\A\rG   N)r]  _OAUTH_SESSION_TTL_SECONDS_oauth_sessions_lockr  r   r-  )staler  rQ  s     @rE   _gc_oauth_sessionsr    s    Y[[55F	 + +]]]]o&;&;&=&=]]] 	+ 	+CT****	++ + + + + + + + + + + + + + + + + +s   AA88A<?A<r~  c                     t          j        d          }|| |t          j                    ddd}t          5  |t          |<   ddd           n# 1 swxY w Y   ||fS )zICreate + register a new OAuth session, return (session_id, session_dict).   pendingN)r  r  r~  r  r  error_message)secretstoken_urlsafer]  r  r  )r  r~  r  r  s       rE   _new_oauth_sessionr    s    


#
#Cikk D 
 $ $#$ $ $ $ $ $ $ $ $ $ $ $ $ $ $9s   AAAr  refresh_tokenexpires_at_msc                    ddl m} | ||d}|j                            dd           |                    |j         dt          j                     dt          j	        d                     }	 |
                    d	d
          5 }|                    t          j        |d                     |                                 t          j        |                                           ddd           n# 1 swxY w Y   t          j        ||           	 |                    t&          j        t&          j        z             n# t,          $ r Y nw xY w	 |                                r|                                 nO# t,          $ r Y nCw xY w# 	 |                                r|                                 w w # t,          $ r Y w w xY wxY w	 ddlm}m}m}	m ddl}
 |d          }fd|                                D             }|D ]7}	 |                     tC          |dd                     (# tD          $ r Y 4w xY w |d|
#                                j$        dd         d|	d d| ||	  	        }|%                    |           dS # tD          $ r&}tL          '                    d|           Y d}~dS d}~ww xY w)zPersist Anthropic PKCE creds to both Hermes file AND credential pool.

    Mirrors what auth_commands.add_command does so the dashboard flow leaves
    the system in the same state as ``hermes auth add anthropic``.
    r   r  )r`  rd  rc  Trs  z.tmp.r      wr  r  rX  )indentNPooledCredential	load_poolAUTH_TYPE_OAUTHSOURCE_MANUALr{  c                 b    g | ]+}t          |d d                               d          )|,S )r  r9   :dashboard_pkce)rp   rb   )r&  r  r  s     rE   rG  z/_save_anthropic_oauth_creds.<locals>.<listcomp>  sB    xxx!Hb1I1I1T1TXeUvUvUv1w1wxAxxxrG   r}  r9   rU  zdashboard PKCEr  )	r  r}  label	auth_typepriorityr  r  r  r  z)anthropic pool add (dashboard) failed: %s)(rp  r_  parentr  	with_namerD  r  getpidr  	token_hexr  r  r  dumpsflushfsyncfilenor   chmodstatS_IRUSRS_IWUSRr  r  r  agent.credential_poolr  r  r  r  uuidentriesremove_entryrp   r  uuid4hex	add_entryr  warning)r  r  r  r_  payloadtmp_pathhandler  r  r  r  poolexistingr  r   r  s                  @rE   _save_anthropic_oauth_credsr    s    ;:::::#%" G
 ##D4#@@@!++"MMMMw7H7K7KMM H]]3]11 	&VLLGA666777LLNNNHV]]__%%%	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	
8/000	$$T\DL%@AAAA 	 	 	D		   "!!! 	 	 	D		   "!!!!" 	 	 	D	
E	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	y%%xxxxt||~~xxx 	 	A!!'!T2"6"67777      zz||#"%#444%''

 

 

 	u E E E@!DDDDDDDDDEs   0F A$C7+F 7C;;F >C;?F ,E F 
EF EF (E? ?
FFG(F;9G;
GGGG?J $H43J 4
I>J  IAJ 
KJ<<Kc            	      ,   t           st          dd          t                      \  } }t          dd          \  }}| |d<   | |d<   dt          d	t
          t          |d
| d}t           dt          j	        
                    |           }|d|t          dS )z9Begin PKCE flow. Returns the auth URL the UI should open.i  z/Anthropic OAuth not available (missing adapter)rK   r{  r|  verifierrr   truecodeS256)r  	client_idresponse_typeredirect_urir  code_challengecode_challenge_methodrr   ?)r  r~  auth_url
expires_in)_ANTHROPIC_OAUTH_AVAILABLEr   _generate_pkce_pairr  _ANTHROPIC_OAUTH_CLIENT_ID_ANTHROPIC_OAUTH_REDIRECT_URI_ANTHROPIC_OAUTH_SCOPES_ANTHROPIC_OAUTH_AUTHORIZE_URLr  parse	urlencoder  )r  	challenger  r  paramsr   s         rE   _start_anthropic_pkcer    s    % g4effff-//Hi";77ICDDM/5(#!'	 	F 1SS6<3I3I&3Q3QSSH0	  rG   r  
code_inputc           	         t           5  t                              |           }ddd           n# 1 swxY w Y   |r|d         dk    s|d         dk    rt          dd          |d	         d
k    rd|d	         |                    d          dS |                                                    dd          }|d                                         }|sddddS t          |          dk    r|d         nd}t          j        dt          ||p|d         t          |d         d                                          }t          j                            t          |dddd          }	 t          j                            |d          5 }t          j        |                                                                          }	ddd           n# 1 swxY w Y   nO# t(          $ rB}
t           5  d|d	<   d|
 |d<   ddd           n# 1 swxY w Y   dd|d         dcY d}
~
S d}
~
ww xY w|	                    d d          }|	                    d!d          }t+          |	                    d"          pd#          }|s5t           5  d|d	<   d$|d<   ddd           n# 1 swxY w Y   dd|d         dS t+          t-          j                    d%z            |d%z  z   }	 t/          |||           nO# t(          $ rB}
t           5  d|d	<   d&|
 |d<   ddd           n# 1 swxY w Y   dd|d         dcY d}
~
S d}
~
ww xY wt           5  d'|d	<   ddd           n# 1 swxY w Y   t0                              d(|            d)d'd*S )+z<Exchange authorization code for tokens. Persists on success.Nr  r{  r~  r|  r  zUnknown or expired sessionrK   r  r  Fr  )r  r  message#r]   r   r  zNo code providedr9   authorization_coderr   r  )
grant_typer  r  rr   r  code_verifierapplication/jsonzhermes-dashboard/1.0)Content-Typez
User-AgentPOST)datar;   r  r  r  zToken exchange failed: r  r  r  i  zNo access token returned  zSave failed: approvedz2oauth/pkce: anthropic login completed (session=%s)T)r  r  )r  r  r<   r   ra   r   r  r  r  r  r  r@   r  r6   r   _ANTHROPIC_OAUTH_TOKEN_URLr  r  r  decoder  r   r]  r  r  rE  )r  r  r  partsr  state_from_callbackexchange_datar  r  rC  r  r  r  r  r  s                  rE   _submit_anthropic_pkcer  "  s   	 / /"":../ / / / / / / / / / / / / / / R4
#{22d6lf6L6L4PQQQQH~""tH~$((?B[B[\\\ $$S!,,E8>>D Ow;MNNN&)%jj1nn%(("J*/$5W5j)      vxx  .
 
 ".0
 
  !  CR^##C#44 	6Z		 2 2 4 455F	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 R R R! 	B 	B$DN$Aa$A$AD!	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B w4;PQQQQQQQQ	R ::nb11LJJ33MVZZ--566J R! 	? 	?$DN$>D!	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? w4;PQQQ	d*++zD/@AMR#L-OOOO R R R! 	8 	8$DN$7A$7$7D!	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 w4;PQQQQQQQQ	R
 
 $ $#X$ $ $ $ $ $ $ $ $ $ $ $ $ $ $IIBJOOO*---s   /33(!G 	9GG GG GG 
H&$H!+H9H!H			H!H		H!H&!H&JJ JK, ,
L86L3=LL3L	L3L	L3-L83L8MMMc           
        K   | dk    rddl m}mm} ddl|d         }t          j        d          pt          j        d          p|j                            d          |j	         |d|j
                  \  fd	}t          j                                        d|           d{V \  }}t          dd
          \  }}t          |d
                   |d
<   t!          |d                   |d<   t#          j                    t!          |d                   z   |d<   |d<   |d<   ||d<   t%          j        t(          |fdd|dd                                                     |d
t          |d                   t          |d                   t!          |d                   t!          |d                   dS | dk    rt          dd
          \  }}	t%          j        t,          |fdd|dd                                                     t#          j                    dz   }
t#          j                    |
k     rt0          5  t2                              |          }ddd           n# 1 swxY w Y   |r"|                    d          s|d         dk    rn1t          j        d           d{V  t#          j                    |
k     t0          5  t2                              |i           }ddd           n# 1 swxY w Y   |                    d          dk    r&t9          d|                    d           pd!"          |                    d          st9          d#d$"          |d
|d         |d%         t!          |                    d          pd&          t!          |                    d          pd'          dS | d(k    rdd)l m}mmm } ddl |            \  }t          j        d*          p|                    d          fd+}t          j!                                        d|           d{V }t          d(d
          \  }}|                    d          }|t!          |          nd|d,<   t          |d                   |d<   ||d-<   |d.<   |d<   |d<   d/|d0<   t!          |d1                   }||d2<   |d3k    r7|d4z  }tE          dt!          |t#          j                    z
                      }nt#          j                    |z   }|}||d<   t%          j        tF          |fdd|dd                                                     |d
t          |d                   t          |d5                   |tE          d6|d,         pd7d8z            dS t9          d9d:|  d;"          )<a  Initiate a device-code flow (Nous, OpenAI Codex, or MiniMax).

    Calls the provider's device-auth endpoint via the existing CLI helpers,
    then spawns a background poller. Returns the user-facing display fields
    so the UI can render the verification page link + user code.
    r  r   )$_nous_device_scope_with_env_override-_request_nous_device_code_with_scope_fallbackPROVIDER_REGISTRYNHERMES_PORTAL_BASE_URLNOUS_PORTAL_BASE_URLr  )default_scopec                                                               d          ddi          5 }  |            cd d d            S # 1 swxY w Y   d S )N      .@Acceptr  r  r;   )clientr  r  r  allow_legacy_fallbackClientTimeout)r+  r"  r  explicit_scopehttpxr  r  s    rE   _do_nous_device_requestz8_start_device_code_flow.<locals>._do_nous_device_request~  s    d++!#56    
 DD!$3'.<*<  	
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   AAAr  intervalr  ri  r  r  r  Tzoauth-poll-rU  )targetargsdaemonrD  	user_codeverification_uri_complete)r  r~  r7  verification_urlr  poll_intervalr  zoauth-codex-
   r  r  g?r  r  r  zdevice-auth failedrK   i  z2device-auth timed out before returning a user coder9  r  r/   r  )_minimax_pkce_pair_minimax_request_user_codeMINIMAX_OAUTH_CLIENT_IDMINIMAX_OAUTH_GLOBAL_BASEMINIMAX_PORTAL_BASE_URLc                                                               d          ddid          5 }  |           cd d d            S # 1 swxY w Y   d S )Nr(  r)  r  Tr  r;   follow_redirects)r+  r  r  r  rr   r-  )r+  r>  r=  r
  r1  r  rr   s    rE   _do_minimax_requestz4_start_device_code_flow.<locals>._do_minimax_request  s    d++!#56!%     11!$35#,                   s   AAAinterval_msr  rr   r  r  
expired_inexpired_in_rawl    J)g     @@verification_urirX  r  r  rm   z	Provider z" does not support device-code flow)$r  r!  r"  r#  r1  r  rr  r  r  r  r  rV  rW  rX  r  r   r   r]  	threadingThread_nous_pollerstart_codex_full_login_worker	monotonicr  r  r<   sleepr   r<  r=  r>  r?  get_event_loopr  _minimax_poller)r  r!  r#  pconfigr2  device_dataeffective_scoper  r  r   deadliner=  r<  r?  r  rD  interval_rawrG  expires_at_tsexpires_in_secondsr>  r=  r"  r
  r  r0  r1  r  r  rr   s                       @@@@@@@@@@rE   _start_device_code_flowrY  d  s      f	
 	
 	
 	
 	
 	
 	
 	
 	
 	

 	#F+I.// 'y/00'&
&++	 	
 %	 D D!-!
 !
 !
~
	 	 	 	 	 	 	 	 	 	 .5-E-G-G-W-W).
 .
 (
 (
 (
 (
 (
 (
$_ 'v}==	T!+m"<==]{:677Z!Y[[3{</H+I+II\"1%['WsfT@WcRTSTRTg@W@W	
 	
 	

%'''![566 #K0K$L M Mk,788 Z!899
 
 	
 n$$#NMBBQ 	+3&)BQB))	
 	
 	
 %'''>##b(n))% - -#'',,- - - - - - - - - - - - - - - aeeK(( AhK9,D,D-$$$$$$$$$ n)) " 	- 	-##C,,A	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	-55??g%%Co8N8N8fRfgggguu[!! 	nC8lmmmm!; !"4 5aeeL118S99 z!2!2!7a88
 
 	
 o%%	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	%7%7%9%9")UI/00M4M
&++ 		 	 	 	 	 	 	 	 	 	 $244DD%
 
 
 
 
 
 
 
 'FF	T #z22!-!9Ct 	]  K 899[ (_W"13[!X
 [677!/---*V3M!$QMDIKK,G(H(H!I!I IKK.8M!/*\"(s2A2w((		
 	
 	

 %'''![566 #K0B$C D D, T-%8%@DT$IJJ
 
 	
 C0kK0k0k0k
l
l
lls$   J  JJ'LLLc                    ddl m}m}m} ddlm}m} ddl}t          5  t          	                    |           }ddd           n# 1 swxY w Y   |sdS |d         }|d         }	|d         }
|d         }|	                    d	          }t          d
t          |d         t          j                    z
                      }	 |                    |                    d          ddi          5 } ||||	|
||          }ddd           n# 1 swxY w Y   |                    |j                  }t          |	                    d          pd          }||	                    d          |	|	                    d	          p||	                    dd          |d         |	                    d          |                                |rC|                    |                                |z   |j                                                  nd|d
} ||ddd|          }ddl m}  ||           t          5  d|d<   ddd           n# 1 swxY w Y   t*                              d|            dS # t.          $ rc}t*                              d | |           t          5  d!|d<   t3          |          |d"<   ddd           n# 1 swxY w Y   Y d}~dS Y d}~dS d}~ww xY w)#zDBackground poller that drives a Nous device-code flow to completion.r   )NOUS_INFERENCE_AUTH_MODE_FRESH_poll_for_tokenrefresh_nous_oauth_from_statedatetimetimezoneNr  r  r  r3  r  <   ri  r(  r)  r  r*  )r+  r  r  r  r  r:  r  inference_base_url
token_typeBearerr  r  tz)
r  rb  r  r  rc  r  r  obtained_atri  r  r;  F)min_key_ttl_secondstimeout_secondsforce_refreshinference_auth_mode)persist_nous_credentialsr  r  z/oauth/device: nous login completed (session=%s)z-nous device-code poll failed (session=%s): %sr  r  )r  r[  r\  r]  r_  r`  r1  r  r  r<   r  r   r]  r.  r/  r>  utc	isoformatfromtimestamp	timestamprl  r  rE  r  r  r   )r  r[  r\  r]  r_  r`  r1  r  r  r  r  r3  r  r  r+  
token_datar>  	token_ttl
auth_state
full_staterl  r  s                         rE   rK  rK    sm            
 ,+++++++LLL	 / /"":../ / / / / / / / / / / / / / / ,-O[!I}%KJHHHWERT,/$)++=>>??J,+\\%--"5"5J\?]\^^ 	bh( /#'%&  J	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ll8<((
|449::	.",..1E"F"F"^^G,,5$..x@@&~6'^^O<<==?? '&&s}}'Bx|&TT^^```"&#
 

 32 #  >
 
 

 	=<<<<<  ,,,! 	( 	('DN	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(		CZPPPPP + + +DjRSTTT! 	+ 	+$DN$'FFD!	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	++s   AA	A	,I/ -D?I/ DI/ DD"I/ 5I;I/ II/ II/ /
K9#KK 4K K	KK	KKc                 T   ddl m}m}m}m}m} ddlm}m} ddl}t          5  t                              |           }	ddd           n# 1 swxY w Y   |	sdS |	d         }
|	d         }|	d         }|	d         }|	                    d	          }|	d
         }	 |                    |                    d          ddid          5 } |||
|||||          }ddd           n# 1 swxY w Y   |                    |j                  } |t!          |d                   |          }t#          dt!          ||                                z
                      }d|	                    dd          |
||||                    dd          |d         |d         |                    d          |                                |                    ||j                                                  |d} ||           t          5  d|	d<   ddd           n# 1 swxY w Y   t*                              d|            dS # t.          $ rc}t*                              d | |           t          5  d!|	d<   t3          |          |	d"<   ddd           n# 1 swxY w Y   Y d}~dS Y d}~dS d}~ww xY w)#u	  Background poller that drives a MiniMax OAuth flow to completion.

    Mirrors `_nous_poller` but calls the MiniMax-specific token endpoint,
    which uses a PKCE-style ``code_verifier`` + ``user_code`` rather than
    the ``device_code`` field used by Nous. On success, builds the same
    auth_state dict that ``_minimax_oauth_login`` (the CLI flow) builds
    and persists via ``_minimax_save_auth_state`` — so the dashboard
    path leaves the system in the same state as
    ``hermes auth add minimax-oauth``.
    r   )_minimax_poll_token"_minimax_resolve_token_expiry_unix_minimax_save_auth_stateMINIMAX_OAUTH_GLOBAL_INFERENCEMINIMAX_OAUTH_SCOPEr^  Nr  r  r7  r  rE  rG  r(  r)  r  TrB  )r+  r  r  r7  r  rF  rE  rF  )r>  r  r  r  rc  rd  r  r  resource_urlre  )r  r  r  rb  r  r  rc  r  r  r{  rg  ri  r  r  r  z2oauth/device: minimax login completed (session=%s)z0minimax device-code poll failed (session=%s): %sr  r  )r  rv  rw  rx  ry  rz  r_  r`  r1  r  r  r<   r.  r/  r>  rm  r   r  rp  rn  ro  r  rE  r  r  r   )r  rv  rw  rx  ry  rz  r_  r`  r1  r  r  r  r7  r  rE  rG  r+  rq  r>  rW  expires_in_srs  r  s                          rE   rQ  rQ  U  sT                 ,+++++++LLL	 / /"":../ / / / / / / / / / / / / / / ,-O[!I[!I)M((=))K*+N2+\\MM$''12!  
 
 	 ,, /##+)'  J	 	 	 	 	 	 	 	 	 	 	 	 	 	 	& ll8<((::
<())s
 
 
 1c-#--//"ABBCC'hhx22."@"($..x@@&~6'8&NN>::==??"00(, 1  ikk&
 

" 	! ,,,! 	( 	('DN	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(		F
SSSSS + + +GUVWWW! 	+ 	+$DN$'FFD!	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	++s   A		AA-H: C"H: "C&&H: )C&*DH:  HH: HH: HH: :
J'#J"'J?J"J	J"J	J""J'c                 v
   	 ddl }ddlm}m}m} d}|                    |                    d                    5 }|                    | dd|id	d
i          }ddd           n# 1 swxY w Y   |j        dk    rt          d|j                   |
                                }|                    dd          }	|                    dd          }
t          dt          |                    dd                              }|	r|
st          d          | d}t          5  t                              |           }|s	 ddd           dS |	|d<   ||d<   |
|d<   ||d<   d|d<   t!          j                    |d         z   |d<   ddd           n# 1 swxY w Y   t!          j                    |d         z   }d}|                    |                    d                    5 }t!          j                    |k     rut!          j        |           |                    | d|
|	dd	d
i          }|j        dk    r|
                                }n!|j        dv rut          d|j                   ddd           n# 1 swxY w Y   |+t          5  d|d<   d |d!<   ddd           n# 1 swxY w Y   dS |                    d"d          }|                    d#d          }|r|st          d$          |                    |                    d                    5 }|                    |d"|| d%||d&d	d'i(          }ddd           n# 1 swxY w Y   |j        dk    rt          d)|j                   |
                                }|                    d*d          }|                    d+d          }|st          d,          dd-lm}m}m}m} ddl} |d.          }t3          j        d/d                                                              d0          p|} |d.|                                j        dd1         d2|d| d3|||4	  	        }|                    |           t          5  d5|d<   ddd           n# 1 swxY w Y   t@          !                    d6|            dS # tD          $ r}t@          #                    d7| |           t          5  t                              |           } | rd8| d<   tI          |          | d!<   ddd           n# 1 swxY w Y   Y d}~dS Y d}~dS d}~ww xY w)9u  Run the complete OpenAI Codex device-code flow.

    Codex doesn't use the standard OAuth device-code endpoints; it has its
    own ``/api/accounts/deviceauth/usercode`` (JSON body, returns
    ``device_auth_id``) and ``/api/accounts/deviceauth/token`` (JSON body
    polled until 200). On success the response carries an
    ``authorization_code`` + ``code_verifier`` that get exchanged at
    CODEX_OAUTH_TOKEN_URL with grant_type=authorization_code.

    The flow is replicated inline (rather than calling
    _codex_device_code_login) because that helper prints/blocks/polls in a
    single function — we need to surface the user_code to the dashboard the
    moment we receive it, well before polling completes.
    r   N)CODEX_OAUTH_CLIENT_IDCODEX_OAUTH_TOKEN_URLDEFAULT_CODEX_BASE_URLzhttps://auth.openai.comr(  r  z!/api/accounts/deviceauth/usercoder  r  r  )r  r;   r  zdeviceauth/usercode returned r7  r9   device_auth_id   r3  5z8device-code response missing user_code or device_auth_idz/codex/devicer9  r  r  ri  z/api/accounts/deviceauth/token)r  r7  >     r  zdeviceauth/token poll returned expiredr  z#Device code expired before approvalr  r  r  z=device-auth response missing authorization_code/code_verifierz/deviceauth/callback)r  r  r  r  r  z!application/x-www-form-urlencoded)r  r;   ztoken exchange returned r  r  z*token exchange did not return access_tokenr  r  HERMES_CODEX_BASE_URLr  rU  zdashboard device_codez:dashboard_device_code)	r  r}  r  r  r  r  r  r  r  r  z7oauth/device: openai-codex login completed (session=%s)z0codex device-code worker failed (session=%s): %sr  )%r1  r  r~  r  r  r.  r/  postrL   RuntimeErrorr  r<   r  r   r  r  r]  rN  rO  r  r  r  r  r  r  r  rr  ra   r  r  r  r  r  rE  r  r  r   )!r  r1  r~  r  r  issuerr+  r  rS  r7  r  r:  r9  r  rU  	code_respr  r  r  
token_resptokensr  r  r  r  r  r  _uuidr  r  r   r  r=  s!                                    rE   rM  rM    su   w,	
 	
 	
 	
 	
 	
 	
 	
 	
 	

 + \\%--"5"5\66 	&;;<<<!#89');<   D	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 s""Qt?OQQRRRiikkOOK44	$)92>>As;??:s#C#CDDEE 	[ 	[YZZZ$333! 		B 		B"&&z22D 		B 		B 		B 		B 		B 		B 		B 		B !*D'7D#$%3D!",D!(D!%tL/A!AD		B 		B 		B 		B 		B 		B 		B 		B 		B 		B 		B 		B 		B 		B 		B >##d<&88	\\%--"5"5\66 	Y&.""X--
=))){{===,:SS+-?@ #  
 #s** $		I#z11"#WTEU#W#WXXX	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y % N N!*X(M_%N N N N N N N N N N N N N N N F ']]+?DD!or::! 	` 	`^___\\%--"5"5\66 	&%"6.'-$C$C$C!6%2  ()LM % 
 
J	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 !S((R*:PRRSSS""zz."55

?B77 	MKLLL	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	y((I-r2288::AA#FF &% 	 ! #{{}} !$)%#;;;%'

 

 

 	u! 	( 	('DN	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(		KZXXXXX , , ,GUVWWW! 	, 	,##J//A ,%(%(VV/"		, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	,,s)  9R/  A'R/ 'A++R/ .A+/B5R/ $FR/ 8FR/ FR/ FA
R/ %BI>2R/ >JR/ JR/ J)R/ )J--R/ 0J-1R/ 6A(R/ $MR/ MR/ MDR/ 5R;R/ RR/ RR/ /
T89#T34TT3T 	 T3#T 	$T33T8z(/api/providers/oauth/{provider_id}/startc                 x   K   t          |           t                       d t          D             } |vrt          dd            t	           fdt          D                       }|d         dk    rt          d  d|d	          d
          	 |d         dk    r dk    rt                      S |d         dk    rt                      d{V S nU# t          $ r  t          $ r>}t          	                    d            t          dt          |                    d}~ww xY wt          dd          )z.Initiate an OAuth login flow. Token-protected.c                     h | ]
}|d          S r  r   rE  s     rE   r(  z$start_oauth_login.<locals>.<setcomp>6	  s    666QtW666rG   rm   zUnknown provider rK   c              3   4   K   | ]}|d          k    |V  dS )r}  Nr   )r&  rF  r  s     rE   r?  z$start_oauth_login.<locals>.<genexpr>9	  s1      VVqqw+?U?U?U?U?U?UVVrG   r~  r  z uses an external CLI; run `r  z
` manuallyr|  r{  r  Nzoauth/start %s failedr  zUnsupported flow)rN   r  r  r   nextr  rY  r  r  r  r   )r  r6   validcatalog_entryr  s   `    rE   start_oauth_loginr  1	  s      7665666E%4U4U4UVVVVVVVV$;VVVVVMV
**!gg}]?[ggg
 
 
 	
<  F**{k/I/I(*** M110========= 2    < < <.<<<CFF;;;;< C0B
C
C
CCs   C 4 C D(*9D##D(c                   $    e Zd ZU eed<   eed<   dS )OAuthSubmitBodyr  r  Nr   r   rG   rE   r  r  R	  s"         OOO
IIIIIrG   r  z)/api/providers/oauth/{provider_id}/submitc                    K   t          |           | dk    r>t          j                                        dt          |j        |j                   d{V S t          dd|            )z5Submit the auth code for PKCE flows. Token-protected.r{  Nrm   zsubmit not supported for rK   )rN   rV  rW  rX  r  r  r  r   )r  r   r6   s      rE   submit_oauth_coder  W	  s       7k!!-//??($/49
 
 
 
 
 
 
 
 	
 C0YK0Y0Y
Z
Z
ZZrG   z4/api/providers/oauth/{provider_id}/poll/{session_id}c                 >  K   t           5  t                              |          }ddd           n# 1 swxY w Y   |st          dd          |d         | k    rt          dd          ||d         |                    d	          |                    d
          dS )uB   Poll a device-code session's status (no auth — read-only state).Nr  zSession not found or expiredrK   r  rm   zProvider mismatch for sessionr  r  ri  )r  r  r  ri  )r  r  r<   r   )r  r  r  s      rE   poll_oauth_sessionr  b	  s       
 / /"":../ / / / / / / / / / / / / / / T4RSSSSJ;&&4STTTT x./22hh|,,	  s   155z*/api/providers/oauth/sessions/{session_id}c                    K   t          |           t          5  t                              | d          }ddd           n# 1 swxY w Y   |dddS d| dS )z0Cancel a pending OAuth session. Token-protected.NFzsession not found)r  r  T)r  r  )rN   r  r  r-  )r  r6   r  s      rE   cancel_oauth_sessionr  s	  s       7	 5 5"":t445 5 5 5 5 5 5 5 5 5 5 5 5 5 5|(;<<<j111s   AAAc           
         ddl m} d } |            }	 |                    |           }|r|                    |          sdg f|                                 S t          |dd          p2t          |dd          p!t          |dd          pt          |dd          }g }|g|                    d	                                          }|D ]<}|                     ||d
d           ||dd           ||dd          d           =n|	                    dd          }i }	|D ]Y}|
                    d
          }
|
                    d          }|
r+|r)|	                    |g                               |           Zd }|}|g}|h|	
                    |          r{fd|	|         D             }|snd|                    |d           |d         d
         }|                    |                               |           |	
                    |          {||f|                                 S # |                                 w xY w)zResolve a session id to the newest child leaf session.

    /model may create child sessions. Dashboard refresh should continue the
    newest child instead of reopening the old parent.
    r   r3  c                     t          | t                    r|                     |          S 	 | |         S # t          $ r 	 | |         cY S # t          $ r Y Y d S w xY ww xY wr  )r   r   r<   r  )rowr   indexs      rE   row_getz+_session_latest_descendant.<locals>.row_get	  s    c4   	 773<<	s8O 	 	 	5z!!!   ttt	s,   4 
AA	A	
AAAANconn_conn
connection_connectionz6SELECT id, parent_session_id, started_at FROM sessionsr}  parent_session_idr]   r:  rX  )r}  r  r:  i'  r  c                 n    	 t          |                     d          pd          S # t          $ r Y dS w xY w)Nr:  r           )r   r<   r  )r  s    rE   startedz+_session_latest_descendant.<locals>.started	  sH    SWW\227a888   sss   #& 
44c                 B    g | ]}|                     d           v|S r  r<  )r&  rr  s     rE   rG  z._session_latest_descendant.<locals>.<listcomp>	  s-    RRR!%%++T:Q:Q!:Q:Q:QrG   T)r   reverse)r[  r4  resolve_session_idget_sessionrg   rp   executefetchallr  r\  r<   
setdefaultsortadd)r  r4  r  rg  r  r  rowsraw_rowsr  childrenridr  r  currentr|   
candidatesr  s                   @rE   _session_latest_descendantr  	  s    '&&&&&	 	 	 
B6##J// 	"..-- 	8f 	



a B%% 0r7D))0r<..0 r=$//	 	 ||H hjj     !'#tQ//)06I1)M)M")'#|Q"?"?      ((uQ(??D 	< 	<C''$--CWW011F <v <##FB//66s;;;	 	 	 uull7## 	RRRRXg%6RRRJ OOO666 mD)GKK   HHW ll7## 	 }








s   /H1 GH1 1Iz/api/sessions/{session_id}c                   K   ddl m}  |            }	 |                    |           }|r|                    |          nd }|st	          dd          ||                                 S # |                                 w xY w)Nr   r3  r  Session not foundrK   )r[  r4  r  r  r   rg   )r  r4  rg  r  sessions        rE   get_session_detailr  	  s      &&&&&&	B##J//),6"..%%%$ 	MC8KLLLL








s   AA+ +Bz,/api/sessions/{session_id}/latest-descendantc                    K   t          |           \  }}|st          dd          |r|d         n| ||t          |o||d         k              dS )Nr  r  rK   r   )requested_session_idr  r|   changed)r  r   r   )r  latestr|   s      rE   get_session_latest_descendantr  	  sr      -j99LFD I4GHHHH+/ ?QZ247!233	  rG   z#/api/sessions/{session_id}/messagesc                   K   ddl m}  |            }	 |                    |           }|st          dd          |                    |          }||d|                                 S # |                                 w xY w)Nr   r3  r  r  rK   )r  messages)r[  r4  r  r   get_messagesrg   )r  r4  rg  r  r  s        rE   get_session_messagesr  	  s      &&&&&&	B##J// 	MC8KLLLL??3''!x88








s   AA* *B c                    K   ddl m}  |            }	 |                    |           st          dd          ddi|                                 S # |                                 w xY w)Nr   r3  r  r  rK   r  T)r[  r4  delete_sessionr   rg   )r  r4  rg  s      rE   delete_session_endpointr  	  s{      &&&&&&	B  ,, 	MC8KLLLLd|








s   )A A(z	/api/logsd   filelevel	componentsearchc                   K   ddl m}m} |                    |           }|st	          dd|            t                      dz  |z  }|                                s| g dS 	 ddlm}	 n# t          $ r i }	Y nw xY w|r|
                                d	k    r|nd }
|rg|                                d
k    rO|	                    |          }|7t	          dd| dd                    t          |	                               nd }t          |
p|p|          } |||st          |d          nd||
|          }|r9|                                fd|D             t          |d           d          }| |dS )Nr   )
_read_tail	LOG_FILESrm   zUnknown log file: rK   rk  )r  r  )COMPONENT_PREFIXESALLallzUnknown component: r  r  r  r  )has_filters	min_levelcomponent_prefixesc                 @    g | ]}|                                 v |S r   )re   )r&  lneedles     rE   rG  zget_logs.<locals>.<listcomp>;
  s+    ;;;v':':!':':':rG   )hermes_cli.logsr  r  r<   r   r   r  hermes_loggingr  rq  upperre   r  r  r   r  )r  r  r  r  r  r  r  log_namer  r  r  comp_prefixesr  rC  r  s                 @rE   get_logsr  

  s#      65555555}}T""H Q4O4O4OPPPP  6)H4H?? +r*** 5555555        C5;;==E#9#9tI 	Y__&&%//*..y99 MY M M%)YYv6H/I/I%J%JM M    ! y;M;V<<KZ9#eS///T(	  F  O;;;;V;;;S__<L<M<MN6***s   %A, ,A;:A;c                   @    e Zd ZU eed<   eed<   dZeed<   dZeed<   dS )CronJobCreater(   scheduler9   rD  r   deliverN)r   r   r   r   r   rD  r  r   rG   rE   r  r  D
  sA         KKKMMMD#NNNGSrG   r  c                       e Zd ZU eed<   dS )CronJobUpdateupdatesNr   r   rG   rE   r  r  K
  s         MMMMMrG   r  c                      ddl m}  	 d |                                 D             S # t          $ r, t                              d           t          |           cY S w xY w)zCReturn dashboard profile records, falling back to a directory scan.r   profilesc                 ,    g | ]}t          |          S r   _profile_to_dictrE  s     rE   rG  z'_cron_profile_dicts.<locals>.<listcomp>V
  s!    JJJ ##JJJrG   zJFailed to list profiles for cron dashboard; falling back to directory scanr  r  list_profilesr  r  r  _fallback_profile_dictsprofiles_mods    rE   _cron_profile_dictsr  R
  s{    3333335JJ\-G-G-I-IJJJJ 5 5 5cddd&|444445s   & 3AAprofilec                 x   ddl m} | pd                                pd}	 |                    |          }|                    |           n0# t
          $ r#}t          dt          |                    d}~ww xY w|                    |          st          dd| d	          ||	                    |          fS )
z=Resolve a profile query value to (profile_name, HERMES_HOME).r   r  r   rm   rK   Nr  	Profile '' does not exist.)
r  r  ra   normalize_profile_namevalidate_profile_namer4  r   r   profile_existsget_profile_dir)r  r  r  canonr  s        rE   _cron_profile_homer  \
  s    333333i
&
&
(
(
5IC<33C88**51111 < < <CFF;;;;<&&u-- Z4X4X4X4XYYYY,..u5555s   *A 
A8A33A8jobhomec                 n    t          |           }||d<   ||d<   t          |          |d<   |dk    |d<   |S )Nr  profile_namerI  r   is_default_profile)r   r   )r  r  r   	annotateds       rE   _annotate_cron_jobr  k
  sG    S		I"Ii 'In"4yyIm&-&:I"#rG   	func_namec                   	
 t          |           \  
	t          5  ddlm} |j        }|j        }|j        }	dz  |_        |j        dz  |_        |j        dz  |_        	  t          ||          |i |}||_        ||_        ||_        n# ||_        ||_        ||_        w xY w	 ddd           n# 1 swxY w Y   t          |t                    r	
fd|D             S t          |t                    rt          |
	          S |S )az  Run cron.jobs helpers against the selected profile's cron directory.

    cron.jobs keeps CRON_DIR/JOBS_FILE/OUTPUT_DIR as module globals resolved
    from the process HERMES_HOME at import time. The dashboard is a single
    process that can inspect many profiles, so temporarily retarget those
    globals while holding a lock and restore them immediately after the call.
    r   )jobsr   z	jobs.jsonoutputNc                 2    g | ]}t          |          S r   )r  )r&  jr   r  s     rE   rG  z*_call_cron_for_profile.<locals>.<listcomp>
  s&    JJJa"1lD99JJJrG   )r  _CRON_PROFILE_LOCKr   r  CRON_DIR	JOBS_FILE
OUTPUT_DIRrp   r   r   r   r  )r  r  r5  kwargs	cron_jobsold_cron_dirold_jobs_fileold_output_dirrC  r   r  s            @@rE   _call_cron_for_profiler  t
  s    ,G44L$	 2 2****** )!+"-!F]	'0;>	(1H<		22WY	22DCFCCF!-I"/I#1I   ".I"/I#1I 1111 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2  &$ KJJJJJ6JJJJ&$ >!&,===Ms*   AB4!B7B4B$$B44B8;B8job_idc                      t                      D ]Y}t          |                    d          pd          }|s)t          |dd          }t	           fd|D                       r|c S Zd S )NrD  r9   	list_jobsTc              3   |   K   | ]6}|                     d           k    p|                     d          k    V  7dS )r}  rD  Nr<  )r&  r  r  s     rE   r?  z)_find_cron_job_profile.<locals>.<genexpr>
  sF      NNAquuT{{f$?f(?NNNNNNrG   )r  r   r<   r  any)r  r  rD  r  s   `   rE   _find_cron_job_profiler  
  s    &((  7;;v&&,"-- 	%dK>>NNNNNNNNN 	KKK	4rG   z/api/cron/jobsr  c                   K   | pd                                 }|                                dk    rt          |dd          S g }t                      D ]y}t	          |                    d          pd          }|s)	 |                    t          |dd                     O# t          $ r t          	                    d|           Y vw xY w|S )Nr  r  TrD  r9   z'Failed to list cron jobs for profile %s)
ra   re   r  r  r   r<   extendr  r  r  )r  	requestedr  itemrD  s        rE   list_cron_jobsr   
  s      !E((**IE!!%idCCC!#D#%% L L488F##)r** 		LKK.t[$GGHHHH 	L 	L 	LNNDdKKKKK	LKs   <$B!!%C	C	z/api/cron/jobs/{job_id}c                    K   |pt          |           }|st          dd          t          |d|           }|st          dd          |S )Nr  Job not foundrK   get_jobr  r   r  r  r  selectedr  s       rE   get_cron_jobr'  
  sg      8088H EODDDD
 9f
=
=C EODDDDJrG   c                    K   	 t          |d| j        | j        | j        | j                  S # t
          $ r=}t                              d           t          dt          |                    d }~ww xY w)N
create_job)r(   r  rD  r  zPOST /api/cron/jobs failedrm   rK   )
r  r(   r  rD  r  r  r  r  r   r   )r   r  r  s      rE   create_cron_jobr*  
  s      <%;]L
 
 
 	
  < < <3444CFF;;;;<s   (- 
A48A//A4c                   K   |pt          |           }|st          dd          	 t          |d| |j                  }n1# t          $ r$}t          dt          |                    |d }~ww xY w|st          dd          |S )Nr  r"  rK   
update_jobrm   )r  r   r  r  r4  r   )r  r   r  r&  r  r  s         rE   update_cron_jobr-  
  s      8088H EODDDDG$X|VT\RR G G GCHH===3FG EODDDDJs   A   
A.
A))A.z/api/cron/jobs/{job_id}/pausec                    K   |pt          |           }|st          dd          t          |d|           }|st          dd          |S )Nr  r"  rK   	pause_jobr$  r%  s       rE   pause_cron_jobr0  
  sg      8088H EODDDD
 ;
?
?C EODDDDJrG   z/api/cron/jobs/{job_id}/resumec                    K   |pt          |           }|st          dd          t          |d|           }|st          dd          |S )Nr  r"  rK   
resume_jobr$  r%  s       rE   resume_cron_jobr3  
  sg      8088H EODDDD
 <
@
@C EODDDDJrG   z/api/cron/jobs/{job_id}/triggerc                    K   |pt          |           }|st          dd          t          |d|           }|st          dd          |S )Nr  r"  rK   trigger_jobr$  r%  s       rE   trigger_cron_jobr6  
  sg      8088H EODDDD
 =&
A
AC EODDDDJrG   c                   K   |pt          |           }|st          dd          	 t          |d|           }n1# t          $ r$}t          dt	          |                    |d }~ww xY w|st          dd          ddiS )Nr  r"  rK   
remove_jobrm   r  T)r  r   r  r4  r   )r  r  r&  rL  r  s        rE   delete_cron_jobr9  
  s      8088H EODDDDG(<HH G G GCHH===3FG EODDDD$<s   : 
A(A##A(c                   6    e Zd ZU eed<   dZeed<   dZeed<   dS )ProfileCreaterD  Fclone_from_default	no_skillsN)r   r   r   r   r   r<  r   r=  r   rG   rE   r;  r;    s<         
III$$$$ItrG   r;  c                       e Zd ZU eed<   dS )ProfileRenamenew_nameNr   r   rG   rE   r?  r?    s         MMMMMrG   r?  c                       e Zd ZU eed<   dS )ProfileSoulUpdatero   Nr   r   rG   rE   rB  rB    s         LLLLLrG   rB  c                 H    	 t          | |          S # t          $ r |cY S w xY wr  )rp   r  )rE  rD  r   s      rE   _profile_attrrD    s;    tT"""   s    !!c                 P   t          | dd          t          t          | dd                    t          t          | dd                    t          | d          t          | d          t          t          | dd                    t          t          | d	d
          pd
          dS )NrD  r9   r|   
is_defaultFr   r  has_envskill_countr   rD  r|   rF  r   r  rG  rH  )rD  r   r   r   )rE  s    rE   r  r  #  s    dFB//M$3344=|UCCDDtW--!$
33dIu==>>=}a@@EAFF  rG   c                     d }g }                                                                  r` | fdd          \  }}|                    dt                    d||dz                                   | fdd          d	                                            }|                                rt          |                                          D ]}|                                r j        	                    |j
                  s6 ||f fd
	d          \  }}|                    |j
        t          |          d|||dz                                   ||f fd	d          d	           |S )Nc                 <    	  |             S # t           $ r |cY S w xY wr  )r  )	callable_r   s     rE   _safez&_fallback_profile_dicts.<locals>._safe0  s7    	9;; 	 	 	NNN	s   	 c                  .                                    S r  _read_config_modeldefault_homer  s   rE   <lambda>z)_fallback_profile_dicts.<locals>.<lambda>9  s    (G(G(U(U rG   NNr   Tz.envc                  .                                    S r  _count_skillsrQ  s   rE   rS  z)_fallback_profile_dicts.<locals>.<lambda>A  s    )C)CL)Q)Q rG   r   rI  c                 .                         |           S r  rO  r   r  s    rE   rS  z)_fallback_profile_dicts.<locals>.<lambda>I  s    8W8WX]8^8^ rG   Fc                 .                         |           S r  rV  rY  s    rE   rS  z)_fallback_profile_dicts.<locals>.<lambda>Q  s    9S9STY9Z9Z rG   )_get_default_hermes_homeis_dirr  r   r  _get_profiles_rootr  iterdir_PROFILE_ID_REmatchrD  )r  rM  r  r   r  profiles_rootr   rR  s   `      @rE   r  r  /  s      &(H88::L 
% U U U U UWcddx%% $v-5577 5!Q!Q!Q!Q!QSTUU
 
 	 	 	 !3355M M113344 	 	E<<>> )D)J)J5:)V)V #e$^$^$^$^$^`lmmOE8OO
E

#$!FN2244$u%%Z%Z%Z%Z%Z\]^^      OrG   c                    ddl m} 	 |                    |            n0# t          $ r#}t	          dt          |                    d}~ww xY w|                    |           st	          dd|  d          |                    |           S )	zIValidate ``name`` and resolve to its directory or raise an HTTPException.r   r  rm   rK   Nr  r  r  )r  r  r  r4  r   r   r  r  )rD  r  r  s      rE   _resolve_profile_dirrc  W  s    333333<**40000 < < <CFF;;;;<&&t,, Y4W4W4W4WXXXX''---s    
AAAc                 :    t          |            | dk    rdn|  dS )z@Return the shell command used to configure a profile in the CLI.r   zhermes setupz setup)rc  rC  s    rE   _profile_setup_commandre  c  s*    !Y..>>tOOOCrG   z/api/profilesc                     K   ddl m}  	 dd |                                 D             iS # t          $ r. t                              d           dt          |           icY S w xY w)Nr   r  r  c                 ,    g | ]}t          |          S r   r  rE  s     rE   rG  z*list_profiles_endpoint.<locals>.<listcomp>m  s!    WWWQ-a00WWWrG   z@GET /api/profiles failed; falling back to profile directory scanr  r  s    rE   list_profiles_endpointrh  i  s      333333CWW,:T:T:V:VWWWXX C C CYZZZ3LAABBBBCs   * 5A"!A"c                 T  K   ddl m} 	 |                    | j        | j        rdnd | j        | j                  }| j        s|                    |d           |                    | j                  }|s|                    | j                   n# t          t          t          f$ r#}t          dt          |                    d }~wt          $ r=}t                              d	           t          d
t          |                    d }~ww xY wd| j        t          |          dS )Nr   r  r   )rD  
clone_fromclone_configr=  T)quietrm   rK   zPOST /api/profiles failedr  r  rD  r|   )r  r  create_profilerD  r<  r=  seed_profile_skillscheck_alias_collisioncreate_wrapper_scriptr4  FileExistsErrorFileNotFoundErrorr   r   r  r  r  )r   r  r|   	collisionr  s        rE   create_profile_endpointru  s  sN     333333<**$($;Eyy0n	 + 
 
 & 	?,,T,>>> !66tyAA	 	:..ty999):; < < <CFF;;;; < < <2333CFF;;;;< 	3t99===s$   BB D'CD8D

Dz"/api/profiles/{name}/setup-commandc                 (   K   dt          |           iS )Ncommand)re  rC  s    rE   get_profile_setup_commandrx    s      -d3344rG   z"/api/profiles/{name}/open-terminalc                   K   	 t          |           }t          j                            d          rt	          j        dddd|g           nt          j        dk    rH|                    dd                              d	d
          }d| d}t	          j        dd|g           nddddd|gfddddd|gfddddd|gfdddd| dgfdddd| dgfdddd| dgfddddd|gfddddd|gfdddd|gfddddd|gfg
}|D ]L\  }}t	          j        d|gt          j        t          j                  d k    rt	          j        |            nMt          d!d"#          n# t          $ r#}t          d$t          |          #          d }~wt          $ r#}t          d!t          |          #          d }~wt          $ r  t          $ r>}t                              d%|            t          d&t          |          #          d }~ww xY wd'|d(S ))Nwinzcmd.exez/crL  r9   darwin\z\\r  z\"z0tell application "Terminal"
activate
do script "z
"
end tell	osascriptz-ezx-terminal-emulatorshz-lczgnome-terminalz--konsolezxfce4-terminalzsh -lc ''zmate-terminal
lxterminaltilix	alacrittykittyxtermwhich)r|  r}  r   rm   z$No supported terminal emulator foundrK   r  z*POST /api/profiles/%s/open-terminal failedr  T)r  rw  )re  r  r'  rb   r  r  r   callr  r   rs  r   r4  r  r  r  )rD  rw  escapedapplescriptterminal_commandsr  
popen_argsr  s           rE   open_profile_terminal_endpointr    s     0<(..<""5)) $	iwGDEEEE\X%%oodF33;;CGGG%    k4=>>>> ')>dESZ([\!$4dD%#QRYdE7CD!$4d<Qw<Q<Q<Q#RS ?D:OW:O:O:O"PQd4Iw4I4I4IJK7D$w?@{D$wGH7D%9:7D$w?@! +<  &
J?j)%-%-   	 
 $Z000E $ #A     < < <CFF;;;; < < <CFF;;;;    < < <CTJJJCFF;;;;< 7+++s0   EE 
H'FHF00H9H  Hz/api/profiles/{name}c                   K   ddl m} 	 |                    | |j                  }n# t          $ r#}t          dt          |                    d }~wt          t          f$ r#}t          dt          |                    d }~wt          $ r>}t                              d|            t          dt          |                    d }~ww xY wd|j        t          |          d	S )
Nr   r  r  rK   rm   zPATCH /api/profiles/%s failedr  Trm  )r  r  rename_profiler@  rs  r   r   r4  rr  r  r  r  )rD  r   r  r|   r  s        rE   rename_profile_endpointr    s      333333<**4?? < < <CFF;;;;( < < <CFF;;;; < < <6===CFF;;;;< s4yyAAAs,   & 
CAC"B  C9CCc                   K   ddl m} 	 |                    | d          }n# t          $ r#}t	          dt          |                    d}~wt          $ r#}t	          dt          |                    d}~wt          $ r>}t          	                    d	|            t	          d
t          |                    d}~ww xY wdt          |          dS )zDelete a profile. The dashboard collects the user's confirmation in
    its own dialog before this request, so we always pass ``yes=True`` to
    skip the CLI's interactive prompt.r   r  T)yesr  rK   Nrm   zDELETE /api/profiles/%s failedr  )r  r|   )
r  r  delete_profilers  r   r   r4  r  r  r  )rD  r  r|   r  s       rE   delete_profile_endpointr    s      
 433333<**4T*:: < < <CFF;;;; < < <CFF;;;; < < <7>>>CFF;;;;< D		***s,   " 
C A

C A55C 9B;;C z/api/profiles/{name}/soulc                    K   t          |           dz  }|                                r@	 |                    d          ddS # t          $ r}t	          dd|           d }~ww xY wd	d
dS )NSOUL.mdr  r  T)ro   r  r  zCould not read SOUL.md: rK   r9   F)rc  r  r  r  r   )rD  	soul_pathr  s      rE   get_profile_soulr    s      $T**Y6I X	X(22G2DDPTUUU 	X 	X 	XC8VST8V8VWWWW	XU+++s   A 
A&A!!A&c                    K   t          |           dz  }	 |                    |j        d           nA# t          $ r4}t                              d|            t          dd|           d }~ww xY wdd	iS )
Nr  r  r  z PUT /api/profiles/%s/soul failedr  zCould not write SOUL.md: rK   r  T)rc  
write_textro   r  r  r  r   )rD  r   r  r  s       rE   update_profile_soulr    s      $T**Y6IUT\G<<<< U U U94@@@4SPQ4S4STTTTU $<s   3 
A1/A,,A1c                   $    e Zd ZU eed<   eed<   dS )SkillTogglerD  enabledN)r   r   r   r   r   r   r   rG   rE   r  r  	  s"         
IIIMMMMMrG   r  z/api/skillsc                     K   ddl m}  ddlm} t	                      } ||          } | d          }|D ]}|d         |v|d<   |S )Nr   )_find_all_skills)get_disabled_skillsT)skip_disabledrD  r  )tools.skills_toolr  hermes_cli.skills_configr  r   )r  r  r   disabledr   r=  s         rE   
get_skillsr    s~      222222<<<<<<]]F""6**HD111F 1 1y0)MrG   z/api/skills/togglec                    K   ddl m}m} t                      } ||          }| j        r|                    | j                   n|                    | j                    |||           d| j        | j        dS )Nr   )r  save_disabled_skillsT)r  rD  r  )r  r  r  r   r  discardrD  r  )r   r  r  r   r  s        rE   toggle_skillr    s      RRRRRRRR]]F""6**H|  ####TY***	dlCCCrG   z/api/tools/toolsetsc                  P  K   ddl m} m}m} ddlm} t                      } ||dd          }g } |             D ]j\  }}}		 t          t           ||                              }
n# t          $ r g }
Y nw xY w||v }|
                    |||	|| |||          |
d           k|S )Nr   )$_get_effective_configurable_toolsets_get_platform_tools_toolset_has_keys)resolve_toolsetcliF)include_default_mcp_servers)rD  r  r   r  	available
configuredr=  )hermes_cli.tools_configr  r  r  toolsetsr  r   r  setr  r  )r  r  r  r  r   enabled_toolsetsrC  rD  r  descr=  
is_enableds               rE   get_toolsetsr  '  s=              
 )(((((]]F**$)  
 FAACC  eT	3t445566EE 	 	 	EEE	--
5!#++D&99
 
 	 	 	 	 Ms   %A((A76A7c                       e Zd ZU eed<   dS )RawConfigUpdate	yaml_textNr   r   rG   rE   r  r  L  s         NNNNNrG   r  z/api/config/rawc                     K   t                      } |                                 sddiS d|                     d          iS )Nyamlr9   r  r  )r   r  r  )r|   s    rE   get_config_rawr  P  sD      D;;== |DNNGN4455rG   c                   K   	 t          j        | j                  }t          |t                    st          dd          t          |           ddiS # t           j        $ r}t          dd|           d }~ww xY w)Nrm   zYAML must be a mappingrK   r  TzInvalid YAML: )r  	safe_loadr  r   r   r   r   	YAMLError)r   parsedr  s      rE   update_config_rawr  X  s      J//&$'' 	RC8PQQQQFd|> J J J4HQ4H4HIIIIJs   AA A>%A99A>z/api/analytics/usagedaysc                   K   ddl m} ddlm}  |            }	 t	          j                    | dz  z
  }|j                            d|f          }d |                                D             }|j                            d|f          }d |                                D             }|j                            d	|f          }	t          |		                                          }
 ||          
                    | 
          }|                    ddddddg d          }|||
| |d|                                 S # |                                 w xY w)Nr   r3  )InsightsEngineQ a}  
            SELECT date(started_at, 'unixepoch') as day,
                   SUM(input_tokens) as input_tokens,
                   SUM(output_tokens) as output_tokens,
                   SUM(cache_read_tokens) as cache_read_tokens,
                   SUM(reasoning_tokens) as reasoning_tokens,
                   COALESCE(SUM(estimated_cost_usd), 0) as estimated_cost,
                   COALESCE(SUM(actual_cost_usd), 0) as actual_cost,
                   COUNT(*) as sessions,
                   SUM(COALESCE(api_call_count, 0)) as api_calls
            FROM sessions WHERE started_at > ?
            GROUP BY day ORDER BY day
        c                 ,    g | ]}t          |          S r   r   r&  r  s     rE   rG  z'get_usage_analytics.<locals>.<listcomp>~  s    111Qa111rG   a  
            SELECT model,
                   SUM(input_tokens) as input_tokens,
                   SUM(output_tokens) as output_tokens,
                   COALESCE(SUM(estimated_cost_usd), 0) as estimated_cost,
                   COUNT(*) as sessions,
                   SUM(COALESCE(api_call_count, 0)) as api_calls
            FROM sessions WHERE started_at > ? AND model IS NOT NULL
            GROUP BY model ORDER BY SUM(input_tokens) + SUM(output_tokens) DESC
        c                 ,    g | ]}t          |          S r   r  r  s     rE   rG  z'get_usage_analytics.<locals>.<listcomp>  s    555DGG555rG   a2  
            SELECT SUM(input_tokens) as total_input,
                   SUM(output_tokens) as total_output,
                   SUM(cache_read_tokens) as total_cache_read,
                   SUM(reasoning_tokens) as total_reasoning,
                   COALESCE(SUM(estimated_cost_usd), 0) as total_estimated_cost,
                   COALESCE(SUM(actual_cost_usd), 0) as total_actual_cost,
                   COUNT(*) as total_sessions,
                   SUM(COALESCE(api_call_count, 0)) as total_api_calls
            FROM sessions WHERE started_at > ?
        )r  r   )total_skill_loadstotal_skill_editstotal_skill_actionsdistinct_skills_used)summary
top_skills)dailyby_modeltotalsperiod_daysr   )r[  r4  agent.insightsr  r]  r  r  r  r   fetchonegenerater<   rg   )r  r4  r  rg  rQ  curr  cur2r  cur3r  insights_reportr   s                rE   get_usage_analyticsr  i  s     &&&&&&------	B<u-h   Y  21#,,..111x 	! Y	 	 65T]]__555x 
! Y
 
 dmmoo&&(.,,5545@@ $$X%&%&'(()	  0
 0
    
 
 	







s   DD= =Ez/api/analytics/modelsc                 f  K   ddl m}  |            }	 t          j                    | dz  z
  }|j                            d|f          }d |                                D             }g }|D ]}|                    d          pd}|d         }	i }
	 dd	lm}  |||	
          }|'|j	        |j
        |j        |j        |j        |j        d}
n# t          $ r Y nw xY w|                    |	||d         |d         |d         |d         |d         |d         |d         |d         |d         |d         |d         |
d           |j                            d|f          }t#          |                                          }||| d|                                 S # |                                 w xY w)zRich per-model analytics for the Models dashboard page.

    Returns token/cost/session breakdown per model plus capability metadata
    from models.dev (context window, vision, tools, reasoning, etc.).
    r   r3  r  a  
            SELECT model,
                   billing_provider,
                   SUM(input_tokens) as input_tokens,
                   SUM(output_tokens) as output_tokens,
                   SUM(cache_read_tokens) as cache_read_tokens,
                   SUM(reasoning_tokens) as reasoning_tokens,
                   COALESCE(SUM(estimated_cost_usd), 0) as estimated_cost,
                   COALESCE(SUM(actual_cost_usd), 0) as actual_cost,
                   COUNT(*) as sessions,
                   SUM(COALESCE(api_call_count, 0)) as api_calls,
                   SUM(tool_call_count) as tool_calls,
                   MAX(started_at) as last_used_at,
                   AVG(input_tokens + output_tokens) as avg_tokens_per_session
            FROM sessions WHERE started_at > ? AND model IS NOT NULL AND model != ''
            GROUP BY model, billing_provider
            ORDER BY SUM(input_tokens) + SUM(output_tokens) DESC
        c                 ,    g | ]}t          |          S r   r  r  s     rE   rG  z(get_models_analytics.<locals>.<listcomp>  s    000AQ000rG   billing_providerr9   r   r  r  Nr  input_tokensoutput_tokenscache_read_tokensreasoning_tokensestimated_costactual_costrh  	api_calls
tool_callslast_used_atavg_tokens_per_session)r   r  r  r  r  r  r  r  rh  r  r  r  r  r  a  
            SELECT COUNT(DISTINCT model) as distinct_models,
                   SUM(input_tokens) as total_input,
                   SUM(output_tokens) as total_output,
                   SUM(cache_read_tokens) as total_cache_read,
                   SUM(reasoning_tokens) as total_reasoning,
                   COALESCE(SUM(estimated_cost_usd), 0) as total_estimated_cost,
                   COALESCE(SUM(actual_cost_usd), 0) as total_actual_cost,
                   COUNT(*) as total_sessions,
                   SUM(COALESCE(api_call_count, 0)) as total_api_calls
            FROM sessions WHERE started_at > ? AND model IS NOT NULL AND model != ''
        )modelsr  r  )r[  r4  r]  r  r  r  r<   r  r  r  r   r  r  r  r  r  r  r   r  rg   )r  r4  rg  rQ  r  r  r  r  r  r	  r  r  r  
totals_curr  s                  rE   get_models_analyticsr    s>      '&&&&&	BPu-h  " Y# $ 10000 "	 "	Cww1228bHWJDCCCCCC++XZPPP>*,*;+-+=.0.C*,*;-/-A(* D     MM#$ #N 3!$_!5%()<%=$'(:$;"%&6"7"=1
O -!,/ #N 3*-.F*G $     " X%% ' Y 
 j))++,, 
 
 	







s1   A:F <CF 
CF CB,F F0)	PtyBridgePtyUnavailableErrorc                       e Zd ZdZdS )r  z5Stub on platforms where pty_bridge can't be imported.N)r   r   r   r  r   rG   rE   r  r  &  s        CCrG   r  s   \x1b\[RESIZE:(\d+);(\d+)\]g?z^[A-Za-z0-9._-]{1,128}$>   rO   
testclientrP   rQ   wsr   c                     t          t          j        dd          rdS | j        r| j        j        nd}|sdS |t
          v S )u  Check if the WebSocket client IP is acceptable.

    Loopback mode: only loopback clients allowed — the legacy
    ``?token=<_SESSION_TOKEN>`` path is the only auth we have, so we
    don't want LAN hosts guessing tokens.

    Gated mode: any peer is allowed — uvicorn's ``proxy_headers=True``
    (enabled when the OAuth gate is active so cookies can pick up
    ``X-Forwarded-Proto``) rewrites ``ws.client.host`` to the
    X-Forwarded-For value, which is the real internet client IP. The
    OAuth gate + single-use ``?ticket=`` is the auth at that point; the
    Host/Origin guard in :func:`_ws_host_origin_is_allowed` is what
    blocks DNS-rebinding here, not the peer IP.
    rz   FTr9   )rp   rq   rr   r+  rS   _LOOPBACK_HOSTS)r  client_hosts     rE   _ws_client_is_allowedr  2  sK     sy/511 t$&I5")..2K t/))rG   c                 d   t          t          j        dd          }|sdS | j                            dd          }t          ||          sdS | j                            dd          }|sdS t          j                            |          }|j	        dvs|j
        sdS t          |j
        |          S )	a  Apply the dashboard Host/Origin guard to WebSocket upgrades.

    FastAPI HTTP middleware does not run for WebSocket routes, so the
    DNS-rebinding Host check used for normal dashboard HTTP requests must be
    repeated here before accepting the upgrade.  Browsers also send an Origin
    header on WebSocket handshakes; when present, require it to target the
    same bound dashboard host.
    rX   NTrS   r9   Forigin>   rk   https)rp   rq   rr   r;   r<   rj   r  r  urlparseschemenetloc)r  rX   rW   r  r  s        rE   _ws_host_origin_is_allowedr  I  s     L$77J t*..,,K[*55 uZ^^Hb))F t\""6**F}---V]-uV]J777rG   c                 >    t          |           ot          |           S )zDReturn True when the WebSocket upgrade matches dashboard boundaries.)r  r  )r  s    rE   _ws_request_is_allowedr   e  s    %b))G.CB.G.GGrG   c                 $   t          t          t          j        dd                    }|r| j                            dd          }|sdS ddlm}m} ddl	m
}m} 	  ||           dS # |$ rI} ||j        t          |          | j        r| j        j        nd| j        j        	           Y d
}~dS d
}~ww xY w| j                            dd          }t%          j        |                                t*                                                    S )a  Validate WS-upgrade auth in either loopback or gated mode.

    Loopback / ``--insecure``: legacy ``?token=<_SESSION_TOKEN>`` query
    parameter, constant-time compared.

    Gated (public bind, no ``--insecure``): ``?ticket=<single-use>`` query
    parameter consumed against the dashboard-auth ticket store. The legacy
    token path is unconditionally rejected in this mode (the SPA bundle
    isn't carrying the token any longer).

    Returns True if the WS should be accepted; callers close with the
    appropriate WS code (4401) on False. Audit-logs the rejection so
    operators can debug "WS keeps closing" issues from the log.
    rz   Fticketr9   r   )
AuditEvent	audit_log)TicketInvalidconsume_ticketT)reasonipr|   Nr  )r   rp   rq   rr   query_paramsr<   hermes_cli.dashboard_auth.auditr  r  $hermes_cli.dashboard_auth.ws_ticketsr  r  WS_TICKET_REJECTEDr   r+  rS   r{   r|   r>   r?   r@   rA   )	r  rz   r  r  r  r  r  r  r  s	            rE   _ws_auth_okr  j  sW    OUCCDDM $$Xr22 	5 	JIIIIIII	
 	
 	
 	
 	
 	
 	
 	


	N6"""4 	 	 	I-3xx&(i7BINNRV[	    55555	 O,,Eu||~~~/D/D/F/FGGGs   A( (B6->B11B6_event_channelsresumesidecar_urlc                    ddl m}m}  ||dz  d          \  }}t          j                                        }|                    dd           |                    dd	           |                    d
d	           | rt          |           \  }}|r|} | |d<   |r||d<   t          |          |rt          |          nd|fS )u  Resolve the argv + cwd + env for the chat PTY.

    Default: whatever ``hermes --tui`` would run.  Tests monkeypatch this
    function to inject a tiny fake command (``cat``, ``sh -c 'printf …'``)
    so nothing has to build Node or the TUI bundle.

    Session resume is propagated via the ``HERMES_TUI_RESUME`` env var —
    matching what ``hermes_cli.main._launch_tui`` does for the CLI path.
    Appending ``--resume <id>`` to argv doesn't work because ``ui-tui`` does
    not parse its argv.

    `sidecar_url` (when set) is forwarded as ``HERMES_TUI_SIDECAR_URL`` so
    the spawned ``tui_gateway.entry`` can mirror dispatcher emits to the
    dashboard's ``/api/pub`` endpoint (see :func:`pub_ws`).
    r   )r  _make_tui_argvzui-tuiF)tui_devNODE_ENV
productionHERMES_TUI_DISABLE_MOUSEry  HERMES_TUI_INLINEHERMES_TUI_RESUMEHERMES_TUI_SIDECAR_URLN)
hermes_cli.mainr  r  r  r  copyr  r  r   r   )	r  r  r  r  argvrz  r~  latest_resume_latest_paths	            rE   _resolve_chat_argvr    s    & =<<<<<<<|h6FFFID#
*//

CNN:|,,, NN-s333NN&,,, *&@&H&H#| 	#"F#)  4(3$%::30s3xxxD#55rG   channelc                    t          t          j        dd          }t          t          j        dd          }|r|sdS d|v r|                    d          sd| d| n| d| }t          t          j        dd          r6d	d
lm}  |dd          }t          j                            || d          }n't          j                            t          | d          }d| d| S )u  ws:// URL the PTY child should publish events to, or None when unbound.

    Loopback / ``--insecure``: uses ``?token=<_SESSION_TOKEN>``.

    Gated mode: mints a single-use ticket via the dashboard-auth ticket
    store (server-side mint, no HTTP round trip — the PTY child is a
    server-spawned process and we trust it). The ticket binds to the
    pseudo-user ``"pty-sidecar"`` so audit logs can distinguish these from
    browser-initiated tickets.

    The single-use lifetime means the PTY child cannot reconnect without a
    new sidecar URL. PTY children open ``/api/pub`` once at startup; if
    reconnect semantics ever become important, this should be upgraded to
    a long-lived process-scoped token.
    rX   N
bound_portr^   rZ   z]:rz   Fr   )mint_ticketzpty-sidecarzserver-internal)user_idr  )r  r   )r  r   zws://z	/api/pub?)
rp   rq   rr   rb   r  r#  r  r  r	  rA   )r   rS   portr  r#  r  qss          rE   _build_sidecar_urlr'    s     39lD11D39lD11D t t#&$;;ts7K7K;TXQaQa[_QaQaFsy/511 SDDDDDD]=NOOO\##v'$J$JKK\##n$Q$QRR(6((B(((rG   r  c                 X  K   t           4 d{V  t          t                              | d                    }ddd          d{V  n# 1 d{V swxY w Y   |D ]K}	 |                    |           d{V  # t
          $ r  t                              d| d           Y Hw xY wdS )z=Fan out one publisher frame to every subscriber on `channel`.Nr   z*broadcast send failed for subscriber on %sT)exc_info)_event_lockr   r  r<   	send_textr  r  r  )r   r  subssubs       rE   _broadcast_eventr.    se      6 6 6 6 6 6 6 6O''44556 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6  _ _	_--(((((((((( 	_ 	_ 	_ LLEwY]L^^^^^	__ _s#   )A
AA!A=='B'&B'c                 t    | j                             dd          }t                              |          r|ndS )z?Return the channel id from the query string or None if invalid.r   r9   N)r	  r<   _VALID_CHANNEL_REr`  r  r   s     rE   _channel_or_close_coder2    s6    o!!)R00G'--g66@77D@rG   z/api/ptyc                    K   t           s                     d           d {V  d S t                     s                     d           d {V  d S t                     s                     d           d {V  d S                                   d {V  t
          s9                     d           d {V                       d           d {V  d S  j                            d          pd }t                     }|rt          |          nd }	 t          ||          \  }}}nS# t          $ rF}                     d| d	           d {V                       d           d {V  Y d }~d S d }~ww xY w	 t          j        |||
          n# t          $ rF}                     d| d	           d {V                       d           d {V  Y d }~d S d }~wt           t"          f$ rF}                     d| d	           d {V                       d           d {V  Y d }~d S d }~ww xY wt%          j                    d fd}t%          j         |                      }		 	                                   d {V }
|
                    d          }|dk    rn|
                    d          }|A|
                    d          }t-          |t.                    r|                    d          nd}|st2                              |          }|r|                                t9          |          k    r]t;          |                    d                    }t;          |                    d                    }                    ||           1                     |           Gn# tB          $ r Y nw xY w|	"                                 	 |	 d {V  n# t$          j#        tH          f$ r Y nw xY w                                 d S # |	"                                 	 |	 d {V  n# t$          j#        tH          f$ r Y nw xY w                                 w xY w)N3  r  1  u   
[31mChat unavailable: the embedded terminal requires a POSIX PTY, which native Windows Python doesn't provide.[0m
[33mInstall Hermes inside WSL2 to use the dashboard's /chat tab — the rest of the dashboard works here.[0m
i  r  )r  r  z
[31mChat unavailable: z[0m
)rz  r~  z
[31mChat failed to start: r7   c                     K   	                      d j        t                     d {V } | d S | st          j        d           d {V  I	                     |            d {V  n# t          $ r Y d S w xY ww)NTr   )rX  r  _PTY_READ_CHUNK_TIMEOUTrV  rO  
send_bytesr  )chunkbridgerc  r  s    rE   pump_pty_to_wszpty_ws.<locals>.pump_pty_to_ws<  s      	..fk#:       E } mA&&&&&&&&&mmE**********   	s   A* *
A87A8Tr   zwebsocket.disconnectbytesr  r  rG   r]   rX  )colsr  r7   N)% _DASHBOARD_EMBEDDED_CHAT_ENABLEDrg   r  r   accept_PTY_BRIDGE_AVAILABLEr+  r	  r<   r2  r'  r  
SystemExitr  spawnr  rs  r  rV  rW  create_taskreceiver   r   r@   
_RESIZE_REr`  endr  r   groupresizer  r   cancelCancelledErrorr  )r  r  r   r  r  rz  r~  r  r<  reader_taskmsgmsg_typer  r  r`  r>  r  r;  rc  s   `                @@rE   pty_wsrP    s     + hhDh!!!!!!!!! r?? hhDh!!!!!!!!!!"%% hhDh!!!!!!!!!
))++ ! llG
 
 	
 	
 	
 	
 	
 	
 	
 hhDh!!!!!!!!! _  **2dF$R((G18B$W---dK+6{SSSc33   llLCLLLMMMMMMMMMhhDh!!!!!!!!!		3C888   llLCLLLMMMMMMMMMhhDh!!!!!!!!!w'   llPPPPQQQQQQQQQhhDh!!!!!!!!!
 #%%D        %nn&6&677K	

$$$$$$CwwvH111'''""C{wwv.8s.C.CLdkk'***  $$S))E C005;;q>>**5;;q>>**4d333LL)	 "     		&	2 	 	 	D	 		&	2 	 	 	D	s   D4 4
F>;E??FF   
I*;G++I?;I  I EO	 P) 	
OP) OP) .O7 7PP)Q9?QQ9Q!Q9 Q!!Q9z/api/wsc                 6  K   t           s|                     d           d {V  d S t          |           s|                     d           d {V  d S t          |           s|                     d           d {V  d S ddlm}  ||            d {V  d S )Nr4  r5  r6  r   )	handle_ws)r@  rg   r  r   tui_gateway.wsrR  )r  rR  s     rE   
gateway_wsrT  z  s      + hhDh!!!!!!!!!r?? hhDh!!!!!!!!!!"%% hhDh!!!!!!!!!((((((
)B--rG   z/api/pubc                   K   t           s|                     d           d {V  d S t          |           s|                     d           d {V  d S t          |           s|                     d           d {V  d S t	          |           }|s|                     d           d {V  d S |                                  d {V  	 	 t          ||                                  d {V            d {V  /# t          $ r Y d S w xY wNr4  r5  r6  i0  )	r@  rg   r  r   r2  rA  r.  receive_textr   r1  s     rE   pub_wsrX    s~     + hhDh!!!!!!!!!r?? hhDh!!!!!!!!!!"%% hhDh!!!!!!!!!$R((G hhDh!!!!!!!!!
))++	E"7"//2C2C,C,C,C,C,C,CDDDDDDDDD	E   s   0C< <
D
	D
z/api/eventsc                   K   t           s|                     d           d {V  d S t          |           s|                     d           d {V  d S t          |           s|                     d           d {V  d S t	          |           }|s|                     d           d {V  d S |                                  d {V  t          4 d {V  t                              |t                                
                    |            d d d           d {V  n# 1 d {V swxY w Y   	 	 |                                  d {V  # t          $ r Y nw xY w	 t          4 d {V  t                              |          }|2|                    |            |st                              |d            d d d           d {V  d S # 1 d {V swxY w Y   d S # t          4 d {V  t                              |          }|2|                    |            |st                              |d            d d d           d {V  w # 1 d {V swxY w Y   w xY wrV  )r@  rg   r  r   r2  rA  r*  r  r  r  r  rW  r   r<   r  r-  )r  r   r,  s      rE   	events_wsrZ    sU     + hhDh!!!!!!!!!r?? hhDh!!!!!!!!!!"%% hhDh!!!!!!!!!$R((G hhDh!!!!!!!!!
))++ ; ; ; ; ; ; ; ;""7CEE2266r:::; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;7	$ //#########		$
     	7 	7 	7 	7 	7 	7 	7 	7"&&w//DR    7#''666	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7; 	7 	7 	7 	7 	7 	7 	7 	7"&&w//DR    7#''666	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7sn   ;D%%
D/2D/7E 
E G& E  G& 1AG
G G&I'4AII'
II'"I#I'r  c                 $    ddl m}  ||           S )uA  Normalise an X-Forwarded-Prefix header value.

    Thin re-export of :func:`hermes_cli.dashboard_auth.prefix.normalise_prefix`
    — the single source of truth lives in the dashboard_auth package so
    the gate middleware, the OAuth routes, the cookie helpers, and the
    SPA mount all agree on validation rules.
    r   )normalise_prefix) hermes_cli.dashboard_auth.prefixr\  )r  r\  s     rE   _normalise_prefixr^    s(     BAAAAAC   rG   applicationc                    t                                           s(|                     d          dt          fd            }dS t           dz  ddt          ffd|                     d	          d
t          dt          fd            }|                     dt          t           dz            d           |                     d          dt          dt          ffd            }dS )a  Mount the built SPA. Falls back to index.html for client-side routing.

    The session token is injected into index.html via a ``<script>`` tag so
    the SPA can authenticate against protected API endpoints without a
    separate (unauthenticated) token-dispensing endpoint.

    When served behind a path-prefix reverse proxy (e.g.
    ``mission-control.tilos.com/hermes/*`` -> local Caddy -> :9119), the
    proxy injects ``X-Forwarded-Prefix: /hermes`` on every request. We
    rewrite the served ``index.html`` so absolute asset URLs (``/assets/...``)
    and the SPA's runtime ``__HERMES_BASE_PATH__`` honour that prefix
    without rebuilding the bundle.
    z/{full_path:path}	full_pathc                 ,   K   t          ddid          S )Nr  z0Frontend not built. Run: cd web && npm run buildr  rL   )r#   )ra  s    rE   no_frontendzmount_spa.<locals>.no_frontend  s)      LM   rG   Nz
index.htmlr9   r   c           	      z                                    }t          rdnd}t          t          t          j        dd                    }|rdnd}|rd| d|  d| d}nd	t           d
| d|  d| d	}| r|                    dd|  d          }|                    dd|  d          }|                    dd|  d          }|                    dd|  d          }|                    dd|  d          }|                    dd|  d          }|                    d| dd          }t          |ddi          S )u#  Return index.html with the session token + base-path injected.

        ``prefix`` is the normalised ``X-Forwarded-Prefix`` (e.g. ``/hermes``)
        or empty string when served at root.

        When the OAuth auth gate is active (``app.state.auth_required``),
        the legacy ``_SESSION_TOKEN`` is NOT injected — the SPA reads
        identity from ``/api/auth/me`` over cookie auth instead.  The
        ``__HERMES_AUTH_REQUIRED__`` flag lets the SPA pick the right
        auth scheme for /api/pty and /api/ws (ticket vs token).
        r  falserz   Fz2<script>window.__HERMES_DASHBOARD_EMBEDDED_CHAT__=z;window.__HERMES_BASE_PATH__="z"";window.__HERMES_AUTH_REQUIRED__=z
;</script>z)<script>window.__HERMES_SESSION_TOKEN__="z,";window.__HERMES_DASHBOARD_EMBEDDED_CHAT__=zhref="/assets/zhref="/assets/zsrc="/assets/zsrc="zhref="/favicon.ico"z/favicon.ico"zhref="/fonts//fonts/zhref="/ds-assets//ds-assets/zsrc="/ds-assets/z</head>r]   Cache-Control#no-store, no-cache, must-revalidate)r;   )	r  r@  r   rp   rq   rr   rA   r   r"   )r   htmlchat_jsgatedgated_jsbootstrap_script_index_paths         rE   _serve_indexzmount_spa.<locals>._serve_index  s    $$&&<I&&'WSY??@@"/66 	=D 06  4<   N  =D 06  4<     	Q << 02K62K2K2KLLD<<1I1I1I1IJJD<< 57U7U7U7UVVD<<1I&1I1I1IJJD<< 35Qf5Q5Q5QRRD<< 24OF4O4O4OPPD||I*:'C'C'CQGG$&KL
 
 
 	
rG   z/assets/{filename}.cssfilenamer6   c                 J  K   t           dz  |  dz  }|                                r>|                                                    t                                                     st	          ddid          S t          |j                            d                    }|                                }|r_dD ]\}|	                    d	| d	| |           }|	                    d
| d
| |           }|	                    d| d| |           }]t          |d          S )Nassets.cssr  z	not foundr  rc  x-forwarded-prefix)rh  z/fonts-terminal/ri  rg  zurl(zurl("zurl('text/css)ro   
media_type)WEB_DISTis_fileresolveis_relative_tor#   r^  r;   r<   r  r   r$   )rs  r6   css_pathr   css	asset_dirs         rE   	serve_csszmount_spa.<locals>.serve_css;  s\     h&H):):)::!! 	I)9)9););)J)J*
 *
 	I  + 6CHHHH"7?#6#67K#L#LMM  "" 	TW T T	kk"4"4"46PV6PY6P6PQQkk"69"6"68T8T8T8TUUkk"5)"5"57Rv7Ry7R7RSS
;;;;rG   z/assetsru  )	directoryrC  c                 n  K   t          |j                            d                    }t          | z  }| ru|                                                    t                                                    r7|                                r#|                                rt          |          S  |          S )Nrw  )	r^  r;   r<   rz  r|  r}  r  r{  r!   )ra  r6   r   	file_pathrr  s       rE   	serve_spazmount_spa.<locals>.serve_spaM  s      "7?#6#67K#L#LMMy(	 	+!!##2283C3C3E3EFF	+   ""	+ !!##		+  	***|F###rG   r9   )rz  r  r<   r   r   mountr%   )r_  rd  r  r  rq  rr  s       @@rE   	mount_spar    s?    ?? 	,	-	-	 	 	 	 
.	-	
 	\)K-
 -
S -
 -
 -
 -
 -
 -
l __-..<# < < < < /.< ix(7J!K!K!KRZ[[[__())$3 $ $ $ $ $ $ *)$ $ $rG   zHermes Tealu/   Classic dark teal — the canonical Hermes look)rD  r  r   zdefault-largezHermes Teal (Large)z1Hermes Teal with bigger fonts and roomier spacingr   Midnightz"Deep blue-violet with cool accentsr   Emberu'   Warm crimson and bronze — forge vibesr   Monou'   Clean grayscale — minimal and focusedr   	Cyberpunku'   Neon green on black — matrix terminalr   u   Roséu-   Soft pink and warm ivory — easy on the eyes      ?default_hexdefault_alphac                    | ||dS t          | t                    r| |dS t          | t                    r|                     d|          }|                     d|          }t          |t                    sdS 	 t	          |          }n# t
          t          f$ r |}Y nw xY w|t          dt          d|                    dS dS )zNormalise a theme layer spec from YAML into `{hex, alpha}` form.

    Accepts shorthand (a bare hex string) or full dict form.  Returns
    ``None`` on garbage input so the caller can fall back to a built-in
    default rather than blowing up.
    Nr  alphar  r  r  r  )	r   r   r   r<   r   r3  r4  r  r  )r   r  r  hex_val	alpha_valalpha_fs         rE   _parse_theme_layerr  m  s     }"];;;% 6}555% 	F))E;//IIg}55	'3'' 	4	$I&&GG:& 	$ 	$ 	$#GGG	$S#c72C2C)D)DEEE4s   ;B B! B!zQsystem-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serifzDui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace15pxz1.550)fontSansfontMonobaseSize
lineHeightletterSpacing_THEME_DEFAULT_TYPOGRAPHYz0.5remcomfortable)radiusdensity_THEME_DEFAULT_LAYOUT>   cardringinputmutedaccentborderpopoverprimarysuccessr  	secondarydestructivecardForegroundmutedForegroundaccentForegroundpopoverForegroundprimaryForegroundsecondaryForegrounddestructiveForeground>   bgherologocrestheadersidebar>	   tabr  pagebadgefooterr  r  backdropprogress>   tiledcockpitstandardi   r  c                     t          | t                    sdS |                     d          }t          |t                    r|                                sdS t          |                     d          t                    r|                     di           ni  t          |                     d          t                    r|                     di           ni d)dt          dt          dt
          d	t          t          t          f         f fd
} |ddd           |ddd           |ddd                               d          p|                     d          pddd}                     d|                     d                    }	 |t          |          nd|d<   n# t          t          f$ r d|d<   Y nw xY wt          |                     d          t                    r|                     di           ni }t          t                    }dD ]E}|                    |          }t          |t                    r|                                r|||<   Ft          |                     d          t                    r|                     di           ni }	t          t                    }
|	                    d          }t          |t                    r|                                r||
d<   |	                    d          }t          |t                    r	|dv r||
d<   |                     di           }i }t          |t                    rQ|                                D ]<\  }}|t          v r.t          |t                    r|                                r|||<   =i }t          |                     d          t                    r|                     di           ni }t          D ]E}|                    |          }t          |t                    r|                                r|||<   F|                    d          }t          |t                    ri }|                                D ]\  }}t          |t                    rj|                    dd                               d!d                                           r.t          |t                    r|                                r|||<   |r||d<   |                     d"          }d}t          |t                    r#|                                r|dt"                   }|                     d#i           }i }t          |t                    r|                                D ]\  }}|t$          vst          |t                    s$i }|                                D ]\  }}t          |t                    r|                    dd                               d!d                                           rUt          |t          t&          t
          f          r3t          |                                          rt          |          ||<   |r|||<   |                     d$          }t          |t                    r|t(          v r|nd%}||                     d&          p||                     d'd           |||
|d(}|r||d<   |r||d<   |||d"<   |r||d#<   |S )*a  Normalise a user theme YAML into the wire format `ThemeProvider`
    expects.  Returns ``None`` if the theme is unusable.

    Accepts both the full schema (palette/typography/layout) and a loose
    form with bare hex strings, so hand-written YAMLs stay friendly.
    NrD  palettecolorsr  r   r  r  r7   c                                          |                      |                     }t          |||          }||n||dS )Nr  )r<   r  )r   r  r  specr  
colors_srcpalette_srcs        rE   _layerz+_normalise_theme_definition.<locals>._layer  sJ    sJNN3$7$788#D+}EE+vv}1]1]]rG   
backgroundz#041c1c	midgroundz#ffe6cb
foregroundz#ffffffr  warmGlowzrgba(255, 189, 56, 0.35))r  r  r  r  noiseOpacityr  
typography)r  r  fontDisplayfontUrlr  r  r  layoutr  r  >   compactspaciousr  colorOverridesru  r   -r9   r   	customCSScomponentStyleslayoutVariantr  r  r   )rD  r  r   r  r  r  r  r  )r   r   r<   r   ra   r   r   r   r3  r4  r  r  r   _THEME_OVERRIDE_KEYS_THEME_NAMED_ASSET_KEYSr   isalnum_THEME_CUSTOM_CSS_MAX_THEME_COMPONENT_BUCKETSr   _THEME_LAYOUT_VARIANTS)!r  rD  r  r  	raw_noisetypo_srcr  r   val
layout_srcr  r  r  overrides_srccolor_overrides
assets_out
assets_srccustom_assets_srccustom_assetscustom_css_val
custom_csscomponent_styles_srccomponent_stylesbucketpropscleanpropr   layout_variant_srclayout_variantrC  r  r  s!                                  @@rE   _normalise_theme_definitionr    sf    dD!! t88FDdC   

 t .88K8KT-R-RZ$((9b)))XZK+5dhhx6H6H$+O+OW(B'''UWJ^ ^C ^c ^% ^$sTWx. ^ ^ ^ ^ ^ ^ ^ f\9c::VKC88f\9c::OOJ//e488J3G3GeKe G 0H0HIII&6?6K%	"2"2"2QTz" & & &"%& .88N8NPT-U-U]txxb)))[]H/00Jl " "ll3c3 	"CIIKK 	"!JsO ,6dhhx6H6H$+O+OW(B'''UWJ'((F^^H%%F&# "6<<>> "!xnnY''G'3 $G/U$U$U#y HH-r22M&(O-&& +%++-- 	+ 	+HC***z#s/C/C*		*'*$ "$J+5dhhx6H6H$+O+OW(B'''UWJ& " "nnS!!c3 	"CIIKK 	"!JsO"x00#T** 1(*)//11 	) 	)HC3$$)KKR((00b99AACC) sC(() IIKK	) &)c" 	1#0Jx  XXk**N $J.#&& <>+?+?+A+A <#$:%:$:;

  88$5r::24&-- 117799 	1 	1MFE555Zt=T=T5$&E${{}} - -etS))-S"--55c2>>FFHH- #53U*;<<- E

((**	- #&e**E$K 1+0 (/22 (#..	3EI_3_3_ 	  '""*dxxr22 ' F  3#2  &%x({ 5$4 !Ms   F$ $F=<F=c                  b   t                      dz  } |                                 sg S g }t          |                     d                    D ]b}	 t	          j        |                    d                    }n# t          $ r Y 8w xY wt          |          }||	                    |           c|S )zScan ~/.hermes/dashboard-themes/*.yaml for user-created themes.

    Returns a list of fully-normalised theme definitions ready to ship
    to the frontend, so the client can apply them without a secondary
    round-trip or a built-in stub.
    zdashboard-themesz*.yamlr  r  )
r   r\  r  globr  r  r  r  r  r  )
themes_dirrC  fr  
normaliseds        rE   _discover_user_themesr  C  s     !""%77J 	FJOOH--.. & &	>!++w+"?"?@@DD 	 	 	H	066
!MM*%%%Ms   (A88
BBz/api/dashboard/themesc                    K   t                      } t          | ddd          }t                      }t                      }g }t          D ]2}|                    |d                    |                    |           3|D ]T}|d         |v r|                    |d         |d         |d         |d           |                    |d                    U||d	S )
an  Return available themes and the currently active one.

    Built-in entries ship name/label/description only (the frontend owns
    their full definitions in `web/src/themes/presets.ts`).  User themes
    from `~/.hermes/dashboard-themes/*.yaml` ship with their full
    normalised definition under `definition`, so the client can apply
    them without a stub.
    r   themer   r   rD  r  r   )rD  r  r   
definition)themesactive)r   r   r  r  _BUILTIN_DASHBOARD_THEMESr  r  )r   r  user_themesr  r  rP  s         rE   get_dashboard_themesr  Y  s       ]]FV['9EEEF'))K55DF&  6a 	 	V9fIwZ]+	
 
 	 	 	 	6///rG   c                       e Zd ZU eed<   dS )ThemeSetBodyrD  Nr   r   rG   rE   r  r  x  s         
IIIIIrG   r  z/api/dashboard/themec                    K   t                      }d|vri |d<   | j        |d         d<   t          |           d| j        dS )z9Set the active dashboard theme (persists to config.yaml).r   r  T)r  r  )r   rD  r   )r   r   s     rE   set_dashboard_themer
  |  sU       ]]F&   {#'9F; +++rG   	api_fielddashboard_dirc                   t          | t                    r|                                 sdS t          |           }|                                rdS 	 ||z                                  }|                                }n# t          t          f$ r Y dS w xY w	 |                    |           n# t          $ r Y dS w xY w| S )u  Validate the manifest's ``api`` field for the plugin loader.

    The web server later imports this file as a Python module via
    ``importlib.util.spec_from_file_location`` (arbitrary code
    execution by design — that's how plugins extend the backend).
    Pre-#29156 the field was used as-is, which meant:

    * An absolute path swallowed the plugin's dashboard directory
      entirely — ``Path('safe/dashboard') / '/tmp/evil.py'`` resolves
      to ``/tmp/evil.py``, so any attacker-controlled manifest could
      point the import at any Python file on disk (GHSA-5qr3-c538-wm9j).
    * A ``../..`` traversal could climb out of the plugin into
      neighbouring directories on the search path.

    Return the original string when the resolved path stays under
    ``dashboard_dir``; return ``None`` (with a warning logged at the
    call site) otherwise so the plugin still loads its static JS/CSS
    but its backend ``api`` is rejected.
    N)
r   r   ra   r   is_absoluter|  r  r  relative_tor4  )r  r  	candidateresolvedr  s        rE   _safe_plugin_api_relpathr    s    ( i%% Y__->-> tYI t!I-6688$$&&\"   ttT""""   tts$   +A> >BBB- -
B;:B;c                  0   g } t                      }ddlm}  |            }t                      dz  df|dz  df|dfg}t	          d          r.|                    t          j                    dz  dz  d	f           |D ]\  }}|                                st          |
                                          D ]}|                                s|d
z  dz  }|                                s5	 t          j        |                    d                    }	|	                    d|j                  }
|
|v r~|                    |
           t%          |	                    d          t&                    r|	                    di           ni }|                    dd|
           |                    dd          d}|                    d          }t%          |t(                    r|                    d          r||d<   t-          |                    d                    rd|d<   |	                    d          }g }t%          |t.                    rd |D             }|	                    d          }|d
z  }t1          ||          }|r|t2                              d|
|           |                     |
|	                    d|
          |	                    dd           |	                    d!d"          |	                    d#d$          |||	                    d%d&          |	                    d'          t-          |          |t)          |          |d(           # t6          $ r'}t2                              d)||           Y d}~d}~ww xY w| S )*ax  Scan plugins/*/dashboard/manifest.json for dashboard extensions.

    Checks three plugin sources (same as hermes_cli.plugins):
    1. User plugins:    ~/.hermes/plugins/<name>/dashboard/manifest.json
    2. Bundled plugins: <repo>/plugins/<name>/dashboard/manifest.json  (memory/, etc.)
    3. Project plugins: ./.hermes/plugins/  (only if HERMES_ENABLE_PROJECT_PLUGINS)
    r   )get_bundled_plugins_dirpluginsuserr   bundledHERMES_ENABLE_PROJECT_PLUGINSz.hermesprojectr   manifest.jsonr  r  rD  r  r|   r  positionrH  )r|   r  overridehiddenTslotsc                 @    g | ]}t          |t                    ||S r   )r   r   )r&  r=  s     rE   rG  z/_discover_dashboard_plugins.<locals>.<listcomp>  s,    NNN1Z35G5GNANQNNNrG   api)r  NzPlugin %s: refusing unsafe api path %r (must be a relative file inside the plugin's dashboard/ directory); backend routes from this plugin will not be mountedr  r   r9   iconPuzzler,   z0.0.0r   zdist/index.jsr  )rD  r  r   r!  r,   r  r  r   r  has_apir  _dir	_api_filez$Bad dashboard plugin manifest %s: %s)r  hermes_cli.pluginsr  r   r   r  r   rz  r\  r  r^  r  r  r  r  r<   rD  r  r   r   r   rb   r   r   r  r  r  r  )r  
seen_namesr  bundled_rootsearch_dirsplugins_rootr  childmanifest_filer  rD  raw_tabtab_infooverride_path	slots_srcr  raw_apir  safe_apir  s                       rE   _discover_dashboard_pluginsr3    s    GeeJ::::::**,,L			Y	&/		 ),	y!K 677 LDHJJ2Y>	JKKK + F Ff""$$ 	L002233 C	 C	E<<>> !K//AM '')) =z-"9"97"9"K"KLLxx
33:%%t$$$
 2<DHHUOOT1R1RZ$((5"---XZ#KK
D

;; 'J > >  !(J 7 7mS11 9m6N6Ns6S6S 9+8HZ(H--.. .)-HX& !HHW--	#%i.. ONN	NNNE ((5// % 33G=YYY x/LL) g    !XXgt44#'88M2#>#> HHVX66#xx	7;;#"!XXg??88E??#H~~$..!)           C]TWXXXCC	H Ns    /AM 7H'M  
N*NN_dashboard_plugins_cacheforce_rescanc                     t           | rt                      a n3t           r,t          d t           D                       rt                      a t           S )Nc              3   f   K   | ],}t          |d                                                     V  -dS )r$  N)r   r\  rE  s     rE   r?  z)_get_dashboard_plugins.<locals>.<genexpr>!  s;      NN4&	??))+++NNNNNNrG   )r4  r3  r  r5  s    rE   _get_dashboard_pluginsr9    sX    '<'#>#@#@  	! ENN5MNNNNN 	E'B'D'D$##rG   z/api/dashboard/pluginsc                     K   t                      } t                      }t          |ddg           pg fd| D             S )z@Return discovered dashboard plugins (excludes user-hidden ones).r   hidden_pluginsr   c                 `    g | ]*}|d          vd |                                 D             +S )rD  c                 D    i | ]\  }}|                     d           ||S r  r  r  s      rE   r-  z4get_dashboard_plugins.<locals>.<listcomp>.<dictcomp>/  s/    ===$!Q1<<+<+<=A===rG   r   )r&  rF  r  s     rE   rG  z)get_dashboard_plugins.<locals>.<listcomp>.  sH       V9F"" 	>=!''))==="""rG   )r9  r   r   )r  r   r  s     @rE   get_dashboard_pluginsr?  &  se       %&&G]]F6;0@"MMMSQSF      rG   z/api/dashboard/plugins/rescanc                  J   K   t          d          } dt          |           dS )z#Force re-scan of dashboard plugins.Tr8  )r  rZ  )r9  r  )r  s    rE   rescan_dashboard_pluginsrA  5  s,       %$777GW...rG   c                   6    e Zd ZU eed<   dZeed<   dZeed<   dS )_AgentPluginInstallBody
identifierFforceTenableN)r   r   r   r   r   rE  r   rF  r   rG   rE   rC  rC  <  s;         OOOE4FDrG   rC  rF  c                 >    d |                                  D             S )Nc                 D    i | ]\  }}|                     d           ||S r  r  r  s      rE   r-  z-_strip_dashboard_manifest.<locals>.<dictcomp>C  s/    @@@TQall3.?.?@Aq@@@rG   r>  )rF  s    rE   _strip_dashboard_manifestrI  B  s    @@QWWYY@@@@rG   c                    ' ddl m} m}m}m}m}m}m}m} t                      }d |D             }	 |            }
 |            }t                      }t          |ddg           pg }t                      dz                                  }g } |             D ]\  }}}}}||
v rd}n	||v rd	}nd
}t          |          }|	                    |          }|dup|dz  dz                                  }d}	 |                                                    |           d}n# t$          $ r Y nw xY w|dv o"|o t          |                                          }d}d} ||          }|                    d          pg }|rX	 ddlm} |D ]=} |                    |           }!|!r$|!j        r|!                                s	d}d| } n>n# t0          $ r Y nw xY w|                    ||pd|pd||||rt5          |          nd|||o#t          |          dz                                  ||||v d           d |D             ''fd|D             }"g }#	  |            D ]\  }$}%|#                    |$|%d           n# t0          $ r g }#Y nw xY wg }&	  |            D ]\  }$}%|&                    |$|%d           n# t0          $ r g }&Y nw xY w||" |            pd|# |            |&ddS )zJAgent discovery + dashboard manifests + optional provider picker metadata.r   )_discover_all_plugins_get_current_context_engine_get_current_memory_provider_discover_context_engines_discover_memory_providers_get_disabled_set_get_enabled_set_read_manifestc                 :    i | ]}t          |d                    |S rC  )r   rE  s     rE   r-  z'_merged_plugins_hub.<locals>.<dictcomp>T  s$    >>>!C&	NNA>>>rG   r   r;  r   r  r  r  inactiveNr  FT>   gitr  r9   provides_tools)registryzhermes auth z.git)rD  r,   r   r  runtime_statushas_dashboard_manifestdashboard_manifestr|   
can_removecan_update_gitrz   auth_commanduser_hiddenc                     h | ]
}|d          S rC  r   r  s     rE   r(  z&_merged_plugins_hub.<locals>.<setcomp>  s    +++1V9+++rG   c                 \    g | ](}t          |d                    vt          |          )S rC  )r   rI  )r&  rF  agent_namess     rE   rG  z'_merged_plugins_hub.<locals>.<listcomp>  s@       qy>>,, 	"!$$,,,rG   )rD  r   )memory_providermemory_optionscontext_enginecontext_options)r  orphan_dashboard_pluginsr  )hermes_cli.plugins_cmdrK  rL  rM  rN  rO  rP  rQ  rR  r9  r   r   r   r|  r   r<   r  r  r4  r\  tools.registryrW  	get_entrycheck_fnr  r  rI  )(rK  rL  rM  rN  rO  rP  rQ  _read_plugin_manifest_atdashboard_listdash_by_namedisabled_setenabled_setr   r;  plugins_root_resolvedr  rD  r,   r   r  dir_strrX  dir_pathdmhas_dash_manifestunder_user_treecan_remove_updaterz   r]  manifest_datarV  rW  tnamer   orphan_dashboardmemory_providersr  r  context_enginesra  s(                                          @rE   _merged_plugins_hubr|  F  s   	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ,--N>>~>>>L$$&&L""$$K ]]F"6;8HRTUUU[Y[N,..:CCEE!#D7L7L7N7N 6 63g{FG<'NN[  &NN'N==d##dNax+/E/W._._.a.a	**+@AAA"OO 	 	 	D	 o%T/Td7mm>R>R>T>T 	
 00::&**+;<<B 
		333333+  E$..u55E  8H8H (,'<d'<'<    	}"&,",&7CE"O";B"?"?"?4+/UT']]V5K4S4S4U4U*(>1
 
 	 	 	 	  ,+d+++K      .01133 	F 	FGAt##Qt$D$DEEEE	F    -/O0022 	E 	EGAt""Ad#C#CDDDD	E    $4;;==C.99;;.	
 
	 	 	sI   4)D
D+*D+>AG
GG(I; ;J
	J
(J9 9KKz/api/dashboard/plugins/hubc                    K   t          |            	 t                      S # t          $ r2}t                              d|           t          dd          |d}~ww xY w)zIUnified agent plugins + dashboard extension metadata (session protected).zplugins/hub failed: %sr  zFailed to build plugins hub.rK   N)rN   r|  r  r  r  r   )r6   r  s     rE   get_plugins_hubr~    sw       7]"$$$ ] ] ]-s3334RSSSY\\]s   ! 
A-AAz$/api/dashboard/agent-plugins/installc                 T  K   t          |            ddlm}  ||j                                        |j        |j                  }|                    d          s&t          d|                    d          pd          t          d	
           |
                    dd            |S )Nr   )dashboard_install_plugin)rE  rF  r  rm   r  zInstall failed.rK   Tr8  after_install_path)rN   rg  r  rD  ra   rE  rF  r<   r   r9  r-  )r6   r   r  rC  s       rE   post_agent_plugin_installr    s      7??????%%j{  F
 ::d 
::g&&;*;
 
 
 	
 ----
JJ#T***MrG   c                 f    |                      d          } | rd| v sd| v rt          dd          | S )z=Reject path-traversal attempts in plugin name URL parameters.r  z..r|  rm   zInvalid plugin name.rK   )ra   r   rC  s    rE   _validate_plugin_namer    sC    ::c??D L44<<44<<4JKKKKKrG   z//api/dashboard/agent-plugins/{name:path}/enablec                    K   t          |            t          |          }ddlm}  ||d          }|                    d          s&t          d|                    d          pd	          |S )
Nr   "dashboard_set_agent_plugin_enabledTr  r  rm   r  zEnable failed.rK   rN   r  rg  r  r<   r   r6   rD  r  rC  s       rE   post_agent_plugin_enabler    s      7 &&DIIIIII//dCCCF::d ]FJJw4G4G4[K[\\\\MrG   z0/api/dashboard/agent-plugins/{name:path}/disablec                    K   t          |            t          |          }ddlm}  ||d          }|                    d          s&t          d|                    d          pd	          |S )
Nr   r  Fr  r  rm   r  zDisable failed.rK   r  r  s       rE   post_agent_plugin_disabler    s      7 &&DIIIIII//eDDDF::d ^FJJw4G4G4\K\]]]]MrG   z//api/dashboard/agent-plugins/{name:path}/updatec                    K   t          |            t          |          }ddlm}  ||          }|                    d          s&t          d|                    d          pd          t          d	           |S )
Nr   )dashboard_update_user_pluginr  rm   r  zUpdate failed.rK   Tr8  )rN   r  rg  r  r<   r   r9  )r6   rD  r  rC  s       rE   post_agent_plugin_updater          7 &&DCCCCCC))$//F::d ]FJJw4G4G4[K[\\\\----MrG   z(/api/dashboard/agent-plugins/{name:path}c                    K   t          |            t          |          }ddlm}  ||          }|                    d          s&t          d|                    d          pd          t          d	           |S )
Nr   )dashboard_remove_user_pluginr  rm   r  zRemove failed.rK   Tr8  )rN   r  rg  r  r<   r   r9  )r6   rD  r  rC  s       rE   delete_agent_pluginr    r  rG   c                   D    e Zd ZU dZee         ed<   dZee         ed<   dS )_PluginProvidersPutBodyNrb  rd  )r   r   r   rb  r   r   r   rd  r   rG   rE   r  r    s:         %)OXc])))$(NHSM(((((rG   r  z/api/dashboard/plugin-providersc                    K   t          |            ddlm}m} |j         ||j                   |j         ||j                   ddiS )zHPersist memory provider / context engine selection (writes config.yaml).r   )_save_context_engine_save_memory_providerNr  T)rN   rg  r  r  rb  rd  )r6   r   r  r  s       rE   put_plugin_providersr    s       7       
 'd2333&T0111$<rG   c                       e Zd ZU eed<   dS )_PluginVisibilityBodyr  N)r   r   r   r   r   r   rG   rE   r  r  (  r   rG   r  z-/api/dashboard/plugins/{name:path}/visibilityc                   K   t          |            t          |          }t                      }d|vs(t          |                    d          t
                    si |d<   |d                             d          pg }t          |t                    sg }|j        r||vr|                    |           n |j        s||v r|	                    |           ||d         d<   t          |           d||j        dS )zXToggle a plugin's sidebar visibility (persists to config.yaml dashboard.hidden_plugins).r   r;  T)r  rD  r  )rN   r  r   r   r<   r   r   r  r  remover   )r6   rD  r   r   hidden_lists        rE   post_plugin_visibilityr  ,  s      7 &&D]]F&  
6::k3J3JD(Q(Q  {{+//0@AAGRKk4(( { !t;..4    [ !T[004   ,7F;()<<<rG   z1/dashboard-plugins/{plugin_name}/{file_path:path}plugin_namer  c                    K   t                      }t           fd|D             d          }|st          dd          t          |d                   }||z                                  }|                    |                                          st          dd          |                                r|                                st          dd	          |j        	                                }i d
dddddddddddddddddddddddd d!d"d#d$d%d&d'd(d}||vrt          dd	          ||         }t          ||d)d*i+          S ),u[  Serve static assets from a dashboard plugin directory.

    Only serves files from the plugin's ``dashboard/`` subdirectory.
    Path traversal is blocked by checking ``resolve().is_relative_to()``.

    Restricted to a browser-fetchable suffix allowlist (JS/CSS/JSON/HTML/
    SVG/PNG/JPG/WOFF). The dashboard loads plugin JS via ``<script src>``
    and CSS via ``<link href>``, neither of which can attach a custom
    auth header — so this route stays unauthenticated to keep the SPA
    working. But user-installed plugins ship a ``plugin_api.py``
    backend module that the browser never fetches; it's only imported
    by :func:`_mount_plugin_api_routes` at startup. Without a suffix
    allowlist, anyone on the loopback port can curl the ``.py`` source
    of a private third-party plugin. Reject everything outside the
    browser-asset set.
    c              3   4   K   | ]}|d          k    |V  dS )rD  Nr   )r&  rF  r  s     rE   r?  z%serve_plugin_asset.<locals>.<genexpr>V  s1      BB6k)A)A1)A)A)A)ABBrG   Nr  zPlugin not foundrK   r$  r  zPath traversal blockedzFile not foundz.jszapplication/javascriptz.mjsrv  rx  z.jsonr  z.htmlz	text/htmlz.svgzimage/svg+xmlz.pngz	image/pngz.jpgz
image/jpegz.jpegz.gifz	image/gifz.webpz
image/webpz.icozimage/x-iconz.woff2z
font/woff2z.woffz	font/woffz.ttfzfont/ttfz.otfzfont/otfz.maprj  rk  )ry  r;   )r9  r  r   r   r|  r}  r  r{  suffixre   r!   )	r  r  r  pluginr  r4  r  content_typesry  s	   `        rE   serve_plugin_assetr  C  s*     $ %&&GBBBBgBBBDIIF H4FGGGGvDY''))F  00 N4LMMMM==?? F&.."2"2 F4DEEEE ]  ""F'( 	
 	#	
 	 	 	 	 	 	 	 	 	, 	 	
  	
!" 	"#M& ]""#
 
 
 	
 v&J "GH   rG   c                  @   t                      D ]} |                     d          }|s|                     d          dk    r#t                              d| d         |           Wt	          | d                   }||z  }	 |                                }|                                }|                    |           n?# t          t          t          f$ r% t                              d| d         |           Y w xY w|
                                s$t                              d| d         |           '	 d	| d          }t          j                            ||          }||j        ^t          j                            |          }|t           j        |<   	 |j                            |           n/# t&          $ r" t           j                            |d
            w xY wt+          |dd
          }	|	#t                              d| d                    t,                              |	d| d                     t                              d| d                    U# t&          $ r-}
t                              d| d         |
           Y d
}
~
d
}
~
ww xY wd
S )a  Import and mount backend API routes from plugins that declare them.

    Each plugin's ``api`` field points to a Python file that must expose
    a ``router`` (FastAPI APIRouter).  Routes are mounted under
    ``/api/plugins/<name>/``.

    Backend import is restricted to ``bundled`` and ``user`` sources.
    Project plugins (``./.hermes/plugins/``) ship with the CWD and are
    therefore attacker-controlled in any threat model where the user
    opens a malicious repo; they can extend the dashboard UI via
    static JS/CSS but their Python ``api`` file is never auto-imported
    by the web server.  See GHSA-5qr3-c538-wm9j (#29156).
    r%  r  r  zPlugin %s: ignoring backend api=%s (project plugins may not auto-import Python code; move the plugin to ~/.hermes/plugins/ if you trust it)rD  r$  zKPlugin %s: refusing to import api file outside its dashboard directory (%s)z,Plugin %s declares api=%s but file not foundhermes_dashboard_plugin_Nrouterz,Plugin %s api file has no 'router' attributez/api/plugins/)r   z+Mounted plugin API routes: /api/plugins/%s/z'Failed to load plugin %s API routes: %s)r9  r<   r  r  r   r|  r  r  r  r4  r  	importlibutilspec_from_file_locationloadermodule_from_specr  modulesexec_moduler  r-  rp   rq   include_routerrE  )r  api_file_namer  api_pathresolved_apiresolved_basemodule_namer  modr  r  s              rE   _mount_plugin_api_routesr    s    )** 8Y 8Y

;// 	::h9,,LL6 v	   VF^,, =0	#++--L)1133M$$]3333z2 		 		 		
 LL+,26NH   H		    	LLGPVYfggg	YEVF^EEK>99+xPPD|t{2.11$77C (+CK$'',,,,   T222 S(D11F~KVTZ^\\\v.NfVn.N.NOOOIICVF^TTTT 	Y 	Y 	YLLBF6NTWXXXXXXXX	Yo8Y 8YsO   =B??9C;:C;74I$-.I$F76I$7,G##7I$AI$$
J."JJ)r  rQ   #  )embedded_chatr%  open_browserr  c          	          ddl }|at           |          t          j        _        t          j        j        rddlm}  |            sg }	 ddlm	} |j
        r|                    d|j
                    n# t          $ r Y nw xY w|r,t          d  dd                    |          z   d	z             t          d  d
          t                              d d                    d  |            D                                  n& t"          vr|rt                              d             t          j        _        t          j        _        |rddlt,          j        dk    pWt1          t2          j                            d                    p+t1          t2          j                            d                    }	|	r0 fd}
t9          j        |
d                                           nt                              d           tA          d  d            |!                    t           dt1          t          j        j                             dS )zStart the web UI server.r   Nr@  )r  u     • nous: zRefusing to bind dashboard to u    — the OAuth auth gate engages on non-loopback binds, but no auth providers are registered.

Bundled providers reported these issues:

zS

Or pass --insecure to skip the auth gate (NOT recommended on untrusted networks).u-   — the OAuth auth gate engages on non-loopback binds, but no auth providers are registered and no bundled plugin reported a reason (was the dashboard_auth/nous plugin removed?).
Install a DashboardAuthProvider plugin, or pass --insecure to skip the auth gate (NOT recommended on untrusted networks).zCDashboard binding to %s with OAuth auth gate enabled. Providers: %sr  c              3   $   K   | ]}|j         V  d S r  rC  rE  s     rE   r?  zstart_server.<locals>.<genexpr>#  s$      77af777777rG   uk   Binding to %s with --insecure — the dashboard has no robust authentication. Only use on trusted networks.linuxDISPLAYWAYLAND_DISPLAYc                      	 t          j        d                               d  d            d S # t          $ r Y d S w xY w)Nr  zhttp://r^   )r]  rO  r  r  )rS   r%  
webbrowsers   rE   _openzstart_server.<locals>._openD  s`    JsOOOOO$;d$;$;T$;$;<<<<<    DDs   /4 
AAT)r4  r6  zzSkipping browser-open: no DISPLAY or WAYLAND_DISPLAY detected (headless Linux). Pass --no-open to suppress this detection.u     Hermes Web UI → http://r^   r  )rS   r%  	log_levelproxy_headers)"uvicornr@  rV   rq   rr   rz   r_  rA  plugins.dashboard_authr  LAST_SKIP_REASONr  r  rC  r  r  rE  rR   r  rX   r"  r  r  r'  r   r  r  r<   rI  rJ  rL  debugprintrun)rS   r%  r  rT   r  r  rA  skip_reasons_nous_plugin_has_displayr  r  s   ``         @rE   start_serverr    s    NNN (5$ 2$EECI
y 8
 	=<<<<<~ '	 ')LGGGGGG0  ''F|'DFF         BT B B B
 ii--.;	;         			II77nn&6&677777		
 	
 	
 	
 
*	*	*|	*<=A	
 	
 	
  CICI  LG# 7BJNN9--..7BJNN#45566 	  	       E$777==????JJO  
 

5
5
5t
5
5666 KK$TY39233      s   *A8 8
BBr  )r  )r  r   )r9   r  )rU  r?  )r   r  NNN)r  r  r   )r0   rT  r  )F)rQ   r  TF(F  r  rV  r>   importlib.utilr  r  r   r  r  r  r  r  rI  r]  urllib.parser  urllib.requestpathlibr   r   r   r   r   r   r   r  __file__r  r|  r  r   r|   insertr  r	   r
   hermes_cli.configr   r   r   r   r   r   r   r   r   r   r   r   r   gateway.statusr   r   utilsr   fastapir   r   r   r   r   fastapi.middleware.corsr    fastapi.responsesr!   r"   r#   r$   fastapi.staticfilesr%   pydanticr&   rq  tools.lazy_depsr'   _lazy_ensurer  rC  r  r  rz  	getLoggerr   r  rq   r  rA   r=   r@  r.   r   r   rS  rR  add_middleware&hermes_cli.dashboard_auth.public_pathsr5   r}   r   rF   rN   	frozensetrR   rV   rj   
middlewarert   rx   r~   r   r   r  r   r   r  
_mcl_entryr   r   _k_vr   r   r   r   r  rr  r  r  r4  r3  r  tupler   r!  r<   rj  rl  ro  rp  r  r  r   r  r  r  r  r  r  r  r  r  r  r  r  r  r  r"  r)  r0  r8  putr:  rF  rI  deleterM  rT  r[  rv  rz  r  r  r  r  r  r  Lockr  rp  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  rY  rK  rQ  rM  r  r  r  r  r  r  r  r  r  r  r  r  r  RLockr  r  r  r  r  r  r   r'  r*  r-  r0  r3  r6  r9  r;  r?  rB  rD  r  r  rc  re  rh  ru  rx  r  patchr  r  r  r  r  r  r  r  r  r  r  r  r  r  hermes_cli.pty_bridger  r  rB  _pty_import_errr  compilerG  r8  r0  r  r  r  r   r  r  r  r*  r   r  r'  r.  r2  	websocketrP  rT  rX  rZ  r^  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r
  r  r3  r4  r9  r?  rA  rC  rI  r|  r~  r  r  r  r  r  r  r  r  r  r  r  r   hermes_cli.dashboard_auth.routesr  _dashboard_auth_routerr  r  r   rG   rE   <module>r     s*  	 	 	         				       



                    3 3 3 3 3 3 3 3 3 3 3 3 3 3 tH~~$+33553|CH$$HOOAss<(())) 4 4 4 4 4 4 4 4                              @ ? ? ? ? ? ? ? ! ! ! ! ! !
WWWWWWWWWWWWWW666666TTTTTTTTTTTT//////""""""" 
 
 

::::::%e4444[[[[[[[[[[[[[[::::::XXXXXXXXXXXX333333&&&&&&& 
 
 
j[ ^[ [ [
 
 	

 	'&
$ 3Drz2Q2Q44
,-...W[W[\dWeWeWloyWyw""gNK888 '&r**/  $)   #% DK $ $ $     D%%	    (     
Ag A$ A A A A(DG D D D D D $-9 . . . $ $ y   
Fc F F$ F F F F )!3 )!C )!D )! )! )! )!X $' $ $ $ $L ; ; ; ; ;
 $7 $ $ $ $,X0I X0 V X0 3PPP X0  +z* !X0* 0=== +X04 0 X& 5X0B )777 CX0L 3PPP MX0V =--- WX0` >222   aX0j /x( kX0t 8*** uX0~ 2x( X0H 4--- IX0R 0888 SX0\ <222 ]X0f "A000$ $gX0 4T#s(^+, X X Xx  !# #c3h   (  s s    " % %cN%% 
#tCH~
% % % %P *).99
 56
-/c4S>)* / / /!!## = =FBOB	W}}2<./    9       9   
    9       9       i     bi 455 "#eIBI.F$L$LMMI " " "LLH	*++   ""'uT4$;%67 ' ' ' 'T u u uD ())F2 2 2 2 -(% % 4S>    .0tC))* / / /!T#Y !c !j>N ! ! ! !H*d *s *tCy * * * * 
 !!  "! 
    	%&& # c    '&8 M Mc M M M M M* 	  &E &ES &Ec &E &E &E ! &ERd38n c3h    . F F F 	    !  	H H H
  !  4    	I' I' I'h$sCx    	T T T" 	  (W (W ! (WV 
RW_ RW RW RW RWn1c3h 1DcN 1 1 1 1h Ml M M M M   $ ML M M M M J
M| 
M 
M 
M 
M 
-| -g - - - -R   8C=  3  s        2>0c3h >0 >0 >0 >0B0$sCx. 0 0 0 0B (2D,  ,+A-  -5  (56  %39  ! 6, S67 tCH~s23 6 6 6r4 # 4 T#s(^ 4  4  4  4 n 	  $ $ ! $> 011'< '<w '< '< '< 21'<\ % -/c4S>)* / / /%y~'' 

'              "& ' ' '!&'!D + + + +C s uS$sCx.=P7Q     @Ec @E# @EVY @E^b @E @E @E @EFtCH~    6?.s ?. ?.S#X ?. ?. ?. ?.Dlms lmtCH~ lm lm lm lm^?+S ?+T ?+ ?+ ?+ ?+DP+ P+ P+ P+ P+ P+fF, F, F, F, F, F,R 
455D Dw D D D 65D@    i   
 
566[ [O [g [ [ [ 76[ 	?@@# 3    A@  89923 2 2 2 2 :92 J3 J J J JX 	%&&
 
 
 
 '&
 	788	C 	 	 	 98	 	.//
3 
 
 
 0/
 ())c    *)  # 1+ 1+
1+1+ C=1+ }	1+
 SM1+ 1+ 1+ 1+r    I       I    %Y_&& 5T$sCx.1 5 5 5 56 6%T	2B 6 6 6 6DcN S  cSVh    HSM c    @3 8C=     	 #    " 	"## s Xc]    $# 
< < < < < < < 	"##
 
# 
] 
Xc] 
 
 
 $#
 
)**  x}    +* 
*++ #     ,+ 
+,, 3 #    -, %&&
 
# 
 
 
 
 '&
$    I       I       	    c C 3    	d38n 	 	 	 	%T$sCx.-A % % % %P	.s 	.t 	. 	. 	. 	.D D D D D D C C C /> > > > >< 	-..5# 5 5 5 /.5 
.//2,s 2, 2, 2, 0/2,j !""B B= B B B #"B "##+ + + + $#+" 	$%%, , , , &%, 	$%%C /@    &%    )   
    		D[ 	D 	D 	D 	D 	   H    i    	6 6 6 	J/ J J J J  	  A AC A A A ! AH 	 !!Y YS Y Y Y "!YT 
				DDDDDDDD    I!    l        	 RZ677
 BJ9::  )KKKLL*k *d * * * *.8; 84 8 8 8 88H{ Ht H H H H
)HK )HD )H )H )H )H^ #%c3h $ $ $glnn !!%*6 *6SM*6#*6 49hsmXd^34*6 *6 *6 *6Z!) !) !) !) !) !)H_C _# _$ _ _ _ _Ay AXc] A A A A zhY h4 h h h hl y t    < zY 4    6 }'7	 '7d '7 '7 '7 '7T	!8C= 	!S 	! 	! 	! 	!m$7 m$ m$ m$ m$n }M~'<  NA  B  BzMqrr7Dmnn6Dmnn;Dmnn7Etuu  c  E T\]abegjbj]kTl    2 dV- - 4S>    ) ) tCH~   
    ONN    
 :99 
 " Nd38n N$sCx.9Q N N N Nbt    , 	 !!0 0 "!0<    9    	  ,L , , , ! ," "t "QT " " " "JeT e e e eR ,0 (4. / / /$ $ $$ $ $ $ $ 	!""  #" 	())/ / *)/    i   Ac3h ADcN A A A ApT#s(^ p p p pf 	%&&]7 ] ] ] '&] 
011W <S    21(      
;<<G 3    =< 
<==W C    >= 
;<<	G 	3 	 	 	 =<	 677	w 	c 	 	 	 87	) ) ) ) )i ) ) )
 	*++ 7N    ,+    I    
9::=' = =DY = = = ;:=, 	<==A# A# A A A >=AHFY FY FYT      N M M M M M   ) * * * 		# 	~  ~ ~ ~
~
~ ~ 	~ ~ ~ ~ ~ ~ ~sf   &,D E9?EE9E33E98E9Q, ,-RRb b&%b&} ~	#~~	