maf-sandbox 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maf_sandbox-0.1.0/LICENSE +21 -0
- maf_sandbox-0.1.0/PKG-INFO +93 -0
- maf_sandbox-0.1.0/README.md +69 -0
- maf_sandbox-0.1.0/pyproject.toml +71 -0
- maf_sandbox-0.1.0/pyproject.toml.orig +87 -0
- maf_sandbox-0.1.0/src/maf_sandbox/__init__.py +105 -0
- maf_sandbox-0.1.0/src/maf_sandbox/_error_detail.py +48 -0
- maf_sandbox-0.1.0/src/maf_sandbox/_protocol.py +183 -0
- maf_sandbox-0.1.0/src/maf_sandbox/_purger.py +41 -0
- maf_sandbox-0.1.0/src/maf_sandbox/_router.py +148 -0
- maf_sandbox-0.1.0/src/maf_sandbox/maf.py +366 -0
- maf_sandbox-0.1.0/src/maf_sandbox/py.typed +0 -0
- maf_sandbox-0.1.0/src/maf_sandbox/testing.py +170 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SOKOLAI BV
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maf-sandbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One seam between an application and any sandbox provider, for Microsoft Agent Framework — reference implementation of agent-framework#7568: backend protocol, selection, and the rule that a weaker isolation boundary is never used in a deployed environment.
|
|
5
|
+
Keywords: agent-framework,microsoft-agent-framework,sandbox,isolation,ai-agents
|
|
6
|
+
Author: SOKOLAI BV
|
|
7
|
+
Author-email: SOKOLAI BV <info@sokolai.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Dist: agent-framework-core>=1.13.0,<2
|
|
19
|
+
Requires-Python: >=3.12, <3.15
|
|
20
|
+
Project-URL: Homepage, https://www.sokol.ai
|
|
21
|
+
Project-URL: Source, https://github.com/sokolaidev/maf-extensions
|
|
22
|
+
Project-URL: Issues, https://github.com/sokolaidev/maf-extensions/issues
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# maf-sandbox
|
|
26
|
+
|
|
27
|
+
> **Experimental.** This package is early-stage (`0.1.0`, `Development Status :: 4 - Beta`) — its API may change or be removed in a future release without notice. Importing it emits a one-time `MafSandboxExperimentalWarning`; suppress it with `warnings.filterwarnings("ignore", category=maf_sandbox.MafSandboxExperimentalWarning)` once you've read the notice.
|
|
28
|
+
|
|
29
|
+
This package is not affiliated with, endorsed by, or a product of Microsoft — it is a third-party reference implementation of [microsoft/agent-framework#7568](https://github.com/microsoft/agent-framework/issues/7568), written for use with [Microsoft Agent Framework](https://aka.ms/AgentFramework) but with no dependency on it in its protocol layer.
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install maf-sandbox
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from maf_sandbox import Isolation, SandboxKey, SandboxRouter, SandboxSpec, WorkspaceContext
|
|
39
|
+
|
|
40
|
+
# Implement SandboxBackend against your own provider — or install maf-sandbox-aca for a
|
|
41
|
+
# ready-made Azure Container Apps Sandboxes backend — then wire it into a router:
|
|
42
|
+
router = SandboxRouter([my_backend], deployed=False)
|
|
43
|
+
sandbox = await router.acquire(SandboxKey(scope="tenant-1", thread_id="t-1", agent_dir="devops"), SandboxSpec(kind="bicep", image="bicep-sandbox:0.46.1", egress_allow=("mcr.microsoft.com",), work_dir="/workspace"))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Threat model
|
|
47
|
+
|
|
48
|
+
This package draws no isolation boundary itself — it is protocol and policy over whatever a `SandboxBackend` implementation actually provides. `Isolation` states three tiers a backend can declare, from strongest to weakest: `vm` (a VM boundary — the whole guest, not just a process, is untrusted), `container` (a shared-kernel boundary), and `process` (no boundary beyond the OS's own process isolation). `SandboxRouter` enforces the one rule below on top of that declaration; the package's job is to make an unsafe backend selection fail loudly at construction, not silently at first use. Beyond backend selection, this layer has nothing else to get wrong: it holds no credentials, executes nothing, and reaches no network — everything security-relevant about a *specific* sandbox lives in the backend that implements it.
|
|
49
|
+
|
|
50
|
+
## The vocabulary
|
|
51
|
+
|
|
52
|
+
| | |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `SandboxKey` | `(scope, thread_id, agent_dir)` — the one sandbox a caller may reach |
|
|
55
|
+
| `SandboxSpec` | what a sandbox of a given *kind* needs: image, egress allowlist, work dir |
|
|
56
|
+
| `Sandbox` | `write_file` + `exec` — all a workload gets |
|
|
57
|
+
| `SandboxBackend` | `acquire` / `dispose` / `dispose_scope` |
|
|
58
|
+
| `SandboxRouter` | picks the backend, enforces the deployed rule |
|
|
59
|
+
| `SandboxPurger` | duck-typed `purge_scoped_thread(scope, thread_id)` for a host's delete path |
|
|
60
|
+
|
|
61
|
+
`SandboxKey`'s scope and thread come from the host's request context through `WorkspaceContext`, whose fields are **callables read at call time** rather than values. That is deliberate: a key a caller can supply is a key a *model* can supply, and that would let one conversation address another's sandbox.
|
|
62
|
+
|
|
63
|
+
`SandboxSpec.egress_allow` is an allowlist — everything not named is denied, so an empty tuple means no network. Stating it positively means a spec that forgets to mention egress gets the closed configuration rather than the open one.
|
|
64
|
+
|
|
65
|
+
## The one rule that is not a convenience
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
DEPLOYED_ISOLATION = frozenset({Isolation.VM})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A backend declares its own `isolation` (`vm` / `container` / `process`). When the host reports it is running **deployed**, the router refuses to select anything weaker than a VM boundary — raising `SandboxBackendNotPermitted` at construction, not at first use, so a misconfigured deployment cannot start with the feature apparently enabled and quietly unsafe.
|
|
72
|
+
|
|
73
|
+
It refuses rather than degrades. Falling back to a stronger backend would hide a misconfiguration; proceeding with the weaker one would break claims the host's security posture makes about every execution surface. Neither is better than an error.
|
|
74
|
+
|
|
75
|
+
A hardened container runtime (gVisor, Kata, Firecracker) is deliberately *not* in the permitted set. Admitting one is a decision for whoever owns those posture claims, taken there first.
|
|
76
|
+
|
|
77
|
+
## Writing a backend
|
|
78
|
+
|
|
79
|
+
Implement `name`, `isolation`, `acquire`, `dispose`, `dispose_scope`. Two things worth knowing before you start:
|
|
80
|
+
|
|
81
|
+
**`acquire` is get-or-create.** A workload's fix-round loop calls it every iteration; returning a cold sandbox each time turns a seconds-long loop into a minutes-long one.
|
|
82
|
+
|
|
83
|
+
**`dispose_scope` must not consult only your process's memory.** A multi-replica host serves a conversation delete wherever it lands, so the replica that created the sandbox is usually not the one deleting it. Derive the set from the service — labels, a listing, whatever your provider offers. A backend that skips this leaves billable compute running and the bug is invisible on a single-replica dev box.
|
|
84
|
+
|
|
85
|
+
Both `dispose` methods are best-effort by contract: purge must never fail a delete.
|
|
86
|
+
|
|
87
|
+
## Provenance
|
|
88
|
+
|
|
89
|
+
Extracted from a production agent application, where this seam was written for its first execution surface: a tool that compiles agent-authored infrastructure code in a sandbox. The deployed-isolation rule above is not a preference — it is what a security review concluded when it worked through what a shared-kernel boundary does *not* close for code an agent wrote.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
Maintained by [SOKOLAI BV](https://www.sokol.ai).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# maf-sandbox
|
|
2
|
+
|
|
3
|
+
> **Experimental.** This package is early-stage (`0.1.0`, `Development Status :: 4 - Beta`) — its API may change or be removed in a future release without notice. Importing it emits a one-time `MafSandboxExperimentalWarning`; suppress it with `warnings.filterwarnings("ignore", category=maf_sandbox.MafSandboxExperimentalWarning)` once you've read the notice.
|
|
4
|
+
|
|
5
|
+
This package is not affiliated with, endorsed by, or a product of Microsoft — it is a third-party reference implementation of [microsoft/agent-framework#7568](https://github.com/microsoft/agent-framework/issues/7568), written for use with [Microsoft Agent Framework](https://aka.ms/AgentFramework) but with no dependency on it in its protocol layer.
|
|
6
|
+
|
|
7
|
+
## Quickstart
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install maf-sandbox
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from maf_sandbox import Isolation, SandboxKey, SandboxRouter, SandboxSpec, WorkspaceContext
|
|
15
|
+
|
|
16
|
+
# Implement SandboxBackend against your own provider — or install maf-sandbox-aca for a
|
|
17
|
+
# ready-made Azure Container Apps Sandboxes backend — then wire it into a router:
|
|
18
|
+
router = SandboxRouter([my_backend], deployed=False)
|
|
19
|
+
sandbox = await router.acquire(SandboxKey(scope="tenant-1", thread_id="t-1", agent_dir="devops"), SandboxSpec(kind="bicep", image="bicep-sandbox:0.46.1", egress_allow=("mcr.microsoft.com",), work_dir="/workspace"))
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Threat model
|
|
23
|
+
|
|
24
|
+
This package draws no isolation boundary itself — it is protocol and policy over whatever a `SandboxBackend` implementation actually provides. `Isolation` states three tiers a backend can declare, from strongest to weakest: `vm` (a VM boundary — the whole guest, not just a process, is untrusted), `container` (a shared-kernel boundary), and `process` (no boundary beyond the OS's own process isolation). `SandboxRouter` enforces the one rule below on top of that declaration; the package's job is to make an unsafe backend selection fail loudly at construction, not silently at first use. Beyond backend selection, this layer has nothing else to get wrong: it holds no credentials, executes nothing, and reaches no network — everything security-relevant about a *specific* sandbox lives in the backend that implements it.
|
|
25
|
+
|
|
26
|
+
## The vocabulary
|
|
27
|
+
|
|
28
|
+
| | |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `SandboxKey` | `(scope, thread_id, agent_dir)` — the one sandbox a caller may reach |
|
|
31
|
+
| `SandboxSpec` | what a sandbox of a given *kind* needs: image, egress allowlist, work dir |
|
|
32
|
+
| `Sandbox` | `write_file` + `exec` — all a workload gets |
|
|
33
|
+
| `SandboxBackend` | `acquire` / `dispose` / `dispose_scope` |
|
|
34
|
+
| `SandboxRouter` | picks the backend, enforces the deployed rule |
|
|
35
|
+
| `SandboxPurger` | duck-typed `purge_scoped_thread(scope, thread_id)` for a host's delete path |
|
|
36
|
+
|
|
37
|
+
`SandboxKey`'s scope and thread come from the host's request context through `WorkspaceContext`, whose fields are **callables read at call time** rather than values. That is deliberate: a key a caller can supply is a key a *model* can supply, and that would let one conversation address another's sandbox.
|
|
38
|
+
|
|
39
|
+
`SandboxSpec.egress_allow` is an allowlist — everything not named is denied, so an empty tuple means no network. Stating it positively means a spec that forgets to mention egress gets the closed configuration rather than the open one.
|
|
40
|
+
|
|
41
|
+
## The one rule that is not a convenience
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
DEPLOYED_ISOLATION = frozenset({Isolation.VM})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A backend declares its own `isolation` (`vm` / `container` / `process`). When the host reports it is running **deployed**, the router refuses to select anything weaker than a VM boundary — raising `SandboxBackendNotPermitted` at construction, not at first use, so a misconfigured deployment cannot start with the feature apparently enabled and quietly unsafe.
|
|
48
|
+
|
|
49
|
+
It refuses rather than degrades. Falling back to a stronger backend would hide a misconfiguration; proceeding with the weaker one would break claims the host's security posture makes about every execution surface. Neither is better than an error.
|
|
50
|
+
|
|
51
|
+
A hardened container runtime (gVisor, Kata, Firecracker) is deliberately *not* in the permitted set. Admitting one is a decision for whoever owns those posture claims, taken there first.
|
|
52
|
+
|
|
53
|
+
## Writing a backend
|
|
54
|
+
|
|
55
|
+
Implement `name`, `isolation`, `acquire`, `dispose`, `dispose_scope`. Two things worth knowing before you start:
|
|
56
|
+
|
|
57
|
+
**`acquire` is get-or-create.** A workload's fix-round loop calls it every iteration; returning a cold sandbox each time turns a seconds-long loop into a minutes-long one.
|
|
58
|
+
|
|
59
|
+
**`dispose_scope` must not consult only your process's memory.** A multi-replica host serves a conversation delete wherever it lands, so the replica that created the sandbox is usually not the one deleting it. Derive the set from the service — labels, a listing, whatever your provider offers. A backend that skips this leaves billable compute running and the bug is invisible on a single-replica dev box.
|
|
60
|
+
|
|
61
|
+
Both `dispose` methods are best-effort by contract: purge must never fail a delete.
|
|
62
|
+
|
|
63
|
+
## Provenance
|
|
64
|
+
|
|
65
|
+
Extracted from a production agent application, where this seam was written for its first execution surface: a tool that compiles agent-authored infrastructure code in a sandbox. The deployed-isolation rule above is not a preference — it is what a security review concluded when it worked through what a shared-kernel boundary does *not* close for code an agent wrote.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
Maintained by [SOKOLAI BV](https://www.sokol.ai).
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "maf-sandbox"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "One seam between an application and any sandbox provider, for Microsoft Agent Framework — reference implementation of agent-framework#7568: backend protocol, selection, and the rule that a weaker isolation boundary is never used in a deployed environment."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12,<3.15"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"agent-framework",
|
|
11
|
+
"microsoft-agent-framework",
|
|
12
|
+
"sandbox",
|
|
13
|
+
"isolation",
|
|
14
|
+
"ai-agents",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["agent-framework-core>=1.13.0,<2"]
|
|
27
|
+
|
|
28
|
+
[[project.authors]]
|
|
29
|
+
name = "SOKOLAI BV"
|
|
30
|
+
email = "info@sokolai.com"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://www.sokol.ai"
|
|
34
|
+
Source = "https://github.com/sokolaidev/maf-extensions"
|
|
35
|
+
Issues = "https://github.com/sokolaidev/maf-extensions/issues"
|
|
36
|
+
|
|
37
|
+
[tool.uv.build-backend]
|
|
38
|
+
module-name = "maf_sandbox"
|
|
39
|
+
module-root = "src"
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
line-length = 100
|
|
43
|
+
target-version = "py312"
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
extend-select = [
|
|
47
|
+
"I",
|
|
48
|
+
"UP",
|
|
49
|
+
"D100",
|
|
50
|
+
"D101",
|
|
51
|
+
"D103",
|
|
52
|
+
"D104",
|
|
53
|
+
]
|
|
54
|
+
ignore = ["UP037"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint.per-file-ignores]
|
|
57
|
+
"tests/**" = [
|
|
58
|
+
"D101",
|
|
59
|
+
"D103",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.pyright]
|
|
63
|
+
include = ["src"]
|
|
64
|
+
typeCheckingMode = "strict"
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
testpaths = ["tests"]
|
|
68
|
+
|
|
69
|
+
[build-system]
|
|
70
|
+
requires = ["uv_build>=0.11.24,<0.12.0"]
|
|
71
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "maf-sandbox"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "One seam between an application and any sandbox provider, for Microsoft Agent Framework — reference implementation of agent-framework#7568: backend protocol, selection, and the rule that a weaker isolation boundary is never used in a deployed environment."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12,<3.15"
|
|
7
|
+
authors = [{ name = "SOKOLAI BV", email = "info@sokolai.com" }]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
10
|
+
keywords = [
|
|
11
|
+
"agent-framework",
|
|
12
|
+
"microsoft-agent-framework",
|
|
13
|
+
"sandbox",
|
|
14
|
+
"isolation",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
]
|
|
17
|
+
# No `License ::` classifier: PEP 639 deprecated them in favour of the `license`
|
|
18
|
+
# expression above, and carrying both is the combination build tools are allowed to reject.
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
# The protocol modules (`_protocol`, `_router`, `_purger`) stay import-clean of everything
|
|
30
|
+
# but the standard library — giving THEM a backend dependency, or a MAF one, would make this
|
|
31
|
+
# layer the thing it exists to keep apart. `TestZeroDependencies` and `TestOnlyDeclaredDependencies`
|
|
32
|
+
# (tests/test_sandbox_router.py) pin that at the source-scan level, which is a stronger and
|
|
33
|
+
# more honest guarantee than a dependency list, since a stray import would still resolve
|
|
34
|
+
# fine in this workspace with every other member already on the path.
|
|
35
|
+
#
|
|
36
|
+
# The dist-level dependency below exists for a different module: `maf_sandbox.maf`, the
|
|
37
|
+
# MAF-glue module this package's dist ships alongside the protocol. Only that module may
|
|
38
|
+
# import `agent_framework`; the boundary test's zero-dependency claim narrows to name the
|
|
39
|
+
# protocol modules explicitly rather than the whole distribution once that module lands.
|
|
40
|
+
dependencies = [
|
|
41
|
+
"agent-framework-core>=1.13.0,<2",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://www.sokol.ai"
|
|
46
|
+
Source = "https://github.com/sokolaidev/maf-extensions"
|
|
47
|
+
Issues = "https://github.com/sokolaidev/maf-extensions/issues"
|
|
48
|
+
|
|
49
|
+
# No [tool.uv.sources] here, deliberately: this repository resolves agent-framework-core
|
|
50
|
+
# from PyPI at the released range declared above — the same artifact every consumer of the
|
|
51
|
+
# published wheel gets. (The host application these packages were extracted from pins MAF
|
|
52
|
+
# from git `main` on its own side; that pin stayed there when the packages moved here.)
|
|
53
|
+
|
|
54
|
+
[tool.uv.build-backend]
|
|
55
|
+
module-name = "maf_sandbox"
|
|
56
|
+
module-root = "src"
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 100
|
|
60
|
+
target-version = "py312"
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
extend-select = ["I", "UP", "D100", "D101", "D103", "D104"]
|
|
64
|
+
ignore = ["UP037"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.per-file-ignores]
|
|
67
|
+
# ruff resolves [tool.ruff] per file by the NEAREST ancestor pyproject.toml that has one —
|
|
68
|
+
# once this package gained its own [tool.ruff] section, the host application's root
|
|
69
|
+
# pyproject.toml per-file-ignore entry for this package's tests stopped reaching these
|
|
70
|
+
# files, so this package carries its own copy.
|
|
71
|
+
"tests/**" = ["D101", "D103"]
|
|
72
|
+
|
|
73
|
+
# Self-contained type checking, scoped to this package — strict, unlike the host
|
|
74
|
+
# application's root config, which runs its own type checker in basic mode: this is new,
|
|
75
|
+
# purpose-built code with no legacy baseline to carry. `tests/` is out for the same reason
|
|
76
|
+
# the root config leaves tests out everywhere else in that repo: fixtures and hand-rolled
|
|
77
|
+
# fakes are not where a strict checker's objections are signal.
|
|
78
|
+
[tool.pyright]
|
|
79
|
+
include = ["src"]
|
|
80
|
+
typeCheckingMode = "strict"
|
|
81
|
+
|
|
82
|
+
[tool.pytest.ini_options]
|
|
83
|
+
testpaths = ["tests"]
|
|
84
|
+
|
|
85
|
+
[build-system]
|
|
86
|
+
requires = ["uv_build>=0.11.24,<0.12.0"]
|
|
87
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Sandbox router: one seam between a host application and any sandbox provider.
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
app -> SandboxRouter -> backend -> the sandbox
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
A workload asks for a sandbox and runs a command in it. A backend decides what actually
|
|
8
|
+
boots — an ACA Sandbox (`maf-sandbox-aca`) today, a local Docker container or an
|
|
9
|
+
in-process fake later. Neither knows about the other, which is what lets the same tool run
|
|
10
|
+
against all of them unchanged.
|
|
11
|
+
|
|
12
|
+
The router exists for two things a backend cannot own:
|
|
13
|
+
|
|
14
|
+
- **Which backend serves a request.** Configuration, not an import, decides.
|
|
15
|
+
- **The deployed-isolation rule.** A backend weaker than a VM boundary is refused outright
|
|
16
|
+
when the host reports it is running deployed — see
|
|
17
|
+
:class:`~maf_sandbox._router.SandboxBackendNotPermitted`. This is the router's one part
|
|
18
|
+
that is a security property rather than a convenience, so it is enforced at construction
|
|
19
|
+
and pinned by tests.
|
|
20
|
+
|
|
21
|
+
This package imports no backend and no host application.
|
|
22
|
+
|
|
23
|
+
One module sits deliberately outside that claim and is deliberately not re-exported here:
|
|
24
|
+
:mod:`maf_sandbox.maf`, the MAF glue (``make_workspace_context``, ``sandboxed_tool``, and the
|
|
25
|
+
purge participant). It is the only module allowed to import ``agent_framework``, and keeping
|
|
26
|
+
it off this ``__init__`` is what lets ``import maf_sandbox`` stay cheap and framework-free for
|
|
27
|
+
a backend or a test that only speaks the protocol. Reach it by name —
|
|
28
|
+
``from maf_sandbox.maf import sandboxed_tool``.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from ._error_detail import error_detail
|
|
34
|
+
from ._protocol import (
|
|
35
|
+
ExecResult,
|
|
36
|
+
Isolation,
|
|
37
|
+
Sandbox,
|
|
38
|
+
SandboxBackend,
|
|
39
|
+
SandboxKey,
|
|
40
|
+
SandboxSpec,
|
|
41
|
+
WorkspaceContext,
|
|
42
|
+
)
|
|
43
|
+
from ._purger import SandboxPurger
|
|
44
|
+
from ._router import (
|
|
45
|
+
DEPLOYED_ISOLATION,
|
|
46
|
+
NoSandboxBackend,
|
|
47
|
+
SandboxBackendNotPermitted,
|
|
48
|
+
SandboxRouter,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"DEPLOYED_ISOLATION",
|
|
53
|
+
"ExecResult",
|
|
54
|
+
"Isolation",
|
|
55
|
+
"MafSandboxExperimentalWarning",
|
|
56
|
+
"NoSandboxBackend",
|
|
57
|
+
"Sandbox",
|
|
58
|
+
"SandboxBackend",
|
|
59
|
+
"SandboxBackendNotPermitted",
|
|
60
|
+
"SandboxKey",
|
|
61
|
+
"SandboxPurger",
|
|
62
|
+
"SandboxRouter",
|
|
63
|
+
"SandboxSpec",
|
|
64
|
+
"WorkspaceContext",
|
|
65
|
+
"error_detail",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# --- Experimental-package notice ---------------------------------------------------------
|
|
69
|
+
# This package is early-stage (0.1.0, "Development Status :: 4 - Beta"). Mirrors
|
|
70
|
+
# `agent_framework`'s own experimental-feature idiom (see its `_feature_stage` module and
|
|
71
|
+
# `ExperimentalWarning`, a `FutureWarning` subclass) but deliberately subclasses
|
|
72
|
+
# `UserWarning` instead: a host that runs under `python -W error` (many CI/production
|
|
73
|
+
# launchers do) would have importing this package alone raise before any of its own code
|
|
74
|
+
# runs if the category were a `FutureWarning`. `UserWarning` keeps the notice
|
|
75
|
+
# informational-by-default while staying a real, catchable, filterwarnings-suppressible
|
|
76
|
+
# category — see the try/except immediately below for how `-W error` is handled anyway.
|
|
77
|
+
#
|
|
78
|
+
# Duplicated (not imported from a shared module) in each of the three maf-sandbox*
|
|
79
|
+
# packages on purpose — a shared warnings module would be a cross-package dependency this
|
|
80
|
+
# split is designed to avoid.
|
|
81
|
+
import warnings as _warnings
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MafSandboxExperimentalWarning(UserWarning):
|
|
85
|
+
"""Warning category for maf-sandbox's experimental-package notice."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _warn_experimental() -> None:
|
|
89
|
+
message = (
|
|
90
|
+
"maf_sandbox is experimental and may change or be removed in future versions "
|
|
91
|
+
"without notice."
|
|
92
|
+
)
|
|
93
|
+
try:
|
|
94
|
+
_warnings.warn(message, category=MafSandboxExperimentalWarning, stacklevel=2)
|
|
95
|
+
except MafSandboxExperimentalWarning:
|
|
96
|
+
# A host running under `python -W error` (or with a blanket
|
|
97
|
+
# `filterwarnings("error")` active) turns the warning above into an exception at
|
|
98
|
+
# the call site. Importing a package must never fail because of an informational
|
|
99
|
+
# notice, so it is swallowed here — this is the one piece of state a `-W error`
|
|
100
|
+
# host is allowed to change: whether the notice was printed, never whether the
|
|
101
|
+
# import succeeded.
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
_warn_experimental()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""``error_detail``: as much of a provider failure as a log can usefully carry.
|
|
2
|
+
|
|
3
|
+
``str()`` on an azure-core ``HttpResponseError`` is just
|
|
4
|
+
``Operation returned an invalid status 'Bad Request'`` — the *reason* is in the response
|
|
5
|
+
body, which that string drops. A 400 that says only "Bad Request" cannot be acted on: it
|
|
6
|
+
took a hand-written probe against the live service to discover that one such failure meant
|
|
7
|
+
the app's identity had no role on the sandbox group.
|
|
8
|
+
|
|
9
|
+
This started life inside the bicep kind, the only caller that needed it. The ACA backend's
|
|
10
|
+
own warning logs have the identical gap — a bare ``%s`` of the exception, dropping the same
|
|
11
|
+
response body — so it moved here where every backend and every kind can reach it, rather than
|
|
12
|
+
being copied.
|
|
13
|
+
|
|
14
|
+
Duck-typed on purpose, and stdlib-only: it reads ``status_code`` and ``response.text()`` off
|
|
15
|
+
whatever it is given, which is how it works against azure-core's ``HttpResponseError`` — or
|
|
16
|
+
any other SDK's exception shaped the same way — without importing it. This is a log-only
|
|
17
|
+
utility; the caller decides separately what a model or end user is told, and that message
|
|
18
|
+
must stay sanitized regardless of what this function returns.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
__all__ = ["error_detail"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def error_detail(exc: Exception) -> str:
|
|
27
|
+
"""As much of a failure as the log can usefully carry.
|
|
28
|
+
|
|
29
|
+
``str()`` on an azure-core ``HttpResponseError`` is just
|
|
30
|
+
``Operation returned an invalid status 'Bad Request'`` — the *reason* is in the response
|
|
31
|
+
body, which that string drops. A 400 that says only "Bad Request" cannot be acted on:
|
|
32
|
+
it took a hand-written probe against the live service to discover that one such failure
|
|
33
|
+
meant the app's identity had no role on the sandbox group. This is log-only; the model
|
|
34
|
+
still sees the sanitized message.
|
|
35
|
+
"""
|
|
36
|
+
parts = [f"{type(exc).__name__}: {exc}"]
|
|
37
|
+
status = getattr(exc, "status_code", None)
|
|
38
|
+
if status is not None:
|
|
39
|
+
parts.append(f"status={status}")
|
|
40
|
+
response = getattr(exc, "response", None)
|
|
41
|
+
if response is not None:
|
|
42
|
+
try:
|
|
43
|
+
body = response.text()
|
|
44
|
+
except Exception: # noqa: BLE001 - diagnostics must not raise
|
|
45
|
+
body = None
|
|
46
|
+
if body:
|
|
47
|
+
parts.append(f"body={body[:600]}")
|
|
48
|
+
return " | ".join(parts)
|