mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-12 22:20:33 +00:00
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:
parent
6eb4833598
commit
13e6fc29e5
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"),
|
||||
[
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user