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,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenID Connect Discovery Implementation
|
|
3
|
+
|
|
4
|
+
Implements OpenID Connect Discovery 1.0 per MCP 2025-11-25 specification.
|
|
5
|
+
|
|
6
|
+
Based on:
|
|
7
|
+
- OpenID Connect Discovery 1.0
|
|
8
|
+
- OAuth 2.0 Authorization Server Metadata (RFC8414)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class OIDCConfig:
|
|
21
|
+
"""OpenID Connect configuration."""
|
|
22
|
+
|
|
23
|
+
# Required endpoints
|
|
24
|
+
issuer: str
|
|
25
|
+
authorization_endpoint: str
|
|
26
|
+
token_endpoint: str
|
|
27
|
+
|
|
28
|
+
# Optional endpoints
|
|
29
|
+
userinfo_endpoint: Optional[str] = None
|
|
30
|
+
jwks_uri: Optional[str] = None
|
|
31
|
+
registration_endpoint: Optional[str] = None
|
|
32
|
+
revocation_endpoint: Optional[str] = None
|
|
33
|
+
introspection_endpoint: Optional[str] = None
|
|
34
|
+
end_session_endpoint: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
# Supported features
|
|
37
|
+
scopes_supported: List[str] = field(default_factory=list)
|
|
38
|
+
response_types_supported: List[str] = field(default_factory=list)
|
|
39
|
+
grant_types_supported: List[str] = field(default_factory=list)
|
|
40
|
+
subject_types_supported: List[str] = field(default_factory=list)
|
|
41
|
+
id_token_signing_alg_values_supported: List[str] = field(default_factory=list)
|
|
42
|
+
token_endpoint_auth_methods_supported: List[str] = field(default_factory=list)
|
|
43
|
+
claims_supported: List[str] = field(default_factory=list)
|
|
44
|
+
code_challenge_methods_supported: List[str] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
# Cache metadata
|
|
47
|
+
_fetched_at: Optional[float] = None
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
50
|
+
return {
|
|
51
|
+
"issuer": self.issuer,
|
|
52
|
+
"authorization_endpoint": self.authorization_endpoint,
|
|
53
|
+
"token_endpoint": self.token_endpoint,
|
|
54
|
+
"userinfo_endpoint": self.userinfo_endpoint,
|
|
55
|
+
"jwks_uri": self.jwks_uri,
|
|
56
|
+
"scopes_supported": self.scopes_supported,
|
|
57
|
+
"response_types_supported": self.response_types_supported,
|
|
58
|
+
"grant_types_supported": self.grant_types_supported,
|
|
59
|
+
"token_endpoint_auth_methods_supported": self.token_endpoint_auth_methods_supported,
|
|
60
|
+
"code_challenge_methods_supported": self.code_challenge_methods_supported,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_dict(cls, data: Dict[str, Any]) -> "OIDCConfig":
|
|
65
|
+
return cls(
|
|
66
|
+
issuer=data["issuer"],
|
|
67
|
+
authorization_endpoint=data["authorization_endpoint"],
|
|
68
|
+
token_endpoint=data["token_endpoint"],
|
|
69
|
+
userinfo_endpoint=data.get("userinfo_endpoint"),
|
|
70
|
+
jwks_uri=data.get("jwks_uri"),
|
|
71
|
+
registration_endpoint=data.get("registration_endpoint"),
|
|
72
|
+
revocation_endpoint=data.get("revocation_endpoint"),
|
|
73
|
+
introspection_endpoint=data.get("introspection_endpoint"),
|
|
74
|
+
end_session_endpoint=data.get("end_session_endpoint"),
|
|
75
|
+
scopes_supported=data.get("scopes_supported", []),
|
|
76
|
+
response_types_supported=data.get("response_types_supported", []),
|
|
77
|
+
grant_types_supported=data.get("grant_types_supported", []),
|
|
78
|
+
subject_types_supported=data.get("subject_types_supported", []),
|
|
79
|
+
id_token_signing_alg_values_supported=data.get("id_token_signing_alg_values_supported", []),
|
|
80
|
+
token_endpoint_auth_methods_supported=data.get("token_endpoint_auth_methods_supported", []),
|
|
81
|
+
claims_supported=data.get("claims_supported", []),
|
|
82
|
+
code_challenge_methods_supported=data.get("code_challenge_methods_supported", []),
|
|
83
|
+
_fetched_at=time.time(),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class OIDCDiscovery:
|
|
88
|
+
"""
|
|
89
|
+
OpenID Connect Discovery client.
|
|
90
|
+
|
|
91
|
+
Fetches and caches OIDC configuration from well-known endpoints.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
cache_ttl: int = 3600,
|
|
97
|
+
):
|
|
98
|
+
"""
|
|
99
|
+
Initialize OIDC discovery client.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
cache_ttl: Cache TTL in seconds
|
|
103
|
+
"""
|
|
104
|
+
self._cache: Dict[str, OIDCConfig] = {}
|
|
105
|
+
self._cache_ttl = cache_ttl
|
|
106
|
+
|
|
107
|
+
async def discover(self, issuer: str, force_refresh: bool = False) -> OIDCConfig:
|
|
108
|
+
"""
|
|
109
|
+
Discover OIDC configuration for an issuer.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
issuer: Issuer URL
|
|
113
|
+
force_refresh: Force cache refresh
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
OIDCConfig instance
|
|
117
|
+
"""
|
|
118
|
+
# Check cache
|
|
119
|
+
if not force_refresh and issuer in self._cache:
|
|
120
|
+
config = self._cache[issuer]
|
|
121
|
+
if config._fetched_at and time.time() - config._fetched_at < self._cache_ttl:
|
|
122
|
+
return config
|
|
123
|
+
|
|
124
|
+
# Fetch configuration
|
|
125
|
+
config = await self._fetch_config(issuer)
|
|
126
|
+
self._cache[issuer] = config
|
|
127
|
+
|
|
128
|
+
return config
|
|
129
|
+
|
|
130
|
+
async def _fetch_config(self, issuer: str) -> OIDCConfig:
|
|
131
|
+
"""Fetch OIDC configuration from well-known endpoint."""
|
|
132
|
+
try:
|
|
133
|
+
import httpx
|
|
134
|
+
except ImportError:
|
|
135
|
+
raise ImportError("httpx required for OIDC discovery. Install with: pip install httpx")
|
|
136
|
+
|
|
137
|
+
# Build well-known URL
|
|
138
|
+
discovery_url = f"{issuer.rstrip('/')}/.well-known/openid-configuration"
|
|
139
|
+
|
|
140
|
+
async with httpx.AsyncClient() as client:
|
|
141
|
+
response = await client.get(discovery_url, follow_redirects=True)
|
|
142
|
+
response.raise_for_status()
|
|
143
|
+
|
|
144
|
+
data = response.json()
|
|
145
|
+
|
|
146
|
+
# Validate issuer matches
|
|
147
|
+
if data.get("issuer") != issuer and data.get("issuer") != issuer.rstrip("/"):
|
|
148
|
+
logger.warning(f"Issuer mismatch: expected {issuer}, got {data.get('issuer')}")
|
|
149
|
+
|
|
150
|
+
return OIDCConfig.from_dict(data)
|
|
151
|
+
|
|
152
|
+
def get_cached(self, issuer: str) -> Optional[OIDCConfig]:
|
|
153
|
+
"""Get cached configuration without fetching."""
|
|
154
|
+
return self._cache.get(issuer)
|
|
155
|
+
|
|
156
|
+
def clear_cache(self, issuer: Optional[str] = None) -> None:
|
|
157
|
+
"""Clear cached configurations."""
|
|
158
|
+
if issuer:
|
|
159
|
+
if issuer in self._cache:
|
|
160
|
+
del self._cache[issuer]
|
|
161
|
+
else:
|
|
162
|
+
self._cache.clear()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class ClientMetadata:
|
|
167
|
+
"""
|
|
168
|
+
OAuth Client ID Metadata Document.
|
|
169
|
+
|
|
170
|
+
Per draft-ietf-oauth-client-id-metadata-document-00
|
|
171
|
+
"""
|
|
172
|
+
client_id: str
|
|
173
|
+
client_name: Optional[str] = None
|
|
174
|
+
client_uri: Optional[str] = None
|
|
175
|
+
logo_uri: Optional[str] = None
|
|
176
|
+
contacts: List[str] = field(default_factory=list)
|
|
177
|
+
tos_uri: Optional[str] = None
|
|
178
|
+
policy_uri: Optional[str] = None
|
|
179
|
+
|
|
180
|
+
# Redirect URIs
|
|
181
|
+
redirect_uris: List[str] = field(default_factory=list)
|
|
182
|
+
|
|
183
|
+
# Grant types
|
|
184
|
+
grant_types: List[str] = field(default_factory=lambda: ["authorization_code"])
|
|
185
|
+
response_types: List[str] = field(default_factory=lambda: ["code"])
|
|
186
|
+
|
|
187
|
+
# Token endpoint auth
|
|
188
|
+
token_endpoint_auth_method: str = "client_secret_basic"
|
|
189
|
+
|
|
190
|
+
# Scopes
|
|
191
|
+
scope: Optional[str] = None
|
|
192
|
+
|
|
193
|
+
# Software statement
|
|
194
|
+
software_id: Optional[str] = None
|
|
195
|
+
software_version: Optional[str] = None
|
|
196
|
+
|
|
197
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
198
|
+
result = {"client_id": self.client_id}
|
|
199
|
+
|
|
200
|
+
if self.client_name:
|
|
201
|
+
result["client_name"] = self.client_name
|
|
202
|
+
if self.client_uri:
|
|
203
|
+
result["client_uri"] = self.client_uri
|
|
204
|
+
if self.logo_uri:
|
|
205
|
+
result["logo_uri"] = self.logo_uri
|
|
206
|
+
if self.contacts:
|
|
207
|
+
result["contacts"] = self.contacts
|
|
208
|
+
if self.redirect_uris:
|
|
209
|
+
result["redirect_uris"] = self.redirect_uris
|
|
210
|
+
if self.grant_types:
|
|
211
|
+
result["grant_types"] = self.grant_types
|
|
212
|
+
if self.response_types:
|
|
213
|
+
result["response_types"] = self.response_types
|
|
214
|
+
if self.token_endpoint_auth_method:
|
|
215
|
+
result["token_endpoint_auth_method"] = self.token_endpoint_auth_method
|
|
216
|
+
if self.scope:
|
|
217
|
+
result["scope"] = self.scope
|
|
218
|
+
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ClientMetadata":
|
|
223
|
+
return cls(
|
|
224
|
+
client_id=data["client_id"],
|
|
225
|
+
client_name=data.get("client_name"),
|
|
226
|
+
client_uri=data.get("client_uri"),
|
|
227
|
+
logo_uri=data.get("logo_uri"),
|
|
228
|
+
contacts=data.get("contacts", []),
|
|
229
|
+
tos_uri=data.get("tos_uri"),
|
|
230
|
+
policy_uri=data.get("policy_uri"),
|
|
231
|
+
redirect_uris=data.get("redirect_uris", []),
|
|
232
|
+
grant_types=data.get("grant_types", ["authorization_code"]),
|
|
233
|
+
response_types=data.get("response_types", ["code"]),
|
|
234
|
+
token_endpoint_auth_method=data.get("token_endpoint_auth_method", "client_secret_basic"),
|
|
235
|
+
scope=data.get("scope"),
|
|
236
|
+
software_id=data.get("software_id"),
|
|
237
|
+
software_version=data.get("software_version"),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def fetch_client_metadata(client_id_url: str) -> ClientMetadata:
|
|
242
|
+
"""
|
|
243
|
+
Fetch OAuth Client ID Metadata Document.
|
|
244
|
+
|
|
245
|
+
Per draft-ietf-oauth-client-id-metadata-document-00
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
client_id_url: URL of the client ID metadata document
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
ClientMetadata instance
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
import httpx
|
|
255
|
+
except ImportError:
|
|
256
|
+
raise ImportError("httpx required for client metadata. Install with: pip install httpx")
|
|
257
|
+
|
|
258
|
+
async with httpx.AsyncClient() as client:
|
|
259
|
+
response = await client.get(client_id_url, follow_redirects=True)
|
|
260
|
+
response.raise_for_status()
|
|
261
|
+
|
|
262
|
+
data = response.json()
|
|
263
|
+
return ClientMetadata.from_dict(data)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def fetch_protected_resource_metadata(resource_url: str) -> Dict[str, Any]:
|
|
267
|
+
"""
|
|
268
|
+
Fetch OAuth 2.0 Protected Resource Metadata.
|
|
269
|
+
|
|
270
|
+
Per RFC 9728
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
resource_url: URL of the protected resource
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Metadata dictionary
|
|
277
|
+
"""
|
|
278
|
+
try:
|
|
279
|
+
import httpx
|
|
280
|
+
except ImportError:
|
|
281
|
+
raise ImportError("httpx required for resource metadata. Install with: pip install httpx")
|
|
282
|
+
|
|
283
|
+
# Build well-known URL
|
|
284
|
+
metadata_url = f"{resource_url.rstrip('/')}/.well-known/oauth-protected-resource"
|
|
285
|
+
|
|
286
|
+
async with httpx.AsyncClient() as client:
|
|
287
|
+
response = await client.get(metadata_url, follow_redirects=True)
|
|
288
|
+
response.raise_for_status()
|
|
289
|
+
return response.json()
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Scope Management
|
|
3
|
+
|
|
4
|
+
Implements incremental scope handling per MCP 2025-11-25 specification.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Scope validation and enforcement
|
|
8
|
+
- WWW-Authenticate challenges for scope escalation
|
|
9
|
+
- Scope hierarchy support
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, List, Optional, Set
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Standard MCP scopes
|
|
20
|
+
MCP_SCOPES = {
|
|
21
|
+
"tools:read": "Read tool definitions",
|
|
22
|
+
"tools:call": "Execute tools",
|
|
23
|
+
"resources:read": "Read resources",
|
|
24
|
+
"resources:subscribe": "Subscribe to resource changes",
|
|
25
|
+
"prompts:read": "Read prompts",
|
|
26
|
+
"prompts:execute": "Execute prompts",
|
|
27
|
+
"sampling:create": "Create sampling requests",
|
|
28
|
+
"tasks:read": "Read tasks",
|
|
29
|
+
"tasks:write": "Create and manage tasks",
|
|
30
|
+
"admin": "Administrative access",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ScopeChallenge:
|
|
36
|
+
"""
|
|
37
|
+
Scope challenge for incremental consent.
|
|
38
|
+
|
|
39
|
+
Used when an operation requires additional scopes.
|
|
40
|
+
"""
|
|
41
|
+
required_scopes: List[str]
|
|
42
|
+
granted_scopes: List[str]
|
|
43
|
+
missing_scopes: List[str]
|
|
44
|
+
error: str = "insufficient_scope"
|
|
45
|
+
error_description: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
def to_www_authenticate(self, realm: Optional[str] = None) -> str:
|
|
48
|
+
"""Generate WWW-Authenticate header value."""
|
|
49
|
+
parts = ["Bearer"]
|
|
50
|
+
params = []
|
|
51
|
+
|
|
52
|
+
if realm:
|
|
53
|
+
params.append(f'realm="{realm}"')
|
|
54
|
+
|
|
55
|
+
if self.missing_scopes:
|
|
56
|
+
params.append(f'scope="{" ".join(self.missing_scopes)}"')
|
|
57
|
+
|
|
58
|
+
params.append(f'error="{self.error}"')
|
|
59
|
+
|
|
60
|
+
if self.error_description:
|
|
61
|
+
params.append(f'error_description="{self.error_description}"')
|
|
62
|
+
|
|
63
|
+
if params:
|
|
64
|
+
parts.append(", ".join(params))
|
|
65
|
+
|
|
66
|
+
return " ".join(parts)
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
69
|
+
return {
|
|
70
|
+
"required_scopes": self.required_scopes,
|
|
71
|
+
"granted_scopes": self.granted_scopes,
|
|
72
|
+
"missing_scopes": self.missing_scopes,
|
|
73
|
+
"error": self.error,
|
|
74
|
+
"error_description": self.error_description,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ScopeManager:
|
|
79
|
+
"""
|
|
80
|
+
MCP Scope Manager.
|
|
81
|
+
|
|
82
|
+
Handles scope validation, enforcement, and incremental consent.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
available_scopes: Optional[Dict[str, str]] = None,
|
|
88
|
+
scope_hierarchy: Optional[Dict[str, List[str]]] = None,
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Initialize scope manager.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
available_scopes: Available scopes with descriptions
|
|
95
|
+
scope_hierarchy: Scope hierarchy (parent -> children)
|
|
96
|
+
"""
|
|
97
|
+
self._available_scopes = available_scopes or MCP_SCOPES.copy()
|
|
98
|
+
self._scope_hierarchy = scope_hierarchy or {
|
|
99
|
+
"admin": list(MCP_SCOPES.keys()),
|
|
100
|
+
"tools:call": ["tools:read"],
|
|
101
|
+
"resources:subscribe": ["resources:read"],
|
|
102
|
+
"tasks:write": ["tasks:read"],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def expand_scopes(self, scopes: List[str]) -> Set[str]:
|
|
106
|
+
"""
|
|
107
|
+
Expand scopes based on hierarchy.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
scopes: List of scopes
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Expanded set of scopes
|
|
114
|
+
"""
|
|
115
|
+
expanded = set(scopes)
|
|
116
|
+
|
|
117
|
+
for scope in scopes:
|
|
118
|
+
if scope in self._scope_hierarchy:
|
|
119
|
+
expanded.update(self._scope_hierarchy[scope])
|
|
120
|
+
|
|
121
|
+
return expanded
|
|
122
|
+
|
|
123
|
+
def validate_scopes(
|
|
124
|
+
self,
|
|
125
|
+
required: List[str],
|
|
126
|
+
granted: List[str],
|
|
127
|
+
) -> tuple[bool, Optional[ScopeChallenge]]:
|
|
128
|
+
"""
|
|
129
|
+
Validate that granted scopes satisfy requirements.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
required: Required scopes
|
|
133
|
+
granted: Granted scopes
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Tuple of (is_valid, challenge)
|
|
137
|
+
"""
|
|
138
|
+
# Expand granted scopes
|
|
139
|
+
expanded_granted = self.expand_scopes(granted)
|
|
140
|
+
|
|
141
|
+
# Check each required scope
|
|
142
|
+
missing = []
|
|
143
|
+
for scope in required:
|
|
144
|
+
if scope not in expanded_granted:
|
|
145
|
+
missing.append(scope)
|
|
146
|
+
|
|
147
|
+
if missing:
|
|
148
|
+
return False, ScopeChallenge(
|
|
149
|
+
required_scopes=required,
|
|
150
|
+
granted_scopes=granted,
|
|
151
|
+
missing_scopes=missing,
|
|
152
|
+
error_description=f"Missing required scopes: {', '.join(missing)}",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return True, None
|
|
156
|
+
|
|
157
|
+
def check_scope(
|
|
158
|
+
self,
|
|
159
|
+
scope: str,
|
|
160
|
+
granted: List[str],
|
|
161
|
+
) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
Check if a single scope is granted.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
scope: Required scope
|
|
167
|
+
granted: Granted scopes
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
True if scope is granted
|
|
171
|
+
"""
|
|
172
|
+
expanded = self.expand_scopes(granted)
|
|
173
|
+
return scope in expanded
|
|
174
|
+
|
|
175
|
+
def get_scope_description(self, scope: str) -> Optional[str]:
|
|
176
|
+
"""Get description for a scope."""
|
|
177
|
+
return self._available_scopes.get(scope)
|
|
178
|
+
|
|
179
|
+
def list_available_scopes(self) -> Dict[str, str]:
|
|
180
|
+
"""List all available scopes with descriptions."""
|
|
181
|
+
return self._available_scopes.copy()
|
|
182
|
+
|
|
183
|
+
def add_scope(self, scope: str, description: str) -> None:
|
|
184
|
+
"""Add a custom scope."""
|
|
185
|
+
self._available_scopes[scope] = description
|
|
186
|
+
|
|
187
|
+
def add_hierarchy(self, parent: str, children: List[str]) -> None:
|
|
188
|
+
"""Add scope hierarchy."""
|
|
189
|
+
if parent in self._scope_hierarchy:
|
|
190
|
+
self._scope_hierarchy[parent].extend(children)
|
|
191
|
+
else:
|
|
192
|
+
self._scope_hierarchy[parent] = children
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class ScopeRequirement:
|
|
197
|
+
"""Scope requirement for an operation."""
|
|
198
|
+
scopes: List[str]
|
|
199
|
+
any_of: bool = False # If True, any scope is sufficient
|
|
200
|
+
description: Optional[str] = None
|
|
201
|
+
|
|
202
|
+
def check(self, granted: List[str], manager: ScopeManager) -> tuple[bool, Optional[ScopeChallenge]]:
|
|
203
|
+
"""Check if requirement is satisfied."""
|
|
204
|
+
if self.any_of:
|
|
205
|
+
# Any scope is sufficient
|
|
206
|
+
for scope in self.scopes:
|
|
207
|
+
if manager.check_scope(scope, granted):
|
|
208
|
+
return True, None
|
|
209
|
+
|
|
210
|
+
return False, ScopeChallenge(
|
|
211
|
+
required_scopes=self.scopes,
|
|
212
|
+
granted_scopes=granted,
|
|
213
|
+
missing_scopes=self.scopes,
|
|
214
|
+
error_description=f"Requires one of: {', '.join(self.scopes)}",
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
# All scopes required
|
|
218
|
+
return manager.validate_scopes(self.scopes, granted)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Operation to scope mapping
|
|
222
|
+
OPERATION_SCOPES: Dict[str, ScopeRequirement] = {
|
|
223
|
+
"tools/list": ScopeRequirement(["tools:read"]),
|
|
224
|
+
"tools/call": ScopeRequirement(["tools:call"]),
|
|
225
|
+
"resources/list": ScopeRequirement(["resources:read"]),
|
|
226
|
+
"resources/read": ScopeRequirement(["resources:read"]),
|
|
227
|
+
"resources/subscribe": ScopeRequirement(["resources:subscribe"]),
|
|
228
|
+
"prompts/list": ScopeRequirement(["prompts:read"]),
|
|
229
|
+
"prompts/get": ScopeRequirement(["prompts:read"]),
|
|
230
|
+
"sampling/createMessage": ScopeRequirement(["sampling:create"]),
|
|
231
|
+
"tasks/create": ScopeRequirement(["tasks:write"]),
|
|
232
|
+
"tasks/get": ScopeRequirement(["tasks:read"]),
|
|
233
|
+
"tasks/list": ScopeRequirement(["tasks:read"]),
|
|
234
|
+
"tasks/cancel": ScopeRequirement(["tasks:write"]),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_operation_scopes(operation: str) -> Optional[ScopeRequirement]:
|
|
239
|
+
"""Get scope requirement for an operation."""
|
|
240
|
+
return OPERATION_SCOPES.get(operation)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def require_scope(*scopes: str, any_of: bool = False):
|
|
244
|
+
"""
|
|
245
|
+
Decorator to require scopes for a function.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
scopes: Required scopes
|
|
249
|
+
any_of: If True, any scope is sufficient
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Decorator function
|
|
253
|
+
"""
|
|
254
|
+
requirement = ScopeRequirement(list(scopes), any_of=any_of)
|
|
255
|
+
|
|
256
|
+
def decorator(func):
|
|
257
|
+
func._scope_requirement = requirement
|
|
258
|
+
return func
|
|
259
|
+
|
|
260
|
+
return decorator
|