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,492 @@
1
+ """Writable skills (W2-A, Reference-parity self-authored skills).
2
+
3
+ POLYROB's :class:`SkillManager` was read-only — the agent could *match* and *load* skills
4
+ but never create, refine, or retire them, so it could not learn durable procedures
5
+ from experience. This mixin adds the write path, composed into ``SkillManager`` so
6
+ all the existing validators/loaders are reused.
7
+
8
+ Safety is designed in, not bolted on — a self-authored skill is a prompt-injection
9
+ **persistence** vector (a skill loaded next session is read as instructions):
10
+
11
+ 1. **Tenant-confined** — writes go ONLY under the user DATA-HOME scope's
12
+ ``user_{user_id}/`` (Task 8: ``skill_store.skills_data_home()``, NOT the
13
+ installed package tree — this is what lets a self-authored skill survive a
14
+ ``polyrob update`` code-swap); the ``^[a-z][a-z0-9-]*$`` id regex
15
+ (``validate_skill_id``) blocks path traversal.
16
+ 2. **Anon-blocked** — an empty/blank ``user_id`` is refused (mirror
17
+ ``MEMORY_REQUIRE_USER_ID``) so authored skills never land in a shared bucket.
18
+ 3. **Validated** — ``validate_skill_id`` → ``validate_skill_content``
19
+ (``MAX_SKILL_CONTENT_CHARS``) before any write.
20
+ 4. **Threat-scanned** — ``threat_scan.is_suspicious`` rejects obviously-injected
21
+ bodies BEFORE persist (unconditional on the write path here — a read-only
22
+ trusted scope, i.e. builtin, is exempt via ``skill_store.scan_exempt``, but
23
+ `create_skill` only ever targets the writable user scope, so this is
24
+ effectively still unconditional; not gated on the memory flag).
25
+ 5. **Quarantined** — when ``SKILLS_WRITABLE_REQUIRE_REVIEW`` (default true) OR the
26
+ author was a non-user-initiated turn (self-wake / background review), the skill
27
+ lands in ``user_{uid}/.pending/`` **inert** (NOT in rules.json), so a forged turn
28
+ can never auto-activate a skill.
29
+ 6. **Atomic** — temp-file + ``os.replace`` so a crash never leaves a half-written
30
+ ``SKILL.md``.
31
+ 7. **Archive-never-delete** — deletes move to ``user_{uid}/.archived/`` (recoverable).
32
+
33
+ Gated by ``SKILLS_WRITABLE`` at the action layer; the methods here are a library and
34
+ do nothing until called.
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import json
39
+ import logging
40
+ import os
41
+ import tempfile
42
+ from pathlib import Path
43
+ from typing import List, Optional
44
+
45
+ from agents.task.agent import skill_store
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+ PROVENANCE_USER = "user"
50
+ PROVENANCE_AGENT = "agent"
51
+ PROVENANCE_BACKGROUND = "background_review"
52
+ # Authors whose writes must NEVER auto-activate, regardless of REQUIRE_REVIEW: a
53
+ # forged (self-wake / background-review) turn. A normal interactive `agent` turn is
54
+ # user-initiated, so it follows the REQUIRE_REVIEW flag like any other.
55
+ _NON_USER_AUTHORS = frozenset({PROVENANCE_BACKGROUND})
56
+
57
+
58
+ class SkillWriteResult:
59
+ """Outcome of a write op (mirrors SkillValidationResult's shape, plus location)."""
60
+
61
+ def __init__(self, skill_id: str, ok: bool, *, errors=None, warnings=None,
62
+ pending: bool = False, path: Optional[str] = None):
63
+ self.skill_id = skill_id
64
+ self.is_valid = ok
65
+ self.ok = ok
66
+ self.errors = errors or []
67
+ self.warnings = warnings or []
68
+ self.pending = pending
69
+ self.path = path
70
+
71
+ def __repr__(self):
72
+ state = "pending" if self.pending else ("ok" if self.ok else "rejected")
73
+ return f"<SkillWriteResult {self.skill_id} {state} errors={self.errors}>"
74
+
75
+
76
+ class SkillWriterMixin:
77
+ """Create / patch / delete user skills. Composed into SkillManager."""
78
+
79
+ def _user_root(self, user_id: str) -> Path:
80
+ # Task 8: routes through SkillManager._user_dirs_root() (data-home by
81
+ # default; the pre-existing single-root test-override contract if
82
+ # `skills_dir` was redirected) rather than always `self.skills_dir` —
83
+ # so a create/patch/delete survives a `polyrob update` code-swap.
84
+ return self._user_dirs_root() / f"user_{user_id}"
85
+
86
+ @staticmethod
87
+ def _require_user(user_id: Optional[str]) -> Optional[str]:
88
+ """Return a clean user_id or None (anon-block). Never write under user_/."""
89
+ if user_id is None:
90
+ return None
91
+ uid = str(user_id).strip()
92
+ return uid or None
93
+
94
+ def _resolve_pending(self, created_by: str, pending: Optional[bool],
95
+ *, overwriting_active: bool = False) -> bool:
96
+ from agents.task.constants import AutonomyConfig
97
+ # Protect curated ACTIVE skills: a non-owner author (agent or background) may
98
+ # never silently replace one — force a .pending proposal (owner promotes).
99
+ if (overwriting_active and created_by != PROVENANCE_USER
100
+ and AutonomyConfig.skill_overwrite_protect()):
101
+ return True
102
+ if pending is not None:
103
+ base = pending
104
+ else:
105
+ base = AutonomyConfig.skills_writable_require_review()
106
+ # Hard rule: a non-user author can NEVER auto-activate — force quarantine even
107
+ # if review is globally disabled.
108
+ if created_by in _NON_USER_AUTHORS:
109
+ return True
110
+ return base
111
+
112
+ def create_skill(self, skill_id: str, content: str, *, user_id: str,
113
+ description: str = "", created_by: str = PROVENANCE_AGENT,
114
+ pending: Optional[bool] = None) -> SkillWriteResult:
115
+ """Author a new user skill (validated, threat-scanned, atomically written)."""
116
+ uid = self._require_user(user_id)
117
+ if uid is None:
118
+ return SkillWriteResult(skill_id, False,
119
+ errors=["empty user_id refused (tenant scope)"])
120
+
121
+ id_ok, id_errors = self.validate_skill_id(skill_id)
122
+ if not id_ok:
123
+ return SkillWriteResult(skill_id, False, errors=id_errors)
124
+
125
+ content_res = self.validate_skill_content(skill_id, content)
126
+ if not content_res.is_valid:
127
+ return SkillWriteResult(skill_id, False, errors=content_res.errors,
128
+ warnings=content_res.warnings)
129
+
130
+ # Threat-scan is the injection-persistence tripwire. Fail-OPEN only on an
131
+ # import error (scanner absent); if the scan itself RAISES we fail-CLOSED
132
+ # (reject) — a write must not slip past a crashing guard.
133
+ try:
134
+ from modules.memory.task.threat_scan import is_suspicious
135
+ except Exception:
136
+ is_suspicious = None
137
+ # Task 8: a read-only + trusted scope (today: builtin) is exempt from a
138
+ # write-time re-scan — future-proofs e.g. a reindex of the shipped
139
+ # library. `create_skill` ALWAYS targets the writable user scope
140
+ # (`skill_store.user_scope()`, writable=True), so `scan_exempt()` is
141
+ # unconditionally False here — this must NEVER weaken scanning on the
142
+ # live (user) write path, regardless of user_scope() also being
143
+ # `trusted` (trusted describes location provenance, not a license to
144
+ # skip scanning newly-written content).
145
+ _write_scope = skill_store.user_scope()
146
+ if is_suspicious is not None and not skill_store.scan_exempt(_write_scope):
147
+ try:
148
+ flagged = is_suspicious(content)
149
+ except Exception as e:
150
+ logger.warning("skill write rejected (threat-scan error, fail-closed): %s/%s: %s",
151
+ uid, skill_id, e)
152
+ return SkillWriteResult(skill_id, False, errors=["threat-scan error (rejected)"])
153
+ if flagged:
154
+ logger.warning("skill write rejected (suspicious content): %s/%s", uid, skill_id)
155
+ return SkillWriteResult(skill_id, False,
156
+ errors=["content failed injection threat-scan"])
157
+ # P3-1: the DESCRIPTION is injected verbatim into the <skill-catalog>
158
+ # prompt for every future session — so it is an injection vector exactly
159
+ # like the body and must be scanned too (latent Hermes #8884). Fail-CLOSED.
160
+ if description:
161
+ try:
162
+ desc_flagged = is_suspicious(description)
163
+ except Exception as e:
164
+ logger.warning("skill write rejected (description scan error, fail-closed): %s/%s: %s",
165
+ uid, skill_id, e)
166
+ return SkillWriteResult(skill_id, False,
167
+ errors=["description scan error (rejected)"])
168
+ if desc_flagged:
169
+ logger.warning("skill write rejected (suspicious description): %s/%s", uid, skill_id)
170
+ return SkillWriteResult(skill_id, False,
171
+ errors=["description failed injection threat-scan"])
172
+
173
+ active_file = self._user_root(uid) / skill_id / "SKILL.md"
174
+ overwriting_active = active_file.exists() or (skill_id in getattr(self, "skill_rules", {}))
175
+ quarantine = self._resolve_pending(created_by, pending,
176
+ overwriting_active=overwriting_active)
177
+ base = self._user_root(uid) / (".pending" if quarantine else "")
178
+ skill_dir = base / skill_id
179
+ skill_file = skill_dir / "SKILL.md"
180
+ try:
181
+ if skill_file.exists():
182
+ self._archive_prior_body(uid, skill_id, skill_file) # non-destructive overwrite
183
+ self._atomic_write(skill_file, content)
184
+ if not quarantine:
185
+ self._upsert_rule(uid, skill_id, description=description,
186
+ created_by=created_by, content=content)
187
+ self._invalidate_skill_cache(uid, skill_id)
188
+ self._record_provenance(skill_id, uid, created_by)
189
+ logger.info("authored skill %s/%s (%s)", uid, skill_id,
190
+ "pending" if quarantine else "active")
191
+ return SkillWriteResult(skill_id, True, pending=quarantine,
192
+ warnings=content_res.warnings, path=str(skill_file))
193
+ except Exception as e:
194
+ logger.error("skill write failed for %s/%s: %s", uid, skill_id, e, exc_info=True)
195
+ return SkillWriteResult(skill_id, False, errors=[f"write failed: {e}"])
196
+
197
+ def patch_skill(self, skill_id: str, *, user_id: str, old_string: str,
198
+ new_string: str, replace_all: bool = False,
199
+ created_by: str = PROVENANCE_AGENT,
200
+ pending: Optional[bool] = None) -> SkillWriteResult:
201
+ """Exact-match edit of an existing user skill body, re-validated + re-scanned."""
202
+ uid = self._require_user(user_id)
203
+ if uid is None:
204
+ return SkillWriteResult(skill_id, False, errors=["empty user_id refused"])
205
+ id_ok, id_errors = self.validate_skill_id(skill_id)
206
+ if not id_ok:
207
+ return SkillWriteResult(skill_id, False, errors=id_errors)
208
+
209
+ skill_file = self._find_skill_file(uid, skill_id)
210
+ if skill_file is None:
211
+ return SkillWriteResult(skill_id, False, errors=[f"skill '{skill_id}' not found for user"])
212
+ # A forged (non-user) turn may refine its OWN pending draft but must never
213
+ # mutate an already-ACTIVE skill (that's user-controlled instruction content).
214
+ if created_by in _NON_USER_AUTHORS and not self._is_pending_path(skill_file):
215
+ return SkillWriteResult(skill_id, False,
216
+ errors=["a background turn cannot patch an active skill"])
217
+ try:
218
+ current = skill_file.read_text(encoding="utf-8")
219
+ except Exception as e:
220
+ return SkillWriteResult(skill_id, False, errors=[f"read failed: {e}"])
221
+
222
+ count = current.count(old_string)
223
+ if count == 0:
224
+ return SkillWriteResult(skill_id, False, errors=["old_string not found"])
225
+ if count > 1 and not replace_all:
226
+ return SkillWriteResult(skill_id, False,
227
+ errors=[f"old_string occurs {count}× — pass replace_all=true"])
228
+ updated = (current.replace(old_string, new_string)
229
+ if replace_all else current.replace(old_string, new_string, 1))
230
+
231
+ # Re-run the full validate+scan gate on the NEW body.
232
+ return self.create_skill(skill_id, updated, user_id=uid, created_by=created_by,
233
+ pending=pending if pending is not None else self._is_pending_path(skill_file))
234
+
235
+ def delete_skill(self, skill_id: str, *, user_id: str,
236
+ absorbed_into: Optional[str] = None,
237
+ created_by: str = PROVENANCE_USER) -> bool:
238
+ """Archive a user skill (recoverable). Never hard-deletes; drops its rule."""
239
+ uid = self._require_user(user_id)
240
+ if uid is None:
241
+ return False
242
+ id_ok, _ = self.validate_skill_id(skill_id)
243
+ if not id_ok:
244
+ return False
245
+ skill_file = self._find_skill_file(uid, skill_id)
246
+ if skill_file is None:
247
+ return False
248
+ # A forged (non-user) turn must never retire an ACTIVE skill.
249
+ if created_by in _NON_USER_AUTHORS and not self._is_pending_path(skill_file):
250
+ logger.info("background turn blocked from deleting active skill %s/%s", uid, skill_id)
251
+ return False
252
+ try:
253
+ archive_dir = self._user_root(uid) / ".archived" / skill_id
254
+ archive_dir.mkdir(parents=True, exist_ok=True)
255
+ dest = archive_dir / "SKILL.md"
256
+ os.replace(str(skill_file), str(dest))
257
+ if absorbed_into:
258
+ (archive_dir / "ABSORBED_INTO").write_text(absorbed_into, encoding="utf-8")
259
+ self._remove_rule(uid, skill_id)
260
+ self.reload_rules()
261
+ logger.info("archived skill %s/%s%s", uid, skill_id,
262
+ f" (absorbed into {absorbed_into})" if absorbed_into else "")
263
+ return True
264
+ except Exception as e:
265
+ logger.error("skill archive failed for %s/%s: %s", uid, skill_id, e, exc_info=True)
266
+ return False
267
+
268
+ def promote_pending_skill(self, skill_id: str, *, user_id: str,
269
+ description: str = "") -> SkillWriteResult:
270
+ """Move a `.pending/` skill into active use (the human/curator review gate)."""
271
+ uid = self._require_user(user_id)
272
+ if uid is None:
273
+ return SkillWriteResult(skill_id, False, errors=["empty user_id refused"])
274
+ id_ok, id_errors = self.validate_skill_id(skill_id)
275
+ if not id_ok:
276
+ return SkillWriteResult(skill_id, False, errors=id_errors)
277
+ pending_file = self._user_root(uid) / ".pending" / skill_id / "SKILL.md"
278
+ if not pending_file.exists():
279
+ return SkillWriteResult(skill_id, False, errors=["no pending skill with that id"])
280
+ content = pending_file.read_text(encoding="utf-8")
281
+ # P2-20: read the ORIGINAL author before we re-create. Promote must ACTIVATE the
282
+ # skill (that's the review gate), so we still create it as PROVENANCE_USER —
283
+ # passing the original author (e.g. background_review) would make create_skill's
284
+ # quarantine logic re-quarantine it and promote would never activate. Instead we
285
+ # re-record the original authorship in provenance AFTER activation, so a promoted
286
+ # skill stays ACTIVE yet remains in the curator's authored scope AND the
287
+ # authored-reuse metric (where a successfully promoted agent skill should count).
288
+ _orig_author = None
289
+ try:
290
+ from modules.skills.skill_usage import get_skill_usage_store
291
+ _prior = get_skill_usage_store().get_provenance(skill_id, uid)
292
+ if _prior and _prior.get("created_by"):
293
+ _orig_author = _prior["created_by"]
294
+ except Exception:
295
+ _orig_author = None
296
+ res = self.create_skill(skill_id, content, user_id=uid, description=description,
297
+ created_by=PROVENANCE_USER, pending=False)
298
+ if res.ok and not res.pending and _orig_author and _orig_author != PROVENANCE_USER:
299
+ try:
300
+ from modules.skills.skill_usage import get_skill_usage_store
301
+ get_skill_usage_store().record_provenance(skill_id, uid, _orig_author)
302
+ except Exception:
303
+ pass
304
+ if res.ok and not res.pending:
305
+ try:
306
+ os.remove(str(pending_file))
307
+ except OSError:
308
+ pass
309
+ return res
310
+
311
+ def list_pending_skills(self, user_id: str) -> List[dict]:
312
+ """Enumerate a tenant's `.pending/` skill drafts (owner transparency surface)."""
313
+ uid = self._require_user(user_id)
314
+ if uid is None:
315
+ return []
316
+ pending_root = self._user_root(uid) / ".pending"
317
+ if not pending_root.is_dir():
318
+ return []
319
+ out: List[dict] = []
320
+ for skill_dir in sorted(pending_root.iterdir()):
321
+ skill_file = skill_dir / "SKILL.md"
322
+ if not skill_file.is_file():
323
+ continue
324
+ try:
325
+ body = skill_file.read_text(encoding="utf-8")
326
+ except Exception:
327
+ continue
328
+ preview = body.strip().replace("\n", " ")
329
+ if len(preview) > 280:
330
+ preview = preview[:277] + "…"
331
+ out.append({
332
+ "kind": "skill",
333
+ "skill_id": skill_dir.name,
334
+ "user_id": uid,
335
+ "preview": preview,
336
+ "chars": len(body),
337
+ "path": str(skill_file),
338
+ })
339
+ return out
340
+
341
+ def reject_pending_skill(self, skill_id: str, *, user_id: str) -> bool:
342
+ """Discard a `.pending/` skill draft (owner rejects). Archive-never-delete;
343
+ never touches an active skill (a non-pending id is refused)."""
344
+ uid = self._require_user(user_id)
345
+ if uid is None:
346
+ return False
347
+ id_ok, _ = self.validate_skill_id(skill_id)
348
+ if not id_ok:
349
+ return False
350
+ pending_file = self._user_root(uid) / ".pending" / skill_id / "SKILL.md"
351
+ if not pending_file.exists():
352
+ return False
353
+ try:
354
+ archive_dir = self._user_root(uid) / ".archived" / skill_id
355
+ archive_dir.mkdir(parents=True, exist_ok=True)
356
+ os.replace(str(pending_file), str(archive_dir / "SKILL.md"))
357
+ logger.info("rejected pending skill %s/%s (archived)", uid, skill_id)
358
+ return True
359
+ except Exception as e:
360
+ logger.error("skill reject failed for %s/%s: %s", uid, skill_id, e, exc_info=True)
361
+ return False
362
+
363
+ # --- internals -----------------------------------------------------------
364
+
365
+ def _archive_prior_body(self, uid: str, skill_id: str, current_file: Path) -> None:
366
+ """Copy the body we're about to overwrite into .archived/ (recoverable). Fail-open."""
367
+ try:
368
+ archive_dir = self._user_root(uid) / ".archived" / skill_id
369
+ archive_dir.mkdir(parents=True, exist_ok=True)
370
+ n = 1 + sum(1 for _ in archive_dir.glob("*-SKILL.md"))
371
+ dest = archive_dir / f"{n}-SKILL.md"
372
+ content = current_file.read_text(encoding="utf-8")
373
+ if dest.exists():
374
+ # Collision-safe fallback: use a unique temp name in the same dir.
375
+ fd, _tmp = tempfile.mkstemp(prefix=f"{n}-", suffix="-SKILL.md", dir=str(archive_dir))
376
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
377
+ f.write(content)
378
+ else:
379
+ dest.write_text(content, encoding="utf-8")
380
+ except Exception as e:
381
+ logger.debug("archive-on-overwrite skipped for %s/%s: %s", uid, skill_id, e)
382
+
383
+ def _atomic_write(self, path: Path, content: str) -> None:
384
+ path.parent.mkdir(parents=True, exist_ok=True)
385
+ fd, tmp = tempfile.mkstemp(dir=str(path.parent), suffix=".tmp")
386
+ try:
387
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
388
+ f.write(content)
389
+ os.replace(tmp, str(path))
390
+ finally:
391
+ if os.path.exists(tmp):
392
+ try:
393
+ os.remove(tmp)
394
+ except OSError:
395
+ pass
396
+
397
+ def _find_skill_file(self, uid: str, skill_id: str) -> Optional[Path]:
398
+ # Defense-in-depth: never join an unvalidated id into a path (callers also
399
+ # validate, but a bad id must never traverse out of user_{uid}/).
400
+ ok, _ = self.validate_skill_id(skill_id)
401
+ if not ok:
402
+ return None
403
+ for sub in ("", ".pending"):
404
+ f = self._user_root(uid) / sub / skill_id / "SKILL.md" if sub else \
405
+ self._user_root(uid) / skill_id / "SKILL.md"
406
+ if f.exists():
407
+ return f
408
+ return None
409
+
410
+ def _is_pending_path(self, path: Path) -> bool:
411
+ return ".pending" in path.parts
412
+
413
+ def _user_rules_path(self, uid: str) -> Path:
414
+ return self._user_root(uid) / "rules.json"
415
+
416
+ def _load_user_rules_raw(self, uid: str) -> dict:
417
+ p = self._user_rules_path(uid)
418
+ if p.exists():
419
+ try:
420
+ return json.loads(p.read_text(encoding="utf-8"))
421
+ except Exception:
422
+ return {}
423
+ return {}
424
+
425
+ def _upsert_rule(self, uid: str, skill_id: str, *, description: str, created_by: str,
426
+ content: str = "") -> None:
427
+ rules = self._load_user_rules_raw(uid)
428
+ entry = rules.get(skill_id, {}) if isinstance(rules.get(skill_id), dict) else {}
429
+ existing_triggers = entry.get("triggers") if isinstance(entry.get("triggers"), dict) else {}
430
+ # An authored skill MUST be match-eligible or it is a dead write: get_skills_for_session
431
+ # skips rules with auto_activate falsey, and only MATCHED skills enter the on-demand
432
+ # catalog/_session_skills that load_skill resolves from. So set auto_activate=True and
433
+ # derive keyword triggers from the id/description so the skill surfaces when relevant.
434
+ entry.update({
435
+ "description": description or entry.get("description", ""),
436
+ "created_by": created_by,
437
+ "auto_activate": True,
438
+ "priority": entry.get("priority", 6), # below curated system skills (1-5)
439
+ "triggers": existing_triggers or {
440
+ "keywords": self._derive_keywords(skill_id, description, content),
441
+ },
442
+ })
443
+ rules[skill_id] = entry
444
+ self._atomic_write(self._user_rules_path(uid), json.dumps(rules, indent=2))
445
+
446
+ @staticmethod
447
+ def _derive_keywords(skill_id: str, description: str, content: str) -> List[str]:
448
+ """Keyword triggers so an authored skill matches relevant future tasks.
449
+
450
+ Uses the skill-id tokens + significant words from the description/first heading.
451
+ Conservative (no giant keyword lists) to avoid over-triggering.
452
+ """
453
+ import re as _re
454
+ stop = {"the", "and", "for", "with", "this", "that", "your", "you", "when",
455
+ "how", "use", "using", "a", "an", "to", "of", "in", "on", "is", "it",
456
+ "skill", "do", "doing", "make", "making", "create", "creating",
457
+ # P3-4: too-generic tokens that over-trigger under substring matching.
458
+ "data", "file", "files", "report", "info", "list", "page", "tool"}
459
+ words: List[str] = [t for t in skill_id.split("-") if len(t) > 2 and t not in stop]
460
+ src = f"{description}\n{content[:200]}".lower()
461
+ for tok in _re.findall(r"[a-z][a-z0-9]{3,}", src):
462
+ if tok not in stop and tok not in words:
463
+ words.append(tok)
464
+ if len(words) >= 5:
465
+ break
466
+ return words[:5]
467
+
468
+ def _invalidate_skill_cache(self, uid: str, skill_id: str) -> None:
469
+ cache = getattr(self, "skill_cache", None)
470
+ if isinstance(cache, dict):
471
+ cache.pop(f"{uid}:{skill_id}", None)
472
+ cache.pop(skill_id, None)
473
+ # Sibling of skill_cache (Task 5): the parsed-frontmatter cache must be
474
+ # invalidated in lockstep, else a stale meta['description'] could survive
475
+ # an overwrite and keep surfacing in the catalog.
476
+ meta_cache = getattr(self, "skill_meta_cache", None)
477
+ if isinstance(meta_cache, dict):
478
+ meta_cache.pop(f"{uid}:{skill_id}", None)
479
+ meta_cache.pop(skill_id, None)
480
+
481
+ def _remove_rule(self, uid: str, skill_id: str) -> None:
482
+ rules = self._load_user_rules_raw(uid)
483
+ if skill_id in rules:
484
+ rules.pop(skill_id, None)
485
+ self._atomic_write(self._user_rules_path(uid), json.dumps(rules, indent=2))
486
+
487
+ def _record_provenance(self, skill_id: str, uid: str, created_by: str) -> None:
488
+ try:
489
+ from modules.skills.skill_usage import get_skill_usage_store
490
+ get_skill_usage_store().record_provenance(skill_id, uid, created_by)
491
+ except Exception:
492
+ pass # provenance is a metric, never block a write on it