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,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registry Bridge Adapter
|
|
3
|
+
|
|
4
|
+
Bridges praisonaiagents.tools registry/lazy TOOL_MAPPINGS into
|
|
5
|
+
praisonai.mcp_server registry WITHOUT duplicating tool definitions
|
|
6
|
+
or importing tools eagerly.
|
|
7
|
+
|
|
8
|
+
This adapter:
|
|
9
|
+
- Enumerates available tools (metadata only)
|
|
10
|
+
- Loads tool handler lazily on first call
|
|
11
|
+
- Handles name collisions with clear errors
|
|
12
|
+
- Is optional and safe-by-default
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Track registered tools to avoid duplicates
|
|
21
|
+
_registered_tools: Set[str] = set()
|
|
22
|
+
_bridge_enabled: bool = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_bridge_available() -> bool:
|
|
26
|
+
"""Check if praisonaiagents.tools is available."""
|
|
27
|
+
try:
|
|
28
|
+
import importlib.util
|
|
29
|
+
spec = importlib.util.find_spec("praisonaiagents.tools")
|
|
30
|
+
return spec is not None
|
|
31
|
+
except (ImportError, ModuleNotFoundError):
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_tool_mappings() -> Dict[str, Any]:
|
|
36
|
+
"""Get TOOL_MAPPINGS from praisonaiagents.tools."""
|
|
37
|
+
try:
|
|
38
|
+
from praisonaiagents.tools import TOOL_MAPPINGS
|
|
39
|
+
return TOOL_MAPPINGS
|
|
40
|
+
except ImportError:
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _create_lazy_handler(module_path: str, class_name: Optional[str], tool_name: str) -> Callable:
|
|
45
|
+
"""
|
|
46
|
+
Create a lazy handler that imports the tool only when called.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
module_path: Python module path
|
|
50
|
+
class_name: Optional class name within module
|
|
51
|
+
tool_name: Tool name for error messages
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Callable that lazily loads and executes the tool
|
|
55
|
+
"""
|
|
56
|
+
_cached_handler = None
|
|
57
|
+
|
|
58
|
+
def lazy_handler(**kwargs) -> Any:
|
|
59
|
+
nonlocal _cached_handler
|
|
60
|
+
|
|
61
|
+
if _cached_handler is None:
|
|
62
|
+
try:
|
|
63
|
+
import importlib
|
|
64
|
+
mod = importlib.import_module(module_path)
|
|
65
|
+
|
|
66
|
+
if class_name:
|
|
67
|
+
tool_class = getattr(mod, class_name)
|
|
68
|
+
tool_instance = tool_class()
|
|
69
|
+
if hasattr(tool_instance, 'run'):
|
|
70
|
+
_cached_handler = tool_instance.run
|
|
71
|
+
elif hasattr(tool_instance, '__call__'):
|
|
72
|
+
_cached_handler = tool_instance
|
|
73
|
+
else:
|
|
74
|
+
raise AttributeError(f"Tool class {class_name} has no run or __call__ method")
|
|
75
|
+
else:
|
|
76
|
+
# Function-based tool
|
|
77
|
+
func_name = tool_name.split('.')[-1]
|
|
78
|
+
if hasattr(mod, func_name):
|
|
79
|
+
_cached_handler = getattr(mod, func_name)
|
|
80
|
+
elif hasattr(mod, 'run'):
|
|
81
|
+
_cached_handler = mod.run
|
|
82
|
+
else:
|
|
83
|
+
raise AttributeError(f"Module {module_path} has no {func_name} or run function")
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"Failed to load tool {tool_name}: {e}")
|
|
87
|
+
raise RuntimeError(f"Tool {tool_name} failed to load: {e}")
|
|
88
|
+
|
|
89
|
+
return _cached_handler(**kwargs)
|
|
90
|
+
|
|
91
|
+
return lazy_handler
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _infer_tool_hints(tool_name: str) -> Dict[str, bool]:
|
|
95
|
+
"""
|
|
96
|
+
Infer tool annotation hints from tool name/category.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
tool_name: Full tool name (e.g., "praisonai.memory.show")
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict with readOnlyHint, destructiveHint, idempotentHint, openWorldHint
|
|
103
|
+
"""
|
|
104
|
+
name_lower = tool_name.lower()
|
|
105
|
+
|
|
106
|
+
# Default hints per MCP 2025-11-25 spec
|
|
107
|
+
hints = {
|
|
108
|
+
"read_only_hint": False,
|
|
109
|
+
"destructive_hint": True,
|
|
110
|
+
"idempotent_hint": False,
|
|
111
|
+
"open_world_hint": True,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Read-only patterns
|
|
115
|
+
read_only_patterns = ['show', 'list', 'get', 'read', 'search', 'find', 'query', 'info', 'status']
|
|
116
|
+
for pattern in read_only_patterns:
|
|
117
|
+
if pattern in name_lower:
|
|
118
|
+
hints["read_only_hint"] = True
|
|
119
|
+
hints["destructive_hint"] = False
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
# Idempotent patterns
|
|
123
|
+
idempotent_patterns = ['set', 'update', 'configure']
|
|
124
|
+
for pattern in idempotent_patterns:
|
|
125
|
+
if pattern in name_lower:
|
|
126
|
+
hints["idempotent_hint"] = True
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
# Closed-world patterns (internal tools)
|
|
130
|
+
closed_world_patterns = ['memory', 'session', 'config', 'local']
|
|
131
|
+
for pattern in closed_world_patterns:
|
|
132
|
+
if pattern in name_lower:
|
|
133
|
+
hints["open_world_hint"] = False
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
return hints
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _extract_category(tool_name: str) -> Optional[str]:
|
|
140
|
+
"""Extract category from tool name."""
|
|
141
|
+
parts = tool_name.split('.')
|
|
142
|
+
if len(parts) >= 2:
|
|
143
|
+
return parts[-2] # e.g., "praisonai.memory.show" -> "memory"
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def register_praisonai_tools(
|
|
148
|
+
namespace_prefix: str = "praisonai.agents.",
|
|
149
|
+
skip_on_collision: bool = True,
|
|
150
|
+
) -> int:
|
|
151
|
+
"""
|
|
152
|
+
Bridge praisonaiagents tools to MCP registry.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
namespace_prefix: Prefix to add to tool names to avoid collisions
|
|
156
|
+
skip_on_collision: If True, skip tools that already exist; if False, raise error
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Number of tools registered
|
|
160
|
+
"""
|
|
161
|
+
global _bridge_enabled
|
|
162
|
+
|
|
163
|
+
if not is_bridge_available():
|
|
164
|
+
logger.debug("praisonaiagents.tools not available, skipping bridge")
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
from ..registry import get_tool_registry, MCPToolDefinition
|
|
168
|
+
|
|
169
|
+
registry = get_tool_registry()
|
|
170
|
+
tool_mappings = get_tool_mappings()
|
|
171
|
+
|
|
172
|
+
registered_count = 0
|
|
173
|
+
|
|
174
|
+
for tool_name, mapping_info in tool_mappings.items():
|
|
175
|
+
# Handle different mapping formats
|
|
176
|
+
if isinstance(mapping_info, tuple):
|
|
177
|
+
if len(mapping_info) >= 2:
|
|
178
|
+
module_path, class_name = mapping_info[0], mapping_info[1]
|
|
179
|
+
else:
|
|
180
|
+
module_path, class_name = mapping_info[0], None
|
|
181
|
+
elif isinstance(mapping_info, str):
|
|
182
|
+
module_path, class_name = mapping_info, None
|
|
183
|
+
else:
|
|
184
|
+
logger.warning(f"Unknown mapping format for {tool_name}: {type(mapping_info)}")
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
# Create namespaced name
|
|
188
|
+
full_name = f"{namespace_prefix}{tool_name}"
|
|
189
|
+
|
|
190
|
+
# Check for collision
|
|
191
|
+
if full_name in _registered_tools:
|
|
192
|
+
if skip_on_collision:
|
|
193
|
+
logger.debug(f"Skipping duplicate tool: {full_name}")
|
|
194
|
+
continue
|
|
195
|
+
else:
|
|
196
|
+
raise ValueError(f"Tool name collision: {full_name}")
|
|
197
|
+
|
|
198
|
+
# Check if already in registry
|
|
199
|
+
existing = registry.get(full_name)
|
|
200
|
+
if existing is not None:
|
|
201
|
+
if skip_on_collision:
|
|
202
|
+
logger.debug(f"Tool already registered: {full_name}")
|
|
203
|
+
continue
|
|
204
|
+
else:
|
|
205
|
+
raise ValueError(f"Tool already in registry: {full_name}")
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
# Create lazy handler
|
|
209
|
+
handler = _create_lazy_handler(module_path, class_name, tool_name)
|
|
210
|
+
|
|
211
|
+
# Infer hints
|
|
212
|
+
hints = _infer_tool_hints(tool_name)
|
|
213
|
+
category = _extract_category(tool_name)
|
|
214
|
+
|
|
215
|
+
# Create tool definition
|
|
216
|
+
tool_def = MCPToolDefinition(
|
|
217
|
+
name=full_name,
|
|
218
|
+
description=f"PraisonAI Agents tool: {tool_name}",
|
|
219
|
+
handler=handler,
|
|
220
|
+
input_schema={"type": "object", "properties": {}}, # Will be refined on first call
|
|
221
|
+
category=category,
|
|
222
|
+
**hints,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Register directly to avoid re-processing
|
|
226
|
+
registry._tools[full_name] = tool_def
|
|
227
|
+
_registered_tools.add(full_name)
|
|
228
|
+
registered_count += 1
|
|
229
|
+
|
|
230
|
+
logger.debug(f"Registered bridged tool: {full_name}")
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.warning(f"Failed to register tool {tool_name}: {e}")
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
_bridge_enabled = True
|
|
237
|
+
logger.info(f"Registry bridge registered {registered_count} tools from praisonaiagents")
|
|
238
|
+
|
|
239
|
+
return registered_count
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def unregister_bridged_tools() -> int:
|
|
243
|
+
"""
|
|
244
|
+
Remove all bridged tools from the registry.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Number of tools removed
|
|
248
|
+
"""
|
|
249
|
+
global _bridge_enabled
|
|
250
|
+
|
|
251
|
+
from ..registry import get_tool_registry
|
|
252
|
+
|
|
253
|
+
registry = get_tool_registry()
|
|
254
|
+
removed_count = 0
|
|
255
|
+
|
|
256
|
+
for tool_name in list(_registered_tools):
|
|
257
|
+
if tool_name in registry._tools:
|
|
258
|
+
del registry._tools[tool_name]
|
|
259
|
+
removed_count += 1
|
|
260
|
+
|
|
261
|
+
_registered_tools.clear()
|
|
262
|
+
_bridge_enabled = False
|
|
263
|
+
|
|
264
|
+
logger.info(f"Removed {removed_count} bridged tools")
|
|
265
|
+
return removed_count
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def is_bridge_enabled() -> bool:
|
|
269
|
+
"""Check if the bridge is currently enabled."""
|
|
270
|
+
return _bridge_enabled
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def get_bridged_tool_count() -> int:
|
|
274
|
+
"""Get the number of bridged tools."""
|
|
275
|
+
return len(_registered_tools)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def list_bridged_tools() -> List[str]:
|
|
279
|
+
"""List all bridged tool names."""
|
|
280
|
+
return list(_registered_tools)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Authentication and Authorization Module
|
|
3
|
+
|
|
4
|
+
Implements OAuth 2.1 and OpenID Connect support per MCP 2025-11-25 specification.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- API Key authentication
|
|
8
|
+
- OAuth 2.1 authorization framework
|
|
9
|
+
- OpenID Connect Discovery
|
|
10
|
+
- Client ID Metadata Documents
|
|
11
|
+
- Incremental scope handling
|
|
12
|
+
- WWW-Authenticate challenges
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .oauth import OAuthConfig, OAuthManager
|
|
19
|
+
from .oidc import OIDCDiscovery, OIDCConfig
|
|
20
|
+
from .api_key import APIKeyAuth
|
|
21
|
+
from .scopes import ScopeManager, ScopeChallenge
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"OAuthConfig",
|
|
25
|
+
"OAuthManager",
|
|
26
|
+
"OIDCDiscovery",
|
|
27
|
+
"OIDCConfig",
|
|
28
|
+
"APIKeyAuth",
|
|
29
|
+
"ScopeManager",
|
|
30
|
+
"ScopeChallenge",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __getattr__(name: str):
|
|
35
|
+
"""Lazy load auth components."""
|
|
36
|
+
if name in ("OAuthConfig", "OAuthManager"):
|
|
37
|
+
from .oauth import OAuthConfig, OAuthManager
|
|
38
|
+
return OAuthConfig if name == "OAuthConfig" else OAuthManager
|
|
39
|
+
elif name in ("OIDCDiscovery", "OIDCConfig"):
|
|
40
|
+
from .oidc import OIDCDiscovery, OIDCConfig
|
|
41
|
+
return OIDCDiscovery if name == "OIDCDiscovery" else OIDCConfig
|
|
42
|
+
elif name == "APIKeyAuth":
|
|
43
|
+
from .api_key import APIKeyAuth
|
|
44
|
+
return APIKeyAuth
|
|
45
|
+
elif name in ("ScopeManager", "ScopeChallenge"):
|
|
46
|
+
from .scopes import ScopeManager, ScopeChallenge
|
|
47
|
+
return ScopeManager if name == "ScopeManager" else ScopeChallenge
|
|
48
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Key Authentication for MCP
|
|
3
|
+
|
|
4
|
+
Simple API key authentication for MCP servers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import hmac
|
|
9
|
+
import logging
|
|
10
|
+
import secrets
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class APIKey:
|
|
20
|
+
"""API Key representation."""
|
|
21
|
+
key_id: str
|
|
22
|
+
key_hash: str
|
|
23
|
+
name: Optional[str] = None
|
|
24
|
+
scopes: List[str] = field(default_factory=list)
|
|
25
|
+
created_at: float = field(default_factory=time.time)
|
|
26
|
+
expires_at: Optional[float] = None
|
|
27
|
+
last_used_at: Optional[float] = None
|
|
28
|
+
metadata: Dict[str, str] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
def is_expired(self) -> bool:
|
|
31
|
+
if self.expires_at is None:
|
|
32
|
+
return False
|
|
33
|
+
return time.time() >= self.expires_at
|
|
34
|
+
|
|
35
|
+
def has_scope(self, scope: str) -> bool:
|
|
36
|
+
if not self.scopes:
|
|
37
|
+
return True # No scope restriction
|
|
38
|
+
return scope in self.scopes or "*" in self.scopes
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class APIKeyAuth:
|
|
42
|
+
"""
|
|
43
|
+
API Key authentication manager.
|
|
44
|
+
|
|
45
|
+
Supports:
|
|
46
|
+
- Key generation and validation
|
|
47
|
+
- Scope-based authorization
|
|
48
|
+
- Key rotation
|
|
49
|
+
- Rate limiting (optional)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
keys: Optional[Dict[str, APIKey]] = None,
|
|
55
|
+
allow_env_key: bool = True,
|
|
56
|
+
env_key_name: str = "MCP_API_KEY",
|
|
57
|
+
):
|
|
58
|
+
"""
|
|
59
|
+
Initialize API key auth.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
keys: Pre-configured API keys
|
|
63
|
+
allow_env_key: Allow API key from environment
|
|
64
|
+
env_key_name: Environment variable name for API key
|
|
65
|
+
"""
|
|
66
|
+
self._keys: Dict[str, APIKey] = keys or {}
|
|
67
|
+
self._allow_env_key = allow_env_key
|
|
68
|
+
self._env_key_name = env_key_name
|
|
69
|
+
self._env_key_hash: Optional[str] = None
|
|
70
|
+
|
|
71
|
+
# Load env key if configured
|
|
72
|
+
if allow_env_key:
|
|
73
|
+
self._load_env_key()
|
|
74
|
+
|
|
75
|
+
def _load_env_key(self) -> None:
|
|
76
|
+
"""Load API key from environment."""
|
|
77
|
+
import os
|
|
78
|
+
env_key = os.environ.get(self._env_key_name)
|
|
79
|
+
if env_key:
|
|
80
|
+
self._env_key_hash = self._hash_key(env_key)
|
|
81
|
+
logger.debug(f"Loaded API key from {self._env_key_name}")
|
|
82
|
+
|
|
83
|
+
def _hash_key(self, key: str) -> str:
|
|
84
|
+
"""Hash an API key for storage."""
|
|
85
|
+
return hashlib.sha256(key.encode()).hexdigest()
|
|
86
|
+
|
|
87
|
+
def generate_key(
|
|
88
|
+
self,
|
|
89
|
+
name: Optional[str] = None,
|
|
90
|
+
scopes: Optional[List[str]] = None,
|
|
91
|
+
expires_in: Optional[int] = None,
|
|
92
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
93
|
+
) -> tuple[str, APIKey]:
|
|
94
|
+
"""
|
|
95
|
+
Generate a new API key.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
name: Key name/description
|
|
99
|
+
scopes: Allowed scopes
|
|
100
|
+
expires_in: Expiration in seconds
|
|
101
|
+
metadata: Additional metadata
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Tuple of (raw_key, APIKey)
|
|
105
|
+
"""
|
|
106
|
+
# Generate key
|
|
107
|
+
raw_key = f"mcp_{secrets.token_urlsafe(32)}"
|
|
108
|
+
key_id = secrets.token_hex(8)
|
|
109
|
+
key_hash = self._hash_key(raw_key)
|
|
110
|
+
|
|
111
|
+
expires_at = None
|
|
112
|
+
if expires_in:
|
|
113
|
+
expires_at = time.time() + expires_in
|
|
114
|
+
|
|
115
|
+
api_key = APIKey(
|
|
116
|
+
key_id=key_id,
|
|
117
|
+
key_hash=key_hash,
|
|
118
|
+
name=name,
|
|
119
|
+
scopes=scopes or [],
|
|
120
|
+
expires_at=expires_at,
|
|
121
|
+
metadata=metadata or {},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
self._keys[key_id] = api_key
|
|
125
|
+
|
|
126
|
+
return raw_key, api_key
|
|
127
|
+
|
|
128
|
+
def validate(
|
|
129
|
+
self,
|
|
130
|
+
key: str,
|
|
131
|
+
required_scope: Optional[str] = None,
|
|
132
|
+
) -> tuple[bool, Optional[APIKey]]:
|
|
133
|
+
"""
|
|
134
|
+
Validate an API key.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
key: Raw API key
|
|
138
|
+
required_scope: Required scope for authorization
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Tuple of (is_valid, api_key)
|
|
142
|
+
"""
|
|
143
|
+
key_hash = self._hash_key(key)
|
|
144
|
+
|
|
145
|
+
# Check environment key
|
|
146
|
+
if self._env_key_hash and hmac.compare_digest(key_hash, self._env_key_hash):
|
|
147
|
+
# Env key has all scopes
|
|
148
|
+
return True, None
|
|
149
|
+
|
|
150
|
+
# Check stored keys
|
|
151
|
+
for api_key in self._keys.values():
|
|
152
|
+
if hmac.compare_digest(key_hash, api_key.key_hash):
|
|
153
|
+
if api_key.is_expired():
|
|
154
|
+
return False, None
|
|
155
|
+
|
|
156
|
+
if required_scope and not api_key.has_scope(required_scope):
|
|
157
|
+
return False, api_key
|
|
158
|
+
|
|
159
|
+
# Update last used
|
|
160
|
+
api_key.last_used_at = time.time()
|
|
161
|
+
|
|
162
|
+
return True, api_key
|
|
163
|
+
|
|
164
|
+
return False, None
|
|
165
|
+
|
|
166
|
+
def validate_header(
|
|
167
|
+
self,
|
|
168
|
+
auth_header: str,
|
|
169
|
+
required_scope: Optional[str] = None,
|
|
170
|
+
) -> tuple[bool, Optional[APIKey]]:
|
|
171
|
+
"""
|
|
172
|
+
Validate Authorization header.
|
|
173
|
+
|
|
174
|
+
Supports:
|
|
175
|
+
- Bearer <key>
|
|
176
|
+
- ApiKey <key>
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
auth_header: Authorization header value
|
|
180
|
+
required_scope: Required scope
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Tuple of (is_valid, api_key)
|
|
184
|
+
"""
|
|
185
|
+
if not auth_header:
|
|
186
|
+
return False, None
|
|
187
|
+
|
|
188
|
+
parts = auth_header.split(" ", 1)
|
|
189
|
+
if len(parts) != 2:
|
|
190
|
+
return False, None
|
|
191
|
+
|
|
192
|
+
scheme, key = parts
|
|
193
|
+
if scheme.lower() not in ("bearer", "apikey"):
|
|
194
|
+
return False, None
|
|
195
|
+
|
|
196
|
+
return self.validate(key, required_scope)
|
|
197
|
+
|
|
198
|
+
def revoke(self, key_id: str) -> bool:
|
|
199
|
+
"""Revoke an API key."""
|
|
200
|
+
if key_id in self._keys:
|
|
201
|
+
del self._keys[key_id]
|
|
202
|
+
return True
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
def list_keys(self) -> List[APIKey]:
|
|
206
|
+
"""List all API keys (without hashes)."""
|
|
207
|
+
return list(self._keys.values())
|
|
208
|
+
|
|
209
|
+
def add_key(
|
|
210
|
+
self,
|
|
211
|
+
raw_key: str,
|
|
212
|
+
name: Optional[str] = None,
|
|
213
|
+
scopes: Optional[List[str]] = None,
|
|
214
|
+
) -> APIKey:
|
|
215
|
+
"""
|
|
216
|
+
Add a pre-existing API key.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
raw_key: Raw API key
|
|
220
|
+
name: Key name
|
|
221
|
+
scopes: Allowed scopes
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
APIKey instance
|
|
225
|
+
"""
|
|
226
|
+
key_id = secrets.token_hex(8)
|
|
227
|
+
key_hash = self._hash_key(raw_key)
|
|
228
|
+
|
|
229
|
+
api_key = APIKey(
|
|
230
|
+
key_id=key_id,
|
|
231
|
+
key_hash=key_hash,
|
|
232
|
+
name=name,
|
|
233
|
+
scopes=scopes or [],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
self._keys[key_id] = api_key
|
|
237
|
+
return api_key
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def create_auth_middleware(
|
|
241
|
+
api_key_auth: APIKeyAuth,
|
|
242
|
+
required_scope: Optional[str] = None,
|
|
243
|
+
exclude_paths: Optional[List[str]] = None,
|
|
244
|
+
):
|
|
245
|
+
"""
|
|
246
|
+
Create authentication middleware for Starlette/FastAPI.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
api_key_auth: APIKeyAuth instance
|
|
250
|
+
required_scope: Required scope for all requests
|
|
251
|
+
exclude_paths: Paths to exclude from auth
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Middleware class
|
|
255
|
+
"""
|
|
256
|
+
exclude_paths = exclude_paths or ["/health", "/"]
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
260
|
+
from starlette.requests import Request
|
|
261
|
+
from starlette.responses import JSONResponse
|
|
262
|
+
except ImportError:
|
|
263
|
+
raise ImportError("starlette required for middleware")
|
|
264
|
+
|
|
265
|
+
class AuthMiddleware(BaseHTTPMiddleware):
|
|
266
|
+
async def dispatch(self, request: Request, call_next):
|
|
267
|
+
# Skip excluded paths
|
|
268
|
+
if request.url.path in exclude_paths:
|
|
269
|
+
return await call_next(request)
|
|
270
|
+
|
|
271
|
+
# Get auth header
|
|
272
|
+
auth_header = request.headers.get("Authorization", "")
|
|
273
|
+
|
|
274
|
+
# Validate
|
|
275
|
+
is_valid, api_key = api_key_auth.validate_header(
|
|
276
|
+
auth_header, required_scope
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if not is_valid:
|
|
280
|
+
return JSONResponse(
|
|
281
|
+
{"error": "Unauthorized"},
|
|
282
|
+
status_code=401,
|
|
283
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Add key info to request state
|
|
287
|
+
request.state.api_key = api_key
|
|
288
|
+
|
|
289
|
+
return await call_next(request)
|
|
290
|
+
|
|
291
|
+
return AuthMiddleware
|