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,179 @@
1
+ """Shared UserDirectory — surface-agnostic identity store (P2c, Singular Chat Interface).
2
+
3
+ ONE identity store behind POLYROB's many surfaces (CLI / WebView / Telegram). It maps a
4
+ raw platform id (a Telegram chat id, a CLI id, …) to a STABLE INTERNAL ``user_id``
5
+ and provides the reverse / email lookups that out-of-band delivery
6
+ (``cron/delivery.py``) needs — keyed on the internal id, sync.
7
+
8
+ Ported (reused, not forked) from the old Telegram-only bot
9
+ (../rob_dev_telegram_version):
10
+ - ``modules/database/user_profiles.py`` — the ``user_profiles`` table shape
11
+ (``user_id TEXT PK`` + ``tg_user_id TEXT UNIQUE``), ``get_or_create_by_tg_id``,
12
+ and the SHA256 ``generate_user_id`` derivation.
13
+ - ``utils/user_id_utils.py`` — ``UserIDResolver`` bidirectional tg<->internal mapping.
14
+
15
+ Key differences from the old bot (deliberate):
16
+ - The internal id is **deterministically derived** from ``(surface, raw_id)`` via
17
+ SHA256, so it is stable WITHOUT a prior row (the old code minted a random UUID per
18
+ row). A row is still upserted so the reverse lookup (internal -> chat id) works.
19
+ - Identity is **namespaced by surface** — the same raw "12345" on telegram vs cli
20
+ resolves to different internal ids, so cross-surface ids never collide.
21
+ - Sync API (no async DB layer) — ``cron/delivery.py`` calls ``get_telegram_chat_id``
22
+ / ``get_email`` without ``await``; SQLite + ``core/sqlite_util`` is the right shape.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import hashlib
27
+ import json
28
+ import logging
29
+ from typing import Optional
30
+
31
+ from core.sqlite_util import execute_retry, wal_connect
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Length of the hex slice used for the internal id (mirrors the old 24-char hash).
36
+ _ID_HEX_LEN = 24
37
+
38
+
39
+ class UserDirectory:
40
+ """Container-registerable identity store over a SQLite ``user_profiles`` table."""
41
+
42
+ def __init__(self, db_path: str):
43
+ self.db_path = db_path
44
+ self._ensure_table()
45
+
46
+ # --- schema --------------------------------------------------------------
47
+ def _ensure_table(self) -> None:
48
+ conn = wal_connect(self.db_path)
49
+ try:
50
+ conn.execute(
51
+ """
52
+ CREATE TABLE IF NOT EXISTS user_profiles (
53
+ user_id TEXT PRIMARY KEY,
54
+ surface_id TEXT,
55
+ tg_user_id TEXT UNIQUE,
56
+ chat_id TEXT,
57
+ email TEXT,
58
+ profile TEXT,
59
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
60
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
61
+ )
62
+ """
63
+ )
64
+ conn.execute(
65
+ "CREATE INDEX IF NOT EXISTS idx_user_profiles_tg "
66
+ "ON user_profiles(tg_user_id)"
67
+ )
68
+ conn.commit()
69
+ finally:
70
+ conn.close()
71
+
72
+ # --- id derivation -------------------------------------------------------
73
+ @staticmethod
74
+ def _derive_internal_id(surface_id: str, raw_id: str) -> str:
75
+ """Deterministic, stable internal id from (surface, raw_id).
76
+
77
+ SHA256 of ``surface_id:raw_id`` so the same raw id on two surfaces never
78
+ collides and the id is reproducible without a prior DB row.
79
+ """
80
+ seed = f"{surface_id}:{str(raw_id).strip()}".encode("utf-8")
81
+ digest = hashlib.sha256(seed).hexdigest()[:_ID_HEX_LEN]
82
+ return f"u_{digest}"
83
+
84
+ # --- public API (contracts) ---------------------------------------------
85
+ def get_or_create_by_tg_id(self, tg_id: str, profile: Optional[dict] = None) -> str:
86
+ """Return a STABLE internal ``user_id`` for a Telegram id (idempotent).
87
+
88
+ Upserts a row mapping ``tg_id`` <-> internal id, persisting ``chat_id`` (==
89
+ the tg id, for reverse lookup), an optional ``email`` and JSON profile blob.
90
+ """
91
+ tg_id = str(tg_id).strip()
92
+ user_id = self._derive_internal_id("telegram", tg_id)
93
+ self._upsert(
94
+ user_id=user_id,
95
+ surface_id="telegram",
96
+ tg_user_id=tg_id,
97
+ chat_id=tg_id,
98
+ profile=profile,
99
+ )
100
+ return user_id
101
+
102
+ def resolve_internal(self, raw_user_id: str, surface_id: str) -> str:
103
+ """Map a raw platform id to a stable internal ``user_id`` (idempotent).
104
+
105
+ ``surface_id == "telegram"`` is exactly ``get_or_create_by_tg_id``. Other
106
+ surfaces derive/persist the same way, namespaced by ``surface_id`` so the
107
+ same raw id on different surfaces resolves to different internal ids.
108
+ """
109
+ raw_user_id = str(raw_user_id).strip()
110
+ if surface_id == "telegram":
111
+ return self.get_or_create_by_tg_id(raw_user_id)
112
+ user_id = self._derive_internal_id(surface_id, raw_user_id)
113
+ self._upsert(
114
+ user_id=user_id,
115
+ surface_id=surface_id,
116
+ tg_user_id=None,
117
+ chat_id=raw_user_id,
118
+ profile=None,
119
+ )
120
+ return user_id
121
+
122
+ def get_telegram_chat_id(self, user_id: str) -> Optional[str]:
123
+ """Reverse lookup: internal ``user_id`` -> telegram chat id (the tg id).
124
+
125
+ Exact name/shape consumed by ``cron/delivery.py::_owner_telegram``.
126
+ """
127
+ row = execute_retry(
128
+ self.db_path,
129
+ "SELECT tg_user_id, chat_id, surface_id FROM user_profiles WHERE user_id = ?",
130
+ (user_id,),
131
+ fetch="one",
132
+ )
133
+ if not row:
134
+ return None
135
+ # Only telegram-origin rows have a meaningful tg chat id.
136
+ if row["surface_id"] != "telegram":
137
+ return None
138
+ return row["tg_user_id"] or row["chat_id"]
139
+
140
+ def get_email(self, user_id: str) -> Optional[str]:
141
+ """Read the optional email for an internal ``user_id`` (None if unset)."""
142
+ row = execute_retry(
143
+ self.db_path,
144
+ "SELECT email FROM user_profiles WHERE user_id = ?",
145
+ (user_id,),
146
+ fetch="one",
147
+ )
148
+ if not row:
149
+ return None
150
+ return row["email"]
151
+
152
+ # --- internals -----------------------------------------------------------
153
+ def _upsert(
154
+ self,
155
+ *,
156
+ user_id: str,
157
+ surface_id: str,
158
+ tg_user_id: Optional[str],
159
+ chat_id: Optional[str],
160
+ profile: Optional[dict],
161
+ ) -> None:
162
+ email = profile.get("email") if profile else None
163
+ profile_json = json.dumps(profile) if profile else None
164
+ # COALESCE preserves previously-set fields when a later call passes None.
165
+ execute_retry(
166
+ self.db_path,
167
+ """
168
+ INSERT INTO user_profiles (user_id, surface_id, tg_user_id, chat_id, email, profile)
169
+ VALUES (?, ?, ?, ?, ?, ?)
170
+ ON CONFLICT(user_id) DO UPDATE SET
171
+ surface_id = excluded.surface_id,
172
+ tg_user_id = COALESCE(excluded.tg_user_id, user_profiles.tg_user_id),
173
+ chat_id = COALESCE(excluded.chat_id, user_profiles.chat_id),
174
+ email = COALESCE(excluded.email, user_profiles.email),
175
+ profile = COALESCE(excluded.profile, user_profiles.profile),
176
+ updated_at = CURRENT_TIMESTAMP
177
+ """,
178
+ (user_id, surface_id, tg_user_id, chat_id, email, profile_json),
179
+ )
@@ -0,0 +1,3 @@
1
+ from .tool import WebFetchTool
2
+
3
+ __all__ = ["WebFetchTool"]
@@ -0,0 +1,144 @@
1
+ """Stateless, SSRF-safe single-page fetch core (no browser, no Chromium).
2
+
3
+ Security model (see docs/plans/2026-06-29-web-fetch-tier1-IMPLEMENTATION-PLAN.md):
4
+ - Auto-redirects are OFF; every hop is re-validated.
5
+ - Each hop is validated with MCPURLValidator.validate_and_resolve(), which returns a
6
+ pinned IP; the connection is pinned to that IP (Host/SNI preserved) so a DNS rebind
7
+ between validation and connect cannot redirect the socket internally.
8
+ - Hard caps on redirects, total time, and response bytes (read incrementally so a
9
+ decompression bomb is aborted before it is fully buffered).
10
+ """
11
+
12
+ import asyncio
13
+ import socket
14
+ import ssl
15
+ from dataclasses import dataclass
16
+ from typing import Callable, Optional
17
+ from urllib.parse import urljoin, urlparse
18
+
19
+ import aiohttp
20
+ import certifi
21
+ from aiohttp.abc import AbstractResolver
22
+
23
+ from tools.mcp.security import get_url_validator
24
+
25
+ _REDIRECT_STATUS = {301, 302, 303, 307, 308}
26
+
27
+ # An honest, disclosed bot UA. Many sites (e.g. Wikipedia) reject requests with no
28
+ # User-Agent; the "Mozilla/5.0 (compatible; ...)" form is the widely-accepted shape.
29
+ _DEFAULT_HEADERS = {
30
+ "User-Agent": "Mozilla/5.0 (compatible; polyrob-web-fetch/1.0; +https://github.com/theselfruleorg)",
31
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.5",
32
+ }
33
+
34
+
35
+ class WebFetchError(Exception):
36
+ """Raised when a fetch is blocked, oversized, or exceeds the redirect cap."""
37
+
38
+
39
+ @dataclass
40
+ class FetchResult:
41
+ final_url: str
42
+ status: int
43
+ content_type: str
44
+ body: bytes
45
+
46
+
47
+ class _PinnedResolver(AbstractResolver):
48
+ """Force a single host to resolve to a pre-validated IP (defeats DNS rebinding).
49
+
50
+ aiohttp connects to the ``host`` field (our pinned IP) but uses ``hostname`` for
51
+ TLS SNI and keeps the URL's host for the Host header, so pinning is transparent.
52
+ """
53
+
54
+ def __init__(self, hostname: str, pinned_ip: str) -> None:
55
+ self._hostname = hostname
56
+ self._ip = pinned_ip
57
+
58
+ async def resolve(self, host, port=0, family=socket.AF_INET):
59
+ ip = self._ip if host == self._hostname else host
60
+ return [{"hostname": host, "host": ip, "port": port,
61
+ "family": family, "proto": 0, "flags": 0}]
62
+
63
+ async def close(self) -> None: # pragma: no cover - trivial
64
+ return None
65
+
66
+
67
+ def _default_session_factory(pinned_ip: Optional[str], hostname: Optional[str]):
68
+ ssl_ctx = ssl.create_default_context(cafile=certifi.where())
69
+ if pinned_ip and hostname:
70
+ connector = aiohttp.TCPConnector(resolver=_PinnedResolver(hostname, pinned_ip), ssl=ssl_ctx)
71
+ else:
72
+ connector = aiohttp.TCPConnector(ssl=ssl_ctx)
73
+ return aiohttp.ClientSession(connector=connector)
74
+
75
+
76
+ async def safe_fetch(
77
+ url: str,
78
+ *,
79
+ max_bytes: int = 10_485_760,
80
+ max_redirects: int = 5,
81
+ timeout_sec: float = 15.0,
82
+ validate: bool = True,
83
+ validator=None,
84
+ session_factory: Optional[Callable] = None,
85
+ ) -> FetchResult:
86
+ """Fetch ``url`` with per-hop SSRF validation, IP pinning, no auto-redirects, and caps.
87
+
88
+ Args:
89
+ validate: when False, skip SSRF validation entirely (single-user/local only).
90
+ validator: an object exposing ``validate_and_resolve(url) -> (ok, err, pinned_ip)``.
91
+ Defaults to the shared MCP URL validator (allow_http=True). Injectable for tests.
92
+ session_factory: ``factory(pinned_ip) -> async-context-session`` (injectable for tests).
93
+ """
94
+ if validate and validator is None:
95
+ validator = get_url_validator(allow_http=True)
96
+
97
+ current = url
98
+ for _hop in range(max_redirects + 1):
99
+ pinned_ip: Optional[str] = None
100
+ # WS-4 (compute posture): a NARROW exception for the agent's OWN sandbox server —
101
+ # a loopback URL on a host port the sandbox actually published skips SSRF
102
+ # validation and pins to 127.0.0.1. Empty unless the sandbox published a port
103
+ # (posture >= 1); never matches RFC1918 or cloud metadata (not loopback).
104
+ _loopback_ok = False
105
+ try:
106
+ from tools.shell.loopback_allow import is_loopback_allowed
107
+ _loopback_ok = is_loopback_allowed(current)
108
+ except Exception:
109
+ _loopback_ok = False
110
+ if validate and _loopback_ok:
111
+ pinned_ip = "127.0.0.1"
112
+ elif validate:
113
+ # validate_and_resolve does a BLOCKING socket.getaddrinfo; offload it to a
114
+ # thread so a slow/sinkhole DNS can't freeze the whole event loop (every
115
+ # other session/request on this worker) for the resolver timeout.
116
+ ok, err, pinned_ip = await asyncio.get_running_loop().run_in_executor(
117
+ None, validator.validate_and_resolve, current)
118
+ if not ok:
119
+ raise WebFetchError(f"blocked URL ({current}): {err}")
120
+ hostname = urlparse(current).hostname
121
+
122
+ if session_factory is not None:
123
+ session_cm = session_factory(pinned_ip)
124
+ else:
125
+ session_cm = _default_session_factory(pinned_ip, hostname)
126
+
127
+ async with session_cm as session:
128
+ timeout = aiohttp.ClientTimeout(total=timeout_sec)
129
+ async with session.get(current, allow_redirects=False, timeout=timeout, headers=_DEFAULT_HEADERS) as resp:
130
+ if resp.status in _REDIRECT_STATUS:
131
+ location = resp.headers.get("Location")
132
+ if not location:
133
+ raise WebFetchError(f"redirect with no Location ({current})")
134
+ current = urljoin(current, location)
135
+ continue
136
+ ctype = resp.headers.get("Content-Type", "application/octet-stream")
137
+ buf = bytearray()
138
+ async for chunk in resp.content.iter_chunked(8192):
139
+ buf.extend(chunk)
140
+ if len(buf) > max_bytes:
141
+ raise WebFetchError(f"response exceeds {max_bytes} bytes ({current})")
142
+ return FetchResult(final_url=current, status=resp.status,
143
+ content_type=ctype, body=bytes(buf))
144
+ raise WebFetchError(f"too many redirects (>{max_redirects})")
@@ -0,0 +1,44 @@
1
+ """Pure HTML->markdown rendering + content-type classification for web_fetch."""
2
+
3
+ from bs4 import BeautifulSoup
4
+ from markdownify import markdownify
5
+
6
+ SPA_SHELL_SIGNAL = (
7
+ "[web_fetch: this page returned almost no readable text — it is likely a "
8
+ "JavaScript-rendered single-page app. Use the 'browser' tool (tool_ids=['browser']) "
9
+ "if you need its rendered content or interaction.]"
10
+ )
11
+
12
+ _BOILERPLATE_TAGS = ("script", "style", "nav", "header", "footer", "aside", "noscript")
13
+ _MIN_TEXT_CHARS = 200
14
+
15
+
16
+ def classify_content(content_type: str, body: bytes) -> str:
17
+ """Return ``"html"``, ``"pdf"``, or ``"binary"`` (content-type first, magic-byte fallback)."""
18
+ ct = (content_type or "").lower()
19
+ if "html" in ct or "xhtml" in ct:
20
+ return "html"
21
+ if "pdf" in ct or body[:5] == b"%PDF-":
22
+ return "pdf"
23
+ if "text/plain" in ct:
24
+ return "html" # treat plain text as renderable passthrough
25
+ return "binary"
26
+
27
+
28
+ def render_html_to_markdown(html: str, max_chars: int = 40_000) -> str:
29
+ """Strip boilerplate, convert to markdown, head-truncate with a marker.
30
+
31
+ Returns ``SPA_SHELL_SIGNAL`` when the page has almost no readable text (likely a
32
+ client-rendered SPA), so the agent's escalation cue is explicit.
33
+ """
34
+ soup = BeautifulSoup(html, "html.parser")
35
+ for tag in soup(_BOILERPLATE_TAGS):
36
+ tag.decompose()
37
+ if len(soup.get_text(strip=True)) < _MIN_TEXT_CHARS:
38
+ return SPA_SHELL_SIGNAL
39
+ root = soup.body or soup
40
+ md = markdownify(str(root)).strip()
41
+ if len(md) > max_chars:
42
+ dropped = len(md) - max_chars
43
+ md = md[:max_chars] + f"\n\n[... {dropped} chars truncated ...]"
44
+ return md
@@ -0,0 +1,60 @@
1
+ """WebFetchTool: stateless web page reader (URL -> markdown). No browser/Chromium."""
2
+
3
+ import os
4
+ from typing import Any, Dict, Optional
5
+
6
+ from core.config import BotConfig
7
+ from tools.base_tool import BaseTool
8
+ from tools.controller.views import WebFetchAction
9
+ from tools.web_fetch.fetcher import safe_fetch, WebFetchError
10
+ from tools.web_fetch.render import render_html_to_markdown, classify_content
11
+
12
+
13
+ def _allow_private_urls() -> bool:
14
+ return os.getenv("WEB_FETCH_ALLOW_PRIVATE_URLS", "false").strip().lower() in ("1", "true", "yes", "on")
15
+
16
+
17
+ class WebFetchTool(BaseTool):
18
+ """Fetch a URL and return clean markdown. The lightweight default web reader."""
19
+
20
+ def __init__(self, name: str, config: BotConfig, container: Optional[Any] = None):
21
+ super().__init__(name=name, config=config, container=container)
22
+ self._enabled = True
23
+
24
+ @property
25
+ def required_config(self) -> Dict[str, str]:
26
+ return {}
27
+
28
+ @BaseTool.action(
29
+ description=(
30
+ "Fetch a single web page by URL and return its main content as markdown. "
31
+ "Use this to READ a page you have the URL for (articles, docs, API pages). "
32
+ "Lightweight and fast — no browser. For SEARCH use perplexity/anysite; for "
33
+ "pages needing login/clicks/forms use the browser tool."
34
+ ),
35
+ param_model=WebFetchAction,
36
+ )
37
+ async def fetch_url(self, params: WebFetchAction) -> str:
38
+ # No services/state to set up — fetch is self-contained.
39
+ url = (params.url or "").strip()
40
+ if not url:
41
+ return "Please provide a url to fetch."
42
+ try:
43
+ result = await safe_fetch(url, validate=not _allow_private_urls())
44
+ except WebFetchError as e:
45
+ return f"Could not fetch the page (blocked or unreachable): {e}"
46
+ except Exception as e: # network/timeout — fail soft, agent can escalate to browser
47
+ return f"Could not fetch the page: {e}"
48
+
49
+ kind = classify_content(result.content_type, result.body)
50
+ if kind == "pdf":
51
+ return (f"[web_fetch: {result.final_url} is a PDF, not an HTML page. "
52
+ f"Save it and use the filesystem document-processing tools to extract text.]")
53
+ if kind == "binary":
54
+ return (f"[web_fetch: {result.final_url} returned non-HTML content "
55
+ f"({result.content_type}); cannot render as markdown.]")
56
+ try:
57
+ html = result.body.decode("utf-8", errors="replace")
58
+ except Exception:
59
+ html = result.body.decode("latin-1", errors="replace")
60
+ return render_html_to_markdown(html, max_chars=params.max_chars)
tools/x402/__init__.py ADDED
@@ -0,0 +1,60 @@
1
+ """Agent x402 paying tool package (gated; default OFF)."""
2
+ import os
3
+
4
+
5
+ def x402_client_enabled() -> bool:
6
+ return os.getenv("X402_CLIENT_ENABLED", "false").lower() == "true"
7
+
8
+
9
+ def register_x402_tool(force: bool = False) -> bool:
10
+ """Register the 'x402_pay' descriptor + class IFF X402_CLIENT_ENABLED (or forced).
11
+
12
+ Mirrors tools/code_exec.register_code_exec_tool. Descriptor inserted first
13
+ (register_tool_class is a silent no-op for unknown names). Never in default tool_ids.
14
+ """
15
+ if not (force or x402_client_enabled()):
16
+ return False
17
+ from tools.descriptors import (
18
+ TOOL_DESCRIPTORS, ToolDescriptor, ToolCategory, register_tool_class,
19
+ )
20
+ from tools.x402.service import X402PayTool
21
+
22
+ if "x402_pay" not in TOOL_DESCRIPTORS:
23
+ TOOL_DESCRIPTORS["x402_pay"] = ToolDescriptor(
24
+ name="x402_pay",
25
+ description="Pay for paid resources via the x402 protocol (agent personal wallet)",
26
+ category=ToolCategory.INTEGRATION,
27
+ is_optional=True,
28
+ init_priority=80,
29
+ )
30
+ register_tool_class("x402_pay", X402PayTool)
31
+ return True
32
+
33
+
34
+ def x402_invoicing_enabled() -> bool:
35
+ from core.env import bool_env
36
+ return bool_env("X402_INVOICE_ENABLED", False)
37
+
38
+
39
+ def register_x402_invoice_tool(force: bool = False) -> bool:
40
+ """Register 'x402_invoice' (x402_request/x402_invoices/accounting) IFF
41
+ X402_INVOICE_ENABLED (agent money loop). Same shape as register_x402_tool;
42
+ never in default tool_ids. Distinct flag from X402_CLIENT_ENABLED — invoicing
43
+ (receivables) needs a treasury address, not an agent wallet."""
44
+ if not (force or x402_invoicing_enabled()):
45
+ return False
46
+ from tools.descriptors import (
47
+ TOOL_DESCRIPTORS, ToolDescriptor, ToolCategory, register_tool_class,
48
+ )
49
+ from tools.x402.invoice_tool import X402InvoiceTool
50
+
51
+ if "x402_invoice" not in TOOL_DESCRIPTORS:
52
+ TOOL_DESCRIPTORS["x402_invoice"] = ToolDescriptor(
53
+ name="x402_invoice",
54
+ description="Create/track x402 payment requests (invoices) + the unified accounting ledger",
55
+ category=ToolCategory.INTEGRATION,
56
+ is_optional=True,
57
+ init_priority=80,
58
+ )
59
+ register_tool_class("x402_invoice", X402InvoiceTool)
60
+ return True
tools/x402/client.py ADDED
@@ -0,0 +1,47 @@
1
+ """x402 client boundary. The tool depends on X402PaymentClient (an interface),
2
+ so the official x402 SDK (RealX402Client, Task 8) is swappable and tests use a
3
+ deterministic FakeX402Client. The signer is the agent wallet's x402 signer —
4
+ the raw key never leaves it.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Optional, Protocol
10
+
11
+ from core.wallet.signer import Signer
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class X402Result:
16
+ body: str
17
+ paid: bool
18
+ amount_usd: float
19
+ tx_hash: Optional[str]
20
+ pay_to: Optional[str]
21
+ status_code: int
22
+
23
+
24
+ class X402PaymentClient(Protocol):
25
+ async def quote(self, url: str) -> Optional[float]: ...
26
+ async def fetch_with_payment(
27
+ self, *, url: str, method: str, body: Optional[str], signer: Signer,
28
+ network: str, facilitator_url: str, max_amount_usd: float,
29
+ ) -> X402Result: ...
30
+
31
+
32
+ class FakeX402Client:
33
+ """Deterministic test double."""
34
+
35
+ def __init__(self, price_usd: Optional[float], pay_to: Optional[str], paid_body: str):
36
+ self._price = price_usd
37
+ self._pay_to = pay_to
38
+ self._body = paid_body
39
+
40
+ async def quote(self, url: str) -> Optional[float]:
41
+ return self._price
42
+
43
+ async def fetch_with_payment(self, *, url, method, body, signer, network, facilitator_url, max_amount_usd) -> X402Result:
44
+ if self._price is None:
45
+ return X402Result(body=self._body, paid=False, amount_usd=0.0, tx_hash=None, pay_to=None, status_code=200)
46
+ return X402Result(body=self._body, paid=True, amount_usd=self._price,
47
+ tx_hash="0xfake", pay_to=self._pay_to, status_code=200)
@@ -0,0 +1,143 @@
1
+ """X402InvoiceTool — the agent invoices and accounts for itself.
2
+
3
+ Three actions, all riding existing seams:
4
+
5
+ - ``x402_request`` — create a payment request (invoice) from inside a session:
6
+ amount (ceiling-bounded), purpose, optional payer hint, expiry. Outward-facing
7
+ money: default-OFF flag, per-day cap, first-class ``payment_requested`` event,
8
+ and listed in the recommended approval set (operators add ``x402_request`` to
9
+ ``APPROVAL_REQUIRED_TOOLS``; at compute posture ≥2 it is auto-gated).
10
+ - ``x402_invoices`` — list this tenant's invoices (read-only).
11
+ - ``accounting`` — the unified ledger view (earned / spent / pending, evidence
12
+ from usage_records + wallet_spend events + x402 receipts). Read-only.
13
+
14
+ Server-tier stores (``modules.x402``/``modules.credits``) are imported lazily at
15
+ action time so the core agent import graph stays server-free (C3 boundary).
16
+ """
17
+ from __future__ import annotations # safe: @BaseTool.action uses explicit param_model
18
+
19
+ import logging
20
+ from typing import Optional
21
+
22
+ from pydantic import BaseModel, Field
23
+
24
+ from tools.base_tool import BaseTool
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class InvoiceParams(BaseModel):
30
+ amount_usd: float = Field(..., gt=0, description="Invoice amount in USD (bounded by X402_INVOICE_MAX_USD)")
31
+ purpose: str = Field(..., min_length=3, description="What the payer is paying for (shown to them and audited)")
32
+ payer_hint: Optional[str] = Field(None, description="Optional payer identity/contact hint for your own records")
33
+ expiry_hours: float = Field(72.0, gt=0, le=24 * 30, description="Hours until the request expires (default 72)")
34
+ payer_surface: Optional[str] = Field(
35
+ None, description="If invoicing a correspondent you already contacted, their surface "
36
+ "(e.g. 'email'/'telegram'); settlement is delivered to you as DATA, not a command")
37
+ payer_address: Optional[str] = Field(
38
+ None, description="The correspondent payer's address/handle on that surface (pairs with payer_surface)")
39
+
40
+
41
+ class InvoiceListParams(BaseModel):
42
+ status: Optional[str] = Field(None, description="Filter: pending|completed|expired (default all)")
43
+
44
+
45
+ class AccountingParams(BaseModel):
46
+ days: int = Field(7, ge=1, le=365, description="Trailing window in days (default 7)")
47
+
48
+
49
+ def _ctx_ids(execution_context) -> tuple:
50
+ user_id = getattr(execution_context, "user_id", "") or ""
51
+ session_id = getattr(execution_context, "session_id", "") or ""
52
+ return user_id, session_id
53
+
54
+
55
+ _ANON_REFUSED = ("this action requires an authenticated tenant — the execution "
56
+ "context has no user_id (anonymous financial state is refused)")
57
+
58
+
59
+ class X402InvoiceTool(BaseTool):
60
+ def __init__(self, name: str = "x402_invoice", config=None, container=None):
61
+ import types
62
+ super().__init__(name=name,
63
+ config=config if config is not None else types.SimpleNamespace(),
64
+ container=container)
65
+
66
+ def _ar(self, *, content: str = None, error: str = None):
67
+ from tools.controller.types import ActionResult
68
+ if error is not None:
69
+ return ActionResult(error=error)
70
+ return ActionResult(extracted_content=content)
71
+
72
+ @BaseTool.action(
73
+ "Create an x402 payment request (invoice): amount, purpose, expiry. "
74
+ "Returns payment instructions the payer needs (recipient/chain/amount/id).",
75
+ param_model=InvoiceParams)
76
+ async def x402_request(self, params: InvoiceParams, execution_context=None):
77
+ user_id, session_id = _ctx_ids(execution_context)
78
+ if not user_id:
79
+ return self._ar(error=f"x402_request refused: {_ANON_REFUSED}")
80
+ try:
81
+ from modules.x402.invoicing import create_payment_request
82
+ correspondent_ref = None
83
+ if params.payer_surface and params.payer_address:
84
+ correspondent_ref = {"surface": params.payer_surface,
85
+ "address": params.payer_address, "thread_id": ""}
86
+ inv = await create_payment_request(
87
+ user_id=user_id, session_id=session_id,
88
+ amount_usd=params.amount_usd, purpose=params.purpose,
89
+ payer_hint=params.payer_hint, expiry_hours=params.expiry_hours,
90
+ correspondent_ref=correspondent_ref,
91
+ )
92
+ except ValueError as e:
93
+ return self._ar(error=f"x402_request refused: {e}")
94
+ except Exception as e:
95
+ logger.error("x402_request failed: %s", e, exc_info=True)
96
+ return self._ar(error=f"x402_request failed: {e}")
97
+ return self._ar(content=(
98
+ f"Payment request created.\n"
99
+ f" request_id: {inv['request_id']}\n"
100
+ f" amount: ${inv['amount_usd']:.2f} {inv['asset'].upper()} on {inv['chain']}\n"
101
+ f" pay to: {inv['recipient']}\n"
102
+ f" purpose: {inv['purpose']}\n"
103
+ f" expires: epoch {inv['expires_at_epoch']}\n"
104
+ "Share these instructions with the payer (e.g. via the message tool). "
105
+ "You will be woken in this session when it settles."
106
+ ))
107
+
108
+ @BaseTool.action("List your x402 payment requests (invoices) and their status",
109
+ param_model=InvoiceListParams)
110
+ async def x402_invoices(self, params: InvoiceListParams, execution_context=None):
111
+ user_id, _ = _ctx_ids(execution_context)
112
+ if not user_id:
113
+ return self._ar(error=f"x402_invoices refused: {_ANON_REFUSED}")
114
+ try:
115
+ from modules.x402.invoicing import list_payment_requests
116
+ rows = await list_payment_requests(user_id=user_id, status=params.status)
117
+ except Exception as e:
118
+ return self._ar(error=f"x402_invoices failed: {e}")
119
+ if not rows:
120
+ return self._ar(content="No payment requests found.")
121
+ lines = ["Payment requests (newest first):"]
122
+ for r in rows:
123
+ lines.append(
124
+ f" {r['request_id']}: ${float(r['amount_usd'] or 0):.2f} "
125
+ f"[{r['status']}] — {r.get('purpose') or '(no purpose)'} "
126
+ f"(created {r.get('created_at')})")
127
+ return self._ar(content="\n".join(lines))
128
+
129
+ @BaseTool.action(
130
+ "Your unified financial ledger: earned / pending / spent (LLM + wallet) "
131
+ "over a trailing window. Read-only, evidence-backed.",
132
+ param_model=AccountingParams)
133
+ async def accounting(self, params: AccountingParams, execution_context=None):
134
+ user_id, _ = _ctx_ids(execution_context)
135
+ if not user_id:
136
+ return self._ar(error=f"accounting refused: {_ANON_REFUSED}")
137
+ try:
138
+ from modules.credits.unified_ledger import build_ledger, format_ledger
139
+ ledger = await build_ledger(user_id, days=params.days)
140
+ return self._ar(content=format_ledger(ledger))
141
+ except Exception as e:
142
+ logger.error("accounting failed: %s", e, exc_info=True)
143
+ return self._ar(error=f"accounting failed: {e}")