
    JjxD	                    8   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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#Z#ddl$m%Z%m&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l0Z0 e)e1          j2        j2        3                                Z4 e5e4          ej6        vr$ej6        7                    d e5e4                     dd	l8m9Z9m:Z: dd
l;m<Z<m=Z=m>Z>m?Z?m@Z@mAZAmBZBmCZCmDZDmEZEmFZFmGZGmHZHmIZImJZJmKZKmLZLmMZMmNZNmOZO ddlPmQZQmRZRmSZS ddlTmUZUmVZVmWZWmXZXmYZYmZZZ ddl[m\Z\ 	 ddl]m^Z^m_Z_m`Z`maZambZbmcZcmdZdmeZe ddlfmgZg ddlhmiZimjZjmkZkmlZl ddlmmnZn ddlompZp no# eq$ rg 	 ddlrmsZt  etdd           ddl]m^Z^m_Z_m`Z`maZambZbmcZcmdZdmeZe ddlfmgZg ddlhmiZimjZjmkZkmlZl ddlmmnZn ddlompZp n# eu$ r  evdejw         d          w xY wY nw xY wdejx        v r e)ejx        d                   n e)e1          j2        dz  Zy ejz        e{          Z|dddde}ddfd Z~dd!Zdefd"Zedd%            Zdd&Zd#d$dej        fd'Zd#d$dee5e)f         fd(Z e^d)e9e*          Zdd+lmZ e                    e           ejx                            d,          p ej        d-          Zd.Zd/Zg Ze-e         ed0<   d1Zd2Ze                    egd3d4gd4g5           dd6lmZ d7ebdefd8Z ed9h          Zee5         ed:<   d7ebd;e5defd<Zd7ebddfd=Z eh d>          Zeed?<   dd@e5dAedefdBZdCe5dDe5defdEZe                    dF          d7ebfdG            Ze                    dF          d7ebfdH            Ze                    dF          d7ebfdI            Ze                    dF          d7ebfdJ            Ze                    dF          d7ebfdK            Zi dLdMdNdOdPdQdRdSdOdPdTdUdVg dWdXdYdUdZd[d\gdXd]dUd^g d_dXd`dUdag dbdXdcdUdddedfgdXdgdUdhg didXdjdUdkg dldXdmdUdng dodXdpdUdqg drdXdsdUdtdudvgdXdwdUdxg dydXdzdUd{d|d}gdXd~dUdg ddXddUdg ddXddUdg ddXdUdg ddXdUdddgdXddddZe,e5e,e5e+f         f         ed<   i ddddddddddddddddddddddddddOddddddZe,e5e5f         ed<   g dZde+de5fdZ	 dde,e5e+f         de5de,e5e,e5e+f         f         fdZ ee=          ZedQ         Zi Ze,e5e,e5e+f         f         ed<   e                                D ]\  ZZeee<   edLk    reedQ<   eZ G d dep          Z G d dep          Z G d dep          Z G d dep          Z G d dep          Z G d dep          Z G d dep          Z G d dep          Z G d dep          Z G d dep          Z G d dep          Z G dĄ dep          ZddddddddddddddΜZe,e5e5f         ed<   dZde5de5fd҄Z G dӄ dep          Z G dՄ dep          Z G dׄ dep          Z G dل dep          Zde5dLe5dee5e5f         fd܄Z	 dddde5dLe5de5de5defdZ ej        d          Z	  e ej        dd                    Zn3# eef$ r) e|                    d ej        d                     dZY nw xY wdeeedz  f         fdZdddddddddZdZdZdZ e)d          Z ed/           G d d                      Zh dZ eh d          Z eddh          Zde5defdZd;e)defdZdZdZdZސd 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d!d"d#d$d%d&d'd(d)d*d&d+d+d$d,Zi d-d.d/ddǐd0d1dd2dd3ddȐd4d5d6d7d8dɐd9dʐd:dːd;d<d=d>dd?dd̐d@d͐dAdBdiZdCe5de)fdDZd;e)de5fdEZdFedefdGZd;e)dee)ej        f         fdHZdIe)de5dz  fdJZde5fdKZdLe5de5fdMZdee)         fdNZe                    dO          d;e5fdP            ZddQd;e)dRede)fdSZdCe5e)z  de)fdTZdUe)dVe)defdWZdCe5dz  de5fdXZd7ebdefdYZdefdZZdefd[Zd/d\d7ebd]edefd^Zdd_dCe5dz  d7ebd`edeee)e5f         fdaZdbede,e5e+f         fdcZdbe֐dVe)de,e5e+f         fddZdee5deee5f         fdfZe                    dg          dd7ebd;e.e5         fdh            Ze                    di          d7ebd;e5fdj            Ze                    d9          d7ebd;e5fdk            Ze                    dl          dmed7ebfdn            ZdoZe                    dp           e_dq           e`dq           e`d/          fd7ebdrecd;e5dsefdt            Z e                    du          dmed7ebfdv            Ze                    dg          dmed7ebfdw            Ze                    dx          d;e5fdy            Ze                    dz          d;e5fd{            Z G d| d}ep          Ze                    d~          dmefd            Ze                    d          d;e5fd            Ze                    d          d;e5fd            Z	e                    d          d             Z
ddl8mZ d Zd;e5de5fdZ G d dep          Z G d dep          Z G d dep          Z G d dep          Z G d dep          Z G d dep          Ze                    d          d;e5fd            Ze                    d          d;e5fd            Ze                    d          d;e5fd            Ze                    d          dd;e5de5de.e5         fd            Ze                    d          	 dd;e5dre5de5de.e5         def
d            Ze                    d          d;e5dre5fd            Ze                    d          d;e5fd            Ze                    d          dd;e5de.e5         fd            Ze                    d          d;e5fd            Ze                    d          defd            Ze                    d          defd            Ze                    d          defd            Z e                    d          defd            Z!e                    d          defd            Z"e                    d          defd            Z#e                    d          defd            Z$e                    d          defd            Z%e                    d          defd            Z&e                    d          dde.e5         fd            Z'dZ(de5de5de.e}         fdńZ)de5de5de5de5de,e5e5f         f
dȄZ*e                    dɦ          dʄ             Z+e                    d˦          d̄             Z, G d̈́ dep          Z-e.                    dϦ          de-fdЄ            Z/e                    dѦ          d҄             Z0e                    dӦ          dde.e5         fdԄ            Z1 G dՄ dep          Z2 G dׄ dep          Z3e                    d٦          dde5de.e5         fdۄ            Z4e                    d٦          de2fd܄            Z5e.                    d٦          de3fd݄            Z6de5fd߄Z7e                    d          d             Z8e                    d          d             Z9e                    d          d             Z:e                    d          d             Z; G d dep          Z<e                    d          dde<dz  fd            Z= eB            dz  Z>e)ed<   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Z?e,e5e5f         ed<   i Z@e,e5ejA        f         ed<   i ZBe,e5e/e5dqf         f         ed<   i ZCe,e5e,e5e+f         f         ed<   dde5de5de}ddfdZDde5fdZEde-e5         de5dejA        fdZFd;e)de}de-e5         fdZGde.e5         de5de-e5         fdZHde.e5         de5de5fdZI ejJ        d           ZKd!e5d"e5de5ddfd#ZLdde.e5         de/ejA        ef         fd$ZMdde.e5         dee5e+f         fd%ZNe                    d&          dde.e5         fd'            ZOe                    d(          d7ebfd)            ZPe                    d*          d+             ZQdde}de-e,e5e+f                  fd-ZRe                    d.          dd/efd0            ZSe                    d1          dmefd2            ZT G d3 d4ep          ZUd5e,e5e+f         de5fd6ZVdaWe.e5         ed7<   d8e.e5         defd9ZXe                    d:          d;             ZYe                    d<          dmeUfd=            ZZe                    d>          dde5d@e}fdA            Z[e                    dB          	 	 	 	 	 	 	 	 	 ddEe}dFe}dGe}dHe5dIe5dJe5dKe5dLe5de.e5         fdM            Z\e                    dN          	 	 	 	 	 	 	 	 ddEe}dFe}dGe}dHe5dIe5de5dJe5dKe5fdQ            Z]e                    dR          ddSe5dEe}de.e5         fdT            Z^de,e5e+f         de,e5e+f         fdUZ_deQde)fdVZ`deQde,e5e+f         fdWZadXeRdFe,e5e+f         de5fdYZbdXeRdFe,e5e+f         defdZZcdeQde,e5e+f         fd[ZddXeRd\e5de5fd]Zee                    d^          de5fd_            Zfe.                    d^          de5defd`            Zge                    da          dde.e5         fdb            Zhe                    dc          dd             Zie                    de          df             Zjdddddi dgZkeedh<   e                    di          dde.e5         fdj            ZldkZme/e5dqf         edl<   e                    dm          d de.e5         dnefdo            Zne                    dp          dde5fdq            Zoe                    dr          dde.e5         fds            Zpe                    dt          dde.e5         fdu            Zqe.                    dt          ddeƐde.e5         fdv            Zre                    dw          ddeÐde.e5         fdx            Zs	 dde5de5dLe5dye5de5de5fdzZtd{e5d|e5dee5e5f         fd}Zude,e5e+f         de,e5e+f         fd~Zve.                    da          ddede.e5         fd            ZwdefdZxe                    d          dde.e5         fd            Zye.                    d          ddede.e5         fd            ZzdddddZ{ee5ee5e5f         f         ed<   ddde-e5         fdZ|e                    d          ded7ebfd            Z}e                    d          ddede.e5         fd            Z~e                    d          	 dded7ebde.e5         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ʐ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didddddddddddddd ddddZee5ee5e+f         f         ed<   dZee5dqf         ed<   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	d0d1d1d/d!d2d3d4dd5d6d7d/d!d8d9d:dd;d<d/d!d=d>dd?d@ddAdBd/d!dCdDddEdFd/d!dGdHddIdJddKdLd/d!dMdNd/d!dOdPd/d!dQdRddSdTd/d!dUZee5ee5e+f         f         edV<   deee5e+f         dqf         fdWZdee5         fdXZ eh dY          Zd!e5dee5dqf         fdZZd!e5dee5dqf         fd[Zd!e5d\ee5e+f         d]e+dz  dee5dqf         fd^Z	 dd!e5d]e+dz  dee5e+f         fd_Zd!e5dee5e+f         dz  fd`Zd"e5dee5e+f         fdaZd!e5fdbZ	 ddcee5e+f         ddee5e5f         deedz  dfedee5e+f         f
dgZd!e5dheddfdiZdjZdke9 Z ejJ        dl          Ze G dm dn                      Zi Zee5ef         edo<    ej                    Zde5fdpZde5defdqZddrZde+de5dz  fdsZdte5due5de5fdvZdddwdxe5d;e5dee5e+f         dz  dye5dz  dee5e+f         f
dzZdddwdxe5d;e5dee5e+f         dz  dye5dz  dee5e+f         f
d{Ze                    d|          defd}            Ze                    d~          de5fd            Zdde.e5         dee5e+f         fdZe                    d          	 dde5dede.e5         fd            Ze                    d~          de5fd            Ze                    d          dde.e5         fd            Ze.                    d          	 dd!e5dede.e5         fd            Ze                    d          dd!e5de.e5         fd            Zdde.e5         de}de5fdZde,e5e+f         fdZde,e5e+f         fdZde,e5e+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dddddddddddddddeddddddeddddddedfZee,e5e+f         dqf         ed<   de5de,e5e+f         fdZde,e5e+f         de.e5         fdZde,e5e+f         de,e5e+f         de.e5         fdZdee,e5e+f                  fdZe                    d          dde.e5         fd            Ze                    d          	 dde5d7ebde.e5         fd            ZdZi Ze,e5e,e5e+f         f         ed<    ej                    Z	 ddlmZmZmZmZmZmZ d/Zn# eq$ r dZY nw xY wdÐZddĄZde.e5         de.e5         fdńZÐde.e5         ddfdƄZ	 dde5de5de.e5         dee5e,e5e+f         f         fdȄZ	 dde5due.e5         de.e5         fdʄZƐde5de5de}ddfd΄Zǐdde.e5         de,e5e+f         fdτZ	 dde5de5de.e5         de,e5e+f         fdфZ	 dde5de.e5         de,e5e+f         fd҄Zʐde5ddfdӄZːde5ddfdԄZ̐de5ddfdՄZ͐de5ddfdքZe                    dצ          	 dde5d7ebde.e5         fd؄            Z G dل dep          Ze                    dۦ          	 dde5ded7ebde.e5         fd܄            Ze                    dݦ          	 dde5de5de.e5         fdބ            Ze                    dߦ          	 dde5d7ebde.e5         fd            ZӐde5fdZ G d dep          Ze                    d          defd            Ze                    d          dde.e5         fd            Ze                    d          dde.e5         fd            Ze                    d          dde.e5         fd            Zِde.e5         fdZe                    d          dde5de.e5         fd            Ze                    d          de5fd            Ze                    d          dde5de.e5         fd            Ze                    d          dde5de.e5         fd            Z G d dep          Ze                    d          de5defd            Ze                    d          dde5de.e5         fd            Z G d dep          Ze                    d          defd            Ze                    d          	 	 	 	 	 ddre5d@e}de.e5         d e.e5         de.e5         f
d            Z G d dep          Z G d dep          Zddde+dede.e5         fd	Zde+de.e-e5                  fd
Zde+de)de.e5         fdZde,e5e+f         ddfdZde,e5e+f         de)de,e5e+f         fdZde.e-e5                  de5ddfdZ ej                    Zde-e,e5e+f                  fdZde.e5         de/e5e)f         fdZde,e5e+f         de5de)de,e5e+f         fdZde.e5         de5fdZde5de.e5         fdZe                    d          dde5fd            Ze                    d          dde5de.e5         fd            Ze                    d           dde5de.e5         dEe}fd!            Ze                    d          ddede5fd"            Ze                    d#          d$             Ze.                    d          dde5dede.e5         fd%            Ze                    d&          dde5de.e5         fd'            Ze                    d(          dde5de.e5         fd)            Ze                    d*          dde5de.e5         fd+            Ze                    d          dde5de.e5         fd,            Zde5de5defd-Ze                    d.          d7ebfd/            Z G d0 d1ep          Z e                    d2          d3             Ze                    d4          dde de5fd5            Z G d6 d7ep          Z G d8 d9ep          Zd:e,e5e+f         de,e5e5f         fd;Zde5d<e,e5e+f         de,e5e+f         fd=Ze                    d>          dde.e5         fd?            Ze                    d>          ddede.e5         fd@            Ze.                    d>          ddede.e5         fdA            Z	e                    dB          dde5de.e5         fdC            Z
e                    dD          dde5de.e5         fdE            Ze                    dF          dde5de.e5         fdG            Z G dH dIep          Ze.                    dJ          	 dde5dede.e5         fdK            Ze                    dL          dde.e5         fdM            Z G dN dOep          Ze                    dP          ddede.e5         fdQ            Zde5de5fdRZe?                    dSdT            G dU dVep          Z G dW dXep          ZdY Ze                    dZ          d[             Ze                    d\          defd]            Ze                    d^          defd_            Ze                    d`          da             Z G db dcep          Zde5dde,e5e+f         de5de,e5e+f         fdeZe                    df          dg             Ze                    dh          di             Ze                    df          defdj            Ze                    dk          de5fdl            Z  G dm dnep          Z!e.                    do          de5de!fdp            Z"e                    dq          dde.e5         fdr            Z#e                    ds          dde.e5         fdt            Z$ G du dvep          Z%dce+dwe}de,e5e+f         fdxZ&e                    dy          dz             Z'e                    dy          de%fd{            Z(e                    d|          de5dwe}fd}            Z) G d~ dep          Z* G d dep          Z+e                    d          d             Z,e.                    d          de*fd            Z-e                    d          de+fd            Z.e                    d          d             Z/e                    d          d             Z0 G d dep          Z1de)fdZ2de)fdZ3e                    d          de1fd            Z4e                    d          de5fd            Z5 G d dep          Z6e                    d          de6fd            Z7de5dz  de5fdZ8e                    d           e_dq           e`d          fdrecd/efd            Z9e                    d          d             Z: G d dep          Z;e                    d          de;fd            Z< G d dep          Z=e                    d          de=fd            Z>e                    d          d             Z?e                    d          d             Z@ G d dep          ZAde.e5         de-e5         fdZBde5d"e5de5fdZCe                    d          ddeAde.e5         fd            ZD G d dep          ZEe                    d          ddeEde.e5         fd            ZF G d dep          ZGe                    d          	 dde.eG         de.e5         fd            ZHddddddddddd
ZIdefdZJdde.e5         defdÄZKe                    dĦ          dde.e5         fdń            ZLe                    dƦ          	 ddSe5dJe5dEe}de.e5         fdǄ            ZMe                    dȦ          dde5de.e5         fdʄ            ZNe                    d˦          dde5de.e5         fd̄            ZO G d̈́ dep          ZP G dτ dep          ZQ G dф dep          ZR G dӄ dep          ZS G dՄ dep          ZT G dׄ dep          ZU G dل dep          ZVdde5d|e+de+fdۄZWde,e5e+f         fd܄ZXde-e,e5e+f                  fd݄ZYde5de)fdބZZde5de5fd߄Z[de)de5dLe5ddfdZ\de)de-d7         de}fdZ]de)de-e5         de}fdZ^e                    d          d             Z_e                    d          dePfd            Z`e                    d          d             Zae                    d          deSfd            Zbe                    d          de5fd            Zce                    d          de5fd            Zde                    d          de5deQfd            Zee                    d          de5fd            Zfe                    d          de5fd            Zge.                    d          de5deRfd            Zhe.                    d          de5deTfd            Zie.                    d          de5deUfd            Zje                    d          de5deVfd            Zk ej                    Zlede.e5         fd            Zmede.e5         fd            Zn G d dep          Zoe                    d           dde.e5         fd            Zpe.                    d          ddeode.e5         fd            Zq G d dep          Zr G d dep          ZsddZte                    d	          dde5de.e5         fd
            Zue                    d           derfd            Zve.                    d	          desfd            Zwe                    d          dde.e5         fd            Zx G d dep          Zye.                    d          dde5deyde.e5         fd            Zze                    d          dde5de.e5         fd            Z{ G d dep          Z|dddZ}de5dede.e5         fdZ~de5de5fdZde5dede.e5         de.e         fdZe                    d           	 dde5de.e5         de.e5         fd!            Z G d" d#ep          Ze.                    d$          	 dde5dede.e5         fd%            Ze.                    d&          	 dde5de|de.e5         fd'            Z G d( d)ep          Ze.                    d*          dde5dede.e5         fd+            Z G d, d-ep          Ze                    d.          	 dde5dede.e5         fd/            Ze                    d0          dde.e5         fd1            Ze                    d2          dde.e5         fd3            Z G d4 d5ep          Ze                    d6          dde.e5         fd7            Ze.                    d6          ddede.e5         fd8            Ze                    d9          d	d:e}de.e5         fd;            Ze                    d<          d	d:e}de.e5         fd=            Zej                            d>          r6	 dd?lmZmZ d/ZnY# eq$ r dZdZ G d@ dAe          ZY n:w xY w	 ddBlmZmZ d/Zn## eq$ r dZdZ G dC dAe          ZY nw xY w ejJ        dD          ZdEZ ejJ        dF          Z eh dG          ZdHdIde.e5         fdJZdHdIdefdKZdHdIde.e5         fdLZdHdIdefdMZdHdIde.e5         fdNZdHdIdefdOZde5fdPZdHdIdee.e5         e5f         fdQZdHdIdefdRZ	 	 	 	 d
dSe.e5         dTe.e5         de.e5         dUe.e5         deee5         e.e5         e.e         f         f
dVZde.e5         fdWZ	 	 	 	 d
dSe.e5         dTe.e5         de.e5         dUe.e5         deee5         e.e5         e.e         f         f
dXZdYe5de.e5         fdZZd#e+dYe5dme5ddfd[ZdHedde.e5         fd\Zd#d$dYe5de)fd]Zd;e)de.e5         fd^Zd;e)ddfd_Zd*e5de5fd`ZdaZdbZdcZddZdae.e	j        j                 ede<    ej                    Zde	j        j        fdfZde5fdgZdHedde.e5         fdhZdie+dje5dkede.e5         de+f
dlZdHeddmej        dme,e5e+f         ddfdnZdHeddmej        doe+dpe}ddf
dqZdre+dee.ee5e+f                  e.e5         f         fdsZe                    dt          dHedddfdu            Ze                    dv          dHedddfdw            Ze                    dx          dHedddfdy            Ze                    dz          dHedddfd{            Ze                    d|          dHedddfd}            ZĐd\e.e5         de5fd~ZŐde^fdZd|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+de5dede.e,e5e+f                  fdZȐddddddZe,e5e5f         ed<   dddZe,e5e5f         ed<   h dZh dZh dZh dZΐdZϐdFe,e5e+f         de.e,e5e+f                  fdZdefdZe                    d          d             Z G d dep          Ze.                    d          defd            ZԐdZ eh d          Ze                    d          d             Z G d dep          Ze.                    d          defd            Zِde+de)de.e5         fdZdefdZdae.e         ed<   ddedefdZe                    dæ          dĄ             Ze                    dŦ          dƄ             Z G dǄ dep          Zde,e5e+f         de,e5e+f         fdʄZde,e5e+f         fd˄Ze                    d̦          d7ebfd̈́            Ze                    dΦ          d7ebdefdτ            Zde5de5fdЄZe                    dѦ          d7ebde5fd҄            Ze                    dӦ          d7ebde5fdԄ            Ze                    dզ          d7ebde5fdք            Ze                    dצ          d7ebde5fd؄            Z G dل dep          Ze.                    dۦ          d7ebdefd܄            Z G d݄ dep          Ze                    dߦ          d7ebde5defd            Ze                    d          de5de5fd            Zd Z e             dd+lmZ e                    e            ee           dddue}de}fdZde}ddfdZd@e5de}dede5ddf
dZ	 	 	 	 	 dd@e5de}dedAede5f
dZd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)windows_detach_flagswindows_hide_flags)Path)AnyDictListOptionalTuple)__version____release_date__)cfg_getDEFAULT_CONFIGOPTIONAL_ENV_VARS clear_model_endpoint_credentialsget_config_pathget_env_pathget_hermes_homeload_configload_envread_raw_config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write_platform_config_field_deep_merge)MemoryProviderProviderFieldget_memory_provider)derive_gateway_busyderive_gateway_drainableget_running_pidget_runtime_status_running_pidparse_active_agentsread_runtime_status)env_var_enabled)FastAPIFileFormHTTPExceptionRequest
UploadFile	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                     ddl m}  |            }t                              d|j        |           |                    | |           dS )u  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 the resolved cron
    scheduler provider here (no live adapters; delivery falls back to the
    per-platform send path).

    Cross-process safe: the built-in provider's ``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   resolve_cron_schedulerz:Desktop cron scheduler started (provider=%s, interval=%ds)rF   N)cron.scheduler_providerrJ   _loginfonamestart)rE   rF   rJ   providers       ;/home/rurouni/.hermes/hermes-agent/hermes_cli/web_server.py_start_desktop_cron_tickerrS      sZ     ?>>>>>%%''HIIJHM[cdddNN:N11111    c                  2    	 dd l } d S # t          $ r Y d S w xY w)Nr   )hermes_cli.gateway	Exception)
hermes_clis    rR   _warm_gateway_modulerY      s;    !!!!!!   s    
c                  T    	 ddl m}   |             S # t          $ r ddlm} |cY S w xY w)Nr   )_get_restart_drain_timeout)%DEFAULT_GATEWAY_RESTART_DRAIN_TIMEOUT)rV   r[   ImportErrorgateway.restartr\   )r[   r\   s     rR   _resolve_restart_drain_timeoutr_      sa    5AAAAAA))+++ 5 5 5IIIIII44445s    ''appr1   c                <  K   i | j         _        t          j                    | j         _        i | j         _        t          j                    | j         _        t          j                                        d t                     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daemonrO   )stateevent_channelsasyncioLock
event_lockpty_active_session_fileschat_argv_lockget_event_looprun_in_executorrY   osgetenv	threadingEventThreadrS   rP   set)r`   	cron_stopcron_threads      rR   	_lifespanry      s     !CI"<>>CI)+CI&
  '|~~CI ,,T3GHHH
 +/I-1K	y!""c))O%%	&-&	
 
 
 	 MMOOOOO ! 9 MMOOOO !s   #D D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.
    )rh   ri   rl   AttributeErrorrj   rk   r`   s    rR   _get_event_stater}      sl    >y')=== > > >#%	 &|~~	y')=====>s    AA('A(c                     	 | j         j        S # t          $ r, t          j                    | j         _        | j         j        cY S w xY w)zReturn the chat-argv resolution lock from app.state.

    Mirrors :func:`_get_event_state`: prefers the lifespan-initialised Lock
    (created on the correct event loop) but lazily initialises it for
    non-``with`` TestClient usages.
    )rh   rn   r{   rj   rk   r|   s    rR   _get_chat_argv_lockr      sN    (y'' ( ( (#*<>>	 y''''(s    3AAc                 l    	 | j         j        S # t          $ r i | j         _        | j         j        cY S w xY w)z?Return channel -> active-session-file state for dashboard PTYs.)rh   rm   r{   r|   s    rR   _get_pty_active_session_filesr      sH    2y11 2 2 2-/	*y11112s    "33Hermes Agent)titleversionlifespan)router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)r   session_headerauthexpecteds       rR   _has_valid_session_tokenr   :  s     _(()=rBBN $-   t?33D)))Ht{{}}hoo.?.?@@@rT   z/api/files/download_QUERY_TOKEN_API_PATHSpathc                     |t           vrdS | j                            dd          }t          |          o=t	          j        |                                t                                                    S )NFtokenr   )r   query_paramsr   boolr   r   r   r   )r   r   r   s      rR   _has_valid_query_tokenr   T  s]    )))u $$Wb11E;;W4.u||~~~?T?T?V?VWWWrT   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)getattrr`   rh   r4   r   )r   s    rR   _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rT   >   	localhost::1	127.0.0.1_LOOPBACK_HOST_VALUEShostallow_publicc                     | t           vS )u  Return True iff the dashboard auth gate must be active.

    Truth table:
      host == loopback        → False (no auth — local-only, trusted operator)
      host != loopback        → True  (gate engages — OAuth or password required)

    "Loopback" is 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.

    ``allow_public`` (the legacy ``--insecure`` escape hatch) NO LONGER disables
    the gate. It is accepted for backward-compat with old launch scripts and
    desktop shells but is ignored: a non-loopback bind ALWAYS requires an auth
    provider (OAuth or the bundled password provider). This closes the
    unauthenticated-public-dashboard hole behind the June 2026 ``hermes-0day``
    MCP-persistence campaign, where ``--insecure --host 0.0.0.0`` left the
    config/MCP/agent surface open to internet scanners.
    )r   )r   r   s     rR   should_require_authr     s    & ,,,rT   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         rR   _is_accepted_hostr     s      u 	A||C ;sB;;!E'
IIII+.!88AHHS!$$Q''	!!I
 &&&t !!H(((111   rT   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   r     r   zVInvalid Host header. Dashboard requests must use the hostname the server was bound to.r   content)r   r`   rh   r   r   r   r<   )r   	call_nextr   r   s       rR   host_header_middlewarer     s       L$77J o))&"55 j99 		@    7#########rT   c                   K   | j         j        }|                    d          rPt          | j        dd          p9t          | j        j        dd          pt          |           pt          | |          }|r|                    d          }t          |          dk    r|d         r	 dd	l
m}m}  |            } |            }n,# t          $ r t                      }t                      }Y nw xY wt                      }	t!          fd
|	D             d          }
|
r|
                    d          nd}|dk    r|v s|vrt%          dddi          S n|dk    r|v rt%          dddi          S  ||            d{V S )u"  Block requests to disabled plugin API routes at request time.

    :func:`_mount_plugin_api_routes` gates at import time, but if a plugin
    is disabled *after* the dashboard is already running, its FastAPI router
    remains mounted until restart.  This middleware enforces the enabled/
    disabled policy on every request to ``/api/plugins/{name}/...`` so that
    runtime config changes take effect immediately.

    Registered BEFORE the auth middlewares (so it executes AFTER them): a
    request that hasn't cleared auth must get auth's 401 first, never this
    gate's 404 — otherwise an unauthenticated caller could fingerprint which
    plugins are installed/enabled by reading the status code. We only reach
    the enabled/disabled check for a request that auth already let through.
    /api/plugins/token_authenticatedFr   /      r   _get_enabled_set_get_disabled_setc              3   N   K   | ]}|                     d           k    |V   dS rO   Nr   .0pplugin_names     rR   	<genexpr>z+_plugin_api_runtime_gate.<locals>.<genexpr>  s7      LLqquuV}}/K/K/K/K/K/KLLrT   Nsourceuser  r   Plugin not foundr   bundled)urlr   r   r   rh   r`   r   r   splitlenhermes_cli.plugins_cmdr   r   rW   rv   _get_dashboard_pluginsnextr   r<   )r   r   r   _authedpartsr   r   enabled_setdisabled_setpluginspluginr   r   s               @rR   _plugin_api_runtime_gater     s/       ;D'' .
 GM#8%@@ 5w{(/5AA5'005 &gt44	 	  $	JJsOOE5zzQ#Ah 	-        '7&6&8&8'8'8':':$ - - -&)ee'*uu-
 566G!LLLLGLLL F 6<GVZZ111F''&,66+[:X:X#/,/)13E(F$ $ $  ;Y
  9,,&,66#/,/)13E(F$ $ $  7#########s   +C &C10C1c                 6   K   ddl m}  || |           d {V S )Nr   )gated_auth_middleware)$hermes_cli.dashboard_auth.middlewarer   )r   r   r   s      rR   _dashboard_auth_gater   0  s=      JJJJJJ&&w	:::::::::rT   c                   K   t          | j        dd          r ||            d{V S t          | j        j        dd          r ||            d{V S | j        j        }|                    d          r;|t          vr2t          |           s#t          | |          st          dddi	          S  ||            d{V S )
zERequire the session token on all /api/ routes except the public list.r   FNr   z/api/r   r   r   r   )
r   rh   r`   r   r   r   _PUBLIC_API_PATHSr   r   r<   )r   r   r   s      rR   auth_middlewarer   6  s      w}3U;; (Yw''''''''' w{ /599 (Yw''''''''';Dw D0A$A$A'00 	9OPWY]9^9^ 	!>2    7#########rT   c                 6   K   ddl m}  || |           d{V S )u  Outermost auth seam: non-interactive bearer-token auth for opted-in routes.

    Registered LAST so it runs FIRST (Starlette middleware is outermost-last).
    A registered token route is fully owned here — authenticate by token,
    attach the principal + ``token_authenticated`` flag, and let the downstream
    cookie/session gates skip enforcement. Non-token routes pass straight
    through untouched.
    r   )token_auth_middlewareN)$hermes_cli.dashboard_auth.token_authr   )r   r   r   s      rR   _token_auth_seamr  M  s@       KJJJJJ&&w	:::::::::rT   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))r   autor  flexz(Reasoning effort for delegated subagents)r   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discardr   zRefresh an already-installed cua-driver during hermes update. Disable this on non-admin macOS accounts where /Applications is not writable.)r  r  )zdelegation.reasoning_effortz%updates.non_interactive_local_changeszupdates.refresh_cua_driver_SCHEMA_OVERRIDESprivacysecuritycontextagentskillscronnetworkcheckpoints	approvalshuman_delaydisplay	dashboardcode_executionprompt_cachinggoalsupdates
onboardingtelegramdiscordcomputer_use_CATEGORY_MERGE)r  rC  terminalrJ  
delegationmemorycompressionrA  browservoicettssttloggingrR  	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  )
isinstancer   intfloatrb  dictr_  s    rR   _infer_typeri    sz    % y% x% x% v% x8rT   r   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	  )itemsr   rd  rg  update_build_schema_from_configri  replacer   r?  rT  r   )rj  rk  schemakeyr_  full_keyr	  entrys           rR   rs  rs    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MrT   _ordered_schemac                   4    e Zd ZU eed<   dZee         ed<   dS )ConfigUpdaterj  Nprofile)__name__
__module____qualname__rg  __annotations__r|  r   str rT   rR   r{  r{  ;  s/         LLL!GXc]!!!!!rT   r{  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 )EnvVarUpdaterv  r_  Nr|  r   api_key)r}  r~  r  r  r  r|  r   r  r  rT   rR   r  r  @  sJ         	HHHJJJ!GXc]!!! GSrT   r  c                   4    e Zd ZU eed<   dZee         ed<   dS )EnvVarDeleterv  Nr|  r}  r~  r  r  r  r|  r   r  rT   rR   r  r  L  /         	HHH!GXc]!!!!!rT   r  c                   4    e Zd ZU eed<   dZee         ed<   dS )EnvVarRevealrv  Nr|  r  r  rT   rR   r  r  Q  r  rT   r  c                   .    e Zd ZU i Zeeef         ed<   dS )MemoryProviderConfigUpdatevaluesN)r}  r~  r  r  r   r  r  r  rT   rR   r  r  V  s*         FDcNrT   r  c                   |    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Zee	         ed<   dS )MessagingPlatformUpdateNenabledenv	clear_envr|  )r}  r~  r  r  r   r   r  r  r   r  r  r   r|  r  rT   rR   r  r  Z  sh         "GXd^"""Cc3hItCy "GXc]!!!!!rT   r  c                   *    e Zd ZU dZee         ed<   dS )TelegramOnboardingStartNbot_name)r}  r~  r  r  r   r  r  r  rT   rR   r  r  c  s&         "Hhsm"""""rT   r  c                   @    e Zd ZU ee         ed<   dZee         ed<   dS )TelegramOnboardingApplyallowed_user_idsNr|  r}  r~  r  r   r  r  r|  r   r  rT   rR   r  r  g  s6         3i!GXc]!!!!!rT   r  c                   4    e Zd ZU eed<   dZee         ed<   dS )AudioTranscriptionRequestdata_urlN	mime_type)r}  r~  r  r  r  r  r   r  rT   rR   r  r  l  s/         MMM#Ix}#####rT   r  c                   2    e Zd ZU eed<   eed<   dZeed<   dS )ManagedFileUploadr   r  T	overwriteN)r}  r~  r  r  r  r  r   r  rT   rR   r  r  q  s4         
IIIMMMItrT   r  c                       e Zd ZU eed<   dS )ManagedDirectoryCreater   Nr}  r~  r  r  r  r  rT   rR   r  r  w           
IIIIIrT   r  c                   (    e Zd ZU eed<   dZeed<   dS )ManagedFileDeleter   F	recursiveN)r}  r~  r  r  r  r  r   r  rT   rR   r  r  {  s+         
IIIItrT   r  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  r  c                     | pd                     dd          d                                                                         }t                              |d          S )Nr   ;r   r   r  )r   r   r   r  r   )r  
normalizeds     rR   _audio_extension_for_mimer    sN    /r((a00399;;AACCJ!%%j':::rT   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"
    scoperQ   r  r   taskbase_urlr  Fconfirm_expensive_modelNr|  )r}  r~  r  __doc__r  r  r  r  r  r  r   r|  r   r  rT   rR   r  r    s           JJJMMMJJJD#NNN Hc GS$)T)))!GXc]!!!!!rT   r  c                   ,    e Zd ZU dZeed<   dZeed<   dS )MoaModelSlotr   rQ   r  N)r}  r~  r  rQ   r  r  r  r  rT   rR   r  r    s-         HcE3OOOOOrT   r  c                       e Zd ZU g Zee         ed<    e            Zeed<   dZe	e
         ed<   dZe	e
         ed<   dZeed<   dZeed	<   dS )
MoaPresetPayloadreference_models
aggregatorNreference_temperatureaggregator_temperature   
max_tokensTr  )r}  r~  r  r  rb  r  r  r  r  r   rf  r  r  re  r  r   r  rT   rR   r  r    s         +-d<(---+|~~J--- .28E?111.2HUO222JGTrT   r  c                       e Zd ZU dZeed<   dZeed<   i Zeee	f         ed<   g Z
ee         ed<    e            Zeed<   dZee         ed	<   dZee         ed
<   dZeed<   dZeed<   dZee         ed<   dS )MoaConfigPayloadr  default_presetr   active_presetpresetsr  r  Nr  r  r  r  Tr  r|  )r}  r~  r  r  r  r  r  r  rg  r  r  rb  r  r  r  r   rf  r  r  re  r  r   r|  r  rT   rR   r  r    s         #NC###M3+-GT#''(--- ,.d<(---+|~~J----18E?111.2HUO222JGT!GXc]!!!!!rT   r  rQ   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_providerr   r   r  rQ   )_AGGREGATOR_PROVIDERS
openrouterr1  z$model normalization failed for %s/%sTexc_info)hermes_cli.modelsr  r  hermes_cli.model_normalizer  r   r   r   rd  rg  r  r   rW   r  r   rM   debug)rQ   r  r  r  r  prov_inmodel_in	canonicalcur_cfgcur_providerr  normalized_models               rR    _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   r  r  c                    t          | t                    si } t          |                     d          pd                                                                          }|                                                                }|| d<   || d<   |                                r|                                | d<   n |                     d          r||k    rd| d<   |                                r.|                                | d<   |                     dd           n,|                     d          r||k    rt          | d	           ||k    rt          | 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.
    rQ   r   r  r  r  apiNF)clear_api_mode)clear_api_keycontext_length)rd  rg  r  r   r   r   popr   )r  rQ   r  r  r  prev_providernew_providers          rR   _apply_main_model_assignmentr    sz   8 i&& 		j117R88>>@@FFHHM>>##))++L$Ij Ii~~ # ( 0 0	*	z	"	" #|}'D'D !#	*
 }} J&}}	)eT""""	y	!	! Jlm&C&C(5IIII}$$(%HHHHMM"D)))rT   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.
    )FNr   z/health/detailedNz/healthGET)methodtimeout   T)_GATEWAY_HEALTH_URLrstripendswithr   urllibr   r5   urlopen_GATEWAY_HEALTH_TIMEOUTstatusjsonloadsreadrW   )baser   reqrespbodys        rR   _probe_gateway_healthr  _  s   &  { %%c**D}}'(( '.s-..../	y	!	! '%s9~~o%&***t,<,<,<=  	.((e(<<C''5L'MM &QU;#%%:diikk22D:& & & & & & & & & &%& & & & & & & & & & & & & & &  	 	 	H	;s=   AD05D$D0D0$D(	(D0+D(	,D00
D=<D=	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)r}  r~  r  r   r  r   r  rT   rR   r  r    s:         rT   r  >   .hg.svn.next.venv.cache.turbodistvenvbuildre   __pycache__node_modules.git>   	auth.json	auth.lockconfig.yamlbws_cache.json.git-credentialsgoogle_oauth.jsongoogle_token.json.anthropic_oauth.jsongoogle_oauth_pending.jsonwebhook_subscriptions.jsoncredentialsz
mcp-tokenspairingrO   c                     |                                  }|dk    s|                    d          s|dk    rdS |t          v S )af  Return True for a basename the managed-files API must never expose.

    Covers ``.env`` / ``.env.<suffix>`` / ``.envrc`` variants plus the
    canonical Hermes credential-store basenames (see
    ``_SENSITIVE_MANAGED_FILE_BASENAMES`` above).

    Case-insensitive so ``.ENV`` / ``.Env.local`` / ``Auth.JSON`` on
    case-insensitive filesystems (macOS/Windows mounts) can't slip past
    the guard.

    Basename-only: for the directory-tree credential stores
    (``mcp-tokens/``, ``pairing/``) that the canonical guards also deny,
    use :func:`_is_sensitive_path`, which the API call sites route through.
    .envz.env.z.envrcT)r   r   !_SENSITIVE_MANAGED_FILE_BASENAMES)rO   lowereds     rR   _is_sensitive_filenamer5    sJ     jjllG&G..w777h;N;Nt777rT   c                 j    t          | j                  rdS t          d | j        D                       S )uf  Return True for any path the managed-files API must never expose.

    Combines the basename denylist (:func:`_is_sensitive_filename`) with a
    credential-directory-tree check: a path is sensitive if its own basename
    is sensitive OR any of its path components is a credential directory
    (``mcp-tokens`` / ``pairing``). The component match is case-insensitive
    and needs no HERMES_HOME resolution, so it blocks these trees wherever
    they sit under the operator-configured managed root — closing the gap
    the canonical guards cover as directory trees but a basename-only check
    would miss.

    Read-side only: this guards list/read/download (the #57505 exfil surface).
    The write endpoints (upload/mkdir/delete) are a separate threat class
    handled by the write-path checks; extending this guard to them is out of
    scope for this fix.
    Tc              3   L   K   | ]}|                                 t          v V   d S N)r   _SENSITIVE_MANAGED_DIR_NAMESr   parts     rR   r   z%_is_sensitive_path.<locals>.<genexpr>  s0      SStzz||;;SSSSSSrT   )r5  rO   anyr   r   s    rR   _is_sensitive_pathr>    s:    " di(( tSS
SSSSSSrT   i   i   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  r  z.mkvzvideo/x-matroskaz.movzvideo/quicktimer  z	video/mp4r  .opuszaudio/ogg; codecs=opusr
  r  r  r  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)Nr   r   zPath is requiredr    Invalid pathzfile:>   r   r   Fstrict)r  r   r4   r   r   r  parseurlparsenetloc
ValueErrorr   url2pathnamer   r   
expanduseris_absolutecwdresolveOSErrorRuntimeError)r^  rawparsed	candidates       rR   _fs_pathrr  D  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c                     | j                                         }|t          v rt          |         S t          j        t          |                     \  }}|pdS )Napplication/octet-stream)suffixr   _FS_MIME_TYPES	mimetypes
guess_typer  )r   ru  guessedro  s       rR   _fs_mime_typerz  X  sQ    [  Ff%%%c$ii00JGQ000rT   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 )r   >   	   
      r   Nr  )r   bytes     rR   r   z#_fs_looks_binary.<locals>.<genexpr>e  s4      QQ44"99[9P9PQ9P9P9P9PQQrT   gQ?)sumr   )r{  
suspiciouss     rR   _fs_looks_binaryr  `  sO     u}}tQQ4QQQQQJD		!D((rT   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 )
Nr   File not foundr     File is not readabler   ra  Path points to a directoryzOnly regular files can be read)rr  r  statFileNotFoundErrorr4   NotADirectoryErrorPermissionErrorrm  S_ISDIRst_modeS_ISREG)r   re   stexcs       rR   _fs_regular_filer  i  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rP   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existsr  rm  parent)rP   	directoryro  r  s       rR   _fs_find_git_rootr  |  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 )NrU  rk  TERMINAL_CWDr   >   rk  r8  rm  Frb  )r   r   r  rq   environr   r   ri  rl  is_dirrm  rn  rk  )cfg_terminalro  rq  s      rR   _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-rk  c                     	 ddddd}t           j        dk    rt                      |d<   t          j        dd| d	d
gfi |}|j        dk    r|j                                        ndS # t          $ r Y dS w xY w)NT   F)capture_outputrZ  r  checkwin32creationflagsgit-Cbranchz--show-currentr   r   )	sysplatformr
   
