Skip to content

Commit 7839124

Browse files
amolrAmol RavandeImran SiddiqueCopilot
authored
Hyperlight sandbox provider for Agent governance Toolkit (#1806)
* Adding Hyperlight provider Signed-off-by: Imran Siddique <imran.siddique@microsoft.com> * Code review comments Signed-off-by: Imran Siddique <imran.siddique@microsoft.com> * Fix dependency confusion Signed-off-by: Imran Siddique <imran.siddique@microsoft.com> * fix(ci): add cspell terms and fix broken Azure design doc link Add Hyperlight-specific terms (Nanvix, microkernel, hypercall, flatbuffer, uncallable, reqwest, bursty, kwarg) to cspell dictionary. Replace broken link to non-existent AZURE-SANDBOX-ISOLATION-DESIGN.md with 'planned' placeholder. Signed-off-by: Imran Siddique <45405841+imran-siddique@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Signed-off-by: Imran Siddique <imran.siddique@microsoft.com> Co-authored-by: Amol Ravande <amolr@microsoft.com> Co-authored-by: Imran Siddique <imran.siddique@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 52d4dbb commit 7839124

15 files changed

Lines changed: 3448 additions & 34 deletions

File tree

.cspell-repo-terms.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,12 @@ cref
143143
nameof
144144
aspnet
145145
appsettings
146+
bursty
147+
flatbuffer
148+
hypercall
149+
kwarg
150+
microkernel
151+
Nanvix
152+
reqwest
146153
requireapproval
154+
uncallable

agent-governance-python/agent-sandbox/pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@ dependencies = []
3939
docker = [
4040
"docker>=7.0.0,<8.0",
4141
]
42+
hyperlight = [
43+
"hyperlight-sandbox>=0.4.0,<0.5",
44+
]
4245
policy = [
4346
"agent-os-kernel>=3.2.0,<4.0",
4447
]
4548
full = [
46-
"agent-sandbox[docker,policy]",
49+
"agent-sandbox[docker,hyperlight,policy]",
4750
]
4851
dev = [
4952
"pytest>=8.0.0,<10.0",

agent-governance-python/agent-sandbox/src/agent_sandbox/__init__.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
3-
"""Agent Sandbox — Docker-based execution isolation for AI agents.
3+
"""Agent Sandbox — execution isolation for AI agents.
44
55
Provides ``SandboxProvider``, the abstract base class for all sandbox
6-
backends, and ``DockerSandboxProvider``, a hardened Docker implementation
7-
with policy-driven resource limits, tool/network proxies, and filesystem
8-
checkpointing via ``docker commit``.
6+
backends, plus two built-in implementations:
7+
8+
* :class:`DockerSandboxProvider` — hardened Docker containers with
9+
policy-driven resource limits, tool/network proxies, and filesystem
10+
checkpointing via ``docker commit``.
11+
* :class:`HyperLightSandboxProvider` — micro-VM isolation backed by the
12+
upstream `hyperlight-sandbox <https://github.com/hyperlight-dev/hyperlight-sandbox>`_
13+
project (CNCF Sandbox). Capability-bound tools and domains, with
14+
in-memory snapshots.
915
"""
1016

1117
from importlib.metadata import PackageNotFoundError, version
@@ -20,14 +26,35 @@
2026
SessionStatus,
2127
)
2228
from agent_sandbox.isolation_runtime import IsolationRuntime
23-
from agent_sandbox.state import SandboxCheckpoint, SandboxStateManager
29+
from agent_sandbox.docker_provider.state import SandboxCheckpoint, SandboxStateManager
2430

2531
# Lazy import: DockerSandboxProvider requires the optional ``docker`` SDK.
2632
try:
27-
from agent_sandbox.docker_sandbox_provider import DockerSandboxProvider
33+
from agent_sandbox.docker_provider import DockerSandboxProvider
2834
except ImportError:
2935
DockerSandboxProvider = None # type: ignore[assignment,misc]
3036

37+
# Lazy import: HyperLightSandboxProvider requires the optional
38+
# ``hyperlight-sandbox`` SDK. The class itself does not import the
39+
# SDK at module load — the dependency is resolved at session-creation
40+
# time — but we still wrap the import in ``try/except ImportError`` for
41+
# symmetry with ``DockerSandboxProvider`` and to remain robust against
42+
# future refactors that might pull the SDK in eagerly.
43+
try:
44+
from agent_sandbox.hyperlight_provider import (
45+
HyperlightBackend,
46+
HyperlightConfig,
47+
HyperLightSandboxProvider,
48+
SnapshotHandle,
49+
hyperlight_config_from_policy,
50+
)
51+
except ImportError:
52+
HyperlightBackend = None # type: ignore[assignment,misc]
53+
HyperlightConfig = None # type: ignore[assignment,misc]
54+
HyperLightSandboxProvider = None # type: ignore[assignment,misc]
55+
SnapshotHandle = None # type: ignore[assignment,misc]
56+
hyperlight_config_from_policy = None # type: ignore[assignment]
57+
3158
try:
3259
__version__ = version("agent-sandbox")
3360
except PackageNotFoundError:
@@ -38,6 +65,9 @@
3865
"DockerSandboxProvider",
3966
"ExecutionHandle",
4067
"ExecutionStatus",
68+
"HyperLightSandboxProvider",
69+
"HyperlightBackend",
70+
"HyperlightConfig",
4171
"IsolationRuntime",
4272
"SandboxCheckpoint",
4373
"SandboxConfig",
@@ -46,4 +76,6 @@
4676
"SandboxStateManager",
4777
"SessionHandle",
4878
"SessionStatus",
79+
"SnapshotHandle",
80+
"hyperlight_config_from_policy",
4981
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
"""Docker-backed sandbox provider for ``agent-sandbox``.
4+
5+
Implements :class:`agent_sandbox.SandboxProvider` on top of hardened
6+
Docker containers with policy-driven resource limits, tool/network
7+
proxies, and filesystem checkpointing via ``docker commit``.
8+
9+
Importing :class:`DockerSandboxProvider` requires the optional
10+
``docker`` SDK to be installed.
11+
"""
12+
13+
from agent_sandbox.docker_provider.provider import (
14+
DockerSandboxProvider,
15+
docker_config_from_policy,
16+
has_iptables,
17+
)
18+
from agent_sandbox.docker_provider.state import (
19+
SandboxCheckpoint,
20+
SandboxStateManager,
21+
)
22+
23+
__all__ = [
24+
"DockerSandboxProvider",
25+
"SandboxCheckpoint",
26+
"SandboxStateManager",
27+
"docker_config_from_policy",
28+
"has_iptables",
29+
]

agent-governance-python/agent-sandbox/src/agent_sandbox/docker_sandbox_provider.py renamed to agent-governance-python/agent-sandbox/src/agent_sandbox/docker_provider/provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
SessionHandle,
3333
SessionStatus,
3434
)
35-
from agent_sandbox.state import SandboxCheckpoint, SandboxStateManager
35+
from agent_sandbox.docker_provider.state import SandboxCheckpoint, SandboxStateManager
3636

3737
logger = logging.getLogger(__name__)
3838

agent-governance-python/agent-sandbox/src/agent_sandbox/state.py renamed to agent-governance-python/agent-sandbox/src/agent_sandbox/docker_provider/state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import TYPE_CHECKING, Any
1111

1212
if TYPE_CHECKING:
13-
from agent_sandbox.docker_sandbox_provider import DockerSandboxProvider
13+
from agent_sandbox.docker_provider.provider import DockerSandboxProvider
1414

1515
logger = logging.getLogger(__name__)
1616

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
"""Hyperlight-backed sandbox provider for ``agent-sandbox``.
4+
5+
Implements :class:`agent_sandbox.SandboxProvider` on top of the upstream
6+
`hyperlight-sandbox <https://github.com/hyperlight-dev/hyperlight-sandbox>`_
7+
project (CNCF Sandbox, Apache-2.0).
8+
9+
See ``docs/proposals/HYPERLIGHT-SANDBOX-ISOLATION-DESIGN.md`` for the
10+
design rationale.
11+
12+
Importing :class:`HyperLightSandboxProvider` does not require
13+
``hyperlight-sandbox`` to be installed; the dependency is only resolved
14+
when the provider is constructed and ``is_available()`` is queried.
15+
"""
16+
17+
from agent_sandbox.hyperlight_provider.config import (
18+
HyperlightConfig,
19+
hyperlight_config_from_policy,
20+
)
21+
from agent_sandbox.hyperlight_provider.provider import (
22+
HyperlightBackend,
23+
HyperLightSandboxProvider,
24+
SnapshotHandle,
25+
)
26+
27+
__all__ = [
28+
"HyperlightBackend",
29+
"HyperlightConfig",
30+
"HyperLightSandboxProvider",
31+
"SnapshotHandle",
32+
"hyperlight_config_from_policy",
33+
]
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
"""Configuration helpers for :class:`HyperLightSandboxProvider`.
4+
5+
The configuration mirrors the design doc's ``HyperlightConfig`` surface:
6+
7+
* ``backend`` and ``module`` select which upstream backend / guest the
8+
session runs on.
9+
* ``heap_size_bytes`` / ``stack_size_bytes`` / ``max_execution_time_ms``
10+
map to upstream's ``SandboxConfiguration`` knobs.
11+
* ``input_dir`` / ``output_dir`` are mounted in the guest as read-only
12+
``/input`` and writable ``/output``.
13+
14+
``hyperlight_config_from_policy`` performs the same kind of policy →
15+
config translation as ``docker_config_from_policy`` so a single
16+
``PolicyDocument`` (or duck-typed equivalent) can drive either backend.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
from dataclasses import dataclass, field
22+
from typing import Any
23+
24+
from agent_sandbox.sandbox_provider import SandboxConfig
25+
26+
# Upstream backend identifiers. Kept as plain strings (not an Enum) to
27+
# avoid forcing callers to import a hyperlight-specific symbol just to
28+
# pass ``backend="wasm"``.
29+
_VALID_BACKENDS: frozenset[str] = frozenset({"wasm", "hyperlightjs", "nanvix"})
30+
31+
# Default heap / stack sizes follow upstream's ``SandboxConfiguration``
32+
# defaults at hyperlight-sandbox v0.4.0.
33+
_DEFAULT_HEAP_BYTES = 64 * 1024 * 1024
34+
_DEFAULT_STACK_BYTES = 2 * 1024 * 1024
35+
_DEFAULT_MAX_EXEC_MS = 60_000
36+
37+
38+
@dataclass
39+
class HyperlightConfig:
40+
"""Provider-specific configuration for a Hyperlight session.
41+
42+
Attributes
43+
----------
44+
backend:
45+
Upstream backend selector — ``"wasm"`` (default; supports the
46+
packaged Python and JS guests), ``"hyperlightjs"`` (JS-only,
47+
smaller footprint), or ``"nanvix"`` (preview microkernel; no
48+
tools, FS, network, or snapshots).
49+
module:
50+
Guest module identifier passed to upstream when ``backend ==
51+
"wasm"``. ``"python_guest"`` runs real Python source via
52+
``Sandbox.run("…")``; ``"javascript_guest"`` runs JS source.
53+
Ignored for ``hyperlightjs`` and ``nanvix``.
54+
heap_size_bytes / stack_size_bytes / max_execution_time_ms:
55+
Upstream ``SandboxConfiguration`` knobs.
56+
input_dir / output_dir:
57+
Optional host paths mounted in the guest as ``/input``
58+
(read-only) and ``/output`` (writable, per-session).
59+
env_vars:
60+
Optional host-side environment exposed to the guest where the
61+
backend supports it. Ignored on backends that have no env model.
62+
"""
63+
64+
backend: str = "wasm"
65+
module: str | None = "python_guest"
66+
heap_size_bytes: int = _DEFAULT_HEAP_BYTES
67+
stack_size_bytes: int = _DEFAULT_STACK_BYTES
68+
max_execution_time_ms: int = _DEFAULT_MAX_EXEC_MS
69+
input_dir: str | None = None
70+
output_dir: str | None = None
71+
env_vars: dict[str, str] = field(default_factory=dict)
72+
73+
def __post_init__(self) -> None:
74+
if self.backend not in _VALID_BACKENDS:
75+
raise ValueError(
76+
f"Unknown Hyperlight backend '{self.backend}'. "
77+
f"Expected one of: {sorted(_VALID_BACKENDS)}"
78+
)
79+
if self.heap_size_bytes <= 0:
80+
raise ValueError("heap_size_bytes must be positive")
81+
if self.stack_size_bytes <= 0:
82+
raise ValueError("stack_size_bytes must be positive")
83+
if self.max_execution_time_ms <= 0:
84+
raise ValueError("max_execution_time_ms must be positive")
85+
# ``module`` is upstream-validated when the Sandbox is built; we
86+
# don't enforce a hardcoded list here so future packaged guests
87+
# work without an SDK release.
88+
89+
@classmethod
90+
def from_sandbox_config(
91+
cls,
92+
cfg: SandboxConfig,
93+
*,
94+
backend: str = "wasm",
95+
module: str | None = "python_guest",
96+
) -> HyperlightConfig:
97+
"""Translate the generic :class:`SandboxConfig` into a
98+
:class:`HyperlightConfig`.
99+
100+
Resource limits (``memory_mb``, ``timeout_seconds``) become
101+
upstream heap / max-execution-time. ``cpu_limit`` is dropped
102+
because Hyperlight pins one vCPU per micro-VM.
103+
"""
104+
return cls(
105+
backend=backend,
106+
module=module,
107+
heap_size_bytes=int(cfg.memory_mb) * 1024 * 1024,
108+
stack_size_bytes=_DEFAULT_STACK_BYTES,
109+
max_execution_time_ms=int(cfg.timeout_seconds * 1000),
110+
input_dir=cfg.input_dir,
111+
output_dir=cfg.output_dir,
112+
env_vars=dict(cfg.env_vars),
113+
)
114+
115+
116+
def hyperlight_config_from_policy(
117+
policy: Any,
118+
base: HyperlightConfig | None = None,
119+
) -> HyperlightConfig:
120+
"""Extract Hyperlight-relevant fields from a policy.
121+
122+
Reads well-known attributes (``defaults.max_memory_mb``,
123+
``defaults.timeout_seconds``, ``sandbox_mounts.input_dir`` /
124+
``output_dir``) when present; missing attributes leave *base*
125+
unchanged. Tool and network allowlists are *not* merged here —
126+
those are applied by :class:`HyperLightSandboxProvider` directly
127+
via ``register_tool`` / ``allow_domain``.
128+
"""
129+
cfg = HyperlightConfig(
130+
backend=base.backend if base else "wasm",
131+
module=base.module if base else "python_guest",
132+
heap_size_bytes=base.heap_size_bytes if base else _DEFAULT_HEAP_BYTES,
133+
stack_size_bytes=base.stack_size_bytes if base else _DEFAULT_STACK_BYTES,
134+
max_execution_time_ms=(
135+
base.max_execution_time_ms if base else _DEFAULT_MAX_EXEC_MS
136+
),
137+
input_dir=base.input_dir if base else None,
138+
output_dir=base.output_dir if base else None,
139+
env_vars=dict(base.env_vars) if base else {},
140+
)
141+
142+
defaults = getattr(policy, "defaults", None)
143+
if defaults is not None:
144+
max_mem_mb = getattr(defaults, "max_memory_mb", None)
145+
if isinstance(max_mem_mb, (int, float)) and max_mem_mb > 0:
146+
cfg.heap_size_bytes = int(max_mem_mb) * 1024 * 1024
147+
timeout_s = getattr(defaults, "timeout_seconds", None)
148+
if isinstance(timeout_s, (int, float)) and timeout_s > 0:
149+
cfg.max_execution_time_ms = int(timeout_s * 1000)
150+
151+
mounts = getattr(policy, "sandbox_mounts", None)
152+
if mounts is not None:
153+
in_dir = getattr(mounts, "input_dir", None)
154+
if in_dir:
155+
cfg.input_dir = str(in_dir)
156+
out_dir = getattr(mounts, "output_dir", None)
157+
if out_dir:
158+
cfg.output_dir = str(out_dir)
159+
160+
return cfg

0 commit comments

Comments
 (0)