diff --git a/backend/packages/harness/deerflow/config/sandbox_config.py b/backend/packages/harness/deerflow/config/sandbox_config.py index d9aac4ab46..7aac600038 100644 --- a/backend/packages/harness/deerflow/config/sandbox_config.py +++ b/backend/packages/harness/deerflow/config/sandbox_config.py @@ -4,7 +4,20 @@ class VolumeMountConfig(BaseModel): """Configuration for a volume mount.""" - host_path: str = Field(..., description="Path on the host machine") + host_path: str = Field( + ..., + description=( + "Source path for the mount. Resolution depends on the active provider: " + "``LocalSandboxProvider`` checks this path from the gateway process — in " + "``make dev`` that is the host machine, but in Docker deployments " + "(``make up`` / docker-compose) it is the path *inside* the " + "``deer-flow-gateway`` container, so the host directory must also be " + "bind-mounted into the gateway service for the mount to take effect. " + "``AioSandboxProvider`` (DooD) passes this value straight to ``docker -v`` " + "for the sandbox container, where it is resolved by the host Docker daemon " + "from the host machine's perspective." + ), + ) container_path: str = Field(..., description="Path inside the container") read_only: bool = Field(default=False, description="Whether the mount is read-only") diff --git a/backend/packages/harness/deerflow/sandbox/local/local_sandbox_provider.py b/backend/packages/harness/deerflow/sandbox/local/local_sandbox_provider.py index 8b6b347ca3..9e65234577 100644 --- a/backend/packages/harness/deerflow/sandbox/local/local_sandbox_provider.py +++ b/backend/packages/harness/deerflow/sandbox/local/local_sandbox_provider.py @@ -147,7 +147,17 @@ def _setup_path_mappings(self) -> list[PathMapping]: mount.container_path, ) continue - # Ensure the host path exists before adding mapping + # Ensure the host path exists before adding mapping. + # + # ``host_path`` is resolved against the filesystem of the + # process running this provider — for ``make dev`` that is + # the host machine, but for ``make up`` it is the + # ``deer-flow-gateway`` container, so any host path that + # isn't bind-mounted into the gateway image will be missing + # here. Skipping silently makes this a high-cost-to-debug + # silent failure (sandbox skill / tool reads an empty dir + # instead of the configured mount), so escalate to ERROR + # and include actionable guidance. See #3244. if host_path.exists(): mappings.append( PathMapping( @@ -157,10 +167,16 @@ def _setup_path_mappings(self) -> list[PathMapping]: ) ) else: - logger.warning( - "Mount host_path does not exist, skipping: %s -> %s", + logger.error( + "sandbox.mounts entry %s -> %s ignored: host_path %s does not exist from the " + "perspective of the gateway process. In Docker deployments (make up / docker-compose), " + "this path must also be bind-mounted into the gateway container — add a matching " + "volume entry under services.gateway.volumes in docker/docker-compose.yaml (and use " + "the in-container path here), or run in local mode (make dev) where the gateway sees " + "the host filesystem directly.", mount.host_path, mount.container_path, + mount.host_path, ) except Exception as e: # Log but don't fail if config loading fails diff --git a/backend/tests/test_local_sandbox_provider_mounts.py b/backend/tests/test_local_sandbox_provider_mounts.py index add5c4ea66..a9b0ec63d6 100644 --- a/backend/tests/test_local_sandbox_provider_mounts.py +++ b/backend/tests/test_local_sandbox_provider_mounts.py @@ -612,6 +612,54 @@ def test_setup_path_mappings_skips_non_absolute_container_path(self, tmp_path): assert [m.container_path for m in provider._path_mappings] == ["/mnt/skills"] + def test_setup_path_mappings_logs_actionable_error_for_missing_host_path(self, tmp_path, caplog): + """Regression for #3244. + + When ``sandbox.mounts[].host_path`` is absent from the gateway process's + filesystem (the typical symptom in Docker production mode: host_path is a + host machine path that is not bind-mounted into the gateway container), + the mount is still skipped — but the failure must be a hard-to-miss ERROR + log with explicit, actionable guidance about Docker bind mounts, not the + old DEBUG/WARNING that buried the silent failure. + """ + skills_dir = tmp_path / "skills" + skills_dir.mkdir() + missing_host_path = tmp_path / "does-not-exist" + + from deerflow.config.sandbox_config import SandboxConfig, VolumeMountConfig + + sandbox_config = SandboxConfig( + use="deerflow.sandbox.local:LocalSandboxProvider", + mounts=[ + VolumeMountConfig(host_path=str(missing_host_path), container_path="/mnt/knowledge", read_only=True), + ], + ) + config = SimpleNamespace( + skills=SimpleNamespace(container_path="/mnt/skills", get_skills_path=lambda: skills_dir, use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage"), + sandbox=sandbox_config, + ) + + with caplog.at_level("ERROR", logger="deerflow.sandbox.local.local_sandbox_provider"): + with patch("deerflow.config.get_app_config", return_value=config): + provider = LocalSandboxProvider() + + # Silent-skip behaviour is preserved (no breaking change for existing deployments). + assert [m.container_path for m in provider._path_mappings] == ["/mnt/skills"] + + # The failure must be observable at ERROR level and reference the offending paths. + error_records = [r for r in caplog.records if r.levelname == "ERROR"] + assert error_records, "expected an ERROR log when host_path is missing" + message = "\n".join(r.getMessage() for r in error_records) + assert str(missing_host_path) in message + assert "/mnt/knowledge" in message + + # And it must include actionable Docker guidance so users don't lose hours + # to a silent empty-mount failure in production. + lowered = message.lower() + assert "docker" in lowered + assert "gateway" in lowered + assert "docker-compose" in lowered + def test_write_file_resolves_container_paths_in_content(self, tmp_path): """write_file should replace container paths in file content with local paths.""" data_dir = tmp_path / "data" diff --git a/config.example.yaml b/config.example.yaml index 4e5a1abcee..8e0e04540e 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -605,8 +605,12 @@ sandbox: allow_host_bash: false # Optional: Mount additional host directories into the sandbox. # Each mount maps a host path to a virtual container path accessible by the agent. + # Note: with LocalSandboxProvider under `make up` (docker-compose), host_path is + # checked from inside the deer-flow-gateway container — you must also bind-mount + # the same directory into services.gateway.volumes in docker/docker-compose.yaml + # for this mount to take effect (see issue #3244). # mounts: - # - host_path: /home/user/my-project # Absolute path on the host machine + # - host_path: /home/user/my-project # Absolute path; see note above for Docker mode # container_path: /mnt/my-project # Virtual path inside the sandbox # read_only: true # Whether the mount is read-only (default: false)