From fb531b24738ddbedc1131ce27c212c6ae1da3e2d Mon Sep 17 00:00:00 2001 From: "Parker J. Rule" Date: Mon, 22 Sep 2025 11:17:24 -0400 Subject: [PATCH] feat(cli): add configuration for server customization ordering (#6179) This adds a configuration option in `HttpConfig` that allows LangGraph Platform users to apply custom authentication hooks before (other) custom middleware. Currently, the order is fixed (custom middleware is always evaluated before custom auth). (Apologies for the noise in [de187a9](https://github.com/langchain-ai/langgraph/pull/6179/commits/de187a989e807c5687c22db1fc065d24030fa6b7), apparently from the forced application of new linter rules.) --- libs/cli/langgraph_cli/config.py | 12 +++ libs/cli/schemas/schema.json | 14 +++ libs/cli/schemas/schema.v0.json | 14 +++ libs/cli/tests/unit_tests/test_util.py | 115 ++++++++++++++++--------- 4 files changed, 115 insertions(+), 40 deletions(-) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 87d4d5c72..9b6986eca 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -18,6 +18,7 @@ DEFAULT_IMAGE_DISTRO = "debian" Distros = Literal["debian", "wolfi", "bullseye", "bookworm"] +MiddlewareOrders = Literal["auth_first", "middleware_first"] class TTLConfig(TypedDict, total=False): @@ -359,6 +360,17 @@ class HttpConfig(TypedDict, total=False): agent's behavior or permissions on a request's headers.""" logging_headers: Optional[ConfigurableHeaderConfig] """Optional. Defines which headers are excluded from logging.""" + middleware_order: Optional[MiddlewareOrders] + """Optional. Defines the order in which to apply server customizations. + + Choices: + - "auth_first": Authentication hooks (custom or default) are evaluated + before custom middleware. + - "middleware_first": Custom middleware is evaluated + before authentication hooks (custom or default). + + Default is `middleware_first`. + """ class Config(TypedDict, total=False): diff --git a/libs/cli/schemas/schema.json b/libs/cli/schemas/schema.json index 21dd34a39..03fe74ee8 100644 --- a/libs/cli/schemas/schema.json +++ b/libs/cli/schemas/schema.json @@ -587,6 +587,20 @@ } ], "description": "Optional. Defines which headers are excluded from logging." + }, + "middleware_order": { + "anyOf": [ + { + "enum": [ + "auth_first", + "middleware_first" + ] + }, + { + "type": "null" + } + ], + "description": "Optional. Defines the order in which to apply server customizations.\n" } }, "required": [] diff --git a/libs/cli/schemas/schema.v0.json b/libs/cli/schemas/schema.v0.json index 21dd34a39..03fe74ee8 100644 --- a/libs/cli/schemas/schema.v0.json +++ b/libs/cli/schemas/schema.v0.json @@ -587,6 +587,20 @@ } ], "description": "Optional. Defines which headers are excluded from logging." + }, + "middleware_order": { + "anyOf": [ + { + "enum": [ + "auth_first", + "middleware_first" + ] + }, + { + "type": "null" + } + ], + "description": "Optional. Defines the order in which to apply server customizations.\n" } }, "required": [] diff --git a/libs/cli/tests/unit_tests/test_util.py b/libs/cli/tests/unit_tests/test_util.py index c26e776e9..f4b439959 100644 --- a/libs/cli/tests/unit_tests/test_util.py +++ b/libs/cli/tests/unit_tests/test_util.py @@ -9,17 +9,17 @@ def test_clean_empty_lines(): input_str = "line1\n\nline2\n\nline3" result = clean_empty_lines(input_str) assert result == "line1\nline2\nline3" - + # Test with no empty lines input_str = "line1\nline2\nline3" result = clean_empty_lines(input_str) assert result == "line1\nline2\nline3" - + # Test with only empty lines input_str = "\n\n\n" result = clean_empty_lines(input_str) assert result == "" - + # Test empty string input_str = "" result = clean_empty_lines(input_str) @@ -29,33 +29,51 @@ def test_clean_empty_lines(): def test_warn_non_wolfi_distro_with_debian(capsys): """Test that warning is shown when image_distro is 'debian'.""" config = {"image_distro": "debian"} - + warn_non_wolfi_distro(config) - + captured = capsys.readouterr() - assert "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security." in captured.out - assert "Wolfi is a security-oriented, minimal Linux distribution designed for containers." in captured.out - assert 'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.' in captured.out + assert ( + "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security." + in captured.out + ) + assert ( + "Wolfi is a security-oriented, minimal Linux distribution designed for containers." + in captured.out + ) + assert ( + 'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.' + in captured.out + ) def test_warn_non_wolfi_distro_with_default_debian(capsys): """Test that warning is shown when image_distro is missing (defaults to debian).""" config = {} # No image_distro key, should default to debian - + warn_non_wolfi_distro(config) - + captured = capsys.readouterr() - assert "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security." in captured.out - assert "Wolfi is a security-oriented, minimal Linux distribution designed for containers." in captured.out - assert 'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.' in captured.out + assert ( + "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security." + in captured.out + ) + assert ( + "Wolfi is a security-oriented, minimal Linux distribution designed for containers." + in captured.out + ) + assert ( + 'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.' + in captured.out + ) def test_warn_non_wolfi_distro_with_wolfi(capsys): """Test that no warning is shown when image_distro is 'wolfi'.""" config = {"image_distro": "wolfi"} - + warn_non_wolfi_distro(config) - + captured = capsys.readouterr() assert captured.out == "" # No output should be generated @@ -63,42 +81,57 @@ def test_warn_non_wolfi_distro_with_wolfi(capsys): def test_warn_non_wolfi_distro_with_other_distro(capsys): """Test that warning is shown when image_distro is something other than 'wolfi'.""" config = {"image_distro": "ubuntu"} - + warn_non_wolfi_distro(config) - + captured = capsys.readouterr() - assert "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security." in captured.out - assert "Wolfi is a security-oriented, minimal Linux distribution designed for containers." in captured.out - assert 'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.' in captured.out + assert ( + "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security." + in captured.out + ) + assert ( + "Wolfi is a security-oriented, minimal Linux distribution designed for containers." + in captured.out + ) + assert ( + 'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.' + in captured.out + ) def test_warn_non_wolfi_distro_output_formatting(): """Test that the warning output is properly formatted with colors and empty line.""" config = {"image_distro": "debian"} - - with patch('click.secho') as mock_secho: + + with patch("click.secho") as mock_secho: warn_non_wolfi_distro(config) - + # Verify click.secho was called with the correct parameters expected_calls = [ ( - ("⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security.",), - {"fg": "yellow", "bold": True} + ( + "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security.", + ), + {"fg": "yellow", "bold": True}, ), ( - (" Wolfi is a security-oriented, minimal Linux distribution designed for containers.",), - {"fg": "yellow"} + ( + " Wolfi is a security-oriented, minimal Linux distribution designed for containers.", + ), + {"fg": "yellow"}, ), ( - (' To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.',), - {"fg": "yellow"} + ( + ' To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.', + ), + {"fg": "yellow"}, ), ( ("",), # Empty line - {} - ) + {}, + ), ] - + assert mock_secho.call_count == 4 for i, (expected_args, expected_kwargs) in enumerate(expected_calls): actual_call = mock_secho.call_args_list[i] @@ -117,16 +150,18 @@ def test_warn_non_wolfi_distro_various_configs(capsys): ({"image_distro": "ubuntu"}, True, "ubuntu distro"), ({"other_config": "value"}, True, "unrelated config keys"), ] - + for config, should_warn, description in test_cases: # Clear any previous output capsys.readouterr() - + warn_non_wolfi_distro(config) - + captured = capsys.readouterr() if should_warn: - assert "⚠️ Security Recommendation" in captured.out, f"Should warn for {description}" + assert "⚠️ Security Recommendation" in captured.out, ( + f"Should warn for {description}" + ) assert "Wolfi" in captured.out, f"Should mention Wolfi for {description}" else: assert captured.out == "", f"Should not warn for {description}" @@ -137,7 +172,7 @@ def test_warn_non_wolfi_distro_return_value(): config = {"image_distro": "debian"} result = warn_non_wolfi_distro(config) assert result is None - + config = {"image_distro": "wolfi"} result = warn_non_wolfi_distro(config) assert result is None @@ -147,7 +182,7 @@ def test_warn_non_wolfi_distro_does_not_modify_config(): """Test that warn_non_wolfi_distro does not modify the input config.""" original_config = {"image_distro": "debian", "other_key": "value"} config_copy = original_config.copy() - + warn_non_wolfi_distro(config_copy) - - assert config_copy == original_config # Config should remain unchanged \ No newline at end of file + + assert config_copy == original_config # Config should remain unchanged