machinaos 0.0.76 → 0.0.78

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 (393) hide show
  1. package/README.md +143 -107
  2. package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
  3. package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
  4. package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
  5. package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
  6. package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
  7. package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
  8. package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
  9. package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
  10. package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
  11. package/client/dist/assets/index-DQ0nwhec.js +257 -0
  12. package/client/dist/assets/index-DxmbVskS.css +1 -0
  13. package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
  14. package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
  15. package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
  16. package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
  17. package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
  18. package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
  19. package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
  20. package/client/dist/index.html +37 -3
  21. package/client/index.html +28 -1
  22. package/client/package.json +44 -40
  23. package/client/src/App.tsx +2 -0
  24. package/client/src/Dashboard.tsx +157 -45
  25. package/client/src/ParameterPanel.tsx +3 -5
  26. package/client/src/adapters/nodeSpecToDescription.ts +1 -0
  27. package/client/src/assets/icons/NodeIcon.tsx +32 -0
  28. package/client/src/assets/icons/index.ts +4 -0
  29. package/client/src/assets/icons/stripe.svg +1 -0
  30. package/client/src/assets/icons/themedGlyphs.ts +404 -0
  31. package/client/src/components/AIAgentNode.tsx +77 -53
  32. package/client/src/components/GenericNode.tsx +34 -52
  33. package/client/src/components/OutputPanel.tsx +64 -147
  34. package/client/src/components/ParameterRenderer.tsx +5 -3
  35. package/client/src/components/SkillEditorModal.tsx +9 -18
  36. package/client/src/components/SquareNode.tsx +97 -115
  37. package/client/src/components/StartNode.tsx +32 -42
  38. package/client/src/components/SvgFilterDefs.tsx +54 -0
  39. package/client/src/components/TeamMonitorNode.tsx +12 -14
  40. package/client/src/components/ToolkitNode.tsx +35 -60
  41. package/client/src/components/TriggerNode.tsx +43 -77
  42. package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
  43. package/client/src/components/credentials/CredentialsModal.tsx +98 -30
  44. package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
  45. package/client/src/components/credentials/catalogueAdapter.ts +17 -1
  46. package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
  47. package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
  48. package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
  49. package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
  50. package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
  51. package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
  52. package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
  53. package/client/src/components/credentials/types.ts +12 -2
  54. package/client/src/components/credentials/useCredentialPanel.ts +43 -19
  55. package/client/src/components/icons/AIProviderIcons.tsx +16 -0
  56. package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
  57. package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
  58. package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
  59. package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
  60. package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
  61. package/client/src/components/parameterPanel/InputSection.tsx +9 -7
  62. package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
  63. package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
  64. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
  65. package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
  66. package/client/src/components/ui/AIResultModal.tsx +1 -1
  67. package/client/src/components/ui/CollapsibleSection.tsx +9 -5
  68. package/client/src/components/ui/CommandPalette.tsx +147 -0
  69. package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
  70. package/client/src/components/ui/ComponentItem.tsx +13 -7
  71. package/client/src/components/ui/ComponentPalette.tsx +24 -13
  72. package/client/src/components/ui/ConsolePanel.tsx +19 -11
  73. package/client/src/components/ui/DropCap.tsx +28 -0
  74. package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
  75. package/client/src/components/ui/InputNodesPanel.tsx +1 -1
  76. package/client/src/components/ui/Modal.tsx +38 -6
  77. package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
  78. package/client/src/components/ui/SettingsPanel.tsx +42 -13
  79. package/client/src/components/ui/StatusBar.tsx +108 -0
  80. package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
  81. package/client/src/components/ui/TopToolbar.tsx +42 -25
  82. package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
  83. package/client/src/components/ui/action-button.tsx +40 -15
  84. package/client/src/components/ui/button.tsx +24 -1
  85. package/client/src/components/ui/dropdown-menu.tsx +24 -2
  86. package/client/src/components/ui/input.tsx +19 -2
  87. package/client/src/components/ui/select.tsx +15 -0
  88. package/client/src/components/ui/textarea.tsx +15 -2
  89. package/client/src/contexts/AuthContext.tsx +148 -109
  90. package/client/src/contexts/ThemeContext.tsx +93 -17
  91. package/client/src/contexts/WebSocketContext.tsx +373 -206
  92. package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
  93. package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
  94. package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
  95. package/client/src/hooks/useAppTheme.ts +209 -7
  96. package/client/src/hooks/useAutoSkillEdges.ts +7 -2
  97. package/client/src/hooks/useCatalogueQuery.ts +67 -1
  98. package/client/src/hooks/useDragVariable.ts +1 -1
  99. package/client/src/hooks/useNodeAllowlist.ts +115 -8
  100. package/client/src/hooks/useOnboarding.ts +20 -8
  101. package/client/src/hooks/useParameterPanel.ts +2 -1
  102. package/client/src/hooks/useReactFlowNodes.ts +2 -1
  103. package/client/src/hooks/useSound.ts +185 -0
  104. package/client/src/hooks/useWorkflowManagement.ts +6 -8
  105. package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
  106. package/client/src/index.css +65 -3
  107. package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
  108. package/client/src/lib/aiModelProviders.ts +8 -0
  109. package/client/src/lib/connectionConfig.ts +107 -0
  110. package/client/src/lib/queryPersist.ts +13 -5
  111. package/client/src/lib/sound.ts +393 -0
  112. package/client/src/main.tsx +20 -0
  113. package/client/src/store/useAppStore.ts +26 -0
  114. package/client/src/styles/canvasAnimations.ts +37 -36
  115. package/client/src/styles/theme.ts +36 -20
  116. package/client/src/test/setup.ts +1 -0
  117. package/client/src/themes/atomic.css +253 -0
  118. package/client/src/themes/base.css +373 -0
  119. package/client/src/themes/cyber.css +890 -0
  120. package/client/src/themes/dark.css +70 -0
  121. package/client/src/themes/edo.css +246 -0
  122. package/client/src/themes/greek.css +293 -0
  123. package/client/src/themes/light.css +78 -0
  124. package/client/src/themes/plague.css +253 -0
  125. package/client/src/themes/renaissance.css +727 -0
  126. package/client/src/themes/rot.css +249 -0
  127. package/client/src/themes/steampunk.css +272 -0
  128. package/client/src/themes/surveillance.css +289 -0
  129. package/client/src/themes/wasteland.css +250 -0
  130. package/client/src/types/INodeProperties.ts +5 -0
  131. package/client/src/types/NodeTypes.ts +11 -1
  132. package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
  133. package/client/src/types/cloudEvents.ts +78 -0
  134. package/client/src/vite-env.d.ts +7 -0
  135. package/client/tsconfig.json +1 -1
  136. package/client/vite.config.js +62 -2
  137. package/install.ps1 +1 -1
  138. package/install.sh +1 -1
  139. package/machina/commands/build.py +51 -7
  140. package/machina/pyproject.toml +4 -0
  141. package/machina/supervisor.py +12 -2
  142. package/machina/tree.py +71 -21
  143. package/package.json +4 -4
  144. package/scripts/install.js +16 -1
  145. package/server/config/ai_cli_providers.json +54 -0
  146. package/server/config/credential_providers.json +109 -2
  147. package/server/config/llm_defaults.json +24 -0
  148. package/server/config/model_registry.json +338 -499
  149. package/server/config/node_allowlist.json +16 -1
  150. package/server/config/pricing.json +8 -0
  151. package/server/constants.py +38 -15
  152. package/server/core/container.py +2 -2
  153. package/server/core/credentials_database.py +35 -2
  154. package/server/core/logging.py +4 -3
  155. package/server/main.py +99 -13
  156. package/server/models/node_metadata.py +1 -0
  157. package/server/nodejs/package.json +8 -6
  158. package/server/nodejs/src/index.ts +22 -5
  159. package/server/nodes/README.md +31 -4
  160. package/server/nodes/agent/_inline.py +2 -0
  161. package/server/nodes/agent/_specialized.py +6 -3
  162. package/server/nodes/agent/ai_agent.py +13 -3
  163. package/server/nodes/agent/chat_agent.py +6 -3
  164. package/server/nodes/agent/claude_code_agent.py +287 -75
  165. package/server/nodes/agent/codex_agent.py +239 -0
  166. package/server/nodes/agent/deep_agent.py +3 -3
  167. package/server/nodes/agent/rlm_agent.py +3 -3
  168. package/server/nodes/android/__init__.py +31 -1
  169. package/server/nodes/android/_base.py +9 -5
  170. package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
  171. package/server/nodes/android/_handlers.py +154 -0
  172. package/server/nodes/android/_option_loaders.py +44 -0
  173. package/server/nodes/android/_refresh.py +127 -0
  174. package/server/{services/android → nodes/android/_relay}/client.py +4 -4
  175. package/server/{routers/android.py → nodes/android/_router.py} +27 -8
  176. package/server/nodes/browser/browser.py +2 -2
  177. package/server/nodes/code/_base.py +6 -2
  178. package/server/nodes/code/_claude_code.py +134 -0
  179. package/server/nodes/document/embedding_generator.py +3 -3
  180. package/server/nodes/document/http_scraper.py +3 -3
  181. package/server/nodes/document/vector_store.py +5 -5
  182. package/server/nodes/email/__init__.py +11 -1
  183. package/server/nodes/email/_filters.py +21 -0
  184. package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
  185. package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
  186. package/server/nodes/email/email_read.py +1 -1
  187. package/server/nodes/email/email_receive.py +54 -5
  188. package/server/nodes/email/email_send.py +1 -1
  189. package/server/nodes/filesystem/shell.py +24 -1
  190. package/server/nodes/google/__init__.py +55 -1
  191. package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
  192. package/server/nodes/google/_base.py +2 -2
  193. package/server/nodes/google/_credentials.py +5 -5
  194. package/server/nodes/google/_filters.py +25 -0
  195. package/server/nodes/google/_handlers.py +57 -0
  196. package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
  197. package/server/nodes/google/_option_loaders.py +107 -0
  198. package/server/nodes/google/_refresh.py +66 -0
  199. package/server/nodes/google/_router.py +131 -0
  200. package/server/nodes/google/gmail_receive.py +41 -4
  201. package/server/nodes/groups.py +1 -0
  202. package/server/nodes/location/_credentials.py +45 -1
  203. package/server/{services/maps.py → nodes/location/_service.py} +18 -3
  204. package/server/nodes/location/gmaps_create.py +4 -4
  205. package/server/nodes/location/gmaps_locations.py +4 -4
  206. package/server/nodes/location/gmaps_nearby_places.py +4 -4
  207. package/server/nodes/model/_base.py +8 -3
  208. package/server/nodes/model/_credentials.py +96 -8
  209. package/server/nodes/model/_local_validator.py +345 -0
  210. package/server/nodes/model/lmstudio_chat_model.py +23 -0
  211. package/server/nodes/model/ollama_chat_model.py +25 -0
  212. package/server/nodes/proxy/_usage.py +2 -2
  213. package/server/nodes/proxy/proxy_config.py +14 -14
  214. package/server/nodes/proxy/proxy_request.py +4 -4
  215. package/server/nodes/scraper/_credentials.py +29 -1
  216. package/server/nodes/scraper/apify_actor.py +9 -9
  217. package/server/nodes/scraper/crawlee_scraper.py +5 -5
  218. package/server/nodes/search/brave_search.py +4 -0
  219. package/server/nodes/search/perplexity_search.py +9 -0
  220. package/server/nodes/search/serper_search.py +3 -0
  221. package/server/nodes/skill/simple_memory.py +12 -0
  222. package/server/nodes/social/_base.py +2 -2
  223. package/server/nodes/stripe/__init__.py +46 -0
  224. package/server/nodes/stripe/_credentials.py +33 -0
  225. package/server/nodes/stripe/_handlers.py +270 -0
  226. package/server/nodes/stripe/_install.py +127 -0
  227. package/server/nodes/stripe/_source.py +174 -0
  228. package/server/nodes/stripe/stripe_action.py +81 -0
  229. package/server/nodes/stripe/stripe_receive.py +92 -0
  230. package/server/nodes/telegram/_credentials.py +52 -1
  231. package/server/nodes/telegram/_handlers.py +19 -18
  232. package/server/nodes/telegram/_service.py +134 -32
  233. package/server/nodes/telegram/telegram_send.py +5 -6
  234. package/server/nodes/text/file_handler.py +2 -2
  235. package/server/nodes/text/text_generator.py +2 -2
  236. package/server/nodes/tool/agent_builder.py +630 -0
  237. package/server/nodes/tool/task_manager.py +144 -2
  238. package/server/nodes/twitter/__init__.py +38 -1
  239. package/server/nodes/twitter/_base.py +7 -7
  240. package/server/nodes/twitter/_credentials.py +1 -1
  241. package/server/nodes/twitter/_filters.py +37 -0
  242. package/server/nodes/twitter/_handlers.py +77 -0
  243. package/server/nodes/twitter/_oauth.py +124 -0
  244. package/server/nodes/twitter/_refresh.py +78 -0
  245. package/server/nodes/twitter/_router.py +29 -0
  246. package/server/nodes/twitter/twitter_receive.py +4 -0
  247. package/server/nodes/visuals.json +64 -19
  248. package/server/nodes/whatsapp/__init__.py +45 -5
  249. package/server/nodes/whatsapp/_base.py +3 -3
  250. package/server/nodes/whatsapp/_filters.py +137 -0
  251. package/server/nodes/whatsapp/_handlers.py +167 -0
  252. package/server/nodes/whatsapp/_option_loaders.py +68 -0
  253. package/server/nodes/whatsapp/_refresh.py +62 -0
  254. package/server/nodes/whatsapp/_runtime.py +1 -1
  255. package/server/pyproject.toml +29 -7
  256. package/server/routers/schemas.py +2 -2
  257. package/server/routers/webhook.py +26 -9
  258. package/server/routers/websocket.py +149 -810
  259. package/server/services/ai.py +89 -8
  260. package/server/services/auth.py +220 -43
  261. package/server/services/claude_oauth.py +126 -100
  262. package/server/services/cli_agent/__init__.py +78 -0
  263. package/server/services/cli_agent/_handlers.py +237 -0
  264. package/server/services/cli_agent/config.py +112 -0
  265. package/server/services/cli_agent/factory.py +48 -0
  266. package/server/services/cli_agent/lockfile.py +141 -0
  267. package/server/services/cli_agent/mcp_server.py +482 -0
  268. package/server/services/cli_agent/protocol.py +173 -0
  269. package/server/services/cli_agent/providers/__init__.py +9 -0
  270. package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
  271. package/server/services/cli_agent/providers/google_gemini.py +80 -0
  272. package/server/services/cli_agent/providers/openai_codex.py +310 -0
  273. package/server/services/cli_agent/service.py +607 -0
  274. package/server/services/cli_agent/session.py +618 -0
  275. package/server/services/cli_agent/types.py +227 -0
  276. package/server/services/cli_agent/workflow_tools.py +233 -0
  277. package/server/services/credential_registry.py +26 -1
  278. package/server/services/deployment/manager.py +26 -145
  279. package/server/services/deployment/poll_registry.py +59 -0
  280. package/server/services/event_waiter.py +76 -246
  281. package/server/services/events/__init__.py +54 -0
  282. package/server/services/events/cli.py +78 -0
  283. package/server/services/events/daemon.py +163 -0
  284. package/server/services/events/envelope.py +281 -0
  285. package/server/services/events/lifecycle.py +99 -0
  286. package/server/services/events/oauth_lifecycle.py +534 -0
  287. package/server/services/events/polling.py +60 -0
  288. package/server/services/events/push.py +36 -0
  289. package/server/services/events/source.py +63 -0
  290. package/server/services/events/triggers.py +118 -0
  291. package/server/services/events/verifiers/__init__.py +25 -0
  292. package/server/services/events/verifiers/base.py +28 -0
  293. package/server/services/events/verifiers/github.py +25 -0
  294. package/server/services/events/verifiers/hmac_basic.py +32 -0
  295. package/server/services/events/verifiers/standard_webhooks.py +47 -0
  296. package/server/services/events/verifiers/stripe.py +42 -0
  297. package/server/services/events/webhook.py +105 -0
  298. package/server/services/handlers/tools.py +28 -186
  299. package/server/services/llm/config.py +7 -0
  300. package/server/services/llm/factory.py +8 -2
  301. package/server/services/memory/__init__.py +52 -0
  302. package/server/services/memory/jsonl.py +80 -0
  303. package/server/services/memory/markdown.py +65 -0
  304. package/server/services/memory/state.py +112 -0
  305. package/server/services/memory/vector_store.py +40 -0
  306. package/server/services/model_registry.py +76 -0
  307. package/server/services/node_allowlist.py +71 -15
  308. package/server/services/node_executor.py +2 -2
  309. package/server/services/node_output_schemas.py +21 -10
  310. package/server/services/node_spec.py +1 -1
  311. package/server/services/oauth_utils.py +1 -1
  312. package/server/services/plugin/__init__.py +2 -0
  313. package/server/services/plugin/base.py +44 -2
  314. package/server/services/plugin/credential.py +288 -1
  315. package/server/services/plugin/deps.py +105 -0
  316. package/server/services/plugin/edge_walker.py +12 -4
  317. package/server/services/plugin/oauth.py +381 -0
  318. package/server/services/plugin/polling.py +247 -0
  319. package/server/services/plugin/registry.py +145 -0
  320. package/server/services/plugin/singleton.py +65 -0
  321. package/server/services/plugin/ws.py +81 -0
  322. package/server/services/process_service.py +31 -2
  323. package/server/services/status_broadcaster.py +155 -238
  324. package/server/services/temporal/workflow.py +7 -7
  325. package/server/services/workflow.py +21 -3
  326. package/server/services/ws_handler_registry.py +111 -28
  327. package/server/skills/GUIDE.md +16 -1
  328. package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
  329. package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
  330. package/server/tests/credentials/test_auth_service.py +16 -9
  331. package/server/tests/credentials/test_credential_broadcasts.py +219 -0
  332. package/server/tests/credentials/test_google_oauth.py +6 -6
  333. package/server/tests/credentials/test_oauth_utils.py +1 -1
  334. package/server/tests/credentials/test_twitter_oauth.py +2 -2
  335. package/server/tests/credentials/test_websocket_handlers.py +44 -20
  336. package/server/tests/llm/test_factory.py +1 -0
  337. package/server/tests/llm/test_wiring.py +5 -1
  338. package/server/tests/nodes/_compat.py +24 -24
  339. package/server/tests/nodes/test_agent_builder.py +439 -0
  340. package/server/tests/nodes/test_ai_tools.py +18 -14
  341. package/server/tests/nodes/test_code_fs_process.py +17 -8
  342. package/server/tests/nodes/test_email.py +10 -9
  343. package/server/tests/nodes/test_google_workspace.py +2 -2
  344. package/server/tests/nodes/test_specialized_agents.py +100 -53
  345. package/server/tests/nodes/test_stripe_plugin.py +293 -0
  346. package/server/tests/nodes/test_telegram_social.py +4 -4
  347. package/server/tests/nodes/test_twitter.py +1 -1
  348. package/server/tests/nodes/test_web_automation.py +2 -2
  349. package/server/tests/nodes/test_whatsapp.py +9 -9
  350. package/server/tests/services/cli_agent/__init__.py +0 -0
  351. package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
  352. package/server/tests/services/cli_agent/test_providers.py +358 -0
  353. package/server/tests/services/cli_agent/test_service.py +298 -0
  354. package/server/tests/services/memory/__init__.py +0 -0
  355. package/server/tests/services/memory/test_jsonl.py +188 -0
  356. package/server/tests/services/test_events.py +333 -0
  357. package/server/tests/test_node_spec.py +56 -16
  358. package/server/tests/test_plugin_helpers.py +116 -0
  359. package/server/tests/test_plugin_self_containment.py +486 -0
  360. package/server/tests/test_status_broadcasts.py +425 -0
  361. package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
  362. package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
  363. package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
  364. package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
  365. package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
  366. package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
  367. package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
  368. package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
  369. package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
  370. package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
  371. package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
  372. package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
  373. package/client/dist/assets/index-D9tZfgvi.js +0 -363
  374. package/client/dist/assets/index-al7snTkG.css +0 -1
  375. package/client/src/components/credentials/providers.tsx +0 -177
  376. package/server/routers/google.py +0 -277
  377. package/server/routers/maps.py +0 -142
  378. package/server/routers/twitter.py +0 -365
  379. package/server/services/claude_code_service.py +0 -106
  380. package/server/services/memory.py +0 -159
  381. package/server/services/node_option_loaders/__init__.py +0 -77
  382. package/server/services/node_option_loaders/android_loaders.py +0 -55
  383. package/server/services/node_option_loaders/google_loaders.py +0 -97
  384. package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
  385. package/server/services/twitter_oauth.py +0 -411
  386. package/server/services/websocket_client.py +0 -29
  387. /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
  388. /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
  389. /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
  390. /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
  391. /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
  392. /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
  393. /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
