polyrob 0.5.0__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 (745) hide show
  1. agents/README.md +453 -0
  2. agents/__init__.py +164 -0
  3. agents/base_agent.py +743 -0
  4. agents/personality/character.py +119 -0
  5. agents/personality/character_manager.py +309 -0
  6. agents/personality/characters/rob.character.json +56 -0
  7. agents/personality/characters/trump.character.json +63 -0
  8. agents/personality/persona_render.py +78 -0
  9. agents/personality/persona_resolver.py +115 -0
  10. agents/prompt/__init__.py +58 -0
  11. agents/prompt/base_prompt.py +92 -0
  12. agents/prompt/system.py +417 -0
  13. agents/task/README_PATH_MANAGEMENT.md +141 -0
  14. agents/task/__init__.py +162 -0
  15. agents/task/agent/__init__.py +70 -0
  16. agents/task/agent/agent_state.py +408 -0
  17. agents/task/agent/async_delegation.py +274 -0
  18. agents/task/agent/autonomy_state.py +275 -0
  19. agents/task/agent/conversation.py +75 -0
  20. agents/task/agent/core/__init__.py +6 -0
  21. agents/task/agent/core/aux_metering.py +61 -0
  22. agents/task/agent/core/background_review.py +157 -0
  23. agents/task/agent/core/construction.py +1179 -0
  24. agents/task/agent/core/conversational_exit.py +49 -0
  25. agents/task/agent/core/correspondent_gate.py +227 -0
  26. agents/task/agent/core/curator.py +246 -0
  27. agents/task/agent/core/episodic_digest.py +103 -0
  28. agents/task/agent/core/error_recovery.py +480 -0
  29. agents/task/agent/core/history_io.py +257 -0
  30. agents/task/agent/core/llm_provisioning.py +287 -0
  31. agents/task/agent/core/llm_runner.py +547 -0
  32. agents/task/agent/core/logging_io.py +276 -0
  33. agents/task/agent/core/loop_detection.py +262 -0
  34. agents/task/agent/core/memory_prefetch.py +325 -0
  35. agents/task/agent/core/memory_writer.py +667 -0
  36. agents/task/agent/core/model_introspection.py +82 -0
  37. agents/task/agent/core/model_swap.py +177 -0
  38. agents/task/agent/core/next_action_internal.py +1331 -0
  39. agents/task/agent/core/output_validation.py +303 -0
  40. agents/task/agent/core/project_context.py +272 -0
  41. agents/task/agent/core/resources.py +93 -0
  42. agents/task/agent/core/result_processing.py +389 -0
  43. agents/task/agent/core/run_loop.py +661 -0
  44. agents/task/agent/core/safety_lifecycle.py +228 -0
  45. agents/task/agent/core/secret_guard.py +293 -0
  46. agents/task/agent/core/self_wake.py +280 -0
  47. agents/task/agent/core/session_metadata.py +104 -0
  48. agents/task/agent/core/step.py +781 -0
  49. agents/task/agent/core/step_execution.py +398 -0
  50. agents/task/agent/core/step_telemetry.py +222 -0
  51. agents/task/agent/core/test_aux_metering.py +50 -0
  52. agents/task/agent/core/test_billing_failover.py +68 -0
  53. agents/task/agent/core/test_fatal_error_classifier.py +38 -0
  54. agents/task/agent/core/test_judge_validation.py +275 -0
  55. agents/task/agent/core/test_model_swap.py +190 -0
  56. agents/task/agent/core/test_reflection_offload.py +54 -0
  57. agents/task/agent/core/turn_input.py +8 -0
  58. agents/task/agent/core/untrusted_wrap.py +103 -0
  59. agents/task/agent/core/user_ingress.py +228 -0
  60. agents/task/agent/hitl_manager.py +396 -0
  61. agents/task/agent/log_sanitize.py +36 -0
  62. agents/task/agent/message_manager/config.py +97 -0
  63. agents/task/agent/message_manager/service.py +758 -0
  64. agents/task/agent/message_manager/tests.py +305 -0
  65. agents/task/agent/message_manager/tool_call_builder.py +539 -0
  66. agents/task/agent/message_manager/tool_message_repair.py +364 -0
  67. agents/task/agent/message_manager/views.py +143 -0
  68. agents/task/agent/messages/__init__.py +8 -0
  69. agents/task/agent/messages/builders.py +309 -0
  70. agents/task/agent/messages/compactor.py +611 -0
  71. agents/task/agent/messages/context_references.py +271 -0
  72. agents/task/agent/messages/filters.py +333 -0
  73. agents/task/agent/messages/guidance.py +328 -0
  74. agents/task/agent/messages/persistence.py +448 -0
  75. agents/task/agent/messages/retrieval.py +506 -0
  76. agents/task/agent/messages/sqlite_persistence.py +85 -0
  77. agents/task/agent/messages/test_compaction_upgrade.py +349 -0
  78. agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
  79. agents/task/agent/messages/test_sqlite_persistence.py +54 -0
  80. agents/task/agent/messages/token_counter.py +532 -0
  81. agents/task/agent/orchestrator.py +776 -0
  82. agents/task/agent/profile_manager.py +105 -0
  83. agents/task/agent/profile_registry.py +173 -0
  84. agents/task/agent/prompts.py +1090 -0
  85. agents/task/agent/scenario_registry.py +226 -0
  86. agents/task/agent/service.py +509 -0
  87. agents/task/agent/session.py +984 -0
  88. agents/task/agent/skill_discovery.py +110 -0
  89. agents/task/agent/skill_frontmatter.py +94 -0
  90. agents/task/agent/skill_manager.py +1201 -0
  91. agents/task/agent/skill_store.py +627 -0
  92. agents/task/agent/skill_validation.py +56 -0
  93. agents/task/agent/skill_writer.py +492 -0
  94. agents/task/agent/sub_agent_manager.py +1137 -0
  95. agents/task/agent/test_session_disk_recovery.py +34 -0
  96. agents/task/agent/tests.py +513 -0
  97. agents/task/agent/tool_call_tracker.py +511 -0
  98. agents/task/agent/views.py +469 -0
  99. agents/task/config.py +432 -0
  100. agents/task/constants.py +1384 -0
  101. agents/task/flag_defaults.py +63 -0
  102. agents/task/goals/__init__.py +1 -0
  103. agents/task/goals/autonomy_marker.py +28 -0
  104. agents/task/goals/board.py +690 -0
  105. agents/task/goals/completion_judge.py +247 -0
  106. agents/task/goals/context.py +62 -0
  107. agents/task/goals/dispatcher.py +848 -0
  108. agents/task/goals/escalation.py +102 -0
  109. agents/task/goals/planner.py +114 -0
  110. agents/task/logging_config.py +155 -0
  111. agents/task/path.py +991 -0
  112. agents/task/robust_parse_config.py +400 -0
  113. agents/task/runtime/__init__.py +0 -0
  114. agents/task/runtime/run_as_session.py +130 -0
  115. agents/task/runtime_safety.py +308 -0
  116. agents/task/session/__init__.py +5 -0
  117. agents/task/session/browser_pool.py +130 -0
  118. agents/task/session/cleanup.py +612 -0
  119. agents/task/session/execution.py +397 -0
  120. agents/task/session/feed.py +200 -0
  121. agents/task/session/hitl_ingress.py +201 -0
  122. agents/task/session/hooks.py +83 -0
  123. agents/task/session/multi_agent.py +243 -0
  124. agents/task/session/workspace.py +91 -0
  125. agents/task/session_registry.py +91 -0
  126. agents/task/session_route.py +35 -0
  127. agents/task/sqlite_session_registry.py +261 -0
  128. agents/task/surface_config.py +185 -0
  129. agents/task/telemetry/__init__.py +58 -0
  130. agents/task/telemetry/event_log.py +180 -0
  131. agents/task/telemetry/formatters.py +642 -0
  132. agents/task/telemetry/manager.py +571 -0
  133. agents/task/telemetry/memory_events.py +66 -0
  134. agents/task/telemetry/self_events.py +53 -0
  135. agents/task/telemetry/sequence.py +160 -0
  136. agents/task/telemetry/service.py +1265 -0
  137. agents/task/telemetry/views.py +882 -0
  138. agents/task/templates.py +98 -0
  139. agents/task/test_aux_chain.py +151 -0
  140. agents/task/test_aux_router.py +41 -0
  141. agents/task/tests/test_parse_robustness.py +170 -0
  142. agents/task/tool_defaults.py +110 -0
  143. agents/task/utils.py +762 -0
  144. agents/task/utils_json.py +768 -0
  145. agents/task/utils_webview.py +86 -0
  146. agents/task/workspace_context.py +229 -0
  147. agents/task_agent_lite.py +2398 -0
  148. api/README.md +683 -0
  149. api/TASK_API_DOCS.md +151 -0
  150. api/__init__.py +0 -0
  151. api/a2a/__init__.py +37 -0
  152. api/a2a/agent_card.py +337 -0
  153. api/a2a/client.py +657 -0
  154. api/a2a/endpoints.py +375 -0
  155. api/a2a/models.py +268 -0
  156. api/a2a/streaming.py +419 -0
  157. api/a2a/task_handler.py +929 -0
  158. api/admin_endpoints.py +1584 -0
  159. api/app.py +923 -0
  160. api/auth_constants.py +28 -0
  161. api/auth_endpoints.py +402 -0
  162. api/auth_state.py +36 -0
  163. api/chat_via_task.py +42 -0
  164. api/conversation_manager.py +231 -0
  165. api/dependencies.py +186 -0
  166. api/eip8004_endpoints.py +452 -0
  167. api/hyperliquid_models.py +174 -0
  168. api/hyperliquid_routes.py +450 -0
  169. api/interfaces.py +207 -0
  170. api/jwt_middleware.py +83 -0
  171. api/kb/__init__.py +1 -0
  172. api/kb/endpoints.py +314 -0
  173. api/mcp_models.py +279 -0
  174. api/mcp_routes.py +438 -0
  175. api/middleware.py +526 -0
  176. api/models.py +176 -0
  177. api/openai_compat/__init__.py +0 -0
  178. api/openai_compat/model_map.py +25 -0
  179. api/openai_compat/models.py +59 -0
  180. api/openai_compat/router.py +140 -0
  181. api/payment_endpoints.py +444 -0
  182. api/payment_verification.py +138 -0
  183. api/polymarket_models.py +155 -0
  184. api/polymarket_routes.py +501 -0
  185. api/pricing_endpoints.py +89 -0
  186. api/session_routing.py +54 -0
  187. api/skill_endpoints.py +453 -0
  188. api/task_http_api.py +1856 -0
  189. api/webhooks.py +52 -0
  190. api/x402_endpoints.py +280 -0
  191. avatar/README.md +44 -0
  192. avatar/__init__.py +1 -0
  193. avatar/config/rob.json +7 -0
  194. avatar/mindprint.js +395 -0
  195. avatar/renders/rob.meta.json +20 -0
  196. avatar/renders/rob.png +0 -0
  197. avatar/studio.html +373 -0
  198. avatar/webview/avatar-live.js +44 -0
  199. cli/README.md +89 -0
  200. cli/__init__.py +0 -0
  201. cli/cli_surface.py +67 -0
  202. cli/commands/__init__.py +0 -0
  203. cli/commands/_bootstrap.py +49 -0
  204. cli/commands/_errors.py +53 -0
  205. cli/commands/chat.py +973 -0
  206. cli/commands/config.py +100 -0
  207. cli/commands/dashboard.py +103 -0
  208. cli/commands/doctor.py +222 -0
  209. cli/commands/email.py +174 -0
  210. cli/commands/gateway.py +346 -0
  211. cli/commands/goals.py +509 -0
  212. cli/commands/init.py +276 -0
  213. cli/commands/journey.py +30 -0
  214. cli/commands/kb.py +280 -0
  215. cli/commands/model.py +107 -0
  216. cli/commands/owner.py +431 -0
  217. cli/commands/pfp.py +232 -0
  218. cli/commands/run.py +414 -0
  219. cli/commands/serve.py +41 -0
  220. cli/commands/session.py +684 -0
  221. cli/commands/skill_install.py +786 -0
  222. cli/commands/skills.py +128 -0
  223. cli/commands/subagents.py +100 -0
  224. cli/commands/surface.py +81 -0
  225. cli/commands/telegram.py +185 -0
  226. cli/commands/todos.py +209 -0
  227. cli/commands/tools.py +102 -0
  228. cli/commands/update.py +316 -0
  229. cli/commands/whatsapp.py +175 -0
  230. cli/config_store.py +276 -0
  231. cli/gitignore.py +43 -0
  232. cli/inventory.py +46 -0
  233. cli/keys.py +101 -0
  234. cli/persona.py +17 -0
  235. cli/polyrob.py +137 -0
  236. cli/ui/__init__.py +64 -0
  237. cli/ui/activity.py +141 -0
  238. cli/ui/app.py +538 -0
  239. cli/ui/banner.py +233 -0
  240. cli/ui/blocks.py +534 -0
  241. cli/ui/bootstrap_notice.py +47 -0
  242. cli/ui/commands/__init__.py +41 -0
  243. cli/ui/commands/h_cron.py +64 -0
  244. cli/ui/commands/h_journey.py +164 -0
  245. cli/ui/commands/h_kb.py +121 -0
  246. cli/ui/commands/h_learn.py +100 -0
  247. cli/ui/commands/h_mcp.py +215 -0
  248. cli/ui/commands/h_self.py +113 -0
  249. cli/ui/commands/h_skills.py +314 -0
  250. cli/ui/commands/handlers.py +1672 -0
  251. cli/ui/commands/registry.py +426 -0
  252. cli/ui/dialog.py +425 -0
  253. cli/ui/event_registry.py +110 -0
  254. cli/ui/event_specs.py +45 -0
  255. cli/ui/events.py +433 -0
  256. cli/ui/identity.py +15 -0
  257. cli/ui/lifecycle.py +159 -0
  258. cli/ui/live_hooks.py +43 -0
  259. cli/ui/model_selector.py +532 -0
  260. cli/ui/persistent_loop.py +181 -0
  261. cli/ui/pick.py +113 -0
  262. cli/ui/plain_renderer.py +351 -0
  263. cli/ui/renderer.py +384 -0
  264. cli/ui/rich_renderer.py +533 -0
  265. cli/ui/secrets.py +115 -0
  266. cli/ui/state.py +376 -0
  267. cli/ui/statusbar.py +231 -0
  268. cli/ui/streaming.py +254 -0
  269. cli/ui/terminal_render.py +105 -0
  270. cli/ui/theme.py +132 -0
  271. cli/update/__init__.py +6 -0
  272. cli/update/context.py +121 -0
  273. cli/update/detect.py +176 -0
  274. cli/update/engine.py +95 -0
  275. cli/update/migrate_guarded.py +55 -0
  276. cli/update/process_guard.py +215 -0
  277. cli/update/runners.py +78 -0
  278. cli/update/snapshot.py +270 -0
  279. cli/update/versions.py +205 -0
  280. core/README.md +377 -0
  281. core/__init__.py +174 -0
  282. core/assets.py +87 -0
  283. core/async_bridge.py +76 -0
  284. core/autonomy_runtime.py +259 -0
  285. core/base_component.py +302 -0
  286. core/bootstrap.py +681 -0
  287. core/bot.py +310 -0
  288. core/config.py +833 -0
  289. core/constants.py +126 -0
  290. core/container.py +462 -0
  291. core/db_manifest.py +101 -0
  292. core/embedding.py +57 -0
  293. core/env.py +39 -0
  294. core/exceptions.py +287 -0
  295. core/flags.py +144 -0
  296. core/flags_catalog.py +345 -0
  297. core/home_migration.py +62 -0
  298. core/identity.py +118 -0
  299. core/initialization.py +1004 -0
  300. core/instance.py +537 -0
  301. core/interactive_gate.py +96 -0
  302. core/logging.py +436 -0
  303. core/owner_doc_writer.py +176 -0
  304. core/pairing.py +193 -0
  305. core/path_safety.py +20 -0
  306. core/paths.py +27 -0
  307. core/payment_config.py +21 -0
  308. core/permissions.py +322 -0
  309. core/runtime_config.py +96 -0
  310. core/runtime_paths.py +135 -0
  311. core/seams.py +35 -0
  312. core/secret_scan.py +25 -0
  313. core/secret_scrub.py +71 -0
  314. core/secrets.py +35 -0
  315. core/security_logging_filter.py +148 -0
  316. core/self_context_writer.py +333 -0
  317. core/self_evolution.py +294 -0
  318. core/session_context.py +47 -0
  319. core/sqlite_util.py +59 -0
  320. core/surfaces/__init__.py +16 -0
  321. core/surfaces/access.py +118 -0
  322. core/surfaces/binding.py +88 -0
  323. core/surfaces/bootstrap.py +78 -0
  324. core/surfaces/circuit.py +168 -0
  325. core/surfaces/continuity.py +77 -0
  326. core/surfaces/correspondents.py +228 -0
  327. core/surfaces/dispatcher.py +216 -0
  328. core/surfaces/envelopes.py +75 -0
  329. core/surfaces/gc.py +30 -0
  330. core/surfaces/idempotency.py +57 -0
  331. core/surfaces/inbound_webhook.py +124 -0
  332. core/surfaces/media.py +32 -0
  333. core/surfaces/message_router.py +91 -0
  334. core/surfaces/outbound_allowlist.py +80 -0
  335. core/surfaces/outbound_dispatcher.py +110 -0
  336. core/surfaces/outbound_mirror.py +37 -0
  337. core/surfaces/outbound_queue.py +107 -0
  338. core/surfaces/outbound_target.py +12 -0
  339. core/surfaces/owner_admin.py +29 -0
  340. core/surfaces/proactive.py +78 -0
  341. core/surfaces/progress.py +108 -0
  342. core/surfaces/rate_bucket.py +21 -0
  343. core/surfaces/registry.py +51 -0
  344. core/surfaces/rendering.py +53 -0
  345. core/surfaces/send_policy.py +14 -0
  346. core/surfaces/serialize.py +24 -0
  347. core/surfaces/session_chat_registry.py +112 -0
  348. core/surfaces/session_policy.py +60 -0
  349. core/surfaces/surface.py +288 -0
  350. core/surfaces/transcription.py +88 -0
  351. core/surfaces/voice_echo.py +26 -0
  352. core/surfaces/voice_guard.py +23 -0
  353. core/tickers.py +174 -0
  354. core/tool_catalog.py +192 -0
  355. core/version.py +75 -0
  356. core/wallet/__init__.py +5 -0
  357. core/wallet/agent_wallet.py +59 -0
  358. core/wallet/audit_sink.py +61 -0
  359. core/wallet/config.py +70 -0
  360. core/wallet/factory.py +87 -0
  361. core/wallet/policy.py +104 -0
  362. core/wallet/signer.py +51 -0
  363. cron/README.md +65 -0
  364. cron/__init__.py +0 -0
  365. cron/delivery.py +277 -0
  366. cron/digest.py +124 -0
  367. cron/jobs.py +207 -0
  368. cron/runner.py +297 -0
  369. cron/schedule.py +194 -0
  370. cron/scheduler.py +201 -0
  371. cron/service.py +57 -0
  372. cron/wake_gate.py +223 -0
  373. data/__init__.py +1 -0
  374. data/prompts/autov2_prompts.json +87 -0
  375. data/prompts/skills/browser-automation/SKILL.md +43 -0
  376. data/prompts/skills/coding-workflow/SKILL.md +36 -0
  377. data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
  378. data/prompts/skills/document-writing/SKILL.md +33 -0
  379. data/prompts/skills/email-comms/SKILL.md +33 -0
  380. data/prompts/skills/file-data-ops/SKILL.md +35 -0
  381. data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
  382. data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
  383. data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
  384. data/prompts/skills/lead-research/SKILL.md +86 -0
  385. data/prompts/skills/market-research-brief/SKILL.md +104 -0
  386. data/prompts/skills/person-analyzer/SKILL.md +124 -0
  387. data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
  388. data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
  389. data/prompts/skills/polymarket-trading/SKILL.md +55 -0
  390. data/prompts/skills/presentation-creator/SKILL.md +36 -0
  391. data/prompts/skills/project-analyzer/SKILL.md +169 -0
  392. data/prompts/skills/rules.json +268 -0
  393. data/prompts/skills/secret-handling/SKILL.md +20 -0
  394. data/prompts/skills/skill-authoring/SKILL.md +31 -0
  395. data/prompts/skills/skill-security-review/SKILL.md +27 -0
  396. data/prompts/skills/social-discovery/SKILL.md +57 -0
  397. data/prompts/skills/task-planning/SKILL.md +33 -0
  398. data/prompts/skills/web-research/SKILL.md +39 -0
  399. data/prompts/skills/web-scraping/SKILL.md +73 -0
  400. data/prompts/skills/x-engagement/SKILL.md +69 -0
  401. data/prompts/system_prompts.json +4 -0
  402. data/subscription_channels.json +5 -0
  403. modules/README.md +503 -0
  404. modules/__init__.py +124 -0
  405. modules/auth/__init__.py +13 -0
  406. modules/auth/api_key_manager.py +157 -0
  407. modules/auth/identity_mapper.py +380 -0
  408. modules/auth/siwe_auth.py +273 -0
  409. modules/auth/tier_manager.py +173 -0
  410. modules/base_module.py +190 -0
  411. modules/credits/__init__.py +29 -0
  412. modules/credits/balance_manager.py +237 -0
  413. modules/credits/cost_utils.py +129 -0
  414. modules/credits/pricing.py +139 -0
  415. modules/credits/unified_ledger.py +148 -0
  416. modules/credits/usage_meter.py +310 -0
  417. modules/credits/usage_tracker.py +674 -0
  418. modules/database/__init__.py +33 -0
  419. modules/database/audit_log.py +322 -0
  420. modules/database/auth_tables.py +276 -0
  421. modules/database/connection.py +644 -0
  422. modules/database/connection_pool.py +305 -0
  423. modules/database/conversation_contexts.py +446 -0
  424. modules/database/database_manager.py +237 -0
  425. modules/database/hyperliquid.py +375 -0
  426. modules/database/polymarket.py +574 -0
  427. modules/database/user_mcp_servers.py +728 -0
  428. modules/database/user_profiles.py +562 -0
  429. modules/database/utils.py +107 -0
  430. modules/database/x402_tables.py +92 -0
  431. modules/eip8004/README.md +1069 -0
  432. modules/eip8004/__init__.py +65 -0
  433. modules/eip8004/contracts.py +498 -0
  434. modules/eip8004/models.py +271 -0
  435. modules/eip8004/registration.py +148 -0
  436. modules/eip8004/reputation.py +367 -0
  437. modules/eip8004/validation.py +277 -0
  438. modules/llm/__init__.py +97 -0
  439. modules/llm/adapters.py +963 -0
  440. modules/llm/anthropic_client.py +831 -0
  441. modules/llm/available_models.py +106 -0
  442. modules/llm/brain_scrubber.py +137 -0
  443. modules/llm/cache_hints.py +162 -0
  444. modules/llm/deepseek_client.py +562 -0
  445. modules/llm/gemini_client.py +1367 -0
  446. modules/llm/llm_client.py +1139 -0
  447. modules/llm/llm_client_registry.py +188 -0
  448. modules/llm/llm_factory.py +237 -0
  449. modules/llm/llm_manager.py +1036 -0
  450. modules/llm/messages.py +374 -0
  451. modules/llm/model_registry.py +1935 -0
  452. modules/llm/nvidia_client.py +125 -0
  453. modules/llm/openai_client.py +800 -0
  454. modules/llm/openrouter_client.py +969 -0
  455. modules/llm/profiles.py +178 -0
  456. modules/llm/test_anthropic_cache_capture.py +35 -0
  457. modules/llm/test_cached_pricing_derivation.py +24 -0
  458. modules/llm/think_scrubber.py +444 -0
  459. modules/llm/token_counter.py +268 -0
  460. modules/memory/__init__.py +35 -0
  461. modules/memory/backend_factory.py +83 -0
  462. modules/memory/cache_manager.py +186 -0
  463. modules/memory/episodic.py +108 -0
  464. modules/memory/local_vector_memory_provider.py +552 -0
  465. modules/memory/memory_manager.py +225 -0
  466. modules/memory/models.py +312 -0
  467. modules/memory/provider.py +210 -0
  468. modules/memory/registry.py +205 -0
  469. modules/memory/sqlite_memory_provider.py +767 -0
  470. modules/memory/task/__init__.py +52 -0
  471. modules/memory/task/compaction_manager.py +157 -0
  472. modules/memory/task/context_retriever.py +809 -0
  473. modules/memory/task/hierarchical_memory.py +1002 -0
  474. modules/memory/task/lexical_retriever.py +71 -0
  475. modules/memory/task/null_context_manager.py +213 -0
  476. modules/memory/task/phase_manager.py +625 -0
  477. modules/memory/task/reflection_service.py +157 -0
  478. modules/memory/task/semantic_retriever.py +218 -0
  479. modules/memory/task/task_context_manager.py +1165 -0
  480. modules/memory/task/test_forgetting_engages.py +24 -0
  481. modules/memory/task/test_reflection_llm.py +39 -0
  482. modules/memory/task/test_threat_scan.py +53 -0
  483. modules/memory/task/threat_scan.py +90 -0
  484. modules/memory/test_sqlite_memory_provider.py +106 -0
  485. modules/memory/user_profile_manager.py +527 -0
  486. modules/payments/__init__.py +11 -0
  487. modules/payments/deposit_monitor.py +478 -0
  488. modules/payments/price_oracle.py +70 -0
  489. modules/payments/treasury_sweeper.py +401 -0
  490. modules/payments/wallet_generator.py +145 -0
  491. modules/pfp/__init__.py +1 -0
  492. modules/pfp/config.py +92 -0
  493. modules/pfp/mesh.py +565 -0
  494. modules/pfp/push.py +69 -0
  495. modules/pfp/renderer.py +85 -0
  496. modules/pfp/store.py +82 -0
  497. modules/skills/__init__.py +1 -0
  498. modules/skills/skill_usage.py +248 -0
  499. modules/transcription/__init__.py +24 -0
  500. modules/transcription/base.py +20 -0
  501. modules/transcription/faster_whisper_transcriber.py +55 -0
  502. modules/x402/README.md +249 -0
  503. modules/x402/__init__.py +22 -0
  504. modules/x402/invoicing.py +416 -0
  505. modules/x402/middleware.py +364 -0
  506. modules/x402/settlement_watcher.py +145 -0
  507. modules/x402/x402_integration.py +344 -0
  508. polyrob-0.5.0.dist-info/METADATA +482 -0
  509. polyrob-0.5.0.dist-info/RECORD +745 -0
  510. polyrob-0.5.0.dist-info/WHEEL +5 -0
  511. polyrob-0.5.0.dist-info/entry_points.txt +2 -0
  512. polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
  513. polyrob-0.5.0.dist-info/top_level.txt +12 -0
  514. surfaces/README.md +70 -0
  515. surfaces/__init__.py +0 -0
  516. surfaces/email/__init__.py +0 -0
  517. surfaces/email/dedup.py +44 -0
  518. surfaces/email/harness.py +187 -0
  519. surfaces/email/inbound.py +159 -0
  520. surfaces/email/seed.py +60 -0
  521. surfaces/email/surface.py +105 -0
  522. surfaces/telegram/__init__.py +0 -0
  523. surfaces/telegram/dedup.py +83 -0
  524. surfaces/telegram/harness.py +886 -0
  525. surfaces/telegram/inbound.py +145 -0
  526. surfaces/telegram/interactive_tools.py +49 -0
  527. surfaces/telegram/markdown.py +347 -0
  528. surfaces/telegram/rate_limit.py +167 -0
  529. surfaces/telegram/surface.py +187 -0
  530. surfaces/telegram/voice.py +52 -0
  531. surfaces/whatsapp/__init__.py +0 -0
  532. surfaces/whatsapp/client.py +51 -0
  533. surfaces/whatsapp/harness.py +62 -0
  534. surfaces/whatsapp/inbound.py +132 -0
  535. surfaces/whatsapp/surface.py +64 -0
  536. surfaces/whatsapp/window.py +34 -0
  537. tools/README.md +578 -0
  538. tools/__init__.py +424 -0
  539. tools/alchemy/__init__.py +5 -0
  540. tools/alchemy/alchemy_tool.py +389 -0
  541. tools/anysite/__init__.py +17 -0
  542. tools/anysite/client.py +88 -0
  543. tools/anysite/tool.py +89 -0
  544. tools/base_tool.py +468 -0
  545. tools/browser/__init__.py +22 -0
  546. tools/browser/actions.py +118 -0
  547. tools/browser/browser.py +1457 -0
  548. tools/browser/browser_manager.py +690 -0
  549. tools/browser/context.py +1748 -0
  550. tools/browser/playwright_utils.py +137 -0
  551. tools/browser/views.py +56 -0
  552. tools/code_exec/SANDBOX_SECURITY.md +144 -0
  553. tools/code_exec/__init__.py +178 -0
  554. tools/code_exec/backend.py +73 -0
  555. tools/code_exec/backends/__init__.py +1 -0
  556. tools/code_exec/backends/docker.py +779 -0
  557. tools/code_exec/backends/local_subprocess.py +154 -0
  558. tools/code_exec/env_policy.py +45 -0
  559. tools/code_exec/result.py +42 -0
  560. tools/code_exec/sandbox_guard.py +59 -0
  561. tools/code_exec/tool.py +227 -0
  562. tools/coding/__init__.py +50 -0
  563. tools/coding/edit.py +107 -0
  564. tools/coding/search.py +113 -0
  565. tools/coding/tool.py +353 -0
  566. tools/collabland/__init__.py +5 -0
  567. tools/collabland/collabland_tool.py +471 -0
  568. tools/controller/__init__.py +0 -0
  569. tools/controller/_helpers.py +165 -0
  570. tools/controller/action_registration.py +1765 -0
  571. tools/controller/approval.py +244 -0
  572. tools/controller/approval_interactive.py +90 -0
  573. tools/controller/delegation.py +195 -0
  574. tools/controller/execution.py +797 -0
  575. tools/controller/execution_context.py +141 -0
  576. tools/controller/hooks.py +135 -0
  577. tools/controller/introspection.py +344 -0
  578. tools/controller/mcp_registrar.py +190 -0
  579. tools/controller/message_send.py +31 -0
  580. tools/controller/registry/__init__.py +4 -0
  581. tools/controller/registry/schema_generators.py +395 -0
  582. tools/controller/registry/schema_sanitizer.py +406 -0
  583. tools/controller/registry/service.py +1266 -0
  584. tools/controller/registry/views.py +151 -0
  585. tools/controller/service.py +409 -0
  586. tools/controller/tool_management.py +334 -0
  587. tools/controller/types.py +28 -0
  588. tools/controller/views.py +450 -0
  589. tools/cronjob_tools.py +166 -0
  590. tools/crypto_trade_gate.py +59 -0
  591. tools/descriptors.py +452 -0
  592. tools/dom/__init__.py +0 -0
  593. tools/dom/history_tree_processor/service.py +244 -0
  594. tools/dom/history_tree_processor/view.py +99 -0
  595. tools/dom/service.py +471 -0
  596. tools/dom/views.py +346 -0
  597. tools/email_tool.py +403 -0
  598. tools/exceptions.py +169 -0
  599. tools/filesystem.py +1038 -0
  600. tools/filesystem_docproc.py +331 -0
  601. tools/filesystem_pdf.py +755 -0
  602. tools/git/__init__.py +42 -0
  603. tools/git/tool.py +258 -0
  604. tools/github/__init__.py +33 -0
  605. tools/github/client.py +94 -0
  606. tools/github/tool.py +237 -0
  607. tools/goal_tools.py +309 -0
  608. tools/hyperliquid/__init__.py +56 -0
  609. tools/hyperliquid/models.py +233 -0
  610. tools/hyperliquid/service.py +1680 -0
  611. tools/knowledge_ingest.py +950 -0
  612. tools/mcp/README.md +535 -0
  613. tools/mcp/__init__.py +52 -0
  614. tools/mcp/catalog.py +115 -0
  615. tools/mcp/config.py +337 -0
  616. tools/mcp/mcp_tool.py +1415 -0
  617. tools/mcp/param_coercion.py +289 -0
  618. tools/mcp/protocol.py +1520 -0
  619. tools/mcp/rate_limit.py +50 -0
  620. tools/mcp/security.py +424 -0
  621. tools/mcp/self_install.py +111 -0
  622. tools/mcp/server_manager.py +829 -0
  623. tools/mcp/subscriptions.py +72 -0
  624. tools/mcp/user_mcp_service.py +899 -0
  625. tools/mcp/validation_tracker.py +89 -0
  626. tools/mcp/views.py +346 -0
  627. tools/oauth/__init__.py +17 -0
  628. tools/oauth/manager.py +75 -0
  629. tools/oauth/provider.py +69 -0
  630. tools/oauth/providers/__init__.py +1 -0
  631. tools/oauth/providers/generic_oauth2.py +96 -0
  632. tools/perplexity_tool.py +280 -0
  633. tools/polymarket/__init__.py +41 -0
  634. tools/polymarket/clob_adapter.py +76 -0
  635. tools/polymarket/models.py +182 -0
  636. tools/polymarket/service.py +2082 -0
  637. tools/self_env/__init__.py +61 -0
  638. tools/self_env/tool.py +274 -0
  639. tools/shell/__init__.py +80 -0
  640. tools/shell/backend_pool.py +118 -0
  641. tools/shell/discipline.py +62 -0
  642. tools/shell/executor.py +157 -0
  643. tools/shell/loopback_allow.py +87 -0
  644. tools/shell/process_registry.py +113 -0
  645. tools/shell/process_tool.py +131 -0
  646. tools/shell/state.py +129 -0
  647. tools/shell/tool.py +151 -0
  648. tools/task_tool.py +637 -0
  649. tools/twitter_tool.py +2086 -0
  650. tools/user_directory.py +179 -0
  651. tools/web_fetch/__init__.py +3 -0
  652. tools/web_fetch/fetcher.py +144 -0
  653. tools/web_fetch/render.py +44 -0
  654. tools/web_fetch/tool.py +60 -0
  655. tools/x402/__init__.py +60 -0
  656. tools/x402/client.py +47 -0
  657. tools/x402/invoice_tool.py +143 -0
  658. tools/x402/real_client.py +250 -0
  659. tools/x402/service.py +131 -0
  660. utils/README.md +498 -0
  661. utils/__init__.py +46 -0
  662. utils/auth_utils.py +114 -0
  663. utils/bounded_collections.py +162 -0
  664. utils/circuit_breaker.py +389 -0
  665. utils/gif_utils.py +435 -0
  666. utils/markdown_utils.py +291 -0
  667. utils/message_utils.py +147 -0
  668. utils/metrics.py +36 -0
  669. utils/path_validator.py +227 -0
  670. utils/rate_limit_manager.py +399 -0
  671. utils/result_size.py +296 -0
  672. utils/time_utils.py +206 -0
  673. utils/user_utils.py +188 -0
  674. webview/README.md +111 -0
  675. webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
  676. webview/__init__.py +8 -0
  677. webview/activity.py +636 -0
  678. webview/owner_auth.py +108 -0
  679. webview/pages.py +448 -0
  680. webview/repair_sessions.py +399 -0
  681. webview/server.py +4323 -0
  682. webview/server_launcher.py +112 -0
  683. webview/static/css/activity.css +89 -0
  684. webview/static/css/chat.css +1772 -0
  685. webview/static/css/components.css +1190 -0
  686. webview/static/css/config-panel.css +651 -0
  687. webview/static/css/pages/admin.css +766 -0
  688. webview/static/css/pages/profile.css +231 -0
  689. webview/static/css/pages/settings.css +297 -0
  690. webview/static/css/pages/signin.css +316 -0
  691. webview/static/css/style.css +4250 -0
  692. webview/static/css/variables.css +147 -0
  693. webview/static/css/workspace-fullwidth-fix.css +109 -0
  694. webview/static/img/favicon.ico +0 -0
  695. webview/static/js/activity.js +394 -0
  696. webview/static/js/admin/activity.js +186 -0
  697. webview/static/js/admin/dashboard.js +163 -0
  698. webview/static/js/admin/user_detail.js +400 -0
  699. webview/static/js/admin/users.js +174 -0
  700. webview/static/js/admin/utils.js +214 -0
  701. webview/static/js/chat.js +3178 -0
  702. webview/static/js/config-panel.js +522 -0
  703. webview/static/js/constants.js +20 -0
  704. webview/static/js/error-handler.js +226 -0
  705. webview/static/js/ethers.min.js +1 -0
  706. webview/static/js/event-filter.js +137 -0
  707. webview/static/js/event-store.js +351 -0
  708. webview/static/js/file-attachments.js +374 -0
  709. webview/static/js/file-loader.js +163 -0
  710. webview/static/js/index.js +103 -0
  711. webview/static/js/performance-utils.js +196 -0
  712. webview/static/js/profile.js +396 -0
  713. webview/static/js/screenshot.js +386 -0
  714. webview/static/js/session.js +1527 -0
  715. webview/static/js/settings.js +1288 -0
  716. webview/static/js/sidebar-data.js +766 -0
  717. webview/static/js/sidebar-toggle.js +296 -0
  718. webview/static/js/socket.io.min.js +7 -0
  719. webview/static/js/stats.js +355 -0
  720. webview/static/js/ui-utils.js +420 -0
  721. webview/static/js/workspace.js +966 -0
  722. webview/stats_service.py +613 -0
  723. webview/templates/__init__.py +1 -0
  724. webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  725. webview/templates/activity.html +36 -0
  726. webview/templates/admin/activity.html +122 -0
  727. webview/templates/admin/dashboard.html +112 -0
  728. webview/templates/admin/user_detail.html +228 -0
  729. webview/templates/admin/users.html +105 -0
  730. webview/templates/autonomy.html +60 -0
  731. webview/templates/error.html +133 -0
  732. webview/templates/finance.html +90 -0
  733. webview/templates/identity.html +73 -0
  734. webview/templates/index.html +42 -0
  735. webview/templates/layout.html +125 -0
  736. webview/templates/memory.html +59 -0
  737. webview/templates/owner_login.html +15 -0
  738. webview/templates/profile.html +121 -0
  739. webview/templates/session.html +312 -0
  740. webview/templates/settings.html +534 -0
  741. webview/templates/sidebar.html +112 -0
  742. webview/templates/signin.html +213 -0
  743. webview/templates/status.html +14 -0
  744. webview/templates/system.html +45 -0
  745. webview/webgate.py +196 -0
