diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d1cbc1d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# AGENTS.md + +## Read first + +- Read `PRD.md`, `CONTEXT.md`, and applicable ADRs before changing behavior. +- Keep the Rust harness synchronous and the Ghidra worker + capability-sandboxed. +- Treat JSON schemas, error codes, ordering, and exit statuses as public API. + +## Change rules + +- Add a regression test for every bug fix. +- Never change golden files or schemas implicitly. +- Keep the Rust operation registry, Java dispatch, schemas, and goldens in + exact parity. +- Preserve strict stdout/stderr framing. +- Preserve the worker's no-network and no-default-data-egress boundary. +- Do not add `unsafe`, an async runtime, database, daemon, or dependency + without an accepted design change. +- Do not weaken bounds, validation, sandboxing, or atomic store behavior. + +## Required checks + +- `cargo fmt --check` +- `cargo clippy --all-targets --all-features -- -D warnings` +- `cargo test --all-targets` +- `cargo deny check` +- `nix flake check` + +## Source control + +- Use Jujutsu for normal local change management. +- Use `jj status`, `jj diff`, `jj log`, `jj describe`, and `jj new`. +- Push Forgejo bookmarks through `jj git push`. +- Keep changes focused and preserve unrelated user work. +- Use direct Git commands only for interop or recovery that Jujutsu cannot + perform. +- Never run `git clean -xdf`; it can delete colocated `.jj/` metadata. diff --git a/CONTEXT.md b/CONTEXT.md index 2a51c56..4e45c3c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -12,32 +12,88 @@ names or locations. _Avoid_: Binary, file, target **Analysis Profile**: -The declared analysis inputs other than the Sample, including the Ghidra -version, analyzer configuration, language selection, and tool protocol version. +The canonical analysis inputs other than the Sample, including Ghidra and Java +versions, Target Specification, fully resolved loader and analyzer options, +analysis-adapter version, and maximum CPU count. _Avoid_: Configuration, settings +**Target Specification**: +The loader, executable format, processor language, and compiler specification +used by Ghidra to interpret a Sample. +_Avoid_: Architecture, target + **Analysis**: The reusable body of facts Ghidra derives from one Sample under one Analysis Profile. _Avoid_: Project, database, workspace +**Quarantined Analysis**: +Analysis data that failed integrity validation and is retained for diagnosis +but can never satisfy a Query. +_Avoid_: Analysis, cache miss + **Query**: A read-only question evaluated against an Analysis. A Query cannot change the Sample or Analysis. _Avoid_: Command, job, operation +**Function**: +A Ghidra-discovered unit of executable behavior identified within an Analysis +by its entry Address. +_Avoid_: Symbol, method + **Function Selector**: An unambiguous reference used to choose a function by address or resolved symbol name. _Avoid_: Function name, function ID +**Call Edge**: +A directed relationship from a caller Function through a callsite Address to +a resolved or unresolved callee, produced by a future targeted Query. +_Avoid_: Call, dependency + +**Reference**: +A Ghidra-derived relationship from one Address to another Address or external +symbol, produced by a future targeted Query. +_Avoid_: Pointer, cross-reference result + +**Address**: +A location within an Analysis identified by an explicit Ghidra address space +and an offset whose width is defined by that space. +_Avoid_: Integer, offset, pointer + **Artifact**: A durable, schema-versioned representation of a Query result tied to its Analysis. _Avoid_: Output, report, result file -**Analysis Store**: -The tool-owned collection of reusable Analyses and Artifacts. It is disposable -and never contains the authoritative Sample. -_Avoid_: Project directory, cache directory, workspace +**Diagnostic Log**: +A tool-owned record of execution diagnostics that may explain an Analysis or +Query failure but is not part of the Query result. +_Avoid_: Artifact, output +**Invocation**: +One synchronous attempt by the Rust harness to run a bounded Ghidra/Java +operation and validate its response. +_Avoid_: Job, session, worker + +**Worker Sandbox**: +The capability-limited environment containing only the untrusted Ghidra/JVM +child for one Invocation; the trusted Rust harness remains outside it. +_Avoid_: Analysis Store, containerized CLI + +**Cleanup Transaction**: +Tool-owned staged data that has been removed from active store visibility and +is awaiting complete deletion or a recoverable retry. +_Avoid_: Analysis, quarantine + +**Target Verification**: +The declared relationship between a Target Specification and the project's +integration-test matrix: either verified or unverified. +_Avoid_: Ghidra confidence, supported Sample + +**Analysis Store**: +The tool-owned collection of reusable Analyses, Quarantined Analyses, Artifacts, +and Diagnostic Logs. It is disposable and never contains the authoritative +Sample. +_Avoid_: Project directory, cache directory, workspace diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/PRD.md b/PRD.md index 78958c2..3adafd7 100644 --- a/PRD.md +++ b/PRD.md @@ -5,6 +5,9 @@ - Status: Draft - Revision: 0.1 - Audience: maintainers and early users +- Repository name: `ghidra-cli` +- Executable name: `ghidr` +- License: Apache-2.0 ## Summary @@ -12,9 +15,20 @@ analysis and decompilation through Ghidra. It hides Ghidra project management and Java process details behind commands that accept a sample path directly. +The repository retains the descriptive `ghidra-cli` name even though another +public project uses the same name for a similar purpose. The installed +executable will use a shorter distinct name. This implementation remains +independent and does not copy source from that GPL-3.0 project. + +The project is developed with AI assistance and human review. AI assistance +does not relax provenance, licensing, security, testing, or maintainership +requirements. Maintainers are responsible for every accepted contribution. + The first release favors a narrow, reliable workflow over a broad command -surface. It is intended to work equally well for a person at a terminal and an -automation agent consuming structured output. +surface. Automation agents are the primary consumers of its interface +contract. Human-readable terminal output is a first-class presentation of the +same underlying results, but it does not take priority over predictable +machine behavior. ## Problem @@ -40,18 +54,26 @@ make a small tool harder to trust. 6. No background process is required for the initial command set. 7. Concurrency is introduced only with a concrete concurrent workflow. 8. Ghidra remains the analysis engine; Rust owns orchestration and policy. +9. Automation behavior takes priority when machine and terminal conveniences + conflict. +10. Bounded output must disclose its bounds; truncation is never implicit. +11. Existing Unix tools perform ad hoc filtering; the CLI does not invent a + query language. ## Users +### Automation agent + +The primary consumer. It needs deterministic commands, bounded output, stable +JSON schemas, meaningful exit codes, zero ambiguous resolution, and enough +provenance to explain where a result came from. It can compose JSON output with +standard tools such as `jq` before placing selected results in model context. + ### Human analyst Wants to inspect an unfamiliar executable, discover functions, and decompile a selected function without opening the Ghidra GUI or learning its project model. - -### Automation agent - -Needs deterministic commands, bounded output, stable JSON schemas, meaningful -exit codes, and enough provenance to explain where a result came from. +Human-readable output presents the same semantics as the automation contract. ### Maintainer @@ -65,9 +87,11 @@ and compatibility failures that are discovered by tests rather than users. - List discovered functions with stable identifiers. - Decompile one selected function. - Reuse compatible analysis without requiring user-managed project names. +- Inspect and explicitly clean tool-owned stored data. - Emit concise terminal output and versioned JSON. - Bound Ghidra execution by configurable time and memory limits. - Operate without network access after dependencies are installed. +- Run on x86-64 Linux through a pinned Nix package. ## Non-goals for the first release @@ -79,28 +103,168 @@ and compatibility failures that are discovered by tests rather than users. - Renaming symbols, applying types, or changing function signatures. - Arbitrary Java or Python script execution. - A custom filtering or query language. +- Manual Ghidra loader, processor-language, or compiler-specification + overrides. +- Raw Samples that require manual Target Specification. - Automatically downloading Ghidra or a JDK. - Managing user-visible Ghidra projects. +- Automatically evicting Analyses or Artifacts. +- Claiming support for untested host platforms. - Supporting every historical Ghidra version. +## Initial platform and distribution + +Version 0.1 officially supports `x86_64-linux`. The implementation should keep +host-specific behavior isolated, but successful compilation on another host +does not constitute support without passing the real Ghidra integration suite. + +The supported installation is a pinned Nix flake package containing compatible +versions of `ghidra-cli`, Ghidra 12.1.2, and JDK 21. The flake pins a nixpkgs +revision that provides those versions; it does not float with a registry or +channel. Users can run the package without a global installation: + +```console +nix run -- doctor +``` + +macOS is the next intended platform. It becomes supported only after CI or a +maintainer-owned Mac runs the same integration fixtures used on Linux. Other +Linux architectures and non-Nix installation methods are deferred. + +Version 0.1 has no persistent configuration file. Runtime policy is expressed +through documented CLI flags and narrowly scoped environment variables. A +configuration file is deferred until repeated real-world usage demonstrates +which settings deserve persistence. + +## Verified Sample targets + +Version 0.1 verifies these Sample targets through real Ghidra integration +fixtures and reviewed golden outputs: + +- ELF, x86-64 little-endian. +- PE32+, x86-64. + +The supported host and verified Sample target are independent: the x86-64 Linux +host analyzes both Linux and Windows executable formats. Mach-O, ARM64, +firmware images, raw binaries, and other Ghidra targets are unverified in +version 0.1. + +A Target Specification that Ghidra recognizes but the version 0.1 matrix does +not cover is analyzed on a best-effort basis. Success remains success, but +response provenance includes: + +```json +{ + "target": { + "loader": "", + "format": "", + "processor_language": "", + "compiler_specification": "", + "verification": "unverified" + }, + "warnings": [ + { + "code": "unverified_target", + "message": "target is outside the verified integration-test matrix", + "details": { + "verified_targets": [ + "elf-x86_64-little-endian", + "pe32plus-x86_64" + ] + } + } + ] +} +``` + +`verification` describes this project's integration coverage, not Ghidra's +confidence in its own result. Failure by Ghidra to recognize a Target +Specification remains a structured error. Ambiguous auto-detection also fails +and returns bounded candidate Target Specifications; the CLI never chooses one +silently. + +Ghidra upgrades are deliberate reviewed changes. An upgrade must change the +pinned nixpkgs revision, pass the complete real-Ghidra integration suite, and +explain any protocol schema or golden-fixture differences before merge. + ## Command-line experience ### Installation diagnosis ```console -$ ghidra-cli doctor -Ghidra 12.1: ready +$ ghidr --format human doctor +Ghidra 12.1.2: ready JDK 21: ready Analysis store: writable ``` `doctor` must not modify a Sample or create an Analysis. It may create and remove a temporary file inside the Analysis Store to verify writability. +It reports the store path and total disk usage. + +Successful `doctor` response data has this version 0.1 shape: + +```json +{ + "ready": true, + "components": { + "ghidra": { + "status": "ready", + "version": "12.1.2", + "launcher": { + "encoding": "utf8", + "value": "/nix/store/.../bin/ghidra-analyzeHeadless" + } + }, + "java": { + "status": "ready", + "version": "21", + "executable": { + "encoding": "utf8", + "value": "/nix/store/.../bin/java" + } + }, + "analysis_store": { + "status": "ready", + "path": { + "encoding": "utf8", + "value": "/home/user/ghidr-store" + }, + "source": "home_fallback", + "usage": { + "logical_bytes": 1048576, + "allocated_bytes": 1114112 + } + }, + "sandbox": { + "status": "ready", + "backend": "bubblewrap", + "verification": "verified" + } + } +} +``` + +Each component status is exactly one of `ready`, `missing`, `incompatible`, +`invalid`, or `unwritable`. The aggregate `ready` is true only when every +required component is ready. `doctor` runs every safe check instead of stopping +at the first failure, and it removes its Analysis Store write probe before +returning. + +Storage usage is always represented by both `logical_bytes`, the sum of file +lengths, and `allocated_bytes`, the sum of Linux `st_blocks * 512` including +directory allocation. Store scans use `lstat` and never follow symlinks. + +A healthy report is a success document on stdout with exit status 0. If any +required component is not ready, `doctor` emits a `doctor_failed` error on +stderr with exit status 1; `error.details.report` contains the same complete +shape with `ready` set to false. This preserves strict JSON framing while +allowing one invocation to diagnose every component. ### Inspect a sample ```console -$ ghidra-cli inspect ./sample +$ ghidr --format human inspect ./sample Sample: 8d969eef... Format: ELF Language: x86:LE:64 @@ -111,37 +275,448 @@ If no compatible Analysis exists, the command creates one automatically. The same bytes at another path reuse the Analysis. Different bytes at the same path create a different Analysis. +Successful `inspect` response data has this minimal version 0.1 shape: + +```json +{ + "sample": { + "sha256": "<64-character digest>", + "size_bytes": 123456 + }, + "program": { + "image_base": { + "space": "ram", + "offset": "0x0000000000400000" + }, + "minimum_address": { + "space": "ram", + "offset": "0x0000000000400000" + }, + "maximum_address": { + "space": "ram", + "offset": "0x0000000000404fff" + }, + "function_count": 42 + }, + "analysis": { + "disposition": "created" + } +} +``` + +The three Address fields are nullable when Ghidra has no applicable memory +Address. `function_count` includes memory, external, and thunk Functions: the +same population returned by `functions`. `analysis.disposition` is exactly one +of `created`, `reused`, or `rebuilt`. Executable format, processor language, +compiler specification, Ghidra version, and Analysis Profile digest live in +the common response provenance and are not duplicated here. + ### List functions ```console -$ ghidra-cli functions ./sample +$ ghidr --format human functions ./sample ADDRESS SIZE NAME 0x00401000 112 _start 0x00401120 248 main ``` -The command supports an explicit result limit. It does not implement a -filtering expression language in the first release; JSON consumers can use -existing tools such as `jq`. +The command returns a bounded result set and discloses whether additional +results exist. It does not implement a filtering expression language in the +first release; JSON consumers can use existing tools such as `jq`. + +`functions` includes every Ghidra Function, including external/imported +functions and thunks. Each item explicitly states `location`, `is_external`, +`is_thunk`, and `decompilable`. External functions remain visible for program +structure but are not valid decompilation targets. + +The minimal version 0.1 function-list item is: + +```json +{ + "name": "parse", + "qualified_name": "Widget::parse", + "entry": { + "space": "ram", + "offset": "0x0000000000401120" + }, + "body_address_count": 248, + "location": "memory", + "is_external": false, + "is_thunk": false, + "thunk_target_entry": null, + "decompilable": true +} +``` + +`thunk_target_entry` is an Address when Ghidra resolves a thunk target and is +otherwise `null`. Version 0.1 deliberately omits signatures, parameters, +locals, call counts, and analyzer-derived heuristics from this collection. +Those details require targeted Queries rather than making every list item +larger and less stable. + +The `functions` Query uses this deterministic total order: + +1. Functions whose `location` is `memory`, followed by functions whose + `location` is `external`. +2. `entry.space` in byte-for-byte ascending order. +3. `entry.offset` in unsigned numeric ascending order. + +Ghidra permits only one Function at an entry Address within an Analysis, so the +Address completes the ordering and no symbol-name tie-breaker is required. The +Query reports the order identifier `location_then_entry_ascending`. + +`name` is Ghidra's exact Function basename (`getName(false)`). +`qualified_name` is Ghidra's exact fully qualified Function name +(`getName(true)`), including its `::` namespace spelling. The CLI preserves +case, UTF-8 bytes, generated names such as `FUN_00401120`, and analyzer-applied +demangling. It does not independently demangle, simplify templates, strip +namespaces, or Unicode-normalize names. + +Function Selector comparison uses those exact strings. Even a qualified name +that resolves to multiple Functions produces the normal ambiguity error with +entry Addresses. Structured namespaces and alternate symbol names are deferred +to a targeted symbol or Function-detail Query. + +Collection Queries share these flags: + +```console +ghidr functions ./sample --limit 100 +ghidr functions ./sample --limit 100 --offset 100 +ghidr functions ./sample --all +ghidr decompile ./sample --name main --max-inline-bytes 1048576 +``` + +- `--limit ` sets the maximum number of returned items. +- `--offset ` skips that many items in the documented stable order. +- `--all` explicitly requests every item. +- Omitting both `--limit` and `--all` applies a default limit of 100 items. +- `--all` cannot be combined with `--limit` or `--offset`. +- A zero `--limit` is invalid; `--all` is the only spelling for unbounded + collection output. +- Invalid combinations fail as invalid arguments rather than choosing an + implicit precedence. +- `--max-inline-bytes ` overrides the 65,536-byte inline budget with a + positive integer measured strictly in bytes. +- `--max-inline-bytes 0`, unit suffixes, and an unbounded-inline sentinel are + invalid. + +Collection JSON includes explicit page metadata: + +```json +{ + "page": { + "order": "location_then_entry_ascending", + "offset": 0, + "limit": 100, + "returned": 100, + "total": 342, + "has_more": true + }, + "items": [] +} +``` + +The ordering is part of the Query contract. Repeating a page against the same +immutable Analysis returns the same items. Human-readable output ends with a +concise notice when more results exist. + +### Oversized results + +Item limits alone do not bound response size because one item, such as a +decompiled function or extracted string, may be very large. Every response is +therefore also subject to a serialized-byte policy. The default successful +inline budget is 65,536 bytes of serialized output. + +The CLI never truncates a JSON document or silently removes fields to meet that +budget. When a complete result would exceed it, the CLI writes the result +atomically as an Artifact in the Analysis Store and returns a successful, +bounded descriptor: + +```json +{ + "schema_version": 1, + "kind": "decompilation", + "provenance": { + "ghidr_version": "0.1.0", + "adapter_protocol_version": 1, + "ghidra_version": "12.1.2", + "java_version": "21", + "sample_sha256": "", + "source_path": { + "encoding": "utf8", + "value": "./sample" + }, + "analysis_profile_sha256": "", + "target": { + "loader": "", + "format": "", + "processor_language": "", + "compiler_specification": "", + "verification": "verified" + }, + "analysis_store": { + "path": { + "encoding": "utf8", + "value": "/home/user/ghidr-store" + }, + "source": "home_fallback" + }, + "sandbox": { + "backend": "bubblewrap", + "verification": "verified" + }, + "limits": { + "max_heap_mib": 2048, + "max_cpu": 2, + "analysis_timeout_seconds": 600, + "decompile_timeout_seconds": 60, + "child_watchdog_seconds": 780 + } + }, + "data": { + "spilled": true, + "artifact": { + "path": { + "encoding": "utf8", + "value": "/home/user/ghidr-store/artifacts///.json" + }, + "bytes": 2097152, + "sha256": "", + "media_type": "application/json", + "contains": "complete_success_response" + } + }, + "warnings": [] +} +``` + +Spilling is not an error: the requested result was produced in full and remains +available to local tools such as `jq` and `rg`. Human-readable output reports +the Artifact path, size, and digest instead of printing partial content. + +The Artifact contains the complete success response that would otherwise have +been written to stdout, including `schema_version`, `kind`, `provenance`, +command `data`, and `warnings`. It uses UTF-8 JSON without a byte-order mark, +the same compact serializer as stdout, and exactly one trailing LF. The +reported `bytes` and `sha256` cover the exact file bytes, including that LF. + +Artifact creation is atomic. A completed Artifact is immutable until explicit +cleanup and never recursively spills regardless of its own size. Version 0.1 +does not add an Artifact-reading command: local automation reads the tagged +absolute path directly with tools such as `jq` and may verify the SHA-256 +before use. `contains` is exactly `complete_success_response`. + +The caller's inline budget applies strictly to successful stdout documents. +Before publishing an Artifact, the CLI serializes its prospective descriptor. +If that descriptor exceeds the selected budget, the CLI removes the temporary +Artifact and fails with `inline_budget_too_small`. Typed error details contain +`configured_bytes` and the exact `required_bytes`; retrying at that value or +higher can return the descriptor. + +Error documents use an independent fixed 65,536-byte serialized safety cap. +This guarantees that a bounded stderr response can explain why no valid success +document fit. A successful document never exceeds `--max-inline-bytes`. + +`--all` removes the collection item bound; it does not remove the serialized +byte budget. Only `--max-inline-bytes ` changes that budget. + +### Decompile one function + +Successful `decompile` response data has this minimal version 0.1 shape: + +```json +{ + "requested_selector": { + "kind": "name", + "value": "main" + }, + "function": { + "name": "main", + "qualified_name": "main", + "entry": { + "space": "ram", + "offset": "0x0000000000401120" + } + }, + "decompilation": { + "syntax": "ghidra_c", + "text": "int main(void) {\n return 0;\n}\n" + } +} +``` + +`requested_selector` records the exact selector kind and value supplied by the +caller. `function` records the unique Function that selector resolved to. +`syntax` is `ghidra_c` because the text is Ghidra's C-like representation and +is not guaranteed to be valid ISO C. Decompiler warnings use the response +envelope's normal `warnings` collection rather than being embedded into the +text. A timeout or failure never returns partial decompilation text. + +The adapter normalizes CRLF and bare CR line endings in `decompilation.text` to +LF. It preserves every other character exactly as returned by Ghidra, +including indentation, comments, blank lines, trailing spaces, and the +presence or absence of a final newline. Version 0.1 does not reformat generated +code. A future formatter or richer representation may be added in response to +demonstrated usage, but it must be an explicit contract change rather than an +unannounced alteration of `text`. + +### Clean stored data + +```console +ghidr clean ./sample +ghidr clean ./sample --dry-run +ghidr clean --digest <64-character-sha256> +ghidr clean --all +ghidr clean --all --dry-run +ghidr clean --all --yes +``` + +The first release never evicts an Analysis or Artifact automatically. Cleanup +is an explicit user request. `clean --all` requires interactive confirmation; +non-interactive callers must provide `--yes` deliberately. + +`clean ` hashes the current Sample bytes, then removes every Analysis +and Artifact for that Sample digest across all Analysis Profiles. It never +removes data for another Sample. Cleanup reports the affected Analysis Profiles +and the logical and allocated usage associated with removed targets. + +`clean --digest ` provides the same Sample-wide behavior when the +original Sample is no longer available. The digest must contain exactly 64 +hexadecimal characters. ``, `--digest`, and `--all` are mutually +exclusive cleanup targets; specifying none or more than one is an invalid +argument. + +Cleanup executes by default. `--dry-run` reports the exact targets and their +logical and allocated usage without deleting anything. Sample- and digest-specific +cleanup requires no confirmation because the destructive verb and target are +both explicit. + +Store-wide cleanup follows the selected output format: + +- JSON mode never prompts. Without `--yes`, it fails with + `confirmation_required`; the typed error details include the prospective item + counts and usage. The caller may then repeat the request with `--yes`. +- Human mode prompts only when attached to a terminal. +- Human mode without a terminal requires `--yes`. +- `--dry-run` never prompts and never deletes in either format. + +Successful cleanup response data has this version 0.1 shape: + +```json +{ + "mode": "executed", + "target": { + "kind": "sample", + "source_path": { + "encoding": "utf8", + "value": "./sample" + }, + "sha256": "" + }, + "matched": { + "analyses": 2, + "quarantined_analyses": 1, + "artifacts": 3, + "diagnostic_logs": 4, + "usage": { + "logical_bytes": 5242880, + "allocated_bytes": 5308416 + }, + "analysis_profile_sha256": [ + "", + "" + ] + }, + "removed": { + "analyses": 2, + "quarantined_analyses": 1, + "artifacts": 3, + "diagnostic_logs": 4, + "usage": { + "logical_bytes": 5242880, + "allocated_bytes": 5308416 + } + } +} +``` + +`mode` is exactly `executed` or `dry_run`. `target` is a tagged union: +`sample` contains the caller's source Path and resolved SHA-256, `digest` +contains the supplied SHA-256, and `all` has no additional fields. `matched` +records the preflight snapshot. Its Analysis Profile digests are unique and +bytewise sorted. For a dry run, `removed` is `null`. + +A successful execution reports exact removed counts, including Quarantined +Analyses and Diagnostic Logs. Matching nothing is a successful result with +zero counts. A large cleanup report uses the normal Artifact-spill behavior. +The reported allocation is the preflight allocation associated with the +successfully deleted targets, not a claim about the filesystem's observed +free-space delta; compression, reflinks, metadata, and concurrent activity can +make those values differ. + +Cleanup uses a recoverable two-phase transaction: + +1. Resolve every target and acquire its lock in deterministic digest order. +2. If any lock is held, fail with `analysis_busy` before changing store data. +3. Atomically rename each target into a tool-owned cleanup transaction + directory on the same filesystem. +4. If any staging rename fails, roll back completed renames and return an + error. +5. Delete staged targets only after every rename succeeds. +6. Report success only after every staged target has actually been deleted. + +If deletion fails after partial progress, return retryable +`cleanup_incomplete`. Typed details include exact removed and remaining item +counts, logical and allocated usage, and the cleanup transaction's tagged +absolute Path. +Transaction contents can never satisfy a Query. A later matching `clean` +resumes deletion of the transaction before cleaning newly matched active data. +This provides recoverable interruption behavior even though deleting multiple +filesystem objects cannot be made truly atomic. ### Decompile a function ```console -$ ghidra-cli decompile ./sample main +$ ghidr --format human decompile ./sample --name main int main(int argc, char **argv) { ... } ``` -A function may be selected by address or a uniquely resolved symbol. An -ambiguous symbol is an error and returns the candidates instead of silently -choosing one. +Exactly one explicit Function Selector is required: + +```console +ghidr decompile ./sample --name main +ghidr decompile ./sample --address ram:0x0000000000401120 +``` + +`--name` performs exact symbol resolution. An ambiguous symbol is an error and +returns bounded candidates instead of silently choosing one. `--address` +requires an explicit address space and canonical hexadecimal offset. The flags +are mutually exclusive; positional selector guessing is unsupported. + +`--address` must equal a function entry Address. An interior Address fails with +`function_entry_required`; typed error details include the containing +function's entry when Ghidra identifies one. Version 0.1 does not provide a +containing-address selector. + +`--name` is exact and case-sensitive. A qualified name matches only that full +qualified name. An unqualified name matches exact basenames across namespaces: +one match succeeds, zero fails with `function_not_found`, and multiple fail +with `function_selector_ambiguous`. Version 0.1 performs no fuzzy, glob, regex, +or demangled-substring matching. ### Structured output ```console -$ ghidra-cli --format json inspect ./sample +$ ghidr inspect ./sample +``` + +JSON is the unconditional default. Human-readable output is explicit: + +```console +$ ghidr --format human inspect ./sample ``` JSON uses a versioned envelope: @@ -151,68 +726,551 @@ JSON uses a versioned envelope: "schema_version": 1, "kind": "inspection", "provenance": { + "ghidr_version": "0.1.0", + "adapter_protocol_version": 1, + "ghidra_version": "12.1.2", + "java_version": "21", "sample_sha256": "", - "ghidra_version": "", - "analysis_profile": "" + "source_path": { + "encoding": "utf8", + "value": "./sample" + }, + "analysis_profile_sha256": "", + "target": { + "loader": "", + "format": "", + "processor_language": "", + "compiler_specification": "", + "verification": "verified" + }, + "analysis_store": { + "path": { + "encoding": "utf8", + "value": "/home/user/ghidr-store" + }, + "source": "xdg" + }, + "sandbox": { + "backend": "bubblewrap", + "verification": "verified" + }, + "limits": { + "max_heap_mib": 2048, + "max_cpu": 2, + "analysis_timeout_seconds": 600, + "decompile_timeout_seconds": null, + "child_watchdog_seconds": 720 + } }, "data": {}, "warnings": [] } ``` -Standard output contains only the requested result. Diagnostics and progress -belong on standard error. +Every Sample-backed Query (`inspect`, `functions`, and `decompile`) uses this +fixed provenance shape. `analysis_store.source` is exactly one of `cli`, +`environment`, `xdg`, or `home_fallback`. `target.verification` is exactly +`verified` or `unverified`. `sandbox.backend` is exactly `bubblewrap`, +`external`, or `off`, and its `verification` is `verified`, `unverified`, or +`disabled`. `decompile_timeout_seconds` is `null` for a Query with no +decompilation phase. Limits report selected policy even when a compatible +Analysis is reused; the derived watchdog records the bound for the current +command. Command-specific facts remain in `data`. + +Every path exposed through JSON uses a tagged Path object. A valid UTF-8 path +is represented without alteration: + +```json +{ + "encoding": "utf8", + "value": "/tmp/sample" +} +``` + +A Linux path containing non-UTF-8 bytes uses standard padded Base64 over its +exact Unix byte sequence: + +```json +{ + "encoding": "unix_bytes_base64", + "value": "L3RtcC9zYW1wbGX/" +} +``` + +Caller-supplied relative Sample paths remain relative and byte-exact in +provenance. Tool-resolved locations, including the Analysis Store, Artifacts, +and Diagnostic Logs, are absolute. Human output renders non-UTF-8 bytes with +an explicit escaped representation. + +JSON mode is a strict framing protocol: + +- On success, stdout contains exactly one JSON document and stderr is empty. +- On failure, stdout is empty and stderr contains exactly one JSON error + document. +- Progress is suppressed. +- Ghidra and adapter diagnostics are written as Diagnostic Logs in the Analysis + Store rather than mixed into either stream. +- An error includes the path, byte size, and content digest of its Diagnostic + Log when one is available. + +Human-readable mode may present progress and concise diagnostics on stderr. + +Every Ghidra/adapter invocation captures diagnostics into a private temporary +log. An ordinary successful invocation deletes that capture. Failure, timeout, +forced termination, protocol violation, or a successful warning that +explicitly references diagnostics causes the CLI to atomically retain it as a +Diagnostic Log. + +Sample-backed Diagnostic Logs remain until cleanup of that Sample or +`clean --all`. Logs from non-Sample commands such as `doctor` remain until +`clean --all`. Version 0.1 performs no automatic log expiry. Store directories +use mode `0700` and log files use mode `0600`. + +The CLI never intentionally records Sample bytes, the caller's source Path, +the full environment, or secrets from environment variables. However, Ghidra +may echo symbols, strings, or other Sample-derived content. Diagnostic Logs are +therefore sensitive rather than claimed to be fully redacted. Every retained +log descriptor includes `"sensitive": true`, its tagged absolute Path, exact +file-byte count, SHA-256, and media type. + +A retained Diagnostic Log is an immutable directory bundle: + +```text +diagnostics// +├── manifest.json +├── stdout.log +└── stderr.log +``` + +`manifest.json` is schema-versioned JSON containing invocation metadata, +termination reason, timestamps, stream byte counts, truncation flags, +encodings, and SHA-256 digests. `stdout.log` and `stderr.log` contain the exact +raw child-process bytes; the manifest states whether each stream is valid +UTF-8 or arbitrary bytes. Agents can query metadata with `jq` and search +ordinary text diagnostics directly with `rg`, `tail`, or `sed` without JSON +escaping or Base64 overhead. + +The entire bundle is published atomically by renaming its private temporary +directory. Its descriptor points to the tagged absolute Path of +`manifest.json`. Version 0.1 uses no SQLite diagnostic database; a future index +may be built over immutable manifests without changing the bundle format. + +The CLI drains child stdout and stderr concurrently so a full pipe cannot block +Ghidra. Version 0.1 applies a fixed 8 MiB capture limit independently to each +stream. A stream within the limit is retained completely as `stdout.log` or +`stderr.log`. For a larger stream, the bundle retains its first 4 MiB and last +4 MiB as separate segment files and inserts no synthetic bytes into either. + +The manifest records total observed bytes, captured bytes, `truncated: true`, +and each segment's original byte offset, length, tagged relative Path, and +SHA-256. Diagnostic truncation does not turn an otherwise successful operation +into failure. Any retained result referencing that bundle emits the +`diagnostics_truncated` warning. Version 0.1 exposes no diagnostic-size flag; +real usage must justify adding one. + +Every warning has exactly three fields: + +```json +{ + "code": "unverified_target", + "message": "target is outside the verified integration-test matrix", + "details": { + "verified_targets": [ + "elf-x86_64-little-endian", + "pe32plus-x86_64" + ] + } +} +``` + +`code` is a stable machine identifier. `message` is concise human context and +must not be parsed for control flow. `details` is always an object with a +warning-code-specific schema and is `{}` when no structured context applies. +Warnings have no `severity` or `retryable` field because every member of the +collection is already non-fatal. A warning never disguises incomplete required +data; that condition is an error. + +The encoder deduplicates warnings that have the same code and canonicalized +details, then sorts the collection in bytewise ascending `code` order followed +by canonicalized-details order. Warning collections remain subject to the +normal serialized-byte bound. + +### Schema compatibility + +Reviewed JSON Schema documents are published under `schemas/v1/`. Every +response carries `"schema_version": 1` and validates against its corresponding +schema. + +Within schema v1: + +- New optional fields are compatible. +- Consumers must ignore unknown fields. +- Existing fields cannot be removed. +- Field types and meanings cannot change. + +Removing a field or changing its type or meaning requires a new major schema +version. The CLI emits only its current schema in the first release; maintaining +multiple output encoders is deferred until real compatibility demand justifies +it. Golden fixtures and schema-validation tests gate every response change. +This policy is recorded in +[ADR 0001](./docs/adr/0001-version-the-json-contract.md). ## Functional requirements ### Sample identity - Compute a SHA-256 digest from the Sample bytes. -- Detect if the Sample changes while it is being read. +- Open the caller's path read-only and record source metadata before copying. +- Reject regular Samples larger than 1,073,741,824 bytes before copying. +- Allow `--max-sample-bytes ` to replace that limit with a positive + integer. +- Reject zero and unlimited sentinel values for maximum Sample size. +- Accept regular files and symlinks whose opened target is a regular file. +- Accept non-UTF-8 Linux Sample paths because Ghidra receives only the + tool-generated private snapshot path. +- Reject directories, FIFOs, sockets, and device files. +- Enforce maximum Sample size while streaming even when initial file metadata + reports a smaller size. +- Before copying, require available Analysis Store filesystem space of at least + the observed Sample size plus 1,073,741,824 reserve bytes. +- If later Analysis exhausts disk space, fail the Analysis, preserve + diagnostics where possible, and remove staging. +- Copy bytes into a private staging snapshot while computing SHA-256. +- Record source metadata again after copying and fail with `sample_changed` if + it changed. +- Pass only the staged snapshot to Ghidra; Java never reads the caller's path. +- Remove the snapshot after successful import or any failure. - Treat identical bytes as the same Sample regardless of path. - Never write to the Sample. - Record the observed path as provenance, not identity. +The digest describes the exact staged bytes used for Analysis. This boundary is +recorded in +[ADR 0005](./docs/adr/0005-analyze-a-staged-sample-snapshot.md). + ### Analysis compatibility An Analysis is reusable only when its Sample digest and Analysis Profile match. At minimum, the Analysis Profile includes: - Ghidra version. +- Java version. +- Analysis-adapter version. - Ghidra language and compiler specification. +- Fully resolved loader options. - Enabled analyzer configuration. -- Java adapter protocol version. +- Maximum analysis CPU count. + +The canonical Profile document has this version 0.1 shape: + +```json +{ + "profile_version": 1, + "ghidra_version": "12.1.2", + "java_version": "21", + "analysis_adapter_version": 1, + "target": { + "loader": "", + "processor_language": "", + "compiler_specification": "" + }, + "loader_options": [], + "analyzer_options": [], + "max_cpu": 2 +} +``` + +Option collections contain fully resolved typed entries rather than only user +overrides. Entries sort bytewise by canonical option name and reject duplicate +names. Canonical JSON uses UTF-8, lexicographically ordered object keys, no +insignificant whitespace, decimal integers, no floating-point values, and no +trailing LF. Strings remain byte-exact UTF-8 without Unicode normalization. +The Analysis Profile digest is SHA-256 over those exact canonical bytes, and +its schema is published under `schemas/v1/`. + +Sample digest, source Path, Analysis Store Path, timestamps, inline and adapter +response limits, Java heap limit, and timeouts do not affect Profile identity. +Target Verification is also excluded because changing the project's +integration-test coverage does not change Ghidra's Analysis. `max_cpu` is +included conservatively because parallel analysis may influence ordering or +tie resolution. The Sample digest and Profile digest together identify an +Analysis. An incompatible Analysis is preserved until normal retention policy removes it; it is not overwritten in place. +Version 0.1 uses one analyzer policy: Ghidra 12.1.2's default auto-analysis +options. The Java adapter records the fully resolved option set in canonical +form, and its digest is part of the Analysis Profile. Users cannot enable, +disable, or tune individual analyzers in version 0.1. + ### Analysis execution - Invoke the official Ghidra headless launcher as a child process. - Pass requests to a small, bundled Java adapter. - Exchange structured request and response files rather than scrape logs. - Capture Ghidra logs separately for diagnostics. +- Build new Analyses in a staging location. +- Promote a staged Analysis atomically only after full successful analysis and + adapter validation. +- Treat Ghidra's native partial results after timeout as unusable. +- On timeout, interruption, or analysis failure, preserve the Diagnostic Log + and remove the partial staging project. +- Pass a 600-second native analysis timeout to Ghidra by default. +- Allow `--analysis-timeout-seconds ` to replace that default with a + positive integer. +- Reject zero and unlimited sentinel values for analysis timeout. +- Apply a hard child-process watchdog equal to the sum of enabled native phase + timeouts plus 120 seconds. +- Give a cached metadata Query with no separately bounded native phase a + 120-second child-process watchdog. +- Derive the watchdog automatically; do not expose a competing total-timeout + flag. - Terminate the child process after each command in the first release. -- Apply a configurable wall-clock timeout. - Report whether termination was graceful or forced. +Bubblewrap/Ghidra starts in its own process group. On watchdog timeout, the +harness marks the invocation timed out, sends `SIGTERM` to the entire group, +continues draining diagnostics for a fixed 10-second grace period, then sends +`SIGKILL` and reaps the group if any process remains. A first user interrupt +uses the same sequence but retains exit status 130; a second interrupt during +the grace period escalates immediately to `SIGKILL`. + +Bubblewrap uses parent-death handling so a crashed harness cannot leave the +worker running. Once cancellation begins, no response or Analysis data can be +promoted even if the worker races to report success. The harness retains +diagnostics, removes staging, and reports termination as `graceful` or +`forced`. Watchdog timeout exits 124 and user interruption exits 130. The grace +period is not configurable in version 0.1. + +The harness performs one worker attempt per invocation. It does not hide +additional attempts behind transport fallback, timeout escalation, or +automatic retry. A caller may explicitly repeat a failed read-only command +when its typed error is retryable. + +Each adapter invocation uses a private directory containing `request.json`, +`response.json.tmp`, and `response.json`. Rust atomically writes mode-`0600` +`request.json` before launching Ghidra. The request contains +`protocol_version: 1`, a random 128-bit invocation ID, an operation enum, +validated tool-owned Paths, selected limits, and operation-specific arguments. +Only the generated request and response Paths are passed as adapter arguments; +Sample selectors and other untrusted values remain inside JSON. + +Version 0.1 limits serialized `request.json` to 1,048,576 bytes (1 MiB). +Rust enforces the ceiling before writing the request or launching Ghidra. Java +independently bounds the bytes read from the request Path and rejects an +oversized request before JSON parsing. An oversized request reaching Java +indicates a harness defect or tampering and is reported by Rust as +`protocol_violation`; the limit is internal and has no CLI override. Sample +bytes never appear in `request.json`. + +The Java adapter strictly rejects unknown or missing protocol fields because +this is an internal boundary whose two sides ship together. It writes +`response.json.tmp`, flushes it, and atomically renames it to `response.json` +only after serialization completes. The response echoes the protocol version, +invocation ID, and operation. + +Rust accepts only the final regular response file and validates all three +echoed values plus the operation-specific schema. A missing, malformed, +mismatched, or merely temporary response is a `protocol_violation`; child +output remains diagnostic data and is never parsed as a fallback result. The +private invocation directory is removed after response validation or after its +Diagnostic Log has been retained. + +Version 0.1 limits a serialized adapter response to 268,435,456 bytes +(256 MiB). Java enforces the ceiling while serializing, and Rust independently +rejects an oversized final response. On overflow, Java discards the partial +temporary response and returns a small `result_too_large` protocol response +containing `limit_bytes`, `observed_at_least_bytes`, and the operation kind. + +No partial Query data or Artifact is published. The completed Analysis remains +reusable because only the Query failed. `result_too_large` is non-retryable +with identical arguments, although a narrower collection page may succeed. +Version 0.1 exposes no response-size override flag; observed usage must justify +raising or configuring the limit. + +### Decompilation execution + +- Apply a 60-second Ghidra decompiler timeout per selected Function by default. +- Allow `--decompile-timeout-seconds ` to replace that default with a + positive integer. +- Reject zero and unlimited sentinel values for decompilation timeout. +- Include the selected decompiler timeout in the derived child-process + watchdog. + +### JVM memory + +- Retain Ghidra headless's default 2,048 MiB maximum Java heap. +- Allow `--max-heap-mib ` to set a positive integer heap limit. +- Describe this value as a Java heap limit, not a total process-memory or RSS + limit. +- Treat an out-of-memory failure as an Analysis or Query failure, preserve its + Diagnostic Log, and never promote staged Analysis data. + +### CPU usage + +- Pass `-max-cpu 2` to Ghidra by default. +- Allow `--max-cpu ` to set a positive integer core limit. +- Reject zero and negative values rather than inheriting Ghidra's implicit + coercion behavior. +- Record the selected core limit in response provenance. + ### Analysis Store -- Use a platform-appropriate user data location by default. -- Allow an explicit store path through one documented option. -- Use atomic writes for manifests and Artifacts. +- Use this version 0.1 layout: + + ```text + / + ├── store.json + ├── analyses/// + │ ├── manifest.json + │ └── project/ + │ ├── analysis.gpr + │ └── analysis.rep/ + ├── artifacts///.json + ├── diagnostics/ + │ ├── // + │ └── global// + ├── quarantine//// + ├── locks//.lock + ├── staging// + └── cleanup// + ``` + +- Use only lowercase ASCII labels, digests, and random IDs for tool-generated + path components; no generated component starts with `.`. +- Record the store-layout version in `store.json`. +- Give every complete Analysis one `manifest.json` and one fixed Ghidra project + named `analysis`. +- Promote the whole Analysis directory from `staging` through a same-filesystem + atomic rename. +- Move the whole invalid Analysis directory into its unique quarantine path. +- Name Artifacts by the SHA-256 of their exact file bytes. +- Scope Diagnostic Log bundles to their Sample digest when available and to + `global` otherwise. +- Keep locks outside Analysis directories so promotion and quarantine cannot + move an active lock. +- Reject symlinks anywhere inside tool-owned storage as corruption. +- Treat directory traversal as authoritative; version 0.1 has no SQLite or + global mutable index. +- Record these fields in every Analysis `manifest.json`: + + ```json + { + "manifest_version": 1, + "state": "complete", + "sample": { + "sha256": "", + "size_bytes": 123456 + }, + "analysis_profile": { + "sha256": "", + "ghidra_version": "12.1.2", + "adapter_protocol_version": 1, + "target": {}, + "analyzer_options_sha256": "" + }, + "created_by": { + "ghidr_version": "0.1.0", + "invocation_id": "", + "completed_at": "" + }, + "project": { + "name": "analysis", + "path": "project", + "files": [ + { + "path": "analysis.gpr", + "size_bytes": 1234, + "sha256": "" + } + ] + } + } + ``` + +- Inventory project files in bytewise relative-Path order. +- Reject symlinks, special files, missing files, unexpected files, and recorded + size or SHA-256 mismatches. +- Recompute Sample and Analysis Profile digests and require them to match their + directory names. +- Hash the complete project tree before reusing an Analysis. +- Open cached Queries through Ghidra's read-only mode. +- Hash the complete tree again before accepting the Query result, proving the + read-only invocation did not mutate it. +- Treat any validation mismatch as corruption and apply the accepted quarantine + and single-rebuild policy. +- Prefer this correctness-first double validation in version 0.1; optimize it + only after measurement demonstrates material cost. +- Resolve the store through this fixed precedence: + 1. `--store `. + 2. `$GHIDR_STORE`. + 3. `$XDG_CACHE_HOME/ghidr` when `XDG_CACHE_HOME` is explicitly set and the + resulting path passes Ghidra path validation. + 4. `~/ghidr-store`. +- Fail when an explicitly configured store path is invalid. +- Require a UTF-8 Analysis Store path because it crosses the Java/Ghidra + boundary. Reject a non-UTF-8 resolved store with `invalid_store_path`; typed + error details retain its exact bytes using the tagged Path representation. +- Never select the working directory automatically. +- Report the resolved path and its source as `cli`, `environment`, `xdg`, or + `home_fallback` through `doctor` and response provenance. +- Keep the store local to one machine in the first release; synchronized or + shared Ghidra project storage is unsupported. +- Use atomic writes for manifests, Artifacts, and Diagnostic Logs. +- Store oversized Query results as complete, content-digested Artifacts. - Detect incomplete Analysis creation after interruption. -- Serialize writers for the same Sample and Analysis Profile. +- Never expose or reuse an incomplete Analysis. +- Acquire an OS-backed per-Analysis lock before staging. +- Fail immediately with retryable `analysis_busy` when another process holds + the same Analysis lock; include the Analysis Profile digest and a suggested + retry delay. +- Never start duplicate Ghidra work for the same Sample and Analysis Profile. +- Allow concurrent reads of a completed immutable Analysis. +- Fail cleanup with `analysis_busy` rather than remove targeted data that is in + use. +- Atomically move a structurally corrupt cached Analysis into quarantine. +- Never use a Quarantined Analysis to satisfy a Query. +- Attempt one fresh rebuild after quarantining corrupt data. +- Return a `corrupt_analysis_rebuilt` warning when that rebuild succeeds and a + structured failure when it does not. +- Preserve quarantined manifests and Diagnostic Logs until explicit cleanup. +- Retain Analyses and Artifacts until an explicit cleanup request. +- Never apply automatic age- or size-based eviction in the first release. - Treat the entire store as disposable. ### Output behavior -- Default to concise human-readable output on a terminal. -- Support explicit JSON output. -- Never change JSON shape based on terminal detection. +- Default to JSON for terminals, pipes, and non-interactive execution. +- Support explicit `--format json` and `--format human` values. +- Never select a format through terminal detection. - Include schema version and provenance in every JSON response. +- Bound collection responses and state the applied bound. +- Apply the selected serialized-byte budget to every successful response and + the fixed 65,536-byte safety cap to every error response. +- Never omit undisclosed results from a successful response. +- Never truncate a JSON document or field to satisfy the byte budget. +- Spill oversized results atomically and return a bounded Artifact descriptor. +- Support `--limit` and `--offset` for bounded collection pages. +- Require `--all` for unbounded collection output. +- Reject ambiguous or conflicting collection flags. +- Document a stable order for every pageable Query. - Use hexadecimal addresses with an explicit address space where necessary. -- Place progress and logs on standard error. +- Represent every JSON Address as an object containing `space` and `offset`. +- Preserve the exact case-sensitive UTF-8 name returned by Ghidra in + `Address.space`; do not case-fold or Unicode-normalize it. +- Compare and order address-space names by their exact UTF-8 bytes. +- Require `--address SPACE:0x...` to match an address-space name exactly. +- Return `address_space_not_found` with bounded valid candidates for an unknown + space, allowing an emitted Address to round-trip into a selector unchanged. +- Encode an Address offset as a lowercase, `0x`-prefixed hexadecimal string + padded to the Address-space width; never encode it as a JSON number. +- Keep JSON composable with standard tools such as `jq`. +- Keep JSON-mode stdout and stderr free of all content outside their single + success or error document. +- Suppress progress in JSON mode and retain full diagnostics as Diagnostic + Logs. +- Provide concise human-readable presentation only when explicitly requested. ### Errors and exit status @@ -226,9 +1284,48 @@ Distinct error categories must cover: - Ghidra analysis failure. - Function not found or function selector ambiguous. - Invalid or incompatible cached Analysis. +- Required destructive confirmation not supplied. - Internal protocol violation. -The exact numeric exit-code allocation is a design task before implementation. +In the default JSON format, every failure emits no stdout and emits one +versioned error document on stderr: + +```json +{ + "schema_version": 1, + "kind": "error", + "error": { + "code": "function_selector_ambiguous", + "message": "symbol 'parse' resolves to more than one function", + "retryable": false, + "details": { + "candidates": [] + } + } +} +``` + +`code` is a stable machine identifier. `message` is concise human context and +must not be parsed for control flow. `retryable` states whether repeating the +same logical request may reasonably succeed without changing its arguments. +`details` has an error-code-specific schema and remains subject to collection +and serialized-byte bounds. + +Argument-parsing failures follow the same contract when the caller requested +JSON. Ambiguous Function Selector resolution is a failure with bounded +candidates in `details`, never a partial success. + +Exit statuses are deliberately coarse: + +- `0`: success. +- `1`: runtime failure. +- `2`: invalid invocation or arguments. +- `124`: timeout. +- `130`: interrupted by the user. + +The versioned JSON `error.code` provides precise diagnosis within those broad +classes. This allocation is recorded in +[ADR 0002](./docs/adr/0002-keep-exit-statuses-coarse.md). ## Architecture boundary @@ -240,9 +1337,18 @@ The exact numeric exit-code allocation is a design task before implementation. - Analysis Store layout, locking, and atomicity. - Child-process lifecycle, timeouts, and log capture. - Typed request and response schemas. +- Published JSON Schema documents and compatibility checks. - Human and JSON rendering. - Exit-code policy. +Rust owns the authoritative compile-time operation registry. Each operation +definition includes its request and response types, public command kind, +published schema name, stable category, and one-sentence agent description. +The category and description are internal metadata in version 0.1, retained so +a future adapter can generate capability discovery without duplicating command +definitions. Runtime reflection and dynamic operation registration are out of +scope. + ### Java adapter responsibilities - Use supported Ghidra APIs. @@ -255,6 +1361,52 @@ The exact numeric exit-code allocation is a design task before implementation. The Java adapter must not listen on a network socket, download dependencies, or execute user-provided scripts. +A conformance test compares the authoritative Rust operation registry with the +Java dispatch set, published schemas, and golden fixtures. It fails when Rust +declares an operation Java cannot handle, Java accepts an undeclared operation, +or an operation lacks its required schema or golden coverage. + +## Rust implementation policy + +Version 0.1 starts as one library-plus-binary Cargo package rather than a +multi-crate workspace: + +```text +src/ +├── main.rs +├── lib.rs +├── cli.rs +├── commands/ +├── domain/ +├── output/ +├── process/ +├── protocol/ +├── sandbox/ +└── store/ +``` + +Supporting source lives under `java/`, `schemas/v1/`, `tests/`, and +`fixtures/`. Domain and protocol types belong in the Rust library so +integration tests exercise the same implementation as the executable. + +The initial dependency set is deliberately small: + +- `clap` for typed CLI parsing. +- `serde` and `serde_json` for protocol and public JSON. +- `schemars` for schemas generated from the same Rust types. +- `sha2`, `hex`, and `base64` for identities and byte representations. +- `thiserror` for typed internal errors. +- `rustix` for Linux locks, process groups, signals, and filesystem metadata. +- `getrandom` for 128-bit invocation and transaction IDs. +- A small RFC3339 time implementation selected during scaffolding. +- Test-only `assert_cmd`, `tempfile`, and `jsonschema`. + +Version 0.1 has no async runtime, plugin framework, database, general logging +framework, or multi-crate workspace. The repository commits `Cargo.lock`, pins +Rust through Nix, and checks dependency licenses, advisories, and sources with +`cargo-deny`. New dependencies require concrete behavior, compatible +permissive licensing, and review of their transitive trees. + ## Synchronous execution decision The first release uses a synchronous command lifecycle because each invocation @@ -272,12 +1424,48 @@ constitute a concrete reason to add an async runtime. - Do not execute the Sample. - Do not provide arbitrary script execution. - Do not require network access during analysis. -- Recommend a sandbox or container for untrusted Samples. +- Do not emit telemetry or send Sample-derived data, metadata, or diagnostics + to a remote destination. - Avoid including Sample bytes or extracted secrets in routine logs. - Validate all paths crossing the Rust/Java boundary. - Apply bounded output, memory, and execution-time policies. - Document the supported Ghidra version and relevant security advisories. +The trusted Rust harness remains outside the narrow Ghidra worker sandbox. It +owns Sample snapshotting, locks, limits, validation, the Analysis Store, and +atomic promotion. The sandboxed Ghidra/JVM child receives only: + +- The tool-generated staged Sample as read-only. +- The current project or staging directory with the minimum required access. +- The private request/response invocation directory. +- The read-only pinned Nix closure. +- A private home and temporary directory. + +It receives no caller source Path, broader workspace, unrelated Analysis data, +or network access. The harness validates all worker output before promotion. +This boundary is recorded in +[ADR 0006](./docs/adr/0006-sandbox-the-ghidra-worker.md). + +Version 0.1 supports three explicit sandbox modes: + +- `--sandbox bubblewrap` is the default. Bubblewrap is a separately maintained + dependency, not part of Ghidra, and is included in the pinned Nix package. + It isolates mount, network, PID, IPC, and UTS namespaces and uses + parent-death handling. +- `--sandbox external` declares that the calling agent/tool harness already + provides equivalent isolation. `ghidr` cannot verify that policy, so + provenance reports `verification: unverified` and emits + `external_sandbox_unverified`. +- `--sandbox off` is an explicit debugging escape hatch. Provenance reports + `verification: disabled` and emits `sandbox_disabled`. + +The CLI never auto-detects or silently changes sandbox mode. `doctor` verifies +an actual Java/Ghidra probe for the Bubblewrap backend. External providers +integrate through the surrounding harness in version 0.1; the CLI does not +accept an arbitrary sandbox-runner command. A provider-neutral internal +backend boundary permits reviewed native integrations later. Sandbox policy is +response provenance, not Analysis Profile identity. + ## Performance expectations Correctness and reproducibility take priority over startup latency in the first @@ -294,32 +1482,69 @@ shows JVM startup to be the dominant usability problem. ## Test strategy +- Run `cargo fmt --check`. +- Run `cargo clippy --all-targets --all-features -- -D warnings`. +- Run `cargo test --all-targets`. +- Run `cargo deny check`. +- Make `nix flake check` the complete local and Forgejo CI entry point. +- Forbid Rust `unsafe` in version 0.1. +- Deny `unwrap`, `expect`, `panic`, `todo`, and `unimplemented` in production + code while permitting narrowly scoped use in tests. +- Select useful strict Clippy lints explicitly rather than enabling every + pedantic lint. +- Compile Java with applicable lint warnings enabled and treated as errors. +- Apply a deterministic Java formatter. +- Format-check Nix and run `statix`. - Unit-test Sample identity, profile identity, store layout, schema validation, selector parsing, and error mapping without Ghidra. +- Validate every structured response against its published JSON Schema. +- Validate structured errors and their typed details against published schemas. +- Verify the Rust operation registry, Java dispatch set, published schemas, and + golden fixtures contain exactly the same operations. +- Verify Rust rejects a request larger than 1 MiB before worker launch and Java + independently rejects one before JSON parsing. - Test process lifecycle against a controllable fake child process. +- Run a purpose-built probe through the production Bubblewrap launcher and + prove it cannot reach a test listener outside the worker network namespace. +- Regress against default data egress: the shipped CLI, environment-variable + surface, and Java protocol expose no remote-destination or telemetry setting. - Maintain tiny, source-controlled fixture programs with known properties. -- Compile fixtures reproducibly for supported architectures. -- Run integration tests against a pinned supported Ghidra release. +- Compile ELF x86-64 and PE32+ x86-64 fixtures reproducibly. +- Run integration tests against pinned Ghidra 12.1.2 and JDK 21. +- Assert verified status for the ELF and PE fixtures and unverified status for + at least one recognized target outside the matrix. +- Run the complete real-Ghidra integration suite before accepting a Ghidra or + JDK upgrade. - Compare JSON against reviewed golden fixtures after normalizing unstable values. +- Never rewrite a golden fixture in CI; updates require an explicit local + command and reviewed diff. +- Add a regression test for every corrected bug. - Verify interruption leaves no Analysis that can be mistaken for complete. +- Verify Ghidra timeout results are never promoted or reused. +- Verify corruption causes quarantine and at most one automatic rebuild. - Verify identical bytes at different paths reuse one compatible Analysis. - Verify changed bytes at the same path do not reuse the old Analysis. +- Verify a source change during snapshot creation aborts before Ghidra starts. +- Verify Ghidra receives the staged snapshot path rather than the caller path. ## Delivery milestones ### Milestone 0: design - Review this PRD and the domain language. -- Choose the project and executable name. -- Choose a license. -- Pin the initially supported Ghidra and JDK versions. -- Define protocol schemas and exit codes. +- Use `ghidr` as the short executable name. +- Use the Apache-2.0 license. +- Pin a nixpkgs revision providing Ghidra 12.1.2 and JDK 21. +- Define the remaining pinned Nix inputs and package closure. +- Define protocol schemas and use the accepted coarse exit-status allocation. +- Publish the initial `schemas/v1/` documents. ### Milestone 1: vertical inspection slice - Create the Rust crate and minimal Java adapter. - Implement `doctor`. +- Implement explicit Analysis Store cleanup. - Implement `inspect`. - Prove Rust to `analyzeHeadless` to Java to JSON end to end. @@ -337,32 +1562,66 @@ shows JVM startup to be the dominant usability problem. ### Milestone 4: packaging -- Provide a reproducible Nix development and test environment. +- Provide a reproducible x86-64 Linux Nix development and test environment. +- Publish a Nix flake package usable through `nix run`. - Document installation and sandboxed use. - Publish the first pre-release. +### Post-version-0.1: agent tracing + +After the reliable inspection-to-decompilation slice is established, add +separate bounded Queries for relationships and targeted detail in this +priority order: + +1. `strings`: a stably ordered page containing exact Address, byte length, + explicit encoding, decoded representation, and bounded reference links. +2. `xrefs`: structured source and destination targets, reference type, operand + index, direction, and containing source Function when present. +3. `calls`: flat caller, callsite, and resolved or unresolved callee edges. +4. `disassemble`: instructions for one explicit, bounded Address range or + selected Function. +5. `function`: signatures, parameters, locals, body ranges, thunk resolution, + and alternate symbols for one selected Function. + +These relationships do not belong in every `functions` item. Exact names and +entry Addresses provide the stable identifiers that later Queries compose +with, while separate schemas can represent relationship-specific ambiguity and +provenance. Every Query retains explicit bounds, stable ordering, typed +per-record fields, and the public byte-spill contract. Version 0.1's rejection +of zero or implicit unlimited sentinels continues to apply. Whole-program graph +renderers, recursive graph algorithms, prose records, and a custom filter +language remain out of scope; downstream tools can transform bounded facts. + +Batch Query execution is considered only after measurements show that repeated +JVM startup materially dominates agent workflows. A batch design must define +maximum item and aggregate byte counts, deterministic order, per-item typed +success or error, cancellation, and explicit fail-fast versus continue +behavior. It must not introduce hidden parallelism. + +After the CLI schemas stabilize, an MCP integration may be built as a separate +thin adapter. It invokes `ghidr` as a process, exposes at most one tool per +stable Query, and forwards the CLI's versioned JSON unchanged. It may generate +capability descriptions and categories from the Rust operation metadata. It +must not call Ghidra directly, own Analysis Store policy, add a second result +contract, depend on mutable current-Program state, or introduce a persistent +listener into the core package. Stdio remains the default MCP transport; any +future network transport requires a separate threat model. + ## Release acceptance criteria The first release is acceptable when: - A new user can run `doctor`, `inspect`, `functions`, and `decompile` without manually creating a Ghidra project. +- A user can inspect store usage and explicitly remove tool-owned data. - Repeating a Query against an unchanged Sample reuses compatible analysis. - All commands provide versioned JSON and documented exit behavior. - An interrupted analysis cannot be mistaken for a complete one. +- A timed-out Analysis attempt cannot satisfy a later Query. - Tests exercise a real pinned Ghidra release and known fixture binaries. - No first-release command mutates the Sample or Analysis. - No analysis command requires a network connection. ## Open decisions -- Final repository and executable name. -- License. -- Initially supported Ghidra patch release. -- Whether the first supported host is Linux-only. -- Default Analysis Store retention policy. -- Numeric exit-code allocation. -- JSON Schema publication and compatibility policy. -- Whether Nix is the sole supported installation path for the first - pre-release. - +- No product-level decision currently blocks implementation. diff --git a/README.md b/README.md index 42e1a39..92b8043 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # ghidra-cli A small, dependable command-line interface for read-only Ghidra analysis. +The repository is named `ghidra-cli`; the installed executable is `ghidr`. The project is currently in its design phase. Its first objective is to make common reverse-engineering questions easy to ask without exposing Ghidra @@ -9,14 +10,15 @@ project management, JVM lifecycle, or scripting details to the user. Proposed usage: ```console -ghidra-cli doctor -ghidra-cli inspect ./sample -ghidra-cli functions ./sample -ghidra-cli decompile ./sample main +ghidr doctor +ghidr inspect ./sample +ghidr functions ./sample +ghidr decompile ./sample --name main ``` Each command will identify the sample by its content, reuse compatible cached -analysis, and produce human-readable or versioned JSON output. +analysis, and produce versioned JSON by default. Human-readable presentation is +available explicitly through `--format human`. ## Documents @@ -24,6 +26,10 @@ analysis, and produce human-readable or versioned JSON output. scope. - [CONTEXT.md](./CONTEXT.md) defines the domain language used throughout the project. +- [docs/adr/](./docs/adr/) records durable architectural decisions. +- [docs/research/](./docs/research/) records cited prior-art investigations, + including the Ghidra MCP comparison that informed protocol safeguards and + the post-version-0.1 roadmap. ## Status @@ -32,5 +38,8 @@ begins after the initial requirements and interface are reviewed. ## License -No license has been selected yet. Until one is added, all rights are reserved. +Licensed under the [Apache License 2.0](./LICENSE). +Development may use AI assistance. Maintainers remain responsible for reviewing +the resulting design and code, verifying provenance, testing behavior, and +ensuring dependency-license compatibility. diff --git a/docs/adr/0001-version-the-json-contract.md b/docs/adr/0001-version-the-json-contract.md new file mode 100644 index 0000000..6bac20f --- /dev/null +++ b/docs/adr/0001-version-the-json-contract.md @@ -0,0 +1,16 @@ +--- +status: accepted +--- + +# Version the JSON contract by compatibility boundary + +Automation agents are the primary interface consumers, so success and error +shapes must remain predictable across upgrades. The project publishes reviewed +JSON Schema files under `schemas/v1/`: optional fields may be added within v1 +and consumers must ignore unknown fields, while removing a field or changing +its type or meaning requires a new major schema version. The CLI initially +emits only its current schema rather than carrying multiple encoders; golden +fixtures and schema validation gate every success and error change. JSON mode +also preserves strict stream framing: a success is the only stdout document, a +failure is the only stderr document, and progress or diagnostic logs never +share either stream. diff --git a/docs/adr/0002-keep-exit-statuses-coarse.md b/docs/adr/0002-keep-exit-statuses-coarse.md new file mode 100644 index 0000000..2710667 --- /dev/null +++ b/docs/adr/0002-keep-exit-statuses-coarse.md @@ -0,0 +1,11 @@ +--- +status: accepted +--- + +# Keep exit statuses coarse + +Shell callers need broad control-flow signals, while automation agents need +precise diagnoses. The CLI therefore reserves exit statuses for success, +invalid invocation, runtime failure, timeout, and interruption; the versioned +JSON `error.code` carries the detailed failure taxonomy. This avoids maintaining +two overlapping error classifications that could drift apart. diff --git a/docs/adr/0003-default-to-json-everywhere.md b/docs/adr/0003-default-to-json-everywhere.md new file mode 100644 index 0000000..d0fbf78 --- /dev/null +++ b/docs/adr/0003-default-to-json-everywhere.md @@ -0,0 +1,13 @@ +--- +status: accepted +--- + +# Default to JSON everywhere + +Automation agents are the primary consumers, so the shortest invocation must +produce the stable machine interface. The CLI defaults to JSON regardless of +whether stdout is a terminal or pipe; human-readable presentation requires +`--format human`. Avoiding TTY-dependent format selection ensures that the same +command has the same framing and semantics in every execution environment. +JSON mode never emits interactive prompts; required confirmation is represented +as a structured error and satisfied by an explicit flag. diff --git a/docs/adr/0004-never-reuse-partial-analysis.md b/docs/adr/0004-never-reuse-partial-analysis.md new file mode 100644 index 0000000..05bce4f --- /dev/null +++ b/docs/adr/0004-never-reuse-partial-analysis.md @@ -0,0 +1,11 @@ +--- +status: accepted +--- + +# Never reuse a partial Analysis + +Ghidra preserves analyzer results completed before its analysis timeout, but +automation consumers cannot safely infer which facts are missing. New Analyses +are therefore built in staging and promoted atomically only after full success; +a timeout, interruption, or analysis failure preserves the Diagnostic Log, +removes the partial staging project, and leaves no reusable Analysis. diff --git a/docs/adr/0005-analyze-a-staged-sample-snapshot.md b/docs/adr/0005-analyze-a-staged-sample-snapshot.md new file mode 100644 index 0000000..fd00ef3 --- /dev/null +++ b/docs/adr/0005-analyze-a-staged-sample-snapshot.md @@ -0,0 +1,13 @@ +--- +status: accepted +--- + +# Analyze a staged Sample snapshot + +Passing the caller's path to Ghidra after hashing creates a race in which the +bytes analyzed may differ from the recorded Sample identity. The CLI instead +copies the input through a read-only handle into private staging while hashing, +verifies that source metadata did not change during the copy, and gives only +that snapshot to Ghidra. The extra temporary I/O and disk use buy reproducible +identity, isolate Java from the caller's path, and allow the snapshot to be +removed after either success or failure. diff --git a/docs/adr/0006-sandbox-the-ghidra-worker.md b/docs/adr/0006-sandbox-the-ghidra-worker.md new file mode 100644 index 0000000..cb81f65 --- /dev/null +++ b/docs/adr/0006-sandbox-the-ghidra-worker.md @@ -0,0 +1,14 @@ +--- +status: accepted +--- + +# Sandbox the Ghidra worker + +Ghidra parses adversarial Samples through a large Java analysis engine, while +the Rust harness must manage trusted locks, store data, and atomic promotion. +The harness therefore remains outside a narrow sandbox containing only the +Ghidra/JVM worker and its invocation capabilities. Bubblewrap is the default +Linux backend, with explicit external-harness and disabled modes for +environments that cannot nest it. This limits a compromised worker's access +without turning the CLI into a daemon or allowing arbitrary sandbox-runner +commands. diff --git a/docs/research/ghidra-mcp-comparison.md b/docs/research/ghidra-mcp-comparison.md new file mode 100644 index 0000000..a0a4e7d --- /dev/null +++ b/docs/research/ghidra-mcp-comparison.md @@ -0,0 +1,425 @@ +# Ghidra MCP comparison for `ghidra-cli` + +Status: reviewed and adopted into the PRD, 2026-07-28 + +## Scope and source snapshots + +This note compares the accepted `ghidra-cli` version 0.1 design with two +primary-source repositories: + +- LaurieWired/GhidraMCP at commit + [`27f316f80139e2d5dec882519a1bdf4aa46ac04c`](https://github.com/LaurieWired/GhidraMCP/tree/27f316f80139e2d5dec882519a1bdf4aa46ac04c), + tagged `1.4`. +- bethington/ghidra-mcp at commit + [`8cd2078e10b9ba28b188cb84ce5b9051a904b995`](https://github.com/bethington/ghidra-mcp/tree/8cd2078e10b9ba28b188cb84ce5b9051a904b995), + tagged `v6.0.0`. + +The comparison uses repository source, manifests, CI, licenses, changelogs, +and the repositories' own issue reports. It does not treat README marketing +claims as independently verified performance evidence. + +## Executive conclusion + +Do not change the version 0.1 architecture or widen its command surface because +of these MCP implementations. The accepted synchronous Rust harness, private +file protocol, one Sample per command, immutable Analysis, strict JSON, and +sandboxed short-lived Ghidra worker directly avoid the largest sources of +complexity visible in both repositories. + +The repositories nevertheless validate several useful mechanisms: + +- Relationship queries are the most valuable next capability for an agent. + Prioritize `strings`, `xrefs`, and direct `calls` after version 0.1, followed + by targeted disassembly and detailed single-Function inspection. +- A large agent surface needs categories and capability discovery. Do not build + that for four commands, but preserve command metadata so a future MCP adapter + can expose a small, generated catalog without hand-written duplication. +- Every process/protocol boundary needs independent size validation and + parity tests. Add a small explicit ceiling for internal `request.json`, and + test that Rust operation types, Java dispatch, and published schemas contain + exactly the same operations. +- Explicit target selection is essential. Keep requiring the Sample and exact + Function Selector on every Query; never introduce a mutable “current + program” fallback. +- An eventual MCP integration should be a thin, separate adapter that invokes + `ghidr` and forwards its schemas. It should not add an embedded HTTP server, + persistent Ghidra session, or a second result contract to the core. + +The principal rejection is breadth. One examined project exposes 272 catalog +entries, including mutations, arbitrary scripts, project lifecycle, emulation, +and debugging; its own users subsequently requested filtering because the +surface was too large for agents ([issue #267](https://github.com/bethington/ghidra-mcp/issues/267)) +and raised broader maintainability and provenance concerns +([issue #307](https://github.com/bethington/ghidra-mcp/issues/307)). The current +`ghidra-cli` narrow vertical slice is a feature, not a gap. + +## LaurieWired/GhidraMCP + +### Architecture and state + +The original project is a three-hop interactive topology: + +```text +MCP client -> Python FastMCP bridge -> HTTP -> plugin in a running Ghidra GUI +``` + +The Python process speaks MCP over stdio by default and can expose SSE; every +tool then makes an HTTP request to a configured Ghidra URL +([bridge initialization and transports](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/bridge_mcp_ghidra.py#L15-L24), +[CLI transport selection](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/bridge_mcp_ghidra.py#L290-L334)). +The Java extension embeds `com.sun.net.httpserver.HttpServer` in Ghidra and +operates on the GUI's `ProgramManager.getCurrentProgram()` +([server construction](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L95-L114), +[current Program resolution](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L1627-L1629)). +Installation therefore requires installing and enabling a GUI extension and +opening Ghidra, in addition to Python and the MCP SDK +([installation prerequisites and workflow](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/README.md#L30-L79)). + +This is session-oriented rather than content-oriented. The Sample is not an +argument to a tool call; the implicit target is whichever Program is current +in the GUI. Cursor-dependent tools expose the current address and Function, +further coupling automation to interactive state +([current-selection tool wrappers](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/bridge_mcp_ghidra.py#L150-L176)). + +### Tool surface and mutation + +The snapshot has 27 Python `@mcp.tool` declarations mapped to 27 Java HTTP +contexts. Read tools cover Functions, classes/namespaces, segments, imports, +exports, data, strings, disassembly, and cross-references. Write tools rename +Functions, data, and variables; change prototypes and local types; and add +comments +([Python tool declarations](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/bridge_mcp_ghidra.py#L60-L288), +[HTTP context registration](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L109-L342)). +Writes run Ghidra transactions on the Swing event thread, so this is not a +read-only analysis layer +([rename transaction](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L512-L540)). + +### Result contract and bounds + +The outer MCP parameter schemas are inferred from Python type annotations, but +the inner HTTP protocol is mostly newline-delimited text. `safe_get` splits +text into lines, while failures are returned as strings in the same value +channel rather than typed MCP errors +([GET behavior](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/bridge_mcp_ghidra.py#L26-L43), +[POST behavior](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/bridge_mcp_ghidra.py#L45-L58)). +The Java server always responds with HTTP 200 and `text/plain`, including +application errors +([response writer](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L1632-L1638)). + +Many lists have `offset` and `limit`, normally defaulting to 0 and 100, but +there is no `total`, `returned`, or `has_more` metadata. Some duplicate tools +are unpaginated, such as `list_functions` +([list wrapper](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/bridge_mcp_ghidra.py#L171-L176)). +Pagination accepts negative and arbitrarily large values without a public +validation contract +([pagination implementation](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L1582-L1606)). +There is no whole-response byte bound or spill mechanism. + +Function selection is ambiguous by construction: name selection returns the +first exact basename encountered, and address selection accepts an interior +address by resolving the containing Function +([name selection](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L493-L509), +[interior-address fallback](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L782-L805)). +These are precisely the ambiguities the accepted `ghidra-cli` selector +contract should continue rejecting. + +The timeout layers are inconsistent. The Python bridge times every HTTP call +out after five seconds, while Java decompilation can run for 30 or 60 seconds +([bridge timeouts](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/bridge_mcp_ghidra.py#L35-L51), +[decompiler timeout](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L1202-L1217)). +An MCP caller can therefore receive a timeout while Ghidra continues working. +This validates `ghidra-cli`'s single derived child watchdog and process-group +termination design. + +### Security, tests, and dependencies + +`new InetSocketAddress(port)` binds the embedded server without an explicit +loopback address, and the request path shown has neither authentication nor a +body-size cap +([server bind](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L95-L108), +[unbounded form read](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/main/java/com/lauriewired/GhidraMCPPlugin.java#L1559-L1579)). +The Ghidra process and plugin are unsandboxed and inherit the GUI user's +filesystem and network access. + +Runtime dependencies are Python 3.10+, `mcp`, and `requests`; the Java build +uses Maven plus manually copied Ghidra JARs +([Python requirements](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/bridge_mcp_ghidra.py#L1-L6), +[system-scoped Ghidra JARs](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/pom.xml#L12-L87)). +The only committed Java test is the Maven-template `assertTrue(true)` smoke +test, so no endpoint, schema, timeout, security, or real-Ghidra regression is +covered +([test source](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/src/test/java/com/lauriewired/AppTest.java#L1-L35)). + +## bethington/ghidra-mcp + +### Architecture and state + +This repository identifies itself as a substantial derivative of the Laurie +project, not an independent implementation +([NOTICE](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/NOTICE#L1-L10)). +It retains the bridge topology but expands it: + +```text +MCP client -> Python MCP bridge -> UDS or HTTP -> Ghidra GUI plugin + -> headless Java server + -> optional debugger service +``` + +The bridge supports stdio, Streamable HTTP, and deprecated SSE +([bridge CLI](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/cli.py#L102-L143)), +while repository documentation describes separate GUI and headless Java +servers and a Python protocol-conversion process +([architecture](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/README.md#L977-L995)). + +The bridge discovers server instances, remembers a connected project and +transport, dynamically registers tools, and attempts reconnection after Ghidra +restarts +([shared mutable state](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/state.py#L14-L35), +[reconnection](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/dispatch.py#L67-L128)). +The cost is visible in the project's own issue history. Concurrent sub-agent +calls caused intermittent bridge shutdown and forced a serialization lock +([issue #91](https://github.com/bethington/ghidra-mcp/issues/91), +[current lock](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/state.py#L18-L25)). +On macOS, a GUI/agent environment mismatch in `$TMPDIR` prevented Unix-socket +discovery until the implementation learned to scan multiple candidate +locations ([issue #170](https://github.com/bethington/ghidra-mcp/issues/170), +[current discovery rationale](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/discovery.py#L20-L40)). + +These are real solutions for a persistent multi-instance service, but they are +states `ghidra-cli` intentionally does not have. + +### Tool surface, discovery, and mutation + +The committed endpoint catalog declares 272 tools across 15 categories +([catalog](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/tests/endpoints.json)). +The surface includes read-only listing, Functions, cross-references, call +graphs, strings, data flow, and memory inspection, but also symbol/type/comment +mutation, project deletion and version control, arbitrary Java script +execution, P-code emulation, and live debugging +([feature inventory](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/README.md#L69-L95), +[API categories](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/README.md#L624-L740)). + +Its most reusable mechanism is annotation-driven registration. Java service +methods carry tool/parameter annotations; one scanner creates deterministic +HTTP endpoint definitions and a machine-readable input schema +([scanner contract and sorting](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/src/main/java/com/xebyte/core/AnnotationScanner.java#L13-L87), +[descriptor construction](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/src/main/java/com/xebyte/core/AnnotationScanner.java#L94-L118)). +The Python bridge fetches that schema and generates callable MCP signatures at +runtime +([dynamic handler construction](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/registry.py#L22-L146), +[schema fetch](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/registry.py#L302-L318)). + +The current bridge can load categories lazily and exposes `search_tools`, +`list_tool_groups`, and load/unload/check helpers +([agent discovery workflow](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/README.md#L377-L393)). +This directly addresses tool overload, but it also introduces another mutable +session dimension and depends on clients honoring `tools/list_changed`. + +### Result contract and bounds + +The Java response type is an improvement over the original but remains a +union of structured success, simple `{ "error": "message" }`, and raw text +passthrough +([sealed `Response`](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/src/main/java/com/xebyte/core/Response.java#L3-L43)). +Consequently, some endpoints return JSON while many paginated endpoints still +return newline-delimited text, and the dynamically generated MCP handlers all +declare `str` results +([text pagination](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/src/main/java/com/xebyte/core/ServiceUtils.java#L232-L244), +[generated return annotation](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/registry.py#L111-L145)). +The live tool schema describes inputs, not stable versioned output schemas or +typed error codes +([schema translation](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/schema.py#L54-L99)). + +Request hardening is much stronger: version 6.0 applies a 64 MiB body ceiling +and bounds actual reads rather than trusting `Content-Length` +([request ceiling](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/src/main/java/com/xebyte/core/SecurityConfig.java#L51-L75), +[HTTP enforcement](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/src/main/java/com/xebyte/GhidraMCPPlugin.java#L2253-L2263)). +Response bounding remains endpoint-specific rather than a universal contract. +For example, the whole-program call graph accepts `limit=0` as unlimited and +can emit text, DOT, Mermaid, adjacency, or address-edge JSON +([call-graph parameters and implementation](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/src/main/java/com/xebyte/core/XrefCallGraphService.java#L782-L877)). +`ghidra-cli`'s one schema, disclosed collection bounds, and universal serialized +byte cap are materially stronger for an automation agent. + +The bridge coordinates endpoint-specific HTTP timeouts and retries GET calls; +write calls deliberately avoid blind retry because completion is ambiguous +after a connection loss +([timeout catalog](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/config.py#L7-L35), +[retry policies](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/dispatch.py#L131-L195)). +Notably, the bridge's `decompile_function` timeout is 45 seconds while the +Java endpoint defaults to 60 seconds +([bridge value](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/config.py#L21-L34), +[Java value](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/src/main/java/com/xebyte/core/FunctionService.java#L149-L186)). +This is further evidence that one owning harness should derive and enforce the +complete child lifetime. + +The project learned an important target-selection lesson. Calls default to a +mutable active Program; strict explicit Program selectors exist only behind an +environment variable because compatibility retained the fallback +([strict-mode rationale](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/README.md#L350-L375), +[bridge enforcement](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/python/bridge_mcp_ghidra/registry.py#L26-L87)). +`ghidra-cli` should keep Sample selection mandatory from its first release and +never acquire this compatibility burden. + +### Security, tests, and dependencies + +Current security controls include loopback defaults, refusal of a non-loopback +bind without bearer authentication, anti-CSRF/DNS-rebinding checks, script +execution disabled by default, filesystem-root containment, request-body +caps, and generic uncaught-error responses +([security configuration](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/README.md#L440-L493), +[safe handler](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/src/main/java/com/xebyte/GhidraMCPPlugin.java#L2210-L2279)). +The change history states that these were hardening changes after earlier +unauthenticated, ungated, or insufficiently bounded behavior +([v6.0.0 security history](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/CHANGELOG.md#L7-L73)). + +These controls are valuable for a network service, but they do not isolate +Ghidra from the user's account. `ghidra-cli` should retain the stronger design +of no listener plus a capability-limited worker sandbox. If it ever adds an +MCP adapter, stdio should remain the default and network transport should be a +separately threat-modeled feature. + +The shipped bridge requires Python 3.10+ and the `mcp` package; tests and +unshipped subsystems add many optional dependencies +([package manifest](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/pyproject.toml#L1-L24), +[dependency groups](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/pyproject.toml#L53-L101)). +The repository has a materially stronger regression practice than the original: +Java offline tests and coverage, a Python version matrix, Windows-specific +tests, performance regressions, and Pester setup tests are gating; formatting +and lint jobs are currently advisory +([CI gates](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/.github/workflows/tests.yml#L16-L181), +[quality jobs](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/.github/workflows/tests.yml#L288-L358)). +Its annotation/schema/catalog parity approach is worth borrowing; its broad +multi-subsystem matrix is not needed for version 0.1. + +## Recommendations for `ghidra-cli` + +### Adopt now + +1. **Keep one source of truth for operations.** Define every internal operation + once with its request type, response type, public command kind, and schema + name. Generate or mechanically verify the Rust schema set and Java dispatch + set against it. The bethington annotation scanner demonstrates the value of + preventing hand-maintained endpoint/schema drift, while `ghidra-cli` can do + this without runtime reflection or dynamic registration. + +2. **Cap both sides of the private protocol.** The accepted 256 MiB response + ceiling is good. Add a much smaller fixed serialized `request.json` ceiling, + suggested at 1 MiB, enforced by Rust before launch and Java before parse. + Requests contain metadata and tool-owned paths, not Sample bytes, so a + larger allowance has no known use. This is a protocol-hardening detail, not + a public CLI flag. + +3. **Add protocol/catalog parity regressions.** A fixture should fail if a Rust + operation lacks Java handling, if Java accepts an unpublished operation, or + if a public schema/golden is missing. Keep the already accepted real-Ghidra + ELF and PE integration tests and strict lint/format gates. + +4. **Retain mandatory targets and stable identifiers.** Every Query keeps an + explicit Sample; Function operations keep exact entry Address or exact name + selection with ambiguity errors. Do not add GUI cursor, active Program, or + implicit containing-Function behavior. + +5. **Preserve extension metadata without exposing it yet.** Command/domain + definitions may carry a stable category and one-sentence agent description. + This costs little and lets a later MCP adapter generate a discoverable + surface, but version 0.1 needs no `search-tools`, runtime group loading, or + config file. + +### Defer in priority order + +1. **`strings` Query.** Return a bounded, stably ordered list with exact Address, + byte length, decoded representation with explicit encoding, and referring + Addresses or a separate bounded reference link. Avoid case-folded server-side + substring filters initially; agents can use `jq`, and raw Sample-derived + strings may be non-UTF-8 or extremely large. + +2. **`xrefs` Query.** Model each Reference structurally: source Address, + destination Address or external target, Ghidra reference type, operand index, + and containing source Function entry when present. Direction should be an + explicit enum. Never emit prose such as `From ... in ... [READ]`. + +3. **Direct `calls` Query.** Start with flat Call Edges containing caller entry, + callsite, and resolved or unresolved callee. Support pagination and stable + ordering. Do not begin with recursive whole-program graphs, DOT/Mermaid + renderers, graph algorithms, or `limit=0` unlimited output; agents can build + graphs from bounded edge pages. + +4. **Targeted `disassemble` and `function` detail Queries.** These are more + useful than widening every `functions` item. A detailed Function result can + later carry signature, parameters, locals, body ranges, thunk resolution, + and alternate symbols without making discovery pages expensive. + +5. **Batch Query execution.** Multiple decompilations in one Ghidra process may + eventually reduce startup cost, but only add it after measurements. It needs + explicit per-item success/error objects, deterministic order, aggregate + byte bounds, cancellation semantics, and no hidden parallelism. + +6. **Thin MCP adapter.** After schemas and CLI behavior stabilize, a separate + package can expose one MCP tool per stable `ghidr` Query, invoke the CLI over + stdio/process execution, and forward its JSON unchanged. If the surface later + grows, generated categories and a capability-search tool become appropriate. + The adapter should not call Ghidra directly or own Analysis Store policy. + +7. **Interactive/mutating analysis.** Renames, types, comments, scripts, + debugging, and shared-project workflows solve a different problem. If ever + pursued, put them behind a separate explicit write-capability model and a + separate domain contract; do not weaken immutable Analysis semantics. + +### Explicitly reject for version 0.1 + +- Embedded HTTP, TCP, UDS, SSE, Streamable HTTP, discovery scans, reconnect + logic, persistent daemons, and background JVMs. +- Mutable current-Program/current-Address/current-Function session state. +- Any automatic first match for duplicate Function names or interior addresses. +- Raw text success/error unions, HTTP-status tunneling, untyped addresses, and + prose records that agents must parse. +- Hidden retry escalation. A caller may explicitly retry a failed read-only + Query; `ghidr` must not multiply time budgets behind the contract. +- Arbitrary Java/Python scripts, mutation, project management, debugger control, + emulation, binary export, and Ghidra Server administration. +- Whole-program graph renderers and custom search/query languages. Emit bounded + facts; let `jq` and downstream tools transform them. +- Runtime tool-group loading for the initial four-command surface. +- A database or global mutable index merely to support discovery or MCP state. + +## Licensing and provenance + +Both repositories use Apache-2.0 +([LaurieWired license](https://github.com/LaurieWired/GhidraMCP/blob/27f316f80139e2d5dec882519a1bdf4aa46ac04c/LICENSE), +[bethington package license](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/pyproject.toml#L1-L9)). +The bethington repository explicitly retains attribution to LaurieWired for +portions that remain +([NOTICE](https://github.com/bethington/ghidra-mcp/blob/8cd2078e10b9ba28b188cb84ce5b9051a904b995/NOTICE#L1-L10)). + +Concepts, public Ghidra API usage patterns, and independently designed schemas +may be studied without making `ghidra-cli` a derivative. If any source or close +adaptation is copied, Apache-2.0 requires preserving applicable copyright, +license, change, and NOTICE obligations. The cleanest course is to keep the +implementation independent, cite these projects as prior art in research and +design notes, and record provenance for any deliberately reused code before it +enters a commit. + +The open maintainability issue also alleges that some contributor changes were +recommitted in ways that obscured authorship. This report does not adjudicate +that allegation, but it reinforces the accepted `ghidra-cli` rule: AI assistance +does not replace review, attribution, or a traceable change history +([issue #307](https://github.com/bethington/ghidra-mcp/issues/307)). + +## Decision impact + +No accepted version 0.1 product decision should be removed. The research adds +three concrete implementation-time checks and a clearer post-0.1 order: + +- internal request-size ceiling; +- operation/Java/schema parity test; +- relationship-query order: `strings`, `xrefs`, direct `calls`, then targeted + disassembly and Function detail. + +Everything else belongs either in a future thin MCP adapter or in a separately +designed interactive/mutating product. + +The PRD now records the request ceiling, operation/dispatch/schema parity, +no-default-egress regression coverage, ordered relationship Queries, bounded +batch prerequisites, and thin-adapter MCP boundary. These adoptions do not add +a public version 0.1 command or widen the Java adapter's authority.