Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ agent = SandboxAgent(
result = Runner.run_sync(
agent,
"Inspect the repo README and summarize what this project does.",
# macOS: sandbox-exec confined. On Linux this raises unless the client is
# constructed with allow_unconfined_linux=True; prefer Docker there.
run_config=RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient())),
)
print(result.final_output)
Expand Down
8 changes: 7 additions & 1 deletion docs/sandbox/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@ For most users, start with one of these two sandbox clients:

| Client | Install | Choose it when | Example |
| --- | --- | --- | --- |
| `UnixLocalSandboxClient` | none | Fastest local iteration on macOS or Linux. Good default for local development. | [Unix-local starter](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/unix_local_runner.py) |
| `UnixLocalSandboxClient` | none | Fastest local iteration on macOS (Seatbelt confinement). On Linux it requires `allow_unconfined_linux=True` because there is no OS-level confinement — prefer `DockerSandboxClient` there. | [Unix-local starter](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/unix_local_runner.py) |
Comment thread
seratch marked this conversation as resolved.
| `DockerSandboxClient` | `openai-agents[docker]` | You want container isolation or a specific image to reproduce a target environment locally. | [Docker starter](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/docker/docker_runner.py) |

</div>

Unix-local is the easiest way to start developing against a local filesystem. Move to Docker or a hosted provider when you need stronger environment isolation or production-style parity.

Environment isolation notes:

- On macOS, commands are wrapped with a `sandbox-exec` profile that confines filesystem access to the workspace.
- On Linux there is no equivalent confinement: creating a session raises by default. Pass `allow_unconfined_linux=True` to explicitly accept unconfined host execution (intended for disposable development machines), or use `DockerSandboxClient`. The sandbox examples gate this opt-in behind `AGENTS_ALLOW_UNCONFINED_LINUX=1` in a shared helper (`examples/sandbox/misc/example_support.py`).
- Sandboxed commands inherit only a minimal allowlist of host environment variables (`PATH`, locale, `TZ`, `TERM`, `TMPDIR`, certificate/CA locations) plus anything declared in the manifest environment. This prevents a sandboxed command from reading host credentials such as `OPENAI_API_KEY` via `printenv`. Pass `inherit_environment=True` to restore full inheritance when a workflow depends on it.

`SandboxPathGrant.host_path` is Docker-only and maps a host path to a different POSIX path inside the container. Unix-local supports only same-path grants. See [Manifest path grants](guide.md#manifest) for details.

To switch from Unix-local to Docker, keep the agent definition the same and change only the run config:
Expand Down
2 changes: 1 addition & 1 deletion docs/sandbox/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ If you do not need access to files or a stateful, mutable filesystem, keep using

## Choose a sandbox client

Start with `UnixLocalSandboxClient` for local development on macOS or Linux. On Windows, use `DockerSandboxClient` or a hosted provider instead. On any supported platform, move to `DockerSandboxClient` when you need container isolation or image parity, or to a hosted provider when you need provider-managed execution.
Start with `UnixLocalSandboxClient` for local development on macOS (it confines commands with `sandbox-exec`). On Linux and Windows, use `DockerSandboxClient` or a hosted provider instead — the Unix-local client has no OS-level confinement on Linux and refuses to start there unless `allow_unconfined_linux=True` is set explicitly. On any supported platform, move to `DockerSandboxClient` when you need container isolation or image parity, or to a hosted provider when you need provider-managed execution.

In most cases, the `SandboxAgent` definition stays the same while the sandbox client and its options change in [`SandboxRunConfig`][agents.run_config.SandboxRunConfig]. See [Sandbox clients](clients.md) for local, Docker, hosted, and remote-mount options.

Expand Down
2 changes: 2 additions & 0 deletions docs/sandbox_agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ async def main() -> None:
build_agent("gpt-5.6-sol"),
"Open `repo/task.md`, fix the issue, run the targeted test, and summarize the change.",
run_config=RunConfig(
# macOS: sandbox-exec confined. On Linux this raises unless the client
# is constructed with allow_unconfined_linux=True; prefer Docker there.
sandbox=SandboxRunConfig(client=UnixLocalSandboxClient()),
workflow_name="Sandbox coding example",
),
Expand Down
4 changes: 2 additions & 2 deletions examples/sandbox/docs/coding_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from agents.sandbox.capabilities import LocalDirLazySkillSource, Skills
from agents.sandbox.capabilities.capabilities import Capabilities
from agents.sandbox.entries import LocalDir
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient

DEFAULT_MODEL = "gpt-5.6-sol"
TARGET_TEST_CMD = "sh tests/test_credit_note.sh"
Expand All @@ -32,6 +31,7 @@

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
from examples.sandbox.misc.example_support import unix_local_client # noqa: E402


def build_agent(model: str) -> SandboxAgent[None]:
Expand Down Expand Up @@ -188,7 +188,7 @@ def _saw_target_test_success(new_items: Sequence[object]) -> bool:

async def main(model: str, prompt: str) -> None:
agent = build_agent(model)
client = UnixLocalSandboxClient()
client = unix_local_client()
sandbox = await client.create(manifest=agent.default_manifest)

try:
Expand Down
5 changes: 3 additions & 2 deletions examples/sandbox/extensions/temporal/local_hello_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell
from agents.sandbox.entries import File
from agents.sandbox.sandboxes import UnixLocalSandboxClient, UnixLocalSandboxClientOptions
from agents.sandbox.sandboxes import UnixLocalSandboxClientOptions
from examples.sandbox.misc.example_support import unix_local_client

TASK_QUEUE = "local-temporal-sandbox-agent"
WORKFLOW_ID = "local-temporal-sandbox-agent-workflow"
Expand Down Expand Up @@ -89,7 +90,7 @@ async def run(self, model: str, trace_mode: str) -> str:
def _client_with_plugin(client: Client, trace_mode: str) -> Client:
plugin = OpenAIAgentsPlugin(
model_params=ModelActivityParameters(start_to_close_timeout=timedelta(seconds=120)),
sandbox_clients=[SandboxClientProvider("local", UnixLocalSandboxClient())],
sandbox_clients=[SandboxClientProvider("local", unix_local_client())],
add_temporal_spans=trace_mode == TRACE_MODE_OPENAI_WITH_TEMPORAL_SPANS,
)
config = client.config()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
if _p not in sys.path:
sys.path.insert(0, _p)

from examples.sandbox.misc.example_support import unix_local_client # noqa: E402
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability # noqa: E402


Expand Down Expand Up @@ -638,10 +639,10 @@ async def run_worker() -> None:
)

