snippbot 0.1.0b1__py3-none-any.whl

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 (708) hide show
  1. snippbot/__init__.py +18 -0
  2. snippbot/__main__.py +6 -0
  3. snippbot/api/__init__.py +16 -0
  4. snippbot/api/agents.py +603 -0
  5. snippbot/api/api_handler.py +2611 -0
  6. snippbot/api/approval.py +111 -0
  7. snippbot/api/assets.py +629 -0
  8. snippbot/api/audio.py +276 -0
  9. snippbot/api/auth_helpers.py +79 -0
  10. snippbot/api/browser.py +507 -0
  11. snippbot/api/browser_ws.py +214 -0
  12. snippbot/api/channels.py +1167 -0
  13. snippbot/api/chat.py +3812 -0
  14. snippbot/api/chat_files.py +366 -0
  15. snippbot/api/custom_providers.py +518 -0
  16. snippbot/api/device_auth.py +480 -0
  17. snippbot/api/device_ws.py +43 -0
  18. snippbot/api/devices.py +1537 -0
  19. snippbot/api/dispatcher.py +369 -0
  20. snippbot/api/email.py +114 -0
  21. snippbot/api/equipment.py +177 -0
  22. snippbot/api/events_ws.py +98 -0
  23. snippbot/api/execution.py +2761 -0
  24. snippbot/api/files.py +351 -0
  25. snippbot/api/game_engine.py +438 -0
  26. snippbot/api/game_saves.py +166 -0
  27. snippbot/api/health.py +117 -0
  28. snippbot/api/hooks.py +735 -0
  29. snippbot/api/image_gen.py +307 -0
  30. snippbot/api/insights.py +584 -0
  31. snippbot/api/internal_token.py +74 -0
  32. snippbot/api/issues.py +711 -0
  33. snippbot/api/license.py +96 -0
  34. snippbot/api/marketplace.py +3753 -0
  35. snippbot/api/marketplace_auth.py +171 -0
  36. snippbot/api/marketplace_oauth.py +315 -0
  37. snippbot/api/mcp_refresh.py +264 -0
  38. snippbot/api/memory_capture.py +402 -0
  39. snippbot/api/memory_maintenance.py +349 -0
  40. snippbot/api/monitor.py +241 -0
  41. snippbot/api/nodes.py +263 -0
  42. snippbot/api/package_builder.py +33 -0
  43. snippbot/api/package_credentials.py +254 -0
  44. snippbot/api/recall_feedback_task.py +99 -0
  45. snippbot/api/reflection_capture.py +819 -0
  46. snippbot/api/remote_sessions.py +796 -0
  47. snippbot/api/router_settings.py +80 -0
  48. snippbot/api/routes.py +13111 -0
  49. snippbot/api/sandbox.py +706 -0
  50. snippbot/api/scenarios.py +129 -0
  51. snippbot/api/scheduler.py +932 -0
  52. snippbot/api/security.py +318 -0
  53. snippbot/api/setup.py +1384 -0
  54. snippbot/api/skill_builder.py +1751 -0
  55. snippbot/api/sub_agents.py +793 -0
  56. snippbot/api/sync.py +108 -0
  57. snippbot/api/thinking.py +730 -0
  58. snippbot/api/tool_metadata_cache.py +145 -0
  59. snippbot/api/wallet.py +846 -0
  60. snippbot/api/workflows.py +843 -0
  61. snippbot/api/workspaces.py +435 -0
  62. snippbot/api/world.py +334 -0
  63. snippbot/api/world_map.py +211 -0
  64. snippbot/channel_adapter/__init__.py +5 -0
  65. snippbot/channel_adapter/__main__.py +92 -0
  66. snippbot/channel_adapter/adapters/__init__.py +50 -0
  67. snippbot/channel_adapter/adapters/discord.py +376 -0
  68. snippbot/channel_adapter/adapters/email.py +477 -0
  69. snippbot/channel_adapter/adapters/google_chat.py +504 -0
  70. snippbot/channel_adapter/adapters/slack.py +311 -0
  71. snippbot/channel_adapter/adapters/teams.py +312 -0
  72. snippbot/channel_adapter/adapters/telegram.py +377 -0
  73. snippbot/channel_adapter/adapters/webhook.py +321 -0
  74. snippbot/channel_adapter/adapters/whatsapp.py +286 -0
  75. snippbot/channel_adapter/app.py +504 -0
  76. snippbot/channel_adapter/attachments.py +92 -0
  77. snippbot/channel_adapter/base.py +146 -0
  78. snippbot/channel_adapter/conversations.py +256 -0
  79. snippbot/channel_adapter/dispatch.py +332 -0
  80. snippbot/channel_adapter/loader.py +190 -0
  81. snippbot/channel_adapter/manager.py +120 -0
  82. snippbot/channel_adapter/models.py +85 -0
  83. snippbot/channel_adapter/rate_limiter.py +101 -0
  84. snippbot/channel_adapter/router.py +161 -0
  85. snippbot/channel_adapter/space_history.py +182 -0
  86. snippbot/channel_adapter/tunnel.py +134 -0
  87. snippbot/cli.py +11 -0
  88. snippbot/daemon.py +435 -0
  89. snippbot/discovery.py +117 -0
  90. snippbot/server.py +2346 -0
  91. snippbot/ui/_x9b404ae321ef/snake.html +283 -0
  92. snippbot/ui/_x9b404ae321ef/space-platformer.html +1132 -0
  93. snippbot/ui/_x9b404ae321ef/space_jumper.html +904 -0
  94. snippbot/ui/_x9b404ae321ef/tower-defense.html +928 -0
  95. snippbot/ui/assets/BrowserPage-BrImTNge.js +14 -0
  96. snippbot/ui/assets/BrowserPage-BrImTNge.js.map +1 -0
  97. snippbot/ui/assets/DeviceDetailPage-CCKl05v2.js +2 -0
  98. snippbot/ui/assets/DeviceDetailPage-CCKl05v2.js.map +1 -0
  99. snippbot/ui/assets/DevicesPage-BCSJuf6M.js +6 -0
  100. snippbot/ui/assets/DevicesPage-BCSJuf6M.js.map +1 -0
  101. snippbot/ui/assets/SandboxPage-CqC7ScBJ.js +27 -0
  102. snippbot/ui/assets/SandboxPage-CqC7ScBJ.js.map +1 -0
  103. snippbot/ui/assets/SecurityDashboardPage-qDDNLxfv.js +2 -0
  104. snippbot/ui/assets/SecurityDashboardPage-qDDNLxfv.js.map +1 -0
  105. snippbot/ui/assets/StepOutputPreview-BZV40eAE.css +1 -0
  106. snippbot/ui/assets/StepOutputPreview-DswkVuEc.js +13 -0
  107. snippbot/ui/assets/StepOutputPreview-DswkVuEc.js.map +1 -0
  108. snippbot/ui/assets/WorkflowBuilderPage-BrdjcQCK.js +367 -0
  109. snippbot/ui/assets/WorkflowBuilderPage-BrdjcQCK.js.map +1 -0
  110. snippbot/ui/assets/WorkflowRunPage-DfB_yOVw.js +2 -0
  111. snippbot/ui/assets/WorkflowRunPage-DfB_yOVw.js.map +1 -0
  112. snippbot/ui/assets/WorkflowsPage-C-0d296F.js +2 -0
  113. snippbot/ui/assets/WorkflowsPage-C-0d296F.js.map +1 -0
  114. snippbot/ui/assets/index-BKZiwq1o.js +2 -0
  115. snippbot/ui/assets/index-BKZiwq1o.js.map +1 -0
  116. snippbot/ui/assets/index-CqrRydCQ.js +1275 -0
  117. snippbot/ui/assets/index-CqrRydCQ.js.map +1 -0
  118. snippbot/ui/assets/index-vyj8Wd2X.css +1 -0
  119. snippbot/ui/assets/logo-alt-D_oF903h.png +0 -0
  120. snippbot/ui/assets/logo-alt-icon-Dt-XLW7h.png +0 -0
  121. snippbot/ui/assets/logo-d_fNF9Hk.png +0 -0
  122. snippbot/ui/assets/logo-icon-C0v6NQ9j.png +0 -0
  123. snippbot/ui/assets/state-BOtVR23t.js +26 -0
  124. snippbot/ui/assets/state-BOtVR23t.js.map +1 -0
  125. snippbot/ui/assets/stepIcons-BL48dla-.js +72 -0
  126. snippbot/ui/assets/stepIcons-BL48dla-.js.map +1 -0
  127. snippbot/ui/assets/ui-BDp1HqTB.js +10 -0
  128. snippbot/ui/assets/ui-BDp1HqTB.js.map +1 -0
  129. snippbot/ui/assets/vendor-C1G_MATc.js +60 -0
  130. snippbot/ui/assets/vendor-C1G_MATc.js.map +1 -0
  131. snippbot/ui/audio/ambient-setup.mp3 +0 -0
  132. snippbot/ui/favicon.png +0 -0
  133. snippbot/ui/frames/common.png +0 -0
  134. snippbot/ui/frames/epic.png +0 -0
  135. snippbot/ui/frames/legendary.png +0 -0
  136. snippbot/ui/frames/rare.png +0 -0
  137. snippbot/ui/frames/uncommon.png +0 -0
  138. snippbot/ui/game/title-bg.png +0 -0
  139. snippbot/ui/index.html +28 -0
  140. snippbot/ui/manifest.json +48 -0
  141. snippbot/ui/old_frames/common.png +0 -0
  142. snippbot/ui/old_frames/epic.png +0 -0
  143. snippbot/ui/old_frames/legendary.png +0 -0
  144. snippbot/ui/old_frames/rare.png +0 -0
  145. snippbot/ui/old_frames/uncommon.png +0 -0
  146. snippbot/ui/sw.js +137 -0
  147. snippbot/wallet/__init__.py +34 -0
  148. snippbot/wallet/manager.py +656 -0
  149. snippbot/websocket/__init__.py +1 -0
  150. snippbot/websocket/websocket_handler.py +640 -0
  151. snippbot-0.1.0b1.dist-info/METADATA +177 -0
  152. snippbot-0.1.0b1.dist-info/RECORD +708 -0
  153. snippbot-0.1.0b1.dist-info/WHEEL +4 -0
  154. snippbot-0.1.0b1.dist-info/entry_points.txt +2 -0
  155. snippbot_cli/__init__.py +7 -0
  156. snippbot_cli/checks.py +696 -0
  157. snippbot_cli/commands/__init__.py +1 -0
  158. snippbot_cli/commands/agents.py +200 -0
  159. snippbot_cli/commands/auth.py +361 -0
  160. snippbot_cli/commands/channel.py +147 -0
  161. snippbot_cli/commands/completions.py +102 -0
  162. snippbot_cli/commands/config.py +168 -0
  163. snippbot_cli/commands/device.py +241 -0
  164. snippbot_cli/commands/dispatcher.py +547 -0
  165. snippbot_cli/commands/doctor.py +148 -0
  166. snippbot_cli/commands/export.py +133 -0
  167. snippbot_cli/commands/marketplace.py +2686 -0
  168. snippbot_cli/commands/project.py +195 -0
  169. snippbot_cli/commands/reset.py +660 -0
  170. snippbot_cli/commands/secrets.py +263 -0
  171. snippbot_cli/commands/security.py +432 -0
  172. snippbot_cli/commands/setup.py +366 -0
  173. snippbot_cli/commands/start.py +123 -0
  174. snippbot_cli/commands/status.py +92 -0
  175. snippbot_cli/commands/stop.py +167 -0
  176. snippbot_cli/daemon_client.py +101 -0
  177. snippbot_cli/main.py +47 -0
  178. snippbot_cli/user_config.py +437 -0
  179. snippbot_core/__init__.py +22 -0
  180. snippbot_core/agent_settings.py +188 -0
  181. snippbot_core/approvals.py +513 -0
  182. snippbot_core/assets/__init__.py +10 -0
  183. snippbot_core/assets/folder_store.py +403 -0
  184. snippbot_core/assets/store.py +383 -0
  185. snippbot_core/audio/__init__.py +15 -0
  186. snippbot_core/audio/models.py +98 -0
  187. snippbot_core/audio/providers/__init__.py +4 -0
  188. snippbot_core/audio/providers/base.py +55 -0
  189. snippbot_core/audio/providers/elevenlabs_stt.py +88 -0
  190. snippbot_core/audio/providers/elevenlabs_tts.py +153 -0
  191. snippbot_core/audio/providers/hume_tts.py +172 -0
  192. snippbot_core/audio/providers/local_stt.py +106 -0
  193. snippbot_core/audio/providers/local_tts.py +164 -0
  194. snippbot_core/audio/providers/openai_stt.py +76 -0
  195. snippbot_core/audio/providers/openai_tts.py +110 -0
  196. snippbot_core/audio/service.py +191 -0
  197. snippbot_core/audio/settings.py +96 -0
  198. snippbot_core/audit_service.py +367 -0
  199. snippbot_core/autonomy/__init__.py +22 -0
  200. snippbot_core/autonomy/bridge.py +140 -0
  201. snippbot_core/browser_settings.py +306 -0
  202. snippbot_core/capability_inference.py +131 -0
  203. snippbot_core/channel_store.py +1070 -0
  204. snippbot_core/chat/__init__.py +77 -0
  205. snippbot_core/chat/agent_store.py +585 -0
  206. snippbot_core/chat/agent_sync.py +244 -0
  207. snippbot_core/chat/chat_store.py +1410 -0
  208. snippbot_core/chat/command_processor.py +684 -0
  209. snippbot_core/chat/complexity_classifier.py +126 -0
  210. snippbot_core/chat/context_builder.py +721 -0
  211. snippbot_core/chat/context_refs.py +626 -0
  212. snippbot_core/chat/context_window.py +1500 -0
  213. snippbot_core/chat/gateway_client.py +626 -0
  214. snippbot_core/chat/model_router.py +217 -0
  215. snippbot_core/chat/models.py +190 -0
  216. snippbot_core/chat/prepare.py +1116 -0
  217. snippbot_core/chat/providers/__init__.py +33 -0
  218. snippbot_core/chat/providers/anthropic.py +213 -0
  219. snippbot_core/chat/providers/anthropic_api.py +846 -0
  220. snippbot_core/chat/providers/auth_checks.py +57 -0
  221. snippbot_core/chat/providers/base.py +120 -0
  222. snippbot_core/chat/providers/claude_native.py +2073 -0
  223. snippbot_core/chat/providers/custom.py +391 -0
  224. snippbot_core/chat/providers/deepseek.py +64 -0
  225. snippbot_core/chat/providers/gemini.py +577 -0
  226. snippbot_core/chat/providers/grok.py +25 -0
  227. snippbot_core/chat/providers/groq.py +23 -0
  228. snippbot_core/chat/providers/mistral.py +19 -0
  229. snippbot_core/chat/providers/openai.py +19 -0
  230. snippbot_core/chat/providers/openai_compat.py +522 -0
  231. snippbot_core/chat/providers/openrouter.py +36 -0
  232. snippbot_core/chat/providers/process_manager.py +538 -0
  233. snippbot_core/chat/providers/registry.py +473 -0
  234. snippbot_core/chat/roster.py +501 -0
  235. snippbot_core/chat/router_settings.py +218 -0
  236. snippbot_core/chat/routing.py +226 -0
  237. snippbot_core/chat/runtime_context.py +230 -0
  238. snippbot_core/chat/session.py +1001 -0
  239. snippbot_core/chat/session_context.py +90 -0
  240. snippbot_core/chat/sse.py +74 -0
  241. snippbot_core/chat/token_counter.py +426 -0
  242. snippbot_core/chat/tool_dispatch.py +364 -0
  243. snippbot_core/chat/tool_profile_merge.py +42 -0
  244. snippbot_core/chat/tool_result_aging.py +334 -0
  245. snippbot_core/chat/turn.py +1004 -0
  246. snippbot_core/chat/turn_orchestrator.py +113 -0
  247. snippbot_core/claude_cli.py +140 -0
  248. snippbot_core/compat.py +26 -0
  249. snippbot_core/config.py +772 -0
  250. snippbot_core/dag_builder.py +796 -0
  251. snippbot_core/db/__init__.py +17 -0
  252. snippbot_core/db/migrations.py +239 -0
  253. snippbot_core/device/__init__.py +25 -0
  254. snippbot_core/device/auth.py +1114 -0
  255. snippbot_core/device/capabilities.py +320 -0
  256. snippbot_core/device/execution.py +274 -0
  257. snippbot_core/device/file_transfer.py +507 -0
  258. snippbot_core/device/groups.py +251 -0
  259. snippbot_core/device/health.py +230 -0
  260. snippbot_core/device/manager.py +232 -0
  261. snippbot_core/device/pairing.py +380 -0
  262. snippbot_core/device/protocol.py +1499 -0
  263. snippbot_core/device/queue.py +193 -0
  264. snippbot_core/device/registry.py +1633 -0
  265. snippbot_core/device/router.py +278 -0
  266. snippbot_core/digest/__init__.py +18 -0
  267. snippbot_core/digest/composer.py +158 -0
  268. snippbot_core/digest/scheduler.py +187 -0
  269. snippbot_core/digest/store.py +189 -0
  270. snippbot_core/dispatch/__init__.py +131 -0
  271. snippbot_core/dispatch/bridge.py +347 -0
  272. snippbot_core/dispatch/decision_log.py +230 -0
  273. snippbot_core/dispatch/dispatcher.py +376 -0
  274. snippbot_core/dispatch/executors.py +447 -0
  275. snippbot_core/dispatch/idempotency.py +95 -0
  276. snippbot_core/dispatch/inject.py +244 -0
  277. snippbot_core/dispatch/policy_gate.py +82 -0
  278. snippbot_core/dispatch/proactive.py +379 -0
  279. snippbot_core/dispatch/proactivity_gate.py +92 -0
  280. snippbot_core/dispatch/settings.py +459 -0
  281. snippbot_core/dispatch/shadow.py +246 -0
  282. snippbot_core/dispatch/tier1_deterministic.py +100 -0
  283. snippbot_core/dispatch/tier2_policy.py +65 -0
  284. snippbot_core/dispatch/tier3_classifier.py +359 -0
  285. snippbot_core/dispatch/tier3_provider.py +386 -0
  286. snippbot_core/dispatch/tier4_fallback.py +67 -0
  287. snippbot_core/dispatch/types.py +218 -0
  288. snippbot_core/email/__init__.py +32 -0
  289. snippbot_core/email/config.py +29 -0
  290. snippbot_core/email/imap_client.py +317 -0
  291. snippbot_core/email/insight_email_scheduler.py +169 -0
  292. snippbot_core/email/service.py +156 -0
  293. snippbot_core/email/store.py +187 -0
  294. snippbot_core/email/subscriber.py +147 -0
  295. snippbot_core/email/templates.py +195 -0
  296. snippbot_core/event_bus.py +342 -0
  297. snippbot_core/events.py +1271 -0
  298. snippbot_core/execution_orchestrator.py +1401 -0
  299. snippbot_core/execution_store.py +1225 -0
  300. snippbot_core/executor.py +682 -0
  301. snippbot_core/files/__init__.py +18 -0
  302. snippbot_core/files/models.py +81 -0
  303. snippbot_core/files/pdf_extractor.py +77 -0
  304. snippbot_core/files/storage.py +201 -0
  305. snippbot_core/files/store.py +373 -0
  306. snippbot_core/game/__init__.py +10 -0
  307. snippbot_core/game/combat.py +624 -0
  308. snippbot_core/game/engine.py +358 -0
  309. snippbot_core/game/game_save_store.py +256 -0
  310. snippbot_core/game/gm_prompts.py +365 -0
  311. snippbot_core/game/world_store.py +1031 -0
  312. snippbot_core/history.py +797 -0
  313. snippbot_core/hooks/__init__.py +46 -0
  314. snippbot_core/hooks/analytics.py +346 -0
  315. snippbot_core/hooks/bridge.py +105 -0
  316. snippbot_core/hooks/bundled/__init__.py +122 -0
  317. snippbot_core/hooks/bundled/audit_logger.py +111 -0
  318. snippbot_core/hooks/bundled/boot_md.py +149 -0
  319. snippbot_core/hooks/bundled/context_files.py +144 -0
  320. snippbot_core/hooks/bundled/dispatcher_audit.py +93 -0
  321. snippbot_core/hooks/bundled/session_memory.py +110 -0
  322. snippbot_core/hooks/chains.py +208 -0
  323. snippbot_core/hooks/dispatcher.py +314 -0
  324. snippbot_core/hooks/engine.py +531 -0
  325. snippbot_core/hooks/executor.py +530 -0
  326. snippbot_core/hooks/filters.py +314 -0
  327. snippbot_core/hooks/models.py +441 -0
  328. snippbot_core/hooks/sandbox.py +222 -0
  329. snippbot_core/hooks/sdk.py +280 -0
  330. snippbot_core/hooks/store.py +1502 -0
  331. snippbot_core/hooks/webhook_auth.py +237 -0
  332. snippbot_core/idle_messages.py +59 -0
  333. snippbot_core/image/__init__.py +0 -0
  334. snippbot_core/image/generate.py +149 -0
  335. snippbot_core/image/ocr.py +113 -0
  336. snippbot_core/image/scene_prompts.py +129 -0
  337. snippbot_core/image/vision.py +324 -0
  338. snippbot_core/insight_generator.py +2176 -0
  339. snippbot_core/insight_queue.py +1278 -0
  340. snippbot_core/interface_settings.py +263 -0
  341. snippbot_core/issues/__init__.py +50 -0
  342. snippbot_core/issues/investigator.py +471 -0
  343. snippbot_core/issues/magic_link.py +86 -0
  344. snippbot_core/issues/models.py +314 -0
  345. snippbot_core/issues/notifier.py +175 -0
  346. snippbot_core/issues/security_audit.py +295 -0
  347. snippbot_core/issues/snapshot.py +67 -0
  348. snippbot_core/issues/store.py +1056 -0
  349. snippbot_core/license/__init__.py +14 -0
  350. snippbot_core/license/models.py +103 -0
  351. snippbot_core/license/store.py +136 -0
  352. snippbot_core/license/validator.py +155 -0
  353. snippbot_core/marketplace/__init__.py +64 -0
  354. snippbot_core/marketplace/agent_config_support.py +428 -0
  355. snippbot_core/marketplace/auto_updater.py +636 -0
  356. snippbot_core/marketplace/bundled.py +236 -0
  357. snippbot_core/marketplace/channel_support.py +201 -0
  358. snippbot_core/marketplace/essentials.py +45 -0
  359. snippbot_core/marketplace/essentials.toml +29 -0
  360. snippbot_core/marketplace/hook_support.py +302 -0
  361. snippbot_core/marketplace/ignore.py +203 -0
  362. snippbot_core/marketplace/installer.py +2015 -0
  363. snippbot_core/marketplace/job_support.py +251 -0
  364. snippbot_core/marketplace/manifest.py +953 -0
  365. snippbot_core/marketplace/manifest_facts.py +333 -0
  366. snippbot_core/marketplace/mcp_support.py +195 -0
  367. snippbot_core/marketplace/oauth_manager.py +534 -0
  368. snippbot_core/marketplace/oauth_providers.py +83 -0
  369. snippbot_core/marketplace/packaging.py +522 -0
  370. snippbot_core/marketplace/permission_grants.py +377 -0
  371. snippbot_core/marketplace/registry_store.py +428 -0
  372. snippbot_core/marketplace/sandbox_support.py +359 -0
  373. snippbot_core/marketplace/tool_support.py +997 -0
  374. snippbot_core/marketplace/workflow_support.py +828 -0
  375. snippbot_core/mcp/__init__.py +27 -0
  376. snippbot_core/mcp/bridge_registry.py +140 -0
  377. snippbot_core/mcp/catalog.py +202 -0
  378. snippbot_core/mcp/catalog_data.json +492 -0
  379. snippbot_core/mcp/client.py +651 -0
  380. snippbot_core/mcp/manager.py +368 -0
  381. snippbot_core/mcp/oauth.py +195 -0
  382. snippbot_core/mcp/sandbox.py +408 -0
  383. snippbot_core/mcp/tool_bridge_server.py +786 -0
  384. snippbot_core/memory/__init__.py +97 -0
  385. snippbot_core/memory/audit_log.py +338 -0
  386. snippbot_core/memory/clustering.py +349 -0
  387. snippbot_core/memory/consolidation.py +822 -0
  388. snippbot_core/memory/episodic.py +1097 -0
  389. snippbot_core/memory/forgetting.py +644 -0
  390. snippbot_core/memory/hybrid_search.py +697 -0
  391. snippbot_core/memory/keyword_search.py +622 -0
  392. snippbot_core/memory/knowledge_graph.py +1547 -0
  393. snippbot_core/memory/llm_extraction.py +718 -0
  394. snippbot_core/memory/recall_feedback.py +203 -0
  395. snippbot_core/memory/sensory_buffer.py +214 -0
  396. snippbot_core/memory/session.py +143 -0
  397. snippbot_core/memory/vector_index.py +740 -0
  398. snippbot_core/memory/write_pipeline.py +628 -0
  399. snippbot_core/memory_search.py +339 -0
  400. snippbot_core/memory_settings.py +482 -0
  401. snippbot_core/models_dev_catalog.py +387 -0
  402. snippbot_core/multimodal/__init__.py +44 -0
  403. snippbot_core/multimodal/image_blocks.py +230 -0
  404. snippbot_core/node_registry.py +399 -0
  405. snippbot_core/package_builder/__init__.py +70 -0
  406. snippbot_core/package_builder/engine.py +2116 -0
  407. snippbot_core/package_builder/fix_verifier.py +510 -0
  408. snippbot_core/package_builder/packager.py +100 -0
  409. snippbot_core/package_builder/plan.py +680 -0
  410. snippbot_core/package_builder/plugins/__init__.py +190 -0
  411. snippbot_core/package_builder/plugins/hook_plugin.py +389 -0
  412. snippbot_core/package_builder/plugins/mcp_server_plugin.py +497 -0
  413. snippbot_core/package_builder/plugins/tool_plugin.py +535 -0
  414. snippbot_core/package_builder/plugins/workflow_plugin.py +732 -0
  415. snippbot_core/package_builder/quality_audit.py +547 -0
  416. snippbot_core/package_builder/rate_limiter.py +242 -0
  417. snippbot_core/package_builder/sandbox.py +399 -0
  418. snippbot_core/package_builder/session.py +424 -0
  419. snippbot_core/package_builder/spec.py +246 -0
  420. snippbot_core/package_builder/store.py +582 -0
  421. snippbot_core/package_builder/system_prompt.py +561 -0
  422. snippbot_core/package_builder/telemetry.py +234 -0
  423. snippbot_core/payments/__init__.py +26 -0
  424. snippbot_core/payments/spend_authorization.py +231 -0
  425. snippbot_core/permissions.py +1099 -0
  426. snippbot_core/proactivity.py +975 -0
  427. snippbot_core/proactivity_settings.py +142 -0
  428. snippbot_core/profile_settings.py +299 -0
  429. snippbot_core/project_store.py +1349 -0
  430. snippbot_core/projects/__init__.py +7 -0
  431. snippbot_core/projects/plan_buffer.py +177 -0
  432. snippbot_core/projects/refine_agent.py +597 -0
  433. snippbot_core/projects/refine_lock.py +82 -0
  434. snippbot_core/projects/refine_session_registry.py +99 -0
  435. snippbot_core/projects/refine_tools.py +177 -0
  436. snippbot_core/provider_settings.py +555 -0
  437. snippbot_core/provider_sync.py +1082 -0
  438. snippbot_core/provider_sync_task.py +150 -0
  439. snippbot_core/providers.py +141 -0
  440. snippbot_core/push/__init__.py +30 -0
  441. snippbot_core/push/models.py +147 -0
  442. snippbot_core/push/service.py +393 -0
  443. snippbot_core/push/store.py +451 -0
  444. snippbot_core/py.typed +0 -0
  445. snippbot_core/remote_session/__init__.py +111 -0
  446. snippbot_core/remote_session/fan_out.py +287 -0
  447. snippbot_core/remote_session/manager.py +1010 -0
  448. snippbot_core/remote_session/models.py +361 -0
  449. snippbot_core/remote_session/security_gate.py +753 -0
  450. snippbot_core/remote_session/security_store.py +1393 -0
  451. snippbot_core/remote_session/settings.py +306 -0
  452. snippbot_core/remote_session/store.py +1041 -0
  453. snippbot_core/risk_scorer.py +512 -0
  454. snippbot_core/sandbox/__init__.py +66 -0
  455. snippbot_core/sandbox/audit.py +630 -0
  456. snippbot_core/sandbox/config.py +400 -0
  457. snippbot_core/sandbox/dockerfiles/Dockerfile.base +7 -0
  458. snippbot_core/sandbox/dockerfiles/Dockerfile.datascience +11 -0
  459. snippbot_core/sandbox/dockerfiles/Dockerfile.node +9 -0
  460. snippbot_core/sandbox/dockerfiles/Dockerfile.python +9 -0
  461. snippbot_core/sandbox/dockerfiles/Dockerfile.rust +9 -0
  462. snippbot_core/sandbox/gpu.py +311 -0
  463. snippbot_core/sandbox/manager.py +1317 -0
  464. snippbot_core/sandbox/network.py +863 -0
  465. snippbot_core/sandbox/pool.py +242 -0
  466. snippbot_core/sandbox/runtime/__init__.py +20 -0
  467. snippbot_core/sandbox/runtime/base.py +390 -0
  468. snippbot_core/sandbox/runtime/docker.py +1133 -0
  469. snippbot_core/sandbox/runtime/podman.py +1207 -0
  470. snippbot_core/sandbox/runtime/process.py +985 -0
  471. snippbot_core/sandbox/smart.py +205 -0
  472. snippbot_core/sandbox/snapshot.py +520 -0
  473. snippbot_core/sandbox/templates.py +894 -0
  474. snippbot_core/scheduler/__init__.py +31 -0
  475. snippbot_core/scheduler/chains.py +372 -0
  476. snippbot_core/scheduler/chat_commands.py +247 -0
  477. snippbot_core/scheduler/conditions.py +509 -0
  478. snippbot_core/scheduler/delivery.py +1058 -0
  479. snippbot_core/scheduler/engine.py +652 -0
  480. snippbot_core/scheduler/executor.py +953 -0
  481. snippbot_core/scheduler/models.py +493 -0
  482. snippbot_core/scheduler/nl_parser.py +716 -0
  483. snippbot_core/scheduler/nl_patterns.py +220 -0
  484. snippbot_core/scheduler/retry.py +102 -0
  485. snippbot_core/scheduler/schedule_types.py +431 -0
  486. snippbot_core/scheduler/sessions.py +174 -0
  487. snippbot_core/scheduler/skills_loader.py +366 -0
  488. snippbot_core/scheduler/smart.py +203 -0
  489. snippbot_core/scheduler/store.py +1382 -0
  490. snippbot_core/security/__init__.py +219 -0
  491. snippbot_core/security/csrf.py +323 -0
  492. snippbot_core/security/db_encryption.py +420 -0
  493. snippbot_core/security/dep_scanner.py +233 -0
  494. snippbot_core/security/dlp.py +374 -0
  495. snippbot_core/security/egress.py +312 -0
  496. snippbot_core/security/env_filter.py +157 -0
  497. snippbot_core/security/error_sanitizer.py +57 -0
  498. snippbot_core/security/file_permissions.py +267 -0
  499. snippbot_core/security/input_validator.py +155 -0
  500. snippbot_core/security/master_key.py +303 -0
  501. snippbot_core/security/mcp_validation.py +178 -0
  502. snippbot_core/security/package_audit.py +1176 -0
  503. snippbot_core/security/package_signing.py +631 -0
  504. snippbot_core/security/prompt_injection.py +527 -0
  505. snippbot_core/security/rate_limiter.py +348 -0
  506. snippbot_core/security/registry_security.py +281 -0
  507. snippbot_core/security/sandbox_profile.py +314 -0
  508. snippbot_core/security/scanner.py +1027 -0
  509. snippbot_core/security/secret_store.py +578 -0
  510. snippbot_core/security/skill_vetting.py +551 -0
  511. snippbot_core/security/tls.py +192 -0
  512. snippbot_core/security/url_validation.py +259 -0
  513. snippbot_core/security/workflow_audit.py +406 -0
  514. snippbot_core/security_settings.py +339 -0
  515. snippbot_core/settings_manager.py +279 -0
  516. snippbot_core/settings_store.py +79 -0
  517. snippbot_core/skill_builder/__init__.py +53 -0
  518. snippbot_core/skill_builder/engine.py +13 -0
  519. snippbot_core/skill_builder/packager.py +7 -0
  520. snippbot_core/skill_builder/session.py +7 -0
  521. snippbot_core/skill_builder/spec.py +7 -0
  522. snippbot_core/skill_builder/system_prompt.py +7 -0
  523. snippbot_core/skills_store.py +950 -0
  524. snippbot_core/snipp/__init__.py +110 -0
  525. snippbot_core/snipp/balance.py +403 -0
  526. snippbot_core/snipp/encrypted_storage.py +479 -0
  527. snippbot_core/snipp/recovery_phrase.py +459 -0
  528. snippbot_core/snipp/signing.py +478 -0
  529. snippbot_core/snipp/wallet.py +340 -0
  530. snippbot_core/strategy.py +231 -0
  531. snippbot_core/sub_agent/__init__.py +57 -0
  532. snippbot_core/sub_agent/aggregator.py +376 -0
  533. snippbot_core/sub_agent/concurrency.py +200 -0
  534. snippbot_core/sub_agent/event_digest.py +176 -0
  535. snippbot_core/sub_agent/lifecycle.py +534 -0
  536. snippbot_core/sub_agent/messaging.py +336 -0
  537. snippbot_core/sub_agent/models.py +545 -0
  538. snippbot_core/sub_agent/orchestrator.py +639 -0
  539. snippbot_core/sub_agent/resource_manager.py +227 -0
  540. snippbot_core/sub_agent/role_prompts.py +242 -0
  541. snippbot_core/sub_agent/session.py +1340 -0
  542. snippbot_core/sub_agent/shared_context.py +238 -0
  543. snippbot_core/sub_agent/spawner.py +264 -0
  544. snippbot_core/sub_agent/store.py +987 -0
  545. snippbot_core/sub_agent/team_models.py +123 -0
  546. snippbot_core/sub_agent/team_prompts.py +188 -0
  547. snippbot_core/subprocess_utils.py +80 -0
  548. snippbot_core/sync.py +222 -0
  549. snippbot_core/task_executor.py +1703 -0
  550. snippbot_core/thinking/__init__.py +25 -0
  551. snippbot_core/thinking/daemon.py +955 -0
  552. snippbot_core/thinking/engagement.py +261 -0
  553. snippbot_core/thinking/engine.py +632 -0
  554. snippbot_core/thinking/models.py +251 -0
  555. snippbot_core/thinking/store.py +626 -0
  556. snippbot_core/thinking/triggers.py +193 -0
  557. snippbot_core/tier_inference.py +187 -0
  558. snippbot_core/tools/__init__.py +22 -0
  559. snippbot_core/tools/_python_dispatcher.py +226 -0
  560. snippbot_core/tools/browser/__init__.py +14 -0
  561. snippbot_core/tools/browser/actions.py +438 -0
  562. snippbot_core/tools/browser/auth_manager.py +439 -0
  563. snippbot_core/tools/browser/device_emulation.py +378 -0
  564. snippbot_core/tools/browser/dom_snapshot.py +722 -0
  565. snippbot_core/tools/browser/exceptions.py +126 -0
  566. snippbot_core/tools/browser/file_handler.py +261 -0
  567. snippbot_core/tools/browser/live_stream.py +293 -0
  568. snippbot_core/tools/browser/manager.py +884 -0
  569. snippbot_core/tools/browser/network_manager.py +511 -0
  570. snippbot_core/tools/browser/profiles.py +438 -0
  571. snippbot_core/tools/browser/recorder.py +514 -0
  572. snippbot_core/tools/browser/screen_recorder.py +995 -0
  573. snippbot_core/tools/browser/ssrf_guard.py +331 -0
  574. snippbot_core/tools/browser/stealth.py +141 -0
  575. snippbot_core/tools/browser/tab_manager.py +277 -0
  576. snippbot_core/tools/browser_manager.py +9 -0
  577. snippbot_core/tools/channel_sender.py +406 -0
  578. snippbot_core/tools/definitions.py +1681 -0
  579. snippbot_core/tools/execution_mode.py +104 -0
  580. snippbot_core/tools/executor.py +4917 -0
  581. snippbot_core/tools/invocation_log.py +182 -0
  582. snippbot_core/tools/marketplace_types.py +39 -0
  583. snippbot_core/tools/registry.py +584 -0
  584. snippbot_core/tools/search_providers.py +349 -0
  585. snippbot_core/tools/summarization.py +255 -0
  586. snippbot_core/tools/workflow_dispatch.py +296 -0
  587. snippbot_core/user_model.py +1709 -0
  588. snippbot_core/worker_pool.py +118 -0
  589. snippbot_core/workflows/__init__.py +51 -0
  590. snippbot_core/workflows/dry_run.py +211 -0
  591. snippbot_core/workflows/dsl.py +365 -0
  592. snippbot_core/workflows/executor.py +825 -0
  593. snippbot_core/workflows/models.py +675 -0
  594. snippbot_core/workflows/parser.py +144 -0
  595. snippbot_core/workflows/run_store.py +556 -0
  596. snippbot_core/workflows/scheduler.py +455 -0
  597. snippbot_core/workflows/schema.py +616 -0
  598. snippbot_core/workflows/state_machine.py +277 -0
  599. snippbot_core/workflows/step_executors/__init__.py +502 -0
  600. snippbot_core/workflows/step_executors/approval_gate.py +197 -0
  601. snippbot_core/workflows/step_executors/conditional.py +103 -0
  602. snippbot_core/workflows/step_executors/llm_step.py +405 -0
  603. snippbot_core/workflows/step_executors/loop_step.py +157 -0
  604. snippbot_core/workflows/step_executors/parallel_step.py +197 -0
  605. snippbot_core/workflows/step_executors/subworkflow.py +116 -0
  606. snippbot_core/workflows/step_executors/tool_step.py +138 -0
  607. snippbot_core/workflows/store.py +568 -0
  608. snippbot_core/workflows/template_store.py +590 -0
  609. snippbot_core/workflows/version_control.py +149 -0
  610. snippbot_core/workspace/__init__.py +67 -0
  611. snippbot_core/workspace/evolution.py +278 -0
  612. snippbot_core/workspace/identity_proposals.py +362 -0
  613. snippbot_core/workspace/pending_writes.py +256 -0
  614. snippbot_core/workspace/tools.py +877 -0
  615. snippbot_core/workspace_manager.py +493 -0
  616. vps/__init__.py +7 -0
  617. vps/config/strategy-presets.json +34 -0
  618. vps/data/.gitkeep +0 -0
  619. vps/data/memory_schema.sql +80 -0
  620. vps/data/schema.sql +268 -0
  621. vps/data/user_model_schema.sql +125 -0
  622. vps/lib/__init__.py +101 -0
  623. vps/lib/adaptive_throttle.py +976 -0
  624. vps/lib/agent_workspace.py +731 -0
  625. vps/lib/anticipation.py +1264 -0
  626. vps/lib/api_handler.py +2586 -0
  627. vps/lib/approvals.py +505 -0
  628. vps/lib/auth.py +970 -0
  629. vps/lib/awakening_api.py +639 -0
  630. vps/lib/benchmark.py +351 -0
  631. vps/lib/chaos.py +959 -0
  632. vps/lib/checkpoint.py +495 -0
  633. vps/lib/config.py +227 -0
  634. vps/lib/connection_pool.py +658 -0
  635. vps/lib/credentials.py +839 -0
  636. vps/lib/daemon.py +437 -0
  637. vps/lib/dag_builder.py +871 -0
  638. vps/lib/delight_tracker.py +707 -0
  639. vps/lib/error_handler.py +880 -0
  640. vps/lib/event_bus.py +341 -0
  641. vps/lib/events.py +354 -0
  642. vps/lib/executor.py +681 -0
  643. vps/lib/expertise_detector.py +716 -0
  644. vps/lib/failure_handler.py +946 -0
  645. vps/lib/failure_learning.py +1194 -0
  646. vps/lib/feedback_learner.py +1033 -0
  647. vps/lib/focus_mode.py +1047 -0
  648. vps/lib/frustration_detector.py +713 -0
  649. vps/lib/history.py +837 -0
  650. vps/lib/idle_daemon.py +640 -0
  651. vps/lib/insight_delivery.py +863 -0
  652. vps/lib/insight_queue.py +1070 -0
  653. vps/lib/interest_detector.py +750 -0
  654. vps/lib/memory/__init__.py +49 -0
  655. vps/lib/memory/consolidation.py +643 -0
  656. vps/lib/memory/episodic.py +954 -0
  657. vps/lib/memory/forgetting.py +644 -0
  658. vps/lib/memory/hybrid_search.py +697 -0
  659. vps/lib/memory/keyword_search.py +565 -0
  660. vps/lib/memory/knowledge_graph.py +1242 -0
  661. vps/lib/memory/vector_index.py +734 -0
  662. vps/lib/memory/write_pipeline.py +622 -0
  663. vps/lib/memory_search.py +326 -0
  664. vps/lib/metrics.py +513 -0
  665. vps/lib/parallel_executor.py +871 -0
  666. vps/lib/permissions.py +1044 -0
  667. vps/lib/preference_learner.py +714 -0
  668. vps/lib/proactivity.py +780 -0
  669. vps/lib/profiling.py +659 -0
  670. vps/lib/project_store.py +926 -0
  671. vps/lib/prometheus.py +596 -0
  672. vps/lib/query_optimizer.py +700 -0
  673. vps/lib/rate_limiter.py +416 -0
  674. vps/lib/recovery.py +813 -0
  675. vps/lib/replay.py +933 -0
  676. vps/lib/request_validator.py +665 -0
  677. vps/lib/resource_limits.py +458 -0
  678. vps/lib/risk_scorer.py +512 -0
  679. vps/lib/room_reader.py +690 -0
  680. vps/lib/sandbox.py +1159 -0
  681. vps/lib/snipp/__init__.py +497 -0
  682. vps/lib/snipp/aggregation.py +449 -0
  683. vps/lib/snipp/api_interceptor.py +494 -0
  684. vps/lib/snipp/audit.py +459 -0
  685. vps/lib/snipp/balance.py +379 -0
  686. vps/lib/snipp/encrypted_storage.py +479 -0
  687. vps/lib/snipp/escrow.py +932 -0
  688. vps/lib/snipp/rating.py +855 -0
  689. vps/lib/snipp/receipt.py +400 -0
  690. vps/lib/snipp/receipt_store.py +554 -0
  691. vps/lib/snipp/recovery_phrase.py +459 -0
  692. vps/lib/snipp/reputation_sync.py +544 -0
  693. vps/lib/snipp/request_router.py +961 -0
  694. vps/lib/snipp/revision_handler.py +550 -0
  695. vps/lib/snipp/reward_callback.py +549 -0
  696. vps/lib/snipp/service_registry.py +812 -0
  697. vps/lib/snipp/signing.py +368 -0
  698. vps/lib/snipp/snipp_client.py +567 -0
  699. vps/lib/snipp/utils.py +152 -0
  700. vps/lib/snipp/wallet.py +243 -0
  701. vps/lib/snipp/work_claim.py +550 -0
  702. vps/lib/speculative.py +802 -0
  703. vps/lib/strategy.py +231 -0
  704. vps/lib/style_adapter.py +661 -0
  705. vps/lib/tool_selector.py +1121 -0
  706. vps/lib/user_goals_api.py +824 -0
  707. vps/lib/user_model.py +1563 -0
  708. vps/lib/websocket_handler.py +545 -0