subprocessrun
returncodestdoutr   rW   )rk  
run_kwargsresults      rR   _fs_git_branchr    s    "	&
 &

 <7""*<*>*>J'D#x)9:
 

 
 )/(9Q(>(>v}""$$$BF   rrs   A$A' '
A54A5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   appendrl  rm  rn  )homerootsoutroots       rR   _media_serve_rootsr    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   ra  r   i  zUnsupported media typec              3   6   K   | ]}|k    p|j         v V  d S r8  parents)r   r  re   s     rR   r   zget_media.<locals>.<genexpr>  s3      JJDv~7!7JJJJJJrT   r  zPath outside media rootsr   r    File too largeasciir  data:;base64,)r   ri  rl  rm  rn  r4   ru  r   _MEDIA_CONTENT_TYPESr  r<  is_filer  st_size_MEDIA_MAX_BYTESbase64	b64encode
read_bytesdecode)r   r  encodedre   s      @rR   	get_mediar    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_existsr  c                    	 |                                                      |          S # t          $ r |rt          dd           t          t
          f$ r t          dd          w xY w)Nrb  r   Path not foundr   r   ra  )ri  rl  r  r4   rm  rn  )r   r  s     rR   _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r  exist_ok  z#Managed files root is unavailable: r   z%Managed files root is not a directory)r   ri  mkdirrl  rm  rn  r4   r  )r^  r  resolvedr  s       rR   _ensure_managed_rootr    s    >>$$&&Da

4$
///<<>>\" a a a4_Z]4_4_````a?? ]4[\\\\Os   +A A9 A44A9r  re   c                      || k    p| |j         v S r8  r  )r  re   s     rR   _path_is_underr    s    T>3TV^33rT   c                 v    t          | pd                                          }d|v rt          dd          |S )Nr   r`  r   ra  r   )r  r   r4   )r^  rZ  s     rR   
_path_textr    s@    x~2$$&&D~~NCCCCKrT   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   Fr   >   r   r   
testclient
testserverr   r   )r   r`   rh   r   hostnamer   clientr   )r   r   client_hostlocal_hostss       rR   _local_dashboard_requestr    s}    w{ /599 uK &B--//D*1.@7>&&bGGIIKSSSK;<+"<<rT   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_HOMEr   Fr   )get_default_hermes_rootrb  )rq   r  r   r   hermes_constantsr  ri  rl  rm  rn  r   _HOSTED_MANAGED_FILES_ROOT)ro  r  r  s      rR    _default_hermes_root_is_opt_datar    s    
*..
+
+
1
1
3
3C u<<<<<<<&&((3355==U=KK\" < < <Cyy##%%--U-;;<---s   6A/ /AB87B8c                      t                      rdS 	 ddlm}   |             sdS n# t          $ r Y dS w xY w	 t	          t
                    }|dk    rdS n# t          $ r Y nw xY wdS )u  Return true when the dashboard should not offer ``hermes update``.

    Containerized dashboards are updated by the outer launcher/image, not by an
    in-browser local update action. Keep this dashboard capability separate
    from install-method detection: manual git/pip installs inside containers can
    still behave like their actual install method in the CLI.

    However, when the install method is ``git`` (a bind-mounted checkout inside
    a container — e.g. the hermes-webui image sharing the Hermes source tree),
    the dashboard's ``hermes update`` button is the correct update path and
    should not be suppressed. Other containerized install methods remain
    externally managed unless their apply path is proven safe inside the
    running container filesystem.
    Tr   )is_containerFr  )r  r  r  rW   r!   PROJECT_ROOT)r  r  s     rR   *_dashboard_local_update_managed_externallyr    s     ()) t111111|~~ 	5	   uu&|44U??5    4s   % 
33A 
A! A!)create_rootr  c                   t           j                            t          d                                          }|r?|rt          |          nt          t          |                    }t          ||d          S t                      r/|rt          t                    nt          }t          ||d          S t          t          j                              }t          |d d          S )Nr   F)r  r  r  T)rq   r  r   _MANAGED_FILES_ROOT_ENVr   r  r  r   r  r  r  r  )r   r  raw_forced_rootr  r  s        rR   _managed_files_policyr  >  s    jnn%<bAAGGIIO ^8Co#O444Y]^mYnYnIoIo!tW\]]]] ()) ^CNn#$>???Tn!tW\]]]]49;;''D4TSWXXXXrT   	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>   rm  r   c              3   "   K   | ]
}|d k    V  dS )..Nr  r:  s     rR   r   z(_resolve_managed_path.<locals>.<genexpr>b  s&      <<D44<<<<<<<rT   r   zPath cannot contain '..'r   zPath must be absoluter  r  r  Path outside managed files root)r  r  r  r  r   ri  rj  r<  r   r4   r  r  r  rO   r  r  )	r^  r   r  policyrZ  r  rq  r  r  s	            rR   _resolve_managed_pathr  Q  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]]**rT   r  c                 R    | j         t          | j                   nd }||| j        dS )N)r  r  r  )r  r  r  )r  r  s     rR   _managed_response_metar  w  s;    -3-?-K#f()))QUK"!1  rT   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   ra  r   r  r  r  zCould not stat path: r   rt  )rO   r   is_directorysizemtimer  )rl  rm  rn  r4   r  r  r  r  rw  rx  rO   r  r  st_mtime)r  re   r  r  r  r  r  s          rR   _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#r  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 )Nr   r  ,r   z!Upload payload must be a data URLr   r   r   r  r   rt  ;base64z%Upload payload must be base64 encodedTvalidatez"Upload payload is not valid base64r  File is too large)r   r   r4   r   r  	b64decodebinasciiErrorrg  r   _MANAGED_FILE_MAX_BYTES)r  rZ  headerr  r  r{  s         rR   _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                 N    g | ]!}t          |          t          |          "S r  )r>  r  )r   childr  s     rR   
<listcomp>z&list_managed_files.<locals>.<listcomp>  sA     
 
 
%e,,
..
 
 
rT   r  zDirectory is not readabler  zCould not read directory: c                 b    | d          t          | d                                                   fS )Nr  rO   )r  r   items    rR   <lambda>z$list_managed_files.<locals>.<lambda>  s+    tN';#;Sf=N=N=T=T=V=V"W rT   rv  )r   r  entries)r  r  r4   r  iterdirr  rm  sortr  r  r  r  )	r   r   re   display_pathr  r  r  r  r  s	           @rR   list_managed_filesr!    s     #8w#G#G FFL==?? F4DEEEE==?? O4MNNNN	X
 
 
 
))
 
 

  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                 8  K   t          ||           \  }}}|                                st          dd          |                                st          dd          t	          |          r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   Path is not a filer  (Access to sensitive files is not allowedr  Could not stat file: r  r  r   rt  r  r  zCould not read file: r  r  )rO   r   r  r  r  )r  r  r4   r  r>  r  r  rm  r  rw  rx  rO   r  r  r  r  r  r  )	r   r   r  re   r   r  r  r  r  s	            rR   read_managed_filer&    s     #8w#G#G FFL==?? F4DEEEE>> J4HIIII&!! `4^____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   B 
B?&B::B? 9D: :$E7E22E7c                 H  K   t          ||           \  }}}|                                st          dd          |                                st          dd          t	          |          r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          t          |          ||j        d          S )u  Stream a managed file as an attachment download.

    Remote clients (desktop app, browser dashboard) open agent-written files
    that live on *this* gateway's disk, not theirs. Auth-gated like every other
    managed-files route — ``auth_middleware`` additionally accepts the session
    token as a ``?token=`` query param here so a shell/browser-opened download
    (which can't set the session header) still authenticates. See ``/api/pty``
    for the same query-token precedent.
    r   r  r   r   r#  r  r$  r  r%  Nr  r  r   rt  
attachmentr   
media_typefilenamecontent_disposition_type)r  r  r4   r  r>  r  r  rm  r  rw  rx  rO   r:   r  )r   r   r  re   _display_pathr  r  r  s           rR   download_managed_filer.    sN      %:$$H$H!FFM==?? F4DEEEE>> J4HIIII&!! `4^____S{{}}$ S S S4QC4Q4QRRRRS%%%4GHHHH$V[11!4R8RI[[!-	   s   B 
B?&B::B?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    'A directory already exists at that pathr   File already existsr  r  File is not writabler  Could not write file: okrx  r   )r  r   r  r  r4   r  r  r  r  r  write_bytesr  rm  r  r  )r/  r   r  re   r   r{  
_mime_typer  s           rR   upload_managed_filer:    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
i   z/api/files/upload-stream.filer  c                 L  K   t          || d          \  }}}|                                r%|                                rt          dd          |                                r|st          dd          	 |j                            dd           n@# t          $ r t          dd	          t          $ r}t          d
d|           d }~ww xY wt          j	        d|j
         ddt          |j                            \  }}	t          |	          }
d}d}	 t          j        |d          5 }	 |                    t                      d {V }|snD|t#          |          z  }|t$          k    rt          dd          |                    |           g	 d d d            n# 1 swxY w Y   t          j        |
|           d}nJ# t          $ r  t          $ r t          dd	          t          $ r}t          d
d|           d }~ww xY w	 |s|
                    d           |                                 d {V  n7# |s|
                    d           |                                 d {V  w xY wdt/          ||          |dt1          |          S )NTr  r1  r2  r   r3  r  r  r4  r  z#Could not create parent directory: rm  .uploadrk  ru  dirr   Fwbr  r  r5  
missing_okr6  )r  r  r  r4   r  r  r  rm  tempfilemkstemprO   r  r   rq   fdopenr  _UPLOAD_CHUNK_BYTESr   r  writert  unlinkr   r  r  )r   r;  r   r  r  re   r   r  tmp_fdtmp_nametmp_pathtotalrenamedr  chunks                  rR   upload_managed_file_streamrO  4  s      $9wRV#W#W#W FFL}} _6==?? _4]^^^^}} Ky K4IJJJJaD48888 L L L4JKKKK a a a4_Z]4_4_````a
  '!6;!!!)V]9K9K  FH H~~HEGYvt$$ 	!!"ii(;<<<<<<<< U#222'C@STTTT		%   ! 		! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	
8V$$$    L L L4JKKKK T T T4RS4R4RSSSST 	  	-OOtO,,,jjll  	-OOtO,,,jjll $VV44  !
(
(	 sg   :B $C;CC G 5A)F+G +F//G 2F/3G I .H<HHI 4J 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  r1  z"A file already exists at that pathr   r  r  zDirectory is not writabler  zCould not create directory: r6  )
r  r   r  r  r4   r  r  rm  r  r  )r/  r   r  re   r   r  s         rR   create_managed_directoryrQ  t  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  r1  r  zCould not delete path: Tr7  r   )r  r   r  r4   r  r  r  r  shutilrmtreermdirrH  rm  r  )r/  r   r  re   r   r  r   s          rR   delete_managed_filerW    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 )NrZ  rO   r   r  s    rR   r  zfs_list.<locals>.<lambda>  s,    4+>'>V@R@R@T@TVZ[aVb&c rT   r  r  ENOENT)r  errorENOTDIREACCESstrerrorz
read-error)rr  rq   scandirrO   _FS_READDIR_HIDDENr  r  r  r  r  r  r  rm  r   )r   re   r  scanrx  r  s         rR   fs_listre    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 )Nr  r  r   rbr  r  r   File read failedr  rZ  utf-8rt  errors)binarybyteSizelanguagemimeTyper   rZ  	truncated)r  rr  r  _FS_TEXT_SOURCE_MAX_BYTESr4   min_FS_TEXT_PREVIEW_MAX_BYTESopenr  r  rm  r  r  _FS_PREVIEW_LANGUAGE_BY_EXTr   ru  r   rz  r  )r   re   r  bytes_to_readhandler{  r  s          rR   fs_read_textrx    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*c                   $    e Zd ZU eed<   eed<   dS )FsWriteTextr   r   Nr  r  rT   rR   rz  rz    s"         
IIILLLLLrT   rz  z/api/fs/write-textc                   K   t          | j                  }| j        pd}t          |                    d                    t
          k    rt          dd          	 |                                }nY# t          $ r d}Y nKt          $ 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          |j                                        st          d	d          |                    d|j         dt'          j                               }	 |                    |d           t'          j        ||           nl# t          $ r( |                    d           t          dd          t          $ r/}|                    d           t          dd|           d}~ww xY wdt          |          t          |                    d                    dS )aJ  Overwrite (or create) a UTF-8 text file for the in-app spot editor.

    Mirrors the local Electron ``hermes:fs:writeText`` hardening: the path is
    resolved + validated by ``_fs_path``, the parent directory must already
    exist (we never build directory trees), only regular files may be replaced,
    and the payload is size-capped. The write is staged to a sibling temp file
    and ``os.replace``-d into place so a crash mid-write can't truncate the
    original. Stale-on-disk detection is the client's job (re-read before save),
    so both transports behave identically.
    r   ri  r  zContent too larger   Nr  r4  r   ra  r  z!Only regular files can be writtenzParent directory does not existrm  z.hermes-tmp-encodingTrA  r  r5  )r7  r   rm  )rr  r   r   r   r   _FS_TEXT_WRITE_MAX_BYTESr4   r  r  r  rm  r  r  r  r  r  r  	with_namerO   rq   getpid
write_textrt  rH  )r/  re   rZ  r  r  tmps         rR   fs_write_textr    s      gl##F? bD
4;;w  #;;;4GHHHHP'-{{}}    L L L4JKKKK P P PCHH4NOOOOP 
~$,rz22~4PQQQQ	~dl2:66~4WXXXX=!! W4UVVVV


Ev{EE	EE
F
FCTtg...

3 L L L

d
###4JKKKK T T T

d
###4RS4R4RSSSST FT[[=Q=Q9R9RSSSs6   A2 2C #C# CC?,F, ,:H&*HH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 )Nr  r  r   r  r  r  r   rh  dataUrlr  r  )r  rr  r  _FS_DATA_URL_MAX_BYTESr4   r  r  r  r  r  rm  r  rz  )r   re   r  r  r  s        rR   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 )Nr  )rr  r  r  r  r  rm  r  )r   re   r  rP   s       rR   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)rk  r  )r  r  )rk  s    rR   fs_default_cwdr  !  s'      


C."5"5666rT   )web_gitc                    K   t          j                    }	  |j        d| g|R   d{V S # t          $ r%}t	          dt          |          pd          d}~ww xY w)zIRun a (blocking) git op off the event loop; map a failed mutation to 400.Nr   zgit operation failedr   )rj   get_running_looprp   rn  r4   r  )fnrf   loopr  s       rR   _git_opr  3  s      #%%DX)T)$:T:::::::::: X X XCHH4V@VWWWWXs   , 
A AAc                 :    t          t          |                     S r8  )r  rr  r=  s    rR   	_git_pathr  <  s    x~~rT   c                       e Zd ZU eed<   dS )GitPathBodyr   Nr  r  rT   rR   r  r  @  r  rT   r  c                   4    e Zd ZU eed<   dZee         ed<   dS )GitFileBodyr   Nr;  )r}  r~  r  r  r  r;  r   r  rT   rR   r  r  D  s/         
IIID(3-rT   r  c                   2    e Zd ZU eed<   eed<   dZeed<   dS )GitCommitBodyr   messageFpushN)r}  r~  r  r  r  r  r   r  rT   rR   r  r  I  s4         
IIILLLD$rT   r  c                       e Zd ZU 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<   dS )GitWorktreeAddBodyr   NrO   r  r  existingBranch)
r}  r~  r  r  r  rO   r   r  r  r  r  rT   rR   r  r  O  sk         
IIID(3- FHSM   D(3-$(NHSM(((((rT   r  c                   2    e Zd ZU eed<   eed<   dZeed<   dS )GitWorktreeRemoveBodyr   worktreePathFforceNr}  r~  r  r  r  r  r   r  rT   rR   r  r  W  s7         
IIIE4rT   r  c                   $    e Zd ZU eed<   eed<   dS )GitBranchSwitchBodyr   r  Nr  r  rT   rR   r  r  ]  s"         
IIIKKKKKrT   r  z/api/git/statusc                 `   K   t          t          j        t          |                      d {V S r8  )r  _web_gitrepo_statusr  r=  s    rR   git_status_router  b  s1      -y?????????rT   z/api/git/worktreesc                 d   K   dt          t          j        t          |                      d {V iS )N	worktrees)r  r  worktree_listr  r=  s    rR   git_worktrees_router  g  s6      wx'=yOOOOOOOOPPrT   z/api/git/branchesc                 d   K   dt          t          j        t          |                      d {V iS )Nbranches)r  r  branch_listr  r=  s    rR   git_branches_router  l  s6      gh&:IdOOLLLLLLLLMMrT   z/api/git/review/listuncommittedr  r  c                 d   K   t          t          j        t          |           ||           d {V S r8  )r  r  review_listr  )r   r  r  s      rR   git_review_list_router  q  s5      -ytLLLLLLLLLrT   z/api/git/review/diffstagedc           	      l   K   dt          t          j        t          |           ||||           d {V iS Ndiff)r  r  review_diffr  )r   r;  r  r  r  s        rR   git_review_diff_router  v  sB       '("6	$uVZ\bccccccccddrT   z/api/git/file-diffc                 f   K   dt          t          j        t          |           |           d {V iS r  )r  r  file_diff_vs_headr  )r   r;  s     rR   git_file_diff_router  }  s8      '("<iootTTTTTTTTUUrT   z/api/git/review/commit-contextc                 `   K   t          t          j        t          |                      d {V S r8  )r  r  review_commit_contextr  r=  s    rR   git_commit_context_router    s1      74IIIIIIIIIrT   z/api/git/review/rev-parserefc                 f   K   dt          t          j        t          |           |           d {V iS )Nsha)r  r  review_rev_parser  )r   r  s     rR   git_rev_parse_router    s8      !:IdOOSQQQQQQQQRRrT   z/api/git/review/ship-infoc                 `   K   t          t          j        t          |                      d {V S r8  )r  r  review_ship_infor  r=  s    rR   git_ship_info_router    s1      2IdOODDDDDDDDDrT   z/api/git/review/stager  c                 v   K   t          t          j        t          | j                  | j                   d {V S r8  )r  r  review_stager  r   r;  r  s    rR   git_stage_router    s9      .	$)0D0DdiPPPPPPPPPrT   z/api/git/review/unstagec                 v   K   t          t          j        t          | j                  | j                   d {V S r8  )r  r  review_unstager  r   r;  r  s    rR   git_unstage_router    s9      0)DI2F2F	RRRRRRRRRrT   z/api/git/review/revertc                 v   K   t          t          j        t          | j                  | j                   d {V S r8  )r  r  review_revertr  r   r;  r  s    rR   git_revert_router    s9      /491E1EtyQQQQQQQQQrT   z/api/git/review/commitc                    K   t          t          j        t          | j                  | j        | j                   d {V S r8  )r  r  review_commitr  r   r  r  r  s    rR   git_commit_router    s?      /491E1Et|UYU^_________rT   z/api/git/review/pushc                 j   K   t          t          j        t          | j                             d {V S r8  )r  r  review_pushr  r   r  s    rR   git_push_router    s5      -y/C/CDDDDDDDDDrT   z/api/git/review/create-prc                 j   K   t          t          j        t          | j                             d {V S r8  )r  r  review_create_prr  r   r  s    rR   git_create_pr_router    s5      2Idi4H4HIIIIIIIIIrT   z/api/git/worktree/addc                    K   d | j         | j        | j        | j        d                                D             }t          t          j        t          | j	                  |           d {V S )Nc                     i | ]
\  }}|||S r  r  )r   rv  r_  s      rR   
<dictcomp>z*git_worktree_add_route.<locals>.<dictcomp>  s6     	 	 	C 	U	 	 	rT   )rO   r  r  r  )
rO   r  r  r  rq  r  r  worktree_addr  r   )r  r  s     rR   git_worktree_add_router    s      	 	 IkI"1	
 

 %''	 	 	G .	$)0D0DgNNNNNNNNNrT   z/api/git/worktree/removec                    K   t          t          j        t          | j                  t          | j                  | j                   d {V S r8  )r  r  worktree_remover  r   r  r  r  s    rR   git_worktree_remove_router    s[       )DI"6"6	$BS8T8TVZV`        rT   z/api/git/branch/switchc                 v   K   t          t          j        t          | j                  | j                   d {V S r8  )r  r  branch_switchr  r   r  r  s    rR   git_branch_switch_router    s9      /491E1Et{SSSSSSSSSrT   z/api/statusr|  c                    K   d }| pd                                 }|r;|                                dk    r#t          |          }|                                 	 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|}|s|t#          |          }|d}|}|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t3          |pi                     dd                    }t5          ||
|          }t7          ||
          }t          j                    	                    d t8                     d {V }t;          t=          t>          j         dd                    }g }	 ddl!m"} d  |            D             }n# t          $ r Y nw xY wi dtF          dtH          d|d|d tK                       d!|d|
d"|d#|d$|d|d%|d&|d'|d(|d|d)|}|sk|&                    tO          tQ                                tO          tS                                tO          tU                                |t          d*           || |j+        tY          j-                      S S # | |j+        tY          j-                      w w xY w)+Nr   currentTpidr   )load_gateway_configc                     h | ]	}|j         
S r  rh  )r   r  s     rR   	<setcomp>zget_status.<locals>.<setcomp>  s'     , , ,#+, , ,rT   gateway_state	platformsc                 $    i | ]\  }}|v 	||S r  r  )r   rv  r_  configured_gateway_platformss      rR   r  zget_status.<locals>.<dictcomp>	  s5     % % %"U::: :::rT   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   r   )r   snows     rR   r   zget_status.<locals>.<genexpr>1	  sm       & &uuZ((0quu]AEE,4J4JKKKsRR RRRR& &rT   active_agents)gateway_runningr  r  )r  r  r   Flist_providersc                     g | ]	}|j         
S r  rO   r   r   s     rR   r  zget_status.<locals>.<listcomp>^	  s    @@@af@@@rT   r   release_dateconfig_versionlatest_config_versioncan_update_hermesr  gateway_platformsgateway_exit_reasongateway_updated_atgateway_busygateway_drainablerestart_drain_timeoutactive_sessionsauth_providers)hermes_homeconfig_pathenv_pathgateway_pidgateway_health_url).r   r   _config_profile_scope	__enter__r    r,   r  rj   r  rp   r  r   gateway.configr  get_connected_platformsrW   r/   r-   rq  hermes_stater  list_sessions_richtimer  r   r.   r*   r+   r_   r   r   r`   rh   hermes_cli.dashboard_authr  r   r   r  rr  r  r   r   r   __exit__r  r  )!r|  status_scoperequested_profilecurrent_ver
latest_verr  r  remote_health_bodyr  aliver  r  r  r  r  gateway_configlocal_runtimeruntimeruntime_pidr  r  dbsessionsr  r  r  r  r   r  _list_providersr  r  r  s!                                  @@rR   
get_statusr0    s}     L B--//  !.4466)CC,->??   v3"6"8"8Z
 &''%T1*. 		@#6 		@+--D.2.B.B+/ / ) ) ) ) ) )%E%  @"&% @"4"8"8"?"?K"$"!8<$	0::::::0022N, ,/=/U/U/W/W, , ,((  	0 	0 	0+/(((	0
 ,--?1?6H6L6L_6]6]?(G  	*=#<8GGK&"&) 	.#KK88M 'K 8 8 >B+7% % % %&7&=&=&?&?% % %!
 #*++m"<"<!(\!:!:" .1>B_1_1_en$&!!  .%7%C !$555$-M  	&}49K9W%M	......B	00r0::ikk"% & & & &'& & & # # 








 	 	 	D	 ,W],?,?QR,S,STT*+''
 
 

 5+'
 
 
 '.&>&@&@&P&P0'
 '
 !
 !
 !
 !
 !
 !
 WSYGGHH$&	SSSSSS@@oo.?.?@@@NN 	 	 	D	
{
,
 k
 $Z	

  %O%Q%Q!Q
 
 ]
  !2
 "#6
 !"4
 ]
 L
  !2
 $%:
 
  ]!
" n#
<  	MM"?#4#455"?#4#455//*&9     #!L!3<>>222 $<#!L!3<>>222 $s   B
Q &.D Q D$!Q #D$$C"Q J AI1 J 1JJ 
Q 
JQ JB'Q  M Q 
M(%Q 'M((B=Q  Q$iU  r   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.r   z%(?:^|[^\d])10\.0\.(\d{5,})(?:[^\d]|$)r   N)researchre  grouprg  )r   r1  r_  matchs       rR   _windows_build_numberr7  	  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)rq   
os_release
os_versionr  )r7  _WINDOWS_11_MIN_BUILDr3  sub)r8  r9  r   r1  r!  s        rR   _display_system_platformrC  	  s}     w$%g~>>*?!?!?V#	  N G "	  rT   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)r8  r9  r   r1  )archr  python_versionpython_implhermes_version	cpu_count)rL  	availableusedpercentrW  )rL  rK  freerL  disk皙?rK   cpu_percent
getloadavgload_avguptime_seconds)r  rsscreate_timenum_threadsprocessTpsutilF)&r  rC  r8  r9  r   machinenoderF  python_implementationr   rq   rI  rX  virtual_memoryrL  rJ  rK  rL  
disk_usager  r   rM  rW   rP  r   rb  	boot_timere  r   Processr  memory_inforT  rU  rV  rQ  rm  r{   )	_platformrN   rX  vmdulabootprocs           rR   get_system_statsrg  	  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)r  pausedinterval_hoursrn  min_idle_hoursstale_after_daysarchive_after_days)rC  rj  rW   r4   
load_state
_safe_callr   )rj  r  rh   s      rR   get_curator_statusry  
  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 )CuratorPauserr  Nr}  r~  r  r   r  r  rT   rR   r{  r{  .
           LLLLLrT   r{  z/api/curator/pausedc                    K   ddl m} |                    t          | j                             dt          | j                  dS )Nr   ri  T)r7  rr  )rC  rj  
set_pausedr   rr  )r  rj  s     rR   set_curator_pausedr  2
  sN      tDK(()))$t{"3"3444rT   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).rj  r  curator-runr  zFailed to run curator: r   NTr7  r  rO   _spawn_hermes_actionrW   r4   r  rf  r  s     rR   run_curatorr  :
  ss      U#Y$6FF U U U4Sc4S4STTTTUtx???    
:5:z/api/learning/graphc                    K   	 ddl m} t          |           5   |            cddd           S # 1 swxY w Y   dS # t          $ r, t                              d           t          dd          w xY w)zLearning graph payload for the desktop panel.

    Profile-scoped view of learned, non-base skills plus memory chunks, with
    graph links derived from skill relations and memory-skill overlap.
    r   )build_learning_graphNzGET /api/learning/graph failedr  zFailed to build learning graphr   )agent.learning_graphr  _profile_scoperW   rM   	exceptionr4   )r|  r  s     rR   get_learning_graphr  D
  s      V======G$$ 	* 	*''))	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* V V V78884TUUUUVs!   = 
0= 4= 4= 6A3c                   4    e Zd ZU eed<   dZee         ed<   dS )LearningNodeRefidNr|  r  r  rT   rR   r  r  U
  s/         GGG!GXc]!!!!!rT   r  c                   >    e Zd ZU eed<   eed<   dZee         ed<   dS )LearningNodeEditr  r   Nr|  r  r  rT   rR   r  r  Z
  s8         GGGLLL!GXc]!!!!!rT   r  z/api/learning/noder  c                    K   ddl m} t          |          5   ||           }ddd           n# 1 swxY w Y   |                    d          s%t	          d|                    dd                    |S )	zXCurrent content of a journey node (skill SKILL.md or memory chunk), for an edit prefill.r   )node_detailNr7  r   r  	not foundr   )agent.learning_mutationsr  r  r   r4   )r  r|  r  ress       rR   get_learning_noder  `
  s       544444		 	   k"oo              774== UCGGI{4S4STTTTJ   044c                   K   ddl m} t          | j                  5   || j                  }ddd           n# 1 swxY w Y   |                    d          s%t          d|                    dd                    |S )	uM   Delete a journey node — skills are archived (restorable), memories removed.r   )delete_nodeNr7  r   r  zdelete failedr   )r  r  r  r|  r  r   r4   )r  r  r  s      rR   delete_learning_noder  l
  s       544444		%	% # #k$'""# # # # # # # # # # # # # # #774== YCGGI4W4WXXXXJs   :>>c                   K   ddl m} t          | j                  5   || j        | j                  }ddd           n# 1 swxY w Y   |                    d          s%t          d|                    dd                    |S )	z<Rewrite a journey node's content (SKILL.md or memory chunk).r   )	edit_nodeNr7  r   r  zedit failedr   )r  r  r  r|  r  r   r   r4   )r  r  r  s      rR   update_learning_noder  x
  s       322222		%	% / /i../ / / / / / / / / / / / / / /774== WCGGI}4U4UVVVVJ   A  AAfn_namec                     	 t          | |d           }t          |          r
 |            n|S # t          $ r |cY S w xY wr8  )r   callablerW   )modr  r  r  s       rR   rx  rx  
  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labelr   )r  rh   zportal features failedr  	logged_inportal_base_urlinference_base_urlrQ   z3https://portal.nousresearch.com/manage-subscription)r  
portal_urlinference_urlrQ   subscription_urlfeatures)r   hermes_cli.authr  rW   hermes_cli.nous_subscriptionr  rq  r   r  r  rM   r  rd  r   rg  r   r  )	cfgr   r  r  r  featsfeatrh   r  s	            rR   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   Tr  r  r  s     rR   run_prompt_sizer  
  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   Tr  r  r  s     rR   run_dumpr  
  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 )	Nrj  migrateconfig-migrater  r  r   Tr  r  r  s     rR   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)r}  r~  r  r  r   r  r  re  r  rT   rR   r  r  
  s4          FDE3rT   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)r7  urlsfailuresredactedauto_delete_seconds)hermes_cli.debugr  r  rj   	to_threadmaxrr  re  r  r   r  rn  r4   rW   rM   r  r  r  r  r  )r  r  r   r  r  s        rR   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.logzskills-installzaction-skills-install.logzskills-uninstallzaction-skills-uninstall.logskills-updatezaction-skills-update.logr  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_COMMANDS_ACTION_RESULTSr   r  	exit_codec           	      n   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           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 ===
ri  rt  rj  
   
Nr  r  )r  r  r  rt  rG  r   strftimer   r  r  r  r  r  )rO   r  r  log_file_namelog_pathlog_files         rR   _record_completed_actionr  4  sk   %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!!!t$$$*3DAAODs   BC''C+.C+c                  `   t           j        dk    rt           j        S t           j        } |                                                     d          r^t
          j                            t
          j                            |           d          }t
          j        	                    |          r|S | S )z=Prefer pythonw.exe for detached dashboard actions on Windows.r  z
python.exezpythonw.exe)
r  r  
executabler   r  rq   r   joindirnameisfile)exepythonws     rR   _dashboard_spawn_executabler  E  s    
|w~
.C
yy{{L)) ',,rws33]CC7>>'"" 	NJrT   
subcommandc                    t           |         }t                              dd           t          |z  }t          |dd          }|                    d| dt          j        d           d	                                           t                      d
dg| }t          t                    t          j        |t          j        i t          j        ddid}t           j        dk    rt%                      |d<   nd|d<   t          j        |fi |}|                                 t*                              |d           t/          |           t0          |<   |t2          |<   |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_NONINTERACTIVErc   )rk  stdinr  stderrr  r  r  start_new_sessionN)r  r  r  rt  rG  r   r  r   r  r  r  r  DEVNULLSTDOUTrq   r  r  r  r	   Popenr   r  r  tupler  r  )r  rO   r  r  r  cmdpopen_kwargsrf  s           rR   r  r  Q  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   '(($0A
OJ
OC <  ##;"*;5s;;$ $L |w(<(>(>_%%,0()C00<00D NNd###":..TM$KrT   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.ri  rt  )r}  rk  r   N)r  	read_textrm  
splitlines)r   r  rZ  r  s       rR   _tail_linesr  x  s     ;;== 	~~wy~AA   			OOEQ5!::E)s   0 ??verbc                 *    t          |           d|gz   S )Ngateway)_profile_cli_argsr|  r  s     rR   _gateway_subcommandr    s    W%%D(999rT   c                 N    d                     dgt          | |                    S )Nrp  hermes)r  r  r  s     rR   _gateway_display_commandr    s'    88XC 3GT B BCDDDrT   z[UW][A-Z0-9]{2,}platform_idrv  c                 V   | dk    s|sdS |dk    r&|                     d          st          dd          |dk    r&|                     d	          st          dd
          |dk    r>d |                    d          D             }d |D             }|rt          dd          dS dS )z@Reject platform credentials that are clearly in the wrong field.slackNSLACK_BOT_TOKENzxoxb-r   zTSlack Bot Token must start with xoxb-. Paste the bot token from OAuth & Permissions.r   SLACK_APP_TOKENzxapp-zkSlack App Token must start with xapp-. Paste the app-level token from Basic Information > App-Level Tokens.SLACK_ALLOWED_USERSc                 ^    g | ]*}|                                 |                                 +S r  r   r:  s     rR   r  z1_validate_messaging_env_value.<locals>.<listcomp>  s-    NNNTNDJJLLNNNrT   r	  c                 R    g | ]$}|d k    t                               |          "|%S )r   )_SLACK_MEMBER_ID_RE	fullmatch)r   user_ids     rR   r  z1_validate_messaging_env_value.<locals>.<listcomp>  s<     
 
 
#~~&9&C&CG&L&L~ ~~rT   zKSlack allowed user IDs must be comma-separated member IDs like U01ABC2DEF3.)r   r4   r   )r  rv  r_  user_idsinvalids        rR   _validate_messaging_env_valuer'    s   gU
(8(8(A(Ai
 
 
 	
 (8(8(A(A A
 
 
 	
 ### ONU[[-=-=NNN
 
#
 
 

  	d    $#	 	rT   c                 *   t          | d          }t                              d          }|V|                                Bt                              d          }||t          |          k    r|dfS t          d          t          |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)``.
    restartr  NTz7gateway restart already in progress for another profileF)r  r  r   pollr  r  rn  r  )r|  r  existingexisting_commands       rR   _spawn_gateway_restartr-    s     %Wi88J  !233H 7+//0ABB#'75;L;L'L'LT>!TUUU
,=>>EErT   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  r0  restart_actionrestart_pidr-  rW   rM   r  r  rN   r  r|  rf  reusedr  s       rR   %_restart_gateway_after_webhook_enabler8    s    
-g66ff 
 
 
OPPP$ XX
 
 	
 	
 	
 	
 	
 	

  
		HH	
 	
 	

  +x      
A+A
AAz/api/gateway/restartc                    K   	 t          |           \  }}nJ# t          $ r  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  r  )r-  r4   rW   rM   r  r  )r|  rf  _reusedr  s       rR   restart_gatewayr<    s      Y.w77gg    Y Y Y89994WRU4W4WXXXXY x!  s    A.AAz/api/gateway/drainc           	        K   ddl m}m}m} 	 |                                  d{V }n# t
          $ r i }Y nw xY wt          |pi                     dd                                                    	                                }t          | j        dd          }t          |dd          pd}|d	k    r, |            }t                              d
||           dd	|dS |dk    rt          dd|d           |t          |          t          |pi                     dd                              }	t                              d||	d                    dd|	d          |            |	d         dS )uK  Begin or cancel an external (NAS-driven) gateway drain.

    Authenticated by the non-interactive token-auth seam: the
    ``dashboard_auth/drain`` plugin registers this exact path as a token route
    and verifies the ``Authorization`` bearer secret. If that plugin isn't
    active (no ``HERMES_DASHBOARD_DRAIN_SECRET``), the route is NOT a token
    route, so on a gated bind the cookie gate handles it (a browser session can
    still drive it from the dashboard) and on a loopback bind the legacy
    session-token gate applies — either way it is never unauthenticated on a
    network-exposed bind.

    Body: ``{"action": "drain"}`` (begin) or ``{"action": "cancel"}`` (cancel).
    Begin writes the ``.drain_request.json`` marker the gateway's
    ``_drain_control_watcher`` observes (flip to ``draining`` + refuse new
    turns); cancel removes it (revert to ``running`` + re-accept). Idempotent
    on both sides. This endpoint only writes/removes the marker — the gateway
    process owns the actual state transition (there is no HTTP control channel
    into the running gateway; the marker IS the channel, decisions.md Q-B).

    The force-override (D6: "unless a user commands it") is NOT here — an
    immediate, drain-skipping action maps onto the existing
    ``POST /api/gateway/restart`` force path, which supersedes a drain.
    r   )clear_drain_requestdrain_requestedwrite_drain_requestNactiondraintoken_principal	principalrK  cancelz1Gateway drain CANCEL requested by %s (existed=%s)T)r7  rA  was_drainingr   zUnknown drain action z; expected 'drain' or 'cancel'r   suppress_notificationF)rD  rG  z>Gateway drain BEGIN requested by %s (suppress_notification=%s)requested_at)r7  rA  rH  drainingrG  )gateway.drain_controlr>  r?  r@  r  rW   r  r   r   r   r   rh   rM   rN   r4   r   )
r   r>  r?  r@  r  rA  principal_objrD  existedr/  s
             rR   gateway_drainrM    s     2         \\^^######   $*"!!(G4455;;==CCEEF GM+<dCCM{D99H[I%%''		EyRYZZZhHHHS6SSS
 
 
 	

 "!i.."DJB#3#34KU#S#STT  G 	IIH'(   / $O%%!()@!A  s   ) 88z/api/hermes/updatec                    K   t                      rd} t          d| d           dddd| dd	S 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.z}Hermes updates are managed outside this dashboard in containerized environments. The built-in local updater is disabled here.r  r   )r  FN#dashboard_update_managed_externallymanaged outside dashboard)r7  r  rO   r^  r  update_commandr  docker_update_unsupportedrr  zFailed to spawn hermes updater  zFailed to start update: r   Tr  )r  r  r!   r  r"   r#   r  rW   rM   r  r4   r  )r  install_methodrf  r  s       rR   update_hermesrT  8  sB      233 
 	
 	!'QGGGG#:9
 
 	
 +<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   ?B 
C.C		C   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-nTr   )r  rZ  r  r   )r   r   r   0Nr      )r  summaryauthorat)r  r  r  r  re  r  r  r  r   r   r  rW   )	r  r  rowsliner   r  r[  r\  r]  s	            rR   _recent_upstream_commitsr`  d  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/checkr  c                 p  K   t                      rdt          ddddddS 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.
    zmanaged-runtimeNFrP  zPHermes updates are managed outside this dashboard in containerized environments.)rS  current_versionbehindupdate_available	can_applyrQ  r  )r  pipr  r  r   )check_for_updatesz.update_checkzUpdate check failedrc  u5   Couldn't reach the update source — try again later.zYou're on the latest version.Trd  commits)r  r   r!   r  r#   r"   hermes_cli.bannerrg  r   rH  rm  rj   r  rW   rM   r  r`  )r  rS  rQ  r/  rg  rc  s         rR   check_hermes_updaterj    s     4 233 
