You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 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 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 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 (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.
Per-app integration (AIContextProvider + an execute_code tool). What Recommended integration pattern for remote HTTP sandboxes like Daytona in MAF? #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
# ── 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.stdoutreturn [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.
Description
What problem does it solve?
Code execution in Agent Framework today is a set of per-runtime tool classes:
agent-framework-hyperlightandagent-framework-montyeach define their own provider and*ExecuteCodeToolshapes, 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: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), aSandboxBackend(acquire/dispose/dispose_scope, get-or-create with warm reuse), aSandboxSpec(workload kind, image reference, egress allowlist where everything unlisted is denied — a spec that forgets egress gets the closed configuration, not the open one), aSandboxKeyderived from the host's request context (never from model input — a model-supplied key would let one conversation address another's sandbox), and a declaredIsolationlevel (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) andagent-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 noapt, so the protocol's operations split intoexec(shell) andrun_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'sAllowedDomainand both packages'FileMountare 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.securitylabel 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?
AIContextProvider+ anexecute_codetool). What Recommended integration pattern for remote HTTP sandboxes like Daytona in MAF? #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.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
Language/SDK
Both