@maudecode/talaria-web 1.15.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/_release.json +4 -4
  2. package/dist/api/automation-router.d.ts +21 -7
  3. package/dist/api/automation-router.js +3 -0
  4. package/dist/api/automation-router.js.map +1 -1
  5. package/dist/api/chat-router.d.ts +142 -6
  6. package/dist/api/chat-router.js +11 -5
  7. package/dist/api/chat-router.js.map +1 -1
  8. package/dist/api/git-router.js +44 -32
  9. package/dist/api/git-router.js.map +1 -1
  10. package/dist/api/raw-routes.js +5 -58
  11. package/dist/api/raw-routes.js.map +1 -1
  12. package/dist/api/router.d.ts +4 -2
  13. package/dist/api/router.js +8 -8
  14. package/dist/api/router.js.map +1 -1
  15. package/dist/api/sessions-router.d.ts +714 -42
  16. package/dist/api/sessions-router.js +57 -23
  17. package/dist/api/sessions-router.js.map +1 -1
  18. package/dist/api/settings-router.d.ts +153 -9
  19. package/dist/api/settings-router.js +61 -16
  20. package/dist/api/settings-router.js.map +1 -1
  21. package/dist/api/sse-routes.js +1 -1
  22. package/dist/api/sse-routes.js.map +1 -1
  23. package/dist/api/tools-router.d.ts +13 -1
  24. package/dist/api/tools-router.js +10 -8
  25. package/dist/api/tools-router.js.map +1 -1
  26. package/dist/app.d.ts +1047 -67
  27. package/dist/auth/gate.js +8 -4
  28. package/dist/auth/gate.js.map +1 -1
  29. package/dist/auth/oidc.d.ts +6 -2
  30. package/dist/auth/oidc.js +16 -11
  31. package/dist/auth/oidc.js.map +1 -1
  32. package/dist/auth/store.d.ts +14 -2
  33. package/dist/auth/store.js +26 -1
  34. package/dist/auth/store.js.map +1 -1
  35. package/dist/cli/dotenv.d.ts +1 -1
  36. package/dist/cli/dotenv.js +36 -3
  37. package/dist/cli/dotenv.js.map +1 -1
  38. package/dist/http/addresses.d.ts +6 -0
  39. package/dist/http/addresses.js +26 -0
  40. package/dist/http/addresses.js.map +1 -1
  41. package/dist/http/context.d.ts +3 -3
  42. package/dist/links.d.ts +26 -0
  43. package/dist/links.js +60 -0
  44. package/dist/links.js.map +1 -0
  45. package/dist/onboarding.js +26 -7
  46. package/dist/onboarding.js.map +1 -1
  47. package/dist/profiles/profiles.js +1 -1
  48. package/dist/profiles/profiles.js.map +1 -1
  49. package/dist/providers/catalog.d.ts +74 -1
  50. package/dist/providers/catalog.js +248 -42
  51. package/dist/providers/catalog.js.map +1 -1
  52. package/dist/providers/tables.d.ts +3 -1
  53. package/dist/providers/tables.js +9 -4
  54. package/dist/providers/tables.js.map +1 -1
  55. package/dist/redact.js +493 -13
  56. package/dist/redact.js.map +1 -1
  57. package/dist/runtime.js +52 -28
  58. package/dist/runtime.js.map +1 -1
  59. package/dist/sessions/anchor.d.ts +22 -3
  60. package/dist/sessions/anchor.js +87 -42
  61. package/dist/sessions/anchor.js.map +1 -1
  62. package/dist/sessions/background-tasks.d.ts +4 -1
  63. package/dist/sessions/background-tasks.js +7 -4
  64. package/dist/sessions/background-tasks.js.map +1 -1
  65. package/dist/sessions/completions.js +1 -1
  66. package/dist/sessions/completions.js.map +1 -1
  67. package/dist/sessions/list.d.ts +8 -4
  68. package/dist/sessions/list.js +24 -17
  69. package/dist/sessions/list.js.map +1 -1
  70. package/dist/sessions/merge.d.ts +70 -7
  71. package/dist/sessions/merge.js +331 -23
  72. package/dist/sessions/merge.js.map +1 -1
  73. package/dist/sessions/pending.d.ts +5 -0
  74. package/dist/sessions/pending.js +30 -10
  75. package/dist/sessions/pending.js.map +1 -1
  76. package/dist/sessions/service.d.ts +57 -3
  77. package/dist/sessions/service.js +327 -56
  78. package/dist/sessions/service.js.map +1 -1
  79. package/dist/sessions/session.d.ts +6 -1
  80. package/dist/sessions/session.js +6 -0
  81. package/dist/sessions/session.js.map +1 -1
  82. package/dist/sessions/shares.js +11 -2
  83. package/dist/sessions/shares.js.map +1 -1
  84. package/dist/sessions/store.js +1 -1
  85. package/dist/sessions/store.js.map +1 -1
  86. package/dist/sessions/turn.d.ts +19 -1
  87. package/dist/sessions/turn.js +218 -113
  88. package/dist/sessions/turn.js.map +1 -1
  89. package/dist/settings.js +9 -0
  90. package/dist/settings.js.map +1 -1
  91. package/dist/sidecar/fake.js +2 -0
  92. package/dist/sidecar/fake.js.map +1 -1
  93. package/dist/tools/commands.d.ts +15 -0
  94. package/dist/tools/commands.js +52 -0
  95. package/dist/tools/commands.js.map +1 -0
  96. package/dist/tools/crons.d.ts +4 -1
  97. package/dist/tools/crons.js +58 -11
  98. package/dist/tools/crons.js.map +1 -1
  99. package/dist/tools/mcp.js +5 -1
  100. package/dist/tools/mcp.js.map +1 -1
  101. package/dist/tools/update-notifications.d.ts +1 -1
  102. package/dist/tools/update-notifications.js +1 -1
  103. package/dist/tools/update-notifications.js.map +1 -1
  104. package/dist/tools/updates.d.ts +8 -3
  105. package/dist/tools/updates.js +133 -34
  106. package/dist/tools/updates.js.map +1 -1
  107. package/dist/workspace/git.d.ts +57 -10
  108. package/dist/workspace/git.js +302 -195
  109. package/dist/workspace/git.js.map +1 -1
  110. package/dist/workspace/media-refs.d.ts +13 -0
  111. package/dist/workspace/media-refs.js +165 -0
  112. package/dist/workspace/media-refs.js.map +1 -0
  113. package/dist/workspace/media.d.ts +19 -0
  114. package/dist/workspace/media.js +61 -2
  115. package/dist/workspace/media.js.map +1 -1
  116. package/dist/workspace/workspaces.d.ts +7 -1
  117. package/dist/workspace/workspaces.js +30 -21
  118. package/dist/workspace/workspaces.js.map +1 -1
  119. package/dist/workspace/worktrees.d.ts +2 -4
  120. package/dist/workspace/worktrees.js +28 -33
  121. package/dist/workspace/worktrees.js.map +1 -1
  122. package/package.json +4 -2
  123. package/sidecar/scripts/replay_sidecar.py +3 -1
  124. package/sidecar/talaria_sidecar/__init__.py +1 -1
  125. package/sidecar/talaria_sidecar/home.py +59 -2
  126. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  127. package/sidecar/talaria_sidecar/methods/aux.py +19 -3
  128. package/sidecar/talaria_sidecar/methods/chat.py +129 -23
  129. package/sidecar/talaria_sidecar/methods/commands.py +46 -31
  130. package/sidecar/talaria_sidecar/methods/config.py +14 -10
  131. package/sidecar/talaria_sidecar/methods/oauth.py +439 -0
  132. package/sidecar/talaria_sidecar/methods/profiles.py +5 -4
  133. package/sidecar/talaria_sidecar/methods/providers.py +161 -1
  134. package/sidecar/talaria_sidecar/methods/runtime.py +2 -0
  135. package/sidecar/talaria_sidecar/methods/text.py +1 -20
  136. package/sidecar/talaria_sidecar/methods/usage.py +7 -1
  137. package/static/dist/FILES.txt +124 -120
  138. package/static/dist/assets/AppShell-ef3bhu32.js +1 -0
  139. package/static/dist/assets/Button-nx37f4u1.js +1 -0
  140. package/static/dist/assets/ChatPage-cdcpykcg.js +25 -0
  141. package/static/dist/assets/Dialog-mec42hvb.js +1 -0
  142. package/static/dist/assets/HubRoute-ecb65zpr.js +2 -0
  143. package/static/dist/assets/{Markdown-dy8t4dy7.js → Markdown-jqrx5es0.js} +125 -125
  144. package/static/dist/assets/RightPanel-d1lgvk4s.js +1 -0
  145. package/static/dist/assets/Select-h4ivl9qf.js +1 -0
  146. package/static/dist/assets/SessionListPanel-ewib046b.js +1 -0
  147. package/static/dist/assets/SettingsLayout-lx654cup.js +1 -0
  148. package/static/dist/assets/States-ehsb6lzo.js +1 -0
  149. package/static/dist/assets/Toaster-mch1pa3t.js +1 -0
  150. package/static/dist/assets/_app-m8d961ef.js +1 -0
  151. package/static/dist/assets/_app.ext._extensionId-mp33hbzi.js +1 -0
  152. package/static/dist/assets/_app.index-dk6zp7e0.js +1 -0
  153. package/static/dist/assets/_app.insights-jt2fk4i1.js +1 -0
  154. package/static/dist/assets/_app.kanban-if4pubeu.js +1 -0
  155. package/static/dist/assets/_app.logs-mfaw9m6g.js +1 -0
  156. package/static/dist/assets/_app.memory-h3eaarb6.js +1 -0
  157. package/static/dist/assets/_app.profiles-ienktovs.js +1 -0
  158. package/static/dist/assets/{_app.session._sessionId-gyoh5ylx.js → _app.session._sessionId-hh27wwu2.js} +1 -1
  159. package/static/dist/assets/_app.settings-dpoe0bzf.js +1 -0
  160. package/static/dist/assets/_app.settings._section-l468ynag.js +3 -0
  161. package/static/dist/assets/_app.skills-kcbncc8i.js +1 -0
  162. package/static/dist/assets/_app.tasks-occdrqhw.js +2 -0
  163. package/static/dist/assets/_app.todos-nnke2rcc.js +1 -0
  164. package/static/dist/assets/_app.workspaces-cdlatvwj.js +1 -0
  165. package/static/dist/assets/{abnfDiagram-VCTEODGH-cjqletra.js → abnfDiagram-VCTEODGH-oity9y4o.js} +1 -1
  166. package/static/dist/assets/architecture-7GRP2DOG-hj11333z.js +1 -0
  167. package/static/dist/assets/architectureDiagram-5GKGNRK7-ceaepnyr.js +36 -0
  168. package/static/dist/assets/{back-e4m6nsnm.js → back-eu64w89w.js} +1 -1
  169. package/static/dist/assets/{blockDiagram-I7D4REHJ-b195z021.js → blockDiagram-I7D4REHJ-n79st4pj.js} +1 -1
  170. package/static/dist/assets/{c4Diagram-7LVT6UL2-olu212k1.js → c4Diagram-7LVT6UL2-mi73akzk.js} +1 -1
  171. package/static/dist/assets/cancel-mzvkdjg8.js +1 -0
  172. package/static/dist/assets/channel-mbvmf2vv.js +1 -0
  173. package/static/dist/assets/{chunk-4HAMMTFA-b8o1nufs.js → chunk-4HAMMTFA-laat0g1q.js} +1 -1
  174. package/static/dist/assets/{chunk-75Z2AOVW-l1seddto.js → chunk-75Z2AOVW-0psy1tr2.js} +1 -1
  175. package/static/dist/assets/{chunk-DU6HZSFF-lo5zg15v.js → chunk-DU6HZSFF-lw8uox8x.js} +3 -3
  176. package/static/dist/assets/{chunk-F27PBJKO-dsdzrp16.js → chunk-F27PBJKO-erassib3.js} +1 -1
  177. package/static/dist/assets/{chunk-GMAD6QVW-f3fxqq8b.js → chunk-GMAD6QVW-fz7rx619.js} +1 -1
  178. package/static/dist/assets/{chunk-GVQU2GXP-h8egxtf6.js → chunk-GVQU2GXP-gdb0epb2.js} +1 -1
  179. package/static/dist/assets/{chunk-IMKFNOWR-iolwd8s0.js → chunk-IMKFNOWR-ea8t26qw.js} +1 -1
  180. package/static/dist/assets/{chunk-L3NEJ4N5-b88bbtwi.js → chunk-L3NEJ4N5-mb1gu9kz.js} +1 -1
  181. package/static/dist/assets/{chunk-OSK3NFVY-e3e7nr6s.js → chunk-OSK3NFVY-eovmccvy.js} +1 -1
  182. package/static/dist/assets/{chunk-P2QGCYS3-mdc46rt1.js → chunk-P2QGCYS3-bo55zjo5.js} +1 -1
  183. package/static/dist/assets/{chunk-POPQ4Y6H-kqlllrrm.js → chunk-POPQ4Y6H-ja7ohe4h.js} +1 -1
  184. package/static/dist/assets/{chunk-PWAF6VOD-he585k2x.js → chunk-PWAF6VOD-e30g9nus.js} +1 -1
  185. package/static/dist/assets/{chunk-SHT3W25Y-gh94y9a4.js → chunk-SHT3W25Y-gbb798f7.js} +3 -3
  186. package/static/dist/assets/{chunk-SVP7TREG-euxp9y7e.js → chunk-SVP7TREG-h2iw8urm.js} +1 -1
  187. package/static/dist/assets/{chunk-TICWLB2K-c8cxes1q.js → chunk-TICWLB2K-fd2cottz.js} +1 -1
  188. package/static/dist/assets/classDiagram-ZZMXUADV-o8r05k0n.js +1 -0
  189. package/static/dist/assets/classDiagram-v2-VYDZK3BY-o8r05k0n.js +1 -0
  190. package/static/dist/assets/{copied-kq0u3ht7.js → copied-h65fc3cx.js} +1 -1
  191. package/static/dist/assets/copy-e6977uwq.js +1 -0
  192. package/static/dist/assets/{cynefin-OW5HDTMX-lxd44ux4.js → cynefin-OW5HDTMX-lbltyl5j.js} +1 -1
  193. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-njpiwat8.js → cynefinDiagram-5FMLGOSQ-lxhsqq4a.js} +1 -1
  194. package/static/dist/assets/{dagre-GXQ25YYZ-nvgy6yju.js → dagre-GXQ25YYZ-gn4nnaln.js} +4 -4
  195. package/static/dist/assets/{diagram-S7CK7UJ4-g98nyv72.js → diagram-S7CK7UJ4-pa3ij6q7.js} +3 -3
  196. package/static/dist/assets/{diagram-UQ7AKVKN-ovgm6lvv.js → diagram-UQ7AKVKN-zbipyz57.js} +1 -1
  197. package/static/dist/assets/{diagram-VSXAHHWV-pb2bn2e4.js → diagram-VSXAHHWV-d1kbpzgf.js} +1 -1
  198. package/static/dist/assets/{diagram-VX7I27RA-6j4mvfqo.js → diagram-VX7I27RA-i3ty14xl.js} +1 -1
  199. package/static/dist/assets/{diagram-Z3DM3KII-dqmb982r.js → diagram-Z3DM3KII-m8xefqa7.js} +1 -1
  200. package/static/dist/assets/{ebnfDiagram-PWID7BFC-ozbpzb6d.js → ebnfDiagram-PWID7BFC-nz7wj05l.js} +1 -1
  201. package/static/dist/assets/endpoints-k7yf25m0.js +64 -0
  202. package/static/dist/assets/{erDiagram-RLTQ6QDP-hl7m8vua.js → erDiagram-RLTQ6QDP-fa41ib83.js} +1 -1
  203. package/static/dist/assets/eventmodeling-NTZA5JFV-kpbq09bx.js +1 -0
  204. package/static/dist/assets/flowDiagram-HODETNUW-lcz0kx14.js +1 -0
  205. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ompixqmh.js → ganttDiagram-EL5Y4UJY-n7zq01qu.js} +1 -1
  206. package/static/dist/assets/{gitGraph-4MIJSDKK-b6vxwp9q.js → gitGraph-4MIJSDKK-c89emb07.js} +1 -1
  207. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-k98f57on.js → gitGraphDiagram-WWUBYQGX-cz7cgr6w.js} +1 -1
  208. package/static/dist/assets/{highlighted-body-KPVGNVTW-cd3ff6ge.js → highlighted-body-KPVGNVTW-d9bapmni.js} +1 -1
  209. package/static/dist/assets/{index-nqed3oum.js → index-h00usru2.js} +3 -3
  210. package/static/dist/assets/index-n73ijkv2.css +2 -0
  211. package/static/dist/assets/{info-A6RAGUB7-hjcwjami.js → info-A6RAGUB7-ndddhhhh.js} +1 -1
  212. package/static/dist/assets/infoDiagram-27XIBGKW-ccxigcpf.js +2 -0
  213. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-bxak4mwu.js → ishikawaDiagram-5VMMS53U-le0z9vtc.js} +1 -1
  214. package/static/dist/assets/{journeyDiagram-3NMN7TZE-khmihclr.js → journeyDiagram-3NMN7TZE-ivsmb26o.js} +1 -1
  215. package/static/dist/assets/{kanban-definition-UXKFOSKX-h1r32wdx.js → kanban-definition-UXKFOSKX-h6n2awt0.js} +1 -1
  216. package/static/dist/assets/lazyRouteComponent-m1rtyk4t.js +1 -0
  217. package/static/dist/assets/{line-friyjof3.js → line-dmzlfe0u.js} +1 -1
  218. package/static/dist/assets/link-budeg5t9.js +1 -0
  219. package/static/dist/assets/login-kjhasvp8.js +1 -0
  220. package/static/dist/assets/mermaid-HWGCJPDP-jb5d211j.js +1 -0
  221. package/static/dist/assets/{mermaid-parser.core-f1gvpwrt.js → mermaid-parser.core-gwyd5bnq.js} +3 -3
  222. package/static/dist/assets/{mindmap-definition-YA3MSWOX-mt2oxwue.js → mindmap-definition-YA3MSWOX-l68gg5h7.js} +1 -1
  223. package/static/dist/assets/onboarding-cdfrk0pj.js +1 -0
  224. package/static/dist/assets/{packet-AYTQ26CC-jik4ixlf.js → packet-AYTQ26CC-gpv06065.js} +1 -1
  225. package/static/dist/assets/{pegDiagram-XKGWAZYB-erxlv6cc.js → pegDiagram-XKGWAZYB-e52vs23u.js} +1 -1
  226. package/static/dist/assets/{pie-WAS4IAKB-mnkoldfl.js → pie-WAS4IAKB-l6m7nz5s.js} +1 -1
  227. package/static/dist/assets/{pieDiagram-E7YTZNPT-i7lwrada.js → pieDiagram-E7YTZNPT-b4is36bq.js} +3 -3
  228. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-fr8ejdoy.js → quadrantDiagram-AXDQQJYC-m8vt2788.js} +1 -1
  229. package/static/dist/assets/{radar-RG4KPBEZ-otarjte2.js → radar-RG4KPBEZ-nxbrhiov.js} +1 -1
  230. package/static/dist/assets/{railroad-74A4TZTK-lw362oto.js → railroad-74A4TZTK-222mu9hi.js} +1 -1
  231. package/static/dist/assets/railroad-abnf-HS5TGJTU-kdvgl6x8.js +1 -0
  232. package/static/dist/assets/railroad-ebnf-LZEXJU2U-itiqslby.js +1 -0
  233. package/static/dist/assets/railroad-peg-WCYAUIDC-go7kov78.js +1 -0
  234. package/static/dist/assets/{railroadDiagram-O6MQD6OU-esyn75pw.js → railroadDiagram-O6MQD6OU-o8lldfzv.js} +1 -1
  235. package/static/dist/assets/{refresh-cw-k66keho6.js → refresh-cw-kbcje1uy.js} +1 -1
  236. package/static/dist/assets/{requirementDiagram-BXWQKSXE-cyhi5u29.js → requirementDiagram-BXWQKSXE-ovyq38kv.js} +1 -1
  237. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-jljpbo0h.js → sankeyDiagram-P5KCCOFB-ou7lznor.js} +1 -1
  238. package/static/dist/assets/{saved-do49si3v.js → saved-bazmcup9.js} +1 -1
  239. package/static/dist/assets/search-jen74z4u.js +1 -0
  240. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-fz0dhlu7.js → sequenceDiagram-WJ2MYXX4-ovckdzf2.js} +1 -1
  241. package/static/dist/assets/{settings_section_extensions_title-b7lojn0o.js → settings_section_extensions_title-ertwwhlx.js} +1 -1
  242. package/static/dist/assets/share._token-dan9hgvj.js +2 -0
  243. package/static/dist/assets/{sliders-horizontal-c4wya4dy.js → sliders-horizontal-f9cb3lh3.js} +1 -1
  244. package/static/dist/assets/{stateDiagram-D77RDMKH-kdnithdx.js → stateDiagram-D77RDMKH-fcrexkkv.js} +1 -1
  245. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-lcfraua2.js +1 -0
  246. package/static/dist/assets/{swimlanes-42K2YHIH-esd6gmz3.js → swimlanes-42K2YHIH-f7usu8fw.js} +1 -1
  247. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ogmanni0.js +8 -0
  248. package/static/dist/assets/{timeline-definition-24CTP7MA-g0qygccv.js → timeline-definition-24CTP7MA-gp1hvz9f.js} +1 -1
  249. package/static/dist/assets/todoStore-eqtid4r4.js +1 -0
  250. package/static/dist/assets/{treeView-Q6P3EWNA-ndchzkuu.js → treeView-Q6P3EWNA-gb9gf11u.js} +1 -1
  251. package/static/dist/assets/{treemap-WGGIJYW6-hwjxj0zz.js → treemap-WGGIJYW6-nuvfnvvi.js} +1 -1
  252. package/static/dist/assets/{untitled-mbr6tuhu.js → untitled-htelhc68.js} +1 -1
  253. package/static/dist/assets/useForm-h2t0u14e.js +1 -0
  254. package/static/dist/assets/useLocale-hcsb84pj.js +1 -0
  255. package/static/dist/assets/useNavigate-nwy70ztj.js +1 -0
  256. package/static/dist/assets/{useParams-lo9u33d1.js → useParams-dqd64jv7.js} +1 -1
  257. package/static/dist/assets/{useSearch-lq1mr090.js → useSearch-j30pvsy7.js} +1 -1
  258. package/static/dist/assets/{vennDiagram-4TSXK5OY-gubovd1r.js → vennDiagram-4TSXK5OY-fzchbzaj.js} +1 -1
  259. package/static/dist/assets/{wardley-WFR3VGLG-ir3om65j.js → wardley-WFR3VGLG-m47mhfbh.js} +1 -1
  260. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-ce0or1ie.js → wardleyDiagram-VM6X3IG4-odg6ct1z.js} +2 -2
  261. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-dexylcjg.js → xychartDiagram-S5SC5T6Z-mrhms5um.js} +1 -1
  262. package/static/dist/index.html +1 -1
  263. package/static/dist/sw.js +1 -1
  264. package/static/dist/assets/AppShell-orat1s43.js +0 -1
  265. package/static/dist/assets/ChatPage-ojhka3hy.js +0 -22
  266. package/static/dist/assets/HubRoute-gxgw1t33.js +0 -2
  267. package/static/dist/assets/RightPanel-c66ani1s.js +0 -1
  268. package/static/dist/assets/Select-fswf66up.js +0 -1
  269. package/static/dist/assets/SessionListPanel-goh5mtt8.js +0 -1
  270. package/static/dist/assets/SettingsLayout-n5d4wq4m.js +0 -1
  271. package/static/dist/assets/States-kxyu50at.js +0 -1
  272. package/static/dist/assets/Toaster-6zmbcqgg.js +0 -1
  273. package/static/dist/assets/_app-fowyruiu.js +0 -1
  274. package/static/dist/assets/_app.ext._extensionId-nvjyrwhy.js +0 -1
  275. package/static/dist/assets/_app.index-c9zv4y7w.js +0 -1
  276. package/static/dist/assets/_app.insights-jf12nofo.js +0 -1
  277. package/static/dist/assets/_app.kanban-lri5a4yu.js +0 -1
  278. package/static/dist/assets/_app.logs-md1p0zqo.js +0 -1
  279. package/static/dist/assets/_app.memory-bzvf2e28.js +0 -1
  280. package/static/dist/assets/_app.profiles-jpg7zmbn.js +0 -1
  281. package/static/dist/assets/_app.settings-mpuwlx4j.js +0 -1
  282. package/static/dist/assets/_app.settings._section-knz8rwoj.js +0 -1
  283. package/static/dist/assets/_app.skills-fwu1dkir.js +0 -1
  284. package/static/dist/assets/_app.tasks-h9gphtl3.js +0 -2
  285. package/static/dist/assets/_app.todos-hd1dnvrh.js +0 -1
  286. package/static/dist/assets/_app.workspaces-l8toyn5f.js +0 -1
  287. package/static/dist/assets/architecture-7GRP2DOG-cavnusaw.js +0 -1
  288. package/static/dist/assets/architectureDiagram-5GKGNRK7-k5a7v36n.js +0 -36
  289. package/static/dist/assets/channel-gozy3dp3.js +0 -1
  290. package/static/dist/assets/classDiagram-ZZMXUADV-n0vs2f2q.js +0 -1
  291. package/static/dist/assets/classDiagram-v2-VYDZK3BY-n0vs2f2q.js +0 -1
  292. package/static/dist/assets/endpoints-k8agnro8.js +0 -9
  293. package/static/dist/assets/eventmodeling-NTZA5JFV-j1y2ekec.js +0 -1
  294. package/static/dist/assets/flowDiagram-HODETNUW-g2cvcbm5.js +0 -1
  295. package/static/dist/assets/index-nfy0ezs6.css +0 -2
  296. package/static/dist/assets/infoDiagram-27XIBGKW-fm06rj36.js +0 -2
  297. package/static/dist/assets/lazyRouteComponent-b2eg0fws.js +0 -1
  298. package/static/dist/assets/link-m90ahx3n.js +0 -1
  299. package/static/dist/assets/login-bw7m2fzo.js +0 -1
  300. package/static/dist/assets/mermaid-HWGCJPDP-njwcl62n.js +0 -1
  301. package/static/dist/assets/onboarding-ret37j1e.js +0 -1
  302. package/static/dist/assets/railroad-abnf-HS5TGJTU-m741owar.js +0 -1
  303. package/static/dist/assets/railroad-ebnf-LZEXJU2U-noa4fbia.js +0 -1
  304. package/static/dist/assets/railroad-peg-WCYAUIDC-o4bpw4nb.js +0 -1
  305. package/static/dist/assets/search-j5ki6a9o.js +0 -1
  306. package/static/dist/assets/share._token-kg8w647b.js +0 -2
  307. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-k2wg2645.js +0 -1
  308. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-n5kvjtgr.js +0 -8
  309. package/static/dist/assets/tabBuild-bjmyvmn5.js +0 -56
  310. package/static/dist/assets/todoStore-kjk5l0sy.js +0 -1
  311. package/static/dist/assets/useForm-ocxu460f.js +0 -1
  312. package/static/dist/assets/useLocale-fzy5jhg6.js +0 -1
