diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index c36bc227d93..c755420cfdd 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -331,6 +331,9 @@
+
+
+
@@ -616,6 +619,7 @@
+
@@ -671,6 +675,7 @@
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.env.example
new file mode 100644
index 00000000000..c5423f93f73
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.env.example
@@ -0,0 +1,6 @@
+FOUNDRY_PROJECT_ENDPOINT=
+ASPNETCORE_URLS=http://+:8088
+ASPNETCORE_ENVIRONMENT=Development
+FOUNDRY_MODEL=gpt-4o
+AZURE_BEARER_TOKEN=DefaultAzureCredential
+LOCAL_CODEACT_PYTHON=python3
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile
new file mode 100644
index 00000000000..1d201d0472d
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile
@@ -0,0 +1,23 @@
+# Use the official .NET 10.0 ASP.NET runtime as a parent image
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
+WORKDIR /app
+
+# Install Python 3 so LocalCodeAct can spawn the embedded runner / validator.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends python3 \
+ && rm -rf /var/lib/apt/lists/*
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+COPY . .
+RUN dotnet restore
+RUN dotnet publish -c Release -o /app/publish
+
+# Final stage
+FROM base AS final
+WORKDIR /app
+COPY --from=build /app/publish .
+EXPOSE 8088
+ENV ASPNETCORE_URLS=http://+:8088
+ENV LOCAL_CODEACT_PYTHON=python3
+ENTRYPOINT ["dotnet", "HostedLocalCodeAct.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile.contributor
new file mode 100644
index 00000000000..c27fb0ab416
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile.contributor
@@ -0,0 +1,24 @@
+# Dockerfile for contributors building from the agent-framework repository source.
+#
+# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry and
+# Microsoft.Agents.AI.LocalCodeAct sources, which means a standard multi-stage
+# Docker build cannot resolve dependencies outside this folder. Instead, pre-publish
+# the app targeting the container runtime and copy the output into the container:
+#
+# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
+# docker build -f Dockerfile.contributor -t hosted-local-codeact .
+# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-local-codeact -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-local-codeact
+#
+# For end-users consuming the NuGet package (not ProjectReference), use the standard
+# Dockerfile which performs a full dotnet restore + publish inside the container.
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
+WORKDIR /app
+
+# Install Python 3 so LocalCodeAct can spawn the embedded runner / validator.
+RUN apk add --no-cache python3
+
+COPY out/ .
+EXPOSE 8088
+ENV ASPNETCORE_URLS=http://+:8088
+ENV LOCAL_CODEACT_PYTHON=python3
+ENTRYPOINT ["dotnet", "HostedLocalCodeAct.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/HostedLocalCodeAct.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/HostedLocalCodeAct.csproj
new file mode 100644
index 00000000000..0c2a417ff7d
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/HostedLocalCodeAct.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ $(NoWarn);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Program.cs
new file mode 100644
index 00000000000..9e5a91503d1
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Program.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// Hosted Local CodeAct sample. Wires Microsoft.Agents.AI.LocalCodeAct into a
+// Foundry hosted agent. The model only sees a single `execute_code` tool;
+// `compute` and `fetch_data` are registered as sandbox-only host tools that
+// generated Python reaches via `await call_tool(...)`. This mirrors the Python
+// `foundry_hosted_agent.py` sample for the local-codeact package.
+//
+// SECURITY: LocalCodeAct executes LLM-generated Python in the agent process.
+// Only deploy this sample to an externally sandboxed environment such as a
+// Foundry hosted-agent container.
+
+using System.ComponentModel;
+using Azure.AI.Projects;
+using Azure.Core;
+using Azure.Identity;
+using DotNetEnv;
+using Hosted_Shared_Contributor_Setup;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry.Hosting;
+using Microsoft.Agents.AI.LocalCodeAct;
+using Microsoft.Extensions.AI;
+
+// Load .env file if present (for local development)
+Env.TraversePath().Load();
+
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
+string pythonExecutable = Environment.GetEnvironmentVariable("LOCAL_CODEACT_PYTHON")
+ ?? (OperatingSystem.IsWindows() ? "python.exe" : "python3");
+
+TokenCredential credential = new ChainedTokenCredential(
+ new DevTemporaryTokenCredential(),
+ new DefaultAzureCredential());
+
+// ── Sandbox-only tools (model never sees these directly) ─────────────────────
+
+[Description("Perform a math operation: add, subtract, multiply, or divide.")]
+static double Compute(
+ [Description("Operation: add, subtract, multiply, or divide.")] string operation,
+ [Description("First numeric operand.")] double a,
+ [Description("Second numeric operand.")] double b) => operation switch
+ {
+ "add" => a + b,
+ "subtract" => a - b,
+ "multiply" => a * b,
+ "divide" => b == 0 ? double.PositiveInfinity : a / b,
+ _ => throw new ArgumentException($"Unknown operation '{operation}'.", nameof(operation)),
+ };
+
+[Description("Fetch records from a named simulated table (users or products).")]
+static IReadOnlyList> FetchData(
+ [Description("Name of the simulated table to query.")] string table)
+{
+ Dictionary>> data = new()
+ {
+ ["users"] =
+ [
+ new Dictionary { ["id"] = 1, ["name"] = "Alice", ["role"] = "admin" },
+ new Dictionary { ["id"] = 2, ["name"] = "Bob", ["role"] = "user" },
+ new Dictionary { ["id"] = 3, ["name"] = "Charlie", ["role"] = "admin" },
+ ],
+ ["products"] =
+ [
+ new Dictionary { ["id"] = 101, ["name"] = "Widget", ["price"] = 9.99 },
+ new Dictionary { ["id"] = 102, ["name"] = "Gadget", ["price"] = 19.99 },
+ ],
+ };
+
+ return data.TryGetValue(table, out var rows) ? rows : [];
+}
+
+// ── LocalCodeAct provider with sandbox-only host tools ───────────────────────
+
+var codeActOptions = new LocalCodeActProviderOptions
+{
+ Tools =
+ [
+ AIFunctionFactory.Create(Compute, name: "compute"),
+ AIFunctionFactory.Create(FetchData, name: "fetch_data"),
+ ],
+ ExecutionLimits = new ProcessExecutionLimits { TimeoutSeconds = 5 },
+};
+
+var codeAct = new LocalCodeActProvider(pythonExecutable, codeActOptions);
+
+// ── Build the hosted agent ───────────────────────────────────────────────────
+
+AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
+ .AsAIAgent(new ChatClientAgentOptions
+ {
+ Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-codeact",
+ Description = "Hosted CodeAct agent with sandbox-only compute and fetch_data tools.",
+ ChatOptions = new ChatOptions
+ {
+ ModelId = deploymentName,
+ Instructions =
+ """
+ You are a helpful assistant. Keep your answers brief. Prefer orchestrating your work
+ in a single `execute_code` block using `await call_tool(...)` over issuing many
+ direct tool calls. The sandbox exposes `compute` and `fetch_data` via `call_tool`.
+ """,
+ },
+ AIContextProviders = [codeAct],
+ });
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddFoundryResponses(agent);
+builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
+
+var app = builder.Build();
+app.MapFoundryResponses();
+
+// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
+// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
+// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
+app.MapDevTemporaryLocalAgentEndpoint();
+
+app.Run();
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md
new file mode 100644
index 00000000000..229ff4f5839
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md
@@ -0,0 +1,159 @@
+# Hosted-LocalCodeAct
+
+A hosted agent that uses [`Microsoft.Agents.AI.LocalCodeAct`](../../../../../src/Microsoft.Agents.AI.LocalCodeAct/README.md)
+to give the model a single `execute_code` tool. Two sandbox-only host tools,
+`compute` and `fetch_data`, are registered on `LocalCodeActProvider` and are
+reachable from inside generated Python via `await call_tool(...)` — never as
+direct LLM tool calls.
+
+This mirrors the Python
+[`foundry_hosted_agent.py`](https://github.com/microsoft/agent-framework/blob/main/python/packages/local_codeact/samples/foundry_hosted_agent.py)
+sample for the `agent-framework-local-codeact` package.
+
+> **⚠️ Security:** LocalCodeAct executes LLM-generated Python in the agent
+> process. The package is not a sandbox — it relies on the Foundry hosted-agent
+> container (or another externally sandboxed environment) for process,
+> filesystem, and network isolation. Do not run this outside of a sandbox.
+
+## Prerequisites
+
+- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
+- Python 3 available on `PATH` (used by `LocalCodeActProvider` to execute the
+ embedded runner and validator). Override with the `LOCAL_CODEACT_PYTHON`
+ environment variable if you need a specific interpreter path.
+- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
+- Azure CLI logged in (`az login`)
+
+## Configuration
+
+Copy the template and fill in your project endpoint:
+
+```bash
+cp .env.example .env
+```
+
+Edit `.env` and set your Azure AI Foundry project endpoint:
+
+```env
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+ASPNETCORE_URLS=http://+:8088
+ASPNETCORE_ENVIRONMENT=Development
+FOUNDRY_MODEL=gpt-4o
+LOCAL_CODEACT_PYTHON=python3
+```
+
+> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
+
+## Running directly (contributors)
+
+This project uses `ProjectReference` to build against the local Agent Framework
+source, including the `Microsoft.Agents.AI.LocalCodeAct` package.
+
+```bash
+cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct
+AGENT_NAME=hosted-local-codeact dotnet run
+```
+
+The agent will start on `http://localhost:8088`.
+
+### Test it
+
+Using the Azure Developer CLI:
+
+```bash
+azd ai agent invoke --local "Fetch all users, find the admins, multiply 7 by 6, and print the users, admins, and the multiplication result. Use execute_code with await call_tool(...)."
+```
+
+Or with curl:
+
+```bash
+curl -X POST http://localhost:8088/responses \
+ -H "Content-Type: application/json" \
+ -d '{"input": "Fetch all users, find the admins, multiply 7 by 6, and print the users, admins, and the multiplication result. Use execute_code with await call_tool(...).", "model": "hosted-local-codeact"}'
+```
+
+## Running with Docker
+
+Since this project uses `ProjectReference`, use `Dockerfile.contributor` which
+takes a pre-published output. The image installs Python 3 so the embedded
+runner and validator scripts can execute.
+
+### 1. Publish for the container runtime (Linux Alpine)
+
+```bash
+dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
+```
+
+### 2. Build the Docker image
+
+```bash
+docker build -f Dockerfile.contributor -t hosted-local-codeact .
+```
+
+### 3. Run the container
+
+Generate a bearer token on your host and pass it to the container:
+
+```bash
+# Generate token (expires in ~1 hour)
+export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
+
+# Run with token
+docker run --rm -p 8088:8088 \
+ -e AGENT_NAME=hosted-local-codeact \
+ -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
+ --env-file .env \
+ hosted-local-codeact
+```
+
+### 4. Test it
+
+```bash
+azd ai agent invoke --local "Fetch all users and print the admins."
+```
+
+## How CodeAct works here
+
+`LocalCodeActProvider` is registered as an `AIContextProvider`. On every run it
+injects:
+
+- A single `execute_code` tool that the model can call with a Python snippet.
+- CodeAct instructions that teach the model to use `await call_tool(...)` for
+ the provider-owned host tools, rather than asking for direct tool calls.
+
+The provider-owned host tools in this sample:
+
+| Tool | Description |
+|------|-------------|
+| `compute(operation, a, b)` | Math operation: `add`, `subtract`, `multiply`, `divide`. |
+| `fetch_data(table)` | Returns rows from a simulated `users` or `products` table. |
+
+`execute_code` runs the generated Python in a separate Python process governed
+by `ProcessExecutionLimits` (5 second timeout in this sample) and the
+default-on AST allow-list validator that rejects disallowed imports, builtins,
+and dynamic-eval constructs before execution.
+
+## Deploying to Foundry (azd spec)
+
+This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent
+spec (`agent.yaml`) for deployment to Foundry.
+
+Initialize an `azd` project from this sample's manifest:
+
+```bash
+mkdir hosted-local-codeact && cd hosted-local-codeact
+azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.manifest.yaml
+```
+
+Then deploy:
+
+```bash
+azd deploy
+```
+
+## NuGet package users
+
+If you are consuming the Agent Framework as a NuGet package (not building from
+source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See
+the commented section in `HostedLocalCodeAct.csproj` for the `PackageReference`
+alternative.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.manifest.yaml
new file mode 100644
index 00000000000..f00aedf60e3
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.manifest.yaml
@@ -0,0 +1,30 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
+name: hosted-local-codeact
+displayName: "Hosted Local CodeAct Agent"
+
+description: >
+ A hosted agent that uses the CodeAct pattern via
+ Microsoft.Agents.AI.LocalCodeAct. The model only sees an `execute_code`
+ tool and orchestrates `compute` and `fetch_data` sandbox-only host tools
+ via `await call_tool(...)` from inside generated Python.
+
+metadata:
+ tags:
+ - AI Agent Hosting
+ - Azure AI AgentServer
+ - Responses Protocol
+ - Local CodeAct
+ - Agent Framework
+
+template:
+ name: hosted-local-codeact
+ kind: hosted
+ protocols:
+ - protocol: responses
+ version: 1.0.0
+ resources:
+ cpu: "0.5"
+ memory: 1Gi
+parameters:
+ properties: []
+resources: []
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.yaml
new file mode 100644
index 00000000000..cdf4b5c9a13
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.yaml
@@ -0,0 +1,9 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
+kind: hosted
+name: hosted-local-codeact
+protocols:
+ - protocol: responses
+ version: 1.0.0
+resources:
+ cpu: "0.5"
+ memory: 1Gi
diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/CodeValidationException.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/CodeValidationException.cs
new file mode 100644
index 00000000000..c81f4d66275
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/CodeValidationException.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.Agents.AI.LocalCodeAct;
+
+///
+/// Exception thrown when AST validation of generated Python code fails.
+///
+public sealed class CodeValidationException : Exception
+{
+ /// Initializes a new instance of the class.
+ public CodeValidationException()
+ {
+ }
+
+ /// Initializes a new instance of the class.
+ /// Validation error message.
+ public CodeValidationException(string message) : base(message)
+ {
+ }
+
+ /// Initializes a new instance of the class.
+ /// Validation error message.
+ /// Underlying exception.
+ public CodeValidationException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/FileMount.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/FileMount.cs
new file mode 100644
index 00000000000..4823bd4e033
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/FileMount.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.LocalCodeAct;
+
+///
+/// File mount access mode.
+///
+public enum FileMountMode
+{
+ /// Read-only access. Files are not scanned for capture after execution.
+ ReadOnly,
+
+ /// Read-write access. New or modified files are captured after execution.
+ ReadWrite,
+}
+
+///
+/// Represents a host directory exposed to locally executed code.
+///
+///
+///
+/// Unlike a true sandbox, mounts in this package expose
+/// directly to the subprocess. The is metadata used to
+/// describe the mount to the model in the function description and to label
+/// captured files. Real isolation must come from the surrounding sandbox
+/// (container, VM, Foundry hosted agent, etc.).
+///
+///
+public sealed class FileMount
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Path on the host filesystem to expose to the subprocess. Must exist.
+ ///
+ /// Logical path used to describe the mount to the model (for example "/input/data.csv").
+ ///
+ /// Access mode for the mount. Defaults to .
+ ///
+ /// Optional per-mount write capture limit (in bytes). When , the global
+ /// applies.
+ ///
+ public FileMount(string hostPath, string mountPath, FileMountMode mode = FileMountMode.ReadWrite, long? writeBytesLimit = null)
+ {
+ this.HostPath = Throw.IfNullOrWhitespace(hostPath);
+ this.MountPath = Throw.IfNullOrWhitespace(mountPath);
+ this.Mode = mode;
+ this.WriteBytesLimit = writeBytesLimit;
+ }
+
+ /// Gets the host filesystem path exposed to the subprocess.
+ public string HostPath { get; }
+
+ /// Gets the logical mount path used to describe the mount to the model.
+ public string MountPath { get; }
+
+ /// Gets the access mode for the mount.
+ public FileMountMode Mode { get; }
+
+ /// Gets the optional per-mount write capture limit (in bytes).
+ public long? WriteBytesLimit { get; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/CodeExecutor.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/CodeExecutor.cs
new file mode 100644
index 00000000000..51d6a7136d1
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/CodeExecutor.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.LocalCodeAct.Internal;
+
+///
+/// Coordinates a single execution: optional validation, snapshot of writable mounts,
+/// running the subprocess, capturing written files, and assembling the final content list.
+///
+internal sealed class CodeExecutor
+{
+ private readonly string _pythonExecutable;
+ private readonly string _runnerScript;
+ private readonly CodeValidator? _validator;
+ private readonly ProcessExecutionLimits _limits;
+ private readonly IReadOnlyDictionary? _environment;
+ private readonly string? _workingDirectory;
+
+ public CodeExecutor(
+ string pythonExecutable,
+ string runnerScript,
+ CodeValidator? validator,
+ ProcessExecutionLimits limits,
+ IReadOnlyDictionary? environment,
+ string? workingDirectory)
+ {
+ this._pythonExecutable = pythonExecutable;
+ this._runnerScript = runnerScript;
+ this._validator = validator;
+ this._limits = limits;
+ this._environment = environment;
+ this._workingDirectory = workingDirectory;
+ }
+
+ /// Immutable snapshot of provider state captured at the start of an invocation.
+ public sealed class RunSnapshot
+ {
+ public RunSnapshot(IReadOnlyList tools, IReadOnlyList fileMounts)
+ {
+ this.Tools = tools;
+ this.FileMounts = fileMounts;
+ }
+
+ public IReadOnlyList Tools { get; }
+
+ public IReadOnlyList FileMounts { get; }
+ }
+
+ public async Task> ExecuteAsync(RunSnapshot snapshot, string code, CancellationToken cancellationToken)
+ {
+ if (this._validator is not null)
+ {
+ await this._validator.ValidateAsync(code, cancellationToken).ConfigureAwait(false);
+ }
+
+ var preState = FileMountHelper.SnapshotWritableMounts(snapshot.FileMounts);
+
+ var bridge = new ProcessBridge(
+ this._pythonExecutable,
+ this._runnerScript,
+ snapshot.Tools,
+ this._limits,
+ this._environment,
+ this._workingDirectory);
+
+ var result = await bridge.RunAsync(code, cancellationToken).ConfigureAwait(false);
+
+ var captured = FileMountHelper.CaptureWrittenFiles(snapshot.FileMounts, preState, this._limits);
+
+ return BuildContents(result, captured);
+ }
+
+ private static List BuildContents(ProcessBridge.ExecutionResult result, List capturedFiles)
+ {
+ var contents = new List();
+
+ if (!string.IsNullOrEmpty(result.Stdout))
+ {
+ var stdoutText = result.StdoutTruncated ? result.Stdout + "\n[stdout truncated]" : result.Stdout;
+ contents.Add(new TextContent(stdoutText));
+ }
+
+ if (!string.IsNullOrEmpty(result.Stderr))
+ {
+ var stderrText = result.StderrTruncated ? result.Stderr + "\n[stderr truncated]" : result.Stderr;
+ contents.Add(new TextContent("stderr:\n" + stderrText));
+ }
+
+ if (result.OutputPresent && result.Output.HasValue)
+ {
+ contents.Add(new TextContent("result:\n" + result.Output.Value.GetRawText()));
+ }
+
+ contents.AddRange(capturedFiles);
+
+ if (contents.Count == 0)
+ {
+ contents.Add(new TextContent("Code executed successfully without output."));
+ }
+
+ return contents;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/CodeValidator.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/CodeValidator.cs
new file mode 100644
index 00000000000..1650870a47d
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/CodeValidator.cs
@@ -0,0 +1,178 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Agents.AI.LocalCodeAct.Internal;
+
+///
+/// Runs the embedded Python AST validator in a child process with a strict timeout.
+///
+internal sealed class CodeValidator
+{
+ private readonly string _pythonExecutable;
+ private readonly string _validatorScript;
+ private readonly TimeSpan _timeout;
+ private readonly IReadOnlyList? _allowedImports;
+ private readonly IReadOnlyList? _blockedImports;
+ private readonly IReadOnlyList? _allowedBuiltins;
+ private readonly IReadOnlyList? _blockedBuiltins;
+
+ public CodeValidator(
+ string pythonExecutable,
+ string validatorScript,
+ TimeSpan timeout,
+ IReadOnlyList? allowedImports,
+ IReadOnlyList? blockedImports,
+ IReadOnlyList? allowedBuiltins,
+ IReadOnlyList? blockedBuiltins)
+ {
+ this._pythonExecutable = pythonExecutable;
+ this._validatorScript = validatorScript;
+ this._timeout = timeout;
+ this._allowedImports = allowedImports;
+ this._blockedImports = blockedImports;
+ this._allowedBuiltins = allowedBuiltins;
+ this._blockedBuiltins = blockedBuiltins;
+ }
+
+ /// Validates Python source code against the configured allow-lists.
+ /// Thrown when validation fails.
+ public async Task ValidateAsync(string code, CancellationToken cancellationToken)
+ {
+ var request = new JsonObject
+ {
+ ["code"] = code,
+ };
+
+ AddList(request, "allowed_imports", this._allowedImports);
+ AddList(request, "blocked_imports", this._blockedImports);
+ AddList(request, "allowed_builtins", this._allowedBuiltins);
+ AddList(request, "blocked_builtins", this._blockedBuiltins);
+
+ var requestJson = request.ToJsonString();
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = this._pythonExecutable,
+ UseShellExecute = false,
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ };
+ startInfo.ArgumentList.Add("-I");
+ startInfo.ArgumentList.Add(this._validatorScript);
+
+ using var process = Process.Start(startInfo)
+ ?? throw new InvalidOperationException("Failed to start Python validator process.");
+
+ using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ timeoutCts.CancelAfter(this._timeout);
+
+ try
+ {
+ await process.StandardInput.WriteLineAsync(requestJson.AsMemory(), timeoutCts.Token).ConfigureAwait(false);
+ await process.StandardInput.FlushAsync(timeoutCts.Token).ConfigureAwait(false);
+ process.StandardInput.Close();
+
+ var stdoutTask = process.StandardOutput.ReadToEndAsync(timeoutCts.Token);
+ var stderrTask = process.StandardError.ReadToEndAsync(timeoutCts.Token);
+
+ await process.WaitForExitAsync(timeoutCts.Token).ConfigureAwait(false);
+
+ var stdout = await stdoutTask.ConfigureAwait(false);
+ var stderr = await stderrTask.ConfigureAwait(false);
+
+ if (process.ExitCode == 0)
+ {
+ return;
+ }
+
+ throw new CodeValidationException(ExtractError(stdout, stderr));
+ }
+ catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
+ {
+ TryKill(process);
+ throw new CodeValidationException($"Code validation exceeded {this._timeout.TotalSeconds:F0} seconds.");
+ }
+ catch
+ {
+ TryKill(process);
+ throw;
+ }
+ }
+
+ private static string ExtractError(string output, string errorOutput)
+ {
+ if (string.IsNullOrWhiteSpace(output))
+ {
+ return string.IsNullOrWhiteSpace(errorOutput) ? "Code validation failed." : errorOutput;
+ }
+
+ try
+ {
+ using var doc = JsonDocument.Parse(output);
+ if (doc.RootElement.TryGetProperty("errors", out var errors) && errors.ValueKind == JsonValueKind.Array)
+ {
+ var sb = new StringBuilder();
+ foreach (var err in errors.EnumerateArray())
+ {
+ if (sb.Length > 0)
+ {
+ sb.Append("; ");
+ }
+
+ sb.Append(err.ValueKind == JsonValueKind.String ? err.GetString() : err.ToString());
+ }
+
+ return sb.Length > 0 ? sb.ToString() : output;
+ }
+
+ if (doc.RootElement.TryGetProperty("message", out var message) && message.ValueKind == JsonValueKind.String)
+ {
+ return message.GetString() ?? output;
+ }
+ }
+ catch (JsonException)
+ {
+ // fall through
+ }
+
+ return output;
+ }
+
+ private static void TryKill(Process process)
+ {
+ try
+ {
+ if (!process.HasExited)
+ {
+ process.Kill(entireProcessTree: true);
+ }
+ }
+ catch
+ {
+#pragma warning disable CA1031 // Do not catch general exception types
+ // best-effort cleanup
+#pragma warning restore CA1031
+ }
+ }
+
+ private static void AddList(JsonObject obj, string key, IReadOnlyList? values)
+ {
+ if (values is null)
+ {
+ return;
+ }
+
+ obj[key] = new JsonArray(values.Select(v => (JsonNode?)JsonValue.Create(v)).ToArray());
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/EmbeddedScripts.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/EmbeddedScripts.cs
new file mode 100644
index 00000000000..cf56f64a994
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/EmbeddedScripts.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.IO;
+
+namespace Microsoft.Agents.AI.LocalCodeAct.Internal;
+
+///
+/// Extracts the embedded Python runner.py and validator.py scripts to a temporary
+/// directory and caches their paths for the lifetime of the process.
+///
+internal static class EmbeddedScripts
+{
+ private static readonly object s_syncRoot = new();
+ private static string? s_runnerPath;
+ private static string? s_validatorPath;
+
+ /// Returns the path to the embedded runner.py, extracting it on first access.
+ public static string GetRunnerScriptPath() => GetOrExtract("runner.py", ref s_runnerPath);
+
+ /// Returns the path to the embedded validator.py, extracting it on first access.
+ public static string GetValidatorScriptPath() => GetOrExtract("validator.py", ref s_validatorPath);
+
+ private static string GetOrExtract(string fileName, ref string? cached)
+ {
+ if (cached is not null && File.Exists(cached))
+ {
+ return cached;
+ }
+
+ lock (s_syncRoot)
+ {
+ if (cached is not null && File.Exists(cached))
+ {
+ return cached;
+ }
+
+ var path = Extract(fileName);
+ cached = path;
+ return path;
+ }
+ }
+
+ private static string Extract(string fileName)
+ {
+ var assembly = typeof(EmbeddedScripts).Assembly;
+ var resourceName = $"Microsoft.Agents.AI.LocalCodeAct.Resources.{fileName}";
+
+ using var stream = assembly.GetManifestResourceStream(resourceName)
+ ?? throw new InvalidOperationException($"Embedded resource '{resourceName}' not found.");
+
+ var dir = Path.Combine(Path.GetTempPath(), "agentframework-localcodeact-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ var path = Path.Combine(dir, fileName);
+
+ using var fileStream = File.Create(path);
+ stream.CopyTo(fileStream);
+ return path;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/ExecuteCodeFunction.cs b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/ExecuteCodeFunction.cs
new file mode 100644
index 00000000000..24adf474475
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/Internal/ExecuteCodeFunction.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ComponentModel;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.LocalCodeAct.Internal;
+
+///
+/// Run-scoped that exposes execute_code to the model.
+///
+internal sealed class ExecuteCodeFunction : AIFunction
+{
+ private const string ExecuteCodeName = "execute_code";
+
+ private readonly CodeExecutor _executor;
+ private readonly CodeExecutor.RunSnapshot _snapshot;
+ private readonly AIFunction _inner;
+
+ public ExecuteCodeFunction(CodeExecutor executor, CodeExecutor.RunSnapshot snapshot, string description)
+ {
+ this._executor = executor;
+ this._snapshot = snapshot;
+ this._inner = AIFunctionFactory.Create(
+ this.ExecuteCodeAsync,
+ new AIFunctionFactoryOptions
+ {
+ Name = ExecuteCodeName,
+ Description = description,
+ });
+ }
+
+ public override string Name => this._inner.Name;
+
+ public override string Description => this._inner.Description;
+
+ public override JsonElement JsonSchema => this._inner.JsonSchema;
+
+ protected override ValueTask