
    |+j                    g   U d Z ddlmZm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
 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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/m0Z0m1Z1 dd	l2m3Z3m4Z4m5Z5m6Z6m7Z7m8Z8m9Z9m:Z:m;Z;m<Z<m=Z=m>Z>m?Z?m@Z@mAZAmBZB dd
lCmDZDmEZE ddlFmGZG 	 ddlHmIZImJZJmKZKmLZLmMZM ddlNmOZO ddlPmQZQmRZRmSZSmTZT ddlUmVZV ddlWmXZX ni# eY$ ra 	 ddlZm[Z\  e\dd           ddlHmIZImJZJmKZKmLZLmMZM ddlNmOZO ddlPmQZQmRZRmSZSmTZT ddlUmVZV ddlWmXZX n# e]$ r  e^dej_         d          w xY wY nw xY wdej`        v r e ej`        d                   n e e(          j)        dz  Za ejb        ec          ZdddddeeddfdZfedd!            Zgdd"Zh eId#e0eg$          Ziej`        j                    d%          p ejk        d&          Zld'Zmd(Zng Zoe$ep         eqd)<   d*Zrd+Zseit                    eOd,d-gd-g.           dd/lumvZw d0eKdexfd1Zyd0eKddfd2Zz e{h d3          Z|e{eqd4<   d5e,d6exdexfd7Z}d8e,d9e,dexfd:Z~ei                    d;          d0eKfd<            Zei                    d;          d0eKfd=            Zei                    d;          d0eKfd>            Zi d?d@dAdBdCdDdEdFdBdCdGdHdIg dJdKdLdHdMdNdOgdKdPdHdQg dRdKdSdHdTg dUdKdVdHdWdXdYgdKdZdHd[g d\dKd]dHd^g d_dKd`dHdag dbdKdcdHddg dedKdfdHdgdhdigdKdjdHdkg dldKdmdHdndodpgdKdqdHdrg dsdKdtdHdug dvdKdwdHdxg dydKdHdzg d{dKdHd|d}d~gdKdZe#e,e#e,e"f         f         eqd<   dddddddddddddBdddZe#e,e,f         eqd<   g dZde"de,fdZ	 dde#e,e"f         de,de#e,e#e,e"f         f         fdZ ee4          ZedD         Zi Ze#e,e#e,e"f         f         eqd<   e                                D ]\  ZZeee<   ed?k    reedD<   eZ G d deX          Z G d deX          Z G d deX          Z G d deX          Z G d deX          Z G d deX          Z G d deX          Z G d deX          Z G d deX          Z G d deX          Z G d deX          ZddddddddddddddZe#e,e,f         eqd<   dZde,de,fdZ G d deX          Zde,d?e,dee,e,f         fdZ	 dddde,d?e,de,de,defdZ ej        d          Z	  ep ej        dd                    Zn3# eef$ r) ed                    d ej        d                     dZY nw xY wdeexedz  f         fdZdddddddddȜZdZdZdZ e d˦          Z ed(̦           G d̈́ dΦ                      Zh dϣZdZdZdZi ddԓdd֓ddؓddړddܓddޓddddԓddؓddddddddddddddddddddddddddddddddZi 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ddiZde,de fdZde de,fdZdedexfdZde dee ej        f         fdZde de,dz  fdZde,fdZde,de,fd Zdee          fd!Zeij                    d"          de,fd#            Zdd$de d%exde fd&Zde,e z  de fd'Zd(e d)e dexfd*Zde,dz  de,fd+Zd0eKdexfd,Zdexfd-Zd(d.d0eKd/exdefd0Zdd1de,dz  d0eKd2exdeee e,f         fd3Zǐd4ede#e,e"f         fd5ZȐd4ed)e de#e,e"f         fd6Zɐd7e,deee,f         fd8Zeij                    d9          dd0eKde%e,         fd:            Zeij                    d;          d0eKde,fd<            Zei                    d=          d>ed0eKfd?            Zei                    d@          d>ed0eKfdA            Zei                    d9          d>ed0eKfdB            Zeij                    dC          de,fdD            Zeij                    dE          de,fdF            Zeij                    dG          de,fdH            Zeij                    dI          de,fdJ            Zeij                    dK          dL             Zeij                    dM          dN             ZאdOZؐdPe,dQe,de%ee         fdRZِdSe,dTe,dPe,dQe,de#e,e,f         f
dUZeij                    dV          dW             Zeij                    dX          dY             Z G dZ d[eX          Zei                    d\          d]efd^            Zei                    d_          d`             Zdae,fdbZeij                    dc          dd             Zei                    de          df             Zei                    dg          dh             Zei                    di          dj             Z G dk dleX          Zei                    dm          dd]edz  fdn            Z e8            doz  Ze eqdp<   i dqdrdsdtdudvdwdxdydzd{d|d}d~ddddddddddddddddddddZe#e,e,f         eqd<   i Ze#e,ej        f         eqd<   i Ze#e,e#e,e"f         f         eqd<   d de,de,deeddfdZde$e,         de,dej        fdZde deede$e,         fdZde&ej        exf         fdZdee,e"f         fdZei                    d          d             Zei                    d          d             Zddeede$e#e,e"f                  fdZeij                    d          ddexfd            Zei                    d          d>efd            Z G d deX          Zde#e,e"f         de,fdZeij                    d          d             Zei                    d          d>efd            Zeij                    d          dde,deefd            Zeij                    d          	 	 	 	 	 	 	 ddeedeedeede,de,de,de,fd            Zeij                    dæ          	 	 	 	 	 	 	 	 ddeedeedeede,de,de,de,de,fdǄ            Zeij                    dȦ          dde,deefdʄ            Zde#e,e"f         de#e,e"f         fd˄Zeij                    d̦          dde%e,         fd̈́            Z eij                    dΦ          dτ             Zeij                    dЦ          dф             Zdddddi dҜZeeqd<   eij                    dԦ          dde%e,         fdՄ            Zd֐Ze&e,df         eqd<   eij                    d٦          dde%e,         fdڄ            Zeij                    dۦ          dde,fd܄            Zeij                    dݦ          dde%e,         fdބ            Zei                    dߦ          dd]ede%e,         fd            Z		 dde,de,d?e,de,de,de,fdZ
de#e,e"f         de#e,e"f         fdZei                    d̦          dd]ede%e,         fd            Zeij                    d          dde%e,         fd            Zei                    d          dd]ede%e,         fd            ZdddddZee,ee,e,f         f         eqd<   ddde$e,         fdZei                    d          d]ed0eKfd            Zei                    d          dd]ede%e,         fd            Zei                    d          	 dd]ed0eKde%e,         fd            Z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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-d-dd.d/d0d1d2d2dd3d4d5d6d7d8dd9d:d;d<d=d>dd?d@dAdBdCdDddEdFdGdHdIdJddKdLdMdNdOdPddQdRdSdTdUdVddWdXdddYdZd[d'd\ddd]d^d_d`dddaZee,ee,e"f         f         eqdb<   dcZee,df         eqdd<   i dedfdgddhdidjdkdldmdndodldpdqdrd(dsdtdudvd(dsdwdxdydldzd{d|dld}d~dd(dddddldddd(dddddlddddldddd,dhdddd(dddddldddd(dddddlddd(ddddldddlddd(ddddlddd(ddddldddlddd(dddd(dddd(ddddlddd(ddZee,ee,e"f         f         eqd<   deee,e"f         df         fdZde{e,         fdZ e{h d          Zde,dee,df         fdZde,dee,df         fdZde,dee,e"f         de"dz  dee,df         fdZ	 dde,de"dz  dee,e"f         fdZde,dee,e"f         dz  fdZde,dee,e"f         fdZde,fdÄZ dee,e"f         dee,e,f         dedz  dee,e"f         fdǄZ!de,dexddfdɄZ"dʐZ#de0 Z$ ej%        d̦          Z&e G d̈́ dΦ                      Z'i Z(ee,e'f         eqd<    ej)                    Z*de,fdЄZ+de,depfdфZ,dd҄Z-de"de,dz  fdӄZ.de,de,de,fdքZ/dddלde,de,d]ee,e"f         dz  de,dz  dee,e"f         f
dڄZ0dddלde,de,d]ee,e"f         dz  de,dz  dee,e"f         f
dۄZ1ei                    dܦ          d]efd݄            Z2eij                    dަ          de,fd            Z3dee,e"f         fdZ4ei                    d          de,d]efd            Z5ei                    dަ          de,fd            Z6eij                    d          d             Z7ei                    d          de,d]efd            Z8ei                    d          de,fd            Z9dde%e,         deede,fdZ:de#e,e"f         fdZ;de#e,e"f         fdZ<ddddddddddddddddddddddd dddddddddddddd	d
dde;dddddde<dfZ=ee#e,e"f         df         eqd<   de,de#e,e"f         fdZ>eij                    d          d             Z?ei                    d          de,d0eKfd            Z@dZAi ZBe#e,e#e,e"f         f         eqd<    ejC                    ZD	 ddlEmFZGmHZImJZKmLZMmNZO d(ZPn# eY$ r dZPY nw xY wdZQddZRde,de,dee,e#e,e"f         f         fdZSde,d e,d!eeddfd"ZTde#e,e"f         fd#ZUd$e,d%e,de#e,e"f         fd&ZVde,de#e,e"f         fd'ZWd(ZXde#e,e"f         fd)ZYd$e,ddfd*ZZde,d e,de,d+e,ddf
d,Z[d$e,ddfd-Z\d$e,ddfd.Z]d$e,ddfd/Z^ei                    d0          de,d0eKfd1            Z_ G d2 d3eX          Z`ei                    d4          de,d]e`d0eKfd5            Zaeij                    d6          de,d$e,fd7            Zbei                    d8          d$e,d0eKfd9            Zcd$e,fd:Zd G d; d<eX          Zeei                    d=          d]eefd>            Zfeij                    d?          d@             Zgei                    dA          dB             Zheij                    dC          dD             Zide%e,         fdEZjeij                    dF          dd$e,de%e,         fdG            Zkeij                    dH          d$e,fdI            Zleij                    dJ          dd$e,de%e,         fdK            Zmei                    dF          dd$e,de%e,         fdL            Zn G dM dNeX          Zoeip                    dF          d$e,d]eofdO            Zqeij                    dP          d$e,fdQ            Zr G dR dSeX          Zsei                    dT          d]esfdU            Zteij                    dV          	 	 	 	 	 d	dXe,deedYe%e,         dZe%e,         d[e%e,         f
d\            Zu G d] d^eX          Zv G d_ d`eX          Zw ej)                    Zxde$e#e,e"f                  fdaZyde%e,         de&e,e f         fdbZzdce#e,e"f         de,dde de#e,e"f         fdeZ{de%e,         dfe,fdgZ|dhe,de%e,         fdiZ}eij                    dj          d
de,fdk            Z~eij                    dl          ddhe,de%e,         fdm            Zeij                    dn          ddhe,de%e,         deefdo            Zei                    dj          dd]evde,fdp            Zeij                    dq          dr             Zei                    dl          ddhe,d]ewde%e,         fds            Zei                    dt          ddhe,de%e,         fdu            Zei                    dv          ddhe,de%e,         fdw            Zei                    dx          ddhe,de%e,         fdy            Zei                    dl          ddhe,de%e,         fdz            Z G d{ d|eX          Zeij                    d}          d~             Zei                    d          dd]ede,fd            Z G d deX          Zde#e,e"f         de#e,e,f         fdZde,de#e,e"f         de#e,e"f         fdZeij                    d          dde%e,         fd            Zei                    d          dd]ede%e,         fd            Zei                    d          dde,de%e,         fd            Zei                    d          dde,de%e,         fd            Z G d deX          Zei                    d          	 dde,d]ede%e,         fd            Zeij                    d          dde%e,         fd            Z G d deX          Zei                    d          dd]ede%e,         fd            Ze                    dd            G d deX          Z G d deX          Zd Zeij                    d          d             Zei                    d          d]efd            Zei                    d          d]efd            Zei                    d          d             Z G d deX          Zde,de#e,e"f         de,de#e,e"f         fdZeij                    d          d             Zei                    d          d             Zei                    d          d]efd            Zei                    d          de,fd            Z G d deX          Zei                    d          de,d]efd            Zei                    d          d             Zei                    d          d             Z G d deX          Zde"deede#e,e"f         fdZeij                    d          d             Zei                    d          d]efd            Zei                    d          de,deefd            Z G dÄ deX          Z G dń deX          Zeij                    dǦ          dȄ             Zei                    dɦ          d]efdʄ            Zei                    d˦          d]efd̄            Zei                    dͦ          d΄             Zei                    dϦ          dЄ             Z G dф deX          Zei                    dӦ          d]efdԄ            Z G dՄ deX          Zei                    dצ          d]efd؄            Zeij                    d٦          dڄ             Z G dۄ deX          Zei                    d٦          d]efd݄            Z G dބ deX          Zei                    d٦          d]efd            Zeij                    d          d             Zei                    d          d             Z G d deX          Zde%e,         de$e,         fdZei                    d          dd]ede%e,         fd            Z G d deX          Zei                    d          dd]eÐde%e,         fd            Z G d deX          Zei                    d          	 dd]e%e         de%e,         fd            ZƐddddddddddd
ZdefdZȐdde%e,         defdZeij                    d          dde%e,         fd             Zeij                    d          	 dde,de,deede%e,         fd            Zeij                    d          dde,fd            Zeij                    d          dde,fd            Z G d d	eX          Z G d
 deX          Z G d deX          Z G d deX          Z G d deX          Z G d deX          Z G d deX          ZԐdde,doe"de"fdZde#e,e"f         fdZde$e#e,e"f                  fdZאde,de fdZؐde,de,fdZِde de,d?e,ddfdZڐde de$d         deefdZېde de$e,         deefd Zeij                    d!          d"             Zei                    d!          d]efd#            Zeij                    d$          d%             Zei                    d$          d]efd&            Zeij                    d'          de,fd(            Zei                    d)          de,fd*            Zeip                    d+          de,d]efd,            Zei                    d+          de,fd-            Zeij                    d.          de,fd/            Zei                    d.          de,d]efd0            Zei                    d1          de,d]efd2            Zei                    d3          de,d]efd4            Zei                    d5          de,d]efd6            Z ej)                    Zede%e,         fd7            Z G d8 d9eX          Zeij                    d:          dde%e,         fd;            Zei                    d<          dd]ede%e,         fd=            Z G d> d?eX          Z G d@ dAeX          ZddBZeij                    dC          dde,de%e,         fdD            Zei                    d:          d]efdE            Zei                    dC          d]efdF            Zeij                    dG          dde%e,         fdH            Z G dI dJeX          Zei                    dK          dde,d]ede%e,         fdL            Zeij                    dM          dde,de%e,         fdN            Z G dO dPeX          Zei                    dQ          	 dde,d]ede%e,         fdR            Z G dS dTeX          Zei                    dU          dde,d]ede%e,         fdV            Z G dW dXeX          Zei                    dY          	 dde,d]ede%e,         fdZ            Z G d[ d\eX          Zeij                    d]          dde%e,         fd^            Z ei                    d]          dd]ede%e,         fd_            Zeij                    d`          ddaeefdb            Zeij                    dc          ddaeefdd            Zej                            de          r6	 ddflmZm	Z	 d(Z
nY# eY$ r dZdZ
 G dg dhe          Z	Y n:w xY w	 ddilmZm	Z	 d(Z
n## eY$ r dZdZ
 G dj dhe          Z	Y nw xY w ej%        dk          ZdlZ ej%        dm          Z e{h dn          Zdodpde%e,         fdqZdodpdexfdrZdodpde%e,         fdsZdodpdexfdtZdodpde%e,         fduZdodpdexfdvZde,fdwZdodpdee%e,         e,f         fdxZdodpdexfdyZ	 	 	 ddze%e,         d{e%e,         de%e,         deee,         e%e,         e%e         f         fd|Zde%e,         fd}Zd~e,de%e,         fdZde"d~e,d>e,ddfdZdoeLde%e,         fdZde,de,fdZei                     d          doeLddfd            Z!ei                     d          doeLddfd            Z"ei                     d          doeLddfd            Z#ei                     d          doeLddfd            Z$de%e,         de,fdZ%deIfdZ&do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gZ'dde"de,depde%e#e,e"f                  fdZ(ddddddZ)e#e,e,f         eqd<   dddZ*e#e,e,f         eqd<   h dZ+h dZ,h dZ-h dZ.dZ/de#e,e"f         de%e#e,e"f                  fdZ0defdZ1eij                    d          d             Z2 G d deX          Z3ei                    d          d]e3fd            Z4dÐZ5 e{h dģ          Z6eij                    dŦ          dƄ             Z7 G dǄ deX          Z8ei                    dŦ          d]e8fdɄ            Z9de"de de%e,         fd̄Z:defd̈́Z;da<e%e         eqd<   ddexdefdЄZ=eij                    dѦ          d҄             Z>eij                    dӦ          dԄ             Z? G dՄ deX          Z@de#e,e"f         de#e,e"f         fd؄ZAde#e,e"f         fdلZBeij                    dڦ          d0eKfdۄ            ZCei                    dܦ          d0eKd]e@fd݄            ZDde,de,fdބZEei                    dߦ          d0eKde,fd            ZFei                    d          d0eKde,fd            ZGei                    d          d0eKde,fd            ZHei                    d          d0eKde,fd            ZI G d deX          ZJei                    d          d0eKd]eJfd            ZK G d deX          ZLei                    d          d0eKde,d]eLfd            ZMeij                    d          de,de,fd            ZNd ZO eO             ddlPmQZR eiS                    eR            e&ei           	 	 	 	 	 dd5e,deedexd6exde,f
dZTd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
    )asynccontextmanagercontextmanagerN)	dataclassdatetimetimezone)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detect_install_methodformat_docker_update_message%recommended_update_command_for_method
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_dist<   
stop_eventzthreading.Eventintervalreturnc                 H   ddl m} t                              d|           |                                 sl	  |dd           n2# t
          $ r%}t                              d|           Y d}~nd}~ww xY w|                     |           |                                 jdS dS )ui  Tick the cron scheduler from inside the desktop dashboard backend.

    The scheduler tick loop normally lives in ``hermes gateway run`` — but the
    desktop app spawns a ``hermes dashboard`` backend, not a gateway, so a cron
    a user creates in the app would never fire. We run a minimal ticker here
    (no live adapters; delivery falls back to the per-platform send path).

    Cross-process safe: ``cron.scheduler.tick`` takes the ``cron/.tick.lock``
    file lock, so this never double-fires alongside a real gateway on the same
    HERMES_HOME — whichever process grabs the lock first wins the tick.
    r   )tickz*Desktop cron ticker started (interval=%ds)F)verbosesynczDesktop cron tick error: %sN)cron.schedulerr9   _loginfois_set	Exceptiondebugwait)r5   r6   	cron_tickes       4/usr/local/lib/hermes-agent/hermes_cli/web_server.py_start_desktop_cron_tickerrF   k   s     100000II:HEEE!! "	9Ie%00000 	9 	9 	9JJ4a88888888	9!!! !! " " " " "s   A 
A4A//A4appr$   c                  K   i | j         _        t          j                    | j         _        d }d }t          j        d          dk    rEt          j                    }t          j	        t          |fdd          }|                                 	 d W V  ||                                 d S d S # ||                                 w w xY w)NHERMES_DESKTOP1Tzdesktop-cron-tickertargetargsdaemonname)stateevent_channelsasyncioLock
event_lockosgetenv	threadingEventThreadrF   startset)rG   	cron_stopcron_threads      rE   	_lifespanr^      s      !CI"<>>CI
 +/I-1K	y!""c))O%%	&-&	
 
 
 	 MMOOOOO ! 9 MMOOOO !s   B- -Cc                     	 | j         j        | j         j        fS # t          $ rD i | j         _        t	          j                    | j         _        | j         j        | j         j        fcY S w xY w)aw  Return (event_channels, event_lock) from app.state.

    Lazily initialises the state if the lifespan hasn't run (e.g. when
    TestClient is constructed without a ``with`` block).  The lifespan
    path is preferred because it guarantees the Lock is created on the
    correct event loop, but the lazy path lets existing non-``with``
    TestClient usages keep working.
    )rP   rQ   rT   AttributeErrorrR   rS   )rG   s    rE   _get_event_statera      sl    >y')=== > > >#%	 &|~~	y')=====>s    AA('A(Hermes Agent)titleversionlifespanHERMES_DASHBOARD_SESSION_TOKEN    zX-Hermes-Session-TokenT_reveal_timestamps      z*^https?://(localhost|127\.0\.0\.1)(:\d+)?$*)allow_origin_regexallow_methodsallow_headers)PUBLIC_API_PATHSrequestc                    | 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authorizationBearer )headersget_SESSION_HEADER_NAMEhmaccompare_digestencode_SESSION_TOKEN)rp   session_headerauthexpecteds       rE   _has_valid_session_tokenr      s     _(()=rBBN $-   t?33D)))Ht{{}}hoo.?.?@@@    c                     t          | j        j        dd          r)t          | j        dd          dS t          dd          t	          |           st          dd          dS )uQ  Authorize a sensitive endpoint, raising 401 if the caller isn't allowed.

    Two auth schemes protect the dashboard, exactly one active per bind:

    * **Loopback / ``--insecure`` mode** (``auth_required`` False): the
      ephemeral ``_SESSION_TOKEN`` is injected into the SPA HTML and echoed
      back via ``X-Hermes-Session-Token`` (or the legacy ``Bearer`` header).
      Validate it here.
    * **Gated / OAuth mode** (``auth_required`` True): ``_SESSION_TOKEN`` is
      NOT injected (the SPA authenticates with a session cookie), so there is
      no token to check. The ``gated_auth_middleware`` has already verified the
      cookie before the request reached this handler — any non-public ``/api/``
      route it lets through carries a verified ``request.state.session``. The
      legacy ``auth_middleware`` likewise short-circuits in this mode. Requiring
      the (absent) token here would 401 every cookie-authenticated request,
      making plugin install/enable/disable and the other ``_require_token``
      endpoints permanently unreachable behind the gate. Defer to the gate.
    auth_requiredFsessionN  Unauthorizedstatus_codedetail)getattrrG   rP   r%   r   )rp   s    rE   _require_tokenr      s|    & w{ /599 D 7=)T22>FNCCCC#G,, DNCCCCD Dr   >   	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.
    )r   )r   r   s     rE   should_require_authr   #  s     --E4DEr   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lowerr   )r   r   hclose	host_onlybound_lcs         rE   _is_accepted_hostr   3  s      u 	A||C ;sB;;!E'
IIII+.!88AHHS!$$Q''	!!I
 &&&t !!H(((111   r   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.
    r   Nr   rr     r   zVInvalid Host header. Dashboard requests must use the hostname the server was bound to.r   content)r   rG   rP   ru   rv   r   r,   )rp   	call_nextr   r   s       rE   host_header_middlewarer   _  s       L$77J o))&"55 j99 		@    7#########r   c                 6   K   ddl m}  || |           d {V S )Nr   )gated_auth_middleware)$hermes_cli.dashboard_auth.middlewarer   )rp   r   r   s      rE   _dashboard_auth_gater     s=      JJJJJJ&&w	:::::::::r   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.r   FNz/api/r   r   r   r   )	r   rG   rP   urlpathr   _PUBLIC_API_PATHSr   r,   )rp   r   r   s      rE   auth_middlewarer     s       w{ /599 (Yw''''''''';Dw D0A$A$A'00 	!>2    7#########r   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 provider)r   groqr   xair   zstt.elevenlabs.model_idzElevenLabs Scribe model	scribe_v2	scribe_v1z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))rr   autor   flexz(Reasoning effort for delegated subagents)rr   lowmediumhighzWhen the chat app / gateway updates Hermes (no terminal prompt), what to do with uncommitted local source edits. 'stash' keeps them and re-applies them after the update; 'discard' throws them away. Terminal updates always ask, regardless of this setting.stashdiscard)zdelegation.reasoning_effortz%updates.non_interactive_local_changes_SCHEMA_OVERRIDESsecurityagentdisplaydiscord)privacycontextskillscronnetworkcheckpoints	approvalshuman_delay	dashboardcode_executionprompt_cachinggoalsupdates
onboarding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8r   rr   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  replacerc   r   r  rv   )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Mr   _ordered_schemac                   4    e Zd ZU eed<   dZee         ed<   dS )ConfigUpdater  Nprofile)__name__
__module____qualname__r  __annotations__r1  r   str r   rE   r0  r0  r  s/         LLL!GXc]!!!!!r   r0  c                   L    e Zd ZU eed<   eed<   dZee         ed<   dZeed<   dS )EnvVarUpdater+  r  Nr1  rr   api_key)r2  r3  r4  r6  r5  r1  r   r:  r7  r   rE   r9  r9  w  sJ         	HHHJJJ!GXc]!!! GSr   r9  c                   4    e Zd ZU eed<   dZee         ed<   dS )EnvVarDeleter+  Nr1  r2  r3  r4  r6  r5  r1  r   r7  r   rE   r<  r<    /         	HHH!GXc]!!!!!r   r<  c                   4    e Zd ZU eed<   dZee         ed<   dS )EnvVarRevealr+  Nr1  r=  r7  r   rE   r@  r@    r>  r   r@  c                   b    e Zd ZU dZee         ed<   i Zee	e	f         ed<   g Z
ee	         ed<   dS )MessagingPlatformUpdateNenabledenv	clear_env)r2  r3  r4  rC  r   r  r5  rD  r   r6  rE  r   r7  r   rE   rB  rB    sR         "GXd^"""Cc3hItCyr   rB  c                   *    e Zd ZU dZee         ed<   dS )TelegramOnboardingStartNbot_name)r2  r3  r4  rH  r   r6  r5  r7  r   rE   rG  rG    s&         "Hhsm"""""r   rG  c                   &    e Zd ZU ee         ed<   dS )TelegramOnboardingApplyallowed_user_idsNr2  r3  r4  r   r6  r5  r7  r   rE   rJ  rJ    s"         3ir   rJ  c                   4    e Zd ZU eed<   dZee         ed<   dS )AudioTranscriptionRequestdata_urlN	mime_type)r2  r3  r4  r6  r5  rP  r   r7  r   rE   rN  rN    s/         MMM#Ix}#####r   rN  c                   2    e Zd ZU eed<   eed<   dZeed<   dS )ManagedFileUploadr   rO  T	overwriteN)r2  r3  r4  r6  r5  rS  r  r7  r   rE   rR  rR    s4         
IIIMMMItr   rR  c                       e Zd ZU eed<   dS )ManagedDirectoryCreater   Nr2  r3  r4  r6  r5  r7  r   rE   rU  rU             
IIIIIr   rU  c                   (    e Zd ZU eed<   dZeed<   dS )ManagedFileDeleter   F	recursiveN)r2  r3  r4  r6  r5  rZ  r  r7  r   rE   rY  rY    s+         
IIIItr   rY  z.aac.flacz.m4a.mp3z.mp4.ogg.wav.webm)z	audio/aac
audio/flacz	audio/m4az	audio/mp3	audio/mp4
audio/mpeg	audio/ogg	audio/wavz
audio/wave
audio/webmzaudio/x-m4azaudio/x-wav
video/webm_AUDIO_MIME_EXTENSIONSi  rP  c                     | pd                     dd          d                                                                         }t                              |d          S )Nrr   ;r   r   r_  )r&  r   r   rg  rv   )rP  
normalizeds     rE   _audio_extension_for_mimerk    sN    /r((a00399;;AACCJ!%%j':::r   c                       e Zd ZU dZeed<   eed<   eed<   dZeed<   dZeed<   dZeed<   d	Z	e
ed
<   dZe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   rr   taskbase_urlr:  Fconfirm_expensive_modelNr1  )r2  r3  r4  __doc__r6  r5  rp  rq  r:  rr  r  r1  r   r7  r   rE   rm  rm    s           JJJMMMJJJD#NNN Hc GS$)T)))!GXc]!!!!!r   rm  ro  c                    ddl m}m} ddlm} | pd                                }|pd                                } ||          }||vrd|v r	 t                                          di           }t          |t                    rIt          |                    dd          pd                                                                          nd}	n# t          $ r d}	Y nw xY wddl m}
 |	r ||	          |
v r ||	          }|	}nd	}d	}||v rU|                    d
          s@	  |||          }|r|}n.# t          $ r! t                              d||d           Y nw xY w||fS )u  Normalize a main-slot (provider, model) pair before persisting.

    The Models page has two assignment paths and only one of them was safe:

    - The "Change" picker sends a real Hermes provider slug — fine.
    - The per-card "Use as → Main model" menu sends ``entry.provider``
      from the analytics rows, falling back to the model's VENDOR prefix
      (``modelVendor("anthropic/claude-opus-4.6") == "anthropic"``) when
      the session row has no ``billing_provider`` (older sessions, NULL
      rows).  That wrote ``provider: anthropic`` +
      ``default: anthropic/claude-opus-4.6`` to config — a vendor-prefixed
      OpenRouter slug on the NATIVE Anthropic provider.  New sessions then
      400 against api.anthropic.com ("model: anthropic/claude-opus-4.6 not
      found") and the user reads it as "changing models does nothing".

    Two repairs, both at this single chokepoint so every caller inherits:

    1. Vendor-name → Hermes-provider mapping: when the provider string is
       not a known Hermes provider/alias (e.g. ``moonshotai``, ``x-ai`` is
       known but ``poolside`` isn't) but the model is a vendor-prefixed
       aggregator slug, keep the user's CURRENT aggregator if they're on
       one, else fall back to openrouter.
    2. Model-format normalization for the resolved provider via
       ``normalize_model_for_provider`` (e.g. ``anthropic/claude-opus-4.6``
       on native anthropic → ``claude-opus-4-6``).
    r   )_KNOWN_PROVIDER_NAMESnormalize_provider)normalize_model_for_providerrr   /r   ro  )_AGGREGATOR_PROVIDERS
openrouterr   z$model normalization failed for %s/%sTexc_info)hermes_cli.modelsru  rv  hermes_cli.model_normalizerw  r   r   rv   r  r  r6  r   r@   ry  r   r=   rA   )ro  r   ru  rv  rw  prov_inmodel_in	canonicalcur_cfgcur_providerry  normalized_models               rE    _normalize_main_model_assignmentr    s   6 LKKKKKKKGGGGGG~2$$&&G""$$H""7++I---#//	!mm''44G gt,,5GKK
B//5266<<>>DDFFF24 L  	 	 	LLL	;;;;;; 	#..|<<@UUU**<88I"GG$I"G ))))2F2Fx2P2P)	a;;HiPP ,+ 	a 	a 	aJJ=w[_J`````	a Hs%   BC C! C!%D6 6(E! E!	model_cfgr
   rq  r:  c                    t          | t                    si } t          |                     d          pd                                                                          }|                                                                }|| d<   || d<   |                                r|                                | d<   n |                     d          r||k    rd| d<   |                                r|                                | d<   n |                     d          r||k    rd| d<   |                     dd           | S )u  Apply a main-slot model assignment to a ``model`` config dict in place.

    Sets ``provider``/``default``, then reconciles ``base_url``:

    - An explicitly supplied ``base_url`` is always persisted (covers
      ``custom``/local endpoints and any provider whose key is bound to a
      non-default host).
    - Otherwise, a stale ``base_url`` is cleared ONLY when switching to a
      *different* provider — that URL belonged to the old provider. When the
      provider is unchanged and no new URL is supplied, the existing
      ``base_url`` is preserved. This keeps a user's custom endpoint (e.g. a
      Xiaomi MiMo Token Plan host, ``https://token-plan-*.xiaomimimo.com/v1``)
      alive when they merely re-pick a model under the same provider — picking
      a model previously wiped it, forcing the registry default and breaking
      Token Plan keys.

    The runtime resolver reads ``model.base_url`` from config (it ignores
    ``OPENAI_BASE_URL``) and only honors it when the configured provider matches
    and the pool entry is on the registry default, so preserving it here is what
    lets the override actually route. The hardcoded ``context_length`` override
    is always dropped since the new model may have a different context window.

    Returns the same dict (coerced to a fresh dict if the input wasn't one) so
    callers can assign it straight back onto the model config.
    ro  rr   r   rq  r:  context_lengthN)r  r  r6  rv   r   r   pop)r  ro  r   rq  r:  prev_providernew_providers          rE   _apply_main_model_assignmentr  %  s=   8 i&& 		j117R88>>@@FFHHM>>##))++L$Ij Ii~~ # ( 0 0	*	z	"	" #|}'D'D !#	*
 }} "&}}	)	y	!	! "lm&C&C!	)MM"D)))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.
    )FNrx  z/health/detailedNz/healthGET)methodtimeout   T)_GATEWAY_HEALTH_URLrstripendswithlenurllibrp   r&   urlopen_GATEWAY_HEALTH_TIMEOUTstatusjsonloadsreadr@   )baser   reqrespbodys        rE   _probe_gateway_healthr  l  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=	image/png
image/jpeg	image/gif
image/webpimage/svg+xmlz	image/bmpimage/x-icon).png.jpg.jpeg.gif.webp.svg.bmp.icoHERMES_DASHBOARD_FILES_ROOTi  @z	/opt/data)frozenc                   4    e Zd ZU eed<   edz  ed<   eed<   dS )ManagedFilesPolicydefault_pathNlocked_rootcan_change_path)r2  r3  r4  r	   r5  r  r7  r   rE   r  r    s:         r   r  >   .hg.svn.next.venv.cache.turbodistvenvbuildrL   __pycache__node_modules.giti   i   i   z.ccz.confiniz.cppcpp.csscssz.csvcsvz.gogoz.graphqlgraphqlz.hz.hpp.htmlhtmlz.javajava.js
javascript.jsonr  z.jsxjsxz.ktkotlinz.lualuaz.mdmarkdownpythonrubyrustshellsqlxmltoml
typescripttsxtextyaml).mjsz.pyz.rbz.rsz.shz.sqlr  z.tomlz.tsz.tsxz.txtz.xmlz.yamlz.ymlz.zshz.avizvideo/x-msvideor  r`  r  r  r  ra  z.mkvzvideo/x-matroskaz.movzvideo/quicktimerb  z	video/mp4rc  .opuszaudio/ogg; codecs=opusr  r  rd  rf  r  raw_pathc                    t          | pd                                          }|st          dd          d|v rt          dd          	 |                                                    d          rZt
          j                            |          }|j        r|j        dvrt          t
          j
                            |j                  }t          |                                          }|                                st          j                    |z  }|                    d	
          S # t$          t&          t          f$ r t          dd          w xY w)Nrr   r   zPath is requiredr    Invalid pathzfile:>   rr   r   Fstrict)r6  r   r%   r   r   r  parseurlparsenetloc
ValueErrorrp   url2pathnamer   r	   
expanduseris_absolutecwdresolveOSErrorRuntimeError)r  rawparsed	candidates       rE   _fs_pathr    sP   
hn"


#
#
%
%C H4FGGGGs{{NCCCCD99;;!!'** 	;\**3//F} !6G!G!G  .--fk::CII((**	$$&& 	/

Y.I   ...\:. D D DNCCCCDs   C!D/ /)Er   c                     | j                                         }|t          v rt          |         S t          j        t          |                     \  }}|pdS )Napplication/octet-stream)suffixr   _FS_MIME_TYPES	mimetypes
guess_typer6  )r   r  guessedr#  s       rE   _fs_mime_typer    sQ    [  Ff%%%c$ii00JGQ000r   datac                 t    | sdS d| v rdS t          d | D                       }|t          |           z  dk    S )NF    Tc              3   .   K   | ]}|d k     |dvdV  dS )rg   >   	   
      r   Nr7  ).0bytes     rE   	<genexpr>z#_fs_looks_binary.<locals>.<genexpr>  s4      QQ44"99[9P9PQ9P9P9P9PQQr   gQ?)sumr  )r  
suspiciouss     rE   _fs_looks_binaryr    sO     u}}tQQ4QQQQQJD		!D((r   c                    t          t          |                     }	 |                                }n# t          $ r t	          dd          t
          $ r t	          dd          t          $ r t	          dd          t          $ r%}t	          dt          |          pd          d }~ww xY wt          j        |j	                  rt	          dd          t          j
        |j	                  st	          dd	          ||fS )
N  File not foundr     File is not readabler   r  zPath points to a directoryzOnly regular files can be read)r  r6  statFileNotFoundErrorr%   NotADirectoryErrorPermissionErrorr  S_ISDIRst_modeS_ISREG)r   rL   stexcs       rE   _fs_regular_filer    sB   c$ii  F	P[[]] F F F4DEEEE F F F4DEEEE L L L4JKKKK P P PCHH4NOOOOP|BJ R4PQQQQ<
## V4TUUUU2:s   3 AB0 B++B0rZ   c                     | }t          d          D ]P}	 |dz                                  rt          |          c S n# t          $ r Y  d S w xY w|j        }||k    r d S |}Qd S )N2   r  )rangeexistsr6  r  parent)rZ   	directoryr#  r#  s       rE   _fs_find_git_rootr%  1  s    I2YY 	 		F"**,, &9~~%%%& 	 	 	444	!Y44		4s   %>
AAc                      t                                          d          pi } t          |                     d          p t          j                            d          pd                                          }|ru|dvrq	 t          |                                                              d          }|	                                rt          |          S n# t          t          f$ r Y nw xY wt          t          j                              S )Nr  r  TERMINAL_CWDrr   >   r  r   r!  Fr  )r   rv   r6  rU   environr   r	   r  r  is_dirr  r  r  )cfg_terminalr  r  s      rE   _fs_default_cwdr+  @  s    ==$$Z006BL
lu%%M)G)GM2
N
N
T
T
V
VC
 s...	S		,,..66e6DDI!! &9~~%&& 	 	 	D	txzz??s    AC C-,C-r  c                     	 t          j        dd| ddgdddd          }|j        d	k    r|j                                        nd
S # t
          $ r Y d
S w xY w)Ngit-Cbranchz--show-currentT   F)capture_outputr  r  checkr   rr   )
subprocessrun
returncodestdoutr   r@   )r  results     rE   _fs_git_branchr8  M  s    
D#x)9:
 
 
 )/(9Q(>(>v}""$$$BF   rrs   AA 
AAc                      t                      } | dz  | dz  | dz  g}g }|D ]A}	 |                    |                                           +# t          t          f$ r Y >w xY w|S )u  Directories ``GET /api/media`` is allowed to read from.

    Confined to where the agent and attach pipeline actually write media on the
    gateway host — its images dir and cache subtree. This stops an authenticated
    client from reading image-extension files anywhere on disk (e.g. a renamed
    key or a screenshot outside the cache) merely because the suffix passes the
    allowlist.
    imagesscreenshotscache)r   appendr  r  r  )homerootsoutroots       rE   _media_serve_rootsrB  [  s     DH_d]2D7NCEC  	JJt||~~&&&&& 	 	 	H	Js   'AA A z
/api/mediac                   K   	 t          |                                                                           n&# t          t          f$ r t          dd          w xY wj                                        t          vrt          dd          t                      }t          fd|D                       st          dd                                          st          d	d
                                          j        t          k    rt          dd          t          j                                                                      d          }ddt          j                                                  d| iS )u  Return a gateway-local image file as a base64 data URL.

    Lets remote clients (the desktop app over the network, or the web dashboard
    in a browser) display images the agent wrote to *this* machine's filesystem
    — they can't read the gateway's local disk directly.

    Auth-gated by the session token like every other /api route. Restricted to
    an image-extension allowlist, a size cap, AND the gateway's own media roots
    (resolved, symlink-safe) so it can't be used to read arbitrary files.
    r   r  r   i  zUnsupported media typec              3   6   K   | ]}|k    p|j         v V  d S Nparents)r
  rA  rL   s     rE   r  zget_media.<locals>.<genexpr>  s3      JJDv~7!7JJJJJJr   r  zPath outside media rootsr  r    File too largeasciirO  data:;base64,)r	   r  r  r  r  r%   r  r   _MEDIA_CONTENT_TYPESrB  anyis_filer  st_size_MEDIA_MAX_BYTESbase64	b64encode
read_bytesdecode)r   r?  encodedrL   s      @rE   	get_mediarW  o  s     Dd&&((0022\" D D DNCCCCD }$8884LMMMM  EJJJJEJJJJJ P4NOOOO>> F4DEEEE{{}}///4DEEEEv002233::7CCG^ 4V]5H5H5J5J K^^U\^^__s	   39 #Arequire_existsrY  c                    	 |                                                      |          S # t          $ r |rt          dd           t          t
          f$ r t          dd          w xY w)Nr  r  Path not foundr   r   r  )r  r  r  r%   r  r  )r   rY  s     rE   _canonical_pathr\    s    D  (((???    	JC8HIIII\" D D DNCCCCDs
   '* A A*c                 F   t          |                                           }	 |                    dd           |                                }n-# t          t
          f$ r}t          dd|           d }~ww xY w|                                st          dd          |S )NTrG  exist_ok  z#Managed files root is unavailable: r   z%Managed files root is not a directory)r	   r  mkdirr  r  r  r%   r)  )r  rA  resolvedr  s       rE   _ensure_managed_rootrc    s    >>$$&&Da

4$
///<<>>\" a a a4_Z]4_4_````a?? ]4[\\\\Os   +A A9 A44A9rA  rL   c                      || k    p| |j         v S rE  rF  )rA  rL   s     rE   _path_is_underre    s    T>3TV^33r   c                 v    t          | pd                                          }d|v rt          dd          |S )Nrr   r  r   r  r   )r6  r   r%   )r  r  s     rE   
_path_textrg    s@    x~2$$&&D~~NCCCCKr   c                     t          | j        j        dd          rdS | j        j        pd                                }| j        r| j        j        nd                                }h d}||v p||v S )Nr   Frr   >   rr   r   
testclient
testserverr   r   )r   rG   rP   r   hostnamer   clientr   )rp   r   client_hostlocal_hostss       rE   _local_dashboard_requestro    s}    w{ /599 uK &B--//D*1.@7>&&bGGIIKSSSK;<+"<<r   c                     t           j                            dd                                          } | sdS 	 ddlm}  |                                                                d          }nL# t          t          f$ r8 t          |                                                               d          }Y nw xY w|t          k    S )NHERMES_HOMErr   Fr   )get_default_hermes_rootr  )rU   r(  rv   r   hermes_constantsrr  r  r  r  r  r	   _HOSTED_MANAGED_FILES_ROOT)r  rr  rA  s      rE    _default_hermes_root_is_opt_dataru    s    
*..
+
+
1
1
3
3C u<<<<<<<&&((3355==U=KK\" < < <Cyy##%%--U-;;<---s   6A/ /AB87B8)create_rootrv  c                   t           j                            t          d                                          }|r?|rt          |          nt          t          |                    }t          ||d          S t          |           rt                      r/|rt          t                    nt          }t          ||d          S t          t          j                              }t          |d d          S )Nrr   F)r  r  r  T)rU   r(  rv   _MANAGED_FILES_ROOT_ENVr   rc  r\  r	   r  ro  ru  rt  r>  )rp   rv  raw_forced_rootrA  r>  s        rE   _managed_files_policyrz    s    jnn%<bAAGGIIO ^8Co#O444Y]^mYnYnIoIo!tW\]]]]#G,, ^0P0R0R ^CNn#$>???Tn!tW\]]]]49;;''D4TSWXXXXr   	for_writer|  c                   t          |          }t          |           }|j        }|	|r|dv r|}n|s|j        }nt	          |                                          }|I|                                s5t          d |j        D                       rt          dd          ||z  }n%|                                st          dd          d|j        v rt          dd          |r3|
                                st          |j                  }||j        z  }nt          ||           }|!t          ||          st          d	d
          ||t          |          fS )N>   r!  rx  c              3   "   K   | ]
}|d k    V  dS )..Nr7  )r
  parts     rE   r  z(_resolve_managed_path.<locals>.<genexpr>  s&      <<D44<<<<<<<r   r   zPath cannot contain '..'r   zPath must be absoluter  rX  r  Path outside managed files root)rz  rg  r  r  r	   r  r  rN  partsr%   r"  r\  r#  rO   re  r6  )	r  rp   r|  policyr  rA  r  r#  rb  s	            rE   _resolve_managed_pathr    s    #7++FhDD););		 	Q'		JJ))++	I$9$9$;$;<<IO<<<<< X#<VWWWWy(II&&(( 	QC8OPPPPy4NOOOO L))++ L !122IN*"9]KKKtX > >4UVVVV8S]]**r   r  c                 R    | j         t          | j                   nd }||| j        dS )N)rA  r  r  )r  r6  r  )r  r  s     rE   _managed_response_metar    s;    -3-?-K#f()))QUK"!1  r   c                 <   	 |                                 }n&# t          t          f$ r t          dd          w xY w| j        &t          | j        |          st          dd          	 |                                }n&# t          $ r}t          dd|           d }~ww xY w|                                }|rd n t          j	        |j
                  d         pd	}|j
        p|j
        pt          |          t          |          ||rd n|j        |j        |d
S )Nr   r  r   r  r  r`  zCould not stat path: r   r  )rO   r   is_directorysizemtimerP  )r  r  r  r%   r  re  r  r)  r  r   rO   r6  rP  st_mtime)r  rL   rb  r  r  r)  rP  s          rE   _managed_file_entryr    sS   D>>##\" D D DNCCCCD%nV=OQY.Z.Z%4UVVVVS]]__ S S S4QC4Q4QRRRRS __FjY%9(-%H%H%K%iOiI=x}=HH.BJ  s     #:+B   
B#
BB#rO  c                 
   | pd                                 }|                    d          rd|vrt          dd          |                    dd          \  }}|dd                              d	d          d
         pd}d|vrt          dd          	 t	          j        |d          }n+# t          j        t          f$ r t          dd          w xY wt          |          t          k    rt          dd          ||fS )Nrr   rK  ,r   z!Upload payload must be a data URLr   r   ri   ri  r   r  ;base64z%Upload payload must be base64 encodedTvalidatez"Upload payload is not valid base64rH  File is too large)r   r   r%   r&  rR  	b64decodebinasciiErrorr  r  _MANAGED_FILE_MAX_BYTES)rO  r  headerrV  rP  r  s         rE   _decode_data_urlr    s-   N!!##D??7## Ys$4WXXXXjja((OFGqrr
  a((+I/II4[\\\\Z$777NJ' Z Z Z4XYYYYZ
4yy***4GHHHH?s   B- -(Cz
/api/filesc                 @  K   t          ||           \  }}|                                st          dd          |                                st          dd          	 fd|                                D             }n@# t
          $ r t          dd          t          $ r}t          d	d
|           d }~ww xY w|                    d            j        }d }|j	        |k    r|||k    rt          |j	                  }|||dt                    S )Nr  r[  r   r   zPath is not a directoryc                 0    g | ]}t          |          S r7  )r  )r
  childr  s     rE   
<listcomp>z&list_managed_files.<locals>.<listcomp>8  s$    TTT%&vu55TTTr   r  zDirectory is not readabler`  zCould not read directory: c                 b    | d          t          | d                                                   fS )Nr  rO   )r6  r   items    rE   <lambda>z$list_managed_files.<locals>.<lambda>>  s+    tN';#;Sf=N=N=T=T=V=V"W r   r+  )r   r#  entries)r  r"  r%   r)  iterdirr  r  sortr  r#  r6  r  )	rp   r   rL   display_pathr  r  r  r#  r  s	           @rE   list_managed_filesr  /  sw     #8w#G#G FFL==?? F4DEEEE==?? O4MNNNNXTTTT6>>CSCSTTT Q Q Q4OPPPP X X X4VQT4V4VWWWWX LLWWLXXX$KF}K$76[;P;PV]##  !
(
(	 s   # B $C(B<<Cz/api/files/readc                   K   t          ||           \  }}}|                                st          dd          |                                st          dd          	 |                                j        }n&# t          $ r}t          dd|           d }~ww xY w|t          k    rt          dd	          t          j	        |j
                  d
         pd}	 t          j        |                                                              d          }n@# t          $ r t          dd          t          $ r}t          dd|           d }~ww xY w|j
        |||d| d| dt!          |          S )Nr  r  r   r   zPath is not a filer`  zCould not stat file: rH  r  r   r  rJ  r  r  zCould not read file: rK  rL  )rO   r   r  rP  rO  )r  r"  r%   rO  r  rP  r  r  r  r   rO   rR  rS  rT  rU  r  r  )	rp   r   r  rL   r  r  r  rP  rV  s	            rE   read_managed_filer  K  s     #8w#G#G FFL==?? F4DEEEE>> J4HIIIIS{{}}$ S S S4QC4Q4QRRRRS%%%4GHHHH$V[11!4R8RIS"6#4#4#6#677>>wGG L L L4JKKKK S S S4QC4Q4QRRRRS 8I88w88  !
(
( s0   "A< <
BBB 9D $E>EEz/api/files/uploadpayloadc                 `  K   t          | j        |d          \  }}}|                                r%|                                rt	          dd          |                                r| j        st	          dd          t          | j                  \  }}	 |j        	                    dd           |
                    |           n@# t          $ r t	          dd	          t          $ r}t	          d
d|           d }~ww xY wdt          ||          |dt          |          S )NTr{    z'A directory already exists at that pathr   zFile already existsr^  r  zFile is not writabler`  zCould not write file: okr-  r   )r  r   r"  r)  r%   rS  r  rO  r#  ra  write_bytesr  r  r  r  )r  rp   r  rL   r  r  
_mime_typer  s           rE   upload_managed_filer  l  st     #8wZ^#_#_#_ FFL}} _6==?? _4]^^^^}} Kw0 K4IJJJJ'(899D*TD48884     L L L4JKKKK T T T4RS4R4RSSSST $VV44  !
(
(	 s   1C $D
1DD
z/api/files/mkdirc                   K   t          | j        |d          \  }}}|                                r%|                                st	          dd          	 |                    dd           n@# t          $ r t	          dd          t          $ r}t	          d	d
|           d }~ww xY wdt          ||          |dt          |          S )NTr{  r  z"A file already exists at that pathr   r^  r  zDirectory is not writabler`  zCould not create directory: r  )
r  r   r"  r)  r%   ra  r  r  r  r  )r  rp   r  rL   r  r  s         rE   create_managed_directoryr    s     #8wZ^#_#_#_ FFL}} Zv}} Z4XYYYYZTD1111 Q Q Q4OPPPP Z Z Z4XSV4X4XYYYYZ $VV44  !
(
(	 s   A0 0$B-B((B-c                 h  K   t          | j        |          \  }}}|j        ||j        k    rt          dd          |j        |k    rt          dd          |                                st          dd          	 |                                r1| j        rt          j	        |           n)|
                                 n|                                 nE# t          $ r8}|                                r	| j        sdnd}t          |d	|           d }~ww xY wd
|dt          |          S )Nr   z$Cannot delete the managed files rootr   z!Cannot delete the filesystem rootr  r[  r  r`  zCould not delete path: Tr  r   )r  r   r  r%   r#  r"  r)  rZ  shutilrmtreermdirunlinkr  r  )r  rp   r  rL   r  r  r   s          rE   delete_managed_filer    s[     #8w#O#O FFL%&F4F*F*F4Z[[[[}4WXXXX==?? F4DEEEE
]==?? 	  f%%%%MMOOO ] ] ]#]]__OW5FOccC<[VY<[<[\\\\] OO0Fv0N0NOOs   AC 
D%3DDz/api/fs/listc           	      *  K   t          |           }	 g }t          j        |          5 }|D ]X}|j        t          v r|                    |j        t          ||j        z            |                    d          d           Y	 d d d            n# 1 swxY w Y   |                    d            d|iS # t          $ r g ddcY S t          $ r g d	dcY S t          $ r g d
dcY S t          $ r }g t          |dd           pddcY d }~S d }~ww xY w)NF)follow_symlinks)rO   r   isDirectoryc                 V    | d          | d                                          | d         fS )Nr  rO   r   r  s    rE   r  zfs_list.<locals>.<lambda>  s,    4+>'>V@R@R@T@TVZ[aVb&c r   r  r  ENOENT)r  errorENOTDIREACCESstrerrorz
read-error)r  rU   scandirrO   _FS_READDIR_HIDDENr=  r6  r)  r  r  r  r  r  r   )r   rL   r  scanr-  r  s         rE   fs_listr    s     d^^FXZ 	4  :!333!J 344#(<<<#F#F       	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	ccddd7## 2 2 211111 3 3 3	22222 2 2 211111 X X XZ(F(F(V,WWWWWWWWXsS   B8 ABB8 BB8 BB8 8D	DD)	D2DDDz/api/fs/read-textc           	        K   t          t          |                     \  }}|j        t          k    rt	          dd          t          |j        t                    }	 |                    d          5 }|                    |          }d d d            n# 1 swxY w Y   nL# t          $ r t	          dd          t          $ r%}t	          dt          |          pd          d }~ww xY wt          |d d	                   |j        t                              |j                                        d
          t#          |          t          |          |                    dd          |j        t          k    dS )NrH  rI  r   rbr  r  r   File read failedi   r  utf-8r)  errors)binarybyteSizelanguagemimeTyper   r  	truncated)r  r  rP  _FS_TEXT_SOURCE_MAX_BYTESr%   min_FS_TEXT_PREVIEW_MAX_BYTESopenr  r  r  r6  r  _FS_PREVIEW_LANGUAGE_BY_EXTrv   r  r   r  rU  )r   rL   r  bytes_to_readhandler  r  s          rE   fs_read_textr    s     !(4..11JFB	z---4DEEEE
$>??MT[[ 	.&;;}--D	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. L L L4JKKKK T T TCHH4R@RSSSST #4;//J/33FM4G4G4I4I6RR!&))FGI66Z"<<  s<   B! 3B	B! BB! BB! !$C* C%%C*z/api/fs/read-data-urlc                   K   t          t          |                     \  }}|j        t          k    rt	          dd          	 t          j        |                                                              d          }nL# t          $ r t	          dd          t          $ r%}t	          dt          |          pd          d }~ww xY wd	d
t          |           d| iS )NrH  rI  r   rJ  r  r  r   r  dataUrlrK  rL  )r  r  rP  _FS_DATA_URL_MAX_BYTESr%   rR  rS  rT  rU  r  r  r6  r  )r   rL   r  rV  r  s        rE   fs_read_data_urlr    s      !(4..11JFB	z***4DEEEET"6#4#4#6#677>>wGG L L L4JKKKK T T TCHH4R@RSSSSTG}V44GGgGGHHs   9A> >$C" CCz/api/fs/git-rootc                    K   t          |           }	 |                                }t          j        |j                  r|n|j        }n# t
          $ r |}Y nw xY wdt          |          iS )NrA  )r  r  r  r  r#  r  r%  )r   rL   r  rZ   s       rE   fs_git_rootr    sy      d^^F[[]],rz22E   %e,,--s   6A
 
AAz/api/fs/default-cwdc                  F   K   t                      } | t          |           dS )N)r  r/  )r+  r8  )r  s    rE   fs_default_cwdr    s'      


C."5"5666r   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 r7  r  )r
  platforms     rE   	<setcomp>zget_status.<locals>.<setcomp>$  s'     (
 (
 (
'HN(
 (
 (
r   gateway_state	platformsc                 $    i | ]\  }}|v 	||S r7  r7  )r
  r+  r  configured_gateway_platformss      rE   
<dictcomp>zget_status.<locals>.<dictcomp>4  s5     ! ! !C666 U666r   exit_reason
updated_at>   stoppedstartup_failedr  >   Nr  running	SessionDBr   limitc           	   3      K   | ]L}|                     d           5|                     d|                     dd                    z
  dk     HdV  MdS )ended_atNlast_active
started_atr   ,  r   rv   )r
  snows     rE   r  zget_status.<locals>.<genexpr>Q  sm       " "55$$,155lA0F0FGGG3NN NNNN" "r   r   Flist_providersc                     g | ]	}|j         
S r7  rO   r
  ps     rE   r  zget_status.<locals>.<listcomp>c  s    <<<Q!&<<<r   rd   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  rR   get_running_looprun_in_executorr  rv   gateway.configr  get_connected_platformsr@   r"   r%  hermes_stater  list_sessions_richtimer  r   r  r   rG   rP   hermes_cli.dashboard_authr   r   r   r6  r   r   r   )current_ver
latest_verr  r  remote_health_bodyloopaliver  r  r  r  r  gateway_configruntimer  r  dbsessionsr   r  _list_providersr  r  s                        @@rE   
get_statusr%    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=iU  rd   platform_labelc                     | pd|pdfD ]N}t          j        d|          }|s	 t          |                    d                    c S # t          $ r Y Kw xY wdS )zAExtract the Windows NT build number from stdlib platform strings.rr   z%(?:^|[^\d])10\.0\.(\d{5,})(?:[^\d]|$)r   N)researchr  groupr  )rd   r&  r  matchs       rE   _windows_build_numberr,    s    -R!526  	BEJJ 		u{{1~~&&&&& 	 	 	H	4s   !A
AAsystemreleasec                     | dk    r=|dk    r7t          ||          }|%|t          k    rt          j        dd|d          }d}| |||d	S )
zAReturn host OS fields for display while preserving stdlib detail.Windows10Nz^Windows-10(?=-)z
Windows-11r   )count11)rU   
os_release
os_versionr  )r,  _WINDOWS_11_MIN_BUILDr(  sub)r-  r.  rd   r&  r  s        rE   _display_system_platformr8    s}     w$%g~>>*?!?!?V#	  N G "	  r   z/api/system/statsc                    K   ddl } i t          |                                 |                                 |                                 |                                            |                                 |                                 |                                 |                                 t          t          j                    d}	 ddl}|                                }|j        |j        |j        |j        d|d<   	 |                    t'          t)                                          }|j        |j        |j        |j        d|d<   n# t,          $ r Y nw xY w	 |                    d	
          |d<   t1          |dd          }|rt3           |                      |d<   n# t,          $ r Y nw xY w	 |                                }t7          t9          j                    |z
            |d<   n# t,          $ r Y nw xY w	 |                                }|j        |                                j         t7          |!                                          |"                                d|d<   n# t,          $ r Y nw xY wd|d<   nQ# t,          $ rD d|d<   	 t3          t          j#                              |d<   n# tH          tJ          f$ r Y nw xY wY nw xY w|S )a!  Host + process system stats for the System page.

    OS / Python / host identity from stdlib; CPU / memory / disk / uptime from
    psutil when available, with graceful degradation when it isn't.  Read-only
    and non-sensitive (no env values, no paths beyond the hermes home root).
    r   N)r-  r.  rd   r&  )archrk  python_versionpython_implhermes_version	cpu_count)total	availableusedpercentr
  )r?  rA  freerB  disk皙?)r6   cpu_percent
getloadavgload_avguptime_seconds)r  rsscreate_timenum_threadsprocessTpsutilF)&r  r8  r-  r.  rd   machinenoder;  python_implementationr   rU   r>  rN  virtual_memoryr?  r@  rA  rB  
disk_usager6  r   rC  r@   rF  r   r  	boot_timer  r  Processr  memory_inforJ  rK  rL  rG  r  r`   )	_platformr>   rN  vmdulabootprocs           rE   get_system_statsr]    s?      !   
"##%%%%''%%''$--//	
 
 
 !!##NN$$#2244 6688%\^^  D 2""$$XGz	
 
X		""3'8'8#9#9::B:	 DLL  	 	 	D		"("4"4c"4"B"BDt44B .#'::Z  	 	 	D		##%%D%(t);%<%<D!"" 	 	 	D			>>##Dx''))-"4#3#3#5#566#//11	 DOO  	 	 	D	X   X	#BMOO44D( 	 	 	D	 Ks   6J AE J 
EJ EJ !AF( 'J (
F52J 4F55J 9:G4 3J 4
H>J  HJ A+I1 0J 1
I>;J =I>>J K#J;:K;KKKKKz/api/curatorc                    K   	 ddl m}  n&# t          $ r}t          dd|           d }~ww xY w	 |                                 }n# t          $ r i }Y nw xY wt          | dd          t          | dd	          t          | d
d           |                    d          t          | dd           t          | dd           t          | dd           dS )Nr   curatorr`  zCurator unavailable: r   
is_enabledT	is_pausedFget_interval_hourslast_run_atget_min_idle_hoursget_stale_after_daysget_archive_after_days)rC  pausedinterval_hoursrd  min_idle_hoursstale_after_daysarchive_after_days)r   r`  r@   r%   
load_state
_safe_callrv   )r`  r  rP   s      rE   get_curator_statusro    s     S!!!!!!! S S S4QC4Q4QRRRRS""$$    g|T::Wk599$W.BDIIyy//$W.BDII&w0FMM(2JDQQ  s!    
.).A AAc                       e Zd ZU eed<   dS )CuratorPauserh  Nr2  r3  r4  r  r5  r7  r   rE   rq  rq             LLLLLr   rq  z/api/curator/pausedr  c                    K   ddl m} |                    t          | j                             dt          | j                  dS )Nr   r_  T)r  rh  )r   r`  
set_pausedr  rh  )r  r`  s     rE   set_curator_pausedrv    sN      tDK(()))$t{"3"3444r   z/api/curator/runc                     K   	 t          ddgd          } n&# t          $ r}t          dd|           d}~ww xY wd| j        dd	S )
zDTrigger a curator review now (backgrounded; tail via action status).r`  r4  curator-runr`  zFailed to run curator: r   NTr  r  rO   _spawn_hermes_actionr@   r%   r  r\  r  s     rE   run_curatorr}  #  ss      U#Y$6FF U U U4Sc4S4STTTTUtx???    
:5:fn_namec                     	 t          | |d           }t          |          r
 |            n|S # t          $ r |cY S w xY wrE  )r   callabler@   )modr  r   fns       rE   rn  rn  -  sW    S'4((||0rrttt0   s   +. ==z/api/portalc                    K   t                      pi } i }	 ddlm}  |            pi }n# t          $ r i }Y nw xY wg }	 ddlm}  ||           }||                                D ]}}t          |dd          rd}n@t          |dd          rt          |dd           r|j        }nt          |dd          rd}nd	}|	                    t          |d
d          |d           ~n*# t          $ r t                              d           Y nw xY wt          |                     d          t                    r|                     d          ni }t          |                    d                    |                    d          |                    d          t!          |pi                     d          pd          d|dS )Nr   )get_nous_auth_status)get_nous_subscription_featuresmanaged_by_nousFzvia Nous Portalactivecurrent_providerznot configuredlabelrr   )r  rP   zportal features failedr   	logged_inportal_base_urlinference_base_urlro  z3https://portal.nousresearch.com/manage-subscription)r  
portal_urlinference_urlro  subscription_urlfeatures)r   hermes_cli.authr  r@   hermes_cli.nous_subscriptionr  r%  r   r  r=  r=   	exceptionr  rv   r  r  r6  )	cfgr}   r  r  r  featsfeatrP   r  s	            rE   get_portal_statusr  :  s$     
--
2CD888888##%%+    H1OOOOOO..s33 	W 	W4!2E:: --EET8U33 -FXZ^8_8_ - 1EET8U33 -$EE,E'$*D*Du U UVVVV 1 1 1/000001 %/swww/?/?$F$FN   BI$((;//00hh011"677b--j99?R@@Q  s   ) 88B%C$ $$D
Dz/api/ops/prompt-sizec                     K   	 t          dgd          } n&# t          $ r}t          dd|           d }~ww xY wd| j        ddS )Nprompt-sizer`  Failed: r   Try  rz  r|  s     rE   run_prompt_sizer  j  sp      F#]O]CC F F F4Ds4D4DEEEEFtx???    
949z/api/ops/dumpc                     K   	 t          dgd          } n&# t          $ r}t          dd|           d }~ww xY wd| j        ddS )Ndumpr`  r  r   Try  rz  r|  s     rE   run_dumpr  s  sp      F#VHf55 F F F4Ds4D4DEEEEFtx888r  z/api/ops/config-migratec                     K   	 t          ddgd          } n&# t          $ r}t          dd|           d }~ww xY wd| j        ddS )	Nr  migrateconfig-migrater`  r  r   Try  rz  r|  s     rE   run_config_migrater  |  su      F#Xy$9;KLL F F F4Ds4D4DEEEEFtx1ABBBr~  c                   ,    e Zd ZU dZeed<   dZeed<   dS )DebugShareRequestTredactr  linesN)r2  r3  r4  r  r  r5  r  r  r7  r   rE   r  r    s4          FDE3r   r  z/api/ops/debug-sharec                   K   ddl m} | pt                      }	 t          j        |t          dt          t          |j                  d                    t          |j
                             d{V }na# t          $ r}t          dd| 	          d}~wt          $ r3}t                              d
           t          dd| 	          d}~ww xY wd|j        |j        |j        |j        dS )a  Upload a redacted debug report + full logs and return the paste URLs.

    Unlike the other diagnostics actions (doctor, dump, prompt-size) this is
    *synchronous*: the whole point of ``debug share`` is the set of shareable
    URLs it produces, so we run the upload in a worker thread and return the
    structured ``{urls, failures, redacted, ...}`` payload directly. The
    dashboard renders those as real, copyable links instead of scraping a log
    tail. Pastes auto-delete after 6 hours (handled inside the share core).
    r   )build_debug_sharer   i  )	log_linesr  N  zUpload failed: r   zdebug share failedr`  r  T)r  urlsfailuresredactedauto_delete_seconds)hermes_cli.debugr  r  rR   	to_threadmaxr  r  r  r  r  r  r%   r@   r=   r  r  r  r  r  )r  r  r  r7  r  s        rE   run_debug_share_endpointr    sI      322222

%#%%CF(!SSY6677
##
 
 
 
 
 
 
 
 

  M M M4Kc4K4KLLLL F F F+,,,4Ds4D4DEEEEF
 OO%9  s$   AA8 8
CBC#.CClogs_ACTION_LOG_DIRgateway-restartzgateway-restart.loggateway-startzgateway-start.loggateway-stopzgateway-stop.loghermes-updatezhermes-update.logdoctorzaction-doctor.logsecurity-auditzaction-security-audit.logbackupzaction-backup.logimportzaction-import.logcheckpoints-prunezaction-checkpoints-prune.logskills-installzaction-skills-install.logskills-uninstallzaction-skills-uninstall.logskills-updatezaction-skills-update.logrx  zaction-curator-run.logr  zaction-prompt-size.logr  zaction-dump.logr  zaction-config-migrate.logtools-post-setupzaction-tools-post-setup.log_ACTION_LOG_FILES_ACTION_PROCS_ACTION_RESULTSr   rO   message	exit_codec           	      8   t           |          }t                              dd           t          |z  }t          |dd          5 }|                    d|  dt          j        d           d	                                           |                    |                    d
d                     |                    d          s|                    d           ddd           n# 1 swxY w Y   t          
                    | d           |ddt          | <   dS )zBRecord a non-spawned action result and write it to the action log.Tr^  abr   	buffering
=== z completed %Y-%m-%d %H:%M:%S ===
r  r)  r  
   
Nr  r  )r  r  ra  r  writer  strftimerz   r  r  r  r  )rO   r  r  log_file_namelog_pathlog_files         rE   _record_completed_actionr    sU   %d+M$666.H	h	*	*	* "hPTPPdm4G&H&HPPPWWYY	
 	
 	
 	w~~gi~@@AAA%% 	"NN5!!!" " " " " " " " " " " " " " " dD!!!*3DAAODs   BC''C+.C+
subcommandc                    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,                              |d           |t0          |<   |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.
    Tr^  r  r   r  r  z	 started r  r  z-mzhermes_cli.mainHERMES_NONINTERACTIVErJ   )r  stdinr6  stderrrD  win32DETACHED_PROCESScreationflagsstart_new_sessionN)r  r  ra  r  r  r  r  rz   sys
executabler6  PROJECT_ROOTr3  DEVNULLSTDOUTrU   r(  r  CREATE_NEW_PROCESS_GROUPr   Popenr   r  r  r  )r  rO   r  r  r  cmdpopen_kwargsr\  s           rE   r{  r{    sn    &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 NNd###M$Kr   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.r  r)  )encodingr  r   N)r"  	read_textr  
splitlines)r   r  r  r  s       rE   _tail_linesr    s     ;;== 	~~wy~AA   			OOEQ5!::E)s   0 ??c                      t                               d          } | |                                 | dfS t          ddgd          dfS )a  Spawn ``hermes gateway restart``, reusing an in-flight restart.

    Multiple dashboard paths can request a restart in quick succession
    (restart button double-click, or a stale cached frontend firing its own
    restart after the server already auto-restarted post-onboarding). Two
    concurrent ``hermes gateway restart`` children race each other on the
    manual kill-and-start path, so reuse the live one instead.

    Returns ``(proc, reused)``.
    r  NTgatewayrestartF)r  rv   pollr{  )existings    rE   _spawn_gateway_restartr  #  sP       !233H 7~I 68IJJEQQr   c                     	 t                      \  } }nC# t          $ r6}t                              d           dt	          |          dcY d}~S d}~ww xY w|r t                              d| j                   dd| j        dS )	z@Best-effort gateway restart after enabling the webhook platform.z6Failed to auto-restart gateway after enabling webhooksFrestart_startedrestart_errorNz:Webhook enable: reusing in-flight gateway restart (pid %s)Tr  r  restart_actionrestart_pidr  r@   r=   r  r6  r>   r  r\  reusedr  s      rE   %_restart_gateway_after_webhook_enabler  4  s    
-//ff 
 
 
OPPP$ XX
 
 	
 	
 	
 	
 	
 	

  
		HH	
 	
 	

  +x      
A+A	AAz/api/gateway/restartc                     K   	 t                      \  } }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.zFailed to spawn gateway restartr`  zFailed to restart gateway: r   NTr  ry  )r  r@   r=   r  r%   r  )r\  _reusedr  s      rE   restart_gatewayr  J  s      Y.00gg Y Y Y89994WRU4W4WXXXXY x!      
A.AAz/api/hermes/updatec                  b  K   t          t                    } | dk    r6t                      }t          d|d           dddd|t	          |           dS 	 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   r  r   )r  FNdocker_update_unsupported)r  r  rO   r  r  update_commandr'  zFailed to spawn hermes updater`  zFailed to start update: r   Try  )r   r  r   r  r   r{  r@   r=   r  r%   r  )install_methodr  r\  r  s       rE   update_hermesr  Y  s       +<88N!!.00 'QGGGG#0CNSS
 
 	
V#XJ@@ V V V67774Ts4T4TUUUUV x  s   A& &
B#0.BB#   c                    	 t          j        ddt          t                    ddddt	          |            gddd	          }|j        d
k    rg S g }|j                                        D ]q}|                                s|	                    d          g dz   dd         }|\  }}}}|
                    |dd         ||t	          |pd
          d           r|S # t          $ r g cY S w xY w)u[  Commits the local checkout is behind ``origin/main`` by, newest first.

    Logs the SAME range the behind-count uses (``HEAD..origin/main`` — see
    ``banner._check_via_local_git``), NOT the branch's ``@{upstream}``. On a
    feature-branch checkout ``@{upstream}`` is the branch's own tip (zero
    commits), which would leave the changelog empty even though the count is
    non-zero. Pinning to ``origin/main`` keeps count and changelog consistent.

    Best-effort: returns [] if not a git checkout, origin/main is unreachable,
    or git is unavailable. Never raises into the request path.
    r-  r.  logz--format=%H%x1f%s%x1f%an%x1f%ctzHEAD..origin/mainz-nTri   )r1  r  r  r   )rr   rr   rr   0N      )shasummaryauthorat)r3  r4  r6  r  r  r5  r6  r  r   r&  r=  r@   )	r  r@  rowsliner  r  r  r  r  s	            rE   _recent_upstream_commitsr  u  sA   !nL!!1#SVV  
 
 
 >QI%'J))++ 	 	D::<< ZZ''*;*;*;;RaR@E',$C&"KKrr7&$bgA,,	        			s   AC BC C.-C.z/api/hermes/update/checkforcec                 6  K   t          t                    }t          |          }|t          dd|dv |dd}|dk    rt	                      |d<   |S 	 ddlm} | r5	 t                      d	z                                   n# t          $ r Y nw xY wt          j        |           d{V }n,# t          $ r t                              d
           d}Y nw xY w||d<   |d|d<   n7|dk    rd|d<   n+d|d<   |dv r"t          j        t                     d{V |d<   |S )u}  Report whether a Hermes update is available, without applying it.

    Powers the dashboard's "check before you update" flow: the System page
    shows the commit-behind count and asks the user to confirm before
    ``POST /api/hermes/update`` actually runs ``hermes update``.

    Returns:
        install_method: 'git' | 'pip' | 'docker' | 'nixos' | 'homebrew' | ...
        current_version: installed Hermes version string
        behind: commits behind upstream (>=1), 0 if up to date,
                -1 if behind by an unknown count (nix/pypi), or null if the
                check could not run (offline, no remote, etc.)
        update_available: convenience bool (behind is non-zero and not null)
        can_apply: True when the dashboard's update button can apply it
                   in place (git/pip); False for docker/nix/homebrew where the
                   user must update out-of-band
        update_command: the recommended command for this install method
        message: human-readable guidance for non-applyable methods
        commits: for git/pip installs that are behind, a list of the commits
                 the local checkout is behind upstream by — each
                 {sha, summary, author, at}. Absent/empty otherwise. The
                 desktop's remote update overlay renders this as "what's
                 changed". Additive: existing consumers ignore it.
    NF)r-  pip)r
  current_versionbehindupdate_available	can_applyr	  r  r   r  r   )check_for_updatesz.update_checkzUpdate check failedr  u5   Couldn't reach the update source — try again later.zYou're on the latest version.Tr  commits)r   r  r   r   r   hermes_cli.bannerr!  r   r  r  rR   r  r@   r=   r  r  )r  r
  r	  r  r!  r  s         rE   check_hermes_updater$    s     4 +<88N:>JJN )&!#~5( G !!9;;	
777777 	 ""_4<<>>>>    ():;;;;;;;;   ,--- GH~T		1<	&*"# ^++'.'89Q'R'R!R!R!R!R!R!RGINs6   B) #A> =B) >
BB) 
BB) )&CCz/api/audio/transcribec                   K   | j         pd                                }|                    d          rd|vrt          dd          |                    dd          \  }}d|vrt          dd	          | j        p%|d
d                              dd          d         pd                                }|                    dd          d                                         }|                    d          s|dk    st          dd          	 t          j        |d          }n+# t          j
        t          f$ r t          dd          w xY w|st          dd          t          |          t          k    rt          dd          d}	 t          |          }t          j        d|d          5 }	|	                    |           |	j        }d d d            n# 1 swxY w Y   ddlm}
 t+          j                    }|                    d |
|           d {V }nJ# t          $ r  t0          $ r3}t2                              d           t          dd|           d }~ww xY w	 |r&	 t7          j        |           n># t:          $ r Y n2w xY wn-# |r&	 t7          j        |           w # t:          $ r Y w w xY ww xY w|                    d          s&t          d|                    d          pd           dt?          |                    d!          pd                                          |                    d"          d#S )$Nrr   rK  r  r   zInvalid audio payloadr   r   r  z$Audio payload must be base64 encodedri   ri  r   re  zaudio/rf  z"Payload must be an audio recordingTr  z!Audio payload is not valid base64zAudio recording is emptyrH  zAudio recording is too largezhermes-desktop-voice-F)r  r  delete)transcribe_audioz"Desktop voice transcription failedr`  zTranscription failed: successr  zTranscription failed
transcriptro  )r  r)  ro  ) rO  r   r   r%   r&  rP  r   rR  r  r  r  r  r  _MAX_TRANSCRIPTION_UPLOAD_BYTESrk  tempfileNamedTemporaryFiler  rO   tools.transcription_toolsr'  rR   r  r  r@   r=   r  rU   r  r  rv   r6  )r  rO  r  rV  rP  normalized_mime_typeaudio_bytes	temp_pathr  tmpr'  r  r7  r  s                 rE   transcribe_audio_uploadr2    sR      &B--//Hw'' M3h+>+>4KLLLLnnS!,,OFG$J
 
 
 	

 	HVABBZ--c155a8HLegg  %??32215;;==''11
<//$H
 
 
 	
Y&w>>>NJ' Y Y Y4WXXXXY  P4NOOOO
;9994RSSSSI*955(*
 
 
 	! IIk"""I	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	?>>>>>'))++D2BINNNNNNNN    T T T;<<<4RS4R4RSSSST 	  		)$$$$   	9 		)$$$$   	 ::i   
::g&&@*@
 
 
 	
 &**\228b99??AAJJz**  s   D' '(E&H" 7G H"  G$$H" 'G$(9H" !J "I)6.I$$I))J 0J 
JJK J/.K /
J<9K ;J<<K c                       e Zd ZU eed<   dS )TTSSpeakRequestr  NrV  r7  r   rE   r4  r4  ;	  rW  r   r4  r  c                    t          |                     d          p|                     d          pd                                          }t          |                     d          pd                                          }|r| d| dn|S )NrO   voice_idVoicer   rr   z ())r6  rv   r   )r  rO   r   s      rE   _elevenlabs_voice_labelr9  ?	  s    uyy  DEIIj$9$9DWEEKKMMD599Z((.B//5577H%-7d!!h!!!!47r   z/api/audio/elevenlabs/voicesc            	        K   t                                          d          p t          j                            d          pd                                } | sdg dS t
          j                            dd| d          	 t          j	                    }d	t          t          t          f         ffd
}|                    d|           d{V }n># t          $ r1}t                              d|           t#          dd          d}~ww xY wg }|                    d          pg D ]}t%          |t&                    st          |                    d          pd                                          }|sQ|                    |t          |                    d          p|          t+          |          d           |                    d            d|dS )zReturn ElevenLabs voices when an API key is configured.

    The desktop UI uses this for the ``tts.elevenlabs.voice_id`` dropdown.
    Only non-secret voice metadata is returned; the API key stays server-side.
    ELEVENLABS_API_KEYrr   F)r@  voicesz#https://api.elevenlabs.io/v1/voicesapplication/json)Acceptz
xi-api-keyru   r7   c                      t           j                            d          5 } t          j        |                                                     d                    cd d d            S # 1 swxY w Y   d S )Nr  r  r  )r  rp   r  r  r  r  rU  )responserp   s    rE   _fetchz%get_elevenlabs_voices.<locals>._fetch\	  s    '''<< Cz(--//"8"8"A"ABBC C C C C C C C C C C C C C C C C Cs   9A))A-0A-Nz ElevenLabs voice list failed: %sr  z Could not load ElevenLabs voicesr   r<  r6  rO   )r6  rO   r  c                 n    t          |                     d          pd                                          S )Nr  rr   )r6  rv   r   r  s    rE   r  z'get_elevenlabs_voices.<locals>.<lambda>t	  s+    TXXg%6%6%<"!=!=!C!C!E!E r   r  T)r   rv   rU   r(  r   r  rp   r&   rR   r  r   r6  r
   r  r@   r=   warningr%   r  r  r=  r9  r  )	r:  r  rB  r  r  r<  r  r6  rp   s	           @rE   get_elevenlabs_voicesrE  F	  s1      zz~~233arz~~FZ7[7[a_ahhjjG 2"b111n$$-(!
 
 %  G
X'))	CS#X 	C 	C 	C 	C 	C 	C ,,T6:::::::: X X X7===4VWWWWX FX&&,"  %&& 	uyy,,23399;; 	 		&))5X66,U33
 
 	 	 	 	 KKEEKFFF000s   AC 
D,D		Dz/api/audio/speakc                   K   | j         pd                                }|st          dd          	 ddlm} t          j                    }|                    d||           d{V }n@# t          $ r3}t          
                    d           t          d	d
|           d}~ww xY w	 t          |t                    rt          j        |          n|}n# t          $ r t          d	d          w xY w|                    d          s&t          d|                    d          pd          |                    d          }|rt           j                            |          st          d	d          t           j                            |          d                                         }dddddd                    |d          }		 t+          |d          5 }
|
                                }ddd           n# 1 swxY w Y   n&# t.          $ r}t          d	d|           d}~ww xY w	 t!          j        |           n:# t.          $ r Y n.w xY w# 	 t!          j        |           w # t.          $ r Y w w xY wxY wt3          j        |                              d          }dd|	 d| |	|                    d          dS )aD  Synthesize speech and return audio as base64 data URL.

    Used by the desktop voice-conversation mode to play back assistant
    responses without exposing the on-disk file path. Reuses the
    existing TTS provider chain (Edge / OpenAI / ElevenLabs / etc.)
    configured in ``~/.hermes/config.yaml`` under ``tts.``.
    rr   r   zText is requiredr   r   )text_to_speech_toolNzDesktop voice TTS failedr`  zSpeech synthesis failed: zInvalid TTS responser(  r  zSpeech synthesis failed	file_pathzAudio file missingr   rb  rc  rd  r`  )r\  r]  r  r^  r[  r  zCould not read audio: rJ  TrK  rL  ro  )r  rO  rP  ro  )r  r   r%   tools.tts_toolrG  rR   r  r  r@   r=   r  r  r6  r  r  rv   rU   r   isfilesplitextr   r  r  r  r  rR  rS  rU  )r  r  rG  r  result_jsonr  r7  rH  extrP  fhr/  rV  s                rE   
speak_textrO  x	  s      LB%%''D H4FGGGGW666666')) 007JDQQQQQQQQ W W W12224UPS4U4UVVVVWL,6{C,H,HYK(((k L L L4JKKKKL ::i   
::g&&C*C
 
 
 	

 

;''I JBGNN955 J4HIIII
'

9
%
%a
(
.
.
0
0C  
c#| 	)T"" 	$b''))K	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ T T T4RS4R4RSSSST	Ii     	 	 	D		Ii     	 	 	D	 {++227;;G8I88w88JJz**	  s   6A) )
B&3.B!!B&*+C C2H G<0H <H  H H H I 
H+H&&H++I /I 
III;I+*I;+
I85I;7I88I;z/api/actions/{name}/statusr  r  c           	        K   t                               |           }|t          dd|            t          |z  }t	          |t          t          |d          d                    }t                              |           }|Ot                              |           }d}|r|                    d          nd}|r|                    d	          nd}	nq|	                                }|du }|j
        }	|P	 |                    d
           n# t          $ r Y nw xY w||	dt          | <   t                              | d           | |||	|dS )zCTail an action log and report whether the process is still running.Nr  zUnknown action: r   r     Fr  r  r  r  )rO   r  r  r  r  )r  rv   r%   r  r  r  r  r  r  r  r  rB   r@   r  )
rO   r  r  r  tailr\  r7  r  r  r  s
             rE   get_action_statusrS  	  sx      &))$//M4Mt4M4MNNNN.HxS]]D!9!9::DT""D| $$T**/5?FJJ{+++4	#)3fjjtIIKK	t#h 		!	$$$$   2;C$H$HOD!dD)))   s   6D 
DDz/api/sessionsexcludecreatedr  offsetmin_messagesarchivedordersourceexclude_sourcesc                 j  K   |dvrt          dd          |dvrt          dd          	 ddlm}  |            }	 t          d|          }	|d	k    }
|d
k    }d |pd                    d          D             }|                    |pd|pd| ||	||
|dk              }|                    |pd|pd|	||
d          }t          j                    }|D ]r}|                    d          du o0||                    d|                    dd                    z
  dk     |d<   t          |                    d                    |d<   s||| |d|
                                 S # |
                                 w xY w# t          $ r, t                              d           t          dd          w xY w)a  List sessions.

    ``archived`` controls how soft-archived sessions are treated:
    ``exclude`` (default) hides them, ``only`` returns just the archived ones
    (used by the desktop "Archived sessions" settings panel), and ``include``
    returns both.

    ``order`` controls pagination order: ``created`` (default, by original
    start time) or ``recent`` (by latest activity across the compression
    chain). ``recent`` keeps a long-running conversation on the first page
    after it auto-compresses into a fresh continuation id.
    rT  onlyincluder   /archived must be one of: exclude, only, includer   rU  recent%order must be one of: created, recentr   r  r^  r_  c                 :    g | ]}|                                 |S r7  r   r
  r  s     rE   r  z get_sessions.<locals>.<listcomp>	
  s%    WWW!QWWYYWAWWWr   rr   r  Nrb  rZ  r[  r  rV  min_message_countinclude_archivedarchived_onlyorder_by_last_activeTrZ  r[  rh  ri  rj  exclude_childrenr  r  r  r  	is_activerX  )r#  r?  r  rV  zGET /api/sessions failedr`  Internal server error)r%   r  r  r  r&  r  session_countr  rv   r  r   r@   r=   r  )r  rV  rW  rX  rY  rZ  r[  r  r"  rh  rj  ri  exclude_listr#  r?  r  r  s                    rE   get_sessionsrr  	  sI     , 555D
 
 
 	
 ))):
 
 
 	
+M******Y[[%	 #A| 4 4$.M'94
 XW(=2'D'DS'I'IWWWL,,~ , 4"3!1+%*h%6 - 	 	H $$~ , 4"3!1+!% %  E )++C 8 8EE*%%- Squu]AEE,4J4JKKKsR +
 !%QUU:%6%6 7 7* (55TZ[[HHJJJJBHHJJJJ M M M12224KLLLLMs#   E< DE# E< #E99E< <6F2z/api/profiles/sessionsrb  allr1  c                    K   |dvrt          dd          |dvrt          dd          ddlm} dd	lm}	 g }
|r0|d
k    r*t          |          \  }}|
                    ||f           nz	 |	                                }d |D             }
n,# t          $ r t          
                    d           g }
Y nw xY w|
s*|
                    d|	                    d          f           t          d|          }|dk    }|dk    }|pd}d |pd                    d          D             }t          t          | |z   |           d          }g }d}i }g }t          j                    }|
D ]\  }}t!          |          dz  }|                                s-	  ||d          }n<# t          $ r/}|                    |t%          |          d           Y d}~pd}~ww xY w	 |                    ||pd|d||||dk              }|                    ||pd|||d          }||z  }|||<   |D ]}||d<   |dk    |d<   |                    d          du o0||                    d|                    d d                    z
  d!k     |d"<   t-          |                    d#                    |d#<   |                    |           n<# t          $ r/}|                    |t%          |          d           Y d}~nd}~ww xY w|                                 # |                                 w xY w|dk    rdnd  |                     fd$d%           |||| z            }|||| ||d&S )'u  Unified, read-only session list aggregated across ALL profiles.

    Intentionally process-light: this opens each profile's ``state.db`` directly
    from disk — it does NOT spawn a dashboard backend per profile. Each returned
    session is tagged with its owning ``profile`` so the desktop renders one
    browsable list and only spins up a profile's backend when the user actually
    interacts (sends a message). A user with a single (default) profile gets the
    same rows as ``/api/sessions``, just tagged ``profile="default"``.
    r]  r   r`  r   ra  rc  r   r  profilesrs  c                 *    g | ]}|j         |j        fS r7  )rO   r   )r
  r>   s     rE   r  z)get_profiles_sessions.<locals>.<listcomp>O
  s!    @@@$	49-@@@r   z0GET /api/profiles/sessions: list_profiles failedr   r^  r_  Nc                 :    g | ]}|                                 |S r7  re  rf  s     rE   r  z)get_profiles_sessions.<locals>.<listcomp>]
  s%    OOO!QWWYYOAOOOr   rr   r  r`  state.dbT)db_path	read_only)r1  r  rb  rg  rl  r1  is_default_profiler  r  r  r  rn  rX  c                 \    |                                p|                      d          pdS )Nr  r   r  )r  sort_keys    rE   r  z'get_profiles_sessions.<locals>.<lambda>
  s'    aeeHooI|1D1DI r   r+  reverse)r#  r?  profile_totalsr  rV  r  )r%   r  r  
hermes_clirv  _cron_profile_homer=  list_profilesr@   r=   r  get_profile_dirr  r&  r  r  r	   r"  r6  r  rp  rv   r  r   r  )!r  rV  rW  rX  rY  r1  rZ  r[  r  profiles_modtargetsrO   r>  infosrh  rj  ri  source_filterrq  per_profilemergedr?  r  r  r  rz  r"  r  r  profile_totalr  windowr~  s!                                   @rE   get_profiles_sessionsr  ,
  s     ( 5554effff)))4[\\\\&&&&&&333333&(G Q7e##'00
dd|$$$$	 ..00E@@%@@@GG 	 	 	NNMNNNGGG	  	QNNI|'C'CI'N'NOPPPA|,,&M9, NdMOO 52<<SAAOOOL c%&.%00#66K#%FE%'N#%F
)++C - -
dt**z)~~ 		 7d;;;BB 	 	 	MMdSXX>>???HHHH	!	(($ , 4!"3!1+%*h%6 ) 	 	D ,,$ , 4"3!1+!% -  M ]"E#0N4  ! !#)*.)*;&'EE*%%- Squu]AEE,4J4JKKKsR + !%QUU:%6%6 7 7*a    !  	@ 	@ 	@MMdSXX>>????????	@ HHJJJJBHHJJJJ % 1 1}}|H
KKIIIISWKXXXF6E>)*F(  s[   / B &B98B9F##
G-%GG C"KL
K<%K72L7K<<LL+z/api/sessions/searchqc                 j  K   | r|                                  sdg iS 	 ddlm}  |            	 t          dt	          t          |pd          d                    i dt          dt          ffd	i d
t          dt          ffdi dt          dt          ddffd}                    | d          D ]}|	                    d          }|	                    d          pd                                 }|pd| } |||d|	                    d          |	                    d          |	                    d          d           ddl
}g }	 |j        d|                                            D ]Z}
|
                    d          s|
                    d          r|	                    |
           B|	                    |
dz              [d                    |	          }t          dz  d          }                    ||           }|D ]}t#                    k    r ny ||d         |	                    d!d          |	                    d"          |	                    d          |	                    d          |	                    d#          d           dt%                                                    i                                 S #                                  w xY w# t*          $ r, t,                              d$           t1          d%d&'          w xY w)(aS  Search sessions by ID plus full-text message content using FTS5.

    Direct session-id matches are surfaced first, then FTS message-content
    matches. Results are deduped by compression lineage, not by raw
    ``session_id``. Auto-compression rotates a conversation onto a fresh
    session id (and leaves the old segment's messages in the FTS index), so one
    logical chat can own many ``sessions`` rows that all match the same query.
    Branches also use ``parent_session_id``, but they are real alternate
    conversations; don't collapse branch-specific hits back into the parent.
    resultsr   r  r   r  d   
session_idr7   c                    | s| S | v r|          S g }| }t                      }| }|r(||vr#|                    |           |                    |           |v r	|         }n	                     |          }n# t          $ r d }Y nw xY w|s|}nt          |t                    r|                    d          nd }|s|}n	                     |          }n# t          $ r d }Y nw xY w|s|}n_|                    d          }|                    d          }	|                    d          dk    o|d uo	|	d uo|	|k    }
|
s|}n	|}|r||v#|D ]}||<   |S )Nparent_session_idr  r  
end_reasonr  )r[   addr=  get_sessionr@   r  r  rv   )r  chaincurvisitedrA  r  r#  parent_sessionparent_ended_atr  is_compression_edgerP  r"  
root_caches               rE   compression_rootz)search_sessions.<locals>.compression_root
  s   ! &%%++%j11 %%! #!c00KK$$$LL%%%j(()#!NN3//$ ! ! ! ! ";Ea;N;NXQUU#6777TXF! ".)+)?)?$ . . .)-.) "&4&8&8&D&DO!"|!4!4J&**<88MI :+47:&d2: '/9	 ( / " CG  #!c00H " , ,D'+Jt$$s$   &A< <B
BC C*)C*root_idc                     | v r|          S | }	                      |           }|r|}n# t          $ r Y nw xY w|| <   |S rE  )get_compression_tipr@   )r  tiprb  r"  	tip_caches      rE   lineage_tipz$search_sessions.<locals>.lineage_tip
  ss    i''$W--!55g>>H '&    D%(	'"
s   + 
88raw_sidr  Nc                     | sd S  |           }|v st                    k    rd S t          |          } |          |d<   ||d<   ||<   d S )Nr  lineage_root)r  r  )r  r  rA  r  r  
safe_limitseens      rE   add_lineage_resultz+search_sessions.<locals>.add_lineage_result  st     F''004<<3t99
#:#:Fw--(3D(9(9%*.'$T


r   Tr  ri  idpreviewrr   zSession ID: rZ  r   r  )snippetrolerZ  r   session_startedz"[^"]*"|\S+"rk   r$  ri   r   )queryr  r  r  r  zGET /api/sessions/search failedr`  zSearch failedr   )r   r  r  r  r  r  r6  r  search_sessions_by_idrv   r(  findallr   r  r=  joinsearch_messagesr  r  valuesr   r@   r=   r  r%   )r  r  r  r  rowsidr  r  r(  termstokenprefix_queryfetch_limitmatchesmr  r"  r  r  r  r  r  s                  @@@@@@@rE   search_sessionsr  
  s       AGGII 2RE******Y[[L	QC$4$4c : :;;J  "J/S /S / / / / / / /b !IS S       " D	%C 	%$ 	%4 	% 	% 	% 	% 	% 	% 	% 	% 	% //VZ/[[  ggdmm779--3::<<!9%9C%9%9""#* $"%''("3"3!$!1!1+.77<+@+@ 	 	 	 	 IIIE#NAGGII>> . .##C(( .ENN3,?,? .LL''''LL----88E??L j1nb11K((|;(OOG  t99
**E""lO#$55B#7#7 !f"#%%//!"w+,551B+C+C 	 	 	 	 tDKKMM223HHJJJJBHHJJJJ E E E8999ODDDDEs#   K< JK# K< #K99K< <6L2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   r  r   r   rO   rr   r   )r  rv   r  r  )r  	model_valctx_lens      rE   _normalize_config_for_webr  F  s     &\\F

7##I)T"" +-- 0!44#--	9==3L3LMMw4>w4L4L)SRS%&&)*%&Mr   z/api/configc                    K   t          |           5  t          t                                }d d d            n# 1 swxY w Y   d |                                D             S )Nc                 D    i | ]\  }}|                     d           ||S r#  r   r
  kvs      rE   r  zget_config.<locals>.<dictcomp>b  s/    EEETQ1<<3D3DEAqEEEr   )_profile_scoper  r   r%  )r1  r  s     rE   
get_configr  ]  s      		 	  : :*;==99: : : : : : : : : : : : : : : FEV\\^^EEEEs   :>>z/api/config/defaultsc                     K   t           S rE  )r   r7  r   rE   get_defaultsr  e  s      r   z/api/config/schemac                  $   K   t           t          dS )N)fieldscategory_order)CONFIG_SCHEMA_CATEGORY_ORDERr7  r   rE   
get_schemar  j  s      #GGGr   r   ro  auto_context_lengthconfig_context_lengtheffective_context_lengthcapabilities_EMPTY_MODEL_INFOz/api/model/infoc                    	 t          |           5  t                      }ddd           n# 1 swxY w Y   |                    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&          $ r  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.
    Nr   rr   r   rO   ro  rq  r  )ro  r   )get_model_context_length)r   rq  ro  r  get_model_capabilitiesro  r   supports_toolssupports_visionsupports_reasoningcontext_windowmax_output_tokensmodel_familyr  zGET /api/model/info failed)r  r   rv   r  r  r6  r  agent.model_metadatar  r@   r  agent.models_devr  r  r  r  r  r  r  r%   r=   r  )r1  r  r  
model_namero  rq  
config_ctxr  auto_ctxconfig_ctx_inteffective_ctxcapsr  mcs                 rE   get_model_infor  y  s   G'G$$ 	  	 --C	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 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%&&&&&'sv   F* ,F* 0F* 0C
F*  D F* D%"F* $D%%.F* <F F* 
FF* FF* *AG/.G/)visionweb_extractr  
skills_hubapprovalmcptitle_generationtriage_specifierkanban_decomposerprofile_describerr`  ._AUX_TASK_SLOTSz/api/model/optionsc           
          	 ddl m}m} t          |           5   | |            dddddd          cddd           S # 1 swxY w Y   dS # t          $ r  t
          $ r, t                              d           t	          dd	
          w xY w)a  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.

    ``profile`` scopes the picker context (current model/provider, custom
    providers from config, per-profile .env auth state) so the Models page
    reads the SAME profile /api/model/set writes.
    r   build_models_payloadload_picker_contextr   T)
max_modelsinclude_unconfiguredpicker_hintscanonical_orderpricingr  NzGET /api/model/options failedr`  zFailed to list model optionsr   )hermes_cli.inventoryr   r  r  r%   r@   r=   r  )r1  r   r  s      rE   get_model_optionsr    s$   TRRRRRRRR G$$ 		 		''##%%%)! $!  		 		 		 		 		 		 		 		 		 		 		 		 		 		 		 		 		 		     T T T67774RSSSSTs.   A A A  AA AA A Bz/api/model/recommended-defaultc                    | pd                                                                 }|dk    r	 ddlm}m}m}m}m}m} ddl	m
}  |            }	 |d          pi }
 |d          }d}	  |d          pi }|                    dd          pd}n# t          $ r d}Y nw xY w|r" ||	|
|          \  }	}
 ||	|
d	          \  }	}n ||	|
|          \  }	}
|	r|	d         nd}d|t          |          d
S # t          $ r# t                              d           dddd
cY S w xY w	 ddlm}m}  | |            d          }|                    dg           D ]d}t'          |                    dd                                                    |k    r)|                    d          pg }||r|d         nddd
c S e|ddd
S # t          $ r# t                              d           |ddd
cY S w xY w)a  Return the recommended default model for a freshly-authenticated provider.

    Mirrors the model-curation `hermes model` does so GUI onboarding lands on a
    sensible default instead of blindly taking the first curated entry. For
    Nous this honors the user's free/paid tier: free users get a free model,
    paid users get the full curated default. For any other provider it falls
    back to the first curated model (same as before).

    Response: {"provider": str, "model": str, "free_tier": bool | None}
    where free_tier is True/False for Nous and None otherwise. `model` may be
    empty if nothing could be resolved (caller degrades gracefully).
    rr   nousr   )get_curated_nous_model_idsget_pricing_for_providercheck_nous_free_tierpartition_nous_models_by_tier&union_with_portal_free_recommendations&union_with_portal_paid_recommendations)get_provider_auth_stateTforce_freshr  )	free_tier)ro  r   r  z0GET /api/model/recommended-default (nous) failedNr  r   )r  	providersslugmodelsz)GET /api/model/recommended-default failed)r   r   r}  r  r  r  r  r  r  r  r  rv   r@   r  r=   r  r  r   r  r6  )ro  r  r  r  r  r  r  r  r  	model_idsr  r  r  rP   _unavailabler   r   r  r  r  r  s                        rE   get_recommended_default_modelr    s2    N!!##))++Dv~~&	H                @?????2244I..v66<"G,,>>>IJ //77=2"YY'8"==C

      


   
%K%Kw
& &"	7 +H*Gw$+ + +'	<< &L%Kw
& &"	7 %.5IaLL2E &T)__UUU 	H 	H 	HNNMNNN &$GGGGG	H
BRRRRRRRR&&':':'<'<LLL;;{B// 	c 	cC37762&&''--//477**0b$(3N6!99B]abbbbb 8 !2DAAA B B BBCCC 2DAAAAABsO   ;C7 ,%B C7 B!C7  B!!AC7 7*D$#D$(BG ?G *G32G3z/api/model/auxiliaryc                 8   	 t          |           5  t                      }ddd           n# 1 swxY w Y   |                    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          $ r, t                              d           t          dd          w xY w)u  Return current auxiliary task assignments.

    Shape:
      {
        "tasks": [
          {"task": "vision", "provider": "auto", "model": "", "base_url": ""},
          ...
        ],
        "main": {"provider": "openrouter", "model": "anthropic/claude-opus-4.7"},
      }

    ``profile`` scopes the read — without it, the Models page would show
    the dashboard profile's auxiliary pins while /api/model/set wrote the
    selected profile's (read/write asymmetry).
    Nr  ro  r   r   rr   rq  )rp  ro  r   rq  r   rO   r  )tasksmainzGET /api/model/auxiliary failedr`  zFailed to read auxiliary configr   )r  r   rv   r  r  r  r=  r6  r%   r@   r=   r  )r1  r  aux_cfgr  slotslot_cfgr  r  s           rE   get_auxiliary_modelsr!  S  sp   "WG$$ 	  	 --C	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 ''+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(   G ,G 0G 0F$G A Hz/api/model/setc                 B   	
K    j         pd                                                                	 j        pd                                 j        pd                                 j        pd                                                                
 j        pd                                 j        pd                                	dvrt          dd          	 rO j	        sH	 ddl
m} t          j        |           d	{V }n# t          $ r d	}Y nw xY w|d
	d|j        dS  	
fd}t          j        |           d	{V 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.
    rr   >   r  r  r   z#scope must be 'main' or 'auxiliary'r   r   )expensive_model_warning)ro  rq  NFT)r  rn  ro  r   confirm_requiredconfirm_messagec            	          t          j        p          5  t                     cd d d            S # 1 swxY w Y   d S rE  )r  r1  _apply_model_assignment_sync)r:  rq  r  r   r1  ro  rn  rp  s   rE   _apply_assignmentz/set_model_assignment.<locals>._apply_assignment  s     788  38UD(G                  s   9= =zPOST /api/model/set failedr`  zFailed to save model assignment)rn  r   r   ro  r   rp  rq  r:  r%   rr  hermes_cli.model_cost_guardr#  rR   r  r@   r  r=   r  )r  r1  r#  rD  r(  r:  rq  r   ro  rn  rp  s   ``   @@@@@@rE   set_model_assignmentr*    sY      Z2$$&&,,..E#**,,HZ2$$&&EIO""$$**,,D#**,,H|!r((**G)))4YZZZZ(W
  	5 	OOOOOO !( 1+%%	! ! !          "" ("(,'.  	 	 	 	 	 	 	 	 	 	 	 	 &'8999999999    W W W34444UVVVVWs7   '	E 1$D E D%"E $D%%E 8%E A Frn  rp  c                    t                      }| dk    r|r|st          dd          t          ||          \  }}t          |                    di           ||||          }||d<   g }|                                                                dk    re	 ddlm}	 dd	l	m
}
  |
|d
d          } |	||d          }t          |          }n,# t          $ r t                              dd           Y nw xY wt          |           |                                                                dv rP|rN	 ddlm}m}  |||| ||                     n,# t          $ r t                              dd           Y nw xY w|                                                                }g }|                    di           }t'          |t(                    rt*          D ]}|                    |          }t'          |t(                    s-t-          |                    dd          pd                                          }|rj|                                dvrT|                                |k    r<|                    ||t-          |                    dd          pd          d           dd|||                    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t1          t*                    }|D ]Z}|t*          vrt          dd |           |                    |          }t'          |t(                    si }||d<   ||d<   |||<   [||d<   t          |           dd|||d!S )"u  Synchronous body of POST /api/model/set.

    Runs inside ``_profile_scope`` (in a worker thread) so every
    load_config/save_config lands in the requested profile.  Raises
    HTTPException for validation errors — the async wrapper re-raises them.
    r  r   z$provider and model required for mainr   r   r
  r   )apply_nous_managed_defaults)_get_platform_toolscliFinclude_default_mcp_serversT)enabled_toolsetsr  z#apply_nous_managed_defaults skippedr{  >   r   r   )_auto_provider_name_save_custom_providerr  z%custom_providers registration skippedr  ro  rr   >   rr   r   )rp  ro  r   rq  )r  rn  ro  r   rq  gateway_tools	stale_aux	__reset__r   )r  rn  resetzprovider required for auxiliaryzunknown auxiliary task: )r  rn  r  ro  r   )r   r%   r  r  rv   r   r   r  r,  hermes_cli.tools_configr-  sortedr@   r=   rA   r   hermes_cli.mainr2  r3  r  r  r  r6  r=  r  )rn  ro  r   rp  rq  r:  r  r  r4  r,  r-  rC  changedr2  r3  r  r5  r  r  r   slot_providerauxr  s                          rE   r'  r'    s    --C 	`u 	`C8^____:8UKK%0GGGR  (E8W
 
	 !G $&>>!!##v--QTTTTTTGGGGGG--E   65%, $  
 !'w Q Q Q 

@4
PPPPPQ
 	C >>!!##':::x:SVVVVVVVV%%,,X66	      S S S 

BT
RRRRRS  ~~''--// "	''+r**gt$$ 	'  ";;t,,!(D11  #HLLR$@$@$FB G G M M O O!	%++--\AA%++--==$$ $$1!$X\\'2%>%>%D"!E!E& &     !j"55*"
 
 	
 ''+

Cc4   {# 	! 	!Dwwt}}Hh-- #)HZ  "HW CIIKC[4@@@ W4UVVVV7tff$"7"7G  &&C8YSW8Y8YZZZZ774==(D)) 	H'!D		C  s$   7C &C87C85 E &E?>E?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  	TypeErrorr  rv   r6  r   r@   )r  ctx_overrider  disk_config
disk_models        rE   _denormalize_config_from_webrD  _  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                 4  K   	 t          | j        p|          5  t          t          | j                             d d d            n# 1 swxY w Y   ddiS # t
          $ r  t          $ r, t                              d           t          dd          w xY w)Nr  TzPUT /api/config failedr`  ro  r   )	r  r1  r   rD  r  r%   r@   r=   r  )r  r1  s     rE   update_configrF    s     MDL3G44 	C 	C4T[AABBB	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	Cd|    M M M/0004KLLLLMs.   A "AA AA AA A Bz/api/envc                 <  K   t          |           5  t                      }d d d            n# 1 swxY w Y   t                      }i }t          j                    D ]\  }}|                    |          }t          |          |rt          |          nd |                    dd          |                    d          |                    dd          |                    dd          |                    dg           |                    dd          ||v d		||<   |S )
Nr   rr   r   r   passwordFtoolsadvanced)	r?   redacted_valuer   r   r   is_passwordrI  rJ  channel_managed)r  r   _channel_managed_env_keysr   r%  rv   r  r    )r1  env_on_diskchannel_keysr7  var_namer>   r  s          rE   get_env_varsrR    sT     		 	  ! !jj! ! ! ! ! ! ! ! ! ! ! ! ! ! !,..LF+133 
 
$))5kk38Bj///d88M26688E??R0088J66XXgr**U33  (<7
 
x M   -11c                 v  K   	 t          | j        p|          5  t          | j        | j                   d d d            n# 1 swxY w Y   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+  r   r   zPUT /api/env failedr`  ro  )r  r1  r   r+  r  r  r%   r6  r@   r=   r  )r  r1  r  s      rE   set_env_varrV    s     MDL3G44 	1 	148TZ000	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	148,,, G G G
 CHH===3F M M M,---4KLLLLMs9   A AA AA A	A 
B8 A??9B8)z https://openrouter.ai/api/v1/keybearer)z https://api.openai.com/v1/modelsrW  )zhttps://api.x.ai/v1/modelsrW  )z7https://generativelanguage.googleapis.com/v1beta/modelsr  )OPENROUTER_API_KEYOPENAI_API_KEYXAI_API_KEYGEMINI_API_KEY_CREDENTIAL_PROBESr  c                    	 | j         sg S |                                 }n# t          $ r g cY S w xY wt          |t                    r|                    d          n|}t          |t                    sg S g }|D ]}t          |t                    r7t          |                    d          pd                                          }n#t          |pd                                          }|r|	                    |           |S )a1  Extract model ids from an OpenAI-compatible ``/v1/models`` response.

    Tolerant of the common shapes: ``{"data": [{"id": ...}]}`` (OpenAI / vLLM /
    llama.cpp) and a bare ``{"data": ["id", ...]}``. Returns ``[]`` on any
    parse/HTTP error so a slightly non-standard endpoint never hard-blocks.
    r  r  rr   )

is_successr  r@   r  r  rv   r  r6  r   r=  )r  r  r  idsr  mids         rE   _parse_model_idsra    s    	I))++   			",Wd";";H7;;vDdD!! 	C  dD!! 	*dhhtnn*++1133CCdjb//''))C 	JJsOOOJs       //z/api/providers/validatec                   K   t          |           ddl}| j        pd                                }| j        pd                                }|sddddS |dk    r|                    d	          d
z   }| j        pd                                }|rdd| ind}	 |                    |                    d                    5 }|	                    ||          }	ddd           n# 1 swxY w Y   dddt          |	          dS # t          $ r ddd| ddcY S w xY wt          	                    |          }
|
sddddS |
\  }}ddi}i }|dk    r	d| |d<   n||d<   	 |                    |                    d                    5 }|	                    |||          }	ddd           n# 1 swxY w Y   n# t          $ r	 ddddcY S w xY w|	j        dv rddddS |	j        dk    s|	j        rddddS ddd|	j         ddS )a]  Live-probe a provider credential before it's saved.

    Returns {ok, reachable, message}. ok=True means the provider accepted the
    key; ok=False + reachable=True means the key is bad (caller should block);
    reachable=False means the network probe couldn't run (caller may save with
    a warning rather than hard-blocking offline users).
    r   Nrr   FTzEnter a value first.)r  	reachabler  OPENAI_BASE_URLrx  z/modelsAuthorizationrt   g       @r  r?  )r  rc  r  r  zCould not reach r!  r>  r=  rW  r+        $@)ru   paramsz/Could not reach the provider to verify the key.)r   r  z9That API key was rejected. Double-check it and try again.  zProvider returned HTTP z for this key.)r   httpxr+  r   r  r  r:  ClientTimeoutrv   ra  r@   r\  r   r^  )r  rp   ri  r+  r  r   r:  ru   rl  r  prober}   rg  s                rE   validate_provider_credentialrm    sY      7LLL8>r
 
 
"
"CZ2$$&&E S$;QRRR ll3)+ <%2,,..<CM?$7g$7$788	[emmC&8&899 8Vzz#wz778 8 8 8 8 8 8 8 8 8 8 8 8 8 8TbL\]aLbLbccc 	[ 	[ 	[e@YSV@Y@Y@YZZZZZ	[ ""3''E ?2>>>IC+,GFx#4U#4#4  uo\\%--"5"5\66 	C&::c76:BBD	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C o o o%<mnnnnno :%%$;vwww3$/"===d7qQUQa7q7q7qrrrsl   )D C&D &C**D -C*.D DD!)F; 
F/#F; /F33F; 6F37F; ;GGc                   K   	 t          | j        p|          5  t          | j                  }d d d            n# 1 swxY w Y   |st	          d| j         d          d| j        dS # t          $ r  t
          $ r$}t	          dt          |                    |d }~wt          $ r, t          	                    d           t	          dd	          w xY w)
Nr   not found in .envr   TrU  r   zDELETE /api/env failedr`  ro  )
r  r1  r   r+  r%   r  r6  r@   r=   r  )r  r1  removedr  s       rE   remove_env_varrq  0  sM     MDL3G44 	1 	1&tx00G	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	YC488W8W8WXXXX48,,,    G G G CHH===3F M M M/0004KLLLLMs4   A+ ;A+ ?A+ ?'A+ +C?B9C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	        p|          5  t                      }ddd           n# 1 swxY w Y   |                    | 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 r7  r7  )r
  tcutoffs     rE   r  z"reveal_env_var.<locals>.<listcomp>U  s    III1a&jjQjjjr   Nrh  z,Too many reveal requests. Try again shortly.r   r  ro  zenv/reveal: %s)r+  r  )r   r  _REVEAL_WINDOW_SECONDSrh   r  _REVEAL_MAX_PER_WINDOWr%   r=  r  r1  r   rv   r+  r=   r>   )r  rp   r1  r  rO  r  ru  s         @rE   reveal_env_varrx  D  sb      7 )++C))FIIII(:IIIqqq
"8884bccccc""" 
/	0	0 ! !jj! ! ! ! ! ! ! ! ! ! ! ! ! ! !OODH%%E}tx4S4S4STTTTII)))8e,,,s   +CC
C
r  Telegramz1Run Hermes from Telegram DMs, groups, and topics.z1https://core.telegram.org/bots/features#botfather)TELEGRAM_BOT_TOKENTELEGRAM_ALLOWED_USERSTELEGRAM_PROXY)rz  )rO   r   docs_urlenv_varsrequired_envDiscordz5Connect Hermes to Discord DMs, channels, and threads.z+https://discord.com/developers/applications)DISCORD_BOT_TOKENDISCORD_ALLOWED_USERSDISCORD_REPLY_TO_MODE)r  slackSlackz&Use Hermes from Slack via Socket Mode.zhttps://api.slack.com/apps)SLACK_BOT_TOKENSLACK_APP_TOKEN
mattermost
Mattermostz:Connect Hermes to Mattermost channels and direct messages.zhttps://mattermost.com/deploy/)MATTERMOST_URLMATTERMOST_TOKENMATTERMOST_ALLOWED_USERS)r  r  matrixMatrixz/Use Hermes in Matrix rooms and direct messages.z%https://matrix.org/ecosystem/servers/)MATRIX_HOMESERVERMATRIX_ACCESS_TOKENMATRIX_USER_IDMATRIX_ALLOWED_USERS)r  r  r  signalSignalz)Connect through a signal-cli REST bridge.z0https://github.com/bbernhard/signal-cli-rest-api)SIGNAL_HTTP_URLSIGNAL_ACCOUNTSIGNAL_ALLOWED_USERS)r  r  whatsappWhatsAppzBUse Hermes through the bundled WhatsApp bridge with QR-based auth.z"https://github.com/tulir/whatsmeow)WHATSAPP_ENABLEDWHATSAPP_MODEWHATSAPP_ALLOWED_USERSr7  homeassistantzHome Assistantz7Control your smart home from Hermes via Home Assistant.z2https://www.home-assistant.io/docs/authentication/)HASS_URL
HASS_TOKENemailEmailz,Talk to Hermes through an IMAP/SMTP mailbox.z@https://hermes-agent.nousresearch.com/docs/user-guide/messaging/)EMAIL_ADDRESSEMAIL_PASSWORDEMAIL_IMAP_HOSTEMAIL_SMTP_HOSTsmszSMS (Twilio)z*Send and receive text messages via Twilio.zhttps://www.twilio.com/console)TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENdingtalkDingTalku+   Connect Hermes to DingTalk groups (钉钉).zGhttps://open.dingtalk.com/document/orgapp/the-robot-development-process)DINGTALK_CLIENT_IDDINGTALK_CLIENT_SECRETfeishuzFeishu / Larkz Use Hermes inside Feishu / Lark.zKhttps://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/intro)FEISHU_APP_IDFEISHU_APP_SECRETFEISHU_ENCRYPT_KEYFEISHU_VERIFICATION_TOKEN)r  r  wecomzWeCom (group bot)z&Send-only WeCom group bot via webhook.z8https://developer.work.weixin.qq.com/document/path/91770)WECOM_BOT_IDWECOM_SECRET)r  wecom_callbackzWeCom (app)z+Two-way WeCom integration via callback app.z8https://developer.work.weixin.qq.com/document/path/90930)WECOM_CALLBACK_CORP_IDWECOM_CALLBACK_CORP_SECRETWECOM_CALLBACK_AGENT_IDWECOM_CALLBACK_TOKENWECOM_CALLBACK_ENCODING_AES_KEY)r  r  r  weixinzWeChat (Official Account)z"Connect a WeChat Official Account.zNhttps://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html)WEIXIN_ACCOUNT_IDWEIXIN_TOKENWEIXIN_BASE_URL)r  r  bluebubbleszBlueBubbles (iMessage)z5Use Hermes through iMessage via a BlueBubbles server.zhttps://bluebubbles.app/)BLUEBUBBLES_SERVER_URLBLUEBUBBLES_PASSWORDBLUEBUBBLES_ALLOWED_USERS)r  r  qqbotzQQ Botz5Connect Hermes to a QQ Bot from the QQ Open Platform.zhttps://q.qq.com)	QQ_APP_IDQQ_CLIENT_SECRETQQ_ALLOWED_USERS)r  r  u   Yuanbao (元宝)z"Connect Hermes to Tencent Yuanbao.)rO   r   r}  r  z
API serverzIExpose Hermes as an OpenAI-compatible HTTP API for tools like Open WebUI.)API_SERVER_ENABLEDAPI_SERVER_KEYAPI_SERVER_PORTAPI_SERVER_HOSTAPI_SERVER_MODEL_NAMEWebhooksz>Receive events from GitHub, GitLab, and other webhook sources.zIhttps://hermes-agent.nousresearch.com/docs/user-guide/messaging/webhooks/)WEBHOOK_ENABLEDWEBHOOK_PORTWEBHOOK_SECRET)yuanbao
api_serverwebhook_PLATFORM_OVERRIDES)r  r   r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  _PLATFORM_ORDERr  z8signal-cli REST API base URL, e.g. http://127.0.0.1:8080zSignal bridge URL)r   r1   r   r  z6Signal account phone number registered with the bridgezSignal account)r   r1   r  z3Comma-separated Signal users allowed to use the botzAllowed Signal usersr  z#Enable the WhatsApp gateway adapterzEnable WhatsApp)r   r1   rJ  r  zWhatsApp bridge modezWhatsApp moder  z5Comma-separated WhatsApp users allowed to use the botzAllowed WhatsApp usersr  z>Home Assistant base URL, e.g. https://homeassistant.local:8123zHome Assistant URLr  uB   Long-lived access token from Home Assistant (Profile → Security)zHome Assistant access token)r   r1   rH  r  z&Email address to send and receive fromzEmail addressr  z&Email account password or app passwordzEmail passwordr  z&IMAP server host (e.g. imap.gmail.com)z	IMAP hostr  z&SMTP server host (e.g. smtp.gmail.com)z	SMTP hostr  zTwilio Account SIDr  zTwilio Auth Tokenr  zWeCom group bot IDzWeCom Bot IDr  zWeCom group bot secretzWeCom Secretr  zWeCom corp IDzWeCom Corp IDzWeCom app corp secretzWeCom Corp SecretzWeCom app agent IDzWeCom Agent IDz!WeCom callback verification tokenzWeCom TokenzWeCom callback AES encoding keyzWeCom AES KeyzWeChat Official Account IDz
Account IDzWeChat callback tokenTokenzWeChat platform base URLzBase URLzFeishu / Lark app IDzApp IDzFeishu / Lark app secretz
App secretzFeishu / Lark encrypt keyzEncrypt keyz Feishu / Lark verification tokenzVerification tokenzDingTalk client ID (App key)z	Client IDz#DingTalk client secret (App secret)zClient secret)r  r  r  r  r  r  r  r  r  r  r  r  r  _MESSAGING_ENV_FALLBACKSc                     ddl m}  t                      }g }| j                                        D ]Y}|j        dk    r|j        |v r|                    |j                   |                    t          |j                             Z	 ddl	m
} |                                D ]N}|j        |v r|                    |j                   |                    t          |j        |                     On,# t          $ r t                              dd           Y nw xY wd t!          t"                    D             |                    fd	
           t'          |          S )a  Build the messaging catalog from the gateway's Platform enum + plugin registry.

    Built-in platforms come from ``gateway.config.Platform`` (LOCAL is excluded).
    Plugin platforms come from ``gateway.platform_registry.plugin_entries()``,
    which lets newly installed adapters (e.g. IRC) appear without a code change
    here. Per-platform UI metadata (description, docs URL, env-var picks) lives
    in :data:`_PLATFORM_OVERRIDES`; anything not overridden gets reasonable
    defaults derived from the platform id and required_env.
    r   )Platformr   )platform_registryz$plugin platform registry unavailableTr{  c                     i | ]\  }}||	S r7  r7  )r
  idxr  s      rE   r  z/_messaging_platform_catalog.<locals>.<dictcomp>  s    AAA(#sS#AAAr   c                                          | d         t          t                              | d                                         fS )Nr  rO   )rv   r  r  r   )rD   rY  s    rE   r  z-_messaging_platform_catalog.<locals>.<lambda>  s4    uyy4#o*>*>??6ARARS r   r  )r  r  r[   __members__r  r  r  r=  _build_catalog_entrygateway.platform_registryr  plugin_entriesrO   r@   r=   rA   	enumerater  r  tuple)r  r  r  memberr  plugin_entryrY  s         @rE   _messaging_platform_catalogr    s    ('''''UUD$&G&--// ; ;<7""<4+FL99::::	J??????-<<>> 	R 	RL D((HH\&'''NN/0A<PPQQQQ		R
  J J J

9D
IIIIIJ BAi&@&@AAAELLSSSS     >>s   A)C6 6&DDc                  &   	 t                      } t                      D ]+}|                     |                    dd                     ,t	          |           S # t
          $ r- t                              dd           t	                      cY S w xY w)a  Env-var keys owned by a Channels page platform card.

    The Channels page is the canonical surface for configuring messaging
    platform credentials (with connection status, test, enable toggle and
    gateway restart). The Keys/Env page consults this set to hide those vars
    so the same fields aren't duplicated in a plainer UI. Best-effort: if the
    gateway catalog can't be built, nothing is flagged and Keys shows it all.
    r~  r7  z+could not build channel-managed env key setTr{  )r[   r  r'  rv   	frozensetr@   r=   rA   )keysr-  s     rE   rN  rN    s    022 	3 	3EKK		*b112222   

@4
PPP{{s   AA 4BB>   GATEWAY_PROXY_KEYGATEWAY_PROXY_URLGATEWAY_ALLOW_ALL_USERSplatform_idc                     ddddddd}| |v r||          S |                                                      dd	          d	z   fS )
z4Env-var prefixes owned by a messaging platform card.)EMAIL_)HASS_)QQ_QQBOT_)TWILIO_)
WECOM_BOT_r  )WECOM_CALLBACK_)r  r  r  r  r  r  -r#  )upperr)  )r  aliasess     rE   _platform_env_prefixesr  	  sd     #"/.+ +G g{##''S11C799r   c                 T   t          |           }g }t          j                    D ]Z\  }|                    d          dk    rt          v r)t          fd|D                       sE|                               [t          t          t          |                              S )zLAll messaging-category env vars for a platform (override + plugin + prefix).r   	messagingc              3   B   K   | ]}                     |          V  d S rE  r  )r
  r  rO   s     rE   r  z._discover_platform_env_vars.<locals>.<genexpr>!  s/      BBv4??6**BBBBBBr   )
r  r   r%  rv   _MESSAGING_KEYS_PAGE_KEYSrN  r=  r  r9  r[   )r  prefixesr  r>   rO   s       @rE   _discover_platform_env_varsr    s    %k22HD'-//  
d88J;..,,,BBBBBBBBB 	DD		""###r   overrider  c                    t          |           }d|v r2t          t                              g |d         |R                     S |E|j        r>t          t                              g t          |j                  |R                     S |S )z5Canonical env-var list for a messaging platform card.r~  )r  r  r  fromkeysr  )r  r  r  
discovereds       rE   _merge_platform_env_varsr  '  s     -[99JXT]]#GXj%9#GJ#G#GHHIIIL$=T]]#SU<+D%E%E#S
#S#STTUUUr   c                    t                               | i           }t          | ||          }d|v rt          |d                   }n|t          |j        pd          }nd}|                    d          r	|d         }n9||j        r|j        }n(|                     dd                                          }|                    d          }|s|	|j        pd}| ||pd|                    dd          ||d	S )
Nr  r7  rO   r#  r$  r   rr   r}  )r  rO   r   r}  r~  r  )	r  rv   r  r  r  r  r)  rc   install_hint)r  r  r  r~  r  rO   r   s          rE   r  r  5  s%    #&&{B77H'X|LLH!!Xn566		!\6<"==||F 5		!l&8	!!""3,,2244,,}--K 6<3"/52 "(bLLR00$  r   c                 H    t                      D ]}|d         | k    r|c S d S )Nr  )r  )r  r-  s     rE   _catalog_lookupr  X  s9    ,..  ;+%%LLL &4r   r+  c                 8   t          j        |           pt                              |           pi }|                    dd          |                    d|           |                    d          |                    dd          |                    dd          dS )	Nr   rr   r1   r   rH  FrJ  )r   r1   r   rL  rJ  )r   rv   r  )r+  r>   s     rE   _messaging_env_infor	  _  s     %%P)A)E)Ec)J)JPbDxxr22((8S))xxxx
E22HHZ//  r   c                 z    ddl m}m}  |            } ||           }|j                            |          }|||fS )Nr   )r  r  )r  r  r  r  rv   )r  r  r  r  r  platform_configs         rE   _gateway_platform_configr  j  sZ    <<<<<<<<  ""Fx$$H&**844O8_,,r   r-  rO  r!  c                    | d         }t                      d u}|r|                    d          ni }t          |t                    r|                    |i           ni }g }| d         D ]{}                    |          pt	          j        |d          }	|                    ||| d         v t          |	          |	rt          |	          nd dt          |                     |	 t          |          \  }
}}t          |o|j                  }t          |o|
                    ||                    }|r |j        r|j                                        nd }n5# t          $ r( d}t!          fd| d         D                       }d }Y nw xY wt          |t                    r|                    d	          nd }|sd
}n|sd}n|r|sd}n|s|sd}|| d         | d         | d         ||||t          |t                    r|                    d          nd t          |t                    r|                    d          nd t          |t                    r|                    d          nd ||dS )Nr  r  r~  rr   r  )r+  requiredr?   rK  Fc              3   l   K   | ].}                     |          pt          j        |d           V  /dS )rr   N)rv   rU   rV   )r
  r+  rO  s     rE   r  z._messaging_platform_payload.<locals>.<genexpr>  sQ       
 
;>KOOC  6BIc2$6$6
 
 
 
 
 
r   rP   disablednot_configuredpending_restartgateway_stoppedrO   r   r}  
error_codeerror_messager  )r  rO   r   r}  rC  
configuredr  rP   r  r  r  home_channelr~  )r!   rv   r  r  rU   rV   r=  r  r    r	  r  rC  _is_platform_connectedr  to_dictr@   rs  )r-  rO  r!  r  r  runtime_platformsruntime_platformr~  r+  r  r   r  r  rC  r  r  rP   s    `               rE   _messaging_platform_payloadr  s  sC    +K%''t3O4;CK000 '..	k2... 
 HZ  

 

$$:	#r(:(:5#88u++7<"F*U"3"3"3$	 
 &c**	
 	
 	
 	
4L5
 5
1/ B?+BCC Q55hPP
 

 #2#?O(00222 	
     
 
 
 
BGBW
 
 
 
 

  *44Dd)K)KUW%%%QU 
  " " 	 " "! "U "! f]+*% * *D11  ... *D11  111 *D11  ...$3  s   'A2E /FFrC  c                    t                      }|                    di           }t          |t                    si }||d<   |                    | i           }t          |t                    si }||| <   ||d<   t	          |           d S )Nr  rC  )r   
setdefaultr  r  r   )r  rC  r  r  r  s        rE   _write_platform_enabledr    s    ]]F!!+r22Ii&& (	'{**;;;Oot,, 1!0	+!(OIr   z+https://setup.hermes-agent.nousresearch.comzHermesDashboard/z^\d+$c                   j    e Zd ZU eed<   eed<   eed<   dZedz  ed<   dZedz  ed<   dZedz  ed<   dS )_TelegramOnboardingPairing
poll_token
expires_atexpires_at_tsN	bot_tokenbot_usernameowner_user_id)	r2  r3  r4  r6  r5  r  r%  r&  r'  r7  r   rE   r!  r!    sl         OOOOOO IsTz   #L#*### $M3:$$$$$r   r!  _telegram_onboarding_pairingsc                      t          j        dt                                                                        d          S )NTELEGRAM_ONBOARDING_URLrx  )rU   rV    _TELEGRAM_ONBOARDING_DEFAULT_URLr   r  r7  r   rE   _telegram_onboarding_base_urlr,    s,    
	+-MNN		r   c                    	 |                      dd          }t          j        |          }|j         |                     t          j                  }|                                S # t          $ r t          j                    dz   cY S w xY w)NZ+00:00)tzinfoiX  )	r)  r   fromisoformatr0  r   utc	timestampr@   r  )r  rj  r  s      rE   _parse_expiry_tsr4    s    !]]311
'
33= ^^8<^88F!!! ! ! !y{{S    !s   A$A' ' B
	B
c                      t          j                     fdt                                          D             } | D ]}t                              |d            d S )Nc                 0    g | ]\  }}|j         k    |S r7  )r$  )r
  
pairing_idrecordr  s      rE   r  z7_prune_telegram_onboarding_pairings.<locals>.<listcomp>   s5       J3&& 	&&&r   )r  r(  r%  r  )expiredr7  r  s     @rE   #_prune_telegram_onboarding_pairingsr:    sv    
)++C   "?"E"E"G"G  G
  < <
%))*d;;;;< <r   c                     t          | pd                                          }t                              |          r|S d S )Nrr   )r6  r   _TELEGRAM_USER_ID_RE	fullmatch)r  rj  s     rE   _normalize_telegram_user_idr>  	  s@    U[b!!''))J%%j11 4r   r  fallbackc                 <    ddddddd                     | |          S )Nz2Telegram pairing was not found. Start a new setup.z*Telegram setup expired. Start a new setup.z6Telegram setup was already claimed. Start a new setup.z-Telegram setup service rejected this request.z)Telegram setup service is not configured.z/Telegram could not finish bot setup. Try again.)	not_foundr9  claimedunauthorized)telegram_manager_bot_token_not_configuredtelegram_token_fetch_failedr  )r  r?  s     rE   "_telegram_onboarding_error_messagerF    s4    I?KG5`'X  
c%r   r  bearer_tokenr  rH  c                   dd l }dt          d}i }|
d|d<   ||d<   |rd| |d<   t                       | }	 |                    |                    d          	          5 } |j        | |fd
|i|}	|	                                 d d d            n# 1 swxY w Y   n# |j        $ r}
	 |
j        	                                }n# t          $ r i }Y nw xY wt          |                    d          p|                    d          pd          }t          |d          }|
j        j        dk    rdnd}|dv rd}t          ||          |
d }
~
w|j        $ r}
t          dd          |
d }
~
wt          $ r}
t          dd          |
d }
~
ww xY w	 |		                                }n$# t          $ r}
t          dd          |
d }
~
ww xY wt#          |t$                    st          dd          |S )Nr   r=  )r>  
User-AgentContent-Typer  rt   re  rf  r  ru   r  r  rr   z)Telegram setup service returned an error.r  r  >   rB  r9    r   z9Telegram setup service is unavailable. Try again shortly.z4Telegram setup service returned an invalid response.)ri  _TELEGRAM_ONBOARDING_USER_AGENTr,  rj  rk  rp   raise_for_statusHTTPStatusErrorrA  r  r@   r6  rv   rF  r   r%   RequestErrorr  r  )r  r   r  rH  ri  ru   request_kwargsr   rl  rA  r  r  r  r   r   s                  rE   !_telegram_onboarding_request_syncrR    s    LLL %5 G &(N"4!%v <#;\#;#; *,,
4d
4
4C \\%--"5"5\66 	(&%v~    !	 H %%'''	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(   M M M	\&&((FF 	 	 	FFF	FJJw''E6::h+?+?E2FF37
 
 !\5<<cc#***KFCCCL   N
 
 
 	    N
 
 
 	   I
 
 
 	
 fd## 
I
 
 
 	
 Ms   )B! #&B	B! BB! BB! !
F,CECECA8EFE,,F9FFF) )
G
3GG
c                N   K   t          j        t          | |||           d {V S )NrG  )rR   r  rR  )r  r   r  rH  s       rE   _telegram_onboarding_requestrT  a  sO       ")!         r   z(/api/messaging/telegram/onboarding/startc                   K   | j         pd                                pd}t          ddd|i           d {V }t          |                    d          pd                                          }t          |                    d          pd                                          }t          |                    d	          pd                                          }t          |                    d
          pd                                          }t          |                    d          p|                                          }t          |                    d          pd                                          }|r|r|r|st          dd          t          5  t                       t          ||t          |                    t          |<   d d d            n# 1 swxY w Y   |||||dS )Nrb   POSTz/v1/telegram/pairingsrH  )r  r7  rr   r"  r#  	deep_link
qr_payloadsuggested_usernamer  7Telegram setup service returned an incomplete response.r   )r"  r#  r$  )r7  rY  rW  rX  r#  )rH  r   rT  r6  rv   r%   _telegram_onboarding_lockr:  r!  r4  r(  )	r  rH  r  r7  r"  r#  rW  rX  rY  s	            rE   start_telegram_onboardingr\  q  sQ     /6688JNH0(#        G W[[..4"55;;==JW[[..4"55;;==JW[[..4"55;;==JGKK,,23399;;IW[[..;)<<BBDDJW[[)=>>D"EEKKMM 
Z 
z 
 
L
 
 
 	

 
# 
 
+---4N!!*:665
 5
 5
%j1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 !0    s   6G  G$'G$z//api/messaging/telegram/onboarding/{pairing_id}r7  c                   K   t           5  t                       t                              |           }|st	          dd          |j        r"d|j        |j        |j        dcd d d            S |j	        }d d d            n# 1 swxY w Y   t          ddt          j                            | d	           |
           d {V }t          |                    d          pd                                          }|dk    rIt           5  t                              |           }|r|j        nd}d d d            n# 1 swxY w Y   d|dS |dk    rt          |                    d          pd                                          }t          |                    d          pd                                          }|st	          dd          t!          |                    d                    }	t           5  t                              |           }|st	          dd          ||_        |pd |_        |	|_        d|j        |j        |j        dcd d d            S # 1 swxY w Y   |dv rYt           5  t                              | d            d d d            n# 1 swxY w Y   t	          dt%          |d                    t	          dd          )Nr  8Telegram setup session was not found. Start a new setup.r   ready)r  r&  r'  r#  r  z/v1/telegram/pairings/rr   )safe)rH  r  waiting)r  r#  r  r&  r  rZ  r'  >   rB  r9  rL  z9Telegram setup is no longer available. Start a new setup.z2Telegram setup service returned an unknown status.)r[  r:  r(  rv   r%   r%  r&  r'  r#  r"  rT  r  r  quoter6  r   r>  r  rF  )
r7  r8  r"  r  r  currentr#  r%  r&  r'  s
             rE   get_telegram_onboarding_statusrd    su     	" ' '+---.22:>> 	Q     	! & 3!'!5$/	 ' ' ' ' ' ' ' ' &
' ' ' ' ' ' ' ' ' ' ' ' ' ' '" 1J!3!3JR!3!H!HJJ        G
 X&&,"--3355F& 	? 	?377
CCG/6>++BJ	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? $:>>>G,,23399;;	7;;~66<"==CCEE 	P    4GKK4P4PQQ& 	 	266zBBF # #U     )F"."6$F#0F ! & 3!'!5$/	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	" '''& 	@ 	@)--j$???	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@5K 
 
 
 	
 C   sO   AB/BB	B
&D<<E E ;AI""I&)I&8J  J$'J$c                     	 t                      \  } }nC# t          $ r6}t                              d           dt	          |          dcY d}~S d}~ww xY w|r t                              d| j                   dd| j        dS )	az  Best-effort gateway restart after saving Telegram QR onboarding.

    The QR flow naturally pulls users into Telegram on another device. If the
    saved token waits on a separate dashboard restart click, Hermes appears
    broken from the chat side. Keep the config save authoritative, but report
    restart failures so the UI can fall back to the existing manual banner.
    z8Failed to auto-restart gateway after Telegram onboardingFr  Nz?Telegram onboarding: reusing in-flight gateway restart (pid %s)Tr  r  r  r  s      rE   *_restart_gateway_after_telegram_onboardingrf    s    
-//ff 
 
 
QRRR$ XX
 
 	
 	
 	
 	
 	
 	

  
		MH	
 	
 	

  +x  r  z5/api/messaging/telegram/onboarding/{pairing_id}/applyc                   K   g }t                      }|j        D ]R}t          |          }|st          dd          ||vr*|                    |           |                    |           S|st          dd          t          5  t                       t          	                    |           }|st          dd          |j
        }|j        }|st          dd          	 d d d            n# 1 swxY w Y   	 t          d	|           t          d
d                    |                     t          dd           nj# t          $ r$}	t          dt!          |	                    |	d }	~	wt"          $ r1}	t$                              d           t          dd          |	d }	~	ww xY wt          5  t                              | d            d d d            n# 1 swxY w Y   t+                      }
dd||
d          d|
S )Nr   z*Allowed Telegram user IDs must be numeric.r   z*Add at least one allowed Telegram user ID.r  r^  r  z Telegram setup is not ready yet.rz  r{  r  r  Tz Telegram onboarding apply failedr`  zFailed to save Telegram setup.r  )r  r  r&  needs_restart)r[   rK  r>  r%   r  r=  r[  r:  r(  rv   r%  r&  r   r  r  r  r6  r@   r=   r  r  rf  )r7  r  rK  r  raw_idrj  r8  r%  r&  r  restart_results              rE   apply_telegram_onboardingrk    s      55D' 	0 	0088
 	C    T!!HHZ   ##J/// 
?
 
 
 	

 
#  +---.22:>> 	Q    $	* 	9   	               +Y777/:J1K1KLLL
D1111 G G GCHH===3F   9:::3
 
 
 	 
# < <%))*d;;;< < < < < < < < < < < < < < < @AAN $+,=>>	 
  sJ   AC11C58C5=AE 
F(E**F(7,F##F(2GG!Gc                    K   t           5  t                              | d            d d d            n# 1 swxY w Y   ddiS )Nr  T)r[  r(  r  )r7  s    rE   cancel_telegram_onboardingrm  >  s      	" < <%))*d;;;< < < < < < < < < < < < < < <$<s   266z/api/messaging/platformsc                  |    K   t                       t                      d fdt                      D             iS )Nr  c                 2    g | ]}t          |          S r7  )r  )r
  r-  rO  r!  s     rE   r  z+get_messaging_platforms.<locals>.<listcomp>J  s5     
 
 
 ({GDD
 
 
r   )r   r"   r  )rO  r!  s   @@rE   get_messaging_platformsrp  E  sZ      **K!##G 
 
 
 
 
466
 
 
 r   z&/api/messaging/platforms/{platform_id}c                   K   t          |           }|st          dd|            t          |d                   }	 |j        D ]1}||vrt          d| d|d                    t	          |           2|j                                        D ]K\  }}||vrt          d| d|d                    |                                }|rt          ||           L|j	        t          | |j	                   d| d	S # t          $ r  t          $ r- t                              d
|            t          dd          w xY w)Nr  Unknown messaging platform: r   r~  r   z is not configurable for rO   T)r  r  z&PUT /api/messaging/platforms/%s failedr`  ro  )r  r%   r[   rE  r   rD  r%  r   r   rC  r  r@   r=   r  )r  r  r-  allowed_envr+  r  trimmeds          rE   update_messaging_platformru  Q  s     K((E 
$P;$P$P
 
 
 	
 eJ'((KM> 	" 	"C+%%# #!KKE&MKK    S!!!!(..** 	- 	-JC+%%# #!KKE&MKK    kkmmG -sG,,,<##K>>>444    M M M?MMM4KLLLLMs   B>C= =AD>z+/api/messaging/platforms/{platform_id}/testc                 8  K   t          |           }|st          dd|            t                      }t          ||t	                                }|d         s|d          d}d|d         |d	S |d
         s:d |d         D             }|rdd                    |           nd}d|d         |d	S |d         sd|d         dd	S |d         dk    rd|d         |d          dd	S |                    d          rd|d         |d         d	S d|d         dd	S )Nr  rr  r   rC  rO   z2 is disabled. Enable it, then restart the gateway.FrP   )r  rP   r  r  c                 >    g | ]}|d          
|d         |d         S )r  r?   r+  r7  )r
  fields     rE   r  z+test_messaging_platform.<locals>.<listcomp>  sE     
 
 
Z 
 */x
%L
 
 
r   r~  zMissing required setup: , zPlatform setup is incomplete.r  zEGateway is not running. Restart the gateway to connect this platform.	connectedTz is connected.r  z]Setup looks complete, but the gateway has not reported a connection yet. Restart the gateway.)r  r%   r   r  r"   r  rv   )r  r-  rO  r  r  missings         rE   test_messaging_platformr|  x  s     K((E 
$P;$P$P
 
 
 	
 **K)%>Q>S>STTG9 L6]VVVgg&67KKK<  L
 
 ,
 
 
 1;tyy'9'9;;;0 	
 gg&67KKK$% 
W%^
 
 	

 w;&&W%-777
 
 	

 {{?## 
W%/
 
 	
 !r  r      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.
    rr   z<entra-id-bearer>r!  r0  r   r   u   …N)r  r  r6  r2  r   r  )r  r~  r  s      rE   _truncate_tokenr    s      r #z%55 #""E

A
axxAGGCLLA%%HHS!R 
1vvG899r   c            
         	 ddl m} m} n# t          $ r 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}	 ddlm	} |d         j
        }n# t          t          f$ r Y nw xY w	 ddlm} n# t          $ r d}Y nw xY w	 ddlm} n# t          $ r d}Y nw xY w|D ]R}|r ||          ndpt!          j        |          }|s(|r ||          nd}	dd| |	 t          |          dddc S dddS )u/  Status for the "Anthropic API Key" catalog entry.

    Two sources, in priority order:
    1. ``~/.hermes/.anthropic_oauth.json`` — Hermes-managed PKCE flow (what
       this entry's Connect button writes)
    2. ``ANTHROPIC_API_KEY`` → ``ANTHROPIC_TOKEN`` → ``CLAUDE_CODE_OAUTH_TOKEN``
       env vars (registry order) — from ``.env``, the shell, or an external
       secret source like Bitwarden (whose keys are injected into the process
       env during ``load_hermes_dotenv()``, so the same check covers them)

    Claude Code's ``~/.claude/.credentials.json`` is deliberately NOT read
    here — it has its own dedicated catalog entry (``claude-code`` →
    ``_claude_code_only_status``). Reporting it under the API-key entry
    double-counts the token and shadows a real ANTHROPIC_API_KEY.
    r   )read_hermes_oauth_credentials_HERMES_OAUTH_FILENaccessTokenThermes_pkcezHermes PKCE (r8  	expiresAtrefreshTokenr  rZ  source_labeltoken_previewr#  has_refresh_token)ANTHROPIC_API_KEYANTHROPIC_TOKENCLAUDE_CODE_OAUTH_TOKEN)PROVIDER_REGISTRY	anthropicget_env_value)format_secret_source_suffixrr   env_varFr  rZ  )agent.anthropic_adapterr  r  ImportErrorr@   rv   r  r  r  r  api_key_env_varsKeyErrorhermes_cli.configr  hermes_cli.env_loaderr  rU   rV   )
r  r  hermes_credsenv_var_orderr  r  r  varr  r  s
             rE   _anthropic_oauth_statusr    s    "	
 	
 	
 	
 	
 	
 	
 	
 	
  " " "(,%!" 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
 
 	
 _M555555)+6G"   3333333   +EEEEEEE + + +&*###+  
 
'4>s###$Q29S>> 	5PX,,S111VX",F,,,U33!&
 
 	
 	
 	
 $///sN    
/ >>;C C#"C#'C. .C=<C=D DD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   )read_claude_code_credentialsNr  Tclaude_code_cliz~/.claude/.credentials.jsonr  r  r  Fr  )r  r  r@   rv   r  r  )r  credss     rE   _claude_code_only_statusr    s    HHHHHH,,..    
=)) 
'9,UYY}-E-EFF))K00!%eii&?&?!@!@
 
 	
 $///s    ""r
  Nous Portaldevice_codezhermes auth add nouszhttps://portal.nousresearch.com)r  rO   flowcli_commandr}  	status_fnopenai-codexzOpenAI OAuth (ChatGPT)zhermes auth add openai-codexz https://platform.openai.com/docs
qwen-oauthzQwen (via Qwen CLI)externalz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	xai-oauthz%xAI Grok OAuth (SuperGrok / Premium+)loopbackzhermes auth add xai-oauthz@https://hermes-agent.nousresearch.com/docs/guides/xai-grok-oauthr  zAnthropic API Keypkcezhermes auth add anthropicz.https://docs.claude.com/en/api/getting-startedclaude-codezAAnthropic OAuth: Required Extra Usage Credits to Use Subscriptionzclaude setup-tokenz+https://docs.claude.com/en/docs/claude-code_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 | d#k    r|                                }t          |                    d                    |                    d          pd$|                    d%          p|                    d          pd&t          |                    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)r  r  r   r}   r
  r  nous_portalr  r  access_tokenaccess_expires_atr  r  r  rZ  openai_codex	auth_modezOpenAI Codexr:  last_refresh)r  rZ  r  r  r#  r  r  r  qwen_cliauth_store_pathzQwen CLIr#  r  minimax_oauthz	MiniMax (regionglobalr8  Tr  	xai_oauth
auth_storezxAI Grok OAuth)r@   r6  r  r}   r  r  rv   r  get_codex_auth_statusget_qwen_auth_statusget_minimax_oauth_auth_statusget_xai_oauth_auth_status)r  r  rD   hauthr  s        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%)   +%%1133C
 "#''+"6"677''(++:{ # 5 5 ^9J9J ^N^!01C1C!D!D"%) # 7 7   &  5 5 5"SVV444444445sN   	 
4/44B0M )B M 
B*M 5A-M #B5M 
N $M;5N ;N z/api/providers/oauthc            
         K   g } t           D ]e}t          |d         |                    d                    }|                     |d         |d         |d         |d         |d         |d           fd| iS )	u0  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" | "loopback"
        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  rO   r  r  r}  )r  rO   r  r  r}  r  r  )r  r  rv   r=  )r  r  r  s      rE   list_oauth_providersr    s      $ I$ 	 	)!D'1553E3EFFD'fIfI]+*
 
 	 	 	 	 ##r   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  r7  r  s     rE   r  z,disconnect_oauth_provider.<locals>.<setcomp>  s    :::Q4:::r   r   zUnknown provider: . Available: ry  r   >   r  r  r   r  )clear_provider_authr  zoauth/disconnect: %sT)r  ro  z!oauth/disconnect: %s (cleared=%s)zdisconnect %s failedr`  N)r   r  r%   r  r9  r  r  r"  r  r@   r  r  r=   r>   r  r  r6  )r  rp   	valid_idsr  r  clearedrD   s          rE   disconnect_oauth_providerr    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_pkcez!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_atr7  )r
  r  sessru  s      rE   r  z&_gc_oauth_sessions.<locals>.<listcomp>Y  s-    ]]]dlASV\A\A\A\A\A\r   N)r  _OAUTH_SESSION_TTL_SECONDS_oauth_sessions_lockr  r%  r  )staler  ru  s     @rE   _gc_oauth_sessionsr  U  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  ro  r  r  r  r  )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  r  r  Tr^  z.tmp.r!     wr  r  r0  )indentNPooledCredential	load_poolAUTH_TYPE_OAUTHSOURCE_MANUALr  c                 b    g | ]+}t          |d d                               d          )|,S )rZ  rr   :dashboard_pkcer   r   r
  rD   r  s     rE   r  z/_save_anthropic_oauth_creds.<locals>.<listcomp>  sB    xxx!Hb1I1I1T1TXeUvUvUv1w1wxAxxxr   r  rr   r}  dashboard PKCEr  )	ro  r  r  	auth_typepriorityrZ  r  r  r  z)anthropic pool add (dashboard) failed: %s)(r  r  r#  ra  	with_namerO   rU   getpidr  	token_hexr  r  r  dumpsflushfsyncfilenor)  chmodr  S_IRUSRS_IWUSRr  r"  r  agent.credential_poolr  r  r  r  uuidr  remove_entryr   r@   uuid4hex	add_entryr=   rD  )r  r  r  r  r  tmp_pathr  r  r  r  r  poolr  rD   r-  r  s                  @rE   _save_anthropic_oauth_credsr  n  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)r   r  r  verifierrP   truecodeS256)r  	client_idresponse_typeredirect_urirn  code_challengecode_challenge_methodrP   ?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  r  	urlencoder  )r  	challenger  r  rg  r  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	  r   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.Nro  r  r  r  r  zUnknown or expired sessionr   r  r  Fr  )r  r  r  #r   r   r  zNo code providedrr   authorization_coderP   r  )
grant_typer  r  rP   r  code_verifierr=  zhermes-dashboard/1.0)rK  rJ  rV  )r  ru   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  rv   r%   r   r&  r  r  r  r  r   rz   r  rp   r&   _ANTHROPIC_OAUTH_TOKEN_URLr  r  r  rU  r@   r  r  r  r=   r>   )r  r&  r  r  r  state_from_callbackexchange_datar  r  r7  rD   r  r  r  r  s                  rE   _submit_anthropic_pkcer1    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} ddl|d         }t	          j        d          pt	          j        d          p|j                            d          |j        |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     rt.          5  t0                              |          }
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     t.          5  t0                              |i           }
ddd           n# 1 swxY w Y   |
                    d          dk    r&t7          d|
                    d          pd !          |
                    d          st7          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.|d/<   t          |d0                   }||d1<   |d2k    r7|d3z  }tC          dt          |t!          j                    z
                      }nt!          j                    |z   }|}||d<   t#          j        tD          |fdd|dd                                                     |d	t          |d                   t          |d4                   |tC          d5|d+         pd6d7z            dS t7          d8d9|  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   )_request_device_coder  NHERMES_PORTAL_BASE_URLNOUS_PORTAL_BASE_URLrx  c                                                               d          ddi          5 }  |           fcd d d            S # 1 swxY w Y   d S )N      .@r>  r=  r  ru   )rl  r  r  rn  rj  rk  )rl  r3  r  ri  r  rn  s    rE   _do_nous_device_requestz8_start_device_code_flow.<locals>._do_nous_device_request$  s    d++!#56     ((%(7"+#	   	                 s   AAAr  r6   r  r#  r  r  rn  Tzoauth-poll-r}  rK   	user_codeverification_uri_complete)r  r  r;  verification_urlr  poll_intervalr  zoauth-codex-r  r  r  rE  r  r`  r  zdevice-auth failedr   i  z2device-auth timed out before returning a user coder=  r  ri   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 )Nr7  r>  r=  Tr  ru   follow_redirects)rl  r  r  r  rP   r9  )rl  rA  r@  r$  ri  r  rP   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+  rP   r  r  
expired_inexpired_in_rawl    J)g     @@verification_urir0  rQ  r,  r   z	Provider z" does not support device-code flow)#r  r3  r  ri  rU   rV   r  r  r  rn  rR   r  r  r  r6  r  r  rW   rY   _nous_pollerrZ   _codex_full_login_worker	monotonicr  r  rv   sleepr%   r?  r@  rA  rB  get_event_loopr  _minimax_poller)r  r  pconfigr:  device_dataeffective_scoper  r  r#  deadliner  r?  rB  r  rG  interval_rawrJ  r$  expires_in_secondsrA  r@  r3  r$  r  ri  r  rn  rP   s                      @@@@@@@@@rE   _start_device_code_flowrX    s      f	
 	
 	
 	
 	
 	
 	
 	
 	#F+I.// 'y/00'&
&++	 	
 %		 	 	 	 	 	 	 	 	 .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$   I..I25I2K==LLg     r@c            	         ddl m}  |                                 }|                                 \  }}}}	 |                     |           |                                 }|                     |          }t          j        d          }t          j        d          }	| 	                    |d         ||||	          }
nq# t          $ rd 	 |                                 |                                 n# t          $ r Y nw xY w	 |                    d           n# t          $ r Y nw xY w w xY wt          dd	          \  }}||d
<   ||d<   ||d<   ||d<   ||d<   ||d<   ||d<   |d         |d<   ||d<   t          j                    t           z   |d<   t#          j        t&          |fdd|dd                                                     |d	|
t+          t                     dS )a  Begin the xAI loopback PKCE flow.

    Binds the local callback server, builds the authorize URL, and spawns a
    background worker that waits for the redirect and finishes the exchange.
    Returns the authorize URL for the client to open in the browser.
    r   r  r  authorization_endpoint)rZ  r  r  rP   nonce      ?r  r  r  serverthreadcallback_resultr  r  r$  rP   token_endpoint	discoveryr#  Tz
oauth-xai-Nr}  rK   r  )r  r}   _xai_oauth_discovery_xai_start_callback_server#_xai_validate_loopback_redirect_uri_oauth_pkce_code_verifier_oauth_pkce_code_challenger  r  _xai_oauth_build_authorize_urlr@   shutdownserver_closer  r  r  _XAI_LOOPBACK_TIMEOUT_SECONDSrW   rY   _xai_loopback_workerrZ   r  )r  ra  r]  r^  r_  r  r  r$  rP   r[  authorize_urlr  r  s                rE   _start_xai_loopback_flowrm    sa    )(((((**,,I494T4T4V4V1FFO\11,???224444X>>	!"%%!"%%<<#,-E#F%$ = 
 
    	OO!!!! 	 	 	D		KKK$$$$ 	 	 	D	 #;
;;ICDNDN-D	'DD!DDM&'78D	!D'DDD#3&##bqb'##   eggg!788	  sT   BB< <
D*(C0/D*0
C=:D*<C==D*DD*
D%"D*$D%%D*c           	          ddl m }m} ddlm} t          5  t
                                         }ddd           n# 1 swxY w Y   |sdS dt          ddf fd}dt          f fd}	 |	                    |d	         |d
         |d         t                    }n&# t          $ r} |d|            Y d}~dS d}~ww xY w |            rdS |                    d          r-|                    d          p|d         }	 |d|	            dS |                    d          |d         k    r |d           dS t          |                    d          pd                                          }
|
s |d           dS 	 |                    |d         |
|d         |d         |d                   }t          |                    dd          pd                                          }t          |                    dd          pd                                          }|r|s |d           dS |                    t          j        dd                                                              d          p9t          j        d d                                                              d          |j        !          } |j        |j                                                                      d"d#          }||t          |                    d$d          pd                                          |                    d%          t          |                    d&          pd'                                          pd'd(} |            rdS |                    ||                    d)          |d         |*           t1          ||||           n&# t          $ r} |d+|            Y d}~dS d}~ww xY wt          5  t
                                         }|d,|d-<   ddd           n# 1 swxY w Y   t2                              d.            dS )/zFWait for the xAI loopback callback, exchange the code, persist tokens.r   r   r  Nr  r7   c                     t           5  t                                        }|
d|d<   | |d<   d d d            d S # 1 swxY w Y   d S )Nr  r  r  )r  r  rv   )r  r  r  s     rE   _failz#_xai_loopback_worker.<locals>._fail  s    ! 	- 	-##J//A}%(%,/"		- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	-s   '=AAc                  X    t           5   t          vcd d d            S # 1 swxY w Y   d S rE  )r  r  )r  s   rE   
_cancelledz(_xai_loopback_worker.<locals>._cancelled  sw    
 " 	5 	5_4	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5s   	##r]  r^  r_  )timeout_secondszxAI authorization timed out: r  error_descriptionzxAI authorization failed: rP   z)xAI authorization failed: state mismatch.r  rr   z5xAI authorization failed: missing authorization code.r`  r  r  r$  )r`  r  r  r+  r  r  r  z6xAI token exchange did not return the expected tokens.HERMES_XAI_BASE_URLrx  XAI_BASE_URL)r?  r/  r.  id_tokenr  
token_typeBearer)r  r  rw  r  rx  ra  )ra  r  r  zxAI token exchange failed: r-  r  z6oauth/loopback: xai-oauth login completed (session=%s))r   r   r  r}   r  r  rv   r6  r  _xai_wait_for_callbackrj  r@   r   #_xai_oauth_exchange_code_for_tokens _xai_validate_inference_base_urlrU   rV   r  DEFAULT_XAI_OAUTH_BASE_URLr  r2  	isoformatr)  _save_xai_oauth_tokens_add_xai_oauth_pool_entryr=   r>   )r  r   r   r  r  rp  rr  callbackr  r   r  r  r  r  rq  r  tokensr  s   `                 rE   rk  rk    s   ++++++++((((((	 / /"":../ / / / / / / / / / / / / / / -s -t - - - - - -5 5 5 5 5 5 5	//NN"#9	 0 
 
    3c33444 z|| ||G 122Ghw6G3633444||GW--9:::x||F##)r**0022D EFFF%;; 01n-z*, < 
 
 7;;~r::@bAAGGIIGKK<<BCCIIKK 	= 	EJKKKF99I+R006688??DD Ay,,2244;;C@@5 : 
 

 $x|HL11;;==EEhPSTT(*GKK
B77=2>>DDFF!++l33gkk,77C8DDJJLLXPX
 
 :<< 	F$$hh{++n-%	 	% 	
 	
 	
 	",xVVVV   1C11222 
 % %
++=$AhK% % % % % % % % % % % % % % % 	IIF
SSSSSs`   >AA*0B 
B>%B99B>B/O 2EO AO 
O5O00O5?"P--P14P1r  c                    	 ddl }ddlm}m}m}m  |d          }fd|                                D             }	|	D ]7}
	 |                    t          |
dd                     (# t          $ r Y 4w xY w |d|
                                j        dd         d	|d d
| |||
  
        }|                    |           dS # t          $ r&}
t                              d|
           Y d}
~
dS d}
~
ww xY w)zMirror `hermes auth add xai-oauth`'s credential-pool insert.

    Best-effort: the auth-store write in _save_xai_oauth_tokens is the source
    of truth for runtime resolution; the pool entry only matters for the
    rotation strategy.
    r   Nr  r  c                 b    g | ]+}t          |d d                               d          )|,S )rZ  rr   :dashboard_xai_pkcer  r  s     rE   r  z-_add_xai_oauth_pool_entry.<locals>.<listcomp>w  sR     
 
 
q(B''22m3X3X3XYY

 
 
r   r  rr   r}  r  r  )
ro  r  r  r  r  rZ  r  r  rq  r  z)xai-oauth pool add (dashboard) failed: %s)r  r  r  r  r  r  r  r  r   r@   r	  r
  r  r=   rD  )r  r  rq  r  r  r  r  r  r  r  rD   r-  r  s               @rE   r  r  d  s   !E	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 y%%
 
 
 
||~~
 
 
  	 	A!!'!T2"6"67777      zz||#"%#888%'%
 
 
 	u E E E@!DDDDDDDDDEs<   ?C $A('C (
A52C 4A55AC 
C6C11C6c                    ddl 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        |j                  }t          |                    d          pd          }||                    d          ||                    d	          p||                    dd          |d         |                    d          |                                |r> |j        |                                |z   |j                                                  n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<   t1          |          |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   )_poll_for_tokenrefresh_nous_oauth_from_stater   Nr  r  r  r6   rn  r4   r#  r7  r>  r=  r8  )rl  r  r  r  r  r>  r  r  rx  ry  r  r  tz)
r  r  r  rn  rx  r  r  obtained_atr#  r  F)rs  force_refresh)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   ri  r  r  rv   r  r  r  rj  rk  r  r2  r~  fromtimestampr3  r  r=   r>   r@   rD  r6  )r  r  r  r   r   ri  r  r  r  r  r6   rn  r  rl  
token_datar  	token_ttl
auth_state
full_stater  rD   s                        rE   rL  rL    s]           ,+++++++LLL	 / /"":../ / / / / / / / / / / / / / / ,-O[!I}%KJHHHWERT,/$)++=>>??J*+\\%--"5"5J\?]\^^ 	bh( /#'%&  J	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 hl8<((
|449::	.",..1E"F"F"^^G,,5$..x@@&~6'^^O<<==?? '&&s}}'Bx|T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! 'H9-I! 9H==I!  H=I! !
K+#K	J2&K	2J6	6K	9J6	:K		Kc                 @   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        |j                  } |t!          |d                   |          }t#          dt!          ||                                z
                      }d|	                    dd          |
||||                    dd          |d         |d         |                    d          |                                 |j        ||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  r;  r+  rH  rJ  r7  r>  r=  TrE  )rl  r  r  r;  r+  rI  rH  rI  )r  r  r  r  rx  ry  r  r  resource_urlr  )ro  r  r  r  r  rn  rx  r  r  r  r  r#  r  r-  r  z2oauth/device: minimax login completed (session=%s)z0minimax device-code poll failed (session=%s): %sr  r  )r  r  r  r  r  r  r   r   ri  r  r  rv   rj  rk  r  r2  r  r  r3  r~  r  r=   r>   r@   rD  r6  )r  r  r  r  r  r  r   r   ri  r  r  r  r;  r+  rH  rJ  rl  r  r  r$  expires_in_sr  rD   s                          rE   rQ  rQ    sS                 ,+++++++LLL	 / /"":../ / / / / / / / / / / / / / / ,-O[!I[!I)M((=))K*+N2+\\MM$''12!  
 
 	 ,, /##+)'  J	 	 	 	 	 	 	 	 	 	 	 	 	 	 	& hl8<((::
<())s
 
 
 1c-#--//"ABBCC'hhx22."@"($..x@@&~6'8&NN>::==??0(0(,  ikk&
 

" 	! ,,,! 	( 	('DN	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(		F
SSSSS + + +GUVWWW! 	+ 	+$DN$'FFD!	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	++s   A		AA-H0 C"H0 "C&&H0 )C&*DH0 6H<H0 HH0 HH0 0
J:#JJ5JJ	JJ		JJc           
      Z	   	 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}  |||d.           t          5  d/|d<   ddd           n# 1 swxY w Y   t(                              d0|            dS # t,          $ r}t(                              d1| |           t          5  t                              |           }|rd2|d<   t1          |          |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)3u  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.comr7  r  z!/api/accounts/deviceauth/usercoder  rK  r=  )r  ru   r  zdeviceauth/usercode returned r;  rr   device_auth_id   r6   5z8device-code response missing user_code or device_auth_idz/codex/devicer=  r  r  r#  z/api/accounts/deviceauth/token)r  r;  >   r  r  zdeviceauth/token poll returned r9  r  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  ru   ztoken exchange returned r  r  z*token exchange did not return access_token)_save_codex_tokens)r  r  r-  z7oauth/device: openai-codex login completed (session=%s)z0codex device-code worker failed (session=%s): %sr  )ri  r  r  r  r  rj  rk  postr   r  r  rv   r  r  r  r  r  rN  rO  r  r=   r>   r@   rD  r6  )r  ri  r  r  r  issuerrl  r  rS  r;  r  r>  r=  r  rU  	code_respr  r)  r+  
token_respr  r  r  r  rD   r  s                             rE   rM  rM  #  s   d,	
 	
 	
 	
 	
 	
 	
 	
 	
 	

 + \\%--"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666666(*
 
 	 	 	 " 	( 	('DN	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(		KZXXXXX , , ,GUVWWW! 	, 	,##J//A ,%(%(VV/"		, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	,,s)  9P!  A'P! 'A++P! .A+/B5P! $FP! 8FP! FP! FA
P! %BI>2P! >JP! JP! J)P! )J--P! 0J-1P! 6A(P! $MP! MP! MBP! 'O9-P! 9O==P!  O=P! !
R*+#R%4RR%R	R%R	R%%R*z(/api/providers/oauth/{provider_id}/startc                     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 |d         dk    r8 dk    r2t          j                    	                    d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  r7  r  s     rE   r  z$start_oauth_login.<locals>.<setcomp>  s    666QtW666r   r   zUnknown provider r   c              3   4   K   | ]}|d          k    |V  dS )r  Nr7  )r
  r  r  s     rE   r  z$start_oauth_login.<locals>.<genexpr>  s1      VVqqw+?U?U?U?U?U?UVVr   r  r  z uses an external CLI; run `r  z
` manuallyr  r  r  Nr  r  zoauth/start %s failedr`  zUnsupported flow)r   r  r  r%   nextr%  rX  rR   r  r  rm  r@   r=   r  r6  )r  rp   validcatalog_entryrD   s   `    rE   start_oauth_loginr    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========= J..;+3M3M 133CC.             < < <.<<<CFF;;;;< C0B
C
C
CCs%   D 4 D AD E,.9E''E,c                   $    e Zd ZU eed<   eed<   dS )OAuthSubmitBodyr  r  NrV  r7  r   rE   r  r    s"         OOO
IIIIIr   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  Nr   zsubmit not supported for r   )r   rR   r  r  r1  r  r  r%   )r  r  rp   s      rE   submit_oauth_coder    s       7k!!-//??($/49
 
 
 
 
 
 
 
 	
 C0YK0Y0Y
Z
Z
ZZr   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 )u*  Poll a session's status (no auth — read-only state).

    Shared by the device-code flows (Nous, OpenAI Codex, MiniMax) and the
    loopback flow (xAI Grok). Both surface progress through the same
    background-worker-updated ``status`` field, so a single poll endpoint
    serves them all.
    Nr  zSession not found or expiredr   ro  r   zProvider mismatch for sessionr  r  r#  )r  r  r  r#  )r  r  rv   r%   )r  r  r  s      rE   poll_oauth_sessionr    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k    r|                    d          }t          |t                    r|                    d          pd	|d<   |                    d
          }|                    d          }	 |(|                                 |                                 n# t          $ r Y nw xY w	 ||
                    d           n# t          $ r Y nw xY wd| dS )z0Cancel a pending OAuth session. Token-protected.NFzsession not found)r  r  r  r  r_  r  	cancelledr]  r^  r\  r  T)r  r  )r   r  r  r  rv   r  r  rh  ri  r@   r  )r  rp   r  r7  r]  r^  s         rE   cancel_oauth_sessionr    s      7	 5 5"":t445 5 5 5 5 5 5 5 5 5 5 5 5 5 5|(;<<<
 xx:%% +,,fd## 	A$jj11@[F7O(##(##	!!!!##%%% 	 	 	D		!C((( 	 	 	D	j111s5   AAA*D 
DDD0 0
D=<D=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   r  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rE  )r  r  rv   r@   )r  r+  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  r  r   r  r0  )r  r  r  '  r  rV  c                 n    	 t          |                     d          pd          S # t          $ r Y dS w xY w)Nr  r           )r  rv   r@   )r  s    rE   startedz+_session_latest_descendant.<locals>.startedF  sH    SWW\227a888   sss   #& 
44c                 B    g | ]}|                     d           v|S r  r  )r
  rr  s     rE   r  z._session_latest_descendant.<locals>.<listcomp>Q  s-    RRR!%%++T:Q:Q!:Q:Q:Qr   Tr  )r  r  resolve_session_idr  r   r   executefetchallr=  r  rv   r  r  r  )r  r  r  r"  r  r  r  raw_rowsr  childrenridr#  r  rc  r   
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c                   &    e Zd ZU ee         ed<   dS )BulkDeleteSessionsr_  NrL  r7  r   rE   r  r  h  s         	cNNNNNr   r  z/api/sessions/bulk-deletec                   K   t          | j                  dk    rt          dd          ddlm}  |            }	 |                    | j                  }d|d|                                 S # |                                 w xY w)	u  Delete every session in ``body.ids`` in a single DB transaction.

    Backs the dashboard's bulk-select-and-delete flow on the sessions
    page. POST (not DELETE) because most HTTP clients refuse to send a
    request body on DELETE and a body is the natural shape for a list
    of IDs — Starlette accepts both, but POSTing a list keeps proxies,
    curl, and the browser ``fetch`` API consistent.

    Per-row contract matches :meth:`SessionDB.delete_sessions`:

    * Unknown IDs are silently skipped (the response ``deleted`` count
      reflects what really happened, not the input length). This is
      deliberate — UI selection state can race against another tab's
      delete, and we'd rather succeed-on-the-rest than fail-the-whole-
      batch.
    * Children of every deleted parent are orphaned, not cascade-
      deleted.
    * Active and archived sessions ARE deleted when explicitly
      selected — unlike ``DELETE /api/sessions/empty``, the user
      hand-picked the rows so we trust the selection.
    * Like the other session-delete endpoints, this does NOT pass a
      ``sessions_dir`` through; on-disk transcript / request-dump
      cleanup runs at the CLI/agent layer on the next prune pass.

    The response carries the actual deleted count, so the dashboard
    can surface it in a toast. The IDs that were removed are not
    echoed back because the client already knows what it asked to
    delete (unknown IDs are silently skipped — see contract above)
    and can prune its in-memory list directly from the request.
    r`  r   z$ids must contain at most 500 entriesr   r   r  Tr  deleted)r  r_  r%   r  r  delete_sessionsr   )r  r  r"  r  s       rE   bulk_delete_sessions_endpointr  l  s      J 48}}s9
 
 
 	
 '&&&&&	B$$TX..w//








s   A0 0Bz/api/sessions/empty/countc                     K   ddl m}   |             }	 d|                                i|                                 S # |                                 w xY w)u   Return the number of empty, ended, non-archived sessions.

    Drives the dashboard's "Delete empty (N)" button — when N is 0 the
    UI hides the affordance so users aren't presented with a button
    that does nothing. Cheap, single-COUNT query.
    r   r  r2  )r  r  count_empty_sessionsr   )r  r"  s     rE   count_empty_sessions_endpointr    s_       '&&&&&	B00223








s	   > Az/api/sessions/emptyc                     K   ddl m}   |             }	 |                                }d|d|                                 S # |                                 w xY w)uX  Delete every empty (``message_count == 0``), ended,
    non-archived session in a single transaction.

    Safety contract mirrors :meth:`SessionDB.delete_empty_sessions`:

    * Active sessions are skipped (``ended_at IS NULL``) so a live
      agent isn't yanked mid-handshake.
    * Archived sessions are skipped — the user explicitly chose to
      keep those rows.
    * Children of deleted parents are orphaned, not cascade-deleted.

    Like the single-session ``DELETE /api/sessions/{id}`` endpoint
    below, this doesn't pass a ``sessions_dir`` through — the on-disk
    transcript / request-dump cleanup is wired at the CLI/agent layer
    but the web server historically leaves file cleanup to the next
    prune-on-startup pass. Matching that pre-existing trade-off keeps
    the two delete endpoints' DB-vs-disk behaviour consistent.
    r   r  Tr  )r  r  delete_empty_sessionsr   )r  r"  r  s      rE   delete_empty_sessions_endpointr    sg      ( '&&&&&	B**,,w//








s   A Az/api/sessions/statsc                    K   ddl m}   |             }	 |                    d          }|                    d          }|                    d          }|                                }i }	 |                    dd          D ]B}t          |                    d	          pd
          }|                    |d          dz   ||<   Cn# t          $ r Y nw xY w|||||d|                                 S # |                                 w xY w)zSession-store statistics for the Sessions page (mirrors `hermes sessions stats`).

    Registered before ``/api/sessions/{session_id}`` so the literal ``stats``
    path isn't captured as a session id by the parameterized route.
    r   r  T)ri  F)rj  r  r  rZ  r.  r   )r?  active_storerX  messages	by_source)	r  r  rp  message_countr  r6  rv   r@   r   )	r  r"  r?  r  rX  r  r  r  srcs	            rE   get_session_statsr    sH      '&&&&&	B  $ 77'''??##$#77##%%$&		***NN ; ;!%%//2U33!*sA!6!6!:	#;  	 	 	D	 (  "
 
 	







s1   AC4 -AC C4 
CC4 C
C4 4D
c                     ddl m} | s
 |            S t          |           \  }} |t          |          dz            S )uk  Open a SessionDB for read paths, optionally for another profile.

    ``profile`` None/empty → this process's own ``state.db`` (the common,
    single-profile case). A named profile opens that profile's on-disk
    ``state.db`` directly so the primary backend can serve cross-profile reads
    (transcripts, detail) without spawning that profile's backend.
    r   r  ry  )rz  )r  r  r  r	   )r1  r  _namer>  s       rE   _open_session_db_for_profiler    sY     '&&&&& y{{$W--KE49T$ZZ*45555r   z/api/sessions/{session_id}c                 :  K   t          |          }	 |                    |           }|r|                    |          nd }|st          dd          |rt	          |          d         |d<   ||                                 S # |                                 w xY w)Nr  Session not foundr   r   r1  )r  r  r  r%   r  r   )r  r1  r"  r  r   s        rE   get_session_detailr    s      	%g	.	.B	##J//),6"..%%%$ 	MC8KLLLL 	@!3G!<!<Q!?GI








s   AB 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  r   r   )requested_session_idr  r   r;  )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	  r   z#/api/sessions/{session_id}/messagesc                 .  K   t          |          }	 |                    |           }|st          dd          |                    |          }|                    |          }||d|                                 S # |                                 w xY w)Nr  r  r   )r  r  )r  r  r%   resolve_resume_session_idget_messagesr   )r  r1  r"  r  r  s        rE   get_session_messagesr    s      	%g	.	.B##J// 	MC8KLLLL**3//??3''!x88








s   AA> >Bc                    K   t          |          }	 |                    |           st          dd          ddi|                                 S # |                                 w xY w)Nr  r  r   r  T)r  delete_sessionr%   r   )r  r1  r"  s      rE   delete_session_endpointr  &  sm      
 
&g	.	.B  ,, 	MC8KLLLLd|








s   )A A'c                   ^    e Zd ZU dZee         ed<   dZee         ed<   dZ	ee         ed<   dS )SessionRenameNrc   rX  r1  )
r2  r3  r4  rc   r   r6  r5  rX  r  r1  r7  r   rE   r  r  4  sP         E8C=#Hhtn### "GXc]!!!!!r   r  c                 |  K   t          |j                  }	 |                    |           }|st          dd          |j        |j        t          dd          |j        O	 |                    ||j        pd           n0# t          $ r#}t          dt          |                    d}~ww xY w|j        |	                    ||j                   d|
                    |          pdd	}|j        t          |j                  |d
<   ||                                 S # |                                 w xY w)a   Update a session: rename (or clear its title) and/or archive it.

    ``title`` renames (empty/null clears the title); ``archived`` soft-hides or
    restores the session. Either field may be omitted. ``profile`` targets
    another profile's session.
    r  r  r   Nr   z5Nothing to update; provide 'title' and/or 'archived'.rr   T)r  rc   rX  )r  r1  r  r%   rc   rX  set_session_titler  r6  set_session_archivedget_session_titler  r   )r  r  r"  r  rD   r7  s         rE   rename_session_endpointr  <  sf      
&dl	3	3B##J// 	MC8KLLLL:$-"7N    :!D$$S$**:;;;; D D D#CFFCCCCD =$##C777r';';C'@'@'FBGG=$!%dm!4!4F:








s1   AD% 'B D% 
B2B--B22AD% %D;z!/api/sessions/{session_id}/exportc                 &  K   ddl m}  |            }	 |                    |           }|st          dd          |                    |          }|t          dd          ||                                 S # |                                 w xY w)z6Export a single session (metadata + messages) as JSON.r   r  r  r  r   )r  r  r  r%   export_sessionr   )r  r  r"  r  r  s        rE   export_session_endpointr  ^  s       '&&&&&	B	##J// 	MC8KLLLL  %%<C8KLLLL








s   AA: :Bc                   8    e Zd ZU dZeed<   dZee         ed<   dS )SessionPruneZ   older_than_daysNrZ  )	r2  r3  r4  r   r  r5  rZ  r   r6  r7  r   rE   r  r  p  s6         OS FHSM     r   r  z/api/sessions/prunec                 Z  K   | j         dk     rt          dd          ddlm}  |            }	 t	                      dz  }|                    | j         | j        pd|                                r|nd	          }d
|d|                                 S # |                                 w xY w)zJDelete ended sessions older than N days (mirrors `hermes sessions prune`).r   r   zolder_than_days must be >= 1r   r   r  r#  N)r   rZ  sessions_dirT)r  rp  )	r   r%   r  r  r   prune_sessionsrZ  r"  r   )r  r  r"  r  rp  s        rE   prune_sessions_endpointr  u  s       a4RSSSS&&&&&&	B	&((:5## 0K'4)5)<)<)>)>HD $ 
 

 w//








s   AB B*z	/api/logsr  filelevel	componentr)  c                   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_FILESr   zUnknown log file: r   r  )r  r  )COMPONENT_PREFIXESALLrs  zUnknown component: r  ry  r`  rQ  )has_filters	min_levelcomponent_prefixesc                 @    g | ]}|                                 v |S r7  r  )r
  lneedles     rE   r  zget_logs.<locals>.<listcomp>  s+    ;;;v':':!':':':r   )hermes_cli.logsr	  r
  rv   r%   r   r"  hermes_loggingr  r  r  r   r  r9  r  r  )r  r  r  r  r)  r	  r
  log_namer  r  r  comp_prefixesr  r7  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                   f    e Zd ZU eed<   eed<   dZeed<   dZeed<   dZee	e                  ed<   dS )	CronJobCreater1   schedulerr   rO   r   deliverNr   )
r2  r3  r4  r6  r5  rO   r  r   r   r   r7  r   rE   r  r    sZ         KKKMMMD#NNNGS"&FHT#Y&&&&&r   r  c                       e Zd ZU eed<   dS )CronJobUpdater  N)r2  r3  r4  r  r5  r7  r   rE   r  r             MMMMMr   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   ru  c                 ,    g | ]}t          |          S r7  _profile_to_dictr  s     rE   r  z'_cron_profile_dicts.<locals>.<listcomp>  s!    JJJ ##JJJr   zJFailed to list profiles for cron dashboard; falling back to directory scanr  rv  r  r@   r=   r  _fallback_profile_dictsr  s    rE   _cron_profile_dictsr&    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c                 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   ru  r   r   r   Nr  	Profile '' does not exist.)
r  rv  r   normalize_profile_namevalidate_profile_namer  r%   r6  profile_existsr  )r1  r  r  canonrD   s        rE   r  r    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jobr>  c                 n    t          |           }||d<   ||d<   t          |          |d<   |dk    |d<   |S )Nr1  profile_namer  r   r|  )r  r6  )r.  r1  r>  	annotateds       rE   _annotate_cron_jobr2    sG    S		I"Ii 'In"4yyIm&-&:I"#r   	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 r7  )r2  )r
  jr>  r0  s     rE   r  z*_call_cron_for_profile.<locals>.<listcomp>  s&    JJJa"1lD99JJJr   )r  _CRON_PROFILE_LOCKr   r5  CRON_DIR	JOBS_FILE
OUTPUT_DIRr   r  r  r  r2  )r1  r3  rM   kwargs	cron_jobsold_cron_dirold_jobs_fileold_output_dirr7  r>  r0  s            @@rE   _call_cron_for_profilerB    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 )NrO   rr   	list_jobsTc              3   |   K   | ]6}|                     d           k    p|                     d          k    V  7dS )r  rO   Nr  )r
  r8  rC  s     rE   r  z)_find_cron_job_profile.<locals>.<genexpr>  sF      NNAquuT{{f$?f(?NNNNNNr   )r&  r6  rv   rB  rN  )rC  r1  rO   r5  s   `   rE   _find_cron_job_profilerG    s    &((  7;;v&&,"-- 	%dK>>NNNNNNNNN 	KKK	4r   z/api/cron/jobsc                   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 )Nrs  rE  TrO   rr   z'Failed to list cron jobs for profile %s)
r   r   rB  r&  r6  rv   extendr@   r=   r  )r1  	requestedr5  r  rO   s        rE   list_cron_jobsrK  $  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 foundr   get_jobrG  r%   rB  rC  r1  selectedr.  s       rE   get_cron_jobrR  6  sg      8088H EODDDD
 9f
=
=C EODDDDJr   z/api/cron/jobs/{job_id}/runsc           	        K   |pt          |           }| }|r=t          |d|           }|r*|                    d          rt          |d                   }	 t	          dt          t          |          d                    }n# t          t          f$ r d}Y nw xY wt          |          }	 |
                    ||d          }t          j                    }	|D ]y}
|
                    d          d	u o0|	|
                    d
|
                    dd                    z
  dk     |
d<   t          |
                    d                    |
d<   |r||
d<   z||d|                                 S # |                                 w xY w)u  Run sessions produced by a cron job, newest first.

    Cron runs are stored as ordinary sessions whose id is
    ``cron_{job_id}_{timestamp}`` (see cron/scheduler.run_job). A job's history
    is therefore every session whose id carries that prefix; ``source='cron'``
    narrows it and the id prefix binds it to this job. Powers the run-history
    list under each job in the desktop cron detail. Same row shape as
    ``/api/sessions`` so the frontend can reuse SessionInfo.

    Backed by ``SessionDB.list_cron_job_runs`` — a bounded ``[prefix, hi)``
    id-range scan, not the compression-chain CTE used for the recents list,
    so the cost scales with the requested window and not the (unbounded) total
    cron history.
    rN  r  r   r  r  r   r  r  Nr  r  r  rn  rX  r1  )runsr  )rG  rB  rv   r6  r  r  r  r@  r  r  list_cron_job_runsr  r  r   )rC  r1  r  rQ  r  r.  limit_nr"  rT  r  r  s              rE   rU  rU  A  s       8088HI '$Xy&AA 	'3774== 	'CIIaSZZ--..z"    
&h	/	/B$$Yga$HHikk 	( 	(Aj!!T) O155lA0F0FGGG3N kN !z!2!233AjM (')w//








s   +B BB+B+E+ +Fc           	         K   	 t          |d| j        | j        | j        | j        | j                  S # t          $ r=}t                              d           t          dt          |                    d }~ww xY w)N
create_job)r1   r  rO   r  r   zPOST /api/cron/jobs failedr   r   )rB  r1   r  rO   r  r   r@   r=   r  r%   r6  )r  r1  rD   s      rE   create_cron_jobrY  o  s      <%;]L;
 
 
 	
  < < <3444CFF;;;;<s   .3 
A:8A55A:z/api/cron/delivery-targetsc                     K   dddddg} 	 ddl m} |                      |                       n*# t          $ r t                              d           Y nw xY wd	| iS )
u  Delivery targets the cron dropdown should offer.

    Always includes the implicit ``local`` option. Beyond that, the list is
    derived dynamically from the configured gateway platforms via
    ``cron.scheduler.cron_delivery_targets()`` — no hardcoded platform list. A
    configured platform that hasn't set its cron home channel is still returned
    with ``home_target_set: false`` so the UI can surface it as "configure a
    home channel first" rather than hiding it.
    r   zLocal (save only)TN)r  rO   home_target_sethome_env_varr   cron_delivery_targetsz%GET /api/cron/delivery-targets failedr  )r<   r^  rI  r@   r=   r  )r  r^  s     rE   get_cron_delivery_targetsr_    s       '# 		
 	
G@888888,,..//// @ @ @>?????@ws   #0 $AA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  rM  r   
update_jobr   )rG  r%   rB  r  r  r6  )rC  r  r1  rQ  r.  r  s         rE   update_cron_jobrb    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  rM  r   	pause_jobrO  rP  s       rE   pause_cron_jobre    sg      8088H EODDDD
 ;
?
?C EODDDDJr   z/api/cron/jobs/{job_id}/resumec                    K   |pt          |           }|st          dd          t          |d|           }|st          dd          |S )Nr  rM  r   
resume_jobrO  rP  s       rE   resume_cron_jobrh    sg      8088H EODDDD
 <
@
@C EODDDDJr   z/api/cron/jobs/{job_id}/triggerc                    K   |pt          |           }|st          dd          t          |d|           }|st          dd          |S )Nr  rM  r   trigger_jobrO  rP  s       rE   trigger_cron_jobrk    sg      8088H EODDDD
 =&
A
AC EODDDDJr   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  rM  r   
remove_jobr   r  T)rG  r%   rB  r  r6  )rC  r1  rQ  rp  r  s        rE   delete_cron_jobrn    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                   8    e Zd ZU eed<   i Zeeef         ed<   dS )AutomationBlueprintInstantiate	blueprintr  N)r2  r3  r4  r6  r5  r  r   r
   r7  r   rE   rp  rp    s3         NNNFDcNr   rp  z/api/cron/blueprintsc                    K   	 ddl m} m} d}	 ddlm} d  |            D             }ddg|}n,# t
          $ r t                              dd	
           Y nw xY wg }| D ][} ||          }|r7|                    dg           D ] }|                    d          dk    r||d<   !|	                    |           \d|iS # t
          $ r=}	t          
                    d           t          dt          |	                    d}	~	ww xY w)a  Return the blueprint catalog as form schemas for the dashboard gallery.

    The ``deliver`` slot's options are rewritten from the user's actually
    configured gateway platforms (plus the universal origin/local/all), so the
    form never offers a platform that isn't connected.
    r   )CATALOGblueprint_catalog_entryNr]  c                 H    g | ]}|                     d           |d           S r  r  )r
  rt  s     rE   r  z(list_cron_blueprints.<locals>.<listcomp>  s+    QQQQQUU4[[Q4QQQr   originr   z?cron_delivery_targets unavailable; using static deliver optionsTr{  r  rO   r  r   
blueprintszGET /api/cron/blueprints failedr`  r   )cron.blueprint_catalogrs  rt  r<   r^  r@   r=   rA   rv   r=  r  r%   r6  )
rs  rt  deliver_optionsr^  r  r  r  r-  frD   s
             rE   list_cron_blueprintsr{    s     <KKKKKKKK	i<<<<<<QQ*?*?*A*AQQQI'=9=OO 	i 	i 	iJJXcgJhhhhh	i  	" 	"A++A..E 78R00 7 7AuuV}}	11'6)NN5!!!!g&& < < <8999CFF;;;;<s8   
C   0 C  &AC  AA&C   
D
8DDz /api/cron/blueprints/instantiatec                   K   	 ddl m}m}m}  || j                  }|t          dd| j                   	  ||| j                  }n,# |$ r$}t          dt          |                    |d}~ww xY w|                    dd           t          |d	fi |S # t
          $ r  t          $ r=}t                              d
           t          dt          |                    d}~ww xY w)zDFill a blueprint's slots and create the cron job (form-submit path).r   )fill_blueprintget_blueprintBlueprintFillErrorNr  zUnknown blueprint: r   i  rv  rX  z,POST /api/cron/blueprints/instantiate failedr   )rx  r}  r~  r  rq  r%   r  r6  r  rB  r@   r=   r  )	r  r1  r}  r~  r  rq  specr  rD   s	            rE   instantiate_blueprintr    sE     <\\\\\\\\\\!M$.11	C8^dn8^8^____	K!>)T[99DD! 	K 	K 	KCCAAAsJ	K
 	4   %g|DDtDDD    < < <EFFFCFF;;;;<s:   5B A B A5A00A55&B C-08C((C-c                       e Zd ZU eed<   dZee         ed<   dZee         ed<   g Ze	e         ed<   i Z
eeef         ed<   dZee         ed<   dZee         ed<   dS )	MCPServerCreaterO   Nr   commandrM   rD  r}   r1  )r2  r3  r4  r6  r5  r   r   r  rM   r   rD  r   r}   r1  r7  r   rE   r  r  (  s         
IIIC#!GXc]!!!D$s)Cc3hD(3-!GXc]!!!!!r   r  rD  c                     i }| pi                                  D ]X\  }}	 |rt          t          |                    nd|t          |          <   7# t          $ r d|t          |          <   Y Uw xY w|S )z5Mask secret-shaped MCP env values for read responses.rr   z***)r%  r    r6  r@   )rD  r@  r  r  s       rE   _redact_mcp_envr  4  s    C!!##    1	 019*SVV,,,rCAKK 	  	  	 CAKKK	 Js   0AA/.A/r  c                    |                     d          rdn|                     d          rdnd}| ||                     d          |                     d          t          |                     d          pg           t          |                     d          pi           |                     d          |                     d	d
          du|                     d          d	S )Nr   r   r  stdiounknownrM   rD  r}   rC  TFrI  )	rO   	transportr   r  rM   rD  r}   rC  rI  )rv   r  r  )rO   r  r  s      rE   _mcp_server_summaryr  ?  s    ''%..\#''):L:L/[wwR[Iwwu~~779%%SWWV__*++swwu~~344779d++58!!  r   z/api/mcp/serversc                    K   ddl m} t          |           5   |            }d d d            n# 1 swxY w Y   dd t          |                                          D             iS )Nr   )_get_mcp_serversserversc                 4    g | ]\  }}t          ||          S r7  )r  )r
  rO   r  s      rE   r  z$list_mcp_servers.<locals>.<listcomp>V  s4     
 
 
/8tSc**
 
 
r   )hermes_cli.mcp_configr  r  r9  r%  )r1  r  r  s      rE   list_mcp_serversr  O  s      666666		 	  % %""$$% % % % % % % % % % % % % % % 	 
 
<B7==??<S<S
 
 
 s   /33c                   K   ddl m}m} | j        pd                                }|st          dd          t          | j        p|          5   |            }d d d            n# 1 swxY w Y   ||v rt          dd| d	          | j        s| j	        st          dd
          i }| j        r| j                                        |d<   | j	        r:| j	                                        |d<   | j
        rt          | j
                  |d<   | j        rt          | j                  |d<   | j        r
| j        |d<   	 t          | j        p|          5   |||           d d d            n# 1 swxY w Y   nU# t
          $ r  t          $ r>}t                               d           t          dt%          |                    |d }~ww xY wt'          ||          S )Nr   )r  _save_mcp_serverrr   r   zServer name is requiredr   r  Server 'z' already existszBProvide either a URL (HTTP/SSE server) or a command (stdio server)r   r  rM   rD  r}   zPOST /api/mcp/servers failed)r  r  r  rO   r   r%   r  r1  r   r  rM   r  rD  r  r}   r@   r=   r  r6  r  )r  r1  r  r  rO   r  server_configr  s           rE   add_mcp_serverr  \  s     HHHHHHHHIO""$$D O4MNNNN	/	0	0 & &##%%& & & & & & & & & & & & & & &x4Ut4U4U4UVVVV8 
DL 
W
 
 
 	

 %'Mx 0#x~~//e| 4#'<#5#5#7#7i 9 	4$(OOM&!x .#DH~~ey * $	fGDL3G44 	2 	2T=111	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2    G G G5666CHH===3FG t]333sN   A&&A*-A*?E: E."E: .E22E: 5E26E: :G9GGz/api/mcp/servers/{name}c                    K   ddl m} t          |          5   ||           }d d d            n# 1 swxY w Y   |st          dd|  d          ddiS )	Nr   )_remove_mcp_serverr  r  ' not foundr   r  T)r  r  r  r%   )rO   r1  r  rp  s       rE   remove_mcp_serverr    s      888888		 	  + +$$T**+ + + + + + + + + + + + + + + R4Pt4P4P4PQQQQ$<s   044z/api/mcp/servers/{name}/testc                 l   K   ddl m}m t                    5   |            ddd           n# 1 swxY w Y    vrt	          dd  d           fd}	 t          j        |           d{V }n*# t          $ r}d	t          |          g d
cY d}~S d}~ww xY wdd |D             dS )zFConnect to the server, list its tools, disconnect.  Returns tool list.r   )r  _probe_single_serverNr  r  r  r   c                  z    t                    5                       cd d d            S # 1 swxY w Y   d S rE  )r  )r  rO   r1  r  s   rE   _probe_scopedz&test_mcp_server.<locals>._probe_scoped  s     G$$ 	= 	=''gdm<<	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	=s   044F)r  r  rI  Tc                     g | ]
\  }}||d S )rO   r   r7  )r
  rt  ds      rE   r  z#test_mcp_server.<locals>.<listcomp>  s$    BBBDAq1Q//BBBr   )r  rI  )	r  r  r  r  r%   rR   r  r@   r6  )rO   r1  r  r  rI  r  r  r  s   ``    @@rE   test_mcp_serverr    s      MLLLLLLL		 	  % %""$$% % % % % % % % % % % % % % %74Pt4P4P4PQQQQ= = = = = = = =	
 '66666666 
 
 
XX
 
 	
 	
 	
 	
 	
 	

 BBEBBB  s*   599"A= =
B$BB$B$c                   4    e Zd ZU eed<   dZee         ed<   dS )MCPEnabledTogglerC  Nr1  r2  r3  r4  r  r5  r1  r   r6  r7  r   rE   r  r    /         MMM!GXc]!!!!!r   r  z/api/mcp/servers/{name}/enabledc                   K   t          |j        p|          5  t                      }|                    d          }t	          |t
                    r| |vrt          dd|  d          t	          ||          t
                    st          dd          t          |j                  ||          d<   t          |           d	d	d	           n# 1 swxY w Y   d
| t          |j                  dS )u$  Enable or disable an MCP server (takes effect on next session/gateway).

    Toggles the ``enabled`` key on the server's config.yaml entry — the same
    flag the agent reads at startup.  Disabled servers stay in config so they
    can be re-enabled without re-entering their settings.
    mcp_serversr  r  r  r   r   zMalformed server configrC  NTr  rO   rC  )
r  r1  r   rv   r  r  r%   r  rC  r   )rO   r  r1  r  r  s        rE   set_mcp_server_enabledr    s5      
/	0	0  mm''-(('4(( 	VD,?,?C8T48T8T8TUUUU'$-.. 	SC8QRRRR#'#5#5i C               dl1C1CDDDs   B*CCCz/api/mcp/catalogc                   K   	 ddl m n@# t          $ r3}t                              d           t          dd|           d}~ww xY wg }	 t          |           5  t                                                    }fd|D             }ddd           n# 1 swxY w Y   |D ]}|j	        }|
                    |j        |j        |j        |j        j        t!          |d	d
          d t!          |dg           pg D             |j        du|                    |j        d          d         |                    |j        d          d         d	           n4# t
          $ r  t          $ r t                              d           Y nw xY wg }	 d                                 D             }n# t          $ r Y nw xY w||dS )a  Browse the Nous-approved MCP catalog (the optional-mcps/ manifests).

    Each entry reports whether it's already installed and enabled so the UI
    can show install / enabled state inline.  This is the same catalog
    `hermes mcp catalog` / `hermes mcp install` read.  ``profile`` scopes
    the installed/enabled annotations (the catalog itself is repo-shipped
    and identical for every profile).
    r   mcp_catalogzmcp_catalog import failedr`  zCatalog unavailable: r   Nc                     i | ]<}|j                             |j                                       |j                   f=S r7  )rO   is_installedra  )r
  rD   r  s     rE   r  z$list_mcp_catalog.<locals>.<dictcomp>  sT        11!&99;;Q;QRSRX;Y;YZ  r   r   nonec                 8    g | ]}|j         |j        |j        d S )rO   r1   r  r  r
  rD   s     rE   r  z$list_mcp_catalog.<locals>.<listcomp>  s8     ! ! ! VqxQZPP! ! !r   rD  )FFr   )	rO   r   rZ  r  r  r  needs_install	installedrC  zlist_mcp_catalog failedc                 "    g | ]\  }}}|||d S ))rO   kindr  r7  )r
  r  r  r  s       rE   r  z$list_mcp_catalog.<locals>.<listcomp>  s8     
 
 
Aq a00
 
 
r   )r  diagnostics)r  r  r@   r=   r  r%   r  r  list_catalogr}   r=  rO   r   rZ  r  r   r   installrv   catalog_diagnostics)	r1  r  r  catalog_entriesinstalled_stater-  r}   r  r  s	           @rE   list_mcp_catalogr    s     S******* S S S23334QC4Q4QRRRRS G2G$$ 	 	";#;#;#=#=>>O   (  O	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 % 	 	E:DNN
$0,"_1$T66::! !$T5"55;! ! ! "'d!:,00^LLQO*..uz>JJ1M     	"     2 2 20111112 K
 
(<<>>
 
 
     {;;;s]    
A	.AA	E 0BE BE !B"B6E .F
	F
F/ /
F<;F<c                   `    e Zd ZU eed<   i Zeeef         ed<   dZeed<   dZ	e
e         ed<   dS )MCPCatalogInstallrO   rD  TenableNr1  )r2  r3  r4  r6  r5  rD  r   r  r  r1  r   r7  r   rE   r  r    sW         
IIICc3hFD!GXc]!!!!!r   r  z/api/mcp/catalog/installc                 <   	
K   ddl m
  j        pd                                }
                    |          		t          dd| d           j        p| j        rYt                    5   j        	                                D ]\  }}|rt          ||           	 ddd           n# 1 swxY w Y   	j        \	 t          t                    d	d
|gz   d          }n0# t
          $ r  t          $ r}t          dd|           d}~ww xY wd|dddS  	
fd}	 t          j        |           d{V  nT# t
          $ r  t          $ r=}t"                              d           t          dt'          |                    d}~ww xY wd|ddS )aJ  Install a catalog MCP into config.yaml.

    For HTTP/stdio entries with required env vars, those are written to .env
    via the standard env path so the agent can read them at session start.
    Entries that need a git bootstrap (``needs_install``) are installed via
    the CLI action path because the clone can take time.
    r   r  rr   Nr  zNo catalog entry ''r   r  r  mcp-installr`  zInstall failed: T)r  rO   
backgroundactionc                      t                    5                       j                   d d d            d S # 1 swxY w Y   d S )N)r  )r  install_entryr  )r  effective_profiler-  r  s   rE   _install_scopedz2install_mcp_catalog_entry.<locals>._install_scopedD  s    -.. 	A 	A%%eDK%@@@	A 	A 	A 	A 	A 	A 	A 	A 	A 	A 	A 	A 	A 	A 	A 	A 	A 	As   ;??z install_mcp_catalog_entry failedr   F)r  rO   r  )r  r  rO   r   	get_entryr%   r1  rD  r  r%  r   r  r{  _profile_cli_argsr@   rR   r  r=   r  r6  )r  r1  rO   r  r  r\  r  r  r  r-  r  s   `       @@@rE   install_mcp_catalog_entryr    s      '&&&&&IO""$$D!!$''E}4P4P4P4PQQQQ /x )-.. 	) 	)(( ) )1 )"1a((()	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) } 	R'!"344y$7OO DD  	 	 	 	R 	R 	RC8P38P8PQQQQ	RDVVVA A A A A A A A>0000000000    > > >9:::CHH====> E:::sB   32B22B69B6#C) )D=DD)E F8FFr  zaction-mcp-install.logc                   $    e Zd ZU eed<   eed<   dS )PairingApprover  r  NrV  r7  r   rE   r  r  ^  s"         MMM
IIIIIr   r  c                   $    e Zd ZU eed<   eed<   dS )PairingRevoker  user_idNrV  r7  r   rE   r  r  c  s"         MMMLLLLLr   r  c                  "    ddl m}   |             S )Nr   PairingStore)gateway.pairingr  r  s    rE   _pairing_storer  h  s     ,,,,,,<>>r   z/api/pairingc                  t   K   t                      } |                                 |                                 dS )N)r  r-  )r  list_pendinglist_approved)stores    rE   list_pairingr  n  s>      E%%''''))  r   z/api/pairing/approvec                   K   t                      }| j        pd                                                                }| j        pd                                                                }|r|st          dd          |                    ||          }|rd|dS |                    |          rt          dd| d	          t          d
d| d| d          )Nrr   r   zplatform and code are requiredr   T)r  userrh  z
Platform 'z0' is locked out after too many failed approvals.r  zCode 'z%' not found or expired for platform 'z'.)	r  r  r   r   r  r  r%   approve_code_is_locked_out)r  r  r  r  r7  s        rE   approve_pairingr  w  s     E#**,,2244HIO""$$**,,D V4 V4TUUUU$//F ,F+++H%% 
ZZZZ
 
 
 	
 OOO8OOO   r   z/api/pairing/revokec                 (  K   t                      }| j        pd                                                                }|r| j        st          dd          |                    || j                  rddiS t          dd| j         d	| d
          )Nrr   r   z!platform and user_id are requiredr   r  Tr  zUser z  not found in approved list for r!  )r  r  r   r   r  r%   revoke)r  r  r  s      rE   revoke_pairingr    s      E#**,,2244H Y4< Y4WXXXX||Hdl++ d|
Pt|PPXPPP   r   z/api/pairing/clear-pendingc                  T   K   t                      } |                                 }d|dS )NT)r  r  )r  clear_pending)r  r2  s     rE   clear_pending_pairingr    s0      E!!E5)))r   c                       e Zd ZU eed<   dZee         ed<   g Zee         ed<   dZ	ee         ed<   g Z
ee         ed<   dZeed<   d	Zeed
<   dZee         ed<   dZee         ed<   dS )WebhookCreaterO   Nr   eventsr1   r   r  r  Fdeliver_onlydeliver_chat_idsecret)r2  r3  r4  r6  r5  r   r   r  r   r1   r   r  r  r  r  r  r7  r   rE   r  r    s         
III!%K#%%%FDI FHSM   FDIGSL$%)OXc]))) FHSM     r   r  routec                    | |                     dd          t          |                     d          pg           |                     dd          t          |                     d                    |                     dd          t          |                     d          pg           |                     d	          | d
|  t          |                     d                    |                     dd          dudS )Nr   rr   r  r  r  r  r1   r   r  z
/webhooks/r  rC  TF)rO   r   r  r  r  r1   r   r  r   
secret_setrC  )rv   r  r  )rO   r  rq  s      rE   _webhook_route_summaryr    s    yy33uyy**0b1199Y..UYY~6677))Hb))uyy**0b11ii--,,d,,599X..//99Y--U:  r   z/api/webhooksc                     K   dd l m}  |                                 |                                 }|                                 fd|                                D             dS )Nr   c                 8    g | ]\  }}t          ||          S r7  )r  )r
  rO   r  rq  s      rE   r  z!list_webhooks.<locals>.<listcomp>  s9     
 
 
e #499
 
 
r   )rC  rq  subscriptions)hermes_cli.webhookr  _get_webhook_base_url_load_subscriptions_is_webhook_enabledr%  )whsubsrq  s     @rE   list_webhooksr    s      ######''))H!!##D))++
 
 
 
#zz||
 
 
  r   z/api/webhooks/enablec                     K   	 t          dd           n># t          $ r1} t                              d           t	          dd          | d } ~ ww xY wt                      }ddd|d          d|S )	Nr  Tz0Failed to enable webhook platform from dashboardr`  z"Failed to enable webhook platform.r   r  )r  r  rC  rh  )r  r@   r=   r  r%   r  )r  rj  s     rE   enable_webhooksr    s      	40000   IJJJ7
 
 
 	 ;<<N+,=>>	 
  s    
A,AAc                   K   dd l }dd l}dd l}dd lm} |                                st          dd          | j        pd                                	                                
                    dd          }|                    d|          st          dd	          | j        r| j        d
k    rt          dd          | j        p|                    d          }| j        pd| d | j        D             || j        pdd | j        D             | j        pd
|                    d|                                          d}| j        rd|d<   | j        rd| j        i|d<   |                                }|||<   |                    |           |                                }	t5          |||	          }
||
d<   |
S )Nr   r   zHWebhook platform is not enabled. Enable it from the Webhooks page first.r   rr   r$  r  z^[a-z0-9][a-z0-9_-]*$zBInvalid name. Use lowercase alphanumeric with hyphens/underscores.r  uK   Direct delivery requires a real target (telegram, discord, …), not 'log'.rg   z Dashboard-created subscription: c                 ^    g | ]*}|                                 |                                 +S r7  re  r  s     rE   r  z"create_webhook.<locals>.<listcomp>  -    ???QWWYY?17799???r   c                 ^    g | ]*}|                                 |                                 +S r7  re  rf  s     rE   r  z"create_webhook.<locals>.<listcomp>  r 	  r   z%Y-%m-%dT%H:%M:%SZ)r   r  r  r1   r   r  r  Tr  chat_iddeliver_extrar  )r(  r  r  r  r  r  r%   rO   r   r   r)  r+  r  r  r  r  r   r  r1   r   r  gmtimer  r  _save_subscriptionsr  r  )r  _re_secrets_timer  rO   r  r  r  rq  r  s              rE   create_webhookr		    s3     ######!!## 
]
 
 
 	

 IO""$$**,,44S#>>D99-t44 
W
 
 
 	

  
T\U22`
 
 
 	

 [6H22266F'T+Td+T+T??dk???+#??dk???<(5nn%95<<>>JJ E  % $n C"+T-A!Bo!!##DDJ4   ''))H$T5(;;GGHNr   z/api/webhooks/{name}c                    K   dd l m} | pd                                                                }|                                }||vrt          dd| d          ||= |                    |           ddiS )	Nr   rr   r  No subscription named 'r  r   r  T)r  r  r   r   r  r%   r	  )rO   r  r+  r  s       rE   delete_webhookr	  #  s      ######:2




$
$
&
&C!!##D
$4Tc4T4T4TUUUUS	4   $<r   c                       e Zd ZU eed<   dS )WebhookEnabledTogglerC  Nrr  r7  r   rE   r	  r	  0  r  r   r	  z/api/webhooks/{name}/enabledc                 P  K   ddl m} | pd                                                                }|                                }||vrt          dd| d          t          |j                  ||         d<   |                    |           d	|t          |j                  d
S )a   Enable or disable a webhook route.

    Disabled routes stay in the subscriptions file (so they can be
    re-enabled) but the gateway rejects incoming events with 403.  The
    gateway hot-reloads the subscriptions file, so this takes effect on the
    next event without a restart.
    r   Nrr   r  r	  r  r   rC  Tr  )	r  r  r   r   r  r%   r  rC  r	  )rO   r  r  r+  r  s        rE   set_webhook_enabledr	  4  s       $#####:2




$
$
&
&C!!##D
$4Tc4T4T4TUUUU--DIi4   T\0B0BCCCr   z/api/gateway/startc                     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 )
Nr  rZ   r  zFailed to spawn gateway startr`  zFailed to start gateway: r   Try  r{  r@   r=   r  r%   r  r|  s     rE   start_gatewayr	  R  s      W#Y$8/JJ W W W67774UPS4U4UVVVVW txAAA    
A.AAz/api/gateway/stopc                     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 )
Nr  stopr  zFailed to spawn gateway stopr`  zFailed to stop gateway: r   Try  r	  r|  s     rE   stop_gatewayr	  \  s      V#Y$7HH V V V56664Ts4T4TUUUUV tx@@@r	  c                   >    e Zd ZU eed<   eed<   dZee         ed<   dS )CredentialPoolAddro  r:  Nr  )r2  r3  r4  r6  r5  r  r   r7  r   rE   r	  r	  o  s:         MMM LLLE8C=r   r	  r  c                 n   t          | dd          pd}|t          | dd          t          | dd          t          | dd          t          | dd          t          | dd	          t          | d
d          t          | dd	          |rt          |          ndt          t          | dd                    d
S )z{Redacted, display-safe view of one PooledCredential.

    ``index`` is 1-based to match CredentialPool.remove_index().
    r  rr   r  Nr  r  rZ  r  r   last_statusrequest_countr  )
r  r  r  r  rZ  r  r	  r	  r  has_refresh)r   r    r  )r-  r  r  s      rE   _pool_entry_summaryr	  w  s    
 E>2..4"EeT4((..UK66%400E:q11umT:: ;;.3;E***GE?DAABB  r   z/api/credentials/poolc            	        K   ddl m}  ddlm} g } |            }t	          |                                          D ]}	  | |          }n+# t          $ r t                              d|           Y 6w xY w|	                                }|sQ|
                    |d t          |d          D             d           d	|iS )
Nr   r  )read_credential_poolzload_pool(%s) failedc                 4    g | ]\  }}t          ||          S r7  )r	  )r
  irD   s      rE   r  z(list_credential_pool.<locals>.<listcomp>  s4       .2a#Aq))  r   r   )rZ   )ro  r  r  )r  r  r  r!	  r9  r  r@   r=   r  r  r=  r  )r  r!	  r  raw_poolr  r  r  s          rE   list_credential_poolr%	    s#     //////444444I $#%%Hhmmoo..  	9[))DD 	 	 	NN1;???H	 ,,.. 	# 6?q6Q6Q6Q  
 
 	 	 	 	 ##s   A%A32A3c           	        K   dd l }ddlm}m}m}m} | j        pd                                                                }| j	        pd                                }|r|st          dd          	  ||          }| j        pd                                p&dt          |                                          dz    }	 |||                                j        d d	         |	|d||
          }
|                    |
           nK# t"          $ r>}t$                              d           t          dt)          |                    |d }~ww xY wd|t          |                                          dS )Nr   )r  r  AUTH_TYPE_API_KEYr  rr   r   z!provider and api_key are requiredr   zkey #r   r}  )ro  r  r  r  r  rZ  r  z!POST /api/credentials/pool failedTr  ro  r2  )r  r  r  r  r'	  r  ro  r   r   r:  r%   r  r  r  r	  r
  r  r@   r=   r  r6  )r  _uuidr  r  r'	  r  ro  r:  r  r  r-  r  s               rE   add_credential_pool_entryr*	    s                 #**,,2244H|!r((**G Y7 Y4WXXXXGy""!r((**O.Oc$,,..6I6IA6M.O.O  {{}} !$'  
 
 
 	u G G G:;;;CHH===3FG Hs4<<>>7J7JKKKs   1BD 
E9EEz(/api/credentials/pool/{provider}/{index}c                   K   ddl m} | pd                                                                } 	  ||           }|                    |          }nK# t
          $ r>}t                              d           t          dt          |                    |d}~ww xY w|t          dd	          d
| t          |                                          dS )zGRemove a pool entry.  ``index`` is 1-based (matches the list response).r   r 	  rr   z#DELETE /api/credentials/pool failedr   r   Nr  zNo pool entry at that indexTr(	  )r  r  r   r   remove_indexr@   r=   r  r%   r6  r  r  )ro  r  r  r  rp  r  s         rE   remove_credential_pool_entryr-	    s       0/////B%%''--//HGy""##E** G G G<===CHH===3FG 4QRRRRHs4<<>>7J7JKKKs    A 
B9BBc                       e Zd ZU eed<   dS )MemoryProviderSelectro  NrV  r7  r   rE   r/	  r/	    s         MMMMMr   r/	  c                       e Zd ZU dZeed<   dS )MemoryResetrs  rL   N)r2  r3  r4  rL   r6  r5  r7  r   rE   r1	  r1	    s"         FCr   r1	  z/api/memoryc                  H  K   ddl m}  t                      }d}|                    d          }t	          |t
                    r$t          |                    d          pd          }g }	  |             D ],\  }}}|                    ||t          |          d           -n*# t          $ r t                              d           Y nw xY wt                      dz  }i }	d	D ]<\  }
}||
z  }|                                r|                                j        nd|	|<   =|||	d
S )Nr   discover_memory_providersrr   r
  ro  )rO   r   r  z discover_memory_providers failedmemories))	MEMORY.mdr
  )USER.mdr  )r  r  builtin_files)plugins.memoryr4	  r   rv   r  r  r6  r=  r  r@   r=   r  r   r"  r  rP  )r4	  r  r  memr  rO   r   r  mem_dirfilesfnamer+  r   s                rE   get_memory_statusr>	    sz     888888
--CF
''(

C#t 0SWWZ((.B//I;-F-F-H-H 	 	)D+z*":..     	  ; ; ;9:::::; *,GED A A
s,0KKMM@TYY[[((qc

   s   *7B" "$C	C	z/api/memory/providerc                   K   | j         pd                                }|                                dv rd}|r3ddlm} d  |            D             }||vrt          dd| d	          t                      }t          |                    d
          t                    si |d
<   ||d
         d<   t          |           d|dS )Nrr   >   built-inr  r   r   r3	  c                     h | ]\  }}}|	S r7  r7  )r
  rO   _d_cs       rE   r  z&set_memory_provider.<locals>.<setcomp>  s    FFF,$BFFFr   r   zUnknown memory provider 'z4'. Run `hermes memory setup` to configure a new one.r   r
  ro  Tr  r  )ro  r   r   r9	  r4	  r%   r   r  rv   r  r   )r  ro  r4	  r  r  s        rE   set_memory_providerrE	    s      #**,,H~~::: <<<<<<FF*C*C*E*EFFF5  q8qqq   
 --Ccggh''.. H (CM*(+++r   z/api/memory/resetc           	        K   | j         pd                                                                }|dvrt          dd          t	                      dz  }g }g }|dv r|                    d           |d	v r|                    d
           |D ]o}||z  }|                                rT	 |                                 |                    |           F# t          $ r}t          dd| d|           d }~ww xY wpd|dS )Nrs  >   rs  r  r
  r   z#target must be all, memory, or userr   r5	  >   rs  r
  r6	  >   rs  r  r7	  r`  zCould not delete : Tr  )	rL   r   r   r%   r   r=  r"  r  r  )r  rL   r;	  r  r  r=	  r   r  s           rE   reset_memoryrH	  )  sQ     k"U))++1133F...4YZZZZ*,GGG"""{###  y!!! ` `;;== 	``u%%%% ` ` `#<^PU<^<^Y\<^<^____`		` 7+++s   *)C
C:C55C:z/api/ops/doctorc                     K   	 t          dgd          } n@# t          $ r3}t                              d           t	          dd|           d }~ww xY wd| j        ddS )Nr  zFailed to spawn doctorr`  zFailed to run doctor: r   Try  r	  r|  s     rE   
run_doctorrJ	  N  s      T#XJ99 T T T/0004RS4R4RSSSST tx:::r  z/api/ops/security-auditc                     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 )
Nr   auditr  zFailed to spawn security auditr`  zFailed to run security audit: r   Try  r	  r|  s     rE   run_security_auditrM	  X  s      \#Z$9;KLL \ \ \78884ZUX4Z4Z[[[[\ tx1ABBBr	  c                   *    e Zd ZU dZee         ed<   dS )BackupRequestNr6  )r2  r3  r4  r6  r   r6  r5  r7  r   rE   rO	  rO	  b  s&          FHSM     r   rO	  z/api/ops/backupc                 ,  K   dg}| j         r,|                    | j                                                    	 t          |d          }n@# t          $ r3}t
                              d           t          dd|           d }~ww xY wd|j        ddS )Nr  zFailed to spawn backupr`  zFailed to run backup: r   Try  )	r6  r=  r   r{  r@   r=   r  r%   r  )r  rM   r\  r  s       rE   
run_backuprQ	  g  s      :D{ )DK%%''(((T#D(33 T T T/0004RS4R4RSSSST tx:::s   A 
B.BBc                   (    e Zd ZU eed<   dZeed<   dS )ImportRequestarchiveFr  N)r2  r3  r4  r6  r5  r  r  r7  r   rE   rS	  rS	  t  s-         LLL E4r   rS	  z/api/ops/importc                   K   | j         pd                                }|st          dd          t          j                            |          st          dd|           d|g}| j        r|                    d           	 t          |d          }n@# t          $ r3}t                              d	           t          d
d|           d }~ww xY wd|j        ddS )Nrr   r   zarchive path is requiredr   r  zArchive not found: r  z--forcezFailed to spawn importr`  zFailed to run import: Try  )rT	  r   r%   rU   r   rJ  r  r=  r{  r@   r=   r  r  )r  rT	  rM   r\  r  s        rE   
run_importrV	    s     |!r((**G P4NOOOO7>>'"" U4S'4S4STTTTgDz IT#D(33 T T T/0004RS4R4RSSSST tx:::s   B 
C .CCz/api/ops/hooksc                  r  K   ddl m}  ddlm} 	 ddlm} t          |          }n# t          $ r g }Y nw xY wg }	 |                     |                       }n*# t          $ r t          
                    d           Y nw xY wg }|D ]}d}	 |                    |j        |j                  }n# t          $ r Y nw xY wd}	 |                    |j                  }n# t          $ r Y nw xY w|                    |j        |j        |j        |j        |du|pi                     d          |d	           ||d
S )zList configured shell hooks from config.yaml with consent + health.

    Reports each hook's allowlist (consent) status and whether the script is
    currently executable, plus the set of valid hook events so the create
    form can offer them.
    r   )r   shell_hooksVALID_HOOKSziter_configured_hooks failedNFapproved_at)eventmatcherr  r  allowedr\	  r  )hooksvalid_events)r  r   r   rY	  hermes_cli.pluginsr[	  r9  r@   iter_configured_hooksr=   r  allowlist_entry_forr]	  r  script_is_executabler=  r^	  r  rv   )	_load_configrY	  r[	  ra	  specsr@  r  r-  r  s	            rE   
list_hooksrh	    s      >=====!!!!!!222222k**    E711,,..AA 7 7 75666667 C  	33DJMMEE 	 	 	D	
	$99$,GGJJ 	 	 	D	

Z|||D(!KR,,];;$
 
 	 	 	 	 ,777sA   & 55A $B ?B  B--
B:9B: C
C('C(c                   f    e Zd ZU eed<   eed<   dZee         ed<   dZee         ed<   dZ	e
ed<   dS )
HookCreater]	  r  Nr^	  r  Tapprove)r2  r3  r4  r6  r5  r^	  r   r  r  rk	  r  r7  r   rE   rj	  rj	    s^         JJJLLL!GXc]!!!!GXc]!!! GTr   rj	  c                   K   ddl m} | j        pd                                }| j        pd                                }|r|st          dd          	 ddlm} ||vr7t          dd| d	d
                    t          |                               n# t
          $ r  t          $ r Y nw xY wt                      }|                    d          }t          |t                    si }||d<   |                    |          }t          |t                    sg }|||<   d|i}| j        r
| j        |d<   | j        t%          | j                  |d<   |                    |           t)          |           d}	| j        rD	 |                    ||           d}	n*# t          $ r t.                              d           Y nw xY wd|||	dS )aO  Add a shell hook to config.yaml (and optionally approve it).

    Shell hooks run arbitrary commands, so this is a privileged action: it
    writes to the ``hooks:`` config block and, when ``approve`` is set, records
    consent in the allowlist so the hook actually fires.  Takes effect on the
    next session / gateway restart.
    r   rX	  rr   r   event and command are requiredr   rZ	  zUnknown event 'z
'. Valid: ry  r`	  r  r^	  Nr  FTzhook consent record failed)r  r]	  r  r-  )r   rY	  r]	  r   r  r%   rb	  r[	  r  r9  r@   r   rv   r  r  r  r^	  r  r  r=  r   rk	  _record_approvalr=   r  )
r  rY	  r]	  r  r[	  r  	hooks_cfgr  	new_entryr-  s
             rE   create_hookrq	    sQ      "!!!!!Z2$$&&E|!r((**G V V4TUUUU
222222##ZZZ$))F;DWDW:X:XZZ    $
        --C  Ii&& !	 GmmE""Ggt$$ #"	%!*G 4I| ,#|	)|"4<00	)NN9H| 9	9((888HH 	9 	9 	9NN788888	9 7QQQs%   AB B.-B.F $GGc                   $    e Zd ZU eed<   eed<   dS )
HookDeleter]	  r  NrV  r7  r   rE   rs	  rs	    s"         JJJLLLLLr   rs	  c                   K   ddl m} | j        pd                                }| j        pd                                |rst          dd          t                      }|                    d          }d}t          |t                    rt          |                    |          t                    rwt          ||                   }fd	||         D             ||<   t          ||                   |k     }||         s||= |s|                    dd
           t          |           	 |                               n# t          $ r Y nw xY w|st          dd          ddiS )zFRemove a hook from config.yaml and revoke its consent allowlist entry.r   rX	  rr   r   rm	  r   r`	  Fc                 p    g | ]2}t          |t                    r|                    d           k    0|3S )r  )r  r  rv   )r
  rD   r  s     rE   r  zdelete_hook.<locals>.<listcomp>  sL     
 
 
q$''
,-EE),<,<,G,G ,G,G,Gr   Nr  zNo matching hook foundr  T)r   rY	  r]	  r   r  r%   r   rv   r  r  r  r  r  r   r  r@   )r  rY	  r]	  r  ro	  rp  beforer  s          @rE   delete_hookrw	    s      "!!!!!Z2$$&&E|!r((**G V V4TUUUU
--C  IG)T"" z)--2F2F'M'M Yu%&&
 
 
 
 '
 
 
	% i&''&0 	!%  	#GGGT"""C7####     N4LMMMM$<s   /E 
EEz/api/ops/checkpointsc                    K   t                      dz  } g }d}|                                 rt          |                                           D ]}|                                sd}d}|                    d          D ]I}|                                r3	 ||                                j        z  }|dz  }9# t          $ r Y Ew xY wJ||z  }|	                    |j
        ||d           ||dS )z8List the /rollback shadow store checkpoints (read-only).r   r   rk   r   )r   r<	  bytes)r#  total_bytes)r   r)  r9  r  rglobrO  r  rP  r  r=  rO   )cp_dirr#  rz	  r  r  r2  rz  s          rE   list_checkpointsr}	  0  s1      .FHK}} FNN,,-- 	 	E<<>> DE[[%%  99;;  00
"   	 4KOO :     
 !===s   !B66
CCz/api/ops/checkpoints/prunec                     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 )
Nr   pruner  z!Failed to spawn checkpoints pruner`  zFailed to prune checkpoints: r   Try  r	  r|  s     rE   prune_checkpointsr	  P  s      [#]G$<>QRR [ [ [:;;;4YTW4Y4YZZZZ[ tx1DEEEr	  c                   4    e Zd ZU eed<   dZee         ed<   dS )SkillInstallRequest
identifierNr1  r=  r7  r   rE   r	  r	  d  s/         OOO!GXc]!!!!!r   r	  c                     | pd                                 }|r|                                dk    rg S ddlm} t	          |           d|                    |          gS )u  Return ``["-p", <name>]`` for a validated non-default profile.

    Hub install/uninstall/update run in a fresh ``hermes`` subprocess, and
    ``_apply_profile_override()`` reads ``-p`` from argv in the child — the
    only mechanism that reaches import-time-bound globals like
    ``skills_hub.SKILLS_DIR``. Empty/"current" means the dashboard's own
    profile (no args, legacy behavior).
    rr   rc  r   ru  -p)r   r   r  rv  _resolve_profile_dirr*  )r1  rJ  r  s      rE   r  r  i  ss     B%%''I 	))Y66	333333###,55i@@AAr   z/api/skills/hub/installc                 d  K   | j         pd                                }|st          dd          	 t          t	          | j        p|          dd|gz   d          }nJ# t          $ r  t          $ r3}t                              d           t          d	d
|           d }~ww xY wd|j	        ddS )Nrr   r   identifier is requiredr   r   r  r  zFailed to spawn skills installr`  zFailed to install skill: Try  )
r	  r   r%   r{  r  r1  r@   r=   r  r  )r  r1  r	  r\  r  s        rE   install_skill_hubr	  z  s      /'R..00J N4LMMMM	W#dl5g66(Iz9ZZ
 
     W W W78884UPS4U4UVVVVW tx1ABBBs   *A B$1.BB$c                   4    e Zd ZU eed<   dZee         ed<   dS )SkillUninstallRequestrO   Nr1  r=  r7  r   rE   r	  r	    s/         
III!GXc]!!!!!r   r	  z/api/skills/hub/uninstallc                 f  K   | j         pd                                }|st          dd          	 t          t	          | j        p|          dd|dgz   d          }nJ# t          $ r  t          $ r3}t                              d	           t          d
d|           d }~ww xY wd|j	        ddS )Nrr   r   zname is requiredr   r   	uninstallz--yesr  z Failed to spawn skills uninstallr`  zFailed to uninstall skill: Try  )
rO   r   r%   r{  r  r1  r@   r=   r  r  )r  r1  rO   r\  r  s        rE   uninstall_skill_hubr	    s      IO""$$D H4FGGGG	Y#dl5g66(KQUW^9__
 
     Y Y Y9:::4WRU4W4WXXXXY tx1CDDDs   +A B%2.B  B%c                   *    e Zd ZU dZee         ed<   dS )SkillsUpdateRequestNr1  )r2  r3  r4  r1  r   r6  r5  r7  r   rE   r	  r	    s&         !GXc]!!!!!r   r	  z/api/skills/hub/updatec                   K   	 | r| j         nd p|}t          t          |          ddgz   d          }nJ# t          $ r  t          $ r3}t
                              d           t          dd|           d }~ww xY wd|j        dd	S )
Nr   r'  r  zFailed to spawn skills updater`  zFailed to update skills: r   Try  )r1  r{  r  r%   r@   r=   r  r  )r  r1  	effectiver\  r  s        rE   update_skills_hubr	    s      	W%)3T\\t?	#i((Hh+??
 
     W W W67774UPS4U4UVVVVW txAAAs   /4 A;.A66A;zOfficial (Nous)zHermes Indexz	skills.shz
Well-Knownz
Direct URLGitHubClawHubzClaude MarketplaceLobeHubz	browse.sh)
officialhermes-indexz	skills-shz
well-knownr   githubclawhubzclaude-marketplacelobehubz	browse-shc           	      z    | j         | j        | j        | j        | j        | j        t          | j        pg           dS )N)rO   r   rZ  r	  trust_levelrepotags)rO   r   rZ  r	  r	  r	  r  r	  )r  s    rE   _skill_meta_to_payloadr	    s@    }(l}QV\r""  r   c                    	 ddl m} | pd                                }|r<|                                dk    r$t	          |          } ||dz  dz  dz            }n
 |            }i }|                                D ][}|                    d          }|rB|                    d	          |                    d
          |                    d          d||<   \|S # t          $ r i cY S w xY w)ah  Map identifier -> installed lock entry for hub-installed skills.

    Lets the UI mark search results that are already installed.  Scoped to
    ``profile``'s skills/.hub/lock.json when provided (HubLockFile takes an
    explicit path, sidestepping the import-time LOCK_FILE binding).
    Best-effort: returns an empty dict if the lock file can't be read.
    r   )HubLockFilerr   rc  r   z.hubz	lock.jsonr	  rO   r	  scan_verdict)rO   r	  r	  )tools.skills_hubr	  r   r   r	  list_installedrv   r@   )r1  r	  rJ  profile_dirlockr@  r-  idents           rE   _installed_hub_identifiersr	    s$   000000]))++	 	!**i77.y99K;{X5>LMMDD;==D((** 	 	EIIl++E !IIf--#(99]#;#;$)IIn$=$= E

 
   			s   CC C)(C)z/api/skills/hub/sourcesc                     K    fd}	 t          j        |           d{V S # t          $ r  t          $ r3}t                              d           t          dd|           d}~ww xY w)u  List the configured skill-hub sources and installed-skill provenance.

    Gives the dashboard something to show BEFORE a search runs — which hubs
    are wired up, their trust tier, and a set of featured skills pulled from
    the centralized index (zero extra API calls).  Without this the Browse-hub
    tab is a blank page with no indication it's even connected to anything.
    ``profile`` scopes the installed-skill provenance to that profile.
    c                  T   ddl m}   |             }g }d}g }|D ]}|                                }|t                              ||          d}|dk    r8	 t          t          |dd                    |d<   n# t          $ r d|d<   Y nw xY w|dk    rn	 t          t          |d	d                    }n# t          $ r d}Y nw xY w||d
<   |r5	 d |                    dd          D             }n# t          $ r g }Y nw xY w|	                    |           |||t                    dS )Nr   create_source_routerF)r  r  r	  is_rate_limitedrate_limitedr	  is_availabler@  c                 ,    g | ]}t          |          S r7  r	  r
  r  s     rE   r  z9list_skills_hub_sources.<locals>._run.<locals>.<listcomp>   s.     $ $ $:;2155$ $ $r   rr      r  )sourcesindex_availablefeaturedr  )r	  r	  	source_id_SKILL_HUB_SOURCE_LABELSrv   r  r   r@   r)  r=  r	  )	r	  r	  r@  r	  r	  r  r  r-  r1  s	           rE   _runz%list_skills_hub_sources.<locals>._run  s   999999&&(( 	 	C--//C155c3?? E
 h2,0>OQV1W1W,X,XE.))  2 2 2,1E.)))2n$$,&*73+N+N&O&OOO  , , ,&+OOO,%4k"" &&$ $?Bzz"TVz?W?W$ $ $ % & & &#%&JJu. 3G<<	
 
 	
s6   !A77B	B	B22C C!C..C=<C=Nz!skills hub sources listing failedr  zHub sources failed: r   )rR   r  r%   r@   r=   r  )r1  r	  r  s   `  rE   list_skills_hub_sourcesr	    s      '
 '
 '
 '
 '
RR&t,,,,,,,,,    R R R:;;;4P34P4PQQQQRs   $ A+.A&&A+z/api/skills/hub/searchc                 &  K   | pd                                 sg i g i dS fd}	 t          j        |           d{V S # t          $ r  t          $ r3}t
                              d           t          dd|           d}~ww xY w)	aK  Search the skill hub across all configured sources.

    Network-bound (parallel source search); runs in a thread so the FastAPI
    loop isn't blocked.  Returns structured results the UI installs by
    identifier via POST /api/skills/hub/install, previews via
    /api/skills/hub/preview, and scans via /api/skills/hub/scan.
    rr   r  source_counts	timed_outr  c                     ddl m} m}  |             }t          t	          d          d          } ||pdd          \  }}}dddd	}i }|D ]c}	|	j        |vr|	||	j        <   |                    |	j        d          |                    ||	j                 j        d          k    r
|	||	j        <   dt          |	                                          d |         }
d
 |
D             ||t                    dS )Nr   )r	  parallel_search_sourcesr   r   rs  rj   )r  r  overall_timeoutr0  )r   trusted	communityc                 ,    g | ]}t          |          S r7  r	  r	  s     rE   r  z3search_skills_hub.<locals>._run.<locals>.<listcomp>S   s!    CCCa.q11CCCr   r	  )r	  r	  r	  r  r  r	  rv   r	  r  r  r	  )r	  r	  r	  cappedall_resultsr	  r	  _rankr  r  dedupedr  r1  r  rZ  s              rE   r	  zsearch_skills_hub.<locals>._run?   s@   RRRRRRRR&&((S]]B''0G0G5%QS1
 1
 1
-]I
 !!<< 	' 	'A|4''%&Q\""1=!,,uyyal9K9WYZ/[/[[[%&Q\"t{{}}%%gvg. DC7CCC*"3G<<	
 
 	
r   Nzskills hub search failedr  zHub search failed: r   )r   rR   r  r%   r@   r=   r  )r  rZ  r  r1  r	  r  r  s    ```  @rE   search_skills_hubr	  0   s       W"OOE VRTUUU
 
 
 
 
 
 
 
4Q&t,,,,,,,,,    Q Q Q12224O#4O4OPPPPQs   A	 	B.BBz/api/skills/hub/previewr	  c                 L  K   | pd                                 st          dd          fd}	 t          j        |           d{V }n@# t          $ r3}t
                              d           t          dd	|           d}~ww xY w|t          d
d           |S )ar  Fetch a hub skill's SKILL.md content + metadata for in-dashboard reading.

    Resolves the identifier across configured sources (same path the CLI
    installer uses), then returns the rendered SKILL.md text and the file
    manifest WITHOUT installing anything.  This is the 'read the actual skill
    before installing' affordance the Browse-hub tab was missing.
    rr   r   r	  r   c                     ddl m}  ddlm}  |            } | |          \  }}}|s|sd S i }d}|r|j        pi                                 D ]N\  }}	t          |	t                    r/	 |	                    d          ||<   4# t          $ r d||<   Y Ew xY w|	||<   O|
                    dd          pd}|p|}
t          |
d          t          |
d	d          pdt          |
d
d          pdt          |
d          pt          |
dd          pdt          |
dd           t          t          |
dd           pg           |t          |                                          d	S )Nr   _resolve_source_meta_and_bundler	  rr   r  z(binary file)SKILL.mdrO   r   rZ  r	  r	  r	  r	  r	  )	rO   r   rZ  r	  r	  r	  r	  skill_mdr<	  )hermes_cli.skills_hubr	  r	  r	  r<	  r%  r  ry	  rU  UnicodeDecodeErrorrv   r   r  r9  r  )r	  r	  r	  metabundle_srcr<	  r	  relr   r  r	  s              rE   r	  zpreview_skill_hub.<locals>._runo   s   IIIIII999999&&((<<UGLLfd 	d 	4 	7!'!3 : : < < 
) 
)Wgu-- 	)5%,^^G%<%<c

- 5 5 5%4c


5 ")E#JJyyR006BHNFAvu--"1mR88>Ba2..4"!!\599BU"1m[AAP[Avt,,FD117R88 EJJLL))

 

 
	
s   *BBBNzskills hub preview failedr  zHub preview failed: r  Skill not found: r   r%   rR   r  r@   r=   r  r	  r	  r7  r  r	  s       @rE   preview_skill_hubr	  b   s       2$$&&E N4LMMMM$
 $
 $
 $
 $
LR(........ R R R23334P34P4PQQQQR ~4O4O4OPPPPM   A 
B.BBz/api/skills/hub/scanc                 L  K   | pd                                 st          dd          fd}	 t          j        |           d{V }n@# t          $ r3}t
                              d           t          dd	|           d}~ww xY w|t          d
d           |S )u  Run the install-time security scan on a hub skill WITHOUT installing it.

    Fetches the bundle, quarantines it, and runs the same `scan_skill` /
    `should_allow_install` pipeline the CLI installer uses — then cleans up the
    quarantine.  Returns the verdict, per-finding detail, trust tier, and the
    install-policy decision so the dashboard can show a visual safety result
    on demand (the 'scan' button the Browse-hub tab was missing).
    rr   r   r	  r   c                     dd l } ddlm} ddlm}m} ddlm}m}  |            } ||          \  }}}	|sd S |j	        dk    rd}
n$t          |dd          pt          |dd          p}
d }	  ||          } |||
          }||                     |d	
           n# ||                     |d	
           w w xY w ||d          \  }}|d	u rd}n|d}nd}d |j        D             }ddddd}|j        D ] }|j        |v r||j        xx         dz  cc<   !|j        |j	        |j        |j        |j        ||||d
S )Nr   r	  )r	  quarantine_bundle)
scan_skillshould_allow_installr	  r	  rr   )rZ  T)ignore_errorsF)r  allowr   blockc                 P    g | ]#}|j         |j        |j        |j        |j        d $S )severityr   r  r  r   r	  )r
  rz  s     rE   r  z0scan_skill_hub.<locals>._run.<locals>.<listcomp>   sJ     	
 	
 	
  JJ } 	
 	
 	
r   )criticalr   r   r   r   )
rO   r	  rZ  r	  verdictr  r  policy_reasonfindingsseverity_counts)r  r	  r	  r	  r	  r	  tools.skills_guardr	  r	  rZ  r   r  r	  r	  
skill_namer	  r	  r  )_shutilr	  r	  r	  r	  r	  r	  r	  r	  r	  scan_sourceq_pathr7  r_	  reasonr  r	  countsrz  r	  s                      rE   r	  zscan_skill_hub.<locals>._run   s+       IIIIIILLLLLLLLGGGGGGGG&&((<<UGLLfd 	4=J&&$KK b11 4r22  	;&&v..FZ{;;;F!vT::: !vT:::: " /.vUCCCd??FF_FFF	
 	
 _	
 	
 	
  aBB 	( 	(AzV##qz"""a'""" %m!-~~# %
 
 	
s   /B! !B=Nzskills hub scan failedr  zHub scan failed: r  r	  r	  r	  s       @rE   scan_skill_hubr	     s       2$$&&E N4LMMMMA
 A
 A
 A
 A
FO(........ O O O/0004M4M4MNNNNO ~4O4O4OPPPPMr	  c                       e Zd ZU eed<   dZeed<   dZeed<   dZeed<   dZ	e
e         ed<   dZe
e         ed<   dZe
e         ed	<   dZe
e         ed
<   g Zed         ed<   g Zee         ed<   g Zee         ed<   dS )ProfileCreaterO   Fclone_from_default	clone_all	no_skillsNr   
clone_fromro  r   r  r  keep_skills
hub_skills)r2  r3  r4  r6  r5  r	  r  r	  r	  r   r   r	  ro  r   r  r   r	  r	  r7  r   rE   r	  r	     s         
III$$$$ItIt!%K#%%%
 !%J$$$"Hhsm"""E8C=
 ,.K'(---
  Kc
 JS	r   r	  c                       e Zd ZU eed<   dS )ProfileRenamenew_nameNrV  r7  r   rE   r	  r	  !  s         MMMMMr   r	  c                       e Zd ZU eed<   dS )ProfileSoulUpdater   NrV  r7  r   rE   r 
  r 
  !!  s         LLLLLr   r 
  c                       e Zd ZU eed<   dS )ProfileActiveUpdaterO   NrV  r7  r   rE   r
  r
  %!  rW  r   r
  c                       e Zd ZU dZeed<   dS )ProfileDescriptionUpdaterr   r   N)r2  r3  r4  r   r6  r5  r7  r   rE   r
  r
  )!  s"         Kr   r
  c                   $    e Zd ZU eed<   eed<   dS )ProfileModelUpdatero  r   NrV  r7  r   rE   r
  r
  -!  s"         MMMJJJJJr   r
  c                       e Zd ZU dZeed<   dS )ProfileDescribeAutoFrS  N)r2  r3  r4  rS  r  r5  r7  r   rE   r
  r
  2!  s"         Itr   r
  c                 H    	 t          | |          S # t          $ r |cY S w xY wrE  )r   r@   )r>   rO   r   s      rE   _profile_attrr

  6!  s;    tT"""   s    !!c                 d   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
          t          t          | dd                    t          | dd          pdt          t          | dd                    t          | d          t          | d          t          | d          t          | d          d udS )NrO   rr   r   
is_defaultFr   ro  has_envskill_countr   r  r   description_autodistribution_namedistribution_versiondistribution_source
alias_pathrO   r   r
  r   ro  r
  r
  r  r   r
  r
  r
  r
  	has_alias)r

  r6  r  r  )r>   s    rE   r"  r"  =!  s   dFB//M$3344=|UCCDDtW--!$
33dIu==>>=}a@@EAFFd4Eu M MNN$T="==C t5G!O!OPP*41DEE -d4J K K,T3HII"466dB  r   c                     d }g }                                                                  r | fdd          \  }}|                    dt                    d||dz                                   | fdd           | fd	d
           | fdd           | fdd
          d d d d
d                                            }|                                rt          |                                          D ]}|                                r j        	                    |j
                  s6 ||f fd	d          \  }}|                    |j
        t          |          d
|||dz                                   ||f fd	d           ||f fd	d
           ||f fd	d           ||f fd	d
          d d d d
d           |S )Nc                 <    	  |             S # t           $ r |cY S w xY wrE  )r@   )	callable_r   s     rE   _safez&_fallback_profile_dicts.<locals>._safeQ!  s7    	9;; 	 	 	NNN	s   	 c                  .                                    S rE  _read_config_modeldefault_homer  s   rE   r  z)_fallback_profile_dicts.<locals>.<lambda>Z!  s    (G(G(U(U r   NNr   Tz.envc                  .                                    S rE  _count_skillsr
  s   rE   r  z)_fallback_profile_dicts.<locals>.<lambda>b!  s    )C)CL)Q)Q r   r   c                  .                                    S rE  _check_gateway_runningr
  s   rE   r  z)_fallback_profile_dicts.<locals>.<lambda>c!  s    \-P-PQ]-^-^ r   Fc                  V                                                        dd          S Nr   rr   read_profile_metarv   r
  s   rE   r  z)_fallback_profile_dicts.<locals>.<lambda>d!  s'    )G)G)U)U)Y)YZgik)l)l r   rr   c                  V                                                        dd          S Nr
  Fr(
  r
  s   rE   r  z)_fallback_profile_dicts.<locals>.<lambda>e!  s'    l.L.L\.Z.Z.^.^_qsx.y.y r   r
  c                 .                         |           S rE  r
  r-  r  s    rE   r  z)_fallback_profile_dicts.<locals>.<lambda>q!  s    8W8WX]8^8^ r   c                 .                         |           S rE  r!
  r-
  s    rE   r  z)_fallback_profile_dicts.<locals>.<lambda>y!  s    9S9STY9Z9Z r   c                 .                         |           S rE  r$
  r-
  s    rE   r  z)_fallback_profile_dicts.<locals>.<lambda>z!  s    \=`=`af=g=g r   c                 V                         |                               dd          S r'
  r(
  r-
  s    rE   r  z)_fallback_profile_dicts.<locals>.<lambda>{!  s(    9W9WX]9^9^9b9bcprt9u9u r   c                 V                         |                               dd          S r+
  r(
  r-
  s    rE   r  z)_fallback_profile_dicts.<locals>.<lambda>|!  s2    l>\>\]b>c>c>g>ghz  }B  ?C  ?C r   )_get_default_hermes_homer)  r=  r6  r"  _get_profiles_rootr9  r  _PROFILE_ID_REr+  rO   )r  r
  rv  r   ro  profiles_rootr-  r
  s   `      @rE   r$  r$  P!  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$u%^%^%^%^%^`eff 5!l!l!l!l!lnpqq %&y&y&y&y&y  |A  !B  !B!%$(#'
 
 	 	 	" !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\]^^#(5e)g)g)g)g)gin#o#o$u%%u%u%u%u%uwyzz$)Eu  +C  +C  +C  +C  +C  EJ  %K  %K%)(,'+"     " Or   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   ru  r   r   Nr  r(  r)  )r  rv  r+  r  r%   r6  r,  r  )rO   r  rD   s      rE   r	  r	  !  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)r	  r  s    rE   _profile_setup_commandr8
  !  s*    !Y..>>tOOOCr   r	  c                 .   ddl m}m}  |t          |                     }	 t	          ||          \  }}t                      }t          |                    di           ||          |d<   t          |            ||           dS #  ||           w xY w)a  Write the main model assignment into a specific profile's config.yaml.

    Scopes ``load_config``/``save_config`` to ``profile_dir`` via the
    context-local HERMES_HOME override so the write lands in the target
    profile's config rather than the dashboard process's active profile.
    Clears any stale ``base_url`` / ``context_length`` the same way
    ``POST /api/model/set`` does, since the new model may differ.
    r   set_hermes_home_overridereset_hermes_home_overrider   N)	rs  r;
  r<
  r6  r  r   r  rv   r   )r	  ro  r   r;
  r<
  r  r  s          rE   _write_profile_modelr=
  !  s     VUUUUUUU$$S%5%566E*:8UKK%mm3CGGGR4H4H(TYZZGC""5)))))""5))))s   AB Br  c                    ddl m}m} d} |t          |                     }	 t	                      }|                    di           }|D ]}|j        pd                                }	|	s i }
|j        r
|j        |
d<   |j	        r
|j	        |
d<   |j
        rt          |j
                  |
d<   |j        rt          |j                  |
d<   |j        r
|j        |
d	<   |
s|
||	<   |d
z  }|rt          |           n'|s%|                    dd           t          |            ||           n#  ||           w xY w|S )a  Write MCP server entries into a specific profile's config.yaml.

    Scopes ``load_config``/``save_config`` to ``profile_dir`` via the
    context-local HERMES_HOME override (same mechanism as
    ``_write_profile_model``) so the entries land in the target profile's
    config rather than the dashboard process's active profile.

    Mirrors the per-server shape the ``POST /api/mcp/servers`` endpoint builds,
    but batched so the whole profile-create write is a single config save.
    Returns the number of servers written.
    r   r:
  r  rr   r   r  rM   rD  r}   r   N)rs  r;
  r<
  r6  r   r  rO   r   r   r  rM   r  rD  r  r}   r   r  )r	  r  r;
  r<
  writtenr  r  r  r]  rO   r-  s              rE   _write_profile_mcp_serversr@
  !  s    VUUUUUUUG$$S%5%566E *mmnn]B// 	 	FK%2,,..D $&Ez *%ze~ 2#)>i { 2 $V[ 1 1fz 0#FJ//e{ , &f  CIqLGG 	 	 GGM4(((""5))))""5))))Ns   C>D. .D;keepc                    ddl m}m} ddlm}m} d |D             }d} |t          |                     }	 g }	| dz  }
|
                                r7|
                    d          D ]!}|		                    |j
        j                   "t                      } ||          }|	D ]$}||vr||vr|                    |           |dz  }%|r |||            ||           n#  ||           w xY w|S )uA  Disable every installed skill in ``profile_dir`` not in ``keep``.

    Profiles manage skill activation via a *disabled* list — all installed
    skills are active by default and users opt out. The builder's skill step
    uses "replace" semantics: the user picks exactly which seeded built-in /
    optional skills stay active, and everything else gets added to the disabled
    list. (Hub skills are installed separately via subprocess and are active on
    install.) Scoped to the profile via the HERMES_HOME override. Returns the
    number of skills newly disabled.
    r   r:
  get_disabled_skillssave_disabled_skillsc                 b    h | ],}||                                 |                                 -S r7  re  rf  s     rE   r  z-_disable_unselected_skills.<locals>.<setcomp>!  s2    ;;;a1;;		;;;r   r   r	  r   )rs  r;
  r<
  hermes_cli.skills_configrD
  rE
  r6  r)  r{	  r=  r#  rO   r   r  )r	  rA
  r;
  r<
  rD
  rE
  keep_setdisabled_countr  r  skills_rootmdr  r  rO   s                  rE   _disable_unselected_skillsrL
  !  sl    VUUUUUUURRRRRRRR;;4;;;HN$$S%5%566E*!	!H, 	1!''
33 1 1  0000mm&&s++ 	$ 	$D8##H(<(<T"""!# 	0  h///""5))))""5))))s   B C$ $C1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   ru  rv  c                 ,    g | ]}t          |          S r7  r!  r  s     rE   r  z*list_profiles_endpoint.<locals>.<listcomp>	"  s!    WWWQ-a00WWWr   z@GET /api/profiles failed; falling back to profile directory scanr#  r%  s    rE   list_profiles_endpointrO
  "  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                 v  K   ddl m} | j        pd                                }|rd}|}| j         }n#| j        p| j        }|rdnd }| j        o| j         }	 |                    | j        || 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| j        pd                                }	| j        pd                                }
d}|	rG|
rE	 t/          ||	|
           d}n0# t$          $ r# t&                              d| j                   Y nw xY wd}| j        rG	 t3          || j                  }n0# t$          $ r# t&                              d| j                   Y nw xY wd}| j        rG	 t7          || j                  }n0# t$          $ r# t&                              d| j                   Y nw xY wg }| j        D ]}|pd                                }|s	 t;          d| j        dd|gd          }|                    ||j        d           T# t$          $ r< t&                              d|| j                   |                    |d d           Y w xY wd| j        t#          |          ||||dS )Nr   ru  rr   Tr   )rO   r	  r	  clone_configr	  r   )quietr   r   zPOST /api/profiles failedr`  Fz'Setting model for new profile %s failedz-Writing MCP servers for new profile %s failedz2Applying skill selection for new profile %s failedr	  r   r  r  )r	  r  z7Spawning hub-skill install %s for new profile %s failed)r  rO   r   	model_setmcp_writtenskills_disabledhub_installs) r  rv  r	  r   r	  r	  create_profilerO   r	  r   seed_profile_skillscheck_alias_collisioncreate_wrapper_scriptr  FileExistsErrorr  r%   r6  r@   r=   r  ro  r   r=
  r  r@
  r	  rL
  r	  r{  r=  r  )r  r  explicit_sourcecloner	  rQ
  r   	collisionrD   ro  r   rS
  rT
  rU
  rV
  r	  r	  r\  s                     rE   create_profile_endpointr_
  "  s$     333333,"3355O 	F $
>)'94>"'1YYT
.Et~3E<**!n%n( + 
 
  	?,,T,>>> !66tyAA	 	:..ty999):; < < <CFF;;;; < < <2333CFF;;;;< #**,,HZ2$$&&EI QE Q	Q x777II 	Q 	Q 	QNNDdiPPPPP	Q K W	W4T4;KLLKK 	W 	W 	WNNJDIVVVVV	W O \	\8t?OPPOO 	\ 	\ 	\NNOQUQZ[[[[[	\ *,Lo D D
!r((** 		D'ty(Iu=  D uTX F FGGGG 	D 	D 	DNNI	  
 uT B BCCCCC	D 	D		"*$  sn   A>C E-DE8EEF) )*GG#G9 9*H&%H&3I	 	*I65I67KALLz/api/profiles/activec                     K   ddl m}  	 |                                 pd}n# t          $ r d}Y nw xY w	 |                                 pd}n# t          $ r d}Y nw xY w||dS )uH  Return the sticky active profile and the profile this dashboard
    process is currently running as.

    ``active`` is the sticky default written by ``hermes profile use`` —
    the profile new CLI invocations pick up. ``current`` is the profile
    the running dashboard/gateway is scoped to (derived from HERMES_HOME).
    r   ru  r   )r  rc  )r  rv  get_active_profiler@   get_active_profile_name)r  r  rc  s      rE   get_active_profile_endpointrc
  z"  s       4333330022?i   6688EI   111s   ! 00A AAc                   K   ddl m} 	 |                    | j                   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	|                    | j                  d
S )u   Set the sticky active profile (mirrors ``hermes profile use``).

    Note: this does not retarget the already-running dashboard process —
    it changes which profile subsequent CLI commands and gateways use.
    r   ru  r  r   Nr   z POST /api/profiles/active failedr`  TrD	  )r  rv  set_active_profilerO   r  r%   r6  r  r@   r=   r  r*  )r  r  rD   s      rE   set_active_profile_endpointrf
  "  s      433333<''	2222 < < <CFF;;;; < < <CFF;;;; < < <9:::CFF;;;;< ,"E"Edi"P"PQQQs,   % 
CACA88C8B==Cz"/api/profiles/{name}/setup-commandc                 (   K   dt          |           iS )Nr  )r8
  r  s    rE   get_profile_setup_commandrh
  "  s      -d3344r   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/crZ   rr   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 'r  zmate-terminal
lxterminaltilix	alacrittykittyxtermwhich)r6  r  r   r   z$No supported terminal emulator foundr   r  z*POST /api/profiles/%s/open-terminal failedr`  T)r  r  )r8
  r  r  r   r3  r  r)  callr  r%   r  r6  r  r@   r=   r  )rO   r  escapedapplescriptterminal_commandsr  
popen_argsrD   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   ru  r  r   r   zPATCH /api/profiles/%s failedr`  T)r  rO   r   )r  rv  rename_profiler	  r  r%   r6  r  r[
  r@   r=   r  )rO   r  r  r   rD   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   ru  T)yesr  r   Nr   zDELETE /api/profiles/%s failedr`  r  )
r  rv  delete_profiler  r%   r6  r  r@   r=   r  )rO   r  r   rD   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)r   r"  r`  zCould not read SOUL.md: r   rr   F)r	  r"  r  r  r%   )rO   	soul_pathrD   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: r   r  T)r	  
write_textr   r  r=   r  r%   )rO   r  r
  rD   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z /api/profiles/{name}/descriptionc                 <  K   ddl m} t          |           }|j        pd                                }	 |                    ||d           nK# t          $ r>}t                              d|            t          dt          |                    d	}~ww xY wd
|ddS )zSet or clear a profile's role description (kanban routing signal).

    Empty string clears the description. Non-empty stores it as a
    user-authored description (``description_auto: false``) so the
    auto-describer won't overwrite it on a sweep.
    r   ru  rr   F)r   r
  z'PUT /api/profiles/%s/description failedr`  r   NT)r  r   r
  )r  rv  r	  r   r   write_profile_metar@   r=   r  r%   r6  )rO   r  r  r	  r  rD   s         rE   #update_profile_description_endpointr
  #  s       433333&t,,K"))++D<''" 	( 	
 	
 	
 	

  < < <@$GGGCFF;;;;< tGGGs   A 
B9BBz/api/profiles/{name}/modelc                   K   t          |           }|j        pd                                }|j        pd                                }|r|st	          dd          	 t          |||           nK# t          $ r>}t                              d|            t	          dt          |                    d}~ww xY wd||d	S )
a  Set the main model (``model.default`` + ``model.provider``) for a
    specific profile's config.yaml, without touching the dashboard's own
    active profile. Mirrors ``POST /api/model/set`` (main scope) but scoped
    to the named profile via the HERMES_HOME override.
    rr   r   zprovider and model are requiredr   z!PUT /api/profiles/%s/model failedr`  NT)r  ro  r   )
r	  ro  r   r   r%   r=
  r@   r=   r  r6  )rO   r  r	  ro  r   rD   s         rE   update_profile_model_endpointr
  ,#  s       't,,K#**,,HZ2$$&&E W5 W4UVVVV<[(E:::: < < <:DAAACFF;;;;< Hu===s   A0 0
B8:9B33B8z"/api/profiles/{name}/describe-autoc                   K   t          |            	 ddlm} |                    | t	          |j                            }nK# t          $ r>}t                              d|            t          dt          |                    d}~ww xY wt	          |j                  |j        |j        t	          |j                  dS )	uo  Auto-generate a profile's description via the auxiliary LLM
    (``auxiliary.profile_describer``). Mirrors ``hermes profile describe
    <name> --auto``.

    A failed generation (no aux client, LLM error, …) is returned as
    ``ok: false`` with a reason rather than an HTTP error so the UI can
    surface it inline and let the operator fix config and retry.
    r   )r  )rS  z*POST /api/profiles/%s/describe-auto failedr`  r   N)r  r	  r   r
  )r	  r  r  describe_profiler  rS  r@   r=   r  r%   r6  r  r	  r   )rO   r  r  outcomerD   s        rE   describe_profile_auto_endpointr
  @#  s       <000000#44TT$.EYEY4ZZ < < <CTJJJCFF;;;;< 7:.* !,,  s   /A 
B9BBc              #   ~  K   | pd                                 }ddlm}m}m} ddlm} ddlm} d}|r|                                dk    r |            }n't          |          } |t          |                    }t          5  |j        }	|j        }
|j        }|j        }||_        |dz  |_        ||_        |dz  |_        	 ||ndV  |	|_        |
|_        ||_        ||_        | ||           n/# |	|_        |
|_        ||_        ||_        | ||           w w xY wddd           dS # 1 swxY w Y   dS )	u  Scope config + skill-directory resolution to ``profile`` for one request.

    Two seams must be redirected for skills/toolsets endpoints:

    1. ``load_config``/``save_config`` resolve ``get_hermes_home()`` at call
       time — the context-local override from ``set_hermes_home_override``
       reaches them (same pattern as ``_write_profile_model``).
    2. ``tools.skills_tool`` and ``tools.skill_manager_tool`` bind
       ``SKILLS_DIR`` at import time, so the override CANNOT reach them.
       Like ``_call_cron_for_profile`` does for cron's module globals,
       temporarily retarget both under a lock and restore them
       immediately after.

    ``profile`` of None/""/"current" means "the dashboard's own profile" —
    config resolution is untouched, but the skill-module globals are still
    retargeted to the *current* ``get_hermes_home()`` so writes land in the
    live home even when the import-time binding is stale (e.g. the process
    imported the modules before a HERMES_HOME override, or under test
    isolation).
    rr   r   )r   r;
  r<
  )skills_tool)skill_manager_toolNrc  r   )r   rs  r   r;
  r<
  rI  r
  r
  r   r	  r6  _SKILLS_PROFILE_LOCKrq  
SKILLS_DIR)r1  rJ  r   r;
  r<
  _skills_tool
_skill_mgrr  r	  old_homeold_skills_dirold_mgr_homeold_mgr_skills_dirs                rE   r  r  l#  s     , B%%''I         
 211111666666E ;	))Y66%o''*955(([)9)9::	 2 2+%0!-'2#. "-"8!,
 +h 6
	2!&!2++<<<'/L$&4L#%1J"$6J! **5111 (0L$&4L#%1J"$6J! **51111 !!2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2s*   ?D2C6*D26,D""D22D69D6c                   >    e Zd ZU eed<   eed<   dZee         ed<   dS )SkillTogglerO   rC  Nr1  )r2  r3  r4  r6  r5  r  r1  r   r7  r   rE   r
  r
  #  s8         
IIIMMM!GXc]!!!!!r   r
  z/api/skillsc                    K   ddl m} ddlm} t	          |           5  t                      } ||          } |d          }d d d            n# 1 swxY w Y   |D ]}|d         |v|d<   |S )Nr   )_find_all_skills)rD
  T)skip_disabledrO   rC  )tools.skills_toolr
  rG
  rD
  r  r   )r1  r
  rD
  r  r  r   r  s          rE   
get_skillsr
  #  s      222222<<<<<<		 	  6 6&&v..!!5556 6 6 6 6 6 6 6 6 6 6 6 6 6 6  1 1y0)Ms   &AAAz/api/skills/togglec                 T  K   ddl m}m} t          | j        p|          5  t                      } ||          }| j        r|                    | j                   n|	                    | j                    |||           d d d            n# 1 swxY w Y   d| j        | j        dS )Nr   rC
  Tr  )
rG
  rD
  rE
  r  r1  r   rC  r   rO   r  )r  r1  rD
  rE
  r  r  s         rE   toggle_skillr
  #  s     RRRRRRRR	/	0	0 / /&&v..< 	$TY''''LL###VX.../ / / / / / / / / / / / / / / 	dlCCCs   A"BBBc                   X    e Zd ZU eed<   eed<   dZee         ed<   dZee         ed<   dS )SkillCreaterO   r   Nr   r1  )r2  r3  r4  r6  r5  r   r   r1  r7  r   rE   r
  r
  #  sL         
IIILLL"Hhsm"""!GXc]!!!!!r   r
  c                   >    e Zd ZU eed<   eed<   dZee         ed<   dS )SkillContentUpdaterO   r   Nr1  r=  r7  r   rE   r
  r
  #  s8         
IIILLL!GXc]!!!!!r   r
  c                  N    	 ddl m}   | d           dS # t          $ r Y dS w xY w)zBest-effort: invalidate the skills system-prompt snapshot after a write.

    Mirrors what ``skill_manage`` does so a dashboard-authored skill is picked
    up by the next session without a manual cache reset.
    r    clear_skills_system_prompt_cacheT)clear_snapshotN)agent.prompt_builderr
  r@   r
  s    rE   _clear_skills_prompt_cacher
  #  sU    IIIIII((======   s    
$$z/api/skills/contentc                   K   ddl m} t          |          5   ||           }|st          dd|  d          |d         dz  }|                                st          dd|  d	          	 |                    d
          }n1# t          $ r$}t          dt          |                    |d}~ww xY w| |t          |          dcddd           S # 1 swxY w Y   dS )zCReturn the raw SKILL.md text for a skill, for the dashboard editor.r   )_find_skillr  zSkill 'z' not found.r   r   r	  z' has no SKILL.md.r  r  r`  N)rO   r   r   )tools.skill_manager_toolr
  r  r%   r"  r  r  r6  )rO   r1  r
  foundr	  r   r  s          rE   get_skill_contentr
  #  s      544444		 	  I ID!! 	VC8T$8T8T8TUUUU=:-   	\C8Z$8Z8Z8Z[[[[	K(('(::GG 	K 	K 	KCCAAAsJ	K#h--HHI I I I I I I I I I I I I I I I I Is6   AC0BC
B5B00B55CCCc                 :  K   ddl m} t          | j                  5   || j        | j        | j        pd          }ddd           n# 1 swxY w Y   |                    d          s%t          d|                    dd                    t                       |S )	uk  Create a new custom skill (SKILL.md) from the dashboard editor.

    Calls the same validated write path as the agent's ``skill_manage``
    tool (frontmatter validation, name/category validation, size limit,
    optional security scan) — but bypasses the agent write-approval gate:
    a write from the authenticated dashboard IS the user acting directly.
    r   )_create_skillNr(  r   r  zFailed to create skill.r   )
r
  r
  r  r1  rO   r   r   rv   r%   r
  )r  r
  r7  s      rE   create_skillr
  #  s       766666		%	% O Oty$,8MNNO O O O O O O O O O O O O O O::i   dFJJwHa4b4bcccc   Ms   AAAc                 |  K   ddl m} t          | j                  5   || j        | j                  }ddd           n# 1 swxY w Y   |                    d          sN|                    dd          }dt          |                                          v rdnd	}t          ||
          t                       |S )zIReplace the SKILL.md of an existing skill (full rewrite) from the editor.r   )_edit_skillNr(  r  zFailed to update skill.	not foundr  r   r   )r
  r
  r  r1  rO   r   rv   r6  r   r%   r
  )r  r
  r7  errr  s        rE   update_skill_contentr
  $  s      544444		%	% 6 6TY556 6 6 6 6 6 6 6 6 6 6 6 6 6 6::i   <jj";<<#s3xx~~'7'777Ss;;;;   Ms   A  AAz/api/tools/toolsetsc                   K   ddl m}m}m}m} ddlm} t          |           5  t                      } ||dd          }d d d            n# 1 swxY w Y   g } |            D ]s\  }	}
}	 t          t           ||	                              }n# t          $ r g }Y nw xY w|	|v }|                    |	 ||
          ||| ||	|          |d           t|S )Nr   )$_get_effective_configurable_toolsetsr-  _toolset_has_keysgui_toolset_label)resolve_toolsetr.  Fr/  )rO   r  r   rC  r@  r  rI  )r8  r
  r-  r
  r
  toolsetsr
  r  r   r9  r[   r@   r=  )r1  r
  r-  r
  r
  r
  r  r1  r7  rO   r  descrI  ra  s                 rE   get_toolsetsr
  $  s                 )(((((		 	  
 
..(-
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 FAACC  eT	3t445566EE 	 	 	EEE	--
&&u--!#++D&99
 
 	 	 	 	 Ms#   AAA+%BB B c                   4    e Zd ZU eed<   dZee         ed<   dS )ToolsetTogglerC  Nr1  r  r7  r   rE   r
  r
  ;$  r  r   r
  z/api/tools/toolsets/{name}c                   K   ddl m}m}m} d  |            D             }| |vrt	          dd|            t          |j        p|          5  t                      }t           ||dd	                    }|j	        r|
                    |            n|                    |             ||d|           d
d
d
           n# 1 swxY w Y   d| |j	        dS )aI  Enable/disable a configurable toolset for the desktop (cli) platform.

    Persists to ``platform_toolsets.cli`` via the same ``_save_platform_tools``
    helper the CLI ``hermes tools`` picker uses, so the GUI and CLI stay in
    lockstep. Scoped to ``body.profile`` when provided. Returns 400 for
    unknown toolset keys.
    r   )r
  r-  _save_platform_toolsc                     h | ]\  }}}|	S r7  r7  r
  ts_keyr#  s      rE   r  z!toggle_toolset.<locals>.<setcomp>O$      OOO1VOOOr   r   Unknown toolset: r   r.  Fr/  NTr  )r8  r
  r-  r
  r%   r  r1  r   r[   rC  r  r   )	rO   r  r1  r
  r-  r
  r  r  rC  s	            rE   toggle_toolsetr
  @$  sr               PO(L(L(N(NOOOE54N4N4NOOOO	/	0	0 	5 	55QQQ
 
 < 	"KKOOD!!!VUG444	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 >>>s   A)CCCz!/api/tools/toolsets/{name}/configc                   K   ddl m}m}m}m} ddlm d  |            D             }| |vrt          dd|            t          |          5  t                      }|
                    |           }g }	d}
|r |||d	
          D ]}fd|
                    dg           D             } |||d	
          }|r
|
|d         }
|	                    |d         |
                    dd          |
                    dd          ||
                    d          t          |
                    d                    |d           ddd           n# 1 swxY w Y   | |du|	|
dS )a  Return the provider matrix + key status for a toolset's config panel.

    Surfaces the same provider rows the CLI ``hermes tools`` picker shows
    (via ``_visible_providers``), each with its ``env_vars`` annotated with
    current ``is_set`` state so the GUI can render provider selection + key
    entry. Toolsets without a ``TOOL_CATEGORIES`` entry return an empty
    provider list and ``has_category: false``. Returns 400 for unknown keys.
    r   )TOOL_CATEGORIESr
  _is_provider_active_visible_providersr  c                     h | ]\  }}}|	S r7  r7  r
  s      rE   r  z%get_toolset_config.<locals>.<setcomp>r$  r
  r   r   r
  r   NTr  c                     g | ]l}|d          |                     d|d                    |                     d          |                     d          t           |d                              dmS )r+  r1   r   r   )r+  r1   r   r   r?   )rv   r  )r
  rD   r  s     rE   r  z&get_toolset_config.<locals>.<listcomp>}$  s     	 	 	   !x"#%%!E(";"; uuU||#$55#3#3"&}}QuX'>'>"?"? 	 	 	r   r~  rO   badgerr   tag
post_setuprequires_nous_auth)rO   r
  r
  r~  r
  r
  rn  )rO   has_categoryr  active_provider)r8  r
  r
  r
  r
  r  r  r%   r  r   rv   r=  r  )rO   r1  r
  r
  r
  r
  r  r  catr  r
  provr~  rn  r  s                 @rE   get_toolset_configr
  `$  sS                 0/////OO(L(L(N(NOOOE54N4N4NOOOO		 	     !!$''	 	**3DIII  	 	 	 	 "XXj"55	 	 	 0/f$OOO	 3!8&*6lO   L!XXgr2288E2.. ("&((<"8"8*.txx8L/M/M*N*N!*" "    1                             D 4*	  s   C5EEEc                   4    e Zd ZU eed<   dZee         ed<   dS )ToolsetProviderSelectro  Nr1  r=  r7  r   rE   r
  r
  $  s/         MMM!GXc]!!!!!r   r
  z#/api/tools/toolsets/{name}/providerc                   K   ddl m}m} d  |            D             }| |vrt          dd|            t	          |j        p|          5  t                      }	  || |j        |           nC# t          $ r6}t          dt          |          
                    d                    d}~ww xY wt          |           ddd           n# 1 swxY w Y   d	| |j        d
S )u  Persist a provider selection for a toolset (no key prompting).

    Delegates to ``apply_provider_selection`` — the shared, non-interactive
    core extracted from the CLI configurator — so the GUI and ``hermes tools``
    write identical config keys (``web.backend``, ``tts.provider``, etc.).
    API keys and post-setup flows are handled by separate endpoints. Returns
    400 for unknown toolset or provider names.
    r   )apply_provider_selectionr
  c                     h | ]\  }}}|	S r7  r7  r
  s      rE   r  z*select_toolset_provider.<locals>.<setcomp>$  r
  r   r   r
  r   r  NT)r  rO   ro  )r8  r
  r
  r%   r  r1  r   ro  r  r6  r   r   )rO   r  r1  r
  r
  r  r  r  s           rE   select_toolset_providerr
  $  sr            
 PO(L(L(N(NOOOE54N4N4NOOOO	/	0	0  	M$$T4=&AAAA 	M 	M 	MCCs8K8KLLLL	MF               $-@@@s6   CA0/C0
B0:1B++B00CCCc                   D    e Zd ZU eeef         ed<   dZee         ed<   dS )ToolsetEnvUpdaterD  Nr1  )r2  r3  r4  r   r6  r5  r1  r   r7  r   rE   r
  r
  $  s:         	c3h!GXc]!!!!!r   r
  z/api/tools/toolsets/{name}/envc                   K   ddl m}m}m} ddlmm} d  |            D             }| |vrt          dd|            t          |j	        p|          5  t                      }|                    |           }	t                      |	rE ||	|d	          D ]6}
|
                    d
g           D ]}                    |d                    7fd|j        D             }|r7t          dd|  dd                    t!          |                               g }g }|j                                        D ]\  }}|rz|                                rf	  |||                                           n0# t&          $ r#}t          dt)          |                    d}~ww xY w|                    |           |                    |           fdD             }ddd           n# 1 swxY w Y   d| |||dS )uX  Persist API keys for a toolset's provider env vars.

    Writes each ``key: value`` to ``~/.hermes/.env`` via ``save_env_value`` —
    the same store ``hermes tools`` writes when it prompts for keys. Keys are
    validated against the env-var allowlist for the toolset's category (the
    union of every visible provider's ``env_vars``), so the GUI can't write an
    arbitrary env var through this endpoint. A blank value is treated as
    "leave unchanged" and skipped. Returns the saved/skipped key lists and the
    refreshed ``is_set`` status. Returns 400 for unknown toolset or env keys.
    r   )r
  r
  r
  )r  r   c                     h | ]\  }}}|	S r7  r7  r
  s      rE   r  z#save_toolset_env.<locals>.<setcomp>$      RRR<61aRRRr   r   r
  r   Tr  r~  r+  c                     g | ]}|v|	S r7  r7  )r
  r  r_	  s     rE   r  z$save_toolset_env.<locals>.<listcomp>$  s#    ;;;!7*:*:1*:*:*:r   zUnknown env var(s) for toolset rG	  ry  Nc                 B    i | ]}|t           |                    S r7  )r  )r
  r  r  s     rE   r  z$save_toolset_env.<locals>.<dictcomp>$  s-    ===!T--**++===r   )r  rO   savedskippedr?   )r8  r
  r
  r
  r  r  r   r%   r  r1  r   rv   r[   r  rD  r  r9  r%  r   r  r6  r=  )rO   r  r1  r
  r
  r
  r   valid_tsr  r
  r
  rD   r  r
  r
  r+  r  r  r  r_	  r  s                      @@rE   save_toolset_envr
  $  s              
 @???????RR+O+O+Q+QRRRH84N4N4NOOOO	/	0	0 > >!!$''EE 	***3DIII * **b11 * *AKK%))))* <;;;dh;;; 	]]]6RY??A[A[]]   
 (..** 	$ 	$JC $ $J"N36666! J J J'CCIIIIJS!!!!s####====W===9> > > > > > > > > > > > > > >: uTZ[[[s7   C=G,E65G,6
F# FF##=G,,G03G0c                   4    e Zd ZU eed<   dZee         ed<   dS )ToolsetPostSetupr+  Nr1  r=  r7  r   rE   r
  r
  $  r>  r   r
  z%/api/tools/toolsets/{name}/post-setupc                   K   ddl m}m} d  |            D             }| |vrt          dd|            |j         |            vrt          dd|j                   	 t          t          |j        p|          dd	|j        gz   d
          }nJ# t          $ r  t          $ r3}t          
                    d           t          dd|           d}~ww xY wd|j        d
|j        dS )u  Spawn a provider's post-setup install hook as a background action.

    Post-setup hooks (npm install for browser/Camofox, pip install for
    KittenTTS/Piper/ddgs, cua-driver fetch, etc.) are long-running and
    text-output, so this follows the spawn-action pattern: it launches
    ``hermes tools post-setup <key>`` and the frontend tails the log via
    ``GET /api/actions/tools-post-setup/status``. The ``key`` is validated
    against the declared post-setup allowlist before spawning. Returns 400
    for unknown toolset or post-setup key.

    ``profile`` spawns the hook as ``hermes -p <profile> tools post-setup``.
    Most hooks install machine-level artifacts (repo node_modules, shared
    pip packages) where the scope is inert, but hooks that read config or
    write per-profile state must see the same HERMES_HOME the rest of the
    drawer's writes targeted — so the scope is threaded for consistency.
    r   )r
  valid_post_setup_keysc                     h | ]\  }}}|	S r7  r7  r
  s      rE   r  z)run_toolset_post_setup.<locals>.<setcomp>%  r
  r   r   r
  r   zUnknown post-setup key: rI  z
post-setupr  z Failed to spawn tools post-setupr`  zFailed to run post-setup: NT)r  r  rO   r+  )r8  r
  r
  r%   r+  r{  r  r1  r@   r=   r  r  )rO   r  r1  r
  r
  r
  r\  r  s           rE   run_toolset_post_setupr
  %  sv     (       
 SR+O+O+Q+QRRRH84N4N4NOOOOx,,....$Itx$I$I
 
 
 	

#dl5g66dh/0
 

     
 
 
9:::$F$F$F
 
 
 	


 tx1CDHUUUs   "/B C&.CCc                   4    e Zd ZU eed<   dZee         ed<   dS )RawConfigUpdate	yaml_textNr1  r=  r7  r   rE   r
  r
  ;%  s/         NNN!GXc]!!!!!r   r
  z/api/config/rawc                   K   t          |           5  t                      }ddd           n# 1 swxY w Y   |                                sdt          |          dS |                    d          t          |          dS )uc  Raw config.yaml text plus its resolved path.

    ``path`` is resolved inside ``_profile_scope`` so the Config page header
    shows the file the switched profile actually reads/writes — /api/status's
    ``config_path`` is machine-global and always reports the dashboard
    process's own profile, which is wrong under the global profile switcher.
    Nrr   )r  r   r  r  )r  r   r"  r6  r  )r1  r   s     rE   get_config_rawr
  @%  s       
	 	  ! !  ! ! ! ! ! ! ! ! ! ! ! ! ! ! !;;== /CII...NNGN44c$iiHHHrS  c                 ^  K   	 t          j        | j                  }t          |t                    st          dd          t          | j        p|          5  t          |           d d d            n# 1 swxY w Y   ddiS # t           j	        $ r}t          dd|           d }~ww xY w)Nr   zYAML must be a mappingr   r  TzInvalid YAML: )
r  	safe_loadr
  r  r  r%   r  r1  r   	YAMLError)r  r1  r  rD   s       rE   update_config_rawr
  P%  s     J//&$'' 	RC8PQQQQDL3G44 	  	 	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 d|> J J J4HQ4H4HIIIIJs<   AB A5)B 5A99B <A9=B B,B''B,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   r  )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 r7  r  r
  r  s     rE   r  z'get_usage_analytics.<locals>.<listcomp>w%  s    111Qa111r   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 r7  r  r  s     rE   r  z'get_usage_analytics.<locals>.<listcomp>%  s    555DGG555r   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)r  
top_skills)dailyby_modeltotalsperiod_daysr   )r  r  agent.insightsr  r  r  r  r  r  fetchonegeneraterv   r   )r
  r  r  r"  ru  r  r  cur2r  cur3r  insights_reportr   s                rE   get_usage_analyticsr  b%  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   r  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 r7  r  r  s     rE   r  z(get_models_analytics.<locals>.<listcomp>%  s    000AQ000r   billing_providerrr   r   r  r  Nr  input_tokensoutput_tokenscache_read_tokensreasoning_tokensestimated_costactual_costr#  	api_calls
tool_callslast_used_atavg_tokens_per_session)r   ro  r  r  r  r  r  r  r#  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 != ''
        )r  r  r  )r  r  r  r  r  r  rv   r  r  r  r  r  r  r  r  r@   r=  r  r  r   )r
  r  r"  ru  r  r  r  r  ro  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rj
  )WinPtyBridgePtyUnavailableErrorc                       e Zd ZdZdS )r'  z,Stub when win_pty_bridge cannot be imported.Nr2  r3  r4  rs  r7  r   rE   r'  r'  &  s        >>Dr   r'  )	PtyBridger'  c                       e Zd ZdZdS )r'  z5Stub on platforms where pty_bridge can't be imported.Nr)  r7  r   rE   r'  r'  )&  s        GGDr   s   \x1b\[RESIZE:(\d+);(\d+)\]g?z^[A-Za-z0-9._-]{1,128}$>   r   ri  r   r   wsr'   c                 4   t          t          j        dd          rdS t          t          j        dd          pd                                                                }|r|t
          vrdS | j        r| j        j        nd}|sdS |t
          v rdS d| d|pd S )	a_  Return a rejection reason for the client IP, or None when allowed.

    Reasons are short machine-parseable tokens logged on the rejection path
    so a "WS keeps closing" report can be diagnosed from agent.log without a
    repro. ``None`` means the peer IP passed this gate.

    See :func:`_ws_client_is_allowed` for the full policy rationale.
    r   FNr   rr   zpeer_not_loopback peer= bound=r  r   rG   rP   r   r   _LOOPBACK_HOSTSrl  r   r,  r   rm  s      rE   _ws_client_reasonr2  5&  s     sy/511 t#)\266<"CCEEKKMMJ j77t$&I5")..2K to%%tL[LL9JsLLLr   c                    t          t          j        dd          rdS t          t          j        dd          pd                                                                }|r|t
          v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 bind: 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.

    Explicit non-loopback bind (``--host 0.0.0.0``, ``--host ::``, or a
    specific address such as a Tailscale/LAN IP, always with
    ``--insecure``): allow any peer. The operator explicitly opted into
    non-loopback exposure, so the loopback-only peer restriction does not
    apply. DNS-rebinding is still blocked by the Host/Origin guard in
    :func:`_ws_host_origin_is_allowed`, which mirrors the HTTP layer and
    requires the Host header to match the bound interface — the same
    defence ``_is_accepted_host`` applies to non-loopback HTTP requests.

    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.
    r   FTr   rr   r/  r1  s      rE   _ws_client_is_allowedr4  K&  s    0 sy/511 t #)\266<"CCEEKKMMJ j77t$&I5")..2K t/))r   c                    t          t          j        dd          }|sdS | j                            dd          }t          ||          s
d|pd d| S | j                            dd          }|sdS t          j                            |          }|j	        d	vrdS |j
        sd
| d| S t          |j
        |          sd
| d| S dS )u  Return a Host/Origin rejection reason, or None when allowed.

    Mirrors :func:`_ws_host_origin_is_allowed` but yields a short
    machine-parseable token (``host_mismatch …`` / ``origin_mismatch …``)
    on rejection so the close path can log *why* the upgrade was refused.
    r   Nr   rr   zhost_mismatch host=r  r.  rv  >   r   httpszorigin_mismatch origin=)r   rG   rP   ru   rv   r   r  r  r  schemer  )r,  r   r   rv  r  s        rE   _ws_host_origin_reasonr8  s&  s    L$77J t*..,,K[*55 ML[%7CLL
LLLZ^^Hb))F t\""6**F}--- t= EDDD
DDDV]J77 EDDD
DDD4r   c                 $    t          |           du 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.
    N)r8  r,  s    rE   _ws_host_origin_is_allowedr;  &  s     ""%%--r   c                 >    t          |           pt          |           S )zDFirst Host/Origin or peer-IP rejection reason, or None when allowed.)r8  r2  r:  s    rE   _ws_request_reasonr=  &  s    !"%%>):2)>)>>r   c                 >    t          |           ot          |           S )zDReturn True when the WebSocket upgrade matches dashboard boundaries.)r;  r4  r:  s    rE   _ws_request_is_allowedr?  &  s    %b))G.CB.G.GGr   c                      t          t          j        dd          rdS t          t          j        dd          pd                                                                } | r| t
          vrdS dS )uG   Short label for the active WS auth mode — logged on every connection.r   Fgatedr   rr   insecurer  )r   rG   rP   r   r   r0  )r   s    rE   _ws_auth_moderC  &  si    sy/511 w#)\266<"CCEEKKMMJ j77z:r   c                    t          t          t          j        dd                    }|rddlm}m} ddlm}m	}m
} | j                            dd          }|rU	  ||           dS # |$ r?} ||j        d	| | j        r| j        j        nd| j        j        
           Y d}~dS d}~ww xY w| j                            dd          }	|	sdS 	  ||	           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          }
|
sdS t'          j        |
                                t,                                                    rdS dS )u  Validate WS-upgrade auth; return ``(reason, credential)``.

    ``reason`` is None when the credential is accepted, else a short
    machine-parseable token explaining the rejection (``no_credential``,
    ``token_mismatch``, ``ticket_invalid``, ``internal_invalid``).
    ``credential`` names which credential type was presented (``ticket``,
    ``internal``, ``token``, or ``none``) so the accepted path can log *how*
    a peer authed, not just that it did.

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

    Gated (public bind, no ``--insecure``): one of two credentials —

    * ``?ticket=<single-use>`` — a browser-minted, single-use, 30s-TTL ticket
      consumed against the dashboard-auth ticket store. This is what the SPA
      (and native clients) use.
    * ``?internal=<process-credential>`` — the process-lifetime internal
      credential, used only by WS clients the server spawns itself (the
      embedded-TUI PTY child attaching to ``/api/ws`` and ``/api/pub``). It
      is multi-use and never expires so the child can reconnect, and is never
      injected into the SPA — see ``dashboard_auth.ws_tickets`` for the
      threat model.

    The legacy ``?token=`` path is unconditionally rejected in gated mode
    (the SPA bundle isn't carrying the token any longer, and a leaked
    ``_SESSION_TOKEN`` must not grant WS access once the gate is engaged).

    Audit-logs the rejection so operators can debug "WS keeps closing"
    issues from the log.
    r   Fr   )
AuditEvent	audit_log)TicketInvalidconsume_internal_credentialconsume_ticketinternalrr   )NrJ  z
internal: )r	  ipr   N)internal_invalidrJ  ticket)no_credentialr  )NrM  )ticket_invalidrM  r  )Nr  )token_mismatchr  )r  r   rG   rP   hermes_cli.dashboard_auth.auditrE  rF  $hermes_cli.dashboard_auth.ws_ticketsrG  rH  rI  query_paramsrv   WS_TICKET_REJECTEDrl  r   r   r   r6  rx   ry   rz   r{   )r,  r   rE  rF  rG  rH  rI  rJ  r  rM  r  s              rE   _ws_auth_reasonrU  &  s8   @ OUCCDDM ). 	JIIIIIII	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 ?&&z266 	6
6++H555''  6 6 6	1---*,);		    6555556 $$Xr22 	+**
	.N6"""!> 	. 	. 	.I-3xx&(i7BINNRV[	    .-----	. O,,E '&&5<<>>>+@+@+B+BCC }$$s0   A) )B-.4B((B-C D+">D&&D+c                 0    t          |           d         du S )zETrue when the WS-upgrade credential is accepted. See _ws_auth_reason.r   N)rU  r:  s    rE   _ws_auth_okrW  	'  s    2q!T))r   resumesidecar_urlc                    ddl m}m} d}|pd                                }|r'|                                dk    rt          |          } ||dz  d          \  }}t          j                                        }		 dd	l	m
}
  |
|	
           n,# t          $ r t                              dd           Y nw xY w|	                    dd           |	                    dd           |	                    dd           |t          |          |	d<   | rt!          |           \  }}|r|} | |	d<   |r||	d<   |t#                      x}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.

    ``HERMES_TUI_GATEWAY_URL`` is injected so the PTY child can attach to
    this process's in-memory ``tui_gateway`` instance instead of spawning
    its own Python gateway subprocess.

    `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`).

    `profile` (when set) scopes the ENTIRE chat to that profile by pointing
    ``HERMES_HOME`` at the profile dir in the child env. Every spawned
    process (the TUI and the ``tui_gateway.entry`` it launches) resolves
    ``get_hermes_home()`` from that env var at its own import, so the child
    binds the profile's config, skills, memory, and state.db from the start
    — the same propagation ``hermes -p <name>`` performs. The in-process
    ``HERMES_TUI_GATEWAY_URL`` attach is SKIPPED for scoped chats: the
    dashboard's in-memory gateway runs under the dashboard's own profile,
    so a profile-scoped chat must spawn its own gateway subprocess.
    r   )r  _make_tui_argvNrr   rc  zui-tuiF)tui_dev)apply_terminal_config_to_env)rD  z9Failed to apply terminal config bridge for dashboard chatTr{  NODE_ENV
productionHERMES_TUI_DISABLE_MOUSErJ   HERMES_TUI_INLINErq  HERMES_TUI_RESUMEHERMES_TUI_SIDECAR_URLHERMES_TUI_GATEWAY_URL)r:  r  r[  r   r   r	  rU   r(  copyr  r]  r@   r=   rA   r  r6  r  _build_gateway_ws_urlr  )rX  rY  r1  r  r[  r	  rJ  argvr  rD  r]  latest_resume_latest_pathgateway_ws_urls                 rE   _resolve_chat_argvrk  '  s   D =<<<<<<<"&KB%%''I 6Y__&&)33*955|h6FFFID#
*//

C_BBBBBB$$----- _ _ _

NY]
^^^^^_NN:|,,, NN-s333NN&,,, --M *&@&H&H#| 	#"F#)  4(3$% 2444> 	;,:C()::30s3xxxD#55s   <B &B87B8c                     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          r0d	d
lm} t          j                            d |            i          }n&t          j                            dt          i          }d| d| S )a	  ws:// URL the PTY child should attach to for JSON-RPC gateway traffic.

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

    Gated mode: the legacy token path is rejected by ``_ws_auth_ok``, so the
    server-spawned PTY child authenticates with the process-lifetime internal
    credential (``?internal=``). It must NOT use a single-use browser ticket:
    the child reads this URL once at startup and reuses it on every reconnect,
    and a 30s-TTL ticket can expire before a slow cold boot even dials.
    r   N
bound_portr   r   ]:r   Fr   internal_ws_credentialrJ  r  ws://z/api/ws?
r   rG   rP   r   rR  rp  r  r  r#  r{   )r   portr  rp  qss        rE   rf  rf  e'  s    39lD11D39lD11D t t $;;ts33; 	DDt  sy/511 ?OOOOOO\##Z1G1G1I1I$JKK\##Wn$=>>'6''2'''r   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          r1d	d
lm} t          j                             |            | d          }n't          j                            t          | d          }d| d| S )a  ws:// URL the PTY child should publish events to, or None when unbound.

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

    Gated mode: authenticates with the process-lifetime internal credential
    (``?internal=``), the same one ``_build_gateway_ws_url`` uses. The PTY
    child is a server-spawned process we trust; the credential is multi-use
    and never expires, so the child can reconnect ``/api/pub`` without a new
    URL. (This previously minted a single-use 30s ticket, which meant the
    child could not reconnect and could miss the window on a slow cold boot.)
    Connections authenticated this way are recorded under the
    ``server-internal`` identity in the audit log.
    r   Nrm  r   r   rn  r   Fr   ro  )rJ  ru  )r  ru  rq  z	/api/pub?rr  )ru  r   rs  r  rp  rt  s         rE   _build_sidecar_urlrw  '  s    39lD11D39lD11D t t#&$;;ts7K7K;TXQaQa[_QaQaFsy/511 	S 	POOOOO\##//11gFF
 
 \##n$Q$QRR(6((B(((r   c                 h  K   t          |           \  }}|4 d{V  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`.Nr7  z*broadcast send failed for subscriber on %sTr{  )ra   r  rv   	send_textr@   r=   rD  )rG   ru  r  rQ   rT   r  r7  s          rE   _broadcast_eventrz  '  sw     !1#!6!6NJ 5 5 5 5 5 5 5 5N&&w33445 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5  _ _	_--(((((((((( 	_ 	_ 	_ LLEwY]L^^^^^	__ _s#   $A
A A)B'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.ru  rr   N)rS  rv   _VALID_CHANNEL_REr+  r,  ru  s     rE   _channel_or_close_coder~  '  s6    o!!)R00G'--g66@77D@r   c                     |                      dd          }t          |          dk    r| S |dd                             dd          dz   S )a  Clamp a WS close reason to the protocol's 123-byte UTF-8 limit.

    RFC 6455 caps the close-frame reason at 123 bytes; uvicorn raises if a
    longer string is passed. Our reasons embed an attacker-controlled origin,
    so truncate defensively rather than crash the close handler.
    r  r)  {   Nx   ignorez...)rz   r  rU  )r  rV  s     rE   _ws_close_reasonr  '  sP     kk'9--G
7||s4C4=22U::r   z/api/ptyc                     K    j         r j         j        nd}t          s:t                              d|                                dd           d {V  d S t                     \  }}t                      }|Mt                              d||||                                dt          d|                      d {V  d S t                     }|Ht                              d	||                                d
t          |                     d {V  d S t                     }|Gt                              d|                                dt          |                     d {V  d S                                   d {V  t                              d|||           t          s9                     d           d {V                       d           d {V  d S  j                            d          pd } j                            d          pd }t#                     }	|	rt%          |	          nd }
	 t'          ||
|          \  }}}n# t(          $ rK}                     d|j         d           d {V                       d           d {V  Y d }~d S d }~wt,          $ rF}                     d| d           d {V                       d           d {V  Y d }~d S d }~ww xY w	 t/          j        |||          n# t2          $ rF}                     d| d           d {V                       d           d {V  Y d }~d S d }~wt4          t6          f$ rF}                     d| d           d {V                       d           d {V  Y d }~d S d }~ww xY wt9          j                    d$ fd}t9          j         |                      }	 	                                   d {V }|                    d          }|dk    rn|                    d          }|A|                    d          }tA          |tB                    r|"                    d          nd }|stF          $                    |          }|r|%                                tM          |          k    r]tO          |(                    d!                    }tO          |(                    d"                    })                    ||#           1*                    |           Gn# tV          $ r Y nw xY w|,                                 	 | d {V  n# t8          j-        t\          f$ r Y nw xY w                                 d S # |,                                 	 | d {V  n# t8          j-        t\          f$ r Y nw xY w                                 w xY w)%Nr  z+pty refused: embedded chat disabled peer=%si4  zembedded chat disabled)r  r	  z3pty auth rejected reason=%s mode=%s cred=%s peer=%s1  zauth: zpty refused: %s peer=%s3  zpty refused: %si8  z$pty accepted peer=%s mode=%s cred=%su   
[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  rX  r1  )rX  rY  r1  z
[31mChat unavailable: z[0m
)r  rD  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   )r  r  _PTY_READ_CHUNK_TIMEOUTrR   rO  
send_bytesr@   )chunkbridger  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.disconnectry	  r  r  r   r   r0  )colsr  r7   N)/rl  r    _DASHBOARD_EMBEDDED_CHAT_ENABLEDr=   r>   r   rU  rC  rD  r  r8  r2  accept_PTY_BRIDGE_AVAILABLEry  rS  rv   r~  rw  rk  r%   r   
SystemExitr*  spawnr'  r  r  rR   r  create_taskreceiver  r6  rz   
_RESIZE_REr+  endr  r  r*  resizer  r(   cancelCancelledErrorr@   )r,  peerauth_reasoncredmodehost_origin_reasonclient_reasonrX  r1  ru  rY  rg  r  rD  r  r  reader_taskmsgmsg_typer  r  r+  r  r  r  r  s   `                       @@rE   pty_wsr  '  s|     Y/29>>CD+ 		?FFFhhD)AhBBBBBBBBB (++K??DAtT	
 	
 	
 hhD)9:P;:P:P)Q)QhRRRRRRRRR/33%.0BDIIIhhD)9:L)M)MhNNNNNNNNN%b))M &666hhD)9-)H)HhIIIIIIIII
))++II4dD$GGG ! llG
 
 	
 	
 	
 	
 	
 	
 	
 hhDh!!!!!!!!! _  **2dFo!!),,4G$R((G18B$W---dK+{G
 
 
c33    llSCJSSSTTTTTTTTThhDh!!!!!!!!!   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   I% %
L/A J55L;LLL$ $
O	.;M//O	;OO	EU V- 
UV- UV- 2U; ;VV-W=WW=W%"W=$W%%W=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 )Nr  r  r  r   )	handle_ws)r  r   rW  r?  tui_gateway.wsr  )r,  r  s     rE   
gateway_wsr  c(  s      + hhDh!!!!!!!!!r?? hhDh!!!!!!!!!!"%% hhDh!!!!!!!!!((((((
)B--r   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          | j        ||                                  d {V            d {V  5# t          $ r Y d S w xY wNr  r  r  i0  )
r  r   rW  r?  r~  r  rz  rG   receive_textr(   r}  s     rE   pub_wsr  (  s     + hhDh!!!!!!!!!r?? hhDh!!!!!!!!!!"%% hhDh!!!!!!!!!$R((G hhDh!!!!!!!!!
))++	M"267"//:K:K4K4K4K4K4K4KLLLLLLLLL	M   s   6D 
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          | j                  \  }}|4 d {V  |                    |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	 |4 d {V  |                    |          }|-|                    |            |s|                    |d            d d d           d {V  d S # 1 d {V swxY w Y   d S # |4 d {V  |                    |          }|-|                    |            |s|                    |d            d d d           d {V  w # 1 d {V swxY w Y   w xY wr  )r  r   rW  r?  r~  r  ra   rG   r  r[   r  r  r(   rv   r   r  )r,  ru  rQ   rT   r  s        rE   	events_wsr  (  si     + hhDh!!!!!!!!!r?? hhDh!!!!!!!!!!"%% hhDh!!!!!!!!!$R((G hhDh!!!!!!!!!
))++!1"&!9!9NJ : : : : : : : :!!'3551155b999: : : : : : : : : : : : : : : : : : : : : : : : : : :6	$ //#########		$
     	6 	6 	6 	6 	6 	6 	6 	6!%%g..DR    6"&&w5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 	6 	6 	6: 	6 	6 	6 	6 	6 	6 	6 	6!%%g..DR    6"&&w5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 	6sn   *6D22
D<?D<E   
E-*G$ ,E--G$ 9AG
GG$	I-AI2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   r   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  r   )r,   )r  s    rE   no_frontendzmount_spa.<locals>.no_frontend(  s)      LM   r   Nz
index.htmlrr   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  falser   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-revalidater?  )	r  r  r  r   rG   rP   r{   r)  r+   )r  r  chat_jsrA  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
 
 
 	
r   z/assets/{filename}.cssfilenamerp   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assetsr  r  r
  r  r  x-forwarded-prefix)r  z/fonts-terminal/r  r  zurl(zurl("zurl('text/css)r   
media_type)WEB_DISTrO  r  is_relative_tor,   r  ru   rv   r  r)  r-   )r  rp   css_pathr  r  	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
;;;;r   z/assetsr  )r$  r  c                   K   t          |j                            d                    }| dk    s|                     d          rt	          dd|  id          S t
          | z  }| ru|                                                    t
                                                    r7|                                r#|	                                rt          |          S  |          S )Nr  apizapi/r   zNo such API endpoint: /r  r  )r  ru   rv   r   r,   r  r  r  r"  rO  r*   )r  rp   r  rH  r  s       rE   	serve_spazmount_spa.<locals>.serve_spa7)  s      "7?#6#67K#L#LMM !5!5f!=!=@Y@@A    y(	 	+!!##2283C3C3E3EFF	+   ""	+ !!##		+  	***|F###r   rr   )r  r"  rv   r6  r&   mountr.   )r  r  r  r  r  r  s       @@rE   	mount_spar  (  s?    ?? 	,	-	-	 	 	 	 
.	-	
 	\)K-
 -
S -
 -
 -
 -
 -
 -
l __-..<# < < < < /.< ix(7J!K!K!KRZ[[[__())$3 $ $ $ $ $ $ *)$ $ $r   zHermes Tealu/   Classic dark teal — the canonical Hermes look)rO   r  r   zdefault-largezHermes Teal (Large)z1Hermes Teal with bigger fonts and roomier spacingz	nous-bluez	Nous Blueu6   Light mode — vivid Nous-blue accents on cream canvasr   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 eyesr\  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  r6  r  rv   r  r@  r  r  r  )r  r  r  hex_val	alpha_valalpha_fs         rE   _parse_theme_layerr  c)  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.55r  )fontSansfontMonobaseSize
lineHeightletterSpacing_THEME_DEFAULT_TYPOGRAPHYz0.5remcomfortable)radiusdensity_THEME_DEFAULT_LAYOUT>   cardringinputmutedaccentborderpopoverprimaryr(  rD  	secondarydestructivecardForegroundmutedForegroundaccentForegroundpopoverForegroundprimaryForegroundsecondaryForegrounddestructiveForeground>   bgherologocrestr  sidebar>	   tabr  pager
  footerr  r  backdropprogress>   tiledcockpitstandardi   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.
    NrO   palettecolorsr\  r+  r  r  r7   c                                          |                      |                     }t          |||          }||n||dS )Nr  )rv   r  )r+  r  r  r  r  
colors_srcpalette_srcs        rE   _layerz+_normalise_theme_definition.<locals>._layer)  sJ    sJNN3$7$788#D+}EE+vv}1]1]]r   r  z#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  colorOverridesr  r   r  rr   r#  	customCSScomponentStyleslayoutVariantr  r  r   )rO   r  r   r  r  r  r!  r\  )r  r  rv   r6  r   r  r   r
   r@  r  r  r  r%  _THEME_OVERRIDE_KEYS_THEME_NAMED_ASSET_KEYSr)  isalnum_THEME_CUSTOM_CSS_MAX_THEME_COMPONENT_BUCKETSr  _THEME_LAYOUT_VARIANTS)!r  rO   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_variantr7  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)  r9  globr  r
  r  r@   r=  r=  )
themes_dirr7  rz  r  
normaliseds        rE   _discover_user_themesrB  9*  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   rO   r  r   )rO   r  r   
definition)themesr  )r   r   rB  r[   _BUILTIN_DASHBOARD_THEMESr  r=  )r  r  user_themesr  rG  rt  s         rE   get_dashboard_themesrJ  O*  s       ]]FV['9EEEF'))K55DF&  6a 	 	V9fIwZ]+	
 
 	 	 	 	6///r   c                       e Zd ZU eed<   dS )ThemeSetBodyrO   NrV  r7  r   rE   rL  rL  n*  rW  r   rL  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   rD  T)r  rD  )r   rO   r   )r  r  s     rE   set_dashboard_themerN  r*  sU       ]]F&   {#'9F; +++r   rD  >   dm-sans	work-sans
space-monosystem-monosystem-sanssource-serifsystem-serifibm-plex-monoibm-plex-sansjetbrains-monoatkinson-hyperlegibleinterfrauncesspectralz/api/dashboard/fontc                  z   K   t                      } t          | ddt                    }|t          vrt          }d|iS )zEReturn the active font override (``"theme"`` = use the theme's font).r   fontrE  )r   r   _FONT_DEFAULT_ID_FONT_CHOICES)r  r^  s     rE   get_dashboard_fontra  *  sB       ]]F6;8HIIID=  D>r   c                       e Zd ZU eed<   dS )FontSetBodyr^  NrV  r7  r   rE   rc  rc  *  rW  r   rc  c                    K   | j         t          v r| j         nt          }t                      }d|vri |d<   ||d         d<   t	          |           d|dS )a-  Set the dashboard font override (persists to config.yaml).

    Accepts any id in the curated catalog, or ``"theme"`` to clear the
    override and fall back to the active theme's own font. Unknown ids are
    coerced to ``"theme"`` rather than 400'd so a stale client can't wedge
    the picker.
    r   r^  T)r  r^  )r^  r`  r_  r   r   )r  r^  r  s      rE   set_dashboard_fontre  *  si       	]224998HD]]F&   {"&F;%%%r   	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  r6  r   r	   r  r  r  r  relative_tor  )rf  rg  r  rb  r  s        rE   _safe_plugin_api_relpathrj  *  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pluginsr  r
  bundledHERMES_ENABLE_PROJECT_PLUGINSz.hermesprojectr   manifest.jsonr  r  rO   r  r   rx  positionr  )r   rr  r  hiddenTslotsc                 @    g | ]}t          |t                    ||S r7  )r  r6  rf  s     rE   r  z/_discover_dashboard_plugins.<locals>.<listcomp>+  s,    NNN1Z35G5GNANQNNNr   r  )rg  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   rr   iconPuzzlerd   z0.0.0r-  zdist/index.jsr  )rO   r  r   rv  rd   r  rt  r-  r  has_apirZ  _dir	_api_filez$Bad dashboard plugin manifest %s: %s)r[   rb	  rl  r   r#   r=  r	   r  r)  r9  r  r"  r  r  r  rv   rO   r  r  r  r6  r   r  r  rj  r=   rD  r@   )rm  
seen_namesrl  bundled_rootsearch_dirsplugins_rootrZ  r  manifest_filer  rO   raw_tabtab_infooverride_path	slots_srcrt  raw_apirg  safe_apir  s                       rE   _discover_dashboard_pluginsr  *  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 )ry  N)r	   r)  r  s     rE   r  z)_get_dashboard_plugins.<locals>.<genexpr>E+  s;      NN4&	??))+++NNNNNNr   )r  r  rN  r  s    rE   _get_dashboard_pluginsr  @+  sX    '<'#>#@#@  	! ENN5MNNNNN 	E'B'D'D$##r   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_pluginsrE  c                 `    g | ]*}|d          vd |                                 D             +S )rO   c                 D    i | ]\  }}|                     d           ||S r  r  r  s      rE   r  z4get_dashboard_plugins.<locals>.<listcomp>.<dictcomp>S+  s/    ===$!Q1<<+<+<=A===r   r%  )r
  r  rs  s     rE   r  z)get_dashboard_plugins.<locals>.<listcomp>R+  sH       V9F"" 	>=!''))==="""r   )r  r   r   )rm  r  rs  s     @rE   get_dashboard_pluginsr  J+  se       %&&G]]F6;0@"MMMSQSF      r   z/api/dashboard/plugins/rescanc                  J   K   t          d          } dt          |           dS )z#Force re-scan of dashboard plugins.Tr  )r  r2  )r  r  )rm  s    rE   rescan_dashboard_pluginsr  Y+  s,       %$777GW...r   c                   6    e Zd ZU eed<   dZeed<   dZeed<   dS )_AgentPluginInstallBodyr	  Fr  Tr  N)r2  r3  r4  r6  r5  r  r  r  r7  r   rE   r  r  `+  s;         OOOE4FDr   r  r  c                 >    d |                                  D             S )Nc                 D    i | ]\  }}|                     d           ||S r  r  r  s      rE   r  z-_strip_dashboard_manifest.<locals>.<dictcomp>g+  s/    @@@TQall3.?.?@Aq@@@r   r  )r  s    rE   _strip_dashboard_manifestr  f+  s    @@QWWYY@@@@r   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 ]\  }}}}}}|h}|r|                    |           ||
z  rd}n
||z  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# t2          $ r Y nw xY w|                    ||pd|pd||||rt7          |          nd|||o#t          |          dz                                  ||||v d           d |D             ))fd|D             }$g }%	  |            D ]\  }&}'|%                    |&|'d           n# t2          $ r g }%Y nw xY wg }(	  |            D ]\  }&}'|(                    |&|'d           n# t2          $ 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 r  )r6  r  s     rE   r  z'_merged_plugins_hub.<locals>.<dictcomp>x+  s$    >>>!C&	NNA>>>r   r   r  rE  rm  r  rC  inactiveNrq  FT>   r-  r  rr   provides_tools)registryzhermes auth r  )rO   rd   r   rZ  runtime_statushas_dashboard_manifestdashboard_manifestr   
can_removecan_update_gitr   auth_commanduser_hiddenc                     h | ]
}|d          S r  r7  r  s     rE   r  z&_merged_plugins_hub.<locals>.<setcomp>+  s    +++1V9+++r   c                 \    g | ](}t          |d                    vt          |          )S r  )r6  r  )r
  r  agent_namess     rE   r  z'_merged_plugins_hub.<locals>.<listcomp>+  s@       qy>>,, 	"!$$,,,r   r  )memory_providermemory_optionscontext_enginecontext_options)rm  orphan_dashboard_pluginsr  )hermes_cli.plugins_cmdr  r  r  r  r  r  r  r  r  r   r   r   r  r  r	   rv   r"  ri  r  r)  tools.registryr  r  check_fnr@   r=  r  )*r  r  r  r  r  r  r  _read_plugin_manifest_atdashboard_listdash_by_namedisabled_setenabled_setr  r  plugins_root_resolvedr  rO   rd   r   rZ  dir_strr+  r  r  dir_pathdmhas_dash_manifestunder_user_treecan_remove_updater   r  manifest_datar  r  tnamer-  orphan_dashboardmemory_providersr  r
  context_enginesr  s*                                            @rE   _merged_plugins_hubr  j+  s   	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ,--N>>~>>>L$$&&L""$$K ]]F"6;8HRTUUU[Y[N,..:CCEE!#D<Q<Q<S<S < <8g{FGS & 	KK\! 	('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   )D;;
EEAG""
G/.G//(J J'&J'-(K 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.r   N)r   r  r@   r=   rD  r%   )rp   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)r  r  r  r   r  zInstall failed.r   Tr  after_install_path)r   r  r  r	  r   r  r  rv   r%   r  r  )rp   r  r  r7  s       rE   post_agent_plugin_installr  +  s      7??????%%j{  F
 ::d 
::g&&;*;
 
 
 	
 ----
JJ#T***Mr   c                 f    |                      d          } | rd| v sd| v rt          dd          | S )z=Reject path-traversal attempts in plugin name URL parameters.rx  r  rl
  r   zInvalid plugin name.r   )r   r%   r  s    rE   _validate_plugin_namer  ,  sC    ::c??D L44<<44<<4JKKKKKr   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rC  r  r   r  zEnable failed.r   r   r  r  r  rv   r%   rp   rO   r  r7  s       rE   post_agent_plugin_enabler  ,  s      7 &&DIIIIII//dCCCF::d ]FJJw4G4G4[K[\\\\Mr   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  r   r  zDisable failed.r   r  r  s       rE   post_agent_plugin_disabler  ,  s      7 &&DIIIIII//eDDDF::d ^FJJw4G4G4\K\]]]]Mr   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  r   r  zUpdate failed.r   Tr  )r   r  r  r  rv   r%   r  )rp   rO   r  r7  s       rE   post_agent_plugin_updater  #,        7 &&DCCCCCC))$//F::d ]FJJw4G4G4[K[\\\\----Mr   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  r   r  zRemove failed.r   Tr  )r   r  r  r  rv   r%   r  )rp   rO   r  r7  s       rE   delete_agent_pluginr  0,  r  r   c                   D    e Zd ZU dZee         ed<   dZee         ed<   dS )_PluginProvidersPutBodyNr  r  )r2  r3  r4  r  r   r6  r5  r  r7  r   rE   r  r  =,  s:         %)OXc])))$(NHSM(((((r   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)r   r  r  r  r  r  )rp   r  r  r  s       rE   put_plugin_providersr  B,  s       7       
 'd2333&T0111$<r   c                       e Zd ZU eed<   dS )_PluginVisibilityBodyrs  Nrr  r7  r   rE   r  r  R,  rs  r   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  rO   rs  )r   r  r   r  rv   r  r  rs  r=  remover   )rp   rO   r  r  hidden_lists        rE   post_plugin_visibilityr  V,  s      7 &&D]]F&  
6::k3J3JD(Q(Q  {{+//0@AAGRKk4(( { !t;..4    [ !T[004   ,7F;()<<<r   z1/dashboard-plugins/{plugin_name}/{file_path:path}plugin_namerH  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 )rO   Nr7  )r
  r  r  s     rE   r  z%serve_plugin_asset.<locals>.<genexpr>,  s1      BB6k)A)A1)A)A)A)ABBr   Nr  zPlugin not foundr   ry  r  zPath traversal blockedr  r  zapplication/javascriptr  r  r  r  r=  r  z	text/htmlr  r  r  r  r  r  r  r  r  r  r  r  r  z.woff2z
font/woff2z.woffz	font/woffz.ttfzfont/ttfz.otfzfont/otfz.mapr  r  )r  ru   )r  r  r%   r	   r  r  r"  rO  r  r   r*   )	r  rH  rm  pluginr  rL   r  content_typesr  s	   `        rE   serve_plugin_assetr  m,  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   r   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).
    rz  rZ  rp  zPlugin %s: ignoring backend api=%s (project plugins may not auto-import Python code; move the plugin to ~/.hermes/plugins/ if you trust it)rO   ry  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)r  rv   r=   rD  r	   r  ri  r  r  r  r"  	importlibutilspec_from_file_locationloadermodule_from_specr  modulesexec_moduler@   r  r   rG   include_routerr>   )r  api_file_namerg  api_pathresolved_apiresolved_basemodule_namer  r  r  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  r   #  rs  open_browserinitial_profilec           	         ddl }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    pWt/          t0          j                            d                    p+t/          t0          j                            d                    }	|	rPd|  d| |rddlm}
 d |
|           z  fd}t;          j        |d                                           nt                               d           tC          d|  d|            |"                    t          | |dt/          t          j        j                  dd           dS )u  Start the web UI server.

    ``initial_profile`` (when set) is appended to the auto-opened browser
    URL as ``?profile=<name>`` so the SPA's profile switcher preselects it
    — used when a profile alias (``<profile> dashboard``) routes to the
    machine dashboard.
    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:
r  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: %sry  c              3   $   K   | ]}|j         V  d S rE  r  r  s     rE   r  zstart_server.<locals>.<genexpr>O-  s$      77af777777r   uk   Binding to %s with --insecure — the dashboard has no robust authentication. Only use on trusted networks.linuxDISPLAYWAYLAND_DISPLAYzhttp://r   )rb  z
/?profile=c                  ~    	 t          j        d                                           d S # t          $ r Y d S w xY w)Nr\  )r  rO  r  r@   )	_open_url
webbrowsers   rE   _openzstart_server.<locals>._openu-  sO    JsOOOOOI.....    DDs   ). 
<<T)rL   rN   zzSkipping browser-open: no DISPLAY or WAYLAND_DISPLAY detected (headless Linux). Pass --no-open to suppress this detection.u     Hermes Web UI → http://rD  g      4@)r   rs  	log_levelproxy_headersws_ping_intervalws_ping_timeout)#uvicornr   rG   rP   r   r  r   plugins.dashboard_authr
  LAST_SKIP_REASONr=  r@   r  r  r=   r>   r   rD  r   rm  r  r  r  r  rU   r(  rv   urllib.parserb  rW   rY   rZ   rA   printr4  )r   rs  r  r   r  r  r   skip_reasons_nous_plugin_has_displayrb  r  r  r  s               @@rE   start_serverr  -  s]    NNN 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 	  	/$////I C......B%%*@*@BBB	      E$777==????JJO  
 

5
5
5t
5
5666 KK$TY39233       s   
*A5 5
BB)r4   )rG   r$   r  )rr   rr   rE  )r   )r  )F)r  )r  r   r   rT  rU  NN)r  r   r   rT  rb  rs  NN)rr   r  r  )r}  )r   r  NNN)rs  )Nr  rE  r
  )rr   rs  r  N)rj   )NNNr"  )r   r  TFrr   (U  rs  
contextlibr   r   rR   rR  r  dataclassesr   r   r   rx   importlib.utilr  r  r  r  rU   r(  r  r  r  r3  r  r+  rW   r  urllib.errorr  r  urllib.requestpathlibr	   r   r
   r   r   r   r   r  __file__r#  r  r  r6  r   insertr  r   r   r  r   r   r   r   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/   r  tools.lazy_depsr0   _lazy_ensurer@   r  r  r(  r  	getLoggerr2  r=   r  rF   r^   ra   rG   rv   r  r{   rw   r  rh   r  r5  rw  rv  add_middleware&hermes_cli.dashboard_auth.public_pathsro   r   r  r   r   r  r   r   r   
middlewarer   r   r   r   r  r  r  r(  r  
_mcl_entryr.  r%  _k_vr0  r9  r<  r@  rB  rG  rJ  rN  rR  rU  rY  rg  r*  rk  rm  r  r  r  r  rV   r  r  r  r@  rD  r  rM  rQ  rx  r  rt  r  r  r  r  r  r  r  r  r  ry	  r  stat_resultr  r%  r+  r8  r  rB  rW  r\  rc  re  rg  ro  ru  rz  r  r  r  r  r  r  r  r  r  r&  r  r  r  r  r  r  r%  r6  r,  r8  r]  ro  rq  putrv  r}  rn  r  r  r  r  r  r  r  r  r  r  r  r  r{  r  r  r  r  r  r  r$  r2  r4  r9  rE  rO  rS  rr  r  r  r  r  r  r  r  r  r  r  r  r!  r*  r'  rD  rF  rR  rV  r\  ra  rm  rq  rx  r  r  r  r  rN  r  r  r  r  r  r  r	  r  r  r  r+  rM  compiler<  r!  r(  RLockr[  r,  r4  r:  r>  rF  rR  rT  r\  rd  rf  rk  rm  rp  ru  r|  r  r  r  r  r  r  r  r  r  rS   r  r  r  r  r  r.  r  r   r  r!  r  r  r  r"  r  r  r  r%  r1  rX  rj  rm  rk  r  rL  rQ  rM  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  patchr  r  r  r  r  r  r  r9  r&  r  r2  rB  rG  rK  rR  rU  rY  r_  rb  re  rh  rk  rn  rp  r{  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r		  r	  r	  r	  r	  r	  r	  r	  r%	  r*	  r-	  r/	  r1	  r>	  rE	  rH	  rJ	  rM	  rO	  rQ	  rS	  rV	  rh	  rj	  rq	  rs	  rw	  r}	  r	  r	  r  r	  r	  r	  r	  r	  r	  r	  r	  r	  r	  r	  r	  r	  r	  r 
  r
  r
  r
  r
  r

  r"  r$  r	  r8
  r=
  r@
  rL
  rO
  r_
  rc
  rf
  rh
  r{
  r~
  r
  r
  r
  r
  r
  r
  r
  r  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r  r%  r  r   hermes_cli.win_pty_bridger&  r*  r'  r  r  hermes_cli.pty_bridger  r  r|  r0  r2  r4  r8  r;  r=  r?  rC  rU  rW  rk  rf  rw  rz  r~  r  	websocketr  r  r  r  r  r  rH  r  r  r  r#  r$  r'  r(  r&  r=  rB  rJ  rL  rN  r_  r`  ra  rc  re  rj  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r   hermes_cli.dashboard_auth.routesr  _dashboard_auth_routerr  r  r7  r   rE   <module>rA     s	f  	 	 	 ; : : : : : : :    ! ! ! ! ! ! ' ' ' ' ' ' ' '            				 				        



                         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""" "+< " "UY " " " "0    4> > > >" gNK)LLL  @AA^EZWEZ[]E^E^/  $(   #% DK $ $ $     D%%	    (     
Ag A$ A A A A(DG D D D D DH $-9 . . . $ $ y   
Fc F F$ F F F F )!3 )!C )!D )! )! )! )!X $' $ $ $ $L ; ; ; ; ;
 $7 $ $ $ $,g0I g0 V g0 3PPP g0  +z* !g0* 0=== +g04 0 DCC 5g0B 0-   Cg0L )777 Mg0V 3PPP Wg0` =--- ag0j >222   kg0t /x( ug0~ 8*** g0H 2x( Ig0R 4--- Sg0\ 0888 ]g0f <222 gg0r A000$ $ G
 Y'	. 	.{g0 g0 g0 4T#s(^+, g g gV   +# #c3h   2  s s    " % %cN%% 
#tCH~
% % % %P *).99
 56
-/c4S>)* / / /!!## = =FBOB	W}}2<./" " " " "9 " " "
	 	 	 	 	9 	 	 	" " " " "9 " " "
" " " " "9 " " "
    i   # # # # #i # # #         i      $ $ $ $ $	 $ $ $
    	       Y       	    * * S#X    #3 ; ; ; ; ; ;
" " " " "i " " "8@s @3 @5c? @ @ @ @H UW2 22 #2,/2;>2NQ2	2 2 2 2j  bi 455 "#eIBI.F$L$LMMI " " "LLH	*++   ""'uT4$;%67 ' ' ' 'Z 	 	  $ 7 + !T+..  $           * , ' !#!U! E! E	!
 E! 
4! 	! 	#! E! V! V! 
<! V! E! 
8!  E!!" 
:#!$ A! ! ! D

K \ K	
 \ L K   L K K % K O  K!" \#$ \% ,Ds Dt D D D D(1 1 1 1 1 1)5 )T ) ) ) )4 E$*>$?    &T cDj    
 
 
 
 
     DJ    ( `# ` ` ` `@ ;@ D D D$ D4 DD D D D D	3: 	$ 	 	 	 	4 4t 4 4 4 4 4t     =g =$ = = = =
.$ 
. 
. 
. 
. DH Y Y Y7 YD YL^ Y Y Y Y$ 	#+ #+ #+Dj#+#+ 	#+
 tS()#+ #+ #+ #+L#5 $sCx.     2 D T#s(^    2s uUCZ'8    "  g Xc]    6 	W C    @ 
'8 7    0 
,B W    ( LP'8 P7 P P P P0 X X X X X2 	S    . 	 !!
I 
I 
I 
I "!
I 	.C . . . . 	7 7  7
 u u up  
3 
 
 
 
 
 
  	
  
#s(^   6 	M M Mr   (    9    	5< 5 5 5  5 
@ @ @S     % % %^ 
 !!@ @ "!@ /9 9 9 
#$$C C %$C    	    
 !!   ):T)A       "! Z ())F2 2 2 2%,%(% &% (	%
 !% 1% !% !% 7% 1% 5% /% +% +% %  1!%" 5#% 4S>   , .0tC))* / / / .0c4S>)* / / /B B3 B B BT B B B B &T#Y &c &j>N & & & &R*d *s *tCy * * * *Rj&6&< = R R R R"tCH~    , 
 !!  "! 
   6- - -T$sCx.-A - - - -` 	#$$H HT H H H %$HV 
!""F+D F F F #"FR    i   84S> 8c 8 8 8 8 	'((.1 .1 )(.1b 
=o = = = =@ 	%&&! !# !c ! ! ! '&!H JM JMJMJM JM 	JM
 JM JM JM JM JM JMZ 	!""s sss s 	s
 s s s s s s #"sl 	  _E _ES _Ec _E _E _E ! _EDd38n c3h    . F Fhsm F F F F 	    !  	H H H
  !  4    	N' N'HSM N' N' N' N'r$sCx    	%T %Tx} %T %T %T %TP 	)**DB DBC DB DB DB +*DBN 	  /W /W(3- /W /W /W ! /Wd 
9W 9W_ 9Wx} 9W 9W 9W 9Wz UWW WWW&)W14W@CWNQW W W Wx1c3h 1DcN 1 1 1 1h 	M 	Ml 	MXc] 	M 	M 	M 	M      0 M ML M8C= M M M M, ID;Z	2 2 DeCHo-.   5 T#Y    6 
#$$:s\ :sG :s :s :s %$:sz JM M| Mhsm M M M M& 
CG- -
-!(-3;C=- - - -Fx2JGV/ x2 NA

 /
 
x2& ?0:> 'x24 S4V> 5x2B H;
 W Cx2Z BFQ= [x2h [8S ix2v  PH.2 wx2D EV

 Ex2f 
C4?C gx2t D]DH ux2B 9a
 ? Cx2Z #?N4) [x2h DN

 ix2J +;dL= Kx2X (N.

 K
 
Yx2n N&I9 ox2~ #;	  bV
   W_I cx2 x2 x2 T#tCH~-. x x xx$sCx   4B7Q%A B7 O" B7 L( B7 <# B7& -! 'B70 N* 1B78 W& 9B7@ [/ AB7J ?! KB7R ?" SB7\ ? ]B7d ? eB7l +&/ mB7v *% wB7@ $8NSSAB7B /  CB7L &! MB7V /%# # ,"   
 ; 
 9!( ( 4 
 /  2  &<xPP1  3  :&" " 6 
 =! {B7 B7 B7 $sDcN23 B B BJ&U4S>3+>%? & & & &R9S>    , &I ' ' '   : :c3h : : : :$S $U38_ $ $ $ $38n * 38_	    26    $'$J 	#s(^       F c3h$)>    S T#s(^    -# - - - -TS>T(,S#XTAET	#s(^T T T Tn t      $Q  "B["B"B !rz(++  % % % % % % % % HJ tC)C$CD I I I+IO-- s    !C !E ! ! ! !< < < <s sTz    c S S     #'#C C CC
C sCx.4
	C
 *C 
#s(^C C C CT #'#  
 sCx.4
	
 * 
#s(^     
455"*A " " " 65"J 	:;;FS F F F <;FRDcN    8 
ABB==2= = = CB=@ =>>    ?> 	#$$  %$ 	122#M #M<S #M #M #M 32#ML 
788.s . . . 98.z   8C=  3  s        2G0c3h G0 G0 G0 G0T0$sCx. 0 0 0 0B -5  (56  %39  ! 6,  7 2V
 
 #2D,  S+A- yD7 tCH~s23 D D DNB # B T#s(^ B  B  B  B J 	  $ $ ! $> 011'< '<w '< '< '< 21'<v % -/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jms jmtCH~ jm jm jm jmf !& ;$sCx. ; ; ; ;|_TS _TT _T _T _T _TD*E*E&)*E58*EHK*E	*E *E *E *EZ<+S <+T <+ <+ <+ <+~P+ P+ P+ P+ P+ P+fs, s, s, s, s, s,l 
455!D !Dw !D !D !D 65!DH    i   
 
566[ [O [g [ [ [ 76[ 	?@@# 3    A@, 899"23 "2 "2 "2 "2 :9"2VJ3 J J J Jn        
%&&/.@ / / / '&/d 	$%%  &% !""  #"8 	   @6(3- 6 6 6 6 	%&&  x}    '& 	788	C 	 	 	 98	 	.//
 
3 
# 
 
 
 0/
 ())
 
c 
HSM 
 
 
 *)
" " " " "I " " " '((c     )(B 	,--c    .-"! ! ! ! !9 ! ! !
 
      ! 0 # 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]    $# 	'((* *S *8C= *PS * * * )(*Z 
< < < < < < <  	%&&    '& 6 	"##
 
# 
] 
Xc] 
 
 
 $#
 
)**  x}    +* 
*++ #     ,+ 
+,, 3 #    -, %&&
 
# 
 
 
 
 '&
$         Y      
 	  < < ! <B 
,--< <&D <s < < < .-<D	" 	" 	" 	" 	"i 	" 	" 	"c3h DcN    c S#X 4S>      		 	HSM 	 	 	 	 
%4 %4 %4# %4 %4 %4 %4P %&& #     '& 
())$ $ $hsm $ $ $ *)$N" " " " "y " " "
 	*++@DE E
E%E08E E E ,+E* 	7< 7<HSM 7< 7< 7< 7<t" " " " "	 " " " 
$%%8; 8;*; 8;hsm 8; 8; 8; &%8;x   ],D E E E    Y   
    I   
      
 !!    "!* 
  
} 
 
 
 ! 
 
&''* * ('*
! 
! 
! 
! 
!I 
! 
! 
! T#s(^ s tTWY\T\~    $    
 !!  "!( /0} 0 0 0 0f "##	s 	 	 	 $#	    9    	'((DC D/C D D D )(D: 
B B  B 
A A A$         	      s 3 4S>    ( 	 !!$ $ "!$4 
!""L*; L L L #"LB 677L LS L L L 87L6    9   
    )   
   D 	  ,$8 , , , ! ,. 
,[ , , , ,H 
; ; ; 
#$$C C %$C! ! ! ! !I ! ! !
 
	;= 	; 	; 	; 	;	 	 	 	 	I 	 	 	 
;= ; ; ; ;" 	,8 ,8 ,8^        
5RJ 5R 5R 5R 5Rp       
 !J ! ! ! !H 	  > > ! >> 
&''F F ('F&" " " " ") " " "
Bx} Bc B B B B" 
#$$C C"5 C C C C %$C"" " " " "I " " "
 
%&&E E$9 EHSM E E E '&E"" " " " ") " " " 
"##IMB B
&
'B9A#B B B $#B& "".  	 	 	 	 	      > 	"##9R 9R8C= 9R 9R 9R $#9Rx 	!""PT.Q .Q
.Q.Q-0.Q@H.Q .Q .Q #".Qb 	"##9 9 9 9 9 $#9x 	  W WS W W W ! W~    I   <    I       	       )       y          
    )    c C 3    d38n    &3T$sCx.-A 3 3 3 3l	.s 	.t 	. 	. 	. 	.D D D D D D*d *c *# *$ * * * **1D 14@Q;R 1WZ 1 1 1 1h!D !S	 !c ! ! ! !H C C C /g g g g gT 	  2 2 ! 2( 
 !!R,? R R R "!R& 	-..5# 5 5 5 /.5 
.//2,s 2, 2, 2, 0/2,j !""B B= B B B #"B "##+ + + + $#+" 	$%%, , , , &%, 	$%%C /@    &% 	+,,HC H?W H H H -,H, 	%&&>c >9K > > > '&>& 
.//s :M    0/P 'y((  72HSM 72 72 72 72t" " " " ") " " " 	 	hsm 	 	 	 	 	
D 
D[ 
D8C= 
D 
D 
D 
D" " " " ") " " "" " " " " " " "
 
 
 
 	I I# I I I I  I$ -[    $ 	%7      	            F" " " " "I " " "
 	%&&? ?s ?- ?(3- ? ? ? '&?> 	,--; ;3 ;# ; ; ; .-;|" " " " "I " " "
 	.//EIA A
A*A5=c]A A A 0/A<" " " " "y " " "
 	)**3\ 3\ 3\,< 3\xPS} 3\ 3\ 3\ +*3\l" " " " "y " " "
 
122@D.V .V
.V%.V08.V .V .V 32.Vl" " " " "i " " "
 	I I(3- I I I I 		J 	J/ 	JHSM 	J 	J 	J 	J" 	  A AC A A A ! AH 	 !!Y YS Y Y Y "!Y\ <5!! 	\\\\\\\\ $   	 %	 	 	 	 	, 	 	 	 	 			HHHHHHHH $   	 %	 	 	 	 	, 	 	 	 	 		 RZ677
 BJ9::  )KKKLLM+ M(3- M M M M,%*k %*d %* %* %* %*P{ x}    D	.; 	.4 	. 	. 	. 	.?; ?8C= ? ? ? ?
H{ Ht H H H H
s    Q% Q%hsmS.@(A Q% Q% Q% Q%h*K *D * * * * !!%!N6 N6SMN6#N6 c]N6 49hsmXd^34	N6 N6 N6 N6b(x} ( ( ( (B!) !) !) !) !) !)H_ _c _C _D _ _ _ _Ay AXc] A A A A
;3 
;3 
; 
; 
; 
; zGY G4 G G G Gj y t    < zY 4    6 }(6	 (6d (6 (6 (6 (6V	!8C= 	!S 	! 	! 	! 	!x$7 x$ x$ x$ x$D }M~'<  NA  B  B{  NF  G  G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vT#s(^ v v v vr 	%&&]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   ) * * * 		# J J
J
J J 	J
 J J J J J Js   ,E F1?FF1F++F10F1V -WW_A_1 _1A_<_;A_<t4Bu uBu&u%Bu&u*Bu< u<BvvBv