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
56 changes: 51 additions & 5 deletions lldb/agent-harness/HARNESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

## Overview

This harness wraps the **LLDB Python API** into a Click-based CLI tool:
`cli-anything-lldb`.
This harness wraps the **LLDB Python API** into a Click-based CLI tool and
debug adapter:
- `cli-anything-lldb` for JSON CLI / REPL workflows
- `cli-anything-lldb-dap` for stdio Debug Adapter Protocol clients

It provides stateful debugging workflows for agent and script usage, with:
- direct `import lldb` integration
- structured dict outputs for JSON mode
- interactive REPL with persistent debug session
- a formal single-session DAP server for AI/editor debugging

## Architecture

Expand All @@ -20,6 +23,7 @@ agent-harness/
└── cli_anything/
└── lldb/
├── lldb_cli.py
├── dap.py
├── core/
│ ├── session.py
│ ├── breakpoints.py
Expand All @@ -38,21 +42,53 @@ agent-harness/

- `--json`: machine-readable output
- `--debug`: include traceback in errors
- `--session-file`: explicit persistent CLI session state path
- `--version`: show package version

## Command Groups

- `target`: create/show target
- `process`: launch/attach/continue/detach/info
- `process`: launch/attach/continue/interrupt/detach/info
- `breakpoint`: set/list/delete/enable/disable
- `thread`: list/select/backtrace/info
- `frame`: select/info/locals
- `step`: over/into/out
- `expr`: evaluate expression
- `memory`: read/find
- `core`: load core dump
- `dap`: run stdio DAP server
- `session`: info/close persistent CLI session
- `repl`: interactive mode (default)

## Debug Adapter Protocol

`cli-anything-lldb-dap` is a stdio DAP server. It owns one in-process
`LLDBSession` and does not use the persistent CLI daemon. Stdout must contain
only DAP `Content-Length` frames; diagnostics go to stderr or `--log-file`.

Supported v1 requests:
- lifecycle: `initialize`, `launch`, `attach`, `configurationDone`, `disconnect`
- breakpoints: `setBreakpoints`, `setFunctionBreakpoints`
- inspection: `threads`, `stackTrace`, `scopes`, `variables`, `setVariable`, `evaluate`, `source`, `loadedSources`, `readMemory`, `modules`, `exceptionInfo`, `disassemble`
- execution: `continue`, `pause`, `next`, `stepIn`, `stepOut`

DAP uses protocol-native pending breakpoint semantics: unresolved breakpoints
return `verified: false`, and later resolution is reported with breakpoint
events.
Variable references are adapter-local and reset on resume. This keeps stopped
frame state honest for AI agents and avoids reusing stale LLDB `SBValue`
objects after execution continues.

Long-running GUI targets can provide DAP stop-rule profiles either with
`cli-anything-lldb-dap --profile PATH`, `cli-anything-lldb dap --profile PATH`,
or launch/attach arguments such as `stopRuleProfile` and inline `stopRules`.
Rules match structured stop context (`reason`, `module`, `function`, `regex`)
and either classify the stop or auto-continue it. Stopped events expose
`body.cliAnythingStop.origin` so clients can distinguish manual pauses,
debugger-internal traps, and ordinary debuggee stops. Profiles are loaded by the
current adapter process only; running DAP sessions must restart and re-attach or
re-launch before new code/profile contents take effect.

## Patterns

1. **Lazy import of LLDB**:
Expand All @@ -61,10 +97,20 @@ agent-harness/
`LLDBSession` owns debugger/target/process lifecycle.
3. **Dict-first API**:
Core methods return JSON-serializable dict/list structures.
4. **Dual output mode**:
4. **Honest breakpoint state**:
Breakpoint payloads include `resolved` and `location_details`; CLI unresolved
breakpoints fail unless `--allow-pending` is explicit.
5. **Dual output mode**:
`_output()` chooses JSON or human-friendly formatting.
5. **Boundary errors**:
6. **Boundary errors**:
Command layer converts exceptions into structured error payloads.
7. **Secure persistent daemon**:
CLI session auth state is written under a per-user directory with restrictive
permissions and RPC dispatch uses an explicit method allowlist.
8. **Structured stop classification**:
DAP stop handling uses profile-driven rules instead of ad hoc substring
checks, while preserving `autoContinueInternalBreakpoints` as a compatibility
shortcut for common NVIDIA/Windows internal traps.

## Dependency Model

Expand Down
1 change: 1 addition & 0 deletions lldb/agent-harness/LLDB.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ This is implemented in `utils/lldb_backend.py`.

All core operations return plain dictionaries:
- process info (`pid`, `state`, `num_threads`)
- stop info (`reason`, `description`, `module`, `function`, `frame`)
- frame info (`function`, `file`, `line`, `address`)
- breakpoints (`id`, `locations`, `condition`)
- expression result (`type`, `value`, `summary`, `error`)
Expand Down
107 changes: 105 additions & 2 deletions lldb/agent-harness/cli_anything/lldb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Command-line interface for LLDB debugger using LLDB Python API.

The package exposes two agent-facing entry points:

- `cli-anything-lldb`: JSON CLI / REPL workflows with a persistent session daemon
- `cli-anything-lldb-dap`: stdio Debug Adapter Protocol server for editor-style and AI debug clients

## Installation

```bash
Expand Down Expand Up @@ -42,11 +47,18 @@ cli-anything-lldb --json target create --exe /path/to/executable
# Launch process
cli-anything-lldb --json process launch --arg foo --arg bar

# Stop at process entry before user code
cli-anything-lldb --json process launch --stop-at-entry

# Set breakpoint by function
cli-anything-lldb --json breakpoint set --function main

# Pending breakpoints are explicit
cli-anything-lldb --json breakpoint set --function PluginEntry --allow-pending

# Continue and inspect
cli-anything-lldb --json process continue
cli-anything-lldb --json process interrupt
cli-anything-lldb --json thread backtrace
cli-anything-lldb --json frame locals

Expand All @@ -63,7 +75,96 @@ cli-anything-lldb
Non-REPL commands share a persistent LLDB session automatically, so commands
such as `target create`, `breakpoint set`, `process launch`, and follow-up
inspection commands can run as separate CLI invocations against the same live
debugger state.
debugger state. The default session state file lives in a per-user application
directory, not the global temp directory. Use `--session-file` or
`CLI_ANYTHING_LLDB_SESSION_FILE` when an agent needs an explicit session path,
and run `session close` when finished.

By default, `breakpoint set` fails if LLDB creates a pending breakpoint with no
resolved locations. Use `--allow-pending` only when the target or symbols are
expected to load later. Breakpoint payloads include `resolved` and
`location_details` so agents can tell whether a stop is actually reachable.

## Debug Adapter Protocol

Run the formal stdio DAP server with:

```bash
cli-anything-lldb-dap
cli-anything-lldb-dap --profile /path/to/stop-rules.json
```

or through the CLI convenience command:

```bash
cli-anything-lldb dap
cli-anything-lldb dap --profile /path/to/stop-rules.json
```

The DAP server owns one in-process `LLDBSession` and writes only DAP frames to
stdout. Debuggee stdout/stderr is suppressed during DAP launches so protocol
messages are not corrupted.

Supported requests include:

- `initialize`, `launch`, `attach`, `configurationDone`, `disconnect`
- `setBreakpoints`, `setFunctionBreakpoints`
- `threads`, `stackTrace`, `scopes`, `variables`, `setVariable`, `evaluate`
- `continue`, `pause`, `next`, `stepIn`, `stepOut`
- `source`, `loadedSources`, `readMemory`, `modules`, `exceptionInfo`, `disassemble`

DAP launch-time unresolved breakpoints are returned as `verified: false` and
updated with breakpoint events after launch if LLDB resolves them.
Variables support expandable child references for structs/classes/arrays, and
`setVariable` can update stopped-frame locals or child values when LLDB allows
the assignment.

For long-running GUI targets, DAP `continue` responds before the blocking LLDB
`SBProcess.Continue()` call completes, then waits on a background thread for the
next stop. DAP `pause` uses `SBProcess.SendAsyncInterrupt()` so the adapter stays
responsive while the debuggee is running. If `setBreakpoints` or
`setFunctionBreakpoints` arrives during an active continue, the adapter first
requests an async interrupt, waits for the continue thread to observe a stopped
state, and only then mutates LLDB breakpoints. If the process does not stop in
time, the request fails clearly instead of hanging the DAP loop.

`launch` and `attach` accept non-standard stop-rule controls for noisy GUI
debuggees:

- `autoContinueInternalBreakpoints`: compatibility boolean that enables built-in
rules for NVIDIA `__jit_debug_register_code` / `jit-debug-register` and
Windows `Exception 0x80000003` at ``ntdll.dll`DbgBreakPoint``.
- `stopRules`: inline structured rules with optional `name`, `action`
(`stop` or `continue`), `origin`, `reason`, `module`, `function`, and `regex`.
Each rule must include at least one matcher, so a profile cannot accidentally
classify every stop.
- `stopRuleProfile` / `stopProfile` / `profile`: external JSON profile path
loaded for that launch/attach request.

The DAP process also accepts `--profile` to load a base profile at adapter
startup. Profiles are JSON objects such as:

```json
{
"autoContinueInternalBreakpoints": true,
"stopRules": [
{
"name": "c4d-nvidia-jit",
"action": "continue",
"origin": "internalTrap",
"module": "nvgpucomp64.dll",
"function": "__jit_debug_register_code"
}
]
}
```

Every DAP `stopped` event includes `body.cliAnythingStop` with
`origin` (`manualPause`, `internalTrap`, or `debuggee`), LLDB stop reason,
module/function/frame metadata, and the matched rule when applicable. Running
`cli-anything-lldb-dap` processes do not hot-load code or profile changes;
restart the adapter and re-attach/re-launch the target for new rules to take
effect.

The persistent session daemon now speaks a localhost JSON socket protocol and
stores its session token in an owner-scoped state file. `memory find` scans in
Expand All @@ -72,7 +173,7 @@ stores its session token in an owner-scoped state file. `memory find` scans in
## Command Groups

- `target`: `create`, `info`
- `process`: `launch`, `attach`, `continue`, `detach`, `info`
- `process`: `launch`, `attach`, `continue`, `interrupt`, `detach`, `info`
- `breakpoint`: `set`, `list`, `delete`, `enable`, `disable`
- `thread`: `list`, `select`, `backtrace`, `info`
- `frame`: `select`, `info`, `locals`
Expand All @@ -81,6 +182,7 @@ stores its session token in an owner-scoped state file. `memory find` scans in
- `memory`: `read`, `find`
- `core`: `load`
- `session`: `info`, `close`
- `dap`
- `repl`

## JSON Output
Expand All @@ -97,6 +199,7 @@ cli-anything-lldb --json process info
cd lldb/agent-harness
pytest cli_anything/lldb/tests/test_core.py -v
pytest cli_anything/lldb/tests/test_full_e2e.py -v
pytest cli_anything/lldb/tests -q
```

E2E tests require:
Expand Down
2 changes: 1 addition & 1 deletion lldb/agent-harness/cli_anything/lldb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""LLDB CLI harness - command-line interface for LLDB debugger."""

__version__ = "0.1.0"
__version__ = "1.0.0"

# The ``lldb`` module is loaded lazily by backend/core code when needed.
# Install LLDB and ensure its Python bindings are discoverable.
17 changes: 15 additions & 2 deletions lldb/agent-harness/cli_anything/lldb/core/breakpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,21 @@
from typing import Any, Dict, Optional


def set_breakpoint(session, file: Optional[str] = None, line: Optional[int] = None, function: Optional[str] = None, condition: Optional[str] = None) -> Dict[str, Any]:
return session.breakpoint_set(file=file, line=line, function=function, condition=condition)
def set_breakpoint(
session,
file: Optional[str] = None,
line: Optional[int] = None,
function: Optional[str] = None,
condition: Optional[str] = None,
allow_pending: bool = False,
) -> Dict[str, Any]:
return session.breakpoint_set(
file=file,
line=line,
function=function,
condition=condition,
allow_pending=allow_pending,
)


def list_breakpoints(session) -> Dict[str, Any]:
Expand Down
Loading
Loading