Skip to content

Commit 2e255a9

Browse files
author
Mateusz
committed
fix(core): support factory-cli user agent in path fix handlers
- Update path fix logic to recognize 'factory' keyword in User-Agent - Fix issue where Droid (factory-cli) relative paths were not being normalized - Ensure consistent handling for both 'droid' and 'factory' agents - Add regression tests for production factory-cli User-Agent strings
1 parent 4349e65 commit 2e255a9

File tree

4 files changed

+123
-9
lines changed

4 files changed

+123
-9
lines changed

src/core/services/tool_call_handlers/droid_antigravity_path_fix_handler.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ async def can_handle(self, context: ToolCallContext) -> bool:
5858
5959
Returns True only if:
6060
1. Handler is enabled
61-
2. Agent contains "droid" (case-insensitive)
61+
2. Agent contains "droid" or "factory" (case-insensitive)
62+
- "droid" is the agent name
63+
- "factory" is the company that builds Droid (factory-cli user agent)
6264
3. Tool arguments contain a path that needs fixing
6365
6466
Args:
@@ -71,11 +73,13 @@ async def can_handle(self, context: ToolCallContext) -> bool:
7173
return False
7274

7375
# Check agent name (from calling_agent or context)
76+
# Droid sends User-Agent: factory-cli/X.Y.Z so we also check for "factory"
7477
agent_name = context.calling_agent or ""
75-
if "droid" not in agent_name.lower():
78+
agent_lower = agent_name.lower()
79+
if "droid" not in agent_lower and "factory" not in agent_lower:
7680
if logger.isEnabledFor(logging.DEBUG):
7781
logger.debug(
78-
"DroidAntigravityPathFix: agent '%s' doesn't contain 'droid'",
82+
"DroidAntigravityPathFix: agent '%s' doesn't contain 'droid' or 'factory'",
7983
agent_name,
8084
)
8185
return False

src/core/services/tool_call_reactor_middleware.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -761,14 +761,16 @@ def _maybe_fix_droid_antigravity_path(
761761
isn't registered, we still want to avoid the "absolute path required" errors
762762
by normalizing obvious relative paths emitted by the backend.
763763
764-
This fix applies to any backend when the calling agent is "droid".
764+
This fix applies to any backend when the calling agent is "droid" or "factory"
765+
(Droid sends User-Agent: factory-cli/X.Y.Z).
765766
766767
Returns:
767768
Tuple of (possibly modified arguments, was_modified flag).
768769
"""
769-
# Check agent name - must contain "droid" to activate
770+
# Check agent name - must contain "droid" or "factory" to activate
771+
# Droid is built by Factory and uses "factory-cli" as user agent
770772
agent = (calling_agent or "").lower()
771-
if "droid" not in agent:
773+
if "droid" not in agent and "factory" not in agent:
772774
return tool_arguments, False
773775

774776
def _extract_path(args: Any) -> tuple[Any, str | None, str | None]:
@@ -1525,14 +1527,16 @@ def _maybe_fix_droid_antigravity_path(
15251527
isn't registered, we still want to avoid the "absolute path required" errors
15261528
by normalizing obvious relative paths emitted by the backend.
15271529
1528-
This fix applies to any backend when the calling agent is "droid".
1530+
This fix applies to any backend when the calling agent is "droid" or "factory"
1531+
(Droid sends User-Agent: factory-cli/X.Y.Z).
15291532
15301533
Returns:
15311534
Tuple of (possibly modified arguments, was_modified flag).
15321535
"""
1533-
# Check agent name - must contain "droid" to activate
1536+
# Check agent name - must contain "droid" or "factory" to activate
1537+
# Droid is built by Factory and uses "factory-cli" as user agent
15341538
agent = (calling_agent or "").lower()
1535-
if "droid" not in agent:
1539+
if "droid" not in agent and "factory" not in agent:
15361540
return tool_arguments, False
15371541

15381542
def _extract_path(args: Any) -> tuple[Any, str | None, str | None]:

tests/unit/core/services/test_tool_call_reactor_middleware.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,50 @@ def test_maybe_fix_droid_antigravity_path_not_modified_for_non_droid_agent() ->
621621
assert modified is False
622622

623623

624+
def test_maybe_fix_droid_antigravity_path_handles_factory_cli_agent() -> None:
625+
"""factory-cli user agent (Droid's actual User-Agent) should have paths fixed.
626+
627+
Droid agent sends User-Agent: factory-cli/X.Y.Z, so the fix should trigger
628+
for both 'droid' and 'factory' in the agent name.
629+
630+
This test verifies the fix for the production bug where Droid sent relative
631+
paths with User-Agent: factory-cli/0.35.0, but the proxy didn't fix them.
632+
"""
633+
args: dict[str, str] = {
634+
"file_path": "tests/unit/services/test_steering_leak_protection.py"
635+
}
636+
fixed, modified = ToolCallReactorFeature._maybe_fix_droid_antigravity_path(
637+
args, "gemini-oauth-antigravity", "factory-cli/0.35.0"
638+
)
639+
assert isinstance(fixed, dict)
640+
assert (
641+
fixed.get("file_path")
642+
== "/tests/unit/services/test_steering_leak_protection.py"
643+
)
644+
assert modified is True
645+
646+
647+
def test_maybe_fix_droid_antigravity_path_handles_factory_variations() -> None:
648+
"""Various factory-related agent names should trigger the path fix."""
649+
factory_agents = [
650+
"factory-cli/0.35.0",
651+
"factory-cli/1.0.0",
652+
"Factory",
653+
"FACTORY",
654+
"MyFactoryAgent",
655+
]
656+
for agent_name in factory_agents:
657+
args: dict[str, str] = {"file_path": "src/test.py"}
658+
fixed, modified = ToolCallReactorFeature._maybe_fix_droid_antigravity_path(
659+
args, "gemini-oauth-antigravity", agent_name
660+
)
661+
assert isinstance(fixed, dict), f"Should return dict for agent: {agent_name}"
662+
assert (
663+
fixed.get("file_path") == "/src/test.py"
664+
), f"Should fix path for agent: {agent_name}"
665+
assert modified is True, f"Should mark as modified for agent: {agent_name}"
666+
667+
624668
class TestVTCToolCallBypass:
625669
"""Tests for VTC (Virtual Tool Calling) tool call bypass in ToolCallReactorFeature."""
626670

tests/unit/core/services/tool_call_handlers/test_droid_antigravity_path_fix_handler.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,32 @@ async def test_can_handle_case_insensitive_agent_match(
8585
result = await enabled_handler.can_handle(context)
8686
assert result is True, f"Should match agent name: {agent_name}"
8787

88+
@pytest.mark.asyncio
89+
async def test_can_handle_factory_cli_user_agent(
90+
self, enabled_handler: DroidAntigravityPathFixHandler
91+
) -> None:
92+
"""Handler should match factory-cli user agent (Droid's actual User-Agent).
93+
94+
Droid agent sends User-Agent: factory-cli/X.Y.Z, so we need to detect
95+
both 'droid' and 'factory' in the agent name.
96+
"""
97+
factory_agents = [
98+
"factory-cli/0.35.0",
99+
"factory-cli/1.0.0",
100+
"Factory",
101+
"FACTORY",
102+
"MyFactoryAgent",
103+
]
104+
for agent_name in factory_agents:
105+
context = self._create_context(
106+
calling_agent=agent_name,
107+
tool_arguments={"file_path": "src/file.py"},
108+
)
109+
result = await enabled_handler.can_handle(context)
110+
assert (
111+
result is True
112+
), f"Should match factory-based agent name: {agent_name}"
113+
88114
@pytest.mark.asyncio
89115
async def test_can_handle_non_matching_agent(
90116
self, enabled_handler: DroidAntigravityPathFixHandler
@@ -182,6 +208,42 @@ async def test_handle_real_scenario_from_cbor(
182208
assert result.should_swallow is False
183209
assert context.tool_arguments["file_path"] == r"\src\core\config\app_config.py"
184210

211+
@pytest.mark.asyncio
212+
async def test_handle_factory_cli_scenario_from_production(
213+
self, enabled_handler: DroidAntigravityPathFixHandler
214+
) -> None:
215+
"""Test the exact scenario that failed in production with factory-cli User-Agent.
216+
217+
From production logs:
218+
- User-Agent: factory-cli/0.35.0
219+
- Path: tests/unit/services/test_steering_leak_protection.py (relative)
220+
- Expected: Should be fixed to \\tests\\unit\\services\\test_steering_leak_protection.py
221+
222+
This test verifies the fix for the bug where Droid's actual User-Agent
223+
(factory-cli/X.Y.Z) wasn't being recognized.
224+
"""
225+
context = self._create_context(
226+
calling_agent="factory-cli/0.35.0", # Actual User-Agent from production
227+
backend_name="gemini-oauth-antigravity",
228+
model_name="gemini-3-pro-high",
229+
tool_name="Read",
230+
tool_arguments={
231+
"file_path": "tests/unit/services/test_steering_leak_protection.py"
232+
},
233+
)
234+
235+
# Verify can_handle returns True for factory-cli
236+
can_handle = await enabled_handler.can_handle(context)
237+
assert can_handle is True, "Handler should match factory-cli/0.35.0 agent"
238+
239+
# Verify handle fixes the path
240+
result = await enabled_handler.handle(context)
241+
assert result.should_swallow is False
242+
assert (
243+
context.tool_arguments["file_path"]
244+
== r"\tests\unit\services\test_steering_leak_protection.py"
245+
)
246+
185247
# ==================== Internal method tests ====================
186248

187249
def test_needs_path_fix_relative_path(

0 commit comments

Comments
 (0)