@@ -41,6 +41,15 @@ import operator
41
41
  from langchain_core.messages import BaseMessage # eager: needed for AgentState type resolution
42
42
  import json
43
43
 
44
+ # Eager imports — both are tiny and read on every chat/agent execution
45
+ # path. ``openai`` is the canonical SDK whose typed exception hierarchy
46
+ # (``BadRequestError`` / ``AuthenticationError`` / ``RateLimitError`` / …)
47
+ # is the contract this module dispatches on; ``NodeUserError`` is the
48
+ # framework's "user-correctable, no-traceback" sentinel used to surface
49
+ # those typed errors cleanly through ``BaseNode.execute()``.
50
+ import openai
51
+ from services.plugin import NodeUserError
52
+
44
53
  if TYPE_CHECKING:
45
54
  from langchain_openai import ChatOpenAI # noqa: F401
46
55
  from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage # noqa: F401
@@ -456,9 +465,24 @@ def detect_provider_from_model(model: str) -> str:
456
465
 
457
466
 
458
467
  def is_model_valid_for_provider(model: str, provider: str) -> bool:
459
- """Check if model name matches the provider's patterns."""
460
- # OpenRouter is a proxy supporting all models (provider/model format) - always valid
461
- if provider == 'openrouter':
468
+ """Check if model name matches the provider's patterns.
469
+
470
+ Pattern-matching is meaningful for cloud providers — `gpt-*` is OpenAI,
471
+ `claude-*` is Anthropic, etc. — and the check guards against picking
472
+ a model from one provider's dropdown after switching to another.
473
+
474
+ For "open-world" providers (OpenRouter proxy, local Ollama / LM Studio
475
+ servers) the model namespace is whatever the user has installed:
476
+ `llama-3.2`, `qwen2.5-coder`, `phi-3-mini`, custom GGUF files, etc.
477
+ None of those contain the literal substrings `ollama` or `lmstudio`,
478
+ so applying the cloud-style filter produces a false negative on every
479
+ valid local model — the call site then "uses default" which for a
480
+ local provider is the SAME model name, emitting a confusing
481
+ "invalid ... using default: <same name>" log line. Treat all three
482
+ as always-valid; the local-server SDK will reject genuinely missing
483
+ models at request time with a clear 404.
484
+ """
485
+ if provider in ('openrouter', 'ollama', 'lmstudio'):
462
486
  return True
