diff --git a/examples/logging_example.py b/examples/logging_example.py deleted file mode 100644 index 6fb691ba..00000000 --- a/examples/logging_example.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python3 -""" -Example demonstrating the FastAPI + Structlog integration in AgentUp. - -This example shows how to use the structured logging system with correlation IDs, -middleware integration, and both text and JSON output formats. -""" - -import asyncio -import tempfile -from pathlib import Path - -import yaml - -from src.agent.config.logging import FastAPIStructLogger, configure_logging_from_config, setup_logging -from src.agent.config.models import LoggingConfig - - -async def main(): - """Demonstrate structured logging features.""" - print("=== AgentUp Structured Logging Example ===\n") - - # Example 1: Basic setup with LoggingConfig - print("1. Setting up basic structured logging...") - config = LoggingConfig( - level="INFO", - format="text", - correlation_id=True, - request_logging=True, - ) - setup_logging(config) - print(" ✓ Structured logging configured") - - # Example 2: Using FastAPIStructLogger - print("\n2. Using FastAPIStructLogger...") - logger = FastAPIStructLogger() - - # Basic logging - logger.info("Application started", component="example", version="1.0.0") - - # Binding context variables - logger.bind(user_id="user-123", request_id="req-456") - logger.info("User action performed", action="login") - - # Different log levels - logger.debug("Debug information", detail="verbose_mode") - logger.warning("Something to watch", metric="high_latency") - logger.error("Error occurred", error_code="E001") - - print(" ✓ Log messages sent with structured context") - - # Example 3: JSON format output - print("\n3. Switching to JSON format...") - json_config = LoggingConfig( - level="INFO", - format="json", - correlation_id=True, - ) - # Reset logging state to demonstrate JSON format - import src.agent.config.logging as logging_module - logging_module._logging_configured = False - setup_logging(json_config) - - json_logger = FastAPIStructLogger("example.json") - json_logger.bind(format="json", demo=True) - json_logger.info("JSON formatted log message", - timestamp="2024-07-08", - structured=True, - data={"key": "value", "number": 42}) - print(" ✓ JSON structured logging enabled") - - # Example 4: Configuration from YAML - print("\n4. Loading configuration from YAML...") - - # Create a temporary YAML config - config_dict = { - "logging": { - "enabled": True, - "level": "DEBUG", - "format": "text", - "correlation_id": True, - "request_logging": True, - "console": {"enabled": True, "colors": True}, - "file": {"enabled": False}, - "modules": { - "example": "DEBUG", - "httpx": "WARNING" - } - }, - "agent": { - "name": "LoggingExample", - "version": "1.0.0" - } - } - - with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: - yaml.dump(config_dict, f, default_flow_style=False) - config_file = f.name - - try: - # Load and configure from YAML - logging_module._logging_configured = False # Reset for demo - configure_logging_from_config(config_dict) - - yaml_logger = FastAPIStructLogger("example.yaml") - yaml_logger.bind(source="yaml_config") - yaml_logger.debug("Configuration loaded from YAML", config_file=config_file) - yaml_logger.info("YAML configuration working", modules=["example", "httpx"]) - - print(" ✓ YAML configuration loaded and applied") - - finally: - # Clean up - Path(config_file).unlink() - - # Example 5: Model binding (if you have objects with 'id' attribute) - print("\n5. Demonstrating model binding...") - - class ExampleModel: - def __init__(self, id: str, name: str): - self.id = id - self.name = name - - model_logger = FastAPIStructLogger("example.models") - user_model = ExampleModel("user-789", "John Doe") - order_model = ExampleModel("order-101", "Coffee Order") - - # Bind models - they'll be logged as user_model: user-789, order_model: order-101 - model_logger.bind(user_model, order_model, operation="checkout") - model_logger.info("Order processed", status="completed") - - print(" ✓ Model binding demonstrated") - - print("\n=== Example Complete ===") - print("\nKey Features Demonstrated:") - print("• Structured logging with contextual information") - print("• Correlation ID support for request tracing") - print("• Both text and JSON output formats") - print("• YAML configuration loading") - print("• Model object binding") - print("• Multiple logger instances") - print("• Module-specific log levels") - - print("\nTo use in a FastAPI app:") - print("1. Configure logging in your agent_config.yaml") - print("2. Import and use StructLogMiddleware") - print("3. Use FastAPIStructLogger in your handlers") - print("4. Enjoy structured, searchable logs!") - - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/examples/middleware-override-example.yaml b/examples/middleware-override-example.yaml deleted file mode 100644 index 37adb3c5..00000000 --- a/examples/middleware-override-example.yaml +++ /dev/null @@ -1,119 +0,0 @@ -# Example: Per-Plugin Middleware Override - -# Global middleware configuration - applies to all handlers by default -middleware: - - name: logged - params: - log_level: 20 # INFO level - - name: timed - params: {} - - name: cached - params: - ttl: 300 # 5 minutes default cache - - name: rate_limited - params: - requests_per_minute: 60 - -# Plugins with custom middleware requirements -plugins: - # Standard plugin - uses global middleware - - plugin_id: standard_plugin - name: Standard Plugin - description: Uses the global middleware configuration - input_mode: text - output_mode: text - # No middleware_override, so uses global middleware - - # Expensive operation - needs longer cache - - plugin_id: expensive_analysis - name: Expensive Analysis - description: Complex analysis that takes time - input_mode: multimodal - output_mode: text - middleware_override: - - name: cached - params: - ttl: 3600 # Cache for 1 hour instead of 5 minutes - - name: logged - params: - log_level: 20 - - name: timed - params: {} - - # Real-time data - no caching - - plugin_id: live_feed - name: Live Feed - description: Always returns fresh data - input_mode: text - output_mode: text - middleware_override: - - name: logged - params: - log_level: 20 - - name: timed - params: {} - - name: rate_limited - params: - requests_per_minute: 120 # Higher rate limit - # Note: No caching middleware - - # Debug mode plugin - verbose logging - - plugin_id: experimental_feature - name: Experimental Feature - description: New feature under development - input_mode: text - output_mode: text - middleware_override: - - name: logged - params: - log_level: 10 # DEBUG level for this skill only - - name: timed - params: {} - - name: retryable - params: - max_retries: 5 # More retries during testing - backoff_factor: 1 - - # High-performance plugin - minimal middleware - - plugin_id: fast_response - name: Fast Response - description: Optimized for speed - input_mode: text - output_mode: text - middleware_override: - - name: timed # Only timing, no logging or caching - params: {} - - # Raw performance - no middleware at all - - plugin_id: bare_metal - name: Bare Metal Performance - description: No middleware overhead whatsoever - input_mode: text - output_mode: text - middleware_override: [] # Empty array = no middleware - - # Selective exclusion - everything except caching - - plugin_id: no_cache_api - name: No Cache API - description: External API that should never be cached - input_mode: text - output_mode: text - middleware_override: - - name: logged - params: - log_level: 20 - - name: timed - params: {} - - name: rate_limited - params: - requests_per_minute: 60 - # Note: Caching middleware excluded - -# The result: -# - standard_plugin: Gets all global middleware (logged, timed, cached, rate_limited) -# - expensive_analysis: Gets custom caching (1 hour), plus logging and timing -# - live_feed: No caching, higher rate limit -# - experimental_feature: DEBUG logging, more retries -# - fast_response: Only timing middleware for maximum performance -# - bare_metal: NO middleware at all - raw performance -# - no_cache_api: All middleware except caching \ No newline at end of file diff --git a/src/agent/security/authenticators/oauth2.py b/src/agent/security/authenticators/oauth2.py index f4ca9dca..a187c62d 100644 --- a/src/agent/security/authenticators/oauth2.py +++ b/src/agent/security/authenticators/oauth2.py @@ -190,17 +190,30 @@ async def _validate_via_introspection(self, token: str, request_info: dict[str, InvalidCredentialsException: If token is invalid or introspection fails """ try: - async with AsyncOAuth2Client(client_id=self.client_id, client_secret=self.client_secret) as client: - # Call introspection endpoint + # GitHub expects JSON payload with "access_token" field, not form data + # Use basic auth with httpx directly instead of AsyncOAuth2Client + import httpx + import base64 + + auth = base64.b64encode(f"{self.client_id}:{self.client_secret}".encode()).decode() + + async with httpx.AsyncClient() as client: response = await client.post( - self.introspection_endpoint, data={"token": token, "token_type_hint": "access_token"} + self.introspection_endpoint, + headers={ + "Authorization": f"Basic {auth}", + "Content-Type": "application/json" + }, + json={"access_token": token} ) response.raise_for_status() introspection_data = response.json() # Check if token is active - if not introspection_data.get("active", False): + # GitHub doesn't return 'active' field, but returns 200 status for valid tokens + # For standard OAuth2 introspection, check 'active' field + if "active" in introspection_data and not introspection_data.get("active", False): raise InvalidCredentialsException("Unauthorized") # Extract user information @@ -208,6 +221,7 @@ async def _validate_via_introspection(self, token: str, request_info: dict[str, introspection_data.get("sub") or introspection_data.get("user_id") or introspection_data.get("username") + or (introspection_data.get("user", {}).get("login") if introspection_data.get("user") else None) or "unknown" ) @@ -219,6 +233,13 @@ async def _validate_via_introspection(self, token: str, request_info: dict[str, scopes = set(scope_value.split()) elif isinstance(scope_value, list): scopes = set(scope_value) + elif "scopes" in introspection_data: + # GitHub returns scopes as "scopes" field + scope_value = introspection_data["scopes"] + if isinstance(scope_value, list): + scopes = set(scope_value) + elif isinstance(scope_value, str): + scopes = set(scope_value.split()) return AuthenticationResult( success=True, diff --git a/src/agent/security/validators.py b/src/agent/security/validators.py index c4a73677..0b9d86a9 100644 --- a/src/agent/security/validators.py +++ b/src/agent/security/validators.py @@ -274,7 +274,7 @@ def _validate_jwt_config(config: dict[str, Any]) -> None: @staticmethod def _validate_oauth2_config(config: dict[str, Any]) -> None: """Validate OAuth2 configuration.""" - oauth2_config = config.get("oauth2", {}) + oauth2_config = config if not isinstance(oauth2_config, dict): raise SecurityConfigurationException("OAuth2 config must be a dictionary")