fix(sandbox): scope provisioner PVC data by user (#2973)

* fix(sandbox): scope provisioner PVC data by user

* Address provisioner PVC review feedback
This commit is contained in:
魔力鸟
2026-05-17 15:23:42 +08:00
committed by GitHub
parent c0233cae26
commit e74e126ed3
7 changed files with 92 additions and 29 deletions
@@ -21,6 +21,8 @@ import logging
import requests
from deerflow.runtime.user_context import get_effective_user_id
from .backend import SandboxBackend
from .sandbox_info import SandboxInfo
@@ -138,6 +140,7 @@ class RemoteSandboxBackend(SandboxBackend):
json={
"sandbox_id": sandbox_id,
"thread_id": thread_id,
"user_id": get_effective_user_id(),
},
timeout=30,
)
@@ -1,11 +1,13 @@
"""Tests for AioSandboxProvider mount helpers."""
import importlib
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
from deerflow.config.paths import Paths, join_host_path
from deerflow.runtime.user_context import reset_current_user, set_current_user
# ── ensure_thread_dirs ───────────────────────────────────────────────────────
@@ -136,3 +138,36 @@ def test_discover_or_create_only_unlocks_when_lock_succeeds(tmp_path, monkeypatc
provider._discover_or_create_with_lock("thread-5", "sandbox-5")
assert unlock_calls == []
def test_remote_backend_create_forwards_effective_user_id(monkeypatch):
"""Provisioner mode must receive user_id so PVC subPath matches user isolation."""
remote_mod = importlib.import_module("deerflow.community.aio_sandbox.remote_backend")
backend = remote_mod.RemoteSandboxBackend("http://provisioner:8002")
token = set_current_user(SimpleNamespace(id="user-7"))
posted: dict = {}
class _Response:
def raise_for_status(self):
return None
def json(self):
return {"sandbox_url": "http://sandbox.local"}
def _post(url, json, timeout): # noqa: A002 - mirrors requests.post kwarg
posted.update({"url": url, "json": json, "timeout": timeout})
return _Response()
monkeypatch.setattr(remote_mod.requests, "post", _post)
try:
backend.create("thread-42", "sandbox-42")
finally:
reset_current_user(token)
assert posted["url"] == "http://provisioner:8002/api/sandboxes"
assert posted["json"] == {
"sandbox_id": "sandbox-42",
"thread_id": "thread-42",
"user_id": "user-7",
}
+14 -8
View File
@@ -92,12 +92,19 @@ class TestBuildVolumeMounts:
userdata_mount = mounts[1]
assert userdata_mount.sub_path is None
def test_pvc_sets_subpath(self, provisioner_module):
"""PVC mode should set sub_path to threads/{thread_id}/user-data."""
def test_pvc_sets_user_scoped_subpath(self, provisioner_module):
"""PVC mode should include user_id in the user-data subPath."""
provisioner_module.USERDATA_PVC_NAME = "my-pvc"
mounts = provisioner_module._build_volume_mounts("thread-42", user_id="user-7")
userdata_mount = mounts[1]
assert userdata_mount.sub_path == "deer-flow/users/user-7/threads/thread-42/user-data"
def test_pvc_defaults_to_default_user_subpath(self, provisioner_module):
"""Older callers should still land under a stable default user namespace."""
provisioner_module.USERDATA_PVC_NAME = "my-pvc"
mounts = provisioner_module._build_volume_mounts("thread-42")
userdata_mount = mounts[1]
assert userdata_mount.sub_path == "threads/thread-42/user-data"
assert userdata_mount.sub_path == "deer-flow/users/default/threads/thread-42/user-data"
def test_skills_mount_read_only(self, provisioner_module):
"""Skills mount should always be read-only."""
@@ -146,13 +153,12 @@ class TestBuildPodVolumes:
pod = provisioner_module._build_pod("sandbox-1", "thread-1")
assert len(pod.spec.containers[0].volume_mounts) == 2
def test_pod_pvc_mode(self, provisioner_module):
"""Pod should use PVC volumes when PVC names are configured."""
def test_pod_pvc_mode_uses_user_scoped_subpath(self, provisioner_module):
"""Pod should use a user-scoped subPath for PVC user-data."""
provisioner_module.SKILLS_PVC_NAME = "skills-pvc"
provisioner_module.USERDATA_PVC_NAME = "userdata-pvc"
pod = provisioner_module._build_pod("sandbox-1", "thread-1")
pod = provisioner_module._build_pod("sandbox-1", "thread-1", user_id="user-7")
assert pod.spec.volumes[0].persistent_volume_claim is not None
assert pod.spec.volumes[1].persistent_volume_claim is not None
# subPath should be set on user-data mount
userdata_mount = pod.spec.containers[0].volume_mounts[1]
assert userdata_mount.sub_path == "threads/thread-1/user-data"
assert userdata_mount.sub_path == "deer-flow/users/user-7/threads/thread-1/user-data"
+5 -1
View File
@@ -144,7 +144,11 @@ def test_provisioner_create_returns_sandbox_info(monkeypatch):
def mock_post(url: str, json: dict, timeout: int):
assert url == "http://provisioner:8002/api/sandboxes"
assert json == {"sandbox_id": "abc123", "thread_id": "thread-1"}
assert json == {
"sandbox_id": "abc123",
"thread_id": "thread-1",
"user_id": "test-user-autouse",
}
assert timeout == 30
return _StubResponse(payload={"sandbox_id": "abc123", "sandbox_url": "http://k3s:31001"})