463
487
  config = get_provider_configs().get(provider)
464
488
  if not config:
@@ -1397,11 +1421,16 @@ class AIService:
1397
1421
  """Fetch available models from provider API.
1398
1422
 
1399
1423
  Native providers use their SDK-based fetch_models(). Groq/Cerebras
1400
- fall back to the LangChain httpx path.
1424
+ fall back to the LangChain httpx path. Local providers (ollama,
1425
+ lmstudio) honour the user's stored ``{provider}_proxy`` base URL
1426
+ the same way ``execute_chat`` does — without this passthrough the
1427
+ probe always hits the JSON default and never sees the user's
1428
+ actual installed models.
1401
1429
  """
1402
1430
  # --- Native SDK path ---
1403
1431
  if is_native_provider(provider):
1404
- native_provider = create_provider(provider, api_key)
1432
+ proxy_url = await self.auth.get_api_key(f"{provider}_proxy")
1433
+ native_provider = create_provider(provider, api_key, proxy_url=proxy_url)
1405
1434
  return await native_provider.fetch_models(api_key)
1406
1435
 
1407
1436
  # --- LangChain fallback (groq, cerebras) ---
@@ -1435,10 +1464,14 @@ class AIService:
1435
1464
  except Exception as e:
1436
1465
  logger.warning(f"[AI] Failed to fetch models from {provider} API: {e}")
1437
1466
 
1438
- # Fallback to curated list from llm_defaults.json
1467
+ # Fallback to curated list from llm_defaults.json. When the
1468
+ # provider has no default_model declared (intentional for local
1469
+ # servers like ollama/lmstudio), return an empty list so the
1470
+ # frontend dropdown shows a real "no models" empty state instead
1471
+ # of a placeholder name the user doesn't actually have.
1439
1472
  if curated:
1440
1473
  return curated
1441
- return [config.default_model]
1474
+ return [config.default_model] if config.default_model else []
1442
1475
 
1443
1476
  async def execute_chat(self, node_id: str, node_type: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
1444
1477
  """Execute AI chat model."""
@@ -1581,6 +1614,16 @@ class AIService:
1581
1614
  "execution_time": time.time() - start_time
1582
1615
  }
1583
1616
 
1617
+ except openai.OpenAIError as e:
1618
+ # Typed openai SDK exception — context overflow (BadRequestError),
1619
+ # bad key (AuthenticationError), missing model (NotFoundError),
1620
+ # server unreachable (APIConnectionError), etc. All
1621
+ # user-correctable. Propagate as NodeUserError so BaseNode.execute()
1622
+ # logs at WARN with no traceback.
1623
+ log_api_call(logger, provider if 'provider' in locals() else 'unknown',
1624
+ model if 'model' in locals() else 'unknown', "chat", False, error=str(e))
1625
+ raise NodeUserError(str(e)) from e
1626
+
1584
1627
  except Exception as e:
1585
1628
  logger.error("AI execution failed", node_id=node_id, error=str(e))
1586
1629
  log_api_call(logger, provider if 'provider' in locals() else 'unknown',
@@ -1927,6 +1970,18 @@ class AIService:
1927
1970
  stream_mode="values",
1928
1971
  ):
1929
1972
  final_state = snapshot
1973
+ # Per-step iteration broadcast (CloudEvents envelope
1974
+ # via broadcast_agent_progress -> wire key
1975
+ # `agent_progress`). The FE updates nodeStatusStore
1976
+ # so the AI agent body shows live "iteration / max".
1977
+ if broadcaster:
1978
+ iter_count = snapshot.get("iteration", 0) if isinstance(snapshot, dict) else 0
1979
+ await broadcaster.broadcast_agent_progress(
1980
+ node_id,
1981
+ workflow_id=workflow_id,
1982
+ iteration=iter_count,
1983
+ max_iterations=recursion_limit,
1984
+ )
1930
1985
  except GraphRecursionError:
1931
1986
  # Append a terminal AIMessage so downstream extraction (and
1932
1987
  # the post-loop _track_token_usage / compact_context call)
@@ -2060,6 +2115,13 @@ class AIService:
2060
2115
  "execution_time": time.time() - start_time
2061
2116
  }
2062
2117
 
2118
+ except openai.OpenAIError as e:
2119
+ # See execute_chat for the rationale — typed SDK errors are
2120
+ # user-correctable and re-raised as NodeUserError so BaseNode
2121
+ # logs at WARN without a traceback.
2122
+ log_api_call(logger, provider, model, "agent", False, error=str(e))
2123
+ raise NodeUserError(str(e)) from e
2124
+
2063
2125
  except Exception as e:
2064
2126
  logger.error("[LangGraph] AI agent execution failed", node_id=node_id, error=str(e))
2065
2127
  log_api_call(logger, provider, model, "agent", False, error=str(e))
@@ -2361,6 +2423,16 @@ class AIService:
2361
2423
  stream_mode="values",
2362
2424
  ):
2363
2425
  final_state = snapshot
2426
+ # Per-step iteration broadcast — see execute_agent
2427
+ # for the rationale.
2428
+ if broadcaster:
2429
+ iter_count = snapshot.get("iteration", 0) if isinstance(snapshot, dict) else 0
2430
+ await broadcaster.broadcast_agent_progress(
2431
+ node_id,
2432
+ workflow_id=workflow_id,
2433
+ iteration=iter_count,
2434
+ max_iterations=recursion_limit,
2435
+ )
2364
2436
  except GraphRecursionError:
2365
2437
  msgs = list((final_state or {}).get("messages") or [])
2366
2438
  msgs.append(AIMessage(content=(
@@ -2516,6 +2588,11 @@ class AIService:
2516
2588
  "execution_time": time.time() - start_time
2517
2589
  }
2518
2590
 
2591
+ except openai.OpenAIError as e:
2592
+ # Typed SDK error — see execute_chat for rationale.
2593
+ log_api_call(logger, provider, model, "chat_agent", False, error=str(e))
2594
+ raise NodeUserError(str(e)) from e
2595
+
2519
2596
  except Exception as e:
2520
2597
  logger.error("[ChatAgent] Execution failed", node_id=node_id, error=str(e))
2521
2598
  log_api_call(logger, provider, model, "chat_agent", False, error=str(e))
@@ -2623,6 +2700,8 @@ class AIService:
2623
2700
  # Email (Himalaya CLI, dual-purpose tools)
2624
2701
  'emailSend': 'email_send',
2625
2702
  'emailRead': 'email_read',
2703
+ # Stripe (CLI pass-through, dual-purpose tool)
2704
+ 'stripeAction': 'stripe_action',
2626
2705
  }
2627
2706
  DEFAULT_TOOL_DESCRIPTIONS = {
2628
2707
  'calculatorTool': 'Perform mathematical calculations. Operations: add, subtract, multiply, divide, power, sqrt, mod, abs',
@@ -2703,6 +2782,8 @@ class AIService:
2703
2782
  # Email (Himalaya CLI, dual-purpose tools)
2704
2783
  'emailSend': 'Send email via SMTP. Specify to, subject, body. Optional: cc, bcc, body_type (text/html).',
2705
2784
  'emailRead': 'Read and manage emails via IMAP. Operations: list (envelopes), search (query), read (message by ID), folders (list), move, delete, flag.',
2785
+ # Stripe (CLI pass-through, dual-purpose tool)
2786
+ 'stripeAction': "Run any Stripe CLI command and return the parsed JSON response. Pass a 'command' field exactly as you would type after 'stripe ' (e.g. 'customers create --email a@b.com', 'charges list --limit 10', 'payment_intents create --amount 2000 --currency usd', 'refunds create --payment-intent pi_xxx', 'trigger charge.succeeded'). Covers every Stripe resource: customers, charges, payment_intents, refunds, invoices, products, prices, subscriptions, payment_methods, setup_intents, transfers, payouts, plus 'trigger <event>' for synthetic test events.",
2706
2787
  }
2707
2788
 
2708
2789
  try:
@@ -2896,7 +2977,7 @@ class AIService:
2896
2977
  )