/* %9.
 
 	
 +<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   .C 7#B C 
B(%C 'B((C &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 )$Nr   r  r	  r   zInvalid audio payloadr   r   r
  z$Audio payload must be base64 encodedr   r  r   r  zaudio/r  z"Payload must be an audio recordingTr  z!Audio payload is not valid base64zAudio recording is emptyr  zAudio recording is too largezhermes-desktop-voice-F)rk  ru  delete)transcribe_audioz"Desktop voice transcription failedr  zTranscription failed: successr^  zTranscription failed
transcriptrQ   )r7  ro  rQ   ) r  r   r   r4   r   r  r   r  r  r  r  rg  r   _MAX_TRANSCRIPTION_UPLOAD_BYTESr  rC  NamedTemporaryFilerG  rO   tools.transcription_toolsrm  rj   r  rp   rW   rM   r  rq   rH  rm  r   r  )r/  r  r  r  r  normalized_mime_typeaudio_bytes	temp_pathru  r  rm  r  r  r  s                 rR   transcribe_audio_uploadrv    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 )TTSSpeakRequestrZ  Nr  r  rT   rR   rx  rx  8  r  rT   rx  rZ  c                    t          |                     d          p|                     d          pd                                          }t          |                     d          pd                                          }|r| d| dn|S )NrO   voice_idVoicer	  r    ())r  r   r   )rZ  rO   r	  s      rR   _elevenlabs_voice_labelr~  <  s    uyy  DEIIj$9$9DWEEKKMMD599Z((.B//5577H%-7d!!h!!!!47rT   _voice_list_last_error	signaturec                 0    | da dS | t           k    rdS | a dS )zReturn True if ``signature`` is new and should be logged now.

    Passing ``None`` clears the latch (call on success). Idempotent per
    signature: the same error logs once until it changes.
    NFT)r  )r  s    rR   _voice_list_error_logged_oncer  I  s1     !%u***u&4rT   z/api/audio/elevenlabs/voicesc            	      V  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
          j        j        $ r}|j        dv r=t#          d|j                   rt$                              d|           dg ddcY d}~S t#          d|j                   rt$                              d|           t+          dd          d}~wt,          $ rM}t#          t          |                    rt$                              d|           t+          dd          d}~ww xY wt#          d           g }|                    d          pg D ]}t/          |t0                    st          |                    d          pd                                          }|sQ|                    |t          |                    d          p|          t5          |          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_KEYr   F)rJ  voicesz#https://api.elevenlabs.io/v1/voicesapplication/json)Acceptz
xi-api-keyr   rG   c                      t           j                            d          5 } t          j        |                                                     d                    cd d d            S # 1 swxY w Y   d S )Nr  r  ri  )r  r   r  r  r  r  r  )responser   s    rR   _fetchz%get_elevenlabs_voices.<locals>._fetcho  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-Nr   r  zhttp-u>   ElevenLabs voices unavailable: %s — check ELEVENLABS_API_KEYunauthorized)rJ  r  r^  z ElevenLabs voice list failed: %sr  z Could not load ElevenLabs voicesr   r  rz  rO   )rz  rO   r  c                 n    t          |                     d          pd                                          S )Nr  r   )r  r   r   r  s    rR   r  z'get_elevenlabs_voices.<locals>.<lambda>  s+    TXXg%6%6%<"!=!=!C!C!E!E rT   r  T)r   r   rq   r  r   r  r   r5   rj   r  r   r  r   rp   r^  	HTTPErrorcoder  rM   rN   warningr4   rW   rd  rg  r  r~  r  )	r  r  r  r/  r  r  rZ  rz  r   s	           @rR   get_elevenlabs_voicesr  Y  s       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 8z!!,-?SX-?-?@@ 		TVY   "'"~NNNNNNNN();););<< 	BLL;SAAA4VWWWW X X X(S22 	BLL;SAAA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0s4   AC G
'A E0'G
-AE00G
=AGG
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.``.
    r   r   zText is requiredr   r   )text_to_speech_toolNzDesktop voice TTS failedr  zSpeech synthesis failed: zInvalid TTS responsern  r^  zSpeech synthesis failed	file_pathzAudio file missingr   r  r  r  r  )r  r  r]  r  r  rg  zCould not read audio: r  Tr  r  rQ   )r7  r  r  rQ   )rZ  r   r4   tools.tts_toolr  rj   r  rp   rW   rM   r  rd  r  r  r  r   rq   r   r  splitextr   rt  r  rm  rH  r  r  r  )r/  rZ  r  r  result_jsonr  r  r  extr  fhrt  r  s                rR   
speak_textr    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}	n|	                                }|du }|j
        }	|k	 |                    d
           n# t          $ r Y nw xY w||	dt          | <   t                              | d           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  r   r4   r  r  rr  r  r  r  r*  r  waitrW   r  r  )
rO   r  r  r  tailrf  r  r  r  r  s
             rR   get_action_statusr    s      &))$//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)))  t,,,   s   6D 
DDz/api/sessionsexcludecreatedr  offsetmin_messagesarchivedorderr   exclude_sources
cwd_prefixc	                   K   |dvrt          dd          |dvrt          dd          d}	|rt          |          \  }	}
	 t          |          }	 t          d|          }|d	k    }|d
k    }d |pd                    d          D             }|                    |pd|pd|pd| |||||dk    	  	        }|                    |pd|pd|pd|||d          }t          j                    }|D ]}|                    d          du o0||                    d|                    dd                    z
  dk     |d<   |	r|	|d<   |	dk    |d<   t          |                    d                    |d<   ||| |d|
                                 S # |
                                 w xY w# t           $ r  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.
    r  onlyincluder   /archived must be one of: exclude, only, includer   r  recent%order must be one of: created, recentNr   r  r  c                 :    g | ]}|                                 |S r  r   r   r   s     rR   r  z get_sessions.<locals>.<listcomp>3  s%    WWW!QWWYYWAWWWrT   r   r	  r  )	r   r  r  r  r  min_message_countinclude_archivedarchived_onlyorder_by_last_activeT)r   r  r  r  r  r  exclude_childrenr  r  r  r  	is_activer|  r  is_default_profiler  )r.  rL  r  r  zGET /api/sessions failedr  Internal server error)r4   _cron_profile_home_open_session_db_for_profiler  r   r  session_countr   r   r   r   rW   rM   r  )r  r  r  r  r  r   r  r  r|  profile_namero  r-  r  r  r  exclude_listr.  rL  r  r   s                       rR   get_sessionsr    s     0 555D
 
 
 	
 ))):
 
 
 	
 #'L 6,W55a1M)'22*	 #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 +   H#/AiL.:i.GA*+ $QUU:%6%6 7 7* (55TZ[[HHJJJJBHHJJJJ    M M M12224KLLLLMs&   F' D%F 9F' F$$F' 'A G'z/api/profiles/sessionsr  allc                     |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   r  r  r   r  profilesr  c                 *    g | ]}|j         |j        fS r  )rO   r   )r   rN   s     rR   r  z)get_profiles_sessions.<locals>.<listcomp>  s!    @@@$	49-@@@rT   z0GET /api/profiles/sessions: list_profiles failedr  r  r  Nc                 :    g | ]}|                                 |S r  r   r  s     rR   r  z)get_profiles_sessions.<locals>.<listcomp>  s%    OOO!QWWYYOAOOOrT   r   r	  r  state.dbT)db_path	read_only)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  c                 \    |                                p|                      d          pdS )Nr  r   r   )r   sort_keys    rR   r  z'get_profiles_sessions.<locals>.<lambda>  s'    aeeHooI|1D1DI rT   rv  reverse)r.  rL  profile_totalsr  r  rk  )r4   r  r  rX   r  r  r  list_profilesrW   rM   r  get_profile_dirr  r   rr  r   r   r  r  r  r  r   r   r   r  )!r  r  r  r  r  r|  r   r  r  profiles_modtargetsrO   r  infosr  r  r  source_filterr  per_profilemergedrL  r  rk  r  r  r-  r  r^  profile_totalr   windowr  s!                                   @rR   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 &B76B7F!!
G+%GGC"K L
K:%K50L5K::LL)z/api/sessions/searchqc                 |  K   | r|                                  sdg iS 	 t          |          	 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*          $ r, t,                              d#           t)          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   rU  d   
session_idrG   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_reasonrX  )rv   addr  get_sessionrW   rd  rg  r   )r  chaincurvisitedr  r   r  parent_sessionparent_ended_atr  is_compression_edgerZ  r-  
root_caches               rR   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 r8  )get_compression_tiprW   )r  tipr  r-  	tip_caches      rR   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   rg  )r  r/  r  r  r  
safe_limitseens      rR   add_lineage_resultz+search_sessions.<locals>.add_lineage_result3  st     F''004<<3t99
#:#:Fw--(3D(9(9%*.'$T


rT   Tr  r  r  previewr   zSession ID: r   r  r  )snippetroler   r  session_startedr   z"[^"]*"|\S+"r   rp  r   r  )queryr  r  r  r  zGET /api/sessions/search failedr  zSearch failedr   )r   r  r  rr  re  r  rg  search_sessions_by_idr   r3  findallr   r  r  r  search_messagesr   rb  r  r   r4   rW   rM   r  )r  r  r|  r  rowsidr  r  r3  termsr   prefix_queryfetch_limitmatchesmr  r-  r  r  r  r  r  s                  @@@@@@@rR   search_sessionsr	    s       AGGII 2SE)'22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; "K88K; ;A L;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   r   r
  )rg  r   rd  re  )rj  	model_valctx_lens      rR   _normalize_config_for_webr  x  s     &\\F

7##I)T"" +-- 0!44#--	9==3L3LMMw4>w4L4L)SRS%&&)*%&MrT   c                 4    t                      | j        z  dz  S )Nzconfig.json)r   rO   rQ   s    rR   _memory_provider_config_pathr    s    x},}<<rT   c                 0   t          |           }|                                si S 	 t          j        |                    d                    }n/# t
          $ r" t                              d|d           i cY S w xY wt          |t                    r|ni S )Nri  r|  z-Failed to read memory provider config from %sTr  )
r  r  r  r  r  rW   rM   r  rd  rg  )rQ   r   r{  s      rR   _read_memory_provider_filer    s    '11D;;== 	z$..'.::;;   DdUYZZZ			 dD))144r1s   (A )A<;A<fieldc                    | j         g| j        R D ]*}|                    |          }|rt          |          c S +t	                      }| j        D ]*}|                    |          }|rt          |          c S +| j        S )zGResolve the stored value for a non-secret field, honoring legacy reads.)rv  aliasesr   r  r   env_fallbacksr  )r  r{  
source_keyr_  env_on_diskenv_keys         rR   _read_field_valuer    s     y15=11  
$$ 	u::	 **K&  (( 	u::	 =rT   c                     t                      }| j        g| j        R D ]}|r|                    |          r dS t	          fd| j        g| j        R D                       S )zEWhether a secret field has a value anywhere it may have been written.Tc              3   B   K   | ]}                     |          V  d S r8  r   )r   r  r{  s     rR   r   z _field_is_set.<locals>.<genexpr>  s/      RR
txx
##RRRRRRrT   )r   r  r  r   r<  rv  r  )r  r{  r  r  s    `  rR   _field_is_setr    s     **KM8E$788   	{w// 	44RRRRuy6Q5=6Q6QRRRRRRrT   c                    t          |           }g }| j        D ]}|j        |j        |j        |j        |j        d |j        D             d}|j        rd|d<   t          ||          |d<   nOt          ||          }|j        dk    r||                                vr|j        }||d<   t          |          |d<   |                    |           | j        | j        |dS )Nc                 8    g | ]}|j         |j        |j        d S )r_  r  r  r   )r   opts     rR   r  z,_memory_provider_payload.<locals>.<listcomp>  s8        )ciXX  rT   )rv  r  kindr  placeholderr  r   r_  is_setr  rO   r  fields)r  r&  rv  r  r"  r  r#  r  	is_secretr  r  allowed_valuesr  r   r  rO   )rQ   r{  r&  r  rx  r_  s         rR   _memory_provider_payloadr)    s   %h//D#%F  9[J , ,  =  
!
 
!
 ? 		*E'N+E488E(OO%eT22EzX%%%u7K7K7M7M*M*M"E'N"5kkE(OeMHNfMMMrT   ro  c                     |pd                                 }| j        dk    r9|s| j        }||                                 vrt	          d| j         d          |S |p| j        S )zIValidate and normalize a submitted non-secret value, or raise ValueError.r   r  zInvalid value for '')r   r"  r  r(  rg  rv  )r  ro  r_  s      rR   _coerce_field_valuer,    sz     YBEzX 	"ME,,....?59???@@@!EM!rT   z#/api/memory/providers/{name}/configc                 R   K   t          |           }|| | g dS t          |          S )Nr%  )r)   r)  )rO   rQ   s     rR   get_memory_provider_configr.    s9      "4((H tr:::#H---rT   c                 p  K   t          |           }|t          dd|            |j        pi }	 t          |          }i }i }|j        D ]}|j        rB|                    |j                  pd                                }|r|j	        r
|||j	        <   K|j        |v r||j                 n,t          |                    |j        |j                            }	t          ||	          ||j        <   t                      }
|
                    d          }t          |t                    si }||
d<   |j        |d<   t#          |
           t%          |          }|j                            dd           |                    |           d	d
lm}  |||d            |j                    D ]\  }}t3          ||           ddiS # t          $ r  t4          $ r$}t          dt          |                    |d }~wt6          $ r- t8                              d|            t          dd          w xY w)Nr   zUnknown memory provider: r   r   rW  rQ   Tr  r   )atomic_json_writei  )moder7  r   z*PUT /api/memory/providers/%s/config failedr  r  )r)   r4   r  r  r&  r'  r   rv  r   r  r  r  r,  r   rd  rg  rO   r   r  r  r  rr  utilsr0  rq  r   rg  rW   rM   r  )rO   r  rQ   r  r+  json_valuessecretsr  	submittedro  rj  memory_configr   r0  r  secretr  s                    rR   update_memory_provider_configr8    s     "4((H4VPT4V4VWWWW[BF,M-h77&("$_ 	E 	EE #ZZ	228b??AA	 7 7-6GEM* 9&& uy!!ei??@@ 
 &9%D%DK	""

8,,-.. 	-M,F8$,Mj!F+H55$666$$$++++++$u5555,w} 	, 	,OGV7F++++d|    G G GCHH===3F M M MCTJJJ4KLLLLMs   FG H5G;;:H5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 ro  r   r   kvs      rR   r  zget_config.<locals>.<dictcomp>.  s/    EEETQ1<<3D3DEAqEEErT   )r  r  r   rq  )r|  rj  s     rR   
get_configr@  )  s      		 	  : :*;==99: : : : : : : : : : : : : : : FEV\\^^EEEEs   :>>z/api/config/defaultsc                     K   t           S r8  )r   r  rT   rR   get_defaultsrB  1  s      rT   z/api/config/schemac                  $   K   t           t          dS )N)r&  category_order)CONFIG_SCHEMA_CATEGORY_ORDERr  rT   rR   
get_schemarG  6  s      #GGGrT   r  rQ   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  r   r  rO   rQ   r  r  r  r   )get_model_context_length)r  r  rQ   rJ  get_model_capabilitiesrQ   r  supports_toolssupports_visionsupports_reasoningcontext_windowmax_output_tokensmodel_familyrH  zGET /api/model/info failed)r  r   r   rd  rg  r  rM  agent.model_metadatarO  rW   re  agent.models_devrQ  rT  rU  rV  rW  rX  rY  r4   rM   r  )r|  r  r  
model_namerQ   r  
config_ctxrO  auto_ctxconfig_ctx_inteffective_ctxcapsrQ  mcs                 rR   get_model_inforc  E  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_extractrX  
skills_hubapprovalmcptitle_generationtriage_specifierkanban_decomposerprofile_describerrj  _AUX_TASK_SLOTSz/api/model/optionsrefreshc                 V   	 ddl m}m} t          |           5   | |            dddddt	          |          t	          |                    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)
u  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.

    ``refresh`` busts the per-provider model-id disk cache so every row
    re-fetches its live catalog — used by the picker's explicit "Refresh
    Models" control. Normal opens leave it false to stay on the 1h cache.
    r   build_models_payloadload_picker_contextT)include_unconfiguredpicker_hintscanonical_orderpricingrL  rn  probe_custom_providersNzGET /api/model/options failedr  zFailed to list model optionsr   )	hermes_cli.inventoryrq  rr  r  r   r4   rW   rM   r  )r|  rn  rq  rr  s       rR   get_model_optionsry    s3   "TRRRRRRRR G$$ 
	 
	''##%%%)! $!W'+G}}	 	 	
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	     T T T67774RSSSSTs/   A( 5A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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).
    r   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)rQ   r  r  z0GET /api/model/recommended-default (nous) failedNrp  	providersslugmodelsz)GET /api/model/recommended-default failed)r   r   r  r|  r}  r~  r  r  r  r  r  r   rW   r   rM   r  rx  rq  rr  r  )rQ   r  r|  r}  r~  r  r  r  r  	model_idsrv  r  r  rh   _unavailabler  rq  rr  r/  r  r  s                        rR   get_recommended_default_modelr    s-    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&&':':'<'<==;;{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 *G10G1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^  rQ   r8  r  r   r  )r  rQ   r  r  r  rO   rR  )tasksmainzGET /api/model/auxiliary failedr  zFailed to read auxiliary configr   )r  r   r   rd  rg  rm  r  r  r4   rW   rM   r  )r|  r  aux_cfgr  slotslot_cfgr  r  s           rR   get_auxiliary_modelsr  $  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/moac                 n   	 ddl m} t          |           5  t                      } |t	          |t
                    r|                    d          ni           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)	z=Return the configured Mixture-of-Agents provider/model slots.r   normalize_moa_configmoaNzGET /api/model/moa failedr  zFailed to read MoA configr   )hermes_cli.moa_configr  r  r   rd  rg  r   r4   rW   rM   r  )r|  r  r  s      rR   get_moa_modelsr  W  s2   
Q>>>>>>G$$ 	Y 	Y--C''*S$:O:O(WUWXX	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y 	Y     Q Q Q23334OPPPPQs0   A4 AA'A4 'A++A4 .A+/A4 4A B4c                 T   	 ddl m} t          | j        p|          5  t	                      }| j        r2| j        | j        d | j                                        D             d}nCd | j	        D             | j
                                        | j        | j        | j        | j        d} ||          }||d<   t!          |           dd	i|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)z3Persist the Mixture-of-Agents provider/model slots.r   r  c           
          i | ]I\  }}|d  |j         D             |j                                        |j        |j        |j        |j        dJS )c                 6    g | ]}|                                 S r  rg  r   r  s     rR   r  z-set_moa_models.<locals>.<dictcomp>.<listcomp>u  s     0a0a0a0a0a0arT   r  r  r  r  r  r  )r  r  rg  r  r  r  r  )r   rO   presets      rR   r  z"set_moa_models.<locals>.<dictcomp>s  su     
  
  
  )D& 0a0aI`0a0a0a*0*;*@*@*B*B5;5Q6<6S*0*;'-~ 
  
  
 rT   )r  r  r  c                 6    g | ]}|                                 S r  r  r  s     rR   r  z"set_moa_models.<locals>.<listcomp>  s     (W(W(W(W(W(WrT   r  r  r7  TNzPUT /api/model/moa failedr  zFailed to save MoA configr   )r  r  r  r|  r   r  r  r  rq  r  r  rg  r  r  r  r  r   r4   rW   rM   r  )r  r|  r  r  ro  r  s         rR   set_moa_modelsr  g  s   &Q>>>>>>DL3G44 	. 	.--C| &*&9%)%7
  
  -1L,>,>,@,@
  
  
  " )X(WAV(W(W(W"&/"6"6"8"8-1-G.2.I"&/#|  .-c22J#CJ$-*-=	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	.>     Q Q Q23334OPPPPQs0   C' B/CC' CC' !C"C' 'A D'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.
    r   >   r  r^  r   z#scope must be 'main' or 'auxiliary'r   r   )expensive_model_warning)rQ   r  NFT)r7  r  rQ   r  confirm_requiredconfirm_messagec            	          t          j        p          5  t                     cd d d            S # 1 swxY w Y   d S r8  )r  r|  _apply_model_assignment_sync)r  r  r  r  r|  rQ   r  r  s   rR   _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)r  r   r   rQ   r  r  r  r  r4   r  hermes_cli.model_cost_guardr  rj   r  rW   r  rM   r  )r  r|  r  r  r  r  r  r  rQ   r  r  s   ``   @@@@@@rR   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r  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    rt*          D ]b}|                    |          }t'          |t(                    si }d|d<   d|d<   |                    dd           t3          |           |||<   c||d<   t          |           ddddS |st          dd           |r|gnt5          t*                    }|D ]}|t*          vrt          dd!|           |                    |          }t'          |t(                    si }t-          |                    d          pd                                                                          }|                                                                }||d<   ||d<   ||k    r+|d"k    r%|                    dd           t3          |           |||<   ||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  r1  )_auto_provider_name_save_custom_providerr  z%custom_providers registration skippedr^  rQ   r   >   r   r8  )r  rQ   r  r  )r7  r  rQ   r  r  gateway_tools	stale_aux	__reset__r8  N)r7  r  resetzprovider required for auxiliaryzunknown auxiliary task: r1  )r7  r  r  rQ   r  )r   r4   r  r  r   r   r   r  r  hermes_cli.tools_configr  sortedrW   rM   r  r   hermes_cli.mainr  r  rd  rg  rm  r  r  r  r   rb  )r  rQ   r  r  r  r  r  r  r  r  r  r  changedr  r  r  r  r  r  r  slot_providerauxr  r  s                           rR   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LLT***,X666 CIIKC[4@@@ W4UVVVV7tff$"7"7G  &&C8YSW8Y8YZZZZ774==(D)) 	HHLL44:;;AACCIIKK~~''--//'!=((\X-E-ELLT***,X666D		C  s$   7C &C87C85 E &E?>E?r  r  c                 <   | pd                                 }|sd|fS 	 ddlm}m}m} n# t
          $ r d|fcY S w xY w	  |||          }n# t
          $ r d}Y nw xY w|r|d         |d         fS d|v r'	  ||          |v }n# t
          $ r d}Y nw xY w|sd|fS d|fS )	u  Infer which provider serves ``model_val`` when the flat Config-page Model
    field changes, given the previously-saved ``prev_provider``.

    Returns ``(provider, model)``; ``provider`` is empty when no switch is
    warranted (leave the existing provider untouched). Two signals, in order:

    1. Curated-catalog detection (``detect_provider_for_model``) — handles the
       ~28 OpenRouter-curated models and direct provider-static catalogs.
    2. Vendor-slug heuristic — a ``vendor/model`` slug cannot belong to a
       single-model / non-aggregator provider (e.g. ``ollama-local``). When the
       current provider is not an aggregator that serves vendor-prefixed slugs,
       route to an aggregator. ``_normalize_main_model_assignment`` (called by
       the caller) keeps the user's current aggregator when they're already on
       one, else falls back to openrouter — the same chokepoint logic as
       ``POST /api/model/set``.
    r   r   )r  detect_provider_for_modelr  Nr   r   Fr  )r   r  r  r  r  rW   )r  r  rO   r  r  r  detectedcur_is_aggregators           rR   _infer_provider_on_model_changer  s  sU   " O""$$D 4x	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	

    4x,,T=AA    ({HQK''
 d{{	& 2 2= A AEZ Z 	& 	& 	& %	&  	&%%t8Os/   
) ::A AA4B BBc                 8   t          |           } |                     dd           |                     dd          }t          |t                    s*	 t          |          }n# t          t
          f$ r d}Y nw xY w|                     d          }t          |t                    rt|rq	 t                      }|                    d          }t          |t                     rt          |                    d          pd          	                                }t          |                    d          pd          	                                }||k    ry|rwt          ||          \  }}|rb|	                                                                |                                k    r&t          ||          \  }	}
t          ||	|
          }|
}||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   rQ   r  )r  r  )rg  r  rd  re  	TypeErrorrg  r   r  r   r   r  r   r  r  rW   )rj  ctx_overrider  disk_config
disk_modelprev_defaultr  r  resolved_modelnorm_provider
norm_models              rR   _denormalize_config_from_webr    sh    &\\F JJ}d### ::4a88LlC(( 	|,,LL:& 	 	 	LLL	 

7##I)S!! /i /-	%--K$11J*d++ (":>>)#<#<#BCCIIKK #JNN:$>$>$D" E E K K M M ,,,3R!=4 40L. $ /(:(:(<(<(B(B(D(DH[H[H]H](](]
 5U(.5 51z &B&z& &
 %/	(1
9%!##3?J/00NN#3T:::",w !!(&2# #w  	 	 	D	Ms%   A" "A87A8*EH
 

HHc                 p  K   	 t          | j        p|          5  t                      }t          | j                  }t          t          ||                     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)Nr7  TzPUT /api/config failedr  r  r   )r  r|  r   r  rj  r   r&   r4   rW   rM   r  )r  r|  r+  incomings       rR   update_configr    s     MDL3G44 		9 		9 '((H3DK@@HHh77888		9 		9 		9 		9 		9 		9 		9 		9 		9 		9 		9 		9 		9 		9 		9 d|    M M M/0004KLLLLMs0   A5 A A&A5 &A**A5 -A*.A5 5A B5c                     	 ddl m}  n# t          $ r i cY S w xY w	 ddlm} n# t          $ r i }Y nw xY wd |                                D             }i } |             D ]}|j        dk    r|j        D ];}||v r|                    ||j	        |j
        |j        |j        pdddd	d
           <|j        r5|                    |j        |j	        |j
        |j
         ddddd	d
           |j        dk    rydD ]v}|                    |i           }|j	        |j
        |                    d          p|j
         d| d|                    d          d|                    dd          d	d
||<   w|j        dk    rq|                    di           }|j	        |j
        |                    d          p	|j
         d|                    d          d|                    dd          d	d
|d<   |S )u  Map provider env vars → desktop card metadata, derived from the catalog.

    Returns ``{env_var: {provider, provider_label, description, url, is_password,
    advanced}}`` for every API-key provider in the unified ``provider_catalog()``
    (i.e. the ``hermes model`` universe). This is what lets the desktop Keys tab
    render a card for a provider even when its env var was never hand-added to
    ``OPTIONAL_ENV_VARS`` — closing the drift where CLI-configurable providers
    (openai-api, kilocode, novita, tencent-tokenhub, copilot, …) were missing
    from the GUI.

    Hand ``OPTIONAL_ENV_VARS`` prose is layered ON TOP of this in the endpoint;
    this only supplies membership + grouping + sensible fallbacks.
    r   provider_catalog)r   c                 |    h | ]9\  }}|pi                      d           r|pi                      d           dk    7|:S )r	  rQ   r   r=  s      rR   r  z1_catalog_provider_env_metadata.<locals>.<setcomp>"  sY       aG==$$*+'rz)B)Bj)P)P 	
)P)P)PrT   keysNTFrQ   )rQ   provider_labelr  r   is_passwordadvancedr	  z base URL overrideaws_sdk)
AWS_REGIONAWS_PROFILEr  r|  r}  r   r  vertexVERTEX_CREDENTIALS_PATHu+    — service account JSON path (or use ADC))hermes_cli.provider_catalogr  rW   hermes_cli.configr   rq  tabapi_key_env_vars
setdefaultr  r  r  
signup_urlbase_url_env_var	auth_typer   )r  _OPT_non_provider_keysmetadenv_varaws_varr+  s           rR   _catalog_provider_env_metadatar    s   @@@@@@@   			???????    jjll  
 D F F5F?? ) 	 	G,,,OO !&'g#$=</4#' % *      	OO" !&'g&'g#A#A#A#( $ *   $ ;)##8 
 
88GR00 !&'g#+<<#>#>#ZQWBZBZPWBZBZBZ#<<..#( (Z > > *! !W  ;(""xx 92>>HF"#''||M::  KgJJJ||E**$$LLT::&	/ 	/D*+ Ks   	 # 22z/api/envc           	        K   t          |           5  t                      d d d            n# 1 swxY w Y   t                      t                      dddt          dt
          dt          dt
          ffd}i }t          j                    D ]\  }} |||          ||<   D ]}||vr ||i           ||<   D ](}||v s|v r ||i d          }d|d	<   d|d
<   |||<   )|S )NF)r1  var_namerN   r1  rG   c                                        |           }                     |           pi }t          |          |rt          |          nd |                     d          p|                     dd          |                     d          |                     d          n|                     d          |                     d          p|                     dd          |                     d|                     dd                    |                     dg           |                     d	|                     d	d                    | v |                     d
d          |                     dd          |dS )Nr  r   r   r	  passwordr  Ftoolsr  rQ   r  )r$  redacted_valuer  r   r	  r  r  r  channel_managedrQ   r  r1  )r   r   r$   )r  rN   r1  r_  cat_metacatalog_metachannel_keysr  s        rR   _rowzget_env_vars.<locals>._rowy  sP   ))##H--3 5kk38Bj///d88M22Uhll=RT6U6U&*hhuoo&A488E???x||TYGZGZ,,LZ0L0L88J]E0R0RSSXXgr**X\\*e-L-LMM  (<7 !Z44&ll+;R@@ /
 
 	
rT   Tr	  r  )	r  r   _channel_managed_env_keysr  r  rg  r   r   rq  )	r|  r  r  r  rN   r  r  r  r  s	         @@@rR   get_env_varsr  r  s     		 	  ! !jj! ! ! ! ! ! ! ! ! ! ! ! ! ! !,..L133L:? 
 
 
s 
$ 
4 
D 
 
 
 
 
 
 
 
> F+133 0 0$4$//x ! 2 26!!#tHb11F8    v\!9!9d8R---"J!MxMs   044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r7  rv  r   r   zPUT /api/env failedr  r  )r  r|  r   rv  r_  rg  r4   r  rW   rM   r  )r  r|  r  s      rR   set_env_varr    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/modelsr  )zhttps://api.x.ai/v1/modelsr  )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  r   )

is_successr  rW   rd  rg  r   rb  r  r   r  )r  r/  r{  idsr  mids         rR   _parse_model_idsr    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   Nr   FTzEnter a value first.)r7  	reachabler  OPENAI_BASE_URLr   z/modelsAuthorizationr   g       @r  r  )r7  r	  r  r  zCould not reach rm  r  r  r  rv        $@)r   paramsz/Could not reach the provider to verify the key.r  z9That API key was rejected. Double-check it and try again.  zProvider returned HTTP z for this key.)r   httpxrv  r   r_  r  r  ClientTimeoutr   r  rW   r  r   r  )r  r   r  rv  r_  r   r  r   r  r  prober   r  s                rR   validate_provider_credentialr    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   Tr  r   zDELETE /api/env failedr  r  )
r  r|  r   rv  r4   rg  r  rW   rM   r  )r  r|  removedr  s       rR   remove_env_varr  (  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 r  r  )r   tcutoffs     rR   r  z"reveal_env_var.<locals>.<listcomp>M  s    III1a&jjQjjjrT   Nr  z,Too many reveal requests. Try again shortly.r   r   r  zenv/reveal: %s)rv  r_  )r   r   _REVEAL_WINDOW_SECONDSr   r   _REVEAL_MAX_PER_WINDOWr4   r  r  r|  r   r   rv  rM   rN   )r  r   r|  r  r  r_  r  s         @rR   reveal_env_varr  <  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
Telegramz1Run Hermes from Telegram DMs, groups, and topics.z1https://core.telegram.org/bots/features#botfather)TELEGRAM_BOT_TOKENTELEGRAM_ALLOWED_USERSTELEGRAM_PROXY)r   )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'  r  SlackzbUse Hermes from Slack via Socket Mode. Add allowed Slack member IDs so connected bots can respond.zhttps://api.slack.com/apps)r  r  r  )r  r  
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)r2  r3  r4  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)r8  r9  whatsappWhatsAppzBUse Hermes through the bundled WhatsApp bridge with QR-based auth.z"https://github.com/tulir/whatsmeow)WHATSAPP_ENABLEDWHATSAPP_MODEWHATSAPP_ALLOWED_USERSr  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)rQ  rR  google_chatzGoogle Chatz0Connect Hermes to Google Chat via Cloud Pub/Sub.zKhttps://hermes-agent.nousresearch.com/docs/user-guide/messaging/google_chat)rO   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)rW  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)rZ  r[  r\  weixinzWeixin / WeChat (Personal)zBConnect a personal WeChat account through Tencent's iLink Bot API.zGhttps://hermes-agent.nousresearch.com/docs/user-guide/messaging/weixin/)WEIXIN_ACCOUNT_IDWEIXIN_TOKENWEIXIN_BASE_URL)r`  ra  bluebubbleszBlueBubbles (iMessage)z5Use Hermes through iMessage via a BlueBubbles server.zhttps://bluebubbles.app/)BLUEBUBBLES_SERVER_URLBLUEBUBBLES_PASSWORDBLUEBUBBLES_ALLOWED_USERS)rd  re  z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)rg  rh  r#  zEhttps://hermes-agent.nousresearch.com/docs/user-guide/messaging/teamsu   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)qqbotteamsyuanbao
api_serverwebhook_PLATFORM_OVERRIDES)rQ  rR  r  r+  r0  r;  r6  rc  r@  rC  rI  rL  rP  rU  rV  rY  r_  rs  ru  rv  rw  _PLATFORM_ORDERr8  z8signal-cli REST API base URL, e.g. http://127.0.0.1:8080zSignal bridge URL)r  rA   r   r9  z6Signal account phone number registered with the bridgezSignal account)r  rA   r:  z3Comma-separated Signal users allowed to use the botzAllowed Signal usersr=  z#Enable the WhatsApp gateway adapterzEnable WhatsApp)r  rA   r  r>  zWhatsApp bridge modezWhatsApp moder?  z5Comma-separated WhatsApp users allowed to use the botzAllowed WhatsApp usersrA  z>Home Assistant base URL, e.g. https://homeassistant.local:8123zHome Assistant URLrB  uB   Long-lived access token from Home Assistant (Profile → Security)zHome Assistant access token)r  rA   r  rE  z&Email address to send and receive fromzEmail addressrF  z&Email account password or app passwordzEmail passwordrG  z&IMAP server host (e.g. imap.gmail.com)z	IMAP hostrH  z&SMTP server host (e.g. smtp.gmail.com)z	SMTP hostrJ  zTwilio Account SIDrK  zTwilio Auth TokenrW  zWeCom group bot IDzWeCom Bot IDrX  zWeCom group bot secretzWeCom SecretrZ  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 KeyzFiLink Bot account ID obtained through QR login in hermes gateway setupziLink Bot account IDzAiLink Bot token obtained through QR login in hermes gateway setupziLink Bot tokenzMiLink API base URL saved by QR login (default: https://ilinkai.weixin.qq.com)ziLink API 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`  ra  rb  rQ  rR  rS  rT  rN  rO  _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 r  r  )r   idxr  s      rR   r  z/_messaging_platform_catalog.<locals>.<dictcomp>  s    AAA(#sS#AAArT   c                                          | d         t          t                              | d                                         fS )Nr  rO   )r   r   ry  r   )er  s    rR   r  z-_messaging_platform_catalog.<locals>.<lambda>  s4    uyy4#o*>*>??6ARARS rT   r  )r  r|  rv   __members__r  r_  r  r  _build_catalog_entrygateway.platform_registryr}  plugin_entriesrO   rW   rM   r  	enumeratery  r  r  )r|  r  r  memberr}  plugin_entryr  s         @rR   _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$  r  z+could not build channel-managed env key setTr  )rv   r  rr  r   	frozensetrW   rM   r  )r  rx  s     rR   r  r    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_USERSc                     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_rX  )WECOM_CALLBACK_)rC  r@  rs  rI  rV  rY  -ro  )upperrt  )r  r  s     rR   _platform_env_prefixesr    sd     #"/.+ +G g{##''S11C799rT   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 r8  r<  )r   rk  rO   s     rR   r   z._discover_platform_env_vars.<locals>.<genexpr>%  s/      BBv4??6**BBBBBBrT   )
r  r   rq  r   _MESSAGING_KEYS_PAGE_KEYSr<  r  r  r  rv   )r  prefixesr  rN   rO   s       @rR   _discover_platform_env_varsr    s    %k22HD'-//  
d88J;..,,,BBBBBBBBB 	DD		""###rT   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  rg  fromkeysr%  )r  r  r  
discovereds       rR   _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rT   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%  r  rO   ro  rp  r  r   r#  )r  rO   r  r#  r$  r%  )	rx  r   r  r  r%  r  rt  r   install_hint)r  r  r  r$  r%  rO   r  s          rR   r  r  9  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$  rT   c                 H    t                      D ]}|d         | k    r|c S d S )Nr  )r  )r  rx  s     rR   _catalog_lookupr  \  s9    ,..  ;+%%LLL &4rT   c           	      b   t          j        |           pt                              |           pi }|                    dd          |                    d|           |                    dd          |                    d          |                    dd          |                    dd          d	S )
Nr  r   rA   helpr   r  Fr  )r  rA   r  r   r  r  )r   r   rz  )rv  rN   s     rR   _messaging_env_infor  c  s     %%P)A)E)Ec)J)JPbDxxr22((8S))$$xxxx
E22HHZ//  rT   c                 z    ddl m}m}  |            } ||           }|j                            |          }|||fS )Nr   )r|  r  )r  r|  r  r  r   )r  r|  r  rj  r  platform_configs         rR   _gateway_platform_configr  o  sZ    <<<<<<<<  ""Fx$$H&**844O8_,,rT   rx  r  r+  scopedc                    | d         }|r|                     d          ni }t          |t                    r|                     |i           ni }t                      d upt	          |          d u}g }| d         D ]}	                     |	          p|rdnt          j        |	d          }
|                    |	|	| d         v t          |
          |
rt          |
          nd dt          |	                     |r	 t                      }|                     d          pi }|                     |          }t          |t                    si }t          |                     d                    }|                     d          }t          |t                    r|nd }n# t          $ r d	}d }Y nw xY wt          fd
| d         D                       }n	 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 }t          |t                    r|                     d          nd }t          |t                    r|                     d          nd }|sd}n|sd}n|r|sd}n|s|s	|dk    rd}n|s|sd}t          |t                    r|                     d          nd }t          |t                    r|                     d          nd }|dk    r|pd}|p|}|| d         | d         | d         ||||||t          |t                    r|                     d          nd ||dS )Nr  r  r$  r   r%  )rv  requiredr$  r  r  home_channelFc              3   B   K   | ]}                     |          V  d S r8  r   r   rv  r  s     rR   r   z._messaging_platform_payload.<locals>.<genexpr>  s/      OO#--OOOOOOrT   c              3   l   K   | ].}                     |          pt          j        |d           V  /dS r   N)r   rq   rr   r  s     rR   r   z._messaging_platform_payload.<locals>.<genexpr>  sT         $$:	#r(:(:     rT   rh   r  r  disablednot_configuredpending_restartr  gateway_stopped
error_codeerror_messagerO   r  r#  r  )r  rO   r  r#  r  
configuredr  rh   r  r  r  r  r$  )r   rd  rg  r,   r-   rq   rr   r  r   r$   r  r   rW   r  r  r  _is_platform_connectedr  to_dict)rx  r  r+  r  r  runtime_platformsruntime_platformr  r$  rv  r_  r  platforms_cfgplat_cfgr  hcr  r  r)  r  r  rh   runtime_gateway_stateruntime_gateway_errorr  r  s    `                        rR   _messaging_platform_payloadr  x  s    +K4;CK000 '..	k2...  	% 	?)'22$>  HZ  
 

 $$Nv)M29S";M;M5#88u++7<"F*U"3"3"3$	 
 &c**	
 	
 	
 	
  ' 
	 --CGGK006BM$((55Hh-- 8<<	2233Gn--B!+B!5!5?224LL 	  	  	 GLLL	  OOOO~9NOOOOO

	 8P9 95NHo ?F/FGGG U"99(OTT J #'6'C,44666 L
  	  	  	 G     0    J  LLL	  *44Dd)K)KUW%%%QU 
 =GwPT<U<U_GKK888[_:DWd:S:S]GKK666Y] " " 	 	" 	"!"" "%555  "U "! &--	\***  &--	_--- 
    3#3
%>)> f]+*% * & *D11  ...$#  s&   >B!F   F10F1A2I
 
/I<;I<r  c                 (    t          | d|           d S )Nr  )r%   )r  r  s     rR   _write_platform_enabledr    s    Y@@@@@rT   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)	r}  r~  r  r  r  rf  r  r  r  r  rT   rR   r  r    sl         OOOOOO IsTz   #L#*### $M3:$$$$$rT   r  _telegram_onboarding_pairingsc                      t          j        dt                                                                        d          S )NTELEGRAM_ONBOARDING_URLr   )rq   rr    _TELEGRAM_ONBOARDING_DEFAULT_URLr   r  r  rT   rR   _telegram_onboarding_base_urlr    s,    
	+-MNN		rT   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  )	rt  r   fromisoformatr  r   utc	timestamprW   r   )r_  r  rp  s      rR   _parse_expiry_tsr    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 r  )r  )r   
pairing_idrecordr  s      rR   r  z7_prune_telegram_onboarding_pairings.<locals>.<listcomp>'  s5       J3&& 	&&&rT   )r   r  rq  r  )expiredr  r  s     @rR   #_prune_telegram_onboarding_pairingsr  %  sv    
)++C   "?"E"E"G"G  G
  < <
%))*d;;;;< <rT   c                     t          | pd                                          }t                              |          r|S d S )Nr   )r  r   _TELEGRAM_USER_ID_REr#  )r_  r  s     rR   _normalize_telegram_user_idr  0  s@    U[b!!''))J%%j11 4rT   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_foundr  claimedr  )telegram_manager_bot_token_not_configuredtelegram_token_fetch_failedr   )r^  r  s     rR   "_telegram_onboarding_error_messager  7  s4    I?KG5`'X  
c%rT   r  bearer_tokenr  r  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  r   r  r  r  r   r^  r  r   z)Telegram setup service returned an error.r   r  >   r  r    r   z9Telegram setup service is unavailable. Try again shortly.z4Telegram setup service returned an invalid response.)r  _TELEGRAM_ONBOARDING_USER_AGENTr  r  r  r   raise_for_statusHTTPStatusErrorr  r  rW   r  r   r  r   r4   RequestErrorrd  rg  )r  r   r  r  r  r   request_kwargsr   r  r  r  rp  r^  r   r   s                  rR   !_telegram_onboarding_request_syncr  B  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 )Nr  )rj   r  r  )r  r   r  r  s       rR   _telegram_onboarding_requestr    sO       ")!         rT   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 )Nr   POSTz/v1/telegram/pairingsr  r  r  r   r  r  	deep_link
qr_payloadsuggested_usernamer  7Telegram setup service returned an incomplete response.r   )r  r  r  )r  r  r  r   r  )r  r   r  r  r   r4   _telegram_onboarding_lockr  r  r  r  )	r  r  r/  r  r  r  r  r   r  s	            rR   start_telegram_onboardingr    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}r  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/r   )safe)r  r  waiting)r  r  r   r  r  r  r  >   r  r  r  z9Telegram setup is no longer available. Start a new setup.z2Telegram setup service returned an unknown status.)r  r  r  r   r4   r  r  r  r  r  r  r  rd  quoter  r   r  r  r  )
r  r  r  r/  r  r  r  r  r  r  s
             rR   get_telegram_onboarding_statusr    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  r2  r5  r6  s       rR   *_restart_gateway_after_telegram_onboardingr    s    
-g66ff 
 
 
QRRR$ XX
 
 	
 	
 	
 	
 	
 	

  
		MH	
 	
 	

  +x  r9  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   |j        p|}
	 t          |
          5  t          d	|           t          d
d                    |                     t!          dd           d d d            n# 1 swxY w Y   nt# t          $ r  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  r1  z Telegram setup is not ready yet.r   r!  r	  rQ  Tz Telegram onboarding apply failedr  zFailed to save Telegram setup.r0  )r7  r  r  needs_restart)rv   r  r  r4   r  r  r  r  r  r   r  r  r|  r  r   r  r  rg  r  rW   rM   r  r  r  )r  r  r|  r  r  raw_idr  r  r  r  effective_profiler  restart_results                rR   apply_telegram_onboardingr  $  s      55D' 	0 	0088
 	C    T!!HHZ   ##J/// 
?
 
 
 	

 
#  +---.22:>> 	Q    $	* 	9   	                /-.. 	6 	6/;;;3SXX>N5O5OPPP#J555	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6     G G GCHH===3F   9:::3
 
 
 	 
# < <%))*d;;;< < < < < < < < < < < < < < < @@QRRN $+,=>>	 
  sn   AC11C58C5E1 AE%E1 %E))E1 ,E)-E1 1G"F$$G"1,GG",HHHc                    K   t           5  t                              | d            d d d            n# 1 swxY w Y   ddiS )Nr7  T)r  r  r  )r  s    rR   cancel_telegram_onboardingr  i  s      	" < <%))*d;;;< < < < < < < < < < < < < < <$<s   266z/api/messaging/platformsc                 $  K   t          |           5 t                      t                      t          t	                                t          | d          fdt                      D             dcd d d            S # 1 swxY w Y   d S )NrP   c           	      :    g | ]}t          |d u          S )Nr  )r  )r   rx  r  r+  
scoped_dirs     rR   r  z+get_messaging_platforms.<locals>.<listcomp>|  sH         ,;
$8N    rT   )r  gateway_start_commandr  )r  r   r/   r  r   r  r  )r|  r  r+  r  s    @@@rR   get_messaging_platformsr  p  s       
	 	  
Jjj%''LNN++%=gw%O%O      9::	  	
 	

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   A#BB	B	z&/api/messaging/platforms/{platform_id}c           	         K   t          |           }|st          dd|            t          |d                   }	 t          |j        p|          5  |j        D ]1}||vrt          d| d|d                    t          |           2|j                                        D ]\\  }}||vrt          d| d|d                    |	                                }|r!t          | ||           t          ||           ]|j        t          | |j                   d d d            n# 1 swxY w Y   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)r7  r  z&PUT /api/messaging/platforms/%s failedr  r  )r  r4   rv   r  r|  r  r   r  rq  r   r'  r   r  r  rW   rM   r  )r  r  r|  rx  allowed_envrv  r_  trimmeds           rR   update_messaging_platformr     sO      K((E 
$P;$P$P
 
 
 	
 eJ'((KMDL3G44 	C 	C~ & &k))'$'"%OOfOO    !%%%%"hnn.. 	1 	1
Uk))'$'"%OOfOO     ++-- 11+sGLLL"3000|''T\BBB+	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C. 444    M M M?MMM4KLLLLMs1   D< CD, D< ,D00D< 3D04D< <AE=z+/api/messaging/platforms/{platform_id}/testc                   K   t          |           }|st          dd|            t          |          5 }t                      }t	          ||t                      |d u          }d d d            n# 1 swxY w Y   |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   r  r   r  r  rO   z2 is disabled. Enable it, then restart the gateway.Frh   )r7  rh   r  r  c                 >    g | ]}|d          
|d         |d         S )r  r$  rv  r  )r   r  s     rR   r  z+test_messaging_platform.<locals>.<listcomp>  sE     
 
 
Z 
 */x
