mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* feat(sandbox): share sandbox identity derivation and acquire serialization (#4741) Remote providers (AIO, E2B, BoxLite, Tenki, OpenSandbox) each inlined the same sha256(user:thread)[:16] sandbox-id expression and kept per-scope lock dicts that grew unboundedly until shutdown. This extracts both mechanisms into shared components without changing provider lifecycle, ids, capacity semantics, or public tool behavior: - sandbox/identity.py: keyword-only derive_sandbox_scope_token (byte-pinned compatibility contract) + is_sandbox_scope_token; per-provider golden vectors pin current behavior including BoxLite's raw-None quirk and each provider's private user_id resolution. - sandbox/acquire_serialization.py: AcquireSerializer — per-key lock table with holder/waiter refcount reclamation, bounded dedicated executor (async waits off both the event loop and the default executor), worker-owned cancellation cleanup (no event-loop callback dependency), idempotent close(). - Each provider adopts both components; AIO/E2B key by (user_id, thread_id) with acquire and (E2B) release serialized; BoxLite/Tenki/OpenSandbox key by derived sandbox id and offload the whole sync acquire to the serializer's executor so a cancelled awaiter cannot overlap a retried same-scope body (leaked-remote-VM regression caught in review). - thread_id=None acquires stay unserialized; provider shutdown()/reset() close the serializer; E2B capacity/ledger/reconciliation and AIO ownership/flock machinery untouched. - blocking-IO anchor proves contended OpenSandbox acquire_async stays off the event loop (teeth verified red/green); AGENTS.md documents the shared components. * refactor(sandbox): address review on acquire serialization (#5089) - Replace unreachable checkin branch with an assertion: run() returns False only after abandon(), which the except handler always re-raises; the old _checkin would have double-decremented the refcount. - Document the task.cancelling() == 0 assumption in hold_async. - Drop unused thread_id/user_id kwargs from BoxLite and Tenki _acquire_scope_locked (OpenSandbox still forwards them). * fix(sandbox): preserve request ContextVars in acquire executor bridge (#5089) loop.run_in_executor() does not copy contextvars, unlike the inherited SandboxProvider.acquire_async() which used asyncio.to_thread(). The BoxLite/OpenSandbox/Tenki acquire_async bridges introduced in this PR therefore dropped the request trace id (logged as trace_id=-). Add AcquireSerializer.run_on_executor(), which copies the calling context and runs the callable through ctx.run, and route all three providers through it. Add regression tests binding request_trace_context and verifying the worker thread observes it.
60 lines
2.3 KiB
Python
60 lines
2.3 KiB
Python
"""Golden-vector tests for the shared sandbox scope token (RFC #4741).
|
|
|
|
The expected strings below are LITERALS computed from the five providers'
|
|
current inline expressions as of 2026-08-30. They pin the compatibility
|
|
contract byte-for-byte; never recompute them from the implementation.
|
|
"""
|
|
|
|
from deerflow.sandbox.identity import (
|
|
SANDBOX_ID_VERSION,
|
|
derive_sandbox_scope_token,
|
|
is_sandbox_scope_token,
|
|
)
|
|
|
|
|
|
class TestGoldenVectors:
|
|
def test_ascii(self):
|
|
assert derive_sandbox_scope_token(user_id="alice", thread_id="thread-1") == "dc977b840cb8638e"
|
|
|
|
def test_empty_user(self):
|
|
# Tenki/OpenSandbox resolution path: user_id or ""
|
|
assert derive_sandbox_scope_token(user_id="", thread_id="t") == "983743815e8fe2ac"
|
|
|
|
def test_empty_thread(self):
|
|
assert derive_sandbox_scope_token(user_id="u", thread_id="") == "27e14d2b41b03178"
|
|
|
|
def test_non_ascii(self):
|
|
assert derive_sandbox_scope_token(user_id="用户", thread_id="线程") == "cfa8a5bf41ddff8d"
|
|
|
|
def test_literal_none_string(self):
|
|
# BoxLite quirk: user_id=None renders as the literal "None" (RFC #4741
|
|
# §2.2; pinned, NOT fixed — unifying is a separate behavior change).
|
|
assert derive_sandbox_scope_token(user_id="None", thread_id="abc") == "c0b80df1b97d187e"
|
|
|
|
def test_version_constant(self):
|
|
assert SANDBOX_ID_VERSION == 1
|
|
|
|
def test_keyword_only(self):
|
|
import inspect
|
|
|
|
sig = inspect.signature(derive_sandbox_scope_token)
|
|
for param in sig.parameters.values():
|
|
assert param.kind is inspect.Parameter.KEYWORD_ONLY
|
|
|
|
|
|
class TestShapeValidation:
|
|
def test_accepts_token(self):
|
|
token = derive_sandbox_scope_token(user_id="alice", thread_id="thread-1")
|
|
assert is_sandbox_scope_token(token) is True
|
|
|
|
def test_rejects_wrong_shape(self):
|
|
assert is_sandbox_scope_token("") is False
|
|
assert is_sandbox_scope_token("dc977b840cb8638") is False # 15 chars
|
|
assert is_sandbox_scope_token("dc977b840cb8638ee") is False # 17 chars
|
|
assert is_sandbox_scope_token("DC977B840CB8638E") is False # uppercase
|
|
assert is_sandbox_scope_token("local:alice:thread-1") is False
|
|
|
|
def test_rejects_non_str(self):
|
|
assert is_sandbox_scope_token(None) is False
|
|
assert is_sandbox_scope_token(123) is False
|