microsoft / microsoft/agent-framework
Python: .NET: [Feature]: Backend-neutral sandbox protocol for agent code execution, with a first-party Azure Container Apps Sandboxes backend
@eavanvalkenburg is already working on this.
Since Aug 10, 2026.
- Dominant language
- Python
- Stars
- 13.6k
- Forks
- 2.3k
- Avg merge
- 2d 45m
- Merged PRs (30d)
- 358
Description
### Description
**What problem does it solve?**
Code execution in Agent Framework today is a set of per-runtime tool classes: `agent-framework-hyperlight` and `agent-framework-monty` each define their own provider and `*ExecuteCodeTool` shapes, hosted code interpreters are provider-side, and there is no shared contract in core that a third execution environment can implement. The gap is visible from four independent directions in this repo's own tracker:
- [#7311](https://github.com/microsoft/agent-framework/issues/7311) proposed a remote micro-VM CodeAct backend (Tenki) and had to mimic the Hyperlight/Monty shape *by convention*, because there is no protocol to implement — and agents that need a real OS (installs, CLIs, persistent files) still "have no CodeAct story" in-tree.
- [#6490](https://github.com/microsoft/agent-framework/discussions/6490) asks, for a Daytona-style remote HTTP sandbox in .NET: "Beyond Hyperlight, is there a plan for a more general sandbox / CodeAct provider abstraction for remote services?" — and what the recommended minimum scope for a community integration would be. Today the honest answer to both is "no, and it depends".
- [#7319](https://github.com/microsoft/agent-framework/discussions/7319) built a local-machine executor bridge and explicitly asks for "a more idiomatic extension point for 'this step must run on host hardware'" rather than bolting on their own seam.
- [#6475](https://github.com/microsoft/agent-framework/issues/6475) (first-party) wants skill resources executed in sandboxes and starts from the observation "we have multiple ways of creating sandboxes that isolate code" — which is the fragmentation itself.
We hit the same wall building a production IaC-validation tool (agents author Bicep; a sandbox runs `bicep build`/`bicep lint`; the compiler's diagnostics come back as the tool result). The tool logic was ~400 lines; everything around it — a backend protocol, an Azure Container Apps Sandboxes backend, lifecycle binding, and the security wiring — was roughly three times that, and none of it is specific to our app. Every team integrating a remote sandbox today rebuilds exactly this layer, with the same bugs: the multi-replica sandbox leak, the model-supplied-key confused deputy, the command-string injection, the silent cost of never reusing a warm sandbox.
**What would the expected behavior be?**
Four pieces, in dependency order — the first is the ask, the rest build on it:
**1. A small sandbox protocol in core.** The contract that made our tool portable is five types: a `Sandbox` (`write_file`, `exec` → `ExecResult`), a `SandboxBackend` (`acquire`/`dispose`/`dispose_scope`, get-or-create with warm reuse), a `SandboxSpec` (workload kind, image reference, **egress allowlist where everything unlisted is denied** — a spec that forgets egress gets the closed configuration, not the open one), a `SandboxKey` derived from the host's request context (**never from model input** — a model-supplied key would let one conversation address another's sandbox), and a declared `Isolation` level (`vm` / `container` / `process`) that a router checks against the deployment context, refusing shared-kernel isolation in deployed environments rather than degrading silently. A workload written against this runs unchanged on any backend; our Bicep tool contains no Azure import and a test enforces that. With this in place, #7311's Tenki, #6490's Daytona, and #7319's local-machine bridge are each *one backend class*, not a new tool surface — and each declares its true isolation instead of leaving it implicit.
**1b. The protocol unifies the existing runtimes — Hyperlight and Monty are backends, not casualties.** `agent-framework-hyperlight` (in-process hypervisor micro-VM) and `agent-framework-monty` (in-process interpreter) slot in as the low-latency end of the same seam, and they sharpen the protocol in two ways. First, **operations are capabilities**: an interpreter has no shell, a micro-VM running WASM has no `apt`, so the protocol's operations split into `exec` (shell) and `run_code` (language runtime), each backend implementing what it truthfully can and declaring it — which turns #7311's prose comparison table (installs / subprocesses / persistent filesystem / host tool callbacks) into machine-readable capability declarations the router matches a spec's requirements against, instead of every integrator re-deriving that table by trial. Host tool callbacks — the CodeAct pattern's differentiator, which Hyperlight and Monty have and remote services generally do not — become one such capability rather than a reason for parallel tool-class hierarchies. Second, **containment is two axes**: Hyperlight is in-process yet hypervisor-isolated, while Monty is nominally the weakest tier but has *no filesystem and no network by construction* — so the deployed-environment policy should read declared boundary strength together with the requested capability surface (a no-I/O interpreter evaluating pure Python is a different risk than a shell in a container), rather than a single isolation ladder. The convergence is already visible in-tree: Hyperlight's `AllowedDomain` and both packages' `FileMount` are the same concepts our ACA integration independently built as the spec's egress allowlist and sandbox file writes — three integrations arriving at the same nouns is the strongest evidence they belong in a shared protocol.
**2. A first-party Azure Container Apps Sandboxes backend.** ACA Sandboxes are Microsoft's own remote hardware-isolated micro-VMs — the exact capability #7311 went to a third-party service for — and the integration has real depth that a first-party client should own once: labeling sandboxes at create time so a conversation delete can purge **from the service, by label**, not from process memory (a multi-replica host serves the delete on a replica that didn't create the sandbox; an in-memory registry leaks billable VMs, and the bug is invisible on a single-replica dev box); resuming a suspended sandbox instead of paying a cold create per fix-round; the label-value length limit (hash, don't truncate — a truncation collision lets one user's purge delete another's sandbox); OCI-reference-to-disk-image resolution; auto-suspend/auto-delete lifecycle policies as the cost backstop when the client process dies (#7311's "server-side lifetime caps" point).
**3. Thread-scoped resource lifecycle.** A sandbox belongs to a conversation, and the framework knows when conversations end; today there is no hook for "this thread owns external resources — dispose them on delete". This generalizes past sandboxes (provider-side threads, vector stores, temp storage). It also surfaces a primitive the framework is missing more broadly: a durable, host-owned conversation/boundary identity that middleware and backends can key state on. We had to invent it twice — once for sandbox keying, once to scope `agent_framework.security` label state to a conversation (see #7455, item 1) — and the "session isolation keys" advice in #6490's comment thread is the same concept appearing a third time.
**4. Security composition with `agent_framework.security` (FIDES).** A sandboxed exec tool should ship its information-flow classification instead of every integrator hand-maintaining it: it is definitionally a **sink under untrusted taint** (code influenced by untrusted content is attacker code — the CodeAct injection channel), and its spec's egress allowlist is machine-readable evidence the confidentiality leg cannot otherwise get — `egress_allow=()` means the tool cannot exfiltrate what it is given, a non-empty list derives the egress cap. This also settles #6490's architecture question ("Agent Executor vs. hosting channel"): the sandbox should surface as a **tool on the middleware chain**, not as a remote agent, because the security boundary (label tracking, approvals, budgets) is process-local — a remote tool ships only the *work* out and keeps the call inside the boundary; a remote agent exits it, and everything it returns must be re-treated as untrusted ingress.
**Are there any alternatives you've considered?**
- *Status quo: one package per runtime/service.* This is how #7311 ended (closed, not merged), and it scales as N incompatible shapes — every backend re-decides keying, reuse, disposal, egress, and injection handling, which are precisely the parts that go wrong quietly.
- *Per-app integration (`AIContextProvider` + an `execute_code` tool).* What #6490 does today and what we did — it works, but the undifferentiated harness is ~3× the workload code, and the hard bugs (multi-replica purge, warm reuse, model-supplied keys) are invariants a framework can enforce once.
- *Sandbox as a remote agent instead of a remote tool.* Rejected for the boundary reason in part 4: it silently exits the middleware chain's security context.
Out of scope, deliberately: image building and provisioning (a deployment property), domain-specific result parsing (a workload property), a host-tool callback bridge into remote sandboxes (Hyperlight/Monty have it in-process; #7311's table shows remote services mostly don't — it can arrive later as an optional backend capability), and an async job API for work longer than a turn (#7319's queue/poll shape; a natural v2 on the same protocol).
(Our interest in in Python and #6490 is an ask for the same abstraction, so the protocol shape should land in both SDKs even if backends arrive at different times.)
### Code Sample
```markdown
# ── 1. A workload tool, written once against the protocol ──────────────────────
# No backend import. The same tool runs on ACA Sandboxes in production, Docker on a
# dev box, and an in-process fake in tests.
from agent_framework import tool
from agent_framework.sandbox import SandboxRouter, SandboxSpec
PYTEST_SPEC = SandboxSpec(
kind="pytest",
image="pytest-sandbox:1.2", # repository:tag; the backend's registry qualifies it
egress_allow=("pypi.org", "files.pythonhosted.org"), # everything else is DENIED
work_dir="/work",
)
def make_test_tools(router: SandboxRouter, store, context) -> list:
if not router.enabled:
return [] # nothing configured -> attach no tool, not a failing one
@tool(name="run_tests")
async def run_tests(files: list[str]) -> str:
"""Write the named workspace files into the sandbox and run pytest."""
key = context.sandbox_key() # scope/thread/agent from HOST request context, never model input
listing = await context.list_files(store)
if any(f not in listing for f in files):
return "Error: unknown file" # names validated BEFORE any command interpolation
sandbox = await router.acquire(key, PYTEST_SPEC) # get-or-create; warm reuse across fix rounds
for f in files:
await sandbox.write_file(f"{PYTEST_SPEC.work_dir}/{f}", await store.read(f))
result = await sandbox.exec("pytest -q", working_directory=PYTEST_SPEC.work_dir, timeout=120)
return result.stdout
return [run_tests]
# ── 2. One router, every runtime — selection by declared capabilities ──────────
from agent_framework.azure.sandboxes import AcaSandboxBackend, AcaSandboxConfig # part 2 (first-party)
from agent_framework.hyperlight import HyperlightSandboxBackend # existing runtimes become backends
from agent_framework.monty import MontySandboxBackend
from agent_framework.sandbox import SandboxRouter, SandboxSpec
router = SandboxRouter(
backends=[
MontySandboxBackend(), # in-process interpreter: run_code + host callbacks; no OS at all
HyperlightSandboxBackend(), # in-process hypervisor micro-VM: run_code + host callbacks
AcaSandboxBackend(AcaSandboxConfig(endpoint=..., sandbox_group=..., registry=...)),
# remote micro-VM: exec/shell, installs, persistent filesystem
],
deployed=True, # policy reads declared isolation TOGETHER WITH requested capabilities:
) # a no-I/O interpreter may pass where a shell-capable container never does
# The spec's requirements drive selection — the same seam serves both worlds:
SandboxSpec(kind="codeact", requires={"run_code", "host_callbacks"}) # -> Monty / Hyperlight
SandboxSpec(kind="pytest", requires={"exec"}, egress_allow=("pypi.org",)) # -> ACA (or Docker on a dev box)
# A community Tenki (#7311), Daytona (#6490), or local-machine (#7319) integration
# is one SandboxBackend implementation — not a new tool class per service.
# ── 3. Existing surfaces become workloads over the same protocol ───────────────
# CodeAct keeps its provider shape; the spec decides what serves it. In-process with
# host callbacks (today's Hyperlight/Monty experience, unchanged):
codeact = CodeActProvider(router=router, spec=SandboxSpec(kind="codeact", requires={"run_code", "host_callbacks"}))
# ...or with a REAL remote OS (the #7311 / #6490 ask) by requiring a shell instead:
codeact_os = CodeActProvider(router=router, spec=SandboxSpec(kind="codeact-os", requires={"exec"}, image="codeact:3.12", egress_allow=()))
agent = Agent(client=client, context_providers=[codeact])
# Skill-resource execution (#6475): the harness runs skill scripts as one more kind,
# with the skill's declared hosts as the sandbox's entire egress:
skill_spec = SandboxSpec(kind="skill-scripts", image="skills:1.0", egress_allow=tuple(skill.allowed_hosts))
# ── 4. Lifecycle + security compose instead of being rebuilt per app ───────────
# Conversation delete reclaims compute FROM THE SERVICE, by label — correct on the
# replica that never created the sandbox:
deleted = await router.dispose_scope(scope, thread_id)
# agent_framework.security derives the exec tool's classification from its spec:
# an exec tool is a sink under untrusted taint; egress_allow=() means it cannot
# exfiltrate what it is given, a non-empty allowlist derives the confidentiality cap.
# Today integrators hand-maintain exactly this mapping in their own middleware.
```
### Language/SDK
Both
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.