%L
 
 
rT   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  r4   r  r   r  r/   r  r   )r  r|  rx  r  r  r/  r  missings           rR   test_messaging_platformr&    s>     K((E 
$P;$P$P
 
 
 	
 
	 	  
Jjj-; 3 5 5jPT>T
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 

 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  s   0A33A7:A7   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.
    r   z<entra-id-bearer>rm  r  r   r   u   …N)r  rd  r  r=  r   r   )r_  r(  r   s      rR   _truncate_tokenr*    s      r #z%55 #""E

A
axxAGGCLLA%%HHS!R 
1vvG899rT   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 (r}  	expiresAtrefreshTokenr  r   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_suffixr   r  Fr  r   )agent.anthropic_adapterr,  r-  r]   rW   r   r*  r   r  r9  r  KeyErrorr  r<  hermes_cli.env_loaderr=  rq   rr   )
r,  r-  hermes_credsenv_var_orderr9  r<  r=  varr_  ru  s
             rR   _anthropic_oauth_statusrE  
  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.jsonr0  r1  r2  Fr>  )r?  rG  rW   r   r*  r   )rG  credss     rR   _claude_code_only_statusrJ  T  s    HHHHHH,,..    
=)) 
'9,UYY}-E-EFF))K00!%eii&?&?!@!@
 
 	
 $///s    ""c                      dddddddS )u  Status for copilot-acp — credentials are owned by the Copilot CLI.

    There is no cheap programmatic credential probe for the ACP subprocess, so
    this is a read-only "managed by the Copilot CLI" card (like claude-code):
    Hermes never claims a login state it can't verify.
    Fcopilot_cliz!Managed by the GitHub Copilot CLINr2  r  r  rT   rR   _copilot_acp_statusrM  l  s%     ;"  rT   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+)zhermes auth add xai-oauthz@https://hermes-agent.nousresearch.com/docs/guides/xai-grok-oauthzcopilot-acpzGitHub Copilot (ACP)zcopilot /loginz"https://docs.github.com/en/copilotr:  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 |                    |           }t          |t                    r>d|v r9t          |                    d                    |                    d          p|                    d'          p| |                    d(          pU|                    d%          p@|                    d          p+|                    d)          p|                    d*          pd+t          |                    d          p|                    d                    |                    d          p|                    d          t          |                    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  rN  access_tokenaccess_expires_atr5  r2  rT  r   openai_codex	auth_modezOpenAI Codexr  last_refresh)r  r   r3  r4  r  r5  rc  rU  qwen_cliauth_store_pathzQwen CLIr  rW  minimax_oauthz	MiniMax (regionglobalr}  TrX  	xai_oauth
auth_storezxAI Grok OAuthrQ   r3  r  rO   r   )rW   r  rX   r   r  r   r   r*  get_codex_auth_statusget_qwen_auth_statusget_minimax_oauth_auth_statusget_xai_oauth_auth_statusget_auth_statusrd  rg  )r\  rS  r  hauthro  s        rR   _resolve_provider_statusrq    s   	99;; 	9 	9 	9!&Q88888888	9S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   ##K00c4   	[C%7%7!#''+"6"677''(++Qswwz/B/BQkGGN++ ww|,,ww011 wwz** wwv	
 !0GGN++Aswwy/A/A" " "ggl33Ssww?R7S7S%)#''2E*F*F%G%G  "  5 5 5"SVV444444445sU   	 
4/44B0S )B S 
B*S 5A-S #B5S E(S 
S)S$S)$S)c                     |                      d          dk    rdS |                      d          dk    rd}t          j        dk    rd| S |S dS )	ua  Shell command that clears an external provider's credentials.

    External providers store their credentials outside Hermes, so the disconnect
    API deliberately refuses them (we never delete files another CLI owns on the
    user's behalf via a silent API call). For the ones we know how to clear we
    instead hand the GUI a command it can *run in the embedded terminal* — the
    user sees exactly what executes, and Hermes then stops resolving the token.

    Claude Code has no scriptable logout (only the interactive ``/logout``), so
    we remove the credential the same way logout does: the macOS Keychain entry
    (``Claude Code-credentials``) and/or the ``~/.claude/.credentials.json``
    file — the two sources ``read_claude_code_credentials()`` consults. Returns
    None for providers we can't safely clear (the GUI shows a manual hint).
    rQ  rV  Nr  rZ  z!rm -f ~/.claude/.credentials.jsondarwinzKsecurity delete-generic-password -s "Claude Code-credentials" 2>/dev/null; )r   r  r  )rQ   rm_files     rR   "_oauth_provider_disconnect_commandru  7  sb     ||Fz))t||D]**5<8##jahjjj4rT   r  c                     |                      d          dk    rt          |           rdS dS |                     d          dk    rdS dS )	zJReturn the manual disconnect path when the API cannot clear this provider.rQ  rV  uC   Managed outside Hermes — run the disconnect command to remove it.z0Managed by that provider's CLI; remove it there.r   r  u2   Remove the API key from Settings → Keys instead.N)r   ru  )rQ   r  s     rR   _oauth_provider_disconnect_hintrw  P  s[    ||Fz))-h77 	Y YXAAzz(y((CC4rT   c            
         g } t                      }t          D ]J}|d         |v r|                    |d                    |                     t	          |                     K	 ddlm}  |            D ]f}|j        dk    s	|j        |v r|                    |j                   |                     |j        |j	        dd|j         |j
        pddd	           gn# t          $ r Y nw xY w| S )
u  Build the Accounts-tab provider list.

    MEMBERSHIP is the union of:
      1. ``_OAUTH_PROVIDER_CATALOG`` — the explicit, hand-tuned cards that carry
         bespoke flow / status_fn / cli_command (including the api-key Anthropic
         PKCE card and the synthetic claude-code subscription row, which are not
         catalog providers), and
      2. every accounts-tab provider in the unified ``provider_catalog()`` (the
         ``hermes model`` universe) — so any OAuth/external provider added as a
         plugin appears automatically, with sensible defaults, even if no
         explicit card was written for it.

    The explicit catalog wins on metadata; the unified catalog guarantees we
    never silently drop a provider the CLI picker offers. Order: explicit cards
    first (their curated order), then any catalog-only providers appended in
    ``hermes model`` order.
    r  r   r  accountsrV  zhermes auth add r   NrP  )rv   r[  r  r  rg  r  r  r  r  r  r  rW   )r^  r  rx  r  r  s        rR   _build_oauth_catalogrz  ]  s=   $ "$DUUD ) ! !;$tDKK    @@@@@@!!## 	 	Au
""afnnHHQVKKf":!&::L.B!     		     Ks   $A7C 
C)(C)z/api/providers/oauthc                   K   t          |           5  g }t                      D ]}t          |d         |                    d                    }t	          ||          }|                    |d         |d         |d         |d         |d         |t          |          |du |d	           d	|icddd           S # 1 swxY w Y   dS )
uz  Enumerate every OAuth-capable LLM provider with current status.

    Response shape (per provider):
        id              stable identifier (used in DELETE path)
        name            human label
        flow            "pkce" | "device_code" | "external"
        cli_command     fallback CLI command for users to run manually
        disconnect_command  shell command that clears an external provider's
                            creds (run in the embedded terminal), else null
        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

    Membership is derived from the unified provider_catalog() so this stays in
    sync with the `hermes model` picker; _OAUTH_OVERRIDES supplies per-provider
    flow/status/cli metadata.
    r  rS  rO   rQ  rR  r#  N)	r  rO   rQ  rR  r#  disconnect_hintdisconnect_commanddisconnectabler  r  )r  rz  rq  r   rw  r  ru  )r|  r  r   r  r|  s        rR   list_oauth_providersr    s0     0 
	 	  ( (	%'' 	 	A-agquu[7I7IJJF=aHHOg&	&	 /jM#2&H&K&K"1T"9 
 
 
 
 
 
 Y'!( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (s   BB;;B?B?z"/api/providers/oauth/{provider_id}c                   K   t          |           t          |          5  d t                      D             }|                    |           }|7t	          dd|  dd                    t          |                               t          |i           }|rt	          d|d          d	|           t          | |                    d
                    }t          ||          }|rt	          d|d          d	|           | dk    rd}	 ddl	m
} |                                r|                                 d}n# t          $ r Y nw xY w	 ddlm}	  |	d          p|}n# t          $ r Y nw xY wt                               d|            t%          |          | dcddd           S 	 ddlm}	m}
  |	|           }| dk    r
 |
             t                               d| |           t%          |          | dcddd           S # t          $ r>}t                               d|            t	          dt+          |                    d}~ww xY w# 1 swxY w Y   dS )zDDisconnect an OAuth provider. Token-protected (matches /env/reveal).c                      i | ]}|d          |S r  r  r  s     rR   r  z-disconnect_oauth_provider.<locals>.<dictcomp>  s    DDD4!DDDrT   Nr   zUnknown provider: . Available: r#  r   rO   z' cannot be disconnected automatically. rS  r:  Fr   r-  T)clear_provider_authzoauth/disconnect: %s)r7  rQ   )r  !invalidate_nous_auth_status_cacher{  z!oauth/disconnect: %s (cleared=%s)zdisconnect %s failedr  )r   r  rz  r   r4   r  r  rw  rq  r?  r-  r  rH  rW   r  r  rM   rN   r   r  r  r  )r\  r   r|  catalog_by_idrQ   r|  r  clearedr-  r  r  r  s               rR   disconnect_oauth_providerr    s      7		 	  7@ 7@DD-A-C-CDDD $$[11HK H H%)YYvm/D/D%E%EH H    :(BGG 	"6*ddSbdd   
 *+x||K7P7PQQ9(FKK 	"6*ddSbdd    +%%GFFFFFF%,,.. #&--///"G   ??????--k::Eg   II,k:::w--[AAY7@ 7@ 7@ 7@ 7@ 7@ 7@ 7@\		@^^^^^^^^))+66Gf$$11333II9;PPPw--[AAi7@ 7@ 7@ 7@ 7@ 7@ 7@ 7@j  	@ 	@ 	@NN1;???CA????	@k7@ 7@ 7@ 7@ 7@ 7@ 7@ 7@ 7@ 7@sy   C.I0E I
EIEIE&%I&
E30I2E33/I0AH
I9IIIII  _oauth_sessions)_OAUTH_CLIENT_ID_OAUTH_TOKEN_URL_OAUTH_TOKEN_URLS_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_atr  )r   r  sessr  s      rR   r  z&_gc_oauth_sessions.<locals>.<listcomp>;  s-    ]]]dlASV\A\A\A\A\A\rT   N)r   _OAUTH_SESSION_TTL_SECONDS_oauth_sessions_lockr  rq  r  )staler  r  s     @rR   _gc_oauth_sessionsr  7  s    Y[[55F	 + +]]]]o&;&;&=&=]]] 	+ 	+CT****	++ + + + + + + + + + + + + + + + + +s   AA88A<?A<c                 j    | pd                                 }|r|                                dk    rd S |S )Nr   r  )r   r   )r|  	requesteds     rR   _oauth_profile_namer  @  s>    B%%''I 	))Y66trT   c                 J    t          |           }|rt          |           d S d S r8  )r  _resolve_profile_dir)r|  r  s     rR   _validate_oauth_profiler  G  s5    &w//L +\*****+ +rT   rQ  c                     t          j        d          }t          |          }|| ||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  rQ   rQ  r|  r  r  r  )r4  token_urlsafer  r   r  r  )r\  rQ  r|  r  r  r  s         rR   _new_oauth_sessionr  M  s     

#
#C&w//Likk D 
 $ $#$ $ $ $ $ $ $ $ $ $ $ $ $ $ $9s   AA!$A!r  c                     t           5  t                              |           }|r|                    d          nd}ddd           n# 1 swxY w Y   |pt          |          S )zCReturn the profile that owns an OAuth session, if one was provided.r|  N)r  r  r   r  )r  r  r  r|  s       rR   _oauth_session_profiler  c  s    
 
 8 8"":..)-7$((9%%%48 8 8 8 8 8 8 8 8 8 8 8 8 8 8 3)(333s   4A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.  r1  r0  Tr  z.tmp.rm     wri  r|  r  )indentN)PooledCredential	load_poolAUTH_TYPE_OAUTHSOURCE_MANUALr:  c                 b    g | ]+}t          |d d                               d          )|,S )r   r   :dashboard_pkce)r   r   )r   r  r  s     rR   r  z/_save_anthropic_oauth_creds.<locals>.<listcomp>  sB    xxx!Hb1I1I1T1TXeUvUvUv1w1wxAxxxrT   r  r   r'  zdashboard PKCEr  )	rQ   r  r  r  priorityr   r_  r  r  z)anthropic pool add (dashboard) failed: %s)(r?  r-  r  r  r  rO   rq   r  r4  	token_hexrt  rG  r  dumpsflushfsyncfilenort  chmodr  S_IRUSRS_IWUSRrm  r  rH  agent.credential_poolr  r  r  r  uuidr  remove_entryr   rW   uuid4hex	add_entryrM   r  )r_  r  r  r-  r/  rK  rw  r  r  r  r  poolr+  r  rx  r  s                  @rR   _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           	      0   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:  rY  r|  verifierrh   truer  S256)r  	client_idresponse_typeredirect_urir  code_challengecode_challenge_methodrh   ?)r  rQ  auth_url
expires_in)_ANTHROPIC_OAUTH_AVAILABLEr4   _generate_pkce_pairr  _ANTHROPIC_OAUTH_CLIENT_ID_ANTHROPIC_OAUTH_REDIRECT_URI_ANTHROPIC_OAUTH_SCOPES_ANTHROPIC_OAUTH_AUTHORIZE_URLr  rd  	urlencoder  )r|  r  	challenger  r  r  r  s          rR   _start_anthropic_pkcer    s    % g4effff-//Hi";HHHICDDM/5(#!'	 	F 1SS6<3I3I&3Q3QSSH0	  rT   
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                                          }d}d}	t          D ]}
t          j                            |
|dddd          }	 t          j                            |d          5 }t          j        |                                                                          }ddd           n# 1 swxY w Y    n# t(          $ r}|}	Y d}~d}~ww xY w|8t           5  d|d	<   d|	 |d<   ddd           n# 1 swxY w Y   dd|d         dS |                    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/          t1          | |                    5  t3          |||           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t           5  d'|d	<   ddd           n# 1 swxY w Y   t4                              d(|            d)d'd*S )+z<Exchange authorization code for tokens. Persists on success.NrQ   r:  rQ  rY  r   zUnknown or expired sessionr   r  r  Fr  )r7  r  r  #r   r   r^  zNo code providedr   authorization_coderh   r  )
grant_typer  r  rh   r  code_verifierr  zhermes-dashboard/1.0)r  r  r  )r{  r   r  rU  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)r7  r  )r  r  r   r4   r   r   r   r  r  r  r  r   _ANTHROPIC_OAUTH_TOKEN_URLSr  r   r5   r  r  r  r  rW   re  r   r  r  r  rM   rN   )r  r  r|  r  r   r  state_from_callbackexchange_datar  last_exc	_endpointr   r  r  r_  r  r  r  s                     rR   _submit_anthropic_pkcer    s$    
 / /"":../ / / / / / / / / / / / / / / R4
#{22d6lf6L6L4PQQQQH~""tH~$((?B[B[\\\ $$S!,,E8>>D Ow;MNNN&)%jj1nn%(("J*/$5W5j)      vxx  FH0  	n$$ 24   % 
 
	''R'88 :DDIIKK$6$6$8$899: : : : : : : : : : : : : : :E 	 	 	HHHHH	 ~! 	I 	I$DN$Hh$H$HD!	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I w4;PQQQ::nb11LJJ33MVZZ--566J R! 	? 	?$DN$>D!	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? 	? w4;PQQQ	d*++zD/@AMR2:wGGHH 	T 	T'm]SSS	T 	T 	T 	T 	T 	T 	T 	T 	T 	T 	T 	T 	T 	T 	T 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0!G#9G
G#G	G#G	G##
G9-G44G9HH#&H#J&&J*-J*'L. L"L. "L&&L. )L&*L. .
M:8M5?MM5M	M5 M	!M5/M:5M:NNNc           
      N  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/|d0<   t          |d1                   }||d2<   |d3k    r7|d4z  }tC          dt          |t!          j                    z
                      }nt!          j                    |z   }|}||d<   t#          j        tD          |fdd|dd                                                     |d	t          |d                   t          |d5                   |tC          d6|d,         pd7d8z            dS | d9k    rSdd:l m# ddlfd;}t          j                                        d|           d{V }t          d9d	|
          \  }}t          |d	                   |d	<   t          |d                   |d<   t!          j                    t          |d                   z   |d<   t#          j        tH          |fdd|dd                                                     |d	t          |d                   t          |                    d          p|d5                   t          |d                   t          |d                   dS t7          d<d=|  d>"          )?a!  Initiate a device-code flow (Nous, OpenAI Codex, MiniMax, or xAI).

    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_coder9  NHERMES_PORTAL_BASE_URLNOUS_PORTAL_BASE_URLr   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  r   )r  r  r  r  r  r  )r  r  r  r  r  r  s    rR   _do_nous_device_requestz8_start_device_code_flow.<locals>._do_nous_device_request5  s    d++!#56     ((%(7"+#	   	                 s   AAArO  r  rF   r  r  r  r  r  Tzoauth-poll-r'  rd   	user_codeverification_uri_complete)r  rQ  r  verification_urlr  poll_intervalrT  zoauth-codex-r  r  r  rO  r^  r  r  zdevice-auth failedr   i  z2device-auth timed out before returning a user coder  r  r   rW  )_minimax_pkce_pair_minimax_request_user_codeMINIMAX_OAUTH_CLIENT_IDMINIMAX_OAUTH_GLOBAL_BASEMINIMAX_PORTAL_BASE_URLc                                                               d          ddid          5 }  |           cd d d            S # 1 swxY w Y   d S )Nr  r  r  Tr  r   follow_redirects)r  r  r  r  rh   r  )r  r  r  r  r  r  rh   s    rR   _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  rh   rh  rg  
expired_inexpired_in_rawl    J)g     @@verification_urir  r  r  rX  )_xai_oauth_request_device_codec                                                               d          ddi          5 }  |           cd d d            S # 1 swxY w Y   d S )N      4@r  r  r  r  )r  r	  r  s    rR   _do_xai_device_requestz7_start_device_code_flow.<locals>._do_xai_device_request  s    d++!#56    > 55f==	> > > > > > > > > > > > > > > > > >s   AA
A
r   z	Provider z" does not support device-code flow)%r  r  r9  r  rq   rr   r  r  r  r  rj   r  rp   r  r  re  r   rs   ru   _nous_pollerrP   _codex_full_login_worker	monotonicr  r  r   sleepr4   r  r  r  r  ro   r  _minimax_pollerr	  _xai_device_poller)r\  r|  r9  pconfigr  device_dataeffective_scoper  r  ro  deadliner   r  r  r  r  interval_rawr 	  r  expires_in_secondsr	  r  r  r  r	  r  r  r  r  r  rh   s                        @@@@@@@@@@rR   _start_device_code_flowr	    s7	      f	
 	
 	
 	
 	
 	
 	
 	
 	#F+I.// 'y/00'&
&++	 	
 %		 	 	 	 	 	 	 	 	 .5-E-G-G-W-W).
 .
 (
 (
 (
 (
 (
 (
$_ 'v}gNNN	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$$#NM7SSS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%
 
 
 
 
 
 
 
 'wWWW	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
 
 	
 k!!BBBBBB	> 	> 	> 	> 	> 	> $466FF(
 
 
 
 
 
 
 
 '{M7SSS	T!+m"<==]{:677Z!Y[[3{</H+I+II\%(s2A2w((		
 	
 	

 %'''![566 # ;<< 312! ! k,788 Z!899

 

 
	
 C0kK0k0k0k
l
l
lls$   I33I7:I7LL	L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
}t'          t)          |                     5   ||dd          }ddl m}  ||           ddd           n# 1 swxY w Y   t          5  d|d<   ddd           n# 1 swxY w Y   t,                              d|            dS # t0          $ rc}t,                              d| |           t          5  d |d<   t5          |          |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  rO  rF   r  rD   r  r  r  r  r  )r  r  r  rO  r  r  r  r  
token_typeBearerr_  r  tz)
r  r  r  r  r	  r_  r  obtained_atr  r  F)timeout_seconds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   r  r  r  r   r  re  r   r  r  r  r  	isoformatfromtimestampr  r  r  r	  rM   rN   rW   r  r  )r  r	  r	  r   r   r  r  r  r  rO  rF   r  r  r  
token_datar  	token_ttl
auth_state
full_stater	  r  s                        rR   r	  r	    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^^```"&#
 

 2:>>?? 	1 	166 $#  J
 A@@@@@$$Z000	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 " 	( 	('DN	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(		CZPPPPP + + +DjRSTTT! 	+ 	+$DN$'FFD!	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	++s   AA
A?,J +D	=J 	DJ DDJ  I	=J 	IJ I
J I-!J -I11J 4I15J 
L#K=K&K=&K*	*K=-K*	.K==L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+          t-          |                     5   ||           ddd           n# 1 swxY w Y   t          5  d|	d<   ddd           n# 1 swxY w Y   t.                              d|            dS # t2          $ rc}t.                              d | |           t          5  d!|	d<   t7          |          |	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  r  r 	  r  r  r  Tr  )r  r  r  r  r  r  r  r  )r  rW  rg  rh  r	  r	  r_  r  resource_urlr	  )rQ   rg  r  r  r  r  r	  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   r  r  r  r   r  r  r  r  re  r  r  r	  r	  r  r  rM   rN   rW   r  r  )r  r%	  r&	  r'	  r(	  r)	  r   r   r  r  r  r  r  r  r  r 	  r  r 	  r  r  expires_in_sr"	  r  s                          rR   r
	  r
	  0  s                 ,+++++++LLL	 / /"":../ / / / / / / / / / / / / / / ,-O[!I[!I)M((=))K*+N3+\\MM$''12!  
 
 	 ,, /##+)'  J	 	 	 	 	 	 	 	 	 	 	 	 	 	 	& hl8<((::
<())s
 
 
 1c-#--//"ABBCC'hhx22."@"($..x@@&~6'8&NN>::==??0(0(,  ikk&
 

" 2:>>?? 	1 	1$$Z000	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1! 	( 	('DN	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(		F
SSSSS + + +GUVWWW! 	+ 	+$DN$'FFD!	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	++s   A		AA-I$ C"I$ "C&&I$ )C&*DI$  HI$ HI$ H 
I$ *H<0I$ <I  I$ I I$ $
K.#KJ5)K5J9	9K<J9	=KKc           	      t   ddl }ddlm}m}m}m} t          5  t                              |           }ddd           n# 1 swxY w Y   |sdS |d         }t          |d                   }t          dt          |d         t          j                    z
                      }		  |d          }
|                    |                    d          d	d
i          5 } |||
d         ||	|          }ddd           n# 1 swxY w Y   t          |                    dd          pd                                          t          |                    dd          pd                                          t          |                    dd          pd                                          |                    d          t          |                    d          pd                                          pdd}t!          t#          |                     5   |||
t%          j        t(          j                                                                      dd          d            |dd           ddd           n# 1 swxY w Y   t          5  d|d<   ddd           n# 1 swxY w Y   t0                              d|            dS # t4          $ rc}t0                              d| |           t          5  d|d<   t          |          |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)!z3Background poller for xAI's OAuth device-code flow.r   N)_save_xai_oauth_tokens_xai_oauth_discovery_xai_oauth_poll_device_tokenunsuppress_credential_sourcerO  rF   rD   r  r	  r  r  r  token_endpoint)r1	  rO  r  r  r_  r   r  id_tokenr  r	  r	  )r_  r  r2	  r  r	  r  r  oauth_device_code)	discoveryrc  rb  rX  r  r  z.oauth/device: xai login completed (session=%s)z,xai device-code poll failed (session=%s): %sr^  r  )r  r  r-	  r.	  r/	  r0	  r  r  r   re  r  r   r  r  r  r   r  r  r   r  r   r  r	  rt  rM   rN   rW   r  )r  r  r-	  r.	  r/	  r0	  r  rO  rF   r  r4	  r  r 	  tokensr  s                  rR   r	  r	    s   LLL            
 / /"":../ / / / / / / / / / / / / / / }%K4
#$$HRT,/$)++=>>??J,+((..	\\MM$''12  
 
 
	 55()9:'%&  J	
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	 
	  
~r B B HbIIOOQQ !D!D!JKKQQSSJNN:r::@bAAGGII$..66jnn\::FhGGMMOO[S[
 
 2:>>?? 	E 	E""#%\(,77AACCKKHVYZZ-	    )(mDDD!	E 	E 	E 	E 	E 	E 	E 	E 	E 	E 	E 	E 	E 	E 	E" " 	( 	('DN	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(		BJOOOOO + + +CZQRSSS! 	+ 	+$DN$'FFD!	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	++s   ?AA#7K
 C=1K
 =DK
 DDK
 AI>2K
 >JK
 J
K
 J"K
 "J&&K
 )J&*K
 

L7#L27LL2L	L2"L	#L22L7c           
      	   	 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} t)          t+          |                     5   |||d.           ddd           n# 1 swxY w Y   t          5  d/|d<   ddd           n# 1 swxY w Y   t,                              d0|            dS # t0          $ r}t,                              d1| |           t          5  t                              |           }|rd2|d<   t5          |          |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.comr  r  z!/api/accounts/deviceauth/usercoder  r  r  )r  r   r  zdeviceauth/usercode returned r  r   device_auth_idr   rF   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 r  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{  r   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^  )r  r  r7	  r8	  r9	  r  r  postr   rn  r  r   r  re  r  r  r   r	  r		  r<	  r  r  rM   rN   rW   r  r  )r  r  r7	  r8	  r9	  issuerr  r  r	  r  r:	  r  r  r  r	  	code_respr*  r  r  
token_respr5	  r_  r  r<	  r  r   s                             rR   r	  r	    s%   e,	
 	
 	
 	
 	
 	
 	
 	
 	
 	

 + \\%--"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2:>>?? 	 	 ,!.      	 	 	 	 	 	 	 	 	 	 	 	 	 	 	
 " 	( 	('DN	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(		KZXXXXX , , ,GUVWWW! 	, 	,##J//A ,%(%(VV/"		, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	,,sM  9Q  A'Q 'A++Q .A+/B5Q $FQ 8FQ FQ FA
Q %BI>2Q >JQ JQ J)Q )J--Q 0J-1Q 6A(Q $MQ MQ MBQ .P	=Q 	PQ P
Q P-!Q -P11Q 4P15Q 
S#S4S6SS	S	S	
SSz(/api/providers/oauth/{provider_id}/startc                    K   t          |           t                       t          |           d t          D             } |vrt	          dd            t           fdt          D                       }|d         dk    rt	          d  d|d	          d
          	 |d         dk    r dk    rt          |          S |d         dk    rt           |           d{V S nU# t          $ r  t          $ r>}t          
                    d            t	          dt          |                    d}~ww xY wt	          dd          )z.Initiate an OAuth login flow. Token-protected.c                     h | ]
}|d          S r  r  r  s     rR   r  z$start_oauth_login.<locals>.<setcomp>E  s    666QtW666rT   r   zUnknown provider r   c              3   4   K   | ]}|d          k    |V  dS )r  Nr  )r   r   r\  s     rR   r   z$start_oauth_login.<locals>.<genexpr>H  s1      VVqqw+?U?U?U?U?U?UVVrT   rQ  rV  z uses an external CLI; run `rR  z
` manuallyrY  r:  r  rO  Nzoauth/start %s failedr  zUnsupported flow)r   r  r  r[  r4   r   r  r	  rW   rM   r  r  )r\  r   r|  validcatalog_entryr  s   `     rR   start_oauth_loginrF	  ;  s      7G$$$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(9999 M110gNNNNNNNNNN 2    < < <.<<<CFF;;;;< C0B
C
C
CCs   #!C) "C) )D;=9D66D;c                   $    e Zd ZU eed<   eed<   dS )OAuthSubmitBodyr  r  Nr  r  rT   rR   rH	  rH	  a  s"         OOO
IIIIIrT   rH	  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   rj   r  rp   r  r  r  r4   )r\  r  r   r|  s       rR   submit_oauth_coderJ	  f  s       7k!!-//??($/49g
 
 
 
 
 
 
 
 	
 C0YK0Y0Y
Z
Z
ZZrT   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, xAI).
    Each surfaces progress through the same background-worker-updated
    ``status`` field, so a single poll endpoint serves them all.
    Nr   zSession not found or expiredr   rQ   r   zProvider mismatch for sessionr  r  r  )r  r  r  r  )r  r  r   r4   )r\  r  r|  r  s       rR   poll_oauth_sessionrL	  v  s       
 / /"":../ / / / / / / / / / / / / / / T4RSSSSJ;&&4STTTT x./22hh|,,	  s   155z*/api/providers/oauth/sessions/{session_id}c                    K   t          |           t          5  t                              | d          }ddd           n# 1 swxY w Y   |dddS d| dS )z0Cancel a pending OAuth session. Token-protected.NFzsession not found)r7  r  T)r7  r  )r   r  r  r  )r  r   r|  r  s       rR   cancel_oauth_sessionrN	    s       7	 5 5"":t445 5 5 5 5 5 5 5 5 5 5 5 5 5 5|(;<<<j111s   AAAc           
         ddl m} d } |            }	 |                    |           }|r|                    |          sdg f|                                 S t          |dd          p2t          |dd          p!t          |dd          pt          |dd          }g }|g|                    d	                                          }|D ]<}|                     ||d
d           ||dd           ||dd          d           =n|	                    dd          }i }	|D ]Y}|
                    d
          }
|
                    d          }|
r+|r)|	                    |g                               |           Zd }|}|g}|h|	
                    |          r{fd|	|         D             }|snd|                    |d           |d         d
         }|                    |                               |           |	
                    |          {||f|                                 S # |                                 w xY w)zResolve a session id to the newest child leaf session.

    /model may create child sessions. Dashboard refresh should continue the
    newest child instead of reopening the old parent.
    r   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r8  )rd  rg  r   rW   )r  rv  indexs      rR   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  r  )r  r  r  '  r  r  c                 n    	 t          |                     d          pd          S # t          $ r Y dS w xY w)Nr  r           )rf  r   rW   )r  s    rR   startedz+_session_latest_descendant.<locals>.started  sH    SWW\227a888   sss   #& 
44c                 B    g | ]}|                     d           v|S r  r   )r   rr  s     rR   r  z._session_latest_descendant.<locals>.<listcomp>  s-    RRR!%%++T:Q:Q!:Q:Q:QrT   Tr  )r  r  resolve_session_idr  r   r   executefetchallr  r  r   r  r  r  )r  r  rR	  r-  r  rS	  r^  raw_rowsr  childrenridr  r[	  r  r   
candidatesr  s                   @rR   _session_latest_descendantre	    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Zee         ed<   dS )BulkDeleteSessionsr  Nr|  r  r  rT   rR   rg	  rg	    s3         	cNNN!GXc]!!!!!rT   rg	  z/api/sessions/bulk-deletec                   K   t          | j                  dk    rt          dd          t          | j                  }	 |                    | 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   Tr7  deleted)r   r  r4   r  r|  delete_sessionsr   )r  r-  rj	  s      rR   bulk_delete_sessions_endpointrl	    s      J 48}}s9
 
 
 	
 
&dl	3	3B$$TX..w//








s   A4 4B
z/api/sessions/empty/countc                    K   t          |           }	 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  count_empty_sessionsr   )r|  r-  s     rR   count_empty_sessions_endpointro	  3  sO       
&g	.	.B00223








s	   = Az/api/sessions/emptyc                    K   t          |           }	 |                                }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.
    Tri	  )r  delete_empty_sessionsr   )r|  r-  rj	  s      rR   delete_empty_sessions_endpointrr	  B  sW      ( 
&g	.	.B**,,w//








s   A   Az/api/sessions/statsc                   K   t          |           }	 |                    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.
    T)r  F)r  rW	  r  r   r  r   r   )rL  active_storer  messages	by_source)r  r  message_countr  r  r   rW   r   )	r|  r-  rL  rt	  r  ru	  rv	  r   srcs	            rR   get_session_statsry	  ^  s8      
&g	.	.B  $ 77'''??##$#77##%%$&		***NN ; ;!%%//2U33!*sA!6!6!:	#;  	 	 	D	 (  "
 
 	







s1   AC3 ,AC C3 
CC3 C
C3 3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  r  )r  )r  r  r  r   )r|  r  _namer  s       rR   r  r  }  sY     '&&&&& y{{$W--KE49T$ZZ*45555rT   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   r|  )r  r^	  r  r4   r  r   )r  r|  r-  r  r   s        rR   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  )re	  r4   r   )r  latestr   s      rR   get_session_latest_descendantr	    sr      -j99LFD I4GHHHH+/ ?QZ247!233	  rT   z#/api/sessions/{session_id}/messagesc                 .  K   t          |          }	 |                    |           }|st          dd          |                    |          }|                    |          }||d|                                 S # |                                 w xY w)Nr   r}	  r   )r  ru	  )r  r^	  r4   resolve_resume_session_idget_messagesr   )r  r|  r-  r  ru	  s        rR   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ddd|                                 S |                    |           ddi|                                 S # |                                 w xY w)NT)r7  already_absentr7  )r  r^	  r   delete_session)r  r|  r-  r  s       rR   delete_session_endpointr	    s      
 
&g	.	.B ##J// 	8$77 	



 	#d|








s   A0 A0 0B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 )SessionRenameNr   r  r|  )
r}  r~  r  r   r   r  r  r  r   r|  r  rT   rR   r	  r	    sP         E8C=#Hhtn### "GXc]!!!!!rT   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'.r   T)r7  r   r  )r  r|  r^	  r4   r   r  set_session_titlerg  r  set_session_archivedget_session_titler   r   )r  r  r-  r  r  r  s         rR   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   t          |          }	 |                    |           }|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^	  r4   export_sessionr   )r  r|  r-  r  r{  s        rR   export_session_endpointr	    s       
&g	.	.B	##J// 	MC8KLLLL  %%<C8KLLLL








