chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import importlib.metadata
from ._execute_code_tool import MontyExecuteCodeTool
from ._provider import MontyCodeActProvider
from ._types import FileMount, FileMountInput, MountMode
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0"
__all__ = [
"FileMount",
"FileMountInput",
"MontyCodeActProvider",
"MontyExecuteCodeTool",
"MountMode",
"__version__",
]
@@ -0,0 +1,558 @@
# Copyright (c) Microsoft. All rights reserved.
"""``MontyExecuteCodeTool`` - a ``FunctionTool`` that runs Python in Monty.
Mirrors the public API of ``HyperlightExecuteCodeTool`` for the subset that
applies to a pure-Python interpreter (no backends to choose from). By default
the Monty sandbox rejects OS / filesystem / network calls with
``PermissionError``; pass ``workspace_root`` or ``file_mounts`` to expose
scoped host directories, and the tool will capture any files written under
``read-write`` mounts as ``Content`` items in the response.
"""
from __future__ import annotations
import json
import mimetypes
from collections.abc import Callable, Iterator, Sequence
from copy import copy
from functools import partial
from pathlib import Path, PurePosixPath
from typing import Any, cast
from agent_framework import Content, FunctionTool
from agent_framework._tools import ApprovalMode, normalize_tools
from ._instructions import build_codeact_instructions, build_execute_code_description
from ._monty_bridge import InlineCodeBridge, generate_type_stubs
from ._types import FileMount, FileMountInput
EXECUTE_CODE_TOOL_NAME = "execute_code"
EXECUTE_CODE_TOOL_DESCRIPTION = "Execute Python in a Monty interpreter."
#: Virtual path that the optional ``workspace_root`` directory is mounted at,
#: matching the Hyperlight default. Use ``file_mounts`` for any other path.
WORKSPACE_MOUNT_PATH = "/input"
#: Maximum bytes per captured output file. Files larger than this are skipped
#: and a ``Content.from_text`` warning is appended in their place.
MAX_CAPTURED_FILE_BYTES = 5 * 1024 * 1024 # 5 MiB
EXECUTE_CODE_INPUT_SCHEMA: dict[str, Any] = {
"type": "object",
"title": "_ExecuteCodeInput",
"properties": {
"code": {
"type": "string",
"title": "Code",
"description": "Python code to execute in a Monty interpreter.",
},
},
"required": ["code"],
}
def _collect_tools(*tool_groups: Any) -> list[FunctionTool]:
"""Merge tool groups, dropping any ``execute_code`` entries and deduping by name."""
tools_by_name: dict[str, FunctionTool] = {}
for tool_group in tool_groups:
normalized_group = normalize_tools(tool_group)
for tool_obj in normalized_group:
if not isinstance(tool_obj, FunctionTool):
continue
if tool_obj.name == EXECUTE_CODE_TOOL_NAME:
continue
tools_by_name.pop(tool_obj.name, None)
tools_by_name[tool_obj.name] = tool_obj
return list(tools_by_name.values())
def _resolve_execute_code_approval_mode(
*,
base_approval_mode: ApprovalMode,
tools: Sequence[FunctionTool],
) -> ApprovalMode:
if base_approval_mode == "always_require":
return "always_require"
if any(tool_obj.approval_mode == "always_require" for tool_obj in tools):
return "always_require"
return "never_require"
def _normalize_mount_path(mount_path: str) -> str:
"""Normalize a virtual mount path to a clean POSIX absolute path."""
raw = mount_path.strip().replace("\\", "/")
if not raw:
raise ValueError("mount_path must not be empty.")
pure = PurePosixPath(raw)
parts = [part for part in pure.parts if part not in {"", "/", "."}]
if any(part == ".." for part in parts):
raise ValueError("mount_path must not contain '..' segments.")
if not parts:
raise ValueError("mount_path must point to a concrete absolute path.")
return "/" + "/".join(parts)
def _resolve_existing_directory(value: str | Path) -> Path:
resolved = Path(value).expanduser().resolve(strict=True)
if not resolved.is_dir():
raise ValueError(f"Path {value!r} must point to an existing directory.")
return resolved
def _is_file_mount_pair(value: Any) -> bool:
if not isinstance(value, tuple) or isinstance(value, FileMount):
return False
items = cast("tuple[object, ...]", value)
if len(items) != 2:
return False
host_path, mount_path = items
return isinstance(host_path, (str, Path)) and isinstance(mount_path, str)
def _normalize_file_mount(file_mount: FileMountInput) -> FileMount:
if isinstance(file_mount, FileMount):
host_path = file_mount.host_path
mount_path = file_mount.mount_path
mode = file_mount.mode
write_limit = file_mount.write_bytes_limit
elif isinstance(file_mount, str):
host_path = file_mount
mount_path = file_mount
mode = "overlay"
write_limit = None
else:
host_path, mount_path = file_mount
mode = "overlay"
write_limit = None
return FileMount(
host_path=_resolve_existing_directory(host_path),
mount_path=_normalize_mount_path(mount_path),
mode=mode,
write_bytes_limit=write_limit,
)
def _to_monty_mount(file_mount: FileMount) -> Any:
"""Convert a public :class:`FileMount` to Monty's ``MountDir``.
Imports lazily through the bridge's loader so missing-dependency errors
surface as the same actionable ``RuntimeError`` the rest of the package
raises, rather than a bare ``ImportError`` from a top-level import.
"""
from ._monty_bridge import load_monty # avoid top-level pydantic_monty import
monty_module = load_monty()
return monty_module.MountDir(
virtual_path=file_mount.mount_path,
host_path=str(file_mount.host_path),
mode=file_mount.mode,
write_bytes_limit=file_mount.write_bytes_limit,
)
def _make_tool_callback(tool_obj: FunctionTool) -> Callable[..., Any]:
"""Return an async callable that invokes ``tool_obj`` with the bridge's kwargs.
Returns the raw native value (no ``Content`` wrapping) so the Monty interpreter
receives real Python objects. ``FunctionTool.invoke`` accepts direct keyword
arguments and handles both sync and async underlying functions internally.
"""
return partial(copy(tool_obj).invoke, skip_parsing=True)
class MontyExecuteCodeTool(FunctionTool):
"""Execute Python code inside a Monty interpreter.
Tools registered on this object are available inside the interpreter as
typed async functions (e.g. ``await tool_name(...)``). Argument types are
validated by the [ty](https://docs.astral.sh/ty/) type checker before any
host tool runs.
Optional filesystem access is exposed via:
- ``workspace_root`` — auto-mounts a host directory at ``/input`` (matching
Hyperlight's default).
- ``file_mounts`` — extra :class:`FileMount` entries for fine-grained
control (mount path, read-only / read-write / overlay mode, write
byte caps).
Files written by sandboxed code to any **read-write** mount are scanned
after execution and returned as ``Content.from_data`` items, mirroring
Hyperlight's ``/output`` flow.
``resource_limits`` is forwarded to Monty's ``ResourceLimits`` to cap CPU
time, memory, output size, recursion depth, and GC frequency.
All mutators (``add_tools``, ``add_file_mounts`` etc.) must be called from
the same task/thread that owns the tool. Monty itself runs on the event
loop, so no internal locking is needed.
"""
def __init__(
self,
*,
tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None = None,
approval_mode: ApprovalMode | None = None,
workspace_root: str | Path | None = None,
file_mounts: FileMountInput | Sequence[FileMountInput] | None = None,
resource_limits: dict[str, Any] | None = None,
) -> None:
super().__init__(
name=EXECUTE_CODE_TOOL_NAME,
description=EXECUTE_CODE_TOOL_DESCRIPTION,
approval_mode="never_require",
func=self._run_code,
input_model=EXECUTE_CODE_INPUT_SCHEMA,
)
self._default_approval_mode: ApprovalMode = approval_mode or "never_require"
self._managed_tools: list[FunctionTool] = []
self._workspace_root: Path | None = (
_resolve_existing_directory(workspace_root) if workspace_root is not None else None
)
self._file_mounts: dict[str, FileMount] = {}
self._resource_limits: dict[str, Any] | None = dict(resource_limits) if resource_limits else None
if tools is not None:
self.add_tools(tools)
if file_mounts is not None:
self.add_file_mounts(file_mounts)
self._refresh_approval_mode()
@property
def description(self) -> str:
# During FunctionTool.__init__, ``_managed_tools`` is not yet set.
if not hasattr(self, "_managed_tools"):
return str(self.__dict__.get("description", EXECUTE_CODE_TOOL_DESCRIPTION))
return build_execute_code_description(
tools=self._managed_tools,
mounts=self._effective_mounts(),
)
@description.setter
def description(self, value: str) -> None:
self.__dict__["description"] = value
def add_tools(
self,
tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]],
) -> None:
"""Add Monty-side tools to this execute_code surface."""
self._managed_tools = _collect_tools(self._managed_tools, tools)
self._refresh_approval_mode()
def get_tools(self) -> list[FunctionTool]:
"""Return the currently managed Monty tools."""
return list(self._managed_tools)
def remove_tool(self, name: str) -> None:
"""Remove one managed Monty tool by name."""
remaining_tools = [tool_obj for tool_obj in self._managed_tools if tool_obj.name != name]
if len(remaining_tools) == len(self._managed_tools):
raise KeyError(f"No managed tool named {name!r} is registered.")
self._managed_tools = remaining_tools
self._refresh_approval_mode()
def clear_tools(self) -> None:
"""Remove all managed Monty tools."""
self._managed_tools = []
self._refresh_approval_mode()
def add_file_mounts(self, file_mounts: FileMountInput | Sequence[FileMountInput]) -> None:
"""Add one or more file mounts.
A single string mounts the same path on both sides. Use a
``(host_path, mount_path)`` tuple or :class:`FileMount` when the paths
differ or when you need to set the mount mode / write limit.
"""
if isinstance(file_mounts, (str, FileMount)) or _is_file_mount_pair(file_mounts):
normalized = [_normalize_file_mount(cast("FileMountInput", file_mounts))]
else:
normalized = [_normalize_file_mount(item) for item in cast("Sequence[FileMountInput]", file_mounts)]
for mount in normalized:
self._file_mounts[mount.mount_path] = mount
def get_file_mounts(self) -> list[FileMount]:
"""Return the configured file mounts (excluding ``workspace_root``)."""
return list(self._file_mounts.values())
def remove_file_mount(self, mount_path: str) -> None:
"""Remove one file mount by its sandbox path."""
normalized = _normalize_mount_path(mount_path)
if normalized not in self._file_mounts:
raise KeyError(f"No file mount exists for {mount_path!r}.")
del self._file_mounts[normalized]
def clear_file_mounts(self) -> None:
"""Remove all configured file mounts."""
self._file_mounts.clear()
@property
def workspace_root(self) -> Path | None:
"""Return the configured workspace root, if any."""
return self._workspace_root
@property
def resource_limits(self) -> dict[str, Any] | None:
"""Return the configured Monty :class:`pydantic_monty.ResourceLimits`, if any."""
return dict(self._resource_limits) if self._resource_limits else None
def build_instructions(self, *, tools_visible_to_model: bool) -> str:
"""Build the current CodeAct instructions for this execute_code surface."""
return build_codeact_instructions(
tools=list(self._managed_tools),
tools_visible_to_model=tools_visible_to_model,
mounts=self._effective_mounts(),
)
def create_run_tool(self) -> MontyExecuteCodeTool:
"""Create a run-scoped snapshot of this execute_code surface."""
return MontyExecuteCodeTool(
tools=self.get_tools(),
approval_mode=self._default_approval_mode,
workspace_root=self._workspace_root,
file_mounts=list(self._file_mounts.values()) or None,
resource_limits=self._resource_limits,
)
def build_serializable_state(self) -> dict[str, Any]:
"""Return a JSON-serializable snapshot of the effective run state."""
approval_mode = _resolve_execute_code_approval_mode(
base_approval_mode=self._default_approval_mode,
tools=self._managed_tools,
)
mounts = self._effective_mounts()
return {
"runtime": "monty",
"approval_mode": approval_mode,
"tool_names": [tool_obj.name for tool_obj in self._managed_tools],
"workspace_root": str(self._workspace_root) if self._workspace_root is not None else None,
"file_mounts": [
{
"host_path": str(mount.host_path),
"mount_path": mount.mount_path,
"mode": mount.mode,
"write_bytes_limit": mount.write_bytes_limit,
}
for mount in mounts
],
"resource_limits": dict(self._resource_limits) if self._resource_limits else None,
}
def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]:
# Materialize the dynamic description so the dump captures the current tool list.
self.__dict__["description"] = self.description
return super().to_dict(exclude=exclude, exclude_none=exclude_none)
def _refresh_approval_mode(self) -> None:
self.approval_mode = _resolve_execute_code_approval_mode(
base_approval_mode=self._default_approval_mode,
tools=self._managed_tools,
)
def _build_tool_map(self, tools: Sequence[FunctionTool]) -> dict[str, Callable[..., Any]]:
return {tool_obj.name: _make_tool_callback(tool_obj) for tool_obj in tools}
def _build_type_stub_map(self, tools: Sequence[FunctionTool]) -> dict[str, Callable[..., Any]]:
"""Return a name -> underlying-Python-callable map for type stub generation.
The raw Python function attached to the ``FunctionTool`` carries the
author's actual ``Annotated`` parameter types, which are what we want
``ty`` to validate against. Tools without an attached function (e.g.
``declaration_only`` tools) are skipped.
"""
stub_map: dict[str, Callable[..., Any]] = {}
for tool_obj in tools:
func = getattr(tool_obj, "func", None)
if callable(func):
stub_map[tool_obj.name] = func
return stub_map
def _effective_mounts(self) -> list[FileMount]:
"""Combine ``workspace_root`` (if set) with the explicit ``file_mounts``."""
mounts: list[FileMount] = []
if self._workspace_root is not None and WORKSPACE_MOUNT_PATH not in self._file_mounts:
mounts.append(
FileMount(
host_path=self._workspace_root,
mount_path=WORKSPACE_MOUNT_PATH,
mode="read-write",
write_bytes_limit=None,
)
)
mounts.extend(self._file_mounts.values())
return mounts
async def _run_code(self, *, code: str) -> list[Content]:
tools = list(self._managed_tools)
mounts = self._effective_mounts()
tool_map = self._build_tool_map(tools)
stub_map = self._build_type_stub_map(tools)
type_stubs = generate_type_stubs(stub_map) if stub_map else None
# Snapshot mtimes of host files in read-write mounts so we can later
# identify which files the sandbox actually touched.
pre_state = _snapshot_writable_mounts(mounts)
bridge = InlineCodeBridge(
tool_map,
type_stubs=type_stubs,
mounts=[_to_monty_mount(mount) for mount in mounts] or None,
resource_limits=self._resource_limits,
)
try:
result = await bridge.run(code)
except Exception as exc:
return [
Content.from_error(
message="Execution error",
error_details=f"{type(exc).__name__}: {exc}",
),
]
contents = _build_execution_contents(result=result)
contents.extend(_capture_written_files(mounts, pre_state))
return contents
def _build_execution_contents(*, result: dict[str, Any]) -> list[Content]:
stdout = str(result.get("stdout") or "").replace("\r\n", "\n")
output_value = result.get("output")
truncated = bool(result.get("truncated"))
outputs: list[Content] = []
if stdout:
text = stdout
if truncated:
text = f"{text}\n\n[stdout truncated]"
outputs.append(Content.from_text(text))
elif truncated:
outputs.append(Content.from_text("[stdout truncated]"))
if output_value is not None:
try:
serialized_output = json.dumps(output_value, ensure_ascii=False)
except (TypeError, ValueError):
serialized_output = repr(output_value)
outputs.append(Content.from_text(serialized_output))
if not outputs:
outputs.append(Content.from_text("Code executed successfully without output."))
return outputs
def _iter_real_files(root: Path) -> Iterator[Path]:
"""Walk ``root`` recursively, yielding only real (non-symlink) files.
``Path.rglob`` follows directory symlinks by default, which combined with
``Path.is_file()`` / ``Path.read_bytes()`` (both follow symlinks) would let
an attacker who controls the workspace pre-place a symlink to a host file
or directory and have our post-execution capture surface it. Skipping every
symlink at both the directory and file level closes that escape.
"""
stack: list[Path] = [root]
while stack:
current = stack.pop()
try:
entries = list(current.iterdir())
except OSError:
continue
for entry in entries:
try:
if entry.is_symlink():
continue
if entry.is_dir():
stack.append(entry)
elif entry.is_file():
yield entry
except OSError:
continue
def _snapshot_writable_mounts(mounts: Sequence[FileMount]) -> dict[str, dict[str, tuple[int, int]]]:
"""Capture (size, mtime_ns) for every real (non-symlink) host file under read-write mounts.
Returns ``{mount_path: {relative_posix_path: (size, mtime_ns)}}``. Used by
:func:`_capture_written_files` to detect new or modified files after the run.
Read-only and overlay mounts are skipped because their writes do not
propagate to the host. Symlinks (file or directory) are deliberately skipped
so an attacker cannot escape the mount by pre-placing a symlink to a host
path outside the workspace.
"""
snapshot: dict[str, dict[str, tuple[int, int]]] = {}
for mount in mounts:
if mount.mode != "read-write":
continue
host_root = Path(mount.host_path)
per_mount: dict[str, tuple[int, int]] = {}
for entry in _iter_real_files(host_root):
try:
stat = entry.lstat() # lstat: never follow symlinks (defensive)
except OSError:
continue
relative = entry.relative_to(host_root).as_posix()
per_mount[relative] = (int(stat.st_size), int(stat.st_mtime_ns))
snapshot[mount.mount_path] = per_mount
return snapshot
def _capture_written_files(
mounts: Sequence[FileMount],
pre_state: dict[str, dict[str, tuple[int, int]]],
) -> list[Content]:
"""Return :class:`Content` items for files the sandbox wrote during the run.
Mirrors Hyperlight's ``/output`` capture flow: any new or modified real
(non-symlink) file under a read-write mount is read back as binary and
surfaced as ``Content.from_data`` with a ``path`` annotation in
``additional_properties``. Symlinks are skipped at both directory and file
level so a malicious workspace cannot trick us into capturing host files
outside the configured mount root.
"""
captured: list[Content] = []
for mount in mounts:
if mount.mode != "read-write":
continue
host_root = Path(mount.host_path)
before = pre_state.get(mount.mount_path, {})
for entry in sorted(_iter_real_files(host_root)):
try:
stat = entry.lstat()
except OSError:
continue
relative = entry.relative_to(host_root).as_posix()
current = (int(stat.st_size), int(stat.st_mtime_ns))
if before.get(relative) == current:
continue # Unchanged.
sandbox_path = f"{mount.mount_path.rstrip('/')}/{relative}"
if stat.st_size > MAX_CAPTURED_FILE_BYTES:
captured.append(
Content.from_text(
f"[file {sandbox_path} omitted: {stat.st_size} bytes "
f"exceeds MAX_CAPTURED_FILE_BYTES={MAX_CAPTURED_FILE_BYTES}]"
)
)
continue
try:
# _iter_real_files already excluded symlinks at every level of
# the walk; reading the file here is safe.
data = entry.read_bytes()
except OSError:
continue
media_type = mimetypes.guess_type(entry.name)[0] or "application/octet-stream"
captured.append(
Content.from_data(
data=data,
media_type=media_type,
additional_properties={"path": sandbox_path},
)
)
return captured
@@ -0,0 +1,125 @@
# Copyright (c) Microsoft. All rights reserved.
"""Dynamic CodeAct instructions and execute_code tool descriptions for Monty."""
from __future__ import annotations
from collections.abc import Sequence
from agent_framework import FunctionTool
from ._types import FileMount
def _format_tool_summaries(tools: Sequence[FunctionTool]) -> str:
if not tools:
return "- No tools are currently registered."
lines: list[str] = []
for tool_obj in tools:
parameters = tool_obj.parameters().get("properties", {})
parameter_names = [name for name in parameters if isinstance(name, str)]
parameter_summary = ", ".join(parameter_names) if parameter_names else "none"
description = str(tool_obj.description or "").strip() or "No description provided."
lines.append(f"- `{tool_obj.name}`: {description} Parameters: {parameter_summary}.")
return "\n".join(lines)
def _format_filesystem_capabilities(mounts: Sequence[FileMount]) -> str:
if not mounts:
return (
"Filesystem access is unavailable. OS-level paths raise `PermissionError`. "
"If you need files, ask the agent operator to configure `workspace_root` or `file_mounts`."
)
lines = ["Filesystem access is enabled. Read and write paths via `pathlib.Path(...)` (or `os.path`)."]
lines.append("Configured mounts:")
for mount in mounts:
cap = ""
if mount.write_bytes_limit is not None:
cap = f", write cap {mount.write_bytes_limit} bytes"
lines.append(f"- `{mount.mount_path}` ({mount.mode}{cap})")
writable = [mount for mount in mounts if mount.mode == "read-write"]
if writable:
writable_paths = ", ".join(f"`{m.mount_path}`" for m in writable)
lines.append(
f"Files written to {writable_paths} are returned to the caller as attached files; "
"use these paths for any output artifacts."
)
return "\n".join(lines)
def build_codeact_instructions(
*,
tools: Sequence[FunctionTool],
tools_visible_to_model: bool,
mounts: Sequence[FileMount] = (),
) -> str:
"""Build dynamic CodeAct instructions for the effective Monty tool set."""
tool_summaries = _format_tool_summaries(tools)
filesystem_text = _format_filesystem_capabilities(mounts)
usage_note = (
"Some tools may also appear directly, but prefer `execute_code` whenever you need to combine "
"Python control flow with sandbox tool calls."
if tools_visible_to_model
else "Provider-owned sandbox tools are not exposed separately; use `execute_code` when you need them."
)
return f"""You have one primary tool: `execute_code`.
Inside `execute_code`, call registered tools directly as async functions:
`result = await tool_name(param=value)`. Always use `await` and keyword arguments.
Your code is type-checked against the tool signatures below before execution.
`await call_tool('name', **kwargs)` is also supported as a fallback but is not type-checked.
For fan-out, use `asyncio.gather`:
`results = await asyncio.gather(tool_a(...), tool_b(...))`.
Surface results to the caller via `print(...)` (captured and returned as text)
or by ending the code with an expression whose value is JSON-encodable - the
value of the final expression is returned alongside captured stdout.
Filesystem capabilities:
{filesystem_text}
Registered tools:
{tool_summaries}
Prefer a single `execute_code` call per request when possible, combining
multiple tool calls with Python control flow.
{usage_note}
"""
def build_execute_code_description(
*,
tools: Sequence[FunctionTool],
mounts: Sequence[FileMount] = (),
) -> str:
"""Build the dynamic ``execute_code`` tool description for standalone usage."""
tool_summaries = _format_tool_summaries(tools)
filesystem_text = _format_filesystem_capabilities(mounts)
return f"""Execute Python code in a Monty interpreter.
Inside the sandbox, call registered tools directly as typed async functions:
`result = await tool_name(param=value)`. Always use `await` and keyword arguments.
Code is type-checked against tool signatures before execution.
`await call_tool('name', **kwargs)` is also supported as a fallback.
For fan-out, use `asyncio.gather`:
`results = await asyncio.gather(tool_a(...), tool_b(...))`.
Filesystem capabilities:
{filesystem_text}
Registered tools:
{tool_summaries}
Surface results via `print(...)` (captured and returned as text) or by ending
with an expression whose value is JSON-encodable.
"""
@@ -0,0 +1,327 @@
# Copyright (c) Microsoft. All rights reserved.
"""Inline (non-durable) Monty execution bridge and type-stub generation.
Adapted from https://github.com/anthonychu/maf-codeact-monty-python.
"""
from __future__ import annotations
import asyncio
import inspect
import keyword
import types
import typing
from collections.abc import Callable, Sequence
from typing import Annotated, Any, cast, get_type_hints
MAX_PRINT_OUTPUT_CHARS = 8192
# Prelude injected into all Monty code so `asyncio.gather` works for fan-out.
_CODEACT_PRELUDE = """\
import asyncio
"""
def _ensure_json_value(value: Any) -> Any:
if value is None or isinstance(value, (str, bool, int)):
return value
if isinstance(value, float):
if value != value or value in (float("inf"), float("-inf")):
raise ValueError("Non-finite floating point values are not JSON-safe.")
return value
if isinstance(value, (list, tuple)):
items = cast("list[object] | tuple[object, ...]", value)
return [_ensure_json_value(item) for item in items]
if isinstance(value, dict):
as_dict = cast("dict[object, object]", value)
return {str(k): _ensure_json_value(v) for k, v in as_dict.items()}
raise ValueError(f"Value of type {type(value).__name__} is not JSON-safe.")
def _external_error(exc: Exception) -> dict[str, str]:
return {"exc_type": type(exc).__name__, "message": str(exc)}
def _parse_call_tool(args: tuple[Any, ...], kwargs: dict[str, Any]) -> tuple[str, dict[str, Any]]:
if not args:
raise ValueError("call_tool requires a tool name as the first argument.")
name = args[0]
if not isinstance(name, str) or not name:
raise ValueError("Tool name must be a non-empty string.")
if len(args) > 1:
raise ValueError(
"call_tool accepts only the tool name as a positional argument. Use keyword arguments for parameters."
)
return name, dict(kwargs)
def _build_code(code: str) -> str:
return f"{_CODEACT_PRELUDE}\n{code}"
def _python_type_repr(annotation: Any) -> str:
"""Convert a Python type annotation to its string representation for stubs."""
if annotation is inspect.Parameter.empty:
return "Any"
if annotation is type(None):
# ``None`` in annotations represents ``NoneType``; emit it literally so
# ``ty`` can validate ``Optional[X]`` / ``Union[..., None]`` / ``-> None``
# signatures correctly.
return "None"
origin = typing.get_origin(annotation)
if origin is Annotated:
args = typing.get_args(annotation)
return _python_type_repr(args[0]) if args else "Any"
if origin is not None:
args = typing.get_args(annotation)
# Normalize ``typing.Union[...]`` and PEP-604 ``X | Y`` to PEP-604 syntax so
# ``None`` is preserved across both forms.
if origin is typing.Union or origin is types.UnionType:
return " | ".join(_python_type_repr(a) for a in args) if args else "Any"
origin_name = getattr(origin, "__name__", None)
if origin_name is None:
origin_name = str(origin)
if origin_name.startswith("<class '"):
origin_name = origin_name[8:-2]
if args:
arg_strs = ", ".join(_python_type_repr(a) for a in args)
return f"{origin_name}[{arg_strs}]"
return origin_name
if hasattr(annotation, "__name__"):
return str(annotation.__name__)
return str(annotation)
def generate_type_stubs(tool_callables: dict[str, Callable[..., Any]]) -> str:
"""Generate Python type stub declarations for tools + DSL primitives.
Stubs are fed to Monty's ``type_check_stubs`` so ``ty`` can validate the
LLM-generated code against the actual tool signatures before any host
call runs.
Tools whose ``name`` is not a valid Python identifier are skipped because
their name cannot be safely splatted into stub source. The model can still
reach them via the ``call_tool("weird name", ...)`` fallback at runtime,
but they will not get type-checked stubs.
"""
lines: list[str] = [
"from typing import Any",
"",
"# DSL primitives",
"async def call_tool(name: str, **kwargs: Any) -> Any:",
" raise NotImplementedError()",
"",
"# Registered tools - call directly with typed arguments",
]
for name, func in sorted(tool_callables.items()):
if not name.isidentifier() or keyword.iskeyword(name):
# A non-identifier name (or a Python keyword) would inject invalid
# / dangerous syntax into the stub source. Skip stub generation;
# the tool stays reachable through ``call_tool(name, ...)``.
continue
try:
sig = inspect.signature(func)
hints = get_type_hints(func, include_extras=True)
except (ValueError, TypeError):
lines.append(f"async def {name}(**kwargs: Any) -> Any:")
lines.append(" raise NotImplementedError()")
lines.append("")
continue
params: list[str] = []
for param_name, param in sig.parameters.items():
annotation = hints.get(param_name, inspect.Parameter.empty)
type_str = _python_type_repr(annotation)
if param.default is not inspect.Parameter.empty:
params.append(f"{param_name}: {type_str} = ...")
else:
params.append(f"{param_name}: {type_str}")
return_annotation = hints.get("return", inspect.Parameter.empty)
return_str = _python_type_repr(return_annotation)
param_str = ", ".join(params)
lines.append(f"async def {name}({param_str}) -> {return_str}:")
lines.append(" raise NotImplementedError()")
lines.append("")
return "\n".join(lines)
class _PrintCollector:
"""Collect Monty stdout, capped at ``MAX_PRINT_OUTPUT_CHARS``."""
def __init__(self) -> None:
self.chunks: list[str] = []
self.truncated: bool = False
self._size: int = 0 # running character count to avoid O(n) per append
def __call__(self, stream: str, text: str) -> None:
if self.truncated:
return
remaining = MAX_PRINT_OUTPUT_CHARS - self._size
if remaining <= 0:
self.truncated = True
return
text_value = str(text)
if len(text_value) > remaining:
clipped = text_value[:remaining]
self.chunks.append(clipped)
self._size += len(clipped)
self.truncated = True
else:
self.chunks.append(text_value)
self._size += len(text_value)
@property
def output(self) -> str:
return "".join(self.chunks)
def load_monty() -> Any:
"""Import ``pydantic_monty`` lazily so unit tests can run without it.
Returns the module so callers can read ``Monty``, ``MontyComplete``,
``FunctionSnapshot``, ``FutureSnapshot``, ``NameLookupSnapshot`` from it.
"""
try:
import pydantic_monty
except ImportError as exc:
raise RuntimeError(
"The `pydantic-monty` package is required to execute Monty CodeAct code. "
"Install it with `pip install pydantic-monty`."
) from exc
return pydantic_monty
class InlineCodeBridge:
"""Execute Monty code inline (non-durable).
Supports both ``await call_tool('name', ...)`` and direct ``await name(...)``
calls. When Monty yields a :class:`FutureSnapshot`, the bridge invokes the
registered host tools and resumes execution with the results.
"""
def __init__(
self,
tool_map: dict[str, Callable[..., Any]],
*,
type_stubs: str | None = None,
mounts: Sequence[Any] | None = None,
resource_limits: dict[str, Any] | None = None,
) -> None:
self.tool_map: dict[str, Callable[..., Any]] = dict(tool_map)
self.type_stubs: str | None = type_stubs
self._mounts = tuple(mounts) if mounts else ()
self._resource_limits = resource_limits
self._pending_calls: dict[int, tuple[str, dict[str, Any]]] = {}
async def run(self, code: str) -> dict[str, Any]:
if not isinstance(code, str) or not code.strip():
raise ValueError("Code must be a non-empty string.")
monty_module = load_monty()
Monty = monty_module.Monty
MontyComplete = monty_module.MontyComplete
FunctionSnapshot = monty_module.FunctionSnapshot
FutureSnapshot = monty_module.FutureSnapshot
NameLookupSnapshot = monty_module.NameLookupSnapshot
printer = _PrintCollector()
monty = Monty(
_build_code(code),
script_name="codeact.py",
type_check=self.type_stubs is not None,
type_check_stubs=self.type_stubs,
)
start_kwargs: dict[str, Any] = {"print_callback": printer}
if self._mounts:
start_kwargs["mount"] = list(self._mounts)
if self._resource_limits:
start_kwargs["limits"] = self._resource_limits
progress = monty.start(**start_kwargs)
while True:
if isinstance(progress, MontyComplete):
return {
"output": _ensure_json_value(progress.output),
"stdout": printer.output,
"truncated": printer.truncated,
}
if isinstance(progress, FunctionSnapshot):
progress = self._handle_function(progress)
continue
if isinstance(progress, FutureSnapshot):
progress = await self._handle_future(progress)
continue
if isinstance(progress, NameLookupSnapshot):
raise RuntimeError(f"Name lookup not supported: {progress.variable_name!r}")
raise RuntimeError(f"Unsupported Monty progress type: {type(progress).__name__}")
def _handle_function(self, snapshot: Any) -> Any:
if snapshot.is_os_function:
return snapshot.resume({
"exc_type": "PermissionError",
"message": "OS and filesystem calls are not available.",
})
function_name = str(snapshot.function_name)
if function_name in self.tool_map:
return self._schedule_direct_tool(snapshot, function_name)
if function_name == "call_tool":
return self._schedule_call_tool(snapshot)
return snapshot.resume({
"exc_type": "NameError",
"message": f"Function {function_name!r} is not available.",
})
def _schedule_direct_tool(self, snapshot: Any, name: str) -> Any:
# Positional args are rejected up-front by ``ty`` because the generated
# stubs declare every parameter as keyword-typed. Anything that slips
# through (e.g. tools with no signature inspection) is forwarded to the
# host tool as-is via kwargs only.
self._pending_calls[int(snapshot.call_id)] = (name, dict(snapshot.kwargs))
return snapshot.resume({"future": ...})
def _schedule_call_tool(self, snapshot: Any) -> Any:
try:
name, kwargs = _parse_call_tool(snapshot.args, snapshot.kwargs)
if name not in self.tool_map:
allowed = ", ".join(sorted(self.tool_map.keys())) or "<none>"
raise ValueError(f"Tool {name!r} is not registered. Available tools: {allowed}")
self._pending_calls[int(snapshot.call_id)] = (name, kwargs)
except Exception as exc:
return snapshot.resume(_external_error(exc))
return snapshot.resume({"future": ...})
async def _handle_future(self, snapshot: Any) -> Any:
pending_call_ids = [int(cid) for cid in snapshot.pending_call_ids]
if not pending_call_ids:
return snapshot.resume({})
entries: list[tuple[int, tuple[str, dict[str, Any]]]] = []
for cid in pending_call_ids:
if cid not in self._pending_calls:
raise RuntimeError(f"Unknown future call ID: {cid}")
entries.append((cid, self._pending_calls.pop(cid)))
tasks = [self._invoke_tool(cid, name, kwargs) for cid, (name, kwargs) in entries]
results = await asyncio.gather(*tasks)
resume_results: dict[int, Any] = dict(results)
return snapshot.resume(resume_results)
async def _invoke_tool(self, cid: int, name: str, kwargs: dict[str, Any]) -> tuple[int, Any]:
# Every entry in ``self.tool_map`` is produced by ``_make_tool_callback``
# as ``partial(FunctionTool.invoke, skip_parsing=True)``. ``FunctionTool.invoke``
# is always ``async def``, so a plain ``await`` is correct for every call and
# avoids relying on ``inspect.iscoroutinefunction(partial(...))``, which can
# return ``False`` for some ``partial`` shapes (cpython#98590) and would route
# the call through ``asyncio.to_thread`` with an unawaited coroutine return.
try:
result = await self.tool_map[name](**kwargs)
return cid, {"return_value": _ensure_json_value(result)}
except Exception as exc:
return cid, _external_error(exc)
@@ -0,0 +1,95 @@
# Copyright (c) Microsoft. All rights reserved.
"""``MontyCodeActProvider`` - context provider injecting Monty-backed CodeAct."""
from __future__ import annotations
from collections.abc import Callable, Sequence
from pathlib import Path
from typing import Any
from agent_framework import AgentSession, ContextProvider, FunctionTool, SessionContext
from agent_framework._tools import ApprovalMode
from ._execute_code_tool import MontyExecuteCodeTool
from ._types import FileMount, FileMountInput
class MontyCodeActProvider(ContextProvider):
"""Inject a Monty-backed CodeAct surface using provider-owned tools.
Mirrors :class:`agent_framework_hyperlight.HyperlightCodeActProvider` for
the subset of capabilities that apply to the Monty interpreter:
``tools``, ``approval_mode``, ``workspace_root``, ``file_mounts``, and
``resource_limits`` (Monty-only).
"""
DEFAULT_SOURCE_ID = "monty_codeact"
def __init__(
self,
source_id: str = DEFAULT_SOURCE_ID,
*,
tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None = None,
approval_mode: ApprovalMode | None = None,
workspace_root: str | Path | None = None,
file_mounts: FileMountInput | Sequence[FileMountInput] | None = None,
resource_limits: dict[str, Any] | None = None,
) -> None:
super().__init__(source_id)
self._execute_code_tool = MontyExecuteCodeTool(
tools=tools,
approval_mode=approval_mode,
workspace_root=workspace_root,
file_mounts=file_mounts,
resource_limits=resource_limits,
)
def add_tools(
self,
tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]],
) -> None:
"""Add provider-owned Monty tools."""
self._execute_code_tool.add_tools(tools)
def get_tools(self) -> list[FunctionTool]:
"""Return the provider-owned Monty tools."""
return self._execute_code_tool.get_tools()
def remove_tool(self, name: str) -> None:
"""Remove one provider-owned Monty tool by name."""
self._execute_code_tool.remove_tool(name)
def clear_tools(self) -> None:
"""Remove all provider-owned Monty tools."""
self._execute_code_tool.clear_tools()
def add_file_mounts(self, file_mounts: FileMountInput | Sequence[FileMountInput]) -> None:
"""Add provider-managed file mounts."""
self._execute_code_tool.add_file_mounts(file_mounts)
def get_file_mounts(self) -> list[FileMount]:
"""Return the provider-managed file mounts (excluding ``workspace_root``)."""
return self._execute_code_tool.get_file_mounts()
def remove_file_mount(self, mount_path: str) -> None:
"""Remove one provider-managed file mount by its sandbox path."""
self._execute_code_tool.remove_file_mount(mount_path)
def clear_file_mounts(self) -> None:
"""Remove all provider-managed file mounts."""
self._execute_code_tool.clear_file_mounts()
async def before_run(
self,
*,
agent: Any,
session: AgentSession | None,
context: SessionContext,
state: dict[str, Any],
) -> None:
"""Inject CodeAct instructions and a run-scoped execute_code tool before each run."""
run_tool = self._execute_code_tool.create_run_tool()
state[self.source_id] = run_tool.build_serializable_state()
context.extend_instructions(self.source_id, run_tool.build_instructions(tools_visible_to_model=False))
context.extend_tools(self.source_id, [run_tool])
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft. All rights reserved.
"""Public types for ``agent-framework-monty``.
Mirrors ``agent_framework_hyperlight._types`` where the Monty runtime exposes
an equivalent concept so users can move between the two providers with minimal
churn.
"""
from __future__ import annotations
from pathlib import Path
from typing import Literal, NamedTuple, TypeAlias
#: Allowed Monty mount modes. ``overlay`` (the Monty default) buffers writes
#: in-memory and is therefore not visible to the host after execution.
#: ``read-only`` rejects writes. ``read-write`` writes through to the host
#: directory.
MountMode: TypeAlias = Literal["overlay", "read-only", "read-write"]
class FileMount(NamedTuple):
"""Map a host directory into the Monty sandbox.
Mirrors :class:`agent_framework_hyperlight.FileMount` with two extra
fields that surface Monty's underlying ``MountDir`` capabilities:
``mode`` selects read-only / read-write / overlay semantics, and
``write_bytes_limit`` caps the total bytes written through this mount.
"""
host_path: str | Path
mount_path: str
mode: MountMode = "overlay"
write_bytes_limit: int | None = None
FileMountHostPath: TypeAlias = str | Path
FileMountInput: TypeAlias = str | tuple[FileMountHostPath, str] | FileMount