PraisonAI 3.0.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 (393) hide show
  1. praisonai/__init__.py +54 -0
  2. praisonai/__main__.py +15 -0
  3. praisonai/acp/__init__.py +54 -0
  4. praisonai/acp/config.py +159 -0
  5. praisonai/acp/server.py +587 -0
  6. praisonai/acp/session.py +219 -0
  7. praisonai/adapters/__init__.py +50 -0
  8. praisonai/adapters/readers.py +395 -0
  9. praisonai/adapters/rerankers.py +315 -0
  10. praisonai/adapters/retrievers.py +394 -0
  11. praisonai/adapters/vector_stores.py +409 -0
  12. praisonai/agent_scheduler.py +337 -0
  13. praisonai/agents_generator.py +903 -0
  14. praisonai/api/call.py +292 -0
  15. praisonai/auto.py +1197 -0
  16. praisonai/capabilities/__init__.py +275 -0
  17. praisonai/capabilities/a2a.py +140 -0
  18. praisonai/capabilities/assistants.py +283 -0
  19. praisonai/capabilities/audio.py +320 -0
  20. praisonai/capabilities/batches.py +469 -0
  21. praisonai/capabilities/completions.py +336 -0
  22. praisonai/capabilities/container_files.py +155 -0
  23. praisonai/capabilities/containers.py +93 -0
  24. praisonai/capabilities/embeddings.py +158 -0
  25. praisonai/capabilities/files.py +467 -0
  26. praisonai/capabilities/fine_tuning.py +293 -0
  27. praisonai/capabilities/guardrails.py +182 -0
  28. praisonai/capabilities/images.py +330 -0
  29. praisonai/capabilities/mcp.py +190 -0
  30. praisonai/capabilities/messages.py +270 -0
  31. praisonai/capabilities/moderations.py +154 -0
  32. praisonai/capabilities/ocr.py +217 -0
  33. praisonai/capabilities/passthrough.py +204 -0
  34. praisonai/capabilities/rag.py +207 -0
  35. praisonai/capabilities/realtime.py +160 -0
  36. praisonai/capabilities/rerank.py +165 -0
  37. praisonai/capabilities/responses.py +266 -0
  38. praisonai/capabilities/search.py +109 -0
  39. praisonai/capabilities/skills.py +133 -0
  40. praisonai/capabilities/vector_store_files.py +334 -0
  41. praisonai/capabilities/vector_stores.py +304 -0
  42. praisonai/capabilities/videos.py +141 -0
  43. praisonai/chainlit_ui.py +304 -0
  44. praisonai/chat/__init__.py +106 -0
  45. praisonai/chat/app.py +125 -0
  46. praisonai/cli/__init__.py +26 -0
  47. praisonai/cli/app.py +213 -0
  48. praisonai/cli/commands/__init__.py +75 -0
  49. praisonai/cli/commands/acp.py +70 -0
  50. praisonai/cli/commands/completion.py +333 -0
  51. praisonai/cli/commands/config.py +166 -0
  52. praisonai/cli/commands/debug.py +142 -0
  53. praisonai/cli/commands/diag.py +55 -0
  54. praisonai/cli/commands/doctor.py +166 -0
  55. praisonai/cli/commands/environment.py +179 -0
  56. praisonai/cli/commands/lsp.py +112 -0
  57. praisonai/cli/commands/mcp.py +210 -0
  58. praisonai/cli/commands/profile.py +457 -0
  59. praisonai/cli/commands/run.py +228 -0
  60. praisonai/cli/commands/schedule.py +150 -0
  61. praisonai/cli/commands/serve.py +97 -0
  62. praisonai/cli/commands/session.py +212 -0
  63. praisonai/cli/commands/traces.py +145 -0
  64. praisonai/cli/commands/version.py +101 -0
  65. praisonai/cli/configuration/__init__.py +18 -0
  66. praisonai/cli/configuration/loader.py +353 -0
  67. praisonai/cli/configuration/paths.py +114 -0
  68. praisonai/cli/configuration/schema.py +164 -0
  69. praisonai/cli/features/__init__.py +268 -0
  70. praisonai/cli/features/acp.py +236 -0
  71. praisonai/cli/features/action_orchestrator.py +546 -0
  72. praisonai/cli/features/agent_scheduler.py +773 -0
  73. praisonai/cli/features/agent_tools.py +474 -0
  74. praisonai/cli/features/agents.py +375 -0
  75. praisonai/cli/features/at_mentions.py +471 -0
  76. praisonai/cli/features/auto_memory.py +182 -0
  77. praisonai/cli/features/autonomy_mode.py +490 -0
  78. praisonai/cli/features/background.py +356 -0
  79. praisonai/cli/features/base.py +168 -0
  80. praisonai/cli/features/capabilities.py +1326 -0
  81. praisonai/cli/features/checkpoints.py +338 -0
  82. praisonai/cli/features/code_intelligence.py +652 -0
  83. praisonai/cli/features/compaction.py +294 -0
  84. praisonai/cli/features/compare.py +534 -0
  85. praisonai/cli/features/cost_tracker.py +514 -0
  86. praisonai/cli/features/debug.py +810 -0
  87. praisonai/cli/features/deploy.py +517 -0
  88. praisonai/cli/features/diag.py +289 -0
  89. praisonai/cli/features/doctor/__init__.py +63 -0
  90. praisonai/cli/features/doctor/checks/__init__.py +24 -0
  91. praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
  92. praisonai/cli/features/doctor/checks/config_checks.py +366 -0
  93. praisonai/cli/features/doctor/checks/db_checks.py +366 -0
  94. praisonai/cli/features/doctor/checks/env_checks.py +543 -0
  95. praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
  96. praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
  97. praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
  98. praisonai/cli/features/doctor/checks/network_checks.py +251 -0
  99. praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
  100. praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
  101. praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
  102. praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
  103. praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
  104. praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
  105. praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
  106. praisonai/cli/features/doctor/engine.py +266 -0
  107. praisonai/cli/features/doctor/formatters.py +310 -0
  108. praisonai/cli/features/doctor/handler.py +397 -0
  109. praisonai/cli/features/doctor/models.py +264 -0
  110. praisonai/cli/features/doctor/registry.py +239 -0
  111. praisonai/cli/features/endpoints.py +1019 -0
  112. praisonai/cli/features/eval.py +560 -0
  113. praisonai/cli/features/external_agents.py +231 -0
  114. praisonai/cli/features/fast_context.py +410 -0
  115. praisonai/cli/features/flow_display.py +566 -0
  116. praisonai/cli/features/git_integration.py +651 -0
  117. praisonai/cli/features/guardrail.py +171 -0
  118. praisonai/cli/features/handoff.py +185 -0
  119. praisonai/cli/features/hooks.py +583 -0
  120. praisonai/cli/features/image.py +384 -0
  121. praisonai/cli/features/interactive_runtime.py +585 -0
  122. praisonai/cli/features/interactive_tools.py +380 -0
  123. praisonai/cli/features/interactive_tui.py +603 -0
  124. praisonai/cli/features/jobs.py +632 -0
  125. praisonai/cli/features/knowledge.py +531 -0
  126. praisonai/cli/features/lite.py +244 -0
  127. praisonai/cli/features/lsp_cli.py +225 -0
  128. praisonai/cli/features/mcp.py +169 -0
  129. praisonai/cli/features/message_queue.py +587 -0
  130. praisonai/cli/features/metrics.py +211 -0
  131. praisonai/cli/features/n8n.py +673 -0
  132. praisonai/cli/features/observability.py +293 -0
  133. praisonai/cli/features/ollama.py +361 -0
  134. praisonai/cli/features/output_style.py +273 -0
  135. praisonai/cli/features/package.py +631 -0
  136. praisonai/cli/features/performance.py +308 -0
  137. praisonai/cli/features/persistence.py +636 -0
  138. praisonai/cli/features/profile.py +226 -0
  139. praisonai/cli/features/profiler/__init__.py +81 -0
  140. praisonai/cli/features/profiler/core.py +558 -0
  141. praisonai/cli/features/profiler/optimizations.py +652 -0
  142. praisonai/cli/features/profiler/suite.py +386 -0
  143. praisonai/cli/features/profiling.py +350 -0
  144. praisonai/cli/features/queue/__init__.py +73 -0
  145. praisonai/cli/features/queue/manager.py +395 -0
  146. praisonai/cli/features/queue/models.py +286 -0
  147. praisonai/cli/features/queue/persistence.py +564 -0
  148. praisonai/cli/features/queue/scheduler.py +484 -0
  149. praisonai/cli/features/queue/worker.py +372 -0
  150. praisonai/cli/features/recipe.py +1723 -0
  151. praisonai/cli/features/recipes.py +449 -0
  152. praisonai/cli/features/registry.py +229 -0
  153. praisonai/cli/features/repo_map.py +860 -0
  154. praisonai/cli/features/router.py +466 -0
  155. praisonai/cli/features/sandbox_executor.py +515 -0
  156. praisonai/cli/features/serve.py +829 -0
  157. praisonai/cli/features/session.py +222 -0
  158. praisonai/cli/features/skills.py +856 -0
  159. praisonai/cli/features/slash_commands.py +650 -0
  160. praisonai/cli/features/telemetry.py +179 -0
  161. praisonai/cli/features/templates.py +1384 -0
  162. praisonai/cli/features/thinking.py +305 -0
  163. praisonai/cli/features/todo.py +334 -0
  164. praisonai/cli/features/tools.py +680 -0
  165. praisonai/cli/features/tui/__init__.py +83 -0
  166. praisonai/cli/features/tui/app.py +580 -0
  167. praisonai/cli/features/tui/cli.py +566 -0
  168. praisonai/cli/features/tui/debug.py +511 -0
  169. praisonai/cli/features/tui/events.py +99 -0
  170. praisonai/cli/features/tui/mock_provider.py +328 -0
  171. praisonai/cli/features/tui/orchestrator.py +652 -0
  172. praisonai/cli/features/tui/screens/__init__.py +50 -0
  173. praisonai/cli/features/tui/screens/main.py +245 -0
  174. praisonai/cli/features/tui/screens/queue.py +174 -0
  175. praisonai/cli/features/tui/screens/session.py +124 -0
  176. praisonai/cli/features/tui/screens/settings.py +148 -0
  177. praisonai/cli/features/tui/widgets/__init__.py +56 -0
  178. praisonai/cli/features/tui/widgets/chat.py +261 -0
  179. praisonai/cli/features/tui/widgets/composer.py +224 -0
  180. praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
  181. praisonai/cli/features/tui/widgets/status.py +167 -0
  182. praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
  183. praisonai/cli/features/workflow.py +720 -0
  184. praisonai/cli/legacy.py +236 -0
  185. praisonai/cli/main.py +5559 -0
  186. praisonai/cli/schedule_cli.py +54 -0
  187. praisonai/cli/state/__init__.py +31 -0
  188. praisonai/cli/state/identifiers.py +161 -0
  189. praisonai/cli/state/sessions.py +313 -0
  190. praisonai/code/__init__.py +93 -0
  191. praisonai/code/agent_tools.py +344 -0
  192. praisonai/code/diff/__init__.py +21 -0
  193. praisonai/code/diff/diff_strategy.py +432 -0
  194. praisonai/code/tools/__init__.py +27 -0
  195. praisonai/code/tools/apply_diff.py +221 -0
  196. praisonai/code/tools/execute_command.py +275 -0
  197. praisonai/code/tools/list_files.py +274 -0
  198. praisonai/code/tools/read_file.py +206 -0
  199. praisonai/code/tools/search_replace.py +248 -0
  200. praisonai/code/tools/write_file.py +217 -0
  201. praisonai/code/utils/__init__.py +46 -0
  202. praisonai/code/utils/file_utils.py +307 -0
  203. praisonai/code/utils/ignore_utils.py +308 -0
  204. praisonai/code/utils/text_utils.py +276 -0
  205. praisonai/db/__init__.py +64 -0
  206. praisonai/db/adapter.py +531 -0
  207. praisonai/deploy/__init__.py +62 -0
  208. praisonai/deploy/api.py +231 -0
  209. praisonai/deploy/docker.py +454 -0
  210. praisonai/deploy/doctor.py +367 -0
  211. praisonai/deploy/main.py +327 -0
  212. praisonai/deploy/models.py +179 -0
  213. praisonai/deploy/providers/__init__.py +33 -0
  214. praisonai/deploy/providers/aws.py +331 -0
  215. praisonai/deploy/providers/azure.py +358 -0
  216. praisonai/deploy/providers/base.py +101 -0
  217. praisonai/deploy/providers/gcp.py +314 -0
  218. praisonai/deploy/schema.py +208 -0
  219. praisonai/deploy.py +185 -0
  220. praisonai/endpoints/__init__.py +53 -0
  221. praisonai/endpoints/a2u_server.py +410 -0
  222. praisonai/endpoints/discovery.py +165 -0
  223. praisonai/endpoints/providers/__init__.py +28 -0
  224. praisonai/endpoints/providers/a2a.py +253 -0
  225. praisonai/endpoints/providers/a2u.py +208 -0
  226. praisonai/endpoints/providers/agents_api.py +171 -0
  227. praisonai/endpoints/providers/base.py +231 -0
  228. praisonai/endpoints/providers/mcp.py +263 -0
  229. praisonai/endpoints/providers/recipe.py +206 -0
  230. praisonai/endpoints/providers/tools_mcp.py +150 -0
  231. praisonai/endpoints/registry.py +131 -0
  232. praisonai/endpoints/server.py +161 -0
  233. praisonai/inbuilt_tools/__init__.py +24 -0
  234. praisonai/inbuilt_tools/autogen_tools.py +117 -0
  235. praisonai/inc/__init__.py +2 -0
  236. praisonai/inc/config.py +96 -0
  237. praisonai/inc/models.py +155 -0
  238. praisonai/integrations/__init__.py +56 -0
  239. praisonai/integrations/base.py +303 -0
  240. praisonai/integrations/claude_code.py +270 -0
  241. praisonai/integrations/codex_cli.py +255 -0
  242. praisonai/integrations/cursor_cli.py +195 -0
  243. praisonai/integrations/gemini_cli.py +222 -0
  244. praisonai/jobs/__init__.py +67 -0
  245. praisonai/jobs/executor.py +425 -0
  246. praisonai/jobs/models.py +230 -0
  247. praisonai/jobs/router.py +314 -0
  248. praisonai/jobs/server.py +186 -0
  249. praisonai/jobs/store.py +203 -0
  250. praisonai/llm/__init__.py +66 -0
  251. praisonai/llm/registry.py +382 -0
  252. praisonai/mcp_server/__init__.py +152 -0
  253. praisonai/mcp_server/adapters/__init__.py +74 -0
  254. praisonai/mcp_server/adapters/agents.py +128 -0
  255. praisonai/mcp_server/adapters/capabilities.py +168 -0
  256. praisonai/mcp_server/adapters/cli_tools.py +568 -0
  257. praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
  258. praisonai/mcp_server/adapters/knowledge.py +93 -0
  259. praisonai/mcp_server/adapters/memory.py +104 -0
  260. praisonai/mcp_server/adapters/prompts.py +306 -0
  261. praisonai/mcp_server/adapters/resources.py +124 -0
  262. praisonai/mcp_server/adapters/tools_bridge.py +280 -0
  263. praisonai/mcp_server/auth/__init__.py +48 -0
  264. praisonai/mcp_server/auth/api_key.py +291 -0
  265. praisonai/mcp_server/auth/oauth.py +460 -0
  266. praisonai/mcp_server/auth/oidc.py +289 -0
  267. praisonai/mcp_server/auth/scopes.py +260 -0
  268. praisonai/mcp_server/cli.py +852 -0
  269. praisonai/mcp_server/elicitation.py +445 -0
  270. praisonai/mcp_server/icons.py +302 -0
  271. praisonai/mcp_server/recipe_adapter.py +573 -0
  272. praisonai/mcp_server/recipe_cli.py +824 -0
  273. praisonai/mcp_server/registry.py +703 -0
  274. praisonai/mcp_server/sampling.py +422 -0
  275. praisonai/mcp_server/server.py +490 -0
  276. praisonai/mcp_server/tasks.py +443 -0
  277. praisonai/mcp_server/transports/__init__.py +18 -0
  278. praisonai/mcp_server/transports/http_stream.py +376 -0
  279. praisonai/mcp_server/transports/stdio.py +132 -0
  280. praisonai/persistence/__init__.py +84 -0
  281. praisonai/persistence/config.py +238 -0
  282. praisonai/persistence/conversation/__init__.py +25 -0
  283. praisonai/persistence/conversation/async_mysql.py +427 -0
  284. praisonai/persistence/conversation/async_postgres.py +410 -0
  285. praisonai/persistence/conversation/async_sqlite.py +371 -0
  286. praisonai/persistence/conversation/base.py +151 -0
  287. praisonai/persistence/conversation/json_store.py +250 -0
  288. praisonai/persistence/conversation/mysql.py +387 -0
  289. praisonai/persistence/conversation/postgres.py +401 -0
  290. praisonai/persistence/conversation/singlestore.py +240 -0
  291. praisonai/persistence/conversation/sqlite.py +341 -0
  292. praisonai/persistence/conversation/supabase.py +203 -0
  293. praisonai/persistence/conversation/surrealdb.py +287 -0
  294. praisonai/persistence/factory.py +301 -0
  295. praisonai/persistence/hooks/__init__.py +18 -0
  296. praisonai/persistence/hooks/agent_hooks.py +297 -0
  297. praisonai/persistence/knowledge/__init__.py +26 -0
  298. praisonai/persistence/knowledge/base.py +144 -0
  299. praisonai/persistence/knowledge/cassandra.py +232 -0
  300. praisonai/persistence/knowledge/chroma.py +295 -0
  301. praisonai/persistence/knowledge/clickhouse.py +242 -0
  302. praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
  303. praisonai/persistence/knowledge/couchbase.py +286 -0
  304. praisonai/persistence/knowledge/lancedb.py +216 -0
  305. praisonai/persistence/knowledge/langchain_adapter.py +291 -0
  306. praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
  307. praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
  308. praisonai/persistence/knowledge/milvus.py +277 -0
  309. praisonai/persistence/knowledge/mongodb_vector.py +306 -0
  310. praisonai/persistence/knowledge/pgvector.py +335 -0
  311. praisonai/persistence/knowledge/pinecone.py +253 -0
  312. praisonai/persistence/knowledge/qdrant.py +301 -0
  313. praisonai/persistence/knowledge/redis_vector.py +291 -0
  314. praisonai/persistence/knowledge/singlestore_vector.py +299 -0
  315. praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
  316. praisonai/persistence/knowledge/upstash_vector.py +266 -0
  317. praisonai/persistence/knowledge/weaviate.py +223 -0
  318. praisonai/persistence/migrations/__init__.py +10 -0
  319. praisonai/persistence/migrations/manager.py +251 -0
  320. praisonai/persistence/orchestrator.py +406 -0
  321. praisonai/persistence/state/__init__.py +21 -0
  322. praisonai/persistence/state/async_mongodb.py +200 -0
  323. praisonai/persistence/state/base.py +107 -0
  324. praisonai/persistence/state/dynamodb.py +226 -0
  325. praisonai/persistence/state/firestore.py +175 -0
  326. praisonai/persistence/state/gcs.py +155 -0
  327. praisonai/persistence/state/memory.py +245 -0
  328. praisonai/persistence/state/mongodb.py +158 -0
  329. praisonai/persistence/state/redis.py +190 -0
  330. praisonai/persistence/state/upstash.py +144 -0
  331. praisonai/persistence/tests/__init__.py +3 -0
  332. praisonai/persistence/tests/test_all_backends.py +633 -0
  333. praisonai/profiler.py +1214 -0
  334. praisonai/recipe/__init__.py +134 -0
  335. praisonai/recipe/bridge.py +278 -0
  336. praisonai/recipe/core.py +893 -0
  337. praisonai/recipe/exceptions.py +54 -0
  338. praisonai/recipe/history.py +402 -0
  339. praisonai/recipe/models.py +266 -0
  340. praisonai/recipe/operations.py +440 -0
  341. praisonai/recipe/policy.py +422 -0
  342. praisonai/recipe/registry.py +849 -0
  343. praisonai/recipe/runtime.py +214 -0
  344. praisonai/recipe/security.py +711 -0
  345. praisonai/recipe/serve.py +859 -0
  346. praisonai/recipe/server.py +613 -0
  347. praisonai/scheduler/__init__.py +45 -0
  348. praisonai/scheduler/agent_scheduler.py +552 -0
  349. praisonai/scheduler/base.py +124 -0
  350. praisonai/scheduler/daemon_manager.py +225 -0
  351. praisonai/scheduler/state_manager.py +155 -0
  352. praisonai/scheduler/yaml_loader.py +193 -0
  353. praisonai/scheduler.py +194 -0
  354. praisonai/setup/__init__.py +1 -0
  355. praisonai/setup/build.py +21 -0
  356. praisonai/setup/post_install.py +23 -0
  357. praisonai/setup/setup_conda_env.py +25 -0
  358. praisonai/setup.py +16 -0
  359. praisonai/templates/__init__.py +116 -0
  360. praisonai/templates/cache.py +364 -0
  361. praisonai/templates/dependency_checker.py +358 -0
  362. praisonai/templates/discovery.py +391 -0
  363. praisonai/templates/loader.py +564 -0
  364. praisonai/templates/registry.py +511 -0
  365. praisonai/templates/resolver.py +206 -0
  366. praisonai/templates/security.py +327 -0
  367. praisonai/templates/tool_override.py +498 -0
  368. praisonai/templates/tools_doctor.py +256 -0
  369. praisonai/test.py +105 -0
  370. praisonai/train.py +562 -0
  371. praisonai/train_vision.py +306 -0
  372. praisonai/ui/agents.py +824 -0
  373. praisonai/ui/callbacks.py +57 -0
  374. praisonai/ui/chainlit_compat.py +246 -0
  375. praisonai/ui/chat.py +532 -0
  376. praisonai/ui/code.py +717 -0
  377. praisonai/ui/colab.py +474 -0
  378. praisonai/ui/colab_chainlit.py +81 -0
  379. praisonai/ui/components/aicoder.py +284 -0
  380. praisonai/ui/context.py +283 -0
  381. praisonai/ui/database_config.py +56 -0
  382. praisonai/ui/db.py +294 -0
  383. praisonai/ui/realtime.py +488 -0
  384. praisonai/ui/realtimeclient/__init__.py +756 -0
  385. praisonai/ui/realtimeclient/tools.py +242 -0
  386. praisonai/ui/sql_alchemy.py +710 -0
  387. praisonai/upload_vision.py +140 -0
  388. praisonai/version.py +1 -0
  389. praisonai-3.0.0.dist-info/METADATA +3493 -0
  390. praisonai-3.0.0.dist-info/RECORD +393 -0
  391. praisonai-3.0.0.dist-info/WHEEL +5 -0
  392. praisonai-3.0.0.dist-info/entry_points.txt +4 -0
  393. praisonai-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,206 @@
