From 7b3c3608532f68b0e4c3ceea5b67c4b2d0008d8c Mon Sep 17 00:00:00 2001 From: Eilen6316 Date: Fri, 8 May 2026 15:33:40 +0800 Subject: [PATCH] fix: keep new agent bootstrap in user scope --- backend/app/gateway/services.py | 19 +++++++ .../tools/builtins/setup_agent_tool.py | 9 ++- backend/tests/test_gateway_services.py | 15 +++++ backend/tests/test_setup_agent_tool.py | 22 ++++++++ .../src/app/workspace/agents/new/page.tsx | 56 +++---------------- frontend/src/core/i18n/locales/en-US.ts | 2 +- frontend/src/core/i18n/locales/zh-CN.ts | 2 +- 7 files changed, 75 insertions(+), 50 deletions(-) diff --git a/backend/app/gateway/services.py b/backend/app/gateway/services.py index 634b8b9d14..0cbea4fafd 100644 --- a/backend/app/gateway/services.py +++ b/backend/app/gateway/services.py @@ -136,6 +136,24 @@ def merge_run_context_overrides(config: dict[str, Any], context: Mapping[str, An runtime_context.setdefault(key, context[key]) +def inject_authenticated_user_context(config: dict[str, Any], request: Request) -> None: + """Stamp the authenticated user into the run context for background tools. + + Tool execution may happen after the request handler has returned, so tools + that persist user-scoped files should not rely only on ambient ContextVars. + The value comes from server-side auth state, never from client context. + """ + + user = getattr(request.state, "user", None) + user_id = getattr(user, "id", None) + if user_id is None: + return + + runtime_context = config.setdefault("context", {}) + if isinstance(runtime_context, dict): + runtime_context["user_id"] = str(user_id) + + def resolve_agent_factory(assistant_id: str | None): """Resolve the agent factory callable from config. @@ -288,6 +306,7 @@ async def start_run( # that carries agent configuration (model_name, thinking_enabled, etc.). # Only agent-relevant keys are forwarded; unknown keys (e.g. thread_id) are ignored. merge_run_context_overrides(config, getattr(body, "context", None)) + inject_authenticated_user_context(config, request) stream_modes = normalize_stream_modes(body.stream_mode) diff --git a/backend/packages/harness/deerflow/tools/builtins/setup_agent_tool.py b/backend/packages/harness/deerflow/tools/builtins/setup_agent_tool.py index 5ea591f76d..97929ad565 100644 --- a/backend/packages/harness/deerflow/tools/builtins/setup_agent_tool.py +++ b/backend/packages/harness/deerflow/tools/builtins/setup_agent_tool.py @@ -13,6 +13,13 @@ logger = logging.getLogger(__name__) +def _get_runtime_user_id(runtime: Runtime) -> str: + context_user_id = runtime.context.get("user_id") if runtime.context else None + if context_user_id: + return str(context_user_id) + return get_effective_user_id() + + @tool def setup_agent( soul: str, @@ -38,7 +45,7 @@ def setup_agent( if agent_name: # Custom agents are persisted under the current user's bucket so # different users do not see each other's agents. - user_id = get_effective_user_id() + user_id = _get_runtime_user_id(runtime) agent_dir = paths.user_agent_dir(user_id, agent_name) else: # Default agent (no agent_name): SOUL.md lives at the global base dir. diff --git a/backend/tests/test_gateway_services.py b/backend/tests/test_gateway_services.py index 013991b829..b024405b5c 100644 --- a/backend/tests/test_gateway_services.py +++ b/backend/tests/test_gateway_services.py @@ -324,6 +324,21 @@ def test_context_does_not_override_existing_configurable(): assert config["configurable"]["subagent_enabled"] is True +def test_inject_authenticated_user_context_overrides_client_user_id(): + """Run context should carry the authenticated user, not client-supplied user_id.""" + from types import SimpleNamespace + + from app.gateway.services import build_run_config, inject_authenticated_user_context + + config = build_run_config("thread-1", None, None) + config["context"] = {"user_id": "spoofed-client"} + request = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(id="auth-user-42"))) + + inject_authenticated_user_context(config, request) + + assert config["context"]["user_id"] == "auth-user-42" + + # --------------------------------------------------------------------------- # build_run_config — context / configurable precedence (LangGraph >= 0.6.0) # --------------------------------------------------------------------------- diff --git a/backend/tests/test_setup_agent_tool.py b/backend/tests/test_setup_agent_tool.py index 0de56d641b..92469ab2e2 100644 --- a/backend/tests/test_setup_agent_tool.py +++ b/backend/tests/test_setup_agent_tool.py @@ -6,6 +6,8 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch +import pytest + from deerflow.tools.builtins.setup_agent_tool import setup_agent # --- Helpers --- @@ -126,3 +128,23 @@ def test_successful_setup_creates_files(self, tmp_path: Path): assert agent_dir.exists() assert (agent_dir / "SOUL.md").read_text() == "# My Agent" assert (agent_dir / "config.yaml").exists() + + @pytest.mark.no_auto_user + def test_runtime_user_id_used_when_contextvar_missing(self, tmp_path: Path): + """setup_agent should not fall back to default when runtime carries user_id.""" + runtime = _DummyRuntime( + context={"agent_name": "test-agent", "user_id": "auth-user-42"}, + tool_call_id="tool-3", + ) + + with patch("deerflow.tools.builtins.setup_agent_tool.get_paths", return_value=_make_paths_mock(tmp_path)): + setup_agent.func( + soul="# My Agent", + description="A test agent", + runtime=runtime, + ) + + expected_dir = tmp_path / "users" / "auth-user-42" / "agents" / "test-agent" + default_dir = tmp_path / "users" / "default" / "agents" / "test-agent" + assert (expected_dir / "SOUL.md").read_text() == "# My Agent" + assert not default_dir.exists() diff --git a/frontend/src/app/workspace/agents/new/page.tsx b/frontend/src/app/workspace/agents/new/page.tsx index d15d97ff9c..a0337cf648 100644 --- a/frontend/src/app/workspace/agents/new/page.tsx +++ b/frontend/src/app/workspace/agents/new/page.tsx @@ -35,7 +35,6 @@ import { AgentNameCheckError, AgentsApiDisabledError, checkAgentName, - createAgent, getAgent, } from "@/core/agents/api"; import { useI18n } from "@/core/i18n/hooks"; @@ -71,20 +70,6 @@ async function getAgentWithRetry(agentName: string) { return null; } -function getCreateAgentErrorMessage( - error: unknown, - networkErrorMessage: string, - fallbackMessage: string, -) { - if (error instanceof TypeError && error.message === "Failed to fetch") { - return networkErrorMessage; - } - if (error instanceof Error && error.message) { - return error.message; - } - return fallbackMessage; -} - export default function NewAgentPage() { const { t } = useI18n(); const router = useRouter(); @@ -93,7 +78,6 @@ export default function NewAgentPage() { const [nameInput, setNameInput] = useState(""); const [nameError, setNameError] = useState(""); const [isCheckingName, setIsCheckingName] = useState(false); - const [isCreatingAgent, setIsCreatingAgent] = useState(false); const [agentName, setAgentName] = useState(""); const [agent, setAgent] = useState(null); const [showSaveHint, setShowSaveHint] = useState(false); @@ -170,36 +154,16 @@ export default function NewAgentPage() { setIsCheckingName(false); } - setIsCreatingAgent(true); - try { - await createAgent({ - name: trimmed, - description: "", - soul: "", - }); - } catch (err) { - if (err instanceof AgentsApiDisabledError) { - setNameError(t.agents.nameStepApiDisabledError); - return; - } - setNameError( - getCreateAgentErrorMessage( - err, - t.agents.nameStepNetworkError, - t.agents.nameStepCheckError, - ), - ); - return; - } finally { - setIsCreatingAgent(false); - } - setAgentName(trimmed); setStep("chat"); - await sendMessage(threadId, { - text: t.agents.nameStepBootstrapMessage.replace("{name}", trimmed), - files: [], - }); + await sendMessage( + threadId, + { + text: t.agents.nameStepBootstrapMessage.replace("{name}", trimmed), + files: [], + }, + { agent_name: trimmed }, + ); }, [ nameInput, sendMessage, @@ -345,9 +309,7 @@ export default function NewAgentPage() { diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts index d2b9538b94..bcca2bbd69 100644 --- a/frontend/src/core/i18n/locales/en-US.ts +++ b/frontend/src/core/i18n/locales/en-US.ts @@ -207,7 +207,7 @@ export const enUS: Translations = { nameStepApiDisabledError: "Custom agent management is not enabled on this server. Please contact your administrator.", nameStepBootstrapMessage: - "The new custom agent name is {name}. Let's bootstrap it's **SOUL**.", + "The new custom agent name is {name}. Help me design its purpose, behavior, and SOUL.md before saving it.", save: "Save agent", saving: "Saving agent...", saveRequested: diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index c4fc6b9457..a37da251b5 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -195,7 +195,7 @@ export const zhCN: Translations = { nameStepApiDisabledError: "服务器未开启自定义智能体管理功能,请联系管理员。", nameStepBootstrapMessage: - "新智能体的名称是 {name},现在开始为它生成 **SOUL**。", + "新智能体的名称是 {name}。请先帮我设计它的用途、行为方式和 SOUL.md,再保存它。", save: "保存智能体", saving: "正在保存智能体...", saveRequested: