fix: redact browser typed text surfaces

This commit is contained in:
rebel 2026-06-24 11:02:35 +05:30 committed by Teknium
parent 5add283ec8
commit 8ff426e53b
8 changed files with 272 additions and 25 deletions

View file

@ -9,6 +9,7 @@ from agent.display import (
capture_local_edit_snapshot,
extract_edit_diff,
get_cute_tool_message,
redact_tool_args_for_display,
set_tool_preview_max_len,
_render_inline_unified_diff,
_summarize_rendered_diff_sections,
@ -86,6 +87,21 @@ class TestBuildToolPreview:
result = build_tool_preview("read_file", {"path": "./package.json", "offset": 1, "limit": 5})
assert result == "package.json L1-5"
def test_browser_type_preview_never_echoes_typed_text(self):
typed_text = "my_secret_password_123"
result = build_tool_preview("browser_type", {"ref": "@e3", "text": typed_text})
assert result is not None
assert typed_text not in result
assert "redacted typed text" in result
def test_browser_type_display_args_never_echo_typed_text(self):
typed_text = "normal-looking-but-sensitive"
safe_args = redact_tool_args_for_display(
"browser_type", {"ref": "@e3", "text": typed_text}
)
assert safe_args == {"ref": "@e3", "text": "[redacted typed text]"}
assert typed_text not in str(safe_args)
def test_unknown_tool_with_fallback_key(self):
"""Unknown tool but with a recognized fallback key should still preview."""
result = build_tool_preview("custom_tool", {"query": "test query"})
@ -242,6 +258,18 @@ class TestCuteToolMessagePreviewLength:
)
assert "2x: Review PR A | Review PR B" in line
def test_browser_type_cute_message_never_echoes_typed_text(self):
typed_text = "my_secret_password_123"
line = get_cute_tool_message(
"browser_type",
{"ref": "@password", "text": typed_text},
0.1,
result='{"success": true, "typed": "[redacted typed text]"}',
)
assert typed_text not in line
assert "redacted typed text" in line
class TestEditDiffPreview:
def test_extract_edit_diff_for_patch(self):

View file

@ -2589,6 +2589,30 @@ class TestConcurrentToolExecution:
assert starts == [("c1", "web_search", {"query": "hello"})]
assert completes == [("c1", "web_search", {"query": "hello"}, '{"success": true}')]
def test_sequential_browser_type_callbacks_never_echo_typed_text(self, agent):
typed_text = "my_secret_password_123"
tool_call = _mock_tool_call(
name="browser_type",
arguments=json.dumps({"ref": "@password", "text": typed_text}),
call_id="c-secret",
)
mock_msg = _mock_assistant_msg(content="", tool_calls=[tool_call])
messages = []
starts = []
completes = []
progress = []
agent.tool_start_callback = lambda tool_call_id, function_name, function_args: starts.append((tool_call_id, function_name, function_args))
agent.tool_complete_callback = lambda tool_call_id, function_name, function_args, function_result: completes.append((tool_call_id, function_name, function_args, function_result))
agent.tool_progress_callback = lambda event, name, preview, args, **kw: progress.append((event, name, preview, args))
with patch("run_agent.handle_function_call", return_value='{"success": true, "typed": "[redacted typed text]"}'):
agent._execute_tool_calls_sequential(mock_msg, messages, "task-1")
assert starts == [("c-secret", "browser_type", {"ref": "@password", "text": "[redacted typed text]"})]
assert completes[0][2] == {"ref": "@password", "text": "[redacted typed text]"}
assert progress[0][2] == "[redacted typed text]"
assert typed_text not in repr(starts + completes + progress)
def test_concurrent_tool_callbacks_fire_for_each_tool(self, agent):
tc1 = _mock_tool_call(name="web_search", arguments='{"query":"one"}', call_id="c1")
tc2 = _mock_tool_call(name="web_search", arguments='{"query":"two"}', call_id="c2")
@ -2610,6 +2634,30 @@ class TestConcurrentToolExecution:
assert {entry[0] for entry in completes} == {"c1", "c2"}
assert {entry[3] for entry in completes} == {'{"id":1}', '{"id":2}'}
def test_concurrent_browser_type_callbacks_never_echo_typed_text(self, agent):
typed_text = "my_secret_password_123"
tc = _mock_tool_call(
name="browser_type",
arguments=json.dumps({"ref": "@password", "text": typed_text}),
call_id="c-secret",
)
mock_msg = _mock_assistant_msg(content="", tool_calls=[tc])
messages = []
starts = []
completes = []
progress = []
agent.tool_start_callback = lambda tool_call_id, function_name, function_args: starts.append((tool_call_id, function_name, function_args))
agent.tool_complete_callback = lambda tool_call_id, function_name, function_args, function_result: completes.append((tool_call_id, function_name, function_args, function_result))
agent.tool_progress_callback = lambda event, name, preview, args, **kw: progress.append((event, name, preview, args))
with patch("run_agent.handle_function_call", return_value='{"success": true, "typed": "[redacted typed text]"}'):
agent._execute_tool_calls_concurrent(mock_msg, messages, "task-1")
assert starts == [("c-secret", "browser_type", {"ref": "@password", "text": "[redacted typed text]"})]
assert completes[0][2] == {"ref": "@password", "text": "[redacted typed text]"}
assert progress[0][2] == "[redacted typed text]"
assert typed_text not in repr(starts + completes + progress)
def test_invoke_tool_handles_agent_level_tools(self, agent):
"""_invoke_tool should handle todo tool directly."""
with patch("tools.todo_tool.todo_tool", return_value='{"ok":true}') as mock_todo:

View file

@ -235,7 +235,35 @@ class TestCamofoxInteractions:
mock_post.return_value = _mock_response(json_data={"ok": True})
result = json.loads(camofox_type("@e3", "hello world", task_id="t5"))
assert result["success"] is True
assert result["typed"] == "hello world"
assert result["typed"] == "[redacted typed text]"
@patch("tools.browser_camofox.requests.post")
def test_type_never_echoes_raw_secret(self, mock_post, monkeypatch):
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
mock_post.return_value = _mock_response(json_data={"tabId": "tab5b", "url": "https://x.com"})
camofox_navigate("https://x.com", task_id="t5b")
typed_text = "my_secret_password_123"
mock_post.return_value = _mock_response(json_data={"ok": True})
result = json.loads(camofox_type("@password", typed_text, task_id="t5b"))
assert result["success"] is True
assert typed_text not in json.dumps(result)
assert result["typed"] == "[redacted typed text]"
@patch("tools.browser_camofox.requests.post")
def test_type_failure_never_echoes_raw_secret(self, mock_post, monkeypatch):
monkeypatch.setenv("CAMOFOX_URL", "http://localhost:9377")
mock_post.return_value = _mock_response(json_data={"tabId": "tab5c", "url": "https://x.com"})
camofox_navigate("https://x.com", task_id="t5c")
typed_text = "my_secret_password_123"
mock_post.side_effect = RuntimeError(f"camofox failed while typing {typed_text}")
raw_result = camofox_type("@password", typed_text, task_id="t5c")
result = json.loads(raw_result)
assert result["success"] is False
assert typed_text not in raw_result
assert "[redacted typed text]" in raw_result
@patch("tools.browser_camofox.requests.post")
def test_scroll(self, mock_post, monkeypatch):

View file

@ -0,0 +1,47 @@
"""Regression tests for browser_type display redaction."""
import json
from unittest.mock import patch
from tools.browser_tool import browser_type
def test_browser_type_never_echoes_raw_typed_text(monkeypatch):
monkeypatch.delenv("CAMOFOX_URL", raising=False)
monkeypatch.delenv("BROWSER_CDP_URL", raising=False)
typed_text = "my_secret_password_123"
with patch(
"tools.browser_tool._run_browser_command",
return_value={"success": True},
) as mock_run:
result = json.loads(browser_type("@password", typed_text, task_id="redaction-test"))
assert result["success"] is True
assert result["typed"] == "[redacted typed text]"
assert typed_text not in json.dumps(result)
mock_run.assert_called_once()
assert mock_run.call_args.args[2] == ["@password", typed_text]
def test_browser_type_failure_never_echoes_raw_typed_text(monkeypatch):
monkeypatch.delenv("CAMOFOX_URL", raising=False)
monkeypatch.delenv("BROWSER_CDP_URL", raising=False)
typed_text = "my_secret_password_123"
with patch(
"tools.browser_tool._run_browser_command",
return_value={
"success": False,
"error": f"backend failed while typing {typed_text}",
"fallback_warning": f"chrome fallback also saw {typed_text}",
},
) as mock_run:
raw_result = browser_type("@password", typed_text, task_id="redaction-test")
result = json.loads(raw_result)
assert result["success"] is False
assert typed_text not in raw_result
assert "[redacted typed text]" in raw_result
mock_run.assert_called_once()
assert mock_run.call_args.args[2] == ["@password", typed_text]