.NET: feat(dotnet): Add LocalCodeAct package for local Python execution#6105
.NET: feat(dotnet): Add LocalCodeAct package for local Python execution#6105eavanvalkenburg wants to merge 15 commits into
Conversation
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 91%
✓ Correctness
This PR has several critical correctness bugs: (1) ProcessBridge deserializes JSON to Dictionary<string, object?> which yields JsonElement values, then uses
as Dictionary<string, object?>andConvert.ToInt32()casts that always fail, silently discarding subprocess results and crashing tool calls; (2) LocalCodeActProvider overides a non-existentProvideContextAsyncmethod instead of the actual base class methodProvideAIContextAsync(InvokingContext, CancellationToken)with incompatible signature and return type; (3) The runner script path is incorrectly passed as the validator script path to CodeValidator; (4) validator.py has no__main__entry point, so AST validation always passes without actually validating.
✓ Security Reliability
The PR introduces a local Python code execution package with critical security and reliability issues: (1) the
runnerScriptparameter is incorrectly passed toCodeValidatoras thevalidatorScript, which means if a custom runner script is configured alongside validation lists, the validation step will execute the code instead of validating it; (2) theCodeValidatorsubprocess has no timeout, creating a potential hang/DoS vector; (3) the_call_toolin runner.py usesid(kwargs)as call_id which can produce 64-bit integers that would overflow when the .NET side usesConvert.ToInt32.
✓ Test Coverage
The test suite covers only construction, metadata, and default values for 3 of the 5 production classes. There are no tests for the core behavior: LocalCodeActProvider.ProvideContextAsync (tool injection and disposal guard), LocalExecuteCodeFunction.InvokeCoreAsync (argument validation, post-dispose exception), CodeValidator.ValidateAsync, or ProcessBridge.RunAsync. The PR rationale acknowledges 'Full execution tests require Python installed and will be added in follow-up,' but several behavioral tests are achievable without Python (e.g., ProvideContextAsync output, InvokeCoreAsync missing 'code' argument, ObjectDisposedException after dispose, CodeValidator construction). Given this is marked as having 'remaining work' for additional tests, these are suggestions rather than blocking issues.
✗ Design Approach
The overall package shape matches the intended CodeAct surface, but I found three design-level blockers in the execution path: the default runner path cannot launch the bundled runner, successful subprocess results are silently discarded when parsed back into .NET, and the default constructor bypasses AST validation even though the package README documents validation as the default behavior.
Flagged Issues
- ProcessBridge.cs:46 — the default path invokes
python -m agent_framework_local_codeact._runner, but the runner is only shipped as an embedded .NET resource with no extraction logic (unlike CodeValidator which extracts its embedded script). Default construction always fails with ModuleNotFoundError. - ProcessBridge.cs:128-151 —
JsonSerializer.Deserialize<Dictionary<string, object?>>materializes nested JSON as boxedJsonElementvalues. Theas Dictionary<string, object?>cast always returns null, silently discarding all subprocess stdout/stderr/output on every successful execution. - LocalExecuteCodeFunction.cs:71-80 —
CodeValidatoris only instantiated when custom allow/block lists are supplied, contradicting README.md:91 which documents validation as the default behavior. The default constructor path runs unvalidated Python.
Automated review by eavanvalkenburg's agents
There was a problem hiding this comment.
Pull request overview
Adds a new .NET package (Microsoft.Agents.AI.LocalCodeAct) intended to run LLM-generated Python in a local subprocess with JSON-lines IPC, plus unit tests and a sample showing basic wiring.
Changes:
- Introduces a new LocalCodeAct implementation (provider +
execute_codefunction) with embedded Python runner/validator resources. - Adds initial unit test project covering basic construction/metadata/defaults.
- Adds a new sample project and README documentation for usage and configuration.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Microsoft.Agents.AI.LocalCodeAct.csproj | New packable project; embeds Resources/*.py and exposes internals to tests. |
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/ProcessBridge.cs | Parent-side subprocess IPC bridge + tool dispatch (currently has critical JSON/type and runner-resolution issues). |
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/CodeValidator.cs | .NET wrapper intended to run embedded Python AST validator (currently mismatched with validator script behavior). |
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalExecuteCodeFunction.cs | execute_code tool implementation (currently mismatched with repo’s AIFunction API and validation wiring). |
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/LocalCodeActProvider.cs | AIContextProvider integration (currently overrides a non-existent method in this repo). |
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/ProcessExecutionLimits.cs | Resource limit record (timeout/stdout/stderr/file size limits). |
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/FileMount.cs | File mount type definitions (host path + logical mount path + mode). |
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/ExecutionMode.cs | ExecutionMode enum (Subprocess-only). |
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Resources/runner.py | Embedded Python child runner implementing JSON-lines protocol + tool calls. |
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Resources/validator.py | Embedded AST validator (currently missing CLI/stdio protocol entrypoint). |
| dotnet/src/Microsoft.Agents.AI.LocalCodeAct/README.md | Package documentation and examples (currently diverges from actual API/behavior). |
| dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/Microsoft.Agents.AI.LocalCodeAct.UnitTests.csproj | New unit test project referencing the new package. |
| dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/LocalExecuteCodeFunctionTests.cs | Basic tests for construction/metadata/disposal (currently missing using System;). |
| dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/ProcessExecutionLimitsTests.cs | Tests for default/custom limit values. |
| dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/FileMountTests.cs | Tests for FileMount defaults/customization. |
| dotnet/samples/LocalCodeAct/LocalCodeAct.csproj | New sample project referencing LocalCodeAct + core agent project. |
| dotnet/samples/LocalCodeAct/Program.cs | Sample demonstrating provider/function instantiation (currently missing using System / System.Threading.Tasks). |
| dotnet/samples/LocalCodeAct/README.md | Sample README with safety guidance and run instructions. |
File Mount Support and Integration Tests AddedCommit: fad8409 New Features
Implementation ParityAll features from Python agent-framework-local-codeact are now implemented in .NET:
Note: Tests are marked to skip when Python is not available, allowing the package to build and basic tests to pass even without Python installed. |
Complete rewrite that follows the Hyperlight package conventions (see Microsoft.Agents.AI.Hyperlight) and addresses all 24 review comments on PR microsoft#6105: Architectural fixes: * LocalCodeActProvider now uses options-class constructor pattern matching HyperlightCodeActProvider. * Override of ProvideAIContextAsync uses the correct (InvokingContext, CancellationToken) signature returning ValueTask<AIContext>. * ExecuteCodeFunction follows the AIFunction Name/Description/JsonSchema property pattern with InvokeCoreAsync override. * Provider exposes AddTools/GetTools/RemoveTools/ClearTools and AddFileMounts/GetFileMounts/RemoveFileMounts/ClearFileMounts CRUD methods, with snapshot-at-invocation semantics under a lock. Runtime/security fixes: * Subprocess IPC uses JsonObject/JsonNode end-to-end (no Dictionary<string, object?> casts that broke under JsonElement deserialization). * Validator runs in its own subprocess with a dedicated timeout (ProcessExecutionLimits.ValidationTimeoutSeconds), never reuses the runner script. * Validation enabled by default; can be opt-ed out via ValidationEnabled = false. * validator.py has a __main__ entrypoint that reads JSON from stdin and exits with structured errors. * validator.py is now compatible with Python 3.9+ (Match nodes added conditionally). * call_id parsed as long to match Python id(kwargs) range. Other: * README rewritten with valid C# syntax (options-class, FileMount constructor) and accurate descriptions of validator and file capture behavior. * Added integration tests that exercise the real subprocess and validator (skipped gracefully when python3 is not on PATH). * All 18 tests pass (15 unit + 3 integration) across net8/net9/net10. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
99ff847 to
fca0bbc
Compare
Complete rewrite that follows the Hyperlight package conventions (see Microsoft.Agents.AI.Hyperlight) and addresses all 24 review comments on PR microsoft#6105: Architectural fixes: * LocalCodeActProvider now uses options-class constructor pattern matching HyperlightCodeActProvider. * Override of ProvideAIContextAsync uses the correct (InvokingContext, CancellationToken) signature returning ValueTask<AIContext>. * ExecuteCodeFunction follows the AIFunction Name/Description/JsonSchema property pattern with InvokeCoreAsync override. * Provider exposes AddTools/GetTools/RemoveTools/ClearTools and AddFileMounts/GetFileMounts/RemoveFileMounts/ClearFileMounts CRUD methods, with snapshot-at-invocation semantics under a lock. Runtime/security fixes: * Subprocess IPC uses JsonObject/JsonNode end-to-end (no Dictionary<string, object?> casts that broke under JsonElement deserialization). * Validator runs in its own subprocess with a dedicated timeout (ProcessExecutionLimits.ValidationTimeoutSeconds), never reuses the runner script. * Validation enabled by default; can be opt-ed out via ValidationEnabled = false. * validator.py has a __main__ entrypoint that reads JSON from stdin and exits with structured errors. * validator.py is now compatible with Python 3.9+ (Match nodes added conditionally). * call_id parsed as long to match Python id(kwargs) range. Other: * README rewritten with valid C# syntax (options-class, FileMount constructor) and accurate descriptions of validator and file capture behavior. * Added integration tests that exercise the real subprocess and validator (skipped gracefully when python3 is not on PATH). * All 18 tests pass (15 unit + 3 integration) across net8/net9/net10. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create Microsoft.Agents.AI.LocalCodeAct package with: - Project file with embedded Python resources - ExecutionMode enum (Subprocess only) - ProcessExecutionLimits record - FileMount record and FileMountMode enum - README.md documentation - Embedded Python runner and validator scripts This is the .NET equivalent of the Python agent-framework-local-codeact package. Next: Implement process bridge and tool integration.
Copy Python runner and validator scripts from the Python implementation as embedded resources for the .NET package.
Implement CodeValidator.cs that: - Extracts embedded Python validator script to temp file - Invokes Python validator with JSON request - Passes custom allow/block lists - Throws CodeValidationException on failures - Cleans up temp files Uses the embedded Resources/validator.py for AST validation.
Implement LocalExecuteCodeFunction as AIFunction: - Accepts Python executable path (required) - Registers host tools for code to call - Validates code via CodeValidator if custom lists provided - Executes via ProcessBridge - Converts result dict to ChatMessage list - Builds dynamic description including available tools Matches Python LocalExecuteCodeTool functionality.
Implement AIContextProvider that: - Injects execute_code tool into context - Adds CodeAct instructions - Enforces single-provider-per-agent via StateKeys - Wraps LocalExecuteCodeFunction lifecycle Minimal provider implementation matching Python LocalCodeActProvider.
Add unit tests: - LocalExecuteCodeFunctionTests (4 tests) - ProcessExecutionLimitsTests (2 tests) - FileMountTests (2 tests) Add sample: - LocalCodeAct/Program.cs - Demonstrates provider and function usage - LocalCodeAct/README.md - Documentation and safety warnings Tests verify basic construction, metadata, and disposal. Sample shows provider creation, function setup, and configuration. Note: Build requires .NET 10 SDK per global.json.
Add sample demonstrating: - LocalCodeActProvider creation and configuration - LocalExecuteCodeFunction direct usage - Execution modes and file mount configuration - Safety warnings and prerequisites Includes project file and README with security guidance.
- Added FileMountHelper.cs for file mount normalization, snapshot, and capture - Updated LocalExecuteCodeFunction to support file mounts parameter - Added file snapshot before/after execution with capture logic - Updated LocalCodeActProvider to pass file mounts through - Created comprehensive IntegrationTests.cs with 10 test cases: - Simple code execution - Timeout handling - Syntax error handling - Blocked import validation - Blocked builtin validation - Custom allowed imports - File mount read/write with capture - Stdout capture - Provider tool injection All features from Python implementation now ported to .NET.
Complete rewrite that follows the Hyperlight package conventions (see Microsoft.Agents.AI.Hyperlight) and addresses all 24 review comments on PR microsoft#6105: Architectural fixes: * LocalCodeActProvider now uses options-class constructor pattern matching HyperlightCodeActProvider. * Override of ProvideAIContextAsync uses the correct (InvokingContext, CancellationToken) signature returning ValueTask<AIContext>. * ExecuteCodeFunction follows the AIFunction Name/Description/JsonSchema property pattern with InvokeCoreAsync override. * Provider exposes AddTools/GetTools/RemoveTools/ClearTools and AddFileMounts/GetFileMounts/RemoveFileMounts/ClearFileMounts CRUD methods, with snapshot-at-invocation semantics under a lock. Runtime/security fixes: * Subprocess IPC uses JsonObject/JsonNode end-to-end (no Dictionary<string, object?> casts that broke under JsonElement deserialization). * Validator runs in its own subprocess with a dedicated timeout (ProcessExecutionLimits.ValidationTimeoutSeconds), never reuses the runner script. * Validation enabled by default; can be opt-ed out via ValidationEnabled = false. * validator.py has a __main__ entrypoint that reads JSON from stdin and exits with structured errors. * validator.py is now compatible with Python 3.9+ (Match nodes added conditionally). * call_id parsed as long to match Python id(kwargs) range. Other: * README rewritten with valid C# syntax (options-class, FileMount constructor) and accurate descriptions of validator and file capture behavior. * Added integration tests that exercise the real subprocess and validator (skipped gracefully when python3 is not on PATH). * All 18 tests pass (15 unit + 3 integration) across net8/net9/net10. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The embedded Python validator script used by the .NET LocalCodeAct package now enforces the builtin allow-list, matching the latest behavior of agent_framework_local_codeact._validator. Names that are real Python builtins must appear in the allow-list, while unknown names (user-defined functions, registered tools) remain allowed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fca0bbc to
cb11573
Compare
Mirrors the Python foundry_hosted_agent.py sample for the local-codeact package: registers compute and fetch_data as sandbox-only host tools on LocalCodeActProvider so the model only sees execute_code and reaches them via await call_tool(...). Includes the standard hosted-agent supporting files (agent.yaml, agent.manifest.yaml, Dockerfile, Dockerfile.contributor, .env.example, README.md) and installs python3 in the container images so the embedded runner and validator can execute. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirror the Python package change: the embedded validator.py invoked by the
.NET ProcessBridge replaces the os.* deny-list with an allow-list of
{environ, path}. Add allowed_os_attrs parameter to validate_code and
_CodeValidator, and surface it via the stdin JSON request schema so the
.NET host can opt in to a broader allow-list when needed.
Default behavior tightens to match the documented contract: any os.*
attribute outside {environ, path} (for example os.listdir, os.open,
os.getcwd) is rejected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Automated Code Review
Reviewers: 2 | Confidence: 89%
✓ Test Coverage
The PR adds 18 tests covering constructor validation, property defaults, provider wiring, and 3 integration tests. However, several critical internal components have no unit-level test coverage: FileMountHelper's capture-limit enforcement paths (3 separate branches), ProcessBridge's tool-call error handling (unknown tool, missing name, tool exception), CodeValidator's timeout and error extraction logic, and the various code-parameter type conversions in InvokeCoreAsync. The integration tests require Python on PATH and will be skipped in environments without it, meaning CI may have zero coverage of actual execution paths.
✗ Design Approach
LocalCodeActProviderOptionsand the package README both say that omittingEnvironmentyields a minimal, non-inherited environment, butProcessBridgereturns early on the null path and therefore inherits the full parent process environment instead.
Flagged Issues
-
ProcessBridgeinherits the full parent environment whenEnvironmentis null (ProcessBridge.cs:110-112), contradicting the documented invariant inLocalCodeActProviderOptionsand the README that the default is a minimal, non-inherited environment.
Automated review by eavanvalkenburg's agents
- validator.py: enforce os.* allow-list on `from os import X` so names like
`system`, `getcwd` cannot bypass the visit_Attribute restriction.
- ProcessBridge.ConfigureEnvironment: document that null Environment inherits
the parent env (matching real behavior) and update the public
LocalCodeActProviderOptions.Environment doc to describe the explicit
empty-dictionary opt-in for a scrubbed environment.
- Tests:
* FileMountHelperTests covers per-file, per-mount, and total
capture-limit branches that return TextContent omissions.
* Integration tests cover unknown-tool dispatch error, tool throwing
exception, and CodeValidator timeout that kills the process and
raises CodeValidationException.
- Sample: drop unused `Microsoft.Agents.AI.Foundry` using in
Hosted-LocalCodeAct/Program.cs to satisfy IDE0005 check-format.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dotnet/samples/LocalCodeAct/ scaffolding sample referenced APIs that don't exist in the current package (`ExecutionMode`, FileMount object-initializer syntax, the old LocalExecuteCodeFunction constructor signature, function.Metadata.*), produced a long list of check-format violations (CHARSET, IMPORTS, IDE0073 header, IDE0005 unused using, IDE1006 Async suffix, RCS1037 trailing whitespace), and did not match any of the documented sample layouts. The hosted-agent example at dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct is the supported entry-point sample for this package. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add UTF-8 BOM to source files (CHARSET) - Remove unused using directives (IDE0005) - Simplify type names (IDE0001/IDE0002/IDE0090) - Rename static field JsonOptions -> s_jsonOptions (IDE1006) - Rename static field SyncRoot -> s_syncRoot (IDE1006) - Add missing this. qualifications in ProcessBridge (IDE0009) - Remove unused _options field from LocalCodeActProvider (IDE0052) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Motivation and Context
Microsoft Foundry hosted agents are already container-isolated, but agent code today has no way to let an LLM author and run Python in-process inside that sandbox. This PR adds a CodeAct provider for .NET that complements the existing
HyperlightandMontyproviders with an in-process Python subprocess option, intended for hosts that have already established their own isolation boundary (Foundry hosted agents, Container Apps, dedicated VMs).This is the .NET counterpart of the Python
agent-framework-local-codeactpackage (PR #6091); both packages share the same Python validator/runner scripts so behavior stays aligned across languages.Description
Adds a new
Microsoft.Agents.AI.LocalCodeActpackage plus a unit-test project and two samples.Package (
dotnet/src/Microsoft.Agents.AI.LocalCodeAct):LocalCodeActProvider—AIContextProviderthat injects anexecute_codeAIFunctionand per-run system instructions describing host tools and mounts.LocalCodeActProviderOptions— approval mode, host tools, file mounts, execution limits, environment, Python executable/runner overrides, and allow/block-list overrides for imports/builtins.LocalExecuteCodeFunction(viaInternal/ExecuteCodeFunction) — theexecute_codetool surface invoked by the model.Internal/ProcessBridge— JSON-lines IPC between host and Python subprocess (request framing, tool callbacks, response parsing, timeouts).Internal/CodeExecutor— orchestrates validation → subprocess execution → captured-file collection.Internal/CodeValidator— invokes the embedded Python validator via stdin JSON and converts errors toCodeValidationException.Internal/FileMountHelper— normalizes mount paths, snapshots writable mounts before execution, and captures new/modified files after.Internal/InstructionBuilder— renders system instructions that document the tool surface, mounts, and policy.FileMount,ProcessExecutionLimits,LocalCodeActProviderOptions,CodeValidationException— public types.Resources/runner.py,Resources/validator.py— embedded Python scripts kept in sync with the Python package; the validator script is identical apart from a_main()stdin entrypoint used by the .NET host.Tests (
dotnet/tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests):18 tests covering option validation, instruction-builder output, file-mount normalization, capture limits, provider wiring, and end-to-end Python subprocess integration (
LocalExecuteCodeFunctionIntegrationTests).Samples:
dotnet/samples/GettingStarted/AgentProviders/LocalCodeAct— minimal getting-started console sample.dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct— Foundry hosted-agent sample withComputeandFetchDatatools (mirrors the Pythonfoundry_hosted_agentsample and the existingHosted-LocalToolslayout). IncludesProgram.cs,HostedLocalCodeAct.csproj,agent.yaml,agent.manifest.yaml,Dockerfile+Dockerfile.contributor(install Python 3 and setLOCAL_CODEACT_PYTHON=python3),.env.example, and a README with the standard run/curl flow.Validation policy:
os.*attribute allow-list of{environ, path}only; every otheros.<attr>is rejected. Override via the validator'sallowed_os_attrsJSON field.read-writemounts; the capture helpers skip symlinks and hardlinks and verify that every entry's resolved path stays under the mount root, so a virtual mount path cannot be redirected (via symlink, hardlink, junction, or..) to data outside its host directory.Contribution Checklist
Related: companion Python PR #6091.