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,627 @@
1
+ """Skill storage scopes (Task 8 — data-safety fix).
2
+
3
+ Splits "where do skills live" into named, precedence-ordered SCOPES so writable
4
+ per-tenant skills can move OUT of the installed package/code tree and into
5
+ data-home, where they survive a ``polyrob update`` code-swap.
6
+
7
+ Before this module existed, ``SkillManager``/``SkillWriterMixin`` wrote
8
+ ``user_<uid>/`` skills directly under the same directory that ships the
9
+ curated SYSTEM skills (``data/prompts/skills/``) — a directory that lives
10
+ INSIDE the installed code/package tree. ``polyrob update``'s code-swap
11
+ replaces that whole tree, so every self-authored skill a user/agent had built
12
+ up was silently destroyed on update.
13
+
14
+ Three named scopes, precedence **project > user > builtin**:
15
+
16
+ - **project** — a per-repo ``.agents/skills`` / ``.claude/skills`` dir.
17
+ STUB: Task 15 owns real discovery/loading; this module only reserves the
18
+ scope + precedence slot so that later work doesn't have to re-litigate
19
+ ordering. Not read from or written to yet.
20
+ - **user** — ``<data_home>/skills/user_<uid>/``. Writable; survives a
21
+ code-tree swap because data-home is never touched by ``polyrob update``.
22
+ - **builtin** — the shipped package ``data/prompts/skills/`` (the curated
23
+ system-skill library). Read-only, pre-vetted/trusted.
24
+
25
+ ``<data_home>`` is resolved via the EXACT same accessor
26
+ ``cli/update/context.py`` uses for its own data-home
27
+ (``core/runtime_paths.py::resolve_runtime_paths``), so ``POLYROB_DATA_DIR`` is
28
+ honored identically across the update and skill-storage code paths.
29
+
30
+ NOTE on migration: this module only changes WHERE NEW reads/writes land. Any
31
+ ``user_<uid>/`` directories that predate this change and still live under the
32
+ package tree are NOT moved by this module — that one-time migration is a
33
+ separate, deliberately-scoped follow-up (Task 9) so a data-moving operation
34
+ gets its own dedicated review.
35
+ """
36
+
37
+ import errno
38
+ import hashlib
39
+ import json
40
+ import logging
41
+ import os
42
+ import shutil
43
+ import tempfile
44
+ from dataclasses import dataclass
45
+ from pathlib import Path
46
+ from typing import List, Optional
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class SkillScope:
53
+ """One named root that skills can be read from / written to.
54
+
55
+ ``trusted`` describes the LOCATION's provenance (pre-vetted shipped
56
+ content vs. arbitrary/unreviewed content) — it is NOT a blanket "skip
57
+ security scanning" flag. See :func:`scan_exempt`, which additionally
58
+ requires ``not writable`` so a writable scope (today: user) can never
59
+ have its write-time content scan suppressed just because the scope is
60
+ marked trusted.
61
+ """
62
+
63
+ name: str
64
+ root: Path
65
+ writable: bool
66
+ trusted: bool
67
+
68
+
69
+ # The literal on-disk directory name under a scope's data-home root.
70
+ _SKILLS_SUBDIR = "skills"
71
+
72
+
73
+ def _builtin_root() -> Path:
74
+ """The installed package's shipped (builtin) skills directory.
75
+
76
+ Resolved from THIS file's own location rather than importing
77
+ ``skill_manager`` — the brief's suggested
78
+ ``Path(skill_manager.__file__).resolve().parents[3]`` would create a
79
+ ``skill_store`` <-> ``skill_manager`` import cycle, since ``skill_manager``
80
+ needs ``skill_store`` for the user/data-home scope. ``skill_store.py``
81
+ lives in the SAME directory as ``skill_manager.py``
82
+ (``agents/task/agent/``), so the identical ``parents[3]`` hop from this
83
+ file resolves to the same repo/package root:
84
+ ``agents/task/agent/<this file>`` -> parents[0]=agent, [1]=task,
85
+ [2]=agents, [3]=repo root. Verified empirically: this path exists and
86
+ contains ``rules.json`` (see ``test_skill_store_scopes.py``).
87
+ """
88
+ return Path(__file__).resolve().parents[3] / "data" / "prompts" / "skills"
89
+
90
+
91
+ def _data_home() -> Path:
92
+ """Resolve ``<data_home>`` the SAME way ``cli/update/context.py`` does,
93
+ so ``POLYROB_DATA_DIR`` (and the local-vs-server default split) is
94
+ honored identically for skill storage and for update snapshot/rollback.
95
+ """
96
+ from core.runtime_paths import resolve_runtime_paths
97
+
98
+ try:
99
+ from agents.task.constants import local_mode_enabled
100
+
101
+ local = local_mode_enabled()
102
+ except Exception:
103
+ local = False
104
+ rp = resolve_runtime_paths(local=local)
105
+ return Path(rp.data_home)
106
+
107
+
108
+ def skills_data_home() -> Path:
109
+ """``<data_home>/skills`` — the writable root for user-authored skills."""
110
+ return _data_home() / _SKILLS_SUBDIR
111
+
112
+
113
+ def builtin_scope() -> SkillScope:
114
+ """The shipped, read-only, pre-vetted system-skill library.
115
+
116
+ NOTE: this scope's root MAY also physically hold legacy ``user_<uid>/``
117
+ dirs at runtime until Task 9's migration moves them into ``user_scope()``.
118
+ Callers enumerating "the builtin skills" MUST exclude ``user_*``/dotted
119
+ entries (see :func:`builtin_skill_ids`) rather than assuming every child
120
+ of this root is a system skill.
121
+ """
122
+ return SkillScope(name="builtin", root=_builtin_root(), writable=False, trusted=True)
123
+
124
+
125
+ def user_scope() -> SkillScope:
126
+ """The writable, tenant-scoped, data-home skill root (survives updates)."""
127
+ return SkillScope(name="user", root=skills_data_home(), writable=True, trusted=True)
128
+
129
+
130
+ def _find_project_root_stub() -> Path:
131
+ """STUB project-skills root (Task 15 owns real discovery/loading).
132
+
133
+ Walks CWD up to the git root looking for a conventional per-repo skills
134
+ dir (``.agents/skills`` then ``.claude/skills``); falls back to
135
+ ``<cwd>/.agents/skills`` (need not exist) so ``project_scope()`` always
136
+ has a concrete ``root``. Bounded walk — never infinite even with an
137
+ unusual filesystem layout.
138
+ """
139
+ cur = Path.cwd().resolve()
140
+ for _ in range(64):
141
+ for rel in (".agents/skills", ".claude/skills"):
142
+ cand = cur / rel
143
+ if cand.is_dir():
144
+ return cand
145
+ if (cur / ".git").exists():
146
+ break
147
+ parent = cur.parent
148
+ if parent == cur:
149
+ break
150
+ cur = parent
151
+ return Path.cwd().resolve() / ".agents" / "skills"
152
+
153
+
154
+ def project_scope() -> SkillScope:
155
+ """Per-repo skills scope. `trusted` reflects the runtime trust gate so a caller that
156
+ inspects the scope sees the real policy; discovery still gates on
157
+ skill_discovery.trust_project_skills_effective().
158
+ """
159
+ from . import skill_discovery
160
+ trusted = False
161
+ try:
162
+ trusted = skill_discovery.trust_project_skills_effective()
163
+ except Exception:
164
+ trusted = False
165
+ root = _find_project_root_stub()
166
+ return SkillScope(name="project", root=root, writable=False, trusted=trusted)
167
+
168
+
169
+ def resolve_scopes() -> List[SkillScope]:
170
+ """All skill scopes in precedence order: project > user > builtin."""
171
+ return [project_scope(), user_scope(), builtin_scope()]
172
+
173
+
174
+ def scan_exempt(scope: SkillScope) -> bool:
175
+ """Whether content already resident in ``scope`` can skip a write-time
176
+ threat re-scan (future-proofs e.g. a reindex of the shipped library).
177
+
178
+ Only ``True`` for a read-only + trusted scope (today: builtin). A
179
+ writable scope is NEVER exempt regardless of its ``trusted`` flag —
180
+ ``trusted`` describes a root's provenance, not a license to skip scanning
181
+ NEW content written into it.
182
+
183
+ ``SkillWriterMixin.create_skill`` only ever targets ``user_scope()``
184
+ (``writable=True``), so this evaluates to ``False`` on every live write
185
+ call today — a deliberate no-op that keeps user-write scanning
186
+ unconditional. It exists so a future builtin-library reindex can reuse
187
+ one policy function instead of re-deriving it.
188
+ """
189
+ return scope.trusted and not scope.writable
190
+
191
+
192
+ def builtin_skill_ids(scope: Optional[SkillScope] = None) -> List[str]:
193
+ """Sorted ids of the shipped system skills under ``scope`` (default:
194
+ :func:`builtin_scope`), EXCLUDING ``user_*`` and dotted directories.
195
+
196
+ Mirrors ``SkillManager._iter_authored_skill_dirs``'s filter so "what
197
+ counts as a builtin skill" can't drift between the two call sites.
198
+ """
199
+ scope = scope or builtin_scope()
200
+ root = scope.root
201
+ if not root.is_dir():
202
+ return []
203
+ return sorted(
204
+ d.name
205
+ for d in root.iterdir()
206
+ if d.is_dir() and not d.name.startswith((".", "user_")) and (d / "SKILL.md").exists()
207
+ )
208
+
209
+
210
+ # =============================================================================
211
+ # Task 9: one-time legacy code-tree -> data-home migration
212
+ #
213
+ # Before Task 8, per-tenant ``user_<uid>/`` skills were written directly under
214
+ # the SAME shipped package tree that holds the curated system-skill library
215
+ # (``builtin_scope().root`` — now this module's ``legacy_root`` parameter).
216
+ # Task 8 redirected ``SkillManager``'s user-scope reads/writes to data-home
217
+ # instead; that alone makes any PRE-EXISTING ``user_<uid>/`` content on that
218
+ # tree instantly invisible (no ``polyrob update`` needed - simply running the
219
+ # new code stops looking there), and it would additionally be destroyed
220
+ # outright by the next ``polyrob update`` code-swap. This section moves that
221
+ # legacy content into data-home, once, so a deployment upgrading past Task 8
222
+ # doesn't lose it.
223
+ # =============================================================================
224
+
225
+ _MIGRATION_MARKER_NAME = ".migrated_v1"
226
+ _MIGRATION_LOCK_NAME = ".migrate.lock"
227
+ _SOURCE_RETAINED_BREADCRUMB = ".migrated_source_retained"
228
+
229
+
230
+ def _is_safe_component(name: str) -> bool:
231
+ """Whether a single path-segment name is safe to treat as a migration
232
+ source/destination component: no traversal (``.``/``..``), no embedded
233
+ separators, not empty.
234
+
235
+ Mirrors the SPIRIT of ``SkillManager.validate_skill_id``'s traversal
236
+ protection without importing ``skill_manager`` (would create an import
237
+ cycle - see the ``_builtin_root`` docstring above). Deliberately looser
238
+ than the full id-format regex (which requires lowercase-start/hyphens
239
+ only) so a legitimately-named pre-Task-8 skill isn't silently abandoned
240
+ by migration merely for predating stricter id validation; the traversal
241
+ guard here is a SECURITY check, not a format check.
242
+ """
243
+ if not name or name in (".", ".."):
244
+ return False
245
+ if "/" in name or "\\" in name or "\x00" in name:
246
+ return False
247
+ return True
248
+
249
+
250
+ def _sha256_file(path: Path) -> str:
251
+ h = hashlib.sha256()
252
+ with open(path, "rb") as f:
253
+ for chunk in iter(lambda: f.read(1 << 16), b""):
254
+ h.update(chunk)
255
+ return h.hexdigest()
256
+
257
+
258
+ def _migrate_one_skill(source_dir: Path, dest_dir: Path) -> bool:
259
+ """Move one skill directory from the legacy code tree into data-home.
260
+
261
+ Never destructive: copy to a temp dir alongside ``dest_dir`` -> sha256
262
+ verify the copied ``SKILL.md`` against the source -> place at ``dest_dir``
263
+ -> ONLY THEN attempt to remove the source. Placement is a fast
264
+ ``os.replace`` when same-filesystem; a CROSS-DEVICE placement (``EXDEV``:
265
+ package tree and data-home on different volumes, or a Windows cross-drive
266
+ move) falls back to a plain ``copytree`` that RE-VERIFIES the placed
267
+ destination and then deliberately LEAVES the source in place (copy+verify,
268
+ NO cross-volume remove — the brief's rule, and matching the EACCES stance).
269
+ On the same-filesystem path, the source removal is best-effort: a
270
+ ``PermissionError``/``OSError`` (read-only / root-owned site-packages
271
+ install) is tolerated, the source is left with a breadcrumb at the
272
+ destination, never raised, never retried.
273
+
274
+ Returns True iff a NEW, verified copy was placed at ``dest_dir``
275
+ (regardless of whether the legacy source could/should also be cleaned up).
276
+ """
277
+ if dest_dir.exists():
278
+ logger.debug("skill migration: destination already exists, skipping %s", dest_dir)
279
+ return False
280
+
281
+ source_skill_md = source_dir / "SKILL.md"
282
+ if source_skill_md.is_symlink() or not source_skill_md.is_file():
283
+ return False
284
+
285
+ try:
286
+ source_hash = _sha256_file(source_skill_md)
287
+ except Exception:
288
+ logger.warning("skill migration: could not hash %s - skipped", source_skill_md)
289
+ return False
290
+
291
+ dest_dir.parent.mkdir(parents=True, exist_ok=True)
292
+ tmp_root = Path(tempfile.mkdtemp(prefix=".migrate_tmp_", dir=str(dest_dir.parent)))
293
+ cross_volume = False
294
+ try:
295
+ tmp_skill_dir = tmp_root / dest_dir.name
296
+ shutil.copytree(source_dir, tmp_skill_dir)
297
+
298
+ tmp_skill_md = tmp_skill_dir / "SKILL.md"
299
+ if not tmp_skill_md.is_file() or _sha256_file(tmp_skill_md) != source_hash:
300
+ logger.warning("skill migration: copy verification failed for %s - skipped", source_dir)
301
+ return False
302
+
303
+ if dest_dir.exists():
304
+ # Lost a race to a concurrent writer (e.g. a fresh agent-authored
305
+ # skill at the same id) between the earlier check and now - never
306
+ # clobber whatever just landed there.
307
+ return False
308
+
309
+ try:
310
+ os.replace(str(tmp_skill_dir), str(dest_dir))
311
+ except OSError as e:
312
+ if getattr(e, "errno", None) != errno.EXDEV:
313
+ raise
314
+ # Cross-device: fall back to a plain copy INTO the destination.
315
+ if dest_dir.exists():
316
+ return False
317
+ shutil.copytree(tmp_skill_dir, dest_dir)
318
+ cross_volume = True
319
+ # Re-verify the PLACED destination (not the tmp): a cross-volume
320
+ # copy is exactly the case where a partial/corrupt write is most
321
+ # likely, and because we are about to DELIBERATELY leave the source
322
+ # (no cross-volume remove), the destination must be provably
323
+ # correct. On mismatch, roll back the bad destination and bail so
324
+ # nothing is counted and the source stays untouched.
325
+ dest_skill_md = dest_dir / "SKILL.md"
326
+ try:
327
+ dest_ok = dest_skill_md.is_file() and _sha256_file(dest_skill_md) == source_hash
328
+ except Exception:
329
+ dest_ok = False
330
+ if not dest_ok:
331
+ logger.warning(
332
+ "skill migration: cross-volume copy verification failed for %s "
333
+ "- rolling back destination, leaving source", source_dir,
334
+ )
335
+ shutil.rmtree(dest_dir, ignore_errors=True)
336
+ return False
337
+ finally:
338
+ shutil.rmtree(tmp_root, ignore_errors=True)
339
+
340
+ if cross_volume:
341
+ # Brief rule: cross-volume placement is copy+verify with NO source
342
+ # removal (a cross-device move isn't atomic, and the source tree may
343
+ # be read-only anyway). Leave the legacy source in place, exactly like
344
+ # the EACCES branch, and drop a breadcrumb at the (writable) dest.
345
+ logger.info(
346
+ "skill migration: cross-volume copy of %s to %s verified; leaving "
347
+ "the legacy source in place (no cross-device remove)",
348
+ source_dir, dest_dir,
349
+ )
350
+ try:
351
+ (dest_dir / _SOURCE_RETAINED_BREADCRUMB).write_text(
352
+ f"cross-volume copy; source not removed: {source_dir}\n", encoding="utf-8"
353
+ )
354
+ except Exception:
355
+ pass # breadcrumb is best-effort only - never let it fail the migration
356
+ return True
357
+
358
+ # Same-filesystem rename path - now best-effort remove the legacy source.
359
+ # Never crash or loop on a permission failure here; the data-home copy is
360
+ # what matters going forward regardless of whether cleanup succeeds.
361
+ try:
362
+ shutil.rmtree(source_dir)
363
+ except (PermissionError, OSError) as e:
364
+ logger.info(
365
+ "skill migration: copied %s to %s but could not remove the legacy "
366
+ "source (%s) - leaving it in place; the data-home copy is authoritative",
367
+ source_dir, dest_dir, e,
368
+ )
369
+ try:
370
+ (dest_dir / _SOURCE_RETAINED_BREADCRUMB).write_text(
371
+ f"source not removed: {source_dir} ({e})\n", encoding="utf-8"
372
+ )
373
+ except Exception:
374
+ pass # breadcrumb is best-effort only - never let it fail the migration
375
+ return True
376
+
377
+
378
+ def _merge_legacy_user_rules(legacy_user_dir: Path, dest_user_dir: Path) -> None:
379
+ """Best-effort merge of a legacy per-user ``rules.json`` into the
380
+ data-home copy, filling in only keys ABSENT at the destination (an
381
+ existing destination entry always wins - never clobbered).
382
+
383
+ Without this, a migrated skill directory has no matching rule and
384
+ ``SkillManager.get_skills_for_session`` never surfaces it: the body moves
385
+ but stays permanently inert, which would defeat the point of migrating it
386
+ at all. The legacy file is left in place untouched; this only enriches
387
+ the destination copy. Fail-open: any parse/write error is logged and
388
+ swallowed, never propagated.
389
+ """
390
+ legacy_rules_file = legacy_user_dir / "rules.json"
391
+ if legacy_rules_file.is_symlink() or not legacy_rules_file.is_file():
392
+ return
393
+ try:
394
+ legacy_rules = json.loads(legacy_rules_file.read_text(encoding="utf-8"))
395
+ except Exception:
396
+ logger.debug("skill migration: could not parse legacy rules.json at %s", legacy_rules_file)
397
+ return
398
+ if not isinstance(legacy_rules, dict) or not legacy_rules:
399
+ return
400
+
401
+ dest_rules_file = dest_user_dir / "rules.json"
402
+ dest_rules: dict = {}
403
+ if dest_rules_file.is_file():
404
+ try:
405
+ loaded = json.loads(dest_rules_file.read_text(encoding="utf-8"))
406
+ except Exception:
407
+ logger.debug(
408
+ "skill migration: existing dest rules.json unparseable at %s - leaving untouched",
409
+ dest_rules_file,
410
+ )
411
+ return # don't risk clobbering something we can't parse
412
+ if isinstance(loaded, dict):
413
+ dest_rules = loaded
414
+
415
+ changed = False
416
+ for skill_id, rule in legacy_rules.items():
417
+ if skill_id not in dest_rules:
418
+ dest_rules[skill_id] = rule
419
+ changed = True
420
+ if not changed:
421
+ return
422
+
423
+ dest_user_dir.mkdir(parents=True, exist_ok=True)
424
+ fd, tmp_name = tempfile.mkstemp(dir=str(dest_user_dir), suffix=".tmp")
425
+ try:
426
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
427
+ json.dump(dest_rules, f, indent=2)
428
+ os.replace(tmp_name, str(dest_rules_file))
429
+ finally:
430
+ if os.path.exists(tmp_name):
431
+ try:
432
+ os.remove(tmp_name)
433
+ except OSError:
434
+ pass
435
+
436
+
437
+ # Dotted sibling dirs under ``user_<uid>/`` that hold real skill CONTENT and
438
+ # must migrate too (Task 9 review fix #2) — as opposed to bookkeeping dotted
439
+ # entries (``.migrate.lock``/``.migrated_v1``/``rules.json``), which are NOT
440
+ # skill dirs and are never walked as such. ``.pending`` = quarantined drafts
441
+ # awaiting review; ``.archived`` = recoverable prior/deleted bodies. Leaving
442
+ # these on the code tree means ``polyrob update`` destroys a user's pending
443
+ # drafts and their entire undo history.
444
+ _QUARANTINE_SUBDIRS = (".pending", ".archived")
445
+
446
+
447
+ def _migrate_skill_dirs_in(src_container: Path, dest_container: Path,
448
+ *, label: str) -> int:
449
+ """Migrate every direct child skill directory of ``src_container`` into
450
+ ``dest_container`` using the shared per-skill pipeline + guards. Returns
451
+ the count placed. Never raises — a listing/per-item failure is logged and
452
+ skipped so one bad entry can't abort the container.
453
+
454
+ ``src_container`` is either a ``user_<uid>/`` dir (top-level skills) or one
455
+ of its ``.pending``/``.archived`` quarantine subdirs; the identical
456
+ symlink / ``..`` / skip-existing / EACCES guards apply in every case.
457
+ """
458
+ moved = 0
459
+ if src_container.is_symlink() or not src_container.is_dir():
460
+ return 0
461
+ try:
462
+ skill_dir_names = sorted(
463
+ d.name
464
+ for d in src_container.iterdir()
465
+ if d.is_dir()
466
+ and not d.is_symlink()
467
+ and not d.name.startswith(".")
468
+ and _is_safe_component(d.name)
469
+ and (d / "SKILL.md").is_file()
470
+ and not (d / "SKILL.md").is_symlink()
471
+ )
472
+ except Exception:
473
+ logger.debug("skill migration: failed to list %s", src_container, exc_info=True)
474
+ return 0
475
+
476
+ for skill_name in skill_dir_names:
477
+ try:
478
+ if _migrate_one_skill(src_container / skill_name, dest_container / skill_name):
479
+ moved += 1
480
+ except Exception:
481
+ logger.warning(
482
+ "skill migration: failed migrating %s/%s (skipped, no crash)",
483
+ label, skill_name, exc_info=True,
484
+ )
485
+ continue
486
+ return moved
487
+
488
+
489
+ def _migrate_pass(legacy_root: Path, home: Path) -> int:
490
+ """Walk ``legacy_root`` for ``user_*`` dirs and migrate their skills into
491
+ ``home`` (``skills_data_home()``). Returns the count of skill directories
492
+ actually placed at a new destination. Never raises - any per-item failure
493
+ is logged and skipped so one bad entry can't abort the whole pass.
494
+
495
+ Per ``user_<uid>/`` this migrates the active (top-level) skills AND the
496
+ ``.pending``/``.archived`` quarantine content (all user data), plus a
497
+ best-effort ``rules.json`` merge.
498
+ """
499
+ moved = 0
500
+ try:
501
+ if legacy_root.is_symlink() or not legacy_root.is_dir():
502
+ return 0
503
+ user_dir_names = sorted(
504
+ d.name
505
+ for d in legacy_root.iterdir()
506
+ if d.is_dir()
507
+ and not d.is_symlink()
508
+ and d.name.startswith("user_")
509
+ and _is_safe_component(d.name)
510
+ )
511
+ except Exception:
512
+ logger.debug("skill migration: failed to list legacy_root %s", legacy_root, exc_info=True)
513
+ return 0
514
+
515
+ for name in user_dir_names:
516
+ user_dir = legacy_root / name
517
+ dest_user_dir = home / name
518
+
519
+ # Active (top-level) skills.
520
+ moved += _migrate_skill_dirs_in(user_dir, dest_user_dir, label=name)
521
+
522
+ # Quarantine content (.pending drafts + .archived history) — same
523
+ # pipeline + guards, preserving the subdir layout in data-home.
524
+ for sub in _QUARANTINE_SUBDIRS:
525
+ moved += _migrate_skill_dirs_in(
526
+ user_dir / sub, dest_user_dir / sub, label=f"{name}/{sub}"
527
+ )
528
+
529
+ try:
530
+ _merge_legacy_user_rules(user_dir, dest_user_dir)
531
+ except Exception:
532
+ logger.debug("skill migration: rules.json merge skipped for %s", user_dir, exc_info=True)
533
+
534
+ return moved
535
+
536
+
537
+ def migrate_legacy_user_skills(legacy_root: Path, *, lock_timeout: float = 5.0) -> int:
538
+ """One-time migration of pre-Task-8 code-tree ``user_<uid>/`` skills into
539
+ data-home (Task 9). Returns the count of skill directories moved.
540
+
541
+ Safety properties (see module section header for the "why"):
542
+
543
+ - **Idempotent**: a ``<data_home>/skills/.migrated_v1`` marker is
544
+ written after a completed pass; any later call returns 0 immediately
545
+ without re-scanning ``legacy_root``.
546
+ - **Single-flight / concurrency-safe**: the whole pass runs under a
547
+ :class:`~agents.task.utils.SafeFileLock` at
548
+ ``<data_home>/skills/.migrate.lock`` so parallel workers/processes
549
+ booting at once don't race each other. On contention/timeout this
550
+ returns 0 rather than blocking indefinitely or racing.
551
+ - **Resumable**: the marker is only written at the END of a pass, so an
552
+ interrupted first attempt (process killed mid-migration) simply
553
+ re-scans on the next call - already-placed destinations are detected
554
+ via the per-skill "skip if dest exists" check and not re-copied.
555
+ - **Traversal/symlink-safe**: never follows a symlinked ``user_*`` dir,
556
+ skill dir, or ``SKILL.md``; rejects unsafe path components.
557
+ - **Never clobbers**: a destination that already exists (e.g. a fresher
558
+ data-home copy, or one created by a concurrent writer) is left alone
559
+ and not counted as moved.
560
+ - **EACCES-tolerant**: a read-only / root-owned legacy tree (typical
561
+ site-packages install) means the source can't be removed after
562
+ copying - that failure is swallowed, the source is left in place with
563
+ a breadcrumb, and migration continues (never loops, never crashes).
564
+ - **Fail-open**: any unexpected exception anywhere in this call is
565
+ logged and swallowed; the count accumulated so far is returned. This
566
+ function must NEVER raise - it's called from ``SkillManager.__init__``
567
+ and must never block construction.
568
+ - **Zero-touch when nothing to migrate**: if ``legacy_root`` (the
569
+ package tree) has NO ``user_*`` dirs, returns 0 *before* resolving or
570
+ creating data-home / the lock / the marker — so constructing a bare
571
+ ``SkillManager()`` on a machine that never had legacy user skills
572
+ never writes anything under the real ``~/.polyrob`` (or wherever
573
+ data-home resolves). The pre-check is on the RAW ``legacy_root``, so
574
+ it's environment-independent.
575
+ """
576
+ moved = 0
577
+ try:
578
+ # Cheap, env-independent pre-check on the package tree BEFORE touching
579
+ # data-home (Task 9 review fix #3). No ``user_*`` dirs => nothing to
580
+ # migrate => return immediately, creating no lock/marker/skills dir.
581
+ # (Idempotency for the "had skills, already migrated" case is still the
582
+ # marker's job below; this only short-circuits the empty case.)
583
+ try:
584
+ has_user_dirs = any(
585
+ p.is_dir() and not p.is_symlink()
586
+ for p in legacy_root.glob("user_*")
587
+ )
588
+ except OSError:
589
+ has_user_dirs = False
590
+ if not has_user_dirs:
591
+ return 0
592
+
593
+ home = skills_data_home()
594
+ home.mkdir(parents=True, exist_ok=True)
595
+ marker = home / _MIGRATION_MARKER_NAME
596
+ if marker.exists():
597
+ return 0
598
+
599
+ from agents.task.utils import SafeFileLock
600
+
601
+ lock_path = home / _MIGRATION_LOCK_NAME
602
+ try:
603
+ with SafeFileLock(str(lock_path), timeout=lock_timeout):
604
+ if marker.exists():
605
+ # Another process finished while we waited for the lock.
606
+ return 0
607
+ # A missing/symlinked legacy_root means we never actually got
608
+ # to inspect anything - don't mark the migration "done" in
609
+ # that case, so a legacy tree that appears later (unusual,
610
+ # but cheap to stay open to) can still be picked up.
611
+ root_was_inspectable = legacy_root.is_dir() and not legacy_root.is_symlink()
612
+ moved = _migrate_pass(legacy_root, home)
613
+ if not root_was_inspectable:
614
+ return moved
615
+ try:
616
+ marker.write_text(f"migrated {moved} skill(s)\n", encoding="utf-8")
617
+ except Exception:
618
+ logger.debug(
619
+ "skill migration: could not write completion marker "
620
+ "(will re-scan on next boot)", exc_info=True,
621
+ )
622
+ except TimeoutError:
623
+ logger.info("skill migration: lock at %s contended - skipping this attempt", lock_path)
624
+ return 0
625
+ except Exception:
626
+ logger.debug("skill migration: unexpected error - skipping (fail-open)", exc_info=True)
627
+ return moved
@@ -0,0 +1,56 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+
4
+ ALLOWED_TOP = {"name", "description", "license", "compatibility", "metadata", "allowed-tools"}
5
+ _NAME_STRICT = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") # ascii kebab, no --, no lead/trail -
6
+
7
+ @dataclass
8
+ class Issue:
9
+ level: str # "error" | "warn"
10
+ code: str
11
+ msg: str
12
+
13
+ def validate_authored(meta: dict, dirname: str) -> list[Issue]:
14
+ out: list[Issue] = []
15
+ name = meta.get("name")
16
+ if not name:
17
+ out.append(Issue("error", "missing_name", "name is required"))
18
+ elif not isinstance(name, str):
19
+ out.append(Issue("error", "name_not_string", "name must be a string"))
20
+ else:
21
+ if len(name) > 64:
22
+ out.append(Issue("error", "name_too_long", "name >64 chars"))
23
+ if not _NAME_STRICT.match(name):
24
+ out.append(Issue("error", "name_charset", "name must be ^[a-z0-9]+(-[a-z0-9]+)*$"))
25
+ if name != dirname:
26
+ out.append(Issue("error", "name_dir_mismatch", f"name {name!r} != dir {dirname!r}"))
27
+ desc = meta.get("description")
28
+ if not desc:
29
+ out.append(Issue("error", "missing_description", "description is required"))
30
+ elif not isinstance(desc, str):
31
+ out.append(Issue("error", "description_not_string", "description must be a string"))
32
+ elif len(desc) > 1024:
33
+ out.append(Issue("error", "description_too_long", "description >1024 chars"))
34
+ extra = set(meta.keys()) - ALLOWED_TOP
35
+ if extra:
36
+ out.append(Issue("error", "extra_top_level_field", f"unexpected top-level: {sorted(extra)}"))
37
+ comp = meta.get("compatibility")
38
+ if comp and len(str(comp)) > 500:
39
+ out.append(Issue("error", "compatibility_too_long", "compatibility >500 chars"))
40
+ md = meta.get("metadata")
41
+ if md is not None and (not isinstance(md, dict) or any(not isinstance(k, str) or not isinstance(v, str) for k, v in md.items())):
42
+ out.append(Issue("error", "metadata_not_string_map", "metadata must be string->string"))
43
+ return out
44
+
45
+ def validate_consumed(meta: dict, dirname: str) -> list[Issue]:
46
+ out: list[Issue] = []
47
+ name = meta.get("name")
48
+ if not meta.get("description"):
49
+ out.append(Issue("error", "missing_description", "description required to load")) # skip
50
+ if name and not isinstance(name, str):
51
+ out.append(Issue("warn", "name_not_string", "name is not a string (loaded anyway)"))
52
+ elif name and len(name) > 64:
53
+ out.append(Issue("warn", "name_too_long", "name >64 (loaded anyway)"))
54
+ if name and isinstance(name, str) and name != dirname:
55
+ out.append(Issue("warn", "name_dir_mismatch", "name!=dir (loaded anyway)"))
56
+ return out