diff --git a/documentation/docs/user-guide/runtime/quickstart.md b/documentation/docs/user-guide/runtime/quickstart.md index 464cf6df..e9d2ebcf 100644 --- a/documentation/docs/user-guide/runtime/quickstart.md +++ b/documentation/docs/user-guide/runtime/quickstart.md @@ -197,4 +197,43 @@ AgentCore Runtime requires ARM64 containers (AWS Graviton). The toolkit handles - **[Add tools with Gateway](../gateway/quickstart.md)** - Connect your agent to APIs and services - **[Enable memory](../../examples/memory-integration.md)** - Give your agent conversation history - **[Configure authentication](../runtime/auth.md)** - Set up OAuth/JWT auth +- **[Request Header Configuration](#request-header-configuration)** - Forward headers to your agent - **[View more examples](../../examples/README.md)** - Learn from complete implementations + +## Request Header Configuration + +When you configure OAuth authentication, the system automatically forwards the `Authorization` header to your agent container. You can also configure additional headers to be forwarded. + +### Automatic OAuth Header Forwarding + +When OAuth is enabled, the `Authorization` header is automatically forwarded to your agent: + +```yaml +# .bedrock_agentcore.yaml +agents: + my-agent: + # ... other configuration ... + authorizer_configuration: + customJWTAuthorizer: + discoveryUrl: "https://example.com/.well-known/openid_configuration" + allowedClients: ["client1", "client2"] + allowedAudience: ["audience1", "audience2"] + + # Automatically added when OAuth is configured + request_header_configuration: + allowed_headers: + - "Authorization" +``` + +### Custom Header Configuration + +You can manually edit the configuration file to forward additional headers: + +```yaml +request_header_configuration: + allowed_headers: + - "Authorization" # Automatically added for OAuth + - "X-User-ID" # Add your custom headers + - "X-Tenant-ID" + - "X-Custom-Header" +``` diff --git a/src/bedrock_agentcore_starter_toolkit/cli/runtime/commands.py b/src/bedrock_agentcore_starter_toolkit/cli/runtime/commands.py index 2efbb4f2..9873fd99 100644 --- a/src/bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +++ b/src/bedrock_agentcore_starter_toolkit/cli/runtime/commands.py @@ -233,15 +233,21 @@ def configure( # Handle OAuth authorization configuration oauth_config = None + request_header_config = None if authorizer_config: # Parse provided JSON configuration try: oauth_config = json.loads(authorizer_config) _print_success("Using provided OAuth authorizer configuration") + # Auto-configure request headers for OAuth + request_header_config = config_manager.get_request_header_config_for_oauth() except json.JSONDecodeError as e: _handle_error(f"Invalid JSON in --authorizer-config: {e}", e) else: oauth_config = config_manager.prompt_oauth_config() + # Auto-configure request headers if OAuth is configured + if oauth_config: + request_header_config = config_manager.get_request_header_config_for_oauth() try: result = configure_bedrock_agentcore( @@ -254,6 +260,7 @@ def configure( enable_observability=not disable_otel, requirements_file=final_requirements_file, authorizer_configuration=oauth_config, + request_header_configuration=request_header_config, verbose=verbose, region=region, protocol=protocol.upper() if protocol else None, diff --git a/src/bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py b/src/bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py index f0b5f987..36d902a9 100644 --- a/src/bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +++ b/src/bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py @@ -145,3 +145,9 @@ def _configure_oauth(self) -> dict: _print_success("OAuth authorizer configuration created") return config + + def get_request_header_config_for_oauth(self) -> Optional[dict]: + """Get request header configuration for OAuth (auto-configured).""" + return { + "allowed_headers": ["Authorization"] + } diff --git a/src/bedrock_agentcore_starter_toolkit/operations/runtime/configure.py b/src/bedrock_agentcore_starter_toolkit/operations/runtime/configure.py index ffe4dcd5..cc2242bd 100644 --- a/src/bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +++ b/src/bedrock_agentcore_starter_toolkit/operations/runtime/configure.py @@ -32,6 +32,7 @@ def configure_bedrock_agentcore( enable_observability: bool = True, requirements_file: Optional[str] = None, authorizer_configuration: Optional[Dict[str, Any]] = None, + request_header_configuration: Optional[Dict[str, Any]] = None, verbose: bool = False, region: Optional[str] = None, protocol: Optional[str] = None, @@ -49,6 +50,7 @@ def configure_bedrock_agentcore( enable_observability: Whether to enable observability requirements_file: Path to requirements file authorizer_configuration: JWT authorizer configuration dictionary + request_header_configuration: Request header configuration dictionary verbose: Whether to provide verbose output during configuration region: AWS region for deployment protocol: agent server protocol, must be either HTTP or MCP @@ -193,6 +195,7 @@ def configure_bedrock_agentcore( ), bedrock_agentcore=BedrockAgentCoreDeploymentInfo(), authorizer_configuration=authorizer_configuration, + request_header_configuration=request_header_configuration, ) # Use simplified config merging diff --git a/src/bedrock_agentcore_starter_toolkit/operations/runtime/launch.py b/src/bedrock_agentcore_starter_toolkit/operations/runtime/launch.py index 48684742..0b05d05a 100644 --- a/src/bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +++ b/src/bedrock_agentcore_starter_toolkit/operations/runtime/launch.py @@ -171,6 +171,7 @@ def _deploy_to_bedrock_agentcore( execution_role_arn=agent_config.aws.execution_role, network_config=network_config, authorizer_config=agent_config.get_authorizer_configuration(), + request_header_config=agent_config.get_request_header_configuration(), protocol_config=protocol_config, env_vars=env_vars, auto_update_on_conflict=auto_update_on_conflict, diff --git a/src/bedrock_agentcore_starter_toolkit/services/runtime.py b/src/bedrock_agentcore_starter_toolkit/services/runtime.py index 835d4d3a..eeceb70f 100644 --- a/src/bedrock_agentcore_starter_toolkit/services/runtime.py +++ b/src/bedrock_agentcore_starter_toolkit/services/runtime.py @@ -122,6 +122,7 @@ def create_agent( execution_role_arn: str, network_config: Optional[Dict] = None, authorizer_config: Optional[Dict] = None, + request_header_config: Optional[Dict] = None, protocol_config: Optional[Dict] = None, env_vars: Optional[Dict] = None, auto_update_on_conflict: bool = False, @@ -142,6 +143,9 @@ def create_agent( if authorizer_config is not None: params["authorizerConfiguration"] = authorizer_config + if request_header_config is not None: + params["requestHeaderConfiguration"] = request_header_config + if protocol_config is not None: params["protocolConfiguration"] = protocol_config @@ -196,6 +200,7 @@ def create_agent( execution_role_arn, network_config, authorizer_config, + request_header_config, protocol_config, env_vars, ) @@ -216,6 +221,7 @@ def update_agent( execution_role_arn: str, network_config: Optional[Dict] = None, authorizer_config: Optional[Dict] = None, + request_header_config: Optional[Dict] = None, protocol_config: Optional[Dict] = None, env_vars: Optional[Dict] = None, ) -> Dict[str, str]: @@ -235,6 +241,9 @@ def update_agent( if authorizer_config is not None: params["authorizerConfiguration"] = authorizer_config + if request_header_config is not None: + params["requestHeaderConfiguration"] = request_header_config + if protocol_config is not None: params["protocolConfiguration"] = protocol_config @@ -297,6 +306,7 @@ def create_or_update_agent( execution_role_arn: str, network_config: Optional[Dict] = None, authorizer_config: Optional[Dict] = None, + request_header_config: Optional[Dict] = None, protocol_config: Optional[Dict] = None, env_vars: Optional[Dict] = None, auto_update_on_conflict: bool = False, @@ -304,7 +314,7 @@ def create_or_update_agent( """Create or update agent.""" if agent_id: return self.update_agent( - agent_id, image_uri, execution_role_arn, network_config, authorizer_config, protocol_config, env_vars + agent_id, image_uri, execution_role_arn, network_config, authorizer_config, request_header_config, protocol_config, env_vars ) return self.create_agent( agent_name, @@ -312,6 +322,7 @@ def create_or_update_agent( execution_role_arn, network_config, authorizer_config, + request_header_config, protocol_config, env_vars, auto_update_on_conflict, diff --git a/src/bedrock_agentcore_starter_toolkit/utils/runtime/schema.py b/src/bedrock_agentcore_starter_toolkit/utils/runtime/schema.py index 322ce5ce..8dce4090 100644 --- a/src/bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +++ b/src/bedrock_agentcore_starter_toolkit/utils/runtime/schema.py @@ -82,11 +82,25 @@ class BedrockAgentCoreAgentSchema(BaseModel): codebuild: CodeBuildConfig = Field(default_factory=CodeBuildConfig) authorizer_configuration: Optional[dict] = Field(default=None, description="JWT authorizer configuration") oauth_configuration: Optional[dict] = Field(default=None, description="Oauth configuration") + request_header_configuration: Optional[dict] = Field(default=None, description="Request header configuration") def get_authorizer_configuration(self) -> Optional[dict]: """Get the authorizer configuration.""" return self.authorizer_configuration + def get_request_header_configuration(self) -> Optional[dict]: + """Get request header configuration for AWS API.""" + if not self.request_header_configuration: + return None + + allowed_headers = self.request_header_configuration.get("allowed_headers", []) + if not allowed_headers: + return None + + return { + "requestHeaderAllowlist": allowed_headers + } + def validate(self, for_local: bool = False) -> List[str]: """Validate configuration and return list of errors. diff --git a/tests/cli/runtime/test_commands.py b/tests/cli/runtime/test_commands.py index 5f0f333d..8fc94ac7 100644 --- a/tests/cli/runtime/test_commands.py +++ b/tests/cli/runtime/test_commands.py @@ -1680,6 +1680,21 @@ def test_configure_oauth(self, tmp_path): mock_prompt.assert_any_call("Enter allowed OAuth audience (comma-separated)", "") mock_success.assert_called_once_with("OAuth authorizer configuration created") + def test_get_request_header_config_for_oauth(self, tmp_path): + """Test get_request_header_config_for_oauth returns correct configuration.""" + from bedrock_agentcore_starter_toolkit.cli.runtime.commands import ConfigurationManager + + with patch("bedrock_agentcore_starter_toolkit.utils.runtime.config.load_config_if_exists", return_value=None): + config_manager = ConfigurationManager(tmp_path / ".bedrock_agentcore.yaml") + + result = config_manager.get_request_header_config_for_oauth() + + expected_config = { + "allowed_headers": ["Authorization"] + } + + assert result == expected_config + def test_configure_oauth_with_existing_values(self, tmp_path): """Test _configure_oauth with existing configuration values as defaults.""" from bedrock_agentcore_starter_toolkit.cli.runtime.commands import ConfigurationManager diff --git a/tests/services/test_runtime.py b/tests/services/test_runtime.py index 529c8cb0..3789ddc7 100644 --- a/tests/services/test_runtime.py +++ b/tests/services/test_runtime.py @@ -162,6 +162,34 @@ def test_create_agent_with_optional_configs(self, mock_boto3_clients): assert call_args["protocolConfiguration"] == protocol_config assert call_args["environmentVariables"] == env_vars + def test_create_agent_with_request_header_config(self, mock_boto3_clients): + """Test create agent with request header configuration.""" + client = BedrockAgentCoreClient("us-west-2") + + network_config = {"networkMode": "PRIVATE"} + authorizer_config = {"type": "IAM"} + request_header_config = {"requestHeaderAllowlist": ["Authorization", "X-User-ID"]} + protocol_config = {"serverProtocol": "MCP"} + env_vars = {"ENV1": "HELLO", "ENV2": "WORLD"} + + result = client.create_agent( + agent_name="test-agent", + image_uri="123456789012.dkr.ecr.us-west-2.amazonaws.com/test:latest", + execution_role_arn="arn:aws:iam::123456789012:role/TestRole", + network_config=network_config, + authorizer_config=authorizer_config, + request_header_config=request_header_config, + protocol_config=protocol_config, + env_vars=env_vars, + ) + + assert result["id"] == "test-agent-id" + assert result["arn"] == "arn:aws:bedrock_agentcore:us-west-2:123456789012:agent-runtime/test-agent-id" + + # Verify the call included request header config + call_args = mock_boto3_clients["bedrock_agentcore"].create_agent_runtime.call_args[1] + assert call_args["requestHeaderConfiguration"] == request_header_config + def test_create_agent_error_handling(self, mock_boto3_clients): """Test create agent error handling.""" client = BedrockAgentCoreClient("us-west-2") diff --git a/tests/utils/runtime/test_config.py b/tests/utils/runtime/test_config.py index df3b7ac9..3e18d560 100644 --- a/tests/utils/runtime/test_config.py +++ b/tests/utils/runtime/test_config.py @@ -351,3 +351,62 @@ def test_merge_agent_config_logging_calls(self, mock_log, tmp_path): mock_log.reset_mock() merge_agent_config(config_path, "agent2", agent2) mock_log.info.assert_called_with("Keeping '%s' as default agent", "agent2") + + +class TestRequestHeaderConfiguration: + """Test request header configuration functionality.""" + + def test_get_request_header_configuration_with_headers(self): + """Test get_request_header_configuration with allowed headers.""" + agent_config = BedrockAgentCoreAgentSchema( + name="test-agent", + entrypoint="test.py", + request_header_configuration={ + "allowed_headers": ["Authorization", "X-User-ID", "X-Custom-Header"] + } + ) + + result = agent_config.get_request_header_configuration() + + expected = { + "requestHeaderAllowlist": ["Authorization", "X-User-ID", "X-Custom-Header"] + } + + assert result == expected + + def test_get_request_header_configuration_empty_headers(self): + """Test get_request_header_configuration with empty allowed headers.""" + agent_config = BedrockAgentCoreAgentSchema( + name="test-agent", + entrypoint="test.py", + request_header_configuration={ + "allowed_headers": [] + } + ) + + result = agent_config.get_request_header_configuration() + + assert result is None + + def test_get_request_header_configuration_no_config(self): + """Test get_request_header_configuration with no configuration.""" + agent_config = BedrockAgentCoreAgentSchema( + name="test-agent", + entrypoint="test.py" + ) + + result = agent_config.get_request_header_configuration() + + assert result is None + + def test_get_request_header_configuration_missing_headers_key(self): + """Test get_request_header_configuration with missing allowed_headers key.""" + agent_config = BedrockAgentCoreAgentSchema( + name="test-agent", + entrypoint="test.py", + request_header_configuration={} + ) + + result = agent_config.get_request_header_configuration() + + assert result is None