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,124 @@
1
+ """WebhookSurface: the shared inbound contract for push-based platforms (WhatsApp,
2
+ BlueBubbles, Telegram-webhook). Owns: signature verify -> fast-200 ack -> parse -> dedup ->
3
+ route -> act. A concrete surface implements only verify/parse/idempotency_key. Fail-open
4
+ end to end: a parse/route fault is logged, never raised to the HTTP layer (so the platform
5
+ gets its 200 and does not retry-storm)."""
6
+ import json
7
+ import logging
8
+ from abc import ABC, abstractmethod
9
+ from typing import List, Optional
10
+
11
+ from core.surfaces.envelopes import InboundMessage
12
+ from core.surfaces.idempotency import IdempotencyStore
13
+ from core.surfaces.dispatcher import route_inbound
14
+ from core.surfaces.serialize import KeyedLock
15
+ from core.surfaces.session_chat_registry import build_session_key
16
+ from core.surfaces.transcription import voice_present, transcribe_inbound_media
17
+ from core.surfaces.voice_guard import voice_needs_guard, voice_unavailable_message
18
+ from core.surfaces.voice_echo import voice_transcript, voice_echo_message
19
+ from agents.task.surface_config import SurfaceConfig
20
+ from surfaces.telegram.harness import act_on_inbound # shared RouteDecision->TaskAgent dispatch
21
+ from surfaces.telegram.inbound import InboundResult # dataclass(.inbound, .decision) — DRY
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class WebhookSurface(ABC):
27
+ def __init__(self, idempotency: IdempotencyStore) -> None:
28
+ self._idem = idempotency
29
+ self._lock = KeyedLock()
30
+
31
+ @property
32
+ @abstractmethod
33
+ def surface_id(self) -> str: ...
34
+
35
+ @abstractmethod
36
+ def verify_signature(self, headers: dict, body: bytes) -> bool: ...
37
+
38
+ def verify_challenge(self, params: dict) -> Optional[str]:
39
+ """GET verification handshake (e.g. Meta hub.challenge). Default: no handshake."""
40
+ return None
41
+
42
+ @abstractmethod
43
+ def parse(self, payload: dict) -> List[InboundMessage]: ...
44
+
45
+ @abstractmethod
46
+ def idempotency_key(self, inbound: InboundMessage) -> str: ...
47
+
48
+ async def handle_post(self, container, headers: dict, body: bytes, task_agent) -> dict:
49
+ # Lower-case header keys so callers can pass platform headers verbatim.
50
+ h = {str(k).lower(): v for k, v in (headers or {}).items()}
51
+ if not self.verify_signature(h, body):
52
+ logger.warning("%s webhook: signature verification FAILED", self.surface_id)
53
+ return {"ok": False, "error": "bad signature"}
54
+ try:
55
+ payload = json.loads(body or b"{}")
56
+ except Exception as e:
57
+ logger.warning("%s webhook: bad JSON: %s", self.surface_id, e)
58
+ return {"ok": True} # ack so the platform stops retrying garbage
59
+ try:
60
+ messages = self.parse(payload)
61
+ except Exception as e:
62
+ logger.error("%s webhook: parse failed: %s", self.surface_id, e, exc_info=True)
63
+ return {"ok": True}
64
+ for inbound in messages:
65
+ try:
66
+ key = self.idempotency_key(inbound)
67
+ if key and self._idem.seen(key):
68
+ continue
69
+ session_key = build_session_key(inbound.identity.source, inbound.identity.user_id)
70
+ async with self._lock.for_key(session_key):
71
+ # Fix 2b: hydrate surface-specific media (e.g. WA media-id -> bytes) before transcription
72
+ if hasattr(self, "hydrate_media"):
73
+ try:
74
+ await self.hydrate_media(inbound.media)
75
+ except Exception:
76
+ pass
77
+ # Fix 2a: transcribe voice-only turns; guard untranscribed voice (never route empty)
78
+ if voice_present(inbound.media) and not (inbound.text or "").strip():
79
+ try:
80
+ _vtext = await transcribe_inbound_media(container, inbound.media)
81
+ except Exception:
82
+ _vtext = None
83
+ if _vtext:
84
+ inbound.text = _vtext
85
+ for _m in inbound.media:
86
+ if getattr(_m, "kind", None) in ("voice", "audio"):
87
+ _m.transcript = _vtext
88
+ break
89
+ if voice_needs_guard(inbound.media, inbound.text):
90
+ await self._send_immediate(
91
+ inbound,
92
+ voice_unavailable_message(SurfaceConfig.voice_transcription_enabled()),
93
+ )
94
+ continue
95
+ # Persistent transcript echo (voice only) — see core.surfaces.voice_echo.
96
+ # Lands before the agent runs; fail-open (never blocks the turn).
97
+ if SurfaceConfig.voice_transcript_echo_enabled():
98
+ _t = voice_transcript(inbound.media)
99
+ if _t:
100
+ try:
101
+ await self._send_immediate(
102
+ inbound, voice_echo_message(_t),
103
+ reply_to=inbound.idempotency_key)
104
+ except Exception as e:
105
+ logger.debug("%s transcript echo failed: %s", self.surface_id, e)
106
+ _mr = getattr(self, "mark_read_inbound", None)
107
+ if _mr is not None:
108
+ try:
109
+ await _mr(inbound)
110
+ except Exception:
111
+ pass
112
+ decision = await route_inbound(container, inbound)
113
+ result = InboundResult(inbound=inbound, decision=decision)
114
+ reply = await act_on_inbound(task_agent, result)
115
+ if reply:
116
+ await self._send_immediate(inbound, reply)
117
+ except Exception as e: # fail-open per message
118
+ logger.error("%s webhook: process failed: %s", self.surface_id, e, exc_info=True)
119
+ return {"ok": True}
120
+
121
+ async def _send_immediate(self, inbound: InboundMessage, text: str, reply_to=None) -> None:
122
+ """Deliver a synchronous reply (DENIED notice / command ack / transcript echo).
123
+ Override per surface. ``reply_to`` optionally quotes the inbound message."""
124
+ logger.debug("%s immediate reply suppressed (no transport): %s", self.surface_id, text[:80])
core/surfaces/media.py ADDED
@@ -0,0 +1,32 @@
1
+ """Typed media envelope shared by every surface. Inbound media (voice/image/doc) is
2
+ normalized to Media so the core transcription/handling seams are transport-agnostic.
3
+ Kept intentionally small: bytes XOR url; a surface fills whichever it has cheaply."""
4
+ from dataclasses import dataclass
5
+ from typing import List, Optional
6
+
7
+ _KINDS = {"voice", "audio", "image", "video", "document", "sticker"}
8
+
9
+
10
+ @dataclass
11
+ class Media:
12
+ kind: str
13
+ mime: Optional[str] = None
14
+ data: Optional[bytes] = None
15
+ url: Optional[str] = None
16
+ caption: Optional[str] = None
17
+ filename: Optional[str] = None
18
+ transcript: Optional[str] = None
19
+
20
+
21
+ def coerce_media(items: list) -> List[Media]:
22
+ out: List[Media] = []
23
+ for it in items or []:
24
+ if isinstance(it, Media):
25
+ out.append(it)
26
+ elif isinstance(it, dict) and it.get("kind") in _KINDS:
27
+ out.append(Media(
28
+ kind=it["kind"], mime=it.get("mime"), data=it.get("data"),
29
+ url=it.get("url"), caption=it.get("caption"), filename=it.get("filename"),
30
+ transcript=it.get("transcript"),
31
+ ))
32
+ return out
@@ -0,0 +1,91 @@
1
+ """MessageRouter: the SINGLE outbound producer seam.
2
+
3
+ Every agent->user emit funnels through publish(). Text is brain-scrubbed here
4
+ (SSOT = modules.llm.brain_scrubber) so every surface inherits the scrub. Surfaces
5
+ subscribe by surface_id; routing resolves session_key -> surface_id via the
6
+ SessionChatRegistry. Fail-open: an unroutable key or a raising surface never
7
+ crashes the agent loop.
8
+ """
9
+ import logging
10
+
11
+ from core.surfaces.envelopes import OutboundMessage
12
+ from core.surfaces.session_chat_registry import SessionChatRegistry
13
+ from modules.llm.brain_scrubber import scrub_brain_blocks
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class MessageRouter:
19
+ def __init__(self, registry: SessionChatRegistry) -> None:
20
+ self._registry = registry
21
+ self._surfaces: dict[str, object] = {}
22
+ self._queue = None
23
+
24
+ def attach_queue(self, q) -> None:
25
+ """Attach a durable OutboundDeliveryQueue. Call from bootstrap after construction."""
26
+ self._queue = q
27
+
28
+ def subscribe(self, surface_id: str, surface) -> None:
29
+ self._surfaces[surface_id] = surface
30
+
31
+ async def publish(self, msg: OutboundMessage) -> None:
32
+ try:
33
+ scrubbed = scrub_brain_blocks(msg.text)
34
+ except Exception: # fail-open: never drop a reply over a scrub bug
35
+ scrubbed = msg.text
36
+ # F3 parity with HITLManager.stream_output: a wholly-brain (or empty) chunk
37
+ # scrubs to None/"" and must be DROPPED, not delivered as an empty bubble.
38
+ if scrubbed is None or not scrubbed.strip():
39
+ return
40
+ if scrubbed != msg.text:
41
+ msg = OutboundMessage(
42
+ session_key=msg.session_key, text=scrubbed, kind=msg.kind,
43
+ partial=msg.partial, stream_id=msg.stream_id,
44
+ reply_to=msg.reply_to, media=msg.media,
45
+ )
46
+ row = self._registry.resolve(msg.session_key)
47
+ if not row:
48
+ logger.debug("message_router: no binding for %s; dropping", msg.session_key)
49
+ return
50
+ surface = self._surfaces.get(row.get("surface_id"))
51
+ if surface is None:
52
+ logger.debug("message_router: no surface %s subscribed", row.get("surface_id"))
53
+ return
54
+ # Durable path (final messages only): enqueue instead of sending directly.
55
+ from agents.task.surface_config import SurfaceConfig
56
+ if (self._queue is not None and not msg.partial
57
+ and SurfaceConfig.outbound_queue_enabled()):
58
+ turn = msg.stream_id or msg.session_key
59
+ idem = f"{msg.session_key}#{turn}#{hash(scrubbed) & 0xffffffff}"
60
+ try:
61
+ self._queue.enqueue(
62
+ idempotency_key=idem, session_key=msg.session_key,
63
+ surface_id=row.get("surface_id"), dest=row.get("chat_id"),
64
+ payload=scrubbed, kind=str(getattr(msg.kind, "value", msg.kind)),
65
+ )
66
+ except Exception as e: # fail-open: fall back to a direct send on a queue fault
67
+ logger.error("outbound enqueue failed, sending directly: %s", e)
68
+ else:
69
+ return
70
+ try:
71
+ if msg.partial:
72
+ await surface.stream(msg) # base buffers if surface can't stream
73
+ else:
74
+ await surface.send(msg)
75
+ except Exception as e: # fail-open
76
+ logger.error("message_router: surface %s raised: %s", row.get("surface_id"), e, exc_info=True)
77
+
78
+ async def send_message(self, chat_id: str, text: str, surface_id: str = "telegram") -> bool:
79
+ """Back-compat shim for cron/delivery.py. Returns True only on a completed send."""
80
+ surface = self._surfaces.get(surface_id)
81
+ if surface is None:
82
+ logger.warning("send_message: no surface %s registered — delivery failed", surface_id)
83
+ return False
84
+ try:
85
+ await surface.send(OutboundMessage(
86
+ session_key=f"direct:{surface_id}:{chat_id}", text=text,
87
+ ))
88
+ return True
89
+ except Exception as e:
90
+ logger.error("send_message shim failed: %s", e, exc_info=True)
91
+ return False
@@ -0,0 +1,80 @@
1
+ """Owner-scoped outbound send-authorization. Default-DENY: a (user_id, surface,
2
+ target) must have an ACTIVE row to be sendable. Owner targets bypass this (resolved
3
+ upstream in outbound_target.resolve_target_tier). Tenant-scoped, WAL+jitter via
4
+ ``core/sqlite_util`` (no hand-rolled retry loop) — mirrors ``agents/task/goals/board.py``.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import time
10
+ from typing import Dict, List
11
+
12
+ from core.sqlite_util import execute_retry
13
+
14
+ _DDL = """
15
+ CREATE TABLE IF NOT EXISTS outbound_allowlist (
16
+ user_id TEXT NOT NULL,
17
+ surface TEXT NOT NULL,
18
+ target TEXT NOT NULL,
19
+ note TEXT NOT NULL DEFAULT '',
20
+ status TEXT NOT NULL DEFAULT 'active',
21
+ created_at REAL NOT NULL,
22
+ PRIMARY KEY (user_id, surface, target)
23
+ );
24
+ """
25
+
26
+
27
+ class OutboundAllowlist:
28
+ def __init__(self, db_path: str) -> None:
29
+ self.db_path = db_path
30
+ parent = os.path.dirname(db_path)
31
+ if parent:
32
+ os.makedirs(parent, exist_ok=True)
33
+ execute_retry(self.db_path, _DDL)
34
+
35
+ def allow(self, user_id: str, surface: str, target: str, note: str = "") -> None:
36
+ execute_retry(
37
+ self.db_path,
38
+ "INSERT INTO outbound_allowlist(user_id,surface,target,note,status,created_at)"
39
+ " VALUES(?,?,?,?, 'active', ?)"
40
+ " ON CONFLICT(user_id,surface,target) DO UPDATE SET status='active', note=excluded.note",
41
+ (user_id, surface, target, note, time.time()),
42
+ )
43
+
44
+ def revoke(self, user_id: str, surface: str, target: str) -> bool:
45
+ rc = execute_retry(
46
+ self.db_path,
47
+ "UPDATE outbound_allowlist SET status='revoked'"
48
+ " WHERE user_id=? AND surface=? AND target=? AND status='active'",
49
+ (user_id, surface, target),
50
+ )
51
+ return bool(rc)
52
+
53
+ def is_allowed(self, user_id: str, surface: str, target: str) -> bool:
54
+ row = execute_retry(
55
+ self.db_path,
56
+ "SELECT 1 FROM outbound_allowlist"
57
+ " WHERE user_id=? AND surface=? AND target=? AND status='active'",
58
+ (user_id, surface, target),
59
+ fetch="one",
60
+ )
61
+ return row is not None
62
+
63
+ def list(self, user_id: str) -> List[Dict]:
64
+ rows = execute_retry(
65
+ self.db_path,
66
+ "SELECT surface,target,note,status,created_at FROM outbound_allowlist"
67
+ " WHERE user_id=? ORDER BY created_at DESC",
68
+ (user_id,),
69
+ fetch="all",
70
+ ) or []
71
+ return [
72
+ {
73
+ "surface": r["surface"],
74
+ "target": r["target"],
75
+ "note": r["note"],
76
+ "status": r["status"],
77
+ "created_at": r["created_at"],
78
+ }
79
+ for r in rows
80
+ ]
@@ -0,0 +1,110 @@
1
+ """Drains OutboundDeliveryQueue: per-dest token-bucket pace, send via the surface, backoff
2
+ on failure, dead-letter after max_attempts. Deterministic backoff base (jitter added only
3
+ in the live run loop, not drain_once, so tests are reproducible)."""
4
+ import asyncio
5
+ import logging
6
+ import random
7
+ from typing import Callable, Optional
8
+
9
+ from core.surfaces.envelopes import OutboundMessage
10
+ from core.surfaces.outbound_queue import OutboundDeliveryQueue
11
+ from core.surfaces.rate_bucket import TokenBucket
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # TYPE_CHECKING import avoids a circular-import risk; the breaker is pure.
16
+ from typing import TYPE_CHECKING
17
+ if TYPE_CHECKING:
18
+ from core.surfaces.circuit import SurfaceCircuitBreaker
19
+
20
+
21
+ class OutboundDispatcher:
22
+ def __init__(self, queue: OutboundDeliveryQueue,
23
+ surface_lookup: Callable[[str], Optional[object]], *,
24
+ max_attempts: int = 6, base_backoff: float = 2.0,
25
+ rate_per_sec: float = 20.0, burst: int = 20,
26
+ circuit: Optional["SurfaceCircuitBreaker"] = None) -> None:
27
+ self._q = queue
28
+ self._lookup = surface_lookup
29
+ self._max = max_attempts
30
+ self._base = base_backoff
31
+ self._bucket = TokenBucket(rate_per_sec, burst)
32
+ self._cb = circuit # SurfaceCircuitBreaker | None
33
+ self._task: Optional[asyncio.Task] = None
34
+ self._stop = asyncio.Event()
35
+
36
+ async def drain_once(self, now: float) -> int:
37
+ delivered = 0
38
+ for row in self._q.claim_due(now):
39
+ surface_id = row["surface_id"]
40
+
41
+ # --- Circuit breaker: skip open surfaces; defer 30 s, attempts unchanged ---
42
+ if self._cb is not None and self._cb.is_open(surface_id):
43
+ self._q.reschedule(row["id"], next_attempt_at=now + 30,
44
+ attempts=row["attempts"])
45
+ logger.debug("outbound circuit OPEN: surface=%s deferred 30s", surface_id)
46
+ continue
47
+
48
+ key = f"{surface_id}:{row['dest']}"
49
+ allowed, retry_after = self._bucket.take(key, now=now)
50
+ if not allowed:
51
+ self._q.reschedule(row["id"], next_attempt_at=now + retry_after,
52
+ attempts=row["attempts"])
53
+ continue
54
+ surface = self._lookup(surface_id)
55
+ ok, err = False, "no surface"
56
+ if surface is not None:
57
+ try:
58
+ res = await surface.send(OutboundMessage(
59
+ session_key=row["session_key"], text=row["payload"],
60
+ ))
61
+ ok = bool(getattr(res, "success", False))
62
+ err = getattr(res, "error", None) or ("ok" if ok else "send returned False")
63
+ except Exception as exc: # fail-open: a raising surface reschedules, never crashes
64
+ ok, err = False, str(exc)
65
+ if ok:
66
+ if self._cb is not None:
67
+ self._cb.record_ok(surface_id)
68
+ self._q.mark_delivered(row["id"])
69
+ delivered += 1
70
+ else:
71
+ if self._cb is not None:
72
+ self._cb.record_fail(surface_id)
73
+ attempts = row["attempts"] + 1
74
+ if attempts >= self._max:
75
+ self._q.dead_letter(row["id"], err)
76
+ logger.error("outbound DEAD-LETTER id=%s surface=%s dest=%s err=%s",
77
+ row["id"], surface_id, row["dest"], err)
78
+ else:
79
+ backoff = self._base * (2 ** (attempts - 1))
80
+ self._q.reschedule(row["id"], next_attempt_at=now + backoff,
81
+ attempts=attempts, error=err)
82
+ return delivered
83
+
84
+ async def run(self, interval: float = 1.0) -> None:
85
+ import time as _t
86
+ while not self._stop.is_set():
87
+ try:
88
+ now_ts = _t.time()
89
+ if now_ts - getattr(self, "_last_reclaim", 0) > 60:
90
+ self._q.reclaim_inflight(older_than=now_ts - 120)
91
+ self._last_reclaim = now_ts
92
+ await self.drain_once(now=now_ts + random.uniform(0, 0.05))
93
+ except Exception as exc:
94
+ logger.error("outbound dispatcher loop error: %s", exc, exc_info=True)
95
+ try:
96
+ await asyncio.wait_for(self._stop.wait(), timeout=interval)
97
+ except asyncio.TimeoutError:
98
+ pass
99
+
100
+ def start(self) -> None:
101
+ if self._task is None:
102
+ self._task = asyncio.ensure_future(self.run())
103
+
104
+ async def stop(self) -> None:
105
+ self._stop.set()
106
+ if self._task is not None:
107
+ try:
108
+ await asyncio.wait_for(self._task, timeout=5.0)
109
+ except Exception:
110
+ self._task.cancel()
@@ -0,0 +1,37 @@
1
+ """Outbound-collapse mirror helpers (P1a).
2
+
3
+ Small factories that mirror an agent's discrete user-facing message into the
4
+ unified MessageRouter seam as a committed (partial=False) OutboundMessage. Additive
5
+ and gated on SINGULAR_CHAT_ENABLED; a no-op (and fail-open) when the flag is OFF or
6
+ no router / session_key has been bound, so the legacy add_to_feed path stays
7
+ byte-identical until later phases wire a router.
8
+ """
9
+ import logging
10
+ from typing import Any, Awaitable, Callable, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def build_discrete_publish(
16
+ router: Any, session_key: Optional[str]
17
+ ) -> Callable[..., Awaitable[None]]:
18
+ async def _publish(text: str) -> None:
19
+ from agents.task.surface_config import SurfaceConfig
20
+
21
+ if not SurfaceConfig.singular_chat_enabled():
22
+ return
23
+ if router is None or not session_key:
24
+ return
25
+ try:
26
+ from core.surfaces.envelopes import OutboundMessage, MessageKind
27
+
28
+ await router.publish(OutboundMessage(
29
+ session_key=session_key,
30
+ text=text,
31
+ kind=MessageKind.AGENT_TEXT,
32
+ partial=False,
33
+ ))
34
+ except Exception as e: # fail-open: outbound mirror is non-critical
35
+ logger.debug("discrete publish mirror failed: %s", e)
36
+
37
+ return _publish
@@ -0,0 +1,107 @@
1
+ """Durable outbound delivery queue (SQLite WAL). Converts MessageRouter's fire-and-forget
2
+ send into at-least-once-with-dedup: publish() enqueues; a dispatcher worker drains with
3
+ backoff + token-bucket, dead-lettering after N attempts. idempotency_key (hash of
4
+ session_key+turn_id+chunk_idx) dedups a redelivery after a worker crash."""
5
+ import logging
6
+ import time as _time
7
+ from typing import List, Optional
8
+
9
+ from core.sqlite_util import wal_connect, execute_retry
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ _SCHEMA = """
14
+ CREATE TABLE IF NOT EXISTS outbound_queue (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ idempotency_key TEXT UNIQUE,
17
+ session_key TEXT NOT NULL,
18
+ surface_id TEXT NOT NULL,
19
+ dest TEXT,
20
+ payload TEXT NOT NULL,
21
+ kind TEXT DEFAULT 'agent_text',
22
+ state TEXT DEFAULT 'pending', -- pending|inflight|delivered|dead
23
+ attempts INTEGER DEFAULT 0,
24
+ next_attempt_at REAL DEFAULT 0,
25
+ last_error TEXT,
26
+ created_at REAL DEFAULT (strftime('%s','now')),
27
+ updated_at REAL DEFAULT (strftime('%s','now'))
28
+ )
29
+ """
30
+
31
+
32
+ class OutboundDeliveryQueue:
33
+ def __init__(self, db_path: str) -> None:
34
+ self.db_path = db_path
35
+ conn = wal_connect(db_path)
36
+ try:
37
+ conn.execute(_SCHEMA)
38
+ conn.commit()
39
+ finally:
40
+ conn.close()
41
+
42
+ def enqueue(self, *, idempotency_key: str, session_key: str, surface_id: str,
43
+ dest: Optional[str], payload: str, kind: str = "agent_text") -> bool:
44
+ inserted = execute_retry(
45
+ self.db_path,
46
+ """INSERT OR IGNORE INTO outbound_queue
47
+ (idempotency_key, session_key, surface_id, dest, payload, kind, next_attempt_at)
48
+ VALUES (?, ?, ?, ?, ?, ?, 0)""",
49
+ (idempotency_key, session_key, surface_id, dest, payload, kind),
50
+ )
51
+ return inserted == 1
52
+
53
+ def claim_due(self, now: float, limit: int = 20) -> List[dict]:
54
+ # Two-step claim under WAL: select due ids, then CAS each to 'inflight'.
55
+ rows = execute_retry(
56
+ self.db_path,
57
+ """SELECT * FROM outbound_queue
58
+ WHERE state='pending' AND next_attempt_at <= ?
59
+ ORDER BY id ASC LIMIT ?""",
60
+ (now, limit), fetch="all",
61
+ ) or []
62
+ claimed = []
63
+ for r in rows:
64
+ n = execute_retry(
65
+ self.db_path,
66
+ "UPDATE outbound_queue SET state='inflight', updated_at=? WHERE id=? AND state='pending'",
67
+ (now, r["id"]),
68
+ )
69
+ if n == 1:
70
+ d = dict(r); d["state"] = "inflight"; claimed.append(d)
71
+ return claimed
72
+
73
+ def mark_delivered(self, row_id: int) -> None:
74
+ execute_retry(self.db_path,
75
+ "UPDATE outbound_queue SET state='delivered', updated_at=? WHERE id=?",
76
+ (_time.time(), row_id))
77
+
78
+ def reschedule(self, row_id: int, *, next_attempt_at: float, attempts: int,
79
+ error: Optional[str] = None) -> None:
80
+ execute_retry(
81
+ self.db_path,
82
+ """UPDATE outbound_queue SET state='pending', attempts=?, next_attempt_at=?,
83
+ last_error=?, updated_at=? WHERE id=?""",
84
+ (attempts, next_attempt_at, error, _time.time(), row_id),
85
+ )
86
+
87
+ def dead_letter(self, row_id: int, error: str) -> None:
88
+ execute_retry(self.db_path,
89
+ "UPDATE outbound_queue SET state='dead', last_error=?, updated_at=? WHERE id=?",
90
+ (error, _time.time(), row_id))
91
+
92
+ def counts(self) -> dict:
93
+ rows = execute_retry(self.db_path,
94
+ "SELECT state, COUNT(*) c FROM outbound_queue GROUP BY state",
95
+ fetch="all") or []
96
+ out = {"pending": 0, "inflight": 0, "delivered": 0, "dead": 0}
97
+ for r in rows:
98
+ out[r["state"]] = r["c"]
99
+ return out
100
+
101
+ def reclaim_inflight(self, older_than: float) -> int:
102
+ """Restart-recovery: return long-inflight rows to 'pending' (a worker died mid-send)."""
103
+ return execute_retry(
104
+ self.db_path,
105
+ "UPDATE outbound_queue SET state='pending' WHERE state='inflight' AND updated_at < ?",
106
+ (older_than,),
107
+ ) or 0
@@ -0,0 +1,12 @@
1
+ """Pure resolver: is this outbound target the owner, an allowlisted party, or denied?
2
+ Owner-address map is injected (from core/instance resolvers at the call site) so this
3
+ stays pure and unit-testable."""
4
+
5
+ def resolve_target_tier(*, surface: str, target: str, user_id: str, allowlist,
6
+ owner_targets: dict) -> str:
7
+ owner_addr = (owner_targets or {}).get(surface)
8
+ if owner_addr is not None and str(target) == str(owner_addr):
9
+ return "owner"
10
+ if allowlist is not None and allowlist.is_allowed(user_id, surface, target):
11
+ return "allowlisted"
12
+ return "denied"
@@ -0,0 +1,29 @@
1
+ """WS-D: owner/access quick-access — one read of the access SSOT.
2
+
3
+ A single place that reports who the agent serves and the per-surface access posture, so
4
+ the `polyrob owner` CLI (and any admin surface) doesn't re-derive it from scattered
5
+ flags. Owner-by-email is reported as a hard OFF in v1 (a forgeable From: can never
6
+ confer OWNER tier until verified-sender lands).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict
11
+
12
+
13
+ def owner_access_summary() -> Dict[str, Any]:
14
+ from core.instance import resolve_owner_principal
15
+ from agents.task.surface_config import SurfaceConfig
16
+ return {
17
+ # STRICT: report only an EXPLICITLY-bound owner (None = running on the
18
+ # auto-derived instance-id default), so the summary reflects real config.
19
+ "owner_principal": resolve_owner_principal(default_to_instance=False),
20
+ "correspondent_access_enabled": SurfaceConfig.correspondent_access_enabled(),
21
+ "require_approval": SurfaceConfig.correspondent_require_approval(),
22
+ "max_new_correspondents_per_day": SurfaceConfig.correspondent_max_new_per_day(),
23
+ "surfaces": {
24
+ "telegram": SurfaceConfig.telegram_surface_enabled(),
25
+ "whatsapp": SurfaceConfig.whatsapp_surface_enabled(),
26
+ "email": SurfaceConfig.email_surface_enabled(),
27
+ },
28
+ "owner_by_email": False, # v1: hard OFF (forgeable From:)
29
+ }