1
+ """
2
+ Template URI Resolver
3
+
4
+ Parses and resolves template URIs to their source locations.
5
+ Supports: local paths, package refs, GitHub refs, HTTP URLs.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+ from typing import Optional, Tuple
13
+
14
+
15
+ class TemplateSource(Enum):
16
+ """Supported template sources."""
17
+ LOCAL = "local"
18
+ PACKAGE = "package"
19
+ GITHUB = "github"
20
+ HTTP = "http"
21
+
22
+
23
+ @dataclass
24
+ class ResolvedTemplate:
25
+ """Resolved template location."""
26
+ source: TemplateSource
27
+ path: str
28
+ version: Optional[str] = None
29
+ owner: Optional[str] = None
30
+ repo: Optional[str] = None
31
+ ref: Optional[str] = None
32
+ url: Optional[str] = None
33
+
34
+ @property
35
+ def cache_key(self) -> str:
36
+ """Generate a unique cache key for this template."""
37
+ if self.source == TemplateSource.LOCAL:
38
+ return f"local/{os.path.abspath(self.path)}"
39
+ elif self.source == TemplateSource.PACKAGE:
40
+ return f"package/{self.path}"
41
+ elif self.source == TemplateSource.GITHUB:
42
+ ref = self.ref or "main"
43
+ return f"github/{self.owner}/{self.repo}/{self.path}@{ref}"
44
+ elif self.source == TemplateSource.HTTP:
45
+ return f"http/{self.url}"
46
+ return f"unknown/{self.path}"
47
+
48
+ @property
49
+ def is_pinned(self) -> bool:
50
+ """Check if this template reference is pinned (not 'latest')."""
51
+ if self.source == TemplateSource.LOCAL:
52
+ return True # Local is always "pinned"
53
+ if self.source == TemplateSource.PACKAGE:
54
+ return True # Package is always "pinned" to installed version
55
+ if self.source == TemplateSource.GITHUB:
56
+ # Pinned if ref is a commit hash (40 hex chars) or a version tag
57
+ if self.ref and (
58
+ re.match(r'^[a-f0-9]{40}$', self.ref) or
59
+ re.match(r'^v?\d+\.\d+', self.ref)
60
+ ):
61
+ return True
62
+ return False
63
+ return False
64
+
65
+
66
+ class TemplateResolver:
67
+ """
68
+ Resolves template URIs to their source locations.
69
+
70
+ Supported URI formats:
71
+ - Local path: ./my-template, /absolute/path/template, ~/templates/my-template
72
+ - Package ref: package:agent_recipes/transcript-generator
73
+ - GitHub ref: github:owner/repo/template-name[@ref]
74
+ - HTTP URL: https://example.com/template.yaml
75
+
76
+ Legacy formats (also supported):
77
+ - praison://local/./path
78
+ - praison://package/name/template
79
+ - praison://github/owner/repo/template@ref
80
+ """
81
+
82
+ # URI patterns
83
+ GITHUB_PATTERN = re.compile(
84
+ r'^(?:github:|praison://github/)([^/]+)/([^/]+)/([^@]+)(?:@(.+))?$'
85
+ )
86
+ PACKAGE_PATTERN = re.compile(
87
+ r'^(?:package:|praison://package/)([^/]+)/(.+)$'
88
+ )
89
+ HTTP_PATTERN = re.compile(
90
+ r'^(?:https?://|praison://https?/).+'
91
+ )
92
+ LOCAL_PATTERN = re.compile(
93
+ r'^(?:praison://local/)?([.~]?/.+|[a-zA-Z]:\\.+)$'
94
+ )
95
+
96
+ @classmethod
97
+ def resolve(cls, uri: str) -> ResolvedTemplate:
98
+ """
99
+ Resolve a template URI to its source location.
100
+
101
+ Args:
102
+ uri: Template URI string
103
+
104
+ Returns:
105
+ ResolvedTemplate with source details
106
+
107
+ Raises:
108
+ ValueError: If URI format is not recognized
109
+ """
110
+ uri = uri.strip()
111
+
112
+ # Check GitHub pattern
113
+ match = cls.GITHUB_PATTERN.match(uri)
114
+ if match:
115
+ owner, repo, path, ref = match.groups()
116
+ return ResolvedTemplate(
117
+ source=TemplateSource.GITHUB,
118
+ path=path,
119
+ owner=owner,
120
+ repo=repo,
121
+ ref=ref or "main"
122
+ )
123
+
124
+ # Check package pattern
125
+ match = cls.PACKAGE_PATTERN.match(uri)
126
+ if match:
127
+ package, template = match.groups()
128
+ return ResolvedTemplate(
129
+ source=TemplateSource.PACKAGE,
130
+ path=f"{package}/{template}"
131
+ )
132
+
133
+ # Check HTTP pattern
134
+ if cls.HTTP_PATTERN.match(uri):
135
+ # Extract actual URL
136
+ url = uri
137
+ if uri.startswith("praison://"):
138
+ url = uri.replace("praison://", "")
139
+ return ResolvedTemplate(
140
+ source=TemplateSource.HTTP,
141
+ path=url,
142
+ url=url
143
+ )
144
+
145
+ # Check local pattern or assume local if nothing else matches
146
+ if cls.LOCAL_PATTERN.match(uri) or os.path.exists(uri) or uri.startswith(("./", "../", "~/")):
147
+ path = uri
148
+ if uri.startswith("praison://local/"):
149
+ path = uri.replace("praison://local/", "")
150
+ # Expand user home
151
+ path = os.path.expanduser(path)
152
+ return ResolvedTemplate(
153
+ source=TemplateSource.LOCAL,
154
+ path=path
155
+ )
156
+
157
+ # Try as simple template name (assume package:agent_recipes/name)
158
+ if re.match(r'^[a-zA-Z][a-zA-Z0-9_-]*$', uri):
159
+ return ResolvedTemplate(
160
+ source=TemplateSource.PACKAGE,
161
+ path=f"agent_recipes/{uri}"
162
+ )
163
+
164
+ raise ValueError(
165
+ f"Unrecognized template URI format: {uri}\n"
166
+ "Supported formats:\n"
167
+ " - Local: ./path, /absolute/path, ~/path\n"
168
+ " - Package: package:agent_recipes/template-name\n"
169
+ " - GitHub: github:owner/repo/template[@ref]\n"
170
+ " - HTTP: https://example.com/template.yaml"
171
+ )
172
+
173
+ @classmethod
174
+ def parse_version(cls, uri: str) -> Tuple[str, Optional[str]]:
175
+ """
176
+ Extract version/ref from a URI.
177
+
178
+ Args:
179
+ uri: Template URI string
180
+
181
+ Returns:
182
+ Tuple of (base_uri, version)
183
+ """
184
+ if "@" in uri and not uri.startswith("http"):
185
+ parts = uri.rsplit("@", 1)
186
+ return parts[0], parts[1]
187
+ return uri, None
188
+
189
+ @classmethod
190
+ def build_github_uri(
191
+ cls,
192
+ owner: str,
193
+ repo: str,
194
+ template: str,
195
+ ref: Optional[str] = None
196
+ ) -> str:
197
+ """Build a GitHub template URI."""
198
+ uri = f"github:{owner}/{repo}/{template}"
199
+ if ref:
200
+ uri += f"@{ref}"
201
+ return uri
202
+
203
+ @classmethod
204
+ def build_package_uri(cls, package: str, template: str) -> str:
205
+ """Build a package template URI."""
206
+ return f"package:{package}/{template}"
@@ -0,0 +1,327 @@
1
+ """
2
+ Template Security
3
+
4
+ Security primitives for template validation and safe extraction.
5
+ Includes checksum verification, allowlists, and safe path handling.
6
+ """
7
+
8
+ import hashlib
9
+ import os
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional, Set
13
+ from dataclasses import dataclass, field
14
+
15
+
16
+ @dataclass
17
+ class SecurityConfig:
18
+ """Security configuration for template operations."""
19
+
20
+ # Allowed template sources
21
+ allowed_sources: Set[str] = field(default_factory=lambda: {
22
+ "github:MervinPraison/agent-recipes",
23
+ "package:agent_recipes",
24
+ })
25
+
26
+ # Allow all local paths by default
27
+ allow_local: bool = True
28
+
29
+ # Allow all GitHub sources (not just allowlisted)
30
+ allow_any_github: bool = True
31
+
32
+ # Allow HTTP sources
33
+ allow_http: bool = False
34
+
35
+ # Require checksum verification for remote templates
36
+ require_checksum: bool = False
37
+
38
+ # Maximum template size in bytes (10MB default)
39
+ max_template_size: int = 10 * 1024 * 1024
40
+
41
+ # Blocked file patterns (security risk)
42
+ blocked_patterns: List[str] = field(default_factory=lambda: [
43
+ r"\.\.\/", # Path traversal
44
+ r"^\/", # Absolute paths in archives
45
+ r"\.exe$", # Executables
46
+ r"\.dll$",
47
+ r"\.so$",
48
+ r"\.dylib$",
49
+ r"\.sh$", # Shell scripts (unless explicitly allowed)
50
+ r"\.bat$",
51
+ r"\.cmd$",
52
+ r"\.ps1$",
53
+ ])
54
+
55
+ # Allowed file extensions
56
+ allowed_extensions: Set[str] = field(default_factory=lambda: {
57
+ ".yaml", ".yml", ".json", ".md", ".txt", ".py",
58
+ ".toml", ".cfg", ".ini", ".env.example"
59
+ })
60
+
61
+
62
+ class TemplateSecurity:
63
+ """
64
+ Security handler for template operations.
65
+
66
+ Provides:
67
+ - Source allowlist validation
68
+ - Checksum verification
69
+ - Safe path extraction
70
+ - File type validation
71
+ """
72
+
73
+ CONFIG_FILE = ".praison/security.yaml"
74
+
75
+ def __init__(self, config: Optional[SecurityConfig] = None):
76
+ """
77
+ Initialize security handler.
78
+
79
+ Args:
80
+ config: Security configuration (loads from file if not provided)
81
+ """
82
+ self.config = config or self._load_config()
83
+
84
+ def _load_config(self) -> SecurityConfig:
85
+ """Load security config from file or use defaults."""
86
+ config_path = Path.home() / self.CONFIG_FILE
87
+
88
+ if config_path.exists():
89
+ try:
90
+ import yaml
91
+ with open(config_path) as f:
92
+ data = yaml.safe_load(f)
93
+ return SecurityConfig(
94
+ allowed_sources=set(data.get("allowed_sources", [])),
95
+ allow_local=data.get("allow_local", True),
96
+ allow_any_github=data.get("allow_any_github", True),
97
+ allow_http=data.get("allow_http", False),
98
+ require_checksum=data.get("require_checksum", False),
99
+ max_template_size=data.get("max_template_size", 10 * 1024 * 1024),
100
+ )
101
+ except Exception:
102
+ pass
103
+
104
+ return SecurityConfig()
105
+
106
+ def is_source_allowed(self, uri: str) -> bool:
107
+ """
108
+ Check if a template source is allowed.
109
+
110
+ Args:
111
+ uri: Template URI
112
+
113
+ Returns:
114
+ True if source is allowed
115
+ """
116
+ # Local paths
117
+ if uri.startswith(("./", "../", "~/", "/")) or os.path.exists(uri):
118
+ return self.config.allow_local
119
+
120
+ # Package references
121
+ if uri.startswith("package:"):
122
+ package = uri.split(":")[1].split("/")[0]
123
+ return (
124
+ f"package:{package}" in self.config.allowed_sources or
125
+ uri in self.config.allowed_sources
126
+ )
127
+
128
+ # GitHub references
129
+ if uri.startswith("github:"):
130
+ if self.config.allow_any_github:
131
+ return True
132
+ # Check specific repo allowlist
133
+ parts = uri.replace("github:", "").split("/")
134
+ if len(parts) >= 2:
135
+ repo_ref = f"github:{parts[0]}/{parts[1]}"
136
+ return repo_ref in self.config.allowed_sources
137
+ return False
138
+
139
+ # HTTP references
140
+ if uri.startswith(("http://", "https://")):
141
+ return self.config.allow_http
142
+
143
+ # Simple template name (assumes default repo)
144
+ if re.match(r'^[a-zA-Z][a-zA-Z0-9_-]*$', uri):
145
+ return True
146
+
147
+ return False
148
+
149
+ def verify_checksum(
150
+ self,
151
+ directory: Path,
152
+ expected_checksum: str,
153
+ algorithm: str = "sha256"
154
+ ) -> bool:
155
+ """
156
+ Verify checksum of template directory.
157
+
158
+ Args:
159
+ directory: Template directory
160
+ expected_checksum: Expected checksum value
161
+ algorithm: Hash algorithm (sha256, sha512, md5)
162
+
163
+ Returns:
164
+ True if checksum matches
165
+ """
166
+ actual = self.calculate_checksum(directory, algorithm)
167
+ return actual == expected_checksum
168
+
169
+ def calculate_checksum(
170
+ self,
171
+ directory: Path,
172
+ algorithm: str = "sha256"
173
+ ) -> str:
174
+ """
175
+ Calculate checksum of template directory.
176
+
177
+ Args:
178
+ directory: Template directory
179
+ algorithm: Hash algorithm
180
+
181
+ Returns:
182
+ Hex digest of checksum
183
+ """
184
+ if algorithm == "sha256":
185
+ hasher = hashlib.sha256()
186
+ elif algorithm == "sha512":
187
+ hasher = hashlib.sha512()
188
+ elif algorithm == "md5":
189
+ hasher = hashlib.md5()
190
+ else:
191
+ raise ValueError(f"Unsupported algorithm: {algorithm}")
192
+
193
+ for file_path in sorted(directory.rglob("*")):
194
+ if file_path.is_file() and not file_path.name.startswith("."):
195
+ hasher.update(file_path.name.encode())
196
+ hasher.update(file_path.read_bytes())
197
+
198
+ return hasher.hexdigest()
199
+
200
+ def validate_path(self, path: str) -> bool:
201
+ """
202
+ Validate a path for security issues.
203
+
204
+ Args:
205
+ path: Path to validate
206
+
207
+ Returns:
208
+ True if path is safe
209
+ """
210
+ for pattern in self.config.blocked_patterns:
211
+ if re.search(pattern, path, re.IGNORECASE):
212
+ return False
213
+ return True
214
+
215
+ def validate_file(self, file_path: Path) -> bool:
216
+ """
217
+ Validate a file for security.
218
+
219
+ Args:
220
+ file_path: Path to file
221
+
222
+ Returns:
223
+ True if file is safe
224
+ """
225
+ # Check extension
226
+ if file_path.suffix.lower() not in self.config.allowed_extensions:
227
+ # Allow files without extension (like README)
228
+ if file_path.suffix:
229
+ return False
230
+ # Files without extension are allowed (like README, LICENSE)
231
+ return True
232
+
233
+ # Check size
234
+ if file_path.exists() and file_path.stat().st_size > self.config.max_template_size:
235
+ return False
236
+
237
+ # Check path (only validate the filename, not the full path)
238
+ return self.validate_path(file_path.name)
239
+
240
+ def validate_template_directory(self, directory: Path) -> List[str]:
241
+ """
242
+ Validate all files in a template directory.
243
+
244
+ Args:
245
+ directory: Template directory
246
+
247
+ Returns:
248
+ List of validation errors (empty if valid)
249
+ """
250
+ errors = []
251
+ total_size = 0
252
+
253
+ for file_path in directory.rglob("*"):
254
+ if file_path.is_file():
255
+ # Check path safety
256
+ if not self.validate_path(str(file_path.relative_to(directory))):
257
+ errors.append(f"Unsafe path: {file_path.relative_to(directory)}")
258
+ continue
259
+
260
+ # Check extension
261
+ if file_path.suffix.lower() not in self.config.allowed_extensions:
262
+ if file_path.suffix: # Has extension but not allowed
263
+ errors.append(f"Blocked file type: {file_path.name}")
264
+
265
+ # Track size
266
+ total_size += file_path.stat().st_size
267
+
268
+ if total_size > self.config.max_template_size:
269
+ errors.append(
270
+ f"Template too large: {total_size} bytes "
271
+ f"(max: {self.config.max_template_size})"
272
+ )
273
+
274
+ return errors
275
+
276
+ def sanitize_template_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
277
+ """
278
+ Sanitize template configuration.
279
+
280
+ Removes potentially dangerous fields and validates structure.
281
+
282
+ Args:
283
+ config: Raw template configuration
284
+
285
+ Returns:
286
+ Sanitized configuration
287
+ """
288
+ # Fields that are safe to keep
289
+ safe_fields = {
290
+ "name", "description", "version", "author", "license",
291
+ "tags", "requires", "config", "workflow", "agents",
292
+ "skills", "cli", "metadata"
293
+ }
294
+
295
+ sanitized = {}
296
+ for key, value in config.items():
297
+ if key in safe_fields:
298
+ sanitized[key] = value
299
+
300
+ return sanitized
301
+
302
+ def add_allowed_source(self, source: str) -> None:
303
+ """Add a source to the allowlist."""
304
+ self.config.allowed_sources.add(source)
305
+
306
+ def remove_allowed_source(self, source: str) -> None:
307
+ """Remove a source from the allowlist."""
308
+ self.config.allowed_sources.discard(source)
309
+
310
+ def save_config(self) -> None:
311
+ """Save current config to file."""
312
+ import yaml
313
+
314
+ config_path = Path.home() / self.CONFIG_FILE
315
+ config_path.parent.mkdir(parents=True, exist_ok=True)
316
+
317
+ data = {
318
+ "allowed_sources": list(self.config.allowed_sources),
319
+ "allow_local": self.config.allow_local,
320
+ "allow_any_github": self.config.allow_any_github,
321
+ "allow_http": self.config.allow_http,
322
+ "require_checksum": self.config.require_checksum,
323
+ "max_template_size": self.config.max_template_size,
324
+ }
325
+
326
+ with open(config_path, "w") as f:
327
+ yaml.dump(data, f, default_flow_style=False)