From 53c5195e5d4ffdf66f34be90251d97fdfe6fba56 Mon Sep 17 00:00:00 2001 From: kongxin Date: Thu, 12 Nov 2026 19:34:10 +0800 Subject: [PATCH] fix(gateway): name the agent problem when run admission rejects agent_name (#8087) * fix(gateway): name the agent problem when run admission rejects agent_name Since #5238, run admission loads the named custom agent's config for every run, with or without a knowledge scope, and folded every failure into "knowledge_scope assistant configuration could not be resolved". Messaging a chat whose custom agent was deleted, or passing a malformed agent_name, pointed callers at the knowledge base instead of the agent. The 422 status is unchanged; the detail now says which problem it is: - Agent '' not found - Invalid agent_name '': must contain only letters, digits, and hyphens after normalization. (shared with custom_agent_name(), which gains a field= argument; existing callers keep the default) - Invalid agent_name: expected a string, got . - Agent '' configuration could not be loaded * docs(changelog): reference #8087 for the run admission agent_name detail * fix(gateway): bound the agent name echoed in run admission errors Address review on #8087: admission now echoes the caller's raw agent_name, which start_run merges from config/context without a size check, so an oversized value was reflected verbatim into the 422 detail and any log line that records it. Cap the echoed name at 200 characters in the invalid-name message (shared with assistant_id validation) and in the not-found / unreadable-config messages. --------- Co-authored-by: Willem Jiang --- CHANGELOG.md | 8 ++ CHANGELOG_zh.md | 6 ++ backend/app/gateway/services.py | 34 ++++-- .../test_gateway_knowledge_scope_admission.py | 100 ++++++++++++++++++ backend/tests/test_runtime_lifecycle_e2e.py | 9 +- 5 files changed, 144 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c07a7efa1..1816c47f8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -390,6 +390,13 @@ This section accumulates work toward the **2.3.0** milestone there. Retrying in a completed, failed, running or paused batch is unchanged. ([#7792]) +- **gateway:** A run that names a missing or invalid custom agent now says so. + Messaging a chat whose custom agent was deleted, or passing a malformed + `agent_name`, returned the 422 detail "knowledge_scope assistant configuration + could not be resolved" even when no knowledge scope was involved. The 422 now + reports `Agent '' not found`, an invalid `agent_name`, or an agent + config that could not be loaded. ([#8087]) + - **agents:** `PUT /api/agents/{name}` with `"description": null` now resets the description to empty. It used to store the null, which the agent config rejects, so the agent then failed every load: the update, GET and repair @@ -16393,6 +16400,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#8071]: https://github.com/bytedance/deer-flow/issues/8071 [#8072]: https://github.com/bytedance/deer-flow/pull/8072 [#8079]: https://github.com/bytedance/deer-flow/pull/8079 +[#8087]: https://github.com/bytedance/deer-flow/pull/8087 [#8117]: https://github.com/bytedance/deer-flow/pull/8117 [#8118]: https://github.com/bytedance/deer-flow/pull/8118 [#8126]: https://github.com/bytedance/deer-flow/pull/8126 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index 088fa6ad65..e585e132e2 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -153,6 +153,11 @@ 为这些条目显示重试按钮。在已完成、失败、运行中或已暂停批次中重试的行为不变。 ([#7792]) +- **网关:** 运行指定的自定义智能体不存在或名称无效时,现在会明确说明原因。此前向一个 + 自定义智能体已被删除的对话发消息,或传入格式错误的 `agent_name`,即使与知识库范围无关, + 也会返回 422 和 "knowledge_scope assistant configuration could not be resolved"。现在 + 422 会提示 `Agent '' not found`、`agent_name` 无效,或智能体配置无法加载。([#8087]) + - **智能体:** `PUT /api/agents/{name}` 传入 `"description": null` 时现在会把描述重置为 空。此前会把 null 原样保存,而智能体配置不接受该值,导致之后每次加载都失败:更新本身、 GET 和修复性更新都返回 500,文件后端会把它从智能体列表中漏掉,数据库后端会让该用户的 @@ -12889,6 +12894,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#8043]: https://github.com/bytedance/deer-flow/pull/8043 [#8046]: https://github.com/bytedance/deer-flow/pull/8046 [#8064]: https://github.com/bytedance/deer-flow/pull/8064 +[#8087]: https://github.com/bytedance/deer-flow/pull/8087 [#8118]: https://github.com/bytedance/deer-flow/pull/8118 [#8126]: https://github.com/bytedance/deer-flow/pull/8126 [#8139]: https://github.com/bytedance/deer-flow/pull/8139 diff --git a/backend/app/gateway/services.py b/backend/app/gateway/services.py index a3e47c9aac..a371c7b899 100644 --- a/backend/app/gateway/services.py +++ b/backend/app/gateway/services.py @@ -1453,11 +1453,17 @@ def _clamp_recursion_limit(value: Any, max_limit: int, default_limit: int) -> in return min(value, max_limit) -def custom_agent_name(assistant_id: str | None) -> str | None: +# Error details echo caller-supplied agent names; bound them so an oversized +# value cannot bloat the response or any log line that records the detail. +_MAX_ECHOED_AGENT_NAME_CHARS = 200 + + +def custom_agent_name(assistant_id: str | None, *, field: str = "assistant_id") -> str | None: """Return the custom agent *assistant_id* selects, or ``None`` for the lead agent. Custom names are normalized (trimmed, lowercased, ``_`` to ``-``) and must - then contain only letters, digits and hyphens. + then contain only letters, digits and hyphens. *field* names the request + field in the error message. Raises: ValueError: *assistant_id* cannot name an agent. @@ -1466,7 +1472,7 @@ def custom_agent_name(assistant_id: str | None) -> str | None: return None normalized = assistant_id.strip().lower().replace("_", "-") if not normalized or not re.fullmatch(r"[a-z0-9-]+", normalized): - raise ValueError(f"Invalid assistant_id {assistant_id!r}: must contain only letters, digits, and hyphens after normalization.") + raise ValueError(f"Invalid {field} {assistant_id!r:.{_MAX_ECHOED_AGENT_NAME_CHARS}}: must contain only letters, digits, and hyphens after normalization.") return normalized @@ -2199,20 +2205,30 @@ async def _load_scope_agent_config( ) -> Any | None: if not assistant_id or assistant_id == _DEFAULT_ASSISTANT_ID: return None + # The caller's config/context ``agent_name`` arrives here unvalidated, and + # every run that names a custom agent passes through, with or without a + # knowledge scope, so each rejection says what is wrong with the agent. + if not isinstance(assistant_id, str): + raise HTTPException( + status_code=422, + detail=f"Invalid agent_name: expected a string, got {type(assistant_id).__name__}.", + ) + try: + normalized = custom_agent_name(assistant_id, field="agent_name") + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc try: - # The caller's config/context ``agent_name`` arrives here unvalidated. - if not isinstance(assistant_id, str): - raise ValueError("agent_name must be a string") - normalized = assistant_id.strip().lower().replace("_", "-") return await asyncio.to_thread( load_agent_config, normalized, user_id=user_id, ) - except (FileNotFoundError, ValueError) as exc: + except FileNotFoundError as exc: + raise HTTPException(status_code=422, detail=f"Agent '{normalized:.{_MAX_ECHOED_AGENT_NAME_CHARS}}' not found") from exc + except ValueError as exc: raise HTTPException( status_code=422, - detail="knowledge_scope assistant configuration could not be resolved", + detail=f"Agent '{normalized:.{_MAX_ECHOED_AGENT_NAME_CHARS}}' configuration could not be loaded", ) from exc diff --git a/backend/tests/test_gateway_knowledge_scope_admission.py b/backend/tests/test_gateway_knowledge_scope_admission.py index 80cece8162..5fc11bc510 100644 --- a/backend/tests/test_gateway_knowledge_scope_admission.py +++ b/backend/tests/test_gateway_knowledge_scope_admission.py @@ -210,3 +210,103 @@ def test_free_form_runtime_scope_fields_are_scrubbed() -> None: strip_internal_context_keys(config) assert config == {"context": {}, "configurable": {}} + + +# -- agent config loading during run admission ------------------------------ +# Every run that names a custom agent loads its config here, with or without a +# knowledge scope, so the 422 detail must say what is wrong with the agent. + + +@pytest.mark.anyio +async def test_scope_agent_lookup_reports_a_missing_agent(monkeypatch) -> None: + from app.gateway import services + + def _missing(name, *, user_id=None): + raise FileNotFoundError(name) + + monkeypatch.setattr(services, "load_agent_config", _missing) + + with pytest.raises(HTTPException) as excinfo: + await services._load_scope_agent_config(assistant_id="Ghost_Agent", user_id="u1") + + assert excinfo.value.status_code == 422 + assert excinfo.value.detail == "Agent 'ghost-agent' not found" + + +@pytest.mark.anyio +async def test_scope_agent_lookup_reports_an_unreadable_agent_config(monkeypatch) -> None: + from app.gateway import services + + def _corrupt(name, *, user_id=None): + raise ValueError("bad yaml") + + monkeypatch.setattr(services, "load_agent_config", _corrupt) + + with pytest.raises(HTTPException) as excinfo: + await services._load_scope_agent_config(assistant_id="broken-agent", user_id="u1") + + assert excinfo.value.status_code == 422 + assert excinfo.value.detail == "Agent 'broken-agent' configuration could not be loaded" + + +@pytest.mark.anyio +@pytest.mark.parametrize( + ("agent_name", "detail"), + [ + ("Bad Name!", "Invalid agent_name 'Bad Name!': must contain only letters, digits, and hyphens after normalization."), + (" ", "Invalid agent_name ' ': must contain only letters, digits, and hyphens after normalization."), + (1, "Invalid agent_name: expected a string, got int."), + ], +) +async def test_scope_agent_lookup_rejects_an_invalid_agent_name_without_loading(monkeypatch, agent_name, detail) -> None: + from app.gateway import services + + def _unexpected(name, *, user_id=None): + raise AssertionError("an invalid agent name must not reach the agent store") + + monkeypatch.setattr(services, "load_agent_config", _unexpected) + + with pytest.raises(HTTPException) as excinfo: + await services._load_scope_agent_config(assistant_id=agent_name, user_id="u1") + + assert excinfo.value.status_code == 422 + assert excinfo.value.detail == detail + + +@pytest.mark.anyio +async def test_scope_agent_lookup_bounds_an_oversized_invalid_agent_name(monkeypatch) -> None: + """Admission echoes the caller's raw ``agent_name``; a huge value must not bloat the 422 or logs.""" + from app.gateway import services + + def _unexpected(name, *, user_id=None): + raise AssertionError("an invalid agent name must not reach the agent store") + + monkeypatch.setattr(services, "load_agent_config", _unexpected) + oversized = "Bad Name!" + "x" * 100_000 + + with pytest.raises(HTTPException) as excinfo: + await services._load_scope_agent_config(assistant_id=oversized, user_id="u1") + + assert excinfo.value.status_code == 422 + assert excinfo.value.detail.startswith("Invalid agent_name 'Bad Name!xxx") + assert excinfo.value.detail.endswith(": must contain only letters, digits, and hyphens after normalization.") + assert len(excinfo.value.detail) < 400 + + +@pytest.mark.anyio +async def test_scope_agent_lookup_bounds_an_oversized_missing_agent_name(monkeypatch) -> None: + """A syntactically valid name has no length cap either, so the not-found detail is bounded too.""" + from app.gateway import services + + def _missing(name, *, user_id=None): + raise FileNotFoundError(name) + + monkeypatch.setattr(services, "load_agent_config", _missing) + + with pytest.raises(HTTPException) as excinfo: + await services._load_scope_agent_config(assistant_id="a" * 100_000, user_id="u1") + + assert excinfo.value.status_code == 422 + assert excinfo.value.detail.startswith("Agent 'aaa") + assert excinfo.value.detail.endswith("' not found") + assert len(excinfo.value.detail) < 400 diff --git a/backend/tests/test_runtime_lifecycle_e2e.py b/backend/tests/test_runtime_lifecycle_e2e.py index a89cdc3ef3..e8e84f991a 100644 --- a/backend/tests/test_runtime_lifecycle_e2e.py +++ b/backend/tests/test_runtime_lifecycle_e2e.py @@ -569,7 +569,7 @@ def test_stream_run_completes_and_persists_runtime_state(isolated_app): _CONFIG_SECTION_ERROR = "invalid_run_config_section" -_UNRESOLVED_AGENT_DETAIL = "knowledge_scope assistant configuration could not be resolved" +_NON_STRING_AGENT_DETAIL = "Invalid agent_name: expected a string, got int." @pytest.mark.parametrize( @@ -592,9 +592,10 @@ _UNRESOLVED_AGENT_DETAIL = "knowledge_scope assistant configuration could not be "Invalid assistant_id ' ': must contain only letters, digits, and hyphens after normalization.", id="assistant-id-blank", ), - pytest.param({"config": {"configurable": {"agent_name": 1}}}, 422, _UNRESOLVED_AGENT_DETAIL, id="configurable-agent-name"), - pytest.param({"config": {"context": {"agent_name": 1}}}, 422, _UNRESOLVED_AGENT_DETAIL, id="config-context-agent-name"), - pytest.param({"context": {"agent_name": 1}}, 422, _UNRESOLVED_AGENT_DETAIL, id="context-agent-name"), + pytest.param({"config": {"configurable": {"agent_name": 1}}}, 422, _NON_STRING_AGENT_DETAIL, id="configurable-agent-name"), + pytest.param({"config": {"context": {"agent_name": 1}}}, 422, _NON_STRING_AGENT_DETAIL, id="config-context-agent-name"), + pytest.param({"context": {"agent_name": 1}}, 422, _NON_STRING_AGENT_DETAIL, id="context-agent-name"), + pytest.param({"context": {"agent_name": "ghost-agent"}}, 422, "Agent 'ghost-agent' not found", id="context-agent-missing"), pytest.param( {"input": {"messages": [{"role": "user", "content": "hi"}, {"role": "ai", "content": "", "tool_calls": "x"}]}}, 400,