Metadata-Version: 2.5
Name: langchain-acasandbox
Version: 0.1.0
Summary: Async Azure Container Apps Sandbox backend for LangChain Deep Agents
Project-URL: Homepage, https://github.com/danigian/langchain-acasandbox
Project-URL: Repository, https://github.com/danigian/langchain-acasandbox.git
Project-URL: Issues, https://github.com/danigian/langchain-acasandbox/issues
Author-email: Daniele Antonio Maggio <1955514+danigian@users.noreply.github.com>
Keywords: agents,azure,container-apps,deepagents,langchain,sandbox
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.13
Requires-Dist: azure-containerapps-sandbox==0.1.0b4
Requires-Dist: azure-core==1.41.0
Requires-Dist: azure-identity<2,>=1.25
Requires-Dist: deepagents==0.7.13
Requires-Dist: pydantic-settings<3,>=2.7
Requires-Dist: pydantic<3,>=2.12
Description-Content-Type: text/markdown

# langchain-acasandbox

`langchain-acasandbox` is an async Azure Container Apps Sandbox provider for LangChain Deep Agents. It provisions a restricted sandbox, exposes command and file operations through the Deep Agents `BaseSandbox` contract, and confirms deletion when the session ends.

The library is intentionally small. A complete hello-world Deep Agent lives in [samples/basic](samples/basic).

> The Azure Container Apps sandbox SDK is a preview dependency. Pin and test this library before using it in production.

## Requirements

- Python 3.13 or later
- An Azure subscription and tenant
- An existing Azure Container Apps sandbox group
- Azure CLI authentication for local development, or workload identity in Azure
- Permission to create, inspect, and delete sandboxes in the group

This project never accepts account keys or client secrets. Credentials stay in the host process and are not forwarded to sandbox commands.

## Installation

From PyPI after publication:

```sh
uv add langchain-acasandbox
```

From this checkout:

```sh
uv sync --locked
```

## Configuration

Create `SandboxConfig` directly when values already come from your application configuration:

```python
from langchain_acasandbox import SandboxConfig

config = SandboxConfig(
    subscription_id="00000000-0000-0000-0000-000000000000",
    resource_group="sandbox-rg",
    sandbox_group="agents",
    tenant_id="00000000-0000-0000-0000-000000000000",
    location="swedencentral",
)
```

Alternatively, `SandboxConfig.from_env()` reads these variables:

| Variable | Required | Default | Meaning |
|---|---:|---|---|
| `AZURE_SUBSCRIPTION_ID` | yes | | Azure subscription |
| `AZURE_TENANT_ID` | yes | | Microsoft Entra tenant |
| `SANDBOX_RESOURCE_GROUP` | yes | | Resource group containing the sandbox group |
| `SANDBOX_GROUP_NAME` | yes | | Sandbox group name |
| `SANDBOX_LOCATION` | no | `swedencentral` | Azure region |
| `AZURE_AUTH_MODE` | no | `cli` | `cli` or `workload` |
| `SANDBOX_DISK` | no | `ubuntu` | Base disk name when no disk ID is supplied |
| `SANDBOX_DISK_ID` | no | | Existing disk image ID |
| `SANDBOX_SNAPSHOT_ID` | no | | Existing snapshot ID; mutually exclusive with disk ID |
| `SANDBOX_IDLE_TIMEOUT_SECONDS` | no | `600` | Azure auto-suspend interval |
| `SANDBOX_AUTO_DELETE_SECONDS` | no | `172800` | Azure fallback auto-delete interval |

Local authentication uses the selected tenant:

```sh
az login --tenant "$AZURE_TENANT_ID"
```

## Basic Usage

Always open manager-owned sandboxes with `async with`. Exiting the block quarantines the backend, deletes the sandbox, and verifies that Azure reports it absent.

```python
import asyncio

from langchain_acasandbox import SandboxConfig, SandboxManager


async def main() -> None:
    manager = SandboxManager(SandboxConfig.from_env())

    async with manager.open() as session:
        result = await session.backend.aexecute("printf 'hello from Azure'", timeout=20)
        if result.exit_code != 0:
            raise RuntimeError(result.output)
        print(result.output)


asyncio.run(main())
```

The public backend methods are async-only:

```python
backend = session.backend

uploads = await backend.aupload_files([
    ("/workspace/input.txt", b"sandbox input\n"),
])
result = await backend.aexecute(
    "wc -c /workspace/input.txt > /workspace/count.txt",
    timeout=30,
)
downloads = await backend.adownload_files(["/workspace/count.txt"])

if uploads[0].error or result.exit_code or downloads[0].error:
    raise RuntimeError("Sandbox operation failed")

print(downloads[0].content.decode())
```

Paths must be absolute. File operations return one response per requested path, with a normalized `error` such as `file_not_found`, `invalid_path`, `is_directory`, or `permission_denied`.

## Deep Agents

Pass the backend directly to `create_deep_agent` and invoke the graph asynchronously:

```python
from deepagents import create_deep_agent
from langchain_openai import ChatOpenAI

from langchain_acasandbox import SandboxConfig, SandboxManager


async def run_agent() -> None:
    manager = SandboxManager(SandboxConfig.from_env())
    model = ChatOpenAI(model="your-model")

    async with manager.open() as session:
        agent = create_deep_agent(model=model, backend=session.backend)
        result = await agent.ainvoke({
            "messages": [{"role": "user", "content": "Create /workspace/result.txt"}]
        })
        print(result["messages"][-1].content)
```

The backend implements Deep Agents async execution and file-transfer methods. Its synchronous `execute`, `upload_files`, and `download_files` methods deliberately raise an error.

## Lifecycle And Failure Behavior

`SandboxManager.open()` creates a sandbox with:

- deny-by-default egress with full traffic inspection
- no ingress ports or connections
- auto-suspend after the configured idle interval
- Azure-side auto-delete as a fallback
- ownership and expiry labels used by the janitor
- a prepared `/workspace` directory

Operations are serialized per backend. The defaults allow a 60-second operation timeout, a 120-second maximum requested timeout, 1 MiB of captured command output, 10 MiB per file, and 100 files per batch.

An unexpected Azure error, cancellation, response-limit breach, or operation timeout quarantines the backend. Later operations fail with `SandboxUnavailable`; do not reuse a quarantined session. A timeout also means remote command termination is unconfirmed, so the manager proceeds to deletion.

Manager-owned sessions have a 30-minute maximum lifetime and a 10-minute inactivity lifetime. Cleanup is idempotent and deletion must be confirmed. Expiry-task cleanup failures are available as `session.cleanup_error`; explicit close failures are raised to the caller. Run the janitor after interrupted processes or uncertain cleanup.

If you instantiate `AzureContainerAppsSandbox` directly around an SDK `SandboxClient`, the adapter borrows that client. You own the client lifecycle and deletion of the Azure resource.

## Operator Commands

Commands use the same environment configuration:

```sh
uv run --env-file .env acasandbox smoke
uv run --env-file .env acasandbox janitor
uv run --env-file .env acasandbox inventory
uv run --env-file .env acasandbox image-import
```

- `smoke` provisions a sandbox, verifies required tools and binary file transfer, then deletes it.
- `janitor` deletes expired sandboxes carrying the library ownership label.
- `inventory` lists sandboxes and disk images in the configured group.
- `image-import` imports the Python 3.13 image pinned in [src/langchain_acasandbox/cli.py](src/langchain_acasandbox/cli.py). This creates a billable Azure resource.

## Development

```sh
make check
```

This runs Ruff, Pyright, and all non-live unit tests. Azure integration tests are opt-in and may incur charges:

```sh
RUN_SANDBOX_LIVE=1 uv run pytest tests/integration_tests -q -m live
```

Infrastructure is under [infra](infra). Start with the [basic hello-world agent](samples/basic).

## Publishing

Releases publish to PyPI through GitHub Actions trusted publishing. Configure a PyPI trusted publisher with:

| Setting | Value |
|---|---|
| Owner | `danigian` |
| Repository | `langchain-acasandbox` |
| Workflow | `publish.yml` |
| Environment | `pypi` |

No PyPI token is stored in GitHub. To publish, update the version in `pyproject.toml`, push `main`, and publish a GitHub release whose tag is exactly `v<version>`, such as `v0.1.0`. The workflow runs all checks, builds and validates both distributions, and publishes the tested artifacts only after the `pypi` environment gate.