s   AA9 9Bc                   R    e Zd ZU dZeed<   dZee         ed<   dZ	ee         ed<   dS )SessionPruneZ   older_than_daysNr   r|  )
r}  r~  r  r	  re  r  r   r   r  r|  r  rT   rR   r	  r	  	   sJ         OS FHSM   !GXc]!!!!!rT   r	  z/api/sessions/prunec                   K   | j         dk     rt          dd          | j        rt          | j                  d         nt	                      }t          | j                  }	 |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.  N)r	  r   sessions_dirT)r7  r  )
r	  r4   r|  r  r   r  prune_sessionsr   r  r   )r  profile_homer-  r	  r  s        rR   prune_sessions_endpointr	     s       a4RSSSS:>,]%dl33A66OL]L]L	%dl	3	3B	#j0## 0K'4)5)<)<)>)>HD $ 
 

 w//








s   #AB; ;Cz	/api/logsr  level	componentr4  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ALLr  zUnknown component: r  r#  r  r  )has_filters	min_levelcomponent_prefixesc                 @    g | ]}|                                 v |S r  r\  )r   lneedles     rR   r  zget_logs.<locals>.<listcomp>X   s+    ;;;v':':!':':':rT   )hermes_cli.logsr	  r	  r   r4   r   r  hermes_loggingr	  r]   r  r   r  r  r   rr  )r;  r  r	  r	  r4  r	  r	  log_namer  r	  r	  comp_prefixesr	  r  r	  s                 @rR   get_logsr	  '   s#      65555555}}T""H Q4O4O4OPPPP  6)H4H?? +r*** 5555555        C5;;==E#9#9tI 	Y__&&%//*..y99 MY M M%)YYv6H/I/I%J%JM M    ! y;M;V<<KZ9#eS///T(	  F  O;;;;V;;;S__<L<M<MN6***s   %A, ,A;:A;c                   :   e Zd ZU dZeed<   eed<   dZeed<   dZeed<   dZe	e
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<   dZe	e         ed<   dZe	e
e                  ed<   dZe	e         ed<   dZeed<   dS )CronJobCreater   rA   schedulerO   r  deliverNrD  r  rQ   r  scriptcontext_fromr  workdirFno_agent)r}  r~  r  rA   r  r  rO   r	  rD  r   r   r  rQ   r  r	  r	  r   r  r	  r	  r   r  rT   rR   r	  r	  a   s        FCMMMD#NNNGS"&FHT#Y&&&E8C="Hhsm""""Hhsm""" FHSM   "&L(3-&&&,0htCy)000!GXc]!!!HdrT   r	  c                       e Zd ZU eed<   dS )CronJobUpdaterO  N)r}  r~  r  rg  r  r  rT   rR   r	  r	  q            MMMMMrT   r	  strip_trailing_slashr	  c                    | d S t          |                                           }|r|                    d          }|pd S )Nr   )r  r   r  )r_  r	  rZ  s      rR   _cron_optional_textr	  u   sF    }tu::D  {{3<4rT   c                     | d S t          | t                    rt          j        d|           }n!t          | t          t
          f          r| }nd S d |D             }|pd S )Nz[\n,]c                     g | ]D}t          |                                          #t          |                                          ES r  )r  r   )r   r  s     rR   r  z%_cron_string_list.<locals>.<listcomp>   s=    JJJ4D		8I8IJSYY__JJJrT   )rd  r  r3  r   rb  r  )r_  	raw_itemsrq  s      rR   _cron_string_listr	  ~   sp    }t% HXu--			ED%=	)	) 		tJJ9JJJE=DrT   r	  c                 P   t          |           }|sdS |dz                                  }t          |                                          }|                                r|                                n||z                                  }	 |                    |          }n'# t          $ r}t          dd|           |d}~ww xY w|                                st          dd|           |	                                st          dd|           t          |          S )zFValidate a dashboard-selected cron script against the profile sandbox.Nscriptsr   zscript must be inside r   zscript does not exist: zscript is not a file: )r	  rl  r   ri  rj  relative_torg  r4   r  r  r  )r_  r	  rZ  scripts_rootr^  rq  relativer  s           rR    _normalize_dashboard_cron_scriptr	     sU   u%%D t 9,5577LDzz$$&&H&.&:&:&<&<e  """<RZCZBcBcBeBeI((66   :L::
 
 
 	
  [4Yi4Y4YZZZZ Z4XY4X4XYYYYx==s   B" "
C,CCjobc                    t          |                     d                    }t          |                     d                    }t          |                     d                    p!t          |                     d                    }t          |                     d                    }|r|st	          dd          d S |s|s|st	          dd	          d S d S d S )
NrA   r	  rD  skillr	  r   zno_agent=True requires a scriptr   z2agent cron jobs require a prompt, skill, or script)r	  r   r	  r   r4   )r	  rA   r	  rD  r	  s        rR   &_validate_dashboard_cron_effective_jobr	     s    !2!233F !2!233Fswwx0011X5FswwwGWGW5X5XFCGGJ''((H  	8    	 
f 
 
G
 
 
 	

 
 
 
 
 
rT   c                    t          | pi           }dD ]}||v rt          ||                   ||<   d|v rt          |d         |          |d<   d|v rt          |d         d          |d<   d|v rt          |d                   pd|d<   d|v rt          |d                   |d<   d	|v rt          |d	                   |d	<   |S )
a5  Normalize dashboard JSON into cron.jobs.update_job's storage shape.

    This intentionally stays in the dashboard adapter layer: cron/jobs.py is the
    source of truth for scheduling behaviour; the dashboard only translates form
    payloads into the shapes that existing core functions already accept.
    )r  rQ   r	  r	  r  Tr	  r	  r  r	  r  )rg  r	  r	  r	  )rO  r	  r  rv  s       rR   !_normalize_dashboard_cron_updatesr	     s    gm$$J/ C C*1*S/BBJsO:?x  
  

8 Z!4z""
 "
 "

: J 3Jy4I J J Ug
9##%6z.7Q%R%R
>"Z'')::FX;Y)Z)Z
%&rT   refsr  c           	      j    | sd S | D ]+}t          |d|          st          dd| d| d          ,d S )Nget_jobr   zcontext_from job 'z' not found in profile 'r+  r   )_call_cron_for_profiler4   )r	  r  r  s      rR   %_validate_dashboard_cron_context_fromr	     s        %lIsCC 	( ( ($( ( (   	 rT   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   r  c                 ,    g | ]}t          |          S r  _profile_to_dictr  s     rR   r  z'_cron_profile_dicts.<locals>.<listcomp>   s!    JJJ ##JJJrT   zJFailed to list profiles for cron dashboard; falling back to directory scan)rX   r  r  rW   rM   r  _fallback_profile_dicts)r  s    rR   _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   r  r  r   r   Nr   	Profile '' does not exist.)
rX   r  r   normalize_profile_namevalidate_profile_namerg  r4   r  profile_existsr  )r|  r  ro  canonr  s        rR   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r  c                 n    t          |           }||d<   ||d<   t          |          |d<   |dk    |d<   |S )Nr|  r  r  r  r  )rg  r  )r	  r|  r  	annotateds       rR   _annotate_cron_jobr	  !  sG    S		I"Ii 'In"4yyIm&-&:I"#rT   target_profile	func_namec                    t          |           \  t          5  ddlm} ddlm}m} |j        }|j        }|j	        }	 |t                              }
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   jobs)reset_hermes_home_overrideset_hermes_home_overriderE  	jobs.jsonoutputNc                 2    g | ]}t          |          S r  )r	  )r   jr  r  s     rR   r  z*_call_cron_for_profile.<locals>.<listcomp>.!  s&    JJJa"1lD99JJJrT   )r  _CRON_PROFILE_LOCKrE  r	  r  r	  r	  CRON_DIR	JOBS_FILE
OUTPUT_DIRr  r   rd  rb  rg  r	  )r	  r	  rf   kwargs	cron_jobsr	  r	  old_cron_dirold_jobs_fileold_output_dirr   r  r  r  s               @@rR   r	  r	  !  s    ,N;;L$	 . .******	
 	
 	
 	
 	
 	
 	
 	

 !)!+"-((T33!F]	'0;>	(1H<		.2WY	22DCFCCF!-I"/I#1I &&u---- ".I"/I#1I &&u-----). . . . . . . . . . . . . . ., &$ KJJJJJ6JJJJ&$ >!&,===Ms*   A$C*B8!C*8"CC**C.1C.job_idc                      t                      D ]Y}t          |                    d          pd          }|s)t          |dd          }t	           fd|D                       r|c S Zd S )NrO   r   	list_jobsTc              3   |   K   | ]6}|                     d           k    p|                     d          k    V  7dS )r  rO   Nr   )r   r	  r	  s     rR   r   z)_find_cron_job_profile.<locals>.<genexpr>:!  sF      NNAquuT{{f$?f(?NNNNNNrT   )r	  r  r   r	  r<  )r	  r|  rO   r	  s   `   rR   _find_cron_job_profiler	  4!  s    &((  7;;v&&,"-- 	%dK>>NNNNNNNNN 	KKK	4rT   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 )Nr  r	  TrO   r   z'Failed to list cron jobs for profile %s)
r   r   r	  r	  r  r   extendrW   rM   r  )r|  r  r	  r  rO   s        rR   list_cron_jobsr	  ?!  s      !E((**IE!!%idCCC!#D#%% L L488F##)r** 		LKK.t[$GGHHHH 	L 	L 	LNNDdKKKKK	LKs   <$B!!%C	C	z/api/cron/jobs/{job_id}c                    K   |pt          |           }|st          dd          t          |d|           }|st          dd          |S )Nr   Job not foundr   r	  r	  r4   r	  r	  r|  selectedr	  s       rR   get_cron_jobr
  Q!  sg      8088H EODDDD
 9f
=
=C EODDDDJrT   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.
    r	  r  r   r  rU  r   rX	  r  Nr  r  r  r  r  r|  )runsr  )r	  r	  r   r  r  rr  re  r  rg  r  list_cron_job_runsr   r   r   )r	  r|  r  r
  r  r	  limit_nr-  r
  r  r   s              rR   r
  r
  \!  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          |          \  }}t          | j        |          }t          | j                  }t          | j                  }t          ||           t          | j                  }t          | j
        |||d           t          |d| j
        pd| j        | j        t          | j                  pd|t          | j                  t          | j                  t          | j        d          ||t          | j                  t          | j                  |          S # t*          $ r  t,          $ r=}t.                              d           t+          d	t3          |          
          d }~ww xY w)N)rA   rD  r	  r	  
create_jobr   r  Tr	  )rA   r	  rO   r	  rD  r  rQ   r  r	  r	  r  r	  r	  zPOST /api/cron/jobs failedr   r   )r  r	  r	  r	  rD  r	  r	  r   r	  r	  rA   r	  r	  rO   r	  r	  r  rQ   r  r  r	  r4   rW   rM   r  r  )	r  r|  r  r	  r	  rD  r	  r	  r  s	            rR   create_cron_jobr
  !  s     "<%7%@%@"l1$+|LL"4;//():;;-lLIII&&.k 	0
 0
 	 	 	 &;$"]'55@%dj11(77(TRRR%.t/DEE'55
 
 
 	
"     < < <3444CFF;;;;<s   D*D/ /F 8E;;F 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  )cron.schedulerr
  r	  rW   rM   r  )r  r
  s     rR   get_cron_delivery_targetsr
  !  s       '# 		
 	
G@888888,,..//// @ @ @>?????@ws   #0 $AAc                 p  K   |pt          |           }|st          dd          	 t          |          \  }}t          |d|           }|st          dd          t	          |j        |          }d|v r#t          |                    d          |           h d}|                    |          r"i ||}	d|v r	d|vrd |	d<   t          |	           t          |d	| |          }
n;# t          $ r  t          $ r$}t          d
t          |                    |d }~ww xY w|
st          dd          |
S )Nr   r
  r   r	  r	  >   r	  rA   r	  rD  r	  rD  r	  
update_jobr   )r	  r4   r  r	  r	  rO  r	  r   intersectionr	  rg  r  )r	  r  r|  r
  r  r	  r+  rO  execution_fields	effectiver	  r  s               rR   update_cron_jobr
  !  s     8088H EODDDDG%7%A%A"l),	6JJ 	ICHHHH3L
 
 W$$1N++   ONN((11 	>/8/w/I7""wg'='=%)	'"29===$\<QQ    G G GCHH===3FG EODDDDJs   B?C( (D <DD z/api/cron/jobs/{job_id}/pausec                    K   |pt          |           }|st          dd          t          |d|           }|st          dd          |S )Nr   r
  r   	pause_jobr
  r
  s       rR   pause_cron_jobr
  !  sg      8088H EODDDD
 ;
?
?C EODDDDJrT   z/api/cron/jobs/{job_id}/resumec                    K   |pt          |           }|st          dd          t          |d|           }|st          dd          |S )Nr   r
  r   
resume_jobr
  r
  s       rR   resume_cron_jobr
  !  sg      8088H EODDDD
 <
@
@C EODDDDJrT   z/api/cron/jobs/{job_id}/triggerc                    K   |pt          |           }|st          dd          t          |d|           }|st          dd          |S )Nr   r
  r   trigger_jobr
  r
  s       rR   trigger_cron_jobr"
  "  sg      8088H EODDDD
 =&
A
AC EODDDDJrT   c                   K   |pt          |           }|st          dd          	 t          |d|           }n1# t          $ r$}t          dt	          |                    |d }~ww xY w|st          dd          ddiS )Nr   r
  r   
remove_jobr   r7  T)r	  r4   r	  rg  r  )r	  r|  r
  r  r  s        rR   delete_cron_jobr%
  "  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                    t          |           \  }}t          5  ddlm} ddlm} |j        }|j        }|j        }|dz  |_        |j        dz  |_        |j        dz  |_        	  |            }	t          |	
                    |dd                    ||_        ||_        ||_        cddd           S # ||_        ||_        ||_        w xY w# 1 swxY w Y   dS )	u0  Run ONE due cron job end-to-end for ``profile`` via the resolved
    scheduler provider's ``fire_due`` (store CAS claim + ``run_one_job``).

    Retargets the ``cron.jobs`` module globals to the profile's cron dir under
    the shared lock — same mechanism as ``_call_cron_for_profile`` — so the
    claim and the run operate on the right profile's ``jobs.json``. Runs with
    no live adapters; delivery falls back to the per-platform send path (the
    dashboard process has no gateway adapter handles, exactly like the desktop
    cron path above).
    r   r	  rI   rE  r	  r	  N)adaptersr  )r  r	  rE  r	  rL   rJ   r	  r	  r	  r   fire_due)
r|  r	  _profile_namer  r	  rJ   r	  r	  r	  rQ   s
             rR   _fire_cron_job_for_profiler*
  "  sZ    -W55M4	 2 2******BBBBBB )!+"-!F]	'0;>	(1H<		2--//H))&4d)KKLL!-I"/I#1I !2 2 2 2 2 2 2 2 ".I"/I#1I 1111!2 2 2 2 2 2 2 2 2 2s*   A
C%.B5C5CCCCz/api/cron/firec                 D  K   ddl m} | j                            dd          }|                    d          r|dd                                         nd}t                      }  |            |t          |dd	d
d          t          |dd	dd          pdt          |dd	dd          pd          }|t          ddid          S 	 | 	                                 d{V }n# t          $ r i }Y nw xY wt          |t                    r|pi                     d          nd}|st          ddid          S t          |          }|st          d|dd          S t          j        t          j        t"          ||                     t          d|dd          S )ue  Chronos managed-cron fire webhook (NAS -> agent).

    Authenticated by a short-lived NAS-minted JWT (verified by the pluggable
    Chronos fire-verifier), NOT the dashboard session cookie — so this path is
    in ``PUBLIC_API_PATHS`` to bypass the dashboard auth gate, and the JWT is
    the real gate. This is the inbound half of scale-to-zero managed cron: NAS
    POSTs here at fire time, the agent verifies, claims the job (store CAS, so
    at-most-once across replicas / on a NAS retry), runs it, and re-arms the
    next one-shot.

    Lives on the dashboard app (not the api_server adapter) because the
    dashboard is the agent's always-reachable public HTTP surface on hosted
    deployments; the gateway may be idle/scaled down.

    Returns 202 immediately and runs the job in the background so a long agent
    turn never trips NAS's HTTP timeout.
    r   )get_fire_verifierr  r   r   rZ  NrE  chronosexpected_audiencer  nas_jwks_urlr  )r   r.
  jwks_or_keyr>	  r^  zinvalid fire tokenr   r   r	  zmissing job_idr   gone)r  r	  r  accepted   )%plugins.cron_providers.chronos.verifyr,
  r   r   r   r   r   r   r<   r  rW   rd  rg  r	  rj   create_taskr  r*
  )	r   r,
  r   r   r  claimsr  r	  r|  s	            rR   cron_fire_webhookr9
  >"  s     & HGGGGG?33D $	 : :BDHNNE
--C   !#vy:MWYZZZCNBOOOWSWsFI|RHHHPD	  F ~W&:;MMMM\\^^######   +5dD+A+AKdjbh'''tF JW&67SIIII$V,,G S v@@cRRRR 4gvFF   :@@cRRRRs   C' 'C65C6c                   8    e Zd ZU eed<   i Zeeef         ed<   dS )AutomationBlueprintInstantiate	blueprintr  N)r}  r~  r  r  r  r  r   r   r  rT   rR   r;
  r;
  {"  s3         NNNFDcNrT   r;
  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   r  s     rR   r  z(list_cron_blueprints.<locals>.<listcomp>"  s+    QQQQQUU4[[Q4QQQrT   originr  z?cron_delivery_targets unavailable; using static deliver optionsTr  r&  rO   r	  r  
blueprintszGET /api/cron/blueprints failedr  r   )cron.blueprint_catalogr>
  r?
  r
  r
  rW   rM   r  r   r  r  r4   r  )
r>
  r?
  deliver_optionsr
  r  r  r]	  rx  fr  s
             rR   list_cron_blueprintsrF
  "  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  rA
  r
  z,POST /api/cron/blueprints/instantiate failedr   )rC
  rH
  rI
  rJ
  r<
  r4   r  r  r  r	  rW   rM   r  )	r  r|  rH
  rI
  rJ
  r<
  specr  r  s	            rR   instantiate_blueprintrL
  "  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   commandrf   r  r   r|  )r}  r~  r  r  r  r   r   rO
  rf   r   r  r   r   r|  r  rT   rR   rN
  rN
  "  s         
IIIC#!GXc]!!!D$s)Cc3hD(3-!GXc]!!!!!rT   rN
  c                   X    e Zd ZU i Zeeeeef         f         ed<   dZe	e         ed<   dS )MCPServersReplaceserversNr|  )
r}  r~  r  rR
  r   r  r   r  r|  r   r  rT   rR   rQ
  rQ
  "  sH         )+GT#tCH~%&+++!GXc]!!!!!rT   rQ
  r  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.r   z***)rq  r$   r  rW   )r  r  r>  r?  s       rR   _redact_mcp_envrT
  "  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   rO
  stdiounknownrf   r  r   r  TFr  )	rO   	transportr   rO
  rf   r  r   r  r  )r   rb  rT
  )rO   r  rX
  s      rR   _mcp_server_summaryrY
  "  s    ''%..\#''):L:L/[wwR[Iwwu~~779%%SWWV__*++swwu~~344779d++58!!  rT   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_serversrR
  c                 4    g | ]\  }}t          ||          S r  )rY
  )r   rO   r  s      rR   r  z$list_mcp_servers.<locals>.<listcomp>"  s4     
 
 
/8tSc**
 
 
rT   )hermes_cli.mcp_configr[
  r  r  rq  )r|  r[
  rR
  s      rR   list_mcp_serversr^
  "  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   |||          st          dd| d          	 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_serverr   r   zServer name is requiredr   r1  Server 'z' already existszBProvide either a URL (HTTP/SSE server) or a command (stdio server)r   rO
  rf   r  r   z1' rejected: suspicious command/args configurationzPOST /api/mcp/servers failed)r]
  r[
  r`
  rO   r   r4   r  r|  r   rO
  rf   rb  r  rg  r   rW   rM   r  r  rY
  )r  r|  r[
  r`
  rO   r+  server_configr  s           rR   add_mcp_serverrc
  "  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 	 	##D-88 # #]d]]]   	 	 	 	 	 	 	 	 	 	 	 	 	 	 	     G G G5666CHH===3FG t]333sN   A&&A*-A*?F "F8F FF FF G"$9GG"c                    K   ddl m} t          | j        p|          5   || j                  \  }}ddd           n# 1 swxY w Y   |s$t          dd                    |                    ddiS )	u  Replace the entire ``mcp_servers`` map (the GUI mcp.json editor's save).

    The generic ``/api/config`` endpoint deep-merges maps, so it can never
    delete a server key, drop an ``enabled: false`` flag, or remove a nested
    field — edits looked saved but the stale entry survived on disk.  This
    endpoint sets the whole map so removals actually persist.  Storage stays
    the config.yaml ``mcp_servers`` key the CLI/TUI already read.
    r   )_replace_mcp_serversNr   ; r   r7  T)r]
  re
  r  r|  rR
  r4   r  )r  r|  re
  r7  issuess        rR   replace_mcp_serversrh
  ,#  s       ;:::::	/	0	0 8 8))$,77
F8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 GDIIf4E4EFFFF$<s   ?AA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   ra
  ' not foundr   r7  T)r]
  rj
  r  r4   )rO   r|  rj
  r  s       rR   remove_mcp_serverrl
  ?#  s      888888		 	  + +$$T**+ + + + + + + + + + + + + + + R4Pt4P4P4PQQQQ$<r  z/api/mcp/servers/{name}/testc                 (   	
K   ddl m}mm t	                    5   |            ddd           n# 1 swxY w Y    vrt          dd  d          i 	                              d          d	k    
	 
fd
}	 t          j        |           d{V \  }}n*# t          $ r}dt          |          g dcY d}~S d}~ww xY w|sddg dS dd |D             	                    dd          	                    dd          dS )zFConnect to the server, list its tools, disconnect.  Returns tool list.r   )r[
  _oauth_tokens_present_probe_single_serverNr   ra
  rk
  r   r   oauthc                      t                    5                      } r           nd}| |fcd d d            S # 1 swxY w Y   d S )N)detailsT)r  )	r  token_presentrn
  ro
  rr
  rO   needs_oauth_tokenr|  rR
  s	     rR   _probe_scopedz&test_mcp_server.<locals>._probe_scoped^#  s     #7++ 	( 	(((wt}gNNNE;LV11$777RVM-'	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(s   'AA	A	Fr7  r^  r  u1   OAuth authentication required — no token found.Tc                     g | ]
\  }}||d S rO   r  r  r   r  r  s      rR   r  z#test_mcp_server.<locals>.<listcomp>#  s$    BBBDAq1Q//BBBrT   prompts	resources)r7  r  r{
  r|
  )r]
  r[
  rn
  ro
  r  r4   r   rj   r  rW   r  )rO   r|  r[
  ru
  r  rs
  r  rn
  ro
  rr
  rt
  rR
  s   ``     @@@@@rR   test_mcp_serverr}
  J#  s&               
	 	  % %""$$% % % % % % % % % % % % % % %74Pt4P4P4PQQQQ G  ))&11W<( ( ( ( ( ( ( ( ( ( ( 	
 &-%6}%E%EEEEEEE}} 
 
 
XX
 
 	
 	
 	
 	
 	
 	

  
H
 
 	
 BBEBBB;;y!,,[[a00	  s+   :>>B) )
C3CCCz/api/mcp/servers/{name}/authc                    	
K   ddl m}mm	m
 t                    5   |            }ddd           n# 1 swxY w Y    |vrt          dd  d          t          |                                        d          st          d	d
                              d          r*                    d          dk    rt          d	d          dd<   	
 fd}	 t          j
        |           d{V S # t          $ rE}t          |          }|                                }d|v rd|v sd|v rd  d}d|g dcY d}~S d}~ww xY w)u  Run the OAuth flow for an HTTP MCP server (opens the system browser).

    Mirrors ``hermes mcp login``: wipe cached OAuth state so the probe forces
    a fresh browser flow, connect, then verify a token actually landed on disk
    (some providers serve tools/list unauthenticated — see
    ``_reauth_oauth_server``).  Blocks until the browser flow completes, so it
    runs in a worker thread.  ``auth: oauth`` is persisted only on success.
    r   )r[
  rn
  ro
  r`
  Nr   ra
  rk
  r   r   r   z2stdio servers authenticate via env keys, not OAuthr   r   rp
  uB   This server uses header/API-key auth, not OAuth — check its key.c                     ddl m} m} t                    5   |            5   | 
          }|                                }	 ddlm}  |                                
           n# t          $ r Y nw xY w	  
	d          }n$# t          $ r |	                    |            w xY w 
          s3|	                    |           ddg dcd d d            cd d d            S  
	           d	d
 |D             dcd d d            cd d d            S # 1 swxY w Y   d d d            d S # 1 swxY w Y   d S )Nr   )HermesTokenStorageforce_interactive_oauth)get_manageri;  )connect_timeoutFu   The server responded, but no OAuth token was obtained — this provider may require a manually-registered OAuth client (see `hermes mcp login`).rv
  Tc                     g | ]
\  }}||d S rx
  r  rz
  s      rR   r  z1auth_mcp_server.<locals>._run.<locals>.<listcomp>#  s$    JJJDAq1Q77JJJrT   )r7  r  )
tools.mcp_oauthr
  r
  r  snapshottools.mcp_oauth_managerr
  removerW   restore)r
  r
  storager  r
  r  rn
  ro
  r`
  r  rO   r|  s         rR   _runzauth_mcp_server.<locals>._run#  s   OOOOOOOO #7++ $	 $	-D-D-F-F $	 $	((..G %%''F??????$$T****   
 -,T3LLL   ''' )(.. 
''';   /$	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	@ T3'''JJEJJJ C$	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	 $	s|   D6 D#A)(D)
A63D5A66D:B	D	!B**(DD6+DD6D"	"D6%D"	&D66D:=D:403regist	forbiddenr+  u  ' only allows pre-approved OAuth clients — it rejected client registration (403), so no browser flow can start. Options: add a pre-registered client to this server's entry (oauth: {client_id: ..., client_secret: ...}), or use the provider's stdio / API-key server instead.Frv
  )r]
  r[
  rn
  ro
  r`
  r  r4   rg  r   rj   r  rW   r  r   )rO   r|  r[
  rR
  r
  r  msgr4  rn
  ro
  r`
  r  s   ``      @@@@rR   auth_mcp_serverr
  #  sd                 
	 	  % %""$$% % % % % % % % % % % % % % %74Pt4P4P4PQQQQ
wt}

C775>> 
G
 
 
 	
 wwy 
cggfoo88W
 
 
 	
 CK+ + + + + + + + + +Z8&t,,,,,,,,, 8 8 8#hh
 ))++C<<X00K74J4J=D = = =  cB777777778s+   ;??)D 
E:EEEc                   4    e Zd ZU eed<   dZee         ed<   dS )MCPEnabledToggler  Nr|  r}  r~  r  r   r  r|  r   r  r  rT   rR   r
  r
  #  /         MMM!GXc]!!!!!rT   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   ra
  rk
  r   r   zMalformed server configr  NTr7  rO   r  )
r  r|  r   r   rd  rg  r4   r   r  r   )rO   r  r|  r  rR
  s        rR   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 ]U}|j	        }|j
        }|j        }|                    i d	|j        d
|j        d|j        d|j        dt#          |dd          dd t#          |dg           pg D             d|j        dt          |j        pg           d|j        d|r|j        ndd|r|j        ndd|rt          |j                  ng d|j        j        t          |j        j                  ndd|j        pdd|j        dud|                    |j        d          d         d|                    |j        d          d                     Wn4# 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 r  )rO   is_installedrk  )r   r  r
  s     rR   r  z$list_mcp_catalog.<locals>.<dictcomp>$  sT        11!&99;;Q;QRSRX;Y;YZ  rT   rO   r  r   rX
  r  r  noner%  c                 8    g | ]}|j         |j        |j        d S )rO   rA   r  r
  r   r  s     rR   r  z$list_mcp_catalog.<locals>.<listcomp>+$  s8     ! ! ! VqxQZPP! ! !rT   r  rO
  rf   r   install_urlinstall_ref	bootstrapdefault_enabledpost_installr   needs_install	installed)FFr  r   zlist_mcp_catalog failedc                 "    g | ]\  }}}|||d S ))rO   r"  r  r  )r   r  r>  r  s       rR   r  z$list_mcp_catalog.<locals>.<listcomp>K$  s8     
 
 
Aq a00
 
 
rT   )r  diagnostics)rX   r
  rW   rM   r  r4   r  rb  list_catalogr   rX
  installr  rO   r  r   r  r   rO
  rf   r   r  r
  r  r
  r
  r   catalog_diagnostics)r|  r  r  catalog_entriesinstalled_staterx  r   rX
  r
  r
  r
  s             @rR   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ImGNN 
u0 %, Y^	
 WT66::  ! !$T5"55;! ! ! 9,  Y^1r22!" y}#& g?w{{4'( g?w{{4)* GT'"3444R+. ";.: $((C#D#D#D34  2 8b56  d!:78 _00^LLQO9: ?..uz>JJ1M;    	"	F     2 2 20111112 K
 
(<<>>
 
 
     {;;;s]    
A	.AA	G? 0BG? BG? !B"EG? ?.H0/H06I 
I"!I"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   r  TenableNr|  )r}  r~  r  r  r  r  r   r
  r   r|  r   r  rT   rR   r
  r
  U$  sW         
IIICc3hFD!GXc]!!!!!rT   r
  z/api/mcp/catalog/installc                 Z   	
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        kt          |          }	 t          t          	          d	d
|gz   |          }n0# t
          $ r  t          $ r}t          dd|           d}~ww xY w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
  r   Nr   zNo catalog entry 'r+  r   rh  r
  r  zInstall failed: T)r7  rO   
backgroundrA  c                      t                    5                       j                   d d d            d S # 1 swxY w Y   d S )N)r
  )r  install_entryr
  )r  r  rx  r
  s   rR   _install_scopedz2install_mcp_catalog_entry.<locals>._install_scoped$  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)r7  rO   r
  )rX   r
  rO   r   	get_entryr4   r|  r  r  rq  r   r
  _mcp_install_action_namer  r  rW   rj   r  rM   r  r  )r  r|  rO   r>  r?  rA  rf  r  r
  r  rx  r
  s   `        @@@rR   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OOO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#C8 8D%D  D%8E F$'8FF$c                 ^   t          j        dd|                                                               d          dd         pd}t	          j        |                                                                           dd         }d| d| }t          	                    |d| d	           |S )
zUnique per-entry mcp-install action name (+ registered log file), so a
    re-click or a second catalog install doesn't overwrite the first's tracked
    process/log while its git clone is still running.
[^a-z0-9]+r  N0   serverr  zmcp-install-action-.log
r3  rB  r   r   hashlibsha1r   	hexdigestr  r  )rO   r  digestrA  s       rR   r
  r
  $  s     6-djjll3399#>>ssCOxD\$++--((2244RaR8F+D++6++F  )?6)?)?)?@@@MrT   computer-use-grantzaction-computer-use-grant.logc                   $    e Zd ZU eed<   eed<   dS )PairingApprover  r  Nr  r  rT   rR   r
  r
  $  s"         MMM
IIIIIrT   r
  c                   $    e Zd ZU eed<   eed<   dS )PairingRevoker  r$  Nr  r  rT   rR   r
  r
  $  s"         MMMLLLLLrT   r
  c                  "    ddl m}   |             S )Nr   PairingStore)gateway.pairingr
  r
  s    rR   _pairing_storer
  $  s     ,,,,,,<>>rT   z/api/pairingc                  t   K   t                      } |                                 |                                 dS )N)r  r  )r
  list_pendinglist_approved)stores    rR   list_pairingr
  $  s>      E%%''''))  rT   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          )Nr   r   zplatform and code are requiredr   T)r7  r   r  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  r4   approve_code_is_locked_out)r  r
  r  r  r  s        rR   approve_pairingr
  $  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   rT   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
          )Nr   r   z!platform and user_id are requiredr   r7  Tr   zUser z  not found in approved list for rm  )r
  r  r   r   r$  r4   revoke)r  r
  r  s      rR   revoke_pairingr
  $  s      E#**,,2244H Y4< Y4WXXXX||Hdl++ d|
Pt|PPXPPP   rT   z/api/pairing/clear-pendingc                  T   K   t                      } |                                 }d|dS )NT)r7  r  )r
  clear_pending)r
  r=  s     rR   clear_pending_pairingr
  $  s0      E!!E5)))rT   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  eventsrA   rD  rW  r	  Fdeliver_onlydeliver_chat_idr7  )r}  r~  r  r  r  r  r   r
  r   rA   rD  r	  r
  r   r
  r7  r  rT   rR   r
  r
   %  s         
III!%K#%%%FDI FHSM   FDIGSL$%)OXc]))) FHSM     rT   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  r   r
  r	  rW  r
  rA   rD  r  z
/webhooks/r7  r  TF)rO   r  r
  r	  r
  rA   rD  r  r   
secret_setr  )r   rb  r   )rO   r
  r  s      rR   _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:  rT   z/api/webhooksc                     K   dd l m}  |                                 |                                 }|                                 fd|                                D             dS )Nr   c                 8    g | ]\  }}t          ||          S r  )r
  )r   rO   r
  r  s      rR   r  z!list_webhooks.<locals>.<listcomp>(%  s9     
 
 
e #499
 
 
rT   )r  r  subscriptions)hermes_cli.webhookrw  _get_webhook_base_url_load_subscriptions_is_webhook_enabledrq  )whsubsr  s     @rR   list_webhooksr
  %  s      ######''))H!!##D))++
 
 
 
