> ## Documentation Index
> Fetch the complete documentation index at: https://usefused.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandboxing

> Assign named Python execution providers to agents, then call them from your authored tools on ADK or LangGraph.

Use a sandbox when your authored Agent Tool needs to run Python outside the agent server. Define the provider, assign it to the agents that need it, and call it from the tool through `context.sandboxes`. The same API works with managed ADK and LangGraph.

```text theme={null}
Model → your business tool → assigned sandbox → result your tool returns
```

An assignment does not create a model-callable execution tool. You choose the business tool's inputs, the code it submits, and the result it returns. Only that submitted code runs in the sandbox; the surrounding tool function and the agent server do not.

## Enable container execution

From the agent folder, install the Fused-maintained Docker Harnest Extension, then resolve it into the agent's runtime lock:

```bash theme={null}
harnest extensions install docker
harnest env sync .
```

Create one file per sandbox, then choose which sandboxes each agent can use. For example, `sandbox/calculations.py` must define a variable named `calculations`. If initialization generated `sandbox/_example.py`, rename it to `calculations.py`; the example already uses that variable name. Underscore-prefixed examples remain ignored.

```python sandbox/calculations.py theme={null}
from harnest.extensions.docker import docker
from harnest.sandbox import SandboxNetworkPolicy


calculations = docker.sandbox(
    image="python:3.12-slim@sha256:<approved-digest>",
    network_policy=SandboxNetworkPolicy.none(),
    timeout_seconds=120,
    max_output_bytes=1_048_576,
)
```

Docker must be available when code executes. The extension owns the Docker SDK and provider implementation; Harnest core supplies the framework-neutral sandbox contract. Compilation and agent startup do not start a container, and there is no fallback to executing submitted code in the agent server. The extension declares `sandbox.provider`, creates no browser or model tool, and works for ordinary Python workloads on managed ADK and LangGraph.

### Assign it to your agent

Add `sandboxes=["calculations"]` to the existing `Agent(...)` declaration in `agent.py`. Keep its other settings, such as the model, unchanged. A minimal complete example is:

```python agent.py theme={null}
from harnest.agent import Agent
from harnest.model import LiteLLMModel

root_agent = Agent(
    name="support",
    model=LiteLLMModel.from_openai_environment(),
    sandboxes=["calculations"],
)
```

Discovering a sandbox file does not give any agent permission to use it. An agent with no `sandboxes` assignment has no named sandbox access. The same declarations and assignments work on ADK and LangGraph.

### Call it from a business tool

Create `tools/total_values.py`. Harnest exposes this tool to the model; the sandbox itself stays behind the tool:

```python tools/total_values.py theme={null}
import json

from harnest import context
from harnest.sandbox import SandboxStatus
from harnest.agent import tool


@tool
async def total_values(values: list[int]) -> dict:
    """Add up to 1,000 whole numbers and return their total."""
    if len(values) > 1_000 or any(type(value) is not int for value in values):
        return {"error": "Provide at most 1,000 whole numbers."}

    # Serialize data as a Python string literal, never as executable code.
    payload = json.dumps(values)
    code = f"import json\nvalues = json.loads({payload!r})\nprint(sum(values))"
    result = await context.sandboxes["calculations"].aexecute(code)
    if result.status != SandboxStatus.SUCCEEDED:
        return {"error": "The calculation failed. Try a smaller input."}
    return {"total": int(result.stdout.strip())}
```

Use `execute(code)` in a synchronous tool or `await aexecute(code)` in an asynchronous tool. Both return `SandboxResult` with `stdout`, `stderr`, `output_files`, and `metadata`. Your tool chooses what to return to the model; the example returns only the total, not the provider response.

The API also accepts keyword-only `input_files=()` for providers that support file transfer. The Docker extension rejects input files and does not mount host files. The example passes serialized values in the code instead.

### Try it with your agent

1. Start Docker and check that `docker version` can reach its daemon.

2. [Configure your agent's model](/docs/harnest/build/models-and-libraries/configure-a-model) in the serving process environment.

3. Add guidance to the agent's `instructions.md`, for example:

   ```text instructions.md theme={null}
   Use total_values when asked to add whole numbers. Base your answer on
   the total returned by the tool rather than inventing an execution result.
   ```

4. From the agent folder, run `harnest test .`, then `harnest serve .`.

5. Open the playground at `http://127.0.0.1:1907/` and ask: “Use total\_values to add 17, 23, and 41.” Check the tool event as well as the final answer; the expected tool result is `{"total": 81}`.

Offline tests validate your project but do not prove Docker execution works. The playground check uses your configured model and the Docker extension. See [Serving agents](/docs/harnest/runtime/serving) for server settings and authentication.

### Assign more than one sandbox

Add another declaration such as `sandbox/research.py`, defining `research = Sandbox.provider(...)` or using another installed extension's factory. Then add its name only to the agents that should use it:

| Agent assignment                         | Available to that agent's authored tools                                |
| ---------------------------------------- | ----------------------------------------------------------------------- |
| `sandboxes=["calculations"]`             | `context.sandboxes["calculations"]`                                     |
| `sandboxes=["calculations", "research"]` | `context.sandboxes["calculations"]` and `context.sandboxes["research"]` |
| No assignment, or `sandboxes=[]`         | None                                                                    |

The model chooses among your business tools, not among automatic sandbox tools. Your tool chooses an assigned sandbox in its implementation. Do not add a model-controlled provider name or arbitrary code parameter unless that broad execution capability is intentional. Provider configuration and invocation identity remain outside the model's control.

Names must be Python-style ASCII identifiers, at most 47 characters: start with a letter, then use letters, digits, or underscores. Names beginning with `_` are ignored examples, not active sandboxes. The filename and variable name must match. Put helpers in `lib/`, not in extra public files inside `sandbox/`.

The root `sandbox/` catalog is available to all agents in the same project, including folder-owned, flat-file, graph, and code-defined SubAgents. Each agent must still declare its own `sandboxes` list: a parent's assignment never grants access to a child. A folder-owned SubAgent can add names in its own `sandbox/`; those names are local to that scope and its descendants and cannot duplicate an ancestor's names.

<Note>
  `Agent(sandbox=...)` has been removed. Put each provider in `sandbox/<name>.py`, assign `sandboxes=["<name>"]` to every allowed agent, and call `context.sandboxes["<name>"]` from an authored tool. A single sandbox uses the same one-element array.
</Note>

### Docker scopes and budgets

The Docker extension works with both frameworks and does not use ADK's sandbox executor. Defaults provide a fresh container per execution, disabled networking, a non-root user, a read-only root filesystem, and bounded writable `/tmp` scratch space.

```python sandbox/calculations.py theme={null}
from harnest.extensions.docker import docker, DockerScope
from harnest.sandbox import SandboxBudget, SandboxNetworkPolicy

calculations = docker.sandbox(
    image="python:3.12-slim@sha256:<approved-digest>",
    scope=DockerScope.EXECUTION,
    budget=SandboxBudget(
        cpu=1.0,
        memory_bytes=512 * 1024 * 1024,
        pids=64,
        scratch_bytes=64 * 1024 * 1024,
    ),
    network_policy=SandboxNetworkPolicy.none(),
    timeout_seconds=300,
    max_output_bytes=1_048_576,
)
```

| Setting                  | Default and behavior                                                                                                      |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `scope`                  | `DockerScope.EXECUTION`: remove the topology after every call                                                             |
| `DockerScope.INVOCATION` | Reuse only for the same agent, user, session, and invocation                                                              |
| `DockerScope.SESSION`    | Reuse only for the same agent, user, and session                                                                          |
| `max_scopes`             | `8`: evict the least recently used retained container before exceeding this cap                                           |
| `budget.cpu`             | `1.0` CPU, enforced by Docker                                                                                             |
| `budget.memory_bytes`    | `512 MiB`, with no additional swap allowance                                                                              |
| `budget.pids`            | `64` processes                                                                                                            |
| `budget.scratch_bytes`   | `64 MiB` for writable `/tmp`; the root filesystem is read-only                                                            |
| `timeout_seconds`        | `300`; includes queue waiting and active execution                                                                        |
| `max_output_bytes`       | `1 MiB` combined stdout/stderr, bounded while streaming                                                                   |
| `network_policy`         | `SandboxNetworkPolicy.none()`; uses Docker's `none` network without services or an internal topology bridge with services |
| `image` / `docker_path`  | Supply exactly one; custom images must provide `python3` and must not declare volumes                                     |
| `base_url`               | Optional Docker daemon URL                                                                                                |

Retained scopes reuse the primary container and its `/tmp` scratch files while that identity remains cached, but not live Python processes or durable storage. Every successful call stops primary-container processes, including detached children. Declared service containers remain running for the retained topology. Topologies are scoped caches and may be evicted; use application storage for persistence. Invocation/session scopes reject requests without the full required identity. A session ID alone never permits reuse across users or agents.

The Docker extension can also create an extension-owned bridge with bounded service containers, internal DNS aliases, readiness checks, and independent budgets. It never publishes declared service ports on the host. See [Docker extension](/docs/harnest/build/extensions/official/docker#add-service-containers) for the topology API and cleanup contract.

The extension starts Docker only on execution. Timeouts, cancellation, and output overflow remove the owned container. Replacement waits for confirmed cleanup; ambiguous creation or failed cleanup blocks further admission until ownership is reconciled. Application shutdown closes created providers and removes all retained containers.

Initial image preparation and Docker control-plane calls use SDK transport timeouts. Harnest checks admission and the execution deadline again when those calls return. An unavailable Docker daemon cannot prove that a container stopped.

The Docker extension does not support input-file transfer or output artifacts. Use stdout or a custom provider with an explicit file contract. Docker shares its host kernel; use a stronger isolation provider if that boundary is insufficient for your workload.

`options` retains the allowlisted parsing/retry fields only for callers explicitly constructing a native ADK adapter with `to_adk_executor()`. Those fields do not change Docker limits or add model tools to named sandbox assignments. Your authored tool decides whether to retry.

## Framework behavior

| Framework | Native integration                                         | Named sandbox result                  |
| --------- | ---------------------------------------------------------- | ------------------------------------- |
| ADK       | Your authored Agent Tool uses ADK's native tool loop       | `SandboxResult` returned to your tool |
| LangGraph | Your authored Agent Tool uses LangGraph's native tool loop | `SandboxResult` returned to your tool |

The Docker extension uses Harnest's shared provider contract underneath either integration. Queued calls are checked again before execution, so cancelled, expired, or revoked requests cannot start later merely because the queue advances. You keep one authoring API; Harnest adapts it to the selected framework's execution loop.

Named assignments do not populate ADK's `code_executor` field or create an automatic LangGraph execution tool. Both frameworks access the same sandbox contract through authored tools.

`context.sandboxes` is available only during a managed invocation. Looking up an unassigned name raises `ContextResourceError`. Using a handle from a different or ended invocation raises `ContextUnavailableError`; do not cache handles or pass them between agents.

## Bring your own provider

A portable provider implements `SandboxBackend`: its `execute(request: SandboxRequest)` method returns `SandboxResult`. Pass a lazy factory to `Sandbox.provider(...)`:

```python sandbox/research.py theme={null}
from harnest.sandbox import Sandbox
from company_sandbox import CompanyBackend

research = Sandbox.provider(
    lambda: CompanyBackend(pool="agents"),
    name="company-sandbox",
    timeout_seconds=120,
    metadata={"region": "eu-west", "labels": {"workload": "research"}},
)
```

Assign this provider with `sandboxes=["research"]`. `CompanyBackend` represents your adapter around a provider SDK, not a Harnest built-in. Put SDK-specific connection settings and credentials in that factory. Harnest calls it when execution first needs the backend, not during compilation.

| Contract         | Properties                                                                                        |
| ---------------- | ------------------------------------------------------------------------------------------------- |
| `SandboxRequest` | `code`, `timeout_seconds`, `context`, `input_files`, `execution_id`, `metadata`, `network_policy` |
| `SandboxContext` | Optional `agent_name`, `invocation_id`, `user_id`, and `session_id`                               |
| `SandboxResult`  | `status`, `exit_code`, `stdout`, `stderr`, `output_files`, `metadata`                             |
| `SandboxFile`    | `name`, `content` as bytes or base64 text, and `mime_type`                                        |

Import these types from `harnest.sandbox`. Your provider must enforce the requested timeout and its own isolation policy. Invocation identity can be absent when an adapter is called outside the runtime; reject such requests if your provider requires a user or session identity. A revoked managed context fails closed rather than becoming anonymous. File names are transport values, not permission to access host paths.

The Docker extension's guard cannot terminate arbitrary third-party SDK calls. Custom providers remain responsible for stopping active work, bounded output collection, and resource cleanup. Harnest checks invocation validity before admitting their calls.

### Declare provider-enforced network policy

Use a Harnest network policy only when the provider enforces it below agent
code. The provider declares its supported controls, and Harnest fails before the
first execution if any requested guarantee is missing:

```python sandbox/research.py theme={null}
from harnest.sandbox import (
    Sandbox,
    SandboxNetworkMode,
    SandboxNetworkPolicy,
    SandboxProviderCapabilities,
)
from company_sandbox import CompanyBackend


class PolicyBackend(CompanyBackend):
    sandbox_capabilities = SandboxProviderCapabilities(
        network_modes=frozenset({
            SandboxNetworkMode.NONE,
            SandboxNetworkMode.ALLOWLIST,
        }),
        host_allowlist=True,
        port_allowlist=True,
        private_network_blocking=True,
    )


research = Sandbox.provider(
    PolicyBackend,
    name="company-sandbox",
    network_policy=SandboxNetworkPolicy.allowlist(
        "api.example.com",
        ports=(443,),
        block_private_networks=True,
    ),
)
```

Modes are `none`, `unrestricted`, and `allowlist`. Allowlist entries are exact
DNS names or IP literals, not URLs or wildcards. Providers must enforce them on
every connection and resolution, including redirects and DNS changes, and must
apply the optional port restriction. `block_private_networks=True` covers
private, loopback, link-local, and metadata destinations.

Application request validation does not satisfy this contract. If a backend
does not expose typed `sandbox_capabilities`, omit `network_policy` and treat
network behavior as entirely provider-owned. Native ADK executors cannot accept
a Harnest network policy. Reusable Harnest Extensions implementing a sandbox
provider declare the `sandbox.provider` extension capability.

<Note>
  Named sandbox capabilities require the neutral `SandboxBackend` contract on both frameworks. Wrap a native-only provider in that contract before assigning it by name. The removed `Agent(sandbox=...)` argument is not a migration path.
</Note>

### Provider metadata

`metadata` accepts arbitrary JSON properties separately from native SDK configuration. Harnest copies and deeply freezes these properties, preserves JSON value types, and omits them from declaration, request, and result representations. Unsupported objects, non-string object keys, nonfinite numbers, and cycles are rejected rather than converted to strings.

Declaration metadata reaches `request.metadata`; the named capability API does not let the model override it. Your provider can return separate `SandboxResult(metadata=...)` properties, such as execution status or usage figures. These reach your authored tool unchanged; return only the fields the model needs.

The Docker extension does not automatically echo declaration metadata into results. Use a provider wrapper when you want to return measured or provider-supplied details. For a remote provider, translate the SDK response into `SandboxResult`. Keep secrets in provider configuration, not returned metadata. Add the provider package to your project's `pyproject.toml`. Do not implement `execute()` with host-side `exec()`, `eval()`, or an unisolated subprocess.

<Warning>
  Your authored tool controls whether provider metadata becomes model-visible. Do not return credentials, secrets, or private identifiers. Legacy model execution adapters expose result metadata automatically. In the legacy ADK code-execution loop, nonempty metadata wraps stdout as JSON with `stdout` and `metadata` keys because that native result has no metadata field. Empty metadata leaves stdout unchanged; stderr keeps ADK's native error behavior.
</Warning>

## Handle failed execution

Check `result.status`, not whether stderr is empty. A program can write warnings to stderr and still succeed, or exit nonzero without an error message.

| Status                  | Meaning                                                                                     |
| ----------------------- | ------------------------------------------------------------------------------------------- |
| `succeeded`             | Execution completed successfully                                                            |
| `failed`                | Nonzero exit or provider-reported failure                                                   |
| `timed_out`             | Execution deadline expired                                                                  |
| `output_limit_exceeded` | Combined output exceeded its budget                                                         |
| `cancelled`             | Provider-reported cancellation; managed caller cancellation also propagates as an exception |

`exit_code` is the process exit code when available. Custom providers must set failure status explicitly: `SandboxResult(status="failed", exit_code=1, stderr="...")`. The default status is `succeeded`; a nonzero exit code cannot claim success. Sanitized `SandboxExecutionError` also exposes a `status` field.

| Failure                                   | Expected behavior and next step                                                                                                                                                       |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Python exception                          | Inspect `result.status`, `result.exit_code`, and `result.stderr` in your authored tool and return a useful, safe failure message. A failed execution need not raise a host exception. |
| Name not assigned                         | `ContextResourceError`: add the intended name to this agent's `sandboxes` list, or correct the lookup. Parent assignments do not grant child access.                                  |
| Handle used outside its invocation        | `ContextUnavailableError`: look up the sandbox inside the current tool call instead of caching a handle.                                                                              |
| Deadline or output budget exceeded        | Execution stops and its container is discarded. A later call starts with a fresh filesystem after cleanup is confirmed. Increase limits only when the workload requires it.           |
| Cancelled, expired, or revoked invocation | Queued code must not start. Active Docker-extension execution is terminated; custom SDK termination remains provider-owned.                                                           |
| Docker unavailable or startup fails       | Execution fails without falling back to the host. Check daemon access, the configured image or Dockerfile, and Python availability in the image.                                      |
| Cleanup cannot be confirmed               | The backend refuses replacement until cleanup is confirmed. For ambiguous creation without an ID, reconcile Docker resources before recreating the backend.                           |

Provider exceptions are exposed as sanitized `SandboxExecutionError`. Diagnose infrastructure failures with trusted provider diagnostics; do not log generated code, credentials, or complete result payloads as routine audit data.

## Failure recovery and cleanup

An ordinary exception escaping a nested execution scope fails that scope and blocks its retained workers. It does not cancel the parent operation. Your provider can catch a recoverable error, such as a missing container, and retry within the remaining parent budget. A shorter helper deadline also stays local. Explicit cancellation and invocation revocation still block further execution.

Import provider controls with `from harnest.sandbox import control`. Use `control.execute(timeout_seconds=...)` for nested execution scopes and `control.current()` to inspect the active control. Use `control.cleanup(...)` to release resources after cancellation or invocation revocation:

```python theme={null}
from harnest.sandbox import control

# Inside your provider's close method; retain this ID if removal fails.
with control.cleanup(timeout_seconds=5) as cleanup:
    cleanup.check()
    client.remove(container_id, timeout=cleanup.remaining())
```

`client.remove` represents your provider SDK's resource-removal call; use its actual timeout parameter. Cleanup has its own finite deadline, and nested cleanup cannot extend it. New sandbox execution is forbidden inside cleanup. Returning restores the original execution control without clearing its cancellation; retained cleanup contexts are then revoked.

This is a cooperative deadline, not a mechanism for killing Python threads. Check before each cleanup operation and apply the remaining budget to SDK I/O and lock acquisition (`cleanup.acquire(lock)`). A blocking SDK that ignores timeouts can still overrun the budget. Treat timeout or uncertain removal as incomplete cleanup, retain the owned resource handle, and prevent replacement until cleanup is confirmed.

## Security ownership

Harnest enforces named agent grants, revocable invocation handles, request validation, and deadline propagation. The Docker extension enforces identity-scoped containers, kernel CPU/memory/process/scratch budgets, host deadlines, bounded output, and failure-safe cleanup. It does not mount host paths and rejects image-declared volumes.

Custom providers remain responsible for their own tenant separation, filesystem/network policy, resource limits, output bounds, cancellation, and cleanup. Harnest validates their requests and results and checks admission; it cannot enforce Docker limits on an arbitrary remote SDK. Deployment permissions do not replace an enforcing provider.
