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,711 @@
1
+ """
2
+ Recipe Security Module
3
+
4
+ Provides security features for recipes:
5
+ - SBOM (Software Bill of Materials) generation
6
+ - Bundle signing and verification
7
+ - Dependency auditing
8
+ - PII redaction
9
+ - Lockfile validation
10
+ """
11
+
12
+ import hashlib
13
+ import json
14
+ import re
15
+ import subprocess
16
+ import sys
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional, Tuple, Union
20
+
21
+
22
+ class SecurityError(Exception):
23
+ """Base exception for security operations."""
24
+ pass
25
+
26
+
27
+ class SignatureError(SecurityError):
28
+ """Signature verification failed."""
29
+ pass
30
+
31
+
32
+ class LockfileError(SecurityError):
33
+ """Lockfile validation failed."""
34
+ pass
35
+
36
+
37
+ def _get_timestamp() -> str:
38
+ """Get current timestamp in ISO format."""
39
+ return datetime.now(timezone.utc).isoformat()
40
+
41
+
42
+ # ============================================================================
43
+ # SBOM Generation
44
+ # ============================================================================
45
+
46
+ def generate_sbom(
47
+ recipe_path: Union[str, Path],
48
+ format: str = "cyclonedx",
49
+ include_python_deps: bool = True,
50
+ include_tools: bool = True,
51
+ ) -> Dict[str, Any]:
52
+ """
53
+ Generate Software Bill of Materials for a recipe.
54
+
55
+ Args:
56
+ recipe_path: Path to recipe directory or bundle
57
+ format: Output format (cyclonedx or spdx)
58
+ include_python_deps: Include Python dependencies
59
+ include_tools: Include tool dependencies
60
+
61
+ Returns:
62
+ SBOM as dictionary
63
+ """
64
+ recipe_path = Path(recipe_path)
65
+
66
+ # Load recipe manifest
67
+ template_path = recipe_path / "TEMPLATE.yaml"
68
+ manifest = {}
69
+ if template_path.exists():
70
+ import yaml
71
+ with open(template_path) as f:
72
+ manifest = yaml.safe_load(f) or {}
73
+
74
+ # Get recipe info
75
+ recipe_name = manifest.get("name", recipe_path.name)
76
+ recipe_version = manifest.get("version", "0.0.0")
77
+
78
+ # Collect components
79
+ components = []
80
+
81
+ # Add praisonai as component
82
+ try:
83
+ import praisonai
84
+ praisonai_version = getattr(praisonai, "__version__", "unknown")
85
+ except ImportError:
86
+ praisonai_version = "unknown"
87
+
88
+ components.append({
89
+ "type": "library",
90
+ "name": "praisonai",
91
+ "version": praisonai_version,
92
+ "purl": f"pkg:pypi/praisonai@{praisonai_version}",
93
+ })
94
+
95
+ # Add Python dependencies from lockfile
96
+ if include_python_deps:
97
+ deps = _get_python_deps(recipe_path)
98
+ for dep in deps:
99
+ components.append({
100
+ "type": "library",
101
+ "name": dep["name"],
102
+ "version": dep["version"],
103
+ "purl": f"pkg:pypi/{dep['name']}@{dep['version']}",
104
+ })
105
+
106
+ # Add tool dependencies
107
+ if include_tools:
108
+ requires = manifest.get("requires", {})
109
+ tools = requires.get("tools", [])
110
+ for tool in tools:
111
+ if isinstance(tool, str):
112
+ components.append({
113
+ "type": "application",
114
+ "name": tool,
115
+ "version": "unknown",
116
+ })
117
+ elif isinstance(tool, dict):
118
+ components.append({
119
+ "type": "application",
120
+ "name": tool.get("name", "unknown"),
121
+ "version": tool.get("version", "unknown"),
122
+ })
123
+
124
+ # External dependencies
125
+ external = requires.get("external", [])
126
+ for ext in external:
127
+ if isinstance(ext, str):
128
+ components.append({
129
+ "type": "application",
130
+ "name": ext,
131
+ "version": "unknown",
132
+ })
133
+
134
+ if format == "cyclonedx":
135
+ return _generate_cyclonedx(recipe_name, recipe_version, components)
136
+ elif format == "spdx":
137
+ return _generate_spdx(recipe_name, recipe_version, components)
138
+ else:
139
+ raise SecurityError(f"Unknown SBOM format: {format}")
140
+
141
+
142
+ def _generate_cyclonedx(
143
+ name: str,
144
+ version: str,
145
+ components: List[Dict[str, Any]],
146
+ ) -> Dict[str, Any]:
147
+ """Generate CycloneDX SBOM."""
148
+ return {
149
+ "bomFormat": "CycloneDX",
150
+ "specVersion": "1.4",
151
+ "serialNumber": f"urn:uuid:{hashlib.md5(f'{name}{version}{_get_timestamp()}'.encode()).hexdigest()}",
152
+ "version": 1,
153
+ "metadata": {
154
+ "timestamp": _get_timestamp(),
155
+ "tools": [{"name": "praisonai-sbom", "version": "1.0.0"}],
156
+ "component": {
157
+ "type": "application",
158
+ "name": name,
159
+ "version": version,
160
+ },
161
+ },
162
+ "components": [
163
+ {
164
+ "type": comp["type"],
165
+ "name": comp["name"],
166
+ "version": comp["version"],
167
+ "purl": comp.get("purl"),
168
+ }
169
+ for comp in components
170
+ ],
171
+ }
172
+
173
+
174
+ def _generate_spdx(
175
+ name: str,
176
+ version: str,
177
+ components: List[Dict[str, Any]],
178
+ ) -> Dict[str, Any]:
179
+ """Generate SPDX SBOM."""
180
+ return {
181
+ "spdxVersion": "SPDX-2.3",
182
+ "dataLicense": "CC0-1.0",
183
+ "SPDXID": "SPDXRef-DOCUMENT",
184
+ "name": f"{name}-{version}",
185
+ "documentNamespace": f"https://praison.ai/sbom/{name}/{version}",
186
+ "creationInfo": {
187
+ "created": _get_timestamp(),
188
+ "creators": ["Tool: praisonai-sbom-1.0.0"],
189
+ },
190
+ "packages": [
191
+ {
192
+ "SPDXID": f"SPDXRef-Package-{i}",
193
+ "name": comp["name"],
194
+ "versionInfo": comp["version"],
195
+ "downloadLocation": "NOASSERTION",
196
+ }
197
+ for i, comp in enumerate(components)
198
+ ],
199
+ }
200
+
201
+
202
+ def _get_python_deps(recipe_path: Path) -> List[Dict[str, str]]:
203
+ """Get Python dependencies from lockfile."""
204
+ deps = []
205
+ lock_dir = recipe_path / "lock"
206
+
207
+ # Try uv.lock
208
+ uv_lock = lock_dir / "uv.lock"
209
+ if uv_lock.exists():
210
+ deps.extend(_parse_uv_lock(uv_lock))
211
+ return deps
212
+
213
+ # Try requirements.lock
214
+ req_lock = lock_dir / "requirements.lock"
215
+ if req_lock.exists():
216
+ deps.extend(_parse_requirements_lock(req_lock))
217
+ return deps
218
+
219
+ # Try poetry.lock
220
+ poetry_lock = lock_dir / "poetry.lock"
221
+ if poetry_lock.exists():
222
+ deps.extend(_parse_poetry_lock(poetry_lock))
223
+ return deps
224
+
225
+ # Fallback: try requirements.txt in recipe root
226
+ req_txt = recipe_path / "requirements.txt"
227
+ if req_txt.exists():
228
+ deps.extend(_parse_requirements_lock(req_txt))
229
+
230
+ return deps
231
+
232
+
233
+ def _parse_uv_lock(path: Path) -> List[Dict[str, str]]:
234
+ """Parse uv.lock file."""
235
+ deps = []
236
+ try:
237
+ import tomllib
238
+ except ImportError:
239
+ import tomli as tomllib
240
+
241
+ try:
242
+ with open(path, "rb") as f:
243
+ data = tomllib.load(f)
244
+
245
+ for pkg in data.get("package", []):
246
+ deps.append({
247
+ "name": pkg.get("name", ""),
248
+ "version": pkg.get("version", ""),
249
+ })
250
+ except Exception:
251
+ pass
252
+
253
+ return deps
254
+
255
+
256
+ def _parse_requirements_lock(path: Path) -> List[Dict[str, str]]:
257
+ """Parse requirements.lock or requirements.txt."""
258
+ deps = []
259
+
260
+ with open(path) as f:
261
+ for line in f:
262
+ line = line.strip()
263
+ if not line or line.startswith("#") or line.startswith("-"):
264
+ continue
265
+
266
+ # Parse package==version
267
+ match = re.match(r"^([a-zA-Z0-9_-]+)==([^\s;]+)", line)
268
+ if match:
269
+ deps.append({
270
+ "name": match.group(1),
271
+ "version": match.group(2),
272
+ })
273
+
274
+ return deps
275
+
276
+
277
+ def _parse_poetry_lock(path: Path) -> List[Dict[str, str]]:
278
+ """Parse poetry.lock file."""
279
+ deps = []
280
+ try:
281
+ import tomllib
282
+ except ImportError:
283
+ import tomli as tomllib
284
+
285
+ try:
286
+ with open(path, "rb") as f:
287
+ data = tomllib.load(f)
288
+
289
+ for pkg in data.get("package", []):
290
+ deps.append({
291
+ "name": pkg.get("name", ""),
292
+ "version": pkg.get("version", ""),
293
+ })
294
+ except Exception:
295
+ pass
296
+
297
+ return deps
298
+
299
+
300
+ # ============================================================================
301
+ # Bundle Signing
302
+ # ============================================================================
303
+
304
+ def sign_bundle(
305
+ bundle_path: Union[str, Path],
306
+ private_key_path: Union[str, Path],
307
+ output_path: Optional[Union[str, Path]] = None,
308
+ ) -> Path:
309
+ """
310
+ Sign a recipe bundle.
311
+
312
+ Args:
313
+ bundle_path: Path to .praison bundle
314
+ private_key_path: Path to private key (PEM format)
315
+ output_path: Output path for signature (default: bundle.sig)
316
+
317
+ Returns:
318
+ Path to signature file
319
+ """
320
+ bundle_path = Path(bundle_path)
321
+ private_key_path = Path(private_key_path)
322
+
323
+ if not bundle_path.exists():
324
+ raise SecurityError(f"Bundle not found: {bundle_path}")
325
+ if not private_key_path.exists():
326
+ raise SecurityError(f"Private key not found: {private_key_path}")
327
+
328
+ # Calculate bundle hash
329
+ bundle_hash = _calculate_file_hash(bundle_path)
330
+
331
+ # Sign using cryptography library (lazy import)
332
+ try:
333
+ from cryptography.hazmat.primitives import hashes, serialization
334
+ from cryptography.hazmat.primitives.asymmetric import padding
335
+ except ImportError:
336
+ raise SecurityError("cryptography package required for signing. Install with: pip install cryptography")
337
+
338
+ # Load private key
339
+ with open(private_key_path, "rb") as f:
340
+ private_key = serialization.load_pem_private_key(f.read(), password=None)
341
+
342
+ # Sign the hash
343
+ signature = private_key.sign(
344
+ bundle_hash.encode(),
345
+ padding.PKCS1v15(),
346
+ hashes.SHA256(),
347
+ )
348
+
349
+ # Write signature
350
+ output_path = Path(output_path) if output_path else bundle_path.with_suffix(".praison.sig")
351
+
352
+ sig_data = {
353
+ "bundle": bundle_path.name,
354
+ "hash": bundle_hash,
355
+ "algorithm": "RSA-PKCS1v15-SHA256",
356
+ "signature": signature.hex(),
357
+ "signed_at": _get_timestamp(),
358
+ }
359
+
360
+ with open(output_path, "w") as f:
361
+ json.dump(sig_data, f, indent=2)
362
+
363
+ return output_path
364
+
365
+
366
+ def verify_bundle(
367
+ bundle_path: Union[str, Path],
368
+ public_key_path: Union[str, Path],
369
+ signature_path: Optional[Union[str, Path]] = None,
370
+ ) -> Tuple[bool, str]:
371
+ """
372
+ Verify a signed recipe bundle.
373
+
374
+ Args:
375
+ bundle_path: Path to .praison bundle
376
+ public_key_path: Path to public key (PEM format)
377
+ signature_path: Path to signature file (default: bundle.sig)
378
+
379
+ Returns:
380
+ Tuple of (valid: bool, message: str)
381
+ """
382
+ bundle_path = Path(bundle_path)
383
+ public_key_path = Path(public_key_path)
384
+ signature_path = Path(signature_path) if signature_path else bundle_path.with_suffix(".praison.sig")
385
+
386
+ if not bundle_path.exists():
387
+ return False, f"Bundle not found: {bundle_path}"
388
+ if not public_key_path.exists():
389
+ return False, f"Public key not found: {public_key_path}"
390
+ if not signature_path.exists():
391
+ return False, f"Signature not found: {signature_path}"
392
+
393
+ # Load signature
394
+ with open(signature_path) as f:
395
+ sig_data = json.load(f)
396
+
397
+ # Calculate current bundle hash
398
+ current_hash = _calculate_file_hash(bundle_path)
399
+
400
+ # Check hash matches
401
+ if current_hash != sig_data["hash"]:
402
+ return False, "Bundle hash mismatch - file may have been modified"
403
+
404
+ # Verify signature
405
+ try:
406
+ from cryptography.hazmat.primitives import hashes, serialization
407
+ from cryptography.hazmat.primitives.asymmetric import padding
408
+ except ImportError:
409
+ return False, "cryptography package required for verification"
410
+
411
+ # Load public key
412
+ with open(public_key_path, "rb") as f:
413
+ public_key = serialization.load_pem_public_key(f.read())
414
+
415
+ # Verify
416
+ try:
417
+ signature = bytes.fromhex(sig_data["signature"])
418
+ public_key.verify(
419
+ signature,
420
+ sig_data["hash"].encode(),
421
+ padding.PKCS1v15(),
422
+ hashes.SHA256(),
423
+ )
424
+ return True, "Signature valid"
425
+ except Exception as e:
426
+ return False, f"Signature verification failed: {e}"
427
+
428
+
429
+ def _calculate_file_hash(path: Path) -> str:
430
+ """Calculate SHA256 hash of a file."""
431
+ sha256 = hashlib.sha256()
432
+ with open(path, "rb") as f:
433
+ for chunk in iter(lambda: f.read(8192), b""):
434
+ sha256.update(chunk)
435
+ return sha256.hexdigest()
436
+
437
+
438
+ # ============================================================================
439
+ # Dependency Auditing
440
+ # ============================================================================
441
+
442
+ def audit_dependencies(
443
+ recipe_path: Union[str, Path],
444
+ check_vulnerabilities: bool = True,
445
+ ) -> Dict[str, Any]:
446
+ """
447
+ Audit recipe dependencies for security issues.
448
+
449
+ Args:
450
+ recipe_path: Path to recipe directory
451
+ check_vulnerabilities: Check for known vulnerabilities
452
+
453
+ Returns:
454
+ Audit report dictionary
455
+ """
456
+ recipe_path = Path(recipe_path)
457
+
458
+ report = {
459
+ "recipe": recipe_path.name,
460
+ "audited_at": _get_timestamp(),
461
+ "lockfile": None,
462
+ "dependencies": [],
463
+ "vulnerabilities": [],
464
+ "warnings": [],
465
+ "passed": True,
466
+ }
467
+
468
+ # Check for lockfile
469
+ lockfile = _find_lockfile(recipe_path)
470
+ if lockfile:
471
+ report["lockfile"] = str(lockfile)
472
+ else:
473
+ report["warnings"].append("No lockfile found - dependencies may not be reproducible")
474
+
475
+ # Get dependencies
476
+ deps = _get_python_deps(recipe_path)
477
+ report["dependencies"] = deps
478
+
479
+ # Check for vulnerabilities using pip-audit if available
480
+ if check_vulnerabilities and deps:
481
+ vulns = _check_vulnerabilities(deps)
482
+ report["vulnerabilities"] = vulns
483
+ if vulns:
484
+ report["passed"] = False
485
+
486
+ # Check for outdated dependencies
487
+ outdated = _check_outdated(deps)
488
+ if outdated:
489
+ report["warnings"].extend([
490
+ f"Outdated: {d['name']} ({d['current']} -> {d['latest']})"
491
+ for d in outdated
492
+ ])
493
+
494
+ return report
495
+
496
+
497
+ def _find_lockfile(recipe_path: Path) -> Optional[Path]:
498
+ """Find lockfile in recipe directory."""
499
+ lock_dir = recipe_path / "lock"
500
+
501
+ candidates = [
502
+ lock_dir / "uv.lock",
503
+ lock_dir / "requirements.lock",
504
+ lock_dir / "poetry.lock",
505
+ recipe_path / "uv.lock",
506
+ recipe_path / "requirements.lock",
507
+ recipe_path / "poetry.lock",
508
+ ]
509
+
510
+ for candidate in candidates:
511
+ if candidate.exists():
512
+ return candidate
513
+
514
+ return None
515
+
516
+
517
+ def _check_vulnerabilities(deps: List[Dict[str, str]]) -> List[Dict[str, Any]]:
518
+ """Check dependencies for known vulnerabilities."""
519
+ vulns = []
520
+
521
+ # Try using pip-audit
522
+ try:
523
+ result = subprocess.run(
524
+ [sys.executable, "-m", "pip_audit", "--format", "json"],
525
+ capture_output=True,
526
+ text=True,
527
+ timeout=60,
528
+ )
529
+ if result.returncode == 0:
530
+ audit_data = json.loads(result.stdout)
531
+ for vuln in audit_data.get("vulnerabilities", []):
532
+ vulns.append({
533
+ "package": vuln.get("name"),
534
+ "version": vuln.get("version"),
535
+ "vulnerability_id": vuln.get("id"),
536
+ "description": vuln.get("description"),
537
+ "fix_versions": vuln.get("fix_versions", []),
538
+ })
539
+ except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
540
+ pass
541
+
542
+ return vulns
543
+
544
+
545
+ def _check_outdated(deps: List[Dict[str, str]]) -> List[Dict[str, str]]:
546
+ """Check for outdated dependencies."""
547
+ # Simplified - would need PyPI API for full implementation
548
+ return []
549
+
550
+
551
+ # ============================================================================
552
+ # Lockfile Validation
553
+ # ============================================================================
554
+
555
+ def validate_lockfile(
556
+ recipe_path: Union[str, Path],
557
+ strict: bool = False,
558
+ ) -> Dict[str, Any]:
559
+ """
560
+ Validate recipe lockfile.
561
+
562
+ Args:
563
+ recipe_path: Path to recipe directory
564
+ strict: Fail if lockfile missing
565
+
566
+ Returns:
567
+ Validation result dictionary
568
+ """
569
+ recipe_path = Path(recipe_path)
570
+
571
+ result = {
572
+ "valid": True,
573
+ "lockfile": None,
574
+ "lockfile_type": None,
575
+ "errors": [],
576
+ "warnings": [],
577
+ }
578
+
579
+ lockfile = _find_lockfile(recipe_path)
580
+
581
+ if not lockfile:
582
+ if strict:
583
+ result["valid"] = False
584
+ result["errors"].append("No lockfile found")
585
+ else:
586
+ result["warnings"].append("No lockfile found - dependencies may not be reproducible")
587
+ return result
588
+
589
+ result["lockfile"] = str(lockfile)
590
+
591
+ # Determine lockfile type
592
+ if "uv.lock" in lockfile.name:
593
+ result["lockfile_type"] = "uv"
594
+ elif "poetry.lock" in lockfile.name:
595
+ result["lockfile_type"] = "poetry"
596
+ else:
597
+ result["lockfile_type"] = "pip"
598
+
599
+ # Validate lockfile format
600
+ try:
601
+ if result["lockfile_type"] == "uv":
602
+ _parse_uv_lock(lockfile)
603
+ elif result["lockfile_type"] == "poetry":
604
+ _parse_poetry_lock(lockfile)
605
+ else:
606
+ _parse_requirements_lock(lockfile)
607
+ except Exception as e:
608
+ result["valid"] = False
609
+ result["errors"].append(f"Invalid lockfile format: {e}")
610
+
611
+ return result
612
+
613
+
614
+ # ============================================================================
615
+ # PII Redaction
616
+ # ============================================================================
617
+
618
+ # Default PII patterns
619
+ PII_PATTERNS = {
620
+ "email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
621
+ "phone": r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b",
622
+ "ssn": r"\b\d{3}-\d{2}-\d{4}\b",
623
+ "credit_card": r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b",
624
+ "ip_address": r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b",
625
+ }
626
+
627
+
628
+ def redact_pii(
629
+ data: Any,
630
+ policy: Optional[Dict[str, Any]] = None,
631
+ fields: Optional[List[str]] = None,
632
+ ) -> Any:
633
+ """
634
+ Redact PII from data based on policy.
635
+
636
+ Args:
637
+ data: Data to redact (dict, list, or string)
638
+ policy: Data policy configuration
639
+ fields: Specific fields to redact (overrides policy)
640
+
641
+ Returns:
642
+ Redacted data
643
+ """
644
+ policy = policy or {}
645
+ mode = policy.get("pii", {}).get("mode", "allow")
646
+
647
+ if mode == "allow":
648
+ return data
649
+
650
+ fields = fields or policy.get("pii", {}).get("fields", list(PII_PATTERNS.keys()))
651
+
652
+ if isinstance(data, dict):
653
+ return {k: redact_pii(v, policy, fields) for k, v in data.items()}
654
+ elif isinstance(data, list):
655
+ return [redact_pii(item, policy, fields) for item in data]
656
+ elif isinstance(data, str):
657
+ return _redact_string(data, fields, mode)
658
+ else:
659
+ return data
660
+
661
+
662
+ def _redact_string(text: str, fields: List[str], mode: str) -> str:
663
+ """Redact PII patterns from a string."""
664
+ for field in fields:
665
+ if field in PII_PATTERNS:
666
+ pattern = PII_PATTERNS[field]
667
+ if mode == "redact":
668
+ text = re.sub(pattern, f"[REDACTED:{field}]", text)
669
+ elif mode == "deny":
670
+ if re.search(pattern, text):
671
+ raise SecurityError(f"PII detected ({field}) and policy is 'deny'")
672
+ return text
673
+
674
+
675
+ def detect_pii(
676
+ data: Any,
677
+ fields: Optional[List[str]] = None,
678
+ ) -> List[Dict[str, Any]]:
679
+ """
680
+ Detect PII in data without redacting.
681
+
682
+ Args:
683
+ data: Data to scan
684
+ fields: Specific fields to check
685
+
686
+ Returns:
687
+ List of detected PII instances
688
+ """
689
+ fields = fields or list(PII_PATTERNS.keys())
690
+ detections = []
691
+
692
+ def scan(value, path=""):
693
+ if isinstance(value, dict):
694
+ for k, v in value.items():
695
+ scan(v, f"{path}.{k}" if path else k)
696
+ elif isinstance(value, list):
697
+ for i, item in enumerate(value):
698
+ scan(item, f"{path}[{i}]")
699
+ elif isinstance(value, str):
700
+ for field in fields:
701
+ if field in PII_PATTERNS:
702
+ matches = re.findall(PII_PATTERNS[field], value)
703
+ for match in matches:
704
+ detections.append({
705
+ "type": field,
706
+ "path": path,
707
+ "sample": match[:4] + "..." if len(match) > 4 else match,
708
+ })
709
+
710
+ scan(data)
711
+ return detections