2897
2978
  return EmptyAndroidSchema
2898
2979
 
2899
- from services.android_service import SERVICE_ACTIONS
2980
+ from nodes.android._dispatcher import SERVICE_ACTIONS
2900
2981
 
2901
2982
  service_info = []
2902
2983
  for svc in connected_services:
@@ -1,8 +1,32 @@
1
- """API key management service with encrypted credentials database."""
1
+ """API key management service with encrypted credentials database.
2
+
3
+ Source of truth: ``CredentialsDatabase`` (encrypted SQLite at credentials.db).
4
+ Two derived in-memory caches exist for performance — both keyed by composite
5
+ strings, both invalidated atomically on every DB write/delete:
6
+
7
+ - ``_api_key_cache``: ``Dict[str, ApiKeyCacheEntry]`` keyed by ``{session}_{provider}``.
8
+ One entry carries decrypted key + models + stored_at. Replaces the previous
9
+ pair of ``_memory_cache`` (key) + ``_models_cache`` (models) which shared the
10
+ same key shape but had separate write/evict sites — invitation to drift.
11
+ - ``_oauth_cache``: ``Dict[str, Dict]`` keyed by ``{customer}_{provider}``.
12
+ Different namespace, different shape, different lifecycle — kept separate.
13
+ Per RFC 9700 (OAuth 2.0 BCP 2024) refresh tokens are NOT cached here;
14
+ ``get_oauth_refresh_token()`` reads from the DB on every call.
15
+
16
+ Async-safety: every read / write is await-free across the cache check, so
17
+ the GIL guarantees atomicity at the bytecode level — no locks needed. Lazy
18
+ DB fallback inside ``get_api_key`` introduces an await, but the only race
19
+ is "two concurrent reads both miss cache and both fetch from DB" — benign,
20
+ since both writes set the same value.
21
+
22
+ Neither cache has a TTL today; eviction is on explicit ``remove_*`` or
23
+ ``clear_cache()`` (logout). External revocation by an upstream provider is
24
+ not detected.
25
+ """
2
26
 
3
27
  import hashlib
4
- import json
5
- from pathlib import Path
28
+ import time
29
+ from dataclasses import dataclass, field
6
30
  from typing import Dict, Any, Optional, List
7
31
 
8
32
  from core.config import Settings
@@ -14,6 +38,20 @@ from core.logging import get_logger
14
38
  logger = get_logger(__name__)
15
39
 
16
40
 
41
+ @dataclass
42
+ class ApiKeyCacheEntry:
43
+ """One in-memory cache entry per ``{session}_{provider}``.
44
+
45
+ Carries the decrypted API key, the models list discovered at validation
46
+ time, and a monotonic timestamp for future TTL support. Single struct
47
+ so the two values can never drift — one write site, one evict site.
48
+ """
49
+
50
+ key: str
51
+ models: List[str] = field(default_factory=list)
52
+ stored_at: float = field(default_factory=time.monotonic)
53
+
54
+
17
55
  class AuthService:
18
56
  """API key management service using encrypted credentials database.
19
57
 
@@ -32,23 +70,51 @@ class AuthService:
32
70
  self.cache = cache # Kept for backward compatibility, not used for API keys
33
71
  self.database = database # Kept for backward compatibility
34
72
  self.settings = settings
35
- # Memory-only cache for decrypted API keys (never persisted to Redis/disk)
36
- self._memory_cache: Dict[str, str] = {}
37
- # Memory-only cache for models list
38
- self._models_cache: Dict[str, List[str]] = {}
39
- # Memory-only cache for OAuth tokens
73
+ # Memory-only cache for decrypted API keys + models (never persisted
74
+ # to Redis/disk). Single entry per provider; one write site, one
75
+ # evict site see module docstring.
76
+ self._api_key_cache: Dict[str, ApiKeyCacheEntry] = {}
77
+ # Memory-only cache for OAuth tokens. Different key namespace
78
+ # (``{customer}_{provider}``); kept separate from API keys.
79
+ # Per RFC 9700 the refresh_token is NOT cached here — only
80
+ # access_token + display fields. Refresh-token access goes via
81
+ # ``get_oauth_refresh_token()`` which reads from the DB.
40
82
  self._oauth_cache: Dict[str, Dict[str, Any]] = {}
41
83
 
42
84
  def hash_api_key(self, api_key: str) -> str:
43
85
  """Create hash for API key identification."""
44
86
  return hashlib.sha256(api_key.encode()).hexdigest()[:16]
45
87
 
88
+ def _bump_catalogue_version(self) -> None:
89
+ """Notify the credential registry that a credential has changed.
90
+
91
+ Bumps the registry's mutation counter so the next call to
92
+ ``CredentialRegistry.get_version()`` returns a new content hash.
93
+ The frontend's conditional ``since: <prior version>`` fetch then
94
+ receives a fresh catalogue with updated ``stored`` flags
95
+ instead of ``{unchanged: true}``. Without this bump, the version
96
+ is constant for the life of the process and the per-provider
97
+ ``stored`` flag stays stale on every connected client until the
98
+ process restarts.
99
+
100
+ Local import to avoid an import cycle at module load time.
101
+ Failures are swallowed (logged at WARNING) so a missing registry
102
+ never blocks a credential mutation from completing.
103
+ """
104
+ try:
105
+ from services.credential_registry import get_credential_registry
106
+
107
+ get_credential_registry().invalidate_version()
108
+ except Exception as e: # noqa: BLE001 — best-effort signal
109
+ logger.warning("Failed to bump credential catalogue version: %s", e)
110
+
46
111
  async def store_api_key(
47
112
  self,
48
113
  provider: str,
49
114
  api_key: str,
50
115
  models: List[str],
51
- session_id: str = "default"
116
+ session_id: str = "default",
117
+ model_params: Optional[Dict[str, Dict[str, Any]]] = None,
52
118
  ) -> bool:
53
119
  """Store API key with models in encrypted credentials database.
