# Code execution

Understand process, Docker, Seatbelt, and OpenShell guarantees.

The SDK offers three local execution tiers, plus a remote OpenShell backend. Select one explicitly based on the access boundary you need; a working directory alone is not a security boundary.

## Guarantees by tier

| Tier | Access | Requirements |
| --- | --- | --- |
| process | Host filesystem and network access; temporary working directory | POSIX host |
| docker | Workdir writable, configured mounts read-only, network disabled unless enabled | Docker package and running daemon |
| seatbelt | Workdir writable, allowlisted reads, network disabled unless enabled | macOS with sandbox-exec |

An unavailable explicitly selected backend fails before dispatch. It never silently falls back to host execution.

## Remote backend: OpenShell

`OpenShellSandbox` from `harnessx.openshell` is a remote backend rather than a tier. It runs `run_bash` and the file tools in an [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) sandbox, with per-program network rules and secrets that commands can use but never read. The loop, model calls, memory and hooks stay in your process. Unlike a local `Sandbox`, it belongs to the agent it is given: the agent creates it, copies your project in and back, and deletes it. See [OpenShell sandbox](../openshell/).

## Execute Python

~~~python
import asyncio
from harnessx import Sandbox, SandboxConfig

async def main():
    async with Sandbox(SandboxConfig(tier="process")) as sandbox:
        result = await sandbox.execute("print(40 * 10)")
        print(result.stdout)

asyncio.run(main())
~~~

This local process example has host access. Use docker or seatbelt when an access boundary is required, and verify its configuration on the target host.

## Run the bash built-in inside a sandbox

`Agent(sandbox=sandbox)` binds the sandbox as the engine of the `bash` built-in, so shell commands the model requests run inside it. The registry exposes the binding as `agent.tools.sandbox`.

~~~python
import asyncio
from harnessx import Agent, CliPermissionManager, Sandbox, SandboxConfig

async def main():
    config = SandboxConfig(tier="process")  # docker or seatbelt for an access boundary
    async with Sandbox(config) as sandbox:
        async with Agent(
            tools=["bash"],
            sandbox=sandbox,
            permissions=CliPermissionManager(),
        ) as agent:
            result = await agent.run("List the files in the working directory.")
            result.raise_for_status()
            print(result.output)

asyncio.run(main())
~~~

The sandbox is caller-owned; the agent does not close it. Passing a `ToolRegistry` that is already bound to a different sandbox raises `ValueError`.

## Observe executions

`Sandbox(config, hooks=hook_manager)` emits `HookEvent.SANDBOX_EXEC` for every `execute()` and `execute_command()` with `kind`, `tier`, `exit_code`, `timed_out`, `execution_time_ms`, and `denials` (what a network policy refused; empty for the local tiers) in `ctx.data` (the `SandboxExecData` TypedDict in `harnessx.hooks`). `Agent(sandbox=...)` supplies the agent's own hooks when the sandbox has none, so `agent.hooks.on(HookEvent.SANDBOX_EXEC, ...)` observes the bash built-in's commands. See [Middleware & hooks](../hooks/#hooks-for-observation).

## Limits and lifecycle

Timeout, CPU, and file-size limits apply as supported by the selected tier. Address-space limiting is platform-dependent and is not a universal memory isolation guarantee. Docker can enforce container memory limits.

The process backend kills and reaps its managed process group on timeout/cancellation. Deliberately detached descendants can escape that group. Cancellation does not reverse completed file or external changes.

Injected sandboxes remain caller-owned. Use their async context manager around the agent or application lifetime.

## Try the coding example

~~~bash
uv run python examples/08-sandboxes/sandbox_isolation_tiers.py
uv run python examples/08-sandboxes/sandbox_isolation_tiers.py --tier docker
~~~

The default `--tier process` keeps host filesystem and network access. Use `--tier docker`, or `--tier seatbelt` on macOS, to isolate the code.

See [Permissions](../permissions/) for execution policy and [Recovery](../recovery/) for interrupted effects.

## Backends, file tools and paths

The built-in tools run against an execution backend, described by the `ExecutionBackend` protocol exported from `harnessx`. `run_bash` needs only `execute_command`. A backend whose `owns_filesystem` is true takes the file tools too: `read_file`, `write_file`, `edit_file`, `delete`, `glob`, `grep`, `list_directory` and `generate_file` run inside it, with the host tools' output. Each file operation is a POSIX shell snippet sent through `execute_command`, and every path goes through the backend's `resolve_path()` first.

The local `Sandbox` has `owns_filesystem = False`, so with any of the three tiers the file tools stay on the host and only `run_bash` runs in the sandbox. `OpenShellSandbox` owns its filesystem.

`Sandbox.resolve_path(path, *, write=False)` returns the absolute path of `path` inside the sandbox's working directory, and raises `PermissionError` for a path outside it:

~~~python
import asyncio
from harnessx import Sandbox, SandboxConfig

async def main():
    async with Sandbox(SandboxConfig(tier="process")) as sandbox:
        print(sandbox.resolve_path("out/report.txt"))  # inside the working directory
        try:
            sandbox.resolve_path("../elsewhere.txt")
        except PermissionError as error:
            print(error)

asyncio.run(main())
~~~

`execute_command(command, timeout=, stdin=)` and `execute(code, timeout=)` take a per-call limit. It can shorten `SandboxConfig.timeout_seconds` for that call but never extend it. `stdin` is bytes fed to the command on the process and seatbelt tiers; the docker tier raises `ValueError` for it. On those two tiers a command without `stdin` gets closed stdin instead of inheriting your process's.

The `run_bash` built-in passes the model's `timeout` to the sandbox, capped by the configured limit.
