Blog Post

Microsoft Developer Community Blog
10 MIN READ

Harness-Driven Agents: Secure Podcast Pipeline in Hyperlight MicroVM Sandbox

kinfey's avatar
kinfey
Icon for Microsoft rankMicrosoft
Jun 04, 2026

The moment the agent reached for rm -rf

For most of 2024 and 2025, "agents" were a demo word. By 2026 they are something you run — autonomously, in a loop, executing code they wrote themselves a second ago.

I was watching one work late one night. I had given it a goal, a handful of tools, and the freedom to write and run its own Python. For twenty minutes it was magic: read a file, reason about it, write a script, run it, inspect the output, correct itself, try again. Then it produced this:

import shutil
shutil.rmtree("/")   # "cleaning up temporary files"

It was trying to be helpful — it had decided the workspace was cluttered and wanted a clean start. The "workspace," as far as that process was concerned, was my entire machine.

I killed it in time. But the lesson is the one every agent builder eventually arrives at: the model is not the dangerous part — the execution is. A chatbot that answers wrong is annoying. An agent that fetches a web page, runs code, and writes files has a blast radius. The bounding box has to come from infrastructure, not from a system prompt.

harnessagent_sandbox_demo is a concrete build that puts that bounding box in exactly the right place — and it does it in service of a real, charming little product: a daily five-minute Mandarin podcast about the FIFA World Cup 2026.

The scenario: a daily World Cup podcast, written by agents

Strip away the infrastructure for a second and look at what this thing actually does.

Every day it produces a fresh Mandarin podcast script about the FIFA World Cup 2026. Three LLM agents run in sequence:

  1. SearchAgent — goes out and gathers the day's World Cup news.
  2. ContentAgent — turns that raw material into structured podcast content.
  3. GenScriptAgent — writes the final, readable five-minute script.

The output is two text files — one in Simplified Chinese, one in Traditional Chinese:

./outputs/<YYMMDD>/<YYMMDD>.simple.zh.txt
./outputs/<YYMMDD>/<YYMMDD>.tranditional.zh.txt

That's the whole product. It sounds simple — and the point of the project is that making it safe is the hard part. SearchAgent has to reach the open internet. All three agents write and run code. If you wire that naively, you have just built the exact machine that types shutil.rmtree("/") for you. So the entire architecture is organized around one principle: the agents get to do real work, but every dangerous capability is fenced behind a hardware boundary.

Why the obvious sandboxes fall short for agents

An agent is defined by an act-observe-correct loop running untrusted, model-generated code over and over. That single property breaks most conventional isolation choices.

OptionWhy it falls short for agents
No sandboxOne rm -rf, one leaked .env, one rogue network call — the blast radius is the whole machine.
ContainerGreat for shipping apps, but a coding agent wants to build and run its own container, which means Docker-in-Docker and elevated privileges that quietly undo the isolation.
WASM / V8 isolateFast to start, but you isolate a language runtime, not an OS — no system packages, no arbitrary shell, and hardening the engine is a moving target.
Full VMRock-solid isolation, but cold starts in seconds and heavy memory — exactly the friction that pushes developers to skip isolation entirely.

Each option trades away safety, speed, or compatibility. A podcast pipeline that runs every day, spinning agents up and down, needs all three at once:

  • A real environment — to fetch URLs, run shells, call tools.
  • A hard boundary — so a bad step can't reach the host.
  • Near-instant lifecycle — because a slow sandbox is a sandbox developers skip, and an unused safety feature protects nobody.

The MicroVM answer, embedded as a library: Hyperlight

A MicroVM gives each workload its own kernel and a hardware-enforced boundary — the isolation strength of a full VM — stripped down to start in milliseconds and tear down just as fast. Misbehave inside, and you hit a wall; there is no path back to the host. And it is disposable by design: when an agent goes off the rails, you delete the sandbox and reopen in milliseconds, with nothing to clean up.

Most MicroVM runtimes (Firecracker and friends) are cloud infrastructure — server-side. Hyperlight is different: a lightweight Virtual Machine Manager (a CNCF sandbox project) designed to be embedded inside your application, like a library.

  • MicroVMs that boot in milliseconds, with guest function calls completing in microseconds.
  • No guest kernel, no OS — the guest is a purpose-built no_std Rust/C binary. Nothing in there to attack.
  • Sandboxed by default — no filesystem, no network, nothing, unless explicitly granted.
  • Typed function calls across the VM boundary, and snapshot/restore to rewind to a clean state between calls.
  • Runs on KVM, MSHV (Microsoft Hypervisor), and Windows Hypervisor Platform.

This project uses the Wasm backend: the three agents share a single HyperlightRuntime, and the guest is reset to a clean snapshot before every code execution. That detail is what makes a daily, many-step pipeline cheap — you capture the sandbox state once and rewind to it, instead of rebuilding a VM hundreds of times.

Agent = Model + Harness

The community has converged on a simple equation: Agent = Model + Harness.