#zz||
 
 
  rT   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 )	Nrw  Tz0Failed to enable webhook platform from dashboardr  z"Failed to enable webhook platform.r   r0  )r7  r  r  r  )r  rW   rM   r  r4   r8  )r  r  s     rR   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   r   rp  r  z^[a-z0-9][a-z0-9_-]*$zBInvalid name. Use lowercase alphanumeric with hyphens/underscores.rW  uK   Direct delivery requires a real target (telegram, discord, …), not 'log'.r   z Dashboard-created subscription: c                 ^    g | ]*}|                                 |                                 +S r  r   r
  s     rR   r  z"create_webhook.<locals>.<listcomp>a%  -    ???QWWYY?17799???rT   c                 ^    g | ]*}|                                 |                                 +S r  r   r  s     rR   r  z"create_webhook.<locals>.<listcomp>d%  r
  rT   z%Y-%m-%dT%H:%M:%SZ)r  r
  r7  rA   rD  r	  r  Tr
  chat_iddeliver_extrar7  )r3  r4  r   r
  rw  r
  r4   rO   r   r   rt  r6  r
  r	  r7  r  r  r
  rA   rD  r  gmtimer
  r
  _save_subscriptionsr
  r
  )r  _re_secrets_timer
  rO   r7  r
  r
  r  r[  s              rR   create_webhookr   D%  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NrT   z/api/webhooks/{name}c                    K   dd l m} | pd                                                                }|                                }||vrt          dd| d          ||= |                    |           ddiS )	Nr   r   r   No subscription named 'r+  r   r7  T)r
  rw  r   r   r
  r4   r
  )rO   r
  rv  r
  s       rR   delete_webhookr  x%  s      ######:2




$
$
&
&C!!##D
$4Tc4T4T4TUUUUS	4   $<rT   c                       e Zd ZU eed<   dS )WebhookEnabledToggler  Nr|  r  rT   rR   r  r  %  r	  rT   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   Nr   r   r  r+  r   r  Tr
  )	r
  rw  r   r   r
  r4   r   r  r
  )rO   r  r
  rv  r
  s        rR   set_webhook_enabledr  %  s       $#####:2




$
$
&
&C!!##D
$4Tc4T4T4TUUUU--DIi4   T\0B0BCCCrT   z/api/gateway/startc                    K   	 t          t          | d          d          }nJ# t          $ r  t          $ r3}t                              d           t          dd|           d }~ww xY wd|j        ddS )	NrP   r  zFailed to spawn gateway startr  zFailed to start gateway: r   Tr  r  r  r4   rW   rM   r  r  r|  rf  r  s      rR   start_gatewayr  %  s      W#$7$I$I?[[    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          t          | d          d          }nJ# t          $ r  t          $ r3}t                              d           t          dd|           d }~ww xY wd|j        ddS )	Nstopr  zFailed to spawn gateway stopr  zFailed to stop gateway: r   Tr  r	  r
  s      rR   stop_gatewayr  %  s      V#$7$H$H.YY    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 )CredentialPoolAddrQ   r  Nr  )r}  r~  r  r  r  r  r   r  rT   rR   r  r  %  s:         MMM LLLE8C=rT   r  rQ	  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_  r   r  Nr  r  r   r  r   last_statusrequest_countr  )
rQ	  r  r  r  r   r  r  r  r4  has_refresh)r   r$   r   )rx  rQ	  r   s      rR   _pool_entry_summaryr  %  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  rT   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 r  )r  )r   ir  s      rR   r  z(list_credential_pool.<locals>.<listcomp>%  s4       .2a#Aq))  rT   r   )rP   )rQ   r  r  )r  r  r  r  r  r  rW   rM   r  r  r  r  )r  r  r  raw_poolr\  r  r  s          rR   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  r   r   z!provider and api_key are requiredr   zkey #r   r'  )rQ   r  r  r  r  r   r_  z!POST /api/credentials/pool failedTr7  rQ   r=  )r  r  r  r  r  r  rQ   r   r   r  r4   r  r   r  r  r  r  rW   rM   r  r  )r  _uuidr  r  r  r  rQ   r  r  r  rx  r  s               rR   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  r   z#DELETE /api/credentials/pool failedr   r   Nr   zNo pool entry at that indexTr   )r  r  r   r   remove_indexrW   rM   r  r4   r  r   r  )rQ   rQ	  r  r  r  r  s         rR   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 )MemoryProviderSelectrQ   Nr  r  rT   rR   r'  r'  =&  s         MMMMMrT   r'  c                       e Zd ZU dZeed<   dS )MemoryResetr  re   N)r}  r~  r  re   r  r  r  rT   rR   r)  r)  B&  s"         FCrT   r)  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_providersr   rW  rQ   )rO   r  r  z discover_memory_providers failedmemories))	MEMORY.mdrW  )USER.mdr   )r  r  builtin_files)plugins.memoryr,  r   r   rd  rg  r  r  r   rW   rM   r  r   r  r  r  )r,  r  r  memr  rO   r  r  mem_dirfilesfnamerv  r   s                rR   get_memory_statusr6  G&  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 )Nr   >   built-inr
  r,  r   r+  c                     h | ]\  }}}|	S r  r  )r   rO   _d_cs       rR   r  z&set_memory_provider.<locals>.<setcomp>s&  s    FFF,$BFFFrT   r   zUnknown memory provider 'z4'. Run `hermes memory setup` to configure a new one.r   rW  rQ   Tr7  r  )rQ   r   r   r1  r,  r4   r   rd  r   rg  r   )r  rQ   r,  rD	  r  s        rR   set_memory_providerr=  j&  s      #**,,H~~::: <<<<<<FF*C*C*E*EFFF5  q8qqq   
 --Ccggh''.. H (CM*(+++rT   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 )Nr  >   r  r   rW  r   z#target must be all, memory, or userr   r-  >   r  rW  r.  >   r  r   r/  r  zCould not delete : Tri	  )	re   r   r   r4   r   r  r  rH  rm  )r  re   r3  rj	  r  r5  r   r  s           rR   reset_memoryr@  &  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   Tr  r  rW   rM   r  r4   r  r  s     rR   
run_doctorrC  &  s      T#XJ99 T T T/0004RS4R4RSSSST tx:::s    
A.AA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 )
NrA  auditr  zFailed to spawn security auditr  zFailed to run security audit: r   Tr  rB  r  s     rR   run_security_auditrF  &  s      \#Z$9;KLL \ \ \78884ZUX4Z4Z[[[[\ tx1ABBB    
A.AAc                   *    e Zd ZU dZee         ed<   dS )BackupRequestNr	  )r}  r~  r  r	  r   r  r  r  rT   rR   rI  rI  &  s&          FHSM     rT   rI  c                  $    t                      dz  S )Nbackups)r   r  rT   rR   _dashboard_backup_dirrL  &  s    y((rT   c                      t          j                                        d          } t                      d|  dt	          j        d           dz  S )Nz%Y-%m-%d-%H%M%Szhermes-backup-r  r   .zip)r   r  r  rL  r4  r  )stamps    rR   _new_dashboard_backup_pathrP  &  sM    LNN##$566E ""%Xe%X%Xg>OPQ>R>R%X%X%XXXrT   z/api/ops/backupc                 F  K   dg}d }| j         r-|                    | j                                                    ntt                      }	 |j                            dd           n&# t          $ r}t          dd|           d }~ww xY w|                    t          |                     	 t          |d          }n@# t          $ r3}t                              d           t          dd|           d }~ww xY wd|j        dd	}|t          |          |d
<   |S )Nr  Tr  r  z#Could not create backup directory: r   zFailed to spawn backupzFailed to run backup: r  archive)r	  r  r   rP  r  r  rm  r4   r  r  rW   rM   r  r  )r  rf   rR  r  rf  r  s         rR   
run_backuprS  &  sb     :D"G{ "DK%%''((((,..	N   ==== 	 	 	BSBB   	
 	CLL!!!T#D(33 T T T/0004RS4R4RSSSST 48X>>H!'llOs0   A( (
B2BB1C 
C?.C::C?z/api/ops/backup/downloadrR  c                 2  K   	 t                                                                          d          }t          |                                                               d          }n@# t          $ r t          dd          t          t          f$ r t          dd          w xY wt          ||          st          d	d
          |	                                st          dd          t          t          |          d|j        d          S )NFrb  Tr   zBackup not foundr   r   zInvalid backup pathr  z0Backup is outside the dashboard backup directoryzapplication/zipr(  r)  )rL  ri  rl  r   r  r4   rm  rn  r  r  r:   r  rO   )rR  
backup_dirre   s      rR   download_dashboard_backuprV  &  s:     K*,,7799AAAOO
g))++3343@@ H H H4FGGGG\" K K K4IJJJJK *f-- h4fgggg>> H4FGGGG[[$!-	   s   A)A. .=B+c                   (    e Zd ZU eed<   dZeed<   dS )ImportRequestrR  Fr  Nr  r  rT   rR   rX  rX  &  s-         LLL E4rT   rX  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 )Nr   r   zarchive path is requiredr   r   zArchive not found: r  --forceFailed to spawn importr  Failed to run import: Tr  )rR  r   r4   rq   r   r  r  r  r  rW   rM   r  r  )r  rR  rf   rf  r  s        rR   
run_importr]  '  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r+  c                    t          | pd          j                                        }t          j        dd|                              d          }|sd}|                                                    d          s| d}|S )Nz
backup.zipz[^A-Za-z0-9._-]+r  z.-rN  )r   rO   r   r3  rB  r   r  )r+  rO   s     rR   _safe_backup_upload_namer_  '  s    (L)).4466D6%sD1177==D ::<<  (( }}}KrT   z/api/ops/import-uploadc                 V  K   t                      }	 |                    dd           n&# t          $ r}t          dd|           d }~ww xY wt	          | j                  }t          j        t          j	                  
                    d          }|d| dt          j        d	           d| z  }t          j        d
|j         d
dt!          |                    \  }}t#          |          }	d}
d}	 t%          j        |d          5 }	 |                     t*                     d {V }|snD|
t-          |          z  }
|
t.          k    rt          dd          |                    |           g	 d d d            n# 1 swxY w Y   t%          j        |	|           d}nJ# t          $ r  t4          $ r t          dd          t          $ r}t          dd|           d }~ww xY w	 |s|	                    d           |                                  d {V  n7# |s|	                    d           |                                  d {V  w xY wt;          j        |          s'|                    d           t          dd          dt!          |          g}|r|                    d           	 tA          |d          }n@# tB          $ r3}tD          #                    d           t          dd|           d }~ww xY wd|j$        dt!          |          |
dS )NTr  r  z+Could not create import staging directory: r   z%Y%m%d-%H%M%Szdashboard-import-r  r   rm  r=  r>  r   Fr@  r  zArchive is too larger  z(Import staging directory is not writablez"Could not write uploaded archive: rA  r   z(Uploaded archive is not a valid zip filer  rZ  r[  r\  )r7  r  rO   rR  uploaded_bytes)%rL  r  rm  r4   r_  r+  r   r  r   r  r  r4  r  rC  rD  rO   r  r   rq   rE  r  rF  r   r  rG  rt  r  rH  r   zipfile
is_zipfiler  r  rW   rM   r  r  )r;  r  staging_dirr  	safe_namerO  re   rI  rJ  rK  rL  rM  r  rN  rf   rf  s                   rR   run_import_uploadrf  #'  sA     
 ())K
$6666 
 
 
FFF
 
 
 	

 )77IL&&//@@EYuYYw7H7K7KYYiYYYF'!6;!!!  FH
 H~~HEGYvt$$ 	!!"ii(;<<<<<<<< U#222'C@VWWWW		%   ! 		! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	
8V$$$    
 
 
=
 
 
 	
  
 
 
===
 
 
 	

 	  	-OOtO,,,jjll  	-OOtO,,,jjllf%% 
&&&=
 
 
 	

 c&kk"D IT#D(33 T T T/0004RS4R4RSSSST xv;;  s{   * 
AAA9F' A)F8F' FF' FF' &H% '.G.G))G..H% %4I K 
L.L		L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matcherrO
  r  allowedrl  r  )hooksvalid_events)r  r   rC  ri  hermes_cli.pluginsrk  r  rW   iter_configured_hooksrM   r  allowlist_entry_forrm  rO
  script_is_executabler  rn  r  r   )	_load_configri  rk  rq  specsr  rK
  rx  r  s	            rR   
list_hooksrx  q'  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 )
HookCreaterm  rO
  Nrn  r  Tapprove)r}  r~  r  r  r  rn  r   r  re  r{  r   r  rT   rR   rz  rz  '  s^         JJJLLL!GXc]!!!!GXc]!!! GTrT   rz  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   rh  r   r   event and command are requiredr   rj  zUnknown event 'z
'. Valid: r#  rp  rO
  rn  Nr  FTzhook consent record failed)r7  rm  rO
  r  )rC  ri  rm  r   rO
  r4   rr  rk  r  r  rW   r   r   rd  rg  rb  rn  r  re  r  r   r{  _record_approvalrM   r  )
r  ri  rm  rO
  rk  r  	hooks_cfgr  	new_entryr  s
             rR   create_hookr  '  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 )
HookDeleterm  rO
  Nr  r  rT   rR   r  r  '  s"         JJJLLLLLrT   r  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   rh  r   r   r}  r   rp  Fc                 p    g | ]2}t          |t                    r|                    d           k    0|3S )rO
  )rd  rg  r   )r   r  rO
  s     rR   r  zdelete_hook.<locals>.<listcomp>'  sL     
 
 
q$''
,-EE),<,<,G,G ,G,G,GrT   Nr   zNo matching hook foundr7  T)rC  ri  rm  r   rO
  r4   r   r   rd  rg  rb  r   r  r   r
  rW   )r  ri  rm  r  r  r  beforerO
  s          @rR   delete_hookr  '  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).rG  r   r   r   )r   r4  bytes)r.  total_bytes)r   r  r  r  rglobr  r  r  rm  r  rO   )cp_dirr.  r  r  r  r=  rE
  s          rR   list_checkpointsr  (  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 )
NrG  pruner  z!Failed to spawn checkpoints pruner  zFailed to prune checkpoints: r   Tr  rB  r  s     rR   prune_checkpointsr  /(  s      [#]G$<>QRR [ [ [:;;;4YTW4Y4YZZZZ[ tx1DEEErG  c                   4    e Zd ZU eed<   dZee         ed<   dS )SkillInstallRequest
identifierNr|  r  r  rT   rR   r  r  C(  s/         OOO!GXc]!!!!!rT   r  c                     | pd                                 }|r|                                dv 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).
    r   >   r  r  r   r  -p)r   r   rX   r  r  r	  )r|  r  r  s      rR   r  r  H(  st     B%%''I 	))-CCC	333333###,55i@@AArT   c                 d   t          j        dd|                                                              d          dd         pd}t	          j        |                                                                          dd         }d|  d| d| }t          	                    |d| d	           |S )
ap  Unique per-skill hub action name (+ registered log file).

    ``_spawn_hermes_action`` tracks one process/log per name, so a shared
    "skills-install"/"skills-uninstall" would make concurrent row-level actions
    overwrite each other's status/log while the UI polls per identifier. Slug
    (readable) + hash (collision-proof) keys each action to its own row.
    r
  r  Nr
  r	  r  zskills-r
  r
  r
  )r  rv  r  r
  rO   s        rR   _hub_action_namer  Y(  s     6-ciikk2288==crcBMgD\#**,,''1133BQB7F+T++D++6++D  ';';';';<<<KrT   z/api/skills/hub/installc                   K   | j         pd                                }|st          dd          t          d|          }	 t	          t          | j        p|          dd|dgz   |          }nJ# t          $ r  t          $ r3}t          	                    d           t          d	d
|           d }~ww xY wd|j
        |dS )Nr   r   identifier is requiredr   r
  rD  --yeszFailed to spawn skills installr  zFailed to install skill: Tr  )r  r   r4   r  r  r  r|  rW   rM   r  r  )r  r|  r  rO   rf  r  s         rR   install_skill_hubr  h(  s     /'R..00J N4LMMMMIz22D
W#dl5g66J89
 

     W W W78884UPS4U4UVVVVW tx666   +A. .B5.B00B5c                   4    e Zd ZU eed<   dZee         ed<   dS )SkillUninstallRequestrO   Nr|  r  r  rT   rR   r  r  |(  s/         
III!GXc]!!!!!rT   r  z/api/skills/hub/uninstallc                   K   | j         pd                                }|st          dd          t          d|          }	 t	          t          | j        p|          dd|dgz   |          }nJ# t          $ r  t          $ r3}t          	                    d           t          d	d
|           d }~ww xY wd|j
        |dS )Nr   r   zname is requiredr   	uninstallrD  r  z Failed to spawn skills uninstallr  zFailed to uninstall skill: Tr  )rO   r   r4   r  r  r  r|  rW   rM   r  r  )r  r|  rO   rA  rf  r  s         rR   uninstall_skill_hubr  (  s     IO""$$D H4FGGGGk400F	Y#dl5g66(KQUW^9__
 
     Y Y Y9:::4WRU4W4WXXXXY tx888r  c                   *    e Zd ZU dZee         ed<   dS )SkillsUpdateRequestNr|  )r}  r~  r  r|  r   r  r  r  rT   rR   r  r  (  s&         !GXc]!!!!!rT   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 )
NrD  rr  r  zFailed to spawn skills updater  zFailed to update skills: r   Tr  )r|  r  r  r4   rW   rM   r  r  )r  r|  r
  rf  r  s        rR   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-index	skills-sh
well-knownr   githubclawhubclaude-marketplacelobehubz	browse-shc           	      z    | j         | j        | j        | j        | j        | j        t          | j        pg           dS )N)rO   r  r   r  trust_levelrepotags)rO   r  r   r  r  r  rb  r  )r  s    rR   _skill_meta_to_payloadr  (  s@    }(l}QV\r""  rT   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   )HubLockFiler   r  rD  z.hubz	lock.jsonr  rO   r  scan_verdict)rO   r  r  )tools.skills_hubr  r   r   r  list_installedr   rW   )r|  r  r  profile_dirlockr  rx  idents           rR   _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                     ddl m}  t          	          5   |             }d d d            n# 1 swxY w Y   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          h d          }|D ]}|o	|d         |v  |d<   |||t          	          dS )Nr   create_source_routerF)r  r  r  is_rate_limitedrate_limitedr  is_availablerJ  c                 ,    g | ]}t          |          S r  r  r   r  s     rR   r  z9list_skills_hub_sources.<locals>._run.<locals>.<listcomp>)  s.     $ $ $:;2155$ $ $rT   r      r  >   r  r  r  r  r  r  r  
searchable)sourcesindex_availablefeaturedr
  )r  r  r  	source_id_SKILL_HUB_SOURCE_LABELSr   r   r   rW   r4  r  r  r  )
r  r  r  r  r  rx	  r  rx  _api_source_idsr|  s
            rR   r
  z%list_skills_hub_sources.<locals>._run(  sq   999999"7++ 	- 	-**,,G	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	 	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 $]]]
 
  	[ 	[E'6'Y5;/;Y"ZE,. 3G<<	
 
 	
sB   .22<!BB0/B0:CC('C(3!DD$#D$Nz!skills hub sources listing failedr  zHub sources failed: r   )rj   r  r4   rW   rM   r  )r|  r
  r  s   `  rR   list_skills_hub_sourcesr  (  s      3
 3
 3
 3
 3
j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.
    r   r  source_counts	timed_outr
  c                  F   ddl m} m} t                    5   |             }d d d            n# 1 swxY w Y   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  r  r   )r  r  overall_timeoutr  )r,  trusted	communityc                 ,    g | ]}t          |          S r  r  r  s     rR   r  z3search_skills_hub.<locals>._run.<locals>.<listcomp>Q)  s!    CCCa.q11CCCrT   r  )r  r  r  r  rr  r  r  r   r  rb  r  r  )r  r  r  cappedall_resultsr  r  _rankr  r]	  dedupedr  r|  r  r   s              rR   r
  zsearch_skills_hub.<locals>._run<)  s   RRRRRRRR"7++ 	- 	-**,,G	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	-S]]B''0G0G5%QS1
 1
 1
-]I
 !!<< 	' 	'A|4''%&Q\""1=!,,uyyal9K9WYZ/[/[[[%&Q\"t{{}}%%gvg. DC7CCC*"3G<<	
 
 	
s   044Nzskills hub search failedr  zHub search failed: r   )r   rj   r  r4   rW   rM   r  )r  r   r  r|  r
  r  r  s    ```  @rR   search_skills_hubr  -)  s       W"OOE VRTUUU
 
 
 
 
 
 
 
6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                 P  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 )a  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.

    Scoped to ``profile`` so a non-default profile with different hub taps
    resolves against ITS source router, not the default profile's.
    r   r   r  r   c                     ddl m}  ddlm} t	                    5   |            } | |          \  }}}d d d            n# 1 swxY w Y   |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  r   ri  z(binary file)SKILL.mdrO   r  r   r  r  r  r  r  )	rO   r  r   r  r  r  r  skill_mdr4  )hermes_cli.skills_hubr  r  r  r  r4  rq  rd  r  r  UnicodeDecodeErrorr   r   rb  r  r  )r  r  r  r  bundle_srcr4  r  relr   r  r  r|  s              rR   r
  zpreview_skill_hub.<locals>._runp)  sC   IIIIII999999"7++ 	Q 	Q**,,G!@!@!P!PD&$	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q  	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#   AAAB**B<;B<Nzskills hub preview failedr  zHub preview failed: r   Skill not found: r   r4   rj   r  rW   rM   r  r  r|  r
  r  r  r  s    `   @rR   preview_skill_hubr  `)  s       2$$&&E N4LMMMM%
 %
 %
 %
 %
 %
N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                 P  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 )uT  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).

    Scoped to ``profile`` so the bundle resolves against that profile's hub
    source router, matching where an install would pull it from.
    r   r   r  r   c                     dd l } ddlm} ddlm}m} ddlm}m} t                    5   |            } ||          \  }}}	d d d            n# 1 swxY w Y   |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  r   )r   T)ignore_errorsF)r  allowr.  blockc                 P    g | ]#}|j         |j        |j        |j        |j        d $S )severityr	  r;  r_  r  r  )r   rE
  s     rR   r  z0scan_skill_hub.<locals>._run.<locals>.<listcomp>)  sJ     	
 	
 	
  JJ } 	
 	
 	
rT   )criticalr<  r;  r:  r   )
rO   r  r   r  verdictr[  r  policy_reasonfindingsseverity_counts)rT  r  r  r  r  r  tools.skills_guardr  r  r  r   r   rU  r  r  
skill_namer  r  r[  )_shutilr  r  r  r  r  r  r  r  r  scan_sourceq_pathr  ro  reasonr  r  countsrE
  r  r|  s                      rR   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"7++ 	Q 	Q**,,G!@!@!P!PD&$	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q  	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   AAAC C$Nzskills hub scan failedr  zHub scan failed: r   r  r  r  s    `   @rR   scan_skill_hubr  )  s      2$$&&E N4LMMMMB
 B
 B
 B
 B
 B
H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         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
<   g Zed         ed<   g Zee         ed<   g Zee         ed<   dS )ProfileCreaterO   N
clone_fromFclone_from_default	clone_all	no_skillsr  rQ   r  rN
  r
  keep_skills
hub_skills)r}  r~  r  r  r  r  r   r  r   r  r  r  rQ   r  r
  r   r  r  r  rT   rR   r
  r
  *  s         
III $J$$$  %$$$ItIt!%K#%%%"Hhsm"""E8C=
 ,.K'(---
  Kc
 JS	rT   r
  c                       e Zd ZU eed<   dS )ProfileRenamenew_nameNr  r  rT   rR   r  r  !*  s         MMMMMrT   r  c                       e Zd ZU eed<   dS )ProfileSoulUpdater   Nr  r  rT   rR   r  r  %*  s         LLLLLrT   r  c                       e Zd ZU eed<   dS )ProfileActiveUpdaterO   Nr  r  rT   rR   r  r  )*  r  rT   r  c                       e Zd ZU dZeed<   dS )ProfileDescriptionUpdater   r  N)r}  r~  r  r  r  r  r  rT   rR   r  r  -*  s"         KrT   r  c                   $    e Zd ZU eed<   eed<   dS )ProfileModelUpdaterQ   r  Nr  r  rT   rR   r  r  1*  s"         MMMJJJJJrT   r  c                       e Zd ZU dZeed<   dS )ProfileDescribeAutoFr  N)r}  r~  r  r  r   r  r  rT   rR   r  r  6*  s"         ItrT   r  c                 H    	 t          | |          S # t          $ r |cY S w xY wr8  )r   rW   )rN   rO   r  s      rR   _profile_attrr  :*  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   r   r   
is_defaultFr  rQ   has_envskill_countr   r  r  description_autodistribution_namedistribution_versiondistribution_source
alias_pathrO   r   r!  r  rQ   r"  r#  r  r  r$  r%  r&  r'  	has_alias)r  r  r   re  )rN   s    rR   r	  r	  A*  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  rT   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r8  )rW   )	callable_r  s     rR   _safez&_fallback_profile_dicts.<locals>._safeU*  s7    	9;; 	 	 	NNN	s   	 c                  .                                    S r8  _read_config_modeldefault_homer  s   rR   r  z)_fallback_profile_dicts.<locals>.<lambda>^*  s    (G(G(U(U rT   NNr  Tr2  c                  .                                    S r8  _count_skillsr2  s   rR   r  z)_fallback_profile_dicts.<locals>.<lambda>f*  s    )C)CL)Q)Q rT   r   c                  .                                    S r8  _check_gateway_runningr2  s   rR   r  z)_fallback_profile_dicts.<locals>.<lambda>g*  s    \-P-PQ]-^-^ rT   Fc                  V                                                        dd          S Nr  r   read_profile_metar   r2  s   rR   r  z)_fallback_profile_dicts.<locals>.<lambda>h*  s'    )G)G)U)U)Y)YZgik)l)l rT   r   c                  V                                                        dd          S Nr$  Fr=  r2  s   rR   r  z)_fallback_profile_dicts.<locals>.<lambda>i*  s'    l.L.L\.Z.Z.^.^_qsx.y.y rT   r)  c                 .                         |           S r8  r0  rx  r  s    rR   r  z)_fallback_profile_dicts.<locals>.<lambda>u*  s    8W8WX]8^8^ rT   c                 .                         |           S r8  r6  rB  s    rR   r  z)_fallback_profile_dicts.<locals>.<lambda>}*  s    9S9STY9Z9Z rT   c                 .                         |           S r8  r9  rB  s    rR   r  z)_fallback_profile_dicts.<locals>.<lambda>~*  s    \=`=`af=g=g rT   c                 V                         |                               dd          S r<  r=  rB  s    rR   r  z)_fallback_profile_dicts.<locals>.<lambda>*  s(    9W9WX]9^9^9b9bcprt9u9u rT   c                 V                         |                               dd          S r@  r=  rB  s    rR   r  z)_fallback_profile_dicts.<locals>.<lambda>*  s2    l>\>\]b>c>c>g>ghz  }B  ?C  ?C rT   )_get_default_hermes_homer  r  r  r  _get_profiles_rootr  r  _PROFILE_ID_REr6  rO   )r  r.  r  r  rQ   profiles_rootrx  r3  s   `      @rR   r	  r	  T*  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%)(,'+"     " OrT   c                    ddl m} 	 |                    |            n0# t          $ r#}t	          dt          |                    d}~ww xY w|                    |           st	          dd|  d          |                    |           S )	zIValidate ``name`` and resolve to its directory or raise an HTTPException.r   r  r   r   Nr   r	  r	  )rX   r  r	  rg  r4   r  r	  r  )rO   r  r  s      rR   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    rR   _profile_setup_commandrM  *  s*    !Y..>>tOOOCrT   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   r	  r	  r  N)	r  r	  r	  r  r  r   r  r   r   )r  rQ   r  r	  r	  r   r  s          rR   _write_profile_modelrP  *  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rR
  c                    ddl m}m} ddl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 ||
|          }|r0t"                              d|
d                    |                     |||
<   |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   rO  )validate_mcp_server_entryr
  r   r   rO
  rf   r  r   z,Profile-create: skipping MCP server '%s': %srf
  r   N)r  r	  r	  hermes_cli.mcp_securityrR  r  r   r  rO   r   r   rO
  rf   rb  r  rg  r   rM   r  r  r   r  )r  rR
  r	  r	  rR  writtenr   r  rh  r
  rO   rx  rg
  s                rR   _write_profile_mcp_serversrU  *  s    VUUUUUUUAAAAAA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  ..tU;;F KTSWS\S\]cSdSdeeeCIqLGG 	 	 GGM4(((""5))))""5))))Ns   D<E2 2E?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   rO  get_disabled_skillssave_disabled_skillsc                 b    h | ],}||                                 |                                 -S r  r   r  s     rR   r  z-_disable_unselected_skills.<locals>.<setcomp>*  s2    ;;;a1;;		;;;rT   rD  r  r   )r  r	  r	  hermes_cli.skills_configrY  rZ  r  r  r  r  r  rO   r   r  )r  rV  r	  r	  rY  rZ  keep_setdisabled_countr   r
  skills_rootmdr  r  rO   s                  rR   _disable_unselected_skillsra  *  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}  	 t          j                    }|                    d | j                   d {V }dd |D             iS # t          $ r. t                              d           dt          |           icY S w xY w)Nr   r  r  c                 ,    g | ]}t          |          S r  r	  r  s     rR   r  z*list_profiles_endpoint.<locals>.<listcomp>+  s!    CCCQ-a00CCCrT   z@GET /api/profiles failed; falling back to profile directory scan)
rX   r  rj   r  rp   r  rW   rM   r  r	  )r  r  r  s      rR   list_profiles_endpointrd  +  s      333333C'))--dL4NOOOOOOOOCC(CCCDD C C CYZZZ3LAABBBBCs   AA 5BBc           	        K   ddl m} | j        pd                                }|rd}|}| j         }n| j        rd}d}d}n| j        }|rdnd }|}	 |                    | 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|dgt=          d|                    }|                    ||j         d           c# t$          $ r< t&                              d|| j                   |                    |d d           Y w xY wd| j        t#          |          ||||dS )Nr   r  r   Tr  F)rO   r  r  clone_configr  r  )quietr   r   zPOST /api/profiles failedr  z'Setting model for new profile %s failedz-Writing MCP servers for new profile %s failedz2Applying skill selection for new profile %s failedr  rD  r
  r  )r  r  z7Spawning hub-skill install %s for new profile %s failed)r7  rO   r   	model_setmcp_writtenskills_disabledhub_installs)!rX   r  r  r   r  r  create_profilerO   r  r  seed_profile_skillscheck_alias_collisioncreate_wrapper_scriptrg  FileExistsErrorr  r4   r  rW   rM   r  rQ   r  rP  r
  rU  r  ra  r  r  r  r  r  )r  r  explicit_sourcecloner  rf  r   	collisionr  rQ   r  rh  ri  rj  rk  r  r  rf  s                     rR   create_profile_endpointrt  +  s8     333333,"3355O  $
>)	 	 
'"'1YYT
<**!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(IugF E22 D uTX F FGGGG 	D 	D 	DNNI	  
 uT B BCCCCC	D 	D		"*$  so   A>C E'DE8E

EF# #*GGG3 3*H H -I *I0/I0AK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   r  r  )r  r  )rX   r  get_active_profilerW   get_active_profile_name)r  r  r  s      rR   get_active_profile_endpointrx  +  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   r  r   r   Nr   z POST /api/profiles/active failedr  Tr<  )rX   r  set_active_profilerO   r  r4   r  rg  rW   rM   r  r	  )r  r  r  s      rR   set_active_profile_endpointr{  +  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 )NrO
  )rM  r  s    rR   get_profile_setup_commandr}  +  s      -d3344rT   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/crP   r   rs  \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)r  r  r   r   z$No supported terminal emulator foundr   r   z*POST /api/profiles/%s/open-terminal failedr  T)r7  rO
  )rM  r  r  r   r  r  rt  callr  r4   r  r  rg  rW   rM   r  )rO   rO
  escapedapplescriptterminal_commandsr  
popen_argsr  s           rR   open_profile_terminal_endpointr  +  s     0<(..<""5)) $	iwGDEEEE\X%%oodF33;;CGGG%    k4=>>>> ')>dESZ([\!$4dD%#QRYdE7CD!$4d<Qw<Q<Q<Q#RS ?D:OW:O:O:O"PQd4Iw4I4I4IJK7D$w?@{D$wGH7D%9:7D$w?@! +<  &
J?j)%-%-   	 
 $Z000E $ #A     < < <CFF;;;; < < <CFF;;;;    < < <CTJJJCFF;;;;< 7+++s0   EE 
H'FHF00H9H  Hz/api/profiles/{name}c                   K   ddl m} 	 |                    | |j                  }n# t          $ r#}t          dt          |                    d }~wt          t          f$ r#}t          dt          |                    d }~wt          $ r>}t                              d|            t          dt          |                    d }~ww xY wd|j        t          |          d	S )
Nr   r  r   r   r   zPATCH /api/profiles/%s failedr  T)r7  rO   r   )rX   r  rename_profiler  r  r4   r  rg  rp  rW   rM   r  )rO   r  r  r   r  s        rR   rename_profile_endpointr  +  s      333333<**4?? < < <CFF;;;;( < < <CFF;;;; < < <6===CFF;;;;< s4yyAAAs,   & 
CAC"B  C9CCc                   K   ddl m} 	 |                    | d          }n# t          $ r#}t	          dt          |                    d}~wt          $ r#}t	          dt          |                    d}~wt          $ r>}t          	                    d	|            t	          d
t          |                    d}~ww xY wdt          |          dS )zDelete a profile. The dashboard collects the user's confirmation in
    its own dialog before this request, so we always pass ``yes=True`` to
    skip the CLI's interactive prompt.r   r  T)yesr   r   Nr   zDELETE /api/profiles/%s failedr  rS  )
rX   r  delete_profiler  r4   r  rg  rW   rM   r  )rO   r  r   r  s       rR   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.mdri  r|  T)r   r  r  zCould not read SOUL.md: r   r   F)r  r  r  rm  r4   )rO   	soul_pathr  s      rR   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  ri  r|  z PUT /api/profiles/%s/soul failedr  zCould not write SOUL.md: r   r7  T)r  r  r   rm  rM   r  r4   )rO   r  r  r  s       rR   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   r  r   F)r  r$  z'PUT /api/profiles/%s/description failedr  r   NT)r7  r  r$  )rX   r  r  r  r   write_profile_metarW   rM   r  r4   r  )rO   r  r  r  rZ  r  s         rR   #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.
    r   r   zprovider and model are requiredr   z!PUT /api/profiles/%s/model failedr  NT)r7  rQ   r  )
r  rQ   r   r  r4   rP  rW   rM   r  r  )rO   r  r  rQ   r  r  s         rR   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   )rl  )r  z*POST /api/profiles/%s/describe-auto failedr  r   N)r7  r  r  r$  )r  rX   rl  describe_profiler   r  rW   rM   r  r4   r  r7  r  r  )rO   r  rl  outcomer  s        rR   describe_profile_auto_endpointr  Q,  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).
    r   r   )r   r	  r	  )skills_tool)skill_manager_toolNr  rD  )r   r  r   r	  r	  r  r  r  r   r  r  _SKILLS_PROFILE_LOCKr  
SKILLS_DIR)r|  r  r   r	  r	  _skills_tool
_skill_mgrr   r  old_homeold_skills_dirold_mgr_homeold_mgr_skills_dirs                rR   r  r  },  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              #     K   | pd                                 }|r|                                dk    rdV  dS ddlm}m} t          |          } |t          |                    }	 |V   ||           dS #  ||           w xY w)u  Await-safe, config-only profile scope for handlers that ``await``.

    Unlike ``_profile_scope`` this touches ONLY the context-local
    ``set_hermes_home_override`` contextvar — it does NOT swap the
    process-global ``skills_tool``/``skill_manager`` module attributes.
    Those globals are shared across all event-loop tasks, so holding them
    across an ``await`` lets a concurrent skills request restore THIS
    request's profile dir on its ``finally`` (cross-contamination). The
    contextvar override is task-local and survives an ``await`` cleanly,
    which is all endpoints that resolve ``get_hermes_home()`` at call time
    (config, env, gateway status) actually need.

    None/""/"current" means the dashboard's own profile — no override.
    r   r  Nr   rO  )r   r   r  r	  r	  r  r  )r|  r  r	  r	  r  r   s         rR   r  r  ,  s        B%%''I 	))Y66


       
 'y11K$$S%5%566E*""5)))))""5))))s   )A: :Bc                   >    e Zd ZU eed<   eed<   dZee         ed<   dS )SkillTogglerO   r  Nr|  )r}  r~  r  r  r  r   r|  r   r  rT   rR   r  r  ,  s8         
IIIMMM!GXc]!!!!!rT   r  z/api/skillsc                   K   ddl m} ddlm} ddlm}m}m}m} t          |           5  t                      } ||          } |d          }	 |            }
 |            } |            }d d d            n# 1 swxY w Y   |	D ]T}|d         |v|d<    ||
                    |d         i                     |d	<   |d         |v rd
n|d         |v rdnd|d<   U|	S )Nr   )_find_all_skills)rY  )_read_bundled_manifest_names_read_hub_installed_namesactivity_count
load_usageT)skip_disabledrO   r  usagehubr   rC  
provenance)tools.skills_toolr  r\  rY  tools.skill_usager  r  r  r  r  r   r   )r|  r  rY  r  r  r  r  rj  r  rD  r  bundled_names	hub_namesr   s                 rR   
get_skillsr  ,  s     222222<<<<<<            
	 	  
0 
0&&v..!!555

 5466--//	
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0  
 
y0)#^EIIai$<$<=='
vY)++EEi=88 	
,
 Ms   A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   rX  Tr
  )
r\  rY  rZ  r  r|  r   r  r>  rO   r  )r  r|  rY  rZ  rj  r  s         rR   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	  r|  )r}  r~  r  r  r  r	  r   r|  r  rT   rR   r  r  -  sL         
IIILLL"Hhsm"""!GXc]!!!!!rT   r  c                   >    e Zd ZU eed<   eed<   dZee         ed<   dS )SkillContentUpdaterO   r   Nr|  r  r  rT   rR   r  r  -  s8         
IIILLL!GXc]!!!!!rT   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  rW   r  s    rR   _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.ri  r|  r  N)rO   r   r   )tools.skill_manager_toolr  r  r4   r  r  rm  r  )rO   r|  r  foundr  r   r  s          rR   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_skillNrn  r   r^  zFailed to create skill.r   )
r  r  r  r|  rO   r   r	  r   r4   r  )r  r  r  s      rR   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_skillNrn  r^  zFailed to update skill.r  r   r   r   )r  r  r  r|  rO   r   r   r  r   r4   r  )r  r  r  errr  s        rR   update_skill_contentr  N-  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;;;;   Mr  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  r  rJ  r  r  )r  r  r  r  r  toolsetsr  r  r   r  rv   rW   r  )r|  r  r  r  r  r  rj  r  r  rO   r  descr  rk  s                 rR   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 )ToolsetToggler  Nr|  r
  r  rT   rR   r  r  -  r
  rT   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 r  r  r   ts_keyro  s      rR   r  z!toggle_toolset.<locals>.<setcomp>-      OOO1VOOOrT   r   Unknown toolset: r   r  Fr  NTr
  )r  r  r  r  r4   r  r|  r   rv   r  r  r>  )	rO   r  r|  r  r  r  rD	  rj  r  s	            rR   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 r  r  r  s      rR   r  z%get_toolset_config.<locals>.<setcomp>-  r  rT   r   r  r   NTr  c                     g | ]l}|d          |                     d|d                    |                     d          |                     d          t           |d                              dmS )rv  rA   r   r  )rv  rA   r   r  r$  )r   r   )r   r  r<  s     rR   r  z&get_toolset_config.<locals>.<listcomp>-  s     	 	 	   !x"#%%!E(";"; uuU||#$55#3#3"&}}QuX'>'>"?"? 	 	 	rT   r$  rO   badger   tag
