Metadata-Version: 2.4
Name: maf-sandbox-wslc
Version: 0.25.0
Summary: WSL containers (wslc) as a developer-machine sandbox backend for Microsoft Agent Framework agents.
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.43.0,<0.44
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

[![PyPI](https://img.shields.io/pypi/v/maf-sandbox-wslc)](https://pypi.org/project/maf-sandbox-wslc/) [![Python](https://img.shields.io/pypi/pyversions/maf-sandbox-wslc)](https://pypi.org/project/maf-sandbox-wslc/) [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/sokolaidev/maf-extensions/blob/main/LICENSE)

> **Experimental.** Releases before 1.0 may change or remove APIs. Importing this package emits `MafSandboxWslcExperimentalWarning`.

Run sandbox commands in Linux containers managed by `wslc`, the container CLI included with WSL. This backend transfers input files and returns command output. It has no Azure dependency.

This is an independent package, not a Microsoft product.

## Quickstart

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

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

backend = WslcSandboxBackend(WslcSandboxConfig())
router = SandboxRouter([backend], min_isolation=Isolation.CONTAINER)
```

Containers share the WSL kernel. The explicit minimum permits container isolation, below the router's default microVM minimum.

Use Windows with WSL 2.9.3 or later. `wsl --version` reports the installed version. The backend's CLI contract has been measured on WSLC 2.9.4.0 and 2.9.12.0.

The Python event loop must support subprocesses. Windows' default Proactor loop does; `WindowsSelectorEventLoopPolicy` does not.

See the [Bicep sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/02_wslc_bicep) or [CodeAct sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/04_wslc_codeact) for complete applications.

## Supported operations

| Setting | Behavior |
|---|---|
| Isolation | `CONTAINER` |
| Capabilities | `EXEC`, `FILES_IN` |
| Guest OS | POSIX |
| Network | `CLOSED`; `ALLOWLIST` with a configured proxy |
| Lifetime | Conversation or separate sandbox per call |
| Transfer ceiling | 8 MiB per file, 32 MiB total, 64 files per direction |
| Cleanup | Disposal; no reclaim or snapshot reset |

Output reads, directory listing, file deletion, runtime `run_code` and host-tool calls are unavailable. A kind requiring one is refused before attachment.

Acquisition checks `sh` for commands. Input transfer also needs the external `/usr/bin/test` command, and `sh` plus `mkdir`, `cat`, `wc`, `mv` and `rm` for the image user: its probe and every write run through that user's shell. A *resolved* image user is needed only where a base has to be created, because a write runs as that user and stamps nothing. Every acquire still asks, though: `id -u` always, and `id -g` when the image names its user rather than numbering it. Working-directory setup needs root `/bin/sh`, `mkdir`, `chown` and `ls` on the pinned system `PATH`, `pwd` as a builtin or command, and the image user's `mkdir` where it leaves the base to that user, checked when a base has to be created rather than at acquisition, so an image whose base already exists needs none of them. Both acquisition and root path probes invoke that absolute executable, without searching the guest's `PATH`. Failed prerequisite checks are retryable; another `test` on `PATH` is not a fallback. The image must protect `/usr/bin/test`, its dependencies and ancestor directories from the runtime user.

## Input files and their limits

Acquisition prepares the storage base for workloads using commands or files. `work_dir=None` selects `/maf-sandbox/work`; an explicit path requests that exact base. Existing directories keep their contents, ownership and modes.

A missing base is created as root only where every directory root acts inside is root's and writable by nobody else, since nothing there can be swapped; the base then goes to the image user through a held `chown`. Anywhere else — under a directory the image user owns, or under `/tmp` — the image user creates it with `mkdir -p`, and a base that user cannot create raises `PermissionError`. A base that was already there keeps its owner, whichever path named it: `acquire` preserves the ownership it finds. An unset `Config.User` means root. Named users or an omitted group need working `id` commands. An `id` that answers something unusable leaves ownership unresolved; one that does not answer at all discards the container, because it may still be running there. Unresolved ownership refuses any capability that has to create a base, `EXEC` and `FILES_IN` alike, before anything is created; an existing base needs no *resolved* identity at all.

**Writes run as the image user.** The file and any missing parents belong to that user. A destination it cannot write raises `PermissionError`; nothing falls back to root. The path check is separate from the write, so a guest can swap a checked parent for a symlink first. The write then reaches only what the image user could write anyway. A write this host stops discards the container, as `exec` does — an expired deadline, or stdout reaching the read cap.

Setup is bounded the same way. Root holds each directory it enters, confirms it with `pwd -P` and has `CDPATH` cleared, and acts only where no swap can reach; where one could, the image user creates the base. Cancelling a write after it starts is not a rollback: short content is refused, but content that fully arrived still lands.

Some path classification runs inside the guest, as root, with the image's `test`. Its answer can pick which refusal a caller sees. A write it lets through still runs as the image user.

Path inspection can also copy an existing guest file into a private host temporary directory. **Its disk use is not bounded by input limits or stdout limits.** Normal exits remove the temporary copy, but a host crash or cleanup failure can leave it behind. Use an enforced temporary-filesystem quota when that consumption is unacceptable.

See the [backend guide](https://github.com/sokolaidev/maf-extensions/blob/main/docs/sandbox/backends/wslc.md) for the precise file contract and remaining limits.

## Network access

Without a proxy, only `Egress.CLOSED` is supported. Containers use `--network none`, and an allowlist request is refused.

Build the packaged proxy and select it in configuration:

```python
from maf_sandbox_wslc import WslcSandboxConfig, proxy_build_context

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

Each allowlisted sandbox gets an internal network and a filtering proxy. The proxy is its only route out. The packaged image builds pinned iron-proxy with a policy patch. It terminates guest TLS to enforce host, method and path rules, validates upstream certificates, and supplies a per-sandbox CA certificate at a fixed readable guest path independent of the workload's work directory. Its CA key stays in the proxy. Clients must honor the injected CA environment or configure trust explicitly. Public destinations require TLS on every port. Private destinations require TLS by default; `WslcSandboxConfig(allow_private_http=True, egress_proxy_image=...)` permits plaintext only when the listed host resolves to a private address. Use that option only for development or test workloads. Unrestricted access remains unavailable.

The router's observer can receive proxy decisions after confirmed removal. Failed removal can leave a window unreported. See [observability](https://github.com/sokolaidev/maf-extensions/blob/main/docs/sandbox/observability.md).

<a id="operator-retention"></a>

## Cleanup and retention

Router-managed calls dispose their sandbox even when a kind claims to keep changes inside its call directory. WSLC cannot establish the ancestry needed for privileged reclaim and declares no snapshot reset.

`dispose(key, kind=...)` removes a selected kind and its infrastructure. `dispose_scope(scope, thread_id)` discovers conversation resources through engine labels, not only process memory.

An operator can clean up stopped workloads and orphan infrastructure:

```python
from datetime import timedelta

result = await backend.reap(timedelta(hours=24), scope="my-app")
print(result.disposed, result.proxies_removed, result.networks_removed)
for failure in result.failures:
    print(failure)
```

Run this under the Windows account that owns the WSLC engine. The backend starts no timer. An external scheduler must select the environment, prevent overlapping sweeps and monitor failures.

Pause and drain acquisition, restarts and other resource changes in the selected scope before sweeping. Engine rechecks cannot make name-based network deletion atomic. Keep Windows and WSL clocks synchronized.

| Resource | Age used for retention |
|---|---|
| Stopped workload | Time since its inspected stop; a restart and stop resets it. |
| Never-started workload | Creation time. |
| Proxy without a workload | Creation time, including for a running orphan proxy. |
| Network alone | Its backend creation-time label; missing labels retain it for manual handling. |

Running or uncertain workload states retain their whole group. Resources exactly at the cutoff are retained. An incomplete inventory prevents deletion; individual failures are reported. Retention does not infer that a host has died or impose a maximum running lifetime.

See [operations](https://github.com/sokolaidev/maf-extensions/blob/main/docs/sandbox/operations.md) for scheduling and cleanup ownership. The [live suite](https://github.com/sokolaidev/maf-extensions/blob/main/packages/maf-sandbox-wslc/tests/test_wslc_e2e.py) covers writes, networks and separate-process retention using explicitly configured images.

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