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
14 changes: 14 additions & 0 deletions src/agents/sandbox/manifest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import abc
import inspect
import os
from collections.abc import Iterator, Mapping
from dataclasses import dataclass
from pathlib import Path, PurePath, PurePosixPath
Expand Down Expand Up @@ -147,6 +148,19 @@ async def resolve(self) -> str:
return self.value


class OsEnvValue(EnvValue):
"""Reads the value from the environment of the process creating the sandbox.

An unset variable resolves to the empty string.
"""

type: Literal["os_env"] = "os_env"
name: str

async def resolve(self) -> str:
return os.environ.get(self.name, "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require trusted host-env bindings

When a manifest or serialized session-state payload is not fully trusted, this lets that payload choose any variable name and have the SDK process read it during manifest.environment.resolve() before create/exec/resume, so a manifest containing {"type":"os_env","name":"OPENAI_API_KEY"} would copy the host API key into the sandbox environment. That makes serialized manifest data itself authorize host secret access; keep OS environment references as trusted application-side configuration or require an explicit allowlist/rebind before resolving them.

AGENTS.md reference: AGENTS.md:L125-L125

Useful? React with 👍 / 👎.



def _serialize_env_value_with_type(value: EnvValue, serialized: object) -> dict[str, Any]:
if EnvValue._subclass_registry.get(value.type) is not type(value):
raise PydanticSerializationError(
Expand Down
3 changes: 2 additions & 1 deletion tests/sandbox/test_compatibility_guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
RcloneMountPattern,
S3FilesMountPattern,
)
from agents.sandbox.manifest import EnvValue, StrEnvValue
from agents.sandbox.manifest import EnvValue, OsEnvValue, StrEnvValue
from agents.sandbox.session.sandbox_client import BaseSandboxClientOptions
from agents.sandbox.session.sandbox_session_state import SandboxSessionState
from agents.sandbox.snapshot import LocalSnapshot, NoopSnapshot, RemoteSnapshot, SnapshotBase
Expand Down Expand Up @@ -894,6 +894,7 @@ def test_core_discriminator_type_strings_are_stable() -> None:
InContainerMountStrategy: "in_container",
DockerVolumeMountStrategy: "docker_volume",
StrEnvValue: "str",
OsEnvValue: "os_env",
}

for cls, expected_type in expected_types.items():
Expand Down
65 changes: 64 additions & 1 deletion tests/sandbox/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@
MountpointMountPattern,
)
from agents.sandbox.errors import InvalidManifestPathError
from agents.sandbox.manifest import EnvEntry, Environment, EnvValue, Manifest, StrEnvValue
from agents.sandbox.manifest import (
EnvEntry,
Environment,
EnvValue,
Manifest,
OsEnvValue,
StrEnvValue,
)
from agents.sandbox.manifest_render import _truncate_manifest_description


Expand Down Expand Up @@ -340,6 +347,62 @@ def test_manifest_round_trips_str_env_value() -> None:
}


@pytest.mark.asyncio
async def test_os_env_value_resolves_from_the_process_environment(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("SANDBOX_TEST_OS_ENV_VALUE", "from-host")

assert await OsEnvValue(name="SANDBOX_TEST_OS_ENV_VALUE").resolve() == "from-host"


@pytest.mark.asyncio
async def test_os_env_value_resolves_unset_and_empty_variables_to_an_empty_string(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("SANDBOX_TEST_OS_ENV_VALUE", raising=False)

assert await OsEnvValue(name="SANDBOX_TEST_OS_ENV_VALUE").resolve() == ""

monkeypatch.setenv("SANDBOX_TEST_OS_ENV_VALUE", "")

assert await OsEnvValue(name="SANDBOX_TEST_OS_ENV_VALUE").resolve() == ""


def test_manifest_round_trips_os_env_value() -> None:
manifest = Manifest(
environment=Environment(value={"TOKEN": OsEnvValue(name="SANDBOX_TEST_OS_ENV_VALUE")})
)

payload = manifest.model_dump(mode="json")
restored = Manifest.model_validate(payload)

assert payload["environment"] == {
"value": {"TOKEN": {"type": "os_env", "name": "SANDBOX_TEST_OS_ENV_VALUE"}}
}
assert type(restored.environment.value["TOKEN"]) is OsEnvValue


@pytest.mark.asyncio
async def test_environment_resolves_os_env_values_alongside_literals(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("SANDBOX_TEST_OS_ENV_VALUE", "from-host")
environment = Environment(
value={
"PLAIN": "literal",
"DIRECT": OsEnvValue(name="SANDBOX_TEST_OS_ENV_VALUE"),
"ENTRY": EnvEntry(value=OsEnvValue(name="SANDBOX_TEST_OS_ENV_VALUE")),
}
)

assert await environment.resolve() == {
"PLAIN": "literal",
"DIRECT": "from-host",
"ENTRY": "from-host",
}


def test_manifest_reads_legacy_discriminator_free_str_env_values() -> None:
payload = {
"environment": {
Expand Down
Loading