Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Sensor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 3 additions & 5 deletions Sensor/adr_sensor/parsers/cursor_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
41 changes: 41 additions & 0 deletions Sensor/tests/test_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down