54
120
 
@@ -57,6 +123,11 @@ class AuthService:
57
123
  api_key: The API key to store (will be encrypted)
58
124
  models: List of available models for this key
59
125
  session_id: Session identifier for multi-user support
126
+ model_params: Optional per-model parameters (context_length etc.)
127
+ — used by local providers (Ollama, LM Studio) where the
128
+ context window depends on what the user has loaded.
129
+ Forwarded straight to the credentials DB so
130
+ ``model_registry`` can read real values at runtime.
60
131
 
61
132
  Returns:
62
133
  True if stored successfully, False otherwise
@@ -66,17 +137,28 @@ class AuthService:
66
137
 
67
138
  logger.info(f"Storing API key for provider: {provider}, session: {session_id}")
68
139
 
69
- # Store in encrypted credentials database
140
+ # 1. DB write first (canonical source).
70
141
  await self.credentials_db.save_api_key(
71
142
  provider=provider,
72
143
  api_key=api_key,
73
144
  models=models,
74
- session_id=session_id
145
+ session_id=session_id,
146
+ model_params=model_params,
147
+ )
148
+
149
+ # 2. Cache update only after DB write succeeds. One entry,
150
+ # not two parallel dicts — see ApiKeyCacheEntry docstring.
151
+ self._api_key_cache[cache_key] = ApiKeyCacheEntry(
152
+ key=api_key,
153
+ models=list(models),
75
154
  )
76
155
 
77
- # Cache decrypted key in memory only (for quick access)
78
- self._memory_cache[cache_key] = api_key
79
- self._models_cache[cache_key] = models
156
+ # 3. Bump the catalogue version so the frontend's conditional
157
+ # fetch (``since: <prior version>``) returns a fresh
158
+ # catalogue instead of ``{unchanged: true}`` — without
159
+ # this the per-provider ``stored`` flag stays stale on
160
+ # every connected client until the process restarts.
161
+ self._bump_catalogue_version()
80
162
 
81
163
  logger.info(f"Stored and cached API key for {provider}")
82
164
  return True
@@ -85,6 +167,26 @@ class AuthService:
85
167
  logger.error("Failed to store API key", provider=provider, error=str(e))
86
168
  return False
87
169
 
170
+ async def get_model_params(
171
+ self, provider: str, session_id: str = "default"
172
+ ) -> Dict[str, Dict[str, Any]]:
173
+ """Return per-model params (context_length etc.) for the provider.
174
+
175
+ Reads straight from the credentials DB — there's no in-memory
176
+ cache for these because they're consulted at most once per
177
+ chat-model execution and the DB read is cheap. Empty dict for
178
+ cloud providers (whose per-model params live in
179
+ ``model_registry.json``) and for any local provider that hasn't
180
+ been validated yet.
181
+ """
182
+ try:
183
+ return await self.credentials_db.get_api_key_model_params(
184
+ provider, session_id
185
+ )
186
+ except Exception as e:
187
+ logger.error("Failed to get model_params", provider=provider, error=str(e))
188
+ return {}
189
+
88
190
  async def get_api_key(self, provider: str, session_id: str = "default") -> Optional[str]:
89
191
  """Get decrypted API key.
90
192
 
@@ -100,15 +202,23 @@ class AuthService:
100
202
  try:
101
203
  cache_key = f"{session_id}_{provider}"
102
204
 
103
- # Check memory cache first (fastest, most secure)
104
- if cache_key in self._memory_cache:
105
- return self._memory_cache[cache_key]
205
+ # Check memory cache first (fastest, most secure).
206
+ entry = self._api_key_cache.get(cache_key)
207
+ if entry is not None:
208
+ return entry.key
106
209
 
107
- # Fallback to encrypted database
210
+ # Fallback to encrypted database. The lazy fetch also pulls
211
+ # the models list so we populate the cache entry fully — no
212
+ # second roundtrip on the next get_stored_models() call.
108
213
  api_key = await self.credentials_db.get_api_key(provider, session_id)
109
214
  if api_key:
110
- # Cache in memory for quick subsequent access
111
- self._memory_cache[cache_key] = api_key
215
+ models = await self.credentials_db.get_api_key_models(
216
+ provider, session_id
217
+ ) or []
218
+ self._api_key_cache[cache_key] = ApiKeyCacheEntry(
219
+ key=api_key,
220
+ models=models,
221
+ )
112
222
  return api_key
113
223
 
114
224
  return None
@@ -131,13 +241,21 @@ class AuthService:
131
241
  cache_key = f"{session_id}_{provider}"
132
242
 
133
243
  # Check memory cache first
134
- if cache_key in self._models_cache:
135
- return self._models_cache[cache_key]
244
+ entry = self._api_key_cache.get(cache_key)
245
+ if entry is not None:
246
+ return entry.models
136
247
 
137
- # Fallback to encrypted database
248
+ # Fallback to encrypted database. Pull the key alongside so
249
+ # the cache entry is populated fully — symmetric with
250
+ # get_api_key()'s lazy-populate path.
138
251
  models = await self.credentials_db.get_api_key_models(provider, session_id)
139
252
  if models:
140
- self._models_cache[cache_key] = models
253
+ api_key = await self.credentials_db.get_api_key(provider, session_id)
254
+ if api_key:
255
+ self._api_key_cache[cache_key] = ApiKeyCacheEntry(
256
+ key=api_key,
257
+ models=list(models),
258
+ )
141
259
  return models
142
260
 
143
261
  return []
@@ -159,13 +277,15 @@ class AuthService:
159
277
  try:
160
278
  cache_key = f"{session_id}_{provider}"
161
279
 
162
- # Remove from memory cache
163
- self._memory_cache.pop(cache_key, None)
164
- self._models_cache.pop(cache_key, None)
165
-
166
- # Remove from encrypted database
280
+ # 1. DB delete first (canonical source).
167
281
  await self.credentials_db.delete_api_key(provider, session_id)
168
282
 
283
+ # 2. Cache evict only after DB succeeds. One pop, not two.
284
+ self._api_key_cache.pop(cache_key, None)
285
+
286
+ # 3. Bump the catalogue version — same reason as store.
287
+ self._bump_catalogue_version()
288
+
169
289
  logger.info(f"Removed API key for {provider}")
170
290
  return True
171
291
 
@@ -192,8 +312,7 @@ class AuthService:
192
312
  Should be called on user logout to ensure decrypted keys
193
313
  don't persist in memory longer than necessary.
194
314
  """