from agents.extensions.sandbox import DaytonaSandboxClient, E2BSandboxClient
from agents.sandbox.sandboxes import DockerSandboxClient, UnixLocalSandboxClient
from agents.sandbox.sandboxes import DockerSandboxClient

sandbox_clients: list[SandboxClientProvider] = [
SandboxClientProvider("local", UnixLocalSandboxClient()),
SandboxClientProvider("local", unix_local_client()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Temporal worker startup independent of local opt-in

On Linux without AGENTS_ALLOW_UNCONFINED_LINUX=1, this unconditional unix_local_client() call raises SystemExit before the worker can register Daytona, E2B, or Docker providers. That blocks the Temporal example even for its default daytona backend or a user selecting Docker, because startup never reaches the later provider-registration and warning logic. Treat the local provider like the other optional backends: only append it when the helper can build it, otherwise let the worker continue without local.

AGENTS.md reference: AGENTS.md:L157-L157

Useful? React with 👍 / 👎.

]
if _os.environ.get("DAYTONA_API_KEY"):
sandbox_clients.append(SandboxClientProvider("daytona", DaytonaSandboxClient()))
Expand Down
5 changes: 2 additions & 3 deletions examples/sandbox/handoffs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
from agents import Agent, Runner
from agents.run import RunConfig
from agents.sandbox import SandboxAgent, SandboxRunConfig
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from examples.sandbox.misc.example_support import text_manifest
from examples.sandbox.misc.example_support import text_manifest, unix_local_client
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability

DEFAULT_QUESTION = (
Expand Down Expand Up @@ -90,7 +89,7 @@ async def main(model: str, question: str) -> None:
result = await Runner.run(
intake_agent,
question,
run_config=RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient())),
run_config=RunConfig(sandbox=SandboxRunConfig(client=unix_local_client())),
)
print(result.final_output)

Expand Down
4 changes: 2 additions & 2 deletions examples/sandbox/healthcare_support/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxPathGrant, SandboxRunConfig
from agents.sandbox.entries import Dir, File, LocalDir
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
from agents.tool_context import ToolContext
from examples.sandbox.healthcare_support.data import HealthcareSupportDataStore
from examples.sandbox.healthcare_support.models import (
Expand All @@ -37,6 +36,7 @@
memory_recap_agent,
)
from examples.sandbox.healthcare_support.tools import HealthcareSupportContext
from examples.sandbox.misc.example_support import unix_local_client

