diff --git a/src/agents/sandbox/manifest.py b/src/agents/sandbox/manifest.py index 97a3677442..4ea0d4652f 100644 --- a/src/agents/sandbox/manifest.py +++ b/src/agents/sandbox/manifest.py @@ -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 @@ -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, "") + + 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( diff --git a/tests/sandbox/test_compatibility_guards.py b/tests/sandbox/test_compatibility_guards.py index 0e3a2bcea4..3c4ca3b2e8 100644 --- a/tests/sandbox/test_compatibility_guards.py +++ b/tests/sandbox/test_compatibility_guards.py @@ -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 @@ -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(): diff --git a/tests/sandbox/test_manifest.py b/tests/sandbox/test_manifest.py index 0f5eef6bc1..f8fb0ddb31 100644 --- a/tests/sandbox/test_manifest.py +++ b/tests/sandbox/test_manifest.py @@ -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 @@ -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": {