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
@@ -0,0 +1,381 @@
1
+ """OAuth 2.0 PKCE shared infrastructure (Wave 11.I, milestone S).
2
+
3
+ Two pieces consumed by every OAuth-using plugin:
4
+
5
+ 1. :class:`OAuthStateStore` -- in-memory PKCE/state store with TTL.
6
+ Both Twitter (hand-rolled PKCE) and Google (``google_auth_oauthlib``-
7
+ wrapped) drive their callback CSRF check through one of these. The
8
+ pre-Wave-11.I plugin folders each hand-rolled their own ``dict`` +
9
+ ``cleanup_expired_states`` helper -- collapsed into this one class.
10
+
11
+ 2. :class:`OAuth2PKCEClient` -- abstract base for plugins that hand-roll
12
+ the OAuth 2.0 PKCE dance themselves (Twitter pattern). Subclass it
13
+ when the upstream API exposes a vanilla token endpoint and the
14
+ `Flow` class from `google_auth_oauthlib` doesn't fit. Override
15
+ :meth:`fetch_user_info` to translate the provider's profile API into
16
+ the unified ``{id, username, name, ...}`` shape consumed by the
17
+ lifecycle helpers in :mod:`services.events.oauth_lifecycle`.
18
+
19
+ Plugins that compose around an upstream library (Google's
20
+ ``google_auth_oauthlib.flow.Flow``) skip the subclass and instantiate
21
+ :class:`OAuthStateStore` directly. The lifecycle factory in
22
+ :mod:`services.events.oauth_lifecycle` is duck-typed against the
23
+ methods listed below -- both subclass and composition paths feed it
24
+ without changes.
25
+
26
+ Persistence: in-memory only. State resets on server restart -- a popup
27
+ opened across a restart will fail the callback CSRF check. Same as
28
+ pre-extraction behaviour. Persistent backing (Redis / DB) is a
29
+ documented follow-up.
30
+
31
+ Cleanup: lazy. The store exposes :meth:`OAuthStateStore.cleanup_expired`
32
+ so plugins can prune from a periodic task; called on demand from the
33
+ callback path so a flood of abandoned popups eventually self-clears
34
+ without a background sweeper.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import base64
40
+ import hashlib
41
+ import secrets
42
+ import time
43
+ from abc import ABC, abstractmethod
44
+ from typing import Any, ClassVar, Dict, List, Optional
45
+ from urllib.parse import urlencode
46
+
47
+ import httpx
48
+
49
+ from core.logging import get_logger
50
+
51
+ logger = get_logger(__name__)
52
+
53
+
54
+ # ============================================================================
55
+ # OAuthStateStore
56
+ # ============================================================================
57
+
58
+
59
+ class OAuthStateStore:
60
+ """In-memory PKCE/state store with TTL cleanup.
61
+
62
+ One instance per plugin (or one shared module-level instance).
63
+ Stores a payload (typically ``{code_verifier, redirect_uri,
64
+ state_data, created_at}``) keyed by the random ``state`` parameter
65
+ the OAuth flow round-trips through the browser.
66
+ """
67
+
68
+ DEFAULT_TTL_SECONDS = 600 # 10 minutes -- auth codes themselves expire faster
69
+
70
+ def __init__(self, *, ttl_seconds: int = DEFAULT_TTL_SECONDS) -> None:
71
+ self.ttl_seconds = ttl_seconds
72
+ self._states: Dict[str, Dict[str, Any]] = {}
73
+
74
+ def put(self, state: str, payload: Dict[str, Any]) -> None:
75
+ """Store a payload under ``state``. Stamps ``created_at``."""
76
+ record = dict(payload)
77
+ record.setdefault("created_at", time.time())
78
+ self._states[state] = record
79
+
80
+ def take(self, state: str) -> Optional[Dict[str, Any]]:
81
+ """Pop and return the payload for ``state``. One-shot.
82
+
83
+ This is the callback-path read: the state is consumed exactly
84
+ once when the user lands back on the redirect URI with the
85
+ ``code`` parameter.
86
+ """
87
+ return self._states.pop(state, None)
88
+
89
+ def peek(self, state: str) -> Optional[Dict[str, Any]]:
90
+ """Return the payload without removing it (read-only check).
91
+
92
+ Used by callback handlers that need to read the redirect_uri /
93
+ state_data BEFORE running the code exchange (which calls
94
+ :meth:`take`).
95
+ """
96
+ return self._states.get(state)
97
+
98
+ def cleanup_expired(self) -> int:
99
+ """Remove states older than ``ttl_seconds``. Returns count removed."""
100
+ now = time.time()
101
+ expired = [
102
+ state
103
+ for state, record in self._states.items()
104
+ if now - record.get("created_at", 0) > self.ttl_seconds
105
+ ]
106
+ for state in expired:
107
+ self._states.pop(state, None)
108
+ if expired:
109
+ logger.debug(f"OAuthStateStore: cleaned {len(expired)} expired states")
110
+ return len(expired)
111
+
112
+ def __len__(self) -> int:
113
+ return len(self._states)
114
+
115
+ def __contains__(self, state: object) -> bool:
116
+ return state in self._states
117
+
118
+
119
+ # ============================================================================
120
+ # PKCE helpers (RFC 7636)
121
+ # ============================================================================
122
+
123
+
124
+ def _generate_code_verifier() -> str:
125
+ """Cryptographically random PKCE code verifier (43-128 chars)."""
126
+ random_bytes = secrets.token_bytes(96)
127
+ verifier = base64.urlsafe_b64encode(random_bytes).rstrip(b"=").decode("ascii")
128
+ return verifier[:128]
129
+
130
+
131
+ def _generate_code_challenge(code_verifier: str) -> str:
132
+ """S256 code challenge: ``BASE64URL(SHA256(code_verifier))``."""
133
+ digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
134
+ return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
135
+
136
+
137
+ def _generate_state() -> str:
138
+ """Random state parameter for CSRF protection (URL-safe)."""
139
+ return secrets.token_urlsafe(32)
140
+
141
+
142
+ # ============================================================================
143
+ # OAuth2PKCEClient (subclass-mode -- Twitter pattern)
144
+ # ============================================================================
145
+
146
+
147
+ class OAuth2PKCEClient(ABC):
148
+ """Hand-rolled OAuth 2.0 PKCE client.
149
+
150
+ Concrete subclasses override :attr:`provider`,
151
+ :attr:`authorization_endpoint`, :attr:`token_endpoint`,
152
+ optionally :attr:`revocation_endpoint`, and the
153
+ :meth:`fetch_user_info` translation. Everything else (PKCE state
154
+ management, code exchange, token refresh, optional revocation)
155
+ lives on this base.
156
+
157
+ Plugins that compose around a third-party Flow class (e.g.
158
+ Google's ``google_auth_oauthlib.flow.Flow``) skip this base and
159
+ instantiate :class:`OAuthStateStore` directly. The lifecycle
160
+ factory is duck-typed against these method signatures -- both
161
+ paths feed it identically.
162
+ """
163
+
164
+ provider: ClassVar[str] = ""
165
+ authorization_endpoint: ClassVar[str] = ""
166
+ token_endpoint: ClassVar[str] = ""
167
+ revocation_endpoint: ClassVar[str] = "" # optional
168
+
169
+ # Per-class shared state store. Plugins can override at class level
170
+ # if they want isolated TTLs / multiple instances.
171
+ state_store: ClassVar[OAuthStateStore] = OAuthStateStore()
172
+
173
+ DEFAULT_SCOPES: ClassVar[List[str]] = []
174
+
175
+ def __init__(
176
+ self,
177
+ client_id: str,
178
+ redirect_uri: str,
179
+ client_secret: Optional[str] = None,
180
+ scopes: Optional[List[str]] = None,
181
+ ) -> None:
182
+ if not self.provider:
183
+ raise ValueError(
184
+ f"{type(self).__name__} must set the ``provider`` ClassVar"
185
+ )
186
+ self.client_id = client_id
187
+ self.client_secret = client_secret
188
+ self.redirect_uri = redirect_uri
189
+ self.scopes = list(scopes) if scopes is not None else list(self.DEFAULT_SCOPES)
190
+
191
+ # ---- authorization-url generation ----------------------------------
192
+
193
+ def generate_authorization_url(
194
+ self,
195
+ *,
196
+ state_data: Optional[Dict[str, Any]] = None,
197
+ extra_params: Optional[Dict[str, str]] = None,
198
+ ) -> Dict[str, str]:
199
+ """Return the redirect URL + state + verifier.
200
+
201
+ Stashes ``code_verifier`` + ``redirect_uri`` (and any
202
+ caller-supplied ``state_data``) in :attr:`state_store` keyed by
203
+ the random state. The callback handler reads this back to
204
+ complete the exchange.
205
+ """
206
+ state = _generate_state()
207
+ code_verifier = _generate_code_verifier()
208
+ code_challenge = _generate_code_challenge(code_verifier)
209
+
210
+ self.state_store.put(state, {
211
+ "code_verifier": code_verifier,
212
+ "redirect_uri": self.redirect_uri,
213
+ "data": state_data or {},
214
+ })
215
+
216
+ params = {
217
+ "response_type": "code",
218
+ "client_id": self.client_id,
219
+ "redirect_uri": self.redirect_uri,
220
+ "scope": " ".join(self.scopes),
221
+ "state": state,
222
+ "code_challenge": code_challenge,
223
+ "code_challenge_method": "S256",
224
+ }
225
+ if extra_params:
226
+ params.update(extra_params)
227
+
228
+ return {
229
+ "url": f"{self.authorization_endpoint}?{urlencode(params)}",
230
+ "state": state,
231
+ "code_verifier": code_verifier,
232
+ }
233
+
234
+ # ---- token endpoint helpers ----------------------------------------
235
+
236
+ def _token_request_auth(self) -> tuple[Dict[str, str], Dict[str, str]]:
237
+ """Build (extra_body, extra_headers) for the token endpoint.
238
+
239
+ Confidential clients (with ``client_secret``) send Basic auth
240
+ in the header. Public clients put ``client_id`` in the body.
241
+ """
242
+ if self.client_secret:
243
+ credentials = base64.b64encode(
244
+ f"{self.client_id}:{self.client_secret}".encode()
245
+ ).decode()
246
+ return {}, {"Authorization": f"Basic {credentials}"}
247
+ return {"client_id": self.client_id}, {}
248
+
249
+ async def exchange_code(self, code: str, state: str) -> Dict[str, Any]:
250
+ """Exchange an auth code for tokens. Pops the state."""
251
+ record = self.state_store.take(state)
252
+ if not record:
253
+ logger.error(f"[{self.provider}] Invalid or expired OAuth state")
254
+ return {"success": False, "error": "Invalid or expired state"}
255
+
256
+ body = {
257
+ "grant_type": "authorization_code",
258
+ "code": code,
259
+ "redirect_uri": record.get("redirect_uri", self.redirect_uri),
260
+ "code_verifier": record["code_verifier"],
261
+ }
262
+ extra_body, extra_headers = self._token_request_auth()
263
+ body.update(extra_body)
264
+ headers = {"Content-Type": "application/x-www-form-urlencoded", **extra_headers}
265
+
266
+ try:
267
+ async with httpx.AsyncClient(timeout=30.0) as client:
268
+ response = await client.post(
269
+ self.token_endpoint, data=body, headers=headers,
270
+ )
271
+ except httpx.HTTPError as exc:
272
+ logger.error(f"[{self.provider}] HTTP error during token exchange: {exc}")
273
+ return {"success": False, "error": str(exc)}
274
+
275
+ if response.status_code != 200:
276
+ error_data = response.json() if response.text else {}
277
+ logger.error(
278
+ f"[{self.provider}] Token exchange failed",
279
+ status=response.status_code, error=error_data,
280
+ )
281
+ return {
282
+ "success": False,
283
+ "error": error_data.get(
284
+ "error_description",
285
+ error_data.get("error", "Token exchange failed"),
286
+ ),
287
+ }
288
+
289
+ data = response.json()
290
+ return {
291
+ "success": True,
292
+ "access_token": data.get("access_token"),
293
+ "refresh_token": data.get("refresh_token"),
294
+ "expires_in": data.get("expires_in"),
295
+ "scope": data.get("scope"),
296
+ "token_type": data.get("token_type", "Bearer"),
297
+ }
298
+
299
+ async def refresh_access_token(self, refresh_token: str) -> Dict[str, Any]:
300
+ """Trade a refresh token for a fresh access token."""
301
+ body = {"grant_type": "refresh_token", "refresh_token": refresh_token}
302
+ extra_body, extra_headers = self._token_request_auth()
303
+ body.update(extra_body)
304
+ headers = {"Content-Type": "application/x-www-form-urlencoded", **extra_headers}
305
+
306
+ try:
307
+ async with httpx.AsyncClient(timeout=30.0) as client:
308
+ response = await client.post(
309
+ self.token_endpoint, data=body, headers=headers,
310
+ )
311
+ except httpx.HTTPError as exc:
312
+ logger.error(f"[{self.provider}] HTTP error during token refresh: {exc}")
313
+ return {"success": False, "error": str(exc)}
314
+
315
+ if response.status_code != 200:
316
+ error_data = response.json() if response.text else {}
317
+ return {
318
+ "success": False,
319
+ "error": error_data.get("error_description", "Token refresh failed"),
320
+ }
321
+
322
+ data = response.json()
323
+ return {
324
+ "success": True,
325
+ "access_token": data.get("access_token"),
326
+ "refresh_token": data.get("refresh_token"),
327
+ "expires_in": data.get("expires_in"),
328
+ "scope": data.get("scope"),
329
+ }
330
+
331
+ async def revoke_token(
332
+ self, token: str, token_type: str = "access_token",
333
+ ) -> Dict[str, Any]:
334
+ """Revoke an access or refresh token (best-effort).
335
+
336
+ Subclasses without :attr:`revocation_endpoint` get a no-op
337
+ ``{"success": True, "skipped": True}`` so callers can always
338
+ attempt a revoke without branching.
339
+ """
340
+ if not self.revocation_endpoint:
341
+ return {"success": True, "skipped": True}
342
+
343
+ body = {"token": token, "token_type_hint": token_type}
344
+ extra_body, extra_headers = self._token_request_auth()
345
+ body.update(extra_body)
346
+ headers = {"Content-Type": "application/x-www-form-urlencoded", **extra_headers}
347
+
348
+ try:
349
+ async with httpx.AsyncClient(timeout=30.0) as client:
350
+ response = await client.post(
351
+ self.revocation_endpoint, data=body, headers=headers,
352
+ )
353
+ except httpx.HTTPError as exc:
354
+ logger.error(f"[{self.provider}] HTTP error during token revoke: {exc}")
355
+ return {"success": False, "error": str(exc)}
356
+
357
+ if response.status_code == 200:
358
+ return {"success": True}
359
+ error_data = response.json() if response.text else {}
360
+ return {
361
+ "success": False,
362
+ "error": error_data.get("error_description", "Revocation failed"),
363
+ }
364
+
365
+ # ---- subclass override --------------------------------------------
366
+
367
+ @abstractmethod
368
+ async def fetch_user_info(self, access_token: str) -> Dict[str, Any]:
369
+ """Translate the provider's profile API into a unified shape.
370
+
371
+ MUST return ``{success: bool, ...}`` -- on success, include
372
+ ``id`` and at least one of ``username`` / ``email`` / ``name``.
373
+ The lifecycle factory uses these to populate the broadcast
374
+ envelope's ``subject`` and the connection-status payload.
375
+ """
376
+
377
+
378
+ __all__ = [
379
+ "OAuthStateStore",
380
+ "OAuth2PKCEClient",
381
+ ]
@@ -0,0 +1,247 @@
1
+ """PollingTriggerNode -- template-method polling-trigger base class.
2
+
3
+ Wave 11.I, milestone L. Subclass owns the four divergence points
4
+ (``setup_service``, ``fetch_ids``, ``fetch_detail``, optional
5
+ ``post_emit``); the base owns the loop body, the seen-id baseline,
6
+ the cancellation surface, and per-cycle error isolation.
7
+
8
+ Auto-registers a poll-coroutine factory in
9
+ :mod:`services.deployment.poll_registry` so
10
+ ``DeploymentManager._setup_event_trigger`` looks the plugin up by
11
+ node type instead of branching on a hardcoded list.
12
+
13
+ Design choice (over lift-and-shift): the gmail and email pollers in
14
+ ``services/deployment/manager.py`` had identical structure -- 80 LOC
15
+ vs 37 LOC, only the four seams differed. Pulling the loop into a
16
+ mixin collapses ~120 LOC of duplicated polling control flow into one
17
+ ~40 LOC body, plus ~15 LOC per concrete poller.
18
+
19
+ Cancellation contract
20
+ ---------------------
21
+ Mirrors the existing ``setup_polling_trigger`` consumer in
22
+ ``services/deployment/triggers.py``:
23
+
24
+ - The deployment manager passes ``is_running_fn``; the loop checks
25
+ it before every sleep and after wake.
26
+ - ``asyncio.CancelledError`` is re-raised, never swallowed (per
27
+ https://docs.python.org/3/library/asyncio-task.html#task-cancellation).
28
+
29
+ Per-cycle error isolation: a transient ``Exception`` from
30
+ ``fetch_ids`` or ``fetch_detail`` logs at ERROR and the loop sleeps
31
+ to the next interval. Permanent errors (auth revoked, account
32
+ deleted) currently keep retrying indefinitely with the same log
33
+ line; ``tenacity``-backed classification is a follow-up extension
34
+ point documented inline.
35
+
36
+ Out of scope (commit-stage)
37
+ ---------------------------
38
+ - ``WorkflowEvent.message(...)`` envelope wrapping: ``setup_polling_trigger``
39
+ consumes raw dicts today; envelope adoption is the U milestone test pass.
40
+ - Persistent watermark cursor: ``seen_ids`` is in-memory and resets
41
+ on restart. Backlog dropped on restart -- existing behaviour.
42
+ - Twitter polling: ``twitterReceive`` declares ``mode="polling"`` but
43
+ has no concrete subclass. Subclassing this base is the conversion path.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import asyncio
49
+ from typing import Any, Callable, ClassVar, Dict, Optional, Set, Tuple
50
+
51
+ from core.logging import get_logger
52
+ from services.plugin.trigger import TriggerNode
53
+
54
+ logger = get_logger(__name__)
55
+
56
+
57
+ class PollingTriggerNode(TriggerNode, abstract=True):
58
+ """Polling trigger with a unified loop body.
59
+
60
+ Subclass MUST override:
61
+
62
+ - :meth:`setup_service` -- build the auth client / SDK handle.
63
+ - :meth:`fetch_ids` -- return the current visible IDs (set[str]).
64
+ - :meth:`fetch_detail` -- return the event payload for one ID.
65
+
66
+ Subclass MAY override:
67
+
68
+ - :meth:`post_emit` -- side effect after enqueue (e.g. mark-as-read).
69
+ - :attr:`poll_interval_clamp` -- (min, max) seconds for the
70
+ ``poll_interval`` param. Default (10, 3600).
71
+ - :attr:`type_alias` -- a second registration key for plugins
72
+ that want to be reachable by a legacy type name without
73
+ changing the plugin's primary ``type``. Currently unused after
74
+ Wave 11.I milestone P retired the gmail alias; kept on the base
75
+ class as a documented escape hatch for future renames.
76
+ """
77
+
78
+ mode: ClassVar[str] = "polling"
79
+
80
+ # (min_seconds, max_seconds) for the user-supplied poll_interval.
81
+ poll_interval_clamp: ClassVar[Tuple[int, int]] = (10, 3600)
82
+
83
+ # Optional secondary registration key. Registers under both
84
+ # ``cls.type`` and ``cls.type_alias`` for plugins with a legacy
85
+ # alias.
86
+ type_alias: ClassVar[str] = ""
87
+
88
+ # ---- subclass hooks ------------------------------------------------
89
+
90
+ async def setup_service(self, params: Dict[str, Any]) -> Any:
91
+ """Build the auth client / service handle that ``fetch_ids`` and
92
+ ``fetch_detail`` will close over.
93
+
94
+ Plugins that don't need a long-lived handle can return any
95
+ opaque value (or the ``params`` dict itself). The returned
96
+ value is passed unchanged to the other hooks.
97
+ """
98
+ raise NotImplementedError
99
+
100
+ async def fetch_ids(
101
+ self, service: Any, params: Dict[str, Any]
102
+ ) -> Set[str]:
103
+ """Return the current set of visible IDs for one poll cycle.
104
+
105
+ Called once for the baseline pass at loop start, then once per
106
+ ``poll_interval`` thereafter. The diff against the previous
107
+ ``seen`` set is the source of truth for "what's new".
108
+ """
109
+ raise NotImplementedError
110
+
111
+ async def fetch_detail(
112
+ self, service: Any, msg_id: str, params: Dict[str, Any]
113
+ ) -> Dict[str, Any]:
114
+ """Fetch the full event payload for one ID.
115
+
116
+ Called once per new ID per cycle. The returned dict goes
117
+ directly onto the deployment manager's queue (no envelope
118
+ wrapping in this commit -- see milestone U / module docstring).
119
+ """
120
+ raise NotImplementedError
121
+
122
+ async def post_emit(
123
+ self, service: Any, msg_id: str, params: Dict[str, Any]
124
+ ) -> None:
125
+ """Optional side effect AFTER the payload was enqueued.
126
+
127
+ Default no-op. Override for "mark-as-read" or similar
128
+ post-processing that should not block the emit and whose
129
+ failure shouldn't kill the loop.
130
+ """
131
+ return None
132
+
133
+ # ---- registration --------------------------------------------------
134
+
135
+ def __init_subclass__(cls, abstract: bool = False, **kwargs):
136
+ super().__init_subclass__(abstract=abstract, **kwargs)
137
+ if abstract or not cls.type:
138
+ return
139
+
140
+ # Auto-register a poll-coroutine factory for the deployment
141
+ # manager (the in-process Run-button path uses :meth:`execute`
142
+ # which we override below). Idempotent on re-import via
143
+ # IdempotentRegistry.
144
+ from services.deployment.poll_registry import (
145
+ register_poll_coroutine_factory,
146
+ )
147
+
148
+ def factory(node_id: str, params: Dict[str, Any]) -> Callable:
149
+ instance = cls()
150
+ return instance._build_poll_coroutine(node_id, params)
151
+
152
+ register_poll_coroutine_factory(cls.type, factory)
153
+ if cls.type_alias:
154
+ # Same factory under the legacy alias so consumers that
155
+ # key by the alias (manager.py POLLING_TRIGGER_TYPES) still
156
+ # find the factory until the rename lands.
157
+ register_poll_coroutine_factory(cls.type_alias, factory)
158
+
159
+ # ---- shared loop ---------------------------------------------------
160
+
161
+ def _clamp_interval(self, raw: Any) -> int:
162
+ lo, hi = self.poll_interval_clamp
163
+ try:
164
+ value = int(raw)
165
+ except (TypeError, ValueError):
166
+ value = self.default_poll_interval
167
+ return max(lo, min(hi, value))
168
+
169
+ def _build_poll_coroutine(
170
+ self, node_id: str, params: Dict[str, Any]
171
+ ) -> Callable[[asyncio.Queue, Callable[[], bool]], Any]:
172
+ """Return the bound poll coroutine the deployment manager
173
+ consumes. Closes over ``self``, ``node_id``, and ``params``.
174
+ """
175
+
176
+ async def poll(
177
+ queue: asyncio.Queue, is_running_fn: Callable[[], bool]
178
+ ) -> None:
179
+ try:
180
+ service = await self.setup_service(params)
181
+ except Exception as exc: # noqa: BLE001 -- single setup failure
182
+ logger.error(
183
+ "Polling trigger setup failed",
184
+ node_id=node_id, node_type=self.type, error=str(exc),
185
+ )
186
+ return
187
+
188
+ interval = self._clamp_interval(params.get("poll_interval"))
189
+
190
+ # Baseline -- avoid re-emitting items the user has had since
191
+ # before the deployment was live. On baseline failure, fall
192
+ # through with an empty set: the next cycle will emit
193
+ # everything currently visible (matches pre-migration gmail
194
+ # behaviour at line 728-736 of services/deployment/manager.py).
195
+ seen: Set[str] = set()
196
+ try:
197
+ seen = await self.fetch_ids(service, params)
198
+ logger.info(
199
+ "Polling trigger baseline established",
200
+ node_id=node_id, node_type=self.type, seen=len(seen),
201
+ )
202
+ except Exception as exc: # noqa: BLE001
203
+ logger.warning(
204
+ "Polling trigger baseline failed; treating all as new",
205
+ node_id=node_id, node_type=self.type, error=str(exc),
206
+ )
207
+
208
+ cycle = 0
209
+ while is_running_fn():
210
+ await asyncio.sleep(interval)
211
+ if not is_running_fn():
212
+ break
213
+ cycle += 1
214
+ try:
215
+ current = await self.fetch_ids(service, params)
216
+ new_ids = current - seen
217
+ if not new_ids:
218
+ continue
219
+ logger.debug(
220
+ "Polling trigger cycle",
221
+ node_id=node_id, node_type=self.type, cycle=cycle,
222
+ current=len(current), seen=len(seen),
223
+ new=len(new_ids),
224
+ )
225
+ for msg_id in new_ids:
226
+ seen.add(msg_id)
227
+ payload = await self.fetch_detail(
228
+ service, msg_id, params
229
+ )
230
+ await queue.put(payload)
231
+ # post_emit failure must NOT block the next emit
232
+ # nor kill the loop; mirrors pre-migration
233
+ # try/except at gmail :760-763 / email :799-806.
234
+ try:
235
+ await self.post_emit(service, msg_id, params)
236
+ except Exception:
237
+ pass
238
+ except asyncio.CancelledError:
239
+ raise
240
+ except Exception as exc: # noqa: BLE001 -- per-cycle isolation
241
+ logger.error(
242
+ "Polling trigger cycle error; retrying next interval",
243
+ node_id=node_id, node_type=self.type,
244
+ error=str(exc),
245
+ )
246
+
247
+ return poll