EXAMPLE_ROOT = Path(__file__).resolve().parent
POLICIES_ROOT = EXAMPLE_ROOT / "policies"
Expand Down Expand Up @@ -428,7 +428,7 @@ async def run_healthcare_support_workflow(
await context.emit("memory_ready", session_id=conversation_session.session_id)

hooks = WorkflowHooks()
sandbox_client = UnixLocalSandboxClient()
sandbox_client = unix_local_client()
sandbox = await sandbox_client.create(manifest=_build_manifest(scenario))
await context.emit(
"sandbox_ready",
Expand Down
4 changes: 2 additions & 2 deletions examples/sandbox/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
from agents.sandbox import LocalSnapshotSpec, Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Filesystem, Memory, Shell
from agents.sandbox.entries import File
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from examples.sandbox.misc.example_support import unix_local_client

DEFAULT_MODEL = "gpt-5.6-sol"
FIRST_PROMPT = "Inspect workspace and fix invoice total bug in src/acme_metrics/report.py."
Expand Down Expand Up @@ -173,7 +173,7 @@ def _run_config(*, sandbox: BaseSandboxSession, workflow_name: str) -> RunConfig
async def main(*, model: str) -> None:
manifest = _build_manifest()
agent = _build_agent(model=model, manifest=manifest)
client = UnixLocalSandboxClient()
client = unix_local_client()

with tempfile.TemporaryDirectory(prefix="sandbox-memory-example-") as snapshot_dir:
# Use a local snapshot so the second run resumes the same workspace in a new sandbox
Expand Down
4 changes: 2 additions & 2 deletions examples/sandbox/memory_multi_agent_multiturn.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
from agents.sandbox import Manifest, MemoryLayoutConfig, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Filesystem, Memory, Shell
from agents.sandbox.entries import Dir, File
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from examples.sandbox.misc.example_support import unix_local_client

DEFAULT_MODEL = "gpt-5.6-sol"
GTM_SESSION_ID = "gtm-q2-pipeline-review"
Expand Down Expand Up @@ -167,7 +167,7 @@ async def main(*, model: str) -> None:
manifest = _build_manifest()
gtm_agent = _build_gtm_agent(model=model, manifest=manifest)
engineering_agent = _build_engineering_agent(model=model, manifest=manifest)
client = UnixLocalSandboxClient()
client = unix_local_client()
sandbox = await client.create(manifest=manifest)
workspace_root = Path(sandbox.state.manifest.root)

Expand Down
27 changes: 26 additions & 1 deletion examples/sandbox/misc/example_support.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
from __future__ import annotations

import os
import sys
from collections.abc import Mapping

from agents.sandbox import Manifest
from agents.sandbox.entries import File
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
from agents.sandbox.session.dependencies import Dependencies


def unix_local_client(*, dependencies: Dependencies | None = None) -> UnixLocalSandboxClient:
"""Build the Unix-local sandbox client used by the sandbox examples.

UnixLocalSandboxClient is OS-confined (sandbox-exec) on macOS only; on Linux
it runs commands unconfined on the host, so the SDK requires an explicit
opt-in. The examples only opt in when AGENTS_ALLOW_UNCONFINED_LINUX=1 is
set; on Linux the Docker sandbox examples are the safer default.
"""

unconfined = sys.platform != "darwin" and (
os.environ.get("AGENTS_ALLOW_UNCONFINED_LINUX") == "1"
)
if sys.platform != "darwin" and not unconfined:
raise SystemExit(
"UnixLocalSandboxClient only has OS-level confinement on macOS; on Linux it "
"runs commands unconfined on the host. Set AGENTS_ALLOW_UNCONFINED_LINUX=1 "
"to opt in, or use the Docker sandbox examples (examples/sandbox/docker/)."
)
return UnixLocalSandboxClient(allow_unconfined_linux=unconfined, dependencies=dependencies)


def text_manifest(files: Mapping[str, str]) -> Manifest:
Expand All @@ -15,7 +40,7 @@ def text_manifest(files: Mapping[str, str]) -> Manifest:


def tool_call_name(raw_item: object) -> str:
"""Return a readable name for a raw tool call item."""
"""Return a readable name for a raw tool call."""

if isinstance(raw_item, dict):
name = raw_item.get("name")
Expand Down
5 changes: 2 additions & 3 deletions examples/sandbox/sandbox_agent_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,11 @@
from agents.sandbox.capabilities.capabilities import Capabilities
from agents.sandbox.entries import File, LocalDir
from agents.sandbox.errors import WorkspaceReadNotFoundError
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from examples.sandbox.misc.example_support import unix_local_client

DEFAULT_MODEL = "gpt-5.5"
COMPACTION_THRESHOLD = 1_000
Expand Down Expand Up @@ -363,7 +362,7 @@ async def main(model_name: str) -> None:
_write_local_skill(skills_root)

agent = _build_agent(model, skills_root)
client = UnixLocalSandboxClient()
client = unix_local_client()
sandbox = await client.create(manifest=agent.default_manifest)

try:
Expand Down
5 changes: 2 additions & 3 deletions examples/sandbox/sandbox_agent_with_remote_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@
from agents import ModelSettings, Runner
from agents.run import RunConfig
from agents.sandbox import Manifest, RemoteSnapshotSpec, SandboxAgent, SandboxRunConfig
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
from agents.sandbox.session import Dependencies

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from examples.sandbox.misc.example_support import text_manifest
from examples.sandbox.misc.example_support import text_manifest, unix_local_client
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability

S3_BUCKET_ENV_VAR = "S3_MOUNT_BUCKET"
Expand Down Expand Up @@ -117,7 +116,7 @@ async def _verify_remote_snapshot_round_trip(*, model: str) -> None:
SNAPSHOT_CLIENT_DEPENDENCY_KEY,
S3SnapshotClient(bucket=_require_s3_bucket(), prefix=SNAPSHOT_OBJECT_PREFIX),
)
client = UnixLocalSandboxClient(dependencies=dependencies)
client = unix_local_client(dependencies=dependencies)

sandbox = await client.create(
manifest=manifest,
Expand Down
9 changes: 6 additions & 3 deletions examples/sandbox/sandbox_agent_with_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
from agents.mcp import MCPServerStdio
from agents.run import RunConfig
from agents.sandbox import SandboxAgent, SandboxRunConfig
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from examples.sandbox.misc.example_support import text_manifest, tool_call_name
from examples.sandbox.misc.example_support import (
text_manifest,
tool_call_name,
unix_local_client,
)
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability

DEFAULT_QUESTION = (
Expand Down Expand Up @@ -94,7 +97,7 @@ async def main(model: str, question: str) -> None:
result = await Runner.run(
agent,
question,
run_config=RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient())),
run_config=RunConfig(sandbox=SandboxRunConfig(client=unix_local_client())),
)
tool_names: list[str] = []
for item in result.new_items:
Expand Down
11 changes: 7 additions & 4 deletions examples/sandbox/sandbox_agents_as_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@
from agents.decorators import tool
from agents.run import RunConfig
from agents.sandbox import SandboxAgent, SandboxRunConfig
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from examples.sandbox.misc.example_support import text_manifest, tool_call_name
from examples.sandbox.misc.example_support import (
text_manifest,
tool_call_name,
unix_local_client,
)
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability

DEFAULT_QUESTION = (
Expand Down Expand Up @@ -157,8 +160,8 @@ async def main(model: str, question: str) -> None:
)

# Each sandbox-backed tool gets its own run configuration so the workspaces stay isolated.
pricing_run_config = RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient()))
rollout_run_config = RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient()))
pricing_run_config = RunConfig(sandbox=SandboxRunConfig(client=unix_local_client()))
rollout_run_config = RunConfig(sandbox=SandboxRunConfig(client=unix_local_client()))

orchestrator = Agent(
name="Revenue Operations Coordinator",
Expand Down
9 changes: 7 additions & 2 deletions examples/sandbox/shared_session_workdirs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import asyncio
import base64
import json
import sys
from pathlib import Path

from agents import ModelSettings, Runner, RunResult, ToolOutputImage
Expand All @@ -18,12 +19,16 @@
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Filesystem, Shell
from agents.sandbox.entries import BaseEntry, Dir, File
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
from agents.sandbox.session import BaseSandboxSession

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from examples.sandbox.misc.example_support import unix_local_client


async def main(*, model: str) -> None:
client = UnixLocalSandboxClient()
client = unix_local_client()
agent_a = _build_agent(name="Task A worker", model=model)
agent_b = _build_agent(name="Task B worker", model=model)
shared_sandbox = await client.create(manifest=_build_manifest())
Expand Down
Loading
Loading