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 <noreply@anthropic.com>

* fix(opensandbox): validate server proxy boolean

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Totoro <msy626836554@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Bas Cancrinus 2026-11-12 02:49:33 +01:00 committed by GitHub
parent 6eb4833598
commit 13e6fc29e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 76 additions and 3 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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.

View File

@ -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"),
[

View File

@ -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