Python: Fix hyperlight WasmSandbox cross-thread Drop and harden hosted-agent sample (#5603)

* update hyperlight to beta and move samples, add hosted agent sample

* Python: Fix hyperlight WasmSandbox cross-thread Drop and harden sample

Root cause: when a worker-side closure raised, the exception's __traceback__
retained frame locals that included the partially constructed PyO3 sandbox.
Future.result() re-raised that exception on the caller thread, and when the
caller's exception was eventually GC'd the frame locals were released
off-thread, dec_ref'ing the unsendable sandbox from the wrong thread and
tripping the PyO3 panic
'_native_wasm::WasmSandbox is unsendable, but is being dropped on another thread'.

Fix:
* Add _SandboxWorker._run_on_worker which catches every exception on the
  worker, drops __traceback__ there, deletes the original exception, and
  re-raises a fresh instance on the caller thread. initialize and execute
  route through it; dispose keeps its bare-submit semantics.
* Add an opt-in diagnostic module _drop_diagnostic (no-op unless
  HYPERLIGHT_TRACE_DROPS=1) that installs a sys.unraisablehook and dumps
  owner-thread + per-thread stacks on any future cross-thread unsendable
  Drop. Useful for triaging similar PyO3 regressions.
* Tests: cross-thread invocation, traceback-leak isolation, _SandboxEntry
  attribute-shape check, and a stale-reference stress test driven through
  asyncio.to_thread.

Sample (samples/04-hosting/foundry-hosted-agents/responses/06_hyperlight_codeact):
* Dockerfile installs agent-framework-* from in-tree source with python/ as
  build context so unreleased fixes can be validated end-to-end.
* call_server.py pins the Responses API version.
* main.py enables include_detailed_errors=True so future tool failures
  surface the actual exception text instead of a bare 'Error: Function
  failed.' string.
* README.md documents the in-tree-package build and the Hyperlight
  hypervisor requirement (/dev/kvm on Linux, MSHV on Windows). Hosted
  environments without hypervisor passthrough surface 'No Hypervisor was
  found for Sandbox'; this is a hosting constraint, not a hyperlight bug.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: remove _drop_diagnostic from hyperlight package

The diagnostic module was useful while bisecting the cross-thread Drop bug,
but it is no longer needed now that _SandboxWorker._run_on_worker prevents
the panic at the source.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: address PR review feedback on hyperlight

- Use lazy agent_framework.hyperlight import in sample main.py.
- Env-driven endpoint (FOUNDRY_AGENT_ENDPOINT) in call_server.py; remove personal URLs.
- Align agent.yaml model deployment with manifest (gpt-4.1-mini).
- Tighten Dockerfile requirements guard; drop dangling deploy.ps1 reference.
- Preserve exception args when sanitizing tracebacks in _run_on_worker.
- Add public _SandboxWorker.is_alive(); update test to avoid private attr.
- Add namespace coverage tests for agent_framework.hyperlight lazy loader.
- Add prominent note: Foundry hosted-agent runtime does not yet support
  Hyperlight (no hypervisor exposed); container works locally with /dev/kvm.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: bump hyperlight-sandbox dependencies to 0.4.x

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: renumber hyperlight codeact sample to 08

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Coerce worker exception args to strings for cross-thread safety

Stringify exc.args on the worker thread before propagating, so any
PyO3 unsendable object captured in args (e.g. via a caller-supplied
callback or underlying SDK) cannot be Dropped on the calling thread.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* moved sample

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-05-05 12:06:16 +02:00
committed by GitHub
Unverified
parent 36b9b41e3b
commit 57c901a245
26 changed files with 967 additions and 393 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ Status is grouped into these buckets:
| `agent-framework-foundry-local` | `python/packages/foundry_local` | `beta` | | `agent-framework-foundry-local` | `python/packages/foundry_local` | `beta` |
| `agent-framework-gemini` | `python/packages/gemini` | `alpha` | | `agent-framework-gemini` | `python/packages/gemini` | `alpha` |
| `agent-framework-github-copilot` | `python/packages/github_copilot` | `beta` | | `agent-framework-github-copilot` | `python/packages/github_copilot` | `beta` |
| `agent-framework-hyperlight` | `python/packages/hyperlight` | `alpha` | | `agent-framework-hyperlight` | `python/packages/hyperlight` | `beta` |
| `agent-framework-lab` | `python/packages/lab` | `beta` | | `agent-framework-lab` | `python/packages/lab` | `beta` |
| `agent-framework-mem0` | `python/packages/mem0` | `beta` | | `agent-framework-mem0` | `python/packages/mem0` | `beta` |
| `agent-framework-ollama` | `python/packages/ollama` | `beta` | | `agent-framework-ollama` | `python/packages/ollama` | `beta` |
@@ -0,0 +1,35 @@
# Copyright (c) Microsoft. All rights reserved.
"""Hyperlight CodeAct namespace for optional Agent Framework connectors.
This module lazily re-exports objects from ``agent-framework-hyperlight``.
"""
import importlib
from typing import Any
_IMPORTS: dict[str, tuple[str, str]] = {
"AllowedDomain": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
"AllowedDomainInput": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
"FileMount": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
"FileMountInput": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
"HyperlightCodeActProvider": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
"HyperlightExecuteCodeTool": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
}
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
import_path, package_name = _IMPORTS[name]
try:
return getattr(importlib.import_module(import_path), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The package {package_name} is required to use `{name}`. "
f"Please use `pip install {package_name}`, or update your requirements.txt or pyproject.toml file."
) from exc
raise AttributeError(f"Module `hyperlight` has no attribute {name}.")
def __dir__() -> list[str]:
return list(_IMPORTS.keys())
+1
View File
@@ -48,6 +48,7 @@ all = [
"agent-framework-foundry", "agent-framework-foundry",
"agent-framework-foundry-local", "agent-framework-foundry-local",
"agent-framework-github-copilot; python_version >= '3.11'", "agent-framework-github-copilot; python_version >= '3.11'",
"agent-framework-hyperlight; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and python_version < '3.14'",
"agent-framework-lab", "agent-framework-lab",
"agent-framework-mem0", "agent-framework-mem0",
"agent-framework-ollama", "agent-framework-ollama",
@@ -0,0 +1,42 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from types import ModuleType
import pytest
import agent_framework.hyperlight as hyperlight
def test_hyperlight_namespace_dir_lists_lazy_exports() -> None:
names = dir(hyperlight)
for expected in (
"AllowedDomain",
"AllowedDomainInput",
"FileMount",
"FileMountInput",
"HyperlightCodeActProvider",
"HyperlightExecuteCodeTool",
):
assert expected in names
def test_hyperlight_namespace_lazy_loads_known_attribute(monkeypatch: pytest.MonkeyPatch) -> None:
sentinel = object()
fake_module = ModuleType("agent_framework_hyperlight")
fake_module.HyperlightCodeActProvider = sentinel # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "agent_framework_hyperlight", fake_module)
assert hyperlight.HyperlightCodeActProvider is sentinel
def test_hyperlight_namespace_unknown_attribute_raises_attribute_error() -> None:
with pytest.raises(AttributeError, match="Module `hyperlight` has no attribute DoesNotExist."):
_ = hyperlight.DoesNotExist # type: ignore[attr-defined]
def test_hyperlight_namespace_missing_package_raises_helpful_error(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setitem(sys.modules, "agent_framework_hyperlight", None)
with pytest.raises(ModuleNotFoundError, match="agent-framework-hyperlight"):
_ = hyperlight.HyperlightCodeActProvider
+4 -3
View File
@@ -1,6 +1,6 @@
# agent-framework-hyperlight # agent-framework-hyperlight
Alpha Hyperlight-backed CodeAct integrations for Microsoft Agent Framework. Hyperlight-backed CodeAct integrations for Microsoft Agent Framework.
## Installation ## Installation
@@ -121,8 +121,9 @@ codeact = HyperlightCodeActProvider(
## Notes ## Notes
- This package is intentionally separate from `agent-framework-core` so CodeAct - This package is intentionally separate from `agent-framework-core` so CodeAct
usage and installation remain optional. usage and installation remain optional. With `agent-framework-core[all]` (or
- Alpha-package samples live under `packages/hyperlight/samples/`. the meta `agent-framework`) installed it is also reachable through the
lazy-loading namespace `agent_framework.hyperlight`.
- `file_mounts` accepts a single string shorthand, an explicit `(host_path, - `file_mounts` accepts a single string shorthand, an explicit `(host_path,
mount_path)` pair, or a `FileMount` named tuple. The host-side path in the mount_path)` pair, or a `FileMount` named tuple. The host-side path in the
explicit forms may be a `str` or `Path`. Use the explicit two-value form when explicit forms may be a `str` or `Path`. Use the explicit two-value form when
@@ -8,10 +8,10 @@ import shutil
import threading import threading
import time import time
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from concurrent.futures import Future, ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from contextlib import suppress from contextlib import suppress
from copy import copy from copy import copy
from dataclasses import dataclass, field from dataclasses import dataclass
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any, Protocol, TypeGuard, TypeVar, cast from typing import Any, Protocol, TypeGuard, TypeVar, cast
@@ -92,39 +92,208 @@ _T = TypeVar("_T")
class _SandboxWorker: class _SandboxWorker:
"""Single-threaded executor that confines all sandbox operations to one OS thread. """Thread-confined actor that owns a sandbox + snapshot.
The Hyperlight ``WasmSandbox`` is declared ``unsendable`` in PyO3, meaning it can only be The Hyperlight ``WasmSandbox`` is declared ``unsendable`` in PyO3: it can only be
accessed from the OS thread that created it; touching it from any other thread triggers a accessed *and dropped* from the OS thread that created it. Touching or
Rust panic that cannot be caught from Python. Every cached :class:`_SandboxEntry` therefore releasing it on any other thread triggers a Rust panic
owns its own ``_SandboxWorker``, and *all* lifecycle and execution calls against the (``"_native_wasm::WasmSandbox is unsendable, but is being dropped on another thread"``)
underlying sandbox object must be routed through :meth:`submit`/:meth:`run`. that cannot be caught from Python.
To make this guarantee airtight, this class is an actor: the underlying
sandbox and snapshot are stored ONLY as worker-local state and are never
exposed to or returned to other threads. Public methods submit closures to
the dedicated single-thread executor and return only sendable results.
Because no caller can ever obtain a strong reference to the unsendable
objects, no caller can ever cause them to be dropped on the wrong thread.
Exception isolation: exceptions raised inside worker closures carry a
``__traceback__`` whose frames retain references to local variables --
including PyO3 unsendable sandbox/native_result objects. Letting such an
exception propagate to the calling thread would defeat the actor model:
when the calling thread GCs the exception, the traceback's frame locals
are dropped on the wrong thread and PyO3 panics. To prevent this, every
exception raised inside a worker closure is caught on the worker, the
traceback is dropped while still on the worker thread, and a sanitized
copy (preserving message and exception type) is re-raised on the caller.
""" """
__slots__ = ("_executor",) __slots__ = ("_executor", "_initialized", "_sandbox", "_snapshot")
def __init__(self, *, name: str = "hl-sandbox") -> None: def __init__(self, *, name: str = "hl-sandbox") -> None:
self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix=name) self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix=name)
# _sandbox/_snapshot are accessed/mutated ONLY from worker-side closures.
self._sandbox: Any = None
self._snapshot: Any = None
self._initialized = False
def submit(self, fn: Callable[..., _T], /, *args: Any, **kwargs: Any) -> Future[_T]: def _run_on_worker(self, fn: Callable[[], _T]) -> _T:
return self._executor.submit(fn, *args, **kwargs) """Run ``fn`` on the worker thread; sanitize any exception's traceback there.
def run(self, fn: Callable[..., _T], /, *args: Any, **kwargs: Any) -> _T: If ``fn`` raises, the exception's ``__traceback__`` is dropped on the worker
return self._executor.submit(fn, *args, **kwargs).result() thread (so any PyO3 unsendable locals captured in frame locals are released
on the owner thread) and a fresh exception of the same type is raised on
the caller's thread carrying only the original message.
"""
def shutdown(self) -> None: def _wrapped() -> tuple[bool, Any]:
# Do not block on shutdown; stop accepting new tasks, but allow the currently running try:
# task and any already-queued tasks to finish before the worker thread exits. return True, fn()
except BaseException as exc:
exc_type = type(exc)
# Capture args (usually (message,)) so the re-raised exception keeps the
# original shape for types whose constructor doesn't accept a single str.
# Coerce each arg to ``str`` on the worker thread: if a caller-supplied
# callback (or an underlying SDK) constructed the exception with a PyO3
# unsendable object in args, forwarding it as-is would re-introduce the
# same cross-thread Drop hazard the traceback nulling avoids. Strings
# are always sendable. Fall back to the str() form if args is empty.
exc_args: tuple[str, ...] = tuple(str(a) for a in exc.args) if exc.args else (str(exc),)
# Drop the traceback on the worker thread so frame locals (which
# may include PyO3 unsendable objects) are released here, not on
# the caller thread that will receive the wrapped exception.
exc.__traceback__ = None
del exc
return False, (exc_type, exc_args)
ok, payload = self._executor.submit(_wrapped).result()
if ok:
return cast(_T, payload)
exc_type, exc_args = cast(tuple[type[BaseException], tuple[str, ...]], payload)
# Re-raise a fresh instance with no chained traceback frames from the worker.
# If the exception type's constructor rejects the captured args (rare), fall
# back to a RuntimeError carrying the string form so we never lose the signal.
try:
raise exc_type(*exc_args)
except TypeError:
raise RuntimeError(f"{exc_type.__name__}: {exc_args}") from None
def initialize(self, build_fn: Callable[[], tuple[Any, Any]]) -> None:
"""Build and install the sandbox+snapshot on the worker thread.
``build_fn`` is invoked with no arguments on the worker thread. It must
return ``(sandbox, snapshot)``. Both references are retained as worker-
local attributes; they do not escape this thread.
"""
def _init_on_worker() -> None:
sandbox, snapshot = build_fn()
self._sandbox = sandbox
self._snapshot = snapshot
self._initialized = True
# Locals fall out of scope on the worker thread; the worker-local
# attributes hold the only strong refs from now on.
self._run_on_worker(_init_on_worker)
def execute(
self,
*,
code: str,
output_dir: TemporaryDirectory[str] | None,
build_contents: Callable[..., list[Content]],
) -> list[Content]:
"""Restore + run + build sendable contents — all on the worker thread.
Returns a plain ``list[Content]`` whose elements never carry strong
references to the underlying sandbox or snapshot.
"""
def _on_worker() -> list[Content]:
sandbox = self._sandbox
snapshot = self._snapshot
sandbox.restore(snapshot)
_clear_directory(output_dir)
result = sandbox.run(code=code)
try:
return build_contents(
result=result,
sandbox=sandbox,
output_dir=output_dir,
code=code,
)
finally:
# ``result`` may carry a back-reference to the sandbox. Force its
# final dec_ref on this thread so Drop runs here, not on whatever
# thread later GCs the ``Content`` list.
del result
return self._run_on_worker(_on_worker)
def is_alive(self) -> bool:
"""Return ``True`` while the worker thread can still accept new submissions.
Useful for tests/observability; returns ``False`` after ``dispose()``.
"""
try:
self._executor.submit(lambda: None).result(timeout=1.0)
except RuntimeError:
return False
return True
def dispose(self) -> None:
"""Release the sandbox+snapshot on the owner worker thread, then shut down.
Safe to call multiple times. After ``dispose`` returns, the sandbox/
snapshot are guaranteed to have been released on the worker thread; any
remaining references held elsewhere have already been impossible (they
never leaked out of this object).
"""
def _dispose_on_worker() -> None:
sandbox = self._sandbox
snapshot = self._snapshot
self._sandbox = None
self._snapshot = None
close_hook = (
(getattr(sandbox, "close", None) or getattr(sandbox, "shutdown", None)) if sandbox is not None else None
)
if callable(close_hook):
with suppress(Exception):
close_hook()
# ``sandbox`` and ``snapshot`` are local on the worker thread and
# will be dec_ref'd here when this frame returns -> Drop on worker.
del sandbox, snapshot
if self._initialized:
try:
# Use the bare executor here -- _dispose_on_worker swallows its
# own errors and never raises, so traceback sanitization is not
# needed and we want dispose to remain robust during teardown.
self._executor.submit(_dispose_on_worker).result()
except RuntimeError:
# Worker already shut down; sandbox/snapshot will leak rather
# than panic on the wrong thread. This is the safest fallback.
pass
finally:
self._initialized = False
# Do not block on shutdown; stop accepting new tasks, but allow any
# already-queued task (including the dispose closure above) to finish.
self._executor.shutdown(wait=False, cancel_futures=False) self._executor.shutdown(wait=False, cancel_futures=False)
@dataclass @dataclass
class _SandboxEntry: class _SandboxEntry:
sandbox: Any """Per-config cached sandbox handle.
snapshot: Any
The unsendable sandbox/snapshot live inside ``worker`` and never appear as
Python attributes on this object. Anything stored here is sendable and
safe to GC on any thread.
"""
worker: _SandboxWorker
input_dir: TemporaryDirectory[str] | None input_dir: TemporaryDirectory[str] | None
output_dir: TemporaryDirectory[str] | None output_dir: TemporaryDirectory[str] | None
worker: _SandboxWorker = field(default_factory=_SandboxWorker)
def dispose(self) -> None:
"""Release the sandbox+snapshot on the worker thread and clean up temp dirs."""
self.worker.dispose()
for tmp_dir in (self.input_dir, self.output_dir):
if tmp_dir is not None:
with suppress(Exception):
tmp_dir.cleanup()
self.input_dir = None
self.output_dir = None
def _load_sandbox_class() -> type[Any]: def _load_sandbox_class() -> type[Any]:
@@ -432,6 +601,23 @@ def _parse_output_files(
return [] return []
def _result_snapshot(result: Any) -> dict[str, Any]:
"""Return a sendable plain-dict snapshot of a sandbox.run() result.
The Hyperlight ``WasmSandbox.run()`` return value is a PyO3 ``unsendable`` object that
can carry a back-reference to the sandbox itself. Storing it on
``Content.raw_representation`` lets it ride out of the owner thread and be garbage
collected elsewhere, which trips the PyO3 ``Drop`` panic. Build a thread-safe summary
of the fields we actually surface and forward that instead, so the original result can
be released on the worker thread that produced it.
"""
return {
"success": bool(getattr(result, "success", False)),
"stdout": str(getattr(result, "stdout", "") or ""),
"stderr": str(getattr(result, "stderr", "") or ""),
}
def _build_execution_contents( def _build_execution_contents(
*, *,
result: Any, result: Any,
@@ -442,10 +628,11 @@ def _build_execution_contents(
success = bool(getattr(result, "success", False)) success = bool(getattr(result, "success", False))
stdout = str(getattr(result, "stdout", "") or "").replace("\r\n", "\n") or None stdout = str(getattr(result, "stdout", "") or "").replace("\r\n", "\n") or None
stderr = str(getattr(result, "stderr", "") or "").replace("\r\n", "\n") or None stderr = str(getattr(result, "stderr", "") or "").replace("\r\n", "\n") or None
snapshot = _result_snapshot(result)
outputs: list[Content] = [] outputs: list[Content] = []
if stdout is not None: if stdout is not None:
outputs.append(Content.from_text(stdout, raw_representation=result)) outputs.append(Content.from_text(stdout, raw_representation=snapshot))
outputs.extend( outputs.extend(
_parse_output_files( _parse_output_files(
@@ -457,7 +644,7 @@ def _build_execution_contents(
if success: if success:
if stderr is not None: if stderr is not None:
outputs.append(Content.from_text(stderr, raw_representation=result)) outputs.append(Content.from_text(stderr, raw_representation=snapshot))
if not outputs: if not outputs:
outputs.append(Content.from_text("Code executed successfully without output.")) outputs.append(Content.from_text("Code executed successfully without output."))
return outputs return outputs
@@ -467,7 +654,7 @@ def _build_execution_contents(
Content.from_error( Content.from_error(
message="Execution error", message="Execution error",
error_details=error_details, error_details=error_details,
raw_representation=result, raw_representation=snapshot,
) )
) )
return outputs return outputs
@@ -533,21 +720,14 @@ class _SandboxRegistry(SandboxRuntime):
Entries are keyed by ``config.cache_key()``. All operations against the underlying Entries are keyed by ``config.cache_key()``. All operations against the underlying
sandbox object are routed through the entry's dedicated single-threaded worker, which sandbox object are routed through the entry's dedicated single-threaded worker, which
both serializes concurrent callers and satisfies the PyO3 ``unsendable`` invariant both serializes concurrent callers and satisfies the PyO3 ``unsendable`` invariant
that the sandbox can only be touched from the thread that created it. that the sandbox can only be touched from the thread that created it. The unsendable
objects never escape the worker; this method returns only sendable plain Python data.
""" """
entry = self._get_or_create_entry(config) entry = self._get_or_create_entry(config)
return entry.worker.run(self._run_on_worker, entry, code) return entry.worker.execute(
@staticmethod
def _run_on_worker(entry: _SandboxEntry, code: str) -> list[Content]:
entry.sandbox.restore(entry.snapshot)
_clear_directory(entry.output_dir)
result = entry.sandbox.run(code=code)
return _build_execution_contents(
result=result,
sandbox=entry.sandbox,
output_dir=entry.output_dir,
code=code, code=code,
output_dir=entry.output_dir,
build_contents=_build_execution_contents,
) )
def _get_or_create_entry(self, config: _RunConfig) -> _SandboxEntry: def _get_or_create_entry(self, config: _RunConfig) -> _SandboxEntry:
@@ -562,22 +742,19 @@ class _SandboxRegistry(SandboxRuntime):
def close(self) -> None: def close(self) -> None:
"""Shut down all per-entry worker threads and release per-entry resources. """Shut down all per-entry worker threads and release per-entry resources.
Safe to call multiple times. Runs any sandbox close hook on the entry's Safe to call multiple times. Each entry's sandbox/snapshot is disposed on the
own worker thread to honor the PyO3 ``unsendable`` invariant. worker thread that created it to honor the PyO3 ``unsendable`` invariant.
""" """
with self._entries_lock: with self._entries_lock:
entries = list(self._entries.values()) entries = list(self._entries.values())
self._entries.clear() self._entries.clear()
for entry in entries: try:
close_hook = getattr(entry.sandbox, "close", None) or getattr(entry.sandbox, "shutdown", None) for entry in entries:
if callable(close_hook): entry.dispose()
with suppress(Exception): finally:
entry.worker.run(close_hook) # Drop our local strong references; entries' own refs to sandbox/snapshot
entry.worker.shutdown() # were already moved into the per-worker disposal closure inside dispose().
for tmp_dir in (entry.input_dir, entry.output_dir): del entries
if tmp_dir is not None:
with suppress(Exception):
tmp_dir.cleanup()
def _create_entry(self, config: _RunConfig) -> _SandboxEntry: def _create_entry(self, config: _RunConfig) -> _SandboxEntry:
input_dir_handle = TemporaryDirectory() if config.filesystem_enabled else None input_dir_handle = TemporaryDirectory() if config.filesystem_enabled else None
@@ -617,8 +794,6 @@ class _SandboxRegistry(SandboxRuntime):
methods=list(allowed_domain.methods) if allowed_domain.methods is not None else None, methods=list(allowed_domain.methods) if allowed_domain.methods is not None else None,
) )
worker = _SandboxWorker()
def _build_sandbox() -> tuple[Any, Any]: def _build_sandbox() -> tuple[Any, Any]:
sandbox = _create_sandbox() sandbox = _create_sandbox()
_configure_sandbox(sandbox=sandbox, expand_missing_scheme=False) _configure_sandbox(sandbox=sandbox, expand_missing_scheme=False)
@@ -636,18 +811,17 @@ class _SandboxRegistry(SandboxRuntime):
snapshot = sandbox.snapshot() snapshot = sandbox.snapshot()
return sandbox, snapshot return sandbox, snapshot
worker = _SandboxWorker()
try: try:
sandbox, snapshot = worker.run(_build_sandbox) worker.initialize(_build_sandbox)
except BaseException: except BaseException:
worker.shutdown() worker.dispose()
raise raise
return _SandboxEntry( return _SandboxEntry(
sandbox=sandbox, worker=worker,
snapshot=snapshot,
input_dir=input_dir_handle, input_dir=input_dir_handle,
output_dir=output_dir_handle, output_dir=output_dir_handle,
worker=worker,
) )
+6 -7
View File
@@ -4,7 +4,7 @@ description = "Hyperlight CodeAct integrations for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0a260429" version = "1.0.0b260501"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -12,7 +12,7 @@ urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=ta
urls.issues = "https://github.com/microsoft/agent-framework/issues" urls.issues = "https://github.com/microsoft/agent-framework/issues"
classifiers = [ classifiers = [
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Development Status :: 3 - Alpha", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
@@ -23,9 +23,9 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.2.2,<2", "agent-framework-core>=1.2.2,<2",
"hyperlight-sandbox>=0.3.0,<0.4", "hyperlight-sandbox>=0.4.0,<0.5",
"hyperlight-sandbox-backend-wasm>=0.3.0,<0.4 ; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and python_version < '3.14'", "hyperlight-sandbox-backend-wasm>=0.4.0,<0.5 ; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and python_version < '3.14'",
"hyperlight-sandbox-python-guest>=0.3.0,<0.4", "hyperlight-sandbox-python-guest>=0.4.0,<0.5",
] ]
[tool.uv] [tool.uv]
@@ -53,7 +53,6 @@ markers = [
extend = "../../pyproject.toml" extend = "../../pyproject.toml"
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"samples/**" = ["INP", "T201"]
"tests/**" = ["D", "INP", "TD", "ERA001", "RUF", "S"] "tests/**" = ["D", "INP", "TD", "ERA001", "RUF", "S"]
[tool.coverage.run] [tool.coverage.run]
@@ -82,7 +81,7 @@ disallow_untyped_decorators = true
[tool.bandit] [tool.bandit]
targets = ["agent_framework_hyperlight"] targets = ["agent_framework_hyperlight"]
exclude_dirs = ["tests", "samples"] exclude_dirs = ["tests"]
[tool.poe] [tool.poe]
executor.type = "uv" executor.type = "uv"
@@ -1,43 +0,0 @@
# Hyperlight CodeAct samples
These samples demonstrate the alpha `agent-framework-hyperlight` package.
## When to use which pattern
- **Provider pattern** (`codeact_context_provider.py`): Use when the tool
registry, file mounts, or network allow-list may change between runs, or when
you want the provider to manage CodeAct instructions and approval computation
automatically on every invocation. This is the recommended default for
production agents that need dynamic capability management or concurrent runs
sharing one provider.
- **Manual static wiring** (`codeact_manual_wiring.py`): Use when the sandbox
tool set and capabilities are fixed for the agent's lifetime. This pattern
builds instructions once, passes `execute_code` alongside direct tools in
`tools=`, and skips the per-run provider lifecycle entirely. Simpler setup,
but changes to the tool registry after construction will not update the
agent's instructions automatically.
- **Standalone tool** (`codeact_tool.py`): Use for the simplest integration
where `execute_code` is added directly to the agent tool list. The tool's own
description advertises `call_tool(...)` and the registered sandbox tools, so
no extra agent instructions are needed. Best for quick prototyping or when
CodeAct is just another tool alongside the agent's direct tools.
## Samples
- `codeact_context_provider.py` shows the provider-owned CodeAct model where the
agent only sees `execute_code` and sandbox tools are owned by
`HyperlightCodeActProvider`.
- `codeact_manual_wiring.py` shows static wiring where `HyperlightExecuteCodeTool`
and its instructions are passed directly to the `Agent` constructor.
- `codeact_tool.py` shows the standalone `HyperlightExecuteCodeTool` surface
where `execute_code` is added directly to the agent tool list.
Run the samples from the repository after installing the workspace dependencies:
```bash
uv run --directory packages/hyperlight python samples/codeact_context_provider.py
uv run --directory packages/hyperlight python samples/codeact_manual_wiring.py
uv run --directory packages/hyperlight python samples/codeact_tool.py
```
@@ -1,253 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
"""Benchmark CodeAct vs. traditional tool-calling for a multi-tool-call task.
This sample runs the same prompt against the same FoundryChatClient twice:
1. **Traditional tool-calling**: the five business tools are passed directly to
the agent, so the model calls each tool individually via the LLM tool-call
interface.
2. **CodeAct**: the same tools are registered on a HyperlightCodeActProvider
and the model sees a single ``execute_code`` tool that calls them from
inside the Hyperlight sandbox via ``call_tool(...)``.
The task (computing grand totals per user) naturally requires many tool calls
to complete. At the end, the sample prints elapsed time and token usage for
each run so the two approaches can be compared.
Run with:
cd python
uv run --directory packages/hyperlight python samples/codeact_benchmark.py
Required environment variables (loaded from ``.env`` if present):
FOUNDRY_PROJECT_ENDPOINT
FOUNDRY_MODEL
"""
from __future__ import annotations
import asyncio
import os
import time
from typing import Annotated, Any, Literal
from agent_framework import Agent, AgentResponse, UsageDetails
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from agent_framework_hyperlight import HyperlightCodeActProvider
load_dotenv()
# 1. Deterministic "business" data and tools.
_USERS: list[dict[str, Any]] = [
{"id": 1, "name": "Alice", "region": "EU", "tier": "gold"},
{"id": 2, "name": "Bob", "region": "US", "tier": "silver"},
{"id": 3, "name": "Charlie", "region": "US", "tier": "gold"},
{"id": 4, "name": "Diana", "region": "APAC", "tier": "bronze"},
{"id": 5, "name": "Evan", "region": "EU", "tier": "silver"},
{"id": 6, "name": "Fiona", "region": "US", "tier": "gold"},
{"id": 7, "name": "George", "region": "APAC", "tier": "gold"},
{"id": 8, "name": "Hana", "region": "EU", "tier": "bronze"},
]
_ORDERS: dict[int, list[dict[str, Any]]] = {
1: [{"product": "Widget", "qty": 3, "unit_price": 9.99}, {"product": "Gadget", "qty": 1, "unit_price": 19.99}],
2: [{"product": "Widget", "qty": 1, "unit_price": 9.99}],
3: [{"product": "Gadget", "qty": 2, "unit_price": 19.99}, {"product": "Thingamajig", "qty": 4, "unit_price": 4.50}],
4: [{"product": "Widget", "qty": 10, "unit_price": 9.99}],
5: [{"product": "Gadget", "qty": 1, "unit_price": 19.99}],
6: [{"product": "Widget", "qty": 2, "unit_price": 9.99}, {"product": "Thingamajig", "qty": 5, "unit_price": 4.50}],
7: [{"product": "Gadget", "qty": 3, "unit_price": 19.99}],
8: [{"product": "Thingamajig", "qty": 2, "unit_price": 4.50}],
}
_DISCOUNTS: dict[str, float] = {"gold": 0.20, "silver": 0.10, "bronze": 0.05}
_TAX_RATES: dict[str, float] = {"EU": 0.21, "US": 0.08, "APAC": 0.10}
def list_users() -> list[dict[str, Any]]:
"""Return all users as a list of dictionaries.
Each entry has keys: id (int), name (str), region (str), tier (str).
"""
return _USERS
def get_orders_for_user(
user_id: Annotated[int, "The user id whose orders to retrieve."],
) -> list[dict[str, Any]]:
"""Return the user's orders as a list of dictionaries.
Each entry has keys: product (str), qty (int), unit_price (float).
"""
return _ORDERS.get(user_id, [])
def get_discount_rate(
tier: Annotated[Literal["gold", "silver", "bronze"], "The customer tier."],
) -> float:
"""Return the discount rate as a float fraction (e.g. 0.2 for 20%)."""
return _DISCOUNTS[tier]
def get_tax_rate(
region: Annotated[Literal["EU", "US", "APAC"], "The region code."],
) -> float:
"""Return the tax rate as a float fraction (e.g. 0.21 for 21%)."""
return _TAX_RATES[region]
def compute_line_total(
qty: Annotated[int, "Line item quantity."],
unit_price: Annotated[float, "Line item unit price."],
discount_rate: Annotated[float, "Discount rate as a fraction (e.g. 0.2 for 20%)."],
tax_rate: Annotated[float, "Tax rate as a fraction (e.g. 0.21 for 21%)."],
) -> float:
"""Compute a single order line total.
Formula: qty * unit_price * (1 - discount_rate) * (1 + tax_rate), rounded to 2 decimals.
"""
subtotal = qty * unit_price
discounted = subtotal * (1.0 - discount_rate)
return round(discounted * (1.0 + tax_rate), 2)
TOOLS = [list_users, get_orders_for_user, get_discount_rate, get_tax_rate, compute_line_total]
# 2. Structured output schema shared between both runs.
class UserTotal(BaseModel):
"""A user's grand total of all their orders."""
user_id: int = Field(description="The user's id.")
name: str = Field(description="The user's display name.")
grand_total: float = Field(description="Sum of all line totals, rounded to 2 decimals.")
class UserGrandTotals(BaseModel):
"""Structured output schema for both runs."""
results: list[UserTotal] = Field(description="One entry per user, sorted by grand_total descending.")
INSTRUCTIONS = "You are a careful assistant. Use the provided tools for every lookup and computation."
BENCHMARK_PROMPT = (
"For every user in our system (there are 8 of them), compute the grand total of all their orders. "
"Use the compute_line_total tool for each user's orders, after looking up the relevant discount and "
"tax rates for that user. "
"Use the provided tools for EVERY data lookup (users, orders, discount rates, tax rates) and for EVERY "
"line-total computation via compute_line_total — do not invent values or hardcode any numbers. "
"The total per order item should apply the discount first and then the tax "
"(e.g. total = qty * unit_price * (1-discount) * (1+tax)). "
"Return one entry per user, sorted by grand_total descending."
)
def get_client() -> FoundryChatClient:
"""Create a FoundryChatClient from environment variables."""
return FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# 3. Two runners that share the same tools, prompt, and structured output schema.
async def _run_traditional() -> tuple[float, AgentResponse]:
agent = Agent(
client=get_client(),
name="TraditionalAgent",
instructions=INSTRUCTIONS,
tools=TOOLS,
default_options={"response_format": UserGrandTotals},
)
start = time.perf_counter()
result = await agent.run(BENCHMARK_PROMPT)
elapsed = time.perf_counter() - start
return elapsed, result
async def _run_codeact() -> tuple[float, AgentResponse]:
codeact = HyperlightCodeActProvider(
tools=TOOLS,
approval_mode="never_require",
)
agent = Agent(
client=get_client(),
name="CodeActAgent",
instructions=INSTRUCTIONS,
context_providers=[codeact],
default_options={"response_format": UserGrandTotals},
)
start = time.perf_counter()
result = await agent.run(BENCHMARK_PROMPT)
elapsed = time.perf_counter() - start
return elapsed, result
# 4. Report results side by side.
def _print_section(title: str) -> None:
bar = "=" * 70
print(f"\n{bar}\n{title}\n{bar}")
def _format_usage(usage: UsageDetails | None) -> str:
if usage is None:
return "usage=<none>"
return (
f"input={usage.get('input_token_count') or 0:>6} "
f"output={usage.get('output_token_count') or 0:>6} "
f"total={usage.get('total_token_count') or 0:>6}"
)
def _print_results(result: AgentResponse) -> None:
if result.value is not None:
for row in result.value.results:
print(f" user_id={row.user_id:>2} name={row.name:<8} grand_total={row.grand_total:>8.2f}")
else:
print(result.text)
async def main() -> None:
"""Run the benchmark and print a comparison."""
trad_time, trad_result = await _run_traditional()
code_time, code_result = await _run_codeact()
_print_section("Traditional tool-calling")
print(f"time={trad_time:7.2f}s {_format_usage(trad_result.usage_details)}")
_print_results(trad_result)
_print_section("CodeAct (HyperlightCodeActProvider)")
print(f"time={code_time:7.2f}s {_format_usage(code_result.usage_details)}")
_print_results(code_result)
_print_section("Comparison")
trad_total = (trad_result.usage_details or {}).get("total_token_count") or 0
code_total = (code_result.usage_details or {}).get("total_token_count") or 0
def pct(new: float, old: float) -> str:
if old == 0:
return "n/a"
delta = (new - old) / old * 100
sign = "+" if delta >= 0 else ""
return f"{sign}{delta:.1f}%"
print(f"time : traditional={trad_time:7.2f}s codeact={code_time:7.2f}s delta={pct(code_time, trad_time)}")
print(f"tokens : traditional={trad_total:7d} codeact={code_total:7d} delta={pct(code_total, trad_total)}")
if __name__ == "__main__":
asyncio.run(main())
@@ -3,6 +3,9 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import contextlib
import dataclasses
import gc
import importlib.metadata import importlib.metadata
import importlib.util import importlib.util
import inspect import inspect
@@ -1042,9 +1045,8 @@ def test_sandbox_registry_close_shuts_down_workers(monkeypatch: pytest.MonkeyPat
registry.close() registry.close()
assert registry._entries == {} assert registry._entries == {}
# Submitting after shutdown must fail; this proves the executor was actually torn down. # After shutdown, the worker must report itself as no longer accepting work.
with pytest.raises(RuntimeError): assert worker.is_alive() is False
worker.submit(lambda: None)
def test_sandbox_registry_close_releases_per_entry_resources(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: def test_sandbox_registry_close_releases_per_entry_resources(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
@@ -1125,3 +1127,243 @@ async def test_make_sandbox_callback_propagates_exceptions() -> None:
callback = execute_code_module._make_sandbox_callback(boom) callback = execute_code_module._make_sandbox_callback(boom)
with pytest.raises(RuntimeError, match="nope"): with pytest.raises(RuntimeError, match="nope"):
callback(x=1) callback(x=1)
class _OwnerThreadTrackedResult:
"""Fake sandbox.run() return value that mirrors a PyO3 ``unsendable`` object's Drop.
Records (rather than panics, since CPython swallows __del__ exceptions) the OS thread
that finalized the object, so tests can assert it was dropped on the sandbox's owner
thread and not on whatever thread happened to GC it.
"""
drop_thread_violations: list[str] = []
def __init__(self, *, owner_thread: int, success: bool = True, stdout: str = "", stderr: str = "") -> None:
self._owner_thread = owner_thread
self.success = success
self.stdout = stdout
self.stderr = stderr
def __del__(self) -> None:
ident = threading.get_ident()
if ident != self._owner_thread:
type(self).drop_thread_violations.append(
f"_OwnerThreadTrackedResult dropped on thread {ident}, owner was {self._owner_thread}"
)
class _ResultDropTrackingFakeSandbox(_FakeSandbox):
"""Fake sandbox whose ``run()`` returns an owner-thread-tracking result."""
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self._owner_thread = threading.get_ident()
def run(self, code: str) -> Any:
del code
# Real Hyperlight runs almost always have non-empty stdout (the executed Python
# ``print`` output); that is the path where _build_execution_contents attaches
# raw_representation=result and the unsendable object escapes the worker thread.
return _OwnerThreadTrackedResult(owner_thread=self._owner_thread, success=True, stdout="hello\n")
def test_sandbox_run_result_is_finalized_on_owner_thread(monkeypatch: pytest.MonkeyPatch) -> None:
"""Regression: the object returned by ``sandbox.run`` must not escape its owner thread.
The Hyperlight ``WasmSandbox`` is unsendable; the value its ``run()`` returns can carry
a back-reference to the sandbox and is itself unsendable. Attaching it to
``Content.raw_representation`` lets it ride out of the worker thread and be garbage
collected on whichever thread the asyncio loop / agent state ends up on, which trips
the PyO3 ``Drop`` panic. Drop must happen on the worker thread that ran ``run()``.
"""
_OwnerThreadTrackedResult.drop_thread_violations.clear()
_FakeSandbox.instances.clear()
monkeypatch.setattr(execute_code_module, "_load_sandbox_class", lambda: _ResultDropTrackingFakeSandbox)
execute_code = HyperlightExecuteCodeTool()
def _drive() -> None:
# Run the whole invocation inside a helper frame so every local
# reference (contents, awaitable, asyncio frames) dies when the
# function returns. Anything still pinning the result is the bug.
contents = asyncio.run(execute_code.invoke(arguments={"code": "None"}))
assert contents and contents[0].type == "text"
_drive()
for _ in range(3):
gc.collect()
assert _OwnerThreadTrackedResult.drop_thread_violations == []
def test_sandbox_is_finalized_on_owner_thread_after_registry_close(monkeypatch: pytest.MonkeyPatch) -> None:
"""Regression: dropping the sandbox object itself must occur on its owner thread.
``_SandboxRegistry.close()`` previously held entries in a local list whose lifetime
extended onto the caller's thread. When that list went out of scope the unsendable
sandbox was finalized on the caller's thread, panicking PyO3 with
"WasmSandbox is unsendable, but is being dropped by another thread".
"""
drop_violations: list[str] = []
class _OwnerDropFakeSandbox(_FakeSandbox):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self._owner_thread = threading.get_ident()
# Do not pin ourselves on the class-level instances list; we want the
# registry/entry to hold the only strong reference so that dispose-time
# drop is what determines the finalizer thread.
_FakeSandbox.instances.remove(self)
def __del__(self) -> None:
ident = threading.get_ident()
if ident != self._owner_thread:
drop_violations.append(f"sandbox dropped on thread {ident}, owner was {self._owner_thread}")
monkeypatch.setattr(execute_code_module, "_load_sandbox_class", lambda: _OwnerDropFakeSandbox)
registry = execute_code_module._SandboxRegistry()
execute_code = HyperlightExecuteCodeTool(_registry=registry)
asyncio.run(execute_code.invoke(arguments={"code": "None"}))
registry.close()
# Release the registry/tool references and force a GC. With the fix in place the
# sandbox is already disposed on the worker thread inside close(); dropping these
# local references must not trigger a wrong-thread __del__ now.
del registry
del execute_code
for _ in range(3):
gc.collect()
assert drop_violations == [], f"sandbox was dropped off-thread despite registry close: {drop_violations}"
def test_worker_failure_does_not_leak_unsendable_via_exception_traceback(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Regression: an exception raised inside a worker closure must not leak unsendable refs.
Production failure mode: ``_build_sandbox`` (or ``sandbox.run``) raises on the
worker thread. ``concurrent.futures`` propagates the exception via
``Future.result()`` to the caller's thread. Python's exception object retains
``__traceback__`` whose frames reference local variables -- including the
partially-built PyO3 unsendable sandbox. When the caller's thread eventually
GCs the exception, those locals are dec_ref'd on the wrong thread and PyO3
panics with
``_native_wasm::WasmSandbox is unsendable, but is being dropped on another thread``.
The fix routes every worker closure through ``_run_on_worker``, which catches
the exception on the worker thread, drops its traceback there, and re-raises
a fresh exception on the caller side carrying only the message.
"""
drop_violations: list[str] = []
class _RaisingFakeSandbox(_FakeSandbox):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self._owner_thread = threading.get_ident()
_FakeSandbox.instances.remove(self)
# Simulate production bug: build raises while ``self`` is alive in
# the calling frame's locals -- the exception traceback will retain
# a reference to this object.
raise RuntimeError("simulated build failure with unsendable in frame locals")
def __del__(self) -> None:
ident = threading.get_ident()
if ident != self._owner_thread:
drop_violations.append(f"sandbox dropped on thread {ident}, owner was {self._owner_thread}")
monkeypatch.setattr(execute_code_module, "_load_sandbox_class", lambda: _RaisingFakeSandbox)
registry = execute_code_module._SandboxRegistry()
execute_code = HyperlightExecuteCodeTool(_registry=registry)
async def _drive(tool: HyperlightExecuteCodeTool) -> None:
for _ in range(4):
with contextlib.suppress(Exception):
await tool.invoke(arguments={"code": "None"})
asyncio.run(_drive(execute_code))
registry.close()
del registry
del execute_code
for _ in range(5):
gc.collect()
assert drop_violations == [], (
f"sandbox dropped off-thread despite worker raising on the owner thread: {drop_violations}"
)
def test_sandbox_entry_does_not_expose_unsendable_attributes() -> None:
"""Architectural regression: the entry must not hold sandbox/snapshot as attributes.
The unsendable PyO3 sandbox/snapshot must live ONLY inside the per-entry worker
thread, accessible only via worker-submitted closures. Any direct ``entry.sandbox``
or ``entry.snapshot`` attribute would let callers obtain a strong reference that
can be released on a non-owner thread, triggering PyO3's unsendable Drop panic
(the production bug we are fixing).
"""
fields = {f.name for f in dataclasses.fields(execute_code_module._SandboxEntry)}
assert "sandbox" not in fields, "_SandboxEntry must not expose `sandbox` directly"
assert "snapshot" not in fields, "_SandboxEntry must not expose `snapshot` directly"
# Whatever attributes remain must be sendable / safe to GC on any thread.
assert fields <= {"worker", "input_dir", "output_dir"}
def test_sandbox_survives_external_thread_holding_stale_reference(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Regression: stale refs held by external executors must not cause wrong-thread Drop.
Production traceback was ``concurrent.futures.thread._worker:95 del work_item`` on
``asyncio_0`` -- an external ``ThreadPoolExecutor`` whose ``_WorkItem`` transitively
held a strong reference to the sandbox via ``self._registry.execute``. When that
work_item was deleted on the external worker thread, the sandbox's refcount could
reach zero there, panicking PyO3.
With the actor-model refactor, ``HyperlightExecuteCodeTool._run_code`` runs the
sandbox call via ``asyncio.to_thread(self._registry.execute, ...)`` which creates
an external work_item containing ``self._registry.execute`` -- but that reference
transitively holds only the registry, not the sandbox. The sandbox lives entirely
inside the per-entry ``_SandboxWorker`` and never escapes; so when the external
work_item is deleted on a non-owner thread, the sandbox's refcount cannot reach
zero there.
"""
drop_violations: list[str] = []
class _OwnerDropFakeSandbox(_FakeSandbox):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self._owner_thread = threading.get_ident()
_FakeSandbox.instances.remove(self)
def __del__(self) -> None:
ident = threading.get_ident()
if ident != self._owner_thread:
drop_violations.append(f"sandbox dropped on thread {ident}, owner was {self._owner_thread}")
monkeypatch.setattr(execute_code_module, "_load_sandbox_class", lambda: _OwnerDropFakeSandbox)
registry = execute_code_module._SandboxRegistry()
execute_code = HyperlightExecuteCodeTool(_registry=registry)
async def _drive_many(tool: HyperlightExecuteCodeTool) -> None:
# Many concurrent invocations push work_items into asyncio's default executor;
# each work_item's args transitively reference the registry. If the registry
# were the sandbox holder, the work_items' deletion on asyncio_0/asyncio_1 etc.
# could trigger a wrong-thread Drop -- which is exactly the production bug.
await asyncio.gather(*[tool.invoke(arguments={"code": "None"}) for _ in range(8)])
asyncio.run(_drive_many(execute_code))
registry.close()
del registry
del execute_code
for _ in range(5):
gc.collect()
assert drop_violations == []
@@ -0,0 +1,31 @@
# Hyperlight CodeAct context provider
Demonstrates the provider-owned [Hyperlight](https://github.com/hyperlight-dev/hyperlight)
CodeAct flow. `HyperlightCodeActProvider` injects an `execute_code` tool into the
agent and keeps the registered sandbox tools (`compute`, `fetch_data`) hidden
from the model — the model must call them from inside the sandbox using
`call_tool(...)`.
## Installation
```bash
pip install agent-framework agent-framework-hyperlight --pre
```
> The Hyperlight Wasm backend is currently published only for `linux/x86_64` and
> `win32/AMD64` with Python `<3.14`. On other platforms `execute_code` will fail
> at runtime when it tries to create the sandbox.
## Prerequisites
- An Azure AI Foundry project endpoint (`FOUNDRY_PROJECT_ENDPOINT`)
- A deployed model (`FOUNDRY_MODEL`)
- Azure CLI authenticated (`az login`)
## Run
```bash
python code_act.py
```
See [`code_act.py`](code_act.py) for the full annotated example.
@@ -10,11 +10,10 @@ from typing import Annotated, Any, Literal
from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool
from agent_framework.foundry import FoundryChatClient from agent_framework.foundry import FoundryChatClient
from agent_framework.hyperlight import HyperlightCodeActProvider
from azure.identity import AzureCliCredential from azure.identity import AzureCliCredential
from dotenv import load_dotenv from dotenv import load_dotenv
from agent_framework_hyperlight import HyperlightCodeActProvider
"""This sample demonstrates the provider-owned Hyperlight CodeAct flow. """This sample demonstrates the provider-owned Hyperlight CodeAct flow.
The sample keeps `compute` and `fetch_data` off the direct agent tool surface and The sample keeps `compute` and `fetch_data` off the direct agent tool surface and
@@ -0,0 +1,37 @@
# Hyperlight local code interpreter
Demonstrates the standalone [Hyperlight](https://github.com/hyperlight-dev/hyperlight)
`HyperlightExecuteCodeTool` — a sandboxed local code interpreter that the agent
can invoke directly. Two patterns are shown:
| File | Pattern |
|------|---------|
| [`local_code_interpreter.py`](local_code_interpreter.py) | **Standalone tool**`HyperlightExecuteCodeTool` is added to the agent tool list and self-describes its sandbox tools, so no extra agent instructions are needed. Best for quick prototyping. |
| [`local_code_interpreter_manual_wiring.py`](local_code_interpreter_manual_wiring.py) | **Manual static wiring** — sandbox tools and CodeAct instructions are built once and passed to the `Agent` constructor alongside a direct-only tool (`send_email`). Best when the tool set is fixed for the agent's lifetime. |
For the recommended provider-driven pattern (with dynamic tool / capability
management), see
[`../../context_providers/code_act/`](../../context_providers/code_act/).
## Installation
```bash
pip install agent-framework agent-framework-hyperlight --pre
```
> The Hyperlight Wasm backend is currently published only for `linux/x86_64` and
> `win32/AMD64` with Python `<3.14`. On other platforms `execute_code` will fail
> at runtime when it tries to create the sandbox.
## Prerequisites
- An Azure AI Foundry project endpoint (`FOUNDRY_PROJECT_ENDPOINT`)
- A deployed model (`FOUNDRY_MODEL`)
- Azure CLI authenticated (`az login`)
## Run
```bash
python local_code_interpreter.py
python local_code_interpreter_manual_wiring.py
```
@@ -8,11 +8,10 @@ from typing import Annotated, Any, Literal
from agent_framework import Agent, tool from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient from agent_framework.foundry import FoundryChatClient
from agent_framework.hyperlight import HyperlightExecuteCodeTool
from azure.identity import AzureCliCredential from azure.identity import AzureCliCredential
from dotenv import load_dotenv from dotenv import load_dotenv
from agent_framework_hyperlight import HyperlightExecuteCodeTool
"""This sample demonstrates the standalone Hyperlight execute_code tool. """This sample demonstrates the standalone Hyperlight execute_code tool.
The sample adds `HyperlightExecuteCodeTool` directly to the agent. The tool's The sample adds `HyperlightExecuteCodeTool` directly to the agent. The tool's
@@ -8,11 +8,10 @@ from typing import Annotated, Any, Literal
from agent_framework import Agent, tool from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient from agent_framework.foundry import FoundryChatClient
from agent_framework.hyperlight import HyperlightExecuteCodeTool
from azure.identity import AzureCliCredential from azure.identity import AzureCliCredential
from dotenv import load_dotenv from dotenv import load_dotenv
from agent_framework_hyperlight import HyperlightExecuteCodeTool
"""This sample demonstrates manual static wiring of CodeAct without a provider. """This sample demonstrates manual static wiring of CodeAct without a provider.
Instead of using `HyperlightCodeActProvider` with `context_providers=`, this Instead of using `HyperlightCodeActProvider` with `context_providers=`, this
@@ -0,0 +1,6 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
@@ -0,0 +1,2 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
@@ -0,0 +1,36 @@
# Build this image with the repository's `python/` directory as the build context so
# the in-tree agent-framework packages can be installed from source. From the repo root:
#
# docker build \
# -f python/samples/04-hosting/foundry-hosted-agents/responses/08_hyperlight_codeact/Dockerfile \
# -t <acr>.azurecr.io/<image>:<tag> \
# python/
FROM python:3.12-slim
WORKDIR /app
# Copy the in-tree agent-framework packages we need. Order matters for editable
# installs because of inter-package dependencies; we install in dependency order
# below. Hyperlight backends are platform gated, so we install them via pip
# resolution rather than copying the wheels.
COPY packages/core /opt/af/core
COPY packages/openai /opt/af/openai
COPY packages/foundry /opt/af/foundry
COPY packages/foundry_hosting /opt/af/foundry_hosting
COPY packages/hyperlight /opt/af/hyperlight
# Copy just the sample we care about into the user agent location.
COPY samples/04-hosting/foundry-hosted-agents/responses/08_hyperlight_codeact/ /app/user_agent/
WORKDIR /app/user_agent
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir /opt/af/core \
&& pip install --no-cache-dir /opt/af/openai \
&& pip install --no-cache-dir /opt/af/foundry \
&& pip install --no-cache-dir /opt/af/foundry_hosting \
&& pip install --no-cache-dir /opt/af/hyperlight \
&& if grep -Eq '^[[:space:]]*[^#[:space:]]' requirements.txt; then pip install --no-cache-dir -r requirements.txt; fi
EXPOSE 8088
CMD ["python", "main.py"]
@@ -0,0 +1,85 @@
# What this sample demonstrates
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that
runs Python in a [Hyperlight](https://github.com/hyperlight-dev/hyperlight)
WebAssembly sandbox via the **CodeAct** pattern, hosted using the **Responses
protocol**. The model is only given a single `execute_code` tool. Local Python
tools (`compute`, `fetch_data`) are registered on `HyperlightCodeActProvider`
and are reachable from inside the sandbox via `call_tool(...)`, never as
direct LLM tools. All of this can be run as a container, however not under all circumstances.
> **⚠️ Foundry hosted-agent runtime support is in progress.**
> Hyperlight requires a hypervisor (`/dev/kvm` on Linux, MSHV on Windows). The
> default Foundry hosted-agent runtime does not currently expose a hypervisor
> to the workload container, so deploying this sample as a Foundry hosted
> agent will fail at runtime with
> `Failed to create sandbox: ... No Hypervisor was found for Sandbox`.
> The sample container itself works end-to-end when run locally with
> `docker run --device=/dev/kvm ...` (see [Hypervisor requirement](#hypervisor-requirement)
> below). We are working with the platform team to enable a hypervisor-capable
> hosting target.
## How It Works
### Model integration
The agent uses `FoundryChatClient` to talk to a Foundry-hosted model deployment.
A `HyperlightCodeActProvider` is attached as a context provider, which on every
run injects the `execute_code` tool plus the CodeAct instructions that teach the
model how to author Python that calls `call_tool(...)` for sandbox-only tools.
See [`main.py`](main.py) for the full implementation.
### Agent hosting
The agent is hosted with `ResponsesHostServer` from
`agent-framework-foundry-hosting`, which exposes a REST endpoint compatible with
the OpenAI Responses protocol.
> The Hyperlight Wasm backend is currently published only for `linux/x86_64` and
> `win32/AMD64` with Python `<3.14`. The hosted container runs `python:3.12-slim`
> on linux/x86_64, which is supported.
### Hypervisor requirement
Hyperlight executes guest WebAssembly inside a micro-VM and **requires a
hypervisor on the host**:
- **Linux:** `/dev/kvm` must be present *and* the container must have access to
it (`docker run --device=/dev/kvm ...`).
- **Windows:** the Microsoft Hypervisor Platform (MSHV) must be enabled.
Without a hypervisor, sandbox creation fails with:
```
Failed to create sandbox: failed to build ProtoWasmSandbox: No Hypervisor was found for Sandbox
```
This affects hosted environments that don't expose `/dev/kvm` to the workload
container (most managed PaaS, including the default Foundry hosted-agent
runtime). To run this sample as a hosted agent you need a hosting target with
nested virtualization and `/dev/kvm` device passthrough — for example an Azure
VM, AKS nodes with KVM enabled, or Azure Container Instances configured for
nested virt.
## Running the Agent Host
Follow the instructions in the
[Running the Agent Host Locally](../../foundry-hosted-agents//README.md#running-the-agent-host-locally)
section of the README in the Foundry Hosted Agent directory.
## Interacting with the agent
Send a POST request to the server with a JSON body containing an `"input"`
field. The model should respond by calling `execute_code` with Python that uses
`call_tool(...)` to reach the sandbox-only tools:
```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 multiplication result. Use execute_code with call_tool(...)."}'
```
## Deploying the Agent to Foundry
Deploying this container to Foundry will not work yet, as soon as it does, we will update this sample.
@@ -0,0 +1,24 @@
name: agent-framework-agent-with-hyperlight-codeact-responses
description: >
An Agent Framework agent with a Hyperlight CodeAct sandbox hosted by Foundry.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
- Hyperlight CodeAct
template:
name: agent-framework-agent-with-hyperlight-codeact-responses
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
resources:
- kind: model
id: gpt-4.1-mini
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -0,0 +1,23 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: agent-framework-agent-with-hyperlight-codeact-responses
description: |
An Agent Framework agent with a Hyperlight CodeAct sandbox hosted by Foundry.
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
- Hyperlight CodeAct
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "1"
memory: 2Gi
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: gpt-4.1-mini
@@ -0,0 +1,41 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "openai>=1.50,<3",
# "azure-identity>=1.19,<2",
# ]
# ///
# Run with: uv run call_server.py
# Copyright (c) Microsoft. All rights reserved.
"""Call the deployed Hyperlight CodeAct Foundry hosted agent via the OpenAI client."""
import os
from azure.identity import AzureCliCredential
from openai import OpenAI
# Set FOUNDRY_AGENT_ENDPOINT to your deployed agent endpoint, e.g.
# https://<your-foundry-resource>.services.ai.azure.com/api/projects/<project>/agents/<agent-name>
ENDPOINT = os.environ.get(
"FOUNDRY_AGENT_ENDPOINT",
"https://<your-foundry-resource>.services.ai.azure.com"
"/api/projects/<project>/agents/<agent-name>",
)
SCOPE = "https://ai.azure.com/.default"
PROMPT = (
"Fetch all users, find the admins, multiply 7 by 6, and print the users, "
"admins and multiplication result. Use execute_code with call_tool(...)."
)
def main() -> None:
token = AzureCliCredential().get_token(SCOPE).token
client = OpenAI(base_url=ENDPOINT, api_key=token, default_query={"api-version": "v1"})
response = client.responses.create(model="hosted-agent", input=PROMPT)
print(response.output_text)
if __name__ == "__main__":
main()
@@ -0,0 +1,89 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Annotated, Any, Literal
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.hyperlight import HyperlightCodeActProvider
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
@tool(approval_mode="never_require")
def compute(
operation: Annotated[
Literal["add", "subtract", "multiply", "divide"],
"Math operation: add, subtract, multiply, or divide.",
],
a: Annotated[float, "First numeric operand."],
b: Annotated[float, "Second numeric operand."],
) -> float:
"""Perform a math operation for sandboxed code."""
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b else float("inf"),
}
return operations[operation]
@tool(approval_mode="never_require")
async def fetch_data(
table: Annotated[str, "Name of the simulated table to query."],
) -> list[dict[str, Any]]:
"""Fetch records from a named table."""
await asyncio.sleep(0.5)
data: dict[str, list[dict[str, Any]]] = {
"users": [
{"id": 1, "name": "Alice", "role": "admin"},
{"id": 2, "name": "Bob", "role": "user"},
{"id": 3, "name": "Charlie", "role": "admin"},
],
"products": [
{"id": 101, "name": "Widget", "price": 9.99},
{"id": 102, "name": "Gadget", "price": 19.99},
],
}
return data.get(table, [])
def main():
# 1. Create the Foundry chat client.
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=DefaultAzureCredential(),
function_invocation_configuration={"include_detailed_errors": True},
)
# 2. Register sandbox tools on a Hyperlight CodeAct provider. The model only
# sees `execute_code`; `compute` and `fetch_data` are reachable from
# inside the sandbox via `call_tool(...)`.
codeact = HyperlightCodeActProvider(
tools=[compute, fetch_data],
approval_mode="never_require",
)
# 3. Build the agent. History is managed by the hosting infrastructure, so
# request the model not to persist server-side conversation state.
agent = Agent(
client=client,
instructions="You are a helpful assistant. Keep your answers brief.",
context_providers=[codeact],
default_options={"store": False},
)
# 4. Serve the agent over the Foundry Responses protocol.
server = ResponsesHostServer(agent)
server.run()
if __name__ == "__main__":
main()
@@ -0,0 +1,3 @@
# agent-framework, agent-framework-foundry-hosting, and agent-framework-hyperlight
# are installed from local source by the Dockerfile (build context = python/).
# Add any sample-only third-party deps here.
@@ -222,4 +222,4 @@ This will package your agent and deploy it to the Foundry environment, making it
For the full deployment guide, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). For the full deployment guide, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
Once deployed, learn more about how to manage deployed agents in the [official management guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/manage-hosted-agent). Once deployed, learn more about how to manage deployed agents in the [official management guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/manage-hosted-agent).
+25 -23
View File
@@ -379,6 +379,7 @@ all = [
{ name = "agent-framework-foundry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-foundry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-foundry-local", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-foundry-local", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-github-copilot", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "agent-framework-github-copilot", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" },
{ name = "agent-framework-hyperlight", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32')" },
{ name = "agent-framework-lab", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-lab", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-mem0", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-mem0", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -407,6 +408,7 @@ requires-dist = [
{ name = "agent-framework-foundry", marker = "extra == 'all'", editable = "packages/foundry" }, { name = "agent-framework-foundry", marker = "extra == 'all'", editable = "packages/foundry" },
{ name = "agent-framework-foundry-local", marker = "extra == 'all'", editable = "packages/foundry_local" }, { name = "agent-framework-foundry-local", marker = "extra == 'all'", editable = "packages/foundry_local" },
{ name = "agent-framework-github-copilot", marker = "python_full_version >= '3.11' and extra == 'all'", editable = "packages/github_copilot" }, { name = "agent-framework-github-copilot", marker = "python_full_version >= '3.11' and extra == 'all'", editable = "packages/github_copilot" },
{ name = "agent-framework-hyperlight", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'all') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'all')", editable = "packages/hyperlight" },
{ name = "agent-framework-lab", marker = "extra == 'all'", editable = "packages/lab" }, { name = "agent-framework-lab", marker = "extra == 'all'", editable = "packages/lab" },
{ name = "agent-framework-mem0", marker = "extra == 'all'", editable = "packages/mem0" }, { name = "agent-framework-mem0", marker = "extra == 'all'", editable = "packages/mem0" },
{ name = "agent-framework-ollama", marker = "extra == 'all'", editable = "packages/ollama" }, { name = "agent-framework-ollama", marker = "extra == 'all'", editable = "packages/ollama" },
@@ -601,7 +603,7 @@ requires-dist = [
[[package]] [[package]]
name = "agent-framework-hyperlight" name = "agent-framework-hyperlight"
version = "1.0.0a260429" version = "1.0.0b260501"
source = { editable = "packages/hyperlight" } source = { editable = "packages/hyperlight" }
dependencies = [ dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -613,9 +615,9 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "agent-framework-core", editable = "packages/core" }, { name = "agent-framework-core", editable = "packages/core" },
{ name = "hyperlight-sandbox", specifier = ">=0.3.0,<0.4" }, { name = "hyperlight-sandbox", specifier = ">=0.4.0,<0.5" },
{ name = "hyperlight-sandbox-backend-wasm", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32')", specifier = ">=0.3.0,<0.4" }, { name = "hyperlight-sandbox-backend-wasm", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32')", specifier = ">=0.4.0,<0.5" },
{ name = "hyperlight-sandbox-python-guest", specifier = ">=0.3.0,<0.4" }, { name = "hyperlight-sandbox-python-guest", specifier = ">=0.4.0,<0.5" },
] ]
[[package]] [[package]]
@@ -1636,7 +1638,7 @@ name = "clr-loader"
version = "0.2.10" version = "0.2.10"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" }
wheels = [ wheels = [
@@ -2949,35 +2951,35 @@ wheels = [
[[package]] [[package]]
name = "hyperlight-sandbox" name = "hyperlight-sandbox"
version = "0.3.0" version = "0.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/fe/ce88996ea3e3e05130d6f0e8cd2ffbe9ab9bf3d9448b7050d4b8d0802b0a/hyperlight_sandbox-0.3.0.tar.gz", hash = "sha256:00491ce267ffbdb206377c79b4afd86510177ad73f4daf2ef7fce02b54eaf801", size = 9251, upload-time = "2026-04-07T03:49:52.542Z" } sdist = { url = "https://files.pythonhosted.org/packages/de/5e/14c69eac7e1c74fbd556c6f890729a3d232d32d65cd9f8cfde72c0534e61/hyperlight_sandbox-0.4.0.tar.gz", hash = "sha256:90d7b91d4d8e17054e282b0daed55c261392a748dafc57e6416d3184cdac910b", size = 9262, upload-time = "2026-05-02T00:00:02.866Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/33/e6dcd6729308d13570ae2d3be0e476019a6f3fea387d7549bb1f77ce0408/hyperlight_sandbox-0.3.0-py3-none-any.whl", hash = "sha256:ba8e6779d64e9c187acd93456851ebafaed2f49380e5d132bc0906a4080d2217", size = 5723, upload-time = "2026-04-07T03:49:53.276Z" }, { url = "https://files.pythonhosted.org/packages/fa/e3/b8c106a274c08a30261105afa5511e0ec55960e86b2f6c51e3095e96647c/hyperlight_sandbox-0.4.0-py3-none-any.whl", hash = "sha256:7ae44d2448ed6ecadb368373c7e45eb395521e7774c86a1cbc1ef9cdfc25cd2a", size = 5723, upload-time = "2026-05-02T00:00:03.811Z" },
] ]
[[package]] [[package]]
name = "hyperlight-sandbox-backend-wasm" name = "hyperlight-sandbox-backend-wasm"
version = "0.3.0" version = "0.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/58/91/c9d68cad7996fdd2f1facef1453156bdd8d52eefa976cc8c827c13029497/hyperlight_sandbox_backend_wasm-0.3.0-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:eda362f5f737b0823326290d7627c76ce0547a78e70f07f8c9d177e34622fc02", size = 3806454, upload-time = "2026-04-07T03:49:24.238Z" }, { url = "https://files.pythonhosted.org/packages/b6/7c/355864a5bc814eeb8788ea35eb2cdf6c60f34afd7a7cd4433a368f26f60e/hyperlight_sandbox_backend_wasm-0.4.0-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:5b5da47c21aeebd2a7bb9394bbd481fe285fafc5b48d1f3685034e203219bcb5", size = 3921265, upload-time = "2026-05-01T23:59:20.42Z" },
{ url = "https://files.pythonhosted.org/packages/9a/6f/6b2399a1caf59dd19b635d99ee1add0c975af7bc3317f5d0f1f9c3f90aa0/hyperlight_sandbox_backend_wasm-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:79347b7ae94f2786691b04cb52130dabc5991e0c03b42a24bad8adc766832655", size = 3283951, upload-time = "2026-04-07T03:49:17.137Z" }, { url = "https://files.pythonhosted.org/packages/46/ec/628fafcbf1483f86df6bf9412b23da68b5cd1156d91b0998ed5c62d0b9c4/hyperlight_sandbox_backend_wasm-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:c514e662fed41ee8222b09da5f1130ac856c91cc4523b354bb8e7b96da352611", size = 3387473, upload-time = "2026-05-01T23:59:25.548Z" },
{ url = "https://files.pythonhosted.org/packages/23/f2/b380c34a0ce8d486a05adb66757f98cca029e1fb1c96b1c29be0d25d3882/hyperlight_sandbox_backend_wasm-0.3.0-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:aff9eec4803fb535a140298e2632529f4150fcf3c6ea3ff2ae4571572a836116", size = 3806601, upload-time = "2026-04-07T03:49:22.853Z" }, { url = "https://files.pythonhosted.org/packages/23/d2/09812f51a02e39236bfb5bfc40b4021e98f07e2f32f8d6a72745884d49f8/hyperlight_sandbox_backend_wasm-0.4.0-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c0935698f58a144150000e978e061fa78daa6f2ac1861980b32571f7c27a53fd", size = 3921230, upload-time = "2026-05-01T23:59:18.635Z" },
{ url = "https://files.pythonhosted.org/packages/b4/5a/fb78cfd934e0523887b8d5b073b7b2aed3b545add21cda3aa95929ac1659/hyperlight_sandbox_backend_wasm-0.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:b6151704dd19862c9869b115752b4504b45d0b2eeb46aa9385a1a3b8be11cfa8", size = 3284164, upload-time = "2026-04-07T03:49:18.556Z" }, { url = "https://files.pythonhosted.org/packages/34/b2/56181b5a21c17ad4636686de3463706a85883aa70dd3b1b160dd7e95627b/hyperlight_sandbox_backend_wasm-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b9486b7617615334d4b06d9462ad57edfe3f161d40d880b866db7d240b4d04e", size = 3388119, upload-time = "2026-05-01T23:59:21.966Z" },
{ url = "https://files.pythonhosted.org/packages/21/bc/4e21f5c7ccd9307ac63a61c71b62a57ee4a9e6eec77fc72ff072907a21f5/hyperlight_sandbox_backend_wasm-0.3.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:cfd1d22ce221774d82a5174d268d56ff70fc1a23fb993a6491358b5d0ed169bf", size = 3802901, upload-time = "2026-04-07T03:49:19.845Z" }, { url = "https://files.pythonhosted.org/packages/79/e5/3cdf21594eb28de7ca1a5a1ade27e137c8f3d7ab48d65fed87a3b74c4039/hyperlight_sandbox_backend_wasm-0.4.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ff4627950708909202ee24c6175dc41e9c05479f89393575e3de0f14e6f5a193", size = 3918189, upload-time = "2026-05-01T23:59:16.666Z" },
{ url = "https://files.pythonhosted.org/packages/9a/41/646be9b0c7bb0f9192e45a77414673aa414eb316c92b5312efe6fb4ce802/hyperlight_sandbox_backend_wasm-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:229ab494a422f2de895a2a27ad6a6a2daed710ea062d7c213878bbe5f5b32fa7", size = 3281220, upload-time = "2026-04-07T03:49:21.368Z" }, { url = "https://files.pythonhosted.org/packages/5b/97/b1bb9893bbeb979d133dc542520125dcbf8394d1a2537e753118b37c7cab/hyperlight_sandbox_backend_wasm-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cce7dc28b9ded034a11a9a8cf7b9ffb838e29006be8d2e01646dd131ba501b73", size = 3383520, upload-time = "2026-05-01T23:59:27.261Z" },
{ url = "https://files.pythonhosted.org/packages/74/3a/f8ec4a41fffba4036dfc3cbddc3dfb6e87466b01afe1cb0a50cc6a0f0eed/hyperlight_sandbox_backend_wasm-0.3.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:b91905ee2ddd36a78b0dd13b1a62be99a995a45121587c111692591e40b36912", size = 3802789, upload-time = "2026-04-07T03:49:15.614Z" }, { url = "https://files.pythonhosted.org/packages/8c/29/deee4e31086628750f0ce1f67da1e28c613fd2df68465de130cbfe51e72d/hyperlight_sandbox_backend_wasm-0.4.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:88e194515e4784f68676b6906c98a4000f913c93172cf07981d8a977e756bbd6", size = 3917939, upload-time = "2026-05-01T23:59:14.805Z" },
{ url = "https://files.pythonhosted.org/packages/3c/62/dfa8c15102f9b8ec5c3b5ffb54b99d60c75e7a6e4d00540757656bc5a5d8/hyperlight_sandbox_backend_wasm-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:eff682761c3b86abfe7e0d523ea0e6d5c7e8299302917c53918743b82c9d1ea2", size = 3280501, upload-time = "2026-04-07T03:49:13.939Z" }, { url = "https://files.pythonhosted.org/packages/15/2a/6822aec3c04c46893406d0d6ed576dbdb4b5c1d76a0124dc220bb45b0d34/hyperlight_sandbox_backend_wasm-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d1cd2269a5651ea9be1f94a3e3388f6af69e41dbc2b808c3b806481fe17ce163", size = 3383110, upload-time = "2026-05-01T23:59:23.736Z" },
] ]
[[package]] [[package]]
name = "hyperlight-sandbox-python-guest" name = "hyperlight-sandbox-python-guest"
version = "0.3.0" version = "0.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/23/6a/f182c4315d31a98dd3b82f9274638e3adb399779584af93c5087bb2f814f/hyperlight_sandbox_python_guest-0.3.0.tar.gz", hash = "sha256:b1de5d8e87375dc6bef744ecd7ae2a7f43d5f6b913b4e990e9872bd439c0b19e", size = 21554625, upload-time = "2026-04-07T03:49:42.672Z" } sdist = { url = "https://files.pythonhosted.org/packages/7d/fd/816d1f3f277ff149a45da5381967aa04c22bc7702b5c14f0acfd9db2cee7/hyperlight_sandbox_python_guest-0.4.0.tar.gz", hash = "sha256:64c3c6c13fe550bf5b680fa0b965cf62bc4668084cc275c3467e3c015e6ead36", size = 21657381, upload-time = "2026-05-01T23:59:46.589Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/8e/4cd754928464f56528645c7421ccbb3fcbe45ad2542f899712b0f2f2c0e1/hyperlight_sandbox_python_guest-0.3.0-py3-none-any.whl", hash = "sha256:3c55a7420666ad9a208893dbdf7ad1b5c8ad4f3a94b1a56e64979719c7ce95c1", size = 21716481, upload-time = "2026-04-07T03:49:39.885Z" }, { url = "https://files.pythonhosted.org/packages/98/ba/efb9aacf993f0ac142da5beb9177b221e49dc860c6ea398de236015a52a0/hyperlight_sandbox_python_guest-0.4.0-py3-none-any.whl", hash = "sha256:0789eb794b99606288402ed3921b5e2630800a69d24117ecd9b82e816568202d", size = 21822062, upload-time = "2026-05-01T23:59:50.99Z" },
] ]
[[package]] [[package]]
@@ -5132,8 +5134,8 @@ name = "powerfx"
version = "0.0.34" version = "0.0.34"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
{ name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" } sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" }
wheels = [ wheels = [
@@ -5806,7 +5808,7 @@ name = "pythonnet"
version = "3.0.5" version = "3.0.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" }
wheels = [ wheels = [