
    agj                    X   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$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/m0Z0m1Z1 ddl2Z2 e+e3      jh                  jh                  jk                         Z6 e7e6      ejp                  vr"ejp                  js                  d e7e6             dd	l:m;Z;m<Z< dd
l=m>Z>m?Z?m@Z@mAZAmBZBmCZCmDZDmEZEmFZFmGZGmHZHmIZImJZJmKZKmLZLmMZMmNZNmOZOmPZPmQZQmRZR ddlSmTZTmUZUmVZVmWZW ddlXmYZYmZZZm[Z[m\Z\m]Z]m^Z^m_Z_ ddl`maZa 	 ddlbmcZcmdZdmeZemfZfmgZgmhZhmiZimjZj ddlkmlZl ddlmmnZnmoZompZpmqZq ddlrmsZs ddltmuZumvZv ddlwmxZx dej                   v r e+ej                   d         n e+e3      jh                  dz  Z ej                  e      Zddddeddfd Zdd!Zdefd"Zedd%       Zdd&Zd#d$dej                  fd'Zd#d$dee7e+f   fd(Z ecd)e;e*      Zdd+lmZ ej)                  e       ej                   j+                  d,      xs  ej,                  d-      Zd.Zd/Zg Ze/e   ed0<   d1Zd2Zej=                  eld3d4gd4g5       dd6lmZ d7egdefd8Z ed9h      Zee7   ed:<   d7egd;e7defd<Zd7egddfd=Z eh d>      Zeed?<   dd@e7dAedefdBZdCe7dDe7defdEZejW                  dF      d7egfdG       ZejW                  dF      d7egfdH       ZejW                  dF      d7egfdI       ZejW                  dF      d7egfdJ       ZejW                  dF      d7egfdK       Zde/e7   fdLZi dMdNdO e       dPdQdRdSdTdUdVdWdXdTdUdYdNdZg d[dPd\dNd]d^d_gdPd`dNdag dbdPdcdNddg dedPdfdNdgdhdigdPdjdNdkg dldPdmdNdng dodPdpdNdqg drdPdsdNdtg dudPdvdNdwg dxdPdydNdzd{d|gdPd}dNd~g ddPddNdg ddPddNdg ddPdNdg ddPdNdddgdPdddddddZe.e7e.e7e-f   f   ed<   i ddddddddddddddddddddddddddTddddddddZe.e7e7f   ed<   g dZde-de7fdZ	 dde.e7e-f   de7de.e7e.e7e-f   f   fdZ ee?      ZedV   Zi Ze.e7e.e7e-f   f   ed<   eju                         D ]  \  ZZeee<   edQk(  seedV<    eZde-defdZde7de/e7   de.e7e-f   de/e7   fdZde.e7e.e7e-f   f   fdZ G d deu      Z G d deu      Z G d deu      Z G d deu      Z G d deu      Z G d deu      Z G d deu      Z G dĄ deu      Z G dƄ deu      Z G dȄ deu      Z G dʄ deu      Z G d̄ deu      Z G d΄ deu      Z G dЄ deu      Z G d҄ deu      Z G dԄ deu      Z G dք deu      ZddddddddddddddZe.e7e7f   ed<   dZde7de7fdZ G d deu      Z G d deu      Z G d deu      Z G d deu      Zde7dQe7dee7e7f   fdZ	 dddde7dQe7de7de7defdZ ej                  d      Z	  e ej                  dd            ZdZdeeedz  f   fdZdefdZdefdZddddd ddddZdZdZdZ e+d      Z ed/       G d	 d
             Zh dZ eh d      Z eddh      Zde7defdZd;e+defdZdZdZdZ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/d0d1d2d3d4d5d+d6d7d8d9d:d;d<d=d>d?d;d@d@d9dAZi dBdCdDddِdEdFddGddHddڐdIdJdKdLdMdېdNdܐdOdݐdPdQdRdSddTddސdUdߐdVdWd iZdXe7de+fdYZd;e+de7fdZZd[edefd\Zd;e+dee+ej                  f   fd]Zd^e+de7dz  fd_Zde7fd`Zdae7de7fdbZdee+   fdcZ ej+                  dd      d;e7fde       Zddfd;e+dgede+fdhZdXe7e+z  de+fdiZdje+dke+defdlZdXe7dz  de7fdmZd7egdefdnZdefdoZdefdpZd/dqd7egdredefdsZ	ddtdXe7dz  d7egduedeee+e7f   fdvZ
dwede.e7e-f   fdxZdwedke+de.e7e-f   fdyZdze7deee7f   fd{ZdZ eh d|      Zd}Zeeee7f   d~f   ed<   de7dz  de7fdZd[ede7dz  fdZdedeee7e7f   fdZej)                  d      ddeΐde0e7   fd       Zej+                  d      dd7egd;e0e7   fd       Zej+                  d      d7egd;e7fd       Zej+                  d9      d7egd;e7fd       Zej)                  d      ded7egfd       ZdZej)                  d       edd~       eed~       eed/      fd7egdehd;e7defd       Zej)                  d      ded7egfd       Zej;                  d      ded7egfd       Zej+                  d      d;e7fd       Zej+                  d      d;e7fd       Z  G d deu      Z!ej)                  d      de!fd       Z"ej+                  d      d;e7fd       Z#ej+                  d      d;e7fd       Z$ej+                  d      d        Z%ddl:m&Z' d Z(d;e7de7fdZ) G d deu      Z* G d deu      Z+ G d deu      Z, G d deu      Z- G d deu      Z. G d deu      Z/ej+                  d      d;e7fd       Z0ej+                  d      d;e7fd       Z1ej+                  d      d;e7fd       Z2ej+                  d      d;e7fd       Z3ej+                  d      dd;e7de7de0e7   fd       Z4ej+                  d      	 dd;e7de7de7de0e7   def
d       Z5ej+                  dë      d;e7de7fdĄ       Z6ej+                  dū      d;e7fdƄ       Z7ej+                  dǫ      dd;e7de0e7   fdɄ       Z8ej+                  dʫ      d;e7fd˄       Z9ej)                  d̫      d͐e+fd΄       Z:ej)                  dϫ      d͐e+fdЄ       Z;ej)                  dѫ      d͐e+fd҄       Z<ej)                  dӫ      d͐e,fdԄ       Z=ej)                  dի      d͐e*fdք       Z>ej)                  d׫      d͐e*fd؄       Z?ej)                  d٫      d͐e-fdڄ       Z@ej)                  d۫      d͐e.fd܄       ZAej)                  dݫ      d͐e/fdބ       ZBdߐddddddddd	ZCe.e7e1e7ef   f   ed<    eh d      ZDde+de0e   de.e7ef   fdZEde.e7e-f   fdZFej+                  d      dde0e7   fd       ZGdZHde7de7de0e   fdZIde7de7de7de7de.e7e7f   f
dZJej+                  d      d        ZKej+                  d      d        ZL G d deu      ZMej                  d      d͐eMfd       ZOej)                  d      d         ZPej+                  d      dde0e7   fd       ZQ G d deu      ZR G d deu      ZSej+                  d      dde7de0e7   fd	       ZTej;                  d      d͐eRfd
       ZUej                  d      d͐eSfd       ZVde7fdZWej+                  d      d        ZXej)                  d      d        ZYej)                  d      d        ZZej)                  d      d        Z[ G d deu      Z\ej)                  d      dd͐e\dz  fd       Z] eD       dz  Z^e+ed<   dZ_dZ`dZai dd d!d"d#d$d%d&d'd(d)d*d+d,d-d.d/d0d1d2d3d4d5d6d7d8d9d:d;d<d=d>d?d@Zbe.e7e7f   edA<   i Zce.e7ej                  f   edB<   i Zee.e7e1e7d~f   f   edC<   i Zfe.e7e.e7e-f   f   edD<   dde7dEe7dFeddfdGZgde7fdHZhdIe/e7   de7dej                  fdJZid;e+dKede/e7   fdLZjde0e7   dMe7de/e7   fdNZkde0e7   dMe7de7fdOZl ej                  dP      Zn ej                  dQ      Zo ej                  dR      ZpdSe7dTe7de7ddfdUZqdde0e7   de1ej                  ef   fdVZrdde0e7   dee7e-f   fdWZsej)                  dX      dde0e7   fdY       Ztej)                  dZ      d7egfd[       Zuej)                  d\      d]        ZvddKede/e.e7e-f      fd^Zwej+                  d_      dd`efda       Zxej)                  db      defdc       Zy G dd deeu      Zzdfe.e7e-f   de7fdgZ{da|e0e7   edh<   die0e7   defdjZ}ej+                  dk      dl        Z~ej)                  dm      dezfdn       Zej+                  do      dde7dpefdq       ZdrZdse/e.e7e-f      de/e.e7e-f      fdtZej+                  du      	 	 	 	 	 	 	 	 	 	 d dvedwedxedye7dze7d{e7d|e7d}e7d~ede0e7   fd       Zej+                  d      	 	 	 	 	 	 	 	 	 ddvedwedxedye7dze7de7d{e7d|e7d~efd       Zej+                  d      	 	 	 	 	 	 dde7dede7dedede7fd       Zej+                  d      dde7dvede0e7   fd       Zde.e7e-f   de.e7e-f   fdZdeUde.e7e-f   fdZ e       Ze-ed<   deUde7de-fdZdeUde-de7fdZdeTde+fdZdeTde.e7e-f   fdZdeUdeؐde.e7e7f   de-fdZdeUdeؐde.e7e7f   defdZd Zdee.e7e-f   e7e.e7e-f   f   fdZdeTde.e7e-f   fdZdeTde.e7e7f   ddfdZdeTde.e7e7f   ddfdZdeTde.e7e7f   ddfdZde.e7e-f   de.e7e7f   fdZdeTde.e7e7f   ddfdZde7de7fdZde-de7fdZde7fdZde7de.e7e-f   fdZde-de/e7   fdZde7de.e7e-f   fdZde7de.e7e-f   fdZdddddZde7de7fdZde7de7fdZde7defdZdde0e7   dvede7fdZde.e7e7f   fdZddddde7de7de7de7de0ejL                     de0e7   de.e7e-f   fdZdddde-de7d9ededejL                  f
dZde.e7e-f   defdZde/e7   de/e.e7e-f      fdZde/e.e7e7f      de/e.e7e-f      fdÄZde7de.e7e-f   fdĄZde7de-de/e.e7e-f      fdńZd;e+de.e7e-f   fdƄZde7de.e7e-f   fdǄZde0e7   de7fdɄZddʜde-d{edefd˄Zde.e7e-f   de-fd̄Zde.e7e-f   d[e.e7e-f   de-fd̈́Zde.e7e-f   d[e.e7e-f   defd΄Z	 dde.e7e-f   d[e.e7e-f   de0e.e7e.e7e-f   f      defdЄZde.e7e-f   d[e.e7e-f   de.e7e-f   fdфZde7de-de.e7e-f   fd҄Zde.e7e-f   de-de-fdӄZde7de-de.e7e-f   ddfdԄZde7de-defdՄZde/e.e7e-f      fdքZde7ddfdׄZde7de-de.e7e-f   ddfd؄Z ej                  d٫      Zde7ddfdڄZej+                  d۫      dde7de0e7   de0e7   fd݄       Zej)                  dޫ      de7defd߄       Zej                  d۫      	 dde7deĐde0e7   de0e7   fd       Zej+                  d      dde0e7   fd       Zej+                  d      d        Zej+                  d      dde0e7   fd       Zdddddi dZeed<   ej+                  d      dde0e7   fd       ZǐdZe1e7d~f   ed<   ej+                  d      	 	 	 	 dde0e7   dededefd       Zej+                  d      dde7fd       Zej+                  d      dde0e7   fd       Zej+                  d      dde0e7   fd       Zej                  d      ddeאde0e7   fd       Zej)                  d      ddeԐde0e7   fd       Z	 dde7de7dQe7de7de7de7fdZϐde7de7dee7e7f   fdZde.e7e-f   de.e7e-f   fd Zej                  d      ddede0e7   fd       ZdefdZej+                  d      dde0e7   fd       Zej                  d      ddede0e7   fd       ZՐdddd	d
Zee7ee7e7f   f   ed<   ddde/e7   fdZאdde7de7de7fdZؐde.e7e-f   de/e7   fdZde.e7e-f   de.e7e-f   fdZde.e7e-f   de7ddfdZde.e7e-f   dede1e7e.e7e-f   f   fdZej+                  d      d        Zej)                  d      defd       Zej)                  d      de7fd       Zej;                  d      de7fd       Zej)                  d      defd       Zej)                  d       ded7egfd!       Zej;                  d      ddede0e7   fd"       Zej)                  d#      	 dded7egde0e7   fd$       Zi dd%d&d'd(d)d*dd+d,d-d.d/d*d0d1d2d3d4d5d*d6d7d8d9d:d;d*d<d=d>d?d@dAd*dBdCdDdEdFdGd*dHdIdJdKdLdMd*dNdOdPdQdRdRd*dSdTdUdVdWdWd*dXdYdZd[d\d\d*d]d^d_d`dadad*dbdcdddedfdgd*dhdidjdkdldmdndodpdqdrd*dsdtdudvdwdxd*dydzd{d|d}d~d*ddddddd*dddddd*ddiddddMddddVddMd*dddddMd*dZee7ee7e-f   f   ed<   dZee7d~f   ed<   i ddddEddddddddddddd/ddddd/ddddd/ddddddddddddd/ddddddddd/dddddddÐdĐddŐdƐdƐd[ddǐdȐdd/ddɐdʐdːdd̐d͐dd/ddϐdАddѐdd/ddӐdԐddՐd֐ddאdd/ddِdڐddېdd/ddݐdސddߐddddd/dddd/dddd/ddddddd/ddZee7ee7e-f   f   ed<   deee7e-f   d~f   fdZdee7   fdZ eh d      ZdSe7dee7d~f   fdZdSe7dee7d~f   fdZdSe7dee7e-f   de-dz  dee7d~f   fdZ	 ddSe7de-dz  dee7e-f   fdZdSe7dee7e-f   dz  fdZdTe7dee7e-f   fdZdSe7fdZ	 ddee7e-f   dee7e7f   dedz  dedee7e-f   f
dZdSe7deddfdZdZh dZe G d  d             Zi Zee7ef   ed<    e j                         Zdede7fdZde-de7fdZde-de7fdZde+fdZde-de7dz  fdZd	e+dee7dz  e7dz  e7dz  f   fd
Zde+ddfdZ d	e+de7dej                  fdZdej                  dz  ddfdZde7dej                  ddfdZde7d	e+de7ddfdZddZd	e+ddfdZde7dedee7e-f   fdZdde0e7   dee7e-f   fdZej)                  d      defd       Z	ej+                  d      de7fd       Z
ej)                  d      	 dde7deːde0e7   fd       Zej;                  d      de7fd       Zd Zd!e; Ze G d" d#             Zi Zee7ef   ed$<    e j                         Zde7fd%Zde7defd&Zdd'Zde-de7dz  fd(Zde7de7de7fd)Zddd*d+e7d;e7dee7e-f   dz  d,e7dz  dee7e-f   f
d-Zddd*d+e7d;e7dee7e-f   dz  d,e7dz  dee7e-f   f
d.Zej)                  d/      defd0       Zej+                  d1      de7fd2       Zdde0e7   dee7e-f   fd3Zej)                  d4      	 dde7deɐde0e7   fd5       Zej;                  d1      de7fd6       Zej+                  d7      dde0e7   fd8       ZdSe7d9e0e7   de0e7   fd:Zej                  d;      	 ddSe7deǐde0e7   fd<       Z ej)                  d=      ddSe7de0e7   fd>       Z!dde0e7   d?ede7fd@Z"de.e7e-f   fdAZ#de.e7e-f   fdBZ$de.e7e-f   fdCZ%dDdEdFdGdHddIdJdKdFdLdMddIdNdOdPdQdRddIdSdTdFdUdVddIdWdXdFdYdZddId[d\dPd]d^e%dId_d`dadbdce#dIdddedPdfdge$dIfZ&ee.e7e-f   d~f   edh<   die7de.e7e-f   fdjZ'de.e7e-f   de0e7   fdkZ(de.e7e-f   de.e7e-f   de0e7   fdlZ)dee.e7e-f      fdmZ*ej+                  dn      dde0e7   fdo       Z+ej;                  dp      	 ddie7d7egde0e7   fdq       Z,drZ-i Z.e.e7e.e7e-f   f   eds<    e j                         Z/	 ddtl0m1Z2m3Z4m5Z6m7Z8m9Z:m;Z< d/Z=duZ>ddvZ?de0e7   de0e7   fdwZ@de0e7   ddfdxZA	 ddie7dye7de0e7   dee7e.e7e-f   f   fdzZB	 dd{e7de0e7   de0e7   fd|ZCd}e7d~e7deddfdZDdde0e7   de.e7e-f   fdZE	 dd{e7de7de0e7   de.e7e-f   fdZF	 ddie7de0e7   de.e7e-f   fdZGd{e7ddfdZHd{e7ddfdZId{e7ddfdZJde-de7fdZKde-de7fdZLd{e7ddfdZMej)                  d      	 ddie7d7egde0e7   fd       ZN G d deu      ZOej)                  d      	 ddie7d͐eOd7egde0e7   fd       ZPej+                  d      	 ddie7d{e7de0e7   fd       ZQej;                  d      	 dd{e7d7egde0e7   fd       ZRd{e7fdZS G d deu      ZT G d deu      ZUdZVd7egdefdZWde0e7   dse/e.e7e-f      de.e7e-f   fdZXej)                  d      d͐eTfd       ZYej)                  d      d7egfd       ZZej+                  d      dde0e7   fd       Z[ej;                  d      dde0e7   fd       Z\ej+                  d      dde0e7   fd       Z]de0e7   fdZ^ej+                  d      dd{e7de0e7   fd       Z_ej+                  d      	 dd{e7de0e7   fd       Z`ej+                  d      	 	 	 d	d{e7de0e7   dve0e   dwefd       Zaej;                  d      dd{e7de0e7   fd       Zb G d deu      Zcej                  d      d{e7d͐ecfd       Zeej+                  d      dd{e7de0e7   fd       Zf G d deu      Zgd͐egfdZhej)                  d      d͐egfd       Ziej+                  d      	 	 	 	 	 d
de7dpede0e7   de0e7   de0e7   f
d       Zj G d deu      Zk G d deu      Zlddde-dede0e7   fdÄZmde-de0e/e7      fdĄZnde-de+de0e7   fdńZode.e7e-f   ddfdǄZpde.e7e-f   de+de.e7e-f   fdȄZqde0e/e7      de7ddfd˄Zrde/e.e7e-f      fd̄Zsde7fd̈́Ztde0e7   de1e7e+f   fd΄Zude.e7e-f   de7de+de.e7e-f   fdЄZvde0e7   de7fdӄZwde7de0e7   fdՄZxdde7fdքZydׄ Zzej+                  dث      dde7fdل       Z{dde7de0e7   fdڄZ|ej+                  d۫      dde7de0e7   fd܄       Z}dde7de0e7   dvefd݄Z~ej+                  dޫ      dde7de0e7   dvefd߄       Zdd͐ekde0e7   fdZej)                  dث      dd͐ekde0e7   fd       Zej+                  d      d        Zdde7d͐elde0e7   fdZej                  d۫      dde7d͐elde0e7   fd       Zdde7de0e7   fdZej)                  d      dde7de0e7   fd       Zdde7de0e7   fdZej)                  d      dde7de0e7   fd       Zdde7de0e7   fdZej)                  d      dde7de0e7   fd       Zdde7de0e7   fdZej;                  d۫      dde7de0e7   fd       Zde7de7defdZej)                  d      d7egfd       Z G d deu      Zej+                  d      d        Zej)                  d      dʐd͐ede7fd       Z G d deu      Z G d deu      Zd͐edee7e.e7e-f   e0e7   f   fdZde.e7e-f   de.e7e7f   fdZde7de.e7e-f   de.e7e-f   fd Zej+                  d      dde0e7   fd       Zej)                  d      dd͐ede0e7   fd       Zej                  d      dd͐ede0e7   fd       Zej;                  d      dde7de0e7   fd       Zej)                  d      dde7de0e7   fd       ZdrZd	Zi Zee7d
f   ed<    e j                         Zi Zeee7e7f   e j                  f   ed<    e j                         ZddZde7de7de7fdZd7egde7de7fdZde j                  fdZdeddfdZej)                  d      dde7d7egde0e7   fd       Zej+                  d      de7d7egfd       Zej+                  d      	 	 	 dde7de0e7   de0e7   de0e7   fd       Z G d deu      Zej                  d      	 dde7d͐ede0e7   fd       Zej+                  d       dde0e7   fd!       Z G d" d#eu      Zej)                  d$      dd͐ede0e7   fd%       Zde7de7fd&Zebja                  d'd(        G d) d*eu      Z G d+ d,eu      Zd- Zej+                  d.      d/        Zej)                  d0      d͐efd1       Zej)                  d2      d͐efd3       Zej)                  d4      d5        Z G d6 d7eu      Zde7d8e.e7e-f   de7de.e7e-f   fd9Zej+                  d:      d;        Zej)                  d<      d=        Zej)                  d:      d͐efd>       Zej;                  d?      de7fd@       Z G dA dBeu      Zej                  dC      de7d͐efdD       Zej)                  dE      dde0e7   fdF       Zej)                  dG      dde0e7   fdH       Z G dI dJeu      Zde-dKede.e7e-f   fdLZej+                  dM      dN        Zej)                  dM      d͐efdO       Zej;                  dP      de7dKefdQ       Z G dR dSeu      Z G dT dUeu      Zej+                  dV      dW        Zej                  dX      d͐efdY       Zej)                  dZ      d͐efd[       Zej)                  d\      d]        Zej)                  d^      d_        Z G d` daeu      Zde+fdbZde+fdcZej)                  dd      d͐efde       Zej+                  df      dge7fdh       Z G di djeu      Zej)                  dk      d͐efdl       ZԐde7dz  de7fdmZej)                  dn       edd~       eed      fdehd`efdo       Zej+                  dp      dq        Z G dr dseu      Zej)                  dp      d͐efdt       Z G du dveu      Zej;                  dp      d͐efdw       Zej+                  dx      dy        Zej)                  dz      d{        Z G d| d}eu      Zސde0e7   de/e7   fd~ZߐdMe7dTe7de7fdZej)                  d      dd͐eސde0e7   fd       Z G d deu      Zej)                  d      dd͐ede0e7   fd       Z G d deu      Zej)                  d      	 dde0e   de0e7   fd       Zddddddddddd
ZdefdZdde0e7   defdZej+                  d      dde0e7   fd       Zej+                  d      	 dde7d{e7dvede0e7   fd       Zej+                  d      dde7de0e7   fd       Zej+                  d      dde7de0e7   fd       Z G d deu      Z G d deu      Z G d deu      Z G d deu      Z G d deu      Z G d deu      Z G d deu      Zdde7d{e-de-fdZde.e7e-f   fdZde/e.e7e-f      fdZde7de+fdZde7de7fdZde+de7dQe7ddfdZde+de/d   defdZde+de/e7   defdZej+                  d      d        Zej)                  d      d͐efd       Zej+                  d      d        Zej)                  d      d͐efd       Zej+                  d      de7fd       Z ej)                  d      de7fd       Zej                  dë      de7d͐efdĄ       Zej;                  dë      de7fdń       Zej+                  dƫ      de7fdǄ       Zej                  dƫ      de7d͐efdȄ       Zej                  dɫ      de7d͐efdʄ       Zej                  d˫      de7d͐efd̄       Zej)                  dͫ      de7d͐efd΄       Z e j                         Z	ede0e7   fdτ       Z
ede0e7   fdЄ       Z G dф deu      Zej+                  dӫ      dde0e7   fdԄ       Zej                  dի      dd͐ede0e7   fdք       Z G dׄ deu      Z G dل deu      ZddۄZej+                  dܫ      dde7de0e7   fd݄       Zej)                  dӫ      d͐efdބ       Zej                  dܫ      d͐efd߄       Zej+                  d      dde0e7   fd       Z G d deu      Zej                  d      dde7d͐ede0e7   fd       Zej+                  d      dde7de0e7   fd       Z G d deu      ZdddZde7dede0e7   fdZde7de7fdZde7dede0e7   de0e   fdZej+                  d      	 dde7de0e7   de0e7   fd       Z G d deu      Zej                  d      	 dde7d͐ede0e7   fd       Z ej                  d      	 dde7d͐ede0e7   fd       Z! G d deu      Z"ej                  d      dde7d͐e"de0e7   fd       Z# G d d eu      Z$ej)                  d      	 dde7d͐e$de0e7   fd       Z%ddddddd	dd
dddddddddddddddgZ&e/e.e7e7f      ed<   e&D  ch c]  } | d   
 c} Z'dedTe7de7de7fdZ(defdZ)defdZ*dedefdZ+defdZ,defdZ-de7dedefdZ.ej+                  d       dde0e7   fd!       Z/ G d" d#eu      Z0ej                  d$      	 dd͐e0de0e7   fd%       Z1ej+                  d&      dde0e7   fd'       Z2ej)                  d(      dde0e7   fd)       Z3 G d* d+eu      Z4ej+                  d,      dde0e7   fd-       Z5ej                  d,      dd͐e4de0e7   fd.       Z6d/ede/e.e7e-f      fd0Z7d1e/e.e7e-f      d2e/e.e7e-f      de/e.e7e-f      fd3Z8d2e/e.e7e-f      de/e.e7e-f      fd4Z9dd5ede0e7   fd6Z:ej+                  d7      dd5ede0e7   fd8       Z;dd5ede0e7   fd9Z<ej+                  d:      dd5ede0e7   fd;       Z=ej|                  j                  d<      r	 dd=l@mAZBmCZC d/ZDn	 dd@lFmBZBmCZC d/ZD ej                  dB      ZGdCZHddDlImJZJmKZKmLZL  eJdEdFdeHG      ZMddJZN ej                  dK      ZO eh dL      ZPdHdIde0e7   fdMZQdHdIdefdNZRdHdIde0e7   fdOZSdHdIdefdPZTdHdIde0e7   fdQZUdHdIdefdRZVde7fdSZWdHdIdee0e7   e7f   fdTZXdHdIdefdUZY	 	 	 	 ddVe0e7   dWe0e7   de0e7   dXe0e7   deee7   e0e7   e0e   f   f
dYZZ edZd[h      Z[de0e7   fd\Z\de0e7   fd]Z]	 	 	 	 ddVe0e7   dWe0e7   de0e7   dXe0e7   deee7   e0e7   e0e   f   f
d^Z^d_e7de0e7   fd`Z_d#e-d_e7de7ddfdaZ`dHeide0e7   fdbZad#d$d_e7de+fdcZbd;e+de0e7   fddZcd;e+ddfdeZdd?e7de7fdfZedgZfdhZgdiZhdjZidaje0e	j                  j                     edk<    e j                         Zmde	j                  j                  fdlZndHeide0e7   fdmZodne-doe7dpede0e7   de-f
dqZpdHeidrej                  de.e7e-f   ddfdsZqdHeidrej                  dte-dueddf
dvZrdwe-dee0ee7e-f      e0e7   f   fdxZsej                  dy      dHeiddfdz       Zuej                  d{      dHeiddfd|       Zvej                  d}      dHeiddfd~       Zwej                  d      dHeiddfd       Zxej                  d      dHeiddfd       Zyde0e7   de7fdZzde7fdZ{decfdZ|d{dddddddddddddddddddddddddddddddgZ}dde-de7dede0e.e7e-f      fdZ~ddddddZe.e7e7f   ed<   dddZe.e7e7f   ed<   h dZh dZh dZh dZdZd[e.e7e-f   de0e.e7e-f      fdZdefdZej+                  d      d        Z G d deu      Zej                  d      d͐efd       ZdZ eh d      Zej+                  d      d        Z G d deu      Zej                  d      d͐efd       Zde-de+de0e7   fdZdefdÄZdae0e   ed<   ddedefdƄZej+                  dǫ      dȄ        Zej+                  dɫ      dʄ        Z G d˄ deu      Zde.e7e-f   de.e7e-f   fd΄Zde.e7e-f   fdτZej+                  dЫ      d7egfdф       Zej)                  dҫ      d7egd͐efdӄ       Zde7de7fdԄZej)                  dի      d7egde7fdք       Zej)                  d׫      d7egde7fd؄       Zej)                  d٫      d7egde7fdڄ       Zej;                  d۫      d7egde7fd܄       Z G d݄ deu      Zej                  d߫      d7egd͐efd       Z G d deu      Zej)                  d      d7egde7d͐efd       Zej+                  d      de7de7fd       Zd Z e        dd+lmZ ej)                  e        e|e       dddedefdZdeddfdZd@e7dedede7ddf
dZ	 	 	 	 	 	 dd@e7dededAede7defdZy# ey$ ro 	 ddlzm{Z|  e|dd       ddlbmcZcmdZdmeZemfZfmgZgmhZhmiZimjZj ddlkmlZl ddlmmnZnmoZompZpmqZq ddlrmsZs ddltmuZumvZv ddlwmxZx n# e}$ r  e~dej                   d      w xY wY Dw xY w# eef$ r* ej                  d ej                  d             dZY ?w xY w# ey$ r dZ=Y  w xY wc c} w # ey$ r dZBdZD G d> d?eE      ZCY #w xY w# ey$ r dZBdZD G dA d?eE      ZCY Dw xY w(  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get_process_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)ProviderConfigSchemaProviderFieldSTORAGE_HONCHO_HOST_BLOCKget_provider_config_schema)derive_gateway_busyderive_gateway_drainableget_running_pid_cached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	SecretStr)run_in_threadpool)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        j                  d|j                  |       |j                  | |       y)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)rJ   N)cron.scheduler_providerrN   _loginfonamestart)rI   rJ   rN   providers       H/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/web_server.py_start_desktop_cron_tickerrW      s5     ?%'HIIJHMM[cdNN:N1    c                  ,    	 dd l } y # t        $ r Y y w xY wNr   )hermes_cli.gateway	Exception)
hermes_clis    rV   _warm_gateway_moduler^      s    ! s    	c                  L    	 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)r[   r`   ImportErrorgateway.restartra   )r`   ra   s     rV   _resolve_restart_drain_timeoutrd      s+    5A)++ 5I445s    ##appr4   c                D  K   i | j                   _        t        j                         | j                   _        i | j                   _        t        j                         | j                   _        t        j                         j                  d t               d }d }t        j                  d      dk(  rBt        j                         }t        j                  t        |fdd      }|j!                          t        j"                  t%        t&                    }	 d  |j)                          t&        j+                          d {    ||j-                          y y 7 # |j)                          t&        j+                          d {  7   ||j-                          w w xY ww)NHERMES_DESKTOP1Tzdesktop-cron-tickertargetargsdaemonrS   )stateevent_channelsasyncioLock
event_lockpty_active_session_fileschat_argv_lockget_event_looprun_in_executorr^   osgetenv	threadingEventThreadrW   rT   create_task
run_reaperPTY_REGISTRYcancel	close_allset)re   	cron_stopcron_threadpty_reaper_tasks       rV   	_lifespanr      sA    !CII"<<>CII)+CII&
  '||~CII ,,T3GH
 +/I-1K	yy!"c)OO%	&&-&	
 	 ))*\*BCO $$&&& MMO ! 	' 	 $$&&& MMO !s6   DF E 'F  EF (FFFF c                 :   	 | j                   j                  | j                   j                  fS # t        $ rc 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.
    )rm   rn   rq   AttributeErrorro   rp   re   s    rV   _get_event_stater      sr    >yy'')=)=== >#%		 &||~		yy'')=)===>s   +. A)BB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.
    )rm   rs   r   ro   rp   r   s    rV   _get_chat_argv_lockr      sH    (yy''' (#*<<>		 yy'''(s    AAAc                     	 | 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.)rm   rr   r   r   s    rV   _get_pty_active_session_filesr     sB    2yy111 2-/		*yy1112s    0A
A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                 b   | j                   j                  t        d      }|r7t        j                  |j                         t        j                               ry| j                   j                  dd      }dt         }t        j                  |j                         |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       rV   _has_valid_session_tokenr   F  s     __(()=rBN$-- ??3D()Ht{{}hoo.?@@rX   z/api/files/download_QUERY_TOKEN_API_PATHSpathc                     |t         vry| j                  j                  dd      }t        |      xr6 t	        j
                  |j                         t        j                               S )NFtokenr   )r   query_paramsr   boolr   r   r   r   )r   r   r   s      rV   _has_valid_query_tokenr   `  sQ    ))  $$Wb1E;W4..u||~~?T?T?VWWrX   c                     t        | j                  j                  dd      r%t        | j                  dd      yt        dd      t	        |       st        dd      y)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)getattrre   rm   r7   r   )r   s    rV   _require_tokenr   g  sY    & w{{  /59 7==)T2>NCC#G,NCC -rX   >   	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     rV   should_require_authr     s    & ,,,rX   host_header
bound_hostc                 6   | sy| j                         }|j                  d      r.|j                  d      }|dk7  r|d| }n-|j                  d      }nd|v r|j                  dd      d   n|}|j	                         }|d	v ry
t
        j                  j                  dd      j                  d      D ch c]0  }|j                         r|j                         j	                         2 }}|j	                         }|t        v r|t        v xs ||v S ||k(  S c c}w )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HERMES_DASHBOARD_PUBLIC_HOSTSr   ,)
strip
startswithfindrsplitlowerrv   environr   splitr   )r   r   hclose	host_only_extrabound_lcs          rV   _is_accepted_hostr     s     	A||CsB;!E
II+.!8AHHS!$Q'	!I
 &&  ?DJJ3O779 	
	F  !H((11HY&5HH  s   45Dhttpc                    K   t        t        j                  dd      }|r7| j                  j	                  dd      }t        ||      st        dddi      S  ||        d{   S 7 w)	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   re   rm   r   r   r   r?   )r   	call_nextr   r   s       rV   host_header_middlewarer     sn      L$7Joo))&"5 j9@  7####s   A A)"A'#A)c                   K   | j                   j                  }|j                  d      rt        | j                  dd      xs< t        | j
                  j                  dd      xs t        |       xs t        | |      }|r|j                  d      }t        |      dk\  r|d   r	 dd	l
m}m}  |       } |       }t               }	t!        fd
|	D        d      }
|
r|
j#                  d      nd}|dk(  r|v s|vr't%        dddi      S |dk(  r|v rt%        dddi      S  ||        d{   S # t        $ r t               }t               }Y w xY w7 'w)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   L   K   | ]  }|j                  d       k(  s|  ywrS   Nr   .0pplugin_names     rV   	<genexpr>z+_plugin_api_runtime_gate.<locals>.<genexpr>+  s      LqquuV}/KL   $$Nsourceuser  r   Plugin not foundr   bundled)urlr   r   r   rm   re   r   r   r   lenhermes_cli.plugins_cmdr   r   r\   r   _get_dashboard_pluginsnextr   r?   )r   r   r   _authedpartsr   r   enabled_setdisabled_setpluginspluginr   r   s               @rV   _plugin_api_runtime_gater    s|      ;;D'
 GMM#8%@ 5w{{((/5A5'05 &gt4	 	 JJsOE5zQ#Ah	- '7&8'8': 56G!LGLF 6<VZZ1F'&,6+[:X#/,/)13E(F$   9,&,6#/,/)13E(F$  7###/ % -&)e'*u-. $s7   B%E)D7 ?A3E2E3E7EEEEc                 <   K   ddl m}  || |       d {   S 7 w)Nr   )gated_auth_middleware)$hermes_cli.dashboard_auth.middlewarer	  )r   r   r	  s      rV   _dashboard_auth_gater  G  s     J&w	::::   c                   K   t        | j                  dd      r ||        d{   S t        | j                  j                  dd      r ||        d{   S | j                  j                  }|j                  d      }|j                  d      r0|t        vr(|s&t        |       st        | |      st        ddd	i
      S  ||        d{   S 7 7 ~7 w)zERequire the session token on all /api/ routes except the public list.r   FNr   /api/mcp/oauth/callback/z/api/r   r   r   r   )
r   rm   re   r   r   r   _PUBLIC_API_PATHSr   r   r?   )r   r   r   is_mcp_oauth_callbacks       rV   auth_middlewarer  M  s      w}}3U;w''' w{{  /59w''';;D OO,FGwD0A$AJ_'09OPWY]9^!>2  7### (
 ( $s4   #CC0CCA7CCCCCc                 <   K   ddl m}  || |       d{   S 7 w)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      rV   _token_auth_seamr  e  s      K&w	::::r  c                      dg} 	 ddl m} | j                   |              t	        t
        j                  |             S # t        $ r | j                  dg       Y ;w xY w)u  Discovered memory providers for the ``memory.provider`` select.

    Directory-scan only (no provider imports), so it's safe at module import
    time. ``""`` (built-in only) is always first; discovery failures degrade to
    the bundled defaults rather than dropping the field. The literal
    ``builtin`` alias is deliberately NOT offered — built-in memory is not a
    provider plugin, and ``_normalize_memory_provider_name`` already maps any
    legacy ``builtin``/``built-in``/``none`` value back to ``""`` (#49513).
    r   r   )list_memory_provider_nameshoncho)plugins.memoryr  extendr\   listdictfromkeys)optionsr  s     rV   _memory_provider_optionsr  x  sX     dG#=134 g&''  #z"#s   ? AAzmemory.providerselectzMemory provider plugin)typedescriptionr  modelstringz0Default model (e.g. anthropic/claude-sonnet-4.6)generalr!  r"  categorymodel_context_lengthnumberz=Context window override (0 = auto-detect from model metadata)zterminal.backendzTerminal execution backend)localdockersshmodaldaytonasingularityzterminal.modal_modezModal sandbox modesandboxfunctionztts.providerzText-to-speech provider)
edge
elevenlabsopenaixaiminimaxmistralgeminineutts	kittenttspiperzstt.providerzSpeech-to-text provider)r*  groqr4  r5  r3  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emberrA  	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approvals.modezDangerous command approval mode)manualsmartrI  zcontext.enginezContext management enginer?  customzhuman_delay.modezSimulated typing delay mode)rI  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   rG  lowmediumhighxhighmaxultrazWhen 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"  booleanzRun the local browser in headed mode (visible window). Also keeps the window open between turns; idle sessions are still reaped after browser.inactivity_timeout.)zdelegation.reasoning_effortz%updates.non_interactive_local_changeszupdates.refresh_cua_driverzbrowser.headed_SCHEMA_OVERRIDESprivacysecuritycontextagentskillscronnetworkcheckpoints	approvalshuman_delaydisplay	dashboardcode_executionprompt_cachinggoalsupdates
onboardingtelegramdiscordmcpcomputer_use_CATEGORY_MERGE)r%  re  terminalrl  
delegationmemorycompressionrc  browservoicettssttloggingrt  	auxiliaryvaluec                     t        | t              ryt        | t              ryt        | t              ryt        | t              ryt        | t
              ryy)z*Infer a UI field type from a Python value.r`  r)  r  objectr$  )
isinstancer   intfloatr  r  )r  s    rV   _infer_typer  ,  sF    %%%%%rX   r   configprefixc                    i }| j                         D ]  \  }}|r| d| n|}|dv r|r|j                  d      d   }nt        |t              r|}nd}t        |t              r|j	                  t        ||             nt        |      |j                  dd      j                  dd      j                         |d}|t        v r|j	                  t        |          t        j                  |d	   |d	         |d	<   |||<    |S )
uF   Walk DEFAULT_CONFIG and produce a flat dot-path → field schema dict..>   _config_versionr   r%  u    → _ r&  r'  )itemsr   r  r  update_build_schema_from_configr  replacer   ra  rw  r   )r  r  schemakeyr  full_keyr'  entrys           rV   r  r  ;  s   
 )+Flln %
U(.fXQse$C ** ||C(+Ht$H HeT"MM3E8DE $E*'//W=EEc3OUUW$%E ,,.x89 / 3 3E*4EuZGX YE*$F8=%> MrX   _ordered_schemac                     t        | t              syt        | j                  d      xs d      j	                         j                         }|r|dk7  ry| j                  d      }t        |t              xr t        |j	                               S )aW  Return True when *value* declares a command-type voice provider.

    Mirrors the runtime discriminators
    (``tools.tts_tool._is_command_provider_config`` /
    ``tools.transcription_tools._is_command_stt_provider_config``) and the
    desktop's ``isCommandProvider`` in
    ``apps/desktop/src/app/settings/helpers.ts``: ``type`` is OPTIONAL and
    case/space-insensitive (absent or normalizing to ``"command"``), and
    ``command`` MUST be a non-empty string. Built-in blocks (which carry
    ``voice``/``model`` and no ``command``) and the ``providers`` container
    itself are rejected.
    Fr!  r   command)r  r  strr   r   r   r   )r  ptyper  s      rV   _is_command_provider_blockr  q  ss     eT"		&!'R(..0668E)#ii	"Ggs#=W]]_(==rX   kindbuiltin_namescfgc                    |D cg c]  }t        |       c}D ch c]   }|j                         j                         " c}| dk(  rddlm} nddlm} dt        ddffd}|j                  |       }t        |t              si }g }|j                  d	      }t        |t              r|j                  |       |j                  |j                         D 	
ci c]  \  }	}
|	d	k7  s|	|
 c}
}	       |D ]`  }|j                         D ]K  \  }}t        |t               s|j                         j                         |vs8t        |      sD ||       M b 	 | dk(  rdd
lm} ndd
lm}  |       D ]  } |t#        |dd              	  |t'        || d             S c c}w c c}w c c}
}	w # t$        $ r Y 0w xY w)u  Return a merged provider option list without hard-coding vendor names.

    *kind* is ``"tts"`` or ``"stt"``. The result keeps the built-in display
    names first (original order — NOT re-sorted), then appends:

    1. Command-type providers declared under the canonical
       ``<kind>.providers.<name>`` location, plus the legacy top-level
       ``<kind>.<name>`` fallback — exactly the dual resolution the runtime
       performs in ``_get_named_provider_config`` /
       ``_get_named_stt_provider_config``. Names colliding with a RUNTIME
       built-in are excluded case-insensitively (the runtime rejects a
       built-in name as a command provider before any config lookup), so a
       ``providers.EDGE`` command block is not offered.
    2. Plugin-registered provider names from ``agent.tts_registry`` /
       ``agent.transcription_registry`` — opportunistic only: plugins
       register at runtime via ``ctx.register_tts_provider()``, and this
       process does not necessarily call ``discover_plugins()``, so the
       registry may legitimately be empty here. (There is no static
       ``provides: [tts]`` manifest convention to scan — real manifests only
       carry ``provides_tools``/``provides_hooks``.)
    3. The current ``<kind>.provider`` value when not already present — a
       custom name that only appears as the active provider stays
       selectable (matches desktop ``enumOptionsFor``'s current-value
       preservation).

    Guard semantics deliberately mirror
    ``apps/desktop/src/app/settings/helpers.ts:commandProviderNames`` so the
    backend schema (web dashboard) and the desktop client agree on which
    names are offered.
    r~  r   )BUILTIN_TTS_PROVIDERS)BUILTIN_STT_PROVIDERSrS   rK   Nc                     t        | t              sy | j                         }|j                         }|r(|vr#j	                  |       j                  |       y y y N)r  r  r   r   appendadd)rS   strippedr  namesseens      rV   _addz&_custom_provider_options.<locals>._add  sN    $$::<nn4LL"HHSM (8rX   	providerslist_providersrU   )r  r   r   tools.tts_toolr  tools.transcription_toolsr  r   r   r  r  r  r  r  agent.tts_registryr  agent.transcription_registryr   r\   r   )r  r  r  n_runtime_builtinsr  sectioncandidate_blocksproviders_mapkvblockrS   r  _list_voice_providers_pr  r  s                   @@rV   _custom_provider_optionsr    s   F ++SV+E',-!AGGIOO-D u}MX3 4  ggdmGgt$ #%KK,M-&.!--/>$!QQ+-=A> "  ;;= 	KD%4%JJL&&(0AA.u5T
	5=R\') 	,BVT*+	, 	dJ	'(Lw ,-> 	?*  s(   F&%F+F0
,F0
1F6 6	GGc                     	 t               } i }dD ]y  }| d}t        j                  |      }t	        |t
              rt	        |j                  d      t              sMt        |t        |d         |       }||d   k7  soi |d|i||<   { |st        S t        t              }|j                  |       |S # t        $ r	 t        cY S w xY w)u  Return CONFIG_SCHEMA with per-request voice provider options merged.

    Computed at request time (not import time) so options reflect the
    CURRENT config.yaml — including providers added after the server
    started, and the profile-scoped config when the request carries a
    ``profile`` param. The module-level ``CONFIG_SCHEMA`` is never mutated;
    entries that change are shallow-copied onto a copied mapping.
    )r~  r  z	.providerr  )	r   r\   CONFIG_SCHEMAr   r  r  r  r  r  )r  overlayr  r  r  mergedfieldss          rV   #_schema_with_voice_provider_optionsr    s    m *,G 8i !!#&%&j99Mt.T)$U95E0FLU9%%7e7Y7GCL8 - F
MM'M  s   
B6 6CCc                   ,    e Zd ZU eed<   dZee   ed<   y)ConfigUpdater  Nprofile)__name__
__module____qualname__r  __annotations__r  r   r   rX   rV   r  r    s    L!GXc]!rX   r  c                   D    e Zd ZU eed<   eed<   dZee   ed<   dZeed<   y)EnvVarUpdater  r  Nr  r   api_key)r  r  r  r  r  r  r   r  r  rX   rV   r  r  
  s&    	HJ!GXc]! GSrX   r  c                   ,    e Zd ZU eed<   dZee   ed<   y)EnvVarDeleter  Nr  r  r  r  r  r  r  r   r  rX   rV   r  r        	H!GXc]!rX   r  c                   ,    e Zd ZU eed<   dZee   ed<   y)EnvVarRevealr  Nr  r  r  rX   rV   r  r    r  rX   r  c                   &    e Zd ZU i Zeeef   ed<   y)MemoryProviderConfigUpdatevaluesNr  r  r  r  r   r  r   r  r  rX   rV   r  r         FDcNrX   r  c                   &    e Zd ZU i Zeeef   ed<   y)MemoryProviderSetupRequestr  Nr  r  rX   rV   r  r  $  r  rX   r  c                   ~    e Zd ZU dZeed<   eed<   eed<   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<   y)CustomEndpointUpdater   idrS   base_urlr#  Nr  context_lengthTdiscover_modelsFmake_default)r  r  r  r  r  r  r  r   r  r  r  r   r  r  rX   rV   r  r  (  sJ    BL
IMJ!GXc]!$(NHSM( OT L$rX   r  c                   b    e Zd ZU dZee   ed<   i Zee	e	f   ed<   g Z
ee	   ed<   dZee	   ed<   y)MessagingPlatformUpdateNenabledenv	clear_envr  )r  r  r  r  r   r   r  r  r   r  r  r   r  r  rX   rV   r  r  3  s@    "GXd^"Cc3hItCy "GXc]!rX   r  c                   "    e Zd ZU dZee   ed<   y)TelegramOnboardingStartNbot_name)r  r  r  r  r   r  r  r  rX   rV   r  r  <  s    "Hhsm"rX   r  c                   2    e Zd ZU ee   ed<   dZee   ed<   y)TelegramOnboardingApplyallowed_user_idsNr  r  r  r  r   r  r  r  r   r  rX   rV   r  r  @  s    3i!GXc]!rX   r  c                   J    e Zd ZU dZee   ed<   dZee   ed<   dZee   ed<   y)WhatsAppOnboardingStartbotmoder   allowed_usersNr  	r  r  r  r  r   r  r  r  r  r  rX   rV   r  r  E  s,    D(3-#%M8C=%!GXc]!rX   r  c                   J    e Zd ZU dZee   ed<   dZee   ed<   dZee   ed<   y)WhatsAppOnboardingApplyNr  r  r  r  r  rX   rV   r  r  K  s,    D(3-#'M8C='!GXc]!rX   r  c                   ,    e Zd ZU eed<   dZee   ed<   y)AudioTranscriptionRequestdata_urlN	mime_type)r  r  r  r  r  r  r   r  rX   rV   r  r  Q  s    M#Ix}#rX   r  c                   0    e Zd ZU eed<   eed<   dZeed<   y)ManagedFileUploadr   r  T	overwriteN)r  r  r  r  r  r  r   r  rX   rV   r  r  V  s    
IMItrX   r  c                   ,    e Zd ZU eed<   dZee   ed<   y)ChatImageUploadr  Nfilename)r  r  r  r  r  r   r   r  rX   rV   r  r  \  s    M"Hhsm"rX   r  c                       e Zd ZU eed<   y)ManagedDirectoryCreater   Nr  r  r  r  r  r  rX   rV   r  r  a      
IrX   r  c                   &    e Zd ZU eed<   dZeed<   y)ManagedFileDeleter   F	recursiveN)r  r  r  r  r  r  r   r  rX   rV   r  r  e  s    
IItrX   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                     | xs dj                  dd      d   j                         j                         }t        j	                  |d      S )Nr   ;r   r   r  )r   r   r   r  r   )r  
normalizeds     rV   _audio_extension_for_mimer  |  sB    /r((a0399;AACJ!%%j'::rX   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<   y)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"
    scoperU   r#  r   taskr  r  Fconfirm_expensive_modelNr  )r  r  r  __doc__r  r  r  r  r  r  r   r  r   r  rX   rV   r  r    sR     JMJD#N Hc GS$)T)!GXc]!rX   r  c                   >    e Zd ZU dZeed<   dZeed<   dZee   ed<   y)MoaModelSlotr   rU   r#  Nreasoning_effort)	r  r  r  rU   r  r  r#  r!  r   r  rX   rV   r   r     s&    HcE3O '+hsm*rX   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   ed<   dZe	e   ed	<   d
Zeed<   y)MoaPresetPayloadreference_models
aggregatorNreference_temperatureaggregator_temperature   
max_tokensreference_max_tokensfanoutTr  )r  r  r  r$  r  r   r  r%  r&  r   r  r'  r)  r  r*  r+  r  r  r   r  rX   rV   r#  r#    sq    +-d<(-+~J- .28E?1.2HUO2J +/(3-. FHSM GTrX   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   ed<   dZee   ed<   dZeed<   dZee   ed<   y)MoaConfigPayloadr?  default_presetr   active_presetpresetsr$  r%  Nr&  r'  r(  r)  r*  r+  Tr  r  )r  r  r  r.  r  r  r/  r0  r  r#  r$  r  r   r%  r&  r   r  r'  r)  r  r*  r+  r  r   r  r  rX   rV   r-  r-    s    #NC#M3+-GT#''(- ,.d<(-+~J--18E?1.2HUO2J*.(3-. FHSM GT!GXc]!rX   r-  rU   c                    ddl m} ddlm}m} ddlm} ddlm}m	} | xs dj                         }|xs dj                         }	 ||      }
	 t               }t        |t              r|j                  d      nd} ||t        |t              r|ni       } ||t        |t              r ||      ng       }||j                   |	fS ||j                   |	fS |
|vrd	|	v r	 |j                  d
i       }t        |t              r;t#        |j                  dd      xs d      j                         j%                         nd}ddlm} |r ||      |v r ||      }
|}nd}
d}|
|v r#|
j)                  d      s	  ||	|
      }|r|}	||	fS ||	fS # t        $ r i }Y Aw xY w# t        $ r d}Y kw xY w# t        $ r t*        j-                  d||	d       Y ||	fS w xY w)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   )get_compatible_custom_providers)_KNOWN_PROVIDER_NAMESnormalize_provider)normalize_model_for_provider)resolve_custom_providerresolve_user_providerr   r  Nr   r#  rU   )_AGGREGATOR_PROVIDERS
openrouterrO  z$model normalization failed for %s/%sTexc_info)hermes_cli.configr2  hermes_cli.modelsr3  r4  hermes_cli.model_normalizer5  hermes_cli.providersr6  r7  r   r   r\   r  r  r   r  r  r   r8  r   rQ   debug)rU   r#  r2  r3  r4  r5  r6  r7  prov_inmodel_in	canonicalr  user_providersuser_providercustom_providercur_cfgcur_providerr8  normalized_models                      rV    _normalize_main_model_assignmentrJ    s   6 BKGS~2$$&G""$H"7+Im .8T-BSWW[)N):nd#CM .0:30E',2O  ))"!!8++--#/	gggr*G gt, GKK
B/526<<>DDF24  	<.|<@UU*<8I"G$I"G )))2F2Fx2P	a;HiP+ H7H[  2  	L	"  	aJJ=w[_J`H	as7   
F  AF 6F. FFF+*F+."GG	model_cfgr   r  r  c                    t        | t              si } t        | j                  d      xs d      j	                         j                         }|j	                         j                         }|| d<   || d<   |j	                         r|j	                         | d<   n| j                  d      r
||k7  rd| d<   |j	                         r&|j	                         | d<   | j                  dd       n4| j                  d      s| j                  d      r||k7  rt        | d	       ||k7  rt        | d
       | j                  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.
    rU   r   r?  r  r  apiNF)clear_api_mode)clear_api_keyr  )r  r  r  r   r   r   popr   )rK  rU   r#  r  r  prev_providernew_providers          rV   _apply_main_model_assignmentrS     s   8 i&		j17R8>>@FFHM>>#))+L$Ij Ii~~ ( 0	*	z	"|}'D !#	*
 }}&}}	)eT"
--	
"immE&:P]@] 	)5I}$(%HMM"D)rX   GATEWAY_HEALTH_URLGATEWAY_HEALTH_TIMEOUT3u>   Invalid GATEWAY_HEALTH_TIMEOUT value %r — using default 3.0sg      @g      ?c                  6   t         syt         j                  d      } | j                  d      r| dt        d        } n | j                  d      r| dt        d        } |  d|  dfD ]  }	 t        j
                  j                  |d      }t        j
                  j                  |t              5 }|j                  d	k(  r2t        j                  |j                               }d
|fcddd       c S 	 ddd        y# 1 sw Y   xY w# t        $ r Y w xY w)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   r8   urlopen_GATEWAY_HEALTH_TIMEOUTstatusjsonloadsreadr\   )baser   reqrespbodys        rV   _probe_gateway_healthrk  q  s   &  %%c*D}}'(.s-../	y	!%s9~o&6)*tfG,<= 	..((e(<C''5L'M &QU;;#%::diik2D:& &%& & &  		s1   ,AD26D (	D5D D		D	DDc                  
   ddl m} m} | j                         sy |d      }	 |j	                  dd      }t        j
                         t        fd|D              |j                          S # |j                          w xY w)a  Return the dashboard status active-session count.

    This is best-effort status garnish, not a critical path.  Use a read-only
    connection so /api/status never tries to initialise or migrate state.db
    while another Hermes process is writing to it.
    r   )DEFAULT_DB_PATH	SessionDBT)	read_only2   )limitcompact_rowsc           	   3      K   | ]?  }|j                  d       ,|j                  d|j                  dd            z
  dk  rd A yw)ended_atNlast_active
started_atr   ,  r   r   )r   snows     rV   r   z0_count_status_active_sessions.<locals>.<genexpr>  sK      
uuZ (quu]AEE,,BCCsJ 
s   AA)hermes_staterm  rn  existslist_sessions_richtimesumr   )rm  rn  dbsessionsry  s       @rV   _count_status_active_sessionsr    sr     8
 !!#	T	"B	((r(Eiik 

 
 	

s   :A0 0Bc                  Z  K   t        j                         } 	 t        j                  | j                  d t              t
               d {   S 7 # t         j                  $ r t        j                  dt
               Y yt        $ r }t        j                  d|       Y d }~yd }~ww xY ww)NrZ  z</api/status active session count exceeded %.2fs; returning 0z0/api/status active session count unavailable: %sr   )
ro   get_running_loopwait_forru   r  _STATUS_ACTIVE_SESSIONS_TIMEOUTTimeoutErrorrQ   r@  r\   )loopexcs     rV   _status_active_sessionsr    s     ##%DL%%  'DE3
 
 	
 
  


J+	
   L

EsKKLsL   B+2A 
AA B+A -B(>B+ B(B#B+#B((B+	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                   2    e Zd ZU eed<   edz  ed<   eed<   y)ManagedFilesPolicydefault_pathNlocked_rootcan_change_path)r  r  r  r   r  r   r  rX   rV   r  r    s    rX   r  >   .hg.svn.next.venv.cache.turbodistvenvbuildrj   __pycache__node_modules.git>   	auth.json	auth.lockbws_cache.json.git-credentialsgoogle_oauth.jsongoogle_token.json.anthropic_oauth.jsongoogle_oauth_pending.jsonwebhook_subscriptions.jsoncredentialsconfig.yamlz
mcp-tokenspairingrS   c                 j    | j                         }|dk(  s|j                  d      s|dk(  ry|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)rS   lowereds     rV   _is_sensitive_filenamer    s:     jjlG&G..w77h;N777rX   c                 f    t        | j                        ry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   H   K   | ]  }|j                         t        v   y wr  )r   _SENSITIVE_MANAGED_DIR_NAMESr   parts     rV   r   z%_is_sensitive_path.<locals>.<genexpr>A  s     Stzz|;;S    ")r  rS   anyr  r   s    rV   _is_sensitive_pathr  .  s(    " dii(S

SSSrX   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.jsonrd  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                 z   t        | xs d      j                         }|st        dd      d|v rt        dd      	 |j                         j	                  d      rht
        j                  j                  |      }|j                  r|j                  dvrt        t
        j                  j                  |j                        }t        |      j                         }|j                         st        j                          |z  }|j#                  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   r7   r   r   r`  parseurlparsenetloc
ValueErrorr   url2pathnamer   r   
expanduseris_absolutecwdresolveOSErrorRuntimeError)r  rawparsed	candidates       rV   _fs_pathr    s   
hn"

#
#
%C4FGGs{NCCD99;!!'*\\**3/F}}6G!G  ..--fkk:CI((*	$$&
Y.I   ..\:. DNCCDs   CD "D:c                     | j                   j                         }|t        v r	t        |   S t        j                  t        |             \  }}|xs dS )Napplication/octet-stream)suffixr   _FS_MIME_TYPES	mimetypes
guess_typer  )r   r  guessedr  s       rV   _fs_mime_typer     sK    [[ Ff%%%%c$i0JGQ000rX   datac                 X    | syd| v ryt        d | D              }|t        |       z  dkD  S )NF    Tc              3   6   K   | ]  }|d k  s	|dvsd  yw)r   >   	   
      r   Nr  )r   bytes     rV   r   z#_fs_looks_binary.<locals>.<genexpr>  s     Q44"9[9PQQs   
gQ?)r~  r   )r  
suspiciouss     rV   _fs_looks_binaryr
    s6    }Q4QQJD	!D((rX   c                    t        t        |             }	 |j                         }t        j                  |j                        rt	        dd      t        j                  |j                        st	        dd	      ||fS # t        $ r t	        dd      t
        $ r t	        dd      t        $ r t	        dd      t        $ r}t	        dt        |      xs d      d }~ww xY w)
Nr   File not foundr     File is not readabler   r  Path points to a directoryzOnly regular files can be read)r  r  statFileNotFoundErrorr7   NotADirectoryErrorPermissionErrorr  S_ISDIRst_modeS_ISREG)r   rj   str  s       rV   _fs_regular_filer    s    c$i F	P[[] ||BJJ4PQQ<<

#4TUU2:  F4DEE F4DEE L4JKK PCH4NOOPs   B AC)
C$$C)rT   c                     | }t        d      D ]9  }	 |dz  j                         rt        |      c S 	 |j                  }||k(  r y |}; y # t        $ r Y  y w xY w)Nrp  r  )ranger{  r  r  parent)rT   	directoryr  r  s       rV   _fs_find_git_rootr    su    I2Y 		F"**,9~% - !!Y		   		s   A	AAc                     t               j                  d      xs i } t        | j                  d      xs# t        j                  j                  d      xs d      j                         }|rJ|dvrF	 t        |      j                         j                  d      }|j                         rt        |      S 	 t        t        j                               S # t        t        f$ r Y .w xY w)Nrx  r  TERMINAL_CWDr   >   r  rV  r  Fr  )r   r   r  rv   r   r   r   r  r  is_dirr  r  r  )cfg_terminalr  r  s      rV   _fs_default_cwdr"    s    =$$Z06BL
lu%M)GM2
N
T
T
VC
s..	S	,,.66e6DI!9~% " txxz? & 		s   2AC C&%C&r  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                  j                         S dS # t        $ r Y yw xY w)NT   F)capture_outputr  r[  checkwin32creationflagsgit-Cbranchz--show-currentr   r   )	sysplatformr
   
subprocessrun
returncodestdoutr   r\   )r  
run_kwargsresults      rV   _fs_git_branchr4    s    "	&

 <<7"*<*>J'D#x)9:

 )/(9(9Q(>v}}""$FBF s   A*A/ -A/ /	A;:A;c                      t               } | dz  | dz  | dz  g}g }|D ]"  }	 |j                  |j                                $ |S # t        t        f$ r Y 8w xY w)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   r  r  r  r  )homerootsoutroots       rV   _media_serve_rootsr=    sp     DH_d]2D7NCEC 	JJt||~&
 J & 		s   AAAz
/api/mediac                   K   	 t        |       j                         j                         j                  j                         t        vrt        dd      t               }t        fd|D              st        dd      j                         st        d	d
      j                         j                  t        kD  rt        dd      t        j                   j#                               j%                  d      }ddt        j                  j                             d| iS # t        t        f$ r t        dd      w xY ww)u  Return a gateway-local image file as a base64 data URL.

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

    Auth-gated by the session token like every other /api route. Restricted to
    an image-extension allowlist, a size cap, AND the gateway's own media roots
    (resolved, symlink-safe) so it can't be used to read arbitrary files.
    r   r  r   i  zUnsupported media typec              3   H   K   | ]  }|k(  xs |j                   v   y wr  parents)r   r<  rj   s     rV   r   zget_media.<locals>.<genexpr>  s&     JDv~7!77Js   "r  zPath outside media rootsr   r    File too largeasciir  data:;base64,)r   r  r  r  r  r7   r  r   _MEDIA_CONTENT_TYPESr=  r  is_filer  st_size_MEDIA_MAX_BYTESbase64	b64encode
read_bytesdecode)r   r:  encodedrj   s      @rV   	get_mediarP    s#    Dd&&(002 }}$884LMM EJEJJ4NOO>>4DEE{{}//4DEEv0023::7CG% 4V]]5H5H5J KLHU\T]^__! \" DNCCDs   E
'D* C>E
*EE
require_existsrR  c                    	 | j                         j                  |      S # t        $ r |rt        dd       t        t
        f$ r t        dd      w xY w)Nr  r   Path not foundr   r   r  )r  r  r  r7   r  r  )r   rR  s     rV   _canonical_pathrU    sb    D (((?? C8HII\" DNCCDs	   " 5Ac                    t        |       j                         }	 |j                  dd       |j                         }|j                         st        dd      |S # t        t
        f$ r}t        dd|       d }~ww xY w)NTrA  exist_ok  z#Managed files root is unavailable: r   z%Managed files root is not a directory)r   r  mkdirr  r  r  r7   r   )r  r<  resolvedr  s       rV   _ensure_managed_rootr\  *  s    >$$&Da

4$
/<<> ??4[\\O	 \" a6YZ]Y^4_``as   #A B,A<<Br<  rj   c                 ,    || k(  xs | |j                   v S r  r@  )r<  rj   s     rV   _path_is_underr^  6  s    T>3TV^^33rX   c                 b    t        | xs d      j                         }d|v rt        dd      |S )Nr   r  r   r  r   )r  r   r7   )r  r  s     rV   
_path_textr`  :  s2    x~2$$&D~NCCKrX   c                    t        | j                  j                  dd      ry| j                  j                  xs dj                         }| j                  r| j                  j                  ndj                         }h d}||v xs ||v S )Nr   Fr   >   r   r   
testclient
testserverr   r   )r   re   rm   r   hostnamer   clientr   )r   r   client_hostlocal_hostss       rV   _local_dashboard_requestrh  A  sr    w{{  /59KK  &B--/D*1..7>>&&bGGIKSK;<+"<<rX   c                  Z   t         j                  j                  dd      j                         } | sy	 ddlm}  |       j                         j                  d      }|t        k(  S # t        t        f$ r4 t        |       j                         j                  d      }Y |t        k(  S w xY w)NHERMES_HOMEr   Fr   )get_default_hermes_rootr  )rv   r   r   r   hermes_constantsrk  r  r  r  r  r   _HOSTED_MANAGED_FILES_ROOT)r  rk  r<  s      rV    _default_hermes_root_is_opt_datarn  J  s    
**..
+
1
1
3C<<&(335==U=K --- \" <Cy##%--U-;---<s   +A' '8B*)B*c                      t               ry	 ddlm}   |        sy	 	 t	        t
              }|dk(  ry	 y# t        $ r Y yw xY w# t        $ r Y yw xY w)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)  )rn  rl  rp  r\   r"   PROJECT_ROOT)rp  rY  s     rV   *_dashboard_local_update_managed_externallyrr  W  so     ()1~ &|4U?      s   4 A 	A A 	AA)create_rootrs  c                |   t         j                  j                  t        d      j	                         }|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)rv   r   r   _MANAGED_FILES_ROOT_ENVr   r\  rU  r   r  rn  rm  r9  )r   rs  raw_forced_rootr<  r9  s        rV   _managed_files_policyrw  }  s    jjnn%<bAGGIO8C#O4Y]^mYnIo!tW\]] ()CN#$>?Tn!tW\]]499;'D4TSWXXrX   	for_writery  c                z   t        |      }t        |       }|j                  }|	|r|dv r|}n|s|j                  }nwt	        |      j                         }|?|j                         s/t        d |j                  D              rt        dd      ||z  }n|j                         st        dd      d|j                  v rt        dd      |r5|j                         s%t        |j                        }||j                  z  }nt        ||       }|t        ||      st        d	d
      ||t        |      fS )N>   r  r   c              3   &   K   | ]	  }|d k(    yw)..Nr  r  s     rV   r   z(_resolve_managed_path.<locals>.<genexpr>  s     <D44<<s   r   zPath cannot contain '..'r   zPath must be absoluter|  rQ  r  Path outside managed files root)rw  r`  r  r  r   r  r  r  r  r7   r{  rU  r  rS   r^  r  )	r  r   ry  policyr  r<  r  r  r[  s	            rV   _resolve_managed_pathr    s.    #7+FhDD);	''	J))+	I$9$9$;<IOO<<#<VWWy(I&&(C8OPPy4NOO))+ !1!12INN*"9]KtX >4UVV8S]**rX   r~  c                 h    | j                   t        | j                         nd }||| j                  dS )N)r<  r  r  )r  r  r  )r~  r  s     rV   _managed_response_metar    s9    -3-?-?-K#f(()QUK"!11 rX   c                 8   	 |j                         }| j                  #t        | j                  |      st        dd      	 |j                         }|j                         }|rd n%t        j                  |j                        d   xs d	}|j                  xs |j                  xs t        |      t        |      ||rd n|j                  |j                  |d
S # t        t        f$ r t        dd      w xY w# t        $ r}t        dd|       d }~ww xY w)Nr   r  r   r  r}  rY  zCould not stat path: r   r  )rS   r   is_directorysizemtimer  )r  r  r  r7   r  r^  r  r   r  r  rS   r  rI  st_mtime)r~  rj   r[  r  r  r   r  s          rV   _managed_file_entryr    s   D>># %nV=O=OQY.Z4UVVS]]_ __FY%9%9(--%H%K%iOiI=x}}=HHBJJ  \" DNCCD  S6KC54QRRSs#   C C; C8;	DDDr  c                    | xs dj                         }|j                  d      rd|vrt        dd      |j                  dd      \  }}|dd  j                  d	d      d
   xs d}d|vrt        dd      	 t	        j
                  |d      }t        |      t        kD  rt        dd      ||fS # t        j                  t        f$ r t        dd      w xY w)Nr   rE  r   r   z!Upload payload must be a data URLr   r   r   r  r   r  ;base64z%Upload payload must be base64 encodedTvalidatez"Upload payload is not valid base64rB  File is too large)r   r   r7   r   rK  	b64decodebinasciiErrorr  r   _MANAGED_FILE_MAX_BYTES)r  r  headerrO  r  r  s         rV   _decode_data_urlr    s    N!!#D??7#s$4WXXjja(OFGqr
  a(+I/II4[\\Z$7 4y**4GHH?	 NNJ' Z4XYYZs   :B4 4'C>   r  r  r  r  r  r  ))s   PNG

r  )s   r  )s   GIF87ar  )s   GIF89ar  )s   BMr  ._CHAT_IMAGE_MAGICr   c                     t        t        | xs d      j                               j                  }t	        j
                  dd|      }|j                         j                  d      }|xs dS )Nr   z[\x00-\x1f]+r  r  pasted-image)r   r  r   rS   resub)r   r  s     rV   _sanitize_chat_image_filenamer    sX    SR(..0166IY7I!'',I&&rX   c                     | d d }|j                  d      r	|dd dk(  ryt        D ]  \  }}|j                  |      s|c S  y )N   s   RIFF      s   WEBPr  )r   r  )r  headsigexts       rV   _chat_image_extensionr    sR    9DwD2J'$9% S??3J rX   payloadc                 ,   t        | j                        \  }}|j                         j                  d      st	        dd      t        |      t        kD  rt        dz  }t	        dd| d      t        |      }|t        vrt	        dd	      |||fS )
Nzimage/r   zUpload payload must be an imager      rB  zImage is too large; cap is z MBzUnsupported image type)	r  r  r   r   r7   r   _CHAT_IMAGE_UPLOAD_MAX_BYTESr  _CHAT_IMAGE_ALLOWED_EXTENSIONS)r  r  r  mbr  s        rV   _decode_chat_image_uploadr    s    &w'7'78OD)??''14UVV
4y//)k:6QRTQUUX4YZZ

%C
004LMMCrX   z/api/chat/image-uploadr  c           	      F  K   t        |       \  }}}t        |      5 }|xs
 t               }t        |      dz  }	 |j	                  dd       t        t        | j                              j                  xs d
}	t        j                  dd|	      j                  d      xs d
}	t        j                         j!                  d      }
|d|
 dt#        j$                  d       d|	 | z  }	 |j'                  |       	 d	d	d	       dt)              |j*                  t-        |      |dS # t
        $ r t        dd      t        $ r}t        dd|       d	}~ww xY w# t
        $ r t        dd      t        $ r}t        dd|       d	}~ww xY w# 1 sw Y   xY ww)u  Persist a browser-provided chat image where the embedded TUI can read it.

    The dashboard /chat page runs Hermes inside an xterm.js PTY. Browser
    clipboard image bytes are not visible to the server-side clipboard, so the
    page uploads them here, then drives the TUI's ``/image <path>`` command
    with the returned gateway-visible path. Files land under
    ``HERMES_HOME/images/`` — the same directory ``clipboard.paste`` /
    ``image.attach`` already use.
    r6  TrW  r  zImage directory is not writabler   rY  z"Could not create image directory: Nr  z[^A-Za-z0-9_.-]+r  z._-z%Y%m%d_%H%M%S
dashboard_r   zCould not write image: )okr   rS   bytesr  )r  _profile_scoper   r   rZ  r  r7   r  r  r   stemr  r  r   r   ry  strftimesecrets	token_hexwrite_bytesr  rS   r   )r  r  r  r  r  scoped_homer9  img_dirr  r  tsrj   s               rV   upload_chat_imager    s     5W=D)S		  YK/o/t*x'	dMM$M6 1'2B2BCDII[^vv)35;;EBTn\\^$$_5Zt1W->->q-A,B!D6#OO	Yt$!Y. FT #  	[C8YZZ 	dC:\]`\a8bcc	d  	[C8YZZ 	YC:QRUQV8WXX	Y'Y Ys^   F!FD)BF+E=,F!)EEEFF=FFFFF!z
/api/filesc                 4  K   t        ||       \  }}}|j                         st        dd      |j                         st        dd      	 |j	                         D cg c]  }t        |      st        ||       }}|j                  d
        |j                  }d }	|j                  |k7  r|||k7  rt        |j                        }	||	|dt        |      S c c}w # t        $ r t        dd      t        $ r}t        dd	|       d }~ww xY ww)Nr   rT  r   r   zPath is not a directoryr  zDirectory is not readablerY  zCould not read directory: c                 F    | d    t        | d         j                         fS )Nr  rS   r  r   items    rV   <lambda>z$list_managed_files.<locals>.<lambda>Q  s%    tN';#;Sf=N=T=T=V"W rX   r  )r   r  entries)r  r{  r7   r   iterdirr  r  r  r  sortr  r  r  r  )
r   r   r~  rj   display_pathchildr  r  r  r  s
             rV   list_managed_filesr  >  s1    #8w#G FFL==?4DEE==?4MNN	X  )
%e,  .
 
 LLWLX$$KF}}K$76[;PV]]# !
(	 

  Q4OPP X6PQTPU4VWWXs=   ADC"  C>C"  ADC" "D DDDz/api/files/readc                   K   t        ||       \  }}}|j                         st        dd      |j                         st        dd      t	        |      rt        dd      	 |j                         j                  }|t        kD  rt        d
d      t        j                  |j                        d   xs d}	 t        j                  |j                               j                  d      }|j                  |||d| d| dt#        |      S # t        $ r}t        dd	|       d }~ww xY w# t         $ r t        dd      t        $ r}t        dd|       d }~ww xY ww)Nr   r  r   r   Path is not a filer  (Access to sensitive files is not allowedrY  Could not stat file: rB  r  r   r  rD  r  zCould not read file: rE  rF  )rS   r   r  r  r  )r  r{  r7   rH  r  r  rI  r  r  r  r  rS   rK  rL  rM  rN  r  r  )	r   r   r~  rj   r  r  r  r  rO  s	            rV   read_managed_filer  ^  sy    #8w#G FFL==?4DEE>>4HII&!4^__S{{}$$ %%4GHH$$V[[1!4R8RIS""6#4#4#67>>wG I;hwi8 !
(   S6KC54QRRS  L4JKK S6KC54QRRSsN   A#E)&D  <E)=2D3 /#E)	D0D++D00E)3E&E!!E&&E)c                   K   t        ||       \  }}}|j                         st        dd      |j                         st        dd      t	        |      rt        dd      	 |j                         j                  }|t        kD  rt        dd      t        j                  |j                        d   xs d}t        t        |      ||j                  d      S # t        $ r}t        dd	|       d
}~ww xY ww)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  rY  r  NrB  r  r   r  
attachmentr   
media_typer   content_disposition_type)r  r{  r7   rH  r  r  rI  r  r  r  r  rS   r=   r  )r   r   r~  rj   _display_pathr  r  r  s           rV   download_managed_filer    s      %:$$H!FFM==?4DEE>>4HII&!4^__S{{}$$ %%4GHH$$V[[1!4R8RI[!-	   S6KC54QRRSs+   A#C?&C  AC?	C<'C77C<<C?z/api/files/uploadc                   K   t        | j                  |d      \  }}}|j                         r|j                         rt	        dd      |j                         r| j
                  st	        dd      t        | j                        \  }}	 |j                  j                  dd       |j                  |       dt        ||      |dt        |      S # t        $ r t	        dd	      t        $ r}t	        d
d|       d }~ww xY ww)NTrx    'A directory already exists at that pathr   File already existsrW  r  File is not writablerY  Could not write file: r  r  r   )r  r   r{  r   r7   r  r  r  r  rZ  r  r  r  r  r  )r  r   r~  rj   r  r  
_mime_typer  s           rV   upload_managed_filer    s    #8wZ^#_ FFL}}6==?4]^^}}w004IJJ'(8(89D*TD484  $VV4 !
(	   L4JKK T6LSE4RSSTs*   BD.C <DD
5DD

Dr  z/api/files/upload-streamfiler  c                   K   t        || d      \  }}}|j                         r|j                         rt        dd      |j                         r|st        dd      	 |j                  j                  dd       t        j                  d|j                   ddt        |j                              \  }}	t        |	      }
d}d}	 t        j                  |d      5 }	 |j                  t                d {   }|sn6|t#        |      z  }|t$        kD  rt        dd      |j'                  |       V	 d d d        t        j(                  |
|       d}	 |s|
j+                  d       |j-                          d {    dt/        ||      |dt1        |      S # t        $ r t        dd	      t        $ r}t        d
d|       d }~ww xY w7 # 1 sw Y   xY w# t        $ r  t        $ r t        dd	      t        $ r}t        d
d|       d }~ww xY w7 # |s|
j+                  d       |j-                          d {  7   w xY ww)NTrx  r  r  r   r  rW  r  r  rY  z#Could not create parent directory: r  .uploadr  r  dirr   FwbrB  r  r  
missing_okr  )r  r{  r   r7   r  rZ  r  r  tempfilemkstemprS   r  r   rv   fdopenrf  _UPLOAD_CHUNK_BYTESr   r  writer  unlinkr   r  r  )r   r  r   r  r~  rj   r  r  tmp_fdtmp_nametmp_pathtotalrenamedr;  chunks                  rV   upload_managed_file_streamr    sY     $9wRV#W FFL}}6==?4]^^}}y4IJJaD48  ''6;;-q!)V]]9KFH H~HEGYYvt$ 	!"ii(;<<U#22'C@STT		%   		! 	

8V$ OOtO,jjl $VV4 !
(	 S  L4JKK a6YZ]Y^4_``a =	! 	!   L4JKK T6LSE4RSST 	 OOtO,jjls   AI"F" ?AIG& !G;G<=G: G& 'IH%I"G GGIGG#G& &'H"HH""H' %I'(IIIIz/api/files/mkdirc                 f  K   t        | j                  |d      \  }}}|j                         r|j                         st	        dd      	 |j                  dd       dt        ||      |dt        |      S # t        $ r t	        dd      t        $ r}t	        d	d
|       d }~ww xY ww)NTrx  r  z"A file already exists at that pathr   rW  r  zDirectory is not writablerY  zCould not create directory: r  )
r  r   r{  r   r7   rZ  r  r  r  r  )r  r   r~  rj   r  r  s         rV   create_managed_directoryr  	  s     #8wZ^#_ FFL}}v}}4XYYZTD1 $VV4 !
(	   Q4OPP Z6RSVRW4XYYZs*   A
B1A;  B1;B.B))B..B1c                 J  K   t        | j                  |      \  }}}|j                  ||j                  k(  rt        dd      |j                  |k(  rt        dd      |j                         st        dd      	 |j                         r3| j                  rt        j                  |       n!|j                          n|j                          d
|dt        |      S # t        $ r5}|j                         r| j                  sdnd}t        |d	|       d }~ww xY ww)Nr   z$Cannot delete the managed files rootr   z!Cannot delete the filesystem rootr   rT  r  rY  zCould not delete path: Tr  r   )r  r   r  r7   r  r{  r   r  shutilrmtreermdirr  r  r  )r  r   r~  rj   r  r  r   s          rV   delete_managed_filer  	  s    #8w#O FFL%&F4F4F*F4Z[[}}4WXX==?4DEE
]==?  f%MMO
 O0Fv0NOO	  ]#]]_W5F5FcC>UVYUZ<[\\]s+   A<D#?AC" D#"	D +0DD  D#z/api/fs/listc           	        K   t        |       }	 g }t        j                  |      5 }|D ]Z  }|j                  t        v r|j                  |j                  t        ||j                  z        |j                  d      d       \ 	 d d d        |j                  d        d|iS # 1 sw Y    xY w# 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       xs ddcY d }~S d }~ww xY ww)NF)follow_symlinks)rS   r   isDirectoryc                 <    | d    | d   j                         | d   fS )Nr  rS   )r   r  s    rV   r  zfs_list.<locals>.<lambda>D	  s)    4+>'>V@R@R@TVZ[aVb&c rX   r  r  ENOENT)r  errorENOTDIREACCESstrerrorz
read-error)r  rv   scandirrS   _FS_READDIR_HIDDENr  r  r   r  r  r  r  r  r   )r   rj   r  scanr  r  s         rV   fs_listr  6	  s    d^FXZZ 	4 ::!33!JJ 34#(<<<#F  	 	cd7##	 	  211 3	22 211 XZ(F(V,WWXsp   D	B2 A B&B2 %D	&B/+B2 2D D	DD	DD	 D(D;D<D	DD	z/api/fs/read-textc           	        K   t        t        |             \  }}|j                  t        kD  rt	        dd      t        |j                  t              }	 |j                  d      5 }|j                  |      }d d d        t        d d	       |j                  t        j                  |j                  j!                         d
      t#        |      t        |      |j%                  dd      |j                  t        kD  dS # 1 sw Y   xY w# t        $ r t	        dd      t        $ r}t	        dt        |      xs d      d }~ww xY ww)NrB  rC  r   rbr  r  r   File read failedr(  r  utf-8r  errors)binarybyteSizelanguagemimeTyper   r  	truncated)r  r  rI  _FS_TEXT_SOURCE_MAX_BYTESr7   min_FS_TEXT_PREVIEW_MAX_BYTESopenrf  r  r  r  r
  _FS_PREVIEW_LANGUAGE_BY_EXTr   r  r   r   rN  )r   rj   r  bytes_to_readhandler  r  s          rV   fs_read_textr  P	  s#    !(4.1JFB	zz--4DEE

$>?MT[[ 	.&;;}-D	. #4;/JJ/33FMM4G4G4I6R!&)FGI6ZZ"<< 	. 	. L4JKK TCH4R@RSSTsC   AED &D 8D  B E D	D E	*EE		Ec                   "    e Zd ZU eed<   eed<   y)FsWriteTextr   r   Nr  r  rX   rV   r  r  h	  s    
ILrX   r  z/api/fs/write-textc                 .  K   t        | j                        }| j                  xs d}t        |j	                  d            t
        kD  rt        dd      	 |j                         }|,t        j                  |j                        rt        d	d      |,t        j                  |j                        st        d	d      |j                  j!                         st        d	d      |j#                  d|j$                   dt'        j(                                }	 |j+                  |d       t'        j,                  ||       dt        |      t        |j	                  d            dS # t        $ r d}Y t        $ r t        dd      t        $ r}t        d	t        |      xs d
      d}~ww xY w# t        $ r  |j/                  d       t        dd      t        $ r'}|j/                  d       t        dd|       d}~ww xY ww)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   r  rB  zContent too larger   Nr  r  r   r  r  z!Only regular files can be writtenzParent directory does not existr  z.hermes-tmp-encodingTr  rY  r  )r  r   r	  )r  r   r   r   r   _FS_TEXT_WRITE_MAX_BYTESr7   r  r  r  r  r  r  r  r  r  r   	with_namerS   rv   getpid
write_textr  r  )r  rj   r  r  r  tmps         rV   fs_write_textr  m	  s     gll#F?? bD
4;;w #;;4GHHP'-{{} 
~$,,rzz24PQQ	~dll2::64WXX==!4UVV


Qv{{m<		}E
FCTtg.


3 FT[[=Q9RSS3   L4JKK PCH4NOOP  L

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

d
#6LSE4RSSTs[   AHE. 'B6H)F; 'H.F89H<F8F33F88H;0H+"HHHz/api/fs/read-data-urlc                   K   t        t        |             \  }}|j                  t        kD  rt	        dd      	 t        j                  |j                               j                  d      }d	d
t        |       d| iS # t        $ r t	        dd      t        $ r}t	        dt        |      xs d      d }~ww xY ww)NrB  rC  r   rD  r  r  r   r  dataUrlrE  rF  )r  r  rI  _FS_DATA_URL_MAX_BYTESr7   rK  rL  rM  rN  r  r  r  r   )r   rj   r  rO  r  s        rV   fs_read_data_urlr#  	  s     !(4.1JFB	zz**4DEET""6#4#4#67>>wG
 }V45XgYGHH	  L4JKK TCH4R@RSSTs(   8C 2B  -C  B=B88B==C z/api/fs/git-rootc                    K   t        |       }	 |j                         }t        j                  |j                        r|n|j                  }dt        |      iS # t
        $ r |}Y w xY ww)Nr<  )r  r  r  r  r  r  r  )r   rj   r  rT   s       rV   fs_git_rootr%  	  sa     d^F[[],,rzz2 %e,--  s(   A*=A A*A'$A*&A''A*z/api/fs/default-cwdc                  :   K   t               } | t        |       dS w)N)r  r+  )r"  r4  )r  s    rV   fs_default_cwdr'  	  s     

C."566s   )web_gitc                    K   t        j                         }	  |j                  d| g|  d{   S 7 # t        $ r}t	        dt        |      xs d      d}~ww xY ww)zIRun a (blocking) git op off the event loop; map a failed mutation to 400.Nr   zgit operation failedr   )ro   r  ru   r  r7   r  )fnrk   r  r  s       rV   _git_opr+  	  sb     ##%DX)T))$:T:::: XCH4V@VWWXs0   A!6 46 A!6 	AAAA!c                 *    t        t        |             S r  )r  r  r  s    rV   	_git_pathr-  	  s    x~rX   c                       e Zd ZU eed<   y)GitPathBodyr   Nr  r  rX   rV   r/  r/  	  r  rX   r/  c                   ,    e Zd ZU eed<   dZee   ed<   y)GitFileBodyr   Nr  )r  r  r  r  r  r  r   r  rX   rV   r1  r1  	  s    
ID(3-rX   r1  c                   0    e Zd ZU eed<   eed<   dZeed<   y)GitCommitBodyr   messageFpushN)r  r  r  r  r  r5  r   r  rX   rV   r3  r3  	  s    
ILD$rX   r3  c                   h    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<   y)GitWorktreeAddBodyr   NrS   r+  rg  existingBranch)
r  r  r  r  r  rS   r   r+  rg  r8  r  rX   rV   r7  r7  	  s?    
ID(3- FHSM D(3-$(NHSM(rX   r7  c                   0    e Zd ZU eed<   eed<   dZeed<   y)GitWorktreeRemoveBodyr   worktreePathFforceNr  r  r  r  r  r<  r   r  rX   rV   r:  r:  	  s    
IE4rX   r:  c                   "    e Zd ZU eed<   eed<   y)GitBranchSwitchBodyr   r+  Nr  r  rX   rV   r?  r?  	  s    
IKrX   r?  z/api/git/statusc                 d   K   t        t        j                  t        |              d {   S 7 wr  )r+  _web_gitrepo_statusr-  r  s    rV   git_status_routerC  	  s#     --y????   '0.0z/api/git/worktreesc                 h   K   dt        t        j                  t        |              d {   iS 7 w)N	worktrees)r+  rA  worktree_listr-  r  s    rV   git_worktrees_routerH  	  s(     wx'='=yOOPPO   (202z/api/git/branchesc                 h   K   dt        t        j                  t        |              d {   iS 7 wNbranches)r+  rA  branch_listr-  r  s    rV   git_branches_routerN  	  s(     gh&:&:IdOLLMMLrI  z/api/git/base-branchesc                 h   K   dt        t        j                  t        |              d {   iS 7 wrK  )r+  rA  base_branch_listr-  r  s    rV   git_base_branches_routerQ  
  s(     gh&?&?4QQRRQrI  z/api/git/review/listr  rg  c                 h   K   t        t        j                  t        |       ||       d {   S 7 wr  )r+  rA  review_listr-  )r   r  rg  s      rV   git_review_list_routerT  	
  s'     --ytLLLLs   )202z/api/git/review/diffstagedc           	      p   K   dt        t        j                  t        |       ||||       d {   iS 7 wNdiff)r+  rA  review_diffr-  )r   r  r  rg  rU  s        rV   git_review_diff_routerZ  
  s4      '("6"6	$uVZ\bccddcs   ,646z/api/git/file-diffc                 j   K   dt        t        j                  t        |       |       d {   iS 7 wrW  )r+  rA  file_diff_vs_headr-  )r   r  s     rV   git_file_diff_router]  
  s*     '("<"<iotTTUUT   )313z/api/git/review/commit-contextc                 d   K   t        t        j                  t        |              d {   S 7 wr  )r+  rA  review_commit_contextr-  r  s    rV   git_commit_context_routera  
  s#     774IIIIrD  z/api/git/review/rev-parserefc                 j   K   dt        t        j                  t        |       |       d {   iS 7 w)Nsha)r+  rA  review_rev_parser-  )r   rb  s     rV   git_rev_parse_routerf  
  s*     !:!:IdOSQQRRQr^  z/api/git/review/ship-infoc                 d   K   t        t        j                  t        |              d {   S 7 wr  )r+  rA  review_ship_infor-  r  s    rV   git_ship_info_routeri  $
  s#     22IdODDDDrD  z/api/git/review/stagerj  c                    K   t        t        j                  t        | j                        | j
                         d {   S 7 wr  )r+  rA  review_stager-  r   r  rj  s    rV   git_stage_routerm  )
  s.     ..	$))0DdiiPPPP   <AAAz/api/git/review/unstagec                    K   t        t        j                  t        | j                        | j
                         d {   S 7 wr  )r+  rA  review_unstager-  r   r  rl  s    rV   git_unstage_routerq  .
  s.     00)DII2F		RRRRrn  z/api/git/review/revertc                    K   t        t        j                  t        | j                        | j
                         d {   S 7 wr  )r+  rA  review_revertr-  r   r  rl  s    rV   git_revert_routert  3
  s.     //4991EtyyQQQQrn  z/api/git/review/commitc                    K   t        t        j                  t        | j                        | j
                  | j                         d {   S 7 wr  )r+  rA  review_commitr-  r   r4  r5  rl  s    rV   git_commit_routerw  8
  s7     //4991Et||UYU^U^____s   AA	A
Az/api/git/review/pushc                 x   K   t        t        j                  t        | j                               d {   S 7 wr  )r+  rA  review_pushr-  r   rl  s    rV   git_push_routerz  =
  s(     --y/CDDDD   1:8:z/api/git/review/create-prc                 x   K   t        t        j                  t        | j                               d {   S 7 wr  )r+  rA  review_create_prr-  r   rl  s    rV   git_create_pr_router~  B
  s(     22Idii4HIIIIr{  z/api/git/worktree/addc                 *  K   | j                   | j                  | j                  | j                  dj	                         D ci c]
  \  }}|r|| }}}t        t        j                  t        | j                        |       d {   S c c}}w 7 
w)N)rS   r+  rg  r8  )
rS   r+  rg  r8  r  r+  rA  worktree_addr-  r   )rj  r  r  r  s       rV   git_worktree_add_router  G
  s     
 IIkkII"11	

 %'	C  	U
	G 	 ..	$))0DgNNN	 Os   ABB4BBBz/api/git/worktree/removec                    K   t        t        j                  t        | j                        t        | j
                        | j                         d {   S 7 wr  )r+  rA  worktree_remover-  r   r;  r<  rl  s    rV   git_worktree_remove_router  V
  sG       )DII"6	$BSBS8TVZV`V`   s   AAAAz/api/git/branch/switchc                    K   t        t        j                  t        | j                        | j
                         d {   S 7 wr  )r+  rA  branch_switchr-  r   r+  rl  s    rV   git_branch_switch_router  ]
  s.     //4991Et{{SSSSrn  )porti!  )r  i!  )r  i!  )webhook_porti="  )r  !  )r  r  )r  i  )r  i  )	webhook
api_servermsgraph_webhookfeishuwecom_callbackbluebubblessmswhatsapp_cloudline_PORT_BINDING_PLATFORM_PORTS>   fatalstoppeddisconnectedprofile_homeruntimec                 \   |xs i j                  d      xs i }|j                         D cg c]6  \  }}|t        v r)t        |t              r|j                  d      t
        vr|8 }}}|si S i }	 t        | dz  d      5 }t        j                  |      xs i }ddd       t        j                  d      t              r|j                  d      ni }	|	xs i j                  d      |j                  d      fD ]]  }
t        |
t              s|
j                         D ]7  \  }}t        |t              s|j                  |i       j                  |       9 _ 	 i }|D ]  }t        |   \  }}|j                  |      xs i }t        |j                  d      t              r|j                  d      ni }|j                  ||xs i j                  ||            }	 t        |      ||<    |S c c}}w # 1 sw Y   \xY w# t        $ r i }Y w xY w# t        t        f$ r |||<   Y w xY w)	a  Best-effort map of ``platform -> host TCP port`` for one profile's gateway.

    Reads the platforms the running gateway reported in its
    ``gateway_state.json`` and resolves each port-binding platform's port from
    the profile's ``config.yaml`` (top-level ``platforms:`` wins over
    ``gateway.platforms:``, matching ``load_gateway_config`` precedence),
    falling back to the adapter default.  Display-only: env-var port overrides
    (e.g. ``WEBHOOK_PORT`` in that profile's .env) are not resolved here.
    	platformsrm   r  r  r  Ngatewayextra)r   r  r  r  r  _PLATFORM_DEAD_STATESr  r  	safe_load
setdefaultr  r\   r  	TypeErrorr  )r  r  r  rS   rm   activeblocksfr  gateway_cfgsrc	plat_name
plat_blockportsport_keydefault_portr  r  r  s                      rV   _profile_platform_portsr  w
  s'    B##K06BI ) 1u//ud#IIg&;; 	F  	 F,.A 	*Q..#)rC	*,6swwy7I4,Pcggi(VX !&B++K8#''+:NO 	HCc4(), H%	:j$/%%i4;;JGH	H E '!=d!C,

4 &B&071CT&J		'"PRii5;B"3"3Hl"KL	'c(E$K' LG	* 	*   :& 	'&E$K	'sH   ;G02H G6BH 7%H H6H ;H HHH+*H+c                     	 ddl m} m} ddlm}  |d      }|D cg c]  \  }}|	 }}}g }d	}|D ]  \  }}		  | |	      s	 	  ||	d
z        }
|
xs i j                  d      xs g D cg c]  }t        |       }}|dk(  rt        |      dkD  rd}|t        |	|
      d}|r||d<   |j                  |        |rd}n$t        |      dkD  rd}nt        |      dk(  rd}nd}|||dS # t
        $ r  t        j                  dd       g dg dcY S w xY wc c}}w # t
        $ r Y w xY w# t
        $ r d}
Y w xY wc c}w )uA  Enumerate profiles and the gateways serving them for ``/api/status``.

    Returns ``{"profiles": [...], "gateway_mode": ..., "gateways": [...]}``:

    * ``profiles`` — every profile on the host (default + named), from
      ``profiles_to_serve(True)`` (the cheap enumeration chokepoint — no
      per-profile config reads or skill counts).
    * ``gateways`` — one entry per profile with a LIVE gateway process:
      ``{"profile", "ports", "served_profiles"?}``.  Liveness reuses
      ``_check_gateway_running`` so this agrees with the profiles sidebar.
    * ``gateway_mode`` — ``"multiplex"`` when the default gateway serves
      multiple profiles (gateway.multiplex_profiles), ``"single"`` for one
      live gateway, ``"multiple"`` for independent per-profile gateways,
      ``"none"`` when nothing is running.
    r   )_check_gateway_runningprofiles_to_serve)r2   Tz+profile/gateway topology enumeration failedr:  unknown)profilesgateway_modegatewaysFzgateway_state.jsonNserved_profilesr?  r   )r  r  	multiplexmultiplesinglenone)hermes_cli.profilesr  r  gateway.statusr2   r\   rQ   r@  r   r  r   r  r  )r  r  r2   homesrS   _homeprofile_namesr  r  r9  r  r   servedr  r  s                  rV   !_collect_profile_gateway_topologyr  
  s    KQ6!$'
 .33kdET3M3%'HI 
d	)$/ 0	)$1E*EFG %,Mr#6#67H#I#ORQQ#a&QQ9VqI,T7;!
 '-E#$'* 	X		X!	%tRRK  K

@4
P	rJJK 4  		  	G	Qs?   C+ DDD,*D=+&DD	D)(D),D:9D:z/api/statusc           	      2  K   d }| xs dj                         }|r.|j                         dk7  rt        |      }|j                          	 t	               \  }}t               }|d u}d }|sRt        rLt        j                         }|j                  d t               d {   \  }	}|	rd}|r|j                  d      }d }
i }d }d }d }	 ddlm}  |       }|j                         D ch c]  }|j                   }}t#               }|}||r|j                  d      r|}|s|t%        |      }|d}|}|r|j                  d      }
|j                  d      xs i }|'|j'                         D ci c]  \  }}||v r|| }}}|j                  d	      }|j                  d
      }|s|
dv r|
nd}
i }n
|r||
dv rd}
|r|
|d}
t)                d {   }t+        |xs i j                  dd            }t-        ||
|      }t/        ||
      }t        j                         j                  d t0               d {   }t3        t5        t6        j8                  dd            }g }	 ddlm}  |       D cg c]  }|j>                   }}d} 	 ddl m!}!  |!       } i dtD        dtF        d|d|dtI                d|d|
d|d|d|d|d |d!|d"|d#|d|d$|d%| i}"t        j                         j                  d tJ               d {   }#|#d&   |"d&<   |#d'   |"d'<   |sR|"jM                  tO        tQ                     tO        tS                     tO        tU                     |t        |#d(   d)       |"|" |jV                  tY        jZ                           S S 7 c c}w # t         $ r d }Y w xY wc c}}w 7 7 c c}w # t         $ r Y Kw xY w# t         $ r d} Y Mw xY w7 # |" |jV                  tY        jZ                           w w xY ww)*Nr   currentTpidr   )load_gateway_configgateway_stater  exit_reason
updated_at>   r  startup_failedr  >   Nr  runningactive_agents)gateway_runningr  r  )r  r  r   Fr  r  )get_nous_session_validityr   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nous_session_validr  r  r  )hermes_homeconfig_pathenv_pathgateway_pidgateway_health_urlr  ).r   r   _config_profile_scope	__enter__r!   r.   r]  ro   r  ru   rk  r   gateway.configr  get_connected_platformsr  r\   r2   r0   r  r  r1   r,   r-   rd   r   r   re   rm   hermes_cli.dashboard_authr  rS   hermes_cli.authr  r   r   rr  r  r  r  r   r   r   __exit__r,  r;  )$r  status_scoperequested_profilecurrent_ver
latest_verr  r  remote_health_bodyr  aliver  r  r  r  configured_gateway_platformsr  gateway_configr-  local_runtimer  runtime_pidr  r  r  r  r  r  r  r   r  _list_providersr   r  r  rc  topologys$                                       rV   
get_statusr  
  s+    L B--/ .446)C,->? K3"6"8Z
 -.%T1*.#6++-D.2.B.B+/ )%E% "&%"4"8"8"?K"$"!8<$	0:02N/=/U/U/W,#+,( , ,-?16H6L6L_6](G =#<8GK&"&)#KK8M 'K 8 >B+7 '8&=&=&?%"U:: J%! %
 #*++m"<!(\!:"1>B_1_en$&! %7%C !$55$-M }49K9W%M 7 99 ,W],?,?QR,ST*+''

 5+'
 '.&>&>&@&P&P0'
 !
 WSYYGH$&	S.=.?@aff@N@ '	+A!:!<
{
,
 k
 $Z	

  %O%Q!Q
 
 ]
  !2
 "#6
 !"4
 ]
 L
  !2
 $%:
 
  ]!
" n#
$ !"4%
B !113CC3
 
 &j1z!).!9~ MM"?#45"?#45/*&9$Z0  #!L!!3<<>2 $})$,  	0+/(	00%, :2!
 A 		   	+!*	+P
: #!L!!3<<>2 $s   AP
AO. N(O. N' %N"8N' :A5O. /N9 AO. N?A)O. ;O<*O. 'O
 6O	O
 O. O A5O. O,A)O. :%PO. "N' 'N62O. 5N66
O. O. O
 
	OO. OO. O)%O. (O))O. .&PPiU  r   platform_labelc                     | xs d|xs dfD ]7  }t        j                  d|      }|s	 t        |j                  d            c S  y# t        $ r Y Fw xY w)zAExtract the Windows NT build number from stdlib platform strings.r   z%(?:^|[^\d])10\.0\.(\d{5,})(?:[^\d]|$)r   N)r  searchr  groupr  )r   r  r  matchs       rV   _windows_build_numberr    sg    -R!526 		BEJ	u{{1~&&   		s   A	AAsystemreleasec                     | dk(  r7|dk(  r2t        ||      }|$|t        k\  rt        j                  dd|d      }d}| |||dS )	zAReturn host OS fields for display while preserving stdlib detail.Windows10z^Windows-10(?=-)z
Windows-11r   )count11)rv   
os_release
os_versionr-  )r  _WINDOWS_11_MIN_BUILDr  r  )r  r  r   r  r  s        rV   _display_system_platformr    se     w$%g~>*?!?VV#	N G "	 rX   z/api/system/statsc                  F  K   ddl } i t        | j                         | j                         | j	                         | j                               | j                         | j                         | j                         | j                         t        t        j                         d}	 ddl}|j                         }|j                  |j                  |j                   |j"                  d|d<   	 |j%                  t'        t)                           }|j                  |j                   |j*                  |j"                  d|d<   	 |j/                  d	
      |d<   t1        |dd      }|rt3         |             |d<   	 |j5                         }t7        t9        j8                         |z
        |d<   	 |j;                         }|j<                  |j?                         j@                  t7        |jC                               |jE                         d|d<   d|d<   |S # t,        $ r Y w xY w# t,        $ r Y w xY w# t,        $ r Y w xY w# t,        $ r Y ?w xY w# t,        $ rA d|d<   	 t3        t        jF                               |d<   Y |S # tH        tJ        f$ r Y Y |S w xY ww xY ww)a!  Host + process system stats for the System page.

    OS / Python / host identity from stdlib; CPU / memory / disk / uptime from
    psutil when available, with graceful degradation when it isn't.  Read-only
    and non-sensitive (no env values, no paths beyond the hermes home root).
    r   N)r  r  r   r  )archrd  python_versionpython_implhermes_version	cpu_count)r  	availableusedpercentrz  )r  r
  freer  disk皙?rO   cpu_percent
getloadavgload_avguptime_seconds)r  rsscreate_timenum_threadsprocessTpsutilF)&r-  r  r  r  r   machinenoder  python_implementationr   rv   r  r  virtual_memoryr  r	  r
  r  
disk_usager  r   r  r\   r  r   r  	boot_timer  r}  Processr  memory_infor  r  r  r  r  r   )	_platformrR   r  vmdulabootprocs           rV   get_system_statsr&    s     !
"##%%%'%%'$--/	
 !!#NN$#224 668%\\^D 2""$XXGGzz	
X		""3'8#9:B::	DL	"("4"4c"4"BDt4B#':Z 	##%D%(t);%<D!"		>>#Dxx'')--"4#3#3#56#//1	DO X KE  		  		
  		  		  X	#BMMO4D K ( 	K	s   B%J!(AI /AH 7H' <3H6 0A!I I J!	H$!I #H$$I '	H30I 2H33I 6	I?I II 	II II J# JJ!JJJ!JJJ!z/api/curatorc                  T  K   	 ddl m}  	 | j	                         }t        | dd      t        | dd	      t        | d
d       |j                  d      t        | dd       t        | dd       t        | dd       dS # t        $ r}t        dd|       d }~ww xY w# t        $ r i }Y w xY ww)Nr   curatorrY  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_hoursr-  min_idle_hoursstale_after_daysarchive_after_days)re  r)  r\   r7   
load_state
_safe_callr   )r)  r  rm   s      rV   get_curator_statusr8  H  s     S!""$ g|T:Wk59$W.BDIyy/$W.BDI&w0FM(2JDQ   S6KC54QRRS  sE   B(A6 B AB(6	B?BBB(B%"B($B%%B(c                       e Zd ZU eed<   y)CuratorPauser1  Nr  r  r  r   r  r  rX   rV   r:  r:  ]      LrX   r:  z/api/curator/pausedc                    K   ddl m} |j                  t        | j                               dt        | j                        dS w)Nr   r(  T)r  r1  )re  r)  
set_pausedr   r1  )rj  r)  s     rV   set_curator_pausedr?  a  s4     tDKK()$t{{"344s   AAz/api/curator/runc                     K   	 t        ddgd      } d| j                  dd	S # t        $ r}t        dd|       d}~ww xY ww)
zDTrigger a curator review now (backgrounded; tail via action status).r)  r/  curator-runrY  zFailed to run curator: r   NTr  r  rS   _spawn_hermes_actionr\   r7   r  r%  r  s     rV   run_curatorrF  i  sX     U#Y$6F txx??  U6McU4STTU"   A" A	A ;A  Az/api/learning/graphc                    K   	 ddl m} t        |       5   |       cddd       S # 1 sw Y   yxY w# t        $ r# t        j                  d       t        dd      w xY w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 failedrY  zFailed to build learning graphr   )agent.learning_graphrI  r  r\   rQ   	exceptionr7   )r  rI  s     rV   get_learning_graphrL  s  s\     V=G$ 	*')	* 	* 	* V784TUUVs2   A!2 &	2 A!/2 A!2 ,AA!c                   ,    e Zd ZU eed<   dZee   ed<   y)LearningNodeRefr  Nr  r  r  rX   rV   rN  rN    s    G!GXc]!rX   rN  c                   6    e Zd ZU eed<   eed<   dZee   ed<   y)LearningNodeEditr  r   Nr  r  r  rX   rV   rP  rP    s    GL!GXc]!rX   rP  z/api/learning/noder  c                    K   ddl m} t        |      5   ||       }ddd       j                  d      st	        d|j                  dd            |S # 1 sw Y   9xY ww)	zXCurrent content of a journey node (skill SKILL.md or memory chunk), for an edit prefill.r   )node_detailNr  r   r4  	not foundr   )agent.learning_mutationsrR  r  r   r7   )r  r  rR  ress       rV   get_learning_noderV    sX      5		  "o774=CGGI{4STTJ	 s   A!	A8A!AA!c                    K   ddl m} t        | j                        5   || j                        }ddd       j                  d      st        d|j                  dd            |S # 1 sw Y   9xY ww)	uM   Delete a journey node — skills are archived (restorable), memories removed.r   )delete_nodeNr  r   r4  zdelete failedr   )rT  rX  r  r  r  r   r7   )rj  rX  rU  s      rV   delete_learning_noderY    sa      5		% #$''"#774=CGGI4WXXJ	# #s   A5A)8A5)A2.A5c                   K   ddl m} t        | j                        5   || j                  | j
                        }ddd       j                  d      st        d|j                  dd            |S # 1 sw Y   9xY ww)	z<Rewrite a journey node's content (SKILL.md or memory chunk).r   )	edit_nodeNr  r   r4  zedit failedr   )rT  r[  r  r  r  r   r   r7   )rj  r[  rU  s      rV   update_learning_noder\    sg      3		% /./774=CGGI}4UVVJ	/ /s   B A48B 4A=9B fn_namec                 h    	 t        | |d       }t        |      r |       S |S # t        $ r |cY S w xY wr  )r   callabler\   )modr]  r?  r*  s       rV   r7  r7    s<    S'4(|rt00 s   # # 11z/api/portalc                    K   t               xs i } i }	 ddlm}  |       xs i }g }	 ddlm}  ||       }|}|j                         D ]j  }t        |dd      rd}n9t        |dd      rt        |dd       r|j                  }nt        |dd      rd}nd	}|j                  t        |d
d      |d       l t        | j                  d      t              r| j                  d      ni }t        |j                  d            |j                  d      |j                  d      t!        |xs i j                  d      xs d      d|dS # t        $ r i }Y /w xY w# t        $ r t        j                  d       Y w xY ww)Nr   )get_nous_auth_statusget_nous_subscription_featuresmanaged_by_nousFzvia Nous Portalr  current_providerznot configuredlabelr   )rg  rm   zportal features failedr#  	logged_inportal_base_urlinference_base_urlrU   z3https://portal.nousresearch.com/manage-subscription)rh  
portal_urlinference_urlrU   subscription_urlfeatures)r   r  rb  r\   hermes_cli.nous_subscriptionrd  r  r   rf  r  rQ   rK  r  r   r  r   r  )	r  r   rb  rn  rd  featsfeatrm   rK  s	            rV   get_portal_statusrr    su    
-
2CD8#%+ H1O.s3 	W4!2E:-ET8U3FXZ^8_ 11ET8U3$E,E'$*Du UV	W %/swww/?$F BI$((;/0hh01"67b--j9?R@Q /  &  1/01sL   E<E E<BE 5BE<EE<EE<E96E<8E99E<z/api/ops/prompt-sizec                     K   	 t        dgd      } d| j                  ddS # t        $ r}t        dd|       d }~ww xY ww)Nprompt-sizerY  Failed: r   TrB  rC  rE  s     rV   run_prompt_sizerv    sT     F#]O]C txx??  Fhse4DEEF   A! A	?:?Az/api/ops/dumpc                     K   	 t        dgd      } d| j                  ddS # t        $ r}t        dd|       d }~ww xY ww)NdumprY  ru  r   TrB  rC  rE  s     rV   run_dumprz    sT     F#VHf5 txx88  Fhse4DEEFrw  z/api/ops/config-migratec                     K   	 t        ddgd      } d| j                  ddS # t        $ r}t        dd|       d }~ww xY ww)	Nr  migrateconfig-migraterY  ru  r   TrB  rC  rE  s     rV   run_config_migrater~    sY     F#Xy$9;KL txx1ABB  Fhse4DEEFrG  c                   *    e Zd ZU dZeed<   dZeed<   y)DebugShareRequestTredactr\  linesN)r  r  r  r  r   r  r  r  r  rX   rV   r  r    s     FDE3rX   r  z/api/ops/debug-sharec                   K   ddl m} | xs
 t               }	 t        j                  |t        dt        t        |j                        d            t        |j                               d{   }d|j                   |j"                  |j$                  |j&                  dS 7 4# t        $ r}t        dd| 	      d}~wt        $ r*}t        j                  d
       t        dd| 	      d}~ww xY ww)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 failedrY  ru  T)r  urlsfailuresredactedauto_delete_seconds)hermes_cli.debugr  r  ro   	to_threadr\  r  r  r  r   r  r  r7   r\   rQ   rK  r  r  r  r  )rj  r  rh  r3  r  s        rV   run_debug_share_endpointr    s      3

%#%CF((!SSYY67

#
 
 OOOO%99 

  MocU4KLL F+,hse4DEEFsG   C6AB$ -B".B$ 20C6"B$ $	C3-B==C3	%C..C33C6logs_ACTION_LOG_DIRi   i    i   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.logrA  zaction-curator-run.logrt  zaction-prompt-size.logry  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_RESULTSr4  	exit_codec           	         t         |    }t        j                  dd       t        |z  }t        |dd      5 }|j	                  d|  dt        j                  d       d	j                                |j	                  |j                  d
d             |j                  d      s|j	                  d       ddd       t        j                  | d       t        j                  | d       |ddt        | <   y# 1 sw Y   BxY w)zBRecord a non-spawned action result and write it to the action log.TrW  abr   	buffering
=== z completed %Y-%m-%d %H:%M:%S ===
r  r  r  
   
Nr  r  )r  r  rZ  r  r  r}  r  r   r_  r  rP  r  r  )rS   r4  r  log_file_namelog_pathlog_files         rV   _record_completed_actionr  f  s    %d+M$6.H	h	* "hTF+dmm4G&H%IPWWY	
 	w~~gi~@A%NN5!" dD!t$*3DAOD" "s   A>C77D c                  f   t         j                  dk7  rt         j                  S t         j                  } | j                         j	                  d      r^t
        j                  j                  t
        j                  j                  |       d      }t
        j                  j                  |      r|S | S )z=Prefer pythonw.exe for detached dashboard actions on Windows.r'  z
python.exezpythonw.exe)
r,  r-  
executabler   r_  rv   r   joindirnameisfile)exepythonws     rV   _dashboard_spawn_executabler  w  sp    
||w~~
..C
yy{L)'',,rwws3]C77>>'"NJrX   
subcommandc                    t         |   }t        j                  dd       t        |z  }t        |dd      }|j	                  d| dt        j                  d       d	j                                t               d
dg| }i t        j                  ddi}|j                  dd       t        t              t        j                  |t        j                   |d}t"        j$                  dk(  rt'               |d<   nd|d<   t        j(                  |fi |}|j+                          t,        j                  |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.
    TrW  r  r   r  r  z	 started r  r  -mzhermes_cli.mainHERMES_NONINTERACTIVErh   _HERMES_GATEWAYN)r  stdinr1  stderrr  r'  r(  start_new_session)r  r  rZ  r  r  r}  r  r   r  rv   r   rP  r  rq  r.  DEVNULLSTDOUTr,  r-  r	   Popenr   r  tupler  r  )	r  rS   r  r  r  cmd
action_envpopen_kwargsr%  s	            rV   rD  rD    sH    &d+M$6.HHda0HNN
i.A BC6JQQS '($0A
OJ
OC >BJJ= 7=JNN$d+ < ####$L ||w(<(>_%,0()C0<0D NNd#":.TM$KrX   r  c                 8   |dk  s| j                         sg S 	 | j                         j                  }|dk  rg S t	        d|t
        z
        }|}t        }d}g }d}	 | j                  d      5 }	||kD  r~||k  ryt        |||z
        }
||
z  }|	j                  |       |	j                  |
      }|j                  |       ||j                  d      z  }t        |dz  t              }||kD  r||k  ry|dkD  r(|	j                  |dz
         |	j                  d      dk7  }ddd       dj                  t        |            j!                  d	d
      j#                         }|r|r|dd }|| d S # t        $ r g cY S w xY w# 1 sw Y   bxY w# t        $ r g cY S w xY w)zBReturn the last ``n`` lines of ``path`` without loading huge logs.r   Fr  r  r$  r   NrX   r  r  r  )r{  r  rI  r  r\  _ACTION_LOG_TAIL_MAX_BYTES$_ACTION_LOG_TAIL_INITIAL_CHUNK_BYTESr  r  seekrf  r  r   _ACTION_LOG_TAIL_MAX_CHUNK_BYTESr  reversedrN  
splitlines)r   r  r  
min_offsetoffset
chunk_sizenewline_countchunksdrop_partial_first_liner  	read_sizer  r  s                rV   _tail_linesr    s   AvT[[]	yy{"" qy	Q99:JF5JMF#YYt_ 	B:%-1*<
FZ,?@	)#F#I.e$U!33 N4
 :%-1*< zFQJ'*0++a.E*A'	B& 	&!"			*	 

 5ab	!:M  		B 	B  	sB   E. F -BE?1-E?F .E<;E<?FF FFverbc                 "    t        |       d|gz   S )Nr  )_profile_cli_argsr  r  s     rV   _gateway_subcommandr    s    W%D(999rX   c                 >    dj                  dgt        | |            S )Nr  hermes)r  r  r  s     rV   _gateway_display_commandr    s!    88XC 3GT BCDDrX   z\d+:[A-Za-z0-9_-]{30,}z\d+z[UW][A-Z0-9]{2,}platform_idr  c                    |sy| dk(  r|dk(  r"t         j                  |      st        dd      |dk(  r\|j                  d      D cg c]#  }|j	                         s|j	                         % }}t        d	 |D              rt        dd
      y| dk7  ry|dk(  r|j                  d      st        dd      |dk(  r|j                  d      st        dd      |dk(  rv|j                  d      D cg c]#  }|j	                         s|j	                         % }}|D cg c]  }|dk7  rt        j                  |      s|  }}|rt        dd      yyc c}w c c}w c c}w )z@Reject platform credentials that are clearly in the wrong field.Nrs  TELEGRAM_BOT_TOKENr   uW   Telegram bot token must be the complete token from @BotFather, such as 123456789:ABC…r   TELEGRAM_ALLOWED_USERSr   c              3   H   K   | ]  }t         j                  |         y wr  )_TELEGRAM_USER_ID_RE	fullmatch)r   user_ids     rV   r   z0_validate_messaging_env_value.<locals>.<genexpr>  s     W7+55g>>Wr  z@Telegram allowed users must be comma-separated numeric user IDs.slackSLACK_BOT_TOKENzxoxb-zTSlack Bot Token must start with xoxb-. Paste the bot token from OAuth & Permissions.SLACK_APP_TOKENzxapp-zkSlack App Token must start with xapp-. Paste the app-level token from Basic Information > App-Level Tokens.SLACK_ALLOWED_USERSr   zKSlack allowed user IDs must be comma-separated member IDs like U01ABC2DEF3.)_TELEGRAM_BOT_TOKEN_REr  r7   r   r   r  r   _SLACK_MEMBER_ID_RE)r  r  r  r  user_idsr  invalids          rV   _validate_messaging_env_valuer    s   j &&/E/O/OPU/Vp  **16S1ARTZZ\

RHRWhWW# #]  	g
(8(8(Ai
 	
 (8(8(A A
 	
 ## .3[[-=NTDJJLNN $
#~&9&C&CG&L 
 

 d   $+ S2 O
s   EE6EE$#E$c                     t        | d      }t        j                  d      }|D|j                         4t        j                  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  Tz7gateway restart already in progress for another profileF)r  r  r   pollr  r  r  rD  )r  r  existingexisting_commands       rV   _spawn_gateway_restartr  !  s     %Wi8J  !23H 7+//0AB#'75;L'LT>!TUU
,=>EErX   c                     	 t        |       \  }}|r t        j                  d|j                         dd|j                  dS # t        $ r-}t        j                  d       dt	        |      dcY d}~S d}~ww xY w)	z@Best-effort gateway restart after enabling the webhook platform.z6Failed to auto-restart gateway after enabling webhooksFrestart_startedrestart_errorNz:Webhook enable: reusing in-flight gateway restart (pid %s)Tr  r  restart_actionrestart_pidr  r\   rQ   rK  r  rR   r  r  r%  reusedr  s       rV   %_restart_gateway_after_webhook_enabler  6  s|    
-g6f 		HHH	

  +xx   
OP$ X
 	

   A 	A8"A3-A83A8z/api/gateway/restartc                    K   	 t        |       \  }}d|j
                  ddS # t        $ r  t        $ r*}t        j	                  d       t        dd|       d}~ww xY ww)	z8Kick off a ``hermes gateway restart`` in the background.zFailed to spawn gateway restartrY  zFailed to restart gateway: r   NTr  rB  )r  r7   r\   rQ   rK  r  )r  r%  _reusedr  s       rV   restart_gatewayr  L  su     Y.w7g xx!    Y896QRUQV4WXXYs$   A!" A!A%AAA!z/api/gateway/drainc           	      x  K   ddl m}m}m} 	 | j	                          d{   }t        |xs i j                  dd            j                         j                         }t        | j                  dd      }t        |dd      xs d}|d	k(  r$ |       }t        j                  d
||       dd	|dS |dk7  rt        dd|d       |t        |      t        |xs i j                  dd                  }	t        j                  d||	d          dd|	d    |       |	d   dS 7 # t
        $ r i }Y w xY ww)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	principalrm  r~   z1Gateway drain CANCEL requested by %s (existed=%s)T)r  r  was_drainingr   zUnknown drain action z; expected 'drain' or 'cancel'r   suppress_notificationF)r	  r  z>Gateway drain BEGIN requested by %s (suppress_notification=%s)requested_at)r  r  r  drainingr  )gateway.drain_controlr  r  r  rd  r\   r  r   r   r   r   rm   rQ   rR   r7   r   )
r   r  r  r  rj  r  principal_objr	  existedr  s
             rV   gateway_drainr  ]  s[    2 \\^# $*"!!(G45;;=CCEF GMM+<dCM{D9H[I%'		EyRYZhHH*6*4RS
 	

 "i."DJB#3#34KU#STG 	IIH'( / $%!()@!A = $ s8   D:D( D%D( C?D:%D( (D73D:6D77D:z/api/hermes/updatec                  t  K   t               rd} t        d| d       dddd| dd	S t        t              }|d
k(  r*t	               } t        d| d       dddd| t        |      d	S 	 t        dgd      }d|j                  ddS # t        $ r*}t        j                  d       t        dd|       d}~ww xY ww)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)r  r  rS   r  r4  update_commandr+  docker_update_unsupportedr  zFailed to spawn hermes updaterY  zFailed to start update: r   TrB  )rr  r  r"   rq  r#   r$   rD  r\   rQ   rK  r7   r  )r4  install_methodr%  r  s       rV   update_hermesr    s      23 	
 	!'QG#:9
 	
 +<8N!.0 'QG#0CNS
 	
V#XJ@
 xx   V676Nse4TUUVs*   A"B8%B 2B8	B5%B00B55B8c                    	 t        j                  ddt        t              ddddt	        |        gddd	      }|j
                  d
k7  rg S g }|j                  j                         D ]Y  }|j                         s|j                  d      g dz   dd }|\  }}}}|j                  |dd ||t	        |xs d
      d       [ |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%  r  r[  r   )r   r   r   0Nr      )rd  summaryauthorat)r.  r/  r  rq  r  r0  r1  r  r   r   r  r\   )	r  r;  rowsr  r  rd  r  r   r!  s	            rV   _recent_upstream_commitsr#    s    !nnL!1#SVH  
 >>QI%'JJ))+ 	D::<ZZ'*;;Ra@E',$C&"KKr7&$bgA,		  	s   A	C A9C CCz/api/hermes/update/checkr<  c                 >  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} | r	 t               dz  j                          t        j                  |       d{   }||d<   |d|d	<   |S |d
k(  rd|d	<   |S d|d<   |dv r$t        j                  t                d{   |d<   |S # t        $ r Y qw xY w7 \# t        $ r t        j                  d       d}Y |w xY w7 @w)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-runtimeNFr  zPHermes updates are managed outside this dashboard in containerized environments.)r  current_versionbehindupdate_available	can_applyr  r4  )r)  pipr+  r4  r   )check_for_updatesz.update_checkzUpdate check failedr&  u5   Couldn't reach the update source — try again later.zYou're on the latest version.Tr'  commits)rr  r   r"   rq  r$   r#   hermes_cli.bannerr*  r   r  r  ro   r  r\   rQ   rK  r#  )r<  r  r  r  r*  r&  s         rV   check_hermes_updater-    so    4 23/* %9.
 	
 +<8N:>JN )&!#~5(G !9;	
7 "_4<<> (():;;
 GH~T	 N 
1<	 N '+"# ^+'.'8'89Q'R!RGIN+   < ,- "Ssf   ADC5 #C$ >C5 C3C5 ?DD	D$	C0-C5 /C00C5 5 DDDDz/api/audio/transcribec                   K   | j                   xs dj                         }|j                  d      rd|vrt        dd      |j	                  dd      \  }}d|vrt        dd	      | j
                  xs |d
d  j	                  dd      d   xs dj                         }|j	                  dd      d   j                         }|j                  d      s|dk(  st        dd      	 t        j                  |d      }|st        dd      t        |      t        kD  rt        dd      d}	 t        |      }t        j                   d|d      5 }	|	j#                  |       |	j$                  }d d d        ddlm}
 t+        j,                         }|j/                  d |
|       d {   }	 |r	 t7        j8                  |       |j=                  d      s t        d|j=                  d      xs d       dt?        |j=                  d!      xs d      j                         |j=                  d"      d#S # t        j                  t        f$ r t        dd      w xY w# 1 sw Y   xY w7 # t        $ r  t0        $ r*}t2        j5                  d       t        dd|       d }~ww xY w# t:        $ r Y w xY w# |r&	 t7        j8                  |       w # t:        $ r Y w w xY ww xY ww)$Nr   rE  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 emptyrB  zAudio recording is too largezhermes-desktop-voice-F)r  r  delete)transcribe_audioz"Desktop voice transcription failedrY  zTranscription failed: successr  zTranscription failed
transcriptrU   )r  r2  rU   ) r  r   r   r7   r   r  r   rK  r  r  r  r  r   _MAX_TRANSCRIPTION_UPLOAD_BYTESr  r  NamedTemporaryFiler  rS   r  r0  ro   r  ru   r\   rQ   rK  rv   r  r  r   r  )r  r  r  rO  r  normalized_mime_typeaudio_bytes	temp_pathr  r  r0  r  r3  r  s                 rV   transcribe_audio_uploadr8  \  s      &B--/Hw'3h+>4KLLnnS!,OFG$J
 	

 	HVABZ--c15a8HLeg  %??3215;;=''1</$H
 	
Y&&w> 4NOO
;994RSSI*95((*
 	! IIk"I	! 	?'')++D2BINN 		)$ ::i ::g&@*@
 	
 &**\28b9??AJJz* S NNJ' Y4WXXY	! 	! O  T;<6LSE4RSST   		)$  s   C%K*(H7 ?0K*0#I/ I!18I/ )I-*I/ /K*2J. A0K*7'IK*!I*&I/ /J+%J&&J++J= .	J:7K*9J::K*=K'KK'	K# K'"K##K''K*c                       e Zd ZU eed<   y)TTSSpeakRequestr  Nr  r  rX   rV   r:  r:    r  rX   r:  r}  c                     t        | j                  d      xs | j                  d      xs d      j                         }t        | j                  d      xs d      j                         }|r| d| dS |S )NrS   voice_idVoicer'  r    ())r  r   r   )r}  rS   r'  s      rV   _elevenlabs_voice_labelr@    sl    uyy DEIIj$9DWEKKMD599Z(.B/557H%-dV2hZq!747rX   _voice_list_last_error	signaturec                 &    | da y| t         k(  ry| a y)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)rA  )rB  s    rV   _voice_list_error_logged_oncerD    s(     !%**&rX   z/api/audio/elevenlabs/voicesc            	        K   t               j                  d      xs# t        j                  j                  d      xs dj	                         } | sdg dS t
        j                  j                  dd| d      	 t        j                         }d	t        t        t        f   ffd
}|j                  d|       d{   }t#        d       g }|j                  d      xs g D ]|  }t/        |t0              st        |j                  d      xs d      j	                         }|sC|j3                  |t        |j                  d      xs |      t5        |      d       ~ |j7                  d        d|dS 7 # t
        j                  j                  $ r}|j                   dv r9t#        d|j                          rt$        j'                  d|       dg ddcY d}~S t#        d|j                          rt$        j)                  d|       t+        dd      d}~wt,        $ r<}t#        t        |            rt$        j)                  d|       t+        dd      d}~ww xY ww)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)r	  voicesz#https://api.elevenlabs.io/v1/voicesapplication/json)Acceptz
xi-api-keyr   rK   c                      t         j                  j                  d      5 } t        j                  | j                         j                  d            cd d d        S # 1 sw Y   y xY w)Nr  rZ  r  )r`  r   ra  rd  re  rf  rN  )responser   s    rV   _fetchz%get_elevenlabs_voices.<locals>._fetch  sR    '''< Czz(--/"8"8"ABC C Cs   2AA(Nr   r  zhttp-u>   ElevenLabs voices unavailable: %s — check ELEVENLABS_API_KEYunauthorized)r	  rG  r  z ElevenLabs voice list failed: %sr  z Could not load ElevenLabs voicesr   rG  r<  rS   )r<  rS   rg  c                 Z    t        | j                  d      xs d      j                         S )Nrg  r   )r  r   r   r  s    rV   r  z'get_elevenlabs_voices.<locals>.<lambda>  s"    TXXg%6%<"!=!C!C!E rX   r  T)r   r   rv   r   r   r`  r   r8   ro   r  r   r  r   ru   r  	HTTPErrorcoderD  rQ   rR   warningr7   r\   r  r  r  r@  r  )	r  r  rM  r  r  rG  r}  r<  r   s	           @rV   get_elevenlabs_voicesrT    s&     z~~23arzz~~FZ7[a_ahhjG"b11nn$$-(!
 % GX'')	CS#X 	C ,,T6::* "$'FX&," %&uyy,2399; 		&)5X6,U3
 	 KKEKF00O ;<<!! X 88z!,uSXXJ-?@		TVY "'"~NN(5
);<LL;SA4VWW X(S2LL;SA4VWWXs]   A8I(<AE> >E<?E> B9I(<E> >I%AHI%I(";HI%)7I  I%%I(z/api/audio/speakc                   K   | j                   xs dj                         }|st        dd      	 ddlm} t        j                         }|j                  d||       d{   }	 t        |t              rt        j                  |      n|}|j                  d      s t        d|j                  d      xs d      |j                  d      }|rt         j"                  j%                  |      st        d	d      t         j"                  j'                  |      d   j)                         }ddddddj                  |d      }		 t+        |d      5 }
|
j-                         }ddd       	 t!        j0                  |       t3        j4                        j7                  d      }dd|	 d| |	|j                  d      dS 7 a# t        $ r*}t        j                  d       t        d	d
|       d}~ww xY w# t        $ r t        d	d      w xY w# 1 sw Y   xY w# t.        $ r}t        d	d|       d}~ww xY w# t.        $ r Y w xY w# 	 t!        j0                  |       w # t.        $ r Y w w xY wxY ww)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 failedrY  zSpeech synthesis failed: zInvalid TTS responser1  r  zSpeech synthesis failed	file_pathzAudio file missingr   r  r  r  r  )r	  r
  r  r  r  r  zCould not read audio: rD  TrE  rF  rU   )r  r  r  rU   )r  r   r7   r  rV  ro   r  ru   r\   rQ   rK  r  r  rd  re  r   rv   r   r  splitextr   r  rf  r  r  rK  rL  rN  )r  r  rV  r  result_jsonr  r3  rW  r  r  fhr6  rO  s                rV   
speak_textr[    si     LLB%%'D4FGGW6'') 007JDQQ
L,6{C,HK(k ::i ::g&C*C
 	

 

;'IBGGNN954HII
''

9
%a
(
.
.
0C 
c#| 	)T" 	$b'')K	$
	IIi  {+227;GI;hwi8JJz*	 S R W126OPSu4UVVW  L4JKKL.	$ 	$ T6LSE4RSST
  			IIi  		s   .I;0G !G"G ''G; B8I;H! H$H! -I A I;G 	G8%G33G88I;;HI;HH! !	H?*H::H??I 	II;II;I8I)(I8)	I52I84I55I88I;z/api/actions/{name}/statusr  c           	      d  K   t         j                  |       }|t        dd|        t        |z  }t	        |t        t        |d      d            }t        j                  |       }|Bt        j                  |       }d}|r|j                  d      nd}|r|j                  d	      nd}	nm|j                         }|du }|j                  }	|K	 |j                  d
       ||	dt        | <   t        j                  | d       t        j                  | d       | |||	|dS # t        $ r Y Kw xY ww)zCTail an action log and report whether the process is still running.Nr   zUnknown action: r   r     Fr  r  rZ  r  )rS   r  r  r  r  )r  r   r7   r  r  r  r\  r  r  r  r  waitr\   rP  r  )
rS   r  r  r  tailr%  r3  r  r  r  s
             rV   get_action_statusr`  L  s:     &))$/M6Ftf4MNN.HxS]D!9:DT"D| $$T*/5FJJ{+4	#)fjjtIIK	t#hh 		!	$ 3<C$HOD!dD)  t,    s+   CD0D! !A D0!	D-*D0,D--D0)system_promptmodel_configr  c                 N    | D ]  }t         D ]  }|j                  |d         ! | S r  )_SESSION_LIST_HEAVY_FIELDSrP  )r  rx  r  s      rV   _strip_session_list_rowsre  |  s5     - 	CEE#t	 OrX   z/api/sessionsrq  r  min_messagesarchivedorderr   exclude_sources
cwd_prefixrH  c
                    |dvrt        dd      |dvrt        dd      d}
|	rt        |	      \  }
}	 t        |	      }	 t        d|      }|d	k(  }|d
k(  }|xs dj	                  d      D cg c]  }|j                         s| }}|j                  |xs d|xs d|xs d| |||||dk(  | 
      }|j                  |xs d|xs d|xs d|||d      }t        j                         }|D ]n  }|j                  d      du xr( ||j                  d|j                  dd            z
  dk  |d<   |
r|
|d<   |
dk(  |d<   t        |j                  d            |d<   p |st        |       ||| |d|j                          S c c}w # |j                          w xY w# t         $ r  t        $ r# t        j                  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.

    Rows omit ``system_prompt``/``model_config`` (the payload-dominating
    fields no list UI reads) unless ``full=1`` is passed.
    excludeonlyincluder   /archived must be one of: exclude, only, includer   createdrecent%order must be one of: created, recentNr   rn  ro  r   r   rs  )
r   ri  rj  rq  r  min_message_countinclude_archivedarchived_onlyorder_by_last_activerr  T)r   rj  ri  ru  rv  rw  exclude_childrenrt  ru  rv  rw  	is_activer  r?  is_default_profilerg  )r  r  rq  r  zGET /api/sessions failedrY  Internal server error)r7   _cron_profile_home_open_session_db_for_profiler\  r   r   r|  session_countr}  r   r   re  r   r\   rQ   rK  )rq  r  rf  rg  rh  r   ri  rj  rH  r  profile_namer  r  ru  rw  rv  rx  exclude_listr  r  ry  s                        rV   get_sessionsr    s@   8 55D
 	
 )):
 	
 #'L,W5a7M)'20	 #A| 4$.M'94
 )8(=2'D'DS'IW!QWWYAWLW,,~ , 4&.$"3!1+%*h%6 "&X - H $$~&.$ , 4"3!1+!% % E ))+C 	8EE*%- Squu]AEE,4JKKsR +  #/AiL.:i.GA*+ $QUU:%6 7*	8 (2 (55TZ[HHJQ XP HHJ  M124KLLMs<   F -F
 /FF	C+F
 4F F
 
FF 5Gz/api/profiles/sessionsc	                 h  " |dvrt        dd      |dvrt        dd      ddlm}	 dd	lm}
 g }|r'|d
k7  r"t        |      \  }}|j                  ||f       nZ	 |
j                         }|D cg c]  }|j                  |j                  f }}|s"|j                  d|
j                  d      f       t        d|      }|dk(  }|dk(  }|xs d}|xs dj                  d      D cg c]  }|j!                         s| }}t#        t        | |z   |       d      }g }d}i }g }t%        j$                         }|D ]  \  }}t'        |      dz  }|j)                         s&	  |	|d      }	 |j-                  ||xs d|d||||dk(  | 	      }|j/                  ||xs d|||d      } || z  }| ||<   |D ]}  }||d<   |dk(  |d<   |j1                  d      du xr( ||j1                  d|j1                  dd            z
  dk  |d <   t3        |j1                  d!            |d!<   |j                  |        	 |j5                           |dk(  rdnd"|j7                  "fd"d#       |||| z    }!|st9        |!       |!||| ||d$S c c}w # t        $ r t        j                  d       g }Y w xY wc c}w # t        $ r(}|j                  |t+        |      d       Y d}~d}~ww xY w# t        $ r'}|j                  |t+        |      d       Y d}~d}~ww xY w# |j5                          w xY w)%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"``.

    Rows omit ``system_prompt``/``model_config`` unless ``full=1`` — same
    list projection as ``/api/sessions``.
    rl  r   rp  r   rq  rt  r   rn  r  allz0GET /api/profiles/sessions: list_profiles failedr?  rn  ro  Nr   r   rY  state.dbTdb_pathro  r  r  rs  	r   ri  rq  r  ru  rv  rw  rx  rr  )r   ri  ru  rv  rw  ry  r  r{  rt  ru  rv  rw  rz  rg  c                 T    | j                        xs | j                  d      xs dS )Nrv  r   r   )rx  sort_keys    rV   r  z'get_profiles_sessions.<locals>.<lambda>W  s"    aeeHoI|1DI rX   r  reverse)r  r  profile_totalsrq  r  r  )r7   rz  rn  r]   r  r}  r  list_profilesrS   r   r\   rQ   rK  get_profile_dirr\  r   r   r  r}  r   r{  r  r|  r  r   r   r   r  re  )#rq  r  rf  rg  rh  r  r   ri  rH  rn  profiles_modtargetsrS   r9  infosrR   ru  rw  rv  source_filterrx  r  per_profiler  r  r  r  ry  r  r  r  r"  profile_totalwindowr  s#                                     @rV   get_profiles_sessionsr    s   0 554eff))4[\\&3&(G7e#'0
dd|$	 ..0E:?@$		499-@G@ NNI|'C'CI'NOPA|,&M9, NdM / 52<<SAO!QWWYAOLO c%&.%0#6K#%FE%'N#%F
))+C /
dt*z)~~	 7d;B#	(($ , 4!"3!1+%*h%6!%X ) D ,,$ , 4"3!1+!% - M ]"E#0N4  !#)*.)*;&'EE*%- Squu]AEE,4JKKsR + !%QUU:%6 7*a ! HHJ_/b !& 1}|H
KKISWKXF6E>*F (( ] A 	NNMNG	 P&  	MMdSX>?	F  	@MMdSX>??	@ HHJsy   J 0JJ (J3>J3"
J8-C	K,6LJ  J0/J08	K)K$$K),	L5LLLLL1z/api/profiles/sessions/sidebarrecents_profilerecents_limitrecents_exclude
cron_limitmessaging_limitmessaging_excludec           
        ! ddl m} ddlm} 	 |j	                         }|D 	cg c]  }	|	j
                  |	j                  f }
}	|
s"|
j                  d|j                  d      f       | xs dj                         xs d}|xs dj                  d      D cg c]  }|j                         s| }}|xs dj                  d      D cg c]  }|j                         s| }}t        t        |d	      d
      }t        t        |d	      d
      }t        t        |d	      d
      }g }g }g }d}i }g }t!        j                          !dt"        t$        t&        t(        f      dt&        dt"        t$        t&        t(        f      f!fd}dddd}|
D ]  \  }}t+        |      dz  }|j-                         s%	  ||d      }	 |dk(  s||k(  rE|j/                   | ||||      |             |j1                  |xs dd	ddd      }||z  }|||<   |j/                   | ||d|      |             |j/                   | ||||      |             |j3                           dt"        t$        t&        t(        f      dt4        dt"        t$        t&        t(        f      fd}  | ||      ||dd | ||      i | ||      t7        |      d|d S c c}	w # t        $ r t        j                  d       g }
Y w xY wc c}w c c}w # t        $ r(}|j                  |t'        |      d       Y d}~d}~ww xY w# t        $ r(}|j                  |t'        |      d       Y d}~d}~ww xY w# |j3                          w xY w)!u  Batched sidebar session slices — one profile-DB open per refresh.

    The desktop sidebar needs three source-scoped windows per refresh: recents
    (local chats, scoped to the active profile), cron sessions (all profiles),
    and messaging-platform sessions (all profiles). Served as three separate
    ``/api/profiles/sessions`` calls they reopened every profile's ``state.db``
    three times and re-counted each refresh. This opens each DB once and runs
    the three filtered queries together, returning the three windows in one
    payload. Read-only and process-light, same row projection and 300s active
    heuristic as ``/api/profiles/sessions``.

    The caller passes the source taxonomy (``recents_exclude`` /
    ``messaging_exclude`` CSV, ``source=cron`` is implicit) so this stays
    taxonomy-agnostic like the per-slice endpoint. All three slices use
    ``min_messages=1`` / ``archived=exclude`` / recency order, matching the
    desktop's per-slice calls.
    r   r  r  z8GET /api/profiles/sessions/sidebar: list_profiles failedr?  r  r   r   r   rY  r"  rS   rK   c           	          | D ]l  }||d<   |dk(  |d<   |j                  d      d u xr( |j                  d|j                  dd            z
  dk  |d	<   t        |j                  d
            |d
<   n | S )Nr  r?  r{  rt  ru  rv  r   rw  rz  rg  )r   r   )r"  rS   rx  ry  s      rV   _tagz+get_profiles_sessions_sidebar.<locals>._tag  s     	4AAiL&*i&7A"#j!T) O155lA0FGG3N kN !z!23AjM	4 rX   N)r   rm  c                >    | j                  ||xs d |dddddd	      S )Nr   r   FTr  )r|  )r  r   rm  caps       rV   _slicez-get_profiles_sessions_sidebar.<locals>._slice  s9    $$#Ot"!% % 

 
	
rX   r  Tr  r  )rm  r  F)ri  ru  rv  rw  ry  rg  )r   r  r  c                 N    | j                  d d       | d | }t        |       |S )Nc                 R    | j                  d      xs | j                  d      xs dS )Nru  rv  r   r   rx  s    rV   r  z@get_profiles_sessions_sidebar.<locals>._window.<locals>.<lambda>  s"    m 4 Pl8K Pq rX   Tr  )r  re  )r"  r  wins      rV   _windowz.get_profiles_sessions_sidebar.<locals>._window  s,    		PZ^	_4Cj %
rX   )r  r  r  r  )r  r  )recentsrg  	messagingr  )rz  rn  r]   r  r  rS   r   r\   rQ   rK  r  r  r   r   r  r\  r}  r   r   r  r   r   r{  r  r  r   r  r   )"r  r  r  r  r  r  rn  r  r  rR   r  recents_scoperx  recents_exclude_listmessaging_exclude_listrecents_capcron_capmessaging_caprecents_rows	cron_rowsmessaging_rowsrecents_totalrecents_profile_totalsr  r  r  rS   r9  r  r  r  rtotalr  ry  s"                                    @rV   get_profiles_sessions_sidebarr  e  s   4 '3**,NS*TdDIItyy+A*T*T 	<#?#?	#JKL$-446?%M(7(=2'D'DS'IW!QWWYAWW*;*Ar)H)H)M[AQRQXQXQZa[[c-+S1K3z1%s+HOQ/5M)+L&(I+-NM-/#%F
))+C	4S#X' 	s 	tDcN7K 	 "4 
  
dt*z)~~	7d;B	%)>##,@kRTXY ))$8$@D&'%*"'%) *  '/5&t,T&F"I4PQ!!VB(>MRTXY HHJ=@d4S>*  d38n9M   k:"4

 WY9:>(
  k +U QR X[X  	MMdSX>?	*  	@MMdSX>??	@ HHJs}   K KK K=-K=
L L7
LBL;K  K:9K:	L8L33L8;	M,M'!M/'M,,M//Nz/api/sessions/searchqc                 v  K   | r| j                         sdg iS 	 t        |      	 t        dt        t	        |xs 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}j                  | d      D ]{  }|j                  d      }|j                  d      xs dj                         }|xs d| } |||d|j                  d      |j                  d      |j                  d      d       } ddl	}g }	 |j                  d| j                               D ]J  }
|
j                  d      s|
j                  d      r|	j                  |
       7|	j                  |
dz          L dj                  |	      }t        dz  d      }j                  ||      }|D ]p  }t!              k\  r n` ||d   |j                  d d      |j                  d!      |j                  d      |j                  d      |j                  d"      d       r dt#        j%                               ij'                          S # j'                          w xY w# t(        $ r  t*        $ r# t,        j/                  d#       t)        d$d%&      w xY w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      d   
session_idrK   c                 \   | s| S | v r|    S g }| }t               }| }|r||vr|j                  |       |j                  |       |v r|   }n	 j                  |      }|s|}nt        |t              r|j                  d      nd }|s|}nn	 j                  |      }|s|}nW|j                  d      }|j                  d      }	|j                  d      dk(  xr |d uxr |	d uxr |	|k\  }
|
s|}n	|}|r||vr|D ]  }||<   	 |S # t        $ r d }Y w xY w# t        $ r d }Y w xY w)Nparent_session_idrt  rv  
end_reasonr{  )r   r  r  get_sessionr\   r  r  r   )r  chaincurvisitedr<  rx  r  parent_sessionparent_ended_atrv  is_compression_edger  r  
root_caches               rV   compression_rootz)search_sessions.<locals>.compression_root  s   !%%+%j11 %!c0KK$LL%j()#!NN3/ ";Ea;NQUU#67TXF!".)+)? *"&4&8&8&DO!"|!4J&**<8MI :+47:&d2: '/9	 ( /" CG c0H " ,D'+Jt$,= % ! ! % .)-.s$   D D DDD+*D+root_idc                 r    | v r|    S | }	 j                  |       }|r|}|| <   |S # t        $ r Y w xY wr  )get_compression_tipr\   )r  tipr[  r  	tip_caches      rV   lineage_tipz$search_sessions.<locals>.lineage_tip6  s]    i'$W--!55g>H& &)	'"
 ! s   * 	66raw_sidr  Nc                     | sy  |       }|v st              k\  ry t        |      } |      |d<   ||d<   ||<   y )Nr  lineage_root)r   r  )r  r  r<  r  r  
safe_limitr  s      rV   add_lineage_resultz+search_sessions.<locals>.add_lineage_resultI  sS    '04<3t9
#:w-(3D(9%*.'$T
rX   T)rq  rv  r  previewr   zSession ID: r   r#  rv  )snippetroler   r#  session_startedr   z"[^"]*"|\S+"r   r  r   rp  )queryrq  r  r  r  zGET /api/sessions/search failedrY  zSearch failedr   )r   r~  r\  r  r  r  r  search_sessions_by_idr   r  findallr   r_  r  r  search_messagesr   r  r  r   r7   r\   rQ   rK  )r  rq  r  r  rowsidr  r  r  termsr   prefix_queryfetch_limitmatchesmr  r  r  r  r  r  r  s                  @@@@@@@rV   search_sessionsr    s     AGGI2SE)'2L	QC$4c :;J  "J/S /S /b !IS S " D	%C 	%$ 	%4 	% 	% //VZ/[ ggdm779-3::<!9|C5%9"#* $"%''("3!$!1+.77<+@		$ E#NAGGI> .##C(ENN3,?LL'LL-	.
 88E?L j1nb1K((|;(OG t9
*"lO#$55B#7 !f"#%%/!"w+,551B+C	 tDKKM23HHJBHHJ  E89ODDEs4   J9J H.I, J +J9,I>>J 5J66J9c                    t        |       } | j                  d      }t        |t               rP|j                  dd      }|j                  d|j                  dd            | d<   t        |t              r|nd| d<   | S 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?  rS   r   r(  )r  r   r  r  )r  	model_valctx_lens      rV   _normalize_config_for_webr    s     &\F

7#I)T"-- 0!4#--	9==3LMw4>w4LRS%& M *+%&MrX   fieldc                 8   | j                   | j                  | j                  | j                  | j                  | j
                  | j                  | j                  | j                  D cg c]&  }|j                  |j                  |j                  d( c}d	S c c}w )zBStatic, storage-independent shape of one field for the UI payload.r  rg  r"  )	r  rg  r  r"  rR   placeholderinliner  r  )
r  rg  r  r"  rR   r  r  r  r  r  )r  opts     rV   _provider_field_entryr    s~     yy

((

((,, }}
 ii#))COOT
 
s   '+B_UNSETr  c                    |xs dj                         }| j                  }|dk(  r;|s| j                  }|| j                         vrt	        d| j
                   d      |S |dk(  rddlm}  ||      S |dk(  r1|st        S 	 t        |      }|j                         rt        |      S |S |dk(  rO|st        S 	 t        j                  |      }t        |t         t"        f      st	        d| j
                   d      |S |r|S t        S # t        $ r}t	        d	| j
                   d      |d
}~ww xY w# t        t        f$ r}t	        d| j
                   d      |d
}~ww xY w)a  Coerce a submitted non-secret value to its native JSON type.

    Values arrive as strings over the API; this converts them to the type the
    Honcho resolver expects (bool/number/list/dict), so e.g. a boolean is stored
    as a JSON ``false`` rather than the string ``"false"`` (which would read as
    truthy). Returns ``_UNSET`` when the field should be removed. Raises
    ``ValueError`` on malformed input.
    r   r   Invalid value for ''r   r   is_truthy_valuer)  zInvalid number for 'Nrd  zInvalid JSON for 'z ' must be a JSON object or array)r   r  r?  allowed_valuesr  r  utilsr  r  r  
is_integerr  rd  re  r  r  r  r  )r  r  r  r  r  r)  r  r  s           rV   _coerce_field_valuer    st    YBE::DxMME,,..2599+Q?@@v~)u%%xM	K5\F %//1s6{=v=v~M	IZZ&F &4,/q+KLMM 5%v%!  	K3EII;a@AsJ	K I& 	I1%))A>?SH	Is0   D 8D3 	D0D++D03E!EE!c                     || j                   S | j                  dk(  rddlm}  ||      rdS dS | j                  dk(  r6t	        |t
        t        f      rt        j                  |      S t        |      S t        |      S )ue  Render a stored native value as the string the generic UI edits.

    ``None`` (key absent) yields the field's declared default. Bools become
    ``"true"``/``"false"``, JSON objects/arrays are re-encoded, numbers are
    stringified — so the renderer's per-kind controls always get the shape they
    expect regardless of how the value sits on disk.
    r   r   r  truefalserd  )
r?  r  r  r  r  r  r  rd  dumpsr  )r  r  r  s      rV   _serialize_field_valuer    sq     }}}zzV)(/v<W<zzVedD\*::e$$5zu:rX   c                 6    t               | j                  z  dz  S )Nconfig.json)r   rS   rU   s    rV   _flat_json_pathr  	  s    x}},}<<rX   c                    t        |       }|j                         si S 	 t        j                  |j	                  d            }t        |t              r|S i S # t
        $ r t        j                  d|d       i cY S w xY w)Nr  r  z-Failed to read memory provider config from %sTr:  )
r  r{  rd  re  	read_textr\   rQ   rS  r  r  )rU   r   r  s      rV   _read_flat_jsonr	    sx    8$D;;=	zz$..'.:; dD)41r1  DdUYZ	s   %A #A>=A>sourcesr  c                     |D ]4  }| j                   g| j                  D ]  }||v s||   ||   c c S  6 | j                  D ]  }|j                  |      }|s|c S  y)zReturn the stored native value from the first source holding it, or ``None``.

    Presence (``key in source``) decides, not truthiness, so a stored ``False``
    or ``0`` survives instead of being mistaken for "unset".
    N)r  aliasesenv_fallbacksr   )r  r
  r  r   
source_keyenv_keyr  s          rV   _read_fieldr    s      * 995u}}5 	*JV#z(:(Fj))	** &&  L rX   c                       j                   g j                  D ]  }|s|j                  |      s y t         fd|D              S )NTc              3   ~   K   | ]4  }j                   gj                  D ]  }|j                  |        6 y wr  )r  r  r   )r   r   r  r  s      rV   r   z)_declared_field_is_set.<locals>.<genexpr>/  s5     XUYY<W<WXqvzz!}X}X   :=)r  r  r   r  )r  r
  r  r  s   `   rV   _declared_field_is_setr  +  sH    MM8E$7$78 swww' X7XXXrX   c                       ddl m} m}m} ||| fS )z>Lazily import the Honcho plugin's resolvers (optional plugin).r   _host_blockresolve_active_hostresolve_config_path)plugins.memory.honcho.clientr  r  r  r  s      rV   _honcho_resolversr  5  s     cb 3[@@rX   c                  8   t               \  } }} |        } |       }i }|j                         r:	 t        j                  |j	                  d            }t        |t              r|ni }|| |||      fS # t        $ r t        j                  d|d       Y /w xY w)zJReturn (root config, active host key, host block) for the current profile.r  r  $Failed to read Honcho config from %sTr:  )
r  r{  rd  re  r  r  r  r\   rQ   rS  )r  r  host_block_ofr   r   r  loadeds          rV   _honcho_read_sourcesr   =  s     ?P>Q;,m D DC{{}	VZZ @AF&vt4&"C mC...  	VLL?PTLU	Vs   9A5 5!BBc                    g }t               }| j                  t        k(  }|r"t               \  }dt        dt
        ffd}nd}t        |       dt        dt
        ffd}| j                  D ]  }t        |      } ||      }|j                  r'd|d<   t        |||      |d<   |j                  |       It        |||      }	|r|j                  s|j                  dv r||d	<   t        ||	      }
|j                   d
k(  r|
|j#                         vr|j$                  }
|
|d<   |r|	d un
t'        |
      |d<   |j                  |        | j(                  | j*                  | j,                  |dS )Nr  rK   c                 0    | j                   dk(  rfS fS Nr   r  )r  
host_blockr  s    rV   sources_forz/_declared_provider_payload.<locals>.sources_forU  s     (-v(=J$IC6IrX   r   c                 
    fS r  r  )r  r  s    rV   r&  z/_declared_provider_payload.<locals>.sources_for[  s
    7NrX   r  is_set>   aiPeer	workspacer  r   rS   rg  docs_urlr  )r   storager*   r   r)   r  r	  r  r  	is_secretr  r  r  r  r  r  r  r  r?  r   rS   rg  r,  )rU   r  r  	is_honchor   r&  r  r  r
  nativer  r  r%  r  s              @@@rV   _declared_provider_payloadr1  M  sq   #%F
*C  $==I 4 6T:	J} 	J 	J x(	} 	 	  %e,e$??E'N4UGSIE(OMM% UGS1U..599@W3W#'E- &uf5::!e53G3G3I&IMMEg09&,tE{he+. MMHNNHYHYekllrX   r  c                 L   | j                   D ]  }|j                  s|j                  |vr ||      }t        |||j                           }|t        u r@|j                  |j                  d       |j                  D ]  }|j                  |d        |||j                  <    y)a  Apply submitted non-secret fields to their backend dict, in place.

    Only keys present in ``values`` are touched, so a partial save never
    clobbers fields owned by another surface. ``_UNSET`` clears the key (and
    its aliases) so it falls back to the host/default mapping.
    N)r  r.  r  r  r  rP  r  )rU   r  
target_forr  rj   coercedaliass          rV   _apply_field_valuesr6  x  s      
(??eiiv5E"%eVEII->?fJJuyy$' (

5$'( !(F599
(rX   c                    ddl m} t        |       | j                  D ]b  }|j                  s|j                  |j                        xs dj                         }|s@|j                  sMt        |j                  |       d t        | |fd       t        |       }|j                  j                  dd        ||d       y )	Nr   atomic_json_writer   c                     S r  r  )r  r  s    rV   r  z&_write_provider_flat.<locals>.<lambda>  s     rX   TrW    r  )r  r9  r	  r  r.  r   r  r   r  r   r6  r  r  rZ  )rU   r  r9  r  	submittedr   r  s         @rV   _write_provider_flatr>    s    'x(H 9??EII.4";;=IU]]u}}i8	9 &*@A8$DKKdT2dH51rX   c                 B   ddl m}m} ddlm} t               \  }}} |       } |       }	 ||	      5  i |	j                         r:	 t        j                  |	j                  d            }
t        |
t              r|
ni j                  d	      }t        |t              r|ni xd	<   } ||      r#t!        fd
|j#                         D        |      n|}|j%                  |      | j&                  D ]  }|j(                  s|j                  |j*                        xs dj-                         }|s@|j.                  rt1        |j.                  |       j                  |j*                        }t        |t2              r|j5                  |      r||j*                  <    t7        | |fd       |	j8                  j;                  dd        ||	d       ddd       y# t        $ r t        j                  d|	d       Y w xY w# 1 sw Y   yxY w)a+  Persist submitted fields to Honcho's real config for the active host.

    Only keys present in ``values`` are touched, so a partial save (e.g. the
    inline panel) never clobbers fields owned by the full-config editor. Blank
    text clears a key so it falls back to the host/default mapping.
    r   )ACCESS_TOKEN_PREFIX_config_refresh_lockr8  r  r  r  Tr:  hostsc              3   2   K   | ]  \  }}|u s|  y wr  r  )r   r  r  r  s      rV   r   z)_write_provider_honcho.<locals>.<genexpr>  s     Ftq!XFs   r   c                 *    | j                   dk(  rS S r#  r$  )r  r  r%  s    rV   r  z(_write_provider_honcho.<locals>.<lambda>  s    %++Y_J_J eh rX   rW  r;  r<  N)plugins.memory.honcho.oauthr@  rA  r  r9  r  r{  rd  re  r  r  r  r\   rQ   rS  r   r   r  r  r  r.  r  r   r  r   r  r   r6  r  rZ  )rU   r  r@  rA  r9  r  r  r  r   r   r  rB  host_keyr  r=  storedr  r  r%  s                   @@@rV   _write_provider_honchorH    s    V'>O>Q;,m D D 
d	#  1 ;;=ZDNNGN$DE *64 8fb  (25$(?uRGGu d+QY4Fu{{}FM_c%%h9
__ 	2E??EII.4";;=I}}u}}i8^^EII.Fvs+0A0ABU0V(1
599%	2 	Hf.hi$6$%0A 1  1  ZCTTXYZ 1  1s7   H
9G-DHA
H-!HHHHHc                 $   i }| j                         D ]z  \  }}|d||<   t        |t              r|||<   $t        |t              r
|rdnd||<   >t        |t        t
        f      rt        j                  |      ||<   mt        |      ||<   | |S )zGThe declared-schema path edits strings; the dashboard may send natives.r   r  r   )r  r  r  r   r  r  rd  r  )r  r;  r  r  s       rV   _stringify_submitted_valuesrJ    s     Clln 
"
U=CHs#CHt$!&vGCHd|,zz%(CH5zCH
" JrX   c                 2   | j                   t        k(  rt        | |       nt        | |       t	               }|j                  d      }t        |t              si }||d<   |j                  d      | j                  k7  r| j                  |d<   t        |       y y )Nrz  rU   )
r-  r*   rH  r>  r   r   r  r  rS   r   )rU   r  r  memory_configs       rV   _update_memory_provider_configrM    s    44x0Xv.]FJJx(MmT*(x$5$,MMj!F 6rX   c                 b    | j                  dd      j                  dd      j                         S )Nr  r  -)r  r   rS   s    rV   _memory_provider_labelrQ    s)    <<S!))#s399;;rX   c                 f    t        | xs d      j                         }|j                         dv ry|S )Nr   >   built-inr  builtin)r  r   r   )rS   rU   s     rV   _normalize_memory_provider_namerU    s0    4:2$$&H~~::OrX   c                 n    	 ddl m}  ||       S # t        $ r t        j	                  d| d       Y y w xY w)Nr   )load_memory_providerz!Failed to load memory provider %sTr:  )r  rW  r\   rQ   r@  )rS   rW  s     rV   _load_memory_providerrX     s:    7#D)) 

6t
Ls    !44c                 P   	 ddl m}  ||       }|i S |dz  }|j                         si S |j                  d      5 }t	        j
                  |      xs i }d d d        t        t              r|S i S # 1 sw Y   xY w# t        $ r t        j                  d| d       i cY S w xY w)	Nr   )find_provider_dirzplugin.yamlz	utf-8-sigr  z.Failed to read memory provider manifest for %sTr:  )r  rZ  r{  r  r  r  r  r  r\   rQ   r@  )rS   rZ  provider_dirmanifest_pathr  manifests         rV   _memory_provider_manifestr^  
  s    4(.I$}4##%I5 	4~~f-3H	4%h5x=2=	4 	4  

CTTX
Y	s8   A? A? A? A3A? 1A? 3A<8A? ?#B%$B%c                     t        | t              sg S | D cg c]5  }t        |      j                         st        |      j                         7 c}S c c}w r  )r  r  r  r   )r  r  s     rV   _string_listr`    s=    eT"	*/E$3t9??3DCIOOEEEs
   AAc                 (   t        |       }g }|j                  d      xs g D ]  }t        |t              st	        |j                  d      xs d      j                         t	        |j                  d      xs d      j                         t	        |j                  d      xs d      j                         d}|d   s|d   s|d   s|j                  |        t        |j                  d            |t        |j                  d            d	S )
Nexternal_dependenciesrS   r   installr&  )rS   rc  r&  pip_dependenciesrequires_env)rd  rb  required_env)r^  r   r  r  r  r   r  r`  )rS   r]  rb  r  deps        rV   _memory_provider_setup_manifestrh  "  s    (.H24||34: 	.#t$-2.4463779-34::<)/R0668

 v;#i.CL!((-	. )6H)IJ!6$X\\.%AB rX   c                 8    t        |       }t        |      |d<   |S )Ndependencies_installed)rh  '_memory_provider_dependencies_installed)rS   setups     rV   _memory_provider_setup_inform  7  s!    +D1E&Me&TE
"#LrX   r  mem0hindsight_client	hindsight)z	honcho-aimem0aizhindsight-clientzhindsight-allrg  c                 T    t        j                  d| d      d   j                         S )Nz
[\[<>=!~;]r   )maxsplitr   )r  r   r   )rg  s    rV   #_memory_provider_dependency_packagert  E  s#    88M33A6<<>>rX   c                 d    t        |       }t        j                  ||j                  dd            S )NrO  r  )rt  _MEMORY_PROVIDER_IMPORT_NAMESr   r  )rg  packages     rV   _memory_provider_import_namerx  I  s*    1#6G(,,Wgooc36OPPrX   c                 V    t        |       }|sy	 t        |       y# t        $ r Y yw xY w)NFT)rx  
__import__rb   )rg  import_names     rV   _dependency_importabler|  N  s4    .s3K; s    	((c                 P    t        | xs d      }t        |      |k  r|S |d |  dS )Nr   z
... truncated ...)r  r   )r  rq  r  s      rV   _trim_setup_outputr~  Y  s5    u{D
4yE6El^.//rX   c                  R   t         j                  j                         } t        j                         }|dz  dz  |dz  dz  |dz  dz  t        d      g}| j                  dd      }t         j                  j                  d |D              }|r|t         j                  z   |z   | d<   | S )	Nz.brv-clibinz.localz.npm-globalz/usr/local/binPATHr   c              3   T   K   | ]   }|j                         st        |       " y wr  )r{  r  )r   r   s     rV   r   z-_memory_provider_setup_env.<locals>.<genexpr>j  s     O4SYOs   (()rv   r   copyr   r9  r   pathsepr  )r  r9  
extra_binsexisting_pathr  s        rV   _memory_provider_setup_envr  `  s    
**//
C99;DzE!x%}u$	J GGFB'MZZ__O:OOFrzz)M9FJrX   )r  	completedr  rc  r  r  r  c           
          | ||||d n|j                   |dnt        |j                        t        |xs |d      dS |j                        dS )Nr   )r  rS   rc  r  r0  r1  r  )r0  r~  r1  r  )r  rS   rc  r  r  r  s         rV   _command_resultr  p  sl     '/dY5I5I!)"/A)BRBR/S$U%]Y5Fr^  MVL\L\^ rX      )r  r[  r[  c          
      T    t        j                  | ||rdnd t               dd|d      S )Nz	/bin/bashTF)r  r  r  r%  r  r[  r&  )r.  r/  r  )r  rl  r  r[  s       rV   _run_setup_commandr    s4     >>"';T&(	 	rX   rl  c                    t        | j                  d            }| j                  d      xs g }t        d |D              }d}|D ]  }t        |t              st        |j                  d      xs d      j                         }t        |j                  d      xs d      j                         }|s|rd}s	 t        t        j                  |      |d	
      }|j                  dk7  sd} |xr |S # t        $ r d}Y w xY w)Nrd  rb  c              3   2   K   | ]  }t        |        y wr  )r|  )r   rg  s     rV   r   z:_memory_provider_dependencies_installed.<locals>.<genexpr>  s     I',Is   Tr&  r   rc  Fr  rl  r[  r   )r`  r   r  r  r  r  r   r  shlexr   r\   r0  )	rl  rd  rb  pip_okexternal_okrg  	check_cmdinstall_cmdr  s	            rV   rk  rk    s
   #EII.@$AB!II&=>D"I8HIIFK$  #t$(.B/557	#''),2399;#	*I&!I 1$K' * !k!  	K	s   :!C55DDdependenciesc           
         | D cg c]  }t        |      r| }}| sg S |st        ddj                  |       d      gS t        j                  d      }|r>|dddt
        j                  dg|}d	t
        j                   d
d
j                  |       }n;t
        j                  ddddg|}t
        j                   dd
j                  |       }	 t        ||d      }t        ddj                  |      |j                  dk(  rdnd||      gS c c}w # t        $ r3}t        ddj                  |      d|t        |            gcY d }~S d }~ww xY w)Nr)  , already_installedr  rS   rc  uvrc  z--pythonz--quietzuv pip install --python r  r  z -m pip install    r  failedr  rS   rc  r  r  r   	installedr  rS   rc  r  r  )r|  r  r  r  whichr,  r  r  r\   r  r0  )r  rg  missinguv_pathr  rl  r  r  s           rV   )_install_memory_provider_pip_dependenciesr    se   *Ns2H2MsNGN	TYY|-DM`a
 	
 ll4 G	:s~~yc[bc,S^^,<Achhw>O=PQ>>4	9OwO^^$$4SXXg5F4GH
&wM	 	7#"+"6"6!";;	
 ; O$  	
YYw'#h
 	
	
s(   DDD 	E(EEEc                 `   g }| D ]e  }|j                  d      xs d}|j                  d      xs d}|j                  d      xs d}|rw	 t        t        j                  |      |d      }|j                  dk(  r |j                  t        d	|d
||             |j                  t        d	||rdnd||             |s|s	 t        ||dd      }|j                  t        d||j                  dk(  rdnd||             |s|j                  dk(  s	 t        t        j                  |      |d      }	|j                  t        d	||	j                  dk(  rdnd||	             h |S # t        $ r6}|j                  t        d	||rdnd|t        |                   Y d }~d }~ww xY w# t        $ r3}|j                  t        d|d|t        |                   Y d }~d }~ww xY w# t        $ r3}|j                  t        d	|d|t        |                   Y d }~#d }~ww xY w)NrS   
dependencyr&  r   rc  r  r  r   external_checkr  r  r  r  r  Trw  )rl  r  r[  external_installr  verified)	r   r  r  r   r0  r  r  r\   r  )
r  r  rg  rS   r  r  r&  r  rc  
post_checks
             rV   ._install_memory_provider_external_dependenciesr    s_    %'G fwwv.,GGG$*	ggi(.B$*KK	*%  ##q(NN'!1!%#6$-&+ #-!,7yX )"' ,'	$ NN+*1*<*<*A;x'% W//14!3I. ) ""J
 NN'!1!%1;1F1F!1K:QY$-&0ifP Ny  	#-!,7yX )!#h 	V  
#/!' +!#h 
L ! 	NN'!1!%#+$-"%c( 	sI   !E0F2AG10	F/9,F**F/2	G.;(G))G.1	H-:(H((H-c                    t        |       }t        |       }||st        dd|        t        |       }g }|j	                  t        |d                |j	                  t        |d                |s|j                  t        d| d             t        d	 |D              }t               D ci c]  }|d
   |
 }}|| ||j                  |       dS c c}w )Nr   Unknown memory provider: r   rd  rb  rl  no_declared_stepsr  c              3   *   K   | ]  }|d    dv  yw)rc  >   r  Nr  )r   r3  s     rV   r   z1_install_memory_provider_setup.<locals>.<genexpr>c  s     FFVHZ/Fs   rS   )r  rU   r  rc  )rX  r^  r7   rh  r  r  r  r  r  r  "_discover_memory_provider_statusesr   )rS   rU   r]  rl  r  r  r  statusess           rV   _install_memory_provider_setupr  M  s    $T*H(.H6OPTv4VWW+D1EGNN<UCU=VWXNN6u=T7UV *	
 
FgF	FB,N,PQSFS QHQ,,t$	  Rs   /Cc                    g }|Mt        |d      rA	 |j                         }t        |t              r |D cg c]  }t        |t              s| }}g }|D ]I  }t        |j                  d      xs d      j                         }|s3|j                  d      xs |j                  d      xs g }t        |t              sg }t        |j                  d	      xs |j                  d
      xs d      j                         j                         }|j                  d      rd}	n-|rd}	n(|dv st        |j                  d      t              rd}	nd}	g }
|D ]"  }t        |      }|
j                  ||dd       $ t        |j                  d      xs d      }|j                  |t        |j                  d      xs  |j                  dd      j                               |	|t        |j                  d      xs d      t        |j                  dd            |j                  dd      |
t        |j                  d      xs d      t        |j                  d      t              r|j                  d      nd t        |j                  d      xs d      xs d d       L |S c c}w # t
        $ r t        j                  d| d       Y |w xY w)Nget_config_schemaz,Failed to read memory provider schema for %sTr:  r  r   choicesr  r  r!  secretr   >   r   r`  r?  r`  r  r  r"  rg  r  r  r  requiredFr   whenenv_var)r  rg  r  r"  r  r  r?  r  r   r  _env_key)hasattrr  r  r  r  r\   rQ   rS  r  r   r   r   r   r  r  r   )rS   rU   
raw_schemar  r  r  r  r  explicit_kindr  r  choicer  r"  s                 rV   !_normalize_memory_provider_schemar  m  s~   ')J2E F	^,,.C#t$14P
5$8OeP
P $&F %#''%.&B'--/'')$@	(:@b'4(GCGGFODswwvD"EKKMSSU778DD11Z	@RTX5YDD 	PFKENNUU2NO	P #''-06B7)JS[[c-B-H-H-JK&sww}5;<SWWZ78wwy"-swwu~+,'1#''&/4'HCGGFOdCGGI.4"5=
 	3%N MY Q 	^LLGX\L]	^s(   $J* J%J%J* %J* *!KKc                     | j                         si S 	 t        j                  | j                  d            }t        |t              r|S i S # t        $ r t
        j                  d| d       i cY S w xY w)Nr  r  z"Failed to read JSON config from %sTr:  )	r{  rd  re  r  r\   rQ   r@  r  r  )r   r  s     rV   _read_json_filer    sm    ;;=	zz$..'.:; dD)41r1  

7
M	s   %A #A32A3c                    t               }i }||  dz  || z  dz  fD ]  }|j                  t        |              	 t               }t        |t              r|j                  d      ni }t        |t              rY|j                  |       }t        |t              r|j                  |       |j                  d      }t        |t              ri ||}t        |t              r|j                  d      ni }| dk(  rBt        |t              r2|j                  d      }	t        |	t              r|j                  |	       |S # t        $ r i }Y w xY w)zIBest-effort read of existing provider config across legacy/native stores.r  r  rz  provider_configr  holographiczhermes-memory-store)r   r  r  r   r\   r  r  r   )
rS   r  r  r   r  
memory_cfgprovider_cfg
legacy_cfgplugins_cfgholographic_cfgs
             rV   %_read_memory_provider_existing_valuesr    s;    "#KF 	en$d]* - 	od+,	-m '1d&;"J*d#!~~d+lD)MM,'^^$56
j$'-
-f-F )33(=#'')$2K}K!>%//*?@ot,MM/*M'  s   
D? ?EEr  c                     | syt               }t        |j                  |       xs# t        j                  j                  |       xs d      S Nr   )r   r  r   rv   r   )r  env_on_disks     rV   _env_lookupr    s:    *K{w'H2::>>'+BHbIIrX   r?  c                    t        | t              r| S | | dk(  r|S t        | t        t        f      rt        |       S t	        |       j                         j                         }|dv ry|dv ryt        d|        )Nr   >   rh   onyesr  T>   r  norI  r   FzInvalid boolean value: )r  r   r  r  r  r   r   r  )r  r?  r  s      rV   _coerce_boolr    s    %}%#u&E{u:##%D))**
.ug6
77rX   c                 T    | j                  dd      }| d   dk(  rt        |d      S |S )Nr?  r   r  r`  Fr  )r   r  )r  r?  s     rV   _field_defaultr    s0    ii	2&GV}	!GU33NrX   c                    | d   dk(  ry|j                  | d         }|dv rt        | j                  d            }|dv rt        |       }| d   dk(  rG| j                  dg       D ch c]  }|d	   	 }}t        |      }||v r|S t        t        |             S | d   d
k(  r!t	        |t	        t        |       d            S t        |      S c c}w )Nr  r  r   r  r  r  r   r  r  r`  Fr  )r   r  r  r  r  )r  r  r  r  alloweds        rV   _field_valuer    s    V} HHU5\"E
EIIj12
u%V} +099Y+CDC3w<DDE
(uHc.2G.HHV}	!E<u8MW\+]^^u: Es   'Cc                     | d   dk(  r9t        t        | j                  d            xs |j                  | d               S t        | |      }|dvS )Nr  r  r  r  r  )r   r  r   r  )r  r  r  s      rV   _field_is_setr    sP    V} K		* 56P$((5<:PQQ%E
""rX   fields_by_keyc                 &   | j                  d      }t        |t              r|sy|j                         D ]Z  \  }}|xs i j                  t	        |            xs t	        |      ddd d}t        ||      }t	        |      t	        |      k7  sZ y y)Nr  Tr  r   )r  r  r?  r  F)r   r  r  r  r  r  )r  r  r  r  dep_keyr   	dep_fieldactuals           rV   _field_visibler  	  s    
 99VDdD!!ZZ\ 	"(b--c'l; 
w<	@
	 i.v;#h-'	 rX   c                     | d   | d   | d   | d   | d   | d   | d   dk(  rdnt        | |      t        | |      | j                  d	g       | j                  d
d      | j                  d      d}|S )Nr  rg  r  r"  r  r  r  r   r  r   r  )r  rg  r  r"  r  r  r  r(  r  r   r  )r  r  r   )r  r  r  s      rV   _public_memory_provider_fieldr    s    U|wf]+]+*%V}0l5$6Ot,99Y+yy#		&!E LrX   c                     t        |       }t        | |      D cg c]  }t        ||       }}| t        |       |t	        |       dS c c}w )NrS   rg  r  rl  )r  r  r  rQ  rm  )rS   rU   r  r  r  s        rV   _memory_provider_payloadr  /  s`    06D 7tXF 	&eT2F 
 '-,T2	 	s   Ac                 f   | d   dk(  r!t        |t        t        |       d            S t        ||nd      j                         }| d   dk(  rP|st        t        |             }| j	                  dg       D ch c]  }|d   	 }}||vrt        d	| d
    d      |S |xs t        |       S c c}w )Nr  r`  Fr  r   r   r  r  r  r  r  )r  r  r  r   r   r  )r  r  r  r  r  s        rV   _coerce_schema_fieldr  =  s    V}	!CnU6KUZ)[\\sB/557EV} u-.E+099Y+CDC3w<DD25<.BCC)N5)) Es   9B.c                    |Xt        |d      rL	 ddlm} t        |      j                  |j                  ur$|j	                  |t        t                            y t               }|j                  d      }t        |t              si }||d<   |j                  |       }t        |t              si }|j                  |       ||| <   t	        |       y # t        $ r& |j	                  |t        t                            Y y w xY w)Nr   r   )MemoryProviderrz  )r  agent.memory_providerr  r\   r   r  r   r!  r   r   r  r  r  )rS   rU   r  _BaseMemoryProviderr  r  r  s          rV   #_save_memory_provider_native_configr  M  s    - @	S >%%-@-L-LL  _->)?@
-C"Jj$'
"HnnT"Ggt$NN6Jt#  	  _->)?@	s   C ,D Dc                     t        |       t        | |      }|D ci c]  }|d   |
 }}|D cg c]  }t        ||      s| }}|D cg c]  }|j                  d      s| }}|syt	        fd|D              S c c}w c c}w c c}w )Nr  r  Tc              3   6   K   | ]  }t        |        y wr  )r  )r   r  r  s     rV   r   z1_memory_provider_is_configured.<locals>.<genexpr>o  s     Ge}UD)Gs   )r  r  r  r   r  )rS   rU   r  r  r  visible_fieldsrequired_fieldsr  s          @rV   _memory_provider_is_configuredr  e  s    06D.tX>F6<=UU5\5(=M=!^E4%ON  +9REIIj<QuRORGGGG > Ss   BBBB$Bc            
         i } 	 ddl m}  |       D ]8  \  }}}t        |      t        |xs d      t        |      dd| t        |      <   : 	 t               }d}|j                  d      }t        |t              rt        |j                  d            }|r|| vr
|d	dd
d| |<   g }t        |       D ]  }| |   }	|	d   rd n
t        |      }
t        |      }|	d   rdnt        ||
      }|	d   rg nt!        ||
      }|	d   rd}n3|	d   s|j                  dd
      sd}n|sd}n|	d   s|rd}n
|	d   sd}nd}|j#                  ||	d   |	d   |||d        |S # t        $ r t
        j                  d       Y -w xY w)Nr   )discover_memory_providersr   F)rS   r"  r	  r  z discover_memory_providers failedrz  rU   z"Configured provider was not found.Tr  r	  rj  unavailableneeds_configreadyr"  )rS   r"  r	  
configuredrc  rl  )r  r  r  r   r\   rQ   rK  r   r   r  r  rU  sortedrX  rm  r  r  r  )
discoveredr  rS   r"  r	  r  r  memr  r  rU   rl  r  schema_fieldsrc  s                  rV   r  r  r  s   ,.J;<,E,G 	(D+yD	";#4"5!)_ 	%Js4y!	 -CF
''(
C#t01DE&
*?	

6 ')Iz" y>4/DT/J+D1!)nU2PQUW_2`
!)n2STXZb2cy>F[!%))4Ld*S"F#F[!m#F[!"FF}-[)$
 	%4 W  ;9:;s   AE E;:E;c           
          | sy t               D ci c]  }|d   |
 }}|j                  |       }|t        dd|  d      |d   dk7  r't        dd|  d	|d   j                  d
d       d      y c c}w )NrS   r   zUnknown memory provider ''.r   rc  r  zMemory provider 'z' is not ready (r  r  z'). Configure it in the dashboard first.)r  r   r7   r  )rS   r  r  s      rV   _require_memory_provider_readyr    s    ,N,PQSFS QHQ
,,t
C
{.tfB7
 	
 8}#D6 *M))#s344[]
 	
   Rs   A3c                 6   t        |       }t        | |      }|D ci c]  }|d   |
 }}i }i }|D ]  }t        |i |||      s|d   dk(  rTt        |j	                  |d         xs d      j                         }	|	r"|j	                  d      r|	|t        |d         <   q|d   |v r||d      n|j	                  |d   t        |            }
t        ||
      ||d   <    t        | ||        |j                         D ]  \  }}t        ||        y c c}w )Nr  r  r  r   r  )r  r  r  r  r   r   r  r  r  r  r   )rS   rU   r  r  r  r  r  config_valuesr  r=  r  r  r  s                rV   $_write_memory_provider_config_valuesr    sH   
 5T:H.tX>F6<=UU5\5(=M=$&M G Ge%B%BM%BMR=H$FJJuU|4:;AACIUYYz22;E*-./ U|v% 5< eElN5,AB 	
 ';5#&FeEl#G" (hF(7==? (w'(/ >s   Dz ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$c                 V    t         j                  | xs d      st        dd|        y)a]  Reject provider names that could traverse outside the plugin dirs.

    ``name`` is interpolated into filesystem paths by ``find_provider_dir()``
    and gates which plugin manifest's setup commands run. A strict charset
    allowlist (no path separators, no dots) makes traversal impossible
    regardless of how the downstream lookup evolves.
    r   r   r  r   N)_MEMORY_PROVIDER_NAME_REr  r7   rP  s    rV   #_require_valid_memory_provider_namer    s1     $--djb96OPTv4VWW :rX   z#/api/memory/providers/{name}/configsurfacec                 r    K   t                 fd}t        j                  |       d {   S 7 w)Nc                     t              5  dk(  r1t              } | dg dcd d d        S t        |       cd d d        S t              }|g t	              dcd d d        S t        |      cd d d        S # 1 sw Y   y xY w)Ndeclaredr   r+  r  )r  r+   r1  rX  rm  r  )r	  rU   rS   r  r  s     rV   _runz(get_memory_provider_config.<locals>._run  s    G$ 	<*$5d;# %)4RSUV	< 	< 2(;	< 	< -T2H !%trLghlLmn	< 	< ,D(;	< 	< 	<s   A?
A?A?*A??B)r  ro   r  )rS   r  r  r
  s   ``` rV   get_memory_provider_configr    s-     '-<" ""4((((s   +757z"/api/memory/providers/{name}/setupc                   K   t        |        t        |       }|t        |       st        dd|        |/|j                  r#	 t        | ||j                         t        |       S t        |       S # t        $ r}t        dt        |            |d }~wt        $ r$ t        j                  d|        t        dd      w xY ww)Nr   r  r   r   z5Failed to persist memory provider setup values for %srY  r|  )r  rX  r^  r7   r  r  r  r  r\   rQ   rK  r  )rS   rj  rU   r  s       rV   setup_memory_providerr    s     '-$T*H 9$ ?
 6OPTv4VWW	Q0xM *$//)$//  	KCCAsJ 	QNNRTXYC8OPP	Qs*   ACA2 C2	C;B0CCc                 P   K   t                |j                  xs i  fd}	 t        j                  |       d {   S 7 # t        $ r  t
        $ r}t	        dt        |            |d }~wt        $ r$ t        j                  d        t	        dd      w xY ww)Nc                     t              5  dk(  r?t              } | t        dd       t        | t	                     ddicd d d        S t              }|t        dd       t        |       t               t               }|j                  d      }t        |t              si }||d<   |d<   t        |       dd	cd d d        S # 1 sw Y   y xY w)
Nr	  r   r  r   r  Trz  rU   r  r  )r  r+   r7   rM  rJ  rX  r  r  r   r   r  r  r   )r	  rU   r  rL  rS   r  r  r  s       rV   r
  z+update_memory_provider_config.<locals>._run$  s    G$ 	0*$5d;#'CB[\`[a@bcc.x9TU[9\]d|	0 	0 -T2H#>WX\W]<^__0xH*40 ]F"JJx0MmT2 "#0x (,M*%$/)	0 	0 	0s   ;CA;CC r   r   z*PUT /api/memory/providers/%s/config failedrY  r|  )
r  r  ro   r  r7   r  r  r\   rQ   rK  )rS   rj  r  r  r
  r  r  s   ` ``  @rV   update_memory_provider_configr    s      (-[[BF0.M&&t,,,,  GCH=3F MCTJ4KLLMs:   $B&A
 AA
 B&A
 
B#A330B##B&z/api/configc                    K   t        |       5  t        t                     }d d d        j                         D ci c]  \  }}|j	                  d      r|| c}}S # 1 sw Y   ?xY wc c}}w wNr  )r  r  r   r  r   )r  r  r  r  s       rV   
get_configr  F  s`     		  :*;=9: $\\^ETQ1<<3DAqDEE: : Fs,   A2A A2A,A,A2 A)%A2z/api/config/defaultsc                     K   t         S wr  )r   r  rX   rV   get_defaultsr  N  s     s   	z/api/config/schemac                 p   K   t        |       5  t               }d d d        t        dS # 1 sw Y   xY ww)N)r  category_order)r  r  _CATEGORY_ORDER)r  r  s     rV   
get_schemar  S  s7     
 
w	' 7467@@7 7s   6*636r#  rU   auto_context_lengthconfig_context_lengtheffective_context_lengthcapabilities_EMPTY_MODEL_INFOz/api/model/infoc                    	 t        |       5  t               }ddd       j                  dd      }t        |t              rX|j                  d|j                  dd            }|j                  dd      }|j                  dd      }|j                  d      }n|rt        |      nd}d}d}d}|st	        t        |	      S 	 d
dlm}  ||||d      }d
}	t        |t              r|d
kD  r|}	|	d
kD  r|	n|}
i }	 d
dlm}  |||      }|E|j                  |j                  |j                  |j                   |j"                  |j$                  d}||||	|
|dS # 1 sw Y   DxY w# t        $ r d
}Y w xY w# t        $ r Y 2w xY w# t&        $ r  t        $ r' t(        j+                  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?  rS   rU   r  r  r  r   )get_model_context_length)r#  r  rU   r  get_model_capabilitiesrU   r#  supports_toolssupports_visionsupports_reasoningcontext_windowmax_output_tokensmodel_familyr  zGET /api/model/info failed)r  r   r   r  r  r  r   agent.model_metadatar"  r\   r  agent.models_devr$  r'  r(  r)  r*  r+  r,  r7   rQ   rK  )r  r  rK  
model_namerU   r  
config_ctxr"  auto_ctxconfig_ctx_inteffective_ctxcapsr$  mcs                 rV   get_model_infor6  g  s   G'G$ 	 -C	 GGGR(	 i&"y)--2KLJ }}Z4H }}Z4H"'78J+4Y"JHHJ)H==		E/ !!&*	H j#&:>'N +91*<( 	?'LB~&(&7&7')'9'9*,*?*?&(&7&7)+)=)=$&OO   #+%3(5 
 	
q	  	 :  	H	0  		   	 '34%&&'sq   F EB)F E' $F :AE8 F E$F 'E52F 4E55F 8	FF FF 6G ?G )visionweb_extractr{  
skills_hubapprovalru  title_generationtriage_specifierkanban_decomposerprofile_describerr)  _AUX_TASK_SLOTSz/api/model/optionsrefreshinclude_unconfiguredexplicit_onlyc                 P   	 ddl m}m} t        |       5   | |       t	        |      t	        |      ddddt	        |      t	        |      t	        |       
      cddd       S # 1 sw Y   yxY w# t
        $ r  t        $ r# t        j                  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)	rB  rA  picker_hintscanonical_orderpricingr  r@  probe_custom_providersprobe_current_custom_providerNzGET /api/model/options failedrY  zFailed to list model optionsr   )	hermes_cli.inventoryrE  rF  r  r   r7   r\   rQ   rK  )r  r@  rA  rB  rE  rF  s         rV   get_model_optionsrM    s    ,TR G$ 	'#%"=1%)*>%?! $!W'+G}26w-.?	 	 	   T674RSSTs)   A0 AA$	A0 $A-)A0 -A0 05B%z/api/model/recommended-defaultc                    | xs dj                         j                         }|dk(  r	 ddlm}m}m}m}m}m}m	} ddl
m}	  |       }
 |d      xs i } |d      }d}	  |	d      xs i }|j                  dd      xs d}|r ||
||      \  }
} ||
|d	      \  }
}n ||
||      \  }
} ||
d
      }d|t        |      dS 	 ddlm}m} ddlm}  | |             }|j                  dg       D ]i  }t)        |j                  dd            j                         |k(  s0|j                  d      xs g D cg c]  }t)        |       }}| |||
      ddc S  |dddS # t        $ r d}Y w xY w# t        $ r t        j!                  d       ddddcY S w xY wc c}w # t        $ r t        j!                  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pick_silent_default_model&union_with_portal_free_recommendations&union_with_portal_paid_recommendations)get_provider_auth_stateTforce_freshri  )	free_tierr  )rU   r#  rZ  z0GET /api/model/recommended-default (nous) failedNrD  )rT  r  slugmodelsz)GET /api/model/recommended-default failed)r   r   r=  rP  rQ  rR  rS  rT  rU  rV  r  rW  r   r\   r   rQ   rK  rL  rE  rF  r  )rU   r[  rP  rQ  rR  rS  rT  rU  rV  rW  	model_idsrI  rZ  rk  rm   _unavailabler#  rE  rF  r  r  r  r\  s                          rV   get_recommended_default_modelr_    s    N!!#))+Dv~'	H   @24I.v6<"G,>IJ /7=2"YY'8"=C
 %Kw
&"	7 +Hw$+'	< &Lw
&"	7 .i&IE &T)_UUBR?&':'<=;;{B/ 	xC37762&'--/47+.778+<+BDQ#a&DD$(3LV^b3crvww	x !2DAAE   
 "  	HNNMN &$GG	H E  BBC 2DAABsg   6E7  "E& AE7 AF& $F& ;F!F& F& &E41E7 3E44E7 7$FF!F& &$GGz/api/model/auxiliaryc                 ~   	 t        |       5  t               }ddd       j                  di       }t        |t              si }g }t
        D ]  }t        |j                  |      t              r|j                  |i       ni }|j                  |t        |j                  dd      xs d      t        |j                  dd      xs d      t        |j                  dd      xs d      d        |j                  di       }t        |t              rPt        |j                  dd      xs d      t        |j                  d	|j                  d
d            xs d      d}nd|rt        |      ndd}||dS # 1 sw Y   dxY w# t        $ r  t        $ r# t        j                  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  rU   rV  r#  r   r  )r  rU   r#  r  r?  rS   r%  )tasksmainzGET /api/model/auxiliary failedrY  zFailed to read auxiliary configr   )r  r   r   r  r  r?  r  r  r7   r\   rQ   rK  )r  r  aux_cfgra  slotslot_cfgrK  rb  s           rV   get_auxiliary_modelsrf  N  s   "WG$ 	 -C	 ''+r*'4(G# 	D0:7;;t;Ld0Sw{{4,Y[HLLZ @ JFKX\\'26<"=Z < BC	 	 GGGR(	i&	j" = CDY]]9immFB6OPVTVWD
 !#yS^bQD--3	  	 4   W894UVVWs"   F E:E!F :F?F 5F<z/api/model/moac                 (   	 ddl m} t        |       5  t               } |t	        |t
              r|j                  d      ni       cddd       S # 1 sw Y   yxY w# t        $ r  t        $ r# t        j                  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 failedrY  zFailed to read MoA configr   )hermes_cli.moa_configrh  r  r   r  r  r   r7   r\   rQ   rK  )r  rh  r  s      rV   get_moa_modelsrk    s    
Q>G$ 	Y-C'*S$:OUWX	Y 	Y 	Y   Q234OPPQs(   A 3A	A AA A 5Bc                 Z   	 ddl m}m} dt        dt        fddt
        dt        ffd}t        | j                  xs |      5  t               }| j                  rM| j                  | j                  | j                  j                         D ci c]  \  }}| ||       c}}d}ni |t        | j                  | j                  | j                  | j                   | j"                  | j$                  | j&                  | j(                  	            } ||      }	|	rt+        d
ddj-                  |	      z          ||      }
|
|d<   t/        |       ddi|
cddd       S c c}}w # 1 sw Y   yxY w# t*        $ r  t0        $ r# t2        j5                  d       t+        dd      w xY w)z3Persist the Mixture-of-Agents provider/model slots.r   )rh  validate_moa_payloadrd  rK   c                 x    | j                         j                         D ci c]  \  }}|	|| c}}S c c}}w r  )r  r  )rd  r  r  s      rV   
_slot_dictz"set_moa_models.<locals>._slot_dict  s1    %)YY[%6%6%8JTQAMAqDJJJs   
66presetc           	          | j                   D cg c]
  } |       c} | j                        | j                  | j                  | j                  | j
                  | j                  | j                  dS c c}w )Nr$  r%  r&  r'  r)  r*  r+  r  rr  )rp  rd  ro  s     rV   _preset_dictz$set_moa_models.<locals>._preset_dict  si    BHBYBY$Z$Z%5$Z():):;)/)E)E*0*G*G$//(.(C(C --!>>	 	$Zs   A7)r.  r/  r0  rr    zInvalid MoA config: ; r   ri  r  TNzPUT /api/model/moa failedrY  zFailed to save MoA config)rj  rh  rm  r   r  r#  r  r  r   r0  r.  r/  r  r$  r%  r&  r'  r)  r*  r+  r  r7   r  r   r\   rQ   rK  )rj  r  rh  rm  rs  r  rS   rp  r  problemsr  ro  s              @rV   set_moa_modelsrw    s   >QT	K\ 	Kd 	K
	!1 
	d 
	 DLL3G4 &	.-C||&*&9&9%)%7%7OS||OaOaOcd|tVl6&: :d #$)-)>)>#'??.2.H.H/3/J/J#'??-1-F-F#{{ $	& ,C0H# #1DIIh4GG 
 .c2J#CJ$-*-M&	. &	.  e&	. &	.N   Q234OPPQs>   AE5 A
E)E##B6E)	E5 #E))E2.E5 2E5 55F*z/api/model/setc                 `   	
K    j                   xs dj                         j                         	 j                  xs dj                          j                  xs dj                          j
                  xs dj                         j                         
 j                  xs dj                          j                  xs dj                         	dvrt        dd      	 rI j                  s=	 ddl
m} t        j                  |       d	{   }|d
	d|j                  dS  	
fd}t        j                  |       d	{   S 7 B# t        $ r d	}Y Mw xY w7 # t        $ r  t        $ r# t         j#                  d       t        dd      w xY w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   >   rb  r  r   z#scope must be 'main' or 'auxiliary'r   r   )expensive_model_warning)rU   r  NFT)r  r  rU   r#  confirm_requiredconfirm_messagec            	          t        j                  xs       5  t               cd d d        S # 1 sw Y   y xY wr  )r  r  _apply_model_assignment_sync)r  r  rj  r#  r  rU   r  r  s   rV   _apply_assignmentz/set_model_assignment.<locals>._apply_assignment  s>     78 38UD(G  s   5>zPOST /api/model/set failedrY  zFailed to save model assignment)r  r   r   rU   r#  r  r  r  r7   r  hermes_cli.model_cost_guardry  ro   r  r\   r4  rQ   rK  )rj  r  ry  rS  r~  r  r  r#  rU   r  r  s   ``   @@@@@@rV   set_model_assignmentr    s     ZZ2$$&,,.E#**,HZZ2$$&EIIO""$**,D#**,H||!r((*G))4YZZ(W
 55O !( 1 1+%%	!  "" ("(,'. 	 	 &&'89991  $ :  W344UVVWsl   C"F.-E6 <"E# E!E# #E6 7F.8$E6 E4E6  F.!E# #E1.E6 0E11E6 65F++F.r  c                 >	   t               }| dk(  r|r|st        dd      t        ||      \  }}|j                  d      }t	        |t
              r|j                  |      nd}|sMt	        |t
              r=|j                  d      r,t        |j                  d      xs d      j                         }t        |j                  d	i       ||||      }	|s)t	        |t
              r|j                  d
      r|d
   |	d
<   |	|d	<   g }
|j                         j                         dk(  r.	 ddl
m} ddlm}  ||dd      } |||d      }t        |      }
t%        |       |j                         j                         dv r|r	 ddlm}m}  |||| ||             |j                         j                         }g }|j                  di       }t	        |t
              rt,        D ]  }|j                  |      }t	        |t
              s%t        |j                  dd      xs d      j                         }|sU|j                         dvsh|j                         |k7  s||j/                  ||t        |j                  d	d      xs d      d        dd|||	j                  dd      |
|dS |j                  d      }t	        |t
              si }|dk(  rpt,        D ]Q  }|j                  |      }t	        |t
              si }d |d<   d|d	<   |j1                  dd       t3        |       |||<   S ||d<   t%        |       dddd!S |st        dd"      |r|gnt5        t,              }|D ]  }|t,        vrt        dd#|       |j                  |      }t	        |t
              si }t        |j                  d      xs d      j                         j                         }|j                         j                         }||d<   ||d	<   ||k7  r"|d$k7  r|j1                  dd       t3        |       |||<    ||d<   t%        |       dd|||d%S # t        $ r t         j#                  dd       Y  w xY w# t        $ r t         j#                  dd       Y w xY w)&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.
    rb  r   z$provider and model required for mainr   r  Nr  r   r#  r  rO  r   )apply_nous_managed_defaults)_get_platform_toolscliFinclude_default_mcp_serversT)enabled_toolsetsrY  z#apply_nous_managed_defaults skippedr:  >   r*  rO  )_auto_provider_name_save_custom_providerrP  z%custom_providers registration skippedr  rU   >   r   rV  )r  rU   r#  )r  r  rU   r#  r  gateway_tools	stale_aux	__reset__rV  )r  r  resetzprovider required for auxiliaryzunknown auxiliary task: rO  )r  r  ra  rU   r#  )r   r7   rJ  r   r  r  r  r   rS  r   ro  r  hermes_cli.tools_configr  r  r\   rQ   r@  r   hermes_cli.mainr  r  r?  r  rP  r   r  )r  rU   r#  r  r  r  r  providers_cfgprovider_entryrK  r  r  r  r  changedr  r  rR  r  rc  rd  re  slot_providerauxr  rQ  s                             rV   r}  r}    s    -CuC8^__:8UK%,8B=RV8W**84]aJ~t<ASAST^A_>--j9?R@FFHH0GGGR (E8W
	 >40""9-#1)#<Ii  G $&>>!!#v-QTG-E 6%, $
 !'w 	C >>!!#'::xSV%,X6	(  ~~'--/ "	''+r*gt$' ";;t,!(D1 #HLLR$@$FB G M M O!%++-\A%++-=$$ $$1!$X\\'2%>%D"!E& "  !j"5*"
 	
 ''+
Cc4 {# 	!Dwwt}Hh-#)HZ  "HWLLT*,X6 CI	! KC[4@@4UVVtf$"7G &C:RSWRX8YZZ774=(D)HHLL4:;AACIIK~~'--/'!=(\X-ELLT*,X6D	 C Y  Q 

@4
PQ.  S 

BT
RSs$   '-Q Q8  Q54Q58 RRr  rQ  c                    | xs dj                         }|sd|fS 	 ddlm}m}m} 	  |||      }|r
|d   |d   fS d|v r	  ||      |v }|sd|fS d|fS # t
        $ r d|fcY S w xY w# t
        $ r d}Y Ew xY w# t
        $ r d}Y ;w xY w)	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   )r8  detect_provider_for_modelr4  Nr   r   Fr9  )r   r=  r8  r  r4  r\   )r  rQ  rS   r8  r  r4  detectedcur_is_aggregators           rV   _infer_provider_on_model_changer    s    " O""$D4x	
 	
,T=A {HQK''
 d{	& 2= AEZ Z !%%t8O+  4x
    	& %	&s4   
A 	A( 
A9 A%$A%(A65A69BBc                 x   t        |       } | j                  dd       | j                  dd      }t        |t              s	 t        |      }| j                  d      }t        |t              r$|r!	 t               }|j                  d      }t        |t               rt        |j                  d      xs d      j                         }t        |j                  d      xs d      j                         }||k7  r`|r^t        ||      \  }}|rM|j                         j                         |j                         k7  rt        ||      \  }	}
t        ||	|
      }|
}||d<   |dkD  r||d	<   n|j                  d	d       || d<   | S |dkD  r||d
| d<   | S | S # t        t
        f$ r d}Y \w xY w# t        $ r Y | S w xY w)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   rU   r  )r?  r  )r  rP  r  r  r  r  r   r  r   r   r  r   rJ  rS  r\   )r  ctx_overrider  disk_config
disk_modelprev_defaultrQ  rR  resolved_modelnorm_provider
norm_models              rV   _denormalize_config_from_webr    s    &\F JJ}d# ::4a8LlC(	|,L 

7#I)S!i-	%-K$1J*d+":>>)#<#BCIIK #JNN:$>$D" E K K M ,3R!=40L. $(:(:(<(B(B(DH[H[H](]
 5U(.51z &B&z&
 %/	(1
9%!#3?J/0NN#3T:",w M !(&2#w M6Mi :& 	L	d  	M	s+   F 2DF, F, F)(F),	F98F9c                 H  K   	 t        | j                  xs |      5  t               }t        | j                        }t        t        ||             d d d        ddiS # 1 sw Y   xY w# t        $ r  t        $ r# t        j                  d       t        dd      w xY ww)Nr  TzPUT /api/config failedrY  r|  r   )r  r  r   r  r  r   r'   r7   r\   rQ   rK  )rj  r  r  incomings       rV   update_configr  C  s     MDLL3G4 		9 '(H3DKK@HHh78		9 d|		9 		9   M/04KLLMs3   B"A* 5AA* B"A'#A* *5BB"c                     	 ddl m}  	 ddlm} |j                         D ch c]4  \  }}|xs i j                  d      r|xs i j                  d      dk7  r|6 }}}i } |        D ]  }|j                  dk7  r|j                  D ]M  }||v r|j                  ||j                  |j                  |j                  |j                  xs ddd	dd
       O |j                  rE|j                  |j                  |j                  |j                  |j                   ddd	ddd
       |j                  dk(  r|dD ]w  }|j                  |i       }	|j                  |j                  |	j                  d      xs |j                   d| d|	j                  d      d	|	j                  dd      dd
||<   y |j                  dk(  s]|j                  di       }	|j                  |j                  |	j                  d      xs |j                   d|	j                  d      d	|	j                  dd      dd
|d<    |S # t        $ r i cY S w xY w# t        $ r i }Y Kw xY wc c}}w )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   r'  rU   keysNTF)rU   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  r\   r<  r   r  r   tabapi_key_env_varsr  r[  rg  r"  
signup_urlbase_url_env_var	auth_type)
r  _OPTr  r  _non_provider_keysmetadr  aws_varr  s
             rV   _catalog_provider_env_metadatar  X  sv   @? jjlaG==$!'rz)Bj)P 	
 
 D F55F? )) 	G,,OO !&'gg#$==<</4#' % *	  OO"" !&'gg&'ggY.@#A#( $ *$ ;;)#8 
88GR0 !&'gg#+<<#>#ZQWWIRPWyXYBZ#<<.#( (Z > *!W
$ ;;("xx 92>HFF"#'''||M:  KggYIJ||E*$$LLT:&	/D*+{FN Kq  	  s'   H; I 9I;I	I	IIz/api/envc           	        K   t        |       5  t               d d d        t               t               dddt        dt
        dt        dt
        ffd}i }t        j                         D ]  \  }} |||      ||<    D ]  }||vs ||i       ||<    D ]%  }||v s|v r ||i d      }d|d	<   d|d
<   |||<   ' |S # 1 sw Y   xY ww)NFrO  var_namerR   rO  rK   c                l   j                  |       }j                  |       xs i }t        |      |rt        |      nd |j                  d      xs |j                  dd      |j                  d      |j                  d      n|j                  d      |j                  d      xs |j                  dd      |j                  d|j                  dd            |j                  dg       |j                  d	|j                  d	d            | v |j                  d
d      |j                  dd      |dS )Nr"  r   r   r'  passwordr  Ftoolsr  rU   r  )r(  redacted_valuer"  r   r'  r  r  r  channel_managedrU   r  rO  )r   r   r%   )r  rR   rO  r  cat_metacatalog_metachannel_keysr  s        rV   _rowzget_env_vars.<locals>._row  s   )##H-3 5k38j/d88M2Uhll=RT6U&*hhuo&A488E?x||TYGZ,LZ0L88J]E0RSXXgr*X\\*e-LM  (<7 !Z4&ll+;R@ /
 	
rX   Tr'  r  )	r  r   _channel_managed_env_keysr  r  r  r   r   r  )	r  r  r3  r  rR   r  r  r  r  s	         @@@rV   get_env_varsr    s    		  !j!,.L13L:? 
s 
$ 
4 
D 
> F+113 0$$/x0
 ! 26!#Hb1F82   v\!98R-"J!Mx Mu! !s"   CC	A2C:C	CCc                 X  K   	 t        | j                  xs |      5  ddlm}  || j                  | j
                        }d d d        |S # 1 sw Y   S xY w# t        $ r}t        dt        |            |d }~wt        $ r# t        j                  d       t        dd      w xY ww)Nr   )save_provider_env_credentialr   r   zPUT /api/env failedrY  r|  )r  r  hermes_cli.credential_lifecycler  r  r  r  r7   r  r\   rQ   rK  )rj  r  r  r3  r  s        rV   set_env_varr    s     MDLL3G4 	H U1$((DJJGF	H 	H  G
 CH=3F M,-4KLLMsK   B*A $A	A 
B*AA B*A 	B'!A88/B''B*)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_PROBESri  c                    	 | j                   sg S | j                         }t        |t              r|j                  d      n|}t        |t              sg S g }|D ]p  }t        |t              r-t        |j                  d      xs d      j                         }nt        |xs d      j                         }|s`|j                  |       r |S # t        $ r g cY S w xY w)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_successrd  r\   r  r  r   r  r  r   r  )ri  r  r  idsr  mids         rV   _parse_model_idsr  '  s    I))+ #-Wd";7;;vDdD!	C dD!dhhtn*+113Cdjb/'')CJJsO J  	s   C C CCfallbackc                     t        j                  dd| xs dj                               j                  d      j                         }|xs |S )Nz[^A-Za-z0-9_-]+rO  r   z-_)r  r  r   r   )r  r  r[  s      rV   _custom_endpoint_idr  B  sA    66$cCI2+<+<+>?EEdKQQSD8rX   r  c                    g }| j                  d      }t        |t              r'|j                  d |j	                         D               n(t        |t
              r|j                  d |D               t        | j                  d      xs | j                  d      xs d      j                         }|r|j                  d|       t               }|D cg c]  }|s||v r|j                  |      r|  c}S c c}w )Nr\  c              3   N   K   | ]  }t        |      j                           y wr  r  r   r   r#  s     rV   r   z5_models_from_custom_endpoint_entry.<locals>.<genexpr>K  s     HUc%j&&(H   #%c              3   N   K   | ]  }t        |      j                           y wr  r  r  s     rV   r   z5_models_from_custom_endpoint_entry.<locals>.<genexpr>M  s     AUc%j&&(Ar  r#  default_modelr   r   )r   r  r  r  r  r  r  r   insertr   r  )r  r\  
raw_modelsr  r  r#  s         rV   "_models_from_custom_endpoint_entryr  G  s    F8$J*d#Hjoo6GHH	J	%AjAA		'*Neii.HNBOUUWMa'UD%ZeRWEZZZs   C:C:!C:3C:c                 f   t        | j                  d      t              r| j                  di       ni }t        |j                  dd      xs d      }t        |j                  d|j                  dd            xs d      }t        |j                  dd      xs d      }g }| j                  d      }t        |t              r|j	                         D ]  \  }}t        |t              st        |j                  d      xs( |j                  d      xs |j                  d	      xs d      j                         }	|	smt        |      }
t        |      }t        |j                  d      xs |j                  d
      xs	 |r|d   nd      }|j                  |
t        |j                  d      xs |
      |	|||j                  d      t        |j                  dd            t        t        |j                  dd      xs d      j                               |j                  d      r(t        t        |j                  dd      xs d            nd |
|k(  dd        |j                         dk(  r|rt        d |D              s|j                  ddd|||r|gng |j                  d      dt        t        |j                  dd      xs d      j                               |j                  d      r(t        t        |j                  dd      xs d            nd ddd       ||||ddS )Nr#  rU   r   r?  rS   r  r  r   rM  r  r   r  r  Tr  )r  rS   r  r#  r\  r  r  has_api_keyapi_key_preview
is_currentr   rO  c              3   ,   K   | ]  }|d    dk(    yw)r  rO  Nr  )r   es     rV   r   z,_custom_endpoint_response.<locals>.<genexpr>w  s     LtefQtWX`M`Lts   Customzdirect-config)rU   r#  r  )	endpointsr  )r  r   r  r  r  r   r  r  r   r%   r   r  r  )r  rK  rf  current_modelcurrent_base_urlr  r  provider_id	raw_entryr  endpoint_idr\  endpoint_models                rV   _custom_endpoint_responser  W  s   (237773CT(J$PRI9==R8>B?	ivr1JKQrRM9==R8>B?&(I$I)T"&/oo&7 	"Ki.9==4j	e8LjPYP]P]^cPdjhjkqqsHk*K7	BF w!7!z9==;Y!zkq^def^gwy{N!IMM&1@[A$' "+--0@"A#'	6G(N#O#C	i(D(J$K$Q$Q$STZcZgZghqZr:c)--	22N2TRT.U#Vx|)-==% 	. 8+0@LtjsLtIt(")6}oB'mm,<=#IMM)R$@$FB G M M OPV_VcVcdmVnz#immIr.J.Pb*QRtx%
 	 ("(
 rX   provider_keyc                     | j                  d      }t        |t              syt        |j                  d      xs d      j	                         j                         |k7  rydD ]  }|j                  |d        || d<   y)u}  Drop the main-slot mirror of a provider that no longer exists.

    ``activate_custom_endpoint`` copies the endpoint's ``base_url`` and
    ``api_key`` onto ``model``. That mirror outranks the environment at client
    construction (#62269), so deleting the endpoint without clearing it leaves
    the agent still authenticating to the deleted host with the deleted key —
    and leaves that key sitting in config.yaml after the operator believes the
    dashboard removed it.

    Only touches ``model`` when it actually names the deleted provider, so an
    endpoint deleted while a *different* provider is active is left alone.
    r#  NrU   r   )rU   r  r  )r   r  r  r  r   r   rP  )r  r  rK  r  s       rV    _detach_main_model_from_providerr    su      Ii&
9==$*+11399;|K4 #eT"#CLrX   c                    t        |j                  xs |j                        }|j                  xs dj                         }|j                  xs dj                         j                  d      }|j                  xs dj                         }|st        dd      |st        dd      t        j                  j                  |      }|j                  r|j                  st        dd      |st        dd      | j                  d	      }t        |t              si }|j                  |      }t        |t              si }t        |      }	|	j!                  |||t#        |j$                        d
       |	j                  d      }
t        |
t              rt        |
      ni }|j                  |      }t        |t              rt        |      ni ||<   ||	d<   |j&                  rE|j&                  dkD  r6t)        |j&                        |	d<   t)        |j&                        |	d   |   d<   |j*                  7|j*                  j                         r|j*                  j                         |	d<   |	||<   || d	<   |j,                  rPt/        | j                  di       |||      | d<   |	j                  d      rt        | d   t              r|	d   | d   d<   ||	fS )Nr   r   r   zname requiredr   zbase_url requiredz%base_url must include scheme and hostzmodel requiredr  )rS   r  r#  r  r\  r   r  r  r#  )r  r  rS   r   r  r^  r#  r7   r`  r  r  schemer  r   r  r  r  r   r  r  r  r  r  rS  )r  rj  r  rS   r  r#  r  r  r  r  existing_models
models_mapcurrent_model_entrys                rV   _write_custom_endpointr 	    s   %dgg&:;KIIO""$D#**,33C8HZZ2$$&EODD4GHH\\""8,F==4[\\4DEE$Ii&	}}[)Hh% !NE	LL 4 45	  ii)O:D_VZ:[o!6acJ$../5?@SUY5Z01`bJu E(Ot22Q6"%d&9&9":36t7J7J3Kh/0||DLL$6$6$8<<--/i"Ik C3GGGR +uh
G 99YJs7|T$B&+I&6CL#rX   z/api/providers/custom-endpointsc                      	 t        t                     S # t        $ r# t        j	                  d       t        dd      w xY w)zAReturn configured OpenAI-compatible custom endpoints for Desktop.z*GET /api/providers/custom-endpoints failedrY  zFailed to list custom endpointsr   )r  r   r\   rQ   rK  r7   r  rX   rV   list_custom_endpointsr	    sA    W(77 WCD4UVVWs	    ,Ac                     	 t               }t        ||       \  }}t        |       t        |      }d|d<   ||d<   |S # t        $ r  t
        $ r# t        j                  d       t	        dd      w xY w)z<Create or update a v12+ ``providers`` custom endpoint entry.Tr  r  z+POST /api/providers/custom-endpoints failedrY  zFailed to save custom endpointr   )r   r 	  r   r  r7   r\   rQ   rK  )rj  r  r  _entryrL  s        rV   upsert_custom_endpointr	    s    Vm4S$?VC,S1$  VDE4TUUVs	   := 5A2z6/api/providers/custom-endpoints/{endpoint_id}/activater  c                    	 t               }t        |       }|j                  d      }t        |t              r|j                  |      nd}t        |t              st        dd      t        |      }t        |j                  d      xs	 |r|d   nd      j                         }t        |j                  d	      xs d      j                         }|r|st        d
d      t        |j                  di       |||      }|j                  d      r|d   |d<   ||d<   t        |       d||dS # t
        $ r  t        $ r$ t        j                  d|        t        dd      w xY w)z?Set a configured custom endpoint as the default model provider.r  Nr   custom endpoint not foundr   r#  r   r   r  r   zcustom endpoint is incompleter  Tr  rU   r#  z7POST /api/providers/custom-endpoints/%s/activate failedrY  z"Failed to activate custom endpoint)r   r  r   r  r  r7   r  r  r   rS  r   r\   rQ   rK  )	r  r  r  r  r  r\  r#  r  rK  s	            rV   activate_custom_endpointr		    sP   Zm*;7GGK(	/9)T/J	l+PT%&C8STT3E:EIIg&G6!9BHNNPuyy,2399;HC8WXX0"1E|UZ\de	99Y#(#3Ii  GCuEE  ZPR]^4XYYZs   D-D0 06E&z-/api/providers/custom-endpoints/{endpoint_id}c                    	 t               }t        |       }|j                  d      }t        |t              r||vrt        dd      |j                  |d       ||d<   t        ||       t        |       t        |      }d|d<   |S # t
        $ r  t        $ r$ t        j                  d|        t        d	d
      w xY w)z7Remove a configured custom endpoint from ``providers``.r  r   r	  r   NTr  z0DELETE /api/providers/custom-endpoints/%s failedrY  z Failed to delete custom endpoint)r   r  r   r  r  r7   rP  r  r   r  r\   rQ   rK  )r  r  r  r  rL  s        rV   delete_custom_endpointr	  !  s    Xm*;7GGK(	)T*l).KC8STTlD)$K(l;C,S1  XI;W4VWWXs   BB	 	6B?z(/api/providers/custom-endpoints/validatec                 p  K   ddl }| j                  xs dj                         j                  d      }|sdddg dS |d	z   }d
di}| j                  r:| j                  j                         r d| j                  j                          |d<   	 |j                  |j                  d            5 }|j                  ||      }ddd       j                  dv rdddg dS |j                  sddd|j                   dg dS dddt        |      dS # 1 sw Y   OxY w# t        $ r ddd| dg dcY S w xY ww)zEProbe a custom endpoint by calling its OpenAI-compatible /models URL.r   Nr   r   FTzEnter an endpoint URL first.r  	reachabler4  r\  /modelsrI  rH  r   Authorization       @rZ  rJ  Could not reach r  rN  z"The endpoint rejected the API key.zEndpoint returned HTTP )httpxr  r   r^  r  ClientTimeoutr   r\   r   r  r  )rj  r	  r  r   r   re  ri  s          rV   validate_custom_endpointr	  8  s\     #**,33C8H$;Yeghh
Y
C+,G||**,%,T\\-?-?-A,B#C e\\%--"4\5 	4::c7:3D	4
 :%$;_kmnn??$=TUYUeUeTffg;htvwwTbDTUYDZ[[	4 	4 e%>NseST<UacddesC   B
D6!D .DD 
AD6DD D30D62D33D6z/api/providers/validatec                   K   t        |       ddl}| j                  xs dj                         }| j                  xs dj                         }|sddddS |dk(  r|j                  d	      d
z   }| j                  xs dj                         }|rdd| ind}	 |j                  |j                  d            5 }|j                  ||      }	ddd       dddt        	      dS t        j                  |      }
|
sddddS |
\  }}ddi}i }|dk(  r	d| |d<   n||d<   	 |j                  |j                  d            5 }|j                  |||      }	ddd       	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 # 1 sw Y   xY w# t        $ r ddd| ddcY S w xY w# 1 sw Y   wxY w# t        $ r	 ddddcY S w xY ww)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.)r  r	  r4  OPENAI_BASE_URLr   r	  r	  r   r	  rZ  rJ  r	  r	  r  rI  rH  r  r        $@)r   paramsz/Could not reach the provider to verify the key.rN  z9That API key was rejected. Double-check it and try again.  zProvider returned HTTP z for this key.)r   r	  r  r   r  r^  r  r	  r	  r   r  r\   r  r   r  )rj  r   r	  r  r  r   r  r   re  ri  prober   r	  s                rV   validate_provider_credentialr	  T  sJ     788>r
 
 
"CZZ2$$&E$;QRR ll3)+ <<%2,,.<C?ggY$78	[emmC&89 8Vzz#wz78TbL\]aLbcc ""3'E2>>IC+,GFx%,UG#4 uo\\%--"5\6 	C&::c76:BD	C
 :%$;vww3$//"==d9PQUQaQaPbbp7qrr=8 8  	[eBRSVRWWX@YZZ	["	C 	C o%<mnnos   BG/!F5 :F)F5 %<G/"!G GG  A	G/)F2.F5 5GG/
GG/GG G,)G/+G,,G/c                   K   	 t        | j                  xs |      5  ddlm}  || j                        }d d d        j                  d      st        d| j                   d      |S # 1 sw Y   6xY w# t        $ r  t        $ r}t        dt        |            |d }~wt        $ r# t        j                  d       t        d	d
      w xY ww)Nr   )remove_provider_env_credentialfoundr    not found in .envr   r   zDELETE /api/env failedrY  r|  )r  r  r  r	  r  r   r7   r  r  r\   rQ   rK  )rj  r  r	  r3  r  s        rV   remove_env_varr"	    s     MDLL3G4 		> W3DHH=F		> zz'"C488*DV8WXX		> 		>   G CH=3F M/04KLLMs>   CA7 A+4A7 *C+A40A7 7C	B  /CCz/api/env/revealc                 R  K   t        |       t        j                         }|t        z
  }t        D cg c]
  }||kD  s	| c}t        dd t	        t              t
        k\  rt        dd      t        j                  |       t        | j                  xs |      5  t               }ddd       j                  | j                        }|t        d| j                   d      t        j                  d| j                         | j                  |dS c c}w # 1 sw Y   txY ww)	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
    Nr	  z,Too many reveal requests. Try again shortly.r   r   r!	  zenv/reveal: %s)r  r  )r   r}  _REVEAL_WINDOW_SECONDSr   r   _REVEAL_MAX_PER_WINDOWr7   r  r  r  r   r   r  rQ   rR   )rj  r   r  ry  cutofftr  r  s           rV   reveal_env_varr(	    s      7 ))+C))F(:I1a&jQIq
"884bccc" 
/	0 !j!OODHH%E}txxj@R4STTII)88e,, J! !s/   1D'
DDAD'D(A3D'D$ D'Telegramz1Run Hermes from Telegram DMs, groups, and topics.z1https://core.telegram.org/bots/features#botfather)r  r  TELEGRAM_PROXY)r  )rS   r"  r,  env_varsrf  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)r3	  r4	  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)r8	  r9	  r:	  signalSignalz)Connect through a signal-cli REST bridge.z0https://github.com/bbernhard/signal-cli-rest-api)SIGNAL_HTTP_URLSIGNAL_ACCOUNTSIGNAL_ALLOWED_USERS)r>	  r?	  whatsappWhatsAppzBUse Hermes through the bundled WhatsApp bridge with QR-based auth.z"https://github.com/tulir/whatsmeow)WHATSAPP_ENABLEDWHATSAPP_MODEWHATSAPP_DM_POLICY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_HOSTr  z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_SECRETr  z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)rV	  rW	  google_chatzGoogle Chatz0Connect Hermes to Google Chat via Cloud Pub/Sub.zKhttps://hermes-agent.nousresearch.com/docs/user-guide/messaging/google_chat)rS   r"  r,  wecomzWeCom (group bot)z&Send-only WeCom group bot via webhook.z8https://developer.work.weixin.qq.com/document/path/91770)WECOM_BOT_IDWECOM_SECRET)r\	  r  zWeCom (app)z+Two-way WeCom integration via callback app.z8https://developer.work.weixin.qq.com/document/path/90930)WECOM_CALLBACK_CORP_IDWECOM_CALLBACK_CORP_SECRETWECOM_CALLBACK_AGENT_IDWECOM_CALLBACK_TOKENWECOM_CALLBACK_ENCODING_AES_KEY)r^	  r_	  r`	  weixinz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)rd	  re	  r  zBlueBubbles (iMessage)z5Use Hermes through iMessage via a BlueBubbles server.zhttps://bluebubbles.app/)BLUEBUBBLES_SERVER_URLBLUEBUBBLES_PASSWORDBLUEBUBBLES_ALLOWED_USERS)rg	  rh	  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)rj	  rk	  r,  zEhttps://hermes-agent.nousresearch.com/docs/user-guide/messaging/teamsu   Yuanbao (元宝)z"Connect Hermes to Tencent Yuanbao.)rS   r"  r,  rf  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yuanbaor  r  _PLATFORM_OVERRIDES)rs  rt  r  r1	  r6	  rA	  r<	  r  rG	  rJ	  r  rR	  r  rZ	  r[	  r  rc	  rv	  rx	  r  r  _PLATFORM_ORDERr>	  z8signal-cli REST API base URL, e.g. http://127.0.0.1:8080zSignal bridge URL)r"  rF   r   r?	  z6Signal account phone number registered with the bridgezSignal account)r"  rF   r@	  z3Comma-separated Signal users allowed to use the botzAllowed Signal usersrC	  z#Enable the WhatsApp gateway adapterzEnable WhatsApp)r"  rF   r  rD	  zWhatsApp bridge modezWhatsApp moderE	  z+How WhatsApp direct messages are authorizedzWhatsApp DM policyrF	  z5Comma-separated WhatsApp users allowed to use the botzAllowed WhatsApp usersrH	  z>Home Assistant base URL, e.g. https://homeassistant.local:8123zHome Assistant URLrI	  uB   Long-lived access token from Home Assistant (Profile → Security)zHome Assistant access token)r"  rF   r  rL	  z&Email address to send and receive fromzEmail addressrM	  z&Email account password or app passwordzEmail passwordrN	  z&IMAP server host (e.g. imap.gmail.com)z	IMAP hostrO	  z&SMTP server host (e.g. smtp.gmail.com)z	SMTP hostrP	  zTwilio Account SIDrQ	  zTwilio Auth Tokenr\	  zWeCom group bot IDzWeCom Bot IDr]	  zWeCom group bot secretzWeCom Secretz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`	  ra	  rb	  rd	  re	  rf	  rV	  rW	  rX	  rY	  rT	  rU	  _MESSAGING_ENV_FALLBACKSc                     ddl m}  t               }g }| j                  j	                         D ]`  }|j
                  dk(  r|j
                  |v r"|j                  |j
                         |j                  t        |j
                               b 	 ddl	m
} |j                         D ]Q  }|j                  |v r|j                  |j                         |j                  t        |j                  |             S 	 t!        t"              D ci c]  \  }}||
 c}}|j%                  fd	       t'        |      S # t        $ r t        j                  dd       Y ew xY wc c}}w )
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                 n    j                  | d   t        t                    | d   j                         fS )Nr  rS   )r   r   rz	  r   )r  rh  s    rV   r  z-_messaging_platform_catalog.<locals>.<lambda>g  s+    uyy4#o*>?6ARS rX   r  )r  r}	  r   __members__r  r  r  r  _build_catalog_entrygateway.platform_registryr~	  plugin_entriesrS   r\   rQ   r@  	enumeraterz	  r  r  )	r}	  r  r  memberr~	  plugin_entryidxr  rh  s	           @rV   _messaging_platform_catalogr	  C  s>    (UD$&G&&--/ ;<<7"<<4+FLL9:;	J?-<<> 	RL  D(HH\&&'NN/0A0A<PQ		R '0&@A(#sS#XAELLS   >  J

9D
IJ Bs   A*E  E&  E#"E#c                      	 t               } t               D ]#  }| j                  |j                  dd             % t	        |       S # t
        $ r$ t        j                  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:  )r   r	  r  r   	frozensetr\   rQ   r@  )r  r  s     rV   r  r  l  si    02 	3EKK		*b12	3 

@4
P{s   AA *A43A4>   GATEWAY_PROXY_KEYGATEWAY_PROXY_URLGATEWAY_ALLOW_ALL_USERSc                 n    ddddddd}| |v r||    S | j                         j                  dd	      d	z   fS )
z4Env-var prefixes owned by a messaging platform card.)EMAIL_)HASS_)QQ_QQBOT_)TWILIO_)
WECOM_BOT_r]	  )WECOM_CALLBACK_)rJ	  rG	  rv	  r  r[	  r  rO  r  )upperr  )r  r  s     rV   _platform_env_prefixesr	    sV     #"/.+G g{##''S1C799rX   c                    t        |       }g }t        j                         D ]I  \  }|j                  d      dk7  rt        v r$t        fd|D              s9|j                         K t        t        t        |                  S )zLAll messaging-category env vars for a platform (override + plugin + prefix).r'  r  c              3   @   K   | ]  }j                  |        y wr  )r   )r   r  rS   s     rV   r   z._discover_platform_env_vars.<locals>.<genexpr>  s     Bv4??6*B   )
r	  r   r  r   _MESSAGING_KEYS_PAGE_KEYSr  r  r  r  r   )r  prefixesr  rR   rS   s       @rV   _discover_platform_env_varsr	    s    %k2HD'--/ 
d88J;.,,BBBD D	"##rX   overrider	  c                     t        |       }d|v r&t        t        j                  g |d   |            S |B|j                  r6t        t        j                  g t        |j                        |            S |S )z5Canonical env-var list for a messaging platform card.r+	  )r	  r  r  r  rf  )r  r	  r	  r  s       rV   _merge_platform_env_varsr	    sx     -[9JXT]]#GXj%9#GJ#GHIIL$=$=T]]#SU<+D+D%E#S
#STUUrX   c                    t         j                  | i       }t        | ||      }d|v rt        |d         }n|t        |j                  xs d      }nd}|j                  d      r|d   }n;||j
                  r|j
                  }n | j                  dd      j                         }|j                  d      }|s||j                  xs d}| ||xs d|j                  dd      ||d	S )
Nrf  r  rS   r  r  r"  r   r,  )r  rS   r"  r,  r+	  rf  )	ry	  r   r	  r  rf  rg  r  r   install_hint)r  r	  r	  r+	  rf  rS   r"  s          rV   r	  r	    s     #&&{B7H'X|LH!Xn56		!\66<"=||F		!l&8&8!!""3,224,,}-K<3"//52 "(bLLR0$ rX   c                 :    t               D ]  }|d   | k(  s|c S  y )Nr  )r	  )r  r  s     rV   _catalog_lookupr	    s*    ,. ;+%L rX   c           	      2   t        j                  |       xs t        j                  |       xs i }|j                  dd      |j                  d|       |j                  dd      |j                  d      |j                  dd      |j                  dd      d	S )
Nr"  r   rF   helpr   r  Fr  )r"  rF   r	  r   r  r  )r   r   r{	  )r  rR   s     rV   _messaging_env_infor	    s      %P)A)E)Ec)JPbDxxr2((8S)$xxxx
E2HHZ/ rX   c                 p    ddl m}m}  |       } ||       }|j                  j	                  |      }|||fS )Nr   )r}	  r  )r  r}	  r  r  r   )r  r}	  r  r  r-  platform_configs         rV   _gateway_platform_configr	    s:    < "F$H&&**84O8_,,rX   r  scopedc                    | d   }|r|j                  d      ni }t        |t              r|j                  |i       ni }t               d uxs t	        |      d u}g }| d   D ]k  }	j                  |	      xs |rdnt        j                  |	d      }
|j                  |	|	| d   v t        |
      |
rt        |
      nd dt        |	             m |r	 t               }|j                  d      xs i }|j                  |      }t        |t              si }t        |j                  d            }|j                  d      }t        |t              r|nd }t        fd
| d   D              }nr	 t        |      \  }}}t        |xr |j                        }t        |xr |j!                  ||            }|r&|j"                  r|j"                  j%                         nd }t        |t              r|j                  d      nd }t        |t              r|j                  d      nd }t        |t              r|j                  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|j                  d      nd }t        |t              r|j                  d      nd }|dk(  r|xs d}|xs |}d }|dk(  rj                  d      xs |rdnt        j                  dd      j'                         }j                  d      xs |rdnt        j                  dd      j'                         }|dv r|ndt        |      t        |      d}|| d   | d   | d   ||||||t        |t              r|j                  d      nd ||d}|||d <   |S # t        $ r d	}d }Y [w xY w# t        $ r d	}t        fd| d   D              }d }Y w xY w)!Nr  r  r+	  r   rf  )r  r  r(  r  r  home_channelFc              3   @   K   | ]  }j                  |        y wr  r   r   r  r  s     rV   r   z._messaging_platform_payload.<locals>.<genexpr>(  s     O#-Or	  c              3   p   K   | ]-  }j                  |      xs t        j                  |d        / ywr   N)r   rv   rw   r	  s     rV   r   z._messaging_platform_payload.<locals>.<genexpr>:  s5       $:		#r(::s   36rm   r  r  disablednot_configuredpending_restartr  gateway_stopped
error_codeerror_messagerA	  rD	  rF	  >   r  	self-chat)r  allowed_users_sethome_channel_setrS   r"  r,  r  )r  rS   r"  r,  r  r  r  rm   r	  r	  r  r	  r+	  whatsapp_setup)r   r  r  r/   r0   rv   rw   r  r   r%   r	  r   r\   r  r	  r  _is_platform_connectedr	  to_dictr   )r  r  r  r	  r  runtime_platformsruntime_platformr  r+	  r  r  r  platforms_cfgplat_cfgr  hcr	  r  r  r-  r	  rm   runtime_gateway_stateruntime_gateway_errorr	  r	  r	  whatsapp_modeallowed_users_valuer  s    `                            rV   _messaging_platform_payloadr	    sU    +K4;K0 '. 	k2.  	% 	?)'2$>  HZ  

 $Nv299S";M5#88u+7<*U"3$	
 &c*	

  
	 -CGGK06BM$((5Hh-8<<	23Gn-B!+B!524L O~9NOO
	 8P95NHo ?F/F/FGG U"99(OTJ #'C'C  ,,446  *44Dd)KW%QU 
 =GwPT<UGKK8[_:DWd:SGKK6Y] 	!!%55 U! &- 	\*  &- 	_- 
   3#3
%>)>Nj OOO, B"))OR"@
%' 	
 OO45 K")),Db"I
%' 	
 &36J%JMPR!%&9!: $\ 2
 f]+*% * & *D1   .$#G& !$2 !NE  	 GL	 &  	 G  0 J  L	 s&   BN -A1N3 N0/N03$OOr  c                     t        | d|       y )Nr  )r&   )r  r  s     rV   _write_platform_enabledr	    s    Y@rX   X  >   r  expired	cancelled	connectedc                       e Zd ZU ej                  dz  ed<   eed<   eed<   eed<   eed<   eed<   dZedz  ed<   d	Z	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Zedz  ed<   dZedz  ed<   y)_WhatsAppOnboardingSessionNr%  r  r  session_path
expires_atexpires_at_tsr  startingrc  
qr_payload
account_idaccount_nameaccount_phoner  )r  r  r  r.  r  r  r  r  r  rc  r	  r	  r	  r	  r  r  rX   rV   r	  r	    s    


T
!!
IOGS4ZFC!Jd
!!Jd
!#L#*# $M3:$E3:rX   r	  _whatsapp_onboarding_sessionsr  c                     t        j                  | t        j                        j	                         j                  dd      S )N+00:00Z)r   fromtimestampr   utc	isoformatr  )r  s    rV   _utc_iso_from_tsr	    s0    !!"hll3==?GGRUVVrX   c                 ~    t        | xs d      j                         j                         }|dvrt        dd      |S )Nr  >   r  r	  r   z+WhatsApp mode must be 'bot' or 'self-chat'.r   )r  r   r   r7   )r  r  s     rV   #_normalize_whatsapp_onboarding_moder	    s>    u~$$&,,.D''4abbKrX   c                     t        | xs d      j                         }|sydj                  d |j                  d      D              S )Nr   r   c              3   b   K   | ]'  }|j                         s|j                  d d       ) yw)r  r   N)r   r  r  s     rV   r   z4_normalize_whatsapp_allowed_users.<locals>.<genexpr>  s#     Ud

DLLb)Us   //)r  r   r  r   )r  r  s     rV   !_normalize_whatsapp_allowed_usersr	    s<    
ekr

 
 
"C88UciinUUUrX   c                       ddl m}   | dd      S )Nr   get_hermes_dirzplatforms/whatsapp/sessionzwhatsapp/session)rl  r	  r	  s    rV   _whatsapp_session_pathr	    s    /68JKKrX   c                     t        | xs d      j                         }|sy |j                  dd      d   j                  dd      d   }t        j                  dd|      }|xs d S )Nr   @r   r   r   z\D+)r  r   r   r  r  )r  r  r  digitss       rV   _whatsapp_phone_from_identifierr	    sb    
ekr

 
 
"C		#q!!$**3215IVVFB	*F>TrX   r	  c                 (   | dz  }	 t        j                  |j                  d            }d d dt        dd ffd} ||j                  d              ||j                  d	              ||       t              fS # t        $ r Y yw xY w)
N
creds.jsonr  r  NNNr  rK   c                    t        | t              sy 9dD ]4  }t        | j                  |      xs d      j	                         }|s2| n :dD ]4  }t        | j                  |      xs d      j	                         }|s2| y  y y )N)r  jidlidr   )rS   verifiedNamenotifypushName)r  r  r  r   r   )r  r  r  r	  r	  s      rV   collectz6_whatsapp_linked_account_from_session.<locals>.collect  s    )T*+ IMM#.4"5;;=!&J	
 E IMM#.4"5;;=#(L	  rX   meaccount)rd  re  r  r\   r   r   r	  )r	  
creds_pathr  r	  r	  r	  s       @@rV   %_whatsapp_linked_account_from_sessionr	    s    ,J **Z1171CD "J#L3 4 " GKKGKK	"#G|%DZ%PPP5    s   %B 	BB
bridge_dirc           
      f   | dz  j                         ryddlm}m} ddlm}  |d      }|st        dd	       |d
d      }	 t        j                  |ddgt        |       dd| |       t                     }|j                  dk7  rd|j                  xs |j                  xs dj!                         }|r"dj#                  |j%                         dd       }t        dd|xs d 	      y# t        j                  $ r}t        dd	      |d}~wt        $ r}t        dd| 	      |d}~ww xY w)zDInstall bridge dependencies when the dashboard is the setup surface.r  Nr   find_node_executablewith_hermes_node_path)env_intnpmrY  z8npm was not found. WhatsApp setup needs Node.js and npm.r   WHATSAPP_NPM_INSTALL_TIMEOUTrw  rc  z--silentT)r  r%  r  r[  r  r(  z2Installing WhatsApp bridge dependencies timed out.z0Failed to install WhatsApp bridge dependencies: r   r  iz(npm install failed for WhatsApp bridge: z	no output)r{  rl  r	  r	  r  r 
  r7   r.  r/  r  r
   TimeoutExpiredr  r0  r  r1  r   r  r  )	r	  r	  r	  r 
  r
  r[  r3  r  r   s	            rV   $_ensure_whatsapp_bridge_dependenciesr
    sT   ^#++-L
u
%CM
 	

 4c:G)Z(J%',.
( A--66==6B==?YYv0023489F=f>S=TU
 	
	  $$ G
 	  EcUK
 	s$   5C- -D0 DD0D++D0r  c                    ddl m} ddlm}m}  |       }|dz  }|j                         st        dd| d       |d	      }|st        dd
      t        |       | j                  dd        |       }||d<   d|d<   t        j                  |t        |      dddt        |       gt        |      t        j                  t        j                  dddd|t               
      S )Nr   )resolve_whatsapp_bridge_dirr	  z	bridge.jsrY  z(WhatsApp bridge script was not found at r  r   r  z4Node.js was not found. WhatsApp setup needs Node.js.TrW  rD	  r  rE	  z--pair-onlyz--pair-jsonz	--sessionr  r  )	r  r1  r  r  r  r  r  r  r(  )!gateway.platforms.whatsapp_commonr
  rl  r	  r	  r{  r7   r
  rZ  r.  r  r  PIPEr  r
   )	r	  r  r
  r	  r	  r	  bridge_scriptr  r  s	            rV   _spawn_whatsapp_pairing_processr

     s    ML,.J,M!=m_AN
 	
  'DI
 	

 )4td3

!CC )C	
 
O  (*# rX   r%  c                     | y | j                         y 	 | j                          | j                  d       y # t        $ r$ 	 | j	                          Y y # t        $ r Y Y y w xY ww xY w)Nr   rZ  )r  	terminater^  r\   kill)r%  s    rV   _terminate_whatsapp_pairingr
  E   se    |yy{		!	 	IIK 		s'   "9 	A&A	A"A&!A""A&
pairing_idc                 R   	 |j                   }||D ]  }|j                         }|s	 t        j                  |      }t        |j                  d      xs d      j                         }t        5  t        j                  |       }|r|j                  |ur	 d d d         y |dk(  rEt        |j                  d      xs d      j                         }|r||_
        d|_        d |_        n|dk(  r|j                  d      }	t        |	t              r~t        |	j                  d      xs d      j                         }
t        |	j                  d      xs d      j                         }|
xs d |_        |xs d |_        t#        |
      |_        d|_        d |_        nK|d	k(  r+d	|_        t        |j                  d	      xs d
      |_        n|dk(  r|j                  dk(  rd|_        d d d         |j'                         }t        5  t        j                  |       }|r|j                  |ur
	 d d d        y |j                  dv r
	 d d d        y d	|_        |dk(  rdnd| d|_        d d d        y # t        j                  $ r Y yw xY w# 1 sw Y   xY w# t(        $ rt}t        5  t        j                  |       }|r7|j                  |u r)|j                  t*        vrd	|_        t        |      |_        d d d        n# 1 sw Y   nxY wY d }~y d }~ww xY w# 1 sw Y   y xY w)Neventr   qrwaitingr	  r   r  rS   r  zWhatsApp pairing failed.r  r	  >   r	  r	  r	  r   9WhatsApp pairing process exited before pairing completed.z*WhatsApp pairing process exited with code r  )r1  r   rd  re  JSONDecodeErrorr  r   _whatsapp_onboarding_lockr	  r%  r	  rc  r  r  r  r	  r	  r	  r	  r^  r\   &_WHATSAPP_ONBOARDING_TERMINAL_STATUSES)r
  r%  streamr  r  r  r
  recordr
  r   r	  r	  r0  r  s                 rV   _watch_whatsapp_pairingr
  T   s   , !2jjl"jjoG GKK06B7==?. 2:>>zJF!V[[%<2 2 } T!2!8b9??A02F-,5FM+/FL+-&{{62%dD1),TXXd^-Ar)B)H)H)JJ+.txx/?/E2+F+L+L+NL0:0BdF-2>2F$F/3RS]3^F0(3'+')(/'*7;;w+?+]C]'^.0V]]j5P(112 2!2D YY[
 
# 
.22:>D0
 
 ==AA
 
   Q H=j\K 	
 
K ++ 2 24  & 	(266zBF&++-&--Gm2m '"3x		( 	( 	(
 	
 
s   (J I6 2J 2'J	J #EJ+J 'L<LL6J	J JJ J	J 	L&L,AL;	LL		LLL&c                    t         5  t        j                  |       }|r|j                  t        v r
	 d d d        y d|_        d d d        	 t        ||      }t         5  t        j                  |       }|r|j                  t        v rt        |       	 d d d        y ||_
        d|_        d d d        t        | |       y # 1 sw Y   ~xY w# t        $ rf}t         5  t        j                  |       }|r)|j                  t        vrd|_        t        |      |_        d d d        n# 1 sw Y   nxY wY d }~y d }~ww xY w# 1 sw Y   xY w)N
installingr  r	  )r
  r	  r   rc  r
  r

  r\   r  r  r
  r%  r
  )r
  r	  r  r
  r%  r  s         rV   _run_whatsapp_pairingr
     s)   	" %.22:>*PP% % %	%.|TB 
# #.22:>*PP'-	# #
 "# J-1% %  & 	(266zBF&--/UU '"3x		( 	( 	(
 	# #sS   +B?B?C 6D=D=?C	D:D5AD$	D5$D-	)D55D:=Ec                  $   t        j                          } g }t        j                         D ]  \  }}|j                  :|j                  t
        vr(|j                  j                         d|_        d|_        |j                  | k  r5|j                  t
        vr#t        |j                         d|_        d|_        |j                  t
        v s|j                  dz   | k  s|j                  |        |D ]  }t        j                  |d         y )Nr  r
  r	  z-WhatsApp QR setup expired. Start a new setup.rw  )r}  r	  r  r%  rc  r
  r  r  r	  r
  r  rP  )ry  
remove_idsr
  r
  s       rV   #_prune_whatsapp_onboarding_sessionsr 
     s    
))+CJ;AAC *
FKK#%KK  ".#FMVFL3&6==@f+f'4%FMJFL==BBvG[G[^aGaehGhj)* ! <
%))*d;<rX   c                     t         j                         D ]Q  }|j                  t        |       k(  s|j                  t
        vs/d|_        d|_        t        |j                         S y )Nr	  z-Superseded by a newer WhatsApp setup session.)	r	  r  r	  r  rc  r
  r  r
  r%  )r	  r  s     rV   '_supersede_whatsapp_onboarding_sessionsr"
     sS    188: 7  C$55(//Qw:w)HOLHN'6	7rX   r
  c                     | |j                   |j                  |j                  |j                  |j                  |j
                  |j                  |j                  |j                  d
S )N)
r
  rc  r	  r	  r  r  r	  r	  r	  r  )	rc  r	  r	  r  r  r	  r	  r	  r  r
  r
  s     rV   _whatsapp_onboarding_payloadr%
     sZ     --''''--''++-- rX   c                     	 t        |       \  }}|r t        j                  d|j                         dd|j                  dS # t        $ r-}t        j                  d       dt	        |      dcY d }~S d }~ww xY w)Nz8Failed to auto-restart gateway after WhatsApp onboardingFr  z?WhatsApp onboarding: reusing in-flight gateway restart (pid %s)Tr  r  r  r  s       rV   *_restart_gateway_after_whatsapp_onboardingr'
     s|    
-g6f 		MHH	

  +xx   
QR$ X
 	

r  z(/api/messaging/whatsapp/onboarding/startc                   K   t        | j                        }t        | j                        }| j                  }t        |      5  t               }t        j                         t        z   }t        |      }|dz  j                         rt        j                  d      }t        |      \  }}	}
t        d ||t        |      |||d||	|
      }t         5  t#                t%        |       |t&        |<   d d d        t)        ||      cd d d        S 	 d d d        t        j                  d      }t        d ||t              |      }t         5  t#                t%        |       |t&        |<   d d d        t+        j,                  t.        |||fd      j1                          t)        ||      S # 1 sw Y   xY w# 1 sw Y   xY w# 1 sw Y   ZxY ww)Nr	  r  r	  )r%  r  r  r	  r	  r	  r  rc  r	  r	  r	  )r%  r  r  r	  r	  r	  r  T)rj   rk   rl   )r	  r  r	  r  r  r  r	  r}   _WHATSAPP_ONBOARDING_TTL_SECONDSr	  r{  r  token_urlsafer	  r	  r  r
  r 
  r"
  r	  r%
  rx   rz   r
  rT   )rj  r  r  effective_profiler	  r	  r	  r
  r	  r	  r	  r
  s               rV   start_whatsapp_onboardingr,
     s    .tyy9D5d6H6HIM	0	1 D-/		&FF%m4
<'//1 ..r2J6[\h6i3Jm/+ .%+)"%)+F + C357E<B-j9C 0
FC1D D 2	D4 &&r*J'#&#!F 
# ;+-/=4:%j1;
 $,- eg'
F;;9C C)D DJ; ;sV   AGBF8F,0F8
G>GG+AG,F5	1F88G=GG	Gz//api/messaging/whatsapp/onboarding/{pairing_id}c                 
  K   t         5  t                t        j                  |       }|st	        dd      |j
                  dk(  rt	        d|j                  xs d      t        | |      cd d d        S # 1 sw Y   y xY ww)Nr   8WhatsApp setup session was not found. Start a new setup.r   r	    zWhatsApp setup expired.)r
  r 
  r	  r   r7   rc  r  r%
  r$
  s     rV   get_whatsapp_onboarding_statusr0
  '!  s|     	" 
@+-.22:>Q  ==I%C8aHabb+J?
@ 
@ 
@s   BA$A7-
B7B <Bz5/api/messaging/whatsapp/onboarding/{pairing_id}/applyc                   K   t         5  t                t        j                  |       }|st	        dd      |j
                  dk7  rt	        dd      t        |j                  xs |j                        }t        |j                  |j                  n|j                        }|dk(  r |s|j                  xs |j                  xs d}|j                  }d d d        |j                  xs |xs }	 t        |      5  t        d	       t        d
d       rt        d|       t        dd       t        dd       d d d        t         5  t        j+                  | d        d d d        t-        |      }	dd|	d    d|	S # 1 sw Y   xY w# 1 sw Y   QxY w# t        $ r  t         $ r}t	        dt#        |            |d }~wt$        $ r(}t&        j)                  d       t	        dd      |d }~ww xY w# 1 sw Y   xY ww)Nr   r.
  r   r	  r  z$WhatsApp setup is not connected yet.r	  r   rD	  rE	  r  rF	  rC	  r  rA	  Tr   z WhatsApp onboarding apply failedrY  zFailed to save WhatsApp setup.r  )r  r-  needs_restart)r
  r 
  r	  r   r7   rc  r	  r  r	  r  r	  r	  r  r  r   r	  r  r  r\   rQ   rK  rP  r'
  )
r
  rj  r  r
  r  r  record_profiler+
  r  restart_results
             rV   apply_whatsapp_onboardingr5
  6!  s     
# (+-.22:>Q  ==K'C8^__24993KL9$($6$6$>F  DDVDV
 ;}"00KF4E4EKM!($ AA>"#45 	6?D1/;7G -v6#J5	6( 
# <%))*d;< @@QRN+,=>> 	 Y( ((	6 	6   GCH=3F 9:3
 	< <s}   HCF H2F =?F<F H
G8!H F	HFF G5*GG5#G00G55H8H=Hc                    K   t         5  t        j                  | d       }d d d        rd|_        t	        |j
                         ddiS # 1 sw Y   +xY ww)Nr	  r  T)r
  r	  rP  rc  r
  r%  r$
  s     rV   cancel_whatsapp_onboardingr7
  n!  sS     	" E.22:tDE##FKK0$<E Es   AA
*A
AAz+https://setup.hermes-agent.nousresearch.comzHermesDashboard/c                   h    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<   y)_TelegramOnboardingPairing
poll_tokenr	  r	  N	bot_tokenbot_usernameowner_user_id)	r  r  r  r  r  r  r;
  r<
  r=
  r  rX   rV   r9
  r9
  z!  s<    OO IsTz #L#*# $M3:$rX   r9
  _telegram_onboarding_pairingsc                  p    t        j                  dt              j                         j	                  d      S )NTELEGRAM_ONBOARDING_URLr   )rv   rw    _TELEGRAM_ONBOARDING_DEFAULT_URLr   r^  r  rX   rV   _telegram_onboarding_base_urlrB
  !  s'    
		+-MN		rX   c                    	 | j                  dd      }t        j                  |      }|j                   |j                  t        j
                        }|j                         S # t        $ r t        j                         dz   cY S w xY w)Nr	  r	  )tzinfor	  )	r  r   fromisoformatrD
  r   r	  	timestampr\   r}  )r  r  r  s      rV   _parse_expiry_tsrG
  !  st    !]]31
''
3== ^^8<<^8F!! !yy{S  !s   A"A% % BBc                      t        j                          } t        j                         D cg c]  \  }}|j                  | k  r| }}}|D ]  }t        j	                  |d         y c c}}w r  )r}  r>
  r  r	  rP  )ry  r
  r
  r	  s       rV   #_prune_telegram_onboarding_pairingsrI
  !  sn    
))+C #@"E"E"GJ3& 	G 
  <
%))*d;<s   A(c                 l    t        | xs d      j                         }t        j                  |      r|S y r  )r  r   r  r  )r  r  s     rV   _normalize_telegram_user_idrK
  !  s0    U[b!'')J%%j1rX   c                 4    dddddddj                  | |      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	  claimedrO  )telegram_manager_bot_token_not_configuredtelegram_token_fetch_failedr   )r  r  s     rV   "_telegram_onboarding_error_messagerQ
  !  s,    I?KG5`'X 
c%rX   rj  bearer_tokenrY  rS
  c                   dd l }dt        d}i }|
d|d<   ||d<   |rd| |d<   t                | }	 |j                  |j	                  d      	      5 } |j
                  | |fd
|i|}	|	j                          d d d        	 	j                         }t#        |t$              st        dd      |S # 1 sw Y   9xY w# |j                  $ r}
	 |
j                  j                         }n# t        $ r i }Y nw xY wt        |j                  d      xs |j                  d      xs 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# t        $ r}
t        dd      |
d }
~
ww xY w)Nr   rH  )rI  
User-AgentContent-Typerd  r   r	  r	  rZ  r   r  rc  r   z)Telegram setup service returned an error.r   r  >   rN
  r	  r/
  r   z9Telegram setup service is unavailable. Try again shortly.z4Telegram setup service returned an invalid response.)r	  _TELEGRAM_ONBOARDING_USER_AGENTrB
  r	  r	  r   raise_for_statusHTTPStatusErrorrL  rd  r\   r  r   rQ
  r   r7   RequestErrorr  r  )rY  r   rj  rS
  r	  r   request_kwargsr   re  rL  r  r  r  r   r   s                  rV   !_telegram_onboarding_request_syncr\
  !  s#     %5G &(N"4!%v%,\N#; *,-dV
4C \\%--"5\6 	(&%v~~   !	H %%'	(B fd#I
 	
 M[	( 	(    M	\\&&(F 	F	FJJw'E6::h+?E2F37
 !\\55<c#**KFCL N
 	  N
 	  I
 	s~   !C 'B5=C F- 5B>:C F*C,+E+,C:7E+9C::A1E++F*=FF*F%%F*-	G	6GG	c                Z   K   t        j                  t        | |||       d {   S 7 w)NrR
  )ro   r  r\
  )rY  r   rj  rS
  s       rV   _telegram_onboarding_requestr^
  !  s4      "")!   s   "+)+z(/api/messaging/telegram/onboarding/startc                 T  K   | j                   xs dj                         xs d}t        ddd|i       d {   }t        |j	                  d      xs d      j                         }t        |j	                  d      xs d      j                         }t        |j	                  d	      xs d      j                         }t        |j	                  d
      xs d      j                         }t        |j	                  d      xs |      j                         }t        |j	                  d      xs d      j                         }|r|r|r|st        dd      t        5  t                t        ||t        |            t        |<   d d d        |||||dS 7 a# 1 sw Y   xY ww)Nr   POSTz/v1/telegram/pairingsr  rl  r
  r   r:
  r	  	deep_linkr	  suggested_usernamer  7Telegram setup service returned an incomplete response.r   )r:
  r	  r	  )r
  rb
  ra
  r	  r	  )r  r   r^
  r  r   r7   _telegram_onboarding_lockrI
  r9
  rG
  r>
  )	rj  r  r  r
  r:
  r	  ra
  r	  rb
  s	            rV   start_telegram_onboardingre
  "  s    /668JNH0(# G W[[.4"5;;=JW[[.4"5;;=JW[[.4"5;;=JGKK,2399;IW[[.;)<BBDJW[[)=>D"EKKMZzL
 	

 
# 
+-4N!!*:65
%j1
 !0   5$
 
s(   6F(FD'F( )F	F(F%!F(z//api/messaging/telegram/onboarding/{pairing_id}c                 H  K   t         5  t                t        j                  |       }|st	        dd      |j
                  r.d|j                  |j                  |j                  dcd d d        S |j                  }d d d        t        ddt        j                  j                  | d	       
       d {   }t        |j                  d      xs d      j                         }|dk(  r9t         5  t        j                  |       }|r|j                  nd}d d d        ddS |dk(  rt        |j                  d      xs d      j                         }t        |j                  d      xs d      j                         }|st	        dd      t!        |j                  d            }	t         5  t        j                  |       }|st	        dd      ||_        |xs d |_        |	|_        d|j                  |j                  |j                  dcd d d        S |dv r<t         5  t        j#                  | d        d d d        t	        dt%        |d            t	        dd      # 1 sw Y   xY w7 # 1 sw Y   dxY w# 1 sw Y   sxY w# 1 sw Y   VxY ww)Nr   8Telegram setup session was not found. Start a new setup.r   r  )rc  r<
  r=
  r	  rX  z/v1/telegram/pairings/r   safe)rS
  rc  r
  )rc  r	  r   r<
  r  rc
  r=
  >   rN
  r	  r/
  z9Telegram setup is no longer available. Start a new setup.z2Telegram setup service returned an unknown status.)rd
  rI
  r>
  r   r7   r;
  r<
  r=
  r	  r:
  r^
  r`  r  quoter  r   rK
  rP  rQ
  )
r
  r
  r:
  r  rc  r  r	  r;
  r<
  r=
  s
             rV   get_telegram_onboarding_statusrk
  4"  s    	" '+-.22:>Q  ! & 3 3!'!5!5$//	' ' &&
'" 1
 !3!3JR!3!H IJ G
 X&,"-335F& 	?377
CG/6++BJ	? $:>>G,2399;	7;;~6<"=CCEP  4GKK4PQ& 	266zBF# #U   )F"."6$F#0F ! & 3 3!'!5!5$//		 	" ''& 	@)--j$?	@5K
 	
 C E' '"	? 	?	 	$	@ 	@s   J"AI-(
J"2I->;J"9I::;J"5&I=BJ"4A"J
J"*J,J"-I72	J"=JJ"
JJ"JJ"c                     	 t        |       \  }}|r t        j                  d|j                         dd|j                  dS # t        $ r-}t        j                  d       dt	        |      dcY d}~S d}~ww xY w)	az  Best-effort gateway restart after saving Telegram QR onboarding.

    The QR flow naturally pulls users into Telegram on another device. If the
    saved token waits on a separate dashboard restart click, Hermes appears
    broken from the chat side. Keep the config save authoritative, but report
    restart failures so the UI can fall back to the existing manual banner.
    z8Failed to auto-restart gateway after Telegram onboardingFr  Nz?Telegram onboarding: reusing in-flight gateway restart (pid %s)Tr  r  r  r  s       rV   *_restart_gateway_after_telegram_onboardingrm
  ~"  s|    
-g6f 		MHH	

  +xx   
QR$ X
 	

r  z5/api/messaging/telegram/onboarding/{pairing_id}/applyc                   K   g }t               }|j                  D ]C  }t        |      }|st        dd      ||vs"|j	                  |       |j                  |       E |st        dd      t        5  t                t        j                  |       }|st        dd      |j                  }|j                  }	|st        dd      	 d d d        |j                  xs |}
	 t        |
      5  t        d	       t        d
dj                  |             t!        dd       d d d        t        5  t        j-                  | d        d d d        t/        |
      }dd	|d    d|S # 1 sw Y   xY w# 1 sw Y   RxY w# t        $ r  t"        $ r}t        dt%        |            |d }~wt&        $ r(}t(        j+                  d       t        dd      |d }~ww xY w# 1 sw Y   xY ww)Nr   z*Allowed Telegram user IDs must be numeric.r   z*Add at least one allowed Telegram user ID.r   rg
  r  z Telegram setup is not ready yet.r  r  r   rs  Tz Telegram onboarding apply failedrY  zFailed to save Telegram setup.r  )r  r-  r<
  r2
  )r   r  rK
  r7   r  r  rd
  rI
  r>
  r   r;
  r<
  r  r  r   r  r	  r  r  r\   rQ   rK  rP  rm
  )r
  rj  r  r  r  raw_idr  r
  r;
  r<
  r+
  r  r4
  s                rV   apply_telegram_onboardingrp
  "  s     5D'' 	008
C  T!HHZ ##J/	0 ?
 	

 
# +-.22:>Q  $$	**9    /-. 	6/;3SXX>N5OP#J5	6 
# <%))*d;< @@QRN $+,=>>	
  M $	6 	6   GCH=3F 9:3
 	< <s   :G.9G.6AE*G.&F 14E6%F -G.3G"
 G.*E3/G.6E?;F GF++G7#GGG."G+'G.c                 z   K   t         5  t        j                  | d        d d d        ddiS # 1 sw Y   ddiS xY ww)Nr  T)rd
  r>
  rP  )r
  s    rV   cancel_telegram_onboardingrr
  "  s<     	" <%))*d;<$<<$<s   ;,;8
;z/api/messaging/platformsc                 
  K   t        |       5 }t               }t               }t        t	                     t        | d      t               D cg c]  }t        ||||d u       c}dcd d d        S c c}w # 1 sw Y   y xY ww)NrT   r	  )r  gateway_start_commandr  )r  r   r2   r  r   r  r	  r	  )r  
scoped_dirr  r  r  s        rV   get_messaging_platformsrw
  "  s      
	  
Jj%'LN+%=gw%O
 9:	  ,;
$8N	

 

 
s.   B>A7A2
$A7(
B2A77B <Br  c                 8   ddl m}m} | |vry|xs dj                         }|r|j	                         dk(  rddlm}  |       }nt        |       |}|dv ryt        d      5   |       j                  s
	 ddd       y	 ddd       d	|  d
| dS # 1 sw Y   xY w)u  Reason enabling ``platform_id`` on the target profile would break a
    multiplexed gateway, or ``None`` when the change is allowed.

    Mirrors the gateway's startup rule (``_start_one_profile_adapters`` in
    gateway/run.py): with ``gateway.multiplex_profiles`` on, the default
    profile owns the single shared HTTP listener and serves every profile via
    the ``/p/<profile>/`` prefix, so a SECONDARY profile must never enable a
    port-binding platform. Without this pre-write check the dashboard happily
    persisted the invalid config and the shared gateway died with
    ``MultiplexConfigError`` on its next start — for ALL profiles. Only
    *enabling* is blocked; disabling/clearing stays allowed so users can
    repair an already-invalid profile.
    r   )PORT_BINDING_PLATFORM_VALUESr  Nr   r  get_active_profile_namer?  rO  r?  zCannot enable 'z' on profile 'z': it binds its own listener port, and gateway.multiplex_profiles is on, so the default profile owns the single shared HTTP listener for every profile. Configure this channel on the default profile instead (disabling or clearing it here is still allowed).)
r  ry
  r  r   r   r  r{
  _resolve_profile_dirr  multiplex_profiles)r  r  ry
  r  	requestedr{
  rj   s          rV    _multiplex_port_binding_conflictr
  "  s      Q66"(b//1I	)Y6?
 )*Y'&&
 
y	) "$77 7
 +nVH =< 	<	 s   "BBz&/api/messaging/platforms/{platform_id}c           
        K   t        |       }|st        dd|        |j                  xs |}|j                  r6t	        | |      }|r(t
        j                  d| |xs d       t        d|      t        |d         }	 t        |j                  xs |      5  |j                  D ]&  }||vrt        d| d	|d
          t        |       ( |j                  j                         D ]J  \  }}||vrt        d| d	|d
          |j                         }	|	s2t        | ||	       t        ||	       L |j                  t!        | |j                         d d d        t
        j                  d| |xs d|j                  t#        |j                        t#        |j                               d| dS # 1 sw Y   \xY w# t        $ r  t$        $ r$ t
        j'                  d|        t        dd      w xY ww)Nr   Unknown messaging platform: r   z\Rejected messaging platform update: platform=%s profile=%s (multiplex port-binding conflict)r  r  r+	  r   z is not configurable for rS   zYMessaging platform updated: platform=%s profile=%s enabled=%s env_keys=%s cleared_keys=%sT)r  r-  z&PUT /api/messaging/platforms/%s failedrY  r|  )r	  r7   r  r  r
  rQ   rR   r   r  r  r    r  r  r   r  r   r	  r  r\   rK  )
r  rj  r  r  target_profileconflictallowed_envr  r  trimmeds
             rV   update_messaging_platformr
  .#  s     K(E&B;-$P
 	
 \\,WN||3KP II4+)	  CAAeJ'(K'MDLL3G4 	C~~ &k)'$'"%&?fO  !%& #hhnn. 	1
Uk)'$'"%&?fO   ++-1+sGL"30	1 ||''T\\B+	C0 			*'iLL4884>>"	
 44C	C 	CD   M?M4KLLMs>   A>G9G  BF4=F4AG  3G94F=9G   6G66G9z+/api/messaging/platforms/{platform_id}/testc                    K   t        |       }|st        dd|        t        |      5 }t               }t	        ||t               |d u      }d d d        d   s|d    d}d|d	   |d
S |d   s@|d   D cg c]  }|d   r
|d   s|d    }}|rddj                  |       nd}d|d	   |d
S |d   s	d|d	   dd
S |d	   dk(  rd|d	   |d    dd
S |j                  d      rd|d	   |d   d
S d|d	   dd
S # 1 sw Y   xY wc c}w w)Nr   r
  r   rt
  r  rS   z2 is disabled. Enable it, then restart the gateway.Frm   )r  rm   r4  r  r+	  r  r(  r  zMissing required setup: r  zPlatform setup is incomplete.r  zEGateway is not running. Restart the gateway to connect this platform.r	  Tz is connected.r	  z]Setup looks complete, but the gateway has not reported a connection yet. Restart the gateway.)r	  r7   r  r   r	  r2   r  r   )	r  r  r  rv
  r  r  r4  r  r  s	            rV   test_messaging_platformr
  q#  s    K(E&B;-$P
 	
 
	  
Jj-; 3 5jPT>T


 96]O#UVgg&67KK<  !,
Z x %L
 
  'tyy'9&:;0 	
 gg&67KK$%W%^
 	

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

 {{?#W%/
 	
 !r M
 

s)   )D$C=*D9D	A.D=DDvisiblec                     | syt        |       rt        | t              syt        |       }d|v r)|j                  d      dk\  r|j	                  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>r  r$  r   r   u   …N)r_  r  r  r  r   r   )r  r
  rx  s      rV   _truncate_tokenr
  #  sw     z%5"E
A
axAGGCLA%HHS!R 
1vG89rX   c            
         	 ddl m} m} d}| r	  |        }|rf|j                  d      rUddd|r |       nd dt        |j                  d            |j                  d	      t        |j                  d
            dS d}	 ddlm	} |d   j                  }	 ddlm} 	 ddlm} |D ]K  }|r ||      ndxs t!        j"                  |      }|s)|r ||      nd}	dd| |	 t        |      dddc S  dddS # t        $ r d} d}Y w xY w# t        $ r d}Y w xY w# t        t        f$ r Y w xY w# t        $ r d}Y w xY w# t        $ r d}Y w xY w)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_get_hermes_oauth_fileNaccessTokenThermes_pkcezHermes PKCE (r?  	expiresAtrefreshTokenrh  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rh  r   )agent.anthropic_adapterr
  r
  rb   r\   r   r
  r   r  r
  r  KeyErrorr<  r
  hermes_cli.env_loaderr
  rv   rw   )
r
  r
  hermes_credsenv_var_orderr
  r
  r
  varr  r  s
             rV   _anthropic_oauth_statusr
  #  s    &	
 L$	 8:L ((7#+H^,B,Ddh+iijk,\-=-=m-LM&**;7!%l&6&6~&F!G
 	
 _M5)+6GG3+E  
'4s#$Q299S>5P,S1VX"eF8,,U3!&
 	

 $//e  &(,%!%&  	 L	 $ "     +&*#+sX   C9 D D D3 E 9D	D	DDD0/D03E EEEc            
          	 ddl m}   |        }|rY|j                  d      rHdddt	        |j                  d            |j                  d      t        |j                  d	            d
S dddS # t        $ r d}Y mw xY w)a  Surface Claude Code CLI credentials as their own provider entry.

    Independent of the Anthropic entry above so users can see whether their
    Claude Code subscription tokens are actively flowing into Hermes even
    when they also have a separate Hermes-managed PKCE login.
    r   )read_claude_code_credentialsNr
  Tclaude_code_cliz~/.claude/.credentials.jsonr
  r
  r
  Fr
  )r
  r
  r\   r   r
  r   )r
  credss     rV   _claude_code_only_statusr
  $  s    H,. =)'9,UYY}-EF))K0!%eii&?!@
 	
 $//  s   A/ /A=<A=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 CLINr
  r  r  rX   rV   _copilot_acp_statusr
  -$  s      ;" rX   rO  Nous Portaldevice_codezhermes auth add nouszhttps://portal.nousresearch.comr  rS   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_CATALOGr  c           
      (   |	  |       S 	 ddlm} | dk(  r|j	                         }t        |j                  d            d|j                  d	      xs d
t        |j                  d            |j                  d      t        |j                  d            dS | dk(  r|j                         }t        |j                  d            |j                  d      xs d|j                  d      xs dt        |j                  d            dd|j                  d      dS | dk(  r|j                         }t        |j                  d            d|j                  d      xs dt        |j                  d            |j                  d      t        |j                  d            dS | dk(  rT|j                         }t        |j                  d            dd|j                  dd        d!d|j                  d      d"dS | d#k(  r|j                         }t        |j                  d            |j                  d      xs d$|j                  d%      xs |j                  d      xs d&t        |j                  d            dd"|j                  d      dS |j                  |       }t        |t              rd|v rt        |j                  d            |j                  d      xs |j                  d'      xs | |j                  d(      xsN |j                  d%      xs; |j                  d      xs( |j                  d)      xs |j                  d*      xs d+t        |j                  d      xs |j                  d            |j                  d      xs |j                  d      t        |j                  d            dS ddiS # t         $ r}dt        |      dcY d}~S d}~ww xY w# t         $ r}dt        |      dcY d}~S d}~ww xY w),z@Dispatch to the right status helper for an OAuth provider entry.NF)rh  r  r   )r   rO  rh  nous_portalri  r
  access_tokenaccess_expires_atr
  r
  r
  r   openai_codex	auth_modezOpenAI Codexr  last_refresh)rh  r   r
  r
  r	  r
  r
  r
  qwen_cliauth_store_pathzQwen CLIr	  r
  minimax_oauthz	MiniMax (regionglobalr?  Tr
  	xai_oauth
auth_storezxAI Grok OAuthrU   r
  r  rS   r   )r\   r  r]   r   rb  r   r   r
  get_codex_auth_statusget_qwen_auth_statusget_minimax_oauth_auth_statusget_xai_oauth_auth_statusget_auth_statusr  r  )r  r
  r  hauthr  s        rV   _resolve_provider_statusr
  $  s   	9;S5,& ,,.C!#''+"67' #(9 : Km!01H!I!gg&9:%)#''2E*F%G  .(--/C!#''+"67''(+=~ # 4 F!01C!D"%* # 7  ,&,,.C!#''+"67$ #(9 : Hj!01H!I!ggl3%)#''2E*F%G  /)557C!#''+"67)"+CGGHh,G+H J!%!ggl3%)  +%113C
 "#''+"67''(+:{ # 5 ^9J ^N^!01C!D"%) # 7  ##K0c4 [C%7!#''+"67''(+Qswwz/BQkGGN+ ww|,ww01 wwz* wwv	
 !0GGN+Aswwy/A" "ggl3Ssww?R7S%)#''2E*F%G & m  	9!&Q88	9h  5"SV445s^   O BO0 BO0 BO0 &AO0 ?BO0 D1O0 	O-O("O-(O-0	P9PPPc                     | j                  d      dk7  ry| j                  d      dk(  rd}t        j                  dk(  rd| S |S y)	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).
    r
  r
  Nr  r
  z!rm -f ~/.claude/.credentials.jsondarwinzKsecurity delete-generic-password -s "Claude Code-credentials" 2>/dev/null; )r   r,  r-  )rU   rm_files     rV   "_oauth_provider_disconnect_commandr
  $  sQ     ||Fz)||D]*5<<8#`ah`ijjrX   c                 p    | j                  d      dk(  rt        |       ryy|j                  d      dk(  ryy)	zJReturn the manual disconnect path when the API cannot clear this provider.r
  r
  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   r
  )rU   rc  s     rV   _oauth_provider_disconnect_hintr
  %  s;    ||Fz)-h7 YAzz(y(CrX   c            
         g } t               }t        D ]8  }|d   |v r|j                  |d          | j                  t	        |             : 	 ddlm}  |       D ]  }|j                  dk7  s|j                  |v r!|j                  |j                         | j                  |j                  |j                  dd|j                   |j                  xs ddd	        	 | S # t        $ r Y | S w xY w)
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  accountsr
  zhermes auth add r   Nr
  )r   r
  r  r  r  r  r  r  r[  rg  r  r\   )r"  r  r  r  r  s        rV   _build_oauth_catalogr
  %  s    $ "$DUD ) !;$tDK 	!@!# 	Auu
"affnHHQVVKKff"!1!&&:LL.B! 		 K  Ks   BC$ $	C10C1z/api/providers/oauthc                 4  K   t        |       5  g }t               D ]a  }t        |d   |j                  d            }t	        ||      }|j                  |d   |d   |d   |d   |d   |t        |      |du |d	       c d	|icddd       S # 1 sw Y   yxY ww)
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  r
  rS   r
  r
  r,  N)	r  rS   r
  r
  r,  disconnect_hintdisconnect_commanddisconnectablerc  r  )r  r
  r
  r   r
  r  r
  )r  r  r   rc  r
  s        rV   list_oauth_providersr
  P%  s     0 
	  (	%' 	A-agquu[7IJF=aHOg&	&	 /jM#2&H&K"1T"9 
 
	 Y'!( ( (s   BA4B
BBBz"/api/providers/oauth/{provider_id}c                   K   t        |       t        |      5  t               D ci c]  }|d   |
 }}|j                  |       }|+t	        dd|  ddj                  t        |                   t        |i       }|rt	        d|d    d	|       t        | |j                  d
            }t        ||      }|rt	        d|d    d	|       | dk(  rrd}	 ddl	m
}	  |	       }
|
j                         r|
j                          d}	 ddlm}  |d      xs |}t         j#                  d|        t%        |      | dcddd       S 	 ddlm}m}  ||       }| dk(  r |        t         j#                  d| |       t%        |      | dcddd       S c c}w # t        $ r Y w xY w# t        $ r Y w xY w# t        $ r1}t         j)                  d|        t	        dt+        |            d}~ww xY w# 1 sw Y   yxY ww)zDDisconnect an OAuth provider. Token-protected (matches /env/reveal).r  Nr   zUnknown provider: . Available: r  r   rS   z' cannot be disconnected automatically. r
  r
  Fr   r
  T)clear_provider_authzoauth/disconnect: %s)r  rU   )r
  !invalidate_nous_auth_status_cacherO  z!oauth/disconnect: %s (cleared=%s)zdisconnect %s failedrY  )r   r  r
  r   r7   r  r  r
  r
  r
  r
  r{  r  r\   r  r
  rQ   rR   r   r
  rK  r  )r  r   r  r   catalog_by_idrU   r
  rc  clearedr
  
oauth_filer
  r
  r  s                 rV   disconnect_oauth_providerr
  {%  sA     7		  8@-A-CD4!DD $$[1+K= 9%%)YYvm/D%E$FH  :(BG"6*++RSbRcd 
 *+x||K7PQ9(FK"6*++RSbRcd  +%GJ35
$$&%%'"G?-k:Eg II,k:w-[A[8@ 8@^		@^)+6Gf$13II9;Pw-[Ak8@ 8@DF      	@NN1;?CA??	@m8@ 8@s   HG6FB(G6/FF*#G6
HA F9
HG6	F'$G6&F''G6*	F63G65F66G69	G3,G..G33G66G?;H  _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  t        j	                         D cg c]  \  }}|d   | k  s| }}}|D ]  }t        j                  |d        	 ddd       yc c}}w # 1 sw Y   yxY w)z:Drop expired sessions. Called opportunistically on /start.
created_atN)r}  _OAUTH_SESSION_TTL_SECONDS_oauth_sessions_lockr
  r  rP  )r&	  r  sessstales       rV   _gc_oauth_sessionsr
  %  s    YY[55F	 +&5&;&;&=]dlASV\A\]] 	+CT*	++ +]+ +s"   A?A9A9 A?9A??Bc                 Z    | xs dj                         }|r|j                         dk(  ry |S )Nr   r  )r   r   )r  r
  s     rV   _oauth_profile_namer
  &  s.    B%%'I	)Y6rX   c                 6    t        |       }|rt        |       y y r  )r
  r}
  )r  r  s     rV   _validate_oauth_profiler
  	&  s    &w/L\* rX   r
  c                     t        j                  d      }t        |      }|| ||t        j                         ddd}t        5  |t
        |<   ddd       ||fS # 1 sw Y   ||fS xY w)zICreate + register a new OAuth session, return (session_id, session_dict).r  pendingN)r  rU   r
  r  r
  rc  r	  )r  r*
  r
  r}  r
  r
  )r  r
  r  r  r  r
  s         rV   _new_oauth_sessionr
  &  ss     


#C&w/LiikD 
 $#$9$9s   
AA%r  c                     t         5  t        j                  |       }|r|j                  d      nd}ddd       xs t        |      S # 1 sw Y   xY w)zCReturn the profile that owns an OAuth session, if one was provided.r  N)r
  r
  r   r
  )r  r  r
  r  s       rV   _oauth_session_profiler  %&  sQ    
 
 8"":.)-$((9%48 3)(338 8s   +A		Ar
  refresh_tokenexpires_at_msc                 B   ddl m}  |       }| ||d}ddlm}  |||dd       	 ddlm}m}m}	m}
 dd	l	} |d
      }|j                         D cg c]$  }t        |dd      j                  |
 d      s#|& }}|D ]  }	 |j                  t        |dd             !  |d
|j                         j                   d	d d|	d|
 d| ||	      }|j#                  |       y	c c}w # t        $ r Y sw xY w# t        $ r }t$        j'                  d|       Y d	}~y	d	}~ww xY w)zPersist Anthropic PKCE creds to both Hermes file AND credential pool.

    Mirrors what auth_commands.add_command does so the dashboard flow leaves
    the system in the same state as ``hermes auth add anthropic``.
    r   r
  )r
  r
  r
  r8  r$  r;  )indentr  )PooledCredential	load_poolAUTH_TYPE_OAUTHSOURCE_MANUALNr
  r   r   z:dashboard_pkcer     zdashboard PKCE)	rU   r  rg  r  priorityr   r
  r  r  z)anthropic pool add (dashboard) failed: %s)r
  r
  r  r9  agent.credential_poolr  r  r  r	  uuidr  r   r   remove_entryr\   uuid4hex	add_entryrQ   rS  )r
  r  r  r
  r
  r  r9  r  r  r  r	  r  poolr  r  r  s                   rV   _save_anthropic_oauth_credsr  0&  sC    ?')J#%"G (j'!%@E	
 	
 	%#||~x!Hb1I1T1TXeWffuUv1wAxx 	A!!'!T2"67	
 ! zz|#"%#_O4%''

 	u# y    E@!DDEsT   *C5 $C!6C!:C5 C&AC5 !C5 &	C2/C5 1C22C5 5	D>DDc           	         t         st        dd      t               \  }}t        dd|       \  }}||d<   ||d<   d	t        d
t
        t        |d|d}t         dt        j                  j                  |       }|d|t        dS )z9Begin PKCE flow. Returns the auth URL the UI should open.i  z/Anthropic OAuth not available (missing adapter)r   r
  r
  r  verifierrm   r  rR  S256)rR  	client_idresponse_typeredirect_urir  code_challengecode_challenge_methodrm   ?)r  r
  auth_url
expires_in)_ANTHROPIC_OAUTH_AVAILABLEr7   _generate_pkce_pairr
  _ANTHROPIC_OAUTH_CLIENT_ID_ANTHROPIC_OAUTH_REDIRECT_URI_ANTHROPIC_OAUTH_SCOPES_ANTHROPIC_OAUTH_AUTHORIZE_URLr`  r  	urlencoder
  )r  r  	challenger  r
  r	  r  s          rV   _start_anthropic_pkcer(  i&  s    %4eff-/Hi";HICDDM/5(#!'	F 116<<3I3I&3Q2RSH0	 rX   
code_inputc           	      x   t         5  t        j                  |       }ddd       r|d   dk7  s|d   dk7  rt        dd      |d	   d
k7  rd|d	   |j                  d      dS |j	                         j                  dd      }|d   j	                         }|sddddS t        |      dkD  r|d   nd}t        j                  dt        ||xs |d   t        |d   d      j                         }d}d}	t        D ]  }
t        j                  j                  |
|dddd      }	 t        j                  j!                  |d      5 }t        j"                  |j%                         j'                               }ddd        n |%t         5  d|d	<   d|	 |d<   ddd       dd|d   dS |j                  d d      }|j                  d!d      }t+        |j                  d"      xs d#      }|s"t         5  d|d	<   d$|d<   ddd       dd|d   dS t+        t-        j,                         d%z        |d%z  z   }	 t/        t1        | |            5  t3        |||       ddd       t         5  d'|d	<   ddd       t4        j7                  d(|        d)d'd*S # 1 sw Y   uxY w# 1 sw Y   (xY w# t(        $ r}|}	Y d}~d}~ww xY w# 1 sw Y   -xY w# 1 sw Y   xY w# 1 sw Y   xY w# t(        $ r<}t         5  d|d	<   d&| |d<   ddd       n# 1 sw Y   nxY wdd|d   dcY d}~S d}~ww xY w# 1 sw Y   xY w)+z<Exchange authorization code for tokens. Persists on success.NrU   r
  r
  r
  r   zUnknown or expired sessionr   rc  r
  Fr	  )r  rc  r4  #r   r   r  zNo code providedr   authorization_coderm   r  )
grant_typer  rR  rm   r  code_verifierrH  zhermes-dashboard/1.0)rV
  rU
  r`
  )r  r   rY  r  rZ  zToken exchange failed: r
  r  r  i  zNo access token returned  zSave failed: approvedz2oauth/pkce: anthropic login completed (session=%s)T)r  rc  )r
  r
  r   r7   r   r   r   rd  r  r"  r#  r   _ANTHROPIC_OAUTH_TOKEN_URLSr`  r   r8   ra  re  rf  rN  r\   r  r}  r  r  r  rQ   rR   )r  r)  r  r
  r  rR  state_from_callbackexchange_datar3  last_exc	_endpointrh  ri  r  r
  r  r  r  s                     rV   _submit_anthropic_pkcer6  &  s~    
 /"":./4
#{2d6lf6L4PQQH~"tH~$((?B[\\ $$S!,E8>>Dw;MNN&)%j1n%("JJ*/$5W5j)   vx  FH0 	nn$$ 24  % 
	''R'8 :DDIIK$6$6$89:" ~! 	I$DN&=hZ$HD!	I w4;PQQ::nb1LJJ3MVZZ-56J! 	?$DN$>D!	? w4;PQQ		d*+zD/@AMR2:wGH 	T'm]S	T 
 $#X$IIBJO*--O/ /L: :  	H		I 	I	? 	?	T 	T R! 	8$DN&3A3$7D!	8 	8 	8 w4;PQQ	R
$ $s   J$!J*2J7J*
K3K6K( KK( 'L0JJ'	"J**	K 3J;;K KKK%!K( (	L-1L(7L	L(L	L("L-(L-0L9c           
      l  K   | dk(  r}ddl mm} ddl|d   }t	        j
                  d      xs# t	        j
                  d      xs |j                  j                  d      |j                  |j                  fd}t        j                         j                  d|       d{   \  }}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        j)                          |d	t        |d         t        |d         t        |d         t        |d         dS | dk(  rt        dd	|
      \  }}	t#        j$                  t*        |fdd|dd        j)                          t!        j,                         dz   }
t!        j,                         |
k  rut.        5  t0        j3                  |      }ddd       r|j3                  d      s|d   dk7  rn5t        j4                  d       d{    t!        j,                         |
k  rut.        5  t0        j3                  |i       }ddd       j3                  d      dk(  r t7        d|j3                  d       xs d!"      |j3                  d      st7        d#d$"      |d	|d   |d%   t        |j3                  d      xs d&      t        |j3                  d      xs d'      dS | d(k(  rdd)l m}mmm} ddl |       \  }t	        j
                  d*      xs |j                  d      fd+}t        j@                         j                  d|       d{   }t        d(d	|
      \  }}|j3                  d      }|t        |      nd|d,<   t        |d         |d<   ||d-<   |d.<   |d<   |d<   d/|d0<   t        |d1         }||d2<   |d3kD  r0|d4z  }tC        dt        |t!        j                          z
              }nt!        j                          |z   }|}||d<   t#        j$                  tD        |fdd|dd        j)                          |d	t        |d         t        |d5         |tC        d6|d,   xs d7d8z        dS | d9k(  rdd:l m# ddlfd;}t        j                         j                  d|       d{   }t        d9d	|
      \  }}t        |d	         |d	<   t        |d         |d<   t!        j                          t        |d         z   |d<   t#        j$                  tH        |fdd|dd        j)                          |d	t        |d         t        |j3                  d      xs |d5         t        |d         t        |d         dS t7        d<d=|  d>"      7 Y# 1 sw Y   xY w7 # 1 sw Y   xY w7 z7 w)?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.
    rO  r   )_request_device_coder
  NHERMES_PORTAL_BASE_URLNOUS_PORTAL_BASE_URLr   c                      j                  j                  d      ddi      5 }  |       fcd d d        S # 1 sw Y   y xY w)N      .@rI  rH  r[  r   )re  ri  r  r  r	  r	  )re  r8  r  r	  ri  r  s    rV   _do_nous_device_requestz8_start_device_code_flow.<locals>._do_nous_device_request&  sa    d+!#56    (%(7"+#	 	  s	   >Ar
  r  rJ   r  r	  ri  r  r  Tzoauth-poll-r
  ri   	user_codeverification_uri_complete)r  r
  r@  verification_urlr  poll_intervalr
  zoauth-codex-r  rc  r
  r  r  rY  r	  zdevice-auth failedr   i  z2device-auth timed out before returning a user coderB  r
  r   r
  )_minimax_pkce_pair_minimax_request_user_codeMINIMAX_OAUTH_CLIENT_IDMINIMAX_OAUTH_GLOBAL_BASEMINIMAX_PORTAL_BASE_URLc                      j                  j                  d      ddid      5 }  |       cd d d        S # 1 sw Y   y xY w)Nr<  rI  rH  Tr[  r   follow_redirects)re  ri  r  r  rm   r>  )re  rF  rE  r'  r	  ri  rm   s    rV   _do_minimax_requestz4_start_device_code_flow.<locals>._do_minimax_requestG'  s]    d+!#56!%    1!$35#,  s	   >Ainterval_msr.  rm   r
  r
  
expired_inexpired_in_rawl    J)g     @@verification_urir$  r]  r/  r
  )_xai_oauth_request_device_codec                      j                  j                  d      ddi      5 }  |       cd d d        S # 1 sw Y   y xY w)N      4@rI  rH  r=  r>  )re  rQ  r	  s    rV   _do_xai_device_requestz7_start_device_code_flow.<locals>._do_xai_device_request'  sK    d+!#56   > 5f=	> > >s	   8Ar   	Provider z" does not support device-code flow)%r  r8  r
  r	  rv   rw   ri  r^  r  r  ro   r  ru   r
  r  r  r}  rx   rz   _nous_pollerrT   _codex_full_login_worker	monotonicr
  r
  r   sleepr7   rD  rE  rF  rG  rt   r\  _minimax_pollerrQ  _xai_device_poller)r  r  r
  pconfigr?  device_dataeffective_scoper  r
  r  deadlinerx  rD  rG  r  rL  interval_rawrO  r	  expires_in_secondsrT  rF  rE  r8  rQ  r'  r  r	  ri  r  rm   s                        @@@@@@@@@@rV   _start_device_code_flowrb  &  sf     f	
 	#F+II./ 'yy/0'&&
&+	 	
 %%		 	 .5-E-E-G-W-W).
 (
$_ 'v}gN	T!+m"<=]{:67Z!YY[3{</H+II\"1%['WsfT+cRTSTgY@W	

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

 %'![56 #K0B$C D, T-%8%@DT$IJ
 	
 k!B	> $446FF(
 
 '{M7S	T!+m"<=]{:67Z!YY[3{</H+II\%s2Awi(		

 %'![56 # ;< 312! k,78 Z!89

 
	
 C)K=Hj0k
llS(
J- - %	- 	-\
p
sv   B)X45X6EX4X#<X4X X4<X4X!DX44X.5E,X4!X1"C-X4X	X4!X+&	X41X4c                 ,   ddl m}m} ddlm}m} ddl}t        5  t        j                  |       }ddd       sy|d   }|d   }|d   }	|d   }
|j                  d	      }t        d
t        |d   t        j                         z
              }	 |j                  |j                  d      ddi      5 } |||||	||
      }ddd        |j                  |j                        }t        j                  d      xs d      }||j                  d      ||j                  d	      xs ||j                  dd      |d   |j                  d      |j!                         |r= |j"                  |j%                         |z   |j                        j!                         nd|d
}t'        t)        |             5   ||dd      }ddl m}  ||       ddd       t        5  d|d<   ddd       t,        j/                  d|        y# 1 sw Y   xY w# 1 sw Y   PxY w# 1 sw Y   NxY w# 1 sw Y   FxY w# t0        $ rU}t,        j3                  d| |       t        5  d |d<   t5        |      |d!<   ddd       n# 1 sw Y   nxY wY d}~yY d}~y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   Nri  r  r
  rJ   r  <   r	  r<  rI  rH  r=  )re  ri  r  r
  r  rC  r  rj  
token_typeBearerr
  r  tz)
ri  rj  r  r  rg  r
  r  obtained_atr	  r  F)timeout_secondsforce_refresh)persist_nous_credentialsr0  rc  z/oauth/device: nous login completed (session=%s)z-nous device-code poll failed (session=%s): %sr  r	  )r  rd  re  r   r   r	  r
  r
  r   r\  r  r}  r	  r	  ry  r	  r	  r	  rF
  r  r  rn  rQ   rR   r\   rS  r  )r  rd  re  r   r   r	  r
  ri  r  r
  rJ   r  r  re  
token_datary  	token_ttl
auth_state
full_statern  r  s                        rV   rV  rV  '  s}    ,	 /"":./,-O[!I}%KJHHHWERT,/$))+=>?J++\\%--"5J\?]\^ 	bh( /#'%&J	 hll8<<(
|49:	.",..1E"F"^^G,5$..x@&~6'^^O<==?  '&&s}}'Bx||T^^`"&#

 2:>? 	16 $#J
 A$Z0	1 " 	('DN	(		CZPa/ /	 	6	1 	1	( 	(  +DjRST! 	+$DN$'FD!	+ 	+ 	+ 	+ 	++s   H$H5 3HC4H5 6HH5 H)$H5 HHH5 H&"H5 )H2.H5 5	J>JI8/	J8J	=JJc                    ddl m}m}m}m}m} ddlm}m} ddl}t        5  t        j                  |       }	ddd       	sy|	d   }
|	d   }|	d   }|	d   }|	j                  d	      }|	d
   }	 |j                  |j                  d      ddid      5 } |||
|||||      }ddd        |j                  |j                        } |t!        d         |      }t#        dt!        ||j%                         z
              }d|	j                  dd      |
||||j                  dd      |d   |d   |j                  d      |j'                          |j(                  ||j                        j'                         |d}t+        t-        |             5   ||       ddd       t        5  d|	d<   ddd       t.        j1                  d|        y# 1 sw Y   xY w# 1 sw Y   <xY w# 1 sw Y   NxY w# 1 sw Y   FxY w# t2        $ rU}t.        j5                  d | |       t        5  d!|	d<   t7        |      |	d"<   ddd       n# 1 sw Y   nxY wY d}~yY d}~y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   Nri  r  r@  r.  rM  rO  r<  rI  rH  TrJ  )re  ri  r  r@  r.  rN  rM  rN  )ry  r
  r
  r
  rg  rh  r
  r  resource_urlri  )rU   r
  ri  rj  r  r  rg  r
  r  ry  rk  r	  r  r0  rc  z2oauth/device: minimax login completed (session=%s)z0minimax device-code poll failed (session=%s): %sr  r	  )r  rt  ru  rv  rw  rx  r   r   r	  r
  r
  r   r	  r	  ry  r	  r  r\  rF
  r	  r	  r  r  rQ   rR   r\   rS  r  )r  rt  ru  rv  rw  rx  r   r   r	  r
  ri  r  r@  r.  rM  rO  re  ro  ry  r	  expires_in_srq  r  s                          rV   rZ  rZ  '  sm     ,	 /"":./,-O[!I[!I)M((=)K*+N3+\\MM$'12!  
 	 , /##+)'J	& hll8<<(:
<()s
 1c-#--/"ABC'hhx2."@"($..x@&~6'8&NN>:==?0(00(,,ik&

" 2:>? 	1$Z0	1! 	('DN	(		F
Sq/ /	 	R	1 	1	( 	(  +GUVW! 	+$DN$'FD!	+ 	+ 	+ 	+ 	++s   G-%H G"C1H 	G)H *G50H GG&!H )G2.H 5G>:H 	I
I'I;	II		IIc           	      f   ddl }ddlm}m}m}m} t        5  t        j                  |       }ddd       sy|d   }t        |d         }t        dt        |d   t        j                         z
              }		  |d      }
|j                  |j                  d      d	d
i      5 } |||
d   ||	|      }ddd       t        j                  dd      xs d      j                         t        |j                  dd      xs d      j                         t        |j                  dd      xs d      j                         |j                  d      t        |j                  d      xs d      j                         xs dd}t!        t#        |             5   |||
t%        j&                  t(        j*                        j-                         j/                  dd      d        |dd       ddd       t        5  d|d<   ddd       t0        j3                  d|        y# 1 sw Y   xY w# 1 sw Y   yxY w# 1 sw Y   NxY w# 1 sw Y   FxY w# t4        $ rU}t0        j7                  d| |       t        5  d|d<   t        |      |d <   ddd       n# 1 sw Y   nxY wY d}~yY d}~y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_sourcer
  rJ   rf  r	  rS  rI  rH  r=  token_endpoint)r  r
  r  rC  r
  r   r  id_tokenr  rg  rh  )r
  r  r  r  rg  r	  r	  oauth_device_code)	discoveryr
  r
  r
  r0  rc  z.oauth/device: xai login completed (session=%s)z,xai device-code poll failed (session=%s): %sr  r	  )r	  r  r|  r}  r~  r  r
  r
  r   r  r\  r}  r	  r	  r  r   r  r  r   ry  r   r	  r	  r  rQ   rR   r\   rS  )r  r	  r|  r}  r~  r  r
  r
  rJ   r  r  re  ro  tokensr  s                  rV   r[  r[  <(  s     
 /"":./}%K4
#$HRT,/$))+=>?J,+(.	\\MM$'12  
 
	 5()9:'%&J	
	  
~r B HbIOOQ !D!JKQQSJNN:r:@bAGGI$..6jnn\:FhGMMO[S[
 2:>? 	E"#%\\(,,7AACKKHVYZ-	 )mD!	E" " 	('DN	(		BJO]/ /
	 
	$	E 	E"	( 	(  +CZQRS! 	+$DN$'FD!	+ 	+ 	+ 	+ 	++s   H 9,I %H-6C"I AH:-I ;II  H*-H72I :I?I II 	J0J+8J	J+J	J++J0c           	         	 | j                         }t        |t              r|j	                  d      }t        |t              rrdD cg c]T  }t        |j	                  |d            j                         r)t        |j	                  |d            j                         V }}|rdj                  |      S t        |t
              r |j                         r|j                         S dD ]F  }|j	                  |      }t        |t
              s%|j                         s6|j                         c S  t        t        | dd      xs d      j                         }|dd S # t        $ r d}Y Zw xY wc c}w )	z8Best-effort extraction of a short provider error detail.Nr  )r4  error_descriptionrR  r!  r   : )r   r4  r  r  rY  )	rd  r\   r  r  r   r  r   r  r   )ri  r  r  r  r  r  r  s          rV   _http_response_error_detailr  |(  sA   ))+ '4 G$eT" Luyyb)*002 EIIc2&'--/E 
 yy''eS!ekkm;;= = 	%CKK$E%%%++-{{}$	% wtVR(.B/557D:'  
s   E AE0E-,E-c                     t        | dd      }t        |       }|j                         }d|v rd|v sd|v rd}nd}|r| d| d	| d
S | d| d
S )z8Dashboard-facing OpenAI Codex device-code start failure.r   r  deviceauthorienablezOpenAI rejected the device-code login request. Your OpenAI account may need device-code authorization enabled before Hermes can start this dashboard login. Enable device-code authorization in OpenAI, then return here and click Login again.zOpenAI rejected the device-code login request. Please try Login again from the dashboard after checking your OpenAI account settings.z (HTTP r  r?  )r   r  r   )ri  rc  r   r   r4  s        rV   _codex_device_code_start_errorr  (  s    T=)4F(.FLLNE5i50H4EA 	T 	 '&F8155YgfXQ''rX   c           
         	 ddl }ddlm}m}m} d}|j                  |j                  d            5 }|j                  | dd|id	d
i      }ddd       j                  dk7  rt        t        |            |j                         }|j                  dd      }	|j                  dd      }
t        dt        |j                  dd                  }|	r|
st        d      | d}t        5  t         j                  |       }|s
	 ddd       y|	|d<   ||d<   |
|d<   ||d<   d|d<   t#        j"                         |d   z   |d<   ddd       t#        j$                         d   z   }d}|j                  |j                  d            5 }t#        j$                         |k  rxt#        j&                  |       |j                  | d|
|	dd	d
i      }|j                  dk(  r|j                         }n'|j                  dv rwt        d|j                         ddd       |t        5  d|d<   d|d <   ddd       y|j                  d!d      }|j                  d"d      }|r|st        d#      |j                  |j                  d            5 }|j                  |d!|| d$||d%d	d&i'      }ddd       j                  dk7  rt        d(|j                         |j                         }|j                  d)d      }|j                  d*d      }|st        d+      dd,lm} t+        t-        |             5   |||d-       ddd       t        5  d.|d<   ddd       t.        j1                  d/|        y# 1 sw Y   .xY w# 1 sw Y   ExY w# 1 sw Y   }xY w# 1 sw Y   yxY w# 1 sw Y   xY w# 1 sw Y   sxY w# 1 sw Y   kxY w# t2        $ rl}t.        j5                  d0| |       t        5  t         j                  |       }|rd1|d<   t7        |      |d <   ddd       n# 1 sw Y   nxY wY d}~yY d}~yd}~ww xY w)2u  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<  rZ  z!/api/accounts/deviceauth/usercoder  rV
  rH  )rd  r   r\  r@  r   device_auth_idr   rJ   5z8device-code response missing user_code or device_auth_idz/codex/devicerB  r
  r  r	  z/api/accounts/deviceauth/token)r  r@  >   r  r   zdeviceauth/token poll returned r	  rc  z#Device code expired before approvalr	  r,  r.  z=device-auth response missing authorization_code/code_verifierz/deviceauth/callback)r-  rR  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  r0  z7oauth/device: openai-codex login completed (session=%s)z0codex device-code worker failed (session=%s): %sr  )r	  r  r  r  r  r	  r	  postr   r  r  rd  r   r\  r  r
  r
  r}  rX  rY  r  r  r  rQ   rR   r\   rS  r  )r  r	  r  r  r  issuerre  ri  r]  r@  r  rC  rB  r
  r_  	code_respr  r,  r.  
token_respr  r
  r  r  r  rx  s                             rV   rW  rW  (  s   e,	
 	

 + \\%--"5\6 	&;;(;<!#89');<  D	 s"=dCDDiikOOK4	$)92>As;??:s#CDEYZZ$X]3! 		B"&&z2D		B 		B !*D'7D#$%3D!",D!(D!%tL/A!AD		B >>#d<&88	\\%--"5\6 	Y&.."X-

=){{h<=,:S+-?@ # 
 ##s* $		I##z1"%DTEUEUDV#WXX	Y % N!*X(M_%N  ']]+?D!or:!^__\\%--"5\6 	&%"6.'-h.B$C!6%2 ()LM % 
J	 !!S(!9*:P:P9QRSS"zz."5

?B7KLL62:>? 	 ,!.  	
 " 	('DN	(		KZXi	 			B 		B	Y 	Y N 	 	,	 	
	( 	(  ,GUVW! 	,##J/A%(%(V/"		, 	, 	, 	, 	,,s   1O M;BO -NO 6NAO 
BNO *N"5O >AO  N.2B
O <N:O OO ;N O NO NO "N+'O +O .N73O :O?O OO 	QQ8+P,#	Q,P5	1QQz(/api/providers/oauth/{provider_id}/startc                 0   K   t        |       t                t        |       t        D ch c]  }|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{   S 	 t	        dd      c c}w 7 # t        $ r  t        $ r1}t        j                  d        t	        dt        |            d}~ww xY ww)z.Initiate an OAuth login flow. Token-protected.r  r   Unknown provider r   c              3   4   K   | ]  }|d    k(  s|  yw)r  Nr  )r   r   r  s     rV   r   z$start_oauth_login.<locals>.<genexpr>0)  s     Vqqw+?UV   r
  r
  z uses an external CLI; run `r
  z
` manuallyr
  r
  r  r
  Nzoauth/start %s failedrY  zUnsupported flow)r   r
  r
  r
  r7   r   r(  rb  r\   rQ   rK  r  )r  r   r  r   validcatalog_entryr  s   `      rV   start_oauth_loginr  #)  s6     7G$56QtW6E6%6G}4UVVV$;VVMV
*!]">}]?[>\\fg
 	
<  F*{k/I(99 M10gNNN 2 C0B
CC3 7& O  <.<CF;;<sR   )DC	ADC DC 6C7C :DC D",DDDc                   "    e Zd ZU eed<   eed<   y)OAuthSubmitBodyr  rR  Nr  r  rX   rV   r  r  I)  s    O
IrX   r  z)/api/providers/oauth/{provider_id}/submitc                    K   t        |       | dk(  rGt        j                         j                  dt        |j
                  |j                  |       d{   S t        dd|        7 w)z5Submit the auth code for PKCE flows. Token-protected.r
  Nr   zsubmit not supported for r   )r   ro   r  ru   r6  r  rR  r7   )r  rj  r   r  s       rV   submit_oauth_coder  N)  si      7k!--/??($//499g
 
 	
 C2KK=0Y
ZZ
s   AA,A*A,z4/api/providers/oauth/{provider_id}/poll/{session_id}c                   K   t         5  t        j                  |      }ddd       st        dd      |d   | k7  rt        dd      ||d   |j                  d	      |j                  d
      dS # 1 sw Y   UxY ww)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   rU   r   zProvider mismatch for sessionrc  r	  r	  )r  rc  r	  r	  )r
  r
  r   r7   )r  r  r  r
  s       rV   poll_oauth_sessionr  ^)  s      
 /"":./4RSSJ;&4STT x./2hh|,	 / /s   A?A3AA?3A<8A?z*/api/providers/oauth/sessions/{session_id}c                    K   t        |       t        5  t        j                  | d      }ddd       dddS d| dS # 1 sw Y   xY ww)z0Cancel a pending OAuth session. Token-protected.NFzsession not found)r  r4  T)r  r  )r   r
  r
  rP  )r  r   r  r
  s       rV   cancel_oauth_sessionr  x)  sU      7	 5"":t45|(;<<j11	5 5s   A?AAAc           
         d }|j                  |       }|r|j                  |      sdg fS t        |dd      xs+ t        |dd      xs t        |dd      xs t        |dd      }g }|V|j                  d|f      j	                         }|D ]/  }|j                   ||dd	       ||d
d       ||dd      d       1 n|j                  dd	d      }i }|D ]K  }|j                  d      }	|j                  d
      }
|	s(|
s+|j                  |
g       j                  |       M d }|}|g}|h}|j                  |      r|||   D cg c]  }|j                  d      |vs| }}|s	 ||fS |j                  |d       |d	   d   }|j                  |       |j                  |       |j                  |      r|||fS c c}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.
    c                     t        | t              r| j                  |      S 	 | |   S # t        $ r 	 | |   cY S # t        $ r Y Y y w xY ww xY wr  )r  r  r   r\   )r  r  indexs      rV   row_getz+_session_latest_descendant.<locals>.row_get)  sX    c4 773<	s8O 	5z! 	s)   ( 	A
9A
	AA
AA
Nconn_conn
connection_connectiona  
            WITH RECURSIVE descendants(id, parent_session_id, started_at) AS (
                SELECT id, parent_session_id, started_at FROM sessions WHERE id = ?
                UNION
                SELECT s.id, s.parent_session_id, s.started_at
                FROM sessions s
                JOIN descendants d ON s.parent_session_id = d.id
            )
            SELECT id, parent_session_id, started_at FROM descendants
            r  r   r  r   rv  r$  )r  r  rv  '  T)rq  r  rr  c                 ^    	 t        | j                  d      xs d      S # t        $ r Y yw xY w)Nrv  r           )r  r   r\   r  s    rV   startedz+_session_latest_descendant.<locals>.started)  s2    	.3!44 		s     	,,r  )resolve_session_idr  r   executefetchallr  r|  r   r  r  r  )r  r  r  r  r  r"  raw_rowsr  childrenridr  r  r  r   r  r
candidatess                    rV   _session_latest_descendantr  )  s   	 



+CbnnS)Rx 	FD! 	,2w%	,2|T*	, 2}d+	 	 D<<	 F
 (* 	  	CKKc4+%,S2Eq%I%c<; 	 $$5$NH 8ggdm,-6+2237	8 G5D5D
,,w
!)'!2NAaeeDk6MaN
N D= 	GT2Q-%G ,,w
 D= Os   	G"Gc                   2    e Zd ZU ee   ed<   dZee   ed<   y)BulkDeleteSessionsr  Nr  r  r  rX   rV   r  r  )  s    	cN!GXc]!rX   r  c                   <    e Zd ZU eeeef      ed<   dZe	e   ed<   y)SessionImportr  Nr  )
r  r  r  r   r   r  r   r  r  r   r  rX   rV   r  r  )  s#    4S>""!GXc]!rX   r  c                    K   t               }| j                         2 3 d {   }t        |      t        |      z   t        kD  rt	        dd      |j                  |       F7 A6 t        |      S w)NrB  z#Session import payload is too larger   )	bytearrayr
  r   _SESSION_IMPORT_MAX_BYTESr7   r  r  )r   rj  r  s      rV   _read_session_import_bodyr  )  sh     ;D~~'  et9s5z!$==C8]^^E' ;s%   A1A%A#A%>A1#A%%A1c                     t        |       }	 |j                  |      |j                          S # |j                          w xY wr  )r~  import_sessionsr   )r  r  r  s      rV   _import_sessions_for_profiler  *  s2    	%g	.B!!(+


s	   . A z/api/sessions/bulk-deletec                     K   t         j                        dkD  rt        dd      dt        f fd}t	        j
                  |       d{   }d|d	S 7 	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.
    rY  r   z$ids must contain at most 500 entriesr   rK   c                      t        j                        } 	 | j                  j                        | j	                          S # | j	                          w xY wr  )r~  r  delete_sessionsr  r   )r  rj  s    rV   _deletez.bulk_delete_sessions_endpoint.<locals>._delete3*  s;    )$,,7	%%dhh/HHJBHHJs   A ANTr  deleted)r   r  r7   r  ro   r  )rj  r  r  s   `  rV   bulk_delete_sessions_endpointr  	*  s^     J 488}s9
 	
S  %%g..G7++ /s   A
AA
Az/api/sessions/importc                   K   	 t        |        d{   }t        j                  |      }	 t        j                  t        |j                  |j                         d{   }|j                  dd      st        d|      |S 7 r# t        $ r  t        $ r}t        dd      |d}~ww xY w7 O# t        $ r}t        dt        |            |d}~ww xY ww)a"  Import one or more sessions exported from the dashboard or CLI.

    This is intentionally separate from ``/api/ops/import``: that endpoint
    restores a whole Hermes backup archive, while this endpoint is scoped to
    session rows/messages and is safe to use from the Sessions page.
    Nr   zInvalid session import payloadr   r  F)r  r  model_validate_jsonr7   r  ro   r  r  r  r  r  r   )r   raw_bodyrj  r  r3  s        rV   import_sessions_endpointr  >*  s     _27;;00:G(()Et||UYUbUbcc ::dE"F;;M <  _4TU[^^_ d GCH=3FGsg   CB BB 2B1 B/ B1 $!CB B,B''B,,C/B1 1	C:CCCz/api/sessions/empty/countc                 f    K   dt         f fd}dt        j                  |       d{   iS 7 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.
    rK   c                      t              } 	 | j                         | j                          S # | j                          w xY wr  )r~  count_empty_sessionsr   r  r  s    rV   _countz-count_empty_sessions_endpoint.<locals>._count`*  s1    )'2	**,HHJBHHJ	   . A r  Nr  ro   r  )r  r  s   ` rV   count_empty_sessions_endpointr  X*  s1     C  7,,V44554s   &1/1z/api/sessions/emptyc                 l    K   dt         f fd}t        j                  |       d{   }d|dS 7 	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.
    rK   c                      t              } 	 | j                         | j                          S # | j                          w xY wr  )r~  delete_empty_sessionsr   r  s    rV   r  z/delete_empty_sessions_endpoint.<locals>._delete~*  s1    )'2	++-HHJBHHJr  NTr  r  )r  r  r  s   `  rV   delete_empty_sessions_endpointr  j*  s8     (S  %%g..G7++ /s   %42
4z/api/sessions/statsc                   K   t        |       }	 |j                  d      }|j                  d      }|j                  d      }|j                         }i }	 |j                  ddd      D ]8  }t	        |j                  d      xs d      }|j                  |d	      d
z   ||<   : 	 |||||d|j                          S # t        $ r Y #w xY w# |j                          w xY w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)rv  F)rw  r  )rq  rv  rr  r   r  r   r   )r  active_storerg  messages	by_source)r~  r  message_countr|  r  r   r\   r   )	r  r  r  r  rg  r  r  rx  r  s	            rV   get_session_statsr  *  s      
&g	.B  $ 7'''?##$#7##%$&		**\`*a ;!%%/2U3!*sA!6!:	#; (  "
 	
  		 	
sB   C$AC AC  'C /C$ 	C	C CC C!!C$c                 f    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  )rz  rn  r}  r   )r  rn  _namer9  s       rV   r~  r~  *  s4     '{$W-KE4T$Z*455rX   z/api/sessions/{session_id}c                    K   t        |      }	 |j                  |       }|r|j                  |      nd }|st        dd      |rt	        |      d   |d<   ||j                          S # |j                          w xY ww)Nr   Session not foundr   r   r  )r~  r  r  r7   r}  r   )r  r  r  r  r   s        rV   get_session_detailr  *  st     	%g	.B	##J/),"..%$C8KLL!3G!<Q!?GI


s   A>A	A) A>)A;;A>z,/api/sessions/{session_id}/latest-descendantc                     K    fd}t        j                  |       d {   \  }}|st        dd      |r|d   n ||t        |xr ||d   k7        dS 7 7w)Nc                  ~    t              } 	 t        |       | j                          S # | j                          w xY wr  )r~  r  r   )r  r  r  s    rV   _lookupz.get_session_latest_descendant.<locals>._lookup*  s/    )'2	-j"=HHJBHHJs   * <r   r  r   r   )requested_session_idr  r   r  )ro   r  r7   r   )r  r  r  latestr   s   ``   rV   get_session_latest_descendantr  *  sl     
 !**733LFD4GHH+/QZ247!23	  4s   AA8Az#/api/sessions/{session_id}/messagesc                     K    fd}t        j                  |       d {   }|t        dd      |\  }}}|||t        |      ddS 7 ,w)Nc                      t              } 	 | j                        }|s	 | j                          y | j                  |      }t	        d      nd }||| j                  ||      f| j                          S # | j                          w xY w)NrY  rq  r  )r~  r  r   resolve_resume_session_idr  get_messages)r  r  _limitrq  r  r  r  s      rV   _readz#get_session_messages.<locals>._read*  s    )'2		''
3C HHJ ..s3C(-(9S_tF6& QQHHJBHHJs   A; 7A; ;Br   r  r   )rq  r  returned)r  r  
pagination)ro   r  r7   r   )	r  r  rq  r  r  r3  r  r  r  s	   ````     rV   get_session_messagesr  *  sg      $$U++F~4GHH"CH
 	 ,s   !AA-Ac                 X    K    fd}t        j                  |       d {   S 7 w)Nc                      t              } 	 | j                        }|sddd| j                          S | j                  |       ddi| j                          S # | j                          w xY w)NT)r  already_absentr  )r~  r  r   delete_sessionr  r  r  r  s     rV   r  z(delete_session_endpoint.<locals>._delete+  sd    )'2	 ''
3C"d; HHJ c"$<HHJBHHJs   A A A-)ro   r  )r  r  r  s   `` rV   delete_session_endpointr    +  s%     
( ""7++++s   *(*c                   J    e Zd ZU dZee   ed<   dZee   ed<   dZ	ee   ed<   y)SessionRenameNr   rg  r  )
r  r  r  r   r   r  r  rg  r   r  r  rX   rV   r  r  +  s.    E8C=#Hhtn# "GXc]!rX   r  c                 ~  K   t        |j                        }	 |j                  |       }|st        dd      |j                  |j
                  t        dd      |j                  !	 |j                  ||j                  xs d       |j
                  |j                  ||j
                         d|j                  |      xs dd	}|j
                  t        |j
                        |d
<   ||j                          S # t        $ r}t        dt        |            d}~ww xY w# |j                          w xY w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)r  r   rg  )r~  r  r  r7   r   rg  set_session_titler  r  set_session_archivedget_session_titler   r   )r  rj  r  r  r  r3  s         rV   rename_session_endpointr  $+  s      
&dll	3B##J/C8KLL::$--"7N  ::!D$$S$***:; ==$##C7r';';C'@'FBG==$!%dmm!4F:

  D#CFCCD 	
sB   D=AD( + D A%D( 0D=	D%
D  D%%D( (D::D=z!/api/sessions/{session_id}/exportc                 z    K    fd}t        j                  |       d{   }|t        dd      |S 7 w)z6Export a single session (metadata + messages) as JSON.c                      t              } 	 | j                        }|r| j                  |      nd 	 | j                          S # | j                          w xY wr  )r~  r  export_sessionr   r  s     rV   _exportz(export_session_endpoint.<locals>._exportI+  sJ    )'2	''
3C-02$$S)d:HHJBHHJs   %A ANr   r  r   )ro   r  r7   )r  r  r  r  s   ``  rV   export_session_endpointr  F+  s>      ""7++D|4GHHK ,s   ;9;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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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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d<   dZee   ed<   dZeed<   dZ eed<   y)SessionPruneZ   older_than_daysNr   r  started_beforestarted_after
title_liker  rj  rf  max_messages
model_likerU   r  chat_id	chat_typebranch_like
min_tokensr)  min_costmax_costmin_tool_callsmax_tool_callsFrv  dry_run)!r  r  r  r  r   r  r  r   r  r  r  r  r  r  rj  rf  r  r  r  rU   r  r  r  r  r  r)  r  r  r  r  rv  r   r  r  rX   rV   r  r  W+  sK   ')OXe_) FHSM !GXc]!&*NHUO*%)M8E?) $J$ $J$ $J$"&L(3-&"&L(3-& $J$"Hhsm"!GXc]!!GXc]!#Ix}#!%K#% $J$ $J$ $Hhuo$ $Hhuo$$(NHSM($(NHSM("d"GTrX   r  c                      j                   duxs  j                  du} j                   j                  dk  r|st        dd      t	         fddD              }d j
                  v } j                  }|s|r|sd} j                  rt         j                        d   n	t               }t         j                        }	 t        d,i d|d	 j                  xs dd
 j                   d j                  d j                  xs dd j                  xs dd j                  xs dd j                  d j                   d j"                  xs dd j$                  xs dd j&                  xs dd j(                  xs dd j*                  xs dd j,                  xs dd j.                  d j0                  d j2                  d j4                  d j6                  d j8                  d j:                  rdnd} j<                  r |j>                  d,i |}dd tA        |      |r|d    d!   nd|r|d"   d!   nd|D 	cg c]5  }	|	d#   |	d	   |	jC                  d$      |	jC                  d%      |	d!   |	d&   d'7 c}	d(|jE                          S |d)z  }
 |jF                  d,d*|
jI                         r|
ndi|}d|d+|jE                          S c c}	w # |jE                          w xY w)-zIDelete ended sessions matching filters (mirrors `hermes sessions prune`).Nr   r   zolder_than_days must be >= 1r   c              3   :   K   | ]  }t        |      d u  y wr  )r   )r   r  rj  s     rV   r   z"_prune_sessions.<locals>.<genexpr>}+  s%      	 	a$	s   )r   r  r  rj  rf  r  r  rU   r  r  r  r  r  r)  r  r  r  r  r  r   r  r  r  r  rj  rf  r  r  rU   r  r  r  r  r  r)  r  r  r  r  rg  FTr   rv  r   r  r   r#  r  )r  r   r   r#  rv  r  )r  removedmatchedoldest_started_atnewest_started_atr  r  sessions_dir)r  r!  r  )%r  r  r  r7   r  model_fields_setr  r}  r   r~  r  r   r  r  rj  rf  r  r  rU   r  r  r  r  r  r)  r  r  r  r  rv  r  list_prune_candidatesr   r   r   prune_sessionsr{  )rj  
has_window_attr_filters_set_older_than_explicit_effective_older_thanr  r  filtersr"  r  r%  r!  s   `           rV   _prune_sessionsr.  s+  s`    	4'I4+=+=T+I  'D,@,@1,DZ4RSS  	
	 	 -0E0EE 00'0D $:>,,%dll3A6OL]L	%dll	3B5 
1
KK'4
  ..
 ,,	

 /4
 /4
 /4
 **
 **
 /4
 mm+t
 \\)T
 \\)T
 ~~-
 ))1T
  !
" #
$ ]]%
& ]]'
(  ..)
*  ..+
, "22T-
0 <<+2++6g6Dt9>BT!W\%:?CT"Xl%; "
   g"#H+!"w!"w&'o)*?);
4 	
 $j0#"## 
)5)<)<)>D

 w/

'
& 	
s%   FK% :K K%  /K%  K% %K7z/api/sessions/prunec                 R   K   t        j                  t        |        d{   S 7 w)zGDelete ended sessions matching filters without blocking the event loop.N)ro   r  r.  rl  s    rV   prune_sessions_endpointr0  +  s!      ""?D9999s   '%'z	/api/logslevel	componentr  c                   K   ddl m}m} |j                  |       }|st	        dd|        t               dz  |z  }|j                         s| g dS 	 ddlm}	 |r|j                         d	k7  r|nd }
|rQ|j                         d
k7  r>|	j                  |      }|-t	        dd| ddj                  t        |	                   d }t        |
xs |xs |      } |||st        |d      nd||
|      }|rA|j                         }|D cg c]  }||j                         v s| c}t        |d       d  }| |dS # t        $ r i }	Y w xY wc c}w w)Nr   )
_read_tail	LOG_FILESr   zUnknown log file: r   r  )r  r  )COMPONENT_PREFIXESALLr  zUnknown component: r
  r  rY  r]  )has_filters	min_levelcomponent_prefixes)hermes_cli.logsr4  r5  r   r7   r   r{  hermes_loggingr6  rb   r	  r   r  r  r   r  )r  r  r1  r2  r  r4  r5  log_namer  r6  r9  comp_prefixesr8  r3  needlels                   rV   get_logsrA  +  sw     6}}T"H6H4OPP 6)H4H??r** 5 5;;=E#9tIY__&%/*..y9 ,YK 8%%)YYv6H/I%J$KM  y;M;V<K#eS/T(	F #;v':!;S_<L<MN6**?    < <s=   AED> B2EE$E(E>E	EEE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<   y)CronJobCreater   rF   schedulerS   r*  deliverNrf  r#  rU   r  scriptcontext_fromr  workdirFno_agent)r  r  r  rF   r  r  rS   rE  rf  r   r   r#  rU   r  rF  rG  r   r  rH  rI  r   r  rX   rV   rC  rC  
,  s    FCMD#NGS"&FHT#Y&E8C="Hhsm""Hhsm" FHSM "&L(3-&,0htCy)0!GXc]!HdrX   rC  c                       e Zd ZU eed<   y)CronJobUpdaterq  N)r  r  r  r  r  r  rX   rV   rK  rK  ,      MrX   rK  strip_trailing_slashrN  c                l    | y t        |       j                         }|r|j                  d      }|xs d S )Nr   )r  r   r^  )r  rN  r  s      rV   _cron_optional_textrP  ,  s7    }u:D{{3<4rX   c                     | y t        | t              rt        j                  d|       }nt        | t        t
        f      r| }ny |D cg c]5  }t        |      j                         st        |      j                         7 }}|xs d S c c}w )Nz[\n,])r  r  r  r   r  r  r   )r  	raw_itemsr  r  s       rV   _cron_string_listrS  ',  su    }%HHXu-		ED%=	)	+4J4D	8ISY__JEJ=D Ks   	B(Bc                    t        |       }|sy|dz  j                         }t        |      j                         }|j	                         r|j                         n||z  j                         }	 |j                  |      }|j                         st        dd|       |j                         st        dd|       t        |      S # t        $ r}t        dd|       |d}~ww xY w)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: )rP  r  r   r  r  relative_tor  r7   r{  rH  r  )r  r  r  scripts_rootr  r  relativer  s           rV    _normalize_dashboard_cron_scriptrY  4,  s    u%D 9,557LDz$$&H&.&:&:&<  "<RZCZBcBcBeI((6 6Mi[4YZZ6LYK4XYYx=  +L>:
 	s   /C 	C*C%%C*jobc                 Z   t        | j                  d            }t        | j                  d            }t        | j                  d            xs t        | j                  d            }t        | j                  d            }|r|st	        dd      y |s|s|st	        dd	      y y y )
NrF   rF  rf  skillrI  r   zno_agent=True requires a scriptr   z2agent cron jobs require a prompt, skill, or script)rP  r   rS  r   r7   )rZ  rF   rF  rf  rI  s        rV   &_validate_dashboard_cron_effective_jobr]  K,  s     !23F !23Fswwx01X5FswwwGW5XFCGGJ'(H8  	fG
 	
 !'fFrX   c                 >   t        | xs i       }dD ]  }||v st        ||         ||<    d|v rt        |d   |      |d<   d|v rt        |d   d      |d<   d|v rt        |d         xs 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#  rU   rH  rF  r  TrM  rE  r*  rG  r  )r  rP  rY  rS  )rq  r  r  r  s       rV   !_normalize_dashboard_cron_updatesr_  `,  s     gm$J/ C*1*S/BJsOC :?x  

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

: J 3Jy4I J Ug
9#%6z.7Q%R
>"Z')::FX;Y)Z
%&rX   refsr  c           	      Z    | sy | D ]#  }t        |d|      rt        dd| d| d       y )Nget_jobr   zcontext_from job 'z' not found in profile 'r  r   )_call_cron_for_profiler7   )r`  r  rb  s      rV   %_validate_dashboard_cron_context_fromrd  ,  sM      %lIsC( .$~Q( rX   c                      ddl m}  	 | j                         D cg c]  }t        |       c}S c c}w # t        $ r# t
        j                  d       t        |       cY S w xY w)zCReturn dashboard profile records, falling back to a directory scan.r   r  zJFailed to list profiles for cron dashboard; falling back to directory scan)r]   r  r  _profile_to_dictr\   rQ   rK  _fallback_profile_dicts)r  r   s     rV   _cron_profile_dictsrh  ,  sT    35-9-G-G-IJ #JJJ 5cd&|445s   4 /4 4 )A A c                  L    	 ddl m}   |        }|dv rdS |S # t        $ r Y yw xY w)uC  Profile to target when a cron request carries no explicit ``profile``.

    A desktop pool backend runs one process per profile (HERMES_HOME already
    scoped), but these cron endpoints deliberately route storage through the
    profiles tree via ``_cron_profile_home`` — so a hardcoded ``"default"``
    fallback would write a non-default profile's job into ``~/.hermes``.
    Resolve the process's own profile instead. ``custom`` (an unrecognized
    HERMES_HOME outside the profiles tree) has no profile-dir equivalent, so
    it keeps the legacy ``default`` fallback.
    r   rz
  r?  r|
  )r  r{
  r\   )r{
  rS   s     rV   _cron_default_profilerj  ,  s;    ?&(  559?4?  s    	##c                 L   ddl m} | xs
 t               j                         xs d}	 |j	                  |      }|j                  |       |j                  |      st        dd| d	      ||j                  |      fS # t        $ r}t        dt        |            d}~ww xY w)
z=Resolve a profile query value to (profile_name, HERMES_HOME).r   r  r?  r   r   Nr   	Profile '' does not exist.)r]   r  rj  r   normalize_profile_namevalidate_profile_namer  r7   r  profile_existsr  )r  r  r  canonr  s        rV   r}  r}  ,  s    3-+-
4
4
6
C)C<33C8**51 &&u-iwFW4XYY,..u555	  <CF;;<s   "A? ?	B#BB#r9  c                 \    t        |       }||d<   ||d<   t        |      |d<   |dk(  |d<   |S )Nr  r  r  r?  r{  )r  r  )rZ  r  r9  	annotateds       rV   _annotate_cron_jobrt  ,  sC    S	I"Ii 'In"4yIm&-&:I"#rX   r
  	func_namec                    t        |       \  }}ddlm} ddlm}m}  |t        |            }		 |j                  |      5   t        ||      |i |}
ddd        ||	       t        
t              r|
D cg c]  }t        |||       c}S t        |
t              rt        |
||      S |
S # 1 sw Y   [xY w#  ||	       w xY wc c}w )a,  Run cron.jobs helpers against the selected profile's cron directory.

    The dashboard is a single process that can inspect many profiles. Route
    storage through cron.jobs' execution-context override so dashboard calls
    cannot retarget a concurrent desktop ticker's load/save transaction.
    r   jobsreset_hermes_home_overrideset_hermes_home_overrideN)r}  rg  rx  rl  rz  r{  r  use_cron_storer   r  r  rt  r  )r
  ru  rk   kwargsr  r9  	cron_jobsrz  r{  r   r3  js               rV   rc  rc  ,  s     ,N;L$&
 %SY/E*%%d+ 	D2WY	2DCFCF	D 	#5)&$CIJa"1lD9JJ&$!&,==M	D 	D 	#5) Ks)   B9  B-B9 7C-B62B9 9
Cjob_idc                      t               D ]H  }t        |j                  d      xs d      }|s$t        |dd      }t	         fd|D              sF|c S  y )NrS   r   	list_jobsTc              3   r   K   | ].  }|j                  d       k(  xs |j                  d      k(   0 yw)r  rS   Nr   )r   r  r  s     rV   r   z)_find_cron_job_profile.<locals>.<genexpr>,  s3     NAquuT{f$?f(??Ns   47)rh  r  r   rc  r  )r  r  rS   rx  s   `   rV   _find_cron_job_profiler  ,  sY    &( 7;;v&,"-%dK>NNNK rX   c                 V   | xs dj                         }|j                         dk7  rt        |dd      S g }t               D ]@  }t	        |j                  d      xs d      }|s$	 |j                  t        |dd             B |S # t        $ r t        j                  d|       Y fw xY w)Nr  r  TrS   r   z'Failed to list cron jobs for profile %s)
r   r   rc  rh  r  r   r  r\   rQ   rK  )r  r
  rx  r  rS   s        rV   _list_cron_jobs_syncr  ,  s    !E((*IE!%idCC!#D#% L488F#)r*	LKK.t[$GHL K  	LNNDdK	Ls   &BB('B(c                    K   t        j                  |       rt        d      t        | g|i | d{   }t        j                  |      rt        d      |S 7 &w)zBRun cron dashboard profile/job I/O outside the FastAPI event loop.z2_run_cron_dashboard_io only accepts sync callablesNz:_run_cron_dashboard_io sync callable returned an awaitable)inspectiscoroutinefunctionr  rD   isawaitable)funcrk   r}  r3  s       rV   _run_cron_dashboard_ior   -  s[     ""4(LMM$T;D;F;;F6"TUUM <s   3AA'Az/api/cron/jobsc                 >   K   t        t        |        d {   S 7 wr  )r  r  r  s    rV   list_cron_jobsr  
-  s     '(<gFFFFs   c                 z    |xs t        |       }|st        dd      t        |d|       }|st        dd      |S )Nr   Job not foundr   rb  r  r7   rc  r  r  selectedrZ  s       rV   _get_cron_job_syncr  -  sE    808HODD
 9f
=CODDJrX   z/api/cron/jobs/{job_id}c                 @   K   t        t        | |       d {   S 7 wr  )r  r  r  r  s     rV   get_cron_jobr  -  s     '(:FGLLLL   c           	         |xs t        |       }| }|r.t        |d|       }|r|j                  d      rt        |d         }	 t	        dt        t        |      d            }t        |      }	 |j                  ||d      }t        j                         }	|D ]g  }
|
j                  d      d	u xr( |	|
j                  d
|
j                  dd            z
  dk  |
d<   t        |
j                  d            |
d<   |sc||
d<   i ||d|j                          S # t        t        f$ r d}Y w xY w# |j                          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.
    rb  r  r   r  r  r   r  rt  Nru  rv  rw  rz  rg  r  )runsrq  )r  rc  r   r  r\  r  r  r  r  r~  list_cron_job_runsr}  r   r   )r  r  rq  r  rC  rZ  limit_nr  r  ry  rx  s              rV   _list_cron_job_runs_syncr  -  sD    808HI$Xy&A3774=CIIaSZ-. 
&h	/B$$Yga$Hiik 	(Aj!T) O155lA0FGG3N kN !z!23AjM')	( w/

# z" " 	
s%   D .BD. ;D. D+*D+.E z/api/cron/jobs/{job_id}/runsc                 B   K   t        t        | ||       d {   S 7 wr  )r  r  )r  r  rq  s      rV   r  r  K-  s     '(@&'SXYYYY   c                    	 t        |      \  }}t        | j                  |      }t        | j                        }t        | j
                        }t        ||       t        | j                        }t        | j                  |||d       t        |d| j                  xs d| j                  | j                  t        | j                        xs d|t        | j                         t        | j"                        t        | j$                  d      ||t        | j&                        t        | j(                        |      S # t*        $ r  t,        $ r0}t.        j1                  d       t+        d	t3        |      
      d }~ww xY w)N)rF   rf  rF  rI  
create_jobr   r*  TrM  )rF   rD  rS   rE  rf  r#  rU   r  rF  rG  r  rH  rI  zPOST /api/cron/jobs failedr   r   )r}  rY  rF  rS  rf  rG  rd  r   rI  r]  rF   rc  rD  rS   rP  rE  r#  rU   r  r  rH  r7   r\   rQ   rK  r  )	rj  r  r  r  rF  rf  rG  rI  r  s	            rV   _create_cron_job_syncr  P-  s@   "<%7%@"l1$++|L"4;;/():):;-lLI&.kk 	0
 	 &;;$"]]'5@%djj1(7(TR%.t/D/DE'5
 	
"   <34CF;;<s   D<D? ?F+E<<Fc                 @   K   t        t        | |       d {   S 7 wr  )r  r  )rj  r  s     rV   create_cron_jobr  v-  s     '(=tWMMMMr  z/api/cron/delivery-targetsc                     K   dddddg} 	 ddl m} | j                   |              d	| iS # t        $ r t        j                  d       Y d	| iS w xY ww)
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  rS   home_target_sethome_env_varr   cron_delivery_targetsz%GET /api/cron/delivery-targets failedr  )cron.schedulerr  r  r\   rQ   rK  )r  r  s     rV   get_cron_delivery_targetsr  {-  sp      '# 		
G@8,./ w  @>?w@s%   	A, AA
AAAc                    |xs t        |       }|st        dd      	 t        |      \  }}t        |d|       }|st        dd      t	        |j
                  |      }d|v rt        |j                  d      |       h d}|j                  |      ri ||}	d|v r	d|vrd |	d<   t        |	       t        |d	| |      }
|
st        dd      |
S # t        $ r  t        $ r}t        d
t        |            |d }~ww xY w)Nr   r  r   rb  rG  >   r\  rF   rF  rf  rI  rf  r\  
update_jobr   )r  r7   r}  rc  r_  rq  rd  r   intersectionr]  r  r  )r  rj  r  r  r  r  r  rq  execution_fields	effectiverZ  r  s               rV   _update_cron_job_syncr  -  s*   808HODDG%7%A"l),	6JCHH3LL
 W$1N+ O((1/8/w/I7"wg'=%)	'"29=$\<Q
 ODDJ   GCH=3FGs   B C C?#C::C?c                 B   K   t        t        | ||       d {   S 7 wr  )r  r  )r  rj  r  s      rV   update_cron_jobr  -  s     '(=vtWUUUUr  c                 z    |xs t        |       }|st        dd      t        |d|       }|st        dd      |S )Nr   r  r   	pause_jobr  r  s       rV   _pause_cron_job_syncr  -  sE    808HODD
 ;
?CODDJrX   z/api/cron/jobs/{job_id}/pausec                 @   K   t        t        | |       d {   S 7 wr  )r  r  r  s     rV   pause_cron_jobr  -  s     '(<fgNNNNr  c                 z    |xs t        |       }|st        dd      t        |d|       }|st        dd      |S )Nr   r  r   
resume_jobr  r  s       rV   _resume_cron_job_syncr  -  sE    808HODD
 <
@CODDJrX   z/api/cron/jobs/{job_id}/resumec                 @   K   t        t        | |       d {   S 7 wr  )r  r  r  s     rV   resume_cron_jobr  -       '(=vwOOOOr  c                 z    |xs t        |       }|st        dd      t        |d|       }|st        dd      |S )Nr   r  r   trigger_jobr  r  s       rV   _trigger_cron_job_syncr  -  sE    808HODD
 =&
ACODDJrX   z/api/cron/jobs/{job_id}/triggerc                 @   K   t        t        | |       d {   S 7 wr  )r  r  r  s     rV   trigger_cron_jobr  -  s     '(>PPPPr  c                     |xs t        |       }|st        dd      	 t        |d|       }|st        dd      ddiS # t        $ r}t        dt	        |            |d }~ww xY w)Nr   r  r   
remove_jobr   r  T)r  r7   rc  r  r  )r  r  r  r!  r  s        rV   _delete_cron_job_syncr  -  sy    808HODDG(<H ODD$<	  GCH=3FGs   A   	A%	A  A%c                 @   K   t        t        | |       d {   S 7 wr  )r  r  r  s     rV   delete_cron_jobr  -  r  r  c                 >   t        |       \  }}ddlm} ddlm} ddlm}m}  |t        |            }	 |j                  |      5   |       }	t        |	j                  |dd            cddd        ||       S # 1 sw Y   nxY w	  ||       y#  ||       w xY w)a  Run ONE due cron job end-to-end for ``profile`` via the resolved
    scheduler provider's ``fire_due`` (store CAS claim + ``run_one_job``).

    Scope both cron storage and the runtime Hermes home so the job's store,
    config, credentials, scripts, skills, and output all belong to the selected
    profile. Runs with no live adapters; delivery falls back to the per-platform
    send path.
    r   rw  rM   ry  N)adaptersr  )r}  rg  rx  rP   rN   rl  rz  r{  r  r|  r   fire_due)
r  r  _profile_namer9  r~  rN   rz  r{  r   rU   s
             rV   _fire_cron_job_for_profiler  -  s     -W5M4&>
 %SY/E*%%d+ 	M-/H))&4d)KL	M 	M 	#5)		M 	M 	M 	#5)"5)s#   B $A<*	B <BB 
Bz/api/cron/firec                   K   ddl m} | j                  j                  dd      }|j	                  d      r|dd j                         nd}t               }  |       |t        |dd	d
d      t        |dd	dd      xs dt        |dd	dd      xs d      }|t        ddid      S 	 | j                          d{   }t        |t              r|xs i j                  d      nd}|st        ddid      S t        t        |       d{   }|st        d|dd      S t        j                   t        j"                  t$        ||             t        d|dd      S 7 # t        $ r i }Y w xY w7 gw)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   r  Nrg  chronosexpected_audiencer  nas_jwks_urlrk  )r   r  jwks_or_keyr  r  zinvalid fire tokenr   r   r  zmissing job_idr   gone)rc  r  r\  accepted   )%plugins.cron_providers.chronos.verifyr  r   r   r   r   r   r   r?   rd  r\   r  r  r  r  ro   r{   r  r  )	r   r  r   r   r  claimsrj  r  r  s	            rV   cron_fire_webhookr  .  s{    & H??3D $	 :DHNNE
-C  !#vy:MWYZCNBOWSWsFI|RHPD	F ~W&:;MM\\^# ,6dD+Adjbh'tFW&67SII
 ++A6JJG v@cRR 4gvF :@cRR+ $  KsJ   B'E7*E$ =E">E$ AE7E5AE7"E$ $E2/E71E22E7c                   0    e Zd ZU eed<   i Zeeef   ed<   y)AutomationBlueprintInstantiate	blueprintr  N)r  r  r  r  r  r  r   r   r  rX   rV   r  r  W.  s    NFDcNrX   r  z/api/cron/blueprintsc                    K   	 ddl m} m} d}	 ddlm}  |       D cg c]  }|j                  d      s|d    }}ddg|}g }| D ]N  } ||      }|r1|j                  dg       D ]  }	|	j                  d      dk(  s||	d<    |j                  |       P d|iS c c}w # t        $ r t        j                  dd	
       Y w xY w# t        $ r0}
t        j                  d       t        dt        |
            d}
~
ww xY w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  r  originr*  z?cron_delivery_targets unavailable; using static deliver optionsTr:  r  rS   rE  r  
blueprintszGET /api/cron/blueprints failedrY  r   )cron.blueprint_catalogr  r  r  r  r   r\   rQ   r@  r  rK  r7   r  )r  r  deliver_optionsr  r'	  r  r  r  r  r  r  s              rV   list_cron_blueprintsr  \.  s    <K	i<*?*AQQQUU4[4QIQ'=9=O  	"A+A.E8R0 7AuuV}	1'6)7 NN5!	" g&& R 	iJJXcgJh	i  <89CF;;<sf   D
C B" BBB" :C ?C DB" " CC CC 	D+C<<DDz /api/cron/blueprints/instantiatec                   K   	 ddl m}m}m}  || j                        }|t        dd| j                         	  ||| j                        }|j                  dd       t        j                  t        |d	fi |}t        |       d{   S # |$ r}t        dt        |            |d}~ww xY w7 (# t
        $ r  t        $ r0}	t        j                  d
       t        dt        |	            d}	~	ww xY ww)zDFill a blueprint's slots and create the cron job (form-submit path).r   )fill_blueprintget_blueprintBlueprintFillErrorNr   zUnknown blueprint: r   rt  r  r  z,POST /api/cron/blueprints/instantiate failedr   )r  r  r  r  r  r7   r  r  rP  	functoolspartialrc  r  r\   rQ   rK  )
rj  r  r  r  r  r  specr  _creater  s
             rV   instantiate_blueprintr  ~.  s     <\\!$..1	C:MdnnM]8^__	K!)T[[9D 	4  ##$:G\ZUYZ+G444 " 	KCCAsJ	K 5  <EFCF;;<sW   C<8B7 B <B7 B5B7 C<B2B--B22B7 7C9	+C44C99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Zee   ed	<   y)
MCPServerCreaterS   Nr   r  rk   r  r   rS
  r  )r  r  r  r  r  r   r   r  rk   r   r  r   r   rS
  rC   r  r  rX   rV   r  r  .  sn    
IC#!GXc]!D$s)Cc3hD(3-(,L(9%,!GXc]!rX   r  c                   D    e Zd ZU i Zeeeeef   f   ed<   dZe	e   ed<   y)MCPServersReplaceserversNr  )
r  r  r  r  r   r  r   r  r  r   r  rX   rV   r  r  .  s,    )+GT#tCH~%&+!GXc]!rX   r  c                 Z   ddl m}m} ddlm} | j
                  xs dj                         }|st        d      | j                  xs dj                         }| j                  xs dj                         }| j                  xs dj                         j                         }| j                  | j                  j                         nd}t        |      t        |      k(  rt        d      |d	vrt        d
|       i }	|r| j                  rt        d      | j                   rt        d      |dk(  r8|r ||      nd}
|
r|
j                         dk(  rt        d       ||      |	d<   n| j                  t        d      ||	d<   |dk(  rod|	d<   ni|dk7  s| j                  t        d      ||	d<   | j                  rt#        | j                        |	d<   | j                   rt%        | j                         |	d<    |||	      }|r t        d| ddj'                  |             ||	|fS )a  Validate a Dashboard MCP create request and build its safe config.

    The returned config never contains the submitted Bearer token. Callers
    persist the token with the shared Bearer helper only after they enter the
    intended profile scope. Keeping this conversion shared makes the
    standalone MCP page and the Profile Builder enforce the same
    transport/auth contract.
    r   )_bearer_auth_headers_strip_bearer_prefix)validate_mcp_server_entryr   zServer name is requiredr  Nz8Provide exactly one of URL (HTTP/SSE) or command (stdio)>   r  oauthr  zUnsupported auth mode: z2Arguments are only supported for stdio MCP serversz>Environment variables are only supported for stdio MCP serversr  r  zBearer token is requiredr   z+Bearer token requires header authenticationr   r  r   z:HTTP authentication is not supported for stdio MCP serversr  rk   r  Server 'z' rejected: ru  )hermes_cli.mcp_configr  r  hermes_cli.mcp_securityr  rS   r   r  r   r  r   r   rS
  get_secret_valuer   rk   r  r  r  r  )rj  r  r  r  rS   r   r  r   rS
  server_configr  issuess               rV   _normalize_mcp_server_creater  .  s.    BIIO""$D23388>r
 
 
"C||!r((*GII&&(..0D ( 	**,  CyDM!STT..24&9::$&M
99QRR88P  8?K-l;QSJ!1!1!3x!? !;<<';D'AM)$*JKK"e7?$+M&!6>T..:L  $+i 99$(OM&!88#'>M% &t];F8D6dii6G5HIJJ,,rX   c                     i }| xs i j                         D ]*  \  }}	 |rt        t        |            nd|t        |      <   , |S # t        $ r d|t        |      <   Y Hw xY w)z5Mask secret-shaped MCP env values for read responses.r   z***)r  r%   r  r\   )r  r;  r  r  s       rV   _redact_mcp_envr   /  sk    C!!#  1	 01*SV,rCAK 
 J  	 CAK	 s   $AA A c                    |j                  d      rdn|j                  d      rdnd}|j                  d      }|j                  d      xs i }|s$t        |t              rt        d |D              rd	}| ||j                  d      |j                  d      t	        |j                  d
      xs g       t        |j                  d      xs i       ||j                  dd      du|j                  d      d	S )Nr   r   r  stdior  r   r   c              3   T   K   | ]   }t        |      j                         d k(   " yw)r   Nr  )r   r  s     rV   r   z&_mcp_server_summary.<locals>.<genexpr>/  s%      603CO+6s   &(r  rk   r  r  TFr  )	rS   	transportr   r  rk   r  r   r  r  )r   r  r  r  r  r  )rS   r  r  r   r   s        rV   _mcp_server_summaryr  /  s    ''%.#''):LwR[I776?Dggi &BGJw-# 67>6 3 wwu~779%SWWV_*+swwu~34779d+58! rX   z/api/mcp/serversc           	         K   ddl m} t        |       5   |       }d d d        dt        j	                               D cg c]  \  }}t        ||       c}}iS # 1 sw Y   AxY wc c}}w w)Nr   _get_mcp_serversr  )r  r	  r  r  r  r  )r  r	  r  rS   r  s        rV   list_mcp_serversr
  !/  sk     6		  %"$% 	<B7==?<S
/8tSc*
 % %
s'   A.A%A.A(A.A%!A.c                 v  K   ddl m}m}m} 	 t	        |       \  }}}t        | j                  xs |      5   |       }	d d d        |	v rt        dd| d      	 t        | j                  xs |      5  | |||      |d<    |||      st        dd| d	      	 d d d        t        ||      S # t
        $ r}t        dt        |            |d }~ww xY w# 1 sw Y   xY w# 1 sw Y   IxY w# t        $ r  t        $ r1}t        j                  d
       t        dt        |            |d }~ww xY ww)Nr   )r	  _save_bearer_auth_token_save_mcp_serverr   r   r  r  z' already existsr   z1' rejected: suspicious command/args configurationzPOST /api/mcp/servers failed)r  r	  r  r  r  r  r7   r  r  r  r\   rQ   rK  r  )
rj  r  r	  r  r  rS   r  rS
  r  r  s
             rV   add_mcp_serverr  ./  sX     G,H,N)m\ 
/	0 &#%&xhtfDT4UVVGDLL3G4 	'+B4+Vi(#D-8# #%dV+\]  9	 t]33/  GCH=3FG& &	 	   G56CH=3FGsy   D9B3 D9CD9C3 5)C'C3 'D93	C<CCD9C$ D9'C0,C3 3D6,D11D66D9c                    K   ddl m} t        | j                  xs |      5   || j                        \  }}ddd       st        ddj                              ddiS # 1 sw Y   +xY ww)	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   ru  r   r  T)r  r  r  r  r  r7   r  )rj  r  r  r  r  s        rV   replace_mcp_serversr  R/  se      ;	/	0 8)$,,7
F8DIIf4EFF$<	8 8s    A.A"*A."A+'A.z/api/mcp/servers/{name}c                    K   ddl m} t        |      5   ||       }d d d        st        dd|  d      ddiS # 1 sw Y    xY ww)	Nr   )_remove_mcp_serverr   r  ' not foundr   r  T)r  r  r  r7   )rS   r  r  r!  s       rV   remove_mcp_serverr  e/  sR     8		  +$T*+htfK4PQQ$<	+ +s   A	<AAAz/api/mcp/servers/{name}/testc                    	
K   ddl m}m	m
 t	              5   |       ddd        vrt        dd  d      i     j                  d      d	k(  	
 fd
}	 t        j                  |       d{   \  }}|sddg dS d|D cg c]
  \  }}||d c}}j                  dd      j                  dd      dS # 1 sw Y   xY w7 X# t        $ r}dt        |      g dcY d}~S d}~ww xY wc c}}w w)zFConnect to the server, list its tools, disconnect.  Returns tool list.r   )r	  _oauth_tokens_present_probe_single_serverNr   r  r  r   r   r  c                      t              5            } r       nd}| |fcd d d        S # 1 sw Y   y xY w)N)detailsT)r  )	r  token_presentr  r  r  rS   needs_oauth_tokenr  r  s	     rV   _probe_scopedz&test_mcp_server.<locals>._probe_scoped/  sL     #7+ 	((wt}gNE;L1$7RVM-'	( 	( 	(s   5>F)r  r  r  u1   OAuth authentication required — no token found.TrS   r"  prompts	resources)r  r  r  r   )r  r	  r  r  r  r7   r   ro   r  r\   r  )rS   r  r	  r  r  r  r  r'	  r  r  r  r  r  r  s   ``       @@@@@rV   test_mcp_serverr!  p/  s-      
	  %"$%7htfK4PQQ G  ))&1W<( ( 	
 &-%6%6}%EE} H
 	
 <ABDAq1Q/B;;y!,[[a0	 W% %<  F 
X
 	

 Csj   DCAD)C CC 	DD&(DCDC 	C>%C93C>4D9C>>	Dr  DashboardOAuthFlow_mcp_oauth_flows_mcp_oauth_transactionsc            
      $   t        j                          t        z
  } t        5  t        j	                         D cg c]  \  }}t        |dd      | k  r| }}}|D ]  }t        j                  |d         	 d d d        y c c}}w # 1 sw Y   y xY w)Nr
  r   )r}  _MCP_DASHBOARD_OAUTH_TTL_mcp_oauth_flows_lockr#  r  r   rP  )r&	  flow_idr
  r
  s       rV   _gc_mcp_oauth_flowsr)  /  s    YY[33F	 0 "2!7!7!9
t\1-6 
 

  	0G  $/	00 0
0 0s   BB  B BBserver_namec                 J    ddl m} | j                  d       d ||d       S )Nr   rj
  r   r  r   rh
  )urllib.parserj
  r^  )r  r*  rj
  s      rV   !_mcp_oauth_callback_url_from_baser.  /  s*    "ooc"##;E+TV<W;XYYrX   c                     ddl m}m} ddlm}m} ddl m} d ||d       } |       }|r| | S  |t        | j                              }	 ||       }
 ||	j                  |
 | ddd            S )	zABuild the externally reachable callback URL for a dashboard flow.r   )r  
urlunparse)prefix_from_requestresolve_public_urlr,  r  r   rh
  )r   r	  r  fragment)
r-  r  r0   hermes_cli.dashboard_auth.prefixr1  r2  rj
  r  r  _replace)r   r*  r  r0  r1  r2  rj
  r  
public_urlrg  r  s              rV   _mcp_oauth_callback_urlr7  /  s~    1X"'k(C'DEF#%JfX&&C(()*D )FdmmF8F8)<Rr\^m_``rX   c                     | j                   | j                  f}t        5  t        j	                  |t        j                               cd d d        S # 1 sw Y   y xY wr  )r  r*  _mcp_oauth_transactions_lockr$  r  rx   rp   )r
  r  s     rV   _mcp_oauth_transactionr:  /  sK    T--
.C	% I&11#y~~7GHI I Is   (AAc                    ddl m}m}m} 	 ddlm}m}m} ddlm	}m
}	 ddlm}
 ddlm}m} ddlm}  |	| j$                        } | |t'        | j$                                    }	 t)        |       }|5   |       5   |
|       5   |       } || j*                        }|j-                         }d}	 |j/                  | j*                  | j$                  	      } || j*                  |t1        t3        |j5                  d
d      xs d      d            } || j*                        st7        d       || j*                  |       |D cg c]
  \  }}||d c}}| _        | j;                          | j<                  rddlm }  || j*                         ddd       ddd       ddd        ||        ||       	 | jO                          yc c}}w # tB        $ r= |jE                  |d       |jG                  | j*                  || j$                  	        w xY w# 1 sw Y   xY w# 1 sw Y   xY w# 1 sw Y   xY w#  ||        ||       w xY w# tB        $ rR}tI        |      }|jK                         }d|v rd|v sd|v rd| j*                   d}| jM                  |       Y d}~d}~ww xY w# | jO                          w xY w)zCRun the normal MCP probe with dashboard redirect/callback handlers.r   )r  r  r  )build_profile_secret_scopereset_secret_scopeset_secret_scopery  )dashboard_oauth_flow)HermesTokenStorageforce_interactive_oauth)get_managerN)r  connect_timeouti;  )rC  uw   The server responded, but no OAuth token was obtained — this provider may require a manually-registered OAuth client.r  )reconnect_mcp_serverT)only_if_absent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.)(r  r  r  r  agent.secret_scoper<  r=  r>  rl  rz  r{  tools.mcp_dashboard_oauthr?  tools.mcp_oauthr@  rA  tools.mcp_oauth_managerrB  r  r   r:  r*  snapshotremover\  r  r   r  r  mark_approvedreconnect_livetools.mcp_toolrD  r\   restorerestore_entryr  r   
mark_errormark_worker_done)r
  r  r  r  r  r<  r=  r>  rz  r{  r?  r@  rA  rB  
home_tokensecret_tokentransactionmanagerr-  r  previous_entryr  r'	  r  rD  r  msgr  s                               rV   _run_dashboard_mcp_oauthr\  /  s    
F 	
 	

 	ZBO7-d.>.>?
'(B4HXHXCY(Z['	306K "57 "9Md9S "%-,T-=-=> ))+!%%,^^(($($4$4 &4 &N 1(((+E#'':KQ2O2TST,UWZ([E
 11A1AB*\  %T%5%5s;LQ!RDAq1Q"?!RDJ&&(**G,T-=-=>5" " "H |,&z2$ 	E "S ! OOF4O@))((&$($4$4 * 
 7" " " " " "H |,&z2 #h
 ))+C<X0K74JD$$% &= =  	" 	s   AI &I 3H6;	H*,H1BG	G=GH*H6%I -I =J5 GAH	HH'#H**H3	/H66H?;I II 	J2 AJ-(J5 -J22J5 5Kz/api/mcp/servers/{name}/authc           
      d   K   ddl m} ddlm} t	        |       t                ddlm} t         |       j                         j                  d            }t        |      5   |       }t         |       j                         j                  d            ddd        vrt        dd	  d
      t        |          }|j                  d      st        dd      |j                  d      r!|j                  d      dk7  rt        dd      d|d<   t        j                   d      }	 ||	 ||j                  d      xs i j                  d      xs t#        |       |k(        }
t$        5  t'        d t(        j+                         D              }|t,        k\  rt        dd      t/         fdt(        j+                         D              rt        dd  d      |
t(        |	<   ddd       t1        j2                  t4        |
|fdd        j7                          	 |
j9                  d !       d{    |
j?                         S # 1 sw Y   xY w# 1 sw Y   qxY w7 -# t:        $ r3}|
j=                  t        |             Y d}~|
j?                         S d}~ww xY ww)"zHStart MCP OAuth and hand the authorization URL to the dashboard browser.r   r  )r"  r   Fr  Nr   r  r  r   r   r   z2stdio servers authenticate via env keys, not OAuthr   r   r  z/This server uses header/API-key auth, not OAuth   r  )r(  r*  r  r  r  rP  c              3   6   K   | ]  }|j                      y wr  )worker_done)r   r
  s     rV   r   z"auth_mcp_server.<locals>.<genexpr>O0  s!      
    
s   r	  z0Too many MCP OAuth flows are already in progressc              3   |   K   | ]3  }|j                   k(  xr |j                  k(  xr |j                    5 y wr  )r*  r  ra  )r   r
  	flow_homerS   s     rV   r   z"auth_mcp_server.<locals>.<genexpr>X0  sO      
  $ %  I-%$$$%
s   9<r  zMCP OAuth for 'z' is already in progressTz
mcp-oauth-ri   r   rZ  ) r  r	  rJ  r"  r   r)  rl  r   r  r  r  r  r7   r  r   r  r*
  r7  r'  r~  r#  r  _MAX_PENDING_MCP_OAUTH_FLOWSr  rx   rz   r\  rT   wait_for_authorization_urlr\   rT  rM  )rS   r   r  r	  r"  r   process_homer  r  r(  r
  r
  r  rc  s   `            @rV   auth_mcp_serverrg  -0  s     7<70(335==U=KLL		  N"$)446>>e>LM	N 7htfK4PQQ
wt}
C775>4hii
wwycggfo84effCK##B'Gggg&,"11.A 2"7D1 L0D 
 ) 
(//1
 
 22I   
 )//1	
 
  (.FG  %)!))* 'C[$ 	
 eg"--b-999 ==?kN N,) )8 	: "C!!==?"st   A!J0%6ICJ07A<I#38J0,I1 I/I1 J0I J0#I,(J0/I1 1	J-:J(J0(J--J0z/api/mcp/oauth/flows/{flow_id}r(  c                    K   t        |       t                t        j                  |       }|t	        dd      |j                         }|j                  |d<   |S w)Nr   zOAuth flow not found or expiredr   r  )r   r)  r#  r   r7   rM  r  )r(  r   r
  rM  s       rV   mcp_oauth_flow_statusri  p0  sU     7(D|4UVV}}H

HWOs   AAz*/api/mcp/oauth/callback/{server_name:path}rR  rm   c                   K   t                t        5  t        j                         D cg c]"  }|j                  | k(  r|j
                  dk(  r|$ }}d d d        t        fdD        d       }|t        dd      S 	 |j                  ||       |rt        dd	      S t        d      S c c}w # 1 sw Y   axY w# t        $ r*}t        |      }d|v rdnd	}t        d
|      cY d }~S d }~ww xY ww)Nauthorization_requiredc              3   ~   K   | ]4  }|j                   &$t        j                  |j                         r| 6 y wr  )expected_stater  r   )r   r  rm   s     rV   r   z%mcp_oauth_callback.<locals>.<genexpr>0  sA      	
''3!&&y'?'?G	 	
r  zA<h1>OAuth flow expired</h1><p>Return to Hermes and try again.</p>r   r  )rR  rm   r  zalready receivedr  r   zP<h1>OAuth callback rejected</h1><p>The callback was invalid or already used.</p>zA<h1>Authorization failed</h1><p>Return to Hermes for details.</p>zR<h1>Authorization received</h1><p>You can close this tab and return to Hermes.</p>)r)  r'  r#  r  r*  rc  r   r>   deliver_callbackr  r  )	r*  rR  rm   r  r
  r  r  reasonr   s	     `      rV   mcp_oauth_callbackrp  |0  s     	 
 )//1
;.77 

 

 	
'	
 		D |_mpqq	
4uEB _mpqqlmm=

 
*  
S/69cs?#
 	

s]   C6B4'B/B4,C6C  C6/B44B=9C6 	C3	C.(C3)C6.C33C6c                   ,    e Zd ZU eed<   dZee   ed<   y)MCPEnabledToggler  Nr  r  r  r  r   r  r  r   r  r  rX   rV   rr  rr  0      M!GXc]!rX   rr  z/api/mcp/servers/{name}/enabledc                   K   t        |j                  xs |      5  t               }|j                  d      }t	        |t
              r| |vrt        dd|  d      t	        ||    t
              st        dd      t        |j                        ||    d<   t        |       d	d	d	       d
| t        |j                        dS # 1 sw Y   "xY ww)u$  Enable or disable an MCP server (takes effect on next session/gateway).

    Toggles the ``enabled`` key on the server's config.yaml entry — the same
    flag the agent reads at startup.  Disabled servers stay in config so they
    can be re-enabled without re-entering their settings.
    mcp_serversr   r  r  r   r   zMalformed server configr  NTr  rS   r  )
r  r  r   r   r  r  r7   r   r  r   )rS   rj  r  r  r  s        rV   set_mcp_server_enabledrx  0  s      
/	0 m''-('4(D,?C(4&8TUU'$-.C8QRR#'#5i C dll1CDD s   CBC#!CC	Cz/api/mcp/catalogc                 ,  K   	 ddl m} g }	 t        |       5  t        |j                               }|D ci c]C  }|j                  |j                  |j                        |j                  |j                        fE }}ddd       D ]  }|j                  }|j                  }	|j                  }
|j                  i d|j                  d	|j                   d
|j"                  d|	j$                  dt'        |dd      dt'        |dg       xs g D cg c]&  }|j                  |j(                  |j*                  d( c}d|	j,                  dt        |	j.                  xs g       d|	j0                  d|
r|
j0                  ndd|
r|
j2                  ndd|
rt        |
j4                        ng d|j6                  j8                  t        |j6                  j8                        ndd|j:                  xs dd|j                  dudj=                  |j                  d      d   d|j=                  |j                  d      d           	 g }	 |j?                         D cg c]  \  }}}|||d! }}}}||d"S # t        $ r*}t        j	                  d       t        dd|       d}~ww xY wc c}w # 1 sw Y   AxY wc c}w # t
        $ r  t        $ r t        j	                  d        Y w xY wc c}}}w # t        $ r Y w xY ww)#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 failedrY  zCatalog unavailable: r   NrS   r"  r   r  r  r!  r  rf  r  )rS   rF   r  r  rk   r   install_urlinstall_ref	bootstrapdefault_enabledpost_installr   needs_installr  )FFr  r   zlist_mcp_catalog failed)rS   r  r4  )r  diagnostics) r]   r{  r\   rQ   rK  r7   r  r  list_catalogrS   is_installedr*  r   r  rc  r  r"  r   r!  r   rF   r  r  rk   r   rb  r~  r  r  r  r   catalog_diagnostics)r  r{  r  r  catalog_entriesr  installed_stater  r   r  rc  r  r  r  r  s                  rV   list_mcp_catalogr  0  s]    S*
 G.2G$ 	";#;#;#=>O ) 11!&&9;;Q;QRSRXRX;YZZO 	 % "	E::DImmGNN 

u00 %,, Y^^	
 WT66: $T5"5;! VVqxxQZZP! 9,,  Y^^1r2!" y}}#& gw{{4'( gw{{4)* T'"3"34R+. ";;..: $((C(C#D34  2 2 8b56  d!:78 _00^LQO9: ?..uzz>J1M; 	"	R K )<<>
 
Aq a0
 
 {;;}  S236KC54QRRS	 	"!0   2012

  s   LJ LK J?AJ:>J? BK +K?DK LL *K>;L ?L	J7%J22J77L:J??K	K 'K;8L:K;;L>L 	LLLLc                   R    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<   y)MCPCatalogInstallrS   r  Tr  Nr  )r  r  r  r  r  r  r   r  r   r  r   r  rX   rV   r  r  1  s1    
ICc3hFD!GXc]!rX   r  z/api/mcp/catalog/installc                 ,   	
K   ddl m  j                  xs dj                         }j	                  |      

t        dd| d       j                  xs |	 j                  rFt        	      5   j                  j                         D ]  \  }}|s	t        ||        	 ddd       
j                  .t        |      }	 t        t        	      d	d
|gz   |      }d|d|dS  	
fd}	 t!        j"                  |       d{    d|ddS # 1 sw Y   oxY w# t
        $ r  t        $ r}t        dd|       d}~ww xY w7 @# t
        $ r  t        $ r0}t$        j'                  d       t        dt)        |            d}~ww xY ww)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   rz  r   Nr   zNo catalog entry 'r  r   ru  rc  rY  zInstall failed: T)r  rS   
backgroundr  c                      t              5  j                   j                         d d d        y # 1 sw Y   y xY w)N)r  )r  install_entryr  )rj  r+
  r  r{  s   rV   _install_scopedz2install_mcp_catalog_entry.<locals>._install_scopedH1  s<    -. 	A%%eDKK%@	A 	A 	As   4=z install_mcp_catalog_entry failedr   F)r  rS   r  )r]   r{  rS   r   	get_entryr7   r  r  r  r  r   rc  _mcp_install_action_namerD  r  r\   ro   r  rQ   rK  r  )rj  r  rS   r  r  r  r%  r  r  r+
  r  r{  s   `        @@@rV   install_mcp_catalog_entryr  1  s     'IIO""$D!!$'E}6Ha4PQQ /xx-. 	)( )1"1a()	) }}  *$/	R'!"34y$7OOD DOOA>000 E::S	) 	)$  	 	RC:J3%8PQQ	R 	1  >9:CH==>s~   A0F6#DD)F	D# $F4E EE FD F#E
5EE

FE F!+FFFc                 (   t        j                  dd| j                               j                  d      dd xs d}t	        j
                  | j                               j                         dd }d| d| }t        j                  |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]+rO  N0   serverr  zmcp-install-action-.log
r  r  r   r   hashlibsha1r   	hexdigestr  r  )rS   r[  digestr  s       rV   r  r  V1  s     66-djjl399#>sCOxD\\$++-(224Ra8FD66(+F  76($)?@MrX   computer-use-grantzaction-computer-use-grant.logc                   "    e Zd ZU eed<   eed<   y)PairingApprover-  rR  Nr  r  rX   rV   r  r  l1  s    M
IrX   r  c                   "    e Zd ZU eed<   eed<   y)PairingRevoker-  r  Nr  r  rX   rV   r  r  q1  s    MLrX   r  c                      ddl m}   |        S )Nr   PairingStore)gateway.pairingr  r  s    rV   _pairing_storer  v1  s    ,>rX   z/api/pairingc                  `   K   t               } | j                         | j                         dS w)N)r
  r0  )r  list_pendinglist_approved)stores    rV   list_pairingr  |1  s0     E%%''') s   ,.z/api/pairing/approvec                   K   t               }| j                  xs dj                         j                         }| j                  xs dj                         j                         }|r|st        dd      |j                  ||      }|rd|dS |j                  |      rt        dd| d	      t        d
d| d| d      w)Nr   r   zplatform and code are requiredr   T)r  r   r	  z
Platform 'z0' is locked out after too many failed approvals.r   zCode 'z%' not found or expired for platform 'r  )	r  r-  r   r   rR  r	  r7   approve_code_is_locked_out)rj  r  r-  rR  r3  s        rV   approve_pairingr  1  s     E#**,224HIIO""$**,D44TUU$/FF++H%z)YZ
 	
 vB8*BO s   CCz/api/pairing/revokec                 (  K   t               }| j                  xs dj                         j                         }|r| j                  st        dd      |j                  || j                        rddiS t        dd| j                   d	| d
      w)Nr   r   z!platform and user_id are requiredr   r  Tr   zUser z  not found in approved list for r  )r  r-  r   r   r  r7   revoke)rj  r  r-  s      rV   revoke_pairingr  1  s     E#**,224H4<<4WXX||Hdll+d|
t||n$DXJaP s   BBz/api/pairing/clear-pendingc                  H   K   t               } | j                         }d|dS w)NT)r  r
  )r  clear_pending)r  r  s     rV   clear_pending_pairingr  1  s'     E!E5))r  c                       e Zd ZU eed<   dZee   ed<   g Zee   ed<   dZ	ee   ed<   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<   y)WebhookCreaterS   Nr"  eventsrF   rF  rf  r  rE  Fdeliver_onlydeliver_chat_idr  )r  r  r  r  r  r"  r   r  r   rF   rF  rf  rE  r  r   r  r  r  rX   rV   r  r  1  s}    
I!%K#%FDI FHSM  FHSM FDIGSL$%)OXc]) FHSM rX   r  routec                    | |j                  dd      t        |j                  d      xs g       |j                  dd      t        |j                  d            |j                  dd      |j                  dd      t        |j                  d	      xs g       |j                  d
      | d|  t        |j                  d            |j                  dd      dudS )Nr"  r   r  rE  r  r  rF   rF  rf  r
  z
/webhooks/r  r  TF)rS   r"  r  rE  r  rF   rF  rf  r
  r   
secret_setr  )r   r  r   )rS   r  r  s      rV   _webhook_route_summaryr  1  s    yy3uyy*0b199Y.UYY~67))Hb)))Hb)uyy*0b1ii-:dV,599X./99Y-U: rX   z/api/webhooksc                     K   dd l m}  | j                         }| j                         }| j	                         ||j                         D cg c]  \  }}t        |||       c}}dS c c}}w w)Nr   )r  r  subscriptions)hermes_cli.webhookr  _get_webhook_base_url_load_subscriptions_is_webhook_enabledr  r  )whr  subsrS   r  s        rV   list_webhooksr  1  sn     #'')H!!#D))+  $zz|
e #49
 
s   A
A/A)
#A/z/api/webhooks/enablec                     K   	 t        dd       t               }ddd|d    d|S # t        $ r(} t        j                  d       t	        dd      | d } ~ ww xY ww)	Nr  Tz0Failed to enable webhook platform from dashboardrY  z"Failed to enable webhook platform.r   r  )r  r-  r  r2
  )r	  r\   rQ   rK  r7   r  )r  r4
  s     rV   enable_webhooksr  1  s~     	40 ;<N+,=>>	
    IJ7
 	s$   A' A	A#AAAc                   K   dd l }dd l}dd l}dd lm} |j                         st        dd      | j                  xs dj                         j                         j                  dd      }|j                  d|      st        dd	      | j                  r| j                  d
k(  rt        dd      | j                  xs |j                  d      }| j                   xs d| | j"                  D cg c]#  }|j                         s|j                         % c}|| j$                  xs d| j&                  D cg c]#  }|j                         s|j                         % c}| j                  xs d
|j)                  d|j+                               d}	| j,                  r7| j,                  j                         r| j,                  j                         |	d<   | j                  rd|	d<   | j.                  rd| j.                  i|	d<   |j1                         }
|	|
|<   |j3                  |
       |j5                         }t7        ||	|      }||d<   |S c c}w c c}w w)Nr   r   zHWebhook platform is not enabled. Enable it from the Webhooks page first.r   r   r  rO  z^[a-z0-9][a-z0-9_-]*$zBInvalid name. Use lowercase alphanumeric with hyphens/underscores.r  uK   Direct delivery requires a real target (telegram, discord, …), not 'log'.r   z Dashboard-created subscription: z%Y-%m-%dT%H:%M:%SZ)r"  r  r  rF   rf  rE  r
  rF  Tr  r  deliver_extrar  )r  r  r}  r  r  r  r7   rS   r   r   r  r  r  rE  r  r*
  r"  r  rF   rf  r  gmtimerF  r  r  _save_subscriptionsr  r  )rj  _re_secrets_timer  rS   r  r  rx  r  r  r  r  s                rV   create_webhookr  1  s    #!!#]
 	

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

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

 [[6H2226F''T-MdV+T&*kk?QWWY1779?++#&*kk?QWWY1779?<<(5nn%95<<>JE {{t{{((*++++-h $n"+T-A-A!Bo!!#DDJ4 '')H$T5(;GGHN- @ @s+   C2I4I
I I<II$C8Iz/api/webhooks/{name}c                    K   dd l m} | xs dj                         j                         }|j	                         }||vrt        dd| d      ||= |j                  |       ddiS w)	Nr   r   r   No subscription named 'r  r   r  T)r  r  r   r   r  r7   r  )rS   r  r  r  s       rV   delete_webhookr  52  sp     #:2



$
$
&C!!#D
$6McURS4TUUS	4 $<s   A&A(c                       e Zd ZU eed<   y)WebhookEnabledToggler  Nr;  r  rX   rV   r  r  B2  rL  rX   r  z/api/webhooks/{name}/enabledc                 .  K   ddl m} | xs dj                         j                         }|j	                         }||vrt        dd| d      t        |j                        ||   d<   |j                  |       d	|t        |j                        d
S w)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  Trw  )	r  r  r   r   r  r7   r   r  r  )rS   rj  r  r  r  s        rV   set_webhook_enabledr  F2  s      $:2



$
$
&C!!#D
$6McURS4TUU-DIi4 T\\0BCCs   BBz/api/gateway/startc                    K   	 t        t        | d      d      }d|j                  ddS # t        $ r  t        $ r*}t        j                  d       t        dd|       d }~ww xY ww)	NrT   r  zFailed to spawn gateway startrY  zFailed to start gateway: r   TrB  rD  r  r7   r\   rQ   rK  r  r  r%  r  s      rV   start_gatewayr  d2  st     W#$7$I?[ txxAA   W676OPSu4UVVW$   A)* A)A&%A!!A&&A)z/api/gateway/stopc                    K   	 t        t        | d      d      }d|j                  ddS # t        $ r  t        $ r*}t        j                  d       t        dd|       d }~ww xY ww)	Nstopr  zFailed to spawn gateway stoprY  zFailed to stop gateway: r   TrB  r  r  s      rV   stop_gatewayr  p2  ss     V#$7$H.Y txx@@   V566Nse4TUUVr  c                   6    e Zd ZU eed<   eed<   dZee   ed<   y)CredentialPoolAddrU   r  Nrg  )r  r  r  r  r  rg  r   r  rX   rV   r  r  2  s    M LE8C=rX   r  r  c                    t        | dd      xs 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  Nrg  r  r   r  r   last_statusrequest_countr  )
r  r  rg  r  r   r  r  r  r
  has_refresh)r   r%   r   )r  r  r   s      rV   _pool_entry_summaryr  2  s    
 E>2.4"EeT4(.UK6%40E:q1umT: ;.3E*GE?DAB rX   z/api/credentials/poolc                  ~  K   ddl m}  ddlm} g } |       }t	        |j                               D ]Z  }	  | |      }|j                         }|s|j                  |t        |d      D cg c]  \  }}t        ||       c}}d       \ d|iS # t        $ r t        j                  d|       Y w xY wc c}}w w)	Nr   r  )read_credential_poolzload_pool(%s) failedr   )rT   )rU   r  r  )r  r  r  r  r  r  r\   rQ   rK  r  r  r	  r  )	r  r  r  raw_poolr  r  r  ir  s	            rV   list_credential_poolr  2  s     /4I $%Hhmmo. 	[)D ,,.#6?q6Q.2a#Aq)
 	 ##  	NN1;?	s3   2B=B/B=,B7B=B41B=3B44	B=c           	        K   dd l }ddlm}m}m}m}m} | j                  xs dj                         j                         }| j                  xs dj                         }|r|st        dd      	  ||      }	| j                  xs dj                         xs dt        |	j                               dz    }
 |||j                         j                   d d	 |
|d||
      }|	j#                  |       |j%                  |      sN	 ddlm}m}  |       j-                  di       }t/        |j-                  |g       xs g       D ]  } |||        	 d|t        |	j                               dS # t0        $ r t2        j5                  d       Y =w xY w# t        $ r  t0        $ r1}t2        j5                  d       t        dt7        |            |d }~ww xY ww)Nr   )r  r  AUTH_TYPE_API_KEYCUSTOM_POOL_PREFIXr	  r   r   z!provider and api_key are requiredr   zkey #r   r
  )rU   r  rg  r  r  r   r
  )_load_auth_storer  suppressed_sourcesz,unsuppress after pool add failed (non-fatal)z!POST /api/credentials/pool failedT)r  rU   r  )r  r  r  r  r  r  r	  rU   r   r   r  r7   rg  r   r  r  r  r  r   r  r  r  r   r  r\   rQ   rK  r  )rj  _uuidr  r  r  r  r	  rU   r  r  rg  r  r  r  
suppressedr  r  s                    rV   add_credential_pool_entryr  2  s      #**,224H||!r((*G74WXX G"!r((*Oc$,,.6IA6M5N.O {{}  !$'  
 	u
 ""#56	O ./334H"M

x < BC @C03?@ Hs4<<>7JKK  OMNO  G:;CH=3FGsP   A.G1BF AE/ F G/FF FF G%,GGGz(/api/credentials/pool/{provider}/{index}c                 @  K   ddl m} ddlm} ddlm} | xs dj                         j                         } 	  ||       }|j                  |      }|t        d
d      g }g }	 || |j                  xs d      }
|
\	 |
j                  | |      }t!        |j"                        }t!        |j$                        }	|j&                  r || |j                         d| t)        |j+                               ||	dS # t        $ r1}t        j                  d       t        dt        |            |d	}~ww xY w# t        $ r] t        j                  d| |j                         	  || |j                         n$# t        $ r t        j                  d       Y nw xY wY w xY ww)u  Remove a pool entry.  ``index`` is 1-based (matches the list response).

    Removal must be sticky (#55217): ``load_pool()`` re-seeds entries from
    their backing source (.env var, OAuth singleton file, custom-provider
    config) on every call, so deleting only the pool row silently reverts on
    the next dashboard refresh.  We dispatch through the same RemovalStep
    registry the CLI ``hermes auth remove`` uses: each source cleans up its
    external state and suppresses ``(provider, source)`` so the seeders skip
    it.  Manual entries have no registered step — nothing external to clean,
    no suppression needed (they aren't re-seeded).
    r   r  )find_removal_step)suppress_credential_sourcer   z#DELETE /api/credentials/pool failedr   r   Nr   zNo pool entry at that indexz>credential source cleanup failed for %s/%s; suppressing anywayz!suppress_credential_source failedT)r  rU   r  cleanedhints)r  r  agent.credential_sourcesr  r  r  r   r   remove_indexr\   rQ   rK  r7   r  r   	remove_fnr  r  r  suppressr   r  )rU   r  r  r  r  r  r!  r  r  r  stepr3  s               rV   remove_credential_pool_entryr  2  s     0::B%%'--/HG"##E* 4QRRGEXw~~';<D	D^^Hg6F6>>*G&E*8W^^D T\\^$ 9  G<=CH=3FG   	D NNP'..D*8W^^D DBCD	Dsq   5FC8 ,F>AD5 F8	D2,D--D22F5*F E43F4FFFFFFFc                       e Zd ZU eed<   y)MemoryProviderSelectrU   Nr  r  rX   rV   r  r  33  s    MrX   r  c                       e Zd ZU dZeed<   y)MemoryResetr  rj   N)r  r  r  rj   r  r  r  rX   rV   r  r  83  s    FCrX   r  z/api/memoryc                  N  K   t               } d}| j                  d      }t        |t              rt	        |j                  d            }t               dz  }i }dD ]9  \  }}||z  }|j                         r|j                         j                  nd||<   ; |t               |dS w)Nr   rz  rU   memories))	MEMORY.mdrz  )USER.mdr   r   )r  r  builtin_files)
r   r   r  r  rU  r   r{  r  rI  r  )r  r  r  mem_dirfilesfnamer  r   s           rV   get_memory_statusr  =3  s     
-CF
''(
C#t01DE *,GED A
s,0KKMTYY[((qc
A
 79 s   B#B%z/api/memory/providerc                    K   t        | j                        }t        |       t               }t	        |j                  d      t              si |d<   ||d   d<   t        |       d|dS w)Nrz  rU   Tr  )rU  rU   r  r   r  r   r  r   )rj  rU   r  s      rV   set_memory_providerr  S3  s`     .t}}=H"8,
-Ccggh'.H (CM*(++s   A'A)z/api/memory/resetc           	        K   | j                   xs dj                         j                         }|dvrt        dd      t	               dz  }g }g }|dv r|j                  d       |d	v r|j                  d
       |D ]:  }||z  }|j                         s	 |j                          |j                  |       < d|dS # t        $ r}t        dd| d|       d }~ww xY ww)Nr  >   r  r   rz  r   z#target must be all, memory, or userr   r  >   r  rz  r  >   r  r   r	  rY  zCould not delete r  Tr  )	rj   r   r   r7   r   r  r{  r  r  )rj  rj   r  r  r  r  r   r  s           rV   reset_memoryr  a3  s     kk"U))+113F..4YZZ*,GGG""{# y! `;;=`u%` 7++  `#>OPUwVXY\X]<^__`s*   BC#!B?8C#?	C CC  C#z/api/ops/doctorc                     K   	 t        dgd      } d| j
                  ddS # t        $ r*}t        j                  d       t	        dd|       d }~ww xY ww)Nr  zFailed to spawn doctorrY  zFailed to run doctor: r   TrB  rD  r\   rQ   rK  r7   r  rE  s     rV   
run_doctorr  3  sb     T#XJ9 txx::  T/06LSE4RSSTs$   A! A	A%AAAz/api/ops/security-auditc                     K   	 t        ddgd      } d| j
                  dd	S # t        $ r*}t        j                  d       t	        dd|       d }~ww xY ww)
Nrc  auditr  zFailed to spawn security auditrY  zFailed to run security audit: r   TrB  r  rE  s     rV   run_security_auditr  3  si     \#Z$9;KL txx1ABB  \786TUXTY4Z[[\$   A" A	A%AAAc                   "    e Zd ZU dZee   ed<   y)BackupRequestNoutput)r  r  r  r  r   r  r  r  rX   rV   r  r  3  s     FHSM rX   r  c                      t               dz  S )Nbackupsr^  r  rX   rV   _dashboard_backup_dirr  3  s    y((rX   c                      t        j                         j                  d      } t               d|  dt	        j
                  d       dz  S )Nz%Y-%m-%d-%H%M%Szhermes-backup-rO  r   .zip)r   ry  r  r  r  r  )stamps    rV   _new_dashboard_backup_pathr#  3  sD    LLN##$56E "~eWAg>O>OPQ>R=SSW%XXXrX   z/api/ops/backupc                   K   dg}d }| j                   xs dj                         }|r|j                  d|g       nDt               }	 |j                  j                  dd       |j                  dt        |      g       	 t        |d      }d|j                  dd}|t        |      |d<   |S # t        $ r}t        dd|       d }~ww xY w# t        $ r*}t        j                  d	       t        dd
|       d }~ww xY ww)Nr  r   z-oTrW  rY  z#Could not create backup directory: r   zFailed to spawn backupzFailed to run backup: rB  archive)r  r   r  r#  r  rZ  r  r7   r  rD  r\   rQ   rK  r  )rj  rk   r%  r  r  r%  rL  s          rV   
run_backupr&  3  s    :D"GkkR&&(FT6N#,.	NN   = 	T3w<()T#D(3 488X>H!'lO  	<SEB 	  T/06LSE4RSSTsN   ADB/ $DC "D/	C8CCD	D%C>>DDz/api/ops/backup/downloadr%  c                   K   	 t               j                         j                  d      }t        |       j                         j                  d      }t        ||      st        d	d
      |j                         st        dd      t        t        |      d|j                  d      S # t        $ r t        dd      t        t        f$ r t        dd      w xY ww)NFr  Tr   zBackup not foundr   r   zInvalid backup pathr  z0Backup is outside the dashboard backup directoryzapplication/zipr  r  )r  r  r  r   r  r7   r  r  r^  rH  r=   r  rS   )r%  
backup_dirrj   s      rV   download_dashboard_backupr)  3  s     K*,779AAAO
g))+3343@ *f-4fgg>>4FGG[$!-	   H4FGG\" K4IJJKs   C"AB- AC"-2CC"c                   &    e Zd ZU eed<   dZeed<   y)ImportRequestr%  Fr<  Nr=  r  rX   rV   r+  r+  3  s    L E4rX   r+  z/api/ops/importc                   K   | j                   xs dj                         }|st        dd      t        j                  j                  |      st        dd|       d|g}| j                  r|j                  d       	 t        |d      }d|j                  ddS # t        $ r*}t        j                  d	       t        d
d|       d }~ww xY ww)Nr   r   zarchive path is requiredr   r   zArchive not found: r  --forceFailed to spawn importrY  Failed to run import: TrB  )r%  r   r7   rv   r   r  r<  r  rD  r\   rQ   rK  r  )rj  r%  rk   r%  r  s        rV   
run_importr0  3  s     ||!r((*G4NOO77>>'"6I'4STTgDzzIT#D(3 txx::  T/06LSE4RSSTs*   A>CB C	C&%CCCc                     t        | xs d      j                  j                         }t        j                  dd|      j                  d      }|sd}|j                         j                  d      s| d}|S )Nz
backup.zipz[^A-Za-z0-9._-]+rO  z.-r!  )r   rS   r   r  r  r   r_  )r   rS   s     rV   _safe_backup_upload_namer2  3  sj    (L)..446D66%sD177=D::<  (t}KrX   z/api/ops/import-uploadc                   K   t               }	 |j                  dd       t	        | j
                        }t        j                  t        j                        j                  d      }|d| dt        j                  d	       d| z  }t        j                  d
|j                   d
dt!        |            \  }}t#        |      }	d}
d}	 t%        j&                  |d      5 }	 | j)                  t*               d {   }|sn6|
t-        |      z  }
|
t.        kD  rt        dd      |j1                  |       V	 d d d        t%        j2                  |	|       d}	 |s|	j7                  d       | j9                          d {    t;        j<                  |      s|j7                  d       t        dd      dt!        |      g}|r|j?                  d       	 tA        |d      }d|jH                  dt!        |      |
dS # t        $ r}t        dd|       d }~ww xY w7 )# 1 sw Y   xY w# t        $ r  t4        $ r t        dd      t        $ r}t        dd|       d }~ww xY w7 # |s|	j7                  d       | j9                          d {  7   w xY w# tB        $ r*}tD        jG                  d       t        dd|       d }~ww xY ww)NTrW  rY  z+Could not create import staging directory: r   z%Y%m%d-%H%M%Szdashboard-import-rO  r   r  r  r  r   Fr  rB  zArchive is too larger  z(Import staging directory is not writablez"Could not write uploaded archive: r  r   z(Uploaded archive is not a valid zip filer  r-  r.  r/  )r  r  rS   r%  uploaded_bytes)%r  rZ  r  r7   r2  r   r   ry  r   r	  r  r  r  r  r  rS   r  r   rv   r  rf  r  r   r  r  r  r  r  r   zipfile
is_zipfiler  rD  r\   rQ   rK  r  )r  r<  staging_dirr  	safe_namer"  rj   r  r  r  r  r  r;  r  rk   r%  s                   rV   run_import_uploadr9  4  s    
 ()K
$6 )7ILL&//@E.ugQw7H7H7K6LAi[YYF''6;;-q!FH
 H~HEGYYvt$ 	!"ii(;<<U#22'C@VWW		%   		! 	

8V$ OOtO,jjlf%&=
 	

 c&k"DIT#D(3
 xxv; {  
@F
 	

( =	! 	!   
=
 	
  
7u=
 	

 	 OOtO,jjl  T/06LSE4RSSTs   KH B)KH3 !H';H$<=H': H3 'KI2AKJ& (K	H!HH!!K$H''H0,H3 3'I/I**I//I4 2K4(J#JJ##K&	K/%KKKz/api/ops/hooksc                  t  K   ddl m}  ddlm} 	 ddlm} t        |      }g }	 |j                   |              }g }|D ]  }d}	 |j                  |j                  |j                        }d}	 |j                  |j                        }|j                  |j                  |j                   |j                  |j"                  |du|xs i j%                  d      |d	        ||d
S # t        $ r g }Y w xY w# t        $ r t        j                  d       Y w xY w# t        $ r Y w xY w# t        $ r Y w xY ww)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)r
  matcherr  r[  r  r@  r  )hooksvalid_events)r<  r   re  r=  hermes_cli.pluginsr?  r  r\   iter_configured_hooksrQ   rK  allowlist_entry_forr
  r  script_is_executabler  rA  r[  r   )	_load_configr=  r?  rC  specsr;  r  r  r  s	            rV   
list_hooksrJ  Q4  sI     >!2k* E711,.A C 	33DJJME 
	$99$,,GJ 	

ZZ||||||D(!KR,,];$
 	* ,77?    7567  		
  		s   D8C% D8C6 	D8&D*D8-D)AD8%C30D82C33D86DD8DD8	D&#D8%D&&D8)	D52D84D55D8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Z	e
ed<   y)
HookCreater
  r  NrA  r[  Tapprove)r  r  r  r  r  rA  r   r[  r  rM  r   r  rX   rV   rL  rL  4  s4    JL!GXc]!!GXc]! GTrX   rL  c                 \  K   ddl m} | j                  xs dj                         }| j                  xs dj                         }|r|st        dd      	 ddlm} ||vr+t        dd| d	d
j                  t        |                   	 t               }|j                  d      }t        |t              si }||d<   |j                  |      }t        |t              sg }|||<   d|i}| j                   r| j                   |d<   | j"                  t%        | j"                        |d<   |j'                  |       t)        |       d}	| j*                  r	 |j-                  ||       d}	d|||	dS # t
        $ r  t        $ r Y w xY w# t        $ r t.        j1                  d       Y ?w xY ww)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   r<  r   r   event and command are requiredr   r>  zUnknown event 'z
'. Valid: r  rB  r  rA  r[  FTzhook consent record failed)r  r
  r  r0  )re  r=  r
  r   r  r7   rD  r?  r  r  r\   r   r   r  r  r  rA  r[  r  r  r   rM  _record_approvalrQ   rK  )
rj  r=  r
  r  r?  r  	hooks_cfgr  	new_entryr0  s
             rV   create_hookrS  4  s     "ZZ2$$&E||!r((*G4TUU
2#(z$))F;DW:X9YZ  $ -C Ii&	 GmmE"Ggt$"	%!*G 4I||#||	)||"4<<0	)NN9H||	9((8H 7QQ?   4  	9NN78	9sO   AF,5E0 CF,F )F,0FF,FF,F)&F,(F))F,c                   "    e Zd ZU eed<   eed<   y)
HookDeleter
  r  Nr  r  rX   rV   rU  rU  4  s    JLrX   rU  c                   K   ddl m} | j                  xs dj                         }| j                  xs dj                         }|r|st        dd      t               }|j                  d      }d}t        |t              rt        |j                  |      t              rt        ||         }||   D cg c](  }t        |t              r|j                  d	      |k(  s|* c}||<   t        ||         |k  }||   s||= |s|j                  dd
       t        |       	 |j                  |       |st        dd      ddiS c c}w # t        $ r Y #w xY ww)zFRemove a hook from config.yaml and revoke its consent allowlist entry.r   r<  r   r   rO  r   rB  Fr  Nr   zNo matching hook foundr  T)re  r=  r
  r   r  r7   r   r   r  r  r  r   rP  r   r  r\   )	rj  r=  r
  r  r  rQ  r!  beforer  s	            rV   delete_hookrX  4  sT     "ZZ2$$&E||!r((*G4TUU
-C IG)T"z)--2F'MYu%& '
q$'AEE),<,G 
	% i&'&0% GGGT"C7# 4LMM$<'
  s6   B5E7-E$>E#E 4E	EEEEz/api/ops/checkpointsc                    K   t               dz  } g }d}| j                         rt        | j                               D ]  }|j                         sd}d}|j	                  d      D ]6  }|j                         s	 ||j                         j                  z  }|dz  }8 ||z  }|j                  |j                  ||d        ||dS # t        $ r Y ow xY ww)z8List the /rollback shadow store checkpoints (read-only).ri  r   r   r   )r   r  r  )r  total_bytes)r   r   r  r  rglobrH  r  rI  r  r  rS   )cp_dirr  rZ  r  r  r  r  s          rV   list_checkpointsr]  4  s      .FHK}}FNN,- 	E<<>DE[[% 99; 0 00
	 4KOO :: 	$ !== # s*   A7C;"C
-C
	CCCCz/api/ops/checkpoints/prunec                     K   	 t        ddgd      } d| j
                  dd	S # t        $ r*}t        j                  d       t	        dd|       d }~ww xY ww)
Nri  pruner  z!Failed to spawn checkpoints prunerY  zFailed to prune checkpoints: r   TrB  r  rE  s     rV   prune_checkpointsr`  5  si     [#]G$<>QR txx1DEE  [:;6STWSX4YZZ[r  c                   ,    e Zd ZU eed<   dZee   ed<   y)SkillInstallRequest
identifierNr  r  r  rX   rV   rb  rb  #5  s    O!GXc]!rX   rb  c                     | xs dj                         }|r|j                         dv rg S ddlm} t	        |       d|j                  |      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   r]   r  r}
  rn  )r  r
  r  s      rV   r  r  (5  sN     B%%'I	)-CC	3#,55i@AArX   c                 .   t        j                  dd|j                               j                  d      dd xs d}t	        j
                  |j                               j                         dd }d|  d| d| }t        j                  |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  rO  Nr  r\  r  zskills-r  r  r  )r  r  r[  r  rS   s        rV   _hub_action_namerg  95  s     66-ciik288=crBMgD\\#**,'113BQ7FTF!D66(+D  d';<KrX   z/api/skills/hub/installc                 p  K   | j                   xs dj                         }|st        dd      t        d|      }	 t	        t        | j                  xs |      dd|dgz   |      }d|j                  |dS # t        $ r  t        $ r*}t        j                  d       t        d	d
|       d }~ww xY ww)Nr   r   identifier is requiredr   rc  rf  --yeszFailed to spawn skills installrY  zFailed to install skill: TrB  )rc  r   r7   rg  rD  r  r  r\   rQ   rK  r  )rj  r  rc  rS   r%  r  s         rV   install_skill_hubrk  H5  s     //'R..0J4LMMIz2D
W#dll5g6J89
 txx66   W786OPSu4UVVW(   :B6*A7 'B67B3	%B..B33B6c                   ,    e Zd ZU eed<   dZee   ed<   y)SkillUninstallRequestrS   Nr  r  r  rX   rV   rn  rn  \5  s    
I!GXc]!rX   rn  z/api/skills/hub/uninstallc                 p  K   | j                   xs dj                         }|st        dd      t        d|      }	 t	        t        | j                  xs |      dd|dgz   |      }d|j                  |dS # t        $ r  t        $ r*}t        j                  d       t        d	d
|       d }~ww xY ww)Nr   r   zname is requiredr   	uninstallrf  rj  z Failed to spawn skills uninstallrY  zFailed to uninstall skill: TrB  )rS   r   r7   rg  rD  r  r  r\   rQ   rK  r  )rj  r  rS   r  r%  r  s         rV   uninstall_skill_hubrq  a5  s     IIO""$D4FGGk40F	Y#dll5g6(KQUW^9__
 txx88   Y9:6QRUQV4WXXYrl  c                   "    e Zd ZU dZee   ed<   y)SkillsUpdateRequestNr  )r  r  r  r  r   r  r  r  rX   rV   rs  rs  t5  s    !GXc]!rX   rs  z/api/skills/hub/updatec                   K   	 | r| j                   nd xs |}t        t        |      ddgz   d      }d|j                  dd	S # t        $ r  t        $ r*}t
        j                  d       t        dd|       d }~ww xY ww)
Nrf  r  r  zFailed to spawn skills updaterY  zFailed to update skills: r   TrB  )r  rD  r  r7   r\   rQ   rK  r  )rj  r  r  r%  r  s        rV   update_skills_hubru  x5  s     	W%)T\\t?	#i(Hh+??
 txxAA   W676OPSu4UVVWs'   B.A BA>%A99A>>B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           
          | j                   | j                  | j                  | j                  | j                  | j
                  t        | j                  xs g       dS )N)rS   r"  r   rc  trust_levelrepotags)rS   r"  r   rc  r  r  r  r  )r  s    rV   _skill_meta_to_payloadr  5  sF    }}((ll}}QVV\r" rX   c                    	 ddl m} | xs dj                         }|r0|j                         dk7  rt	        |      } ||dz  dz  dz        }n |       }i }|j                         D ]L  }|j                  d      }|s|j                  d	      |j                  d
      |j                  d      d||<   N |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  rf  z.hubz	lock.jsonrc  rS   r  scan_verdict)rS   r  r  )tools.skills_hubr  r   r   r}
  list_installedr   r\   )r  r  r
  profile_dirlockr;  r  idents           rV   _installed_hub_identifiersr  5  s    0]))+	*i7.y9K{X5>LMD=D((* 	EIIl+E!IIf-#(99]#;$)IIn$=E
	 
 	s   A;B8 >9B8 8CCz/api/skills/hub/sourcesc                     K    fd}	 t        j                  |       d{   S 7 # t        $ r  t        $ r*}t        j                  d       t        dd|       d}~ww xY w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        g }d}g }D ]  }|j                         }|t        j                  ||      d}|dk(  r	 t        t        |dd            |d<   |dk(  rH	 t        t        |d	d            }||d
<   |r*	 |j                  dd      D cg c]  }t        |       }}|j                  |        t        h d      }	|D ]  }|xr |d   |	v  |d<    |||t        
      dS # 1 sw Y   xY w# t        $ r d|d<   Y w xY w# t        $ r d}Y w xY wc c}w # t        $ r g }Y w xY w)Nr   create_source_routerF)r  rg  r}  is_rate_limitedrate_limitedrz  is_availabler	  r   r  )rq  >   r}  r~  r  r{  r|  r  r  
searchable)r
  index_availablefeaturedr  )r  r  r  	source_id_SKILL_HUB_SOURCE_LABELSr   r   r   r\   r  r  r  r	  r  )r  r
  r;  r  r  r  r  r  r  _api_source_idsr  s             rV   r
  z%list_skills_hub_sources.<locals>._run5  s   9"7+ 	-*,G	- 	C--/C155c3?E
 h2,0>OQV1W,XE.) n$,&*73+N&OO &5k""&?Bzz"TVz?W$:;215$ $
 JJu5	B $]
  	[E'6'Y5;/;Y"ZE,	[ . 3G<	
 	
W	- 	- ! 2,1E.)2
 ! ,&+O,$ % &#%&sS   D	D<D)D?/D:D?	DD&%D&)D76D7:D??EENz!skills hub sources listing failedr  zHub sources failed: r   )ro   r  r7   r\   rQ   rK  )r  r
  r  s   `  rV   list_skills_hub_sourcesr  5  sh     3
jR&&t,,,,  R:;6J3%4PQQRs0   A() ') A() A%%A  A%%A(z/api/skills/hub/searchc                   K   | xs dj                         sg i g i dS fd}	 t        j                  |       d{   S 7 # t        $ r  t        $ r*}t
        j                  d       t        dd|       d}~ww xY w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                  L   ddl m} m} t              5   |        }d d d        t	        t        d      d      } |xs dd      \  }}}dddd	}i }|D ]v  }	|	j                  |vr|	||	j                  <   !|j                  |	j                  d      |j                  ||	j                     j                  d      kD  sh|	||	j                  <   x t        |j                               d | }
|
D cg c]  }t        |       c}||t              d
S # 1 sw Y   xY wc c}w )Nr   )r  parallel_search_sourcesr   rp  r  r   )r  r  overall_timeoutr$  )rT  trusted	communityr  )r  r  r  r  r  r\  rc  r   r  r  r  r  r  )r  r  r
  cappedall_resultsr  r  _rankr  r  dedupedr  rq  r  r  r   s               rV   r
  zsearch_skills_hub.<locals>._run6  s%   R"7+ 	-*,G	-S]B'0G5%QS1
-]I
 !!< 	'A||4'%&Q\\"1==!,uyyall9K9W9WYZ/[[%&Q\\"		'
 t{{}%gv. <CCa.q1C*"3G<	
 	
#	- 	-$ Ds   D2D!DNzskills hub search failedr  zHub search failed: r   )r   ro   r  r7   r\   rQ   rK  )r  r   rq  r  r
  r  r  s    ```  @rV   search_skills_hubr  6  s      W"OOERTUU
6Q&&t,,,,  Q126I#4OPPQs:   &BA A
A 	B
A B%BBBz/api/skills/hub/previewrc  c                 4  K   | xs dj                         st        dd      fd}	 t        j                  |       d{   }|t        d
d       |S 7 # t        $ r*}t
        j                  d       t        dd	|       d}~ww xY ww)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   ri  r   c                     ddl m}  ddlm} t	              5   |       } | |      \  }}}d d d        ssy i }d}|rg|j
                  xs i j                         D ]0  \  }}	t        |	t              r	 |	j                  d      ||<   ,|	||<   2 |j                  dd      xs d}xs |}
t        |
d      t        |
d	d      xs dt        |
d
d      xs dt        |
d      xs t        |
dd      xs dt        |
dd       t        t        |
dd       xs g       |t        |j                               d	S # 1 sw Y   xY w# t        $ r d||<   Y w xY w)Nr   _resolve_source_meta_and_bundler  r   r  z(binary file)SKILL.mdrS   r"  r   rc  r  r  r  r  )	rS   r"  r   rc  r  r  r  skill_mdr  )hermes_cli.skills_hubr  r  r  r  r  r  r  r  rN  UnicodeDecodeErrorr   r   r  r  r  )r  r  r
  r  bundle_srcr  r  relr   r  r  r  s              rV   r
  zpreview_skill_hub.<locals>._runP6  ss   I9"7+ 	Q*,G!@!PD&$	Q d!'!3 : : < 
)Wgu-5%,^^G%<c
 ")E#J
) yyR06BHNFAvu-"1mR8>Ba2.4"!!\59BU"1m[AP[Avt,FD17R8 EJJL)

 
	
/	Q 	Q  . 5%4c
5s   D;6E;EEENzskills hub preview failedr  zHub preview failed: r   Skill not found: r   r7   ro   r  r\   rQ   rK  rc  r  r
  r3  r  r  s    `   @rV   preview_skill_hubr  @6  s      2$$&E4LMM%
NR((.. ~6Gw4OPPM / R236J3%4PQQR:   *BA" A A" B A" "	B+%BBBz/api/skills/hub/scanc                 4  K   | xs dj                         st        dd      fd}	 t        j                  |       d{   }|t        d
d       |S 7 # t        $ r*}t
        j                  d       t        dd	|       d}~ww xY ww)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   ri  r   c                  t   dd l } ddlm} ddlm}m} ddlm}m} t              5   |       } ||      \  }}}	d d d        sy |j                  dk(  rd}
n t        |dd      xs t        dd      xs }
d }	  ||      } |||
      }|| j                  |d	
       	  ||d      \  }}|d	u rd}n|d}nd}|j                  D cg c]<  }|j                  |j                  |j                   |j"                  |j$                  d> }}ddddd}|j                  D ](  }|j                  |v s||j                  xx   dz  cc<   * |j&                  |j                  |j(                  |j*                  |j,                  ||||d
S # 1 sw Y   SxY w# || j                  |d	
       w w xY wc c}w )Nr   r  )r  quarantine_bundle)
scan_skillshould_allow_installry  rc  r   )r   T)ignore_errorsF)r<  allowaskr  )severityr'  r  r  r"  )criticalrZ  rY  rX  r   )
rS   rc  r   r  verdictr  r~  policy_reasonfindingsseverity_counts)r  r  r  r  r  r  tools.skills_guardr  r  r  r   r   r  r  r  r'  r  r  r"  
skill_namer  r  r  )_shutilr  r  r  r  r  r
  r  r  r  scan_sourceq_pathr3  r  ro  r~  r  r  countsr  r  s                      rV   r
  zscan_skill_hub.<locals>._run6  s    ILG"7+ 	Q*,G!@!PD&$	Q ==J&$K b1 4r2  	;&v.F{;F!vT:.vUCd?F_FF __	
  JJJJ }}	
 	
  aB 	(AzzV#qzz"a'"	(
 %%mm!--~~~~# %
 	
a	Q 	Q( !vT: "	
s   F<F AF5FF2Nzskills hub scan failedr  zHub scan failed: r   r  r  r  s    `   @rV   scan_skill_hubr  6  s      2$$&E4LMMB
HO((.. ~6Gw4OPPM / O/06Gu4MNNOr  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<   y)ProfileCreaterS   N
clone_fromFclone_from_default	clone_all	no_skillsr"  rU   r#  r  rv  keep_skills
hub_skills)r  r  r  r  r  r  r   r  r   r  r  r"  rU   r#  rv  r   r  r  r  rX   rV   r  r  6  s    
I $J$  %$ItIt!%K#%"Hhsm"E8C=
 ,.K'(-
  Kc
 JS	rX   r  c                       e Zd ZU eed<   y)ProfileRenamenew_nameNr  r  rX   rV   r  r  7  s    MrX   r  c                       e Zd ZU eed<   y)ProfileSoulUpdater   Nr  r  rX   rV   r  r  7  s    LrX   r  c                       e Zd ZU eed<   y)ProfileActiveUpdaterS   Nr  r  rX   rV   r  r  	7  r  rX   r  c                       e Zd ZU dZeed<   y)ProfileDescriptionUpdater   r"  N)r  r  r  r"  r  r  r  rX   rV   r  r  7  s    KrX   r  c                   "    e Zd ZU eed<   eed<   y)ProfileModelUpdaterU   r#  Nr  r  rX   rV   r  r  7  s    MJrX   r  c                       e Zd ZU dZeed<   y)ProfileDescribeAutoFr  N)r  r  r  r  r   r  r  rX   rV   r  r  7  s    ItrX   r  c                 >    	 t        | |      S # t        $ r |cY S w xY wr  )r   r\   )rR   rS   r?  s      rV   _profile_attrr  7  s(    tT"" s    c                    t        | dd      t        t        | dd            t        t        | dd            t        | d      t        | d      t        t        | dd            t        t        | d	d
      xs d
      t        t        | dd            t        | dd      xs dt        t        | dd            t        | d      t        | d      t        | d      t        | d      d udS )NrS   r   r   
is_defaultFr#  rU   has_envskill_countr   r  r"  description_autodistribution_namedistribution_versiondistribution_source
alias_pathrS   r   r  r#  rU   r  r  r  r"  r  r  r  r  	has_alias)r  r  r   r  )rR   s    rV   rf  rf  !7  s    dFB/M$34=|UCDtW-!$
3dIu=>=}a@EAFd4Eu MN$T="=C t5G!OP*41DE -d4J K,T3HI"46dB rX   c                 2    d }g } j                         j                         rv | fdd      \  }}|j                  dt              d||dz  j	                          | fdd       | fd	d
       | fdd       | fdd
      d d d d
d        j                         }|j                         rt        |j                               D ]  }|j                         r% j                  j                  |j                        s9 ||f fd	d      \  }}|j                  |j                  t        |      d
|||dz  j	                          ||f fd	d       ||f fd	d
       ||f fd	d       ||f fd	d
      d d d d
d        |S )Nc                 4    	  |        S # t         $ r |cY S w xY wr  )r\   )	callable_r?  s     rV   _safez&_fallback_profile_dicts.<locals>._safe57  s#    	; 	N	s   	 c                  &    j                         S r  _read_config_modeldefault_homer  s   rV   r  z)_fallback_profile_dicts.<locals>.<lambda>>7  s    (G(G(U rX   NNr?  Tr  c                  &    j                         S r  _count_skillsr  s   rV   r  z)_fallback_profile_dicts.<locals>.<lambda>F7  s    )C)CL)Q rX   r   c                  &    j                         S r  r  r  s   rV   r  z)_fallback_profile_dicts.<locals>.<lambda>G7  s    \-P-PQ]-^ rX   Fc                  F    j                         j                  dd      S Nr"  r   read_profile_metar   r  s   rV   r  z)_fallback_profile_dicts.<locals>.<lambda>H7  s!    )G)G)U)Y)YZgik)l rX   r   c                  F    j                         j                  dd      S Nr  Fr  r  s   rV   r  z)_fallback_profile_dicts.<locals>.<lambda>I7  s!    l.L.L\.Z.^.^_qsx.y rX   r  c                 &    j                  |       S r  r  r  r  s    rV   r  z)_fallback_profile_dicts.<locals>.<lambda>U7  s    8W8WX]8^ rX   c                 &    j                  |       S r  r  r  s    rV   r  z)_fallback_profile_dicts.<locals>.<lambda>]7  s    9S9STY9Z rX   c                 &    j                  |       S r  r   r  s    rV   r  z)_fallback_profile_dicts.<locals>.<lambda>^7  s    \=`=`af=g rX   c                 F    j                  |       j                  dd      S r  r  r  s    rV   r  z)_fallback_profile_dicts.<locals>.<lambda>_7  s"    9W9WX]9^9b9bcprt9u rX   c                 F    j                  |       j                  dd      S r  r  r  s    rV   r  z)_fallback_profile_dicts.<locals>.<lambda>`7  s)    l>\>\]b>c>g>ghz  }B  ?C rX   )_get_default_hermes_homer   r  r  r{  _get_profiles_rootr  r  _PROFILE_ID_REr  rS   )r  r  r  r#  rU   profiles_rootr  r  s   `      @rV   rg  rg  47  s    &(H88:L UWcdx% $v-557 !QSTU$%^`ef !lnpq %&y  |A  !B!%$(#'
 	" !335MM1134 	E<<>)D)D)J)J5::)V#$^`lmOE8OO

E
#$!FN224$%%Z\]^#(e)gin#o$%%uwyz$)u  +C  EJ  %K%)(,'+" 		* OrX   c                     ddl m} 	 |j                  |        |j                  |       st	        dd|  d      |j                  |       S # t        $ r}t	        dt        |            d}~ww xY w)	zIValidate ``name`` and resolve to its directory or raise an HTTPException.r   r  r   r   Nr   rl  rm  )r]   r  ro  r  r7   r  rp  r  )rS   r  r  s      rV   r}
  r}
  j7  ss    3<**40 &&t,ivEV4WXX''--	  <CF;;<s   A 	A0A++A0c                 0    t        |        | dk(  rdS |  dS )z@Return the shell command used to configure a profile in the CLI.r?  zhermes setupz setup)r}
  rP  s    rV   _profile_setup_commandr  v7  s"    !Y.>CtfFOCrX   r  c                     ddl m}m}  |t        |             }	 t	        ||      \  }}t               }t        |j                  di       ||      |d<   t        |        ||       y#  ||       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{  rz  r#  N)	rl  r{  rz  r  rJ  r   rS  r   r   )r  rU   r#  r{  rz  r   r  s          rV   _write_profile_modelr  |7  sk     V$S%56E*:8UK%m3CGGGR4H(TYZGC"5)"5)s   AA( (
A2r  c                    ddl m}m} ddlm} d} |t        |             }	 t               }|j                  di       }|D ]*  }		 t        |	      \  }
}}| ||
|      |d	<   |||
<   |d
z  }, |rt        |       n|s|j                  dd       t        |        ||       |S # t        $ rC}|	j                  xs dj                         xs d}t        j                  d||       Y d}~d}~ww xY w#  ||       w xY w)a  Write MCP server entries into a specific profile's config.yaml.

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

    Mirrors the per-server shape the ``POST /api/mcp/servers`` endpoint builds,
    but batched so the whole profile-create write is a single config save.
    Returns the number of servers written.
    r   r  )r  rv  r   z	<unnamed>z,Profile-create: skipping MCP server '%s': %sNr   r   )rl  r{  rz  r  r  r  r   r  r  r  rS   r   rQ   rS  r   rP  )r  r  r{  rz  r  writtenr   r  ru  r  rS   r  rS
  r  display_names                  rV   _write_profile_mcp_serversr  7  s    V=G$S%56E*mnn]B/ 	F	,H,P)e\ '#:4#Ni CIqLG	  GGM4("5)N+   & 1r88:IkB 
 ( 	#5)s6   !C4 B%AC4 %	C1.9C,'C4 ,C11C4 4
C>keepc                    ddl m}m} ddlm}m} |D ch c]&  }|s|j                         s|j                         ( }}d} |t        |             }		 g }
| dz  }|j                         r;|j                  d      D ]'  }|
j                  |j                  j                         ) t               } ||      }|
D ]"  }||vs||vs|j                  |       |dz  }$ |r	 |||        ||	       |S c c}w #  ||	       w xY w)uA  Disable every installed skill in ``profile_dir`` not in ``keep``.

    Profiles manage skill activation via a *disabled* list — all installed
    skills are active by default and users opt out. The builder's skill step
    uses "replace" semantics: the user picks exactly which seeded built-in /
    optional skills stay active, and everything else gets added to the disabled
    list. (Hub skills are installed separately via subprocess and are active on
    install.) Scoped to the profile via the HERMES_HOME override. Returns the
    number of skills newly disabled.
    r   r  get_disabled_skillssave_disabled_skillsrf  r  r   )rl  r{  rz  hermes_cli.skills_configr  r  r   r  r   r[  r  r  rS   r   r  )r  r  r{  rz  r  r  rx  keep_setdisabled_countr   r  skills_rootmdr  r	  rS   s                   rV   _disable_unselected_skillsr%  7  s    VR#';a1	;H;N$S%56E*!	!H,!''
3 1  01m&s+ 	$D8#H(<T"!#	$  h/"5)' <$ 	#5)s(   C6C6C6A-C; C; 	#C; ;
Dz/api/profilesc                  .  K   ddl m}  	 t        j                         }|j	                  d | j
                         d {   }d|D cg c]  }t        |       c}iS 7 c c}w # t        $ r% t        j                  d       dt        |       icY S w xY ww)Nr   r  r  z@GET /api/profiles failed; falling back to profile directory scan)r]   r  ro   r  ru   r  rf  r\   rQ   rK  rg  )r  r  r  r   s       rV   list_profiles_endpointr'  7  s     3C'')--dL4N4NOO(CQ-a0CDD PC CYZ3LABBCsJ   B3A$ A	A$ AA$ BA$ A$ $+BBBBc           	        K   ddl m} | j                  xs dj                         }|rd}|}| j                   }n'| j                  rd}d}d}n| j
                  }|rdnd }|}	 |j                  | j                  || j                  || j                  | j                        }|s|j                  |d       |j                  | j                        }|s|j                  | j                         | j*                  xs dj                         }	| j,                  xs dj                         }
d}|	r|
r	 t/        ||	|
       d}d}| j0                  r	 t3        || j0                        }d}| j4                  r	 t7        || j4                        }g }| j8                  D ]^  }|xs dj                         }|s	 t;        d| j                  dd|dgt=        d|            }|j?                  ||j@                  d       ` d| j                  t#        |      ||||dS # t        t        t        f$ r}t!        d	t#        |      
      d }~wt$        $ r0}t&        j)                  d       t!        dt#        |      
      d }~ww xY w# t$        $ r$ t&        j)                  d| j                         Y kw xY w# t$        $ r$ t&        j)                  d| j                         Y vw xY w# t$        $ r$ t&        j)                  d| j                         Y w xY w# t$        $ r9 t&        j)                  d|| j                         |j?                  |d d       Y w xY ww)Nr   r  r   Tr?  F)rS   r  r  clone_configr  r"  )quietr   r   zPOST /api/profiles failedrY  z'Setting model for new profile %s failedz-Writing MCP servers for new profile %s failedz2Applying skill selection for new profile %s failedre  rf  rc  rj  )rc  r  z7Spawning hub-skill install %s for new profile %s failed)r  rS   r   	model_setmcp_writtenskills_disabledhub_installs)!r]   r  r  r   r  r  create_profilerS   r  r"  seed_profile_skillscheck_alias_collisioncreate_wrapper_scriptr  FileExistsErrorr  r7   r  r\   rQ   rK  rU   r#  r  rv  r  r  r%  r  rD  rg  r  r  )rj  r  explicit_sourcecloner  r)  r   	collisionr  rU   r#  r+  r,  r-  r.  rc  r  r%  s                     rV   create_profile_endpointr7  7  s7    3,"335O $
>>)	 
''"'YT
<**!nn%nn(( + 
 ,,T,> !66tyyA	..tyy9 #**,HZZ2$$&EIE	Q x7I
 K	W4T4;K;KLK O	\8t?O?OPO *,Loo D
!r((*	D'tyy(IugF E2D uTXX FGD& 		D	"*$ y ):; <CF;; <23CF;;<  	QNNDdiiP	Q  	WNNJDIIV	W  	\NNOQUQZQZ[	\"  	DNNI		
 uT BC	Ds   A M#BH /AM2J  MJ0 &M5K  (M4AL8MI=+II=+I88I==M )J-)M,J--M0)KMKM )L	MLM>MMMMz/api/profiles/activec                     K   ddl m}  	 | j                         xs d}	 | j	                         xs d}||dS # t        $ r d}Y 'w xY w# t        $ r d}Y #w xY ww)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  )r]   r  get_active_profiler\   r{
  )r  r  r  s      rV   get_active_profile_endpointr:  a8  sr      4002?i668EI 11    sB   A8 A	 AAAAA	AAAAc                   K   ddl m} 	 |j                  | j                         d	|j                  | j                        d
S # t        $ r}t        dt        |            d}~wt        $ r}t        dt        |            d}~wt        $ r0}t        j                  d       t        dt        |            d}~ww xY ww)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 failedrY  Tr  )r]   r  set_active_profilerS   r  r7   r  r  r\   rQ   rK  rn  )rj  r  r  s      rV   set_active_profile_endpointr=  v8  s      4<''		2 ,"E"Edii"PQQ  <CF;; <CF;; <9:CF;;<s?   CA C	C A""C .BC +B;;C  Cz"/api/profiles/{name}/setup-commandc                 $   K   dt        |       iS w)Nr  )r  rP  s    rV   get_profile_setup_commandr?  8  s     -d344s   z"/api/profiles/{name}/open-terminalc                   K   	 t        |       }t        j                  j                  d      rt	        j
                  dddd|g       nt        j                  dk(  rA|j                  dd      j                  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 ]U  \  }}t	        j                  d|gt        j                  t        j                        d k(  s@t	        j
                  |        n t        d!d"#      d'|d(S # t        $ r}t        d$t        |      #      d }~wt        $ r}t        d!t        |      #      d }~wt        $ r  t        $ r1}t        j                  d%|        t        d&t        |      #      d }~ww xY ww))Nr  zcmd.exez/crT   r   r
  \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xtermr  )r1  r  r   r   z$No supported terminal emulator foundr   r   z*POST /api/profiles/%s/open-terminal failedrY  T)r  r  )r  r,  r-  r   r.  r  r  callr  r7   r  r  r  r\   rQ   rK  )rS   r  escapedapplescriptterminal_commandsr  
popen_argsr  s           rV   open_profile_terminal_endpointrO  8  s]    0<(.<<""5)iwGDE\\X%oodF3;;CGG%Y '  k4=> ')>dESZ([\!$4dD%#QRYdE7CD!$4dhwiq<Q#RS ?DHWIQ:O"PQdhwiq4IJK7D$w?@{D$wGH7D%9:7D$w?@! +< &
J??j)%--%-- 	
 $$Z0 $ #A  7++  <CF;; <CF;;  <CTJCF;;<sG   G+D2E! 7%E! G+!	G(*F  G(F""G(7,G##G((G+z/api/profiles/{name}c                   K   ddl m} 	 |j                  | |j                        }d|j                  t        |      d	S # t        $ r}t        dt        |            d }~wt        t        f$ r}t        dt        |            d }~wt        $ r1}t        j                  d|        t        dt        |            d }~ww xY ww)
Nr   r  r   r   r   zPATCH /api/profiles/%s failedrY  T)r  rS   r   )r]   r  rename_profiler  r  r7   r  r  r3  r\   rQ   rK  )rS   rj  r  r   r  s        rV   rename_profile_endpointrR  8  s     3<**4? s4yAA  <CF;;( <CF;; <6=CF;;<s=   C? C	CAC0BC,B>>CCc                 \  K   ddl m} 	 |j                  | d      }dt        |      dS # t        $ r}t	        dt        |            d}~wt        $ r}t	        dt        |            d}~wt        $ r1}t        j                  d	|        t	        d
t        |            d}~ww xY ww)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)r  r   r   Nr   zDELETE /api/profiles/%s failedrY  r  )
r]   r  delete_profiler  r7   r  r  r\   rQ   rK  )rS   r  r   r  s       rV   delete_profile_endpointrU  8  s     
 4<**4T*: D	**  <CF;; <CF;; <7>CF;;<s<   B,+ B,	B)A

B)A,,B)8,B$$B))B,z/api/profiles/{name}/soulc                    K   t        |       dz  }|j                         r	 |j                  d      ddS d	d
dS # t        $ r}t	        dd|       d }~ww xY ww)NSOUL.mdr  r  T)r   r{  rY  zCould not read SOUL.md: r   r   F)r}
  r{  r  r  r7   )rS   	soul_pathr  s      rV   get_profile_soulrY  8  sx     $T*Y6I	X(22G2DPTUU U++  	XC:RSTRU8VWW	Xs%   A< A	AAAAc                    K   t        |       dz  }	 |j                  |j                  d       dd	iS # t        $ r+}t        j                  d|        t        dd|       d }~ww xY ww)
NrW  r  r  z PUT /api/profiles/%s/soul failedrY  zCould not write SOUL.md: r   r  T)r}
  r  r   r  rQ   rK  r7   )rS   rj  rX  r  s       rV   update_profile_soulr[  8  sv     $T*Y6IUT\\G< $<  U94@6OPQs4STTUs$   A*3 A*	A'&A""A''A*z /api/profiles/{name}/descriptionc                   K   ddl m} t        |       }|j                  xs dj	                         }	 |j                  ||d       d
|ddS # t        $ r1}t        j                  d|        t        dt        |            d	}~ww xY ww)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 failedrY  r   NT)r  r"  r  )r]   r  r}
  r"  r   write_profile_metar\   rQ   rK  r7   r  )rS   rj  r  r  r  r  s         rV   #update_profile_description_endpointr^  8  s      4&t,K"))+D<''" 	( 	
 tGG  <@$GCF;;<s(   0B
A B
	B,BBB
z/api/profiles/{name}/modelc                 \  K   t        |       }|j                  xs dj                         }|j                  xs dj                         }|r|st	        dd      	 t        |||       d||d	S # t        $ r1}t        j                  d|        t	        dt        |            d}~ww xY ww)
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 failedrY  NTr	  )
r}
  rU   r   r#  r7   r  r\   rQ   rK  r  )rS   rj  r  rU   r#  r  s         rV   update_profile_model_endpointr`  9  s      't,K#**,HZZ2$$&E54UVV<[(E: Hu==  <:DACF;;<s*   AB,A/ )B,/	B)8,B$$B))B,z"/api/profiles/{name}/describe-autoc                 v  K   t        |        	 ddlm} |j                  | t	        |j
                              }t	        |j                        |j                  |j                  t	        |j                        dS # t        $ r1}t        j                  d|        t        dt        |            d}~ww xY ww)	uo  Auto-generate a profile's description via the auxiliary LLM
    (``auxiliary.profile_describer``). Mirrors ``hermes profile describe
    <name> --auto``.

    A failed generation (no aux client, LLM error, …) is returned as
    ``ok: false`` with a reason rather than an HTTP error so the UI can
    surface it inline and let the operator fix config and retry.
    r   )r>  )r  z*POST /api/profiles/%s/describe-auto failedrY  r   N)r  ro  r"  r  )r}
  r]   r>  describe_profiler   r  r\   rQ   rK  r7   r  r  ro  r"  )rS   rj  r>  outcomer  s        rV   describe_profile_auto_endpointrd  '9  s      <0#44TT$..EY4Z
 7::..** !,   <CTJCF;;<s(   B9,A< AB9<	B6,B11B66B9c              #   h  K   | xs dj                         }ddlm}m}m} ddlm} ddlm} d}|r|j                         dk(  r |       }nt        |      } |t        |            }t        5  |j                  }	|j                  }
|j                  }|j                  }||_        |dz  |_        ||_        |dz  |_        	 ||nd |	|_        |
|_        ||_        ||_        |	 ||       	 ddd       y# |	|_        |
|_        ||_        ||_        |	 ||       w w xY w# 1 sw Y   yxY ww)	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{  rz  )skills_tool)skill_manager_toolNr  rf  )r   rl  r   r{  rz  r  rf  rg  r   r}
  r  _SKILLS_PROFILE_LOCKrj  
SKILLS_DIR)r  r
  r   r{  rz  _skills_tool
_skill_mgrr   r  old_homeold_skills_dirold_mgr_homeold_mgr_skills_dirs                rV   r  r  S9  sM    , B%%'I 
 26E	)Y6%'*95([)9:	 2++%00!--'22#. "-"8!,
 +h 6
	2!&!2+<'/L$&4L#%1J"$6J! *51#2 2 (0L$&4L#%1J"$6J! *51 !!2 2s7   A,D2.AD&C:
'D&1	D2:)D##D&&D/+D2c              #      K   | xs dj                         }|r|j                         dk(  rd yddlm}m} t        |      } |t        |            }	 |  ||       y#  ||       w xY w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   r  )r   r   rl  r{  rz  r}
  r  )r  r
  r{  rz  r  r   s         rV   r  r  9  so       B%%'I	)Y6

 'y1K$S%56E*"5)"5)s   AA0A# 	A0#
A--A0c                   6    e Zd ZU eed<   eed<   dZee   ed<   y)SkillTogglerS   r  Nr  )r  r  r  r  r  r   r  r   r  rX   rV   rr  rr  9  s    
IM!GXc]!rX   rr  z/api/skillsc                 j  K   ddl m} ddlm} ddlm}m}m}m} t        |       5  t               } ||      } |d      }	 |       }
 |       } |       }d d d        	D ]A  }|d   v|d<    |
j                  |d   i             |d	<   |d   v rd
n
|d   v rdnd|d<   C |	S # 1 sw Y   QxY ww)Nr   )_find_all_skills)r  )_read_bundled_manifest_names_read_hub_installed_namesactivity_count
load_usageT)skip_disabledrS   r  usagehubr   re  
provenance)tools.skills_toolrt  r   r  tools.skill_usageru  rv  rw  rx  r  r   r   )r  rt  r  ru  rv  rw  rx  r  r	  rf  rz  bundled_names	hub_namesrx  s                 rV   
get_skillsr  9  s     2<  
	  
0&v.!5
 56-/	
0  
y0)#EIIai$<='
vY)+Ei=8 	
,
 M'
0 
0s   $B31B'AB3'B0,B3z/api/skills/togglec                 f  K   ddl m}m} t        | j                  xs |      5  t               } ||      }| j                  r|j                  | j                         n|j                  | j                          |||       d d d        d| j                  | j                  dS # 1 sw Y   #xY ww)Nr   r  Trw  )
r   r  r  r  r  r   r  r_  rS   r  )rj  r  r  r  r  r	  s         rV   toggle_skillr  9  s     R	/	0 /&v.<<TYY'LL#VX./ 		dllCC/ /s   "B1AB%"B1%B.*B1c                   J    e Zd ZU eed<   eed<   dZee   ed<   dZee   ed<   y)SkillCreaterS   r   Nr'  r  )r  r  r  r  r  r'  r   r  r  rX   rV   r  r  9  s(    
IL"Hhsm"!GXc]!rX   r  c                   6    e Zd ZU eed<   eed<   dZee   ed<   y)SkillContentUpdaterS   r   Nr  r  r  rX   rV   r  r  9  s    
IL!GXc]!rX   r  c                  B    	 ddl m}   | d       y# t        $ r Y yw xY w)zBest-effort: invalidate the skills system-prompt snapshot after a write.

    Mirrors what ``skill_manage`` does so a dashboard-authored skill is picked
    up by the next session without a manual cache reset.
    r    clear_skills_system_prompt_cacheT)clear_snapshotN)agent.prompt_builderr  r\   r  s    rV   _clear_skills_prompt_cacher  9  s$    I(= s    	z/api/skills/contentc                 t  K   ddl m} t        |      5   ||       }|st        dd|  d      |d   dz  }|j	                         st        dd|  d	      	 |j                  d
      }| |t        |      dcddd       S # t        $ r}t        dt        |            |d}~ww xY w# 1 sw Y   yxY ww)zCReturn the raw SKILL.md text for a skill, for the dashboard editor.r   )_find_skillr   zSkill 'z' not found.r   r   r  z' has no SKILL.md.r  r  rY  N)rS   r   r   )tools.skill_manager_toolr  r  r7   r{  r  r  r  )rS   r  r  r 	  r  r   r  s          rV   get_skill_contentr  9  s      5		  ID!C'$|8TUU=:- C'$GY8Z[[	K(('(:G #h-HI I  	KCCAsJ	KI IsA   B8AB,B,B,:
B8	B)B$$B))B,,B51B8c                 6  K   ddl m} t        | j                        5   || j                  | j
                  | j                  xs d      }ddd       j                  d      st        d|j                  dd            t                |S # 1 sw Y   CxY ww)	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_skillNr1  r   r  zFailed to create skill.r   )
r  r  r  r  rS   r   r'  r   r7   r  )rj  r  r3  s      rV   create_skillr  :  s~      7		% Otyy$,,8MNO::i FJJwHa4bcc MO Os   B-BABBBc                 Z  K   ddl m} t        | j                        5   || j                  | j
                        }ddd       j                  d      s>|j                  dd      }dt        |      j                         v rdnd	}t        ||
      t                |S # 1 sw Y   dxY ww)zIReplace the SKILL.md of an existing skill (full rewrite) from the editor.r   )_edit_skillNr1  r  zFailed to update skill.rS  r   r   r   )r  r  r  r  rS   r   r   r  r   r7   r  )rj  r  r3  errrc  s        rV   update_skill_contentr  $:  s      5		% 6TYY56::i jj";<#s3x~~'77Ss;; M6 6s   B+BA#B+B($B+z/api/tools/toolsetsc                   K   ddl m}m}m}m}m} ddlm} ddlm	} t        |       5  t               } |       }	|	D 
ch c]  \  }
}} ||
       }}
}|D ci c]  }| |||d       }}d d d        g }	D ]e  \  }
}}	 t        t         ||
                  } ||
      }|
|   v }|j                  |
 ||      || | |||            || ||
      |d	       g |S c c}}
w c c}w # 1 sw Y   xY w# t        $ r g }Y lw xY ww)Nr   )$_get_effective_configurable_toolsetsr  _toolset_configuration_platform_toolset_has_keysgui_toolset_label)r  )resolve_toolsetFr  )	rS   rg  r"  r-  r  r  r	  r  r  )r  r  r  r  r  r  hermes_cli.platformsr  toolsetsr  r  r   r  r   r\   r  )r  r  r  r  r  r  r  r  r  toolset_rowsrS   r  target_platformsr-  enabled_by_platformr3  rg  descr  target_platformr*  s                        rV   get_toolsetsr  3:  sd      4(		  
;=DP
6@dAq+D1
 
 -
  ),1 
 

 F) eT	3t456E :$?0AA
&u-'/@ "#+D&9
 	( MA


 
$  	E	s^   &DC/C$C/C*,C/.DC;AD$C//C84D;D	DD		Dc                   ,    e Zd ZU eed<   dZee   ed<   y)ToolsetToggler  Nr  rs  r  rX   rV   r  r  e:  rt  rX   r  z/api/tools/toolsets/{name}c                   K   ddl m}m}m}m}  |       D ch c]  \  }}}|
 }	}}| |	vrt        dd|         ||       }
t        |j                  xs |      5  t               }t         |||
d            }|j                  r|j                  |        n|j                  |         |||
|       ddd       d	| |
|j                  d
S c c}}w # 1 sw Y    xY ww)a  Enable/disable a configurable toolset for its configuration platform.

    Most toolsets persist to ``platform_toolsets.cli``. Platform-restricted
    toolsets instead target their supported platform (for example, Discord's
    native toolsets persist to ``platform_toolsets.discord``). The shared
    ``_save_platform_tools`` helper keeps the GUI and CLI in lockstep. Scoped
    to ``body.profile`` when provided. Returns 400 for unknown toolset keys.
    r   )r  r  _save_platform_toolsr  r   Unknown toolset: r   Fr  NT)r  rS   r-  r  )r  r  r  r  r  r7   r  r  r   r   r  r  r_  )rS   rj  r  r  r  r  r  ts_keyr  r  r  r  r  s                rV   toggle_toolsetr  j:  s       )M(NO1VOEO56Gv4NOO5d;O	/	0 ?,1
 <<KKOOD!V_g>? #<<	 ' P
? ?s(   C!C8C!AC6C!CC!z!/api/tools/toolsets/{name}/configc                 |  K   ddl m}m}m}m}m}m} ddlm} ddl	m
}	  |       D 
ch c]  \  }
}}|

 }}
}| |vrt        dd|        t        |      5  t               }|j                  |       }g }d}d}d}|rU |	|d	
      } |||d	
      D ]<  }|j                  dg       D cg c]P  }|d   |j                  d|d         |j                  d      |j                  d      t         ||d               dR }} |||d	
      }|r||d   }|d   |j                  dd      |j                  dd      ||j                  d      t        |j                  d            | |||||      d}| dk(  r'|j                  d      r|d   |d<    ||d         |d<   | dk(  r|j                  d      r|d   |d<   |j!                  |       ? | dk(  r	 ddlm}m}  |       } |       }ddd       | dud}| dk(  r
|d <   |d!<   |S c c}}
w c c}w # t(        $ r d}d}Y <w xY w# 1 sw Y   AxY ww)"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_providersprovider_readiness_statusweb_provider_capabilitiesr
  rc  r   r  r   NTrX  r+	  r  rF   r   r?  )r  rF   r   r?  r(  rS   badger   tag
post_setuprequires_nous_auth)rn  rz  )rS   r  r  r+	  r  r  rz  rc  webweb_backendr  r~  tts_provider)_get_extract_backend_get_search_backend)rS   has_categoryr  active_provideractive_search_backendactive_extract_backend)r  r  r  r  r  r  r  r<  r
  ro  rd  r7   r  r   r   r   r  tools.web_toolsr  r  r\   )rS   r  r  r  r  r  r  r  r
  rd  r  r  r  r  catr  r  r  r  rn  provr  r+	  rz  r  r  r  r  s                               rV   get_toolset_configr  :  s      0K(L(NO1VOEO56Gv4NOO		  H.!!$'	 $!% 6f$OH*3DI /& "XXj"5	   !x"#%%!E("; uuU|#$55#3"&}QuX'>"?	 	 0f$O	!8&*6lO L!XXgr288E2. ("&((<"8*.txx8L/M*N!*
 8fx9  5=TXXm%<
 *.m)<C&*CDDW*XC'5=TXXn%=
 +/~*>C'  %_/&` 5=
.U(;(=%)=)?&KH.T 4*	G u}+@'(,B()Nm P"	r  .(,%)-&.MH. H.sZ   'H<H"H<AH0,AHCH0H0(H<H0H-*H0,H--H00H95H<c                   @    e Zd ZU eed<   dZee   ed<   dZee   ed<   y)ToolsetProviderSelectrU   N
capabilityr  )r  r  r  r  r  r  r   r  r  rX   rV   r  r  ;  s%    M !%J$!GXc]!rX   r  	image_gen	video_gen)r  r  r  provider_rowc                     | dk(  r(|j                  d      xs |j                  d      rdS dS | dk(  r|j                  d      S y)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     rV   _resolve_toolset_model_pluginr  ;  s]      78 
!%%&89E	
?C	
  788rX   r   c                 <    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       rV   _toolset_model_catalogr  &;  s'    
 (55$[11rX   c                     ddl m}mm} |j	                  |       }|y ||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  NTrX  c              3   L   K   | ]  }|j                  d       k(  s|  ywr   r   )r   r   rU   s     rV   r   z-_find_toolset_provider_row.<locals>.<genexpr>?;  s      B1f(AQBr   c              3   :   K   | ]  } |d       s|  yw)TrX  Nr  )r   r   r  r  s     rV   r   z-_find_toolset_provider_row.<locals>.<genexpr>A;  s     Mq/6tLMs   )r  r  r  r  r   r   )r  r  rU   r  r  r  r"  r  s    ``    @rV   _find_toolset_provider_rowr  2;  sb      

f
%C
{c6t<DBBDIIMDMt rX   z!/api/tools/toolsets/{name}/modelsc                 "  K   t         j                  |       }|| 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        | |      \  }}|j                  |      }	d}
t        |	t              rA|	j                  d      }t        |t              r |j                         r|j                         }
|
|vr||v r|nd}
ddd       j                         D cg c]M  \  }}||j                  d|      |j                  dd      |j                  dd      |j                  d	d      d
O }}}| t        |      r|j                  d      nd|
dS # 1 sw Y   xY wc c}}w w)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)rS   
has_modelsr\  r  r?  r#  rl  speedr   	strengthsprice)r  rl  r  r  r  rS   )rS   r  rU   r  r\  r  r?  )_MODEL_CATALOG_TOOLSETSr   r  r   r  r  r  r  r  r  r   r  r   )rS   rU   r  r  r  r  r  catalogr  section_cfgr  r  model_idr  r\  s                  rV   get_toolset_modelsr  E;  s     &))$/GERD]abb		  J(vx@=@.tS9d#J J "8f!Ejj)k4(//'*C#s#		))+'!'4'?mTG+J> &mmo	 Hd xx	84XXgr*+r2XXgr*	
	F 	 6l'*CGGFO  CJ J.	s6   +F1E=
F(A?E='FAF	)F=FFc                   @    e Zd ZU eed<   dZee   ed<   dZee   ed<   y)ToolsetModelSelectr#  NrU   r  )r  r  r  r  r  rU   r   r  r  rX   rV   r  r  ;  s#    J"Hhsm"!GXc]!rX   r  z /api/tools/toolsets/{name}/modelc                 `  K   t         j                  |       }|t        dd|        |j                  xs dj	                         }|st        dd      t        |j                  xs |      5  t               }t        | ||j                        }|rt        | |      nd}|st        dd|        t        | |      \  }}	||vrt        dd|d	|      |j                  |i       }
t        |
t              si }
|
||<   ||
d
<   t        |       ddd       d| |dS # 1 sw Y   xY ww)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)r  rS   r#  r  )r  r   r7   r#  r   r  r  r   r  rU   r  r  r  r  r  r   )rS   rj  r  r  r  r  r  r  r  _defaultr  s              rV   select_toolset_modelr  ;  sW     &))$/G&DTF$K
 	
 

 b'')H4GHH	/	0 (vt}}E=@.tS9d@G 
 34@7"'|=
K 
 ''4+t,K)F7O'GF-0 x6JJ1 s   A.D.0B#D"D."D+'D.z#/api/tools/toolsets/{name}/providerc           	        K   ddl m}m}m}m}m} ddlm}m}	  |       D 
ch c]  \  }
}}|

 }}
}| |vrt        dd|        j                  ;| dk7  rt        dd	      j                  d
vrt        ddj                  d      t        j                  xs |      5  t               }j                  |j                  |       }|r |||d      ng }t        fd|D        d      }|t        ddj                   d|       |j                  d      }|st        ddj                   d      j                   ||      vr&t        dj                    dj                         |j#                  di       }t%        |t&              si }||d<   ||j                   d<   n	  || j                   |       t/        |       d| j                   d}j                  j                  |d<   |j                  |       }d}|rt        fd |||d      D        d      }|xs i j                  d      }|rm |	|d      }|j0                  }|j                  |      }t3        |xr- |j4                  xr |r|j7                  |      n|j8                        }|s
d|d<   ||d<   ddd       |S c c}}
w # t(        $ r*}t        dt+        |      j-                  d            d}~ww xY w# 1 sw Y   S xY ww)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.

    For the ``web`` toolset only, an optional ``capability`` ('search' |
    'extract') scopes the selection to ``web.search_backend`` /
    ``web.extract_backend`` — the same per-capability overrides the runtime
    dispatchers (``tools.web_tools._get_search_backend`` /
    ``_get_extract_backend``) resolve first. The provider must actually
    support the requested capability (a search-only backend can't be the
    extract backend). Omitting ``capability`` keeps the legacy whole-provider
    behavior (writes ``web.backend``).

    Managed Nous rows (``managed_nous_feature``) additionally report the
    Portal entitlement state: the CLI flow gates these selections on
    ``ensure_nous_portal_access`` (inline login), but the GUI has no inline
    prompt, so selecting one while logged out / unentitled used to write the
    config keys and then never activate (``_is_provider_active`` requires
    ``managed_by_nous``). The response now carries an additive
    ``needs_nous_auth: true`` + ``feature`` so the client can drive the
    existing Nous Portal OAuth flow (``POST /api/providers/oauth/nous/start``)
    and refetch.
    r   )r  apply_provider_selectionr  r  r  )!MANAGED_FEATURE_COVERAGE_CATEGORYrd  r   r  r   Nr  z:capability selection is only supported for the web toolset)r  extractzUnknown capability: z! (expected 'search' or 'extract')TrX  c              3   `   K   | ]%  }|j                  d       j                  k(  s"| ' ywr   r   rU   r   r   rj  s     rV   r   z*select_toolset_provider.<locals>.<genexpr>;  s$     Pqv$--1OPs   #..r  z for toolset r  rU  z has no web backend keyz does not support _backendr  )r  rS   rU   r  c              3   ^   K   | ]$  }|j                  d       j                  k(  r| & ywr   r  r  s     rV   r   z*select_toolset_provider.<locals>.<genexpr> <  s-      uuV}5 s   *-managed_nous_featureneeds_nous_authfeature)r  r  r  r  r  r  ro  r  rd  r7   r  r  r  r   r   r   rU   r  r  r  r
  r  r   r   account_infor   rh  tool_gateway_entitled_fortool_gateway_entitled)rS   rj  r  r  r  r  r  r  r  rd  r  r  r  r  r  r  r  backendweb_cfgr  rL  r  managed_featurern  acctr'  entitleds    `                         rV   select_toolset_providerr  ;  sW    > 
 )M(NO1VOEO56Gv4NOO"5=S  ??"77-doo-@@ab 
 
/	0 F6??&
 "%%d+CMP*3DIVXIPIPRVWD|# #.t}}.?}THU  hh}-G# #&t}}&77NO  &?&HH# #"mm_,>t>OP  ''r2Ggt, 'u4;Gt'x01Q(t}}fE 	F*.$--#X??&%)__H\" !!$'/VN
 C 9"//*@A5f$OH((D8<<_MH NN   228<33H .2*+&5#MF6N Oo Pd  Q#CHNN3<OPPQEF6N OsN   !LKA7L(DK?8K	C-K?9L		K<%K77K<<K??L	Lc                   6    e Zd ZU eeef   ed<   dZee   ed<   y)ToolsetEnvUpdater  Nr  )r  r  r  r   r  r  r  r   r  rX   rV   r  r  ;<  s    	c3h!GXc]!rX   r  z/api/tools/toolsets/{name}/envc                   K   ddl m}m}m} ddlm}m}  |       D 	ch c]  \  }}	}	|
 }
}}	| |
vrt        dd|        t        |j                  xs |      5  t               }|j                  |       }t               }|r; |||d      D ]-  }|j                  d	g       D ]  }|j                  |d
           / |j                  D cg c]	  }||vs| }}|r+t        dd|  ddj                  t!        |                   g }g }|j                  j#                         D ]R  \  }}|r:|j%                         r*	  |||j%                                |j+                  |       B|j+                  |       T |D ci c]  }|t-         ||             }}ddd       d| dS c c}	}w c c}w # t&        $ r}t        dt)        |            d}~ww xY wc c}w # 1 sw Y   HxY ww)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   r   r  r   TrX  r+	  r  zUnknown env var(s) for toolset r  r  N)r  rS   savedskippedr(  )r  r  r  r  r<  r
  r   r7   r  r  r   r   r   r  r  r  r  r  r   r  r  r  r   )rS   rj  r  r  r  r  r
  r   r  r  valid_tsr  r  r  r  r  r  r  r   r  r  r  r  rc  s                           rV   save_toolset_envr  @<  s     
 @+O+QR<61aRHR86Gv4NOO	/	0 >!!$'E*3DI **b1 *AKK%)** #hh;!7*:1;;8b6RY?A[@\] 
 ((..* 	$JCJ"36 S!s#	$ 6==!T-*++==9>: uTZ[[C S < " J'CCIIJ >9> >sq   G/F,0G/A1G#	F2F2A%G#F7)G#GG#G/2G#7	G GGG##G,(G/c                   ,    e Zd ZU eed<   dZee   ed<   y)ToolsetPostSetupr  Nr  r  r  rX   rV   r  r  w<  r  rX   r  z%/api/tools/toolsets/{name}/post-setupc                   K   ddl m}m}  |       D ch c]  \  }}}|
 }}}| |vrt        dd|        |j                   |       vrt        dd|j                         	 t        t        |j                  xs |      dd|j                  gz   d	      }d|j                  d	|j                  dS c c}}w # t        $ r  t        $ r*}	t        j                  d
       t        dd|	       d}	~	ww xY ww)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_keysr   r  r   zUnknown post-setup key: r  z
post-setupr  z Failed to spawn tools post-setuprY  zFailed to run post-setup: NT)r  r  rS   r  )r  r  r  r7   r  rD  r  r  r\   rQ   rK  r  )
rS   rj  r  r  r  r  r  r  r%  r  s
             rV   run_toolset_post_setupr  |<  s    (
 ,P+QR<61aRHR86Gv4NOOxx,..&>txxj$I
 	

#dll5g6dhh/0
 txx1CDHHUU- S   
9:&@$F
 	

s4   C:B5AC:'3B; !C:;C7%C22C77C:r*  Localz4Run commands directly on this machine. No isolation.)rS   rg  r"  r+  DockerzIRun commands in an isolated Docker container with a persistent workspace.r/  zSingularity / ApptainerzKRun commands in a Singularity/Apptainer container (HPC-friendly, rootless).r-  Modalz&Run commands in a Modal cloud sandbox.r.  Daytonaz(Run commands in a Daytona cloud sandbox.r,  SSHz'Run commands on a remote host over SSH._TERMINAL_BACKENDSterminal_cfgr  c                     | j                  |      }|2t        |      j                         rt        |      j                         S 	 ddlm}  ||      xs dj                         S # t
        $ r Y yw xY w)zHRead a terminal.* setting from config.yaml, falling back to its env var.r   r
  r   )r   r  r   r<  r
  r\   )r  r  r  r  r
  s        rV   _terminal_cfg_valuer  <  sm    S!ESZ--/5z!!3g&,"3355 s   A' '	A32A3c                      t        j                  d      sy	 t        j                  g dddd      } | j                  dk(  ryy	# t        j
                  $ r Y y
t        $ r}dd| fcY d }~S d }~ww xY w)Nr+  )needs_setupu=   Docker CLI not found — install Docker Desktop or docker-ce.)r+  rR   z--formatz{{.ServerVersion}}Tr$  r  r   r  r   )r  u7   Docker daemon not reachable — start Docker and retry.)r  z)Docker daemon not responding (timed out).r  zDocker probe failed: )r  r  r.  r/  r0  r
  r\   rE  s     rV   _probe_docker_backendr  <  s    <<!
>~~@	
 ??a 
 $$ LK >!6se<==>s#   *A A2A2!A-'A2-A2c                  Z    t        j                  d      st        j                  d      ryy)Nr/  	apptainerr  )r  z0Neither singularity nor apptainer found on PATH.)r  r  r  rX   rV   _probe_singularity_backendr  =  s!    ||M"fll;&?rX   c                     t        | dd      }t        | dd      }g }|s|j                  d       |s|j                  d       |rddd	j                  |       d
fS d| d| fS )Nssh_hostTERMINAL_SSH_HOSTssh_userTERMINAL_SSH_USERzterminal.ssh_hostzterminal.ssh_userr  zSet z and z: in config.yaml (or the matching TERMINAL_SSH_* env vars).r  r	  )r  r  r  )r  r   r   r  s       rV   _probe_ssh_backendr  =  s    |Z9LMD|Z9LMDG*+*+7<<())cd
 	
 vQtf%&&rX   c                      	 ddl m}   |        ry	 	 ddlm}  |d      r	 |d      ryy# t        $ r Y $w xY w# t        $ r Y yw xY w)Nr   )has_direct_modal_credentialsr  r
  MODAL_TOKEN_IDMODAL_TOKEN_SECRET)r  ua   Modal credentials not found — set MODAL_TOKEN_ID and MODAL_TOKEN_SECRET (or run `modal setup`).)tools.tool_backend_helpersr   r\   r<  r
  )r   r
  s     rV   _probe_modal_backendr$  =  se    K')  *3)*}=Q/R     s   * 9 	66	AAc                  D    	 ddl m}   | d      ry	 y# t        $ r Y yw xY w)Nr   r
  DAYTONA_API_KEYr  )r  z/Set DAYTONA_API_KEY to use the Daytona backend.)r<  r
  r\   r
  s    rV   _probe_daytona_backendr'  2=  s8    3*+  , N  Ms    	c                     	 | dk(  ry| dk(  r
t               S | dk(  r
t               S | dk(  rt        |      S | dk(  r
t               S | dk(  r
t	               S dd	|  fS # t
        $ r}dd
| fcY d}~S d}~ww xY w)z:Return ``(status, detail)`` for one backend. Never raises.r*  r  r+  r/  r,  r-  r.  r  zUnknown backend: zProbe failed: N)r  r  r  r$  r'  r\   )rS   r  r  s      rV   _probe_terminal_backendr)  ==  s    77? 8(**= -//5=%l337?'))9)++!24&9:: 7u5667s=   A A A A A A A 	A5$A0*A50A5z/api/tools/terminal/backendsc                   K   t        |       5  t               }|j                  d      }t        |t              si }t        |j                  d      xs d      j                         j                         }|t        vrd}g }t        D ];  }t        |d   |      \  }}|j                  |d   |d   |d   |d   |k(  ||d       = 	 ddd       d	S # 1 sw Y   xY ww)
u  Terminal execution backend rows with health probes for the picker panel.

    Returns ``{active, backends: [{name, label, description, active, status,
    detail}]}`` where ``status`` is ``ready`` / ``needs_setup`` /
    ``unavailable`` and ``detail`` carries setup guidance for non-ready rows.
    Probes are fast (<~2s each) and defensive — a probe failure surfaces as a
    status, never an error response.
    rx  r  r*  rS   rg  r"  )rS   rg  r"  r  rc  r   N)r  backends)r  r   r   r  r  r  r   r   _TERMINAL_BACKEND_NAMESr  r)  r  )r  r  r  r  r+  r  rc  r   s           rV   get_terminal_backendsr-  Q=  s      
	  zz*-,-L\%%i0;G<BBDJJL00F% 		C4S[,ONFFOOFW"=1f+/   		( (33) s   C B8CC CC c                   ,    e Zd ZU eed<   dZee   ed<   y)TerminalBackendSelectr  Nr  r  r  rX   rV   r/  r/  r=  s    L!GXc]!rX   r/  z/api/tools/terminal/backendc                   K   | j                   xs dj                         j                         }|t        vr9t	        dd| j                   ddj                  t        t                           t        | j                  xs |      5  t               }|j                  di       }t        |t              si }||d<   ||d<   t        |       d	d	d	       d
|dS # 1 sw Y   xY ww)u  Persist ``terminal.backend`` in config.yaml.

    Validates against the known backend set (the same enum the raw-config
    settings row exposes). Selecting a backend that still needs setup is
    allowed — the picker shows guidance instead of blocking, matching the CLI.
    r   r   zUnknown terminal backend: z. Use one of: r  r   rx  r  NT)r  r  )r  r   r   r,  r7   r  r  r  r  r   r  r  r  r   )rj  r  r  r  r  s        rV   select_terminal_backendr1  w=  s      ||!r((*002G--//? @99V,C%DEFH
 	
 
/	0 ((R8,-L!-F:")YF 7++ s   BC&	ACC&C#C&z/api/tools/computer-use/statusc                 f   K   ddl m} t        |       5   |       cddd       S # 1 sw Y   yxY ww)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.permissionsr3  r  )r  r3  s     rV   get_computer_use_statusr5  =  s.      C		  %"$% % %s   1%
1.1z)/api/tools/computer-use/permissions/grantc                   K   t         j                  dk7  rt        dd      	 t        t	        |       g dz   d      }d|j                  ddS # t        $ r  t
        $ r*}t        j                  d       t        dd	|       d
}~ww xY ww)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.
    r
  r   z3Computer Use permission grants are a macOS concept.r   )zcomputer-usepermissionsgrantr  z.Failed to spawn computer-use permissions grantrY  zFailed to request permissions: NTrB  )	r,  r-  r7   rD  r  r\   rQ   rK  r  r  s      rV   grant_computer_use_permissionsr9  =  s      ||xH
 	

#g&67 
 txx1EFF   
GH&EcU$K
 	

s'   !BA BB
 %BB

Bc                   ,    e Zd ZU eed<   dZee   ed<   y)RawConfigUpdate	yaml_textNr  r  r  rX   rV   r;  r;  =  s    N!GXc]!rX   r;  z/api/config/rawc                    K   t        |       5  t               }ddd       j                         sdt        |      dS |j	                  d      t        |      dS # 1 sw Y   ExY ww)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   r  r  )r  r   r{  r  r  )r  r   s     rV   get_config_rawr>  =  s^      
	  ! !;;=CI..NNGN4c$iHH	! !s   A)AAA)A&"A)c                 X  K   	 t        j                  | j                        }t        |t              st        dd      t        | j                  xs |      5  t        |d       d d d        ddiS # 1 sw Y   xY w# t         j                  $ r}t        dd|       d }~ww xY ww)	Nr   zYAML must be a mappingr   F)merge_existingr  TzInvalid YAML: )
r  r  r<  r  r  r7   r  r  r   	YAMLError)rj  r  r  r  s       rV   update_config_rawrB  =  s     
J/&$'C8PQQDLL3G4 	6 u5	6 d|		6 	6
 >> JnQC4HIIJsA   B*AA? A3'A? 2B*3A<8A? ?B'B""B''B*r&	  c                     	 | j                   j                  d|f      }|j                         D cg c]  }t        |       c}S c c}w # t        $ r g cY S w xY w)a  Per-(model, task) auxiliary usage within the window (issue #23270).

    Reads the task-dimension rows (task != '') that record_auxiliary_usage
    writes into session_model_usage. Returns [] when the table predates the
    task column (older DB opened read-only by newer code).
    ai  
            SELECT u.model,
                   u.task,
                   u.billing_provider,
                   SUM(u.input_tokens) as input_tokens,
                   SUM(u.output_tokens) as output_tokens,
                   SUM(u.cache_read_tokens) as cache_read_tokens,
                   SUM(u.reasoning_tokens) as reasoning_tokens,
                   COALESCE(SUM(u.estimated_cost_usd), 0) as estimated_cost,
                   COUNT(DISTINCT u.session_id) as sessions,
                   SUM(COALESCE(u.api_call_count, 0)) as api_calls,
                   MAX(u.last_seen) as last_used_at
            FROM session_model_usage u
            JOIN sessions s ON s.id = u.session_id
            WHERE s.started_at > ? AND u.task != ''
            GROUP BY u.model, u.task, u.billing_provider
            ORDER BY SUM(u.input_tokens) + SUM(u.output_tokens) DESC
        )r  r  r  r  r\   )r  r&	  r  r  s       rV   _aux_usage_rowsrD  =  s]    hh  " Y#$ "%0AQ000  	s"   /A AA A AAby_modelaux_rowsc           
         |s| S i }| D ]  }|||j                  d      xs d<    |D ]y  }|j                  d      xs d}|j                  |      }||dddddd}|||<   |j                  d      xs d|j                  d      xs dz   |d<   |j                  d      xs d|j                  d      xs dz   |d<   |j                  d      xs d|j                  d      xs dz   |d<   |j                  d      xs d|j                  d      xs dz   |d<   |j                  d	g       }|j                  |j                  d
      xs d|j                  d      xs d|j                  d      xs d|j                  d      xs d|j                  d      xs dd       | t        |j	                               }|j                  d d       |S )uO  Fold aux usage rows into the sessions-derived per-model list.

    Aux usage lives only in session_model_usage (never in the sessions
    counters), so adding it here cannot double-count. Models that ONLY
    appear via aux calls (e.g. a dedicated vision model) get their own
    entry — previously they were entirely invisible.
    r#  r  r   )r#  input_tokensoutput_tokensestimated_costr  	api_callsrH  rI  rJ  rK  	aux_tasksr  r   )r  rH  rI  rJ  rK  c                 X    | j                  d      xs d| j                  d      xs dz   S NrH  r   rI  r   r  s    rV   r  z*_merge_aux_into_by_model.<locals>.<lambda>F>  (    quu^,1aeeO6L6QPQR rX   Tr  r   r  r  r  r  r  )	rE  rF  r  r  r  r#  rj   ra  r3  s	            rV   _merge_aux_into_by_modelrR  >  s    (*F 403swww,9-4  -IE"> !!""#F #F5M"(**^"<"AcggnF]Fbab!c~#)::o#>#C!P_H`Hede"f$*JJ/?$@$EA#''RbJcJhgh#i %zz+6;!@T@YXYZ{!!+r2GGFO)rGGN38q WW_5:!gg&67<1-2
 	%2 &--/"F
KKR   MrX   c                 0   i }| D ]  }|j                  d      xs d}|j                  ||ddddg d      }|dxx   |j                  d      xs dz  cc<   |dxx   |j                  d      xs dz  cc<   |dxx   |j                  d      xs dz  cc<   |dxx   |j                  d      xs dz  cc<   |j                  d	      xs d
}||d   vs|d   j                  |        t        |j	                               }|j                  d d       |S )z?Aggregate aux usage rows across models into a per-task summary.r  r   r   )r  rH  rI  rJ  rK  r\  rH  rI  rJ  rK  r#  r  r\  c                 X    | j                  d      xs d| j                  d      xs dz   S rN  r   rO  s    rV   r  z#_aux_task_summary.<locals>.<lambda>b>  rP  rX   Tr  rQ  )rF  by_taskr  r  r  r#  r3  s          rV   _aux_task_summaryrV  L>  s-   )+G &wwv$"t&
  	
.SWW^499	/cggo6;!;	
sww'78=A=	+#''+.3!3 -I(#hKu%!&" '.."#F
KKR   MrX   daysc           
      
   ddl m} t        |      }	 t        j                         | dz  z
  }|j                  j                  d|f      }|j                         D cg c]  }t        |       }}|j                  j                  d|f      }|j                         D cg c]  }t        |       }	}t        ||      }
t        |	|
      }	|j                  j                  d|f      }t        |j                               } ||      j                  |       }|j                  dddddd	g d
      }||	t        |
      || ||j                  dg       d|j                          S c c}w c c}w # |j                          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
        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
        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 > ?
        )rW  rf  )total_skill_loadstotal_skill_editstotal_skill_actionsdistinct_skills_used)r  
top_skillsr  )dailyrE  rU  totalsperiod_daysrf  r  )agent.insightsrY  r~  r}  r  r  r  r  rD  rR  fetchonegenerater   rV  r   )rW  r  rY  r  r&	  r  r  r`  cur2rE  rF  cur3ra  insights_reportrf  s                  rV   _get_usage_analyticsri  h>  s   -	%g	.BJu-hh   Y #&,,.1Qa11xx 	! Y	 &*]]_5DG55 #2v.+Hh?xx 
! Y
 dmmo&(,5545@ $$X%&%&'(()	 0
    )2 %(("5
 	
w 2 6^ 	
s+   A	E0 E&.1E0 E+1B$E0 &
E0 0Fz/api/analytics/usagec                 T   K   t        j                  t        | |       d {   S 7 wr  )ro   r  ri  rW  r  s     rV   get_usage_analyticsrl  >  s"     ""#7wGGGG   (&(c                    t        |      }	 t        j                         | dz  z
  }|j                  j                  d|f      }|j	                         D cg c]  }t        |       }}t        ||      D ]  }|j                  |j                  d      xs d|j                  d      xs d|j                  d      xs d|j                  d	      xs d|j                  d
      xs d|j                  d      xs d|j                  d      xs dd|j                  d      xs d|j                  d      xs dd|j                  d      d|j                  d      xs dd        i }|D ]6  |j                  j                  d      xs dg       j                         8 g }	|j                         D ]Y  }
|
D cg c]  }|j                  d      s| }}t        |      dk(  r|d   |
D ]  u sj                  d      rt        fddD              }|r0j                  d      xs dj                  d      xs dz   d<   t        j                  d      xs dj                  d      xs d      d<   j                  d      xs dj                  d	      xs dz   }j                  d      xs d}|r||z  ndd<    |	j                         |	j                  fd|
D               I|	j                  |
       \ |	j                  d d       g }|	D ]  j                  d      xs d}d   }i }	 ddlm}  |||      }|E|j$                  |j&                  |j(                  |j*                  |j,                  |j.                  d}|j                  ||d   d	   d
   d   d   d   d   d   d   d   d   |d        |j                  j                  d |f      }t        |j3                               }||| d!|j5                          S c c}w c c}w # t0        $ r Y w xY w# |j5                          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.).
    rZ  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
        r#  r  billing_providerr   rH  r   rI  cache_read_tokensreasoning_tokensrJ  r  rK  last_used_atr  )r#  ro  rH  rI  rp  rq  rJ  actual_costr  rK  
tool_callsrr  avg_tokens_per_sessionaux_taskr   c              3   N   K   | ]  }j                  |      xs d d k7    ywr   Nr   )r   r  r  s     rV   r   z(_get_models_analytics.<locals>.<genexpr>?  s+      $ *q0$   "%rH  rI  rp  rq  rJ  rs  rK  rt  ru  c              3   v   K   | ]/  ur)j                  d       st        fddD              r 1 yw)ro  c              3   N   K   | ]  }j                  |      xs d d k7    ywrx  r   )r   r  r  s     rV   r   z2_get_models_analytics.<locals>.<genexpr>.<genexpr>?  s*      : sqQ.:ry  rz  N)r   r  )r   r  rj   s    @rV   r   z(_get_models_analytics.<locals>.<genexpr>?  sC      12c :	$: 7 s   59c                 X    | j                  d      xs d| j                  d      xs dz   S rN  r   rO  s    rV   r  z'_get_models_analytics.<locals>.<lambda>.?  s(    15505A!%%:P:UTUV rX   Tr  r#  r%  r&  rs  rt  )r#  rU   rH  rI  rp  rq  rJ  rs  r  rK  rt  rr  ru  r  a  
            SELECT COUNT(DISTINCT model) as distinct_models,
                   SUM(input_tokens) as total_input,
                   SUM(output_tokens) as total_output,
                   SUM(cache_read_tokens) as total_cache_read,
                   SUM(reasoning_tokens) as total_reasoning,
                   COALESCE(SUM(estimated_cost_usd), 0) as total_estimated_cost,
                   COALESCE(SUM(actual_cost_usd), 0) as total_actual_cost,
                   COUNT(*) as total_sessions,
                   SUM(COALESCE(api_call_count, 0)) as total_api_calls
            FROM sessions WHERE started_at > ? AND model IS NOT NULL AND model != ''
        )r\  ra  rb  )r~  r}  r  r  r  r  rD  r  r   r  r  r   r  r\  r  r  r.  r$  r'  r(  r)  r*  r+  r,  r\   rd  r   )rW  r  r  r&	  r  r  r  r  rows_by_modelr"  
model_rowsprovider_rows	has_usagetotal_tokensr  r\  rU   r/  r4  r$  r5  
totals_curra  r  rj   s                          @@rV   _get_models_analyticsr  >  s    
&g	.Bfu-hh  " Y#$ &)\\^4DG44 #2v. 	COO)6Y$'GG,>$?$E2 # 7 <1!$!9!>Q%(WW-@%A%FQ$'GG,>$?$D1"%''*:";"@q GGJ/41 WW[16Q # 7*+GGFO1r 	0 :< 	MC$$SWWW%5%;R@GGL	M &('..0 .	(J(2P1aee<N6OQPMP=!Q&&q)% bCf}0B(C  # $	$$ !I ! *0**Z*@*EA#''R\J]Jbab)cF:&-0N1K1PqRURYRYZhRiRnmn-oF>*$*JJ~$>$C!

SbHcHhgh#iL%zz*5:HRZ|h7N`aF34-b. F# ) $ J'].	(` 			V 	 	

  "	Cww128bHWJDC+XZP>*,*;*;+-+=+=.0.C.C*,*;*;-/-A-A(*D MM#$ #N 3!$_!5%()<%=$'(:$;"%&6"7"=1
O -!,/ #N 3*-.F*G $ '"	H XX%% ' Y
 j))+, 
 	
c 5H QJ  P 	
sX   A	Q P4*EQ P9P9E1Q AP>%A>Q 4
Q >	Q
Q 	Q

Q Qz/api/analytics/modelsc                 T   K   t        j                  t        | |       d{   S 7 w)z?Return model analytics without blocking the serving event loop.N)ro   r  r  rk  s     rV   get_models_analyticsr  n?  s$      ""#8$HHHHrm  r  )WinPtyBridgePtyUnavailableErrorc                       e Zd ZdZy)r  z,Stub when win_pty_bridge cannot be imported.Nr  r  r  r  r  rX   rV   r  r  ?  s    >rX   r  )	PtyBridger  c                       e Zd ZdZy)r  z5Stub on platforms where pty_bridge can't be imported.Nr  r  rX   rV   r  r  ?  s    GrX   s   \x1b\[RESIZE:(\d+);(\d+)\]g?)PtySessionRegistryRegistryFullr|   i  r  )ttlmax_sessions
buffer_capread_timeoutwsr:   c                 d   K   t        j                         d fd}t        j                   |             }	 	 	  j                          d{   }|j                  d      dk(  rn|j                  d      }|4|j                  d      }t        |t              r|j                  d      nd}|syt        j                  |      }|r`|j                         t        |      k(  rDj                  t        |j                  d	            t        |j                  d
                   j!                  |       |j%                          	 | d{    t        j*                  j,                         d{    y7 1# t        $ r Y Qw xY w# t"        $ r Y `w xY w7 M# t         j&                  t(        f$ r Y fw xY w7 G# |j%                          	 | d{  7   n# t         j&                  t(        f$ r Y nw xY wt        j*                  j,                         d{  7   w xY ww)u  Original 1:1 socket<->PTY pump: stream until disconnect, then close the
    bridge. Used when no ``?attach=`` token is supplied (keep-alive opt-in).

    Behavior is identical to the pre-keep-alive ``pty_ws`` body, including the
    #54028 half-open-socket protection (reader EOF → close the WS so the
    writer's ``ws.receive()`` unparks) and the #53227 ``to_thread`` offloads
    for the blocking ``bridge.close()``.
    Nc                  b  K   	 	 j                  d j                  t               d {   } | C	 	 t        j                  j
                         d {    	 j                          d {    y | st        j                  d       d {    	 j                  |        d {    7 7 [# t        $ r Y dw xY w7 S# t        $ r Y y w xY w7 D7 +# t        $ re Y 	 t        j                  j
                         d {  7   n# t        $ r Y nw xY w	 j                          d {  7   y # t        $ r Y y w xY ww xY w# 	 t        j                  j
                         d {  7   n# t        $ r Y nw xY w	 j                          d {  7   w # t        $ r Y w w xY wxY wwrZ   )	ru   rf  _PTY_READ_CHUNK_TIMEOUTro   r  r   r\   rY  
send_bytes)r  bridger  r  s    rV   pump_pty_to_wsz$_legacy_pump.<locals>.pump_pty_to_ws?  s    #	"22&++'>  =,''555hhj  5 !--***--... 6 6  ! 5 + /  ''555 hhj   - ''555 hhj   s  F/%E B/E "B3 B1B3 C .C/C 3F/4E CE C )C*C .E 1B3 3	B?<F/>B??F/C 	CF/CF/E C 	E E ""D DD 
F/	DF/DF/D5 .D1/D5 4F/5	E>F/ EF/EE F,
"E3,E/-E32F,3	E?<F,>E??F,FFFF,	F)&F,(F))F,,F/r!  websocket.disconnectr  r  r  rX   r   r$  colsr"  rK   N)ro   r  r{   receiver  r   r  r  r   
_RESIZE_REr  endr   resizer  r  r  r;   r~   CancelledErrorr\   r  r   )	r  r  r  reader_taskr[  r  r  r  r  s	   ``      @rV   _legacy_pumpr  ?  s     ##%D$L %%n&67K.JJL(
 wwv"88'''"C{wwv.8s.Cdkk'*$$S)EC03u{{1~#6SQ=PQLL) 0 		 ---7 )  $  
 &&	2 		- 		&&	2 		---s   7H0F F FF C)F  H0F& F$F& "H0=G>H0F 	FF FF 	F!G  F!!G $F& &G?H0GH0H-G%G!G%$H-%H>H- H%H-&H)'H--H0z^[A-Za-z0-9._-]{1,128}$>   r   rb  r   r   c                 N   t        t        j                  dd      ryt        t        j                  dd      xs dj                         j	                         }|r	|t
        vry| j                  r| j                  j                  nd}|s	d|xs d S |t
        v ryd| d	|xs 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   re   rm   r   r   _LOOPBACK_HOSTSre  r   r  r   rf  s      rV   _ws_client_reasonr  @  s     syy/51#))\26<"CCEKKMJj7$&II"))..2K .j.?C-@AAo%$[M9Js8KLLrX   c                 $   t        t        j                  dd      ryt        t        j                  dd      xs dj                         j	                         }|r	|t
        vry| j                  r| j                  j                  nd}|sy|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      rV   _ws_client_is_allowedr  #@  sw    0 syy/51 #))\26<"CCEKKMJj7$&II"))..2K /))rX   c                    t        t        j                  dd      }|sy| j                  j	                  dd      }t        ||      sd|xs d d| S | j                  j	                  dd      }|syt        j                  j                  |      }|j                  d	vry|j                  sd
| d| S t        |j                  |      sd
| d| S y)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  r  >   r   httpszorigin_mismatch origin=)r   re   rm   r   r   r   r`  r  r  r  r  )r  r   r   r  r  s        rV   _ws_host_origin_reasonr  N@  s     L$7J**..,K[*5$[%7C$8
|LLZZ^^Hb)F\\""6*F}}-- ==(
|DDV]]J7(
|DDrX   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    rV   _ws_host_origin_is_allowedr  p@  s     ""%--rX   c                 2    t        |       xs t        |       S )zDFirst Host/Origin or peer-IP rejection reason, or None when allowed.)r  r  r  s    rV   _ws_request_reasonr  |@  s    !"%>):2)>>rX   c                 2    t        |       xr t        |       S )zDReturn True when the WebSocket upgrade matches dashboard boundaries.)r  r  r  s    rV   _ws_request_is_allowedr  @  s    %b)G.CB.GGrX   c                      t        t        j                  dd      ryt        t        j                  dd      xs dj                         j	                         } | r	| t
        vryy)uG   Short label for the active WS auth mode — logged on every connection.r   Fgatedr   r   insecureloopback)r   re   rm   r   r   r  )r   s    rV   _ws_auth_moder  @  sN    syy/51#))\26<"CCEKKMJj7rX   c                 R   t        t        t        j                  dd            }|rcddlm}m} ddlm}m	}m
} | j                  j                  dd      }|r
	  ||       y| j                  j                  dd      }	|	sy	  ||	       y| j                  j                  dd      }
|
syt'        j(                  |
j+                         t,        j+                               ryy# |$ rY} ||j                  d	| | j                  r| j                  j                  nd| j                   j"                  
       Y d}~yd}~ww xY w# |$ r_} ||j                  t%        |      | j                  r| j                  j                  nd| j                   j"                  
       Y d}~yd}~ww xY w)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: )ro  ipr   N)internal_invalidr  ticket)no_credentialr  )Nr  )ticket_invalidr  r   )Nr   )token_mismatchr   )r   r   re   rm   hermes_cli.dashboard_auth.auditr  r  $hermes_cli.dashboard_auth.ws_ticketsr  r  r  r   r   WS_TICKET_REJECTEDre  r   r   r   r  r   r   r   r   )r  r   r  r  r  r  r  r  r  r  r   s              rV   _ws_auth_reasonr  @  sg   @ OUCDM 	J	
 	
 ??&&z26
6+H5' $$Xr2*
	.6"! OO,E&5<<>>+@+@+BC$= ! 611'u-*,))			 66   	.--3x&(iiBIINNRVV[[	 .	.s2   C! E !D?&AD::D?F&AF!!F&c                 "    t        |       d   du S )zETrue when the WS-upgrade credential is accepted. See _ws_auth_reason.r   N)r  r  s    rV   _ws_auth_okr  @  s    2q!T))rX   resumesidecar_urlactive_session_filec                 .   ddl m}m}m} d}|xs dj	                         }|r|j                         dk7  rt        |      } ||dz  d      \  }	}
t        j                  j                         }	 dd	l
m}  ||
        ||       |j                  dd       |j                  dd       |j                  dd       |j                  dd       d|d<   |t!        |      |d<   | r8t#        ||nd      }	 t%        | |      \  }}|j'                          |r|} | |d<   |r||d<   |r||d<   |t)               x}r||d<   t+        |	      |
rt!        |
      |fS d|fS # t        $ r t        j                  dd       Y w xY w# |j'                          w xY w)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   )rq  _apply_tui_python_env_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_MOUSErh   HERMES_TUI_INLINE	COLORTERM	truecolorHERMES_TUI_DASHBOARDrj  HERMES_TUI_RESUMEHERMES_TUI_SIDECAR_URLHERMES_TUI_ACTIVE_SESSION_FILEHERMES_TUI_GATEWAY_URL)r  rq  r  r  r   r   r}
  rv   r   r  r<  r  r\   rQ   r@  r  r  r~  r  r   _build_gateway_ws_urlr  )r  r  r  r  rq  r  r  r  r
  argvr  r  r  
_resume_dblatest_resume_latest_pathgateway_ws_urls                    rV   _resolve_chat_argvr  @  s   R TS"&KB%%'IY__&)3*95|h6FID#
**//
C_B$- #NN:|, NN-s3NN&, NN;,"%C -M1$0Id

	*DVZ*X'M<"F#) (3$%0C,- 244>4,:C():3s3x#55D#55m  _

NY]
^_F s   0E 6F  E?>E?Fr   r   c                      t         j                  j                  dd      j                         } | r| S t	        t
        j                  dd      }|sy|t        v ry|S )uo  Return the host the in-container WS client should dial.

    Resolution order:

    1. Explicit ``HERMES_DASHBOARD_WS_HOST`` env var — wins always. Operators
       running the dashboard behind a forward proxy can pin a routable host
       (e.g. ``127.0.0.1``, the container's internal IP, or a sidecar DNS
       name) and bypass auto-detection entirely.
    2. The configured bind host — if it's a wildcard (``0.0.0.0`` / ``::``),
       substitute ``127.0.0.1`` since both the dashboard and its TUI child
       run in the same container.
    3. Any other bind host (loopback or LAN IP) — preserved verbatim.
    HERMES_DASHBOARD_WS_HOSTr   r   Nr   )rv   r   r   r   r   re   rm   _WILDCARD_HOSTS)explicitr   s     rV   _resolve_client_ws_hostr  hA  sR     zz~~8"=CCEH399lD1DKrX   c                     t               } t        t        j                  dd      }| r|syd| v r| j	                  d      sd|  d| n|  d| }t        t        j                  dd      r-dd	lm} t        j                  j                  d
 |       i      }n%t        j                  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.
    
bound_portNr   r   ]:r   Fr   internal_ws_credentialr  r   ws://z/api/ws?r  r   re   rm   r   r  r  r`  r  r&  r   )r   r  r  r  qss        rV   r  r  A  s     #$D399lD1Dt $;ts3 D6D6vQtf  syy/51O\\##Z1G1I$JK\\##Wn$=>6((2$''rX   c                    K   | ||d}|||d<   t        t              4 d{    t        j                  t        fi | d{   cddd      d{    S 7 77 7 	# 1 d{  7  sw Y   yxY ww)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   re   ro   r  r  )r  r  r  r  r}  s        rV   _resolve_chat_argv_asyncr  A  s     " "F
 &(;$%"3' 
 
&&

 

 
 


 
 
 
sV   !A6AA6A!AA!	A6AA6A!A6!A3'A*(A3/A6channelc                    t               }t        t        j                  dd      }|r|syd|v r|j	                  d      sd| d| n| d| }t        t        j                  dd      r.dd	lm} t        j                  j                   |       | d
      }n&t        j                  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   Fr   r  )r  r  )r   r  r  z	/api/pub?r  )r  r   r  r  r  r  s         rV   _build_sidecar_urlr  A  s     #$D399lD1Dt#&$;ts7KqbTXSYYZ[_Z`QaFsyy/51 	P\\##/1gF
 \\##n$QR6()B4((rX   c                 Z  K   t        |       \  }}|4 d{    t        |j                  |d            }ddd      d{    D ]  }	 |j                  |       d{     y7 Q7 (# 1 d{  7  sw Y   8xY w7  # t        $ r t
        j                  d|d       Y ^w xY ww)z=Fan out one publisher frame to every subscriber on `channel`.Nr  z*broadcast send failed for subscriber on %sTr:  )r   r  r   	send_textr\   rQ   rS  )re   r  r  rn   rq   r  r  s          rV   _broadcast_eventr  A  s     !1#!6NJ 5 5N&&w345 5  _	_--(((_5 5 5 5 5
 ) 	_ LLEwY]L^	_sy   B+A)B+A-B+A+	B+B!B"B&B++B+-A?3A64A?;B+B!B(%B+'B((B+c                 l    | j                   j                  dd      }t        j                  |      r|S dS )z?Return the channel id from the query string or None if invalid.r  r   N)r   r   _VALID_CHANNEL_REr  r  r  s     rV   _channel_or_close_coder  A  s0    oo!!)R0G'--g67@D@rX   c                     t        |       }|j                  |      }||S t        j                  dd      \  }}t	        j
                  |       t        |      }|||<   |S )zHReturn the per-channel file where a dashboard TUI writes its active sid.zhermes-pty-active-r  )r  r  )r   r   r  r  rv   r   r   )re   r  r  r  fdr  r   s          rV    _active_session_file_for_channelr  A  s^    )#.Eyy!H##+?PLBHHRL>DE'NKrX   c                     	 t        j                  | j                  d            }t        |j                  d      xs d      j                         }|xs d S # t        t         j                  f$ r Y y w xY w)Nr  r  r  r   )rd  re  r  r  r
  r  r   r   )r   r  r  s      rV   _read_active_session_filer  B  sm    zz$..'.:; TXXl+1r288:J	 T))* s   %A A54A5c                 H    	 | j                  d       y # t        $ r Y y w xY w)NTr  )r  r  r  s    rV   _forget_active_session_filer	  B  s'    t$ s    	!!c                 v    | j                  dd      }t        |      dk  r| S |dd j                  dd      dz   S )a  Clamp a WS close reason to the protocol's 123-byte UTF-8 limit.

    RFC 6455 caps the close-frame reason at 123 bytes; uvicorn raises if a
    longer string is passed. Our reasons embed an attacker-controlled origin,
    so truncate defensively rather than crash the close handler.
    r  r  {   Nx   ignorez...)r   r   rN  )r  rO  s     rV   _ws_close_reasonr  B  sC     kk'9-G
7|s4C=2U::rX   zhermes> g      N@iP  r   _console_executorc                      t         Vt        5  t         ;t        j                  j	                  t
        d      a t        j                  d        ddd       t         S t         S # 1 sw Y   t         S xY w)zALazily create the bounded console worker pool (once per process).Nzhermes-console)max_workersthread_name_prefixc                  @    t         xr t         j                  dd      S )NFT)r^  cancel_futures)r  shutdownr  rX   rV   r  z'_get_console_executor.<locals>.<lambda>OB  s#    - T)22d2S rX   )r  _console_executor_lock
concurrentfuturesThreadPoolExecutor_CONSOLE_EXECUTOR_MAX_WORKERSatexitregisterr  rX   rV   _get_console_executorr  AB  sp      # 	 ($.$6$6$I$I ='7 %J %! T	 	 s   AA##A1c                 h    | j                   j                  d      xs dj                         }|xs d S )Nr  r   )r   r   r   )r  r  s     rV   _console_profile_from_wsr  UB  s.    ""9-3::<G?drX   enginer  	confirmedc                j    t        |      5  | j                  ||      cd d d        S # 1 sw Y   y xY w)Nr!  )r  r  )r   r  r!  r  s       rV   _execute_console_liner$  ZB  s1     
	  9~~di~89 9 9s   )2	send_lockc                    K   |4 d {    | j                  |       d {    d d d       d {    y 7 .7 7 	# 1 d {  7  sw Y   y xY wwr  )	send_json)r  r%  r  s      rV   _console_sendr(  gB  sP     
  $ $ll7###$ $ $#$ $ $ $sH   A8A>:>A<A>AAAAAr3  
command_idc          
      X  K   |j                   xs d}|j                  }|dk(  rQ|j                  r%t        | |d|d|j                  |d       d {    t        | |d|d|t        d       d {    y |dk(  rHt        | |d||j                  xs d	|d
       d {    t        | |d|d|t        d       d {    y |dk(  rQt        | |d|||j
                  xs d| dt        d       d {    t        | |d|d|t        d       d {    y |dk(  r8t        | |d|d       d {    t        | |d|d|t        d       d {    y |dk(  rt        | |d|d|dd       d {    y t        | |d|d| |d
       d {    y 7 D7 (7 7 7 7 7 q7 T7 57 w)Nr   r  r  r1  )r!  r  r
  r  r  completer!  r  rc  r  rF   r  zCommand failed.r!  r  r4  r  rz  zRun `z`?)r!  r  r  r4  rF   clear)r!  r  exitzUnknown console result status: )r  rc  r  r(  _CONSOLE_PROMPTconfirmation_message)r  r%  r3  r)  r  rc  s         rV   _console_send_resultr2  pB  sp     nn"G]]F~==$$&"MM&
 
 
 " ")

 
	
 
	
 	 !===,="		
 		
 		
 " !")

 
	
 
	
 	##* "!66ME'":M)

 
	
 
	
 " ,")

 
	
 
	
 	B	G:+NOOO" !")

 
	
 
	
 	"  "

 
	
 
	
 	

8A		
	 	 	M

	
		

	

	

	
 	P
	

	
	s   AF*FF*/F0-F*FF*<F=6F*3F4F*F F*0F"1F*F$ F*0F&1F*F(F*F*F*F*F* F*"F*$F*&F*(F*r[  c                 :   | j                  d      }|| j                  d      }|yt        |t              r	 |j                  d      }	 t        j                  |      }t        |t              sy|d fS # t        $ r Y yw xY w# t
        j                  $ r Y yw xY w)Nr  r  r  r  )Nz"Console frames must be UTF-8 JSON.)Nz$Console frames must be JSON objects.)	r   r  r  rN  r  rd  re  r
  r  )r[  r  r  s      rV   _console_json_payloadr4  B  s    !ggfoC
{ggg
{#u	>**W%C<**S/ gt$;D= " 	>=	>  <;<s#   A5 B 5	B BBBz/api/consolec           	         K    j                   r j                   j                  nd}t        s2t        j	                  d|        j                  dd       d {    y t               \  }}t               }|At        j                  d||||        j                  dt        d|              d {    y t               }|<t        j                  d	||        j                  d
t        |             d {    y t               }|;t        j                  d|        j                  dt        |             d {    y  j                          d {    t               t        j                         	 ddlm}  |t$              rj'                         dk7  rt)               t        j	                  d|||xs d       t-         dxs dt6        d       d {    d d ddt.        dt8        dt:        dd f fd d!d"dt.        dt8        dd ffd#}		 	 	  j=                          d {   }
|
jA                  d%      }|d&k(  rntC        |
      \  }}|rt-         d|t6        d       d {    `|ct/        |jA                  d%      xs d      jE                         j'                         }|d'k(  rt-         d(t6        d)       d {    |d*k(  rrGjG                         s7d+z  jI                          d d t-         d,d-t6        d.       d {    n?r d t-         d,d-t6        d.       d {    nt-         d,d/t6        d.       d {    Nr/jG                         st-         dd0t6        d       d {    |d1k(  rt/        |jA                  d2      xs xs d      jE                         }st-         dd3t6        d       d {    |k7  rt-         dd4t6        d       d {    d  |	|d$"       d {    |d5v rt/        |jA                  d      xs |jA                  d2      xs d      jE                         }|st-         d,d6t6        d.       d {    srt-         dd7t6        d       d {     |	|       d {    t-         dd8|xs d t6        d       d {    ˉr-jG                         sjI                          	  d {    y y y 7 7 7 97 7 # t*        $ ro}t-         dt/        |j0                        dd       d {  7    j                  dt        t/        |j0                                     d {  7   Y d }~y d }~wt2        $ rW}t        j5                  d       t-         dd| dd       d {  7    j                  d       d {  7   Y d }~y d }~ww xY w7 )7 # t>        $ r Y *w xY w7 7 D7 7 7 7 7 >7 7 
7 7 7 |7 Y# tJ        $ r Y aw xY w7 =# t        jL                  t2        f$ r Y y w xY w# rMjG                         s<jI                          	  d {  7   w # t        jL                  t2        f$ r Y w w xY ww w xY ww)9Nr  z/console refused: embedded chat disabled peer=%s4  embedded chat disabledrR  ro  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_limitr  r  r   )r!  r4  rF   0  zconsole failed to initializezConsole unavailable:   rR  z3console accepted peer=%s mode=%s cred=%s profile=%sr  )r!  r  rF   r  r!  r)  rK   c                  K   	 t        j                         }t        j                  |j                  t	               t        j                  t        | |
            t               d {   }|k7  r
	 |k(  rd y y |j                  dk(  r|j                  nd 	t        ||       d {    |j                  dk(  rj                  d       d {    	 |k(  rd y y 7 z7 97 # t         j                  $ r  t         j                  $ rE |k(  r=d 	t        d|d	| d
       d {  7   t        d|d| t         d       d {  7   Y yt"        $ r}|k(  rsd 	t$        j'                  d       t        d|t)        |      xs |j*                  j,                  | d
       d {  7   t        d|d| t         d       d {  7   Y d }~d }~ww xY w# |k(  rd w w xY ww)N)r!  r  rZ  rz  )r)  r/  r/  rA  r  z9Command timed out. Hermes Console returned to the prompt.r-  r+  r[  r,  zconsole command failed)ro   r  r  ru   r  r  r  r$   _CONSOLE_COMMAND_TIMEOUT_SECONDSrc  r  r2  r   r  r  r(  r0  r\   rQ   rK  r  	__class__r  )r  r!  r)  r  r3  r  active_taskcommand_generationr   pending_confirmationr  r%  r  s         rV   run_commandzconsole_ws.<locals>.run_commandZC  sF    T	#++-D"++$$)+%%-"+ '	 9 FF // //" 0 #)--3E"E4 ! '%	   }}&hhDh)))//" 0cP *E %% 	## 	//'+$# '(W#'   $ *("+#'"1
 
 
  	//'+$78# '(#&s8#Es}}/E/E#'		 	 	 $ *(")#'"1
 
 
	P //" 0s   G0A&C, +C&,C, 0G# 6	G0?/G# .C(/(G# C*G# 	G0&C, (G# *G# ,AG .D1/G EG G# G AG0F31GGGG# G  G# #
G--G0Fr#  c                X   K   dz  }t        j                   | ||            y w)Nr   )r!  r)  )ro   r{   )r  r!  r)  rE  rF  rH  s      rV   start_commandz!console_ws.<locals>.start_commandC  s3     a'
))	jI
s   '*Tr!  r  pingpong)r!  rF   r~   r   r+  r	  )r!  rc  rF   idlez%A console command is already running.confirmr  z'No command is waiting for confirmation.z0Confirmation does not match the pending command.>   inputr  r  zAConfirm or cancel the pending command before running another one.zUnsupported console frame: )'re  r    _DASHBOARD_EMBEDDED_CHAT_ENABLEDrQ   rR   r   r  r  rS  r  r  r  acceptr  ro   rp   hermes_cli.console_enginer=  _CONSOLE_OUTPUT_LIMITr   r}
  r7   r(  r  r   r\   rK  r0  r   r  r  r  r   r4  r   doner~   r;   r  )r  peerauth_reasoncredr  host_origin_reasonclient_reasonr=  r  rJ  r[  msg_typer  r  
frame_typer  r  rE  rF  r   rG  r  rH  r%  s   `                @@@@@@@rV   
console_wsr\  C  s    YY299>>CD+		CTJhhD)AhBBB'+K?DEtT	
 hhD)9F;-:P)QhRRR/3%24FMhhD)9:L)MhNNN%b)M *M:hhD)9-)HhIII
))+&r*GIA$2GHw}})3 )6 	II=9 
+)%	
   (,K*.V# V#4 V#S V#T V# V#p =B 
# 
T 
d 
bJJL( wwvH11237NGU# '#("1   W[[06B7==?EEGJV## &"1   X%{'7'7'9&!+&&&("&K+/('!$.&1&5   *+/('!$.&1&5   (!$.&,&5   ;#3#3#5# '#J"1   Y&gkk)4R8LRPRSYY[+'!$+'P&5   22'!$+'Y&5   '+$#Gt<<<117;;v.N'++i2HNBOUUW'!$.&*&5   ''!$+!7 '6   #D)))#!<Z=N3<OP-  a x {//1 !!!  2;a
 	C 	S 	O 	J   szz?
 	
 	
 hhD)9#cjj/)JhKKK 5623%8
 	
 	
 hhDh!!!*b )   = *   "**I6 	 {//1 !!!**I6 	  2;s  A[#!T8"A[#<T;=A[#T>A[#U[#&U'#[#3U >9[#7X 8A[#<Y >X& X#X& A Y X6AY 5X96AY X<!Y %X?&Y Y0Y 4Y5AY 
Y#Y .Y/Y YA#Y (Y) Y 	Y
Y Y%Y YY "[#+Y0 0Y-1Y0 5[#;[#>[#[#[#	X'V:7U:87V:/V20V:5[#:X,X2W53XXX[#X[##X& &	X3/Y 2X33Y 9Y <Y ?Y Y Y Y Y Y Y Y Y Y 	Y*&Z )Y**Z -Y0 0Z	[#Z[##[ 3Z?8Z;9Z?>[ ?[[ [[  [#z/api/ptyc                   K   | j                   r| j                   j                  nd}t        s2t        j	                  d|       | j                  dd       d {    y t        |       \  }}t               }|At        j                  d||||       | j                  dt        d|              d {    y t        |       }|<t        j                  d	||       | j                  d
t        |             d {    y t        |       }|;t        j                  d|       | j                  dt        |             d {    y | j                          d {    t        j	                  d|||       t        s4| j                  d       d {    | j                  d       d {    y | j                  j!                  d      xs d }|}| j                  j!                  d      xs d }	t#        |       }
|
rt%        |
      nd }| j                  j!                  d      xs dj'                         j)                         dv }d }|
r3t+        | j,                  |
      }|rd }t/        |       n|st1        |      }|||	d}|t3        |      |d<   	 t5        d)i | d {   \  | j                  j!                  d      xs d }|}|rrj!                  d      xs |}||s|	r| d|	xs d d|xs d }fd}|	  |       }tC        | |       d {    y 	 tD        jG                  ||       d {   \  }}|jK                  |        d {    	 	 	 | jM                          d {   }|j!                  d       d!k(  rn|j!                  d"      }|4|j!                  d#      }tQ        |t2              r|jS                  d$      nd%}|sytT        jW                  |      }|rj|jY                         t[        |      k(  rN|j\                  j_                  ta        |jc                  d&            ta        |jc                  d'            (       |j\                  je                  |       tD        ji                  ||        y 7 ;7 7 7 X7 B7 7 7 # t6        $ rM}| j                  d|j8                   d       d {  7   | j                  d       d {  7   Y d }~y d }~wt:        $ rC}| j                  d| d       d {  7   | j                  d       d {  7   Y d }~y d }~ww xY w# t<        $ rC}| j                  d| d       d {  7   | j                  d       d {  7   Y d }~y d }~wt>        t@        f$ rC}| j                  d| d       d {  7   | j                  d       d {  7   Y d }~y d }~ww xY w7 7 # t<        $ rC}| j                  d| d       d {  7   | j                  d       d {  7   Y d }~y d }~wt>        t@        tH        f$ rC}| j                  d| d       d {  7   | j                  d       d {  7   Y d }~y d }~ww xY w7 87  # tN        $ r Y -w xY w# tf        $ r Y =w xY w# tD        ji                  ||        w xY ww)*Nr  z+pty refused: embedded chat disabled peer=%sr6  r7  r8  z3pty auth rejected reason=%s mode=%s cred=%s peer=%sr9  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@  rA  r  r  freshr   >   rh   r  r  r  r  r  z
[31mChat unavailable: z[0m
attachr  r  c                  4    t        j                         S )N)r  r  )r  spawn)r  r  r  s   rV   _spawnzpty_ws.<locals>._spawnD  s    t#66rX   z
[31mChat failed to start: )ra  r!  r  r  r  r  rX   r   r$  r  r  )5re  r   rP  rQ   rR   r   r  r  rS  r  r  r  rQ  _PTY_BRIDGE_AVAILABLEr  r   r   r  r  r   r   r  re   r	  r  r  r  r7   r   
SystemExitr  r  r  r  r}   attach_or_spawnr  r_  r  r  r  r   r  r  r  r   r  r  r  r  r  r;   detach)r  rU  rV  rW  r  rX  rY  
raw_resumer  r  r  r  rY  r  resolve_kwargsr  attach_tokenregistry_resumerb  r  r   _createdr[  r  r  r  r  r  r  s                             @@@rV   pty_wsrl  _D  sc    YY299>>CD+		?FhhD)AhBBB (+K?DAtT	
 hhD)9F;-:P)QhRRR/3%.0BDIhhD)9:L)MhNNN%b)M &6hhD)9-)HhIII
))+II4dD$G !llG
 	
 	
 hhDh!!! $$X.6$JFoo!!),4G$R(G18$W-dK??&&w/52<<>DDF K K +/>rvvwOF'(;<./BCF "N
 &034G0H,-7I.IIc3 ??&&x08DL Oc''"56D*_&r'-R?;Pb:QR7 		XF 2v&&&".">"> #? #
 
 ..
.JJL(
 wwv"88'''"C{wwv.8s.Cdkk'* $$S)EC0%%3u{{1~+>SUVEX%YNN  %- 8 	L"-O 	C  	S 	O 	J 	
 	"D J ll;CJJ<{STTThhDh!!! ll;C5LMMMhhDh!!!	, # 	,,!?uKPQQQ(((%%%!7+ 	,,!CC5TUUU(((%%%	 	'

  ll;C5LMMMhhDh!!!w5 ll;C5LMMMhhDh!!!
  )  (  
 	L"-sj  A[RA[7R8A[ RA[R 	[!R#"6[R&[2R)3C)[R/ +R,,R/ 4A[U [&W1'[-W7 W4W7 [#Z$[)Z1 +Z! >Z?Z! C=Z1  [[[ [#[&[)[,R/ /	U8"T ST 5S86T ;[ UU
$T'%U
?U U
[
U[	W.V3U64VVV[W.+W)WW)W!W)$[)W..[4W7 7	Z X>XX>3X64X>9[>ZZ-Y0.ZZ	Z[Z[Z! !	Z.*Z1 -Z..Z1 1	Z>:[ =Z>>[ [[z/api/wsc                 "  K   t         s| j                  d       d {    y t        |       s| j                  d       d {    y t        |       s| j                  d       d {    y ddlm}  ||        d {    y 7 h7 D7  7 w)Nr;  rA  r9  r   )	handle_ws)rP  r   r  r  tui_gateway.wsrn  )r  rn  s     rV   
gateway_wsrp  E  s     +hhDh!!!r?hhDh!!!!"%hhDh!!!(
B- 	" 	" 	"
 sE   BB%BB	%B*B+BBB	BBBz/api/pubc                   K   t         s| j                  d       d {    y t        |       s| j                  d       d {    y t        |       s| j                  d       d {    y t	        |       }|s| j                  d       d {    y | j                          d {    	 	 t        | j                  || j                          d {          d {    67 7 7 7 [7 D7 7 # t        $ r Y y w xY wwNr;  rA  r9  r?  )
rP  r   r  r  r  rQ  r  re   receive_textr;   r  s     rV   pub_wsrt  9E  s     +hhDh!!!r?hhDh!!!!"%hhDh!!!$R(GhhDh!!!
))+"2667"//:K4KLLL ' 	" 	" 	"
 	"  5LL s   DC(%DC*%D*C,+'DC.D+C0,D1%C6 C2C6 "C4#C6 (D*D,D.D0D2C6 4C6 6	D?DDDz/api/eventsc           	      r  K   t         s| j                  d       d {    y t        |       s| j                  d       d {    y t        |       s| j                  d       d {    y t	        |       }|s| j                  d       d {    y | j                          d {    t        | j                        \  }}|4 d {    |j                  |t                     j                  |        d d d       d {    	 	 | j                          d {    7 	7 7 7 7 7 c7 ,# 1 d {  7  sw Y   <xY w7 )# t        $ r Y nw xY w	 |4 d {  7   |j                  |      }|%|j                  |        |s|j                  |d        d d d       d {  7   y # 1 d {  7  sw Y   y xY w# |4 d {  7   |j                  |      }|%|j                  |        |s|j                  |d        d d d       d {  7   w # 1 d {  7  sw Y   w xY wxY wwrr  )rP  r   r  r  r  rQ  r   re   r  r   r  rs  r;   r   r_  rP  )r  r  rn   rq   r  s        rV   	events_wsrv  UE  s     +hhDh!!!r?hhDh!!!!"%hhDh!!!$R(GhhDh!!!
))+!1"&&!9NJ : :!!'35155b9: :6 //###	 / 	" 	" 	"
 	" : : : : : $  	6 	6!%%g.DR "&&w5	6 	6 	6 	6 	6: 	6 	6!%%g.DR "&&w5	6 	6 	6 	6 	6s_  H7D'%H7D*%H7*D,+'H7D.H7+D0,"H7D2H7*D6<H7D4H7E !E"E 'H7*H7,H7.H70H72H74H76E<D?=EH7E 	EG EG H7#E&$H7(9F3!H7,F/-H73G9F<:GH7H4G
H49HH4HH4H1%H(&H1-H44H7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)r4  rx  )r  rx  s     rV   _normalise_prefixry  E  s     BC  rX   c                  @   	 t               } t        | ddd      rt        t              syt	        fdt
        D              ryt               D ]#  }|j                  d      k7  r|j                  d      xs i }|j                  d	      xs i }|j                  d
      xs i }t        |t              r|j                  dd      nd}t        |t              r|j                  dd      nd}|j                  d      xs i }|j                  d      xs	 t        d   }|j                  d      xs	 t        d   }	dt        dt        fd}
d |
|       d |
|       d |
|       d |
|	       d	c S  y# t        $ r t        j                  dd       Y yw xY w)uV  Critical-CSS shim for the active user theme.

    Returns a ``<style>`` block with the ``:root`` CSS variables that
    ``ThemeProvider.applyTheme()`` installs once the
    ``/api/dashboard/themes`` round-trip completes.  The goal is to
    eliminate the green flash where the first paint shows the bundle's
    default Hermes Teal canvas before the SPA flips the configured user
    theme into place.

    Built-in themes return an empty string — their full definitions live
    in ``web/src/themes/presets.ts`` and are applied by the bundle
    before paint, so no shim is needed for them.
    rm  themer?  r  r   c              3   .   K   | ]  }|d    k(    ywr   r  )r   br  s     rV   r   z5_render_active_theme_bootstrap_css.<locals>.<genexpr>E  s     FqqyF"Fs   rS   paletter  	midgroundr  z#0a0a0az#e5e5e5
typographyfontSansbaseSizerx  rK   c                 8    t        |       j                  dd      S )Nz</z<\/)r  r  r  s    rV   _escz0_render_active_theme_bootstrap_css.<locals>._escE  s    1v~~dF33rX   z;<style id="hermes-theme-bootstrap">:root{--background-base:z;--midground-base:z;--theme-font-sans:z;--theme-base-size:z;}html,body{background-color:var(--background-base);color:var(--midground-base);font-family:var(--theme-font-sans);font-size:var(--theme-base-size);}</style>ztheme bootstrap render failedTr:  )r   r   r  r  r  _BUILTIN_DASHBOARD_THEMES_discover_user_themesr   r  _THEME_DEFAULT_TYPOGRAPHYr\   rQ   r@  )r  r{  r~  bgmgbg_hexmg_hextypo	font_sans	base_sizer  r  s              @rV   "_render_active_theme_bootstrap_cssr  E  s   6gyIZ4F,EFF*, *	Eyy F*ii	*0bG\*0bB[)/RB1;B1ERVVE9-9F1;B1ERVVE9-9F99\*0bD,U0I*0UI,U0I*0UI4 4 4 %%)&\N 3$$(L> 2%%))_$5 6%%))_$5 6;*	V  

2T
Bs#   +E: E: D/E: 8E: : FFapplicationc                    t         j                  j                  d      dk(  }|st        j	                         s'|rdnd| j                  d      dt
        ffd       }yt        d	z  dd
t
        ffd| j                  d      dt
        dt        fd       }| j                  dt        t        dz        d       | j                  d      dt
        dt        ffd       }y)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.
    HERMES_SERVE_HEADLESSrh   u_   Headless backend (hermes serve): web UI disabled — use `hermes dashboard` for the browser UI.z0Frontend not built. Run: cd web && npm run buildz/{full_path:path}	full_pathc                 *   K   t        did      S w)Nr  r   r  )r?   )r  _msgs    rV   no_frontendzmount_spa.<locals>.no_frontendE  s     $SAAs   Nz
index.htmlr  c           	      n   j                  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|j                  dd|  d      }|j                  dd|  d      }|j                  dd|  d      }|j                  dd|  d      }|j                  dd|  d      }|j                  dd|  d      }t               }|r|j                  d| dd      }|j                  d| dd      }t        |ddi      S )u#  Return index.html with the session token + base-path injected.

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

        When the OAuth auth gate is active (``app.state.auth_required``),
        the legacy ``_SESSION_TOKEN`` is NOT injected — the SPA reads
        identity from ``/api/auth/me`` over cookie auth instead.  The
        ``__HERMES_AUTH_REQUIRED__`` flag lets the SPA pick the right
        auth scheme for /api/pty and /api/ws (ticket vs token).
        r  r  r  r   r   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-revalidaterJ  )
r  rP  r   r   re   rm   r   r  r  r>   )r  r  chat_jsr  gated_jsbootstrap_scripttheme_bootstrap_index_paths          rV   _serve_indexzmount_spa.<locals>._serve_indexE  s    $$g$6<&'WSYY?@"6==DI F006x 833;* =  <N;K L==DI F006x 833;* =   << 0F6((2KLD<<51IJD<< 5x}7UVD<<6&1IJD<< 3vfX[5QRD<< 2eF8;4OPD =><<	o->g+FJD||I*:);7'CQG$&KL
 	
rX   z/assets/{filename}.cssr   r   c                   K   t         dz  |  dz  }|j                         r1|j                         j                  t         j                               st	        ddid      S t        |j                  j                  d            }|j                  d	      }|rUd
D ]P  }|j                  d| d| |       }|j                  d| d| |       }|j                  d| d| |       }R t        |d      S w)Nassetsr  r  rS  r   r  x-forwarded-prefixr  r  )r  z/fonts-terminal/r  r  zurl(zurl("zurl('text/css)r   r  )WEB_DISTrH  r  is_relative_tor?   ry  r   r   r  r  r@   )r   r   css_pathr  r  	asset_dirs         rV   	serve_csszmount_spa.<locals>.serve_css5F  s    h&H:T)::!)9)9);)J)J*
  + 6CHH"7??#6#67K#LM  ' 2W T	kkD"4VHYK6PQkkF9+"6&8TUkkE)"5vhyk7RST 
;;s   C:C<z/assetsr  )r  rP  c                   K   t        |j                  j                  d            }| dk(  s| j                  d      rt	        dd|  id      S t
        | z  }| r\|j                         j                  t
        j                               r+|j                         r|j                         rt        |      S  |      S w)Nr  rM  zapi/r   zNo such API endpoint: /r   r  )ry  r   r   r   r?   r  r  r  r{  rH  r=   )r  r   r  rW  r  s       rV   	serve_spazmount_spa.<locals>.serve_spaGF  s     "7??#6#67K#LM !5!5f!=4YK@A  y(	 !!#2283C3C3EF  "!!#	**F##s   B<B?r   )	rv   r   r   r  r{  r  r8   mountrA   )r  	_headlessr  r  r  r  r  r  s        @@@rV   	mount_spar  E  s    " 

673>I) 5 D	 	 
,	-	B 	B 
.	B\)K7
S 7
@ __-.<# < < /< ix(7J!KRZ[__()$3 $ $ *$rX   zHermes Tealu/   Classic dark teal — the canonical Hermes lookz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 canvasrC  Midnightz"Deep blue-violet with cool accentsrD  Emberu'   Warm crimson and bronze — forge vibesrA  Monou'   Clean grayscale — minimal and focusedrE  	Cyberpunku'   Neon green on black — matrix terminalrF  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              rZ| j                  d|      }| j                  d|      }t        |t              sy	 t	        |      }|t        dt        d|            dS y# t
        t        f$ r |}Y -w xY w)zNormalise a theme layer spec from YAML into `{hex, alpha}` form.

    Accepts shorthand (a bare hex string) or full dict form.  Returns
    ``None`` on garbage input so the caller can fall back to a built-in
    default rather than blowing up.
    Nr  alphar  r  r        ?)	r  r  r  r   r  r  r  r\  r  )r  r  r  hex_val	alpha_valalpha_fs         rV   _parse_theme_layerr  sF  s     }"];;%}55%))E;/IIg}5	'3'	$I&G S#c72C)DEE :& 	$#G	$s   #B BBzQsystem-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serifzDui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace15pxz1.55r  )r  fontMonor  
lineHeightletterSpacingr  z0.5remcomfortable)radiusdensity_THEME_DEFAULT_LAYOUT>   cardringrO  mutedaccentborderpopoverprimaryr1  rS  	secondarydestructivecardForegroundmutedForegroundaccentForegroundpopoverForegroundprimaryForegroundsecondaryForegrounddestructiveForeground>   r  herologocrestr  sidebar>	   r  r  pager  footerr  r  backdropprogress>   tiledcockpitstandardi   c                     t        | t              sy| j                  d      }t        |t              r|j	                         syt        | j                  d      t              r| j                  di       ni  t        | j                  d      t              r| j                  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       j                  d      xs | j                  d      xs ddd} j                  d| j                  d            }	 |t        |      nd|d<   t        | j                  d      t              r| j                  di       ni }t        t              }dD ]:  }|j                  |      }t        |t              s%|j	                         s6|||<   < t        | j                  d      t              r| j                  di       ni }	t        t              }
|	j                  d      }t        |t              r|j	                         r||
d<   |	j                  d      }t        |t              r	|dv r||
d<   | j                  di       }i }t        |t              rH|j                         D ]5  \  }}|t        v st        |t              s |j	                         s1|||<   7 i }t        | j                  d      t              r| j                  di       ni }t        D ]:  }|j                  |      }t        |t              s%|j	                         s6|||<   < |j                  d      }t        |t              ri }|j                         D ]n  \  }}t        |t              s|j                  dd       j                  d!d       j!                         sHt        |t              sY|j	                         sj|||<   p |r||d<   | j                  d"      }d}t        |t              r|j	                         r	|dt"         }| j                  d#i       }i }t        |t              r|j                         D ]  \  }}|t$        vst        |t              si }|j                         D ]  \  }}t        |t              s|j                  dd       j                  d!d       j!                         sHt        |t        t&        t
        f      sdt        |      j	                         s~t        |      ||<    |s|||<    | j                  d$      }t        |t              r
|t(        v r|nd%}|| j                  d&      xs || j                  d'd       |||
|d(}|r||d<   |r||d<   |||d"<   |r||d#<   |S # t        t        f$ r	 d|d<   Y zw xY w)*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.
    NrS   r~  colorsr  r  r  r  rK   c                 r    j                  | j                  |             }t        |||      }||S ||dS )Nr  )r   r  )r  r  r  r  r  
colors_srcpalette_srcs        rV   _layerz+_normalise_theme_definition.<locals>._layerF  s?    sJNN3$78#D+}E+v]}1]]rX   r  z#041c1cr  z#ffe6cb
foregroundz#ffffffr  warmGlowzrgba(255, 189, 56, 0.35))r  r  r  r  noiseOpacityr  r  )r  r  fontDisplayfontUrlr  r  r  layoutr  r  >   compactspaciousr  colorOverridesr  rO  rO  r   r  	customCSScomponentStyleslayoutVariantr  rg  r"  )rS   rg  r"  r~  r  r  r  r  )r  r  r   r  r   r  r   r   r  r  r  r  r  _THEME_OVERRIDE_KEYS_THEME_NAMED_ASSET_KEYSr  isalnum_THEME_CUSTOM_CSS_MAX_THEME_COMPONENT_BUCKETSr  _THEME_LAYOUT_VARIANTS)!r  rS   r  r~  	raw_noisetypo_srcr  r  val
layout_srcr  r  r  overrides_srccolor_overrides
assets_out
assets_srccustom_assets_srccustom_assetscustom_css_val
custom_csscomponent_styles_srccomponent_stylesbucketpropscleanpropr  layout_variant_srclayout_variantr3  r  r  s!                                  @@rV   _normalise_theme_definitionr  F  si    dD!88FDdC 

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

  88$5r:24&-1779 	1MFE55Zt=T$&E${{} -etS)S"-55c2>FFH"53U*;<E
((*"%e*E$K- +0 (	1 /2 (#.3EI_3_ 	  '"*dxxr2 'F #2 %x({$4 !M[ z" &"%&s   V6 6WWc                  .   t               dz  } | j                         sg S g }t        | j                  d            D ]G  }	 t	        j
                  |j                  d            }t        |      }|7|j                  |       I |S # t        $ r Y Ww xY w)a  Scan ~/.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.

    Uses the dashboard process launch home, not ``get_hermes_home()``, so a
    transient profile override from embedded chat does not hide themes that
    live under the server's own ``HERMES_HOME``.
    zdashboard-themesz*.yamlr  r  )
r   r   r  globr  r  r  r\   r  r  )
themes_dirr3  r  r  
normaliseds        rV   r  r  IG  s     )*-??J	FJOOH-. &	>>!++w+"?@D 16
!MM*%& M  		s    %B	BBz/api/dashboard/themesc                  V  K   t               } t        | ddd      }t               }t               }g }t        D ]'  }|j                  |d          |j                  |       ) |D ]=  }|d   |v r|j                  |d   |d   |d   |d       |j                  |d          ? ||d	S w)
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.
    rm  r{  r?  r  rS   rg  r"  )rS   rg  r"  
definition)themesr  )r   r   r  r   r  r  r  )r  r  user_themesr  r  r'	  s         rV   get_dashboard_themesr  cG  s      ]FV['9EF')K5DF& 6a  	V9fIwZ]+	
 	 	6	 //s   B'B)c                       e Zd ZU eed<   y)ThemeSetBodyrS   Nr  r  rX   rV   r  r  G  r  rX   r  z/api/dashboard/themec                    K   t               }d|vri |d<   | j                  |d   d<   t        |       d| j                  dS w)z9Set the active dashboard theme (persists to config.yaml).rm  r{  T)r  r{  )r   rS   r   )rj  r  s     rV   set_dashboard_themer  G  sK      ]F&  {#'99F; ++s   A A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                  h   K   t               } t        | ddt              }|t        vrt        }d|iS w)zEReturn the active font override (``"theme"`` = use the theme's font).rm  fontr  )r   r   _FONT_DEFAULT_ID_FONT_CHOICES)r  r-  s     rV   get_dashboard_fontr0  G  s7      ]F6;8HID= D>s   02c                       e Zd ZU eed<   y)FontSetBodyr-  Nr  r  rX   rV   r2  r2  G  r  rX   r2  c                    K   | j                   t        v r| j                   nt        }t               }d|vri |d<   ||d   d<   t	        |       d|dS w)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.
    rm  r-  T)r  r-  )r-  r/  r.  r   r   )rj  r-  r  s      rV   set_dashboard_fontr4  G  sZ      		]24998HD]F&  {"&F;%%s   AA	api_fielddashboard_dirc                4   t        | t              r| j                         syt        |       }|j	                         ry	 ||z  j                         }|j                         }	 |j                  |       | S # t        t        f$ r Y yw xY w# t        $ r Y yw xY w)u  Validate the manifest's ``api`` field for the plugin loader.

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

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

    Return the original string when the resolved path stays under
    ``dashboard_dir``; return ``None`` (with a warning logged at the
    call site) otherwise so the plugin still loads its static JS/CSS
    but its backend ``api`` is rejected.
    N)
r  r  r   r   r  r  r  r  rV  r  )r5  r6  r  r[  rg  s        rV   _safe_plugin_api_relpathr8  G  s    ( i%Y__->YI!I-668$$&T"  \"   s#   #A6 #B 6BB	BBc                  D   g } t               }ddlm}  |       }t               dz  df|dz  df|dfg}t	        d      r+|j                  t        j                         dz  dz  d	f       |D ]~  \  }}|j                         st        |j                               D ]J  }|j                         s|d
z  dz  }|j                         s.	 t        j                  |j                  d            }	|	j                  d|j                         }
|
|v ru|j#                  |
       t%        |	j                  d      t&              r|	j                  di       ni }|j                  dd|
       |j                  dd      d}|j                  d      }t%        |t(              r|j+                  d      r||d<   t-        |j                  d            rd|d<   |	j                  d      }g }t%        |t.              r#|D cg c]  }t%        |t(              s|s| }}|	j                  d      }|d
z  }t1        ||      }|r|t2        j5                  d|
|       | j                  |
|	j                  d|
      |	j                  dd      |	j                  d d!      |	j                  d"d#      |||	j                  d$d%      |	j                  d&      t-        |      |t)        |      |d'       M  | S c c}w # t6        $ r"}t2        j5                  d(||       Y d}~~d}~ww xY w))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   rz  r   HERMES_ENABLE_PROJECT_PLUGINSz.hermesprojectrm  manifest.jsonr  r  rS   r  r   r   positionr  )r   r>  r	  hiddenTslotsrM  )r6  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 mountedrg  r"  r   iconPuzzler   z0.0.0r  zdist/index.jsr  )rS   rg  r"  rA  r   r  r@  r  r  has_apir   _dir	_api_filez$Bad dashboard plugin manifest %s: %s)r   rD  r:  r   r3   r  r   r  r   r  r  r{  rd  re  r  r   rS   r  r  r  r  r   r   r  r8  rQ   rS  r\   )r  
seen_namesr:  bundled_rootsearch_dirsplugins_rootr   r  manifest_filer  rS   raw_tabtab_infooverride_path	slots_srcr@  rx  raw_apir6  safe_apir  s                        rV   _discover_dashboard_pluginsrQ  G  s    GeJ:*,L 
!	"Y	.7		 ),	y!K 67DHHJ2Y>	JK + Ff""$L0023 C	E<<>!K//AM '')=zz-"9"97"9"KLxx

3:%t$
 2<DHHUOT1R$((5"-XZ#KK!D6
; 'J > !(J 7mS1m6N6Ns6S+8HZ(H-.)-HX& !HHW-	#%i.(1N1Z35GAQNEN ((5/ % 33G=Yx/LL) g  !XXgt4#'88M2#> HHVX6#xx	7;#"!XXg?88E?#H~$.!)  eC	FN NI OB  C]TWXs?   AK4C)K4:K/K/K/CK4/K44	L=LL_dashboard_plugins_cacheforce_rescanc                     t         | rt               a t         S t         r t        d t         D              r
t               a t         S )Nc              3   V   K   | ]!  }t        |d          j                           # yw)rD  N)r   r   r   r   s     rV   r   z)_get_dashboard_plugins.<locals>.<genexpr>]H  s$     N4&	?))++Ns   '))rR  rQ  r  rS  s    rV   r   r   XH  s>    '<#>#@  $# 
"N5MNN'B'D$##rX   z/api/dashboard/pluginsc                    	
K   t               } t               }t        |ddg       xs g 
	 ddlm}m}  |       	 |       dt        dt        f	
fd}| D cg c]@  } ||      r6|j                         D ci c]  \  }}|j                  d	      r|| c}}B c}}}S # t        $ r t               	t               Y w xY wc c}}w c c}}}w w)
zPReturn discovered dashboard plugins (excludes user-hidden and non-enabled ones).rm  hidden_pluginsr  r   r   r   rK   c                     | j                  dd      }|v ry| j                  d      dk(  r|v ry|vryy| j                  d      dk(  r|v ryy)NrS   r   Fr   r   r   Tr   )r   rS   r  r  r?  s     rV   
_is_activez)get_dashboard_plugins.<locals>._is_activetH  sh    uuVR 6>55?f$|#;&  UU8_	)|#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?  s           @@@rV   get_dashboard_pluginsr\  bH  s      %&G]F6;0@"MSQSFN&((*
d t "  a= '')=$!Q1<<+<A= '  eu( 	>sR   (CB( C C>CCC#C(CCCCCCz/api/dashboard/plugins/rescanc                  >   K   t        d      } dt        |       dS w)z#Force re-scan of dashboard plugins.TrW  )r  r  )r   r   )r  s    rV   rescan_dashboard_pluginsr^  H  s!      %$7GW..s   c                   4    e Zd ZU eed<   dZeed<   dZeed<   y)_AgentPluginInstallBodyrc  Fr<  Tr  N)r  r  r  r  r  r<  r   r  r  rX   rV   r`  r`  H  s    OE4FDrX   r`  r   c                 z    | j                         D ci c]  \  }}|j                  d      r|| c}}S c c}}w r  )r  r   )r   r  r  s      rV   _strip_dashboard_manifestrb  H  s/    WWY@TQall3.?AqD@@@s   77c                     ddl m} m}m}m}m}m}m} t               }|D ci c]  }t        |d         | }	} |       }
 |       }t               }t        |ddg       xs g }t               dz  j                         }g } |        D ]p  \  }}}}}}|h}|r|j                  |       ||
z  rd}n
||z  rd	}nd
}t        |      }|	j!                  |      }|duxs |dz  dz  j#                         }d}	 |j                         j%                  |       d}|dv xr |xr t        |      j)                         }d}d} ||      }|j!                  d      xs g } | rH	 ddlm}! | D ]<  }"|!j/                  |"      }#|#s|#j0                  s$|#j1                         r5d}d| } n |j5                  ||xs d|xs d||||rt7        |      nd|||xr t        |      dz  j#                         ||||v d       s |D $ch c]  }$|$d   	 }%}$|D cg c]  }t        |d         |%vrt7        |       }&}t9               }'g }(	  |       D ]  \  })}*|(j5                  |)|*d        	 ||&t;         |             |' |       |(ddS c c}w # t&        $ r Y w xY w# t2        $ r Y w xY wc c}$w c c}w # t2        $ r g }(Y Zw xY w)zJAgent discovery + dashboard manifests + optional provider picker metadata.r   )_discover_all_plugins_get_current_context_engine_get_current_memory_provider_discover_context_enginesr   r   _read_manifestrS   rm  rY  r  r  r	  r  inactiveNr=  FT>   r)  r   r   provides_tools)registryzhermes auth r  )rS   r   r"  r   runtime_statushas_dashboard_manifestdashboard_manifestr   
can_removecan_update_gitr   auth_commanduser_hiddenr  )memory_providermemory_optionscontext_enginecontext_options)r  orphan_dashboard_pluginsr  )r   rd  re  rf  rg  r   r   rh  r   r  r   r   r   r  r  r   r   r{  rV  r  r   tools.registryrk  r  check_fnr\   r  rb  r  rU  )+rd  re  rf  rg  r   r   _read_plugin_manifest_atdashboard_listr   dash_by_namer  r  r  rY  plugins_root_resolvedr"  rS   r   r"  r   dir_strr  r  rl  dir_pathdmhas_dash_manifestunder_user_treecan_remove_updater   rq  manifest_datarj  rk  tnamer  r  agent_namesorphan_dashboardmemory_providerscontext_enginesr  r  s+                                              rV   _merged_plugins_hubr  H  sW      ,-N/=>!C&	NA%>L>$&L"$K ]F"6;8HRTU[Y[N,.:CCE!#D<Q<S <8g{FGS &KK\!'N{"&N'N=d#dNax+/E/W._._.a	**+@A"O
 o%T/Td7m>R>R>T 	
 0:&**+;<B	3+ E$..u5E8H(,)5dV'< 	}"&,",&7CE";B"?4+/UT']V5K4S4S4U*(>1
 	]<| '++1V9+K+  qy>, 	"!$  :;,.O02 	EGAt""Ad#CD	E $4>?[?]^.9;.	
	 	u ?D  		*  & ,  sY   J6!J
J=J
J
J J*"J/#J4 
	JJ	J'&J'4KKz/api/dashboard/plugins/hubc                    K   t        |        	 t               S # t        $ r)}t        j	                  d|       t        dd      |d}~ww xY ww)zIUnified agent plugins + dashboard extension metadata (session protected).zplugins/hub failed: %srY  zFailed to build plugins hub.r   N)r   r  r\   rQ   rS  r7   )r   r  s     rV   get_plugins_hubr  I  sP      7]"$$ ]-s34RSY\\]s$   A	 A	A$AAAz$/api/dashboard/agent-plugins/installc                 <  K   t        |        ddlm}  ||j                  j	                         |j
                  |j                        }|j                  d      s t        d|j                  d      xs d      t        d	
       |j                  dd        |S w)Nr   )dashboard_install_plugin)r<  r  r  r   r  zInstall failed.r   TrW  after_install_path)r   r   r  rc  r   r<  r  r   r7   r   rP  )r   rj  r  r3  s       rV   post_agent_plugin_installr  I  s     7?%jj{{F
 ::d::g&;*;
 	
 -
JJ#T*Ms   BBc                 V    | j                  d      } | rd| v sd| v rt        dd      | S )z=Reject path-traversal attempts in plugin name URL parameters.r   r|  rA  r   zInvalid plugin name.r   )r   r7   rP  s    rV   _validate_plugin_namer  .I  s1    ::c?D44<44<4JKKKrX   z//api/dashboard/agent-plugins/{name:path}/enablec                    K   t        |        t        |      }ddlm}  ||d      }|j	                  d      s t        d|j	                  d      xs d	      |S w)
Nr   "dashboard_set_agent_plugin_enabledTr  r  r   r  zEnable failed.r   r   r  r   r  r   r7   r   rS   r  r3  s       rV   post_agent_plugin_enabler  6I  sT     7 &DI/dCF::dFJJw4G4[K[\\M   AAz0/api/dashboard/agent-plugins/{name:path}/disablec                    K   t        |        t        |      }ddlm}  ||d      }|j	                  d      s t        d|j	                  d      xs d	      |S w)
Nr   r  Fr  r  r   r  zDisable failed.r   r  r  s       rV   post_agent_plugin_disabler  BI  sT     7 &DI/eDF::dFJJw4G4\K\]]Mr  z//api/dashboard/agent-plugins/{name:path}/updatec                    K   t        |        t        |      }ddlm}  ||      }|j	                  d      s t        d|j	                  d      xs d      t        d	       |S w)
Nr   )dashboard_update_user_pluginr  r   r  zUpdate failed.r   TrW  )r   r  r   r  r   r7   r   )r   rS   r  r3  s       rV   post_agent_plugin_updater  NI  Z     7 &DC)$/F::dFJJw4G4[K[\\-M   A$A&z(/api/dashboard/agent-plugins/{name:path}c                    K   t        |        t        |      }ddlm}  ||      }|j	                  d      s t        d|j	                  d      xs d      t        d	       |S w)
Nr   )dashboard_remove_user_pluginr  r   r  zRemove failed.r   TrW  )r   r  r   r  r   r7   r   )r   rS   r  r3  s       rV   delete_agent_pluginr  [I  r  r  c                   6    e Zd ZU dZee   ed<   dZee   ed<   y)_PluginProvidersPutBodyNrs  ru  )r  r  r  rs  r   r  r  ru  r  rX   rV   r  r  hI  s    %)OXc])$(NHSM(rX   r  z/api/dashboard/plugin-providersc                    K   t        |        ddlm}m} |j                  (t        |j                        }t        |        ||       |j                   ||j                         ddiS w)zHPersist memory provider / context engine selection (writes config.yaml).r   )_save_context_engine_save_memory_providerr  T)r   r   r  r  rs  rU  r  ru  )r   rj  r  r  rs  s        rV   put_plugin_providersr  mI  sg      7
 '9$:N:NO&7o.&T001$<s   A*A,c                       e Zd ZU eed<   y)_PluginVisibilityBodyr?  Nr;  r  rX   rV   r  r  I  r<  rX   r  z-/api/dashboard/plugins/{name:path}/visibilityc                   K   t        |        t        |      }t               }d|vst        |j	                  d      t
              si |d<   |d   j	                  d      xs g }t        |t              sg }|j                  r||vr|j                  |       n!|j                  s||v r|j                  |       ||d   d<   t        |       d||j                  dS w)zXToggle a plugin's sidebar visibility (persists to config.yaml dashboard.hidden_plugins).rm  rY  T)r  rS   r?  )r   r  r   r  r   r  r  r?  r  rN  r   )r   rS   rj  r  hidden_lists        rV   post_plugin_visibilityr  I  s      7 &D]F& 
6::k3JD(Q {{+//0@AGRKk4({{t;.4 [[T[04 ,7F;()<<s   CCz1/dashboard-plugins/{plugin_name}/{file_path:path}rW  c                 X   K   t               }t         fd|D        d      }|st        dd      	 ddlm}m}  |       } |       }|j                  d      d	k(  r |v s |vr2t        dd      |j                  d      d
k(  r |v rt        dd      t        |d         }||z  j                         }	|	j                  |j                               st        dd      |	j                         r|	j                         st        dd      |	j                  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 # t        $ r t               }t               }Y ]w xY ww)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(  s|  ywr   r  r   s     rV   r   z%serve_plugin_asset.<locals>.<genexpr>I  s     B6k)A1Br  Nr   r   r   r   r   r   r   r   rD  r  zPath traversal blockedr  r  zapplication/javascriptr  r  r  r  rH  r  z	text/htmlr  r  r  r  r  r  r  r  r  r  r  r  r  z.woff2z
font/woff2z.woffz	font/woffz.ttfzfont/ttfz.otfzfont/otfz.mapr  r  )r  r   )r   r   r7   r   r   r   r\   r   r   r   r  r  r{  rH  r  r   r=   )r   rW  r  r  r   r   r  r  rg  rj   r  content_typesr  s   `            rV   serve_plugin_assetr  I  sQ    * %&GBgBDIF4FGGN&((* zz(v%,&+[*HC8JKK	H		*,&C8JKKvDY'')F  04LMM==?&.."24DEE ]]  "F'( 	
 	#	
 	 	 	 	 	 	 	 	 	, 	 	
  	
!" 	"#M& ]"#
 	
 v&J "GH c  eus)   /F*F 	D=F*F'#F*&F''F*c                     	 ddl m} m}  |        } |       }t               D ])  }|j                  d      }|s|j                  dd      }|j                  d      dk(  r6||v rt        j                  d|       Y||vrFt        j                  d	|       t|j                  d      d
k(  r||v rt        j                  d|       |j                  d      dk(  rt        j                  d|d   |       t        |d         }||z  }	 |j                         }	|j                         }
|	j                  |
       |j!                         st        j                  d|d   |       C	 d|d    }t"        j$                  j'                  ||      }||j(                  |t"        j$                  j+                  |      }|t,        j.                  |<   	 |j(                  j1                  |       t5        |dd      }|t        j                  d|d          t6        j9                  |d|d           t        j;                  d|d          , y# t        $ r t	               }t	               }Y Xw xY w# t        t        t        f$ r t        j                  d|d   |       Y w xY w# t        $ r" t,        j.                  j3                  |d        w xY w# t        $ r%}t        j                  d|d   |       Y d}~d}~ww xY w)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   rE  rS   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)rD  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   )r  z+Mounted plugin API routes: /api/plugins/%s/z'Failed to load plugin %s API routes: %s)r   r   r   r\   r   r   r   rQ   r@  rS  r   r  rV  r  r  r  r{  	importlibutilspec_from_file_locationloadermodule_from_specr,  modulesexec_modulerP  r   re   include_routerrR   )r   r   r  r  r  api_file_namer   r6  api_pathresolved_apiresolved_basemodule_namer  r`  r   r  s                   rV   _mount_plugin_api_routesr  I  s   ,N&((*
 )* QY

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

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

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

I ::h9,LL6 v	 VF^, =0	#++-L)113M$$]3  LLGPVYfg	Y4VF^4DEK>>99+xPD|t{{2..11$7C (+CKK$'', S(D1F~KVTZ^\vfVn=M.NOIICVF^T_QY	  eu^ z2 		
 LL+,26NH 		8  T2  	YLLBF6NTWXX	Ys_   I 1I4&6K2KJ),(K6KI10I14.J&%J&)+KK	L L  Lr  zuvicorn.Serverc                     | j                   rF| j                   d   j                  r-| j                   d   j                  d   j                         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   )r  socketsgetsockname)r  r  s     rV   _read_bound_portr  qJ  sI     ~~&..+33~~a ((+779!<<OrX   actual_portc                    t         j                  j                  d      }|syd}	 t        |      }|j                  j                  dd       t        j                  dt        |       id      }t        j                  d	d
t        |j                        |j                   ddd      5 }|j                  |       |j                          t        j                  |j!                                |j                  }ddd       t        j"                  ||       y# 1 sw Y    xY w# t$        $ rO}|r,	 t        |      j'                  d       n# t$        $ r Y nw xY wt(        j+                  d||       Y d}~y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   TrW  r  )r   r   )
separatorswr  r  z.tmpF)r  r  r  r  r/  r  z+Failed to write dashboard ready file %r: %s)rv   r   r   r   r  rZ  rd  r  r  r  r4  r  rS   r  flushfsyncfilenor  r\   r  rQ   rS  )r  rj   r  r   r  rZ  r  s          rV   _write_dashboard_ready_filer  ~J  sE    ZZ^^78FHQF|$6**fc+&67JO((DKK ii[?
 	 HHWHHJHHRYY[!wwH	 	

8T"	 	  QX%%%6 BFCPPQsU   BD' +AD<D' D$ D' '	E?0E:3EE:	EE:EE::E?open_browserinitial_profilec                   	 |syddl 	t        j                  dk7  xsR t        t        j
                  j                  d            xs( t        t        j
                  j                  d            }|st        j                  d       y| dvr| nd}d	| d
| |rddl	m
} d ||       z  	fd}t        j                  |d      j                          y)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                  r    	 t        j                  d       j                          y # t        $ r Y y w xY w)Nr  )r}  rY  r  r\   )	_open_url
webbrowsers   rV   _openz"_maybe_open_browser.<locals>._openJ  s1    	JJsOOOI& 		s   &* 	66T)rj   rl   )r  r,  r-  r   rv   r   r   rQ   r@  r-  rj
  rx   rz   rT   )
r   r  r  r  _has_display_display_hostrj
  r  r  r  s
           @@rV   _maybe_open_browserr  J  s      	 	3

y)*	3

012 
 

K	
 	 (99D{M-+7I&z%"8!9::	 E$/557rX   r  headlessc           	          ddl }	 ddlm}  |        t               t        j                  _	        |r t        vrt        j                  d        t        j                  j                  rrddlm}	  |	       s2g }
	 ddlm} |j                   r|
j#                  d|j                           d	}	 dd
lm} ddlm}  |       }|j-                  d      xs i j-                  d      xs i }|j-                  d      xs i j-                  d      xs g }t/        |j-                  d            xr- t/        |j-                  d      xs |j-                  d            }|rt1        |      |z  rd|z   }|
r't3        d  ddj5                  |
      z   dz   |z         t3        d  d|z         t        j7                  d dj5                  d  |	       D                      t        j                  _         dv }|j;                  t         dt/        t        j                  j                        |rdnd|rdnd      |j=                         fd }t>        j@                  d!k7  rtC        jD                   |              y	 dd"l#m$} jK                         }| | |       |#       ytC        jD                   |              y# t        $ r!}t        j                  d|       Y d}~d}~ww xY w# t        $ r Y Vw xY w# t        $ r Y w xY w# t        $ r? d}d}	 tC        jL                  tC        jN                                n# t        $ r Y nw xY wY w xY w)$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.

    ``headless`` is the ``serve`` path: the JSON-RPC/WS backend with no UI
    build and no SPA mount (mount_spa() honours ``HERMES_SERVE_HEADLESS``), so
    the banner announces the bind rather than a browser URL.
    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  )rO  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).r;  )_BASIC_AUTH_PLUGIN_KEYSrm  
basic_authr  r	  usernamepassword_hashr  zThe 'basic' dashboard-auth plugin is in plugins.disabled but dashboard.basic_auth is configured.
Remove 'basic' from plugins.disabled (or run `hermes plugins enable basic`), then restart the dashboard.

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   4   K   | ]  }|j                     y wr  rP  rV  s     rV   r   zstart_server.<locals>.<genexpr>NK  s     7aff7s   )r   r   r   rS  rS  )r   r  	log_levelproxy_headersws_ping_intervalws_ping_timeoutc                    K   j                   sj                          j                        _        j	                         5  j                          d {    j                  r
	 d d d        y t              } | t        j                  _
        t        |        	rdnd}t        | d|  d       	rt        d
 d|         nt        d	
 d|         t        
|        	 d
dlm}  |t!        j"                                ddt!        j"                         dt*        dd ffdj-                  j/                         z          j1                          d {    j2                  rj5                          d {    d d d        y 7 F# t$        $ r }t&        j)                  d|       Y d }~d }~ww xY w7 `7 ># 1 sw Y   y xY ww)N)r  HERMES_BACKEND_READYHERMES_DASHBOARD_READYz port=T)r  z  Hermes backend listening on r   u     Hermes Web UI → http://r   )install_loop_noise_filterz%loop noise filter install skipped: %sg       @g      @r   rK   c                     j                         }|| z
  }|kD  rt        j                  d|       j                  |z          y )Nz1event loop stalled %.1fs (GIL pressure suspected))r}  rQ   rS  
call_later)r   ry  drift_hb_interval_hb_loop_hb_stall_threshold_loop_heartbeats      rV   r  z5start_server.<locals>._serve.<locals>._loop_heartbeatK  sN    mmoh..LLK ## /33ErX   )r  loadlifespan_classr   capture_signalsstartupshould_exitr  re   rm   r  r  printr  tui_gateway.loop_noiser  ro   r  r\   rQ   r@  r  r  r}  	main_loopr  r  )r  ready_tokenr  r  r  r  r  r  r  r  r   r  r  r  r  s       @@@@rV   _servezstart_server.<locals>._serveK  s     }}KKM //7##% B	(.."""!!B	( B	(
 +6DAK#.CII '4 5=0BZK[M}5TB 6tfAk]KL3D6;-HIk<QIL)'*B*B*DE L"%//1H
% 
D 
 
 ox}}/M ""$$$~~oo'''EB	( B	(">  I

BCHHI@ %'EB	( B	(s   AG#
GF$G0	G#9A8G2 F'A G2G3#GGG	G#$G'	G0GGGGGG G#r'  )asyncio_run)loop_factory)(uvicornhermes_cli.nous_auth_keepaliver  r\   rQ   r@  r   re   rm   r   r   rS  r  r  plugins.dashboard_authrO  LAST_SKIP_REASONr  r<  r   r   r  r   r   r   rd  r  rR   r   ConfigServerr,  r-  ro   r/  uvicorn._compatr  get_loop_factoryset_event_loop_policyWindowsSelectorEventLoopPolicy)r   r  r  r   r  r  r  r  r  r  skip_reasons_nous_plugin	_fix_hint	_load_cfgr  _cfg_ba	_disabled
_has_creds_is_loopbackr  _runner_loop_factoryr  r  s   ``` ``                 @@rV   start_serverr  J  sh   & AL!# 2$7CII $99 	
 yy 	= ')LG00 ''&|'D'D&EF	I FJ {xx,277EK!XXi06B;;JGM2	 "#''*"56 4GGO,C
0C<
 3y>4K#K) "!"I  4TF ;A B ii-	.
      0 7" #%./ 
 			KII7n&677	
  CIID <<L^^$TY 399223
 ".4 ,$  F" ^^F#FH( H(r ||wFH://1 }5FHg  A

:C@@AT  N  j  	))668  		sr   K 0K> 7CL L 	K;K66K;>	L
L	LLM&,'MM&	M M&M  M&%M&)rf  r  )re   r4   )Fr  )r   r   r  )uncommittedN)r  NF)r   )r  )r\  )
r  r   r   rm  rr  NNNFN)	r  r   r   rm  rs  r  NNF)r  r  Nrp  r  N)r   r  N)i  r  )NFFFr  )r
  )NNr   )re  r  NNN)r  )Nr  r	  )r   r  r  Nr	  )r   N)r  r:   rK   N)NNNNr  )r   i#  TFr   F(  r  
contextlibr   r   ro   r  rK  r  concurrent.futuresr  r  dataclassesr   r   r   r  r   r  importlib.utilr  rd  r  r  rv   r  r  r  r  r  r.  r,  r  rx   r}  urllib.errorr`  r-  r5  hermes_cli._subprocess_compatr	   r
   urllib.requestpathlibr   rP  r   r   r   r   r   r  __file__r  r  rq  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'   plugins.memory.config_schemar(   r)   r*   r+   r  r,   r-   r.   r/   r0   r1   r2   r  r3   fastapir4   r5   r6   r7   r8   r9   r:   r;   fastapi.middleware.corsr<   fastapi.responsesr=   r>   r?   r@   fastapi.staticfilesrA   pydanticrB   rC   starlette.concurrencyrD   rb   tools.lazy_depsrE   _lazy_ensurer\   rd  r  r   r  	getLoggerr  rQ   r  rW   r^   r  rd   r   r   rp   r   r  r   re   hermes_cli.memory_oauthr   _memory_oauth_routerr  r   r*
  r   r   rP  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  ra  rw  r  r  r  r  
_mcl_entryr  r  _k_vr  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r3  r  r  r   r#  r-  r  rJ  rS  rw   r]  rb  r  r  rS  r  rk  r  r  rG  rJ  ru  r  rm  r  r  r  r  r  r  r"  r  r  r  r  r  r  r   r  r
  stat_resultr  r  r"  r4  r  r=  rP  rU  r\  r^  r`  rh  rn  rr  rw  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r/  r  r  r  r  r  r#  r%  r'  r(  rA  r+  r-  r/  r1  r3  r7  r:  r?  rC  rH  rN  rQ  rT  rZ  r]  ra  rf  ri  rm  rq  rt  rw  rz  r~  r  r  r  r  r  r  r  r  r  r  r  r&  r8  r:  putr?  rF  rL  rN  rP  rV  rY  r\  r7  rr  rv  rz  r~  r  r  r  r  r  r  r  r  r  r  r  r  r  rD  r  r  r  compiler  r  r  r  r  r  r  r  r  r#  r-  r8  r:  r@  rA  rD  rT  r[  r`  rd  re  r  r  r  r  r  r  r  r  r  r  r  r	  r  r  r  r   r1  r6  r>  rH  rJ  rM  rQ  rU  rX  r^  r`  rh  rm  rv  rt  rx  r|  r~  r  CompletedProcessr  r  rk  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?  rM  r_  rf  rk  rw  r  r}  r  r  r  r  r  r  r  r  r  r  r  r  r 	  r	  r	  r		  r	  r	  r	  r"	  r(	  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,
  r0
  r5
  r7
  rA
  rW
  r9
  r>
  rd
  rB
  rG
  rI
  rK
  rQ
  r\
  r^
  re
  rk
  rm
  rp
  rr
  rw
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r
  r"  r
  _ANTHROPIC_OAUTH_TOKEN_URLr
  r1  r
  r#  r
  r$  r
  r!  r   r%  r
  r
  r
  r
  r  r  r(  r6  rb  rV  rZ  r[  r  r  rW  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r~  r  r  r  r   r  patchr  r  r  r.  r0  rA  rC  rK  rP  rS  rY  r]  r_  rd  rh  rj  r}  rt  rc  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&  rd  r#  r'  r$  r9  r)  r.  r7  r:  r\  rg  ri  rp  rr  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#  r&  r)  r+  r0  r2  r9  rJ  rL  rS  rU  rX  r]  r`  rb  r  rg  rk  rn  rq  rs  ru  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  rf  rg  r}
  r  r  r  r%  r'  r7  r:  r=  r?  rO  rR  rU  rY  r[  r^  r`  rd  rh  r  r  rr  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  rD  rR  rV  ri  rl  r  r  r-  r   hermes_cli.win_pty_bridger  r  r  rc  r  hermes_cli.pty_bridger  r  hermes_cli.pty_sessionr  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  rC  rS  r  r  r  r  r  r  r  r$  r(  r2  r4  	websocketr\  rl  rp  rt  rv  ry  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r.  r/  r0  r2  r4  r8  rQ  rR  r   r\  r^  r`  rb  r  r  r  r  r  r  r  r  r  r  r  r  r  r   hermes_cli.dashboard_auth.routes_dashboard_auth_routerr  r  r  r  r  s   0rV   <module>r@     sSj  	 ;       ! '        	 	      
       R   3 3 H~$$++335|CHH$HHOOAs<() 4     .    "
   7TT/-7. 3Drzz2Q4

,-.W[\dWeWlWloyWyw"2+< 2 2UY 2*5 5 * *Z>"(Y (7<< (2y 2T#t)_ 2 NK)L C   ' (  @A^EZWEZEZ[]E^/  $(   #% DK $     D%%	  (
Ag A$ A. *34I3J)K 	# KXG X3 X4 XDG D DH $- . $ y 
-c - -$ -,4!3 4!C 4!D 4!n $' $ $: ?$G ?$ ?$V ; ; ;
 $7 $ $. 
;G 
; 
;$($s) (*s0/+-s0 Is0 Vs0  3P!s0* +z*+s04 0z5s0> 0 D?s0L 0- Ms0V )7Ws0` 3Pas0j =-ks0t >2 us0~ 8-s0H 2x(Is0R 4-Ss0\ 08]s0f <2gs0r AT$ G
 Y'	. #  {_s0 4T#s(^+, sl#z#w# g# G	#
 w# 7# # 9# # g# g# W# y#" '##* 	+#4 
75#< G=#c3h Ds s " %cN%% 
#tCH~
%P *.9
 56
-/c4S>)* /!!# =FBOB	W}2<./=  >c >d >,^
^9^ 
c3h^ 
#Y	^BT#tCH~2E-F :"9 "
	9 	"9 "
"9 "
      9 "i "#i #"i "
"i ""i "$	 $
	 #i #
Y 	  * S#X  #3 ; ; ;
"i "8+9 +y  "y ""Ws W3 W5c? Wv UW:: #:,/:;>:NQ:	:z  bii 45 "#IBII.F$LM #' 'uT4$;%67 'Ts 8s ( 	  $ 7 + !+.  $   4 %. / % !0  )*   8 8 8*TT Td 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(1 1 1)5 )T )4 E$*>$? &T cDj 
 
  &DJ ( `# ` `@ ;@ D$ D4 DD D	3: 	$ 	4 4t 4 4t  =g =$ =
.$ 
.#D #L DH Y7 YD YL^ Y. 	#+Dj#+#+ 	#+
 tS()#+L#5 $sCx.  2 D T#s(^ 2s uUCZ'8 "  0 !*+]!^ 4 5ucz*C/0 'C$J '3 ' #*    5S;Q   
"#'_ 'x} ' $'T g Xc]  > 	W C  D 	        F 
'8 7  > "  
$% CyS	4j	<<
< < 	< &<~ 
,B W  ( LP'8 P7 P P0 X X X2 	S  .) 
 
+T +T  +T\ 	 !
I 
I "
I 	.C . . 	7  7 + *XC C ) ) 
I ) )I ) 
 	@ @ @ 	QC Q Q 	N3 N N 	!"S S #S 	 Mc M# MXVY] M !M 	 afe
ee!$e<DSMeZ^e !e 	VC Vs V V 	)*J J +J 	$%SC Shsm S &S 	$%EC E &E 
!"Q Q #Q 
#$S+ S %S 
"#R R $R 
"#` ` $` 
 !E{ E "E 
%&JK J 'J 
!"O'9 O #O 
$%*?  & 
"#T(; T $T  %$$)!,
< d3c3h#78 
 ""FG .$ .$ .DQTVYQYN .b9S4S> 9Sx [3hsm [3 [3|  
3 
 
 
  	
  
#s(^6 	M Mr  (9  	5< 5  5 
@ @ 	Vhsm V  V "i "
"y " 	 hsm    !_  " 	%5  S  % %^ 
 !@ "@ /9 9 
#$C %C	  
 ! ):T)A   " Z ()F2 2' '/ $#,  %,%(% &% (	%
 !% 1% !% !% 7% 1% 5% /% +% +% %  1!%" 5#% 4S> , .0tC)))* //1 $sE#s(O+, 1 .0c4S>)* /B3 B B BT B"	S 	,T#Y ,c ,j>N>N ,^,d ,s ,tCy ,^:# :c :d3i :Ehsm E3 E3 E
 $$=> !rzz&)  bjj!45 /s / /S /T /dFHSM FU:CSCSUYCY=Z F*8C= DQTVYQYN , 
 !8C=  "  
E E  EP 
(  (V- -T$sCx.-A -` 	#$VT V %Vr 
!"F+D F #FRi 84S> 8c 8 )-  ,Xc] t   	'(@1 )@1F 
=o = =@ 	%&"# "c " '"X ? tDcN'; T#s(^@T  !_M_M_M _M 	_M
 _M _M _M _M _M c]_M _MD 	!"{{{ { 	{
 { { { { { #{| 	)* !AAA A 	A
 A A +AH 	 `ES `Ec `E# `E !`EFd38n c3h 4 4S> ( h .&} .&3 .&3 .&b-   2=2 =t =	22 	2tCH~ 	2} u 4S> c $Y- Y% Yd3PS8n YY] YA/eDcNCc3h$GH / (m)= (m$sCx. (mV("6 (S#X (_c (*2#7 2c3h 2TX 2$11%9 114S> 11VZ 11hS#X 4S> $-A 4PSUXPX> ^b  < < <# #  C DcN $F FS	 F# $sCx. *c d38n  * 	! ?S ?S ?Qc Qc Q
  0hsm 0C 03 0DcN * 7;
  	
  
334 C= 
#s(^0   	
    &"4S> "d "<&DI &$tTWY\T\~J^ &RltCH~&l	$sCx.l^ c3h @2C 23 24SRUXCW 2j2$ 24S> 2" "S#X "JJ# J3 J 16 8 8 8$ 8$sCx. S S#X d38n  &#c3h #tCH~ #$ # :>S>
sCx. Dd38n!456 
	*c3h tCH~ RVWZ\_W_R` "3 # $sCx. *S#X *S *S * c S $sTWx. ]a 0
H 
H 
H 
H7Dc3h,@ 7t
 
 
((
(( cN( 
	(D &2::&IJ 	Xc 	Xd 	X 	./)3 )# )X`adXe ) 0), 
./0c 01K 0 00( 	./im%M
%M/%M:B3-%MYabeYf%M 0%MP Fhsm F F 	  ! 	Ahsm A A  ! 4  	N'HSM N' N'r$sCx  	!!&	-Tc]-T-T -T 	-T -T` 	)*IBC IB +IBX 	 /W(3- /W !/Wd 	QHSM Q Q 	@Q) @QHSM @Q @QF 
9W_ 9Wx} 9W 9Wz UWmmm&)m14m@CmNQmd/s /3 /5QTVYQY? /dJc3h JDcN JZ Ml MXc] M M(h hV ; ; ;| ML M8C= M M: ID;Z	2 DeCHo-. 5 T#Y 6S C s 
[d38n [c [ 64S> 6d38n 6r$sCx.  PT .=S#X =6J =uUXZ^_bdg_gZhUhOi =@ 	*+W ,W 
+,V!5 V -V" 
BCZ# Z DZ: ;<X X =X, 
45\)= \ 6\6 
#$:s\ :sG :s %:sz JM| Mhsm M M6 
CG-
-!(-3;C=- -FH2JGV/H2 NA

 /
H2& {0Q>'H24 S4V>5H2B H;
 WCH2Z BFQ=[H2h [8
 iH2@  PH.2AH2N EV

OH2p 
C4?CqH2~ D]DHH2L 9a
 ?MH2d IaeH2n #?N4)oH2| DN

}H2^ ,[]L=_H2l (N.

 K
mH2D N&I9 	[ #;	 bV
  W_ICH2 T#tCH~-. HX$sCx 6G7Q%AG7 O"G7 L(G7 <#G7& -!'G70 D&1G7: N*;G7B W&CG7J [/KG7T ?!UG7\ ?"]G7f ?gG7n ?oG7v +&/wG7@ *%AG7J $8NSKG7L / MG7X '!
 /%# ," 
 ;
 9!( `(
 [# g& &<xP1 3 :&" 6
 =!EG7 $sDcN23 GT&U4S>3+>%? &R9S> , & '  : :c3h :$S $U38_ $38n * 38_	 26  $'$J 	#s(^ F c3h$)> 	S 	T#s(^ 	-# - 	SS>Sc3hS D[S 	S
 
#s(^SlA At A A $'  )W &     HJ tC)C$CD I+IOO- W W3 Ws s VS VS VL L3 3: Q QsTzSVY]S]_bei_iGiAj QB,
T ,
d ,
^*$ *c *jFVFV *Zj&6&6&= $ :
 :
:3C3C :
 :
z.c . .S .T .8<*7$ 74 7S :T Y]^acf^fYg  QUVY[^V^Q_ * 
455<*A 5< 65<p 	:;@S @ <@ 
ABMQ4424=Ec]4 C4n =>  ? $Q  $4[M"B 
% % % HJ tC)C$CD I+IOO- s !C !E !<s sTz c S S  #'#CC
C sCx.4
	C
 *C 
#s(^CT #'#
 sCx.4
	
 * 
#s(^  
45"*A " 6"J 	:;FS F <FR QUVY[^V^Q_ 8 
ABMQAA2A=Ec]A CAH =>  ? 	#$
8C= 
 %
(00)1#0c]0f 	12NR?M?M3?M>Fsm?M 3?MD 
781s 1Xc] 1 91@ 8C=  3  s  2G0c3h G0T0$sCx. 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^[ # [ T#s(^ [ |c3h HSM 2
d38n 
d3PS8n 
YabeYf 
/d4S>2 /d 	 '( '( !'(T 01 "@@@@@@ c]@@ 2@@N % -/c4S>)* /%y~~' 
'    "& "E +# 8C= +Xc] +t + "
 c] 3S#X	0 #44sm4 c]46Ec 6E# 6EVY 6E^b 6Er8C= DcN < "M.M.M. c]M. 
#s(^	M.d "QmQmc]Qm 
#s(^Qmh=+S =+T =+@Q+ Q+ Q+h=+3 =+4 =+@c c 4( ( (,t, t, t,n 
45 ""D"D"D c]"D 6"DJi 
 
56
 "	[[
[ [ c]	[ 7[ 	?@ " c] A2 89 "222 c]2 :2(N3 Nv" "
"I " - W  (3- 4SRUXCW \`adfiai\j  
%&1,.@ 1, '1,h 
 !G  "2 	$%6# 6 &6" !",(3- , #,< 	Xc]   <6(3- 6 	%& x}  ' 	78 "c] 9* 	./ "	c] C= 	 0D (),c ,HSM , *,6"I " '(c   )B 	,-c HSM  . 9 8O, Od 
 : : !: # 1+
1+1+ C=1+ }	1+
 SM1+ 1+rI  I  EJ s T hWZm 
S 
Xd3i%8 
C t QT .
S#X 
4 
*#s(^ 
#s(^B
49
 
"5T$sCx.1 5@s @(6 6%T	2B 6DcN S  cSVh 8C= S 83 8C= # " 	G# G Gs Xc]  	"#Ms MXc] M $M*S *8C= *PS *Z 	'(ZS Z8C= ZPS Z )Z#< #< #<L 
N N N N 	%&  ' 6# ] Xc] D 	"#V# V] VXc] V $V x}  
)*O Ox} O +O#   
*+P# P P ,P3 #  
+,Q3 Q# Q -Q
# 
 
 %&P# P P 'P* *S *T *4 
7SW 7S 7S~ Y  
 	 < !<B 
,-<&D <s < .<L"i ""	 "D-
D-
3S#X-.D-Nc3h DcN c S#X 4S> , 		HSM 	 	 
 4  4#  4  4F 	$5   $ %&#   ' 
()8 8hsm 8 *8v #   46 $s001 6&	( AC eCHoy~~=> C-y~~/ 	0Z Z# Z# ZaW a3 a3 a"IINN IM  M  M ` 
()? ?g ? ? *?D 	)* w  + 	56 	&n&n
3-&n C=&n C=	&n 7&nR"y "
 	*+@DE
E%E08E ,E* 	I<HSM I< I<X"	 " 
$%<;*; <;hsm <; &<;~3 3    13R SY 
I 
   
 !  "* 
 
} 
 !
 
&'* (*!I ! T#s(^ s tTWY\T\~ &   
 ! "( /2} 2 2j "#	s 	 $	9  	'(DC D/C D )D: 
B# B  B 
A A A( 	  s 3 4S> ( 	 !$ "$4 
!"0L*; 0L #0Lf 676 6S 6 86D9 
) 
  * 	 
,$8 
, !
, 
,[ , ,H 
; ; 
#$C %C!I !
)t )YD Y
 
=  6 	#$S  %,	I 	 
;= ; ;"sTz c  
"#Cyu+J
JJ $JZ 	,8 ,8^  
5RJ 5R 5Rp 
 !J ! !H 	 > !>> 
&'F (F&") "
Bx} Bc B"3 S S  
#$7"5 7 7 %7&"I "
 
%&9$9 9HSM 9 '9$") " 
"#IMB
&
'B9A#B $B& "". 	 	  > 	"#ER8C= ER $ERP 	!"PT/Q
/Q/Q-0/Q@H/Q #/Qd 	"#= =8C= = $=@ 	 [S [ [ ![FI 8I 	 ) y  
) c C 3 d38n &3T$sCx.-A 3l	.s 	.t 	.D D D*d *c *# *$ **,D ,4@Q;R ,WZ ,^!D !S	 !c !H C C /m m m` 	 2 !2( 
 !R,? R "R& 	-.5# 5 /5 
./2,s 2, 02,j !"B B= B #B "#+ + $+" 	$%, , &, 	$%C /@  & 	+,HC H?W H -H, 	%&>c >9K > '>& 
./s :M  0P 'y(  72HSM 72 72t *8C= * *B") " hsm  > 	
D[ 
D8C= 
D 
D") "" "
 	I# I I  I$ -[  $ 	%7    	. .  .b"I "
 	%&(s (- ((3- ( '(V 	,-j3 j# j .jZ"I "  # T hsm  	23 	2S 	2s D HSM V^_cVd & 	,-HL8
8!#88@8 .8v" " 	+,BF+K
+K'+K2:3-+K -+K\ 	./EIA
A*A5=c]A 0AH"y "
 	)*3\ 3\,< 3\xPS} 3\ +3\l"y "
 
12@D.V
.V%.V08.V 3.V~ M b *d ? A @5, Dc3h( B 3EE33v;E 
d 
 
s 
s 
>u >2E 'T 'e ' e *N N7# 7T 7e 7( 	'(4# 4 )4@"I "
 	&':>,
,*23-, (,P 	)*
%8C= 
% +
% 
56G(3- G 7GF"i "
 	I(3- I I 	J/ JHSM J J& $tCH~*> B-4S>"-.24S>.B-	$sCx.-`T#s(^ 4 d38n9M 8Ns N(3- Nb 	 HC Hx} H !Hm m8C= m` 	 !IS I I "I. <<5!	\\ $	HH $ RZZ67
  P O O!(	S.l BJJ9:  KLM+ M(3- M6(*k (*d (*V{ x} D	.; 	.4 	.?; ?8C= ?
H{ Ht H
s Q% Q%hsmS.@(A Q%h*K *D * !!%!)-	k6SMk6#k6 c]k6 "#	k6
 49hsmXd^34k6j Y-.# 8(x} (D !!%!)-	
SM
#
 c]
 "#	

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

9
9

9 	
9
 c]
9 	
9$$||$ #s(^$ 
	$zz||z z
 z 
zzs uXd38n-ExPS}-T'U ( ~Z Zt Z Zz
 zl.Y l.4 l. l.t y t  < zY 4  6 }(6	 (6d (6 (6V	!8C= 	!S 	!DC DNJ$7 J$h }M~'<  NA  B{  NF  GzMqr7Dmn6Dmn;Dmn7Etu	 c  E T\]abegjbj]kTl 2 dV- 4S>  ) tCH~ 
  O  
 : 
 " Nd38n N$sCx.9Q Nbt 4 	 !0 "0<9  	 ,L , !,     	  )  	&; &  &*" "t "QT "JiT iZ ,0 (4. /$ $$ $ 	!"$ #$N 	()/ */i Ac3h ADcN ApT#s(^ pf 	%&]7 ] '] 
01W <S  2(   
;<G 3  = 
<=W C  > 
;<	G 	3 	 =	 67	w 	c 	 8	)i )
 	*+ 7N  ,"I  
9:=' = =DY = ;=, 	<=T# T# T >TnoYf   N   ) * 	#
- 
 
 
$QS $QT $QN(8
(8(8/3(8FI(8	(8X l
l
l l 	l
 l lMS	  

:%e4	
 	
 	
 	;XX31; 
 ^^,,Y[
 	

 	<
l' 	I "LLH		*+ ""hD  '!&'Nw F^  	 %	, 	 		  	 %	, 	 		s   %:DV+ 9DX" V&!DY pDY%y DY* yDZ V+DXV1A
DW<W;DXW<DXXDXXDXX"-DYYDYYDY"Y!DY"Y*DZZDZZDZ)Z(DZ)