Metadata-Version: 2.5
Name: aice-sandbox
Version: 0.2.0
Summary: Python SDK for the AICE Sandbox Service
Author-email: Adanian Labs <engineering@adanianlabs.io>
License: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: aice-core<0.5,>=0.3.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Description-Content-Type: text/markdown

# aice-sandbox

Python SDK for the AICE Sandbox Service: isolated gVisor sessions an agent can run
code in, and links to the files it produces. Same auth, transport and error model as every other AICE SDK (`aice-core`).

## Install

Not on PyPI yet. From this workspace:

```bash
pip install -e ../core -e .
```

## Quick start

```python
from aice_sandbox import SandboxClient

client = SandboxClient(
    base_url="http://localhost:8080",
    api_key=PROJECT_KEY,          # console project key; its aud must include "sandbox"
    user_id="3fa85f64-5717-4562-b3fc-2c963f66afa6",   # the user you act for
)

session = client.session.create()                  # tenant comes from the key
result = session.exec(["python3", "-c", "print(6 * 7)"])
print(result.stdout)  # "42"

session.write("data.txt", b"some content")
content = session.read("data.txt")
files = session.list()
session.destroy()
```

Act for a different user on one call with `aice_core.acting_as(user_id)`.
A session created under one project's key is invisible (404) to every other
project's key.

`session.create(manifest={"network": "install_only"})` allows `session.install([...])`
through the service's egress proxy; the default session has no network.

## Getting files out: artifacts

A session's files live in a tmpfs that disappears with the session. To hand one
to a user, publish it: the service copies it to its artifact store (MinIO/S3)
and returns a presigned link.

```python
session.exec(["python3", "-c",
              "import matplotlib.pyplot as plt; plt.plot([1, 3, 2]); plt.savefig('chart.png')"])
art = session.publish("chart.png")
art.url         # presigned GET link, valid until art.expires_at (1 h by default)
art.mime        # "image/png"
art.artifact_id # keep this, not the url

# Later, after the link expired or the session is gone:
art = client.artifacts.url(artifact_id)
```

Any file type works, up to the service's size limit (25 MB by default). The
artifact belongs to the acting user: another user, or another project's key,
gets `ArtifactNotFoundError`. `client.artifacts.url` also works for files an
agent published with the sandbox MCP tool `publish_file`.

`mime` tells a frontend how to show the file. Images (`image/png`, `image/jpeg`,
…) go in an `<img>`. `application/vnd.plotly.v1+json` (a `*.plotly.json` file
written with `fig.to_json()`) is an interactive chart: fetch it and call
`Plotly.newPlot(el, spec.data, spec.layout)`. Anything else is a download named
`filename`. The service's README ("Artifacts") has the full table. The sandbox
image preinstalls numpy, pandas, matplotlib and plotly.

Project admins (`user_role="admin"`, `"owner"` or `"service_role"`) can read and change
their own project's container mode:

```python
client.tenant_config.update(project_id, {"container_sharing": "shared"})
```

## Errors

Every exception derives from `SandboxSDKError` (itself an `aice_core.AiceError`):

| Exception | When |
|---|---|
| `AuthenticationError` | 401: key missing, invalid, expired or not issued for `sandbox`; or no `user_id` |
| `ForbiddenError` | 403: e.g. tenant config without an admin role, or for another project |
| `NetworkNotPermittedError` | 403 from `install()`: the session's manifest grants no network |
| `SessionNotFoundError` | 404: the session never existed, was destroyed, or belongs to another project |
| `SandboxFileNotFoundError` | 404 from `read()` or `publish()`: no such file in the session |
| `ArtifactNotFoundError` | 404 from `artifacts.url()`: no such artifact for this project and user |
| `QuotaExceededError` | 429: `.scope` is `"user"`, `"tenant"` or `"global"`, `.limit` the cap |

On a service with no artifact store, `publish()` and `artifacts.url()` raise
`SandboxSDKError` with `.code == "artifacts_disabled"`.

## Testing

```bash
pytest
```

Uses `respx` to stub the HTTP layer. End-to-end coverage against a real service is in
the sandbox service repo's own suites, which use this package.
