Skip to content

fix(agents): anchor agent-name validation against a trailing newline - #5960

Merged
WillemJiang merged 2 commits into
bytedance:mainfrom
Lesereingrape:fix/agent-name-trailing-newline
Sep 30, 2026
Merged

WillemJiang merged 2 commits into
bytedance:mainfrom
Lesereingrape:fix/agent-name-trailing-newline

Conversation

@Lesereingrape

@Lesereingrape Lesereingrape commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Why

Trigger. While reading the validators around the custom-agent routes I found the same agent-name grammar enforced two different ways. config/agents_config.validate_agent_name, persistence/managed_subagents/base.py, app/channels/manager.py and scheduled_tasks.py all use AGENT_NAME_PATTERN.fullmatch(...), but _validate_agent_name in the Gateway router and DeerMem's inlined copy of the pattern still use .match(...). Python's $ also matches before a trailing newline, so .match("^[A-Za-z0-9-]+$") accepts "reviewer\n". This is the same defect class #5911 (fix(paths): reject trailing newlines in scoped IDs) just fixed for the user / integration / project IDs — the two agent-name copies were left behind.

Pain, measured on main. POST /api/agents {"name": "reviewer\n"} passes the router's 422 guard, is normalized, and reaches FileAgentStore.create(), which validates the same grammar with fullmatch and raises:

ERROR app.gateway.routers.agents: Failed to create agent 'reviewer
': Invalid agent name 'reviewer
'. Must match pattern: ^[A-Za-z0-9-]+$
ValueError: Invalid agent name 'reviewer\n'. Must match pattern: ^[A-Za-z0-9-]+$
  at app/gateway/routers/agents.py:373 -> persistence/agents/file.py:121 -> config/agents_config.py:171
AssertionError: assert 500 == 422

So the caller gets a 500 Internal Server Error — with the actionable message swallowed into the server log — for an input the API already rejects cleanly on every other route. On the memory side validate_agent_name("reviewer\n") returned None, and agent_facts_directory() built agents/reviewer\n/facts, a bucket the host's own strict validator refuses, so facts written there can never be read back through the agent APIs.

What changed

  • POST /api/agents (and GET/PUT/DELETE /api/agents/{name}, GET /agents/name/{name}) now answer 422 with the pattern message for a name that only differs by a trailing newline, instead of 500. Nothing that was previously a valid name changes: .match and .fullmatch on this grammar differ only for strings ending in exactly one newline.
  • DeerMem's validate_agent_name — and with it agent_facts_directory, agent_metadata_directory, agent_usage_path, agent_eviction_audit_path, fact_file_path and the users/*/agents/* scan in storage.py — rejects the same names.

Deliberately out of scope: DeerFlowClient.__init__ has the same .match call on this pattern (client.py:233), but open PR #4313 rewrites the constructor lines immediately above it, so including it here would guarantee a conflict. Tracked as #6032 so the one-word change is not lost after merge.

Surface area

  • Backend API — endpoint / SSE event / request-response shape under backend/app

