From b28e2a2b4846996feee34a5d36fd412f6b003134 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 16 Jul 2026 09:37:38 +0200 Subject: [PATCH 1/2] Fix LocalCodeAct validation and package checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aecf332f-b940-41a7-ac3a-6fbbe9892141 --- .github/workflows/dotnet-build-and-test.yml | 1 + .../Resources/validator.py | 25 ++++++++++- ...ocalExecuteCodeFunctionIntegrationTests.cs | 41 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index e42379cf6d9..f428e53ca28 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -163,6 +163,7 @@ jobs: # Change to project directory to ensure local nuget.config is used pushd consoleapp dotnet add packcheck.csproj package Microsoft.Agents.AI --prerelease + dotnet add packcheck.csproj package Microsoft.Agents.AI.LocalCodeAct --prerelease dotnet build -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} packcheck.csproj # Clean up diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Resources/validator.py b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Resources/validator.py index 69453999d57..a29c6e45ca9 100644 --- a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Resources/validator.py +++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Resources/validator.py @@ -271,6 +271,7 @@ def __init__( self._allowed_builtins = allowed_builtins if allowed_builtins is not None else ALLOWED_BUILTINS self._blocked_builtins = blocked_builtins if blocked_builtins is not None else BLOCKED_BUILTINS self._allowed_os_attrs = allowed_os_attrs if allowed_os_attrs is not None else ALLOWED_OS_ATTRS + self._os_aliases: set[str] = {"os"} def validate(self, code: str) -> None: """Validate code and raise CodeValidationError if it violates policy.""" @@ -303,6 +304,10 @@ def visit_Import(self, node: ast.Import) -> None: self._errors.append(f"Import of '{alias_node.name}' is not allowed (blocked: {module_name})") elif module_name not in self._allowed_imports: self._errors.append(f"Import of '{alias_node.name}' is not allowed (not in allow-list)") + if alias_node.name == "os": + self._os_aliases.add(alias_node.asname or "os") + elif alias_node.name.startswith("os.") and alias_node.asname is None: + self._os_aliases.add("os") self.generic_visit(node) def visit_ImportFrom(self, node: ast.ImportFrom) -> None: @@ -324,6 +329,24 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: self._errors.append(f"Import from 'os' of '{alias_node.name}' is not allowed") self.generic_visit(node) + def visit_Assign(self, node: ast.Assign) -> None: + """Track re-bindings of the ``os`` module.""" + if isinstance(node.value, ast.Name) and node.value.id in self._os_aliases: + for target in node.targets: + if isinstance(target, ast.Name): + self._os_aliases.add(target.id) + self.generic_visit(node) + + def visit_AnnAssign(self, node: ast.AnnAssign) -> None: + """Track annotated re-bindings of the ``os`` module.""" + if ( + isinstance(node.value, ast.Name) + and node.value.id in self._os_aliases + and isinstance(node.target, ast.Name) + ): + self._os_aliases.add(node.target.id) + self.generic_visit(node) + def visit_Call(self, node: ast.Call) -> None: """Validate function calls. @@ -357,7 +380,7 @@ def visit_Attribute(self, node: ast.Attribute) -> None: # Enforce the `os` attribute allow-list. Anything outside `ALLOWED_OS_ATTRS` # (file I/O, process control, mutating helpers, etc.) is rejected so the # validator matches the documented `os.environ` / `os.path`-only contract. - if isinstance(node.value, ast.Name) and node.value.id == "os" and node.attr not in self._allowed_os_attrs: + if isinstance(node.value, ast.Name) and node.value.id in self._os_aliases and node.attr not in self._allowed_os_attrs: self._errors.append(f"Access to os.{node.attr} is not allowed") # Block access to certain dangerous attributes diff --git a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionIntegrationTests.cs index f1e6f6e8b99..93ed040dc5a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionIntegrationTests.cs @@ -64,6 +64,47 @@ await Assert.ThrowsAsync(async () => await function.InvokeAsync(args, CancellationToken.None)); } + [Theory] + [InlineData("import os\nos.system('id')")] + [InlineData("import os as x\nx.system('id')")] + [InlineData("import os\n_o = os\n_o.system('id')")] + [InlineData("import os as x\na = x\nb = a\nb.popen('id')")] + [InlineData("import os.path\nos.system('id')")] + public async Task ExecuteCode_ValidationBlocksDisallowedOsAccessAsync(string code) + { + SkipIfNoPython(); + + var function = new LocalExecuteCodeFunction(s_python!); + + var args = new AIFunctionArguments + { + ["code"] = code, + }; + + var ex = await Assert.ThrowsAsync(async () => + await function.InvokeAsync(args, CancellationToken.None)); + Assert.Contains("os.", ex.Message, StringComparison.Ordinal); + } + + [Theory] + [InlineData("import os\nprint(os.environ.get('PATH') is not None)")] + [InlineData("import os as x\nprint(x.path.join('a', 'b'))")] + [InlineData("import os.path as p\nprint(p.join('a', 'b'))")] + public async Task ExecuteCode_AllowsPermittedOsAccessAsync(string code) + { + SkipIfNoPython(); + + var function = new LocalExecuteCodeFunction(s_python!); + + var args = new AIFunctionArguments + { + ["code"] = code, + }; + + var result = await function.InvokeAsync(args, CancellationToken.None); + Assert.NotNull(result); + } + [Fact] public async Task ExecuteCode_CapturesFilesInWritableMountAsync() { From 83c51149a7c4e1b49aa7286bdba6be0e070cc887 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 16 Jul 2026 11:05:55 +0200 Subject: [PATCH 2/2] Address LocalCodeAct alias review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aecf332f-b940-41a7-ac3a-6fbbe9892141 --- .../Resources/validator.py | 17 +++++++++++++---- .../LocalExecuteCodeFunctionIntegrationTests.cs | 3 +++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Resources/validator.py b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Resources/validator.py index a29c6e45ca9..53b6ca895d0 100644 --- a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Resources/validator.py +++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Resources/validator.py @@ -281,6 +281,7 @@ def validate(self, code: str) -> None: raise CodeValidationError(f"Syntax error in generated code: {exc}") from exc self._errors = [] + self._os_aliases = {"os"} self.visit(tree) if self._errors: @@ -331,10 +332,8 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: def visit_Assign(self, node: ast.Assign) -> None: """Track re-bindings of the ``os`` module.""" - if isinstance(node.value, ast.Name) and node.value.id in self._os_aliases: - for target in node.targets: - if isinstance(target, ast.Name): - self._os_aliases.add(target.id) + for target in node.targets: + self._track_os_alias_targets(target, node.value) self.generic_visit(node) def visit_AnnAssign(self, node: ast.AnnAssign) -> None: @@ -347,6 +346,16 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: self._os_aliases.add(node.target.id) self.generic_visit(node) + def _track_os_alias_targets(self, target: ast.AST, value: ast.AST) -> None: + if isinstance(target, ast.Starred): + target = target.value + + if isinstance(target, ast.Name) and isinstance(value, ast.Name) and value.id in self._os_aliases: + self._os_aliases.add(target.id) + elif isinstance(target, (ast.Tuple, ast.List)) and isinstance(value, (ast.Tuple, ast.List)): + for target_item, value_item in zip(target.elts, value.elts): + self._track_os_alias_targets(target_item, value_item) + def visit_Call(self, node: ast.Call) -> None: """Validate function calls. diff --git a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionIntegrationTests.cs index 93ed040dc5a..45952809de8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionIntegrationTests.cs @@ -70,6 +70,9 @@ await Assert.ThrowsAsync(async () => [InlineData("import os\n_o = os\n_o.system('id')")] [InlineData("import os as x\na = x\nb = a\nb.popen('id')")] [InlineData("import os.path\nos.system('id')")] + [InlineData("import os\na, _ = (os, 1)\na.system('id')")] + [InlineData("import os\n[a, _] = [os, 1]\na.system('id')")] + [InlineData("import os\nx: object = os\nx.system('id')")] public async Task ExecuteCode_ValidationBlocksDisallowedOsAccessAsync(string code) { SkipIfNoPython();