@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.
- package/_release.json +4 -4
- package/dist/api/automation-router.d.ts +21 -7
- package/dist/api/automation-router.js +3 -0
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +142 -6
- package/dist/api/chat-router.js +11 -5
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.js +44 -32
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.js +5 -58
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +4 -2
- package/dist/api/router.js +8 -8
- package/dist/api/router.js.map +1 -1
- package/dist/api/sessions-router.d.ts +714 -42
- package/dist/api/sessions-router.js +57 -23
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +153 -9
- package/dist/api/settings-router.js +61 -16
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.js +1 -1
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-router.d.ts +13 -1
- package/dist/api/tools-router.js +10 -8
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +1047 -67
- package/dist/auth/gate.js +8 -4
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/oidc.d.ts +6 -2
- package/dist/auth/oidc.js +16 -11
- package/dist/auth/oidc.js.map +1 -1
- package/dist/auth/store.d.ts +14 -2
- package/dist/auth/store.js +26 -1
- package/dist/auth/store.js.map +1 -1
- package/dist/cli/dotenv.d.ts +1 -1
- package/dist/cli/dotenv.js +36 -3
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/http/addresses.d.ts +6 -0
- package/dist/http/addresses.js +26 -0
- package/dist/http/addresses.js.map +1 -1
- package/dist/http/context.d.ts +3 -3
- package/dist/links.d.ts +26 -0
- package/dist/links.js +60 -0
- package/dist/links.js.map +1 -0
- package/dist/onboarding.js +26 -7
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.js +1 -1
- package/dist/profiles/profiles.js.map +1 -1
- package/dist/providers/catalog.d.ts +74 -1
- package/dist/providers/catalog.js +248 -42
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/tables.d.ts +3 -1
- package/dist/providers/tables.js +9 -4
- package/dist/providers/tables.js.map +1 -1
- package/dist/redact.js +493 -13
- package/dist/redact.js.map +1 -1
- package/dist/runtime.js +52 -28
- package/dist/runtime.js.map +1 -1
- package/dist/sessions/anchor.d.ts +22 -3
- package/dist/sessions/anchor.js +87 -42
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-tasks.d.ts +4 -1
- package/dist/sessions/background-tasks.js +7 -4
- package/dist/sessions/background-tasks.js.map +1 -1
- package/dist/sessions/completions.js +1 -1
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/list.d.ts +8 -4
- package/dist/sessions/list.js +24 -17
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +70 -7
- package/dist/sessions/merge.js +331 -23
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/pending.d.ts +5 -0
- package/dist/sessions/pending.js +30 -10
- package/dist/sessions/pending.js.map +1 -1
- package/dist/sessions/service.d.ts +57 -3
- package/dist/sessions/service.js +327 -56
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +6 -1
- package/dist/sessions/session.js +6 -0
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/shares.js +11 -2
- package/dist/sessions/shares.js.map +1 -1
- package/dist/sessions/store.js +1 -1
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/turn.d.ts +19 -1
- package/dist/sessions/turn.js +218 -113
- package/dist/sessions/turn.js.map +1 -1
- package/dist/settings.js +9 -0
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/fake.js +2 -0
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/tools/commands.d.ts +15 -0
- package/dist/tools/commands.js +52 -0
- package/dist/tools/commands.js.map +1 -0
- package/dist/tools/crons.d.ts +4 -1
- package/dist/tools/crons.js +58 -11
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/mcp.js +5 -1
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +1 -1
- package/dist/tools/update-notifications.js +1 -1
- package/dist/tools/update-notifications.js.map +1 -1
- package/dist/tools/updates.d.ts +8 -3
- package/dist/tools/updates.js +133 -34
- package/dist/tools/updates.js.map +1 -1
- package/dist/workspace/git.d.ts +57 -10
- package/dist/workspace/git.js +302 -195
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media-refs.d.ts +13 -0
- package/dist/workspace/media-refs.js +165 -0
- package/dist/workspace/media-refs.js.map +1 -0
- package/dist/workspace/media.d.ts +19 -0
- package/dist/workspace/media.js +61 -2
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/workspaces.d.ts +7 -1
- package/dist/workspace/workspaces.js +30 -21
- package/dist/workspace/workspaces.js.map +1 -1
- package/dist/workspace/worktrees.d.ts +2 -4
- package/dist/workspace/worktrees.js +28 -33
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +4 -2
- package/sidecar/scripts/replay_sidecar.py +3 -1
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/home.py +59 -2
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +19 -3
- package/sidecar/talaria_sidecar/methods/chat.py +129 -23
- package/sidecar/talaria_sidecar/methods/commands.py +46 -31
- package/sidecar/talaria_sidecar/methods/config.py +14 -10
- package/sidecar/talaria_sidecar/methods/oauth.py +439 -0
- package/sidecar/talaria_sidecar/methods/profiles.py +5 -4
- package/sidecar/talaria_sidecar/methods/providers.py +161 -1
- package/sidecar/talaria_sidecar/methods/runtime.py +2 -0
- package/sidecar/talaria_sidecar/methods/text.py +1 -20
- package/sidecar/talaria_sidecar/methods/usage.py +7 -1
- package/static/dist/FILES.txt +124 -120
- package/static/dist/assets/AppShell-ef3bhu32.js +1 -0
- package/static/dist/assets/Button-nx37f4u1.js +1 -0
- package/static/dist/assets/ChatPage-cdcpykcg.js +25 -0
- package/static/dist/assets/Dialog-mec42hvb.js +1 -0
- package/static/dist/assets/HubRoute-ecb65zpr.js +2 -0
- package/static/dist/assets/{Markdown-dy8t4dy7.js → Markdown-jqrx5es0.js} +125 -125
- package/static/dist/assets/RightPanel-d1lgvk4s.js +1 -0
- package/static/dist/assets/Select-h4ivl9qf.js +1 -0
- package/static/dist/assets/SessionListPanel-ewib046b.js +1 -0
- package/static/dist/assets/SettingsLayout-lx654cup.js +1 -0
- package/static/dist/assets/States-ehsb6lzo.js +1 -0
- package/static/dist/assets/Toaster-mch1pa3t.js +1 -0
- package/static/dist/assets/_app-m8d961ef.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-mp33hbzi.js +1 -0
- package/static/dist/assets/_app.index-dk6zp7e0.js +1 -0
- package/static/dist/assets/_app.insights-jt2fk4i1.js +1 -0
- package/static/dist/assets/_app.kanban-if4pubeu.js +1 -0
- package/static/dist/assets/_app.logs-mfaw9m6g.js +1 -0
- package/static/dist/assets/_app.memory-h3eaarb6.js +1 -0
- package/static/dist/assets/_app.profiles-ienktovs.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-gyoh5ylx.js → _app.session._sessionId-hh27wwu2.js} +1 -1
- package/static/dist/assets/_app.settings-dpoe0bzf.js +1 -0
- package/static/dist/assets/_app.settings._section-l468ynag.js +3 -0
- package/static/dist/assets/_app.skills-kcbncc8i.js +1 -0
- package/static/dist/assets/_app.tasks-occdrqhw.js +2 -0
- package/static/dist/assets/_app.todos-nnke2rcc.js +1 -0
- package/static/dist/assets/_app.workspaces-cdlatvwj.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-cjqletra.js → abnfDiagram-VCTEODGH-oity9y4o.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-hj11333z.js +1 -0
- package/static/dist/assets/architectureDiagram-5GKGNRK7-ceaepnyr.js +36 -0
- package/static/dist/assets/{back-e4m6nsnm.js → back-eu64w89w.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-b195z021.js → blockDiagram-I7D4REHJ-n79st4pj.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-olu212k1.js → c4Diagram-7LVT6UL2-mi73akzk.js} +1 -1
- package/static/dist/assets/cancel-mzvkdjg8.js +1 -0
- package/static/dist/assets/channel-mbvmf2vv.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-b8o1nufs.js → chunk-4HAMMTFA-laat0g1q.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-l1seddto.js → chunk-75Z2AOVW-0psy1tr2.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-lo5zg15v.js → chunk-DU6HZSFF-lw8uox8x.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-dsdzrp16.js → chunk-F27PBJKO-erassib3.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-f3fxqq8b.js → chunk-GMAD6QVW-fz7rx619.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-h8egxtf6.js → chunk-GVQU2GXP-gdb0epb2.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-iolwd8s0.js → chunk-IMKFNOWR-ea8t26qw.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-b88bbtwi.js → chunk-L3NEJ4N5-mb1gu9kz.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-e3e7nr6s.js → chunk-OSK3NFVY-eovmccvy.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-mdc46rt1.js → chunk-P2QGCYS3-bo55zjo5.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-kqlllrrm.js → chunk-POPQ4Y6H-ja7ohe4h.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-he585k2x.js → chunk-PWAF6VOD-e30g9nus.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-gh94y9a4.js → chunk-SHT3W25Y-gbb798f7.js} +3 -3
- package/static/dist/assets/{chunk-SVP7TREG-euxp9y7e.js → chunk-SVP7TREG-h2iw8urm.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-c8cxes1q.js → chunk-TICWLB2K-fd2cottz.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-o8r05k0n.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-o8r05k0n.js +1 -0
- package/static/dist/assets/{copied-kq0u3ht7.js → copied-h65fc3cx.js} +1 -1
- package/static/dist/assets/copy-e6977uwq.js +1 -0
- package/static/dist/assets/{cynefin-OW5HDTMX-lxd44ux4.js → cynefin-OW5HDTMX-lbltyl5j.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-njpiwat8.js → cynefinDiagram-5FMLGOSQ-lxhsqq4a.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-nvgy6yju.js → dagre-GXQ25YYZ-gn4nnaln.js} +4 -4
- package/static/dist/assets/{diagram-S7CK7UJ4-g98nyv72.js → diagram-S7CK7UJ4-pa3ij6q7.js} +3 -3
- package/static/dist/assets/{diagram-UQ7AKVKN-ovgm6lvv.js → diagram-UQ7AKVKN-zbipyz57.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-pb2bn2e4.js → diagram-VSXAHHWV-d1kbpzgf.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-6j4mvfqo.js → diagram-VX7I27RA-i3ty14xl.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-dqmb982r.js → diagram-Z3DM3KII-m8xefqa7.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-ozbpzb6d.js → ebnfDiagram-PWID7BFC-nz7wj05l.js} +1 -1
- package/static/dist/assets/endpoints-k7yf25m0.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-hl7m8vua.js → erDiagram-RLTQ6QDP-fa41ib83.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-kpbq09bx.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-lcz0kx14.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ompixqmh.js → ganttDiagram-EL5Y4UJY-n7zq01qu.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-b6vxwp9q.js → gitGraph-4MIJSDKK-c89emb07.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-k98f57on.js → gitGraphDiagram-WWUBYQGX-cz7cgr6w.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-cd3ff6ge.js → highlighted-body-KPVGNVTW-d9bapmni.js} +1 -1
- package/static/dist/assets/{index-nqed3oum.js → index-h00usru2.js} +3 -3
- package/static/dist/assets/index-n73ijkv2.css +2 -0
- package/static/dist/assets/{info-A6RAGUB7-hjcwjami.js → info-A6RAGUB7-ndddhhhh.js} +1 -1
- package/static/dist/assets/infoDiagram-27XIBGKW-ccxigcpf.js +2 -0
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-bxak4mwu.js → ishikawaDiagram-5VMMS53U-le0z9vtc.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-khmihclr.js → journeyDiagram-3NMN7TZE-ivsmb26o.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-h1r32wdx.js → kanban-definition-UXKFOSKX-h6n2awt0.js} +1 -1
- package/static/dist/assets/lazyRouteComponent-m1rtyk4t.js +1 -0
- package/static/dist/assets/{line-friyjof3.js → line-dmzlfe0u.js} +1 -1
- package/static/dist/assets/link-budeg5t9.js +1 -0
- package/static/dist/assets/login-kjhasvp8.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-jb5d211j.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-f1gvpwrt.js → mermaid-parser.core-gwyd5bnq.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-mt2oxwue.js → mindmap-definition-YA3MSWOX-l68gg5h7.js} +1 -1
- package/static/dist/assets/onboarding-cdfrk0pj.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-jik4ixlf.js → packet-AYTQ26CC-gpv06065.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-erxlv6cc.js → pegDiagram-XKGWAZYB-e52vs23u.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-mnkoldfl.js → pie-WAS4IAKB-l6m7nz5s.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-i7lwrada.js → pieDiagram-E7YTZNPT-b4is36bq.js} +3 -3
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-fr8ejdoy.js → quadrantDiagram-AXDQQJYC-m8vt2788.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-otarjte2.js → radar-RG4KPBEZ-nxbrhiov.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-lw362oto.js → railroad-74A4TZTK-222mu9hi.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-kdvgl6x8.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-itiqslby.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-go7kov78.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-esyn75pw.js → railroadDiagram-O6MQD6OU-o8lldfzv.js} +1 -1
- package/static/dist/assets/{refresh-cw-k66keho6.js → refresh-cw-kbcje1uy.js} +1 -1
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-cyhi5u29.js → requirementDiagram-BXWQKSXE-ovyq38kv.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-jljpbo0h.js → sankeyDiagram-P5KCCOFB-ou7lznor.js} +1 -1
- package/static/dist/assets/{saved-do49si3v.js → saved-bazmcup9.js} +1 -1
- package/static/dist/assets/search-jen74z4u.js +1 -0
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-fz0dhlu7.js → sequenceDiagram-WJ2MYXX4-ovckdzf2.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-b7lojn0o.js → settings_section_extensions_title-ertwwhlx.js} +1 -1
- package/static/dist/assets/share._token-dan9hgvj.js +2 -0
- package/static/dist/assets/{sliders-horizontal-c4wya4dy.js → sliders-horizontal-f9cb3lh3.js} +1 -1
- package/static/dist/assets/{stateDiagram-D77RDMKH-kdnithdx.js → stateDiagram-D77RDMKH-fcrexkkv.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-lcfraua2.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-esd6gmz3.js → swimlanes-42K2YHIH-f7usu8fw.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ogmanni0.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-g0qygccv.js → timeline-definition-24CTP7MA-gp1hvz9f.js} +1 -1
- package/static/dist/assets/todoStore-eqtid4r4.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-ndchzkuu.js → treeView-Q6P3EWNA-gb9gf11u.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-hwjxj0zz.js → treemap-WGGIJYW6-nuvfnvvi.js} +1 -1
- package/static/dist/assets/{untitled-mbr6tuhu.js → untitled-htelhc68.js} +1 -1
- package/static/dist/assets/useForm-h2t0u14e.js +1 -0
- package/static/dist/assets/useLocale-hcsb84pj.js +1 -0
- package/static/dist/assets/useNavigate-nwy70ztj.js +1 -0
- package/static/dist/assets/{useParams-lo9u33d1.js → useParams-dqd64jv7.js} +1 -1
- package/static/dist/assets/{useSearch-lq1mr090.js → useSearch-j30pvsy7.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-gubovd1r.js → vennDiagram-4TSXK5OY-fzchbzaj.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-ir3om65j.js → wardley-WFR3VGLG-m47mhfbh.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-ce0or1ie.js → wardleyDiagram-VM6X3IG4-odg6ct1z.js} +2 -2
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-dexylcjg.js → xychartDiagram-S5SC5T6Z-mrhms5um.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +1 -1
- package/static/dist/assets/AppShell-orat1s43.js +0 -1
- package/static/dist/assets/ChatPage-ojhka3hy.js +0 -22
- package/static/dist/assets/HubRoute-gxgw1t33.js +0 -2
- package/static/dist/assets/RightPanel-c66ani1s.js +0 -1
- package/static/dist/assets/Select-fswf66up.js +0 -1
- package/static/dist/assets/SessionListPanel-goh5mtt8.js +0 -1
- package/static/dist/assets/SettingsLayout-n5d4wq4m.js +0 -1
- package/static/dist/assets/States-kxyu50at.js +0 -1
- package/static/dist/assets/Toaster-6zmbcqgg.js +0 -1
- package/static/dist/assets/_app-fowyruiu.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-nvjyrwhy.js +0 -1
- package/static/dist/assets/_app.index-c9zv4y7w.js +0 -1
- package/static/dist/assets/_app.insights-jf12nofo.js +0 -1
- package/static/dist/assets/_app.kanban-lri5a4yu.js +0 -1
- package/static/dist/assets/_app.logs-md1p0zqo.js +0 -1
- package/static/dist/assets/_app.memory-bzvf2e28.js +0 -1
- package/static/dist/assets/_app.profiles-jpg7zmbn.js +0 -1
- package/static/dist/assets/_app.settings-mpuwlx4j.js +0 -1
- package/static/dist/assets/_app.settings._section-knz8rwoj.js +0 -1
- package/static/dist/assets/_app.skills-fwu1dkir.js +0 -1
- package/static/dist/assets/_app.tasks-h9gphtl3.js +0 -2
- package/static/dist/assets/_app.todos-hd1dnvrh.js +0 -1
- package/static/dist/assets/_app.workspaces-l8toyn5f.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-cavnusaw.js +0 -1
- package/static/dist/assets/architectureDiagram-5GKGNRK7-k5a7v36n.js +0 -36
- package/static/dist/assets/channel-gozy3dp3.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-n0vs2f2q.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-n0vs2f2q.js +0 -1
- package/static/dist/assets/endpoints-k8agnro8.js +0 -9
- package/static/dist/assets/eventmodeling-NTZA5JFV-j1y2ekec.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-g2cvcbm5.js +0 -1
- package/static/dist/assets/index-nfy0ezs6.css +0 -2
- package/static/dist/assets/infoDiagram-27XIBGKW-fm06rj36.js +0 -2
- package/static/dist/assets/lazyRouteComponent-b2eg0fws.js +0 -1
- package/static/dist/assets/link-m90ahx3n.js +0 -1
- package/static/dist/assets/login-bw7m2fzo.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-njwcl62n.js +0 -1
- package/static/dist/assets/onboarding-ret37j1e.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-m741owar.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-noa4fbia.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-o4bpw4nb.js +0 -1
- package/static/dist/assets/search-j5ki6a9o.js +0 -1
- package/static/dist/assets/share._token-kg8w647b.js +0 -2
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-k2wg2645.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-n5kvjtgr.js +0 -8
- package/static/dist/assets/tabBuild-bjmyvmn5.js +0 -56
- package/static/dist/assets/todoStore-kjk5l0sy.js +0 -1
- package/static/dist/assets/useForm-ocxu460f.js +0 -1
- 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
|
-
|
|
38
|
-
|
|
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 =
|
|
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
|
|