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
23 changes: 23 additions & 0 deletions .github/workflows/sensor-pi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Pi parser platforms
on:
pull_request:
paths: ['Sensor/**', '.github/workflows/sensor-pi.yml']
push:
branches: [main]
paths: ['Sensor/**', '.github/workflows/sensor-pi.yml']
permissions:
contents: read
jobs:
parser:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- run: uv sync --extra dev --python 3.12
working-directory: Sensor
- run: uv run pytest tests/test_pi_parser.py -q
working-directory: Sensor
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ This repository contains the open-source **ADR Discovery**, **ADR Sensor**, **AD
| Path | ADR component | Description |
| -------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------ |
| [Discovery/](Discovery/) | ADR Discovery | Inventory the AI apps, CLI agents, IDE extensions, model runtimes, and MCP servers on an endpoint, and flag unknown surfaces for review |
| [Sensor/](Sensor/) | ADR Observability | Collect and normalize agent telemetry from Claude Code, Cursor, Codex, GitHub Copilot CLI, opencode, Claude Desktop, and others |
| [Sensor/](Sensor/) | ADR Observability | Collect and normalize agent telemetry from Claude Code, Cursor, Codex, GitHub Copilot CLI, Pi, opencode, Claude Desktop, and others |
| [Detection/](Detection/) | ADR Benchmark + Detection | Dual-agent detector, 134 MCP servers, 304 benchmark tasks, baselines, figure scripts |
| [docs/REPRODUCIBILITY.md](docs/REPRODUCIBILITY.md) | Evaluation | Step-by-step workflow to reproduce benchmark detection and paper figures |

Expand Down
62 changes: 61 additions & 1 deletion Sensor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ADR Sensor is a Python library that collects telemetry from AI coding agents to
| **GitHub Copilot CLI** | `copilot` | JSONL (`~/.copilot/session-state/`) | macOS, Linux, Windows |
| **Warp Terminal** | `warp` | SQLite (`warp.sqlite`) | macOS, Windows |
| **opencode** | `opencode` | SQLite (`opencode.db`) or JSON tree | macOS, Linux |
| **Pi coding agent** | `pi` | JSONL (`~/.pi/agent/sessions/`) | macOS, Linux, Windows |

### Claude Desktop Agent Mode

Expand Down Expand Up @@ -92,13 +93,68 @@ known built-in and contains an underscore is recorded as `tool_type: "mcp_tool"`
its `server_name` populated.


### Pi coding agent

The `pi` source reads Pi's persisted session JSONL files, including legacy v1
linear sessions and v2/v3 tree-structured sessions. It captures conversations,
tool calls with full recorded arguments and results, tool failures, and user-run
shell commands. No content redaction or additional truncation is applied.

| Operating system | Default session directory |
| ---------------- | ------------------------- |
| macOS | `/Users/<user>/.pi/agent/sessions/` |
| Linux | `/home/<user>/.pi/agent/sessions/` |
| Windows | `%USERPROFILE%\.pi\agent\sessions\` |

Pi supports native Windows with Git Bash by default; its optional PowerShell tool
does not change session storage. ADR Sensor only reads the JSONL and does not
need either shell. Session directories are searched recursively. Set
`PI_CODING_AGENT_DIR` for both processes if Pi's agent directory was moved, or
`PI_CODING_AGENT_SESSION_DIR` to override the entire sessions root. If Pi is
started with `--session-dir`, set the same root through
`PI_CODING_AGENT_SESSION_DIR` for ADR Sensor, or pass `base_path` to `PiParser`.
An explicit parser `base_path` takes precedence over environment variables.

The export is a forensic history of **all branches recorded in the file**, not
just the active model context. Tool results are matched to calls on their own
ancestor path, so reused call IDs on sibling branches are not mixed. If several
results refer to a shared ancestor call, its first result stays on the invocation
and additional results appear as `tool` messages, with `tool_call_entry_id` in
their metadata. Orphan results are preserved without inventing invocations.
User shell commands have role `user`, not `assistant`.

`session_context.entries` retains entry IDs/parents, typed content (including
recorded thinking and images), provider/model details, stop reasons, full tool
result details, extension entries, labels, branch summaries, and compactions.
`token_usage.cumulative` sums recorded assistant, nested-tool, compaction, and
branch-summary usage. Raw usage and cost records remain in entry metadata.
Custom extension tools are reported as function calls: the session format does
not establish a universal MCP server identity, so the parser does not guess one.

The default lookback is 14 days by file modification time; `--all-history`
disables it. Resumed sessions update existing `--save-sessions` exports rather
than producing duplicate files. Malformed JSONL rows are skipped and counted;
unsupported future session versions are skipped explicitly. There is no capture
for `--no-session`/in-memory runs, deleted files, or content Pi itself never wrote.
Pi can truncate shell output before persistence; the parser preserves its
`truncated`/`fullOutputPath` metadata but does not follow external output files.
This source does not collect global settings, credentials, or a tool inventory.

Storage, format, and platform behavior were checked against Pi's
[session-format reference](https://github.com/earendil-works/pi/blob/71dca871bc80b6bc97be37f0ca3189399d651fff/packages/coding-agent/docs/session-format.md),
[directory configuration](https://github.com/earendil-works/pi/blob/71dca871bc80b6bc97be37f0ca3189399d651fff/packages/coding-agent/src/config.ts),
[session manager](https://github.com/earendil-works/pi/blob/71dca871bc80b6bc97be37f0ca3189399d651fff/packages/coding-agent/src/core/session-manager.ts),
and [Windows guide](https://github.com/earendil-works/pi/blob/71dca871bc80b6bc97be37f0ca3189399d651fff/packages/coding-agent/docs/windows.md).
CI exercises contract fixtures on native macOS, Linux, and Windows; these tests
do not run authenticated Pi model sessions.

## Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│ AI Agent Logs │
│ Claude, Cursor, Cline, Codex, Copilot CLI, Warp │
Claude Desktop, opencode
│ Claude Desktop, opencode, Pi
└───────────────────────────────┬─────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
Expand Down Expand Up @@ -155,6 +211,7 @@ adr-sensor --source codex
adr-sensor --source copilot
adr-sensor --source claude_desktop
adr-sensor --source opencode
adr-sensor --source pi

# Save individual session files (incremental)
adr-sensor --save-sessions
Expand Down Expand Up @@ -378,6 +435,8 @@ cannot run on the current platform are skipped rather than failing.
| `XDG_CACHE_HOME` | `AgentObserver` | Base for `--save-sessions` output (`$XDG_CACHE_HOME/adr_sensor`, default `~/.cache/adr_sensor`) |
| `XDG_DATA_HOME` | opencode parser | Overrides the opencode data directory (default `~/.local/share/opencode`) |
| `OPENCODE_DB` | opencode parser | Overrides the opencode SQLite filename or path (`:memory:` is ignored) |
| `PI_CODING_AGENT_DIR` | Pi parser | Agent directory containing `sessions/` (default `~/.pi/agent`) |
| `PI_CODING_AGENT_SESSION_DIR` | Pi parser | Sessions root; takes precedence over `PI_CODING_AGENT_DIR` |
| `APPDATA` | Cursor, Cline, Claude Desktop parsers | Windows roaming app-data root. Consulted first so redirected/roaming profiles resolve correctly (default `~/AppData/Roaming`) |
| `LOCALAPPDATA` | Warp parser | Windows local app-data root, same redirected-profile handling (default `~/AppData/Local`) |

Expand Down Expand Up @@ -428,6 +487,7 @@ adr-sensor/
│ │ ├── codex_parser.py
│ │ ├── copilot_parser.py
│ │ ├── opencode_parser.py
│ │ ├── pi_parser.py
│ │ └── warp_parser.py
│ ├── schemas/
│ │ ├── agent_event_schema.py # AgentEvent, ChatMessage, ToolUsage
Expand Down
2 changes: 1 addition & 1 deletion Sensor/adr_sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Security observability library for AI coding agents. Collects telemetry from
Claude Code, Cursor, Cline, OpenAI Codex CLI, GitHub Copilot CLI, Warp
Terminal, opencode, and Claude Desktop Agent Mode (including Dispatch
Terminal, opencode, Pi, and Claude Desktop Agent Mode (including Dispatch
sessions) to enable threat detection and security monitoring.

Usage:
Expand Down
1 change: 1 addition & 0 deletions Sensor/adr_sensor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def main():
adr-sensor --source claude_desktop Ingest Claude Desktop agent-mode logs only (macOS/Windows)
adr-sensor --source copilot Ingest GitHub Copilot CLI logs only
adr-sensor --source opencode Ingest opencode logs only
adr-sensor --source pi Ingest Pi session conversations and tools
adr-sensor --save-sessions Save individual session files
adr-sensor --output-format jsonl Export as JSONL
adr-sensor --all-history Include all logs (not just last 2 weeks)
Expand Down
7 changes: 5 additions & 2 deletions Sensor/adr_sensor/observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from .parsers.copilot_parser import CopilotParser
from .parsers.cursor_parser import CursorParser
from .parsers.opencode_parser import OpencodeParser
from .parsers.pi_parser import PiParser
from .parsers.warp_parser import WarpParser
from .schemas.agent_event_schema import AgentEvent
from .schemas.system_config_schema import SystemConfiguration
Expand Down Expand Up @@ -61,6 +62,7 @@ class AgentObserver:
("codex", "Codex"),
("copilot", "GitHub Copilot CLI"),
("opencode", "opencode"),
("pi", "Pi"),
)

#: Sources that only produce logs on some operating systems. A source absent
Expand All @@ -69,7 +71,7 @@ class AgentObserver:
"claude_desktop": ("Darwin", "Windows"),
}

CONTENT_AWARE_INCREMENTAL_SOURCES = frozenset({"codex", "copilot"})
CONTENT_AWARE_INCREMENTAL_SOURCES = frozenset({"codex", "copilot", "pi"})

def __init__(self, output_dir: Optional[Path] = None, max_age_days: Optional[int] = None):
"""Initialize the AgentObserver.
Expand All @@ -92,6 +94,7 @@ def __init__(self, output_dir: Optional[Path] = None, max_age_days: Optional[int
self.opencode_parser = (
OpencodeParser(max_age_days=max_age_days) if max_age_days is not None else OpencodeParser()
)
self.pi_parser = PiParser(max_age_days=max_age_days) if max_age_days is not None else PiParser()

self.output_dir = output_dir if output_dir else Path("output")
self.output_dir.mkdir(exist_ok=True)
Expand Down Expand Up @@ -129,7 +132,7 @@ def ingest_all(

Args:
source_filter: Which source to ingest. One of 'all', 'claude', 'cursor',
'claude_desktop', 'cline', 'warp', 'codex', 'copilot', 'opencode'.
'claude_desktop', 'cline', 'warp', 'codex', 'copilot', 'opencode', 'pi'.

Returns:
Tuple of (agent_events, system_configs).
Expand Down
2 changes: 2 additions & 0 deletions Sensor/adr_sensor/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .copilot_parser import CopilotParser
from .cursor_parser import CursorParser
from .opencode_parser import OpencodeParser
from .pi_parser import PiParser
from .warp_parser import WarpParser

__all__ = [
Expand All @@ -23,5 +24,6 @@
"CopilotParser",
"CursorParser",
"OpencodeParser",
"PiParser",
"WarpParser",
]
Loading