Microsoft Agent Framework.NETAI AgentsSecurity

The Agent Harness in Microsoft Agent Framework (.NET Guide)

The Agent Harness in Microsoft Agent Framework gives .NET agents sandboxed shell execution, file access, and human-in-the-loop approvals. How to wire it up.

A language model on its own can only generate text. To turn it into an agent that actually does things — runs commands, edits files, works through a multi-step plan, and keeps going until the job is finished — you need a runtime wrapped around the model. That runtime is the agent harness, and as of Build 2026 the Microsoft Agent Framework ships one in the box.

If the framework’s 1.0 GA on 2 April 2026 gave .NET developers a unified way to build agents, the harness is the next layer up: an opinionated, batteries-included agent loop with sandboxed shell execution (a .NET-only feature at launch), scoped filesystem access, human-in-the-loop approval flows, and automatic context compaction for long-running sessions. The Microsoft Learn documentation for it landed in July 2026, so everything below reflects the current shipped surface.

This post covers what the harness is, how to wire it up in C#, and how to run it without handing the model the keys to your machine.

What is an agent harness?

Every serious agent product — coding assistants, autonomous research agents, task automators — is built on some form of harness. The harness is the engine around the model. It:

  • runs the loop that calls the model and executes the tools the model asks for;
  • manages conversation history and context so the model stays within its limits over hundreds of tool calls;
  • applies approval and safety policies before actions are taken;
  • keeps the agent progressing toward task completion instead of stopping after one reply.

Until now, if you wanted this on .NET you built it yourself: the retry loop, the token accounting, the approval prompts, the crash recovery. The Agent Framework harness packages all of that as a ready-made agent. Internally it is a ChatClientAgent with a curated stack of framework features layered on — each also available standalone. You supply the chat client; everything else has a sensible default you can disable or replace.

What ships in the box

The harness bundles a substantial set of capabilities, each individually configurable:

  • Function invocation — the automatic tool-calling loop, with a configurable iteration limit.
  • Per-service-call history persistence — chat history is saved after every individual model call, which enables crash recovery and mid-run inspection.
  • Compaction — token-budget-aware context-window management for long tool-calling chains.
  • Todo provider — a persistent todo list the agent uses to track multi-step plans.
  • Agent mode provider — plan/execute mode tracking (interactive planning first, autonomous execution after approval).
  • File memory provider — session-scoped, file-based memory for notes and artifacts that persist across turns.
  • File access provider — read/write file tools scoped to a working directory.
  • Tool approval — approval gates with “don’t ask again” standing rules and heuristic auto-approval for safe operations.
  • OpenTelemetry — built-in tracing following the generative-AI semantic conventions.
  • Web search — a hosted search tool, on by default.
  • Optional extras — an Agent Skills provider, background sub-agents for parallel delegation, a shell environment, and looping until a completion condition is met.

Creating a harness agent in C#

The harness lives in the Microsoft.Agents.AI.Harness package and is exposed as the HarnessAgent class in the Microsoft.Agents.AI namespace. The quickest path is the AsHarnessAgent() extension method on any IChatClient — Azure OpenAI, Foundry, OpenAI, Anthropic, whatever you already use:

using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

// chatClient is any IChatClient implementation.
AIAgent agent = chatClient.AsHarnessAgent();

AgentResponse response = await agent.RunAsync("Plan a weekend trip to Seattle.");
Console.WriteLine(response.Text);

Configuration goes through HarnessAgentOptions. One detail worth internalizing: instructions are layered. Harness-level instructions (HarnessAgentOptions.HarnessInstructions) describe general operating behavior — how to break down tasks, when to use tools — and the harness ships opinionated defaults in HarnessAgent.DefaultInstructions. Task-specific instructions go on ChatOptions.Instructions and are merged in:

AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
    Name = "research-agent",
    ChatOptions = new ChatOptions
    {
        Instructions = "You are a research assistant focused on academic sources.",
        Tools = [AIFunctionFactory.Create(GetStockPrice)],
    },
});

Every default capability has a matching disable flag, so you keep the pipeline you want and drop the rest:

AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
    HarnessInstructions = "Custom operating guidelines here.",
    DisableTodoProvider = true,      // No todo list
    DisableAgentModeProvider = true, // No plan/execute modes
    DisableWebSearch = true,         // No hosted web search tool
    DisableFileMemory = true,        // No file-based session memory
});

Other flags include DisableFileAccess, DisableAgentSkillsProvider, DisableToolAutoApproval, and DisableOpenTelemetry. You can also inject your own context providers via AIContextProviders.

Sandboxed shell execution with ShellExecutor (.NET only)

This is the headline feature, and at launch it is exclusive to the .NET SDK: give the agent a real shell via a ShellExecutor from the Microsoft.Agents.AI.Tools.Shell package. Passing one to the harness adds an approval-gated shell-execution tool plus a context provider that tells the model which OS, shell, and working directory it is operating in:

using Microsoft.Agents.AI.Tools.Shell;

// A shell confined to a working directory. Commands require approval
// by default; the deny-list is a UX pre-filter, not a security boundary.
await using var shell = new LocalShellExecutor(new LocalShellExecutorOptions
{
    WorkingDirectory = workingDir,
    ConfineWorkingDirectory = true,
    Policy = new ShellPolicy(denyList: [@"\brm\s+-rf\b", @"\bsudo\b"]),
});

AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
    ShellExecutor = shell,
});

Three things in that snippet deserve emphasis:

The deny-list is not a security boundary

Microsoft’s own documentation is blunt about this, and it matters. A regex deny-list stops the model from casually running rm -rf or sudo, but a creative command line can route around any pattern filter — bash -c wrappers, encoded payloads, aliases. Treat the deny-list as a UX pre-filter that reduces noise in the approval queue, nothing more. The actual security boundaries are the approval gate (on by default — every command needs a human yes) and the environment the executor runs in. Microsoft’s recommendation for local shell execution is exactly what you would hope: run it in an isolated environment — a container, a throwaway VM, a locked-down CI runner — and keep explicit approval in place before commands run.

Confinement is scoped, not virtualized

ConfineWorkingDirectory = true scopes the shell to a working directory, which protects against the agent wandering into C:\Windows by accident. It is process-level scoping, not virtualization. If the agent runs on your workstation, it runs with your privileges. For anything beyond local experimentation, the executor belongs inside a container with a minimal filesystem, no ambient cloud credentials, and egress rules. The same threat model applies to filesystem access via the file access provider — scope it to a dedicated working directory, never a home directory. I cover the broader attack surface — prompt injection turning tool output into instructions, exfiltration via tool parameters — in securing AI agents in .NET.

The executor owns a lifecycle

LocalShellExecutor is IAsyncDisposable (await using above). You create it, hand it to the harness, and dispose it when the session ends — which makes it natural to construct a fresh, isolated executor per agent session rather than sharing one across tenants.

Human-in-the-loop approval flows

Shell commands are approval-gated by the harness automatically. For your own sensitive tools, the framework’s approval mechanism is explicit: wrap a function so that invoking it produces an approval request instead of an execution, surface that request to a human, and feed the decision back into the run.

The flow in .NET centers on two types: ApprovalRequiredAIFunction (wraps any AIFunction to require sign-off) and FunctionApprovalRequestContent (the request that comes back in the response). Conceptually, the loop looks like this:

// Conceptual — check the official samples against your installed package version.
var deleteTool = new ApprovalRequiredAIFunction(
    AIFunctionFactory.Create(DeleteDeploymentSlot));

AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
    ChatOptions = new ChatOptions { Tools = [deleteTool] },
});

AgentResponse response = await agent.RunAsync("Clean up the staging slots.", session);

// The run pauses and returns approval requests instead of executing.
var approvals = response.Messages
    .SelectMany(m => m.Contents)
    .OfType<FunctionApprovalRequestContent>();

foreach (var request in approvals)
{
    Console.WriteLine($"Agent wants to call: {request.FunctionCall.Name}");
    bool approved = AskHuman(request);   // your UI: console, Teams card, web dialog

    // Feed the decision back and continue the run.
    response = await agent.RunAsync(
        new ChatMessage(ChatRole.User, [request.CreateResponse(approved)]),
        session);
}

