From 13e6fc29e54201ce650b5f48743f88444b4d46f5 Mon Sep 17 00:00:00 2001 From: Bas Cancrinus <7222814+bascan@users.noreply.github.com> Date: Thu, 12 Nov 2026 02:49:33 +0100 Subject: [PATCH] feat(opensandbox): add opt-in secure_access sandbox option (#8162) * feat(opensandbox): add opt-in secure_access sandbox option Forward a new `sandbox.secure_access` option (default false) to SandboxSync.create so OpenSandbox can require a per-sandbox endpoint token at the ingress gateway and server proxy. The server returns the token as an endpoint header and SDK 0.1.15 already attaches endpoint headers to execd and file requests, so DeerFlow needs no token handling. Non-boolean values are rejected at provider startup so a quoted "false" cannot silently enable it. Documented in config.example.yaml, CONFIGURATION.md, the SandboxConfig docstring and the provider README. Co-Authored-By: Claude Opus 5.5 * fix(opensandbox): validate server proxy boolean --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Totoro Co-authored-by: Willem Jiang --- backend/docs/CONFIGURATION.md | 5 +++ .../deerflow/community/opensandbox/AGENTS.md | 4 +++ .../deerflow/community/opensandbox/README.md | 14 ++++++++ .../community/opensandbox/provider.py | 16 +++++++-- .../harness/deerflow/config/sandbox_config.py | 3 ++ backend/tests/test_opensandbox_provider.py | 36 +++++++++++++++++++ config.example.yaml | 1 + 7 files changed, 76 insertions(+), 3 deletions(-) diff --git a/backend/docs/CONFIGURATION.md b/backend/docs/CONFIGURATION.md index 9f30e64a96..93454716de 100644 --- a/backend/docs/CONFIGURATION.md +++ b/backend/docs/CONFIGURATION.md @@ -1646,6 +1646,7 @@ sandbox: request_timeout: 30 # management request timeout seconds ready_timeout: 30 # create/readiness timeout seconds use_server_proxy: false # proxy execd/file traffic through server + secure_access: false # per-sandbox endpoint token; Kubernetes + gateway mode only sandbox_timeout: 14400 # remote lifetime; 0 = explicit cleanup bash_command_timeout: 600 # default remote command timeout seconds replicas: 3 # active + warm cap per gateway process @@ -1654,6 +1655,10 @@ sandbox: PYTHONUNBUFFERED: "1" ``` +`use_server_proxy` and `secure_access` accept YAML booleans. Omitted or `null` +values default to `false`. Quoted values such as `"false"` cause a provider +startup error. + Install the optional SDK before selecting this provider: ```bash diff --git a/backend/packages/harness/deerflow/community/opensandbox/AGENTS.md b/backend/packages/harness/deerflow/community/opensandbox/AGENTS.md index 4430e23674..440ad3c5db 100644 --- a/backend/packages/harness/deerflow/community/opensandbox/AGENTS.md +++ b/backend/packages/harness/deerflow/community/opensandbox/AGENTS.md @@ -6,6 +6,10 @@ General sandbox rules are in the [sandbox guide](../../sandbox/AGENTS.md). cd backend && python -m pytest tests -q -k "opensandbox" ``` +Validate `use_server_proxy` and `secure_access` at provider startup. Accept +booleans, default missing or null values to `false`, and reject strings and +other types before creating an SDK connection. + ## Command deadlines - **Only a `CommandExecError` with `context deadline exceeded` and no numeric diff --git a/backend/packages/harness/deerflow/community/opensandbox/README.md b/backend/packages/harness/deerflow/community/opensandbox/README.md index ae75adf09c..c51c83dc3f 100644 --- a/backend/packages/harness/deerflow/community/opensandbox/README.md +++ b/backend/packages/harness/deerflow/community/opensandbox/README.md @@ -22,6 +22,7 @@ sandbox: # request_timeout: 30 # ready_timeout: 30 # use_server_proxy: false + # secure_access: false # sandbox_timeout: 14400 # remote lifetime; 0 means explicit cleanup only # bash_command_timeout: 600 # default command timeout # replicas: 3 # active + warm sandboxes per gateway process @@ -35,6 +36,19 @@ sandbox: reach the OpenSandbox management service but cannot directly reach sandbox `execd` endpoints. +Use unquoted `true` or `false` for `use_server_proxy` and `secure_access`. +Omitted and `null` values default to `false`. Quoted booleans and other +non-boolean values cause a provider startup error. + +`secure_access: true` creates each sandbox with OpenSandbox +[Secure Access](https://github.com/opensandbox-group/OpenSandbox/blob/main/docs/guides/secure-access.md): +the ingress gateway, and the server proxy, then reject endpoint requests that +lack the sandbox's access token. The server issues the token with the endpoint +and the SDK attaches it to every execd and file request, so no token setting is +needed in DeerFlow. The server must run the Kubernetes runtime with +`[ingress] mode = "gateway"`; the Docker runtime and other ingress modes reject +the create with HTTP 400. + Values in `sandbox.environment` that start with `$` are resolved from the Gateway process environment once, when `config.yaml` loads; a missing variable fails the load. The provider injects the resolved values unchanged, so a secret diff --git a/backend/packages/harness/deerflow/community/opensandbox/provider.py b/backend/packages/harness/deerflow/community/opensandbox/provider.py index aef7422329..af169cb425 100644 --- a/backend/packages/harness/deerflow/community/opensandbox/provider.py +++ b/backend/packages/harness/deerflow/community/opensandbox/provider.py @@ -114,8 +114,16 @@ class OpenSandboxProvider(WarmPoolLifecycleMixin[OpenSandboxSandbox], SandboxPro def _load_config(self) -> dict[str, Any]: sandbox_config = get_app_config().sandbox - def option(name: str, default: Any = None) -> Any: - return getattr(sandbox_config, name, default) + def option(name: str) -> Any: + return getattr(sandbox_config, name, None) + + def bool_option(name: str) -> bool: + value = option(name) + if value is None: + return False + if not isinstance(value, bool): + raise ValueError(f"sandbox.{name} must be true or false") + return value api_key = option("api_key") domain = option("domain") @@ -143,7 +151,8 @@ class OpenSandboxProvider(WarmPoolLifecycleMixin[OpenSandboxSandbox], SandboxPro "domain": domain, "protocol": protocol, "request_timeout": self._positive_float("request_timeout", option("request_timeout"), DEFAULT_REQUEST_TIMEOUT), - "use_server_proxy": bool(option("use_server_proxy", False)), + "use_server_proxy": bool_option("use_server_proxy"), + "secure_access": bool_option("secure_access"), "image": option("image") or DEFAULT_IMAGE, "ready_timeout": self._positive_float("ready_timeout", option("ready_timeout"), DEFAULT_READY_TIMEOUT), "sandbox_timeout": None if sandbox_timeout == 0 else sandbox_timeout, @@ -310,6 +319,7 @@ class OpenSandboxProvider(WarmPoolLifecycleMixin[OpenSandboxSandbox], SandboxPro env=self._config["environment"] or None, metadata=metadata, connection_config=connection_config, + secure_access=self._config["secure_access"], ) try: bootstrap = remote.commands.run( diff --git a/backend/packages/harness/deerflow/config/sandbox_config.py b/backend/packages/harness/deerflow/config/sandbox_config.py index 8ba66767b6..b96e58fd58 100644 --- a/backend/packages/harness/deerflow/config/sandbox_config.py +++ b/backend/packages/harness/deerflow/config/sandbox_config.py @@ -225,6 +225,9 @@ class SandboxConfig(BaseModel): OpenSandboxProvider specific options: api_key, domain, protocol, request_timeout, use_server_proxy: OpenSandbox management and execd connection settings. + secure_access: Ask OpenSandbox to require a per-sandbox access token on + sandbox endpoints (default: false). Needs a Kubernetes runtime in + ingress gateway mode; other servers reject the create with 400. ready_timeout: Create/readiness deadline in seconds (default: 30). sandbox_timeout: Remote lifetime in seconds (default: 14400); 0 requires explicit provider cleanup. diff --git a/backend/tests/test_opensandbox_provider.py b/backend/tests/test_opensandbox_provider.py index c387b128bd..412322a4c8 100644 --- a/backend/tests/test_opensandbox_provider.py +++ b/backend/tests/test_opensandbox_provider.py @@ -388,6 +388,42 @@ def test_null_sandbox_timeout_uses_default(monkeypatch: pytest.MonkeyPatch) -> N provider.shutdown() +@pytest.mark.parametrize(("configured", "expected"), [({}, False), ({"secure_access": None}, False), ({"secure_access": False}, False), ({"secure_access": True}, True)]) +def test_create_forwards_secure_access(monkeypatch: pytest.MonkeyPatch, configured: dict[str, Any], expected: bool) -> None: + provider, sdk = _install(monkeypatch, config=configured) + + provider.acquire("thread-1", user_id="user-1") + + assert sdk.create_calls[0]["secure_access"] is expected + provider.shutdown() + + +@pytest.mark.parametrize("value", ["true", "false", 1, 0]) +def test_provider_rejects_non_boolean_secure_access(monkeypatch: pytest.MonkeyPatch, value: Any) -> None: + with pytest.raises(ValueError, match="sandbox.secure_access must be true or false"): + _install(monkeypatch, config={"secure_access": value}) + + +@pytest.mark.parametrize( + ("configured", "expected"), + [({}, False), ({"use_server_proxy": None}, False), ({"use_server_proxy": False}, False), ({"use_server_proxy": True}, True)], +) +def test_create_forwards_use_server_proxy(monkeypatch: pytest.MonkeyPatch, configured: dict[str, Any], expected: bool) -> None: + provider, sdk = _install(monkeypatch, config=configured) + + try: + provider.acquire("thread-1", user_id="user-1") + assert sdk.create_calls[0]["connection_config"].kwargs["use_server_proxy"] is expected + finally: + provider.shutdown() + + +@pytest.mark.parametrize("value", ["true", "false", 1, 0]) +def test_provider_rejects_non_boolean_use_server_proxy(monkeypatch: pytest.MonkeyPatch, value: Any) -> None: + with pytest.raises(ValueError, match=r"sandbox\.use_server_proxy must be true or false"): + _install(monkeypatch, config={"use_server_proxy": value}) + + @pytest.mark.parametrize( ("config_key", "value", "message"), [ diff --git a/config.example.yaml b/config.example.yaml index 2ec5aba56e..331aa46d8d 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -2012,6 +2012,7 @@ sandbox: # # request_timeout: 30 # finite positive management API request timeout seconds # # ready_timeout: 30 # finite positive create/readiness timeout seconds # # use_server_proxy: false # proxy execd/file traffic through server +# # secure_access: false # per-sandbox endpoint token; needs Kubernetes + ingress gateway mode # # sandbox_timeout: 14400 # finite non-negative remote lifetime seconds; 0 = explicit cleanup # # bash_command_timeout: 600 # default remote command timeout seconds # # replicas: 3 # active + warm cap per gateway process