Bug fix verification

  • Test path that reproduces the bug: backend/tests/test_custom_agent.py::test_agent_name_validation_rejects_trailing_newline[reviewer\n] and backend/tests/test_custom_agent.py::TestAgentsAPI::test_trailing_newline_in_agent_name_is_rejected[reviewer\n]
  • Did it go red on main and green on this branch? yes — on main both parametrised reviewer\n cases fail (Failed … assert 500 == 422, and DeerMem's validator returns instead of raising); on this branch tests/test_custom_agent.py is 83 passed.

Validation

  • uv run pytest tests/test_custom_agent.py → 83 passed (on main with only the tests added: 2 failed, 81 passed)
  • uv run pytest tests/test_memory_storage.py tests/test_deermem_self_contained.py tests/test_bench_deermem_scope_isolation.py tests/test_agents_router_model_settings.py tests/test_memory_cancel_by_agent.py tests/test_paths_user_isolation.py tests/blocking_io/test_agents_router.py tests/test_custom_agent.py → 264 passed
  • uv run pytest tests/test_memory*.py tests/test_bench_deermem*.py tests/test_harness_boundary.py → 889 passed, 1 skipped
  • uv run ruff format --check + uv run ruff check on the three changed files (ruff 0.15.12) → clean
  • I could not run make test / make lint (no make on this Windows host); the files above are the router's coverage plus every test module that imports the two validators this PR touches.

AI assistance

Tool(s) used: Qoder CLI agent (Claude), running the /contributor routine autonomously.

How you used it: the agent diffed the router's guard against the sibling validators that already use fullmatch, measured the 500 traceback above on main, wrote the failing tests first, then made the two one-word changes and re-ran the numbers quoted here. No human read the diff before submission, which is why the attestation box below is left unticked — it asserts that a person has read and takes responsibility for the change, and that person has not seen it yet.

  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output. (left for a human; see above)

Two copies of the shared ^[A-Za-z0-9-]+$ grammar still used re.match,
whose $ also matches before a final newline, while every other copy uses
fullmatch. POST /api/agents {"name": "reviewer\n"} therefore passed the
router guard and died inside the strict file store as an HTTP 500, and
DeerMem accepted the same name as an agents/{name}/facts directory.

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against 08a797c. The fix itself is correct and complete: in Python, $ only matches before a single trailing \n (not \r/\v/\f/U+2028/U+2029, which the character class already excludes), so fullmatch closes exactly the remaining gap, and no previously-valid name changes behavior. Coverage is complete: all five agent-name routes funnel through _validate_agent_name, DeerMem's path builders all go through validate_agent_name, and every other copy of this grammar (agents_config, managed_subagents, channels/manager, scheduled_tasks, benchmark contract) already used fullmatch. Deferring the last .match in DeerFlowClient.__init__ to avoid #4313 conflicts is reasonable (I verified that PR rewrites client.py), but I'd like it tracked — see inline. Only two minor notes below.

HTTPException: 422 if the name is invalid.
"""
if not AGENT_NAME_PATTERN.match(name):
if not AGENT_NAME_PATTERN.fullmatch(name):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix — this closes the last .match on this grammar except one: backend/packages/harness/deerflow/client.py:233 (DeerFlowClient.__init__) still validates with .match, so a trailing-newline name slips past it there too. Deferring to avoid conflicts with #4313 (which rewrites that file) is reasonable, but the deferral currently lives only in the PR body — could you open/link a follow-up issue so the one-word change isn't lost after merge?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #6032 to track the client.py:233 .match and linked it from the PR description; the change itself stays out of this PR because #4313 rewrites that file.

Comment thread backend/tests/test_custom_agent.py Outdated

@pytest.mark.parametrize("name", ["reviewer\n", "reviewer\n\n"])
def test_trailing_newline_in_agent_name_is_rejected(self, agent_client, name):
"""The router's ``AGENT_NAME_PATTERN.match`` accepted these and the store 500'd.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor accuracy nit: .match only accepted the "reviewer\n" param — "reviewer\n\n" was already rejected on main (Python's $ matches only before the single final newline), and the DeerMem test's "reviewer \n" param was too (the space is outside the class, so the match never reaches $). That matches your own main-branch run (2 of the 4 new params failed). Worth rewording so the docstring states the actual regression, e.g. "The router's AGENT_NAME_PATTERN.match accepted "reviewer\n" and the store 500'd" — otherwise a future reader may think \n\n needed this fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — reworded in cd9c12b. Verified the claim directly: against ^[A-Za-z0-9-]+$, .match accepts only "reviewer\n"; "reviewer\n\n" and "reviewer \n" never reach $. Both docstrings now name the single param that regressed and say the other one is parametrized to pin the grammar rather than to report a second fix.

…ally covered

.match only accepted "reviewer\n" on main; "reviewer\n\n" and "reviewer \n"
were already rejected, so the docstrings should not read as if every param in
these parametrizations was broken before the fix.
@WillemJiang
WillemJiang marked this pull request as ready for review September 30, 2026 11:28
@github-actions github-actions Bot added area:agents Agents, subagents, graph wiring, prompts, langgraph.json area:backend Gateway / runtime / core backend under backend/ needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines labels Sep 30, 2026
@WillemJiang WillemJiang added this to the 2.2.0 milestone Sep 30, 2026
@WillemJiang
WillemJiang merged commit f613727 into bytedance:main Sep 30, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agents Agents, subagents, graph wiring, prompts, langgraph.json area:backend Gateway / runtime / core backend under backend/ needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants