Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
440355a
feat(dotnet): Add LocalCodeAct package scaffold
eavanvalkenburg May 27, 2026
1de8f10
feat(dotnet): Add embedded Python runner and validator
eavanvalkenburg May 27, 2026
70d8ef5
feat(dotnet): Add CodeValidator wrapper
eavanvalkenburg May 27, 2026
6f89bf1
feat(dotnet): Add LocalExecuteCodeFunction
eavanvalkenburg May 27, 2026
b41c2a5
feat(dotnet): Add LocalCodeActProvider
eavanvalkenburg May 27, 2026
f9eef6c
feat(dotnet): Add tests and sample for LocalCodeAct
eavanvalkenburg May 27, 2026
4e2257d
feat(dotnet): Add LocalCodeAct sample project
eavanvalkenburg May 27, 2026
6dfe52e
feat(dotnet): Add file mount support and integration tests
eavanvalkenburg May 27, 2026
9765fb0
Rewrite .NET LocalCodeAct to address all PR review comments
eavanvalkenburg May 27, 2026
fd8d035
Sync embedded validator.py with Python package allow-list enforcement
eavanvalkenburg May 27, 2026
4fe3be0
Add Hosted-LocalCodeAct foundry hosted-agent sample
eavanvalkenburg May 28, 2026
953d086
fix(local-codeact-dotnet): sync validator os.* allow-list with Python
eavanvalkenburg May 28, 2026
8218662
fix(local-codeact-dotnet): address review + tighten validator
eavanvalkenburg May 28, 2026
404ba6c
chore(local-codeact-dotnet): remove stale orphan sample
eavanvalkenburg May 28, 2026
03abd80
style(local-codeact-dotnet): satisfy check-format rules
eavanvalkenburg May 28, 2026
19a8293
fix(local-codeact-dotnet): wire hosted sample into solution
eavanvalkenburg Jun 15, 2026
5ffe957
fix(local-codeact-dotnet): sync embedded Python scripts
eavanvalkenburg Jun 15, 2026
54e5d68
fix(local-codeact-dotnet): exercise Python integration on Windows
eavanvalkenburg Jun 15, 2026
9945d56
.NET: Address LocalCodeAct API review feedback
eavanvalkenburg Jun 15, 2026
8250dae
.NET: Address LocalCodeAct concurrency review
eavanvalkenburg Jun 17, 2026
f03ce1f
.NET: Simplify LocalCodeAct function wrappers
eavanvalkenburg Jun 17, 2026
5e69d11
.NET: Update LocalCodeAct factory result tests
eavanvalkenburg Jun 17, 2026
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
5 changes: 5 additions & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,9 @@
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/HostedLocalCodeAct.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj" />
</Folder>
Expand Down Expand Up @@ -616,6 +619,7 @@
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hyperlight/Microsoft.Agents.AI.Hyperlight.csproj" />
<Project Path="src/Microsoft.Agents.AI.LocalCodeAct/Microsoft.Agents.AI.LocalCodeAct.csproj" />
<Project Path="src/Microsoft.Agents.AI.Mcp/Microsoft.Agents.AI.Mcp.csproj" />
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
Expand Down Expand Up @@ -671,6 +675,7 @@
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/Microsoft.Agents.AI.LocalCodeAct.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mcp.UnitTests/Microsoft.Agents.AI.Mcp.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mem0.UnitTests/Microsoft.Agents.AI.Mem0.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
LOCAL_CODEACT_PYTHON=python3
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
</ItemGroup>

<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.LocalCodeAct\Microsoft.Agents.AI.LocalCodeAct.csproj" />
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
</ItemGroup>

<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.LocalCodeAct" Version="1.6.1-preview.260514.1" />
</ItemGroup>
-->

</Project>
Original file line number Diff line number Diff line change
@@ -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<IReadOnlyDictionary<string, object>> FetchData(
[Description("Name of the simulated table to query.")] string table)
{
Dictionary<string, IReadOnlyList<IReadOnlyDictionary<string, object>>> data = new()
{
["users"] =
[
new Dictionary<string, object> { ["id"] = 1, ["name"] = "Alice", ["role"] = "admin" },
new Dictionary<string, object> { ["id"] = 2, ["name"] = "Bob", ["role"] = "user" },
new Dictionary<string, object> { ["id"] = 3, ["name"] = "Charlie", ["role"] = "admin" },
],
["products"] =
[
new Dictionary<string, object> { ["id"] = 101, ["name"] = "Widget", ["price"] = 9.99 },
new Dictionary<string, object> { ["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();
Original file line number Diff line number Diff line change
@@ -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://<your-account>.services.ai.azure.com/api/projects/<your-project>
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.
Loading
Loading