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
@@ -1,25 +1,30 @@
1
+ """Google Workspace OAuth 2.0 (composition pattern -- Wave 11.I, S.2).
2
+
3
+ Composes around ``google_auth_oauthlib.flow.Flow`` rather than
4
+ subclassing :class:`services.plugin.oauth.OAuth2PKCEClient`. The Flow
5
+ library handles the PKCE flow, scope handling, and offline-access
6
+ mechanics that Google's token endpoint expects -- hand-rolling those
7
+ would lose the ``OAUTHLIB_RELAX_TOKEN_SCOPE=1`` workaround for
8
+ oauthlib upstream issue #562.
9
+
10
+ What we DO share with the Twitter (subclass) path:
11
+
12
+ * :class:`OAuthStateStore` from :mod:`services.plugin.oauth` --
13
+ identical TTL + cleanup contract, deduplicates the state dict +
14
+ ``cleanup_expired_states`` helper that pre-S lived here too.
15
+ * The async method shape (``async exchange_code``,
16
+ ``async fetch_user_info``, ``async refresh_access_token``,
17
+ ``async revoke_token``) consumed by
18
+ :func:`services.events.oauth_lifecycle.make_oauth_lifecycle_handlers`
19
+ and :func:`make_oauth_callback_router`. Sync calls into Flow /
20
+ Credentials wrap through ``asyncio.to_thread``.
1
21
  """
2
- Google Workspace OAuth 2.0 using google-auth-oauthlib library.
3
-
4
- Unified OAuth for all Google services:
5
- - Gmail (send, search, read emails)
6
- - Google Calendar (create, list, update, delete events)
7
- - Google Drive (upload, download, list, share files)
8
- - Google Sheets (read, write, append data)
9
- - Google Tasks (create, list, complete tasks)
10
- - Google Contacts (create, list, search contacts)
11
-
12
- Two access modes:
13
- 1. Owner Mode - Your own Google account (Credentials Modal)
14
- 2. Customer Mode - Customer's Google account (database storage)
15
-
16
- API endpoints loaded from config/google_apis.json
17
- Docs: https://developers.google.com/identity/protocols/oauth2
18
- """
19
22
 
23
+ from __future__ import annotations
24
+
25
+ import asyncio
20
26
  import json
21
27
  import os
22
- import time
23
28
  import warnings
24
29
  from pathlib import Path
25
30
  from typing import Any, Dict, List, Optional
@@ -44,19 +49,28 @@ os.environ.setdefault("OAUTHLIB_RELAX_TOKEN_SCOPE", "1")
44
49
  # errors, deprecation notices) keeps surfacing.
45
50
  warnings.filterwarnings("ignore", message=r"Scope has changed.*")
46
51
 
52
+ import httpx
47
53
  from google.auth.transport.requests import Request
48
54
  from google.oauth2.credentials import Credentials
49
55
  from google_auth_oauthlib.flow import Flow
50
56
  from googleapiclient.discovery import build
51
57
 
52
58
  from core.logging import get_logger
59
+ from services.plugin.oauth import OAuthStateStore
53
60
 
54
61
  logger = get_logger(__name__)
55
62
 
56
- # Load Google API config from JSON
57
- _config_path = Path(__file__).parent.parent / "config" / "google_apis.json"
63
+
64
+ # ============================================================================
65
+ # Config loaders (consumed by _auth_helper, oauth_utils, _credentials, tests)
66
+ # ============================================================================
67
+
68
+ # Walk up two parents from server/nodes/google/_oauth.py -> server/, then
69
+ # into config/google_apis.json.
70
+ _config_path = Path(__file__).resolve().parents[2] / "config" / "google_apis.json"
58
71
  _google_config: Dict[str, Any] = {}
59
72
 
73
+
60
74
  def _load_config() -> Dict[str, Any]:
61
75
  """Load Google API config from JSON file."""
62
76
  global _google_config
@@ -70,33 +84,53 @@ def _load_config() -> Dict[str, Any]:
70
84
  _google_config = _get_default_config()
71
85
  return _google_config
72
86
 
87
+
73
88
  def _get_default_config() -> Dict[str, Any]:
74
- """Return default config if JSON fails to load."""
89
+ """Default config if JSON load fails."""
75
90
  return {
76
91
  "oauth": {
77
92
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
78
93
  "token_uri": "https://oauth2.googleapis.com/token",
79
94
  "revoke_uri": "https://oauth2.googleapis.com/revoke",
80
- "userinfo_uri": "https://www.googleapis.com/oauth2/v2/userinfo"
95
+ "userinfo_uri": "https://www.googleapis.com/oauth2/v2/userinfo",
81
96
  },
82
97
  "scopes": {
83
- "userinfo": ["openid", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"],
84
- "gmail": ["https://www.googleapis.com/auth/gmail.send", "https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/gmail.modify"],
85
- "calendar": ["https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/calendar.events"],
86
- "drive": ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file"],
98
+ "userinfo": [
99
+ "openid",
100
+ "https://www.googleapis.com/auth/userinfo.email",
101
+ "https://www.googleapis.com/auth/userinfo.profile",
102
+ ],
103
+ "gmail": [
104
+ "https://www.googleapis.com/auth/gmail.send",
105
+ "https://www.googleapis.com/auth/gmail.readonly",
106
+ "https://www.googleapis.com/auth/gmail.modify",
107
+ ],
108
+ "calendar": [
109
+ "https://www.googleapis.com/auth/calendar",
110
+ "https://www.googleapis.com/auth/calendar.events",
111
+ ],
112
+ "drive": [
113
+ "https://www.googleapis.com/auth/drive",
114
+ "https://www.googleapis.com/auth/drive.file",
115
+ ],
87
116
  "sheets": ["https://www.googleapis.com/auth/spreadsheets"],
88
117
  "tasks": ["https://www.googleapis.com/auth/tasks"],
89
- "contacts": ["https://www.googleapis.com/auth/contacts", "https://www.googleapis.com/auth/contacts.readonly"]
90
- }
118
+ "contacts": [
119
+ "https://www.googleapis.com/auth/contacts",
120
+ "https://www.googleapis.com/auth/contacts.readonly",
121
+ ],
122
+ },
91
123
  }
92
124
 
125
+
93
126
  def get_oauth_endpoints() -> Dict[str, str]:
94
- """Get OAuth endpoint URLs from config."""
127
+ """OAuth endpoint URLs from config."""
95
128
  config = _load_config()
96
129
  return config.get("oauth", _get_default_config()["oauth"])
97
130
 
131
+
98
132
  def get_callback_paths() -> Dict[str, str]:
99
- """Get OAuth callback paths from config."""
133
+ """OAuth callback paths from config."""
100
134
  config = _load_config()
101
135
  oauth = config.get("oauth", {})
102
136
  return {
@@ -106,63 +140,70 @@ def get_callback_paths() -> Dict[str, str]:
106
140
 
107
141
 
108
142
  def get_service_config(service: str) -> Dict[str, Any]:
109
- """Get service-specific config (base_url, version, etc.)."""
143
+ """Service-specific config (base_url, version, etc.)."""
110
144
  config = _load_config()
111
145
  return config.get("services", {}).get(service, {})
112
146
 
147
+
113
148
  def get_all_scopes() -> List[str]:
114
- """Get combined scopes for all Google Workspace services."""
149
+ """Combined scopes for all Google Workspace services."""
115
150
  config = _load_config()
116
151
  scopes_config = config.get("scopes", _get_default_config()["scopes"])
117
152
  all_scopes = []
118
153
  for scope_list in scopes_config.values():
119
154
  all_scopes.extend(scope_list)
120
- return list(dict.fromkeys(all_scopes)) # Remove duplicates, preserve order
155
+ return list(dict.fromkeys(all_scopes)) # dedupe, preserve order
156
+
121
157
 
122
158
  def get_scopes_for_services(services: List[str]) -> List[str]:
123
- """Get scopes for specific services only."""
159
+ """Scopes for specific services only."""
124
160
  config = _load_config()
125
161
  scopes_config = config.get("scopes", _get_default_config()["scopes"])
126
- scopes = []
127
- # Always include userinfo
128
- scopes.extend(scopes_config.get("userinfo", []))
162
+ scopes = list(scopes_config.get("userinfo", []))
129
163
  for service in services:
130
164
  scopes.extend(scopes_config.get(service, []))
131
165
  return list(dict.fromkeys(scopes))
132
166
 
133
- # Combined scopes for all services (loaded from config)
167
+
134
168
  GOOGLE_WORKSPACE_SCOPES = get_all_scopes()
169
+ DEFAULT_SCOPES = GOOGLE_WORKSPACE_SCOPES # legacy alias
135
170
 
136
- # Legacy alias for backward compatibility
137
- DEFAULT_SCOPES = GOOGLE_WORKSPACE_SCOPES
138
171
 
139
- # In-memory state store (use Redis in production)
140
- _oauth_states: Dict[str, Dict[str, Any]] = {}
172
+ # ============================================================================
173
+ # GoogleOAuth (composition wrapper)
174
+ # ============================================================================
141
175
 
142
176
 
143
177
  class GoogleOAuth:
144
- """Google Workspace OAuth 2.0 using google-auth-oauthlib Flow.
178
+ """Google Workspace OAuth 2.0 client (composition wrapper).
179
+
180
+ Conforms to the duck-typed protocol consumed by
181
+ :func:`services.events.oauth_lifecycle.make_oauth_lifecycle_handlers`
182
+ and :func:`make_oauth_callback_router`: shared :class:`OAuthStateStore`,
183
+ async ``exchange_code`` / ``fetch_user_info`` /
184
+ ``refresh_access_token`` / ``revoke_token``.
145
185
 
146
- Provides unified OAuth for all Google Workspace services.
147
- API endpoints loaded from config/google_apis.json for easy updates.
186
+ Internally, ``Flow.from_client_config`` does the PKCE dance and
187
+ Flow.fetch_token does the token exchange under the
188
+ ``OAUTHLIB_RELAX_TOKEN_SCOPE=1`` env var.
148
189
  """
149
190
 
191
+ # Plugin-scoped state store -- isolated from Twitter's instance.
192
+ state_store = OAuthStateStore()
193
+
150
194
  def __init__(
151
195
  self,
152
196
  client_id: str,
153
197
  client_secret: str,
154
198
  redirect_uri: str,
155
199
  scopes: Optional[List[str]] = None,
156
- ):
200
+ ) -> None:
157
201
  self.client_id = client_id
158
202
  self.client_secret = client_secret
159
203
  self.redirect_uri = redirect_uri
160
204
  self.scopes = scopes or GOOGLE_WORKSPACE_SCOPES
161
205
 
162
- # Get OAuth endpoints from config
163
206
  oauth_endpoints = get_oauth_endpoints()
164
-
165
- # Build client config in Google's format
166
207
  self.client_config = {
167
208
  "web": {
168
209
  "client_id": client_id,
@@ -173,91 +214,50 @@ class GoogleOAuth:
173
214
  }
174
215
  }
175
216
  self.token_uri = oauth_endpoints["token_uri"]
217
+ self.revoke_uri = oauth_endpoints.get(
218
+ "revoke_uri", "https://oauth2.googleapis.com/revoke",
219
+ )
176
220
 
177
221
  def generate_authorization_url(
178
- self,
179
- state_data: Optional[Dict[str, Any]] = None,
222
+ self, *, state_data: Optional[Dict[str, Any]] = None,
180
223
  ) -> Dict[str, str]:
181
- """
182
- Generate OAuth authorization URL.
183
-
184
- Args:
185
- state_data: Optional data (customer_id, mode, redirect_after)
186
-
187
- Returns:
188
- Dict with url and state
189
- """
190
- # Create Flow from client config
224
+ """Build the Google authorization URL + register state."""
191
225
  flow = Flow.from_client_config(
192
- self.client_config,
193
- scopes=self.scopes,
194
- redirect_uri=self.redirect_uri,
226
+ self.client_config, scopes=self.scopes, redirect_uri=self.redirect_uri,
195
227
  )
196
-
197
- # Generate authorization URL with offline access for refresh tokens
198
228
  authorization_url, state = flow.authorization_url(
199
229
  access_type="offline",
200
230
  include_granted_scopes="true",
201
- prompt="consent", # Force consent to get refresh token
231
+ prompt="consent", # force consent to get refresh token
202
232
  )
