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
webview/activity.py ADDED
@@ -0,0 +1,636 @@
1
+ """Global activity stream — the `/activity` terminal's server side.
2
+
3
+ One normalized event shape for everything that happens on the instance:
4
+
5
+ {id, ts, source, user_id, session_id, kind, summary, payload}
6
+
7
+ Sources (all cross-process safe — the webview is a separate process from the
8
+ agent in prod):
9
+
10
+ - ``feed`` — per-session ``feed/*.json`` files (rich agent telemetry),
11
+ pushed live by a single recursive ``watchfiles`` watcher.
12
+ - ``telemetry`` — the durable ``telemetry_events.db`` append-only log
13
+ (cron_run / self_wake / wallet_spend / tool_denied / ...).
14
+ - ``goal`` — ``goals.db::goal_events`` (id-cursor tail).
15
+ - ``skill`` — ``skill_usage.db::skill_install_audit`` (id-cursor tail).
16
+
17
+ Design notes: NO ``from __future__ import annotations`` (repo landmine);
18
+ flags via ``os.environ``/``core.env.bool_env`` (never ``BotConfig.get``);
19
+ everything fail-open — a missing DB / unreadable file degrades to silence,
20
+ never a 500 or a crashed watcher.
21
+ """
22
+ import asyncio
23
+ import itertools
24
+ import json
25
+ import logging
26
+ import os
27
+ import sqlite3
28
+ import time
29
+ from collections import OrderedDict, deque
30
+ from pathlib import Path
31
+ from typing import Any, Dict, List, Optional, Tuple
32
+
33
+ from fastapi import APIRouter, HTTPException, Request
34
+ from fastapi.responses import HTMLResponse, JSONResponse
35
+ from fastapi.templating import Jinja2Templates
36
+
37
+ from webview import webgate
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ router = APIRouter()
42
+
43
+
44
+ def _templates() -> Jinja2Templates:
45
+ """Same asset base as server.py/pages.py (packaged dir, repo fallback)."""
46
+ try:
47
+ from core.assets import webgate_asset_dir
48
+ templates_dir = webgate_asset_dir() / "templates"
49
+ except Exception: # fail-open to the repo checkout
50
+ templates_dir = Path(__file__).resolve().parent / "templates"
51
+ return Jinja2Templates(directory=str(templates_dir))
52
+
53
+
54
+ _TEMPLATES = _templates()
55
+ _TEMPLATES.env.globals["console_display_name"] = webgate.console_display_name
56
+ _TEMPLATES.env.globals["branding"] = webgate.branding_config
57
+ # Posture default for the layout's tenant-nav block (P0-3) — same global as
58
+ # server.py's/pages.py's envs.
59
+ _TEMPLATES.env.globals["is_multitenant_posture"] = webgate.is_multitenant
60
+ try:
61
+ from core.version import get_version
62
+ _TEMPLATES.env.globals["get_version"] = get_version
63
+ except Exception:
64
+ _TEMPLATES.env.globals["get_version"] = lambda: ""
65
+
66
+ # Feed kinds that are pure token-stream noise in a global terminal. They stay
67
+ # visible in the per-session view; the global stream drops them.
68
+ _SUPPRESSED_FEED_KINDS = {"streaming_output"}
69
+
70
+ _PAYLOAD_CAP_BYTES = 16000
71
+
72
+
73
+ def _snip(value: Any, n: int = 100) -> str:
74
+ text = str(value or "").strip().replace("\n", " ")
75
+ return text[: n - 1] + "…" if len(text) > n else text
76
+
77
+
78
+ def _cap_payload(obj: Any) -> Any:
79
+ """Bound payload size so one huge event can't bloat the buffer/wire."""
80
+ try:
81
+ dumped = json.dumps(obj, default=str)
82
+ except Exception:
83
+ return {"_truncated": True, "keys": []}
84
+ if len(dumped) <= _PAYLOAD_CAP_BYTES:
85
+ return obj
86
+ keys = sorted(obj.keys()) if isinstance(obj, dict) else []
87
+ return {"_truncated": True, "keys": keys, "preview": dumped[:2000]}
88
+
89
+
90
+ # Goal-title cache (P1-6): goal events carry only a goal_id; the title lives in
91
+ # goals.db. Bounded LRU, fail-open (a miss/error is just "no title"), and only
92
+ # successful lookups are cached so a transient DB error can't stick.
93
+ _GOAL_TITLE_CACHE: "OrderedDict[str, str]" = OrderedDict()
94
+ _GOAL_TITLE_CACHE_MAX = 256
95
+
96
+
97
+ def _goal_title(goal_id: Any) -> Optional[str]:
98
+ if not goal_id:
99
+ return None
100
+ gid = str(goal_id)
101
+ cached = _GOAL_TITLE_CACHE.get(gid)
102
+ if cached is not None:
103
+ _GOAL_TITLE_CACHE.move_to_end(gid)
104
+ return cached
105
+ db_path = os.path.join(_data_dir(), "goals.db")
106
+ try:
107
+ con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1.0)
108
+ try:
109
+ row = con.execute("SELECT title FROM goals WHERE id=?", (gid,)).fetchone()
110
+ finally:
111
+ con.close()
112
+ except Exception:
113
+ return None
114
+ title = str(row[0]) if row and row[0] else None
115
+ if title:
116
+ _GOAL_TITLE_CACHE[gid] = title
117
+ while len(_GOAL_TITLE_CACHE) > _GOAL_TITLE_CACHE_MAX:
118
+ _GOAL_TITLE_CACHE.popitem(last=False)
119
+ return title
120
+
121
+
122
+ def _ensure_goal_title(data: Dict[str, Any]) -> Dict[str, Any]:
123
+ """Fill ``title`` from goals.db when a goal event carries only goal_id."""
124
+ if isinstance(data, dict) and data.get("goal_id") and not data.get("title"):
125
+ title = _goal_title(data.get("goal_id"))
126
+ if title:
127
+ data["title"] = title
128
+ return data
129
+
130
+
131
+ def summarize(kind: str, data: Dict[str, Any]) -> str:
132
+ """One terminal line of human summary per event kind. Defensive: every
133
+ field is optional; unknown kinds fall back to the kind itself."""
134
+ d = data if isinstance(data, dict) else {}
135
+ if kind == "tool_execution":
136
+ name = d.get("tool_name") or d.get("action") or d.get("name") or "?"
137
+ ok = d.get("success", True)
138
+ return f"tool {name} {'ok' if ok else 'FAILED'}"
139
+ if kind == "llm_request":
140
+ model = d.get("model_name") or d.get("model") or "?"
141
+ tokens = d.get("token_count") or d.get("total_tokens") or 0
142
+ cost = d.get("cost_estimate")
143
+ tail = f" ${cost:.4f}" if isinstance(cost, (int, float)) and cost else ""
144
+ return f"llm {model} {tokens}tk{tail}"
145
+ if kind == "step":
146
+ n = d.get("iteration", d.get("step", "?"))
147
+ note = _snip(d.get("task_progress") or d.get("reasoning") or "", 80)
148
+ return f"step {n}" + (f": {note}" if note else "")
149
+ if kind == "session_start":
150
+ return f"session started: {_snip(d.get('task') or d.get('task_description') or '', 90)}"
151
+ if kind in ("session_completion", "task_complete"):
152
+ return "session completed"
153
+ if kind == "agent_end":
154
+ return "agent ended"
155
+ if kind == "agent_registration":
156
+ return f"agent {d.get('agent_name') or d.get('name') or '?'} registered"
157
+ if kind == "available_actions":
158
+ actions = d.get("actions")
159
+ count = len(actions) if isinstance(actions, (list, dict)) else d.get("count", "?")
160
+ return f"actions registered ({count})"
161
+ if kind == "status":
162
+ return f"status → {d.get('status', '?')}"
163
+ if kind == "error":
164
+ return f"ERROR: {_snip(d.get('error_message') or d.get('message') or d.get('error') or '', 120)}"
165
+ if kind == "cron_run":
166
+ return f"cron {d.get('job_id', '?')} → {d.get('outcome', '?')}"
167
+ if kind == "self_wake":
168
+ return f"self-wake → {d.get('outcome', '?')} ({_snip(d.get('reason'), 60)})"
169
+ if kind == "wallet_spend":
170
+ return f"wallet spend ${d.get('amount_usd', '?')} @ {d.get('venue', '?')}"
171
+ if kind == "tool_denied":
172
+ return f"tool DENIED: {d.get('tool') or d.get('tool_name') or '?'}"
173
+ if kind == "tool_timeout":
174
+ return f"tool timeout: {d.get('tool') or d.get('tool_name') or '?'}"
175
+ if kind == "autonomy_tick":
176
+ return f"autonomy tick {d.get('loop', '?')} alive={d.get('alive', '?')}"
177
+ if kind == "owner_notice":
178
+ return f"owner notice: {_snip(d.get('message') or d.get('text') or '', 100)}"
179
+ if kind == "skill_install":
180
+ return f"skill installed: {d.get('name', '?')} (by {d.get('approver') or d.get('source') or '?'})"
181
+ if kind.startswith("goal_"):
182
+ # goal_run rides outcome (started/done/failed/blocked); goal-board
183
+ # events carry the verb in the kind (created/claimed/succeeded/...).
184
+ verb = kind[len("goal_"):]
185
+ outcome = d.get("outcome") or d.get("status")
186
+ if outcome:
187
+ verb = f"{verb} {outcome}" if verb == "run" else f"{verb} → {outcome}"
188
+ title = _snip(d.get("title") or "", 70)
189
+ gid = _snip(d.get("goal_id") or "?", 12)
190
+ line = f"goal {verb}: {title} ({gid[:8]})" if title else f"goal {gid[:8]} {verb}"
191
+ reason = d.get("reason") or d.get("error")
192
+ if reason and str(outcome or "") in ("failed", "blocked", "gave_up"):
193
+ line += f" — {_snip(reason, 80)}"
194
+ return line
195
+ return kind
196
+
197
+
198
+ def normalize_feed_event(user_id: str, session_id: str, raw: Dict[str, Any]) -> Optional[Dict[str, Any]]:
199
+ """Normalize one per-session feed file's JSON into the activity shape.
200
+
201
+ Handles both writer shapes: telemetry service files ``{..., _seq, _ts_ms,
202
+ _id}`` and SessionManager files ``{timestamp, type, data}``. Returns None
203
+ for suppressed kinds.
204
+ """
205
+ if not isinstance(raw, dict):
206
+ return None
207
+ kind = raw.get("type") or raw.get("event_type") or "event"
208
+ if kind in _SUPPRESSED_FEED_KINDS:
209
+ return None
210
+ if raw.get("_ts_ms"):
211
+ ts = float(raw["_ts_ms"]) / 1000.0
212
+ else:
213
+ try:
214
+ ts = float(raw.get("timestamp") or 0.0) or time.time()
215
+ except (TypeError, ValueError):
216
+ ts = time.time()
217
+ data = raw.get("data") if isinstance(raw.get("data"), dict) else raw
218
+ ev_id = raw.get("_id") or f"feed:{session_id}:{raw.get('_seq') or int(ts * 1000)}"
219
+ return {
220
+ "id": ev_id,
221
+ "ts": ts,
222
+ "source": "feed",
223
+ "user_id": user_id or "",
224
+ "session_id": session_id or "",
225
+ "kind": kind,
226
+ "summary": summarize(kind, data),
227
+ "payload": _cap_payload(raw),
228
+ }
229
+
230
+
231
+ def _parse_json_field(value: Any) -> Dict[str, Any]:
232
+ if isinstance(value, dict):
233
+ return value
234
+ try:
235
+ parsed = json.loads(value or "{}")
236
+ return parsed if isinstance(parsed, dict) else {}
237
+ except Exception:
238
+ return {}
239
+
240
+
241
+ def normalize_db_event(source: str, row: Dict[str, Any]) -> Dict[str, Any]:
242
+ """Normalize one SQLite tail row (telemetry / goal / skill) into the
243
+ activity shape."""
244
+ if source == "goal":
245
+ payload = _parse_json_field(row.get("payload"))
246
+ payload.setdefault("goal_id", row.get("goal_id"))
247
+ _ensure_goal_title(payload)
248
+ kind = f"goal_{row.get('kind', 'event')}"
249
+ return {
250
+ "id": f"goal:{row.get('id')}",
251
+ "ts": float(row.get("created_at") or 0.0),
252
+ "source": "goal",
253
+ "user_id": str(payload.get("user_id") or ""),
254
+ "session_id": str(payload.get("session_id") or ""),
255
+ "kind": kind,
256
+ "summary": summarize(kind, payload),
257
+ "payload": _cap_payload(payload),
258
+ }
259
+ if source == "skill":
260
+ payload = {
261
+ "name": row.get("name"),
262
+ "source": row.get("source"),
263
+ "resolved_sha": row.get("resolved_sha"),
264
+ "approver": row.get("approver"),
265
+ }
266
+ return {
267
+ "id": f"skill:{row.get('id')}",
268
+ "ts": float(row.get("ts") or 0.0),
269
+ "source": "skill",
270
+ "user_id": str(row.get("user_id") or ""),
271
+ "session_id": "",
272
+ "kind": "skill_install",
273
+ "summary": summarize("skill_install", payload),
274
+ "payload": payload,
275
+ }
276
+ # default: durable telemetry event log row
277
+ attrs = _parse_json_field(row.get("attrs"))
278
+ kind = str(row.get("kind") or "event")
279
+ if kind.startswith("goal"):
280
+ _ensure_goal_title(attrs)
281
+ return {
282
+ "id": f"telemetry:{row.get('id')}",
283
+ "ts": float(row.get("ts") or 0.0),
284
+ "source": "telemetry",
285
+ "user_id": str(row.get("user_id") or ""),
286
+ "session_id": str(row.get("session_id") or ""),
287
+ "kind": kind,
288
+ "summary": summarize(kind, attrs),
289
+ "payload": _cap_payload(attrs),
290
+ }
291
+
292
+
293
+ class SqliteTail:
294
+ """Id-cursor tail over an append-only SQLite table (read-only, WAL-safe).
295
+
296
+ ``prime()`` sets the cursor to the current MAX(id) so a fresh webview
297
+ never floods the stream with history; ``poll()`` returns only rows that
298
+ arrived since. A missing DB/table is silence, never an error — the tail
299
+ starts delivering when the file appears.
300
+ """
301
+
302
+ def __init__(self, db_path: str, table: str, id_col: str = "id"):
303
+ self.db_path = str(db_path)
304
+ self.table = table
305
+ self.id_col = id_col
306
+ self.cursor = 0
307
+
308
+ def _query(self, sql: str, params: tuple = ()) -> List[Dict[str, Any]]:
309
+ con = sqlite3.connect(f"file:{self.db_path}?mode=ro", uri=True, timeout=1.0)
310
+ try:
311
+ con.row_factory = sqlite3.Row
312
+ return [dict(r) for r in con.execute(sql, params).fetchall()]
313
+ finally:
314
+ con.close()
315
+
316
+ def prime(self) -> None:
317
+ try:
318
+ rows = self._query(f"SELECT MAX({self.id_col}) AS m FROM {self.table}")
319
+ self.cursor = int(rows[0]["m"] or 0)
320
+ except Exception:
321
+ self.cursor = 0
322
+
323
+ def poll(self, limit: int = 500) -> List[Dict[str, Any]]:
324
+ try:
325
+ rows = self._query(
326
+ f"SELECT * FROM {self.table} WHERE {self.id_col} > ? "
327
+ f"ORDER BY {self.id_col} ASC LIMIT ?",
328
+ (self.cursor, limit),
329
+ )
330
+ except Exception:
331
+ return []
332
+ if rows:
333
+ self.cursor = int(rows[-1][self.id_col])
334
+ return rows
335
+
336
+
337
+ def _data_dir() -> str:
338
+ """Data home for goals.db/skill_usage.db — same resolution as pages.py."""
339
+ return os.environ.get("POLYROB_DATA_DIR", "data")
340
+
341
+
342
+ def _sessions_data_root() -> Optional[str]:
343
+ """The PathManager session root (feed dirs + telemetry_events.db)."""
344
+ try:
345
+ from agents.task.path import pm
346
+ return str(pm().data_root)
347
+ except Exception:
348
+ return None
349
+
350
+
351
+ def activity_db_sources() -> List[Tuple[str, SqliteTail]]:
352
+ """The three SQLite tails feeding the system half of the stream."""
353
+ root = _sessions_data_root()
354
+ tel = (
355
+ os.path.join(root, "telemetry_events.db")
356
+ if root
357
+ else os.path.join(_data_dir(), "telemetry_events.db")
358
+ )
359
+ return [
360
+ ("telemetry", SqliteTail(tel, "telemetry_events")),
361
+ ("goal", SqliteTail(os.path.join(_data_dir(), "goals.db"), "goal_events")),
362
+ ("skill", SqliteTail(os.path.join(_data_dir(), "skill_usage.db"), "skill_install_audit")),
363
+ ]
364
+
365
+
366
+ def feed_path_info(path: str, data_root: str) -> Optional[Tuple[str, str]]:
367
+ """Derive ``(user_id, session_id)`` from a feed file path, or None.
368
+
369
+ Accepts exactly ``{data_root}/{user}/{session}/feed/*.json``.
370
+ """
371
+ try:
372
+ rel = Path(path).resolve().relative_to(Path(data_root).resolve())
373
+ except Exception:
374
+ return None
375
+ parts = rel.parts
376
+ if len(parts) != 4 or parts[2] != "feed" or not parts[3].endswith(".json"):
377
+ return None
378
+ return parts[0], parts[1]
379
+
380
+
381
+ class ActivityHub:
382
+ """In-process fan-in for normalized activity events.
383
+
384
+ Ring-buffers the last 1000 events for reconnect snapshots/backfill and
385
+ owns the watcher + tail loop lifecycle (started lazily on first
386
+ ``join_activity``, stopped when the activity room empties).
387
+ """
388
+
389
+ def __init__(self):
390
+ self.buffer: deque = deque(maxlen=1000)
391
+ self._counter = itertools.count(1)
392
+ self._sio = None
393
+ self._tasks: List[asyncio.Task] = []
394
+ self._sources: List[Tuple[str, SqliteTail]] = []
395
+ self._seen_files: deque = deque(maxlen=2048)
396
+ self._seen_set: set = set()
397
+ self.started = False
398
+
399
+ def record(self, ev: Dict[str, Any]) -> Dict[str, Any]:
400
+ if not ev.get("id"):
401
+ ev["id"] = f"act:{next(self._counter)}"
402
+ self.buffer.append(ev)
403
+ return ev
404
+
405
+ def recent(self, limit: int = 200) -> List[Dict[str, Any]]:
406
+ if limit <= 0:
407
+ return []
408
+ return list(self.buffer)[-limit:]
409
+
410
+ # --- lifecycle --------------------------------------------------------- #
411
+
412
+ def start(self, sio) -> None:
413
+ """Start the tail loop + feed watcher (idempotent; needs a running loop)."""
414
+ if self.started:
415
+ return
416
+ self.started = True
417
+ self._sio = sio
418
+ self._sources = activity_db_sources()
419
+ for _, tail in self._sources:
420
+ tail.prime()
421
+ self._tasks = [
422
+ asyncio.create_task(self._tail_loop()),
423
+ asyncio.create_task(self._run_watcher()),
424
+ ]
425
+ logger.info("activity hub started (tails + feed watcher)")
426
+
427
+ def stop(self) -> None:
428
+ """Cancel the loops (sync contexts / backstop). Prefer ``aclose()``
429
+ from async code so the cancelled tasks are actually awaited."""
430
+ self.started = False
431
+ for task in self._tasks:
432
+ task.cancel()
433
+ self._tasks = []
434
+ logger.info("activity hub stopped")
435
+
436
+ async def aclose(self) -> None:
437
+ """Cancel AND drain the loops — no pending-task warnings at loop close."""
438
+ self.started = False
439
+ tasks, self._tasks = self._tasks, []
440
+ for task in tasks:
441
+ task.cancel()
442
+ for task in tasks:
443
+ try:
444
+ await task
445
+ except (asyncio.CancelledError, Exception):
446
+ pass
447
+ logger.info("activity hub stopped")
448
+
449
+ async def _emit(self, ev: Dict[str, Any]) -> None:
450
+ self.record(ev)
451
+ if self._sio is not None:
452
+ try:
453
+ await self._sio.emit("activity_event", ev, room="activity")
454
+ except Exception as exc: # emit failure must never kill the loops
455
+ logger.debug(f"activity emit failed: {exc}")
456
+
457
+ async def _tail_loop(self) -> None:
458
+ try:
459
+ interval = float(os.environ.get("WEBVIEW_ACTIVITY_TAIL_SEC", "2.0") or 2.0)
460
+ except (TypeError, ValueError):
461
+ interval = 2.0
462
+ while True:
463
+ try:
464
+ for source, tail in self._sources:
465
+ for row in tail.poll():
466
+ await self._emit(normalize_db_event(source, row))
467
+ except asyncio.CancelledError:
468
+ raise
469
+ except Exception as exc:
470
+ logger.debug(f"activity tail error: {exc}")
471
+ await asyncio.sleep(interval)
472
+
473
+ async def _run_watcher(self) -> None:
474
+ try:
475
+ from watchfiles import Change, awatch
476
+ except Exception:
477
+ logger.warning("watchfiles unavailable — feed half of activity stream disabled")
478
+ return
479
+ root = _sessions_data_root()
480
+ if not root or not os.path.isdir(root):
481
+ logger.warning("activity watcher: no session data root — feed stream disabled")
482
+ return
483
+
484
+ def _wants(change, path: str) -> bool:
485
+ return path.endswith(".json") and (os.sep + "feed" + os.sep) in path
486
+
487
+ try:
488
+ async for changes in awatch(root, watch_filter=_wants):
489
+ for change, path in changes:
490
+ if change not in (Change.added, Change.modified):
491
+ continue
492
+ if path in self._seen_set:
493
+ continue # feed files are write-once; first event wins
494
+ self._seen_set.add(path)
495
+ self._seen_files.append(path)
496
+ if len(self._seen_files) == self._seen_files.maxlen:
497
+ oldest = self._seen_files.popleft()
498
+ self._seen_set.discard(oldest)
499
+ info = feed_path_info(path, root)
500
+ if not info:
501
+ continue
502
+ ev = self._read_feed_file(path, info[0], info[1])
503
+ if ev:
504
+ await self._emit(ev)
505
+ except asyncio.CancelledError:
506
+ raise
507
+ except Exception as exc:
508
+ logger.warning(f"activity feed watcher stopped: {exc}")
509
+
510
+ def _read_feed_file(self, path: str, user_id: str, session_id: str) -> Optional[Dict[str, Any]]:
511
+ try:
512
+ with open(path, "r", encoding="utf-8") as fh:
513
+ raw = json.load(fh)
514
+ except Exception:
515
+ return None
516
+ return normalize_feed_event(user_id, session_id, raw)
517
+
518
+
519
+ _hub: Optional[ActivityHub] = None
520
+
521
+
522
+ def get_hub() -> ActivityHub:
523
+ global _hub
524
+ if _hub is None:
525
+ _hub = ActivityHub()
526
+ return _hub
527
+
528
+
529
+ # --- access gate + HTTP surface --------------------------------------------- #
530
+
531
+ def _require_activity_access(request: Request) -> None:
532
+ """The global stream is inherently cross-tenant, so:
533
+
534
+ - flag off → 404 (feature not there);
535
+ - local → open (loopback operator IS the owner);
536
+ - own_ops → the auth middleware already required the single owner's
537
+ cookie for any non-public path, which /activity is — nothing extra;
538
+ - multitenant → admin tier / is_admin / the instance owner ONLY. A plain
539
+ authenticated tenant is refused: this page shows everyone's activity.
540
+ """
541
+ if not webgate.activity_enabled():
542
+ raise HTTPException(status_code=404, detail="Activity stream disabled")
543
+ if not webgate.requires_owner_login():
544
+ return
545
+ if webgate.is_multitenant():
546
+ state = getattr(request, "state", None)
547
+ user_id = getattr(state, "user_id", None)
548
+ tier = getattr(state, "tier", None)
549
+ is_admin = bool(getattr(state, "is_admin", False))
550
+ if is_admin or tier == "admin" or (user_id and user_id == webgate.local_owner_id()):
551
+ return
552
+ raise HTTPException(status_code=403, detail="Owner/admin access required")
553
+ return # own_ops: single-owner model (H2b)
554
+
555
+
556
+ def _recent_feed_events(per_session: int = 20, sessions: int = 3) -> List[Dict[str, Any]]:
557
+ """Cold-start seed: the last few feed events of the most recent sessions."""
558
+ root = _sessions_data_root()
559
+ if not root or not os.path.isdir(root):
560
+ return []
561
+ candidates = []
562
+ try:
563
+ with os.scandir(root) as users:
564
+ for user_entry in users:
565
+ if not user_entry.is_dir():
566
+ continue
567
+ try:
568
+ with os.scandir(user_entry.path) as session_dirs:
569
+ for sess_entry in session_dirs:
570
+ if not sess_entry.is_dir():
571
+ continue
572
+ feed = os.path.join(sess_entry.path, "feed")
573
+ if os.path.isdir(feed):
574
+ candidates.append(
575
+ (os.path.getmtime(feed), user_entry.name, sess_entry.name, feed)
576
+ )
577
+ except OSError:
578
+ continue
579
+ except OSError:
580
+ return []
581
+ out: List[Dict[str, Any]] = []
582
+ for _, user_id, session_id, feed in sorted(candidates, reverse=True)[:sessions]:
583
+ try:
584
+ names = sorted(n for n in os.listdir(feed) if n.endswith(".json"))[-per_session:]
585
+ except OSError:
586
+ continue
587
+ for name in names:
588
+ try:
589
+ with open(os.path.join(feed, name), "r", encoding="utf-8") as fh:
590
+ raw = json.load(fh)
591
+ except Exception:
592
+ continue
593
+ ev = normalize_feed_event(user_id, session_id, raw)
594
+ if ev:
595
+ out.append(ev)
596
+ return out
597
+
598
+
599
+ def _cold_backfill(limit: int) -> List[Dict[str, Any]]:
600
+ """Backfill when the hub buffer is cold: recent DB rows + recent feeds."""
601
+ events: List[Dict[str, Any]] = []
602
+ for source, tail in activity_db_sources():
603
+ try:
604
+ rows = tail._query(
605
+ f"SELECT * FROM {tail.table} ORDER BY {tail.id_col} DESC LIMIT ?",
606
+ (min(limit, 100),),
607
+ )
608
+ except Exception:
609
+ continue
610
+ events.extend(normalize_db_event(source, row) for row in reversed(rows))
611
+ events.extend(_recent_feed_events())
612
+ events.sort(key=lambda ev: ev.get("ts", 0.0))
613
+ return events[-limit:]
614
+
615
+
616
+ @router.get("/activity", response_class=HTMLResponse)
617
+ async def activity_page(request: Request) -> Any:
618
+ _require_activity_access(request)
619
+ return _TEMPLATES.TemplateResponse(
620
+ "activity.html",
621
+ {"request": request, "read_only": webgate.read_only()},
622
+ )
623
+
624
+
625
+ @router.get("/api/activity/backfill")
626
+ async def activity_backfill(request: Request, limit: int = 200) -> JSONResponse:
627
+ _require_activity_access(request)
628
+ try:
629
+ limit = max(1, min(int(limit), 1000))
630
+ except (TypeError, ValueError):
631
+ limit = 200
632
+ hub = get_hub()
633
+ events = hub.recent(limit)
634
+ if not events:
635
+ events = _cold_backfill(limit)
636
+ return JSONResponse({"events": events})