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.
- praisonai/__init__.py +54 -0
- praisonai/__main__.py +15 -0
- praisonai/acp/__init__.py +54 -0
- praisonai/acp/config.py +159 -0
- praisonai/acp/server.py +587 -0
- praisonai/acp/session.py +219 -0
- praisonai/adapters/__init__.py +50 -0
- praisonai/adapters/readers.py +395 -0
- praisonai/adapters/rerankers.py +315 -0
- praisonai/adapters/retrievers.py +394 -0
- praisonai/adapters/vector_stores.py +409 -0
- praisonai/agent_scheduler.py +337 -0
- praisonai/agents_generator.py +903 -0
- praisonai/api/call.py +292 -0
- praisonai/auto.py +1197 -0
- praisonai/capabilities/__init__.py +275 -0
- praisonai/capabilities/a2a.py +140 -0
- praisonai/capabilities/assistants.py +283 -0
- praisonai/capabilities/audio.py +320 -0
- praisonai/capabilities/batches.py +469 -0
- praisonai/capabilities/completions.py +336 -0
- praisonai/capabilities/container_files.py +155 -0
- praisonai/capabilities/containers.py +93 -0
- praisonai/capabilities/embeddings.py +158 -0
- praisonai/capabilities/files.py +467 -0
- praisonai/capabilities/fine_tuning.py +293 -0
- praisonai/capabilities/guardrails.py +182 -0
- praisonai/capabilities/images.py +330 -0
- praisonai/capabilities/mcp.py +190 -0
- praisonai/capabilities/messages.py +270 -0
- praisonai/capabilities/moderations.py +154 -0
- praisonai/capabilities/ocr.py +217 -0
- praisonai/capabilities/passthrough.py +204 -0
- praisonai/capabilities/rag.py +207 -0
- praisonai/capabilities/realtime.py +160 -0
- praisonai/capabilities/rerank.py +165 -0
- praisonai/capabilities/responses.py +266 -0
- praisonai/capabilities/search.py +109 -0
- praisonai/capabilities/skills.py +133 -0
- praisonai/capabilities/vector_store_files.py +334 -0
- praisonai/capabilities/vector_stores.py +304 -0
- praisonai/capabilities/videos.py +141 -0
- praisonai/chainlit_ui.py +304 -0
- praisonai/chat/__init__.py +106 -0
- praisonai/chat/app.py +125 -0
- praisonai/cli/__init__.py +26 -0
- praisonai/cli/app.py +213 -0
- praisonai/cli/commands/__init__.py +75 -0
- praisonai/cli/commands/acp.py +70 -0
- praisonai/cli/commands/completion.py +333 -0
- praisonai/cli/commands/config.py +166 -0
- praisonai/cli/commands/debug.py +142 -0
- praisonai/cli/commands/diag.py +55 -0
- praisonai/cli/commands/doctor.py +166 -0
- praisonai/cli/commands/environment.py +179 -0
- praisonai/cli/commands/lsp.py +112 -0
- praisonai/cli/commands/mcp.py +210 -0
- praisonai/cli/commands/profile.py +457 -0
- praisonai/cli/commands/run.py +228 -0
- praisonai/cli/commands/schedule.py +150 -0
- praisonai/cli/commands/serve.py +97 -0
- praisonai/cli/commands/session.py +212 -0
- praisonai/cli/commands/traces.py +145 -0
- praisonai/cli/commands/version.py +101 -0
- praisonai/cli/configuration/__init__.py +18 -0
- praisonai/cli/configuration/loader.py +353 -0
- praisonai/cli/configuration/paths.py +114 -0
- praisonai/cli/configuration/schema.py +164 -0
- praisonai/cli/features/__init__.py +268 -0
- praisonai/cli/features/acp.py +236 -0
- praisonai/cli/features/action_orchestrator.py +546 -0
- praisonai/cli/features/agent_scheduler.py +773 -0
- praisonai/cli/features/agent_tools.py +474 -0
- praisonai/cli/features/agents.py +375 -0
- praisonai/cli/features/at_mentions.py +471 -0
- praisonai/cli/features/auto_memory.py +182 -0
- praisonai/cli/features/autonomy_mode.py +490 -0
- praisonai/cli/features/background.py +356 -0
- praisonai/cli/features/base.py +168 -0
- praisonai/cli/features/capabilities.py +1326 -0
- praisonai/cli/features/checkpoints.py +338 -0
- praisonai/cli/features/code_intelligence.py +652 -0
- praisonai/cli/features/compaction.py +294 -0
- praisonai/cli/features/compare.py +534 -0
- praisonai/cli/features/cost_tracker.py +514 -0
- praisonai/cli/features/debug.py +810 -0
- praisonai/cli/features/deploy.py +517 -0
- praisonai/cli/features/diag.py +289 -0
- praisonai/cli/features/doctor/__init__.py +63 -0
- praisonai/cli/features/doctor/checks/__init__.py +24 -0
- praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
- praisonai/cli/features/doctor/checks/config_checks.py +366 -0
- praisonai/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai/cli/features/doctor/checks/env_checks.py +543 -0
- praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
- praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
- praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
- praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai/cli/features/doctor/engine.py +266 -0
- praisonai/cli/features/doctor/formatters.py +310 -0
- praisonai/cli/features/doctor/handler.py +397 -0
- praisonai/cli/features/doctor/models.py +264 -0
- praisonai/cli/features/doctor/registry.py +239 -0
- praisonai/cli/features/endpoints.py +1019 -0
- praisonai/cli/features/eval.py +560 -0
- praisonai/cli/features/external_agents.py +231 -0
- praisonai/cli/features/fast_context.py +410 -0
- praisonai/cli/features/flow_display.py +566 -0
- praisonai/cli/features/git_integration.py +651 -0
- praisonai/cli/features/guardrail.py +171 -0
- praisonai/cli/features/handoff.py +185 -0
- praisonai/cli/features/hooks.py +583 -0
- praisonai/cli/features/image.py +384 -0
- praisonai/cli/features/interactive_runtime.py +585 -0
- praisonai/cli/features/interactive_tools.py +380 -0
- praisonai/cli/features/interactive_tui.py +603 -0
- praisonai/cli/features/jobs.py +632 -0
- praisonai/cli/features/knowledge.py +531 -0
- praisonai/cli/features/lite.py +244 -0
- praisonai/cli/features/lsp_cli.py +225 -0
- praisonai/cli/features/mcp.py +169 -0
- praisonai/cli/features/message_queue.py +587 -0
- praisonai/cli/features/metrics.py +211 -0
- praisonai/cli/features/n8n.py +673 -0
- praisonai/cli/features/observability.py +293 -0
- praisonai/cli/features/ollama.py +361 -0
- praisonai/cli/features/output_style.py +273 -0
- praisonai/cli/features/package.py +631 -0
- praisonai/cli/features/performance.py +308 -0
- praisonai/cli/features/persistence.py +636 -0
- praisonai/cli/features/profile.py +226 -0
- praisonai/cli/features/profiler/__init__.py +81 -0
- praisonai/cli/features/profiler/core.py +558 -0
- praisonai/cli/features/profiler/optimizations.py +652 -0
- praisonai/cli/features/profiler/suite.py +386 -0
- praisonai/cli/features/profiling.py +350 -0
- praisonai/cli/features/queue/__init__.py +73 -0
- praisonai/cli/features/queue/manager.py +395 -0
- praisonai/cli/features/queue/models.py +286 -0
- praisonai/cli/features/queue/persistence.py +564 -0
- praisonai/cli/features/queue/scheduler.py +484 -0
- praisonai/cli/features/queue/worker.py +372 -0
- praisonai/cli/features/recipe.py +1723 -0
- praisonai/cli/features/recipes.py +449 -0
- praisonai/cli/features/registry.py +229 -0
- praisonai/cli/features/repo_map.py +860 -0
- praisonai/cli/features/router.py +466 -0
- praisonai/cli/features/sandbox_executor.py +515 -0
- praisonai/cli/features/serve.py +829 -0
- praisonai/cli/features/session.py +222 -0
- praisonai/cli/features/skills.py +856 -0
- praisonai/cli/features/slash_commands.py +650 -0
- praisonai/cli/features/telemetry.py +179 -0
- praisonai/cli/features/templates.py +1384 -0
- praisonai/cli/features/thinking.py +305 -0
- praisonai/cli/features/todo.py +334 -0
- praisonai/cli/features/tools.py +680 -0
- praisonai/cli/features/tui/__init__.py +83 -0
- praisonai/cli/features/tui/app.py +580 -0
- praisonai/cli/features/tui/cli.py +566 -0
- praisonai/cli/features/tui/debug.py +511 -0
- praisonai/cli/features/tui/events.py +99 -0
- praisonai/cli/features/tui/mock_provider.py +328 -0
- praisonai/cli/features/tui/orchestrator.py +652 -0
- praisonai/cli/features/tui/screens/__init__.py +50 -0
- praisonai/cli/features/tui/screens/main.py +245 -0
- praisonai/cli/features/tui/screens/queue.py +174 -0
- praisonai/cli/features/tui/screens/session.py +124 -0
- praisonai/cli/features/tui/screens/settings.py +148 -0
- praisonai/cli/features/tui/widgets/__init__.py +56 -0
- praisonai/cli/features/tui/widgets/chat.py +261 -0
- praisonai/cli/features/tui/widgets/composer.py +224 -0
- praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
- praisonai/cli/features/tui/widgets/status.py +167 -0
- praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
- praisonai/cli/features/workflow.py +720 -0
- praisonai/cli/legacy.py +236 -0
- praisonai/cli/main.py +5559 -0
- praisonai/cli/schedule_cli.py +54 -0
- praisonai/cli/state/__init__.py +31 -0
- praisonai/cli/state/identifiers.py +161 -0
- praisonai/cli/state/sessions.py +313 -0
- praisonai/code/__init__.py +93 -0
- praisonai/code/agent_tools.py +344 -0
- praisonai/code/diff/__init__.py +21 -0
- praisonai/code/diff/diff_strategy.py +432 -0
- praisonai/code/tools/__init__.py +27 -0
- praisonai/code/tools/apply_diff.py +221 -0
- praisonai/code/tools/execute_command.py +275 -0
- praisonai/code/tools/list_files.py +274 -0
- praisonai/code/tools/read_file.py +206 -0
- praisonai/code/tools/search_replace.py +248 -0
- praisonai/code/tools/write_file.py +217 -0
- praisonai/code/utils/__init__.py +46 -0
- praisonai/code/utils/file_utils.py +307 -0
- praisonai/code/utils/ignore_utils.py +308 -0
- praisonai/code/utils/text_utils.py +276 -0
- praisonai/db/__init__.py +64 -0
- praisonai/db/adapter.py +531 -0
- praisonai/deploy/__init__.py +62 -0
- praisonai/deploy/api.py +231 -0
- praisonai/deploy/docker.py +454 -0
- praisonai/deploy/doctor.py +367 -0
- praisonai/deploy/main.py +327 -0
- praisonai/deploy/models.py +179 -0
- praisonai/deploy/providers/__init__.py +33 -0
- praisonai/deploy/providers/aws.py +331 -0
- praisonai/deploy/providers/azure.py +358 -0
- praisonai/deploy/providers/base.py +101 -0
- praisonai/deploy/providers/gcp.py +314 -0
- praisonai/deploy/schema.py +208 -0
- praisonai/deploy.py +185 -0
- praisonai/endpoints/__init__.py +53 -0
- praisonai/endpoints/a2u_server.py +410 -0
- praisonai/endpoints/discovery.py +165 -0
- praisonai/endpoints/providers/__init__.py +28 -0
- praisonai/endpoints/providers/a2a.py +253 -0
- praisonai/endpoints/providers/a2u.py +208 -0
- praisonai/endpoints/providers/agents_api.py +171 -0
- praisonai/endpoints/providers/base.py +231 -0
- praisonai/endpoints/providers/mcp.py +263 -0
- praisonai/endpoints/providers/recipe.py +206 -0
- praisonai/endpoints/providers/tools_mcp.py +150 -0
- praisonai/endpoints/registry.py +131 -0
- praisonai/endpoints/server.py +161 -0
- praisonai/inbuilt_tools/__init__.py +24 -0
- praisonai/inbuilt_tools/autogen_tools.py +117 -0
- praisonai/inc/__init__.py +2 -0
- praisonai/inc/config.py +96 -0
- praisonai/inc/models.py +155 -0
- praisonai/integrations/__init__.py +56 -0
- praisonai/integrations/base.py +303 -0
- praisonai/integrations/claude_code.py +270 -0
- praisonai/integrations/codex_cli.py +255 -0
- praisonai/integrations/cursor_cli.py +195 -0
- praisonai/integrations/gemini_cli.py +222 -0
- praisonai/jobs/__init__.py +67 -0
- praisonai/jobs/executor.py +425 -0
- praisonai/jobs/models.py +230 -0
- praisonai/jobs/router.py +314 -0
- praisonai/jobs/server.py +186 -0
- praisonai/jobs/store.py +203 -0
- praisonai/llm/__init__.py +66 -0
- praisonai/llm/registry.py +382 -0
- praisonai/mcp_server/__init__.py +152 -0
- praisonai/mcp_server/adapters/__init__.py +74 -0
- praisonai/mcp_server/adapters/agents.py +128 -0
- praisonai/mcp_server/adapters/capabilities.py +168 -0
- praisonai/mcp_server/adapters/cli_tools.py +568 -0
- praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
- praisonai/mcp_server/adapters/knowledge.py +93 -0
- praisonai/mcp_server/adapters/memory.py +104 -0
- praisonai/mcp_server/adapters/prompts.py +306 -0
- praisonai/mcp_server/adapters/resources.py +124 -0
- praisonai/mcp_server/adapters/tools_bridge.py +280 -0
- praisonai/mcp_server/auth/__init__.py +48 -0
- praisonai/mcp_server/auth/api_key.py +291 -0
- praisonai/mcp_server/auth/oauth.py +460 -0
- praisonai/mcp_server/auth/oidc.py +289 -0
- praisonai/mcp_server/auth/scopes.py +260 -0
- praisonai/mcp_server/cli.py +852 -0
- praisonai/mcp_server/elicitation.py +445 -0
- praisonai/mcp_server/icons.py +302 -0
- praisonai/mcp_server/recipe_adapter.py +573 -0
- praisonai/mcp_server/recipe_cli.py +824 -0
- praisonai/mcp_server/registry.py +703 -0
- praisonai/mcp_server/sampling.py +422 -0
- praisonai/mcp_server/server.py +490 -0
- praisonai/mcp_server/tasks.py +443 -0
- praisonai/mcp_server/transports/__init__.py +18 -0
- praisonai/mcp_server/transports/http_stream.py +376 -0
- praisonai/mcp_server/transports/stdio.py +132 -0
- praisonai/persistence/__init__.py +84 -0
- praisonai/persistence/config.py +238 -0
- praisonai/persistence/conversation/__init__.py +25 -0
- praisonai/persistence/conversation/async_mysql.py +427 -0
- praisonai/persistence/conversation/async_postgres.py +410 -0
- praisonai/persistence/conversation/async_sqlite.py +371 -0
- praisonai/persistence/conversation/base.py +151 -0
- praisonai/persistence/conversation/json_store.py +250 -0
- praisonai/persistence/conversation/mysql.py +387 -0
- praisonai/persistence/conversation/postgres.py +401 -0
- praisonai/persistence/conversation/singlestore.py +240 -0
- praisonai/persistence/conversation/sqlite.py +341 -0
- praisonai/persistence/conversation/supabase.py +203 -0
- praisonai/persistence/conversation/surrealdb.py +287 -0
- praisonai/persistence/factory.py +301 -0
- praisonai/persistence/hooks/__init__.py +18 -0
- praisonai/persistence/hooks/agent_hooks.py +297 -0
- praisonai/persistence/knowledge/__init__.py +26 -0
- praisonai/persistence/knowledge/base.py +144 -0
- praisonai/persistence/knowledge/cassandra.py +232 -0
- praisonai/persistence/knowledge/chroma.py +295 -0
- praisonai/persistence/knowledge/clickhouse.py +242 -0
- praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
- praisonai/persistence/knowledge/couchbase.py +286 -0
- praisonai/persistence/knowledge/lancedb.py +216 -0
- praisonai/persistence/knowledge/langchain_adapter.py +291 -0
- praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
- praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
- praisonai/persistence/knowledge/milvus.py +277 -0
- praisonai/persistence/knowledge/mongodb_vector.py +306 -0
- praisonai/persistence/knowledge/pgvector.py +335 -0
- praisonai/persistence/knowledge/pinecone.py +253 -0
- praisonai/persistence/knowledge/qdrant.py +301 -0
- praisonai/persistence/knowledge/redis_vector.py +291 -0
- praisonai/persistence/knowledge/singlestore_vector.py +299 -0
- praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
- praisonai/persistence/knowledge/upstash_vector.py +266 -0
- praisonai/persistence/knowledge/weaviate.py +223 -0
- praisonai/persistence/migrations/__init__.py +10 -0
- praisonai/persistence/migrations/manager.py +251 -0
- praisonai/persistence/orchestrator.py +406 -0
- praisonai/persistence/state/__init__.py +21 -0
- praisonai/persistence/state/async_mongodb.py +200 -0
- praisonai/persistence/state/base.py +107 -0
- praisonai/persistence/state/dynamodb.py +226 -0
- praisonai/persistence/state/firestore.py +175 -0
- praisonai/persistence/state/gcs.py +155 -0
- praisonai/persistence/state/memory.py +245 -0
- praisonai/persistence/state/mongodb.py +158 -0
- praisonai/persistence/state/redis.py +190 -0
- praisonai/persistence/state/upstash.py +144 -0
- praisonai/persistence/tests/__init__.py +3 -0
- praisonai/persistence/tests/test_all_backends.py +633 -0
- praisonai/profiler.py +1214 -0
- praisonai/recipe/__init__.py +134 -0
- praisonai/recipe/bridge.py +278 -0
- praisonai/recipe/core.py +893 -0
- praisonai/recipe/exceptions.py +54 -0
- praisonai/recipe/history.py +402 -0
- praisonai/recipe/models.py +266 -0
- praisonai/recipe/operations.py +440 -0
- praisonai/recipe/policy.py +422 -0
- praisonai/recipe/registry.py +849 -0
- praisonai/recipe/runtime.py +214 -0
- praisonai/recipe/security.py +711 -0
- praisonai/recipe/serve.py +859 -0
- praisonai/recipe/server.py +613 -0
- praisonai/scheduler/__init__.py +45 -0
- praisonai/scheduler/agent_scheduler.py +552 -0
- praisonai/scheduler/base.py +124 -0
- praisonai/scheduler/daemon_manager.py +225 -0
- praisonai/scheduler/state_manager.py +155 -0
- praisonai/scheduler/yaml_loader.py +193 -0
- praisonai/scheduler.py +194 -0
- praisonai/setup/__init__.py +1 -0
- praisonai/setup/build.py +21 -0
- praisonai/setup/post_install.py +23 -0
- praisonai/setup/setup_conda_env.py +25 -0
- praisonai/setup.py +16 -0
- praisonai/templates/__init__.py +116 -0
- praisonai/templates/cache.py +364 -0
- praisonai/templates/dependency_checker.py +358 -0
- praisonai/templates/discovery.py +391 -0
- praisonai/templates/loader.py +564 -0
- praisonai/templates/registry.py +511 -0
- praisonai/templates/resolver.py +206 -0
- praisonai/templates/security.py +327 -0
- praisonai/templates/tool_override.py +498 -0
- praisonai/templates/tools_doctor.py +256 -0
- praisonai/test.py +105 -0
- praisonai/train.py +562 -0
- praisonai/train_vision.py +306 -0
- praisonai/ui/agents.py +824 -0
- praisonai/ui/callbacks.py +57 -0
- praisonai/ui/chainlit_compat.py +246 -0
- praisonai/ui/chat.py +532 -0
- praisonai/ui/code.py +717 -0
- praisonai/ui/colab.py +474 -0
- praisonai/ui/colab_chainlit.py +81 -0
- praisonai/ui/components/aicoder.py +284 -0
- praisonai/ui/context.py +283 -0
- praisonai/ui/database_config.py +56 -0
- praisonai/ui/db.py +294 -0
- praisonai/ui/realtime.py +488 -0
- praisonai/ui/realtimeclient/__init__.py +756 -0
- praisonai/ui/realtimeclient/tools.py +242 -0
- praisonai/ui/sql_alchemy.py +710 -0
- praisonai/upload_vision.py +140 -0
- praisonai/version.py +1 -0
- praisonai-3.0.0.dist-info/METADATA +3493 -0
- praisonai-3.0.0.dist-info/RECORD +393 -0
- praisonai-3.0.0.dist-info/WHEEL +5 -0
- praisonai-3.0.0.dist-info/entry_points.txt +4 -0
- 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)
|