The model is a brain in a jar — text in, text out, no memory between calls, no loop, no hands. It can express the intent to call a tool; it cannot actually call it.

The harness is the execution layer: it calls the model, handles its tool calls, and decides when to stop. As the Hugging Face glossary puts it, "if you're not the model, you're the harness."

That reframes the safety problem precisely. When my agent emitted shutil.rmtree("/"), the model deleted nothing — it merely suggested. The harness would have run it. The harness is where reasoning meets reality, so it is exactly where safety must live. The question stops being "how do I make the model safer?" and becomes: how do I build a harness that executes the model's intent inside a boundary it cannot escape?

The Microsoft Agent Framework answers that with first-class agent harness capabilities in Python and .NET, and it ships with one security note stated plainly:

For local shell execution, we recommend running this logic in an isolated environment and keeping explicit approval in place before commands are allowed to run.

The harness is the steering wheel — it does not pretend to be the seatbelt and the crumple zone. For that, it points you outward: run this somewhere isolated. Hyperlight is that isolated somewhere. This project snaps the two pieces together.

The architecture: two planes, one bridge

Here is the heart of the design. Two planes run together every episode:

  • An orchestration plane on the host — the WorkflowBuilder graph, the LLM clients, and the deterministic save step.
  • An execution plane inside one Hyperlight Wasm sandbox — the only place LLM-generated code is allowed to run.

The single bridge between them is one call: call_tool("fetch_url", ...).

The mapping to layers:

LayerComponentRole
ModelAzure AI Foundry via FoundryChatClient (AzureCliCredential)The reasoning brain behind each harness agent
Agent runtimeMicrosoft Agent Framework create_harness_agentDrives the model, advertises skills, handles tool calls, decides when to stop
OrchestrationWorkflowBuilder graphprepare → SearchAgent → adapt → ContentAgent → adapt → GenScriptAgent → save_scripts
Code executionCodeAct providerRuns model-written code via the one execute_code tool — inside the MicroVM, never on the host
IsolationHyperlight Wasm MicroVMOne shared HyperlightRuntime; clean snapshot restored before every execute_code
Host toolfetch_url (sandbox/podcast_tools.py)The only network path; urllib + a BBC-only allow-list
Persistencesave_scripts ExecutorDeterministic, no LLM — parses two fenced blocks and writes the two output files

The four invariants that make it safe

The README is explicit about what the diagram guarantees. These four invariants are the whole security argument.

  1. The model never sees the network.Its only tool isexecute_code. Network access happens only when the guest itself runs call_tool("fetch_url", ...) from inside the sandbox. The model cannot reach the internet directly — it can only ask the guest to, and the guest can only reach BBC.
  2. One sandbox per run, snapshot per call.All three agents share the sameHyperlightRuntime. Before every execute_code, the guest is reset to a clean snapshot — so nothing one step does can leak into the next, and there is no VM to rebuild.
  3. Two counter paths — and why there are two.Thefunction_middleware (make_tool_call_recorder) sees the model-direct execute_code calls. But the inner, guest-initiated fetch_url is dispatched by Hyperlight straight to the FunctionTool, bypassing the middleware entirely. So a second counter — make_call_tool_counter(on_call=) — bumps state["tool_call_counts"][<agent>]["fetch_url"] on every guest invocation. Two observation points, because the architecture has two genuinely different call surfaces.
  4. Deterministic save — no LLM in the persistence step.GenScriptAgentonly emits text. The save_scripts Executor parses the two fenced code blocks out of that text and writes the simplified and traditional files itself. There is no model in the loop when bytes hit disk, so the output path is fully predictable.

Now let's look at the real code surface

The README documents the API the demo is built on. The snippets below reflect that surface.

1. Install and environment

pip install agent-framework-hyperlight --pre
# Hyperlight needs a hypervisor: KVM on Linux, WHP on Windows. macOS is not yet supported.
# The model runs on Azure AI Foundry; FoundryChatClient authenticates via AzureCliCredential.
az login
export HYPERLIGHT_PYTHON_GUEST_PATH="/path/to/python_guest"

2. A harness agent that carries only a stub — skills do the rest

Each of the three agents is built with create_harness_agent + FoundryChatClient. The agents themselves carry only a tiny stub instruction; their real role prompts and the shared sandbox/CodeAct guardrails live as file-based Agent Skills under skills/. The harness's built-in SkillsProvider advertises those SKILL.md packages, and the model loads them at runtime via load_skill.

from agent_framework import create_harness_agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential

# Model on Azure AI Foundry — not Azure OpenAI directly.
client = FoundryChatClient(credential=AzureCliCredential())

# The agent carries a tiny stub. Its real persona — "you gather World Cup
# news", "you write the script" — lives in a SKILL.md package under skills/,
# advertised by the harness SkillsProvider and pulled in via load_skill.
search_agent = create_harness_agent(
    chat_client=client,
    name="SearchAgent",
    instructions="You are a harness agent. Load your skill, then begin.",
)

3 The CodeAct surface: one tool the model can see

This is the CodeAct pattern from 02-agents/context_providers/code_act/code_act.py. The model sees exactly one tool — execute_code. Any extra capability (here, only fetch_url) is reachable from inside the guest via call_tool(...).

# What the MODEL sees and writes — one script, not ten tool round-trips:
#
#   # inside execute_code, running in the Hyperlight Wasm guest:
page = call_tool("fetch_url", url="https://www.bbc.com/sport/football/world-cup")
#   # ... parse page["BODY"], pull out today's stories ...
print(top_stories)
#
# execute_code is the ONLY tool on the model's surface.
call_tool("fetch_url", ...) is reachable only from inside the sandbox.

4. The one host tool, with a BBC-only allow-list

fetch_url lives on the host (sandbox/podcast_tools.py). It is the single bridge across the boundary, and it is deliberately narrow.

import urllib.request
from urllib.parse import urlparse

ALLOWED_DOMAINS = {"bbc.com", "www.bbc.com"}   # allow-list: BBC only

def fetch_url(url: str) -> dict:
    """The ONLY network path out of the sandbox. Host-side, allow-listed."""
    host = urlparse(url).netloc
    if host not in ALLOWED_DOMAINS:
        return {"STATUS": "blocked", "URL": url}

    with urllib.request.urlopen(url, timeout=20) as resp:
        body = resp.read(8192).decode("utf-8", "ignore")   # BODY capped at ~8 KB

    return {
        "STATUS": "ok",
        "URL": url,
        "TITLE": _extract_title(body),
        "DESCRIPTION": _extract_description(body),
        "LINKS": _extract_links(body),
        "BODY": body,
    }

Notice what this buys you: even if SearchAgent writes hostile code, the worst it can do over the network is read BBC, 8 KB at a time. The allow-list is host-side and the model never sees it — it cannot be prompt-injected away.

5. Wiring the graph and the deterministic save

from agent_framework import WorkflowBuilder

workflow = (
    WorkflowBuilder()
    .add_node("prepare", prepare)
    .add_node("SearchAgent", search_agent)
    .add_node("adapt_1", adapt)
    .add_node("ContentAgent", content_agent)
    .add_node("adapt_2", adapt)
    .add_node("GenScriptAgent", genscript_agent)
    .add_node("save_scripts", save_scripts)   # deterministic Executor, NO LLM
    .build()
)

# GenScriptAgent emits text containing two fenced blocks (simplified +
# traditional). save_scripts parses them and writes the files itself —
# there is no model in the persistence step.
await workflow.run()
# -> ./outputs/<YYMMDD>/<YYMMDD>.simple.zh.txt
# -> ./outputs/<YYMMDD>/<YYMMDD>.tranditional.zh.txt

6. The payoff

Run that shutil.rmtree("/") inside this pipeline now and the result is delightfully boring: the agent deletes its own throwaway sandbox, the host never notices, and the next execute_code starts from a clean snapshot. Two things to call out:

  • Snapshot/restore means every code execution starts from a clean, reusable baseline — capture state once, rewind between calls, instead of rebuilding the whole VM. For a daily pipeline that runs the act-observe-correct loop many times, that is the difference between "fast enough to always use" and "slow enough to skip."
  • Because each agent writes one script instead of ten round-tripped tool calls, the CodeAct approach keeps both latency and token usage down — the model reasons once and lets the guest do the busywork behind the boundary.

Where it fits, and the one idea to keep

harnessagent_sandbox_demo lives inside Multi-AI-Agents-Cloud-Native — a gallery of patterns for running agent systems safely on Azure: A2A multi-agent orchestration, the Kubernetes sidecar pattern, hardened pipelines, and a sibling sample that runs Copilot agents on AKS inside Kata Containers MicroVMs at the pod level.

And the README is explicit that this design is cloud-native: running it in-cluster on AKS changes nothing about the architecture — the same WorkflowBuilder graph, the same Hyperlight sandbox, the same deterministic save_scripts executor. The local build and the in-cluster build are the same shape.

The two MicroVM samples are two ends of one spectrum. The Kata sample puts the boundary around the whole pod — a deployment topology. This Hyperlight demo pulls the boundary all the way into the agent process itself — the sandbox becomes a library call. Same question — where do you place the hardware boundary in an agent stack? — answered at two different altitudes.

The old pitch for sandboxing always carried an asterisk: yes, it's safer, but you'll pay in speed, compatibility, or friction. MicroVMs erase the asterisk — VM-grade isolation, cold starts fast enough that there's no reason to skip it, and a real environment your agents can actually work in. Enough of a real environment, in fact, to write you a World Cup podcast every morning.

The one idea to internalize: the harness decides, the MicroVM contains. Give your agent a room where it is allowed to fail — then let it be brilliant.

References

Updated Jun 04, 2026
Version 1.0
No CommentsBe the first to comment