feat(isolation): wire user_id through all Paths and memory callsites

Pass user_id=get_effective_user_id() at every callsite that invokes
Paths methods or memory functions, enabling per-user filesystem isolation
throughout the harness and app layers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rayhpeng
2026-04-12 15:16:23 +08:00
parent 9af2f3e73c
commit 7ce9333200
24 changed files with 137 additions and 70 deletions
+37 -10
View File
@@ -1241,7 +1241,10 @@ class TestMemoryManagement:
with patch("deerflow.agents.memory.updater.import_memory_data", return_value=imported) as mock_import:
result = client.import_memory(imported)
mock_import.assert_called_once_with(imported)
assert mock_import.call_count == 1
call_args = mock_import.call_args
assert call_args.args == (imported,)
assert "user_id" in call_args.kwargs
assert result == imported
def test_reload_memory(self, client):
@@ -1487,9 +1490,12 @@ class TestUploads:
class TestArtifacts:
def test_get_artifact(self, client):
from deerflow.runtime.user_context import get_effective_user_id
with tempfile.TemporaryDirectory() as tmp:
paths = Paths(base_dir=tmp)
outputs = paths.sandbox_outputs_dir("t1")
user_id = get_effective_user_id()
outputs = paths.sandbox_outputs_dir("t1", user_id=user_id)
outputs.mkdir(parents=True)
(outputs / "result.txt").write_text("artifact content")
@@ -1500,9 +1506,12 @@ class TestArtifacts:
assert "text" in mime
def test_get_artifact_not_found(self, client):
from deerflow.runtime.user_context import get_effective_user_id
with tempfile.TemporaryDirectory() as tmp:
paths = Paths(base_dir=tmp)
paths.sandbox_user_data_dir("t1").mkdir(parents=True)
user_id = get_effective_user_id()
paths.sandbox_outputs_dir("t1", user_id=user_id).mkdir(parents=True)
with patch("deerflow.client.get_paths", return_value=paths):
with pytest.raises(FileNotFoundError):
@@ -1513,9 +1522,12 @@ class TestArtifacts:
client.get_artifact("t1", "bad/path/file.txt")
def test_get_artifact_path_traversal(self, client):
from deerflow.runtime.user_context import get_effective_user_id
with tempfile.TemporaryDirectory() as tmp:
paths = Paths(base_dir=tmp)
paths.sandbox_user_data_dir("t1").mkdir(parents=True)
user_id = get_effective_user_id()
paths.sandbox_outputs_dir("t1", user_id=user_id).mkdir(parents=True)
with patch("deerflow.client.get_paths", return_value=paths):
with pytest.raises(PathTraversalError):
@@ -1699,13 +1711,16 @@ class TestScenarioFileLifecycle:
def test_upload_then_read_artifact(self, client):
"""Upload a file, simulate agent producing artifact, read it back."""
from deerflow.runtime.user_context import get_effective_user_id
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
uploads_dir = tmp_path / "uploads"
uploads_dir.mkdir()
paths = Paths(base_dir=tmp_path)
outputs_dir = paths.sandbox_outputs_dir("t-artifact")
user_id = get_effective_user_id()
outputs_dir = paths.sandbox_outputs_dir("t-artifact", user_id=user_id)
outputs_dir.mkdir(parents=True)
# Upload phase
@@ -1955,11 +1970,14 @@ class TestScenarioThreadIsolation:
def test_artifacts_isolated_per_thread(self, client):
"""Artifacts in thread-A are not accessible from thread-B."""
from deerflow.runtime.user_context import get_effective_user_id
with tempfile.TemporaryDirectory() as tmp:
paths = Paths(base_dir=tmp)
outputs_a = paths.sandbox_outputs_dir("thread-a")
user_id = get_effective_user_id()
outputs_a = paths.sandbox_outputs_dir("thread-a", user_id=user_id)
outputs_a.mkdir(parents=True)
paths.sandbox_user_data_dir("thread-b").mkdir(parents=True)
paths.sandbox_outputs_dir("thread-b", user_id=user_id).mkdir(parents=True)
(outputs_a / "result.txt").write_text("thread-a artifact")
with patch("deerflow.client.get_paths", return_value=paths):
@@ -2864,9 +2882,12 @@ class TestUploadDeleteSymlink:
class TestArtifactHardening:
def test_artifact_directory_rejected(self, client):
"""get_artifact rejects paths that resolve to a directory."""
from deerflow.runtime.user_context import get_effective_user_id
with tempfile.TemporaryDirectory() as tmp:
paths = Paths(base_dir=tmp)
subdir = paths.sandbox_outputs_dir("t1") / "subdir"
user_id = get_effective_user_id()
subdir = paths.sandbox_outputs_dir("t1", user_id=user_id) / "subdir"
subdir.mkdir(parents=True)
with patch("deerflow.client.get_paths", return_value=paths):
@@ -2875,9 +2896,12 @@ class TestArtifactHardening:
def test_artifact_leading_slash_stripped(self, client):
"""Paths with leading slash are handled correctly."""
from deerflow.runtime.user_context import get_effective_user_id
with tempfile.TemporaryDirectory() as tmp:
paths = Paths(base_dir=tmp)
outputs = paths.sandbox_outputs_dir("t1")
user_id = get_effective_user_id()
outputs = paths.sandbox_outputs_dir("t1", user_id=user_id)
outputs.mkdir(parents=True)
(outputs / "file.txt").write_text("content")
@@ -2991,9 +3015,12 @@ class TestBugArtifactPrefixMatchTooLoose:
def test_exact_prefix_without_subpath_accepted(self, client):
"""Bare 'mnt/user-data' is accepted (will later fail as directory, not at prefix)."""
from deerflow.runtime.user_context import get_effective_user_id
with tempfile.TemporaryDirectory() as tmp:
paths = Paths(base_dir=tmp)
paths.sandbox_user_data_dir("t1").mkdir(parents=True)
user_id = get_effective_user_id()
paths.sandbox_outputs_dir("t1", user_id=user_id).mkdir(parents=True)
with patch("deerflow.client.get_paths", return_value=paths):
# Accepted at prefix check, but fails because it's a directory.