Skip to content

feat: add LLDB CLI harness#227

Open
AiMiDi wants to merge 10 commits intoHKUDS:mainfrom
AiMiDi:codex/lldb-harness-windows-fixes
Open

feat: add LLDB CLI harness#227
AiMiDi wants to merge 10 commits intoHKUDS:mainfrom
AiMiDi:codex/lldb-harness-windows-fixes

Conversation

@AiMiDi
Copy link
Copy Markdown
Contributor

@AiMiDi AiMiDi commented Apr 17, 2026

Description

Add an in-repo LLDB harness for stateful native debugging via the LLDB Python API. This CLI covers target/process/breakpoint/thread/frame/expression/memory workflows, keeps debugger state across separate CLI invocations, and includes the branch's Windows session persistence and cleanup fixes.

Type of Change

  • New Software CLI (in-repo) — adds a CLI harness inside this monorepo

For New Software CLIs (in-repo)

  • LLDB.md SOP document exists at lldb/agent-harness/LLDB.md
  • SKILL.md exists at lldb/agent-harness/cli_anything/lldb/skills/SKILL.md (current repo convention)
  • Unit tests at lldb/agent-harness/cli_anything/lldb/tests/test_core.py are present and pass without backend
  • E2E tests at lldb/agent-harness/cli_anything/lldb/tests/test_full_e2e.py are present
  • README.md includes the new software (with link to harness directory)
  • registry.json includes an entry with source_url: null
  • repl_skin.py in utils/ is an unmodified copy from the plugin

General Checklist

  • Code follows existing patterns and conventions
  • --json flag is supported on new commands
  • Commit messages follow the conventional format (feat:, fix:, docs:, test:)
  • I have tested my changes locally

Test Results

python -m pytest cli_anything/lldb/tests/test_core.py -v
============================= 18 passed in 0.29s ==============================

python -m pytest cli_anything/lldb/tests/test_full_e2e.py -v
================== 3 passed, 1 skipped, 2 warnings in 9.48s ==================

@AiMiDi AiMiDi changed the title [codex] add LLDB CLI harness feat: add LLDB CLI harness Apr 17, 2026
@AiMiDi AiMiDi marked this pull request as ready for review April 17, 2026 09:02
@AiMiDi
Copy link
Copy Markdown
Contributor Author

AiMiDi commented Apr 17, 2026

@yuh-yang @omerarslan0 @zhangxilong-43 I referenced recent merged harness PRs in this repo and would appreciate a review when you have time. Thanks!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new in-repo LLDB CLI harness (cli-anything-lldb) that uses the LLDB Python API and introduces a persistent background session daemon to keep debugger state across separate CLI invocations, with both --json and REPL workflows.

Changes:

  • Added the LLDB harness package (Click CLI + LLDBSession core wrapper + utilities) and session persistence via a local daemon.
  • Added unit + E2E test suites and harness documentation (HARNESS.md / LLDB.md / TEST.md / SKILL.md).
  • Updated repo-level registry/docs/gitignore to include and surface the new LLDB harness.

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
registry.json Registers the new lldb CLI entry for discovery/installation.
lldb/agent-harness/setup.py Adds packaging metadata and console entrypoint for cli-anything-lldb.
lldb/agent-harness/cli_anything/lldb/utils/session_server.py Implements the background daemon that owns a persistent LLDBSession.
lldb/agent-harness/cli_anything/lldb/utils/session_client.py Client utilities + RPC proxy to talk to the daemon and manage the session file.
lldb/agent-harness/cli_anything/lldb/utils/repl_skin.py Adds REPL UI skin file under the LLDB harness.
lldb/agent-harness/cli_anything/lldb/utils/output.py Adds JSON + table output helpers used by the CLI.
lldb/agent-harness/cli_anything/lldb/utils/lldb_backend.py Adds LLDB Python binding discovery/import fallback (lldb -P).
lldb/agent-harness/cli_anything/lldb/utils/errors.py Adds structured error helpers for JSON mode + CLI exits.
lldb/agent-harness/cli_anything/lldb/utils/init.py Declares the utils package.
lldb/agent-harness/cli_anything/lldb/tests/test_full_e2e.py Adds E2E scenarios for persistent workflows + attach cleanup semantics.
lldb/agent-harness/cli_anything/lldb/tests/test_core.py Adds mock-based unit tests for core/session/backend/CLI behavior.
lldb/agent-harness/cli_anything/lldb/tests/TEST.md Documents LLDB harness test plan and results.
lldb/agent-harness/cli_anything/lldb/skills/SKILL.md Adds agent-facing skill definition and usage notes.
lldb/agent-harness/cli_anything/lldb/lldb_cli.py Implements the Click CLI (groups, --json, REPL, session lifecycle).
lldb/agent-harness/cli_anything/lldb/core/session.py Implements the stateful LLDBSession wrapper over the LLDB API.
lldb/agent-harness/cli_anything/lldb/core/breakpoints.py Adds higher-level breakpoint wrappers.
lldb/agent-harness/cli_anything/lldb/core/inspect.py Adds higher-level inspection wrappers.
lldb/agent-harness/cli_anything/lldb/core/threads.py Adds thread wrapper helpers.
lldb/agent-harness/cli_anything/lldb/core/init.py Declares the core package.
lldb/agent-harness/cli_anything/lldb/main.py Enables python -m cli_anything.lldb execution.
lldb/agent-harness/cli_anything/lldb/init.py Declares harness package + version.
lldb/agent-harness/cli_anything/lldb/README.md Adds harness README (install, prerequisites, usage, testing).
lldb/agent-harness/LLDB.md Adds LLDB backend/SOP notes for the harness.
lldb/agent-harness/HARNESS.md Adds harness specification overview and architecture diagram.
README.md Adds LLDB to the top-level catalog and updates counts/structure.
README_CN.md Adds LLDB to the Chinese catalog and updates counts/structure.
.gitignore Ensures lldb/agent-harness/ is included by the monorepo ignore rules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread README.md Outdated
<a href="#-demonstrations"><img src="https://img.shields.io/badge/Demos-16_Apps-green?style=for-the-badge" alt="Demos"></a>
<a href="#-test-results"><img src="https://img.shields.io/badge/Tests-2%2C130_Passing-brightgreen?style=for-the-badge" alt="Tests"></a>
<a href="#-demonstrations"><img src="https://img.shields.io/badge/Demos-31_Apps-green?style=for-the-badge" alt="Demos"></a>
<a href="#-test-results"><img src="https://img.shields.io/badge/Tests-2%2C159_Passing-brightgreen?style=for-the-badge" alt="Tests"></a>
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Tests badge shows 2,159 passing tests, but later in this README the total is 2,173 (and the unit/e2e breakdown sums to 2,173). Please update the badge count so the headline test number is consistent throughout the doc.

Suggested change
<a href="#-test-results"><img src="https://img.shields.io/badge/Tests-2%2C159_Passing-brightgreen?style=for-the-badge" alt="Tests"></a>
<a href="#-test-results"><img src="https://img.shields.io/badge/Tests-2%2C173_Passing-brightgreen?style=for-the-badge" alt="Tests"></a>

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
**CLI-Anything** — *Make any software with a codebase Agent-native.*

<sub>A methodology for the age of AI agents | 16 professional software demos | 1,839 passing tests</sub>
<sub>A methodology for the age of AI agents | 31 professional software demos | 2,159 passing tests</sub>
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This closing tagline says "2,159 passing tests", but the Test Summary table above reports 2,173 total passing tests. Please align this number with the actual totals to avoid drifting metrics in the README.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +115
with conn:
request = conn.recv()
response, should_stop = server.handle(request)
conn.send(response)
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multiprocessing.connection uses pickle for conn.recv(). If the authkey leaks (e.g., via the state file), a local attacker can send a crafted pickle payload and achieve code execution in the daemon. For safer IPC, consider switching to a JSON-over-socket protocol (or otherwise ensure the authkey cannot be read by other users and strictly validate incoming message types/shape before dispatch).

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +97
listener = Listener(("127.0.0.1", 0), authkey=authkey)
raw_socket = listener._listener._socket # type: ignore[attr-defined]
raw_socket.settimeout(1.0)
_write_state_file(state_file, listener.address, authkey)
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relies on private internals (listener._listener._socket) to set a timeout, which is brittle across Python versions/implementations and may break on some platforms. Please avoid accessing private attributes here (e.g., implement the daemon with an explicit socket/selectors accept loop, or use a design that doesn’t require reaching into Listener internals).

Copilot uses AI. Check for mistakes.
try:
threads = _require_process().threads().get("threads", [])
selected = next((t for t in threads if t.get("selected")), None)
_output(ctx, selected or {"error": "No selected thread"})
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When no thread is selected, this returns a JSON payload with an "error" field but still exits successfully (exit code 0). For consistency with other commands (and to help agents detect failure), please raise a ClickException / call ctx.exit(1) in this branch so the command fails with a non-zero status.

Suggested change
_output(ctx, selected or {"error": "No selected thread"})
if selected is None:
_output(ctx, {"error": "No selected thread"})
ctx.exit(1)
_output(ctx, selected)
except click.exceptions.Exit:
raise

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +101
- Commands are stateless between separate process invocations.
- Use REPL for persistent state:
- run `cli-anything-lldb`
- execute multi-step commands in one session
- Call `target create` before process or core commands.
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skill file says commands are stateless between separate process invocations, but this harness explicitly persists state across separate CLI invocations via the session daemon (also described in the README). Please correct this to reflect the actual persistent-session behavior so agents don’t misuse the CLI.

Suggested change
- Commands are stateless between separate process invocations.
- Use REPL for persistent state:
- run `cli-anything-lldb`
- execute multi-step commands in one session
- Call `target create` before process or core commands.
- Session state persists across separate `cli-anything-lldb` invocations via the harness/session daemon.
- You can use separate commands as part of one multi-step workflow and rely on previously created target/process/debugger context still being available.
- The REPL is also available for interactive workflows:
- run `cli-anything-lldb`
- execute multi-step commands in one session
- Call `target create` before process or core commands when no target has been created in the current persistent session yet.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +49
"gimp": "\033[38;5;214m", # warm orange
"blender": "\033[38;5;208m", # deep orange
"inkscape": "\033[38;5;39m", # bright blue
"audacity": "\033[38;5;33m", # navy blue
"libreoffice": "\033[38;5;40m", # green
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the harness SOP, utils/repl_skin.py must be copied from cli-anything-plugin/repl_skin.py unmodified (see cli-anything-plugin/HARNESS.md:75-76). This copy differs from the plugin version (e.g., formatting/alignment in the accent color map and constant comments), so please replace it with an exact byte-for-byte copy to keep skins consistent across harnesses.

Suggested change
"gimp": "\033[38;5;214m", # warm orange
"blender": "\033[38;5;208m", # deep orange
"inkscape": "\033[38;5;39m", # bright blue
"audacity": "\033[38;5;33m", # navy blue
"libreoffice": "\033[38;5;40m", # green
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"gimp": "\033[38;5;214m", # warm orange
"blender": "\033[38;5;208m", # deep orange
"inkscape": "\033[38;5;39m", # bright blue
"audacity": "\033[38;5;33m", # navy blue
"libreoffice": "\033[38;5;40m", # green
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +35
state_file.parent.mkdir(parents=True, exist_ok=True)
payload = {
"host": address[0],
"port": address[1],
"authkey": _encode_authkey(authkey),
"pid": os.getpid(),
}
state_file.write_text(json.dumps(payload), encoding="utf-8")


Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The session state file stores the daemon authkey and is written without enforcing restrictive permissions. On multi-user systems this can allow other users to read the authkey and connect to the daemon. Please write the file with owner-only permissions (and ensure the parent dir is not world-readable) and consider validating file ownership before trusting its contents.

Suggested change
state_file.parent.mkdir(parents=True, exist_ok=True)
payload = {
"host": address[0],
"port": address[1],
"authkey": _encode_authkey(authkey),
"pid": os.getpid(),
}
state_file.write_text(json.dumps(payload), encoding="utf-8")
state_file.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
try:
os.chmod(state_file.parent, 0o700)
except OSError:
pass
payload = {
"host": address[0],
"port": address[1],
"authkey": _encode_authkey(authkey),
"pid": os.getpid(),
}
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
flags |= getattr(os, "O_NOFOLLOW", 0)
fd = os.open(state_file, flags, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as state_fp:
json.dump(payload, state_fp)
try:
os.chmod(state_file, 0o600)
except OSError:
pass

Copilot uses AI. Check for mistakes.
def process_info(ctx):
"""Show process status."""
try:
data = _require_process()._process_info()
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process info calls a private session method (_process_info) over RPC. It would be better to expose a public process_info()/process_status() API on LLDBSession and call that instead, so the CLI is not coupled to underscored internals (and so the RPC surface is intentional/documented).

Suggested change
data = _require_process()._process_info()
process = _require_process()
if hasattr(process, "process_info"):
data = process.process_info()
elif hasattr(process, "process_status"):
data = process.process_status()
else:
data = process._process_info()

Copilot uses AI. Check for mistakes.
@yuh-yang
Copy link
Copy Markdown
Collaborator

Thanks for the LLDB harness.

You posted:

python -m pytest cli_anything/lldb/tests/test_core.py -v
============================= 18 passed in 0.29s ==============================

python -m pytest cli_anything/lldb/tests/test_full_e2e.py -v
================== 3 passed, 1 skipped, 2 warnings in 9.48s ==================

I was wondering why 1 test was skipped and what the 2 warnings were on your side.

In my review environment, the unit suite also passed, but I could not fully reproduce the non-skipped E2E path because this environment does not have the lldb Python bindings installed. My local E2E run was skipped for environment reasons plus the optional core-file gate:

  • 3 skips: lldb module not importable
  • 1 skip: LLDB_TEST_CORE not set or file missing

File structure looks fine, SKILL.md is present, and I did not find an obvious serious bug in the implementation from the diff review.

One minor question: I noticed cli_anything/lldb/utils/repl_skin.py is not a byte-identical plugin copy and adds a small non-interactive TTY fallback. Not blocking on my side, but I’m curious why you chose to diverge from the canonical copy here.

If your local LLDB-backed E2E results are accurate, this looks close to mergeable.

@AiMiDi
Copy link
Copy Markdown
Contributor Author

AiMiDi commented Apr 18, 2026

I removed the remaining skipped core-load E2E case. The negative-path test now uses an auto-generated placeholder file, so the default LLDB E2E suite runs fully without extra env setup.

Current local results on Windows:

  • test_core.py: 18 passed
  • test_full_e2e.py: 4 passed, 2 warnings

The two warnings are SWIG DeprecationWarnings from the LLDB Python bindings, not harness logic failures.

Copy link
Copy Markdown
Collaborator

@omerarslan0 omerarslan0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Click/Session architecture is clean, handle_error is centralized, and the attach/launch detach logic is correct. MERGEABLE, 18 unit + 3 E2E passing. A few small items:

  • The process_info command calls the private LLDBSession._process_info() — should be promoted to a public method
  • SKILL.md still states "Commands are stateless between separate process invocations", but this PR adds a persistent session — wording should be updated
  • memory find reads the entire region into Python and scans linearly; please add a max-scan cap or chunked read, and document the limit in the command help
  • SBAttachInfo / listener usage differs between attach_pid and attach_name — worth unifying on a single pattern
  • README test totals are inconsistent (2,173 vs 2,120→2,173); reconcile to one number

@AiMiDi
Copy link
Copy Markdown
Contributor Author

AiMiDi commented Apr 21, 2026

I addressed the remaining LLDB review notes on this PR.

Updates made:

  • promoted process info to the public session API instead of calling _process_info() directly
  • updated SKILL.md to describe the actual persistent-session behavior for non-REPL invocations
  • changed memory find to a chunked scan with a documented 1 MiB per-call cap
  • unified the attach flow so attach_pid and attach_name both go through the same SBAttachInfo path
  • replaced the daemon IPC transport with a localhost JSON socket protocol and tightened session state-file handling so the token is stored in an owner-scoped file instead of using the previous pickle-based multiprocessing.connection RPC path
  • synced cli_anything/lldb/utils/repl_skin.py to the current canonical cli-anything-plugin/repl_skin.py copy
  • refreshed the LLDB docs / test summaries and repo README totals to match the current branch state

Current local results on Windows:

  • python -m pytest cli_anything/lldb/tests/test_core.py -q -> 23 passed
  • python -m pytest cli_anything/lldb/tests/test_full_e2e.py -v -rs -> 4 passed, 2 warnings
  • python -m pytest cli_anything/lldb/tests -q -> 27 passed, 2 warnings

The remaining warnings are SWIG DeprecationWarnings from the LLDB Python bindings, not harness logic failures.

AiMiDi added 2 commits April 21, 2026 16:45
…windows-fixes

# Conflicts:
#	.gitignore
#	README.md
#	README_CN.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants