Skip to content

Commit 0021187

Browse files
Harden tool param key normalization and character checks
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent e934581 commit 0021187

2 files changed

Lines changed: 130 additions & 3 deletions

File tree

hyperbrowser/tools/__init__.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,44 @@ def _to_param_dict(params: Mapping[str, Any]) -> Dict[str, Any]:
137137
) from exc
138138
for key in param_keys:
139139
if isinstance(key, str):
140-
if not key.strip():
140+
try:
141+
normalized_key = key.strip()
142+
if not isinstance(normalized_key, str):
143+
raise TypeError("normalized tool param key must be a string")
144+
except HyperbrowserError:
145+
raise
146+
except Exception as exc:
147+
raise HyperbrowserError(
148+
"Failed to normalize tool param key",
149+
original_error=exc,
150+
) from exc
151+
if not normalized_key:
141152
raise HyperbrowserError("tool params keys must not be empty")
142-
if key != key.strip():
153+
try:
154+
has_surrounding_whitespace = key != normalized_key
155+
except HyperbrowserError:
156+
raise
157+
except Exception as exc:
158+
raise HyperbrowserError(
159+
"Failed to normalize tool param key",
160+
original_error=exc,
161+
) from exc
162+
if has_surrounding_whitespace:
143163
raise HyperbrowserError(
144164
"tool params keys must not contain leading or trailing whitespace"
145165
)
146-
if any(ord(character) < 32 or ord(character) == 127 for character in key):
166+
try:
167+
contains_control_character = any(
168+
ord(character) < 32 or ord(character) == 127 for character in key
169+
)
170+
except HyperbrowserError:
171+
raise
172+
except Exception as exc:
173+
raise HyperbrowserError(
174+
"Failed to validate tool param key characters",
175+
original_error=exc,
176+
) from exc
177+
if contains_control_character:
147178
raise HyperbrowserError(
148179
"tool params keys must not contain control characters"
149180
)

tests/test_tools_mapping_inputs.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ def __init__(self):
4848
self.extract = _AsyncExtractManager()
4949

5050

51+
def _run_scrape_tool_sync(params: Mapping[str, object]) -> None:
52+
client = _Client()
53+
WebsiteScrapeTool.runnable(client, params)
54+
55+
56+
def _run_scrape_tool_async(params: Mapping[str, object]) -> None:
57+
async def run() -> None:
58+
client = _AsyncClient()
59+
await WebsiteScrapeTool.async_runnable(client, params)
60+
61+
asyncio.run(run())
62+
63+
5164
def test_tool_wrappers_accept_mapping_inputs():
5265
client = _Client()
5366
params = MappingProxyType({"url": "https://example.com"})
@@ -207,3 +220,86 @@ def __getitem__(self, key: str) -> object:
207220
WebsiteScrapeTool.runnable(client, _BrokenValueMapping())
208221

209222
assert exc_info.value.original_error is None
223+
224+
225+
@pytest.mark.parametrize("runner", [_run_scrape_tool_sync, _run_scrape_tool_async])
226+
def test_tool_wrappers_wrap_param_key_strip_failures(runner):
227+
class _BrokenStripKey(str):
228+
def strip(self, chars=None): # type: ignore[override]
229+
_ = chars
230+
raise RuntimeError("tool param key strip exploded")
231+
232+
with pytest.raises(
233+
HyperbrowserError, match="Failed to normalize tool param key"
234+
) as exc_info:
235+
runner({_BrokenStripKey("url"): "https://example.com"})
236+
237+
assert isinstance(exc_info.value.original_error, RuntimeError)
238+
239+
240+
@pytest.mark.parametrize("runner", [_run_scrape_tool_sync, _run_scrape_tool_async])
241+
def test_tool_wrappers_preserve_hyperbrowser_param_key_strip_failures(runner):
242+
class _BrokenStripKey(str):
243+
def strip(self, chars=None): # type: ignore[override]
244+
_ = chars
245+
raise HyperbrowserError("custom tool param key strip failure")
246+
247+
with pytest.raises(
248+
HyperbrowserError, match="custom tool param key strip failure"
249+
) as exc_info:
250+
runner({_BrokenStripKey("url"): "https://example.com"})
251+
252+
assert exc_info.value.original_error is None
253+
254+
255+
@pytest.mark.parametrize("runner", [_run_scrape_tool_sync, _run_scrape_tool_async])
256+
def test_tool_wrappers_wrap_non_string_param_key_strip_results(runner):
257+
class _BrokenStripKey(str):
258+
def strip(self, chars=None): # type: ignore[override]
259+
_ = chars
260+
return object()
261+
262+
with pytest.raises(
263+
HyperbrowserError, match="Failed to normalize tool param key"
264+
) as exc_info:
265+
runner({_BrokenStripKey("url"): "https://example.com"})
266+
267+
assert isinstance(exc_info.value.original_error, TypeError)
268+
269+
270+
@pytest.mark.parametrize("runner", [_run_scrape_tool_sync, _run_scrape_tool_async])
271+
def test_tool_wrappers_wrap_param_key_character_validation_failures(runner):
272+
class _BrokenIterKey(str):
273+
def strip(self, chars=None): # type: ignore[override]
274+
_ = chars
275+
return self
276+
277+
def __iter__(self):
278+
raise RuntimeError("tool param key iteration exploded")
279+
280+
with pytest.raises(
281+
HyperbrowserError, match="Failed to validate tool param key characters"
282+
) as exc_info:
283+
runner({_BrokenIterKey("url"): "https://example.com"})
284+
285+
assert isinstance(exc_info.value.original_error, RuntimeError)
286+
287+
288+
@pytest.mark.parametrize("runner", [_run_scrape_tool_sync, _run_scrape_tool_async])
289+
def test_tool_wrappers_preserve_hyperbrowser_param_key_character_validation_failures(
290+
runner,
291+
):
292+
class _BrokenIterKey(str):
293+
def strip(self, chars=None): # type: ignore[override]
294+
_ = chars
295+
return self
296+
297+
def __iter__(self):
298+
raise HyperbrowserError("custom tool param key iteration failure")
299+
300+
with pytest.raises(
301+
HyperbrowserError, match="custom tool param key iteration failure"
302+
) as exc_info:
303+
runner({_BrokenIterKey("url"): "https://example.com"})
304+
305+
assert exc_info.value.original_error is None

0 commit comments

Comments
 (0)