snippbot/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Snippbot - Autonomous AI agents for your local machine.
2
+
3
+ This is the main package for running Snippbot locally. It provides:
4
+ - HTTP API server for the UI and external integrations
5
+ - WebSocket server for real-time updates
6
+ - Background daemon for autonomous task execution
7
+
8
+ Usage:
9
+ snippbot start # Start the daemon
10
+ snippbot status # Check daemon status
11
+ snippbot config set ... # Configure settings
12
+ """
13
+
14
+ __version__ = "0.1.0b1"
15
+
16
+ from snippbot.server import create_app, run_server
17
+
18
+ __all__ = ["__version__", "create_app", "run_server"]
snippbot/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow running as: python -m snippbot"""
2
+
3
+ from snippbot.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,16 @@
1
+ """REST API handlers.
2
+
3
+ This module provides the HTTP API for Snippbot:
4
+ - /api/projects - Project management
5
+ - /api/tasks - Task management
6
+ - /api/memory - Memory search and episodes
7
+ - /api/insights - Proactive insights
8
+ - /api/goals - Goal tracking
9
+ - /api/snipp - Token economy
10
+ - /api/daemon - Daemon control
11
+ """
12
+
13
+ from snippbot.api.routes import api_routes
14
+ from snippbot.api.health import health_routes
15
+
16
+ __all__ = ["api_routes", "health_routes"]
snippbot/api/agents.py ADDED
@@ -0,0 +1,603 @@
1
+ """Agent profile and conversation roster API endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Any, Optional
11
+
12
+ from starlette.requests import Request
13
+ from starlette.responses import JSONResponse
14
+ from starlette.routing import Route
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # PROFILE.md metadata extraction — used to enrich gateway agents (which only
21
+ # expose `role` from their own parser) with archetype + expertise tags.
22
+ # ---------------------------------------------------------------------------
23
+
24
+ _PROFILE_SEARCH_DIRS: list[Path] = []
25
+
26
+
27
+ def _profile_search_dirs() -> list[Path]:
28
+ """Directories to scan for `<agent_id>/workspace/PROFILE.md`.
29
+
30
+ Matches the gateway's discovery paths (see claude_gateway.config.discover_agents):
31
+ repo-root `vps/agents/` plus the two per-user install locations.
32
+ `parents[5]` resolves to the monorepo root when this file lives at
33
+ `packages/local/src/snippbot/api/agents.py`.
34
+ """
35
+ global _PROFILE_SEARCH_DIRS
36
+ if _PROFILE_SEARCH_DIRS:
37
+ return _PROFILE_SEARCH_DIRS
38
+ here = Path(__file__).resolve()
39
+ candidates = [
40
+ here.parents[5] / "vps" / "agents" if len(here.parents) >= 6 else None,
41
+ Path.home() / ".snippai" / "agents",
42
+ Path.home() / ".snippbot" / "agents",
43
+ Path.home() / "projects" / "snippbot" / "vps" / "agents",
44
+ ]
45
+ _PROFILE_SEARCH_DIRS = [p for p in candidates if p is not None and p.exists()]
46
+ return _PROFILE_SEARCH_DIRS
47
+
48
+
49
+ def _read_profile_metadata(agent_id: str) -> dict[str, Any]:
50
+ """Read `- **Field:** value` lines from an agent's profile.
51
+
52
+ Tries IDENTITY.md (post-Phase-5 canonical), then PROFILE.md (v1).
53
+ Extracts Role, Archetype, Vibe, and an optional Expertise line
54
+ (comma-separated tags). Returns an empty dict if neither file can be
55
+ found or read — callers must not rely on any field being present.
56
+ """
57
+ for base in _profile_search_dirs():
58
+ workspace = base / agent_id / "workspace"
59
+ for filename in ("IDENTITY.md", "PROFILE.md"):
60
+ profile_path = workspace / filename
61
+ if not profile_path.is_file():
62
+ continue
63
+ try:
64
+ text = profile_path.read_text(encoding="utf-8", errors="replace")
65
+ except OSError:
66
+ continue
67
+
68
+ meta: dict[str, Any] = {}
69
+ for line in text.splitlines():
70
+ m = re.match(r"^\s*-\s+\*\*([A-Za-z][A-Za-z ]*):\*\*\s*(.*)$", line)
71
+ if not m:
72
+ continue
73
+ field = m.group(1).strip().lower()
74
+ value = m.group(2).strip()
75
+ if not value:
76
+ continue
77
+ if field == "role":
78
+ meta["role"] = value
79
+ elif field == "archetype":
80
+ meta["archetype"] = value
81
+ elif field == "vibe":
82
+ meta["vibe"] = value
83
+ elif field == "expertise":
84
+ tags: list[str] = []
85
+ seen: set[str] = set()
86
+ for raw in value.split(","):
87
+ tag = raw.strip()
88
+ if tag and tag.lower() not in seen:
89
+ seen.add(tag.lower())
90
+ tags.append(tag)
91
+ if tags:
92
+ meta["expertise_tags"] = tags
93
+ return meta
94
+ return {}
95
+
96
+
97
+ def _compose_short_bio(role: Optional[str], archetype: Optional[str]) -> Optional[str]:
98
+ """Blend role + archetype into a single short_bio string for the popover."""
99
+ role = (role or "").strip() or None
100
+ archetype = (archetype or "").strip() or None
101
+ if role and archetype:
102
+ return f"{role} — {archetype}"
103
+ return role or archetype
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Lazy singletons (same pattern as chat.py)
107
+ # ---------------------------------------------------------------------------
108
+
109
+ _agent_store = None
110
+ _roster_store = None
111
+
112
+
113
+ def get_agent_store():
114
+ global _agent_store
115
+ if _agent_store is None:
116
+ from snippbot_core.chat.agent_store import AgentStore
117
+ _agent_store = AgentStore()
118
+ return _agent_store
119
+
120
+
121
+ def get_roster_store():
122
+ global _roster_store
123
+ if _roster_store is None:
124
+ from snippbot_core.chat.roster import RosterStore
125
+ _roster_store = RosterStore()
126
+ return _roster_store
127
+
128
+
129
+ # ===========================================================================
130
+ # Agent profile endpoints
131
+ # ===========================================================================
132
+
133
+
134
+ async def list_available_tools(request: Request) -> JSONResponse:
135
+ """GET /api/tools/available — list all tool names + priority + source.
136
+
137
+ Powers the Agent Tool Profile picker (PLAN_AGENT_TOOL_PROFILES Phase 4).
138
+ Shape: ``{"tools": [{"name", "description", "priority", "source"}], "count"}``.
139
+
140
+ Includes MCP tools and is not filtered by ``chat_only``, so the UI can
141
+ expose every tool an agent might possibly invoke.
142
+ """
143
+ try:
144
+ from snippbot_core.tools.registry import get_tool_registry
145
+
146
+ defs = await get_tool_registry().async_get_enabled_tool_definitions(
147
+ include_mcp=True,
148
+ chat_only=True,
149
+ )
150
+ # Pull `_source` off the raw (un-cleaned) tool list — `async_get_enabled_tool_definitions`
151
+ # strips it. Need to call the inner helper to retain metadata.
152
+ all_with_meta = await get_tool_registry().async_get_all_tools(
153
+ include_mcp=True,
154
+ chat_only=True,
155
+ )
156
+ source_by_name: dict[str, str] = {
157
+ t.get("name", ""): t.get("_source", "unknown") for t in all_with_meta
158
+ }
159
+ tools: list[dict[str, Any]] = []
160
+ for d in defs:
161
+ name = d.get("name")
162
+ if not name:
163
+ continue
164
+ tools.append({
165
+ "name": name,
166
+ "description": (d.get("description") or "")[:300],
167
+ "priority": d.get("_priority", 5),
168
+ "source": source_by_name.get(name, "unknown"),
169
+ })
170
+ return JSONResponse({"tools": tools, "count": len(tools)})
171
+ except Exception:
172
+ logger.exception("Failed to list available tools")
173
+ return JSONResponse({"error": "tool registry unavailable"}, status_code=500)
174
+
175
+
176
+ async def list_all_agents(request: Request) -> JSONResponse:
177
+ """GET /api/agents/all — unified list of gateway agents + AgentStore profiles.
178
+
179
+ Returns normalized shape: ``{id, handle, name, short_bio?, source,
180
+ avatar_url?, expertise_tags?, preferred_model?}``.
181
+
182
+ Source discriminator lets the UI show badges or scope behavior (e.g.
183
+ only AgentStore profiles are editable). Handles are deduped — if the
184
+ same handle exists in both sources, the AgentStore profile wins (user
185
+ config beats gateway convention).
186
+
187
+ Fixes B1 by giving the frontend a single source of truth; fixes B2 by
188
+ exposing AgentStore profiles with their true `handle` rather than
189
+ their UUID `id`, so `@!handle` round-trips correctly through
190
+ `parse_mentions` → `handle_to_id` on the backend.
191
+ """
192
+ from snippbot.api.chat import get_gateway_client
193
+
194
+ store = get_agent_store()
195
+ gateway_client = get_gateway_client()
196
+
197
+ # Fetch both sources in parallel.
198
+ store_task = asyncio.to_thread(store.list_agents)
199
+ try:
200
+ gateway_agents, store_profiles = await asyncio.gather(
201
+ gateway_client.list_agents(), store_task, return_exceptions=True
202
+ )
203
+ except Exception as exc: # pragma: no cover — gather shouldn't raise
204
+ logger.warning("Failed to list unified agents: %s", exc)
205
+ return JSONResponse({"error": str(exc)}, status_code=500)
206
+
207
+ if isinstance(gateway_agents, Exception):
208
+ logger.debug("Gateway agent list failed (using empty): %s", gateway_agents)
209
+ gateway_agents = []
210
+ if isinstance(store_profiles, Exception):
211
+ logger.warning("AgentStore list failed (using empty): %s", store_profiles)
212
+ store_profiles = []
213
+
214
+ # Pre-build gateway display map so a store row with the same handle can
215
+ # borrow the gateway's richer display fields (name, bio, tags, avatar)
216
+ # when the store row was auto-seeded with only an id/handle (e.g. to
217
+ # persist `preferred_model` for a gateway-only agent like Elon).
218
+ gateway_by_handle: dict[str, dict[str, Any]] = {}
219
+ for agent in gateway_agents or []:
220
+ gw_id = agent.get("id", "") if isinstance(agent, dict) else ""
221
+ if not gw_id:
222
+ continue
223
+ profile_meta = await asyncio.to_thread(_read_profile_metadata, gw_id)
224
+ role = agent.get("role") or profile_meta.get("role") or agent.get("description")
225
+ archetype = profile_meta.get("archetype")
226
+ gateway_by_handle[gw_id] = {
227
+ "id": gw_id,
228
+ "name": agent.get("display_name") or agent.get("name") or gw_id,
229
+ "short_bio": _compose_short_bio(role, archetype),
230
+ "expertise_tags": profile_meta.get("expertise_tags") or [],
231
+ "avatar_url": f"/gateway/api/agents/{gw_id}/avatar",
232
+ }
233
+
234
+ seen_handles: set[str] = set()
235
+ merged: list[dict] = []
236
+
237
+ # Store profiles first, enriched with gateway fields when the store row
238
+ # matches a gateway handle and the store field is empty/default.
239
+ for profile in store_profiles:
240
+ handle = profile.handle
241
+ if handle in seen_handles:
242
+ continue
243
+ seen_handles.add(handle)
244
+ gw = gateway_by_handle.get(handle, {})
245
+ # A store row is considered a "stub" for this field if it's empty OR
246
+ # equals the id/handle (which is how insert_agent_with_id seeds them).
247
+ store_name_is_stub = not profile.name or profile.name == profile.id or profile.name == profile.handle
248
+ merged.append({
249
+ "id": profile.id,
250
+ "handle": handle,
251
+ "name": (gw.get("name") if store_name_is_stub and gw.get("name") else None) or profile.name,
252
+ "short_bio": profile.short_bio or gw.get("short_bio"),
253
+ "avatar_url": profile.avatar_url or gw.get("avatar_url"),
254
+ "expertise_tags": profile.expertise_tags or gw.get("expertise_tags") or [],
255
+ "preferred_model": profile.preferred_model,
256
+ "tool_profile": profile.tool_profile,
257
+ "source": "store",
258
+ })
259
+
260
+ # Gateway agents without a matching store row.
261
+ for agent_id, gw in gateway_by_handle.items():
262
+ if agent_id in seen_handles:
263
+ continue
264
+ seen_handles.add(agent_id)
265
+ entry: dict[str, Any] = {
266
+ "id": agent_id,
267
+ "handle": agent_id, # gateway convention: handle == id
268
+ "name": gw["name"],
269
+ "short_bio": gw["short_bio"],
270
+ "avatar_url": gw["avatar_url"],
271
+ "source": "gateway",
272
+ }
273
+ if gw["expertise_tags"]:
274
+ entry["expertise_tags"] = gw["expertise_tags"]
275
+ merged.append(entry)
276
+
277
+ return JSONResponse(merged)
278
+
279
+
280
+ # Caps on tool_profile shape (S2 audit finding). Single-user local app, so
281
+ # these are defence-in-depth against a misbehaving UI or malformed JSON, not
282
+ # adversarial input.
283
+ _TOOL_PROFILE_MAX_TOOLS_LIST_LEN = 1000
284
+ _TOOL_PROFILE_MAX_TOOL_NAME_LEN = 200
285
+ _TOOL_PROFILE_ALLOWED_KEYS = frozenset({"mode", "tools", "include_core", "max_tools"})
286
+
287
+
288
+ def _validate_tool_profile(tp: Any) -> Optional[str]:
289
+ """Return an error message if ``tp`` is not a valid tool_profile, else None.
290
+
291
+ Schema:
292
+ {
293
+ "mode": "allowlist" | "blocklist", # required when tp is set
294
+ "tools": [str, ...], # required, can be empty
295
+ "include_core": bool, # optional, default true
296
+ "max_tools": int, # optional, positive
297
+ }
298
+
299
+ Unknown keys are rejected (S3) so the persisted row stays canonical.
300
+ ``tools`` is capped at 1000 names of 200 chars each (S2).
301
+ """
302
+ if not isinstance(tp, dict):
303
+ return "'tool_profile' must be an object or null"
304
+ unknown = set(tp.keys()) - _TOOL_PROFILE_ALLOWED_KEYS
305
+ if unknown:
306
+ return f"tool_profile contains unknown keys: {sorted(unknown)}"
307
+ mode = tp.get("mode")
308
+ if mode not in ("allowlist", "blocklist"):
309
+ return "tool_profile.mode must be 'allowlist' or 'blocklist'"
310
+ tools = tp.get("tools", [])
311
+ if not isinstance(tools, list) or not all(isinstance(t, str) for t in tools):
312
+ return "tool_profile.tools must be a list of strings"
313
+ if len(tools) > _TOOL_PROFILE_MAX_TOOLS_LIST_LEN:
314
+ return f"tool_profile.tools must contain at most {_TOOL_PROFILE_MAX_TOOLS_LIST_LEN} entries"
315
+ if any(len(t) > _TOOL_PROFILE_MAX_TOOL_NAME_LEN for t in tools):
316
+ return f"tool_profile.tools entries must be ≤ {_TOOL_PROFILE_MAX_TOOL_NAME_LEN} chars"
317
+ if "include_core" in tp and not isinstance(tp["include_core"], bool):
318
+ return "tool_profile.include_core must be a boolean"
319
+ if "max_tools" in tp:
320
+ mt = tp["max_tools"]
321
+ if not isinstance(mt, int) or isinstance(mt, bool) or mt <= 0:
322
+ return "tool_profile.max_tools must be a positive integer"
323
+ return None
324
+
325
+
326
+ async def update_agent(request: Request) -> JSONResponse:
327
+ """PATCH /api/agents/{agent_id} — edit profile fields.
328
+
329
+ If the agent doesn't have a chat-DB row yet (e.g. a gateway-only agent
330
+ like 'elon' or 'donna'), and the caller is setting `preferred_model`,
331
+ a minimal row is auto-seeded so the preference can be persisted. This
332
+ keeps the multi-agent chat path's `agent_store.get_agent(...)` read
333
+ consistent with what gets written here.
334
+ """
335
+ agent_id = request.path_params["agent_id"]
336
+
337
+ try:
338
+ body = await request.json()
339
+ except Exception:
340
+ return JSONResponse({"error": "Invalid JSON body"}, status_code=400)
341
+
342
+ # Validate field types: each editable field must be the right shape if
343
+ # present. Rejects accidental int/object writes that would corrupt the row
344
+ # and 500 subsequent reads at pydantic validation time.
345
+ _str_fields = ("name", "handle", "short_bio", "preferred_model", "avatar_url", "system_prompt")
346
+ for _field in _str_fields:
347
+ if _field in body and body[_field] is not None and not isinstance(body[_field], str):
348
+ return JSONResponse(
349
+ {"error": f"'{_field}' must be a string"},
350
+ status_code=400,
351
+ )
352
+ if "expertise_tags" in body and body["expertise_tags"] is not None:
353
+ if not isinstance(body["expertise_tags"], list) or not all(isinstance(t, str) for t in body["expertise_tags"]):
354
+ return JSONResponse(
355
+ {"error": "'expertise_tags' must be a list of strings"},
356
+ status_code=400,
357
+ )
358
+
359
+ tool_profile_present = "tool_profile" in body
360
+ tool_profile = body.get("tool_profile") if tool_profile_present else None
361
+ if tool_profile_present and tool_profile is not None:
362
+ err = _validate_tool_profile(tool_profile)
363
+ if err:
364
+ return JSONResponse({"error": err}, status_code=400)
365
+
366
+ store = get_agent_store()
367
+
368
+ existing = await asyncio.to_thread(store.get_agent, agent_id)
369
+ if existing is None:
370
+ # PLAN_PER_AGENT_THINKING_MODE.md soak audit: don't lazy-seed rows
371
+ # for unknown agent IDs. Option A's startup sync now eagerly seeds
372
+ # every gateway-folder agent, so a missing row genuinely means
373
+ # "no such agent" (or the sync hasn't completed yet). Fall back to
374
+ # checking the gateway directories: if a matching folder exists,
375
+ # seed (handles the brief startup-race window or a folder added
376
+ # mid-session); otherwise 404 to prevent orphan rows that would
377
+ # be deleted on the next sync (the "recreate-delete loop" the
378
+ # audit flagged).
379
+ gateway_folder_exists = any(
380
+ (root / agent_id).is_dir() for root in _profile_search_dirs()
381
+ )
382
+ if not gateway_folder_exists:
383
+ return JSONResponse(
384
+ {
385
+ "error": (
386
+ f"Agent '{agent_id}' not found. Create a gateway "
387
+ f"agent folder under vps/agents/<handle>/ first."
388
+ )
389
+ },
390
+ status_code=404,
391
+ )
392
+ await asyncio.to_thread(
393
+ store.insert_agent_with_id,
394
+ agent_id=agent_id,
395
+ name=body.get("name") or agent_id,
396
+ handle=body.get("handle") or agent_id,
397
+ )
398
+
399
+ # Use store sentinel so "field omitted from body" is distinguishable
400
+ # from "tool_profile: null" (which clears the profile).
401
+ from snippbot_core.chat.agent_store import AgentStore as _AS
402
+ tp_arg: Any = body["tool_profile"] if tool_profile_present else _AS._UNSET
403
+
404
+ updated = await asyncio.to_thread(
405
+ store.update_agent,
406
+ agent_id=agent_id,
407
+ name=body.get("name"),
408
+ handle=body.get("handle"),
409
+ short_bio=body.get("short_bio"),
410
+ expertise_tags=body.get("expertise_tags"),
411
+ preferred_model=body.get("preferred_model"),
412
+ avatar_url=body.get("avatar_url"),
413
+ system_prompt=body.get("system_prompt"),
414
+ tool_profile=tp_arg,
415
+ )
416
+
417
+ if not updated and existing is not None:
418
+ return JSONResponse({"error": "No fields to update"}, status_code=400)
419
+
420
+ agent = await asyncio.to_thread(store.get_agent, agent_id)
421
+
422
+ # PLAN_AGENT_TOOL_PROFILES Phase 5.4 — fire telemetry when the tool
423
+ # profile changed. Best-effort: dashboard observability must never
424
+ # block a successful PATCH.
425
+ if tool_profile_present:
426
+ try:
427
+ from snippbot_core.event_bus import get_event_bus
428
+ from snippbot_core.events import AGENT_TOOL_PROFILE_UPDATED
429
+
430
+ tp_val = body["tool_profile"]
431
+ payload: dict[str, Any] = {
432
+ "agent_id": agent_id,
433
+ "source": "api",
434
+ }
435
+ if tp_val is None:
436
+ payload.update({"mode": None, "tool_count": 0, "include_core": True})
437
+ elif isinstance(tp_val, dict):
438
+ payload.update({
439
+ "mode": tp_val.get("mode"),
440
+ "tool_count": len(tp_val.get("tools") or []),
441
+ "include_core": bool(tp_val.get("include_core", True)),
442
+ })
443
+ get_event_bus().publish(AGENT_TOOL_PROFILE_UPDATED, payload)
444
+ except Exception:
445
+ logger.debug("Failed to publish AGENT_TOOL_PROFILE_UPDATED", exc_info=True)
446
+
447
+ return JSONResponse(agent.model_dump(mode="json"))
448
+
449
+
450
+ # ===========================================================================
451
+ # Conversation roster endpoints
452
+ # ===========================================================================
453
+
454
+
455
+ async def list_roster(request: Request) -> JSONResponse:
456
+ """GET /api/chat/conversations/{conversation_id}/agents — list active roster."""
457
+ conversation_id = request.path_params["conversation_id"]
458
+ store = get_roster_store()
459
+ members = await asyncio.to_thread(store.active_members, conversation_id)
460
+ return JSONResponse([m.to_dict() for m in members])
461
+
462
+
463
+ async def add_to_roster(request: Request) -> JSONResponse:
464
+ """POST /api/chat/conversations/{conversation_id}/agents — add agent to roster."""
465
+ conversation_id = request.path_params["conversation_id"]
466
+
467
+ try:
468
+ body = await request.json()
469
+ except Exception:
470
+ return JSONResponse({"error": "Invalid JSON body"}, status_code=400)
471
+
472
+ agent_id = body.get("agent_id")
473
+ if not agent_id:
474
+ return JSONResponse({"error": "'agent_id' is required"}, status_code=400)
475
+
476
+ # NOTE: We don't verify the agent exists in AgentStore — roster members
477
+ # can reference either AgentStore profiles OR gateway-managed agents
478
+ # (Snippbot workspace agents like Donna, Elon, etc.). The roster stores
479
+ # the agent_id as a free-form reference.
480
+
481
+ roster_store = get_roster_store()
482
+ try:
483
+ member = await asyncio.to_thread(
484
+ roster_store.add_member,
485
+ conversation_id=conversation_id,
486
+ agent_id=agent_id,
487
+ role=body.get("role", "member"),
488
+ )
489
+ except Exception as exc:
490
+ logger.warning("Failed to add agent to roster: %s", exc)
491
+ return JSONResponse({"error": str(exc)}, status_code=400)
492
+
493
+ return JSONResponse(member.to_dict(), status_code=201)
494
+
495
+
496
+ async def update_roster_member(request: Request) -> JSONResponse:
497
+ """PATCH /api/chat/conversations/{conversation_id}/agents/{agent_id} — update role/muted."""
498
+ conversation_id = request.path_params["conversation_id"]
499
+ agent_id = request.path_params["agent_id"]
500
+
501
+ try:
502
+ body = await request.json()
503
+ except Exception:
504
+ return JSONResponse({"error": "Invalid JSON body"}, status_code=400)
505
+
506
+ roster_store = get_roster_store()
507
+
508
+ # Verify membership exists
509
+ member = await asyncio.to_thread(roster_store.get_member, conversation_id, agent_id)
510
+ if member is None:
511
+ return JSONResponse({"error": "Agent is not in this conversation roster"}, status_code=404)
512
+
513
+ role = body.get("role")
514
+ muted = body.get("muted")
515
+
516
+ # Promote to lead — `roster_store.promote_lead` atomically updates
517
+ # both conversation_agents.role and conversations.lead_agent_id in a
518
+ # single transaction (fixes B6 / M5 / N7 — they used to be separate
519
+ # thread-worker calls with a crash-window race).
520
+ # Pass thinking_store so the slot auto-fills on lead promotion when the
521
+ # new lead is Jarvis-default (PLAN_PER_AGENT_THINKING_MODE.md).
522
+ if role == "lead":
523
+ from snippbot_core.thinking.store import get_thinking_store
524
+ thinking_store = get_thinking_store()
525
+ promoted = await asyncio.to_thread(
526
+ roster_store.promote_lead,
527
+ conversation_id=conversation_id,
528
+ new_lead_agent_id=agent_id,
529
+ thinking_store=thinking_store,
530
+ )
531
+ if not promoted:
532
+ return JSONResponse({"error": "Failed to promote agent to lead"}, status_code=400)
533
+
534
+ # Handle muted separately if also provided
535
+ if muted is not None:
536
+ await asyncio.to_thread(
537
+ roster_store.update_member,
538
+ conversation_id=conversation_id,
539
+ agent_id=agent_id,
540
+ muted=muted,
541
+ )
542
+ else:
543
+ updated = await asyncio.to_thread(
544
+ roster_store.update_member,
545
+ conversation_id=conversation_id,
546
+ agent_id=agent_id,
547
+ role=role,
548
+ muted=muted,
549
+ )
550
+ if not updated:
551
+ return JSONResponse({"error": "No fields to update"}, status_code=400)
552
+
553
+ # Return the updated member
554
+ updated_member = await asyncio.to_thread(roster_store.get_member, conversation_id, agent_id)
555
+ return JSONResponse(updated_member.to_dict())
556
+
557
+
558
+ async def remove_from_roster(request: Request) -> JSONResponse:
559
+ """DELETE /api/chat/conversations/{conversation_id}/agents/{agent_id} — remove from roster."""
560
+ conversation_id = request.path_params["conversation_id"]
561
+ agent_id = request.path_params["agent_id"]
562
+
563
+ roster_store = get_roster_store()
564
+ removed = await asyncio.to_thread(
565
+ roster_store.remove_member,
566
+ conversation_id=conversation_id,
567
+ agent_id=agent_id,
568
+ )
569
+
570
+ if not removed:
571
+ return JSONResponse({"error": "Agent is not in this conversation roster"}, status_code=404)
572
+
573
+ return JSONResponse({"ok": True, "conversation_id": conversation_id, "agent_id": agent_id})
574
+
575
+
576
+ # ===========================================================================
577
+ # Route table
578
+ # ===========================================================================
579
+
580
+ # NOTE: These routes are spread into api_routes which is mounted under /api
581
+ # in server.py, so paths here must NOT include the /api prefix.
582
+ #
583
+ # GET /agents, GET /agents/all, and POST /agents are intentionally NOT
584
+ # registered here — they're defined directly in `routes.py` (next to the
585
+ # workspace-manager handlers) and registered earlier in api_routes so that
586
+ # Starlette's first-match resolves them deterministically. The unified
587
+ # AgentStore-aware handler `list_all_agents` (imported by routes.py) is
588
+ # the one bound to /agents/all. Adding duplicates here would re-introduce
589
+ # the route shadow this module deleted.
590
+ agent_routes = [
591
+ Route("/agents/{agent_id}", update_agent, methods=["PATCH"]),
592
+ Route("/tools/available", list_available_tools, methods=["GET"]),
593
+ ]
594
+
595
+ # Roster routes must be injected into chat_routes (which is Mounted at /chat),
596
+ # because Starlette matches Mount("/chat") before reaching top-level routes.
597
+ # Paths are relative to the /chat mount prefix.
598
+ roster_routes = [
599
+ Route("/conversations/{conversation_id}/agents", list_roster, methods=["GET"]),
600
+ Route("/conversations/{conversation_id}/agents", add_to_roster, methods=["POST"]),
601
+ Route("/conversations/{conversation_id}/agents/{agent_id}", update_roster_member, methods=["PATCH"]),
602
+ Route("/conversations/{conversation_id}/agents/{agent_id}", remove_from_roster, methods=["DELETE"]),
603
+ ]