diff --git a/Sensor/README.md b/Sensor/README.md index bd81ecf..1daa2df 100644 --- a/Sensor/README.md +++ b/Sensor/README.md @@ -202,6 +202,10 @@ Each parsed session produces an `AgentEvent` with the following structure: } ``` +Tool arguments and results can contain source code, credentials, or other sensitive +content copied from the local environment. Treat sensor output as sensitive data and +review it before sharing. + ### `session_context` Parsers that can recover session-level configuration attach it under diff --git a/Sensor/adr_sensor/parsers/cursor_parser.py b/Sensor/adr_sensor/parsers/cursor_parser.py index 068ad76..7bb9053 100644 --- a/Sensor/adr_sensor/parsers/cursor_parser.py +++ b/Sensor/adr_sensor/parsers/cursor_parser.py @@ -286,11 +286,9 @@ def extract_tools_from_bubble(self, bubble: Dict[str, Any]) -> List[ToolUsage]: status = tool_data.get("status", "unknown") error = tool_data.get("error", "") - result = None - if tool_name == "list_dir": - result = tool_data.get("result", "") - if result and isinstance(result, str): - result = truncate_middle(result.strip(), max_length=1000, edge_chars=400) + result = tool_data.get("result") + if result is not None: + result = truncate_middle(str(result), max_length=1000, edge_chars=400) tool = ToolUsage( tool_name=tool_name, diff --git a/Sensor/tests/test_parsers.py b/Sensor/tests/test_parsers.py index 4112a14..2626341 100644 --- a/Sensor/tests/test_parsers.py +++ b/Sensor/tests/test_parsers.py @@ -435,6 +435,47 @@ def test_parse_no_directory(self): assert entries == [] +class TestCursorParser: + @pytest.mark.parametrize("tool_name", ["list_dir", "read_file", "run_command"]) + def test_extracts_result_for_every_tool(self, tool_name): + tools = CursorParser().extract_tools_from_bubble( + {"toolFormerData": {"name": tool_name, "params": {}, "result": "completed"}} + ) + + assert len(tools) == 1 + assert tools[0].tool_name == tool_name + assert tools[0].result == "completed" + + def test_absent_result_remains_none(self): + tools = CursorParser().extract_tools_from_bubble( + {"toolFormerData": {"name": "read_file", "params": {}}} + ) + + assert tools[0].result is None + + def test_result_whitespace_is_preserved(self): + tools = CursorParser().extract_tools_from_bubble( + {"toolFormerData": {"name": "search", "params": {}, "result": " \n first\nsecond\t "}} + ) + + assert tools[0].result == " \n first\nsecond\t " + + def test_non_string_result_is_converted_to_string(self): + tools = CursorParser().extract_tools_from_bubble( + {"toolFormerData": {"name": "count", "params": {}, "result": 3}} + ) + + assert tools[0].result == "3" + + def test_result_is_middle_truncated_at_1000_characters(self): + result = "a" * 600 + "z" * 600 + tools = CursorParser().extract_tools_from_bubble( + {"toolFormerData": {"name": "read_file", "params": {}, "result": result}} + ) + + assert tools[0].result == "a" * 400 + "... [truncated 400 chars] ..." + "z" * 400 + + def _build_warp_db(db_path: Path, conversations: list) -> None: """Create a synthetic warp.sqlite matching the schema WarpParser queries.