diff --git a/apodex/tests/test_deployment_config.py b/apodex/tests/test_deployment_config.py index b412924..7213fb5 100644 --- a/apodex/tests/test_deployment_config.py +++ b/apodex/tests/test_deployment_config.py @@ -39,8 +39,12 @@ def test_default_compose_pulls_release_image_and_preserves_cli_state() -> None: assert agent["pull_policy"] == "always" assert agent["environment"]["APODEX_IN_CONTAINER"] == "1" assert agent["environment"]["SANDBOX_BACKEND"] == "container" + assert agent["environment"]["FRONTIER_AGENT_REQUIRE_TOOL_USER"] == "1" assert "security_opt" not in agent assert ".:/project" in agent["volumes"] + # Compose injects these files into the harness environment, but the + # project bind must not expose their on-disk contents to tool commands. + assert "/dev/null:/project/.env:ro" in agent["volumes"] assert agent["working_dir"] == "/project" assert "./.apodex/runs:/apodex-runs" in agent["volumes"] assert "frontier-agent-inputs:/inputs:ro" in agent["volumes"] @@ -58,6 +62,18 @@ def test_default_compose_pulls_release_image_and_preserves_cli_state() -> None: ) assert agent["environment"]["APODEX_WORKSPACE_LINK"] == "/workspace" + evaluator = compose["services"]["eval"] + assert evaluator["environment"]["SANDBOX_BACKEND"] == "bwrap" + assert evaluator["environment"]["SANDBOX_PROFILE"] == "service" + + for overlay, filename in ( + ("compose.sglang.yaml", ".env.sglang"), + ("compose.transformers.yaml", ".env.transformers"), + ): + assert f"/dev/null:/project/{filename}:ro" in ( + _yaml(overlay)["services"]["agent"]["volumes"] + ) + def test_development_compose_is_the_only_compose_file_that_builds() -> None: compose = _yaml("compose.yaml") diff --git a/compose.sglang.yaml b/compose.sglang.yaml index 8307bbb..4ee62d4 100644 --- a/compose.sglang.yaml +++ b/compose.sglang.yaml @@ -65,6 +65,8 @@ services: max-file: "3" agent: + volumes: + - /dev/null:/project/.env.sglang:ro depends_on: model: condition: service_healthy diff --git a/compose.transformers.yaml b/compose.transformers.yaml index 2626756..7bbe894 100644 --- a/compose.transformers.yaml +++ b/compose.transformers.yaml @@ -44,6 +44,8 @@ services: restart: unless-stopped agent: + volumes: + - /dev/null:/project/.env.transformers:ro depends_on: model: condition: service_healthy diff --git a/compose.yaml b/compose.yaml index d8b0644..e3aa4e1 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,6 +9,10 @@ services: APODEX_IN_CONTAINER: "1" HOME: /root SANDBOX_BACKEND: container + # A missing agent-tool account would expose the harness environment to + # model-authored commands through /proc. Service deployments must stop + # instead of accepting the documented env-only degradation. + FRONTIER_AGENT_REQUIRE_TOOL_USER: "1" APODEX_LOCAL_UTC_OFFSET: ${APODEX_LOCAL_UTC_OFFSET:-+0000} FRONTIER_AGENT_WORKSPACE_DIR: /workspace APODEX_SESSION_WORKSPACES_ROOT: /apodex-runs @@ -31,6 +35,9 @@ services: APODEX_HOST_GID: ${APODEX_HOST_GID:-} volumes: - .:/project + # The harness receives .env through env_file above. Hide the on-disk + # copy from model commands, whose host-mapped UID can read /project. + - /dev/null:/project/.env:ro - ./.apodex/runs:/apodex-runs - frontier-agent-inputs:/apodex-inputs - frontier-agent-inputs:/inputs:ro @@ -51,6 +58,9 @@ services: # The eval runner can execute multiple isolated benchmark tasks in one # harness container, so it retains the nested bubblewrap backend. SANDBOX_BACKEND: bwrap + # Multi-task workers need a private PID namespace and fresh procfs. The + # service profile fails closed when the runtime cannot provide them. + SANDBOX_PROFILE: service APODEX_HOST_UID: ${APODEX_HOST_UID:-} APODEX_HOST_GID: ${APODEX_HOST_GID:-} volumes: diff --git a/docs/install/docker.md b/docs/install/docker.md index 4a8f433..75c220d 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -34,6 +34,11 @@ Compose writes session records and deliverables to `.apodex/runs//`. Its named state volume is retained for legacy sessions. Attached inputs are copied into a separate volume that tools can only read. See [run artifacts and timestamps](../run-artifacts.md) for the on-disk layout. +The agent receives `.env` through its process environment; Compose masks the +on-disk file inside `/project` so model commands cannot read it. The SGLang and +Transformers overrides also mask their respective env files. Keep any custom +credential file outside the mounted project, since project files are available +to the agent by design. The convenience helper wraps the same thing: @@ -63,6 +68,7 @@ docker run --rm -it \ --env-file .env \ -e APODEX_IN_CONTAINER=1 \ -e SANDBOX_BACKEND=container \ + -e FRONTIER_AGENT_REQUIRE_TOOL_USER=1 \ -e FRONTIER_AGENT_WORKSPACE_DIR=/workspace \ -e APODEX_RUNS_ROOT=/apodex-runs \ -e APODEX_RUNS_ROOT_PINNED=1 \ @@ -71,6 +77,7 @@ docker run --rm -it \ -e APODEX_INPUT_STAGING_ROOT=/apodex-inputs \ -e FRONTIER_AGENT_INPUTS_ROOT=/inputs \ -v "$(pwd):/workspace" \ + -v /dev/null:/workspace/.env:ro \ -v "$(pwd)/.apodex/runs:/apodex-runs" \ -v frontier-agent-inputs:/apodex-inputs \ -v frontier-agent-inputs:/inputs:ro \