Because the request round-trips through the session, the human can be anywhere — a console prompt in dev, an approval card in a web app in production. The state machine does not care how long the human takes to answer, which pairs well with the harness’s per-call history persistence: a pending approval survives a process restart.

Two refinements make this workable at scale:

  • Standing approval rules. The ToolApprovalAgent middleware supports “don’t ask again” semantics — a human approves git status once and the rule persists, so the queue only ever contains genuinely novel or risky actions. The harness also applies heuristic auto-approval for obviously safe, read-only operations; set DisableToolAutoApproval = true if your compliance posture demands a human on every call.
  • Plan-then-execute. The agent mode provider structures work into an interactive plan phase (the agent drafts a todo list and gets your sign-off) and an autonomous execute phase. Approving a plan up front and then letting standing rules handle the mechanical steps is a far better UX than fifty consecutive “Allow?” prompts — and a far better audit trail too.

Context management across long-running sessions

An agent that runs a 200-step task will blow through any context window unless something manages the history. The harness’s answer is compaction: monitor token usage during long tool-calling chains and compress history before overflow. Enabling the default token-budget-aware strategy takes two numbers:

AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
    MaxContextWindowTokens = 128_000,
    MaxOutputTokens = 16_384,
});

Under the hood, compaction strategies can be chained: compact verbose tool results first (ToolResultCompactionStrategy), keep a sliding window of recent conversation (SlidingWindowCompactionStrategy), and truncate the oldest message groups as a last resort (TruncationCompactionStrategy). Swap in your own via HarnessAgentOptions.CompactionStrategy, or set DisableCompaction = true to opt out. When the harness manages history itself (the default InMemoryChatHistoryProvider rather than service-stored threads), the same compaction provider is applied to session-stored history, so the persisted state stays bounded too.

Alongside compaction, the file memory provider gives the agent durable, session-scoped scratch space on disk (agent-file-memory/{session}/) for notes and artifacts — memory that survives compaction because it lives outside the chat history entirely. For deeper patterns on what belongs in the context window versus external stores, see agent memory and state persistence in .NET.

Storage is pluggable across the board: the harness persists through an AgentFileStore abstraction, with a FileSystemAgentFileStore default and room for alternatives like blob storage — so the same agent code runs against local disk in dev and durable cloud storage in production.

Looping until the job is done

By default the harness runs once per invocation. Supply LoopEvaluator instances to re-invoke the agent until a completion condition holds — a marker string, a predicate, or an AI judge:

AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
    LoopEvaluators = [new CompletionMarkerLoopEvaluator("DONE")],
});

The loop is the outermost decorator, so each iteration is a complete agent run — independently tool-approved and independently traced. Your approval gates and your OpenTelemetry spans apply on every pass, not just the first.

A security checklist before you ship

An agent with a shell and a filesystem is an execution environment for model-generated code. Before production:

  1. Isolate the executor. Container or disposable VM, minimal image, no mounted secrets, restricted egress. Assume every command the model writes could be adversarial — because via prompt injection, it can be.
  2. Keep the approval gate on for anything destructive or irreversible. Use standing rules to keep it humane; never disable it to make a demo smoother.
  3. Treat the deny-list as ergonomics, not defense. Layer it inside real boundaries, never instead of them.
  4. Scope filesystem access to a dedicated working directory per session.
  5. Trace everything. The built-in OpenTelemetry support gives you a per-tool-call audit trail — keep it enabled and retained.
  6. Test the gates, not just the happy path. Verify that denied approvals actually stop execution — testing and evaluating agents covers how to put approval flows under automated test.

Note: APIs evolve quickly across harness releases. The snippets above follow the shapes documented on Microsoft Learn as of July 2026; treat them as a conceptual map and check the official samples against the exact package version you install.

Where to go next

The harness is the strongest signal yet of where Microsoft is taking agents on .NET: not a chatbot API, but a runtime for autonomous software that acts on real systems — with the guardrails to make that survivable.


Have a correction or a topic you want covered? Email mani.bc72@gmail.com.