From c97c3e6aa7b805c4e0d8d07db479e82eb6102f7e Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:28:54 +0000 Subject: [PATCH 1/3] .Net: Bump .Net packages, minor versions only (#13452) ### Motivation and Context ### Description ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- dotnet/Directory.Packages.props | 154 +++++++++--------- dotnet/SK-dotnet.slnx | 72 -------- .../Extensions/AgentDefinitionExtensions.cs | 10 +- .../Yaml/AzureAIKernelAgentYamlTests.cs | 3 +- .../samples/AgentUtilities/BaseAzureTest.cs | 6 +- 5 files changed, 89 insertions(+), 156 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index b282f3a37f95..6feaea959ef7 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -20,29 +20,29 @@ - + - - - - + + + + - - - + + + - + - - + + - - - - - + + + + + @@ -64,30 +64,33 @@ - + - - + + - - - - - - - - - + + + + + + + + + + + + - - + + - + @@ -95,66 +98,67 @@ - + - - - - - - + + + + + + - - + + - + - - + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + @@ -167,10 +171,10 @@ - - + + - + @@ -199,7 +203,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -225,8 +229,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/dotnet/SK-dotnet.slnx b/dotnet/SK-dotnet.slnx index ea1e02fd7de6..b2ff323d726c 100644 --- a/dotnet/SK-dotnet.slnx +++ b/dotnet/SK-dotnet.slnx @@ -27,58 +27,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -123,26 +71,6 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs b/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs index 6a3fba42584e..47617b3fd104 100644 --- a/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs +++ b/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; using Azure.AI.Agents.Persistent; using Azure.AI.Projects; using Azure.Core; -using Azure.Core.Pipeline; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel.Http; @@ -156,8 +156,8 @@ public static AIProjectClient GetProjectsClient(this AgentDefinition agentDefini AIProjectClientOptions options = new() { - Transport = new HttpClientTransport(httpClient), - RetryPolicy = new RetryPolicy(maxRetries: 0) // Disable retry policy if a custom HttpClient is provided. + Transport = new HttpClientPipelineTransport(httpClient), + RetryPolicy = new ClientRetryPolicy(maxRetries: 0) // Disable retry policy if a custom HttpClient is provided. }; return new AIProjectClient(new Uri(endpoint), tokenCredential, options); } @@ -319,9 +319,9 @@ private static OpenApiToolDefinition CreateOpenApiToolDefinition(AgentToolDefini private static IEnumerable GetConnectionIds(this AIProjectClient projectClient, AgentToolDefinition tool) { HashSet connections = [.. tool.GetToolConnections()]; - Connections connectionClient = projectClient.GetConnectionsClient(); + AIProjectConnectionsOperations connectionOperations = projectClient.Connections; return - connectionClient.GetConnections() + connectionOperations.GetConnections() .Where(connection => connections.Contains(connection.Name)) .Select(connection => connection.Id); } diff --git a/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs b/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs index e79020daf82c..0a2139542733 100644 --- a/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs +++ b/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.ClientModel.Primitives; using System.ComponentModel; using System.Net; using System.Net.Http; @@ -57,7 +58,7 @@ public AzureAIKernelAgentYamlTests() new FakeTokenCredential(), new AIProjectClientOptions { - Transport = new HttpClientTransport(this._projectHttpClient) + Transport = new HttpClientPipelineTransport(this._projectHttpClient) }); builder.Services.AddSingleton(projectClient); diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAzureTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAzureTest.cs index 5d2da75b00cf..a0584909760d 100644 --- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAzureTest.cs +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAzureTest.cs @@ -28,9 +28,9 @@ protected AIProjectClient CreateFoundryProjectClient() protected async Task GetConnectionId(string connectionName) { AIProjectClient client = CreateFoundryProjectClient(); - Connections connectionClient = client.GetConnectionsClient(); - Connection connection = - await connectionClient.GetConnectionsAsync().Where(connection => connection.Name == connectionName).FirstOrDefaultAsync() ?? + AIProjectConnectionsOperations connectionOperations = client.Connections; + AIProjectConnection connection = + await connectionOperations.GetConnectionsAsync().Where(connection => connection.Name == connectionName).FirstOrDefaultAsync() ?? throw new InvalidOperationException($"Connection '{connectionName}' not found in project '{TestConfiguration.AzureAI.Endpoint}'."); return connection.Id; } From f511c965c1145a6dc0b19f4958a06cdbde6244da Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Mon, 19 Jan 2026 04:22:19 +0100 Subject: [PATCH 2/3] Python: inversed the filter logic for InMemory vector stores (#13457) ### Motivation and Context Fixes some issues with the forbidden list approach for the in-memory vector store filtering. ### Description ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .../semantic_kernel/connectors/in_memory.py | 148 +++++++++++++++--- .../test_conversation_summary_plugin_unit.py | 28 ---- 2 files changed, 130 insertions(+), 46 deletions(-) diff --git a/python/semantic_kernel/connectors/in_memory.py b/python/semantic_kernel/connectors/in_memory.py index b90ba71bf999..e60ee0f18661 100644 --- a/python/semantic_kernel/connectors/in_memory.py +++ b/python/semantic_kernel/connectors/in_memory.py @@ -92,6 +92,80 @@ class InMemoryCollection( supported_key_types: ClassVar[set[str] | None] = {"str", "int", "float"} supported_search_types: ClassVar[set[SearchType]] = {SearchType.VECTOR} + # Allowlist of AST node types permitted in filter expressions. + # This can be overridden in subclasses to extend or restrict allowed operations. + allowed_filter_ast_nodes: ClassVar[set[type]] = { + ast.Expression, + ast.Lambda, + ast.arguments, + ast.arg, + # Comparisons and boolean operations + ast.Compare, + ast.BoolOp, + ast.UnaryOp, + ast.And, + ast.Or, + ast.Not, + ast.Eq, + ast.NotEq, + ast.Lt, + ast.LtE, + ast.Gt, + ast.GtE, + ast.In, + ast.NotIn, + ast.Is, + ast.IsNot, + # Data access + ast.Name, + ast.Load, + ast.Attribute, + ast.Subscript, + ast.Index, # For Python 3.8 compatibility + ast.Slice, + # Literals + ast.Constant, + ast.List, + ast.Tuple, + ast.Set, + ast.Dict, + # Basic arithmetic (useful for computed comparisons) + ast.BinOp, + ast.Add, + ast.Sub, + ast.Mult, + ast.Div, + ast.Mod, + ast.FloorDiv, + # Function calls (restricted to safe builtins separately) + ast.Call, + } + + # Allowlist of function/method names that can be called in filter expressions. + allowed_filter_functions: ClassVar[set[str]] = { + "len", + "str", + "int", + "float", + "bool", + "abs", + "min", + "max", + "sum", + "any", + "all", + "lower", + "upper", + "strip", + "startswith", + "endswith", + "contains", + "get", + "keys", + "values", + "items", + } + def __init__( self, record_type: type[TModel], @@ -100,7 +174,17 @@ def __init__( embedding_generator: EmbeddingGeneratorBase | None = None, **kwargs: Any, ): - """Create a In Memory Collection.""" + """Create a In Memory Collection. + + In Memory collections are ephemeral and exist only in memory. + They do not persist data to disk or any external storage. + + > [Important] + > Filters are powerful things, so make sure to not allow untrusted input here. + > Filters for this collection are parsed and evaluated using Python's `ast` module, so code might be executed. + > We only allow certain AST nodes and functions to be used in the filter expressions to mitigate security risks. + + """ super().__init__( record_type=record_type, definition=definition, @@ -243,39 +327,67 @@ def _get_filtered_records(self, options: VectorSearchOptions) -> dict[TKey, Attr return filtered_records def _parse_and_validate_filter(self, filter_str: str) -> Callable: - """Parse and validate a string filter as a lambda expression, then return the callable.""" - forbidden_names = { - "__import__", - "open", - "eval", - "exec", - "__builtins__", - "__class__", - "__bases__", - "__subclasses__", - } + """Parse and validate a string filter as a lambda expression, then return the callable. + + Uses an allowlist approach - only explicitly permitted AST node types and function names + are allowed. This can be customized by overriding `allowed_filter_ast_nodes` and + `allowed_filter_functions` class attributes. + """ try: tree = ast.parse(filter_str, mode="eval") except SyntaxError as e: raise VectorStoreOperationException(f"Filter string is not valid Python: {e}") from e - # Only allow lambda expressions + + # Only allow lambda expressions at the top level if not (isinstance(tree, ast.Expression) and isinstance(tree.body, ast.Lambda)): raise VectorStoreOperationException( "Filter string must be a lambda expression, e.g. 'lambda x: x.key == 1'" ) - # Walk the AST to look for forbidden names and attribute access + + # Get the lambda parameter name(s) to allow them as valid Name nodes + lambda_node = tree.body + lambda_param_names = {arg.arg for arg in lambda_node.args.args} + + # Walk the AST to validate all nodes against the allowlist for node in ast.walk(tree): - if isinstance(node, ast.Name) and node.id in forbidden_names: - raise VectorStoreOperationException(f"Use of '{node.id}' is not allowed in filter expressions.") - if isinstance(node, ast.Attribute) and node.attr in forbidden_names: - raise VectorStoreOperationException(f"Use of '{node.attr}' is not allowed in filter expressions.") + node_type = type(node) + + # Check if the node type is allowed + if node_type not in self.allowed_filter_ast_nodes: + raise VectorStoreOperationException( + f"AST node type '{node_type.__name__}' is not allowed in filter expressions." + ) + + # For Name nodes, only allow the lambda parameter + if isinstance(node, ast.Name) and node.id not in lambda_param_names: + raise VectorStoreOperationException( + f"Use of name '{node.id}' is not allowed in filter expressions. " + f"Only the lambda parameter(s) ({', '.join(lambda_param_names)}) can be used." + ) + + # For Call nodes, validate that only allowed functions are called + if isinstance(node, ast.Call): + func_name = None + if isinstance(node.func, ast.Name): + func_name = node.func.id + elif isinstance(node.func, ast.Attribute): + func_name = node.func.attr + + if func_name and func_name not in self.allowed_filter_functions: + raise VectorStoreOperationException( + f"Function '{func_name}' is not allowed in filter expressions. " + f"Allowed functions: {', '.join(sorted(self.allowed_filter_functions))}" + ) + try: code = compile(tree, filename="", mode="eval") func = eval(code, {"__builtins__": {}}, {}) # nosec except Exception as e: raise VectorStoreOperationException(f"Error compiling filter: {e}") from e + if not callable(func): raise VectorStoreOperationException("Compiled filter is not callable.") + return func def _run_filter(self, filter: Callable, record: AttributeDict[TAKey, TAValue]) -> bool: diff --git a/python/tests/unit/core_plugins/test_conversation_summary_plugin_unit.py b/python/tests/unit/core_plugins/test_conversation_summary_plugin_unit.py index 34a3c0450823..1b383b815022 100644 --- a/python/tests/unit/core_plugins/test_conversation_summary_plugin_unit.py +++ b/python/tests/unit/core_plugins/test_conversation_summary_plugin_unit.py @@ -1,15 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. -from unittest.mock import AsyncMock, Mock -import pytest - -from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase -from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings -from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.core_plugins.conversation_summary_plugin import ConversationSummaryPlugin -from semantic_kernel.functions.kernel_arguments import KernelArguments -from semantic_kernel.kernel import Kernel from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig @@ -25,23 +17,3 @@ def test_conversation_summary_plugin_with_deprecated_value(kernel): plugin = ConversationSummaryPlugin(config, kernel=kernel) assert plugin._summarizeConversationFunction is not None assert plugin.return_key == "summary" - - -@pytest.mark.asyncio -async def test_summarize_conversation(kernel: Kernel): - service = AsyncMock(spec=ChatCompletionClientBase) - service.service_id = "default" - service.get_chat_message_contents = AsyncMock( - return_value=[ChatMessageContent(role="assistant", content="Hello World!")] - ) - service.get_prompt_execution_settings_class = Mock(return_value=PromptExecutionSettings) - kernel.add_service(service) - config = PromptTemplateConfig( - name="test", description="test", execution_settings={"default": PromptExecutionSettings()} - ) - kernel.add_plugin(ConversationSummaryPlugin(config), "summarizer") - args = KernelArguments(input="Hello World!") - - await kernel.invoke(plugin_name="summarizer", function_name="SummarizeConversation", arguments=args) - args["summary"] == "Hello world" - service.get_chat_message_contents.assert_called_once() From 1e1fb14d68eb67151473fe8a8e0cc0aef9f57ba1 Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:30:52 +0900 Subject: [PATCH 3/3] Python: Bump Python version to 1.39.2 for a release (#13459) ### Motivation and Context Bump Python version to 1.39.2 for a release ### Description Bump Python version to 1.39.2 for a release ### Contribution Checklist - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- python/semantic_kernel/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/semantic_kernel/__init__.py b/python/semantic_kernel/__init__.py index e5e0ab4d6e53..85fe5e176387 100644 --- a/python/semantic_kernel/__init__.py +++ b/python/semantic_kernel/__init__.py @@ -2,7 +2,7 @@ from semantic_kernel.kernel import Kernel -__version__ = "1.39.1" +__version__ = "1.39.2" DEFAULT_RC_VERSION = f"{__version__}-rc9"