A SandboxAgent is a specialized agent for tasks that require isolated file-system access; it extends Agent with sandbox-specific fields (default_manifest, capabilities, run_as, base_instructions) while keeping transport details transport-agnostic via RunConfig. The sandbox manifests, environment resolution, and entry types form a declarative system for configuring workspace mounts, file permissions, environment variables, and user identity — all validated against exposure policies and deserialized polymorphically at runtime. A time-of-check/time-of-use (TOCTOU) race condition occurs when a symlink is swapped between path validation and the actual filesystem operation, allowing an attacker to redirect file access outside the intended sandbox root.
The recommended pattern is to use a plain Agent plus Runner for prompt/tool/conversation tasks, and switch to SandboxAgent only when the agent must inspect or modify real files in an isolated workspace.[1]
SandboxAgent, defined in src/agents/sandbox/sandbox_agent.py, is a subclass of Agent[TContext] that adds sandbox-specific fields (default_manifest, base_instructions, capabilities, run_as) to the base Agent interface.[2] Runtime transport details — sandbox client, client options, and live session — are NOT stored on SandboxAgent itself; they are provided at run time through RunConfig(sandbox=...), keeping the agent declaration transport-agnostic.[2]
SandboxAgent.default_manifest holds the default sandbox Manifest applied when Runner creates a new session; it defaults to None.[2] SandboxAgent.capabilities is a Sequence[Capability] (defaulting to Capabilities.default()) whose entries can mutate the manifest, add instructions, and expose tools.[2] SandboxAgent.run_as sets the user identity presented to model-facing sandbox tools such as shell, file reads, and patches; it accepts a User object, a plain string, or None.[2] SandboxAgent.base_instructions overrides the SDK sandbox base prompt and accepts a string, an async or sync callable (RunContextWrapper, Agent) -> str | None, or None; most callers should use instructions instead.[2] shell_tool.py in src/agents/sandbox/capabilities/tools/shell_tool.py correctly forwards the configured workdir to the subprocess when shell commands are supplied as a list rather than a string. tests/sandbox/test_posix_tool_paths.py includes test coverage verifying that shell commands supplied as lists respect the configured working directory.
SandboxAgent.__post_init__ coerces a raw dict passed as default_manifest into a typed Manifest via _coerce_manifest, so callers may supply either type.[2] SandboxAgent.__post_init__ coerces a raw dict passed as run_as into a User Pydantic model via coerce_pydantic_config, so callers may pass a plain dict.[2] SandboxAgent.__post_init__ raises TypeError if base_instructions is not a string, callable, or None, giving an explicit error message that names the invalid type.[2] SandboxAgent.__post_init__ raises TypeError if run_as is not a string, User, or None, giving an explicit error message that names the invalid type.[2] SandboxAgent._sandbox_concurrency_guard is an internal field excluded from __init__ and repr, used to guard concurrent sandbox access; it is not part of the public API.[2]
Manifest in src/agents/sandbox/manifest.py is a Pydantic BaseModel with a fixed version: Literal[1] = 1, a root defaulting to /workspace, and fields for entries, environment, users, groups, extra_path_grants, and remote_mount_command_allowlist.[3] Manifest.remote_mount_command_allowlist defaults to a fixed list of safe read/write commands: ls, find, stat, cat, less, head, tail, du, grep, rg, wc, sort, cut, cp, tee, echo, mkdir, rm.[3] Manifest entries are deserialized from raw mappings using BaseEntry.parse(entry) inside the _parse_entries field validator, supporting polymorphic entry types.[3] JSON serialization of Manifest entries converts Path keys to their POSIX string representation via .as_posix(), ensuring cross-platform compatibility in serialized output.[3]
Manifest actively rejects any input containing mount-credential-exposure policy keys (e.g. mount_credential_exposure_policy, inContainerMountCredentialExposureAllowedPaths) via a model_validator, enforcing that this policy can only be set on a trusted Manifest instance in code, not via untrusted input.[3]
EnvValue is an abstract Pydantic BaseModel in src/agents/sandbox/manifest.py serving as an extension point for environment variable sources; concrete subtypes register themselves via __pydantic_init_subclass__ under a string type discriminator.[3] EnvValue.parse() accepts an existing EnvValue instance unchanged, a plain {"value": str} mapping (coerced to StrEnvValue), or a typed mapping dispatched to the registered subclass; it raises ValueError for unknown type strings.[3] StrEnvValue is the built-in EnvValue subtype registered under type="str" in src/agents/sandbox/manifest.py; its resolve() simply returns self.value as a string.[3] EnvEntry in src/agents/sandbox/manifest.py wraps an EnvValue with optional description and ephemeral (default False) metadata fields.[3] Environment.resolve() in src/agents/sandbox/manifest.py concurrently resolves all env values using gather_with_cancel rather than a bare asyncio.gather, so that if one lookup fails the remaining sibling coroutines are cancelled rather than left running.[3]
UnixLocalSandboxClient is supported only on macOS and Linux; on Windows, DockerSandboxClient (with the openai-agents[docker] extra) or a hosted sandbox client must be used instead.[4] SandboxConcurrencyLimits in src/agents/run_config.py defaults to 4 concurrent manifest entries (manifest_entries=4) and 4 concurrent file copies per local_dir entry (local_dir_files=4); either can be set to None to remove the limit.[5] SandboxArchiveLimits in src/agents/run_config.py defaults to a 1 GiB input size limit, a 4 GiB extracted-bytes limit, and a 100,000-member limit; all three can be set to None to disable that specific limit.[5] src/agents/sandbox/session/pty_types.py and src/agents/sandbox/sandboxes/unix_local.py handle asyncio.CancelledError during PTY startup, releasing file descriptors when cancellation arrives before PTY initialization completes. PTY output is drained before session teardown across all sandbox backends (Blaxel, Cloudflare, Daytona, e2b, Modal, Docker, UnixLocalSandboxClient), preventing truncated output on fast-completing runs; draining logic lives in src/agents/sandbox/session/pty_output.py and each backend module. The Unix-local sandbox file API in src/agents/sandbox/sandboxes/ resolves and validates paths atomically within each operation, preventing TOCTOU symlink race conditions where symlink swapping between validation and filesystem access could be exploited. tests/sandbox/test_unix_local_file_io.py and tests/sandbox/test_unix_local_user_file_io.py include test coverage for symlink race conditions in the Unix-local sandbox file API.
Example: minimal sandbox agent run using SandboxAgent, UnixLocalSandboxClient, and Runner.run_sync with a SandboxRunConfig.
from agents import Runner
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.entries import GitRepo
from agents.sandbox.sandboxes import UnixLocalSandboxClient
agent = SandboxAgent(
name="Workspace Assistant",
instructions="Inspect the sandbox workspace before answering.",
default_manifest=Manifest(entries={"repo": GitRepo(repo="openai/openai-agents-python", ref="main")}),
)
result = Runner.run_sync(
agent,
"Inspect the repo README and summarize what this project does.",
run_config=RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient())),
)
print(result.final_output)
Sources