@@ -0,0 +1,50 @@
1
+ """Per-(user, server) MCP tool-execution rate limiter (WS-B3).
2
+
3
+ A sliding-window limiter keyed by an arbitrary hashable (typically
4
+ ``(user_id, server_name)``) so a single agent/user cannot hammer expensive MCP
5
+ tools (crawl/scrape). Distinct from the per-tool throttle in ``BaseTool.rate_limit``
6
+ (global, service-dependent) and the add_server limiter in ``user_mcp_service``.
7
+
8
+ The clock is injectable (``time_fn``) so behavior is testable without real sleeps.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import time
13
+ from typing import Callable, Dict, Hashable, List
14
+
15
+
16
+ class MCPExecRateLimiter:
17
+ def __init__(
18
+ self,
19
+ max_calls: int = 20,
20
+ window_seconds: int = 60,
21
+ time_fn: Callable[[], float] = time.time,
22
+ ):
23
+ self.max_calls = max_calls
24
+ self.window_seconds = window_seconds
25
+ self._time_fn = time_fn
26
+ self._calls: Dict[Hashable, List[float]] = {}
27
+
28
+ def _prune(self, key: Hashable, now: float) -> List[float]:
29
+ cutoff = now - self.window_seconds
30
+ kept = [t for t in self._calls.get(key, []) if t > cutoff]
31
+ self._calls[key] = kept
32
+ return kept
33
+
34
+ def check(self, key: Hashable) -> bool:
35
+ """Return True and record the call if under the limit, else False."""
36
+ now = self._time_fn()
37
+ kept = self._prune(key, now)
38
+ if len(kept) >= self.max_calls:
39
+ return False
40
+ kept.append(now)
41
+ return True
42
+
43
+ def retry_after(self, key: Hashable) -> float:
44
+ """Seconds until the next slot frees (0 if a slot is available now)."""
45
+ now = self._time_fn()
46
+ kept = self._prune(key, now)
47
+ if len(kept) < self.max_calls:
48
+ return 0.0
49
+ oldest = min(kept)
50
+ return max(0.0, (oldest + self.window_seconds) - now)
tools/mcp/security.py ADDED
@@ -0,0 +1,424 @@
1
+ """
2
+ MCP Security utilities.
3
+
4
+ Provides encryption for API keys and URL validation for SSRF protection.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import socket
10
+ import ipaddress
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Optional, Tuple, Dict, Any
14
+ from urllib.parse import urlparse
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Path for persisted encryption key (development fallback).
19
+ # Anchored to the install/repo root (this file is tools/mcp/security.py ->
20
+ # parents[2]), NOT the process CWD: a relative path would regenerate the dev
21
+ # Fernet key whenever CWD changed, orphaning previously-encrypted MCP creds.
22
+ def _key_file_path() -> Path:
23
+ return Path(__file__).resolve().parents[2] / "data" / ".mcp_encryption_key"
24
+
25
+
26
+ _KEY_FILE_PATH = _key_file_path()
27
+
28
+
29
+ def _is_production() -> bool:
30
+ """Check if running in production mode."""
31
+ env = os.getenv('POLYROB_ENV', os.getenv('ENVIRONMENT', 'development')).lower()
32
+ return env in ('production', 'prod')
33
+
34
+
35
+ def _load_or_generate_key() -> str:
36
+ """
37
+ Load encryption key from environment, file, or generate new one.
38
+
39
+ Priority:
40
+ 1. MCP_ENCRYPTION_KEY environment variable (always preferred)
41
+ 2. Persisted key file (development fallback)
42
+ 3. Generate new key (development only, persisted to file)
43
+
44
+ In production, FAILS HARD if no key is configured.
45
+
46
+ Returns:
47
+ Fernet key as string
48
+
49
+ Raises:
50
+ RuntimeError: In production if MCP_ENCRYPTION_KEY is not set
51
+ """
52
+ from cryptography.fernet import Fernet
53
+
54
+ # 1. Try environment variable first
55
+ key_str = os.getenv('MCP_ENCRYPTION_KEY')
56
+ if key_str:
57
+ logger.debug("Using MCP_ENCRYPTION_KEY from environment")
58
+ return key_str
59
+
60
+ # 2. In production, fail hard - no fallbacks allowed
61
+ if _is_production():
62
+ raise RuntimeError(
63
+ "CRITICAL: MCP_ENCRYPTION_KEY environment variable is required in production! "
64
+ "Generate a key with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
65
+ )
66
+
67
+ # 3. Development: Try to load from persisted file
68
+ if _KEY_FILE_PATH.exists():
69
+ try:
70
+ key_str = _KEY_FILE_PATH.read_text().strip()
71
+ if key_str:
72
+ logger.info(f"Loaded MCP encryption key from {_KEY_FILE_PATH}")
73
+ # Also set in environment so it's consistent for this session
74
+ os.environ['MCP_ENCRYPTION_KEY'] = key_str
75
+ return key_str
76
+ except Exception as e:
77
+ logger.warning(f"Failed to read key file {_KEY_FILE_PATH}: {e}")
78
+
79
+ # 4. Development: Generate new key and persist it
80
+ logger.warning(
81
+ "MCP_ENCRYPTION_KEY not set - generating and persisting key for development. "
82
+ "This key will be saved to data/.mcp_encryption_key and reused across restarts. "
83
+ "In production, set MCP_ENCRYPTION_KEY environment variable!"
84
+ )
85
+
86
+ key_str = Fernet.generate_key().decode()
87
+
88
+ # Persist to file for reuse
89
+ try:
90
+ _KEY_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
91
+ _KEY_FILE_PATH.write_text(key_str)
92
+ # Set restrictive permissions (owner read/write only)
93
+ _KEY_FILE_PATH.chmod(0o600)
94
+ logger.info(f"Persisted MCP encryption key to {_KEY_FILE_PATH}")
95
+ except Exception as e:
96
+ logger.warning(f"Failed to persist key to {_KEY_FILE_PATH}: {e}")
97
+
98
+ # Set in environment for this session
99
+ os.environ['MCP_ENCRYPTION_KEY'] = key_str
100
+
101
+ return key_str
102
+
103
+
104
+ class MCPEncryption:
105
+ """Encrypt/decrypt sensitive MCP data using Fernet (AES-128-CBC)."""
106
+
107
+ def __init__(self, key: Optional[bytes] = None):
108
+ """
109
+ Initialize encryption with a key.
110
+
111
+ Args:
112
+ key: Fernet key (32 url-safe base64-encoded bytes).
113
+ If not provided, loads from MCP_ENCRYPTION_KEY env var or persisted file.
114
+
115
+ Raises:
116
+ RuntimeError: In production if MCP_ENCRYPTION_KEY is not set
117
+ """
118
+ # Lazy import to avoid dependency issues if cryptography not installed
119
+ from cryptography.fernet import Fernet
120
+
121
+ if key:
122
+ self.fernet = Fernet(key)
123
+ else:
124
+ # Load key using proper priority chain
125
+ key_str = _load_or_generate_key()
126
+ self.fernet = Fernet(key_str.encode() if isinstance(key_str, str) else key_str)
127
+
128
+ def encrypt(self, data: str) -> bytes:
129
+ """
130
+ Encrypt a string.
131
+
132
+ Args:
133
+ data: Plain text string to encrypt
134
+
135
+ Returns:
136
+ Encrypted bytes
137
+ """
138
+ if not data:
139
+ return b''
140
+ return self.fernet.encrypt(data.encode())
141
+
142
+ def decrypt(self, encrypted: bytes) -> str:
143
+ """
144
+ Decrypt to string.
145
+
146
+ Args:
147
+ encrypted: Encrypted bytes
148
+
149
+ Returns:
150
+ Decrypted string
151
+ """
152
+ if not encrypted:
153
+ return ''
154
+ return self.fernet.decrypt(encrypted).decode()
155
+
156
+ def encrypt_dict(self, data: Dict[str, Any]) -> bytes:
157
+ """
158
+ Encrypt a dictionary as JSON.
159
+
160
+ Args:
161
+ data: Dictionary to encrypt
162
+
163
+ Returns:
164
+ Encrypted bytes
165
+ """
166
+ if not data:
167
+ return b''
168
+ return self.encrypt(json.dumps(data))
169
+
170
+ def decrypt_dict(self, encrypted: bytes) -> Dict[str, Any]:
171
+ """
172
+ Decrypt to dictionary.
173
+
174
+ Args:
175
+ encrypted: Encrypted bytes
176
+
177
+ Returns:
178
+ Decrypted dictionary
179
+ """
180
+ if not encrypted:
181
+ return {}
182
+ return json.loads(self.decrypt(encrypted))
183
+
184
+ @staticmethod
185
+ def generate_key() -> bytes:
186
+ """
187
+ Generate a new encryption key.
188
+
189
+ Returns:
190
+ New Fernet key (url-safe base64-encoded)
191
+ """
192
+ from cryptography.fernet import Fernet
193
+ return Fernet.generate_key()
194
+
195
+
196
+ class MCPURLValidator:
197
+ """
198
+ Validate MCP server URLs for security (SSRF protection).
199
+
200
+ Blocks:
201
+ - Non-HTTPS URLs
202
+ - Localhost and loopback addresses
203
+ - Private IP ranges (10.x, 172.16-31.x, 192.168.x)
204
+ - Link-local addresses (169.254.x)
205
+ - Cloud metadata endpoints
206
+ """
207
+
208
+ # Blocked hostnames
209
+ BLOCKED_HOSTS = {
210
+ 'localhost',
211
+ '127.0.0.1',
212
+ '0.0.0.0',
213
+ '::1',
214
+ '[::1]',
215
+ 'metadata.google.internal',
216
+ 'metadata.goog',
217
+ '169.254.169.254', # AWS/GCP/Azure metadata
218
+ 'metadata.internal',
219
+ }
220
+
221
+ # Blocked IP networks (private/internal ranges). Belt-and-suspenders alongside the
222
+ # property-based classification in _is_blocked_ip (which is the authoritative check).
223
+ BLOCKED_NETWORKS = [
224
+ ipaddress.ip_network('0.0.0.0/8'), # "this network" (0.x routes to localhost)
225
+ ipaddress.ip_network('10.0.0.0/8'), # Private class A
226
+ ipaddress.ip_network('172.16.0.0/12'), # Private class B
227
+ ipaddress.ip_network('192.168.0.0/16'), # Private class C
228
+ ipaddress.ip_network('127.0.0.0/8'), # Loopback
229
+ ipaddress.ip_network('169.254.0.0/16'), # Link-local
230
+ ipaddress.ip_network('100.64.0.0/10'), # CGNAT / shared address space
231
+ ipaddress.ip_network('::1/128'), # IPv6 loopback
232
+ ipaddress.ip_network('fc00::/7'), # IPv6 private
233
+ ipaddress.ip_network('fe80::/10'), # IPv6 link-local
234
+ ipaddress.ip_network('::ffff:0:0/96'), # IPv4-mapped IPv6
235
+ ]
236
+
237
+ @staticmethod
238
+ def _is_blocked_ip(ip_obj: "ipaddress._BaseAddress") -> bool:
239
+ """Authoritative SSRF check by IP *property*, not a hand-maintained range list.
240
+
241
+ Unmaps IPv4-mapped IPv6 (``::ffff:a.b.c.d``) first — otherwise a v4 network
242
+ check silently misses it — then blocks any non-global address class
243
+ (private/loopback/link-local/reserved/multicast/unspecified) plus CGNAT.
244
+ """
245
+ if isinstance(ip_obj, ipaddress.IPv6Address):
246
+ if ip_obj.ipv4_mapped is not None:
247
+ ip_obj = ip_obj.ipv4_mapped
248
+ elif getattr(ip_obj, "sixtofour", None) is not None:
249
+ ip_obj = ip_obj.sixtofour
250
+ if (ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local
251
+ or ip_obj.is_reserved or ip_obj.is_multicast or ip_obj.is_unspecified):
252
+ return True
253
+ # CGNAT is not is_private on every Python version — block explicitly.
254
+ if ip_obj.version == 4 and ip_obj in ipaddress.ip_network('100.64.0.0/10'):
255
+ return True
256
+ return False
257
+
258
+ def __init__(self, allow_http: bool = False):
259
+ """
260
+ Initialize validator.
261
+
262
+ Args:
263
+ allow_http: If True, allow HTTP URLs (for local development only)
264
+ """
265
+ self.allow_http = allow_http
266
+
267
+ def validate(self, url: str) -> Tuple[bool, Optional[str]]:
268
+ """
269
+ Validate URL is safe for user MCP servers.
270
+
271
+ Args:
272
+ url: URL to validate
273
+
274
+ Returns:
275
+ Tuple of (is_valid, error_message)
276
+ error_message is None if valid
277
+
278
+ Note:
279
+ This 2-tuple contract is depended upon by existing callers
280
+ (``user_mcp_service.add_server``/``update_server``). For connect-time
281
+ SSRF/DNS-rebinding protection, callers that need the resolved IP
282
+ should use :meth:`validate_and_resolve`, which returns the validated
283
+ pinned IP so the socket connects to the IP that was actually checked.
284
+ """
285
+ is_valid, error, _pinned_ip = self.validate_and_resolve(url)
286
+ return is_valid, error
287
+
288
+ def validate_and_resolve(
289
+ self, url: str
290
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
291
+ """
292
+ Validate URL AND return the validated, resolved IP to pin.
293
+
294
+ Resolves the hostname exactly once and checks every resolved address
295
+ against the blocked networks. On success it returns a single concrete
296
+ IP that the caller should connect to directly (pinning), so a DNS
297
+ rebind between validation and connection cannot redirect the socket to
298
+ an internal/metadata address.
299
+
300
+ Args:
301
+ url: URL to validate
302
+
303
+ Returns:
304
+ Tuple of (is_valid, error_message, pinned_ip)
305
+ - On success: (True, None, "<resolved public ip>")
306
+ - On failure: (False, "<reason>", None)
307
+
308
+ Security:
309
+ The returned ``pinned_ip`` is the address that was validated. Callers
310
+ MUST connect to this IP (preserving the original Host header / TLS
311
+ SNI) rather than re-resolving DNS, otherwise the rebinding window
312
+ reopens.
313
+ """
314
+ try:
315
+ parsed = urlparse(url)
316
+
317
+ # Check scheme
318
+ if not self.allow_http and parsed.scheme != 'https':
319
+ return False, "URL must use HTTPS", None
320
+
321
+ if parsed.scheme not in ('http', 'https'):
322
+ return False, f"Invalid scheme: {parsed.scheme}", None
323
+
324
+ # Check hostname exists
325
+ hostname = parsed.hostname
326
+ if not hostname:
327
+ return False, "Invalid URL: no hostname", None
328
+
329
+ # Check for blocked hostnames
330
+ hostname_lower = hostname.lower()
331
+ if hostname_lower in self.BLOCKED_HOSTS:
332
+ return False, f"Blocked host: {hostname}", None
333
+
334
+ # Check for blocked hostname patterns
335
+ if hostname_lower.endswith('.internal') or hostname_lower.endswith('.local'):
336
+ return False, f"Internal hostname not allowed: {hostname}", None
337
+
338
+ # Check port (optional - some MCP servers use non-standard ports)
339
+ port = parsed.port
340
+ if port is not None:
341
+ # Block commonly abused ports
342
+ if port in (22, 23, 25, 110, 143, 445, 3389):
343
+ return False, f"Port {port} is not allowed", None
344
+
345
+ # Resolve hostname ONCE and check every IP. The first valid,
346
+ # non-blocked address is returned as the pin target.
347
+ pinned_ip: Optional[str] = None
348
+ try:
349
+ # Get all IPs for the hostname
350
+ addresses = socket.getaddrinfo(hostname, port)
351
+ for addr_info in addresses:
352
+ ip_str = addr_info[4][0]
353
+ try:
354
+ ip_obj = ipaddress.ip_address(ip_str)
355
+ except ValueError:
356
+ # Not a valid IP address, skip
357
+ continue
358
+
359
+ # Property-based classification (authoritative) — closes the whole
360
+ # class of internal addresses incl. IPv4-mapped IPv6, 0.0.0.0/8, CGNAT.
361
+ if self._is_blocked_ip(ip_obj):
362
+ return False, f"IP address in blocked range: {ip_str}", None
363
+ # Belt-and-suspenders: explicit range list too.
364
+ for network in self.BLOCKED_NETWORKS:
365
+ if ip_obj in network:
366
+ return False, f"IP address in blocked range: {ip_str}", None
367
+
368
+ # First clean address becomes the pin target
369
+ if pinned_ip is None:
370
+ pinned_ip = ip_str
371
+
372
+ except socket.gaierror as e:
373
+ return False, f"Cannot resolve hostname: {hostname} ({e})", None
374
+
375
+ if pinned_ip is None:
376
+ return False, f"Cannot resolve hostname to an IP: {hostname}", None
377
+
378
+ return True, None, pinned_ip
379
+
380
+ except Exception as e:
381
+ return False, f"URL validation error: {e}", None
382
+
383
+ def validate_server_type(self, server_type: str) -> Tuple[bool, Optional[str]]:
384
+ """
385
+ Validate server type is allowed for user MCP servers.
386
+
387
+ STDIO is NOT allowed for user servers (security risk - executes local processes).
388
+
389
+ Args:
390
+ server_type: Server type to validate ('sse', 'http', 'streamable_http', 'stdio')
391
+
392
+ Returns:
393
+ Tuple of (is_valid, error_message)
394
+ """
395
+ allowed_types = ('sse', 'http', 'streamable_http')
396
+
397
+ if server_type == 'stdio':
398
+ return False, "STDIO server type is not allowed for user servers (security risk)"
399
+
400
+ if server_type not in allowed_types:
401
+ return False, f"Invalid server type: {server_type}. Must be one of: {', '.join(allowed_types)}"
402
+
403
+ return True, None
404
+
405
+
406
+ def get_encryption() -> MCPEncryption:
407
+ """Get singleton encryption instance."""
408
+ global _encryption_instance
409
+ if '_encryption_instance' not in globals():
410
+ _encryption_instance = MCPEncryption()
411
+ return _encryption_instance
412
+
413
+
414
+ def get_url_validator(allow_http: bool = False) -> MCPURLValidator:
415
+ """
416
+ Get URL validator instance.
417
+
418
+ Args:
419
+ allow_http: Allow HTTP URLs (development only)
420
+
421
+ Returns:
422
+ MCPURLValidator instance
423
+ """
424
+ return MCPURLValidator(allow_http=allow_http)
@@ -0,0 +1,111 @@
1
+ """Agent-initiated MCP install (P0-C).
2
+
3
+ Ties together: allowlist check (MCPCatalog) → exfiltration/injection screen (reusing the
4
+ memory threat scanner) → approval gate → MCPServerManager.add_server + config persist.
5
+ Gated OFF by default (MCP_SELF_INSTALL_ENABLED). The server runs OUT of process, so this
6
+ is the lowest-risk "install its own tools" rung. ``screen_config`` is pure (no I/O). No
7
+ action closures — ``from __future__`` is safe.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ from typing import Any, Dict, Optional, Tuple
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def self_install_enabled() -> bool:
19
+ from core.env import bool_env
20
+ return bool_env("MCP_SELF_INSTALL_ENABLED", False)
21
+
22
+
23
+ def screen_config(cfg: Dict[str, Any]) -> Optional[str]:
24
+ """Return a rejection reason if ``cfg`` looks injected/exfiltrating, else None.
25
+
26
+ Serialises the whole config and runs the shared instruction-override scanner over it
27
+ (catch a prompt-injection smuggled through a description/arg), plus conservative
28
+ structural checks (no remote-script-piped-to-shell stdio command).
29
+ """
30
+ try:
31
+ blob = json.dumps(cfg, default=str)
32
+ except Exception:
33
+ return "config is not JSON-serialisable"
34
+ from modules.memory.task.threat_scan import is_suspicious, has_invisible_unicode
35
+ if is_suspicious(blob):
36
+ return "config rejected: contains instruction-override / prompt-injection text"
37
+ if has_invisible_unicode(blob):
38
+ return "config rejected: contains invisible/bidi control characters"
39
+ cmd = cfg.get("command")
40
+ if isinstance(cmd, (list, tuple)):
41
+ joined = " ".join(str(c) for c in cmd).lower()
42
+ for bad in ("curl", "wget", "| sh", "|sh", "| bash", "|bash"):
43
+ if bad in joined:
44
+ return f"config rejected: stdio command looks like a remote-exec ({bad!r})"
45
+ return None
46
+
47
+
48
+ def _entry_to_config_dict(entry) -> Dict[str, Any]:
49
+ d: Dict[str, Any] = {"transport": entry.transport, "description": entry.description, "trust": entry.trust}
50
+ if entry.url:
51
+ d["url"] = entry.url
52
+ if entry.command:
53
+ d["command"] = list(entry.command)
54
+ return d
55
+
56
+
57
+ async def perform_mcp_install(
58
+ server_id: str,
59
+ *,
60
+ catalog,
61
+ server_manager,
62
+ approve,
63
+ persist=None,
64
+ context: Any = None,
65
+ ) -> Tuple[bool, str]:
66
+ """Install a catalog MCP server: gate → allowlist → screen → approve → add_server → persist.
67
+
68
+ ``approve`` is an async callable ``(action_name, params, context) -> bool`` (the
69
+ approval provider). Returns ``(ok, message)``.
70
+ """
71
+ if not self_install_enabled():
72
+ return False, "MCP self-install is disabled (set MCP_SELF_INSTALL_ENABLED=true)."
73
+ if not catalog.is_allowed(server_id):
74
+ return False, f"'{server_id}' is not in the MCP install allowlist ({catalog.ids()})."
75
+ entry = catalog.get(server_id)
76
+ if entry is None:
77
+ return False, f"no catalog entry for '{server_id}'."
78
+
79
+ cfg_dict = _entry_to_config_dict(entry)
80
+ reason = screen_config(cfg_dict)
81
+ if reason:
82
+ return False, reason
83
+
84
+ try:
85
+ allowed = await approve("mcp_install", {"server_id": server_id, **cfg_dict}, context)
86
+ except Exception as e:
87
+ return False, f"approval error for mcp_install: {e}"
88
+ if not allowed:
89
+ return False, f"mcp_install of '{server_id}' was not approved."
90
+
91
+ from tools.mcp.config import MCPServerConfig, MCPServerType
92
+ type_map = {
93
+ "sse": MCPServerType.SSE,
94
+ "http": getattr(MCPServerType, "HTTP", MCPServerType.SSE),
95
+ "stdio": MCPServerType.STDIO,
96
+ }
97
+ config = MCPServerConfig(
98
+ type=type_map.get(entry.transport, MCPServerType.SSE),
99
+ url=entry.url,
100
+ command=list(entry.command) if entry.command else None,
101
+ enabled=True,
102
+ )
103
+ ok = await server_manager.add_server(server_id, config)
104
+ if not ok:
105
+ return False, f"MCP server '{server_id}' failed to install/connect."
106
+ if persist is not None:
107
+ try:
108
+ persist(server_id, cfg_dict)
109
+ except Exception as e:
110
+ logger.warning("mcp_install persist failed for %s: %s", server_id, e)
111
+ return True, f"Installed MCP server '{server_id}' ({entry.description})."