195
- self._memory_cache.clear()
196
- self._models_cache.clear()
315
+ self._api_key_cache.clear()
197
316
  self._oauth_cache.clear()
198
317
  logger.debug("Cleared all credential memory caches")
199
318
 
@@ -226,6 +345,7 @@ class AuthService:
226
345
  try:
227
346
  cache_key = f"{customer_id}_{provider}"
228
347
 
348
+ # 1. DB write first (canonical source).
229
349
  await self.credentials_db.save_oauth_tokens(
230
350
  provider=provider,
231
351
  access_token=access_token,
@@ -236,15 +356,22 @@ class AuthService:
236
356
  customer_id=customer_id
237
357
  )
238
358
 
239
- # Cache in memory
359
+ # 2. Cache only the access token + display fields. Refresh
360
+ # tokens are long-lived secrets per RFC 9700 (OAuth 2.0
361
+ # BCP 2024) and are NOT cached in memory — readers go
362
+ # through ``get_oauth_refresh_token()`` which hits the DB.
240
363
  self._oauth_cache[cache_key] = {
241
364
  "access_token": access_token,
242
- "refresh_token": refresh_token,
243
365
  "email": email,
244
366
  "name": name,
245
- "scopes": scopes
367
+ "scopes": scopes,
246
368
  }
247
369
 
370
+ # 3. Bump the catalogue version so the frontend's conditional
371
+ # fetch returns fresh ``stored`` flags after this OAuth
372
+ # save (login).
373
+ self._bump_catalogue_version()
374
+
248
375
  logger.info(f"Stored OAuth tokens for {provider}")
249
376
  return True
250
377
 
@@ -257,27 +384,42 @@ class AuthService:
257
384
  provider: str,
258
385
  customer_id: str = "owner"
259
386
  ) -> Optional[Dict[str, Any]]:
260
- """Get OAuth tokens from cache or encrypted database.
387
+ """Get OAuth display tokens from cache or encrypted database.
388
+
389
+ Returns ``{access_token, email, name, scopes}`` only — the
390
+ refresh token is intentionally NOT included. Callers that need
391
+ the refresh token (token-refresh + revoke flows) must call
392
+ :meth:`get_oauth_refresh_token` explicitly. This split implements
393
+ RFC 9700 (OAuth 2.0 BCP 2024) §5.1 — refresh tokens are
394
+ long-lived secrets and must not live in process memory.
261
395
 
262
396
  Args:
263
397
  provider: OAuth provider name
264
398
  customer_id: Customer identifier
265
399
 
266
400
  Returns:
267
- Dict with access_token, refresh_token, email, name, scopes or None
401
+ Dict with ``access_token``, ``email``, ``name``, ``scopes`` or
402
+ ``None`` if no tokens are stored.
268
403
  """
269
404
  try:
270
405
  cache_key = f"{customer_id}_{provider}"
271
406
 
272
- # Check memory cache first
407
+ # Check memory cache first.
273
408
  if cache_key in self._oauth_cache:
274
409
  return self._oauth_cache[cache_key]
275
410
 
276
- # Fallback to encrypted database
411
+ # Fallback to encrypted database. Strip refresh_token before
412
+ # caching so the in-memory copy is short-lived-display-only.
277
413
  tokens = await self.credentials_db.get_oauth_tokens(provider, customer_id)
278
414
  if tokens:
279
- self._oauth_cache[cache_key] = tokens
280
- return tokens
415
+ display = {
416
+ "access_token": tokens.get("access_token"),
417
+ "email": tokens.get("email"),
418
+ "name": tokens.get("name"),
419
+ "scopes": tokens.get("scopes"),
420
+ }
421
+ self._oauth_cache[cache_key] = display
422
+ return display
281
423
 
282
424
  return None
283
425
 
@@ -285,6 +427,38 @@ class AuthService:
285
427
  logger.error("Failed to get OAuth tokens", provider=provider, error=str(e))
286
428
  return None
287
429
 
430
+ async def get_oauth_refresh_token(
431
+ self,
432
+ provider: str,
433
+ customer_id: str = "owner",
434
+ ) -> Optional[str]:
435
+ """Read the OAuth refresh token directly from the encrypted DB.
436
+
437
+ Per RFC 9700 (OAuth 2.0 BCP 2024) §5.1 the refresh token is a
438
+ long-lived bearer secret and must not be cached in process
439
+ memory. Every call decrypts from disk; this is acceptable
440
+ because refresh tokens are accessed rarely (only at access-token
441
+ renewal + on revoke / logout).
442
+
443
+ Args:
444
+ provider: OAuth provider name
445
+ customer_id: Customer identifier
446
+
447
+ Returns:
448
+ The decrypted refresh token, or ``None`` if no tokens are
449
+ stored for ``(provider, customer_id)``.
450
+ """
451
+ try:
452
+ tokens = await self.credentials_db.get_oauth_tokens(provider, customer_id)
453
+ if tokens:
454
+ return tokens.get("refresh_token")
455
+ return None
456
+ except Exception as e:
457
+ logger.error(
458
+ "Failed to get OAuth refresh token", provider=provider, error=str(e)
459
+ )
460
+ return None
461
+
288
462
  async def refresh_oauth_tokens_with_breaker(
289
463
  self,
290
464
  provider: str,
@@ -388,11 +562,14 @@ class AuthService:
388
562
  try:
389
563
  cache_key = f"{customer_id}_{provider}"
390
564
 
391
- # Remove from memory cache
565
+ # 1. DB delete first (canonical source).
566
+ await self.credentials_db.delete_oauth_tokens(provider, customer_id)
567
+
568
+ # 2. Cache evict after DB succeeds.
392
569
  self._oauth_cache.pop(cache_key, None)
393
570
 
394
- # Remove from encrypted database
395
- await self.credentials_db.delete_oauth_tokens(provider, customer_id)
571
+ # 3. Bump the catalogue version — same reason as store.
572
+ self._bump_catalogue_version()
396
573
 
397
574
  logger.info(f"Removed OAuth tokens for {provider}")
398
575
  return True