@@ -0,0 +1,439 @@
1
+ """``oauth.*``: the Agent's device-code sign-ins for Nous Portal, OpenAI Codex, xAI and MiniMax (TAL-398).
2
+
3
+ ``oauth.start`` asks the provider for a code and answers what the user has to see; a daemon thread then waits for the
4
+ approval and saves the credential with the Agent's own auth-store helpers under the flow's profile home, the steps its
5
+ dashboard and ``hermes auth add`` run. Only the lifecycle is ours: a flow belongs to the home that started it, a cancel
6
+ or an expiry means nothing is saved afterwards, and a new start for the same home and provider supersedes the old one.
7
+ Flows live in this process only.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+ import secrets
15
+ import threading
16
+ import time
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+ from typing import Any, Callable
21
+
22
+ from ..errors import InvalidParams, RpcError
23
+ from ..home import profile_home_param, scoped_home
24
+ from ..rpc import CallContext
25
+
26
+ log = logging.getLogger("talaria_sidecar.oauth")
27
+
28
+ _CODEX_ISSUER = "https://auth.openai.com"
29
+ _CODEX_EXPIRES_IN = 15 * 60 # OpenAI's device-code lifetime, as the Agent dashboard uses it
30
+ # An ended flow stays readable this long so the last poll still sees its ending.
31
+ _ENDED_TTL = 10 * 60
32
+ # A pending flow past its code's expiry by this much ends expired even if its provider call has not returned yet.
33
+ _EXPIRY_GRACE = 60
34
+
35
+ _DENIED = "Sign-in was declined."
36
+ _EXPIRED = "The sign-in code expired before it was approved."
37
+
38
+
39
+ class _Stop(Exception):
40
+ """Raised inside a cancelled flow's provider call so the worker unwinds without saving."""
41
+
42
+
43
+ @dataclass
44
+ class Flow:
45
+ flow_id: str
46
+ provider: str
47
+ home: Path
48
+ expires_at: float
49
+ interval: int
50
+ status: str = "pending"
51
+ error: str | None = None
52
+ ended_at: float | None = None
53
+ # The OAuth ``error`` code of the provider's last non-200 answer (``access_denied``, ``expired_token``).
54
+ oauth_error: str = ""
55
+ cancelled: threading.Event = field(default_factory=threading.Event)
56
+ # Serialises the cancel check with the credential save: a cancel cannot land between them.
57
+ lock: threading.Lock = field(default_factory=threading.Lock)
58
+
59
+ def end(self, status: str, error: str | None = None) -> None:
60
+ self.status, self.error, self.ended_at = status, error, time.time()
61
+
62
+ def view(self) -> dict:
63
+ return {"flow_id": self.flow_id, "provider": self.provider, "status": self.status, "error": self.error}
64
+
65
+
66
+ class _Watched:
67
+ """The HTTP client handed to an Agent poll helper: stops a cancelled flow and records the OAuth error code."""
68
+
69
+ def __init__(self, client: Any, flow: Flow):
70
+ self._client, self._flow = client, flow
71
+
72
+ def _guard(self) -> None:
73
+ if self._flow.cancelled.is_set():
74
+ raise _Stop()
75
+
76
+ def post(self, *args: Any, **kwargs: Any) -> Any:
77
+ self._guard()
78
+ response = self._client.post(*args, **kwargs)
79
+ if response.status_code != 200:
80
+ try:
81
+ payload = response.json()
82
+ except Exception: # noqa: BLE001 - a non-JSON error carries no OAuth code
83
+ payload = None
84
+ if isinstance(payload, dict) and isinstance(payload.get("error"), str):
85
+ self._flow.oauth_error = payload["error"]
86
+ return response
87
+
88
+ def send(self, *args: Any, **kwargs: Any) -> Any:
89
+ self._guard()
90
+ return self._client.send(*args, **kwargs)
91
+
92
+ def __getattr__(self, name: str) -> Any:
93
+ return getattr(self._client, name)
94
+
95
+
96
+ def _first_line(exc: BaseException) -> str:
97
+ text = (str(exc).strip().splitlines() or [""])[0].strip() or type(exc).__name__
98
+ return text[:300]
99
+
100
+
101
+ def _remaining(flow: Flow) -> int:
102
+ return max(1, int(flow.expires_at - time.time()))
103
+
104
+
105
+ def _now_z() -> str:
106
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
107
+
108
+
109
+ def _client(timeout: float, **kwargs: Any) -> Any:
110
+ import httpx
111
+
112
+ return httpx.Client(timeout=httpx.Timeout(timeout), headers={"Accept": "application/json"}, **kwargs)
113
+
114
+
115
+ # ── providers: (begin, wait, save) over the Agent's own steps ──────────
116
+ # begin() -> (display, state) runs under the flow's home; wait(flow, state) -> tokens polls; save(flow, state, tokens)
117
+ # runs under the home with the flow's lock held.
118
+
119
+
120
+ def _nous_begin() -> tuple[dict, dict]:
121
+ from hermes_cli import anon_auth
122
+ from hermes_cli.auth import PROVIDER_REGISTRY, _request_device_code
123
+
124
+ if anon_auth.guest_enabled() and anon_auth.has_guest():
125
+ # The free tier's sign-in moves its connectors to the account (``anon_auth.run_sign_in``); a plain device-code
126
+ # sign-in here would orphan them.
127
+ raise RpcError("This profile uses the Nous free tier. Sign in from Hermes with `hermes portal`.", condition="oauth_unsupported")
128
+ pconfig = PROVIDER_REGISTRY["nous"]
129
+ portal = (os.getenv("HERMES_PORTAL_BASE_URL") or os.getenv("NOUS_PORTAL_BASE_URL") or pconfig.portal_base_url).rstrip("/")
130
+ with _client(15.0) as client:
131
+ data = _request_device_code(client, portal, pconfig.client_id, pconfig.scope)
132
+ display = {"user_code": str(data["user_code"]), "verification_url": str(data["verification_uri_complete"]), "expires_in": int(data["expires_in"]), "interval": int(data["interval"])}
133
+ return display, {"portal": portal, "client_id": pconfig.client_id, "scope": pconfig.scope, "device_code": str(data["device_code"])}
134
+
135
+
136
+ def _nous_wait(flow: Flow, state: dict) -> dict:
137
+ from hermes_cli.auth import _poll_for_token
138
+
139
+ with _client(15.0) as client:
140
+ return _poll_for_token(client=_Watched(client, flow), portal_base_url=state["portal"], client_id=state["client_id"], device_code=state["device_code"],
141
+ expires_in=_remaining(flow), poll_interval=flow.interval)
142
+
143
+
144
+ def _nous_save(flow: Flow, state: dict, token_data: dict) -> None:
145
+ from hermes_cli import anon_auth
146
+ from hermes_cli.auth import persist_nous_credentials, refresh_nous_oauth_from_state
147
+
148
+ now = datetime.now(timezone.utc)
149
+ ttl = int(token_data.get("expires_in") or 0)
150
+ auth_state = {
151
+ "portal_base_url": state["portal"], "inference_base_url": token_data.get("inference_base_url"), "client_id": state["client_id"],
152
+ "scope": token_data.get("scope") or state["scope"], "token_type": token_data.get("token_type", "Bearer"),
153
+ "access_token": token_data["access_token"], "refresh_token": token_data.get("refresh_token"), "obtained_at": now.isoformat(),
154
+ "expires_at": datetime.fromtimestamp(now.timestamp() + ttl, tz=timezone.utc).isoformat() if ttl else None, "expires_in": ttl,
155
+ }
156
+ full_state = refresh_nous_oauth_from_state(auth_state, timeout_seconds=15.0, force_refresh=False)
157
+ persist_nous_credentials(full_state)
158
+ anon_auth.settle_after_upgrade(full_state)
159
+
160
+
161
+ def _codex_begin() -> tuple[dict, dict]:
162
+ from hermes_cli.auth import CODEX_OAUTH_CLIENT_ID
163
+ from hermes_cli.auth_codex import _codex_request_device_code
164
+
165
+ data = _codex_request_device_code(_CODEX_ISSUER, CODEX_OAUTH_CLIENT_ID)
166
+ display = {"user_code": str(data["user_code"]), "verification_url": f"{_CODEX_ISSUER}/codex/device", "expires_in": _CODEX_EXPIRES_IN, "interval": int(data["interval"])}
167
+ return display, {"device_auth_id": str(data["device_auth_id"]), "user_code": str(data["user_code"])}
168
+
169
+
170
+ def _codex_wait(flow: Flow, state: dict) -> dict:
171
+ """The Agent's Codex poll (``auth_codex._codex_poll_authorization_code``) cannot be stopped, so its loop runs here:
172
+ 403/404 is still pending, a transport error is retried a few times in a row, and the exchange is the Agent's."""
173
+ import httpx
174
+ from hermes_cli.auth import CODEX_OAUTH_CLIENT_ID
175
+ from hermes_cli.auth_codex import _codex_exchange_authorization_code, _codex_http_client
176
+
177
+ payload = {"device_auth_id": state["device_auth_id"], "user_code": state["user_code"]}
178
+ blips = 0
179
+ code_resp = None
180
+ with _codex_http_client(timeout=httpx.Timeout(15.0)) as client:
181
+ while code_resp is None:
182
+ if flow.cancelled.wait(flow.interval):
183
+ raise _Stop()
184
+ if time.time() >= flow.expires_at:
185
+ raise TimeoutError(_EXPIRED)
186
+ try:
187
+ response = client.post(f"{_CODEX_ISSUER}/api/accounts/deviceauth/token", json=payload, headers={"Content-Type": "application/json"})
188
+ except httpx.TransportError:
189
+ blips += 1
190
+ if blips >= 6:
191
+ raise
192
+ continue
193
+ blips = 0
194
+ if response.status_code == 200:
195
+ code_resp = response.json()
196
+ elif response.status_code not in (403, 404):
197
+ raise RuntimeError(f"OpenAI device sign-in returned HTTP {response.status_code}.")
198
+ if flow.cancelled.is_set():
199
+ raise _Stop()
200
+ tokens = _codex_exchange_authorization_code(_CODEX_ISSUER, CODEX_OAUTH_CLIENT_ID, code_resp)
201
+ return {"access_token": tokens.get("access_token", ""), "refresh_token": tokens.get("refresh_token", "")}
202
+
203
+
204
+ def _codex_save(flow: Flow, state: dict, tokens: dict) -> None:
205
+ from hermes_cli.auth import _save_codex_tokens
206
+
207
+ _save_codex_tokens(tokens, _now_z())
208
+
209
+
210
+ def _xai_begin() -> tuple[dict, dict]:
211
+ from hermes_cli.auth import _xai_oauth_discovery, _xai_oauth_request_device_code
212
+
213
+ discovery = _xai_oauth_discovery(20.0)
214
+ with _client(20.0) as client:
215
+ data = _xai_oauth_request_device_code(client)
216
+ display = {"user_code": str(data["user_code"]), "verification_url": str(data.get("verification_uri_complete") or data["verification_uri"]),
217
+ "expires_in": int(data["expires_in"]), "interval": int(data["interval"])}
218
+ return display, {"discovery": discovery, "device_code": str(data["device_code"])}
219
+
220
+
221
+ def _xai_wait(flow: Flow, state: dict) -> dict:
222
+ from hermes_cli.auth import _xai_oauth_poll_device_token
223
+
224
+ with _client(20.0) as client:
225
+ data = _xai_oauth_poll_device_token(_Watched(client, flow), token_endpoint=state["discovery"]["token_endpoint"], device_code=state["device_code"],
226
+ expires_in=_remaining(flow), poll_interval=flow.interval)
227
+ return {
228
+ "access_token": str(data.get("access_token", "") or "").strip(), "refresh_token": str(data.get("refresh_token", "") or "").strip(),
229
+ "id_token": str(data.get("id_token", "") or "").strip(), "expires_in": data.get("expires_in"),
230
+ "token_type": str(data.get("token_type") or "Bearer").strip() or "Bearer",
231
+ }
232
+
233
+
234
+ def _xai_save(flow: Flow, state: dict, tokens: dict) -> None:
235
+ from hermes_cli.auth import _save_xai_oauth_tokens, mark_provider_active_if_unset, unsuppress_credential_source
236
+
237
+ # As the Agent dashboard does: persist without taking over an active chat provider, become active only when none
238
+ # is, and re-enable the device-code source an earlier removal suppressed.
239
+ _save_xai_oauth_tokens(tokens, discovery=state["discovery"], auth_mode="oauth_device_code", set_active=False, last_refresh=_now_z())
240
+ mark_provider_active_if_unset("xai-oauth")
241
+ unsuppress_credential_source("xai-oauth", "device_code")
242
+
243
+
244
+ def _minimax_begin() -> tuple[dict, dict]:
245
+ from hermes_cli.auth import MINIMAX_OAUTH_CLIENT_ID, MINIMAX_OAUTH_GLOBAL_BASE, _minimax_pkce_pair, _minimax_request_user_code
246
+
247
+ verifier, challenge, csrf_state = _minimax_pkce_pair()
248
+ portal = (os.getenv("MINIMAX_PORTAL_BASE_URL") or MINIMAX_OAUTH_GLOBAL_BASE).rstrip("/")
249
+ with _client(15.0, follow_redirects=True) as client:
250
+ data = _minimax_request_user_code(client, portal_base_url=portal, client_id=MINIMAX_OAUTH_CLIENT_ID, code_challenge=challenge, state=csrf_state)
251
+ interval_ms = int(data["interval"]) if data.get("interval") is not None else None
252
+ # ``expired_in`` is a unix-ms deadline or a TTL in seconds (the Agent's own heuristic).
253
+ expired_in = int(data["expired_in"])
254
+ expires_in = max(0, int(expired_in / 1000.0 - time.time())) if expired_in > 1_000_000_000_000 else expired_in
255
+ display = {"user_code": str(data["user_code"]), "verification_url": str(data["verification_uri"]), "expires_in": expires_in, "interval": max(2, (interval_ms or 2000) // 1000)}
256
+ return display, {"portal": portal, "client_id": MINIMAX_OAUTH_CLIENT_ID, "verifier": verifier, "user_code": str(data["user_code"]), "expired_in": expired_in, "interval_ms": interval_ms}
257
+
258
+
259
+ def _minimax_wait(flow: Flow, state: dict) -> dict:
260
+ from hermes_cli.auth import _minimax_poll_token
261
+
262
+ with _client(15.0, follow_redirects=True) as client:
263
+ return _minimax_poll_token(_Watched(client, flow), portal_base_url=state["portal"], client_id=state["client_id"], user_code=state["user_code"],
264
+ code_verifier=state["verifier"], expired_in=state["expired_in"], interval_ms=state["interval_ms"])
265
+
266
+
267
+ def _minimax_save(flow: Flow, state: dict, token_data: dict) -> None:
268
+ from hermes_cli.auth import MINIMAX_OAUTH_GLOBAL_INFERENCE, MINIMAX_OAUTH_SCOPE, _minimax_resolve_token_expiry_unix, _minimax_save_auth_state
269
+
270
+ now = datetime.now(timezone.utc)
271
+ expires_at = _minimax_resolve_token_expiry_unix(int(token_data["expired_in"]), now=now)
272
+ _minimax_save_auth_state({
273
+ "provider": "minimax-oauth", "region": "global", "portal_base_url": state["portal"], "inference_base_url": MINIMAX_OAUTH_GLOBAL_INFERENCE,
274
+ "client_id": state["client_id"], "scope": MINIMAX_OAUTH_SCOPE, "token_type": token_data.get("token_type", "Bearer"),
275
+ "access_token": token_data["access_token"], "refresh_token": token_data["refresh_token"], "resource_url": token_data.get("resource_url"),
276
+ "obtained_at": now.isoformat(), "expires_at": datetime.fromtimestamp(expires_at, tz=timezone.utc).isoformat(), "expires_in": max(0, int(expires_at - now.timestamp())),
277
+ })
278
+
279
+
280
+ Provider = tuple[Callable[[], tuple[dict, dict]], Callable[[Flow, dict], dict], Callable[[Flow, dict, dict], None]]
281
+ PROVIDERS: dict[str, Provider] = {
282
+ "nous": (_nous_begin, _nous_wait, _nous_save),
283
+ "openai-codex": (_codex_begin, _codex_wait, _codex_save),
284
+ "xai-oauth": (_xai_begin, _xai_wait, _xai_save),
285
+ "minimax-oauth": (_minimax_begin, _minimax_wait, _minimax_save),
286
+ }
287
+
288
+ _FLOWS: dict[str, Flow] = {}
289
+ _FLOWS_LOCK = threading.Lock()
290
+ # The newest start per (home, provider): only it may publish a flow, whatever order the provider requests finish in.
291
+ _GENERATIONS: dict[tuple[str, str], int] = {}
292
+
293
+
294
+ def _ending(flow: Flow, exc: BaseException) -> tuple[str, str]:
295
+ """Classify why a provider call ended without tokens: declined, expired, or another failure with its reason."""
296
+ code = str(getattr(exc, "code", None) or getattr(exc, "oauth_error_code", None) or "")
297
+ if flow.oauth_error == "access_denied" or code in ("access_denied", "authorization_denied"):
298
+ return "denied", _DENIED
299
+ if flow.oauth_error == "expired_token" or code in ("expired_token", "device_code_timeout", "timeout") or isinstance(exc, TimeoutError) or time.time() >= flow.expires_at:
300
+ return "expired", _EXPIRED
301
+ return "error", _first_line(exc)
302
+
303
+
304
+ def _run(flow: Flow, wait: Callable[[Flow, dict], dict], save: Callable[[Flow, dict, dict], None], state: dict) -> None:
305
+ try:
306
+ tokens = wait(flow, state)
307
+ with flow.lock:
308
+ if flow.status != "pending": # cancelled or expired while the provider answered: nothing is saved
309
+ return
310
+ with scoped_home(flow.home):
311
+ save(flow, state, tokens)
312
+ flow.end("approved")
313
+ log.info("oauth: %s sign-in approved (flow=%s)", flow.provider, flow.flow_id[:6])
314
+ except _Stop:
315
+ pass
316
+ except Exception as exc: # noqa: BLE001 - the thread has no caller; the ending is the flow's status
317
+ with flow.lock:
318
+ if flow.status == "pending":
319
+ flow.end(*_ending(flow, exc))
320
+ log.info("oauth: %s sign-in ended %s (flow=%s)", flow.provider, flow.status, flow.flow_id[:6])
321
+
322
+
323
+ def _cancel(flow: Flow) -> None:
324
+ with flow.lock:
325
+ if flow.status == "pending":
326
+ flow.cancelled.set()
327
+ flow.end("cancelled")
328
+
329
+
330
+ def _home_key(home: Path) -> str:
331
+ try:
332
+ return str(home.resolve())
333
+ except OSError:
334
+ return str(home)
335
+
336
+
337
+ def _same_home(left: Path, right: Path) -> bool:
338
+ try:
339
+ return left.resolve() == right.resolve()
340
+ except OSError:
341
+ return False
342
+
343
+
344
+ def _gc(now: float) -> None:
345
+ with _FLOWS_LOCK:
346
+ for flow_id, flow in list(_FLOWS.items()):
347
+ if flow.ended_at is not None and now - flow.ended_at > _ENDED_TTL:
348
+ del _FLOWS[flow_id]
349
+
350
+
351
+ def _pending(home: Path, provider: str) -> list[Flow]:
352
+ with _FLOWS_LOCK:
353
+ return [f for f in _FLOWS.values() if f.provider == provider and f.status == "pending" and _same_home(f.home, home)]
354
+
355
+
356
+ def start(home: Path, provider: str, *, providers: dict[str, Provider] | None = None) -> dict:
357
+ steps = (providers or PROVIDERS).get(provider)
358
+ if steps is None:
359
+ raise InvalidParams(f"{provider} has no device-code sign-in")
360
+ begin, wait, save = steps
361
+ _gc(time.time())
362
+ key = (_home_key(home), provider)
363
+ with _FLOWS_LOCK:
364
+ generation = _GENERATIONS[key] = _GENERATIONS.get(key, 0) + 1
365
+ # A new start replaces the pending flow for this home and provider before the code request, so an approval of the
366
+ # old code that lands while the provider answers can no longer save.
367
+ for old in _pending(home, provider):
368
+ _cancel(old)
369
+ try:
370
+ with scoped_home(home):
371
+ display, state = begin()
372
+ except RpcError:
373
+ raise
374
+ except Exception as exc: # noqa: BLE001
375
+ log.debug("oauth: %s start failed", provider, exc_info=True)
376
+ raise RpcError(_first_line(exc), condition="oauth_failed") from exc
377
+ if not str(display["verification_url"]).lower().startswith(("https://", "http://")):
378
+ # The browser opens this link: anything but a web address is refused before a flow exists.
379
+ raise RpcError("The provider answered an invalid sign-in link.", condition="oauth_failed")
380
+ flow = Flow(secrets.token_urlsafe(16), provider, home, time.time() + int(display["expires_in"]), max(1, int(display["interval"])))
381
+ with _FLOWS_LOCK:
382
+ if _GENERATIONS.get(key) != generation:
383
+ # A newer start for this home and provider began while this one waited on the provider: it owns the sign-in.
384
+ raise RpcError("A newer sign-in for this provider replaced this one.", condition="oauth_superseded")
385
+ superseded = [f for f in _FLOWS.values() if f.provider == provider and f.status == "pending" and _same_home(f.home, home)]
386
+ _FLOWS[flow.flow_id] = flow
387
+ for old in superseded:
388
+ _cancel(old)
389
+ threading.Thread(target=_run, args=(flow, wait, save, state), daemon=True, name=f"oauth-{flow.flow_id[:6]}").start()
390
+ return {"flow_id": flow.flow_id, "provider": provider, "status": "pending", **display}
391
+
392
+
393
+ def _flow(home: Path, flow_id: str) -> Flow:
394
+ with _FLOWS_LOCK:
395
+ flow = _FLOWS.get(flow_id)
396
+ if flow is None or not _same_home(flow.home, home):
397
+ raise RpcError("Unknown or expired sign-in.", condition="oauth_flow_not_found")
398
+ return flow
399
+
400
+
401
+ def poll(home: Path, flow_id: str) -> dict:
402
+ flow = _flow(home, flow_id)
403
+ if flow.status == "pending" and time.time() > flow.expires_at + _EXPIRY_GRACE:
404
+ # The provider call outlived its code: end it now, and a late approval can no longer save.
405
+ with flow.lock:
406
+ if flow.status == "pending":
407
+ flow.cancelled.set()
408
+ flow.end("expired", _EXPIRED)
409
+ return flow.view()
410
+
411
+
412
+ def cancel(home: Path, flow_id: str) -> dict:
413
+ flow = _flow(home, flow_id)
414
+ _cancel(flow)
415
+ return flow.view()
416
+
417
+
418
+ def _flow_id(params: dict) -> str:
419
+ flow_id = str(params.get("flow_id") or "").strip()
420
+ if not flow_id:
421
+ raise InvalidParams("flow_id is required")
422
+ return flow_id
423
+
424
+
425
+ def register(reg) -> None:
426
+ @reg.method("oauth.start")
427
+ def start_(ctx: CallContext, params: dict) -> dict:
428
+ provider = str(params.get("provider") or "").strip().lower()
429
+ if not provider:
430
+ raise InvalidParams("provider is required")
431
+ return start(profile_home_param(params), provider)
432
+
433
+ @reg.method("oauth.poll", requires_agent=False)
434
+ def poll_(ctx: CallContext, params: dict) -> dict:
435
+ return poll(profile_home_param(params), _flow_id(params))
436
+
437
+ @reg.method("oauth.cancel", requires_agent=False)
438
+ def cancel_(ctx: CallContext, params: dict) -> dict:
439
+ return cancel(profile_home_param(params), _flow_id(params))
@@ -18,6 +18,7 @@ from pathlib import Path
18
18
  from ..errors import InvalidParams, RpcError
19
19
  from ..home import profile_home_param
20
20
  from ..rpc import CallContext
21
+ from .config import yaml_parser
21
22
 
22
23
  log = logging.getLogger("talaria_sidecar.profiles")
23
24
  _PROFILE_ID_RE = re.compile(r"[a-z0-9][a-z0-9_-]{0,63}")
@@ -34,12 +35,12 @@ _TERMINAL_ENV = {
34
35
 
35
36
 
36
37
  def _yaml_load(path: Path):
37
- try:
38
- import yaml
39
- except Exception: # noqa: BLE001
38
+ """A YAML mapping, or None when the file is absent or malformed; a missing parser raises ``yaml_unavailable``."""
39
+ if not path.exists():
40
40
  return None
41
+ parser = yaml_parser()
41
42
  try:
42
- data = yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else None
43
+ data = parser.safe_load(path.read_text(encoding="utf-8"))
43
44
  except Exception: # noqa: BLE001
44
45
  return None
45
46
  return data if isinstance(data, dict) else None
@@ -11,6 +11,9 @@ from __future__ import annotations
11
11
 
12
12
  import dataclasses
13
13
  import logging
14
+ import sys
15
+ import threading
16
+ from pathlib import Path
14
17
  from typing import Any
15
18
 
16
19
  from ..errors import InvalidParams, RpcError
@@ -61,19 +64,176 @@ def auth_status(provider_id: str | None) -> dict:
61
64
  return _plain(status) if isinstance(status, dict) else {"logged_in": bool(status)}
62
65
 
63
66
 
67
+ def _module_dir(module: Any) -> Path | None:
68
+ try:
69
+ return Path(module.__file__).resolve().parent
70
+ except Exception: # noqa: BLE001 - namespace/builtin modules have no file
71
+ return None
72
+
73
+
74
+ # The Agent's module-name prefix for provider plugins it imports from a ``$HERMES_HOME`` (``providers._import_plugin_dir``).
75
+ _USER_PLUGIN_MODULE = "_hermes_user_provider_"
76
+ _LOAD_LOCK = threading.Lock()
77
+ _LOAD_ATTEMPTED: set[Path] = set()
78
+
79
+
80
+ def _user_plugin_profiles() -> dict[int, tuple[Any, Path]]:
81
+ """Each registered profile a ``$HERMES_HOME`` provider plugin module holds, with that module's directory."""
82
+ from providers import list_providers
83
+ from providers.base import ProviderProfile
84
+
85
+ registered = {id(p) for p in list_providers()}
86
+ out: dict[int, tuple[Any, Path]] = {}
87
+ for name, module in list(sys.modules.items()):
88
+ directory = _module_dir(module) if module is not None and name.startswith(_USER_PLUGIN_MODULE) else None
89
+ if directory is None:
90
+ continue
91
+ for value in list(vars(module).values()):
92
+ if isinstance(value, ProviderProfile) and id(value) in registered:
93
+ out.setdefault(id(value), (value, directory))
94
+ return out
95
+
96
+
97
+ def _load_plugin(root: Path) -> None:
98
+ """Load the scoped profile's installed plugin that the process-wide discovery (run under the launch profile) skipped.
99
+
100
+ Uses the Agent's own loader, as ``hermes plugins dev`` does, once per directory. A plugin never displaces a provider
101
+ that is already registered, so one profile's plugin cannot change what another profile's provider id runs.
102
+ """
103
+ import providers
104
+
105
+ with _LOAD_LOCK:
106
+ if root in _LOAD_ATTEMPTED:
107
+ return
108
+ _LOAD_ATTEMPTED.add(root)
109
+ before = {p.name: p for p in providers.list_providers()}
110
+ try:
111
+ providers._import_plugin_dir(root, "user")
112
+ except Exception: # noqa: BLE001 - a broken plugin reads as not loaded
113
+ log.debug("loading provider plugin %s failed", root, exc_info=True)
114
+ return
115
+ after = {p.name: p for p in providers.list_providers()}
116
+ for name, previous in before.items():
117
+ if after.get(name) is not previous:
118
+ providers.register_provider(previous)
119
+
120
+
121
+ def installed_plugin_profiles() -> list[tuple[str, Any]]:
122
+ """``(manifest name, profile | None)`` for each enabled model-provider plugin installed in the scoped home.
123
+
124
+ Bundled providers are built-ins, not plugins. The Agent keeps one provider registry per process, so a profile
125
+ belongs to this home only when the plugin module that registered it lives in one of this home's plugin
126
+ directories; a plugin the launch discovery skipped is loaded here. ``None`` means the plugin could not be loaded:
127
+ it failed to import, its provider id is taken by another plugin, or another profile's plugin has the same directory
128
+ name. ponytail: ownership is read from the plugin module's globals, so a plugin that registers an unbound inline
129
+ profile also reads as not loaded; track registrations in the Agent if one ever does.
130
+ """
131
+ try:
132
+ from hermes_cli.plugins_discovery import _get_disabled_plugins, _get_enabled_plugins, collect_directory_manifests, gate_manifest
133
+ except Exception as exc: # noqa: BLE001
134
+ raise RpcError(f"plugin providers unavailable: {exc}", condition="plugins_unavailable") from exc
135
+ disabled, enabled = _get_disabled_plugins(), _get_enabled_plugins()
136
+ roots = {
137
+ Path(m.path).resolve(): str(m.name)
138
+ for m in collect_directory_manifests()
139
+ if m.kind == "model-provider" and m.source == "user" and m.path and gate_manifest(m, disabled, enabled).enabled
140
+ }
141
+ if not roots:
142
+ return []
143
+
144
+ def owned() -> dict[Path, list]:
145
+ found: dict[Path, list] = {root: [] for root in roots}
146
+ for profile, directory in _user_plugin_profiles().values():
147
+ root = next((r for r in roots if directory == r or r in directory.parents), None)
148
+ if root is not None:
149
+ found[root].append(profile)
150
+ return found
151
+
152
+ found = owned()
153
+ missing = [root for root, profiles in found.items() if not profiles]
154
+ if missing:
155
+ for root in missing:
156
+ _load_plugin(root)
157
+ found = owned()
158
+ return [(name, p) for root, name in roots.items() for p in (found[root] or [None])]
159
+
160
+
161
+ def _other_profile_plugin(provider_id: str) -> bool:
162
+ """Whether ``provider_id`` is a plugin provider that another profile installed (the registry is process-wide)."""
163
+ from providers import get_provider_profile
164
+
165
+ profile = get_provider_profile(provider_id)
166
+ if profile is None or id(profile) not in _user_plugin_profiles():
167
+ return False
168
+ return not any(p is profile for _name, p in installed_plugin_profiles())
169
+
170
+
171
+ def _setup_state(profile: Any) -> str:
172
+ """The Agent's own setup verdict for a plugin provider, without spawning its CLI or reading its credentials."""
173
+ try:
174
+ status = auth_status(profile.name)
175
+ except RpcError:
176
+ return "unavailable"
177
+ if status.get("error"):
178
+ return "unavailable"
179
+ if profile.auth_type == "external_process":
180
+ return "ready" if status.get("configured") else "missing_cli"
181
+ return "ready" if status.get("logged_in") or status.get("configured") else "needs_setup"
182
+
183
+
184
+ def plugin_providers() -> list[dict]:
185
+ """Sanitized rows for this profile's enabled model-provider plugins: identity and setup state, no paths or secrets."""
186
+ rows = []
187
+ for manifest_name, profile in installed_plugin_profiles():
188
+ if profile is None:
189
+ rows.append({"name": manifest_name, "display_name": manifest_name, "auth_type": "", "setup": "not_loaded"})
190
+ continue
191
+ name = str(profile.name).strip().lower()
192
+ rows.append({"name": name, "display_name": str(profile.display_name or name).strip(), "auth_type": str(profile.auth_type), "setup": _setup_state(profile)})
193
+ return rows
194
+
195
+
196
+ def _plugin_catalog(provider_id: str) -> list[str]:
197
+ """Discovery for an installed plugin the Agent's catalog has no fetcher for: the profile's own listing, then its fallback.
198
+
199
+ API-key plugins already get both from ``provider_model_ids`` with their key, so only keyless auth types list here.
200
+ """
201
+ for _name, profile in installed_plugin_profiles():
202
+ if profile is None or str(profile.name).strip().lower() != provider_id.strip().lower():
203
+ continue
204
+ try:
205
+ live = profile.fetch_models(timeout=8.0) if profile.supports_model_listing and profile.auth_type != "api_key" else None
206
+ except Exception: # noqa: BLE001 - a broken plugin answers with its fallback catalog
207
+ log.debug("fetch_models(%r) failed", provider_id, exc_info=True)
208
+ live = None
209
+ return [str(m) for m in (live or profile.fallback_models or ()) if m]
210
+ return []
211
+
212
+
64
213
  def model_ids(provider_id: str, *, force_refresh: bool) -> list[str]:
65
214
  try:
66
215
  from hermes_cli.models import provider_model_ids
67
216
  except Exception as exc: # noqa: BLE001
68
217
  raise RpcError(f"model catalog unavailable: {exc}", condition="providers_unavailable") from exc
218
+ try:
219
+ if _other_profile_plugin(provider_id):
220
+ return []
221
+ except Exception: # noqa: BLE001 - no readable provider registry means no plugin to keep apart
222
+ log.debug("plugin ownership check for %r failed", provider_id, exc_info=True)
69
223
  try:
70
224
  ids = provider_model_ids(provider_id, force_refresh=force_refresh)
71
225
  except TypeError:
72
226
  ids = provider_model_ids(provider_id)
73
227
  except Exception: # noqa: BLE001
74
228
  log.debug("provider_model_ids(%r) failed", provider_id, exc_info=True)
229
+ ids = []
230
+ ids = [str(m) for m in (ids or []) if m]
231
+ if ids:
232
+ return ids
233
+ try:
234
+ return _plugin_catalog(provider_id)
235
+ except RpcError:
75
236
  return []
76
- return [str(m) for m in (ids or []) if m]
77
237
 
78
238
 
79
239
  def resolve_runtime(*, requested: str | None, api_key: str | None, base_url: str | None, target_model: str | None) -> dict:
@@ -7,6 +7,7 @@ import re
7
7
 
8
8
  from .. import SIDECAR_RPC_VERSION
9
9
  from ..errors import InvalidParams, RpcError
10
+ from ..home import edit_launch_env
10
11
  from ..rpc import CallContext
11
12
 
12
13
 
@@ -53,6 +54,7 @@ def register(registry) -> None:
53
54
  os.environ.pop(name, None)
54
55
  for name, value in to_set.items():
55
56
  os.environ[name] = value
57
+ edit_launch_env(to_set, to_unset)
56
58
  # Cached agents bound the credentials they were built with; none of them may outlive a credential change.
57
59
  from .chat import evict_all_agents
58
60