post_setuprequires_nous_auth)rO   r  r  r$  r  r  r  )rO   has_categoryr  active_provider)r  r  r  r  r  r  r<  r4   r  r   r   r  r   )rO   r|  r  r  r  r  rD	  rj  catr  r  provr$  r  r<  s                 @rR   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 )ToolsetProviderSelectrQ   Nr|  r  r  rT   rR   r  r  -  s/         MMM!GXc]!!!!!rT   r  	image_gen	video_gen)r  r  r  provider_rowc                     | dk    r.|                     d          p|                     d          rdndS | dk    r|                     d          S dS )a$  Map a provider picker row to its model-catalog plugin name.

    Plugin-backed rows carry ``image_gen_plugin_name`` / ``video_gen_plugin_name``;
    the managed "Nous Subscription" image row instead carries the legacy
    ``imagegen_backend: "fal"`` marker (same underlying FAL catalog).
    r  image_gen_plugin_nameimagegen_backendfalNr  video_gen_plugin_namer   )r  r   s     rR   _resolve_toolset_model_pluginr  -  sm      788 
!%%&899CEEt	
  78884rT   r   c                 J    ddl m}m} | dk    r ||          S  ||          S )zHReturn ``(catalog_dict, default_model)`` for a toolset's plugin backend.r   )_plugin_image_gen_catalog_plugin_video_gen_catalogr  )r  r  r	  )r  r   r  r	  s       rR   _toolset_model_catalogr
  .  sY           
 ((555$$[111rT   c                     ddl m}mm} |                    |           }|dS  ||d          }rt          fd|D             d          S t          fd|D             d          S )zFResolve a provider picker row by name, or the active row when omitted.r   )r  r  r  NTr  c              3   N   K   | ]}|                     d           k    |V   dS r   r   )r   r   rQ   s     rR   r   z-_find_toolset_provider_row.<locals>.<genexpr>.  s7      BB1f(A(AQ(A(A(A(ABBrT   c              3   8   K   | ]} |d           |V  dS )Tr  Nr  )r   r   r  rj  s     rR   r   z-_find_toolset_provider_row.<locals>.<genexpr>.  s;      MMq//6tLLLMMMMMMMrT   )r  r  r  r  r   r   )r  rj  rQ   r  r  r  r^  r  s    ``    @rR   _find_toolset_provider_rowr  .  s              

f
%
%C
{tc6t<<<D JBBBBBBBDIIIMMMMMDMMMt  rT   z!/api/tools/toolsets/{name}/modelsc                    K   t                               |           }|| dg dddS t          |          5  t                      }t	          | ||          }|rt          | |          nd}|s| dg dddcddd           S t          | |          \  }}|                    |          }	d}
t          |	t                    rR|	                    d          }t          |t                    r(|
                                r|
                                }
|
|vr||v r|nd}
ddd           n# 1 swxY w Y   d |                                D             }| t          |          |r|                    d          nd|||
|dS )u  Return the model catalog for a toolset backend (image/video gen).

    The GUI counterpart of the model picker `hermes tools` runs after a
    backend is selected — e.g. FAL's multi-model catalog (speed / strengths /
    price per model). ``provider`` names a picker row; omitted, the currently
    active provider is used. Toolsets without model catalogs return
    ``has_models: false``.
    NF)rO   
has_modelsr  r  r  r  c           
          g | ]]\  }}||                     d |          |                     dd          |                     dd          |                     dd          d^S )rJ  speedr   	strengthsprice)r  rJ  r  r  r  r   )r   model_idr  s      rR   r  z&get_toolset_models.<locals>.<listcomp>J.  s|     	 	 	 Hd xx	844XXgr**+r22XXgr**	
 	
	 	 	rT   rO   )rO   r  rQ   r   r  r  r  )_MODEL_CATALOG_TOOLSETSr   r  r   r  r  r
  rd  rg  r  r   rq  r   )rO   rQ   r|  sectionrj  r  r   catalogdefault_modelsection_cfgr  ro  r  s                rR   get_toolset_modelsr  #.  s0      &))$//GERD]abbb		 	  J J(vx@@=@J.tS999d 	# J J J J J J J J "8f!E!Ejj))k4(( 	&//'**C#s## &		 &))++'!!'4'?'?mmTG+J J J J J J J J J J J J J J J.	 	 &mmoo	 	 	F 6ll'*4CGGFOOO   s   =D) BD))D-0D-c                   N    e Zd ZU eed<   dZee         ed<   dZee         ed<   dS )ToolsetModelSelectr  NrQ   r|  )r}  r~  r  r  r  rQ   r   r|  r  rT   rR   r  r  _.  sC         JJJ"Hhsm"""!GXc]!!!!!rT   r  z /api/tools/toolsets/{name}/modelc                   K   t                               |           }|t          dd|            |j        pd                                }|st          dd          t          |j        p|          5  t                      }t          | ||j	                  }|rt          | |          nd}|st          dd|            t          | |          \  }}	||vrt          dd|d	|          |                    |i           }
t          |
t                    si }
|
||<   ||
d
<   t          |           ddd           n# 1 swxY w Y   d| ||dS )u$  Persist a backend model selection (``image_gen.model`` / ``video_gen.model``).

    Validates the model against the resolved backend's catalog — the same
    write the CLI's post-selection model picker performs. Returns 400 for
    toolsets without model catalogs or unknown model ids.
    Nr   zToolset has no model catalog: r   r   zmodel is requiredz'No model-capable backend is active for zUnknown model z for backend r  T)r7  rO   r  r   )r  r   r4   r  r   r  r|  r   r  rQ   r  r
  r  rd  rg  r   )rO   r  r|  r  r  rj  r  r   r  _defaultr  s              rR   select_toolset_modelr   e.  s      &))$//G$KT$K$K
 
 
 	
 
 b''))H I4GHHHH	/	0	0  (vt}EE=@J.tS999d 	GGG   
 34@@7""KKKKK   
 ''44+t,, 	*K)F7O'GF-              0 x6JJJs   7CEE
E
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 r  r  r  s      rR   r  z*select_toolset_provider.<locals>.<setcomp>.  r  rT   r   r  r   r  NT)r7  rO   rQ   )r  r"  r  r4   r  r|  r   rQ   r@  r  r   r   )rO   r  r|  r"  r  rD	  rj  r  s           rR   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 )ToolsetEnvUpdater  Nr|  )r}  r~  r  r   r  r  r|  r   r  rT   rR   r&  r&  .  s:         	c3h!GXc]!!!!!rT   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 r  r  r  s      rR   r  z#save_toolset_env.<locals>.<setcomp>.      RRR<61aRRRrT   r   r  r   Tr  r$  rv  c                     g | ]}|v|	S r  r  )r   r>  ro  s     rR   r  z$save_toolset_env.<locals>.<listcomp>.  s#    ;;;!7*:*:1*:*:*:rT   zUnknown env var(s) for toolset r?  r#  Nc                 B    i | ]}|t           |                    S r  )r   )r   r>  r<  s     rR   r  z$save_toolset_env.<locals>.<dictcomp>.  s-    ===!T--**++===rT   )r7  rO   savedskippedr$  )r  r  r  r  r  r<  r   r4   r  r|  r   r   rv   r  r  r  r  rq  r   rg  r  r  )rO   r  r|  r  r  r  r   valid_tsrj  r  r  r  rW
  r,  r-  rv  r_  r  r  ro  r<  s                      @@rR   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 )ToolsetPostSetuprv  Nr|  r  r  rT   rR   r1  r1  .  r  rT   r1  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 r  r  r  s      rR   r  z)run_toolset_post_setup.<locals>.<setcomp>/  r)  rT   r   r  r   zUnknown post-setup key: r  z
post-setupr  z Failed to spawn tools post-setupr  zFailed to run post-setup: NT)r7  r  rO   rv  )r  r  r3  r4   rv  r  r  r|  rW   rM   r  r  )rO   r  r|  r  r3  r.  rf  r  s           rR   run_toolset_post_setupr5  .  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z/api/tools/computer-use/statusc                 x   K   ddl m} t          |           5   |            cddd           S # 1 swxY w Y   dS )zCross-platform Computer Use readiness for the desktop card.

    See ``tools.computer_use.permissions.computer_use_status`` for the payload
    shape. Read-only and fast (shells ``cua-driver doctor`` + macOS
    ``permissions status``).
    r   )computer_use_statusN)tools.computer_use.permissionsr7  r  )r|  r7  s     rR   get_computer_use_statusr9  2/  s       CBBBBB		 	  % %""$$% % % % % % % % % % % % % % % % % %s   
/33z)/api/tools/computer-use/permissions/grantc                 :  K   t           j        dk    rt          dd          	 t          t	          |           g dz   d          }nJ# t          $ r  t
          $ r3}t                              d           t          dd	|           d
}~ww xY wd|j        ddS )a  Spawn ``hermes computer-use permissions grant`` as a background action.

    macOS-only: ``cua-driver permissions grant`` launches CuaDriver via
    LaunchServices so the TCC dialog is attributed to com.trycua.driver, then
    waits for approval. The frontend polls ``GET /api/actions/computer-use-
    grant/status`` and re-reads ``/status`` once it exits. Windows/Linux have
    no TCC toggles to grant, so this returns 400 there.
    rs  r   z3Computer Use permission grants are a macOS concept.r   )zcomputer-usepermissionsgrantr
  z.Failed to spawn computer-use permissions grantr  zFailed to request permissions: NTr  )	r  r  r4   r  r  rW   rM   r  r  r
  s      rR   grant_computer_use_permissionsr=  @/  s       |xH
 
 
 	

#g&&6667 
 

     
 
 
GHHH$Kc$K$K
 
 
 	


 tx1EFFFs   "A B.B

Bc                   4    e Zd ZU eed<   dZee         ed<   dS )RawConfigUpdate	yaml_textNr|  r  r  rT   rR   r?  r?  d/  s/         NNN!GXc]!!!!!rT   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.
    Nr   )r[  r   ri  r|  )r  r   r  r  r  )r|  r   s     rR   get_config_rawrB  i/  s       
	 	  ! !  ! ! ! ! ! ! ! ! ! ! ! ! ! ! !;;== /CII...NNGN44c$iiHHHs   -11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   r7  TzInvalid YAML: )
r[  	safe_loadr@  rd  rg  r4   r  r|  r   	YAMLError)r  r|  rp  r  s       rR   update_config_rawrF  y/  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} t          |          }	 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g           d|                                 S # |                                 w xY w)Nr   )InsightsEngineQ a}  
            SELECT date(started_at, 'unixepoch') as day,
                   SUM(input_tokens) as input_tokens,
                   SUM(output_tokens) as output_tokens,
                   SUM(cache_read_tokens) as cache_read_tokens,
                   SUM(reasoning_tokens) as reasoning_tokens,
                   COALESCE(SUM(estimated_cost_usd), 0) as estimated_cost,
                   COALESCE(SUM(actual_cost_usd), 0) as actual_cost,
                   COUNT(*) as sessions,
                   SUM(COALESCE(api_call_count, 0)) as api_calls
            FROM sessions WHERE started_at > ?
            GROUP BY day ORDER BY day
        c                 ,    g | ]}t          |          S r  r  r   r]	  s     rR   r  z'get_usage_analytics.<locals>.<listcomp>/  s    111Qa111rT   a  
            SELECT model,
                   SUM(input_tokens) as input_tokens,
                   SUM(output_tokens) as output_tokens,
                   COALESCE(SUM(estimated_cost_usd), 0) as estimated_cost,
                   COUNT(*) as sessions,
                   SUM(COALESCE(api_call_count, 0)) as api_calls
            FROM sessions WHERE started_at > ? AND model IS NOT NULL
            GROUP BY model ORDER BY SUM(input_tokens) + SUM(output_tokens) DESC
        c                 ,    g | ]}t          |          S r  r  rL  s     rR   r  z'get_usage_analytics.<locals>.<listcomp>/  s    555DGG555rT   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 > ?
        )rG  rD  )total_skill_loadstotal_skill_editstotal_skill_actionsdistinct_skills_used)r[  
top_skillsr  )dailyby_modeltotalsperiod_daysrD  r  )agent.insightsrI  r  r   rT	  r_	  r`	  rg  fetchonegenerater   r   )rG  r|  rI  r-  r  r  rS  cur2rT  cur3rU  insights_reportrD  s                rR   get_usage_analyticsr]  /  s     ------	%g	.	.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
     %(("55	
 	
 	







s   D#E E'z/api/analytics/modelsc                 >  K   t          |          }	 t          j                    | dz  z
  }|j                            d|f          }d |                                D             }i }|D ]@|                                        d          pdg                                          Ag }|                                D ]v}d |D             }	t          |	          dk    r>|	d         |D ]u s                    d	          rt          fd
dD                       }
|
r:                    d          pd                    d          pdz   d<   t                              d          pd                    d          pd          d<                       d          pd                    d          pdz   }                    d          pd}|r||z  ndd<   |                               |                    fd|D                        a|                    |           x|                    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.).
    rJ  a  
            SELECT model,
                   billing_provider,
                   SUM(input_tokens) as input_tokens,
                   SUM(output_tokens) as output_tokens,
                   SUM(cache_read_tokens) as cache_read_tokens,
                   SUM(reasoning_tokens) as reasoning_tokens,
                   COALESCE(SUM(estimated_cost_usd), 0) as estimated_cost,
                   COALESCE(SUM(actual_cost_usd), 0) as actual_cost,
                   COUNT(*) as sessions,
                   SUM(COALESCE(api_call_count, 0)) as api_calls,
                   SUM(tool_call_count) as tool_calls,
                   MAX(started_at) as last_used_at,
                   AVG(input_tokens + output_tokens) as avg_tokens_per_session
            FROM sessions WHERE started_at > ? AND model IS NOT NULL AND model != ''
            GROUP BY model, billing_provider
            ORDER BY SUM(input_tokens) + SUM(output_tokens) DESC
        c                 ,    g | ]}t          |          S r  r  rL  s     rR   r  z(get_models_analytics.<locals>.<listcomp>/  s    444DGG444rT   r  r   c                 <    g | ]}|                     d           |S )billing_providerr   rL  s     rR   r  z(get_models_analytics.<locals>.<listcomp>/  s*    PPP1aee<N6O6OPQPPPrT   r   r   ra  c              3   N   K   | ]}                     |          pd d k    V   dS r   Nr   )r   rv  r  s     rR   r   z'get_models_analytics.<locals>.<genexpr>0  sK       $ $ *q0$ $ $ $ $ $rT   input_tokensoutput_tokenscache_read_tokensreasoning_tokensestimated_costactual_cost	api_calls
tool_callsr.  last_used_atre  rf  avg_tokens_per_sessionc              3      K   | ]:u                     d           st          fddD                       6V  ;dS )ra  c              3   N   K   | ]}                     |          pd d k    V   dS rc  r   )r   rv  r]	  s     rR   r   z1get_models_analytics.<locals>.<genexpr>.<genexpr>0  sJ       : : sqQ.: : : : : :rT   rd  N)r   r<  )r   r]	  re   s    @rR   r   z'get_models_analytics.<locals>.<genexpr>0  s        122 '69 : : : :	$: : : 7 7 ' & rT   c                 `    |                      d          pd|                      d          pdz   S )Nre  r   rf  r   )r]	  s    rR   r  z&get_models_analytics.<locals>.<lambda>-0  s.    155005A!%%:P:P:UTUV rT   Tr  rP  rR  NrS  rg  rh  ri  rj  rk  rl  )r  rQ   re  rf  rg  rh  ri  rj  r.  rk  rl  rm  rn  rL  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  rU  rV  )r  r   rT	  r_	  r`	  r  r   r  r  r   r<  r  r	  r  r[  rQ  rT  rU  rV  rW  rX  rY  rW   rg  rX  r   )rG  r|  r-  r  r  ra	  rows_by_modelr^  
model_rowsprovider_rows	has_usagetotal_tokensr.  r  rQ   r\  ra  rQ  rb  
totals_currU  r  re   s                        @@rR   get_models_analyticsrx  /  s      
&g	.	.BPu-h  " Y# $ 54S\\^^444 :< 	M 	MC$$SWWW%5%5%;R@@GGLLLL%''..00 .	( .	(JPP
PPPM=!!Q&&&q)% b bCf}}0B(C(C}  # $ $ $ $	$$ $ $ ! !I ! ! *0**Z*@*@*EA#''R\J]J]Jbab)cF:&-0N1K1K1PqRURYRYZhRiRiRnmn-o-oF>*$*JJ~$>$>$C!

SbHcHcHhgh#iL%zz*55:HRZ7a|h7N7N`aF344F###    )      $ J''''		VV 	 	
 	
 	

  "	 "	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   I%N ;<J87N 8
KN KB,N Nr  )WinPtyBridgePtyUnavailableErrorc                       e Zd ZdZdS )rz  z,Stub when win_pty_bridge cannot be imported.Nr}  r~  r  r  r  rT   rR   rz  rz  0  s        >>DrT   rz  )	PtyBridgerz  c                       e Zd ZdZdS )rz  z5Stub on platforms where pty_bridge can't be imported.Nr|  r  rT   rR   rz  rz  0  s        GGDrT   s   \x1b\[RESIZE:(\d+);(\d+)\]g?z^[A-Za-z0-9._-]{1,128}$>   r   r  r   r   wsr7   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|p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   r   zmissing_or_empty_peer bound=r  zpeer_not_loopback peer= bound=r   r`   rh   r   r   _LOOPBACK_HOSTSr  r   r  r   r  s      rR   _ws_client_reasonr  0  s     sy/511 t#)\266<"CCEEKKMMJ j77t$&I5")..2K B Bj.?CAAAo%%tL[LL9JsLLLrT   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   r   r  r  s      rR   _ws_client_is_allowedr  0  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  u/))rT   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   r   zhost_mismatch host=r  r  rA
  >   r   httpszorigin_mismatch origin=)r   r`   rh   r   r   r   r  rd  re  schemerf  )r  r   r   rA
  rp  s        rR   _ws_host_origin_reasonr  0  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4rT   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)r  r  s    rR   _ws_host_origin_is_allowedr  1  s     ""%%--rT   c                 >    t          |           pt          |           S )zDFirst Host/Origin or peer-IP rejection reason, or None when allowed.)r  r  r  s    rR   _ws_request_reasonr  1  s    !"%%>):2)>)>>rT   c                 >    t          |           ot          |           S )zDReturn True when the WebSocket upgrade matches dashboard boundaries.)r  r  r  s    rR   _ws_request_is_allowedr  1  s    %b))G.CB.G.GGrT   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   r   insecureloopback)r   r`   rh   r   r   r  )r   s    rR   _ws_auth_moder  1  si    sy/511 w#)\266<"CCEEKKMMJ j77z:rT   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internalr   )Nr  z
internal: )r  ipr   N)internal_invalidr  ticket)no_credentialr
  )Nr  )ticket_invalidr  r   )Nr   )token_mismatchr   )r   r   r`   rh   hermes_cli.dashboard_auth.auditr  r  $hermes_cli.dashboard_auth.ws_ticketsr  r  r  r   r   WS_TICKET_REJECTEDr  r   r   r   r  r   r   r   r   )r  r   r  r  r  r  r  r  r  r  r   s              rR   _ws_auth_reasonr  &1  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)r  r  s    rR   _ws_auth_okr  z1  s    2q!T))rT   resumesidecar_urlactive_session_filec                    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           d|
d<   |t          |          |
d<   | rt!          |           \  }}|r|} | |
d<   |r||
d<   |r||
d<   |t#                      x}r||
d<   t%          |          |	rt          |	          nd|
fS )uT  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`).

    `active_session_file` (when set) is forwarded as
    ``HERMES_TUI_ACTIVE_SESSION_FILE``. The TUI writes the current session id
    there whenever it creates/resumes/switches sessions, giving the dashboard a
    small cross-process breadcrumb for reconnecting after an unexpected browser
    WebSocket close.

    `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_argvNr   r  zui-tuiF)tui_dev)apply_terminal_config_to_env)r  z9Failed to apply terminal config bridge for dashboard chatTr  NODE_ENV
productionHERMES_TUI_DISABLE_MOUSErc   HERMES_TUI_INLINEHERMES_TUI_DASHBOARDr  HERMES_TUI_RESUMEHERMES_TUI_SIDECAR_URLHERMES_TUI_ACTIVE_SESSION_FILEHERMES_TUI_GATEWAY_URL)r  r  r  r   r   r  rq   r  copyr  r  rW   rM   r  r  r  re	  _build_gateway_ws_urlrb  )r  r  r|  r  r  r  r  r  argvrk  r  r  latest_resume_latest_pathgateway_ws_urls                  rR   _resolve_chat_argvr  1  s   R =<<<<<<<"&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&,,,"%C --M *&@&H&H#| 	#"F#)  4(3$% D0C,- 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_credentialr  r   ws://z/api/ws?
r   r`   rh   r   r  r  r  rd  r  r   )r   portrf  r  qss        rR   r  r  1  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'''rT   c                    K   | ||d}|||d<   t          t                    4 d{V  t          j        t          fi | d{V cddd          d{V  S # 1 d{V swxY w Y   dS )a)  Resolve chat argv without blocking the dashboard event loop.

    ``_resolve_chat_argv`` may run ``npm install`` / ``npm run build`` through
    ``_make_tui_argv``.  Keep that synchronous work off the WebSocket event
    loop so reverse proxies and existing dashboard connections can continue
    to exchange keepalives while the TUI launch command is prepared.  The
    async lock preserves the previous one-build-at-a-time behavior when
    multiple browser tabs connect at once without occupying worker threads
    while queued connections wait.
    r  r  r|  Nr  )r   r`   rj   r  r  )r  r  r|  r  r	  s        rR   _resolve_chat_argv_asyncr  2  s:     " " F
 &(;$%"3'' 
 
 
 
 
 
 
 
&
 

 
 
 
 
 
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   A
A$'A$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   Nr  r   r   r  r   Fr   r  )r  r  )r   r  r  z	/api/pub?r  )r  r   r  rf  r  r  s         rR   _build_sidecar_urlr  "2  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(((rT   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`.Nr  z*broadcast send failed for subscriber on %sTr  )r}   rb  r   	send_textrW   rM   r  )r`   r  r/  ri   rl   r
  rB  s          rR   _broadcast_eventr  F2  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.r  r   N)r   r   _VALID_CHANNEL_REr6  r  r  s     rR   _channel_or_close_coder  U2  s6    o!!)R00G'--g66@77D@rT   c                     t          |           }|                    |          }||S t          j        dd          \  }}t	          j        |           t          |          }|||<   |S )zHReturn the per-channel file where a dashboard TUI writes its active sid.Nzhermes-pty-active-rL  )rk  ru  )r   r   rC  rD  rq   r   r   )r`   r  r4  r+  fdr^  r   s          rR    _active_session_file_for_channelr  \2  sk    )#..Eyy!!H#+?PPPLBHRLLL>>DE'NKrT   c                    	 t          j        |                     d                    }n# t          t           j        f$ r Y d S w xY wt          |                    d          pd                                          }|pd S )Nri  r|  r  r   )r  r  r  rm  JSONDecodeErrorr  r   r   )r   r{  r  s      rR   _read_active_session_filer  j2  s    z$..'.::;;T)*   tt TXXl++1r2288::Js   (+ AAc                 V    	 |                      d           d S # t          $ r Y d S w xY w)NTrA  )rH  rm  r=  s    rR   _forget_active_session_filer  t2  sC    t$$$$$   s    
((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.
    ri  rt  {   Nx   ignorez...)r   r   r  )rZ  r  s     rR   _ws_close_reasonr  {2  sP     kk'9--G
7||s4C4=22U::rT   zhermes> g      N@iP  r   _console_executorc                      t           at          5  t           ;t          j                            t
          d          a t          j        d            ddd           n# 1 swxY w Y   t           S )zALazily create the bounded console worker pool (once per process).Nzhermes-console)max_workersthread_name_prefixc                  H    t           ot                               dd          S )NFT)r  cancel_futures)r  shutdownr  rT   rR   r  z'_get_console_executor.<locals>.<lambda>2  s&    - T)22d2SS rT   )r  _console_executor_lock
concurrentfuturesThreadPoolExecutor_CONSOLE_EXECUTOR_MAX_WORKERSatexitregisterr  rT   rR   _get_console_executorr  2  s      # 	 	 ($.$6$I$I ='7 %J % %! T T  	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 s   AAA"%A"c                  &    t                      rdndS )z@Choose local vs hosted command policy for the dashboard console.hostedr  )r  r  rT   rR   _dashboard_console_contextr  2  s    799F88wFrT   c                 f    | j                             d          pd                                }|pd S )Nr|  r   )r   r   r   )r  r|  s     rR   _console_profile_from_wsr  2  s2    ""9--3::<<G?drT   enginer_  	confirmedc                    t          |          5  |                     ||          cd d d            S # 1 swxY w Y   d S )Nr  )r  r_	  )r  r_  r  r|  s       rR   _execute_console_liner  2  s     
	 	  9 9~~di~889 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9s   488	send_lockc                    K   |4 d {V  |                      |           d {V  d d d           d {V  d S # 1 d {V swxY w Y   d S r8  )	send_json)r  r  r/  s      rR   _console_sendr  2  s      
  $ $ $ $ $ $ $ $ll7#########$ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $s   :
AAr  
command_idc          
      (  K   |j         pd}|j        }|dk    rM|j        r"t          | |d|d|j        |d           d {V  t          | |d|d|t          d           d {V  d S |dk    rGt          | |d||j        pd	|d
           d {V  t          | |d|d|t          d           d {V  d S |dk    rQt          | |d|||j        pd| dt          d           d {V  t          | |d|d|t          d           d {V  d S |dk    r>t          | |d|d           d {V  t          | |d|d|t          d           d {V  d S |dk    rt          | |d|d|dd           d {V  d S t          | |d|d| |d
           d {V  d S )Nr   r7  r	  r  )r  r  streamr{  rO
  completer  r  r  rO
  rA   r^  zCommand failed.r  r  r  rO
  r  zRun `z`?)r  r  rO
  r  rA   clear)r  r  exitzUnknown console result status: )rO
  r  r	  r  _CONSOLE_PROMPTconfirmation_message)r  r  r  r  rO
  r  s         rR   _console_send_resultr  2  s      n"G]F~~= 	$$&"M& 
 
 
 
 
 
 
 
 
 " ") 

 

 
	
 
	
 
	
 
	
 
	
 
	
 
	
 	 !==,="	 	
 	
 		
 		
 		
 		
 		
 		
 		
 " !") 

 

 
	
 
	
 
	
 
	
 
	
 
	
 
	
 	###* "!6M:M':M:M:M) 

 

 
	
 
	
 
	
 
	
 
	
 
	
 
	
 " ,") 

 

 
	
 
	
 
	
 
	
 
	
 
	
 
	
 	B	G:+N+NOOOOOOOOO" !") 

 

 
	
 
	
 
	
 
	
 
	
 
	
 
	
 	"  " 

 

 
	
 
	
 
	
 
	
 
	
 
	
 
	
 	

AAA		
 	
	 	 	 	 	 	 	 	 	 	 	rT   r
  c                 j   |                      d          }||                      d          }|dS t          |t                    r(	 |                    d          }n# t          $ r Y dS w xY w	 t          j        |          }n# t
          j        $ r Y dS w xY wt          |t                    sdS |d fS )NrZ  r  r4  ri  )Nz"Console frames must be UTF-8 JSON.)Nz$Console frames must be JSON objects.)	r   rd  r  r  r  r  r  r  rg  )r
  ro  r/  s      rR   _console_json_payloadr  P3  s    !ggfooC
{ggg
{z#u >	>**W%%CC! 	> 	> 	>===	><*S// < < <;;;<gt$$ <;;D=s$   A 
A+*A+/B BBz/api/consolec           	         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                     t                      }t          j                    	 ddlm}  |t&          |          r'                                dk    rt+                     n# t,          $ rt}	t/           dt1          |	j                  dd           d {V                       dt          t1          |	j                                       d {V  Y d }	~	d S d }	~	wt4          $ r_}	t                              d           t/           dd|	 dd           d {V                       d           d {V  Y d }	~	d S d }	~	ww xY wt                              d||||pd           t/           d|pdt8          d           d {V  d d ddt0          dt:          dt<          dd f fd d!d"dt0          dt:          dd ffd#}
	 	 	                                   d {V }n# t@          $ r Y nw xY w|!                    d%          }|d&k    rntE          |          \  }}|r!t/           d|t8          d           d {V  |t1          |!                    d%          pd          #                                                                }|d'k    r t/           d(t8          d)           d {V  |d*k    rrR$                                s>d+z  %                                 d d t/           d,d-t8          d.           d {V  nEr#d t/           d,d-t8          d.           d {V  n t/           d,d/t8          d.           d {V  r6$                                s"t/           dd0t8          d           d {V  |d1k    rt1          |!                    d2          ppd          #                                }s"t/           dd3t8          d           d {V  ,|k    r"t/           dd4t8          d           d {V  Td  |
|d$"           d {V  k|d5v rt1          |!                    d          p|!                    d2          pd          #                                }|s"t/           d,d6t8          d.           d {V  މr"t/           dd7t8          d           d {V   |
|           d {V  t/           dd8|pd t8          d           d {V  ;n# tL          $ r Y nw xY wrP$                                s>%                                 	  d {V  d S # t          j'        t4          f$ r Y d S w xY wd S d S # rN$                                s;%                                 	  d {V  w # t          j'        t4          f$ r Y w w xY ww w xY w)9Nr  z/console refused: embedded chat disabled peer=%s4  embedded chat disabledr  r  z7console auth rejected reason=%s mode=%s cred=%s peer=%s1  auth: zconsole refused: %s peer=%s3  zconsole refused: %s8  r   )HermesConsoleEngine)output_limitrB  r  r^  r   )r  r  rA   0  zconsole failed to initializezConsole unavailable:   r  z>console accepted peer=%s mode=%s cred=%s context=%s profile=%sr  )r  rB  r|  rA   r_  r  r  rG   c                  K   	 t          j                    }t          j        |                    t	                      t          j        t          | |
                    t                     d {V }|k    r	 |k    rd d S d S |j	        dk    r|j
        nd 	t          ||           d {V  |j	        dk    r                    d           d {V  n# t           j        $ r  t           j        $ rI |k    r@d 	t          d|d	| d
           d {V  t          d|d| t           d           d {V  Y nt"          $ r}|k    rsd 	t$                              d           t          d|t)          |          p|j        j        | d
           d {V  t          d|d| t           d           d {V  Y d }~nd }~ww xY w|k    rd d S d S # |k    rd w xY w)N)r  r|  r  r  )r  r	  r  r  r^  z9Command timed out. Hermes Console returned to the prompt.r  r  r  r  zconsole command failed)rj   r  wait_forrp   r  	functoolspartialr   _CONSOLE_COMMAND_TIMEOUT_SECONDSr  rO
  r  r   CancelledErrorTimeoutErrorr  r
  rW   rM   r  r  	__class__r}  )r_  r  r  r  r  r  active_taskcommand_generationr  pending_confirmationr|  r  r  s         rR   run_commandzconsole_ws.<locals>.run_command3  sr     T	#+--D"+$$)++%-"+ '  	 	 9        FF /// ///" 0/ #)-3E"E"E4 ! '%	          }&&hhDh)))))))))E % 	 	 	# 	 	 	///'+$# '(W#'          $ *("+#'"1 
 
 
 
 
 
 
 
 
  	 	 	///'+$7888# '(#&s88#Es}/E#'	 	 	 	 	 	 	 	 	 	 $ *(")#'"1 
 
 
 
 
 
 
 
 
	P ///" 0/z///"""""sE   A0C 5G AG A$GG 	GA9G
G 
GG 
G(Fr  c                Z   K   dz  }t          j         | ||                    d S )Nr   )r  r  )rj   r7
  )r_  r  r  r$  r%  r'  s      rR   start_commandz!console_ws.<locals>.start_command4  sA      a'
)K	jIII
 
rT   Tr  websocket.disconnectpingpong)r  rA   rE  r   r  	cancelled)r  r  rA   idlez%A console command is already running.confirmrO
  z'No command is waiting for confirmation.z0Confirmation does not match the pending command.>   inputrO
  r7  zAConfirm or cancel the pending command before running another one.zUnsupported console frame: )(r  r    _DASHBOARD_EMBEDDED_CHAT_ENABLEDrM   rN   r   r  r  r  r  r  r  acceptr  r  rj   rk   hermes_cli.console_enginer  _CONSOLE_OUTPUT_LIMITr   r  r4   r  r  r   rW   r  r
  r   re  receivern  r   r  r   donerE  r8   r!  )r  peerauth_reasoncredr1  host_origin_reasonclient_reasonrB  r  r  r)  r
  msg_typer/  r^  
frame_typerO
  r_  r$  r%  r  r&  r|  r'  r  s   `                 @@@@@@@rR   
console_wsr>  d3  s     Y/29>>CD+ 		CTJJJhhD)AhBBBBBBBBB'++K??DEtT	
 	
 	
 hhD)9:P;:P:P)Q)QhRRRRRRRRR/33%24FMMMhhD)9:L)M)MhNNNNNNNNN%b))M *M:::hhD)9-)H)HhIIIIIIIII
))++&r**G(**GI!AAAAAA$$.
 
 
  	*w}})33 )))   sz?? 
 
 	
 	
 	
 	
 	
 	
 	
 hhD)9#cj//)J)JhKKKKKKKKK   56668388 
 
 	
 	
 	
 	
 	
 	
 	
 hhDh!!!!!!!!! 	IIH9   
+)%		
 	
	 	 	 	 	 	 	 	 	 (,K*.V# V#4 V#S V#T V# V# V# V# V# V# V# V# V# V# V# V#p =B 
 
 
# 
T 
d 
 
 
 
 
 
 
 
bX	JJLL((((((   wwvH1112377NGU 
# '#("1          W[[006B77==??EEGGJV### &"1          X%% "{'7'7'9'9 "&!+&&&((("&K+/('!$.&1&5           * +/('!$.&1&5           (!$.&,&5           
;#3#3#5#5 
# '#J"1          Y&&gkk)44R8LRPRSSYY[[+ 
'!$+'P&5          222'!$+'Y&5          '+$#mGt<<<<<<<<<<1117;;v..N'++i2H2HNBOOUUWW 
'!$.&*&5          ' '!$+!7 '6          #mD)))))))))#PZ=N3PP-         aX	r      	{//11 	   !!!!!!!!!!*I6   		 	 	 	; 	{//11 	   !!!!!!!!!*I6   		 	s   AH 
K3A)JK3AK..K3?Z< N Z< 
N*&Z< )N**LZ< ;\" <
[	\" [		\" 7\ \\"+]5]]5]0-]5/]00]5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 }
 j                            d          pd                                                                dv }d }|	r:t+           j        |	          }|rd }t/          |           n|st1          |          }||
|d}|t3          |          |d<   	 t5          d(i | d {V \  }}}n# t6          $ 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        t@          j!        |||           d {V n# tD          $ rF}                     d| d           d {V                       d           d {V  Y d }~d S d }~wtF          tH          f$ rF}                     d| d           d {V                       d           d {V  Y d }~d S d }~ww xY wt=          j%                    d) fd}t=          j&         |                      }	 	 	  '                                 d {V }n# tP          $ r Y n1w xY w|                    d          }|d k    rn|                    d!          }|A|                    d"          }tS          |t2                    r|*                    d#          nd$}|stV          ,                    |          }|r|-                                t]          |          k    r]t_          |0                    d%                    }t_          |0                    d&                    }1                    ||'           D2                    |           Zn# tf          $ r Y nw xY w|4                                 	 | d {V  n# t<          j5        tl          f$ r Y nw xY wt=          j        j                   d {V  d S # |4                                 	 | d {V  n# t<          j5        tl          f$ r Y nw xY wt=          j        j                   d {V  w xY w)*Nr  z+pty refused: embedded chat disabled peer=%sr  r  r  z3pty auth rejected reason=%s mode=%s cred=%s peer=%sr  r  zpty refused: %s peer=%sr  zpty refused: %sr  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
r  r  r  r|  freshr   >   rc   onr  r  r  r  z
[31mChat unavailable: z[0m
)rk  r  z
[31mChat failed to start: rG   c                  4  K   	 	                      d j        t                     d {V } | `	 	 t          j        j                   d {V  n# t          $ r Y nw xY w	                                  d {V  d S # t          $ r Y d S w xY w| st          j        d           d {V  	                     |            d {V  nn# t          $ ra Y 	 t          j        j                   d {V  n# t          $ r Y nw xY w	                                  d {V  d S # t          $ r Y d S w xY ww xY w3# 	 t          j        j                   d {V  n# t          $ r Y nw xY w	                                  d {V  w # t          $ r Y w w xY wxY w)NTr   )	rp   r  _PTY_READ_CHUNK_TIMEOUTrj   r  r   rW   r		  
send_bytes)rN  bridger  r  s    rR   pump_pty_to_wszpty_ws.<locals>.pump_pty_to_ws55  s     #	"22&+'>        =,'5555555555   hhjj            7  !-*********--..........    '5555555555   hhjj            -6'5555555555   hhjj           s   *D9 A 
AA"A> >
BBD9 -C	 D9 	
D4D9 C5 5
DDD" "
D0/D03D44D9 9F;EF
E(%F'E((F,FF
FFFFTr  r*  r  rZ  ri  rT   r   r  )colsr^  r  rG   N)7r  r   r1  rM   rN   r   r  r  r  r  r  r  r2  _PTY_BRIDGE_AVAILABLEr  r   r   r  r  r   r   r  r`   r  r  r  r  r4   r   
SystemExitrj   r  r}  spawnrz  r  rm  r  r7
  r5  rn  rd  r   
_RESIZE_REr6  endr   re  r5  resizerG  r8   rE  r!  rW   )r  r7  r8  r9  r1  r:  r;  r  r|  r  r  r  r  resolve_kwargsr  rk  r  r  rF  reader_taskr
  r<  ro  rZ  r6  rG  r^  rE  r  s   `                          @@rR   pty_wsrQ  4  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?&&w//52<<>>DDFF K K +/ D>rvwOO 	DF'(;<<<< 	D./BCCF " N
 &034G0H0H,-7II.IIIIIIII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!!!!!!!!!		($CSQQQQQQQQQ   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$ $ $ $ $ $ $ $L %nn&6&677K#.	JJLL((((((     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3	4     		&	2 	 	 	D	----------- 		&	2 	 	 	D	----------s   (K? ?
N"	A MN";NN"&(O 
Q4;PQ4.;Q//Q4/X 1S X 
SX SD0X 
Y6 
XY6 XY6 0X9 9YY6[Z[Z.+[-Z..#[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)r1  r   r  r  tui_gateway.wsrS  )r  rS  s     rR   
gateway_wsrU  5  s      + hhDh!!!!!!!!!r?? hhDh!!!!!!!!!!"%% hhDh!!!!!!!!!((((((
)B--rT   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  r  )
r1  r   r  r  r  r2  r  r`   receive_textr8   r  s     rR   pub_wsrY  5  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rW  )r1  r   r  r  r  r2  r}   r`   r  rv   r  rX  r8   r   r>  r  )r  r  ri   rl   r
  s        rR   	events_wsr[  5  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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]  )ro  r]  s     rR   _normalise_prefixr_  5  s(     BAAAAAC   rT   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   r2
  )r<   )rb  s    rR   no_frontendzmount_spa.<locals>.no_frontend6  s)      LM   rT   Nz
index.htmlr   rk  c           	      ~                        d          }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).
        ri  r|  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  r1  r   r   r`   rh   r   rt  r;   )rk  rH  chat_jsr  gated_jsbootstrap_script_index_paths         rR   _serve_indexzmount_spa.<locals>._serve_index6  s    $$g$66<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
 
 
 	
rT   z/assets/{filename}.cssr+  r   c                 N  K   t           dz  |  dz  }|                                r>|                                                    t                                                     st	          ddid          S t          |j                            d                    }|                    d	          }|r_d
D ]\}|	                    d| d| |           }|	                    d| d| |           }|	                    d| d| |           }]t          |d          S )NassetsrB  r^  r  r   r2
  x-forwarded-prefixri  r|  )rh  z/fonts-terminal/ri  rg  zurl(zurl("zurl('text/css)r   r*  )WEB_DISTr  rl  is_relative_tor<   r_  r   r   r  rt  r=   )r+  r   css_pathrk  rC  	asset_dirs         rR   	serve_csszmount_spa.<locals>.serve_cssQ6  sa     h&H):):)::!! 	I)9)9););)J)J*
 *
 	I  + 6CHHHH"7?#6#67K#L#LMM  ' 22 	TW T T	kk"4"4"46PV6PY6P6PQQkk"69"6"68T8T8T8TUUkk"5)"5"57Rv7Ry7R7RSS
;;;;rT   z/assetsrr  )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 )Nrs  r  zapi/r   zNo such API endpoint: /r   r2
  )r_  r   r   r   r<   ru  rl  rv  r  r  r:   )rb  r   rk  r  rp  s       rR   	serve_spazmount_spa.<locals>.serve_spac6  s      "7?#6#67K#L#LMM !5!5f!=!=@Y@@A    y(	 	+!!##2283C3C3E3EFF	+   ""	+ !!##		+  	***|F###rT   r   )ru  r  r   r  r5   mountr>   )r`  rd  ry  r{  ro  rp  s       @@rR   	mount_spar~  6  s?    ?? 	,	-	-	 	 	 	 
.	-	
 	\)K-
 -
S -
 -
 -
 -
 -
 -
l __-..<# < < < < /.< ix(7J!K!K!KRZ[[[__())$3 $ $ $ $ $ $ *)$ $ $rT   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 eyes      ?default_hexdefault_alphac                    | ||dS t          | t                    r| |dS t          | t                    r|                     d|          }|                     d|          }t          |t                    sdS 	 t	          |          }n# t
          t          f$ r |}Y nw xY w|t          dt          d|                    dS dS )zNormalise a theme layer spec from YAML into `{hex, alpha}` form.

    Accepts shorthand (a bare hex string) or full dict form.  Returns
    ``None`` on garbage input so the caller can fall back to a built-in
    default rather than blowing up.
    Nr  alphar  r  rZ	  r  )	rd  r  rg  r   rf  r  rg  r  rr  )r_  r  r  hex_val	alpha_valalpha_fs         rR   _parse_theme_layerr  6  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.55rY  )fontSansfontMonobaseSize
lineHeightletterSpacing_THEME_DEFAULT_TYPOGRAPHYz0.5remcomfortable)radiusdensity_THEME_DEFAULT_LAYOUT>   cardringr0  mutedaccentborderpopoverprimaryrn  r  	secondarydestructivecardForegroundmutedForegroundaccentForegroundpopoverForegroundprimaryForegroundsecondaryForegrounddestructiveForeground>   bgherologocrestr  sidebar>	   r  r  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  rv  r  r  rG   c                                          |                      |                     }t          |||          }||n||dS )Nr  )r   r  )rv  r  r  rK
  rp  
colors_srcpalette_srcs        rR   _layerz+_normalise_theme_definition.<locals>._layer6  sJ    sJNN3$7$788#D+}EE+vv}1]1]]rT   r
  z#041c1c	midgroundz#ffe6cb
foregroundz#ffffffrZ	  warmGlowzrgba(255, 189, 56, 0.35))r
  r  r  r  noiseOpacityr  
typography)r  r  fontDisplayfontUrlr  r  r  layoutr  r  >   compactspaciousr  colorOverridesrr  r1  r  r   ro  	customCSScomponentStyleslayoutVariantr  r  r  )rO   r  r  r  r  r  r  r  )rd  rg  r   r  r   rf  r   r   r  rg  r  r  rq  _THEME_OVERRIDE_KEYS_THEME_NAMED_ASSET_KEYSrt  isalnum_THEME_CUSTOM_CSS_MAX_THEME_COMPONENT_BUCKETSre  _THEME_LAYOUT_VARIANTS)!r{  rO   r  r  	raw_noisetypo_srcr  rv  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_variantr  r  r  s!                                  @@rR   _normalise_theme_definitionr  6  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*.yamlri  r|  )
r   r  r  globr[  rD  r  rW   r  r  )
themes_dirr  rE
  r{  
normaliseds        rR   _discover_user_themesr  e7  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.
    rK  themer  r/
  rO   r  r  )rO   r  r  
definition)themesr  )r   r   r  rv   _BUILTIN_DASHBOARD_THEMESr  r  )rj  r  user_themesr  r  r  s         rR   get_dashboard_themesr  {7  s       ]]FV['9EEEF'))K55DF&  6a 	 	V9fIwZ]+	
 
 	 	 	 	6///rT   c                       e Zd ZU eed<   dS )ThemeSetBodyrO   Nr  r  rT   rR   r  r  7  r  rT   r  z/api/dashboard/themec                    K   t                      }d|vri |d<   | j        |d         d<   t          |           d| j        dS )z9Set the active dashboard theme (persists to config.yaml).rK  r  T)r7  r  )r   rO   r   )r  rj  s     rR   set_dashboard_themer  7  sU       ]]F&   {#'9F; +++rT   r  >   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).rK  fontr/
  )r   r   _FONT_DEFAULT_ID_FONT_CHOICES)rj  r  s     rR   get_dashboard_fontr  7  sB       ]]F6;8HIIID=  D>rT   c                       e Zd ZU eed<   dS )FontSetBodyr  Nr  r  rT   rR   r	  r	  7  r  rT   r	  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.
    rK  r  T)r7  r  )r  r  r  r   r   )r  r  rj  s      rR   set_dashboard_fontr  7  si       	]224998HD]]F&   {"&F;%%%rT   	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)
rd  r  r   r   rj  rl  rm  rn  r	  rg  )r  r  rq  r  r  s        rR   _safe_plugin_api_relpathr  7  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_dirr   r   rW  r   HERMES_ENABLE_PROJECT_PLUGINSz.hermesprojectrK  manifest.jsonri  r|  rO   r  r   r   positionrM  )r   r  r  hiddenTslotsc                 @    g | ]}t          |t                    ||S r  )rd  r  r  s     rR   r  z/_discover_dashboard_plugins.<locals>.<listcomp>A8  s,    NNN1Z35G5GNANQNNNrT   r  )r  NzPlugin %s: refusing unsafe api path %r (must be a relative file inside the plugin's dashboard/ directory); backend routes from this plugin will not be mountedr  r  r   iconPuzzler   z0.0.0rx  zdist/index.jsrC  )rO   r  r  r  r   r  r  rx  rC  has_apir   _dir	_api_filez$Bad dashboard plugin manifest %s: %s)rv   rr  r  r   r0   r  r   rk  r  r  r  r  r  r  r  r   rO   r  rd  rg  r  r   r   rb  r  rM   r  rW   )r   
seen_namesr  bundled_rootsearch_dirsplugins_rootr   r  manifest_filer{  rO   raw_tabtab_infooverride_path	slots_srcr  raw_apir  safe_apir  s                       rR   _discover_dashboard_pluginsr)   8  s    GeeJ::::::**,,L			Y	&/		 ),	y!K 677 LDHJJ2Y>	JKKK + F Ff""$$ 	L002233 C	 C	E<<>> !K//AM '')) =z-"9"97"9"K"KLLxx
33:%%t$$$
 2<DHHUOOT1R1RZ$((5"---XZ#KK
D

;; 'J > >  !(J 7 7mS11 9m6N6Ns6S6S 9+8HZ(H--.. .)-HX& !HHW--	#%i.. ONN	NNNE ((5// % 33G=YYY x/LL) g    !XXgt44#'88M2#>#> HHVX66#xx	7;;#"!XXg??88E??#H~~$..!)           C]TWXXXCC	H Ns    /AM 7H'M  
N*NN_dashboard_plugins_cacheforce_rescanc                     t           | rt                      a n3t           r,t          d t           D                       rt                      a t           S )Nc              3   f   K   | ],}t          |d                                                     V  -dS )r  N)r   r  r  s     rR   r   z)_get_dashboard_plugins.<locals>.<genexpr>q8  s;      NN4&	??))+++NNNNNNrT   )r*  r)  r<  r+  s    rR   r   r   l8  sX    '<'#>#@#@  	! ENN5MNNNNN 	E'B'D'D$##rT   z/api/dashboard/pluginsc                  L  K   t                      } t                      }t          |ddg           pg 	 ddlm}m}  |             |            n,# t          $ r t                      t                      Y nw xY wdt          dt          ffdfd	| D             S )
zPReturn discovered dashboard plugins (excludes user-hidden and non-enabled ones).rK  hidden_pluginsr/
  r   r   r   rG   c                     |                      dd          }|v rdS |                      d          dk    r|v rdS |vrdS n|                      d          dk    r|v rdS dS )NrO   r   Fr   r   r   Tr   )r   rO   r   r   r  s     rR   
_is_activez)get_dashboard_plugins.<locals>._is_active8  s    uuVR  6>>555??f$$|##u;&&u 'UU8__	))|##utrT   c                 b    g | ]+} |          d  |                                 D             ,S )c                 D    i | ]\  }}|                     d           ||S r;  r<  r=  s      rR   r  z4get_dashboard_plugins.<locals>.<listcomp>.<dictcomp>8  s/    ===$!Q1<<+<+<=A===rT   rq  )r   r   r2  s     rR   r  z)get_dashboard_plugins.<locals>.<listcomp>8  sM       :a====!''))===  rT   )
r   r   r   r   r   r   rW   rv   rg  r   )r   rj  r   r   r2  r   r   r  s       @@@@rR   get_dashboard_pluginsr6  v8  s
      %&&G]]F6;0@"MMMSQSFNNNNNNNN&&((((**   eeuud t              s   A &A?>A?z/api/dashboard/plugins/rescanc                  J   K   t          d          } dt          |           dS )z#Force re-scan of dashboard plugins.Tr.  )r7  r=  )r   r   )r   s    rR   rescan_dashboard_pluginsr8  8  s,       %$777GW...rT   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)r}  r~  r  r  r  r  r   r
  r  rT   rR   r:  r:  8  s;         OOOE4FDrT   r:  r   c                 >    d |                                  D             S )Nc                 D    i | ]\  }}|                     d           ||S r;  r<  r=  s      rR   r  z-_strip_dashboard_manifest.<locals>.<dictcomp>8  s/    @@@TQall3.?.?@Aq@@@rT   r5  )r   s    rR   _strip_dashboard_manifestr=  8  s    @@QWWYY@@@@rT   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_providersr   r   _read_manifestc                 :    i | ]}t          |d                    |S r  )r  r  s     rR   r  z'_merged_plugins_hub.<locals>.<dictcomp>8  s$    >>>!C&	NNA>>>rT   rK  r0  r/
  r   r  r  inactiveNr  FT>   r  r   r   provides_tools)registryzhermes auth r$  )rO   r   r  r   runtime_statushas_dashboard_manifestdashboard_manifestr   
can_removecan_update_gitr   auth_commanduser_hiddenc                     h | ]
}|d          S r  r  rL  s     rR   r  z&_merged_plugins_hub.<locals>.<setcomp>9  s    +++1V9+++rT   c                 \    g | ](}t          |d                    vt          |          )S r  )r  r=  )r   r   agent_namess     rR   r  z'_merged_plugins_hub.<locals>.<listcomp>9  s@       qy>>,, 	"!$$,,,rT   ry
  )memory_providermemory_optionscontext_enginecontext_options)r   orphan_dashboard_pluginsr  )r   r?  r@  rA  rB  rC  r   r   rD  r   r   r   r   rl  r  r   r   r  r	  rg  r  tools.registryrH  r
  check_fnrW   r  r=  )*r?  r@  rA  rB  rC  r   r   _read_plugin_manifest_atdashboard_listdash_by_namer   r   rj  r0  plugins_root_resolvedr^  rO   r   r  r   dir_strrv  r  rI  dir_pathdmhas_dash_manifestunder_user_treecan_remove_updater   rN  manifest_datarG  rH  tnamerx  orphan_dashboardmemory_providersr  r  context_enginesrR  s*                                            @rR   _merged_plugins_hubri  8  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   ri  rW   rM   r  r4   )r   r  s     rR   get_plugins_hubrk  (9  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
  r7  r   r^  zInstall failed.r   Tr.  after_install_path)r   r   rm  r  r   r  r
  r   r4   r   r  )r   r  rm  r  s       rR   post_agent_plugin_installro  39  s      7??????%%j{  F
 ::d 
::g&&;*;
 
 
 	
 ----
JJ#T***MrT   c                 f    |                      d          } | rd| v sd| v rt          dd          | S )z=Reject path-traversal attempts in plugin name URL parameters.r   r  r  r   zInvalid plugin name.r   )r   r4   r  s    rR   _validate_plugin_namerq  H9  sC    ::c??D L44<<44<<4JKKKKKrT   z//api/dashboard/agent-plugins/{name:path}/enablec                    K   t          |            t          |          }ddlm}  ||d          }|                    d          s&t          d|                    d          pd	          |S )
Nr   "dashboard_set_agent_plugin_enabledTr  r7  r   r^  zEnable failed.r   r   rq  r   rt  r   r4   r   rO   rt  r  s       rR   post_agent_plugin_enablerx  P9  s      7 &&DIIIIII//dCCCF::d ]FJJw4G4G4[K[\\\\MrT   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   rs  Fru  r7  r   r^  zDisable failed.r   rv  rw  s       rR   post_agent_plugin_disablerz  \9  s      7 &&DIIIIII//eDDDF::d ^FJJw4G4G4\K\]]]]MrT   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_pluginr7  r   r^  zUpdate failed.r   Tr.  )r   rq  r   r|  r   r4   r   )r   rO   r|  r  s       rR   post_agent_plugin_updater}  h9        7 &&DCCCCCC))$//F::d ]FJJw4G4G4[K[\\\\----MrT   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_pluginr7  r   r^  zRemove failed.r   Tr.  )r   rq  r   r  r   r4   r   )r   rO   r  r  s       rR   delete_agent_pluginr  u9  r~  rT   c                   D    e Zd ZU dZee         ed<   dZee         ed<   dS )_PluginProvidersPutBodyNrS  rU  )r}  r~  r  rS  r   r  r  rU  r  rT   rR   r  r  9  s:         %)OXc])))$(NHSM(((((rT   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_providerNr7  T)r   r   r  r  rS  rU  )r   r  r  r  s       rR   put_plugin_providersr  9  s       7       
 'd2333&T0111$<rT   c                       e Zd ZU eed<   dS )_PluginVisibilityBodyr  Nr|  r  rT   rR   r  r  9  r}  rT   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).rK  r0  T)r7  rO   r  )r   rq  r   rd  r   rg  rb  r  r  r
  r   )r   rO   r  rj  hidden_lists        rR   post_plugin_visibilityr  9  s      7 &&D]]F&  
6::k3J3JD(Q(Q  {{+//0@AAGRKk4(( { !t;..4    [ !T[004   ,7F;()<<<rT   z1/dashboard-plugins/{plugin_name}/{file_path:path}r  c                    K   t                      }t           fd|D             d          }|st          dd          	 ddlm}m}  |            } |            }n,# t          $ r t                      }t                      }Y nw xY w|                    d          d	k    r |v s |vrt          dd          n.|                    d          d
k    r |v r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0          S )1u  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.

    User plugins must be in plugins.enabled before their assets are
    served. (#46435, GHSA-mcfc-hp25-cjv7)
    c              3   4   K   | ]}|d          k    |V  dS r   r  r   s     rR   r   z%serve_plugin_asset.<locals>.<genexpr>9  s1      BB6k)A)A1)A)A)A)ABBrT   Nr   r   r   r   r   r   r   r   r  r  zPath traversal blockedr  rJ  zapplication/javascriptr\  rB  rt  rL  r  rG  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.maprj  rk  )r*  r   )r   r   r4   r   r   r   rW   rv   r   r   rl  rv  r  r  ru  r   r:   )r   r  r   r   r   r   r   r   r  re   ru  content_typesr*  s   `            rR   serve_plugin_assetr  9  s     * %&&GBBBBgBBBDIIF H4FGGGGNNNNNNNN&&((((**   eeuu zz(v%%,&&+[*H*HC8JKKKK +I	H			*	*,&&C8JKKKK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   s   A &BBc                  *   	 ddl m} m}  |             } |            }n,# t          $ r t	                      }t	                      }Y nw xY wt                      D ]8}|                    d          }|s|                    dd          }|                    d          dk    rA||v rt                              d|           j||vrt                              d	|           n9|                    d          d
k    r ||v rt                              d|           |                    d          dk    r$t          	                    d|d         |           t          |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t5          |dd          }|#t          	                    d|d                    t6                              |d|d                     t                              d|d                     # t          $ r-}t          	                    d|d         |           Y d}~2d}~ww xY wdS )u  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).

    Additionally, user plugins must be explicitly enabled via the
    ``plugins.enabled`` allow-list in config.yaml before their backend
    code is imported. Without this gate, an installed-but-not-enabled
    plugin's Python code would execute at dashboard startup — a code
    execution vector that bypasses the user's intent. (#46435,
    GHSA-mcfc-hp25-cjv7)
    r   r   r  rO   r   r   r   z3Plugin %s: skipping API mount (explicitly disabled)z6Plugin %s: skipping API mount (not in plugins.enabled)r   r  zPlugin %s: ignoring backend api=%s (project plugins may not auto-import Python code; move the plugin to ~/.hermes/plugins/ if you trust it)r  zKPlugin %s: refusing to import api file outside its dashboard directory (%s)z,Plugin %s declares api=%s but file not foundhermes_dashboard_plugin_Nr   z,Plugin %s api file has no 'router' attributer   )rk  z+Mounted plugin API routes: /api/plugins/%s/z'Failed to load plugin %s API routes: %s)r   r   r   rW   rv   r   r   rM   r  r  r   rl  r	  rm  rn  rg  r  	importlibutilspec_from_file_locationloadermodule_from_specr  modulesexec_moduler  r   r`   include_routerrN   )r   r   r   r   r   api_file_namer   r  api_pathresolved_apiresolved_basemodule_namerK
  r  r   r  s                   rR   _mount_plugin_api_routesr  
:  s   ,NNNNNNNN&&((((**   eeuu )** QY QY

;// 	jj,,
 ::h6))l**

I   +--

L    . ZZ!!Y..l**

I   ::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aQY QYs^    &AA5=F339G0/G0,4M".MJ,+M,,K7MAM
N#"NNr
  zuvicorn.Serverc                     | j         rB| j         d         j        r0| j         d         j        d                                         d         S |S )aS  Read the OS-assigned port from a live uvicorn server socket.

    After ``server.startup()`` the socket is bound.  Returns the actual
    port so ephemeral (port-0) discovery works without a pre-bind TOCTOU.
    Falls back to *fallback* if the socket list is empty (shouldn't happen
    but guards against uvicorn internals changing).
    r   r   )rR
  socketsgetsockname)r
  r  s     rR   _read_bound_portr  :  sJ     ~ =&.+3 =~a (+7799!<<OrT   actual_portc                 6   t           j                            d          }|sdS d}	 t          |          }|j                            dd           t          j        dt          |           id          }t          j
        d	d
t          |j                  |j         ddd          5 }|                    |           |                                 t          j        |                                           |j        }ddd           n# 1 swxY w Y   t          j        ||           dS # t$          $ r^}|r5	 t          |                              d           n# t$          $ r Y nw xY wt(                              d||           Y d}~dS d}~ww xY w)a  Optionally publish the dashboard port through an atomic ready file.

    Windows Desktop can launch dashboard backends with ``pythonw.exe`` to avoid
    console flashes. That path cannot rely on stdout for the port announcement,
    so Electron passes ``HERMES_DESKTOP_READY_FILE`` and waits for this JSON.
    Normal CLI/dashboard launches still use the stdout READY line below.
    HERMES_DESKTOP_READY_FILENr   Tr  r  )r	  r   )
separatorsr  ri  rm  z.tmpF)r}  r?  rk  ru  rl  rA  z+Failed to write dashboard ready file %r: %s)rq   r  r   r   r  r  r  r  re  rC  rq  r  rO   rG  r  r  r  rt  rW   rH  rM   r  )r  re   rJ  r   r/  r  r  s          rR   _write_dashboard_ready_filer  :  s    Z^^788F HQF||$666*fc+&6&67JOOO(DK  i???
 
 
 	 HHWHHJJJHRYY[[!!!wH	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	
8T""""" Q Q Q 	X%%%6666   BFCPPPPPPPPPQsa   BD0 +ADD0 DD0 DD0 0
F:F=#E! F!
E.+F-E..FFopen_browserinitial_profilec                   	 |sdS ddl 	t          j        dk    pWt          t          j                            d                    p+t          t          j                            d                    }|st                              d           dS | dvr| nd}d	| d
| |rddl	m
} d ||           z  	fd}t          j        |d                                           dS )a2  Open the dashboard URL in the user's browser if appropriate.

    Skips on headless Linux (no ``DISPLAY`` / ``WAYLAND_DISPLAY``) to avoid
    TUI browsers (links, lynx) that would SIGHUP the server process.
    Maps ``0.0.0.0`` / ``::`` binds to ``127.0.0.1`` so the browser opens
    a reachable URL.
    Nr   linuxDISPLAYWAYLAND_DISPLAYzzSkipping browser-open: no DISPLAY or WAYLAND_DISPLAY detected (headless Linux). Pass --no-open to suppress this detection.)r   r   r   zhttp://r   )r
  z
/?profile=c                  ~    	 t          j        d                                           d S # t          $ r Y d S w xY w)Nr  )r   r		  rt  rW   )	_open_url
webbrowsers   rR   _openz"_maybe_open_browser.<locals>._open:  sO    	JsOOOOOI&&&&& 	 	 	DD	s   ). 
<<T)re   rg   )r  r  r  r   rq   r  r   rM   r  urllib.parser
  rs   ru   rP   )
r   r  r  r  _has_display_display_hostr
  r  r  r  s
           @@rR   _maybe_open_browserr  :  sB      	 	3
y))**	3
01122 
  

K	
 	
 	
 	 (999DD{M7-77+77I ;&&&&&&:%%"8"8:::	      E$///5577777rT   r   #  r  c           	          ddl }	 ddlm}  |             n2# t          $ r%}t                              d|           Y d}~nd}~ww xY wt                     t          j        _	        |r$ t          vrt                              d            t          j        j	        rddlm}  |            sg }		 ddlm}
 |
j        r|	                    d|
j                    n# t          $ r Y nw xY wd	}|	r/t%          d
  dd                    |	          z   dz   |z             t%          d
  d|z             t                              d d                    d  |            D                                   t          j        _         dv }|                    t           dt/          t          j        j	                  |rdnd|rdnd          |                               fd}t2          j        dk    rt7          j         |                       dS 	 ddlm}                                 }nK# t          $ r> d}d}	 t7          j         t7          j!                               n# t          $ r Y nw xY wY nw xY w| | |            |           dS t7          j         |                       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   N)start_nous_auth_keepalivez%Nous auth keepalive did not start: %su  --insecure no longer bypasses dashboard authentication. A non-loopback bind (%s) now ALWAYS requires an auth provider (OAuth or the bundled password provider). Configure one — see below — or bind to 127.0.0.1 and reach it over an SSH tunnel / Tailscale.r  )r{  u     • nous: u  Configure an auth provider before exposing the dashboard:
  • Password: set dashboard.basic_auth.username + password_hash in config.yaml
    (hash with: python -c "from plugins.dashboard_auth.basic import hash_password; print(hash_password('your-password'))")
  • OAuth: run `hermes dashboard register` (Nous Portal) or install a DashboardAuthProvider plugin.
There is no unauthenticated public-bind option — to keep it local, bind 127.0.0.1 and tunnel in (SSH / Tailscale).zRefusing to bind dashboard to u    — the auth gate engages on non-loopback binds, but no auth providers are registered.

Bundled providers reported these issues:
r  z

uY    — the auth gate engages on non-loopback binds, but no auth providers are registered.

z=Dashboard binding to %s with auth gate enabled. Providers: %sr#  c              3   $   K   | ]}|j         V  d S r8  r  r  s     rR   r   zstart_server.<locals>.<genexpr>G;  s$      77af777777rT   )r   r   r   r  r	  )r   r  	log_levelproxy_headersws_ping_intervalws_ping_timeoutc                    K   j         s                                                               _                                        5                                   d {V  j        r	 d d d            d S t                    } | t          j	        _
        t          |            t          d|  d           t          d d|             t          | 
	           	 ddlm}  |t!          j                               n2# t$          $ r%}t&                              d	|           Y d }~nd }~ww xY wd
dt!          j                    dt*          dd ffd                                                    z                                               d {V  j        r                                 d {V  d d d            d S # 1 swxY w Y   d S )N)r  zHERMES_DASHBOARD_READY port=T)r  u     Hermes Web UI → http://r   r   )install_loop_noise_filterz%loop noise filter install skipped: %sg       @g      @r   rG   c                                                      }|| z
  }|k    rt                              d|                               |z              d S )Nz1event loop stalled %.1fs (GIL pressure suspected))r   rM   r  
call_later)r   r  drift_hb_interval_hb_loop_hb_stall_threshold_loop_heartbeats      rR   r  z5start_server.<locals>._serve.<locals>._loop_heartbeat;  sq    mmooh...LLK   ## /33E    rT   )loadedloadlifespan_classr   capture_signalsstartupshould_exitr  r`   rh   r  r  printr  tui_gateway.loop_noiser  rj   r  rW   rM   r  rf  r  r   	main_loopr[	  r  )r  r  r  r  r  r  r  rj  r   r  r  r  r
  s      @@@@rR   _servezstart_server.<locals>._serve;  s      } 	KKMMM //77##%% 9	( 9	(.."""""""""! 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	(
 +6DAAAK#.CI '444>>>dKKKKDDD{DDEEEk<QQQILLLLLL))'*B*D*DEEEE I I I

BCHHHHHHHHI L"%/11H
% 
D 
 
 
 
 
 
 
 
 
 ox}}/M   ""$$$$$$$$$~ (oo'''''''''s9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	( 9	(sD   #G A,G ."DG 
E D;6G ;E  BG  G$'G$r  )asyncio_run)loop_factory)"uvicornhermes_cli.nous_auth_keepaliver  rW   rM   r  r   r`   rh   r   r   r  r!  r  plugins.dashboard_authr{  LAST_SKIP_REASONr  rJ  r  rN   r   Configr   Serverr  r  rj   r  uvicorn._compatr  get_loop_factoryset_event_loop_policyWindowsSelectorEventLoopPolicy)r   r  r  r   r  r  r  r  r  skip_reasons_nous_plugin	_fix_hint_is_loopbackr  _runner_loop_factoryrj  r
  s   ``` `           @@rR   start_serverr  :  s    NNNALLLLLL!!#### A A A

:C@@@@@@@@A 2$77CI  
$999 	
 	
 	
 y 6
 	=<<<<<~ ,	 ')LGGGGGG0  ''F|'DFF      	I   	 BT B B B ii--	.
       # # # #%./  
 			KII77nn&6&677777	
 	
 	
  CID <<L^^$TY 39233
 ".74 ,6$   F" ^^F##F?( ?( ?( ?( ?( ?( ?( ?( ?( ?(` |wFFHH:::::://11   	)688     	 	 	D	 }555555FFHHs\    
AAA7*C" "
C/.C/$H? ?J%I43J4
J>J JJJ)rD   rH  )r`   r1   )Fr|  )r   r   r8  )r  N)r  NF)r   )rU  )r  )	rU  r   r   r  r  NNNN)rU  r   r   r  r  r  NN)r   rU  N)NF)r'  )rC  r  NNN)r  )NrU  r/
  r4  )r   r  rU  Nr  )r   N)NNNNr  )r   r  TFr   (  r  
contextlibr   r   rj   r  r  r  concurrent.futuresr  r  dataclassesr   r   r   r
  r   importlib.utilr  r  r]  rw  rq   r3  r4  rT  r  r  r  rC  rs   r   urllib.errorr  r  rb  hermes_cli._subprocess_compatr	   r
   urllib.requestpathlibr   r2  r   r   r   r   r   r[  __file__r  rl  r  r  r   insertrX   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.memory_providersr'   r(   r)   gateway.statusr*   r+   r,   r-   r.   r/   r2  r0   fastapir1   r2   r3   r4   r5   r6   r7   r8   fastapi.middleware.corsr9   fastapi.responsesr:   r;   r<   r=   fastapi.staticfilesr>   pydanticr?   r]   tools.lazy_depsr@   _lazy_ensurerW   rJ  r  r  ru  	getLoggerr}  rM   re  rS   rY   rf  r_   ry   r}   rk   r   rg  r   r`   hermes_cli.memory_oauthr   _memory_oauth_routerr  r   r  r   r   r1  r   r  r  r  add_middleware&hermes_cli.dashboard_auth.public_pathsr   r   r   r   r  r   r   r   r   r   r   
middlewarer   r   r   r   r  r?  rT  rF  ri  rs  rE  
_mcl_entryry  rq  _k_vr{  r  r  r  r  r  r  r  r  r  r  r  r  rp  r  r  r  r  r  r  r  r  rr   r  r  rg  r  r  r  r  r  r  r  r  r  rc  r3  r9  r5  r>  r  rq  rs  r~  ru  rv  rr  rz  r  r  stat_resultr  r  r  r  rb  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r!  r&  r.  r=	  r:  rF  rO  rQ  rl  rW  re  rx  rz  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  r0  rA  r7  rC  rg  ry  r{  putr  r  r  r  r  r  r  r  rx  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  compiler"  r'  r-  r8  r<  rM  rT  r`  rj  rv  rx  r~  r  r  r  r  r  r  r  r	  r  r  r  r  r  r)  r,  r.  r8  r@  rB  rG  rM  rc  rm  ry  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  rx  ry  rz  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  RLockr  r  r  r  r  r  r  r  r  r  r  r  r  r  r   r&  r*  rE  rJ  rM  r[  rq  ru  rw  rz  r  r  r  r  r  r?  r  r  r  _ANTHROPIC_OAUTH_TOKEN_URLr  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r	  r	  r
	  r	  r	  rF	  rH	  rJ	  rL	  rN	  re	  rg	  rl	  ro	  rr	  ry	  r  r~	  r	  r	  r	  r	  patchr	  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*
  r9
  r;
  rF
  rL
  rN
  rQ
  rT
  rY
  r^
  rc
  rh
  rl
  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)  r6  r=  r@  rC  rF  rI  rL  rP  rS  rV  rX  r]  r_  rf  rx  rz  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  rM  rP  rU  ra  rd  rt  rx  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  r5  r9  r=  r?  rB  rF  r]  rx  r  r   hermes_cli.win_pty_bridgery  r}  rz  rI  rn  hermes_cli.pty_bridgerL  rC  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r
  r   r4  r  r  r  r  r  r  r  r  r  r  r  r  	websocketr>  rQ  rU  rY  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   r6  r8  r:  r=  ri  rk  ro  rq  rx  rz  r}  r  r  r  r  r  r  r   hermes_cli.dashboard_auth.routes_dashboard_auth_routerr  r  r  r  r  rT   rR   <module>r     s  	 	 	 ; : : : : : : :             ! ! ! ! ! ! ' ' ' ' ' ' ' '             				 				        



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

 /
 
C2& {0Q> 'C24 S4V> 5C2B H;
 W CC2Z BFQ= [C2h [8S iC2v  PH.2 wC2D EV

 EC2f 
C4?C gC2t D]DH uC2B 9a
 ? CC2Z Ia [C2d #?N4) eC2r DN

 sC2T ,[]L= UC2b (N.

 K
 
cC2z N&I9  	[ #;	  bV
   W_I yC2 C2 C2 T#tCH~-. C C CN$sCx   6B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!( ( `( 
 [#  g&  &<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(^ 	 	 	 	-# - - - - 	@ @S>@c3h@ D[@ 	@
 
#s(^@ @ @ @FA At A A A A A $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  QUVY[^V^Q_    8 
ABBMQA AA2A=Ec]A A A CBAH =>>    ?> 	#$$
 
8C= 
 
 
 %$
( 	122NR'M 'M'M3'M>Fsm'M 'M 'M 32'MT 
7881 1s 1Xc] 1 1 1 981@   8C=  3  s        2G0c3h G0 G0 G0 G0T0$sCx. 0 0 0 00T#s(^    @ -5  (56  %39  ! 6,  7 2V
 
 &'8(  #2D,  S+A- IL7 tCH~s23 L L L^[ # [ T#s(^ [  [  [  [ |c3h HSM    2
d38n 
d3PS8n 
YabeYf 
 
 
 
/d4S>2 / / / /d 	  '( '( '( '( '( ! '(T 011 "?@ ?@?@?@ c]?@ ?@ ?@ 21?@L % -/c4S>)* / / /%y~'' 
'                "& ' ' '!&'!D + + + +# 8C=    +Xc] +t + + + + " 
 c] 3S#X	   0 #4 44sm4 c]4 4 4 4@Ec @E# @EVY @E^b @E @E @E @EF 8C= DcN    < "M. M.M.M. c]M. 
#s(^	M. M. M. M.d "Qm QmQmc]Qm 
#s(^Qm Qm Qm Qmh=+S =+T =+ =+ =+ =+@Q+ Q+ Q+ Q+ Q+ Q+h=+3 =+4 =+ =+ =+ =+@t, t, t, t, t, t,n 
455 ""D "D"D"D c]"D "D "D 65"DJ    i   
 
566
 "	[ [[
[ [ c]	[ [ [ 76[ 	?@@ "  c]   A@2 899 "2 222 c]2 2 2 :92(J3 J J J Jn" " " " " " " "
 
%&&..@ . . . '&.b 	$%% #    &% !"" (3-    #"6 	 Xc]     <6(3- 6 6 6 6 	%&&  x}    '& 	788	C 	 	 	 98	 	.//
 
3 
# 
 
 
 0/
 ()) c HSM    *)0" " " " "I " " " '((c     )(B 	,-- c HSM    .-" " " " "9 " " " 
      ! . # 1+ 1+
1+1+ C=1+ }	1+
 SM1+ 1+ 1+ 1+r    I        I    EJ   s T hWZm    
S 
Xd3i%8 
 
 
 
C t QT    .
S#X 
4 
 
 
 
*#s(^ 
#s(^   B
49
 
   " %Y_&& 5T$sCx.1 5 5 5 56 6%T	2B 6 6 6 6DcN S  cSVh    #8C= #S # # # #L3 8C=     	 #    " 	"## s Xc]    $# 	'((* *S *8C= *PS * * * )(*Z 
#< #< #< #< #< #< #<L 	%&&    '& 6 	"## # ] Xc]    $#D 
)**  x}    +* 
*++ #     ,+ 
+,, 3 #    -, %&&
 
# 
 
 
 
 '&
2 2S 2T 2 2 2 2> 
4SW 4S 4S 4S 4Sx         Y      
 	  < < ! <B 
,--< <&D <s < < < .-<D	" 	" 	" 	" 	"i 	" 	" 	"" " " " "	 " " "c3h DcN    c S#X 4S>      		 	HSM 	 	 	 	 
)4 )4 )4# )4 )4 )4 )4X 	 $5     $ %&& #     '& 
())8 8 8hsm 8 8 8 *)8v 
())c8 c8 c8hsm c8 c8 c8 *)c8L" " " " "y " " "
 	*++@DE E
E%E08E E E ,+E* 	I< I<HSM I< I< I< I<X" " " " "	 " " " 
$%%<; <;*; <;hsm <; <; <; &%<;~3 3       13R S S S    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 B B  B 
A A A 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 ! ! !
)t ) ) ) )YD Y Y Y Y
 
=    4 	#$$S    %$,	 	 	 	 	I 	 	 	 
;= ; ; ; ;"sTz c     
"##tCyy$u++J J
JJ J J $#JZ 	,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"3 S S     
#$$7 7"5 7 7 7 7 %$7&" " " " "I " " "
 
%&&9 9$9 9HSM 9 9 9 '&9$" " " " ") " " " 
"##IMB B
&
'B9A#B B B $#B& "".  	 	 	 	 	      > 	"##ER ER8C= ER ER ER $#ERP 	!""PT/Q /Q
/Q/Q-0/Q@H/Q /Q /Q #"/Qd 	"##= = =8C= = = = $#=@ 	  [ [S [ [ [ [ ! [F    I   8    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 *# *$ * * * **6D 64@Q;R 6WZ 6 6 6 6r!D !S	 !c ! ! ! !H C C C /m m m m m` 	  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 *8C= * * * *B" " " " ") " " "  hsm    > 	
D 
D[ 
D8C= 
D 
D 
D 
D" " " " ") " " "" " " " " " " "
 
 
 
 	I I# I I I I  I$ -[    $ 	%7      	            F" " " " "I " " "
 	%&&? ?s ?- ?(3- ? ? ? '&?> 	,--; ;3 ;# ; ; ; .-;|" " " " "I " " "   # T hsm     	23 	2S 	2 	2 	2 	2s D HSM V^_cVd    & 	,--HL8 8
8!#88@8 8 8 .-8v" " " " " " " " 	+,,BF+K +K
+K'+K2:3-+K +K +K -,+K\ 	.//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z 	)**
% 
%8C= 
% 
% 
% +*
% 
566G G(3- G G G 76GF" " " " "i " " "
 	I I(3- I I I I 		J 	J/ 	JHSM 	J 	J 	J 	J" 	  C CC Cx} C C C ! CL 	 !!W WS W W W W "!WX <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6(*k (*d (* (* (* (*V{ x}    D	.; 	.4 	. 	. 	. 	.?; ?8C= ? ? ? ?
H{ Ht H H H H
s    Q% Q%hsmS.@(A Q% Q% Q% Q%h*K *D * * * * !!%!)-	Y6 Y6SMY6#Y6 c]Y6 "#	Y6
 49hsmXd^34Y6 Y6 Y6 Y6x(x} ( ( ( (D !!%!)-	
 
SM
#
 c]
 "#	

 49hsmXd^34
 
 
 
>!) !) !) !) !) !)H_ _c _C _D _ _ _ _Ay AXc] A A A A) c d    D Xc]    d t    
;3 
;3 
; 
; 
; 
;* #'    !" EI 8J.AB I I I')) z1D    (GC G G G G
 x}    

9
9

9 	
9
 c]
9 	
9 
9 
9 
9$$|$ #s(^$ 
	$ $ $ $zz|z z
 z 
z z z zzs uXd38n-ExPS}-T'U    ( ~` `t ` ` ` `F zx.Y x.4 x. x. x. x.L 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. / / /$ $ $$ $ $ $ $ 	!""$ $ #"$N 	())/ / *)/    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 = = = ;:=, 	<==T# T# T T T >=TnoY oY oYf      N M M M M M   ) * * * 		#
- 
 
 
 
 
 
$QS $QT $Q $Q $Q $QN(8
(8(8/3(8FI(8	(8 (8 (8 (8X D D
D
D D 	D
 D D D D D Ds   2F G3AGG3G--G32G3	[$ $-\\E0!BF FBFFBFj5Ck kCk'k&Ck'k+Ck= k=CllCl