203
-
204
- # Store state data for callback verification
205
- # PKCE: google-auth-oauthlib auto-generates code_verifier; save it
206
- # so exchange_code() can restore it on the new Flow instance.
207
- _oauth_states[state] = {
208
- "created_at": time.time(),
233
+ self.state_store.put(state, {
209
234
  "data": state_data or {"mode": "owner"},
210
235
  "redirect_uri": self.redirect_uri,
211
236
  "code_verifier": getattr(flow, "code_verifier", None),
212
- }
213
-
214
- logger.info("Generated Google OAuth URL", state=state[:8])
215
-
216
- return {
217
- "url": authorization_url,
218
- "state": state,
219
- }
220
-
221
- def exchange_code(self, code: str, state: str) -> Dict[str, Any]:
222
- """
223
- Exchange authorization code for credentials.
237
+ })
238
+ return {"url": authorization_url, "state": state}
224
239
 
225
- Args:
226
- code: Authorization code from callback
227
- state: State for verification
228
-
229
- Returns:
230
- Dict with tokens and user info
231
- """
232
- # Verify state
233
- oauth_state = _oauth_states.pop(state, None)
234
- if not oauth_state:
240
+ async def exchange_code(self, code: str, state: str) -> Dict[str, Any]:
241
+ """Exchange an auth code for credentials (async wrapper)."""
242
+ record = self.state_store.take(state)
243
+ if not record:
235
244
  return {"success": False, "error": "Invalid or expired state"}
236
245
 
237
- state_data = oauth_state.get("data", {})
246
+ state_data = record.get("data", {})
247
+ code_verifier = record.get("code_verifier")
238
248
 
239
- try:
240
- # Create Flow and fetch token
249
+ def _exchange_sync() -> Dict[str, Any]:
241
250
  flow = Flow.from_client_config(
242
251
  self.client_config,
243
252
  scopes=self.scopes,
244
253
  redirect_uri=self.redirect_uri,
245
254
  state=state,
246
255
  )
247
- # PKCE: restore code_verifier so token exchange includes it
248
- code_verifier = oauth_state.get("code_verifier")
249
256
  if code_verifier:
250
257
  flow.code_verifier = code_verifier
251
258
  flow.fetch_token(code=code)
252
-
253
- # Get credentials from flow
254
259
  creds = flow.credentials
255
-
256
- # Get user info
257
- user_info = self._get_user_info(creds)
258
-
259
- logger.info("Google OAuth successful", email=user_info.get("email", "")[:20])
260
-
260
+ user_info = self._get_user_info_sync(creds)
261
261
  return {
262
262
  "success": True,
263
263
  "access_token": creds.token,
@@ -267,30 +267,85 @@ class GoogleOAuth:
267
267
  "client_id": creds.client_id,
268
268
  "client_secret": creds.client_secret,
269
269
  "scopes": list(creds.scopes) if creds.scopes else self.scopes,
270
+ "scope": " ".join(creds.scopes) if creds.scopes else "",
270
271
  "state_data": state_data,
271
272
  "email": user_info.get("email"),
272
273
  "name": user_info.get("name"),
273
274
  "picture": user_info.get("picture"),
274
275
  }
275
276
 
276
- except Exception as e:
277
- logger.error("Token exchange failed", error=str(e))
278
- return {"success": False, "error": str(e)}
277
+ try:
278
+ return await asyncio.to_thread(_exchange_sync)
279
+ except Exception as exc: # noqa: BLE001
280
+ logger.error(f"[google] Token exchange failed: {exc}")
281
+ return {"success": False, "error": str(exc)}
282
+
283
+ async def fetch_user_info(self, access_token: str) -> Dict[str, Any]:
284
+ """Build credentials from an access token + fetch user info."""
285
+ creds = self.build_credentials(
286
+ access_token=access_token,
287
+ refresh_token="", # not needed for a single read
288
+ client_id=self.client_id,
289
+ client_secret=self.client_secret,
290
+ token_uri=self.token_uri,
291
+ scopes=self.scopes,
292
+ )
293
+ try:
294
+ info = await asyncio.to_thread(self._get_user_info_sync, creds)
295
+ except Exception as exc: # noqa: BLE001
296
+ return {"success": False, "error": str(exc)}
297
+ if not info:
298
+ return {"success": False, "error": "Failed to read user info"}
299
+ return {"success": True, **info}
300
+
301
+ async def refresh_access_token(self, refresh_token: str) -> Dict[str, Any]:
302
+ """Async wrapper around :meth:`refresh_credentials`."""
303
+ return await asyncio.to_thread(
304
+ self.refresh_credentials,
305
+ refresh_token, self.client_id, self.client_secret, self.token_uri,
306
+ )
307
+
308
+ async def revoke_token(
309
+ self, token: str, token_type: str = "access_token",
310
+ ) -> Dict[str, Any]:
311
+ """Best-effort token revocation via Google's revoke endpoint."""
312
+ try:
313
+ async with httpx.AsyncClient(timeout=15.0) as client:
314
+ response = await client.post(
315
+ self.revoke_uri,
316
+ data={"token": token},
317
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
318
+ )
319
+ except httpx.HTTPError as exc:
320
+ logger.warning(f"[google] revoke_token network error: {exc}")
321
+ return {"success": False, "error": str(exc)}
322
+ if response.status_code == 200:
323
+ return {"success": True}
324
+ return {
325
+ "success": False,
326
+ "error": (response.json() if response.text else {}).get(
327
+ "error_description", "Revocation failed",
328
+ ),
329
+ }
330
+
331
+ # ---- internal helpers ----------------------------------------------
279
332
 
280
- def _get_user_info(self, creds: Credentials) -> Dict[str, Any]:
281
- """Get user info using credentials."""
333
+ def _get_user_info_sync(self, creds: Credentials) -> Dict[str, Any]:
334
+ """Sync user-info read; called inside ``asyncio.to_thread``."""
282
335
  try:
283
336
  service = build("oauth2", "v2", credentials=creds)
284
- user_info = service.userinfo().get().execute()
337
+ info = service.userinfo().get().execute()
285
338
  return {
286
- "email": user_info.get("email"),
287
- "name": user_info.get("name"),
288
- "picture": user_info.get("picture"),
339
+ "email": info.get("email"),
340
+ "name": info.get("name"),
341
+ "picture": info.get("picture"),
289
342
  }
290
- except Exception as e:
291
- logger.error("Failed to get user info", error=str(e))
343
+ except Exception as exc: # noqa: BLE001
344
+ logger.error(f"[google] Failed to get user info: {exc}")
292
345
  return {}
293
346
 
347
+ # ---- statics: kept for _auth_helper.py + other consumers ----------
348
+
294
349
  @staticmethod
295
350
  def refresh_credentials(
296
351
  refresh_token: str,
@@ -298,21 +353,9 @@ class GoogleOAuth:
298
353
  client_secret: str,
299
354
  token_uri: Optional[str] = None,
300
355
  ) -> Dict[str, Any]:
301
- """
302
- Refresh expired credentials.
303
-
304
- Args:
305
- refresh_token: The refresh token
306
- client_id: OAuth client ID
307
- client_secret: OAuth client secret
308
- token_uri: Token endpoint (loaded from config if not provided)
309
-
310
- Returns:
311
- Dict with new access_token
312
- """
356
+ """Refresh expired credentials. Returns ``{success, access_token, ...}``."""
313
357
  if not token_uri:
314
358
  token_uri = get_oauth_endpoints()["token_uri"]
315
-
316
359
  try:
317
360
  creds = Credentials(
318
361
  token=None,
@@ -322,15 +365,14 @@ class GoogleOAuth:
322
365
  client_secret=client_secret,
323
366
  )
324
367
  creds.refresh(Request())
325
-
326
368
  return {
327
369
  "success": True,
328
370
  "access_token": creds.token,
329
371
  "expires_in": 3600,
330
372
  }
331
- except Exception as e:
332
- logger.error("Token refresh failed", error=str(e))
333
- return {"success": False, "error": str(e), "needs_reauth": True}
373
+ except Exception as exc: # noqa: BLE001
374
+ logger.error(f"[google] Token refresh failed: {exc}")
375
+ return {"success": False, "error": str(exc), "needs_reauth": True}
334
376
 
335
377
  @staticmethod
336
378
  def build_credentials(
@@ -341,14 +383,9 @@ class GoogleOAuth:
341
383
  token_uri: Optional[str] = None,
342
384
  scopes: Optional[List[str]] = None,
343
385
  ) -> Credentials:
344
- """
345
- Build Credentials object from stored tokens.
346
-
347
- Use this to create credentials for Google API calls.
348
- """
386
+ """Build Credentials object from stored tokens (used by _auth_helper)."""
349
387
  if not token_uri:
350
388
  token_uri = get_oauth_endpoints()["token_uri"]
351
-
352
389
  return Credentials(
353
390
  token=access_token,
354
391
  refresh_token=refresh_token,
@@ -358,53 +395,49 @@ class GoogleOAuth:
358
395
  scopes=scopes or GOOGLE_WORKSPACE_SCOPES,
359
396
  )
360
397
 
361
- # Service builders for each Google API
362
398
  @staticmethod
363
399
  def build_gmail_service(creds: Credentials):
364
- """Build Gmail API service from credentials."""
365
400
  return build("gmail", "v1", credentials=creds)
366
401
 
367
402
  @staticmethod
368
403
  def build_calendar_service(creds: Credentials):
369
- """Build Calendar API service from credentials."""
370
404
  return build("calendar", "v3", credentials=creds)
371
405
 
372
406
  @staticmethod
373
407
  def build_drive_service(creds: Credentials):
374
- """Build Drive API service from credentials."""
375
408
  return build("drive", "v3", credentials=creds)
376
409
 
377
410
  @staticmethod
378
411
  def build_sheets_service(creds: Credentials):
379
- """Build Sheets API service from credentials."""
380
412
  return build("sheets", "v4", credentials=creds)
381
413
 
382
414
  @staticmethod
383
415
  def build_tasks_service(creds: Credentials):
384
- """Build Tasks API service from credentials."""
385
416
  return build("tasks", "v1", credentials=creds)
386
417
 
387
418
  @staticmethod
388
419
  def build_people_service(creds: Credentials):
389
- """Build People API service (Contacts) from credentials."""
390
420
  return build("people", "v1", credentials=creds)
391
421
 
392
422
 
393
- # Backward compatibility alias
423
+ # Legacy alias.
394
424
  GmailOAuth = GoogleOAuth
395
425
 
396
426
 
397
- def cleanup_expired_states(max_age_seconds: int = 600):
398
- """Remove expired OAuth states."""
399
- current_time = time.time()
400
- expired = [
401
- state for state, data in _oauth_states.items()
402
- if current_time - data["created_at"] > max_age_seconds
403
- ]
404
- for state in expired:
405
- _oauth_states.pop(state, None)
427
+ # Module-level alias to the state store's backing dict so the contract
428
+ # tests in tests/credentials/test_google_oauth.py can ``_oauth_states.clear()``.
429
+ # Same trick the Twitter migration uses.
430
+ _oauth_states = GoogleOAuth.state_store._states
406
431
 
407
432
 
408
- def get_pending_state(state: str) -> Optional[Dict[str, Any]]:
409
- """Get pending state without removing it."""
410
- return _oauth_states.get(state)
433
+ __all__ = [
434
+ "GoogleOAuth",
435
+ "GmailOAuth",
436
+ "GOOGLE_WORKSPACE_SCOPES",
437
+ "DEFAULT_SCOPES",
438
+ "get_oauth_endpoints",
439
+ "get_callback_paths",
440
+ "get_service_config",
441
+ "get_all_scopes",
442
+ "get_scopes_for_services",
443
+ ]
@@ -0,0 +1,107 @@
1
+ """Google Workspace ``loadOptionsMethod`` loaders.
2
+
3
+ Wave 11.I, milestone M.2. Each function is registered with
4
+ ``services.ws_handler_registry.register_option_loader`` from
5
+ ``__init__.py``.
6
+
7
+ Reuses :func:`._auth_helper.get_google_credentials` so the OAuth dance
8
+ is identical to the workflow-execution path. ``params`` may carry
9
+ ``account_mode`` / ``customer_id`` (multi-tenant customer mode) -- the
10
+ auth helper falls back to owner tokens otherwise.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ from typing import Any, Dict, List
17
+
18
+ from googleapiclient.discovery import build
19
+
20
+ from ._auth_helper import get_google_credentials
21
+
22
+
23
+ async def _google_service(api: str, version: str, params: Dict[str, Any]):
24
+ """Build a googleapiclient service under the right OAuth credentials."""
25
+ creds = await get_google_credentials(params, {})
26
+ loop = asyncio.get_event_loop()
27
+ return await loop.run_in_executor(
28
+ None, lambda: build(api, version, credentials=creds)
29
+ )
30
+
31
+
32
+ async def load_gmail_labels(params: Dict[str, Any]) -> List[Dict[str, Any]]:
33
+ """Gmail labels for the label-filter selector on gmailReceive and
34
+ gmail (search)."""
35
+ service = await _google_service("gmail", "v1", params)
36
+ loop = asyncio.get_event_loop()
37
+ response = await loop.run_in_executor(
38
+ None, lambda: service.users().labels().list(userId="me").execute()
39
+ )
40
+ labels = response.get("labels", [])
41
+ # Stable sort: system labels alphabetical first, then user labels.
42
+ labels.sort(
43
+ key=lambda label: (
44
+ label.get("type") != "system",
45
+ (label.get("name") or "").lower(),
46
+ )
47
+ )
48
+ return [
49
+ {"value": label["id"], "label": label.get("name") or label["id"]}
50
+ for label in labels
51
+ ]
52
+
53
+
54
+ async def load_calendar_list(params: Dict[str, Any]) -> List[Dict[str, Any]]:
55
+ """Calendar list for the calendarId picker on calendar CRUD."""
56
+ service = await _google_service("calendar", "v3", params)
57
+ loop = asyncio.get_event_loop()
58
+ response = await loop.run_in_executor(
59
+ None, lambda: service.calendarList().list().execute()
60
+ )
61
+ entries = response.get("items", [])
62
+ # Primary first, rest alphabetised.
63
+ entries.sort(
64
+ key=lambda c: (not c.get("primary", False), (c.get("summary") or "").lower())
65
+ )
66
+ return [
67
+ {
68
+ "value": c.get("id", ""),
69
+ "label": c.get("summary") or c.get("id", ""),
70
+ "description": "Primary" if c.get("primary") else c.get("description", ""),
71
+ }
72
+ for c in entries
73
+ ]
74
+
75
+
76
+ async def load_drive_folders(params: Dict[str, Any]) -> List[Dict[str, Any]]:
77
+ """Drive folders for the folderId picker on drive upload/list."""
78
+ service = await _google_service("drive", "v3", params)
79
+ loop = asyncio.get_event_loop()
80
+ query = "mimeType='application/vnd.google-apps.folder' and trashed=false"
81
+ response = await loop.run_in_executor(
82
+ None,
83
+ lambda: service.files()
84
+ .list(q=query, fields="files(id, name, parents)", pageSize=200)
85
+ .execute(),
86
+ )
87
+ folders = response.get("files", [])
88
+ folders.sort(key=lambda f: (f.get("name") or "").lower())
89
+ return [
90
+ {"value": f.get("id", ""), "label": f.get("name") or f.get("id", "")}
91
+ for f in folders
92
+ ]
93
+
94
+
95
+ async def load_tasklists(params: Dict[str, Any]) -> List[Dict[str, Any]]:
96
+ """Task lists for the tasklistId picker on tasks CRUD."""
97
+ service = await _google_service("tasks", "v1", params)
98
+ loop = asyncio.get_event_loop()
99
+ response = await loop.run_in_executor(
100
+ None, lambda: service.tasklists().list().execute()
101
+ )
102
+ lists = response.get("items", [])
103
+ lists.sort(key=lambda label: (label.get("title") or "").lower())
104
+ return [
105
+ {"value": label.get("id", ""), "label": label.get("title") or label.get("id", "")}
106
+ for label in lists
107
+ ]