Metadata-Version: 2.4
Name: maf-sandbox-wslc
Version: 0.2.0
Summary: WSL containers (wslc) as a developer-machine sandbox backend for Microsoft Agent Framework agents — reference implementation of agent-framework#7568.
Keywords: wsl,containers,sandbox,agent-framework,microsoft-agent-framework,isolation
Author: SOKOLAI BV
Author-email: SOKOLAI BV <info@sokolai.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Typing :: Typed
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: Microsoft :: Windows
Classifier: Topic :: Software Development :: Testing
Requires-Dist: maf-sandbox>=0.2.0,<0.3
Requires-Python: >=3.12, <3.15
Project-URL: Homepage, https://www.sokol.ai
Project-URL: Source, https://github.com/sokolaidev/maf-extensions
Project-URL: Issues, https://github.com/sokolaidev/maf-extensions/issues
Description-Content-Type: text/markdown

# maf-sandbox-wslc

> **Experimental.** This package is early-stage (pre-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 `MafSandboxWslcExperimentalWarning`; suppress it with `warnings.filterwarnings("ignore", category=maf_sandbox_wslc.MafSandboxWslcExperimentalWarning)` once you've read the notice.

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) for [Microsoft Agent Framework](https://aka.ms/AgentFramework).

```
app  ->  maf_sandbox  ->  maf_sandbox_wslc  ->  the container
```

The developer-machine sandbox backend: a container created by `wslc`, the container CLI that ships with WSL, in about half a second — no subscription, no daemon, no login, and no dependency but [`maf-sandbox`](https://github.com/sokolaidev/maf-extensions/tree/main/packages/maf-sandbox) itself. A workload written against the protocol runs here unchanged, which is what makes it a workload rather than an integration.

## Quickstart

```bash
pip install maf-sandbox-wslc
```

```python
from maf_sandbox import SandboxRouter
from maf_sandbox_wslc import WslcSandboxBackend, WslcSandboxConfig

router = SandboxRouter([WslcSandboxBackend(WslcSandboxConfig())])
```

[`samples/02_wslc_bicep`](https://github.com/sokolaidev/maf-extensions/tree/main/samples/02_wslc_bicep) runs those two lines end to end: a one-turn agent that validates a Bicep file against the compiler and takes the container down afterwards. Its sibling `samples/01_acas_bicep` is the same program on a VM-isolated Azure backend, and the diff between them is two imports and one constructor.

## Requirements

**Windows with WSL 2.9.3 or later.** `wslc` is part of WSL; `wsl --version` reports the version and `wsl --update` moves it forward. There is nothing else to install. The command-line contract this backend depends on — argv passed to `exec` natively, `cp` from a tar on stdin, label filters on `list`, `WSLC_E_*` codes on stderr — was verified against **wslc 2.9.4.0**. Every call spawns `wslc.exe`, so the host's event loop has to be one that can start subprocesses — asyncio's default Proactor loop on Windows does, and a host that installs `WindowsSelectorEventLoopPolicy` has to undo that first, or every acquire fails with a message saying so.

## What this backend declares

**`Isolation.CONTAINER`.** A container shares the host kernel and sits next to whatever the host process holds, so `SandboxRouter(..., deployed=True)` refuses this backend outright, at construction. That refusal is the feature: this is a backend for the machine you are already sitting at, and the router will not be argued into treating it as anything else. Use a VM-isolated backend where a deployment's credentials are in the picture.

**`Egress.CLOSED` by default, `Egress.ALLOWLIST` on request.** With no proxy configured every container is created `--network none`: the CLI cannot allow one host and deny the rest, so a spec's allowlist is honoured by denying everything — confining *more* than a workload asked for, which the router permits with a warning precisely because the failure is loud, and a workload built for this reports the shortfall rather than passing an incomplete result off as a clean one.

Set `egress_proxy_image` and the declaration becomes `ALLOWLIST`: each sandbox gets its own internal network and a dual-homed filtering proxy, and the spec's allowlist is enforced by topology — the container has no route out except the proxy, which opens a CONNECT tunnel only to the hosts the spec names. TLS is not decrypted, and the sandbox never resolves an external name itself. The proxy is shipped as source, not as an image you must trust: build it from the packaged recipe, whose only pinned dependency is its Azure Linux base.

```python
from pathlib import Path
from maf_sandbox_wslc import proxy_build_context, WslcSandboxConfig

print(f"wslc build -t maf-egress-proxy:local {proxy_build_context()}")  # run this once
config = WslcSandboxConfig(egress_proxy_image="maf-egress-proxy:local")
```

## The backend

`WslcSandboxBackend` implements `maf_sandbox.SandboxBackend`:

| | |
|---|---|
| `acquire(key, spec)` | get-or-create, keyed `(scope, thread, agent)`. A running container is reused, a stopped one started, a missing one created — so a fix-round loop does not pay a cold start per iteration |
| `write_file(path, content)` | a one-entry tar on stdin to `cp - <container>:/`, which creates the parent directories from the entry name |
| `dispose(key)` | `remove -f` on the one container the key names |
| `dispose_scope(scope, thread)` | delete every container for a conversation — **by label, read back from wslc**, not from process memory |
| `isolation` | `container` — which is what makes the router refuse it in a deployed environment |
| `egress` | `closed`, or `allowlist` when `egress_proxy_image` is set — an internal network behind a filtering proxy, torn down with the sandbox |

Container names are derived from the key rather than remembered, so `acquire` and `dispose` agree on one without a registry to keep in sync. Labels are the durable record `dispose_scope` selects on, and their values are digested when they are long or carry a separator — the same mapping on both sides, because transforming one and not the other makes a purge quietly select nothing.

`stop` is never used. A container whose init process ignores `SIGTERM` takes ten seconds to stop and under a quarter of a second to remove, and there is nothing in a sandbox worth waiting for.

---

Maintained by [SOKOLAI BV](https://www.sokol.ai).
