Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,14 @@ public async Task<ExecutionResult> RunAsync(string code, CancellationToken cance

private void ConfigureEnvironment(ProcessStartInfo startInfo)
{
// Null => inherit the parent environment (documented contract on
// LocalCodeActProviderOptions.Environment). Callers wanting a scrubbed
// environment pass an empty dictionary.
if (this._environment is null)
{
return;
}

startInfo.Environment.Clear();
foreach (var kvp in this._environment)

if (this._environment is not null)
{
startInfo.Environment[kvp.Key] = kvp.Value;
foreach (var kvp in this._environment)
{
startInfo.Environment[kvp.Key] = kvp.Value;
}
}

// Without these on Windows, Python may fail to load its standard library.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ public sealed class LocalCodeActProviderOptions
/// Gets or sets environment variables passed to the subprocess.
/// </summary>
/// <remarks>
/// When <see langword="null"/>, the subprocess inherits the parent process environment
/// (the default <see cref="System.Diagnostics.ProcessStartInfo"/> behavior). To run with
/// a restricted environment, supply a dictionary containing only the variables the
/// subprocess should see — pass an empty dictionary for a fully scrubbed environment.
/// The subprocess does not inherit the parent process environment. When this property is
/// <see langword="null"/> or empty, the subprocess runs with a scrubbed environment.
/// Otherwise, the subprocess receives only the variables in the supplied dictionary.
/// On Windows, a small set of system variables (SYSTEMROOT, SYSTEMDRIVE, COMSPEC,
/// PATHEXT, TEMP, TMP) is back-filled from the parent environment when not already
/// present so Python can locate its standard library.
Expand Down
3 changes: 2 additions & 1 deletion dotnet/src/Microsoft.Agents.AI.LocalCodeAct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ scanned for **new** files after execution, and those files are returned as
## Environment Variables

Pass environment variables explicitly. The subprocess does NOT inherit the host
environment by default:
environment by default. On Windows, the system variables required for Python to
load its standard library are retained:

```csharp
using var provider = new LocalCodeActProvider("/usr/bin/python3", new LocalCodeActProviderOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,84 @@ public async Task ExecuteCode_AllowsPermittedOsAccessAsync(string code)
Assert.NotNull(result);
}

[Fact]
public async Task ExecuteCode_DefaultEnvironmentDoesNotInheritParentVariablesAsync()
{
SkipIfNoPython();

// Arrange
var variableName = $"AF_LOCALCODEACT_PARENT_{Guid.NewGuid():N}";
var parentValue = $"parent-value-{Guid.NewGuid():N}";
var originalValue = Environment.GetEnvironmentVariable(variableName);
Environment.SetEnvironmentVariable(variableName, parentValue);

try
{
var function = new LocalExecuteCodeFunction(s_python!);
var args = new AIFunctionArguments
{
["code"] = $"import os\nprint(os.environ.get('{variableName}', 'NOT_FOUND'))",
};

// Act
var result = await function.InvokeAsync(args, CancellationToken.None);

// Assert
var combined = GetResultText(result);
Assert.Contains("NOT_FOUND", combined, StringComparison.Ordinal);
Assert.DoesNotContain(parentValue, combined, StringComparison.Ordinal);
}
finally
{
Environment.SetEnvironmentVariable(variableName, originalValue);
}
}

[Fact]
public async Task ExecuteCode_ExplicitEnvironmentDoesNotInheritOtherParentVariablesAsync()
{
SkipIfNoPython();

// Arrange
var parentVariableName = $"AF_LOCALCODEACT_PARENT_{Guid.NewGuid():N}";
var childVariableName = $"AF_LOCALCODEACT_CHILD_{Guid.NewGuid():N}";
var parentValue = $"parent-value-{Guid.NewGuid():N}";
var childValue = $"child-value-{Guid.NewGuid():N}";
var originalValue = Environment.GetEnvironmentVariable(parentVariableName);
Environment.SetEnvironmentVariable(parentVariableName, parentValue);

try
{
var options = new LocalCodeActProviderOptions
{
Environment = new Dictionary<string, string>
{
[childVariableName] = childValue,
},
};
var function = new LocalExecuteCodeFunction(s_python!, options);
var args = new AIFunctionArguments
{
["code"] =
$"import os\nprint(os.environ.get('{childVariableName}', 'NOT_FOUND'))\n" +
$"print(os.environ.get('{parentVariableName}', 'NOT_FOUND'))",
};

// Act
var result = await function.InvokeAsync(args, CancellationToken.None);

// Assert
var combined = GetResultText(result);
Assert.Contains(childValue, combined, StringComparison.Ordinal);
Assert.Contains("NOT_FOUND", combined, StringComparison.Ordinal);
Assert.DoesNotContain(parentValue, combined, StringComparison.Ordinal);
}
finally
{
Environment.SetEnvironmentVariable(parentVariableName, originalValue);
}
}

[Fact]
public async Task ExecuteCode_CapturesFilesInWritableMountAsync()
{
Expand Down
Loading