31 Commits
Author SHA1 Message Date
Anton Kochkov 4ee627c4dc Update rmcp to 3.8 to serve MCP 2026-07-28 clients
* A client of MCP 2026-07-28, such as FastMCP 4.1.4, begins with
  `server/discover` and falls back to `initialize` on an error. rmcp 1.8
  accepts nothing but `initialize` first: it logged "expect initialized
  request, but received: ... server/discover" and exited, so the client
  only saw the connection close.
* rmcp 3.8 answers `ping` and `server/discover` before `initialize`
  (with the supported revisions, 2024-11-05 to 2026-07-28), serves the
  requests of 2026-07-28 clients without `initialize`, and answers other
  early requests with an error instead of exiting. FastMCP 4.1.4 now
  connects and calls tools on both backends.
* Port to its API: `ContentBlock`, `Role`, `Resource`,
  `ResourceTemplate`, and `ReadResourceResponse` for `read_resource`.
  rmcp 3 needs Rust 1.88.
* tests/protocol.rs speaks raw JSON-RPC for both kinds of client; with
  rmcp 1.8 both tests fail on the closed connection.
2026-10-30 22:29:05 +08:00
Anton Kochkov 8b1eb552c8 Return Rizin's messages from every raw command
* Agents learn Rizin's commands from its errors and usage messages. The
  `command` tool returned them only when Rizin reported a failure, so:
  - through a pipe to Rizin 0.9.1, which reports no status, a wrong
    command (`axt foo bar baz`, `s nosuchflag`, `wa "bogus"`) came back
    as an empty success and its error only reached the server log;
  - a command that succeeds and still says something lost it, e.g.
    `e asm.bits=7`, which is OK on Rizin 0.9.1 and 0.10 but logs
    "cannot set '7'", or the warnings of `ihj`.
* Return the messages after the output as their own text item, so the
  output itself (JSON, say) stays as Rizin printed it.
* Without a status, take a line that printed nothing but an `ERROR`
  message as failed, as the decompile tool already does.
2026-10-30 22:29:05 +08:00
Anton Kochkov a6f3b27549 Analyze GNU ls in the tests also on Ubuntu 26.04
* The tests analyze `/bin/ls` and rely on what GNU `ls` is: a C program
  whose `main` calls many functions and references ASCII strings, with
  about 1300 strings in the whole file. Since Ubuntu 25.10, and so on
  the Ubuntu 26.04 that `ubuntu-latest` is moving to, `ls` is the
  multicall binary of uutils (0.10.0 on 26.04): a Rust program of
  11.8 MB whose `main` only dispatches, with 138456 strings in the whole
  file, 18 MB of `izzj` that the paging test reads once per page.
* GNU coreutils stays installed there as the required package
  gnu-coreutils, which names its programs with a `gnu` prefix, so take
  `/usr/bin/gnuls` when it exists, in the tests and the benchmark.
2026-10-30 22:29:05 +08:00
Anton Kochkov 587c7a2331 Keep the ROP searches of the tests to 64 KiB of code
* The contract test timed out on both CI channels after 120 s at
  `/Rj "ret"`: `ubuntu-latest` is moving to Ubuntu 26.04, where `ls` is
  the multicall binary of uutils, 11.8 MB with 4.8 MB of code instead of
  the 82 KB of GNU `ls`, and the time of `/R` grows with the bytes
  searched.
* Search the first 64 KiB of the largest executable section instead, in
  the contract test and in the search_rop tests, so their time does not
  depend on the target. That window of `/bin/ls` still has over 1000
  gadgets, so the listing still exceeds a page.
2026-10-30 22:29:05 +08:00
Anton Kochkov 3d0664c248 Search ROP gadgets without Rizin's gadget cache
* `gadget.cache` is on by default, and a search served from the cache
  differs from a fresh one: the cache lists the gadgets by address while
  a fresh search lists them per terminating instruction, so on `/bin/ls`
  the order differs from the 58th gadget on, and the pages of a listing
  were cut from two orders.
* `/R` keeps only the first gadget of identical bytes, and the cache
  holds what the search that built it kept: after a search of the whole
  file, a search of 0x10000-0x14d70 of `/bin/ls` returned 236 gadgets
  instead of 284, on Rizin 0.9.1 and 0.10.
* Turn the cache off for each search: every call is then a fresh search
  of its range, so its result only depends on the range and the query.
2026-10-30 22:29:05 +08:00
Anton Kochkov 5cf264891b Let search_rop search an address range
* `/R` lifts every candidate instruction to RzIL, so its time grows with
  the bytes searched: `/Rj "ret"` takes about three minutes for the
  7.4 MB of code of the uutils 0.0.24 multicall binary, which is `ls` on
  Ubuntu 26.04, beyond the 120 s command timeout. A timeout kills a pipe
  session and leaves an in-process one blocked until the search ends.
* `from`/`to` bound the search through `search.from`/`search.to`, which
  both Rizin 0.9.1 and 0.10 apply to `/R`. They are resolved by the
  server first: an unknown flag in a `search.*` value evaluates to 0 with
  status OK, and `search.to=0` only warns and then searches everything
  from `search.from` on, so either would silently widen the search.
* The description says when to restrict the search.
2026-10-30 22:29:05 +08:00
Anton Kochkov a12be1c383 Refuse an unmapped address in disassemble with Rizin 0.10
* Since rizin@5b34e04, `pdj` no longer fails when it cannot read all of
  its window, so that what it could read is printed: at 0xdeadbeef of
  `/bin/ls` it now lists 0xff `invalid` instructions with status OK and
  no message, and the tool returned 32 of them as if they were code.
* When the first instruction is `invalid`, ask `aij`, which has no
  `read` where nothing readable is mapped (on 0.9.1 and 0.10 alike), and
  refuse the address then. 0xff bytes that are mapped are still listed.
* The contract test pins the `read` field of `aij`.
2026-10-30 22:29:05 +08:00
Anton Kochkov 2cb37ec0d6 Update the README for the changes
* The README states the tool counts (85 in all, 62 by default), that it
  was tested against Rizin `dev` (0.10.0) and v0.9.1, and the cost of the
  status round trip of the pipe backend.
2026-10-30 18:35:59 +08:00
Anton Kochkov ac55e418f5 Add contract tests for the commands and JSON fields the tools use
* Until Rizin publishes versioned JSON schemas (rizin#6543 B2, F1), a
  renamed command or field silently turns into an empty or wrong tool
  result. Pin what the server relies on instead, so the nightly CI run
  against Rizin `dev` fails loudly.
* Every command the tools, resources and backends run must exist: its `?`
  help succeeds exactly when it does (an unknown command or subcommand is
  INVALID). `_s` and `Flj`, which the server uses only when present, are
  required only from Rizin 0.10 on; plugin commands are exempt.
* The command names in the string literals passed to the command helpers
  in `src/server.rs` must all be in that list, so a new command cannot be
  added without being checked.
* The JSON fields read by function_summary, the annotation tools,
  search_rop, list_flags, the FLIRT tools, decompilers and
  disassemble_function must be present with the expected types.
2026-10-30 18:35:59 +08:00
Anton Kochkov b096e9dab8 Explain missing optional components and add --sigdb
* A source install of Rizin has an empty sigdb (`install_sigdb` defaults
  to false) and no decompiler plugins, and `flirt_list`, `flirt_apply_sigdb`
  and `decompilers` only reported emptiness, without saying why.
* `flirt_list` returns the sigdb as a paged JSON listing (`Flj`) and, when
  it is empty, a `hint`: build with `-Dinstall_sigdb=true`, or clone
  rizinorg/sigdb and pass `--sigdb <dir>`, which the server now sets as
  `flirt.sigdb.path` on every open. `flirt_apply_sigdb` gives the same
  hint when nothing matched because the sigdb is empty, and lists the
  applied files without the " signature file" suffix.
* `decompilers` adds the plugin directory and how to install rz-ghidra and
  jsdec when one is missing.
* The README gets a table of the optional components; CI clones the sigdb
  to test `--sigdb`.
* `Fl` has a JSON mode only since Rizin 0.10; before it, `flirt_list`
  returns the `Fl` table.
2026-10-30 18:35:59 +08:00
Anton Kochkov 28fea5c258 Annotate every tool with MCP safety hints
* Without annotations, MCP clients must assume every tool may destroy
  data and reach the outside world, so they cannot surface the safe
  queries as such.
* Mark the 55 queries `readOnlyHint`; for the others set
  `destructiveHint` (session replacement, overwritten names and comments,
  written bytes and files, raw commands) and `idempotentHint`.
  `openWorldHint` is set for the raw commands, the debugger (it runs the
  target), and the Silhouette tools (a remote server).
* A test requires the hints on every tool and keeps them consistent with
  the capability gates.
2026-10-30 18:35:59 +08:00
Anton Kochkov 095d44b5e0 Add open_data to open a sample sent as data
* A client could only open files on the server's filesystem, so a sample
  delivered in the chat had to be staged first.
* `open_data` takes base64 (or hex) data, up to 64 MiB, writes it to a new
  0600 file in a per-process temporary directory under a sanitized name,
  and opens it like `open_file`. The file belongs to the session and is
  deleted when it ends (close, reopen, or death), after the backend has
  released it.
2026-10-30 18:35:59 +08:00
Anton Kochkov 5c9a7c5516 Add function_summary, an aggregate view of a function
* Understanding a function took one round trip per aspect (info,
  signature, callers, callees, strings, decompilation). This is
  rizin#6543 D2, implemented here first.
* `function_summary` returns the name, address, size, blocks, complexity,
  argument and local counts, and signature from `afij`; callers from
  `axtj`; distinct callees from the CALL references; the referenced
  strings; and the first lines of the decompilation with the best engine
  available. `max_items` (default 20) and `snippet_lines` (default 30) cap
  it, with totals for what was left out.
* Strings come from `izj`, by containment: a reference often points into
  a string whose tail another string shares, and `ps` there guessed
  UTF-16 for ASCII text.
* The decompile engine selection moved into a helper shared with
  `decompile`; the analyze_function prompt now starts with the summary.
2026-10-30 18:35:59 +08:00
Anton Kochkov bb18b601ba Add export_annotations and save_project to keep the work
* Everything an agent renamed, commented or patched was lost with
  `close_file`.
* Record every state-changing command the tools run (the annotation
  commands and verified writes, at resolved addresses) in a journal kept
  until the next open, also past `close_file`. `export_annotations`
  returns it as a Rizin script to replay with `. <script>`.
* `save_project` saves the session as a Rizin project with `Ps`; it writes
  a file, so it is offered only with `--allow-write`, and it checks that
  the file was written.
* A test replays an exported script on a fresh analysis and reopens a
  saved project with `rizin -p`.
2026-10-30 18:35:59 +08:00
Anton Kochkov aa53d2dbec Add tools to rename and annotate behind --allow-write
* Without them, an agent could not record what it understood (the
  hypothesis -> name -> re-read loop of RE), short of raw commands, and
  its findings did not show up in later disassembly or decompilation.
* Add `rename_function` (afn), `rename_variable` (afvn), `set_comment`
  (CC, replacing unless `append`) and `remove_comment` (CC-), `set_flag`
  (f+, since `f` refuses an address that already has a flag) and
  `remove_flag` (f-), and `set_function_signature` (afs). Names, comments
  and prototypes are passed with `quote_arg()`; flag names must be
  `[A-Za-z0-9_.:]`. Each tool resolves the location to an address first
  and returns the state Rizin stored.
* `f`, `afvn` and others report success while only logging an error
  ("cannot find var by name"), so these tools also fail on a logged error.
* They change the analysis, not the file, so a read-only file is fine,
  but as the QA plan asks they are offered only with `--allow-write`.
* A location that is neither an address nor a flag is rejected also where
  the pipe cannot report the status of `s @ <loc>` (before Rizin 0.10).
2026-10-30 18:35:59 +08:00
Anton Kochkov 7cd8dd0b80 Keep list_flags from changing the selected flag space
* Found while fixing the QA items: `list_flags` with a space ran
  `fs <space>;flj`, which selects that space for the rest of the session,
  so every later `list_flags` without a space (and any raw `fl`) only
  listed it: on /bin/ls, 605 string flags instead of 997 flags.
* Select the space for this listing only, with `fss+ <space>;flj;fss-`.
  `fss+` creates a space that does not exist, so an unknown space is
  answered from `fslj` with an empty listing.
2026-10-30 18:35:59 +08:00
Anton Kochkov df1579e0ad Make the pseudo decompiler output identical on both backends
* The `pseudo` engine's text differed between backends (2652 vs 3112
  bytes in the QA run) with identical eval settings. The cause is in
  Rizin: since rizin#6062, `ds_init()` suppresses the jump arrows
  (`asm.lines`) whenever `core->is_pipe` is set, and `rz_core_cmd_str()`
  sets it for every captured command, so in-process `pd*` text never has
  them. Clearing `is_pipe` from the outside is not possible, and would
  let `!` commands write to the MCP stdout.
* Render the pseudo code without jump arrows (`asm.lines=false`), which
  makes both backends print the same text, and with `pdr` instead of
  `pdf`: on /bin/ls, `pdf @ main` takes ~30 s (in Rizin 0.9.1 and dev
  alike) because every pointer operand classifies a buffer as large as
  the whole function, while `pdr` prints the same blocks in 0.4 s.
* A test decompiles `main`, `entry0` and the three largest functions on
  both backends and requires identical text within 10 s each.
2026-10-30 18:35:59 +08:00
Anton Kochkov 410424658e Fix misleading errors of disassemble, il_function, FLIRT and decompile
* `disassemble` at an unmapped address said "no output for a command
  expecting JSON"; with command status it now fails with Rizin's "Failed
  to read chunk ... at 0xdeadbeef" (covered by a new test).
* `il_function`: `plf` fails without a message where there is no
  function, so say so when `afij` finds none; empty output of the text
  command is the new `EmptyOutput`, not `EmptyJson`.
* `flirt_apply` reported "Found 0 FLIRT signatures" for a missing file,
  since `Fs` returns OK and only logs "Can't open": check that a plain
  path exists, fail on logged errors, and return the number of matched
  functions.
* `flirt_apply_sigdb` checked its filter, a substring of the sigdb paths,
  with the path validator, rejecting `@` and other characters; quote it
  instead. It now returns the number of functions named (counted in the
  `flirt` flag space) and the signature files applied.
* `decompile` moved to the next engine on any error and ended with
  "install the plugin" even after a timeout, retrying a wedged session
  twice. Only a missing plugin (NONEXISTINGCMD) or an empty result moves
  on now; any other failure is returned as is. The skipped engines and why
  are in the structured result.
* A pipe to Rizin before 0.10 sees a failure only as no output plus an
  error message: `il_function` checks for a function on empty output as
  well, and `decompile` treats an error message without output as a
  failure of that engine.
2026-10-30 18:35:59 +08:00
Anton Kochkov 6ab5e8af66 Report a dead session as closed, with the reason
* After a command timeout killed the session, `session_info` still said
  `open: true`, because `state()` did not consult the backend; only the
  next command cleared it. A pipe whose rizin exited (e.g. after a raw
  `q`) was not even marked dead, so every later command failed with
  "worker is gone".
* `state()` drops a session whose backend is dead, and the session
  manager clears one that dies during a command, keeping the reason (e.g.
  "rizin command timed out after 120s: aaaa") in `ended` until another
  file is opened. A transport failure now marks the backend dead.
* Not downgraded: the QA run saw the in-process backend "recover" after a
  `!sleep` wedge only because `sleep` returned. A new test shows that,
  until the timed-out call returns, it holds the global Rizin lock and
  opening a new in-process session fails at the startup timeout; the
  rzcore and README notes now say so.
2026-10-30 18:35:59 +08:00
Anton Kochkov 4e576bdaa9 Leave disabled tools out of the tool catalog
* With `--no-raw`, `command` and `command_json` were still listed and only
  failed when called, so clients offered them and agents burned turns on
  them. The write and debugger tools had the same problem without
  `--allow-write` / `--allow-debug`.
* Build the server's tool router per configuration and remove the tools
  of each disabled capability (`RAW_TOOLS`, `WRITE_TOOLS` including
  `flirt_create`, `DEBUG_TOOLS`); a call to one is refused as an unknown
  tool, and the existing guards stay as a second line.
* A test keeps the lists in sync with every `write_*` and `debug_*` tool.
2026-10-30 18:35:59 +08:00
Anton Kochkov 80aa63f8aa Page listings and bound the size of every result
* Listings had no limit: `list_strings` returned ~313 KB of JSON on
  /bin/ls and `search_rop ret` ~1 MB, and the text content repeated the
  structured content pretty-printed. Tens of MB on a real sample blow the
  client's context, and an agent could not ask for the first 50 items.
* Listing tools (strings, functions, symbols, imports, exports, sections,
  segments, relocations, libraries, classes, flags, flag spaces, xrefs,
  every search, both diffs, hash algorithms, resources, and the debugger
  listings) take `offset`/`limit` (default 100 items, `--default-limit`)
  and return `items`, `total`, `offset`, `returned`, `truncated`, and
  `next_offset` while more remain. `disassemble_function` pages the
  function's instructions the same way, in `ops_page`.
* No result exceeds `--max-output-bytes` (128 KiB by default): a page is
  cut to the items that fit, text (raw `command`, decompiled code) is cut
  at a line end with a notice, and an object still over the ceiling is an
  error result asking for a narrower request rather than silently cut
  JSON.
* The text content is the structured content serialized compactly. On
  /bin/ls the first page of `list_strings` is now 10.7 KB and of
  `search_rop ret` 20 KB.
* Rizin still builds the whole listing (no native pagination yet,
  rizin#6543 D1); the slicing happens in the server.
2026-10-30 18:35:59 +08:00
Anton Kochkov f91edac4c2 Return structuredContent as a JSON object
* Found while testing the QA fixes with the official SDKs: MCP defines
  `structuredContent` as an object, and every tool that returned a JSON
  array there (list_functions, sections, imports, strings, searches, ...)
  made the TypeScript SDK (1.32.1) throw a ZodError ("expected record,
  received array") and the Python SDK (2.3.0) fail validation, so those
  tools were unusable from such clients.
* Wrap arrays as `{"items": [...]}` and other non-objects (the `null` of
  absent metadata) as `{"result": ...}`; the text content is the same
  object, serialized.
* An MCP-level test calls the listing tools on both backends and checks
  that each structured result is an object.
2026-10-30 18:35:59 +08:00
Anton Kochkov 686ee318fc Open an empty session for "-" on both backends
* The in-process backend passed `-` to `rz_core_file_open_load()`, which
  looked for a file named `-` and failed with "file not found: -". The
  pipe backend ran `rizin -q0 -`, which reads a file from stdin, i.e.
  from the command pipe, so it never became ready and the open failed at
  the startup timeout.
* Treat `-` as "no target": create the core without opening a file, or
  run `rizin -q0` without a target.
* When Rizin cannot open a target, both backends now return
  `OpenFailed` ("rizin cannot open '<target>'" and Rizin's reason, logged
  in process or read from stderr before rizin exits), instead of "file
  not found" in process and a handshake error through the pipe.
* Refuse directories up front: the CLI does, but the library opens one as
  an endless file.
2026-10-30 18:35:59 +08:00
Anton Kochkov b432844fa9 Fix write_assembly and verify every write
* `wa mov eax, 1 @ 0x20` assembles each space-separated word as its own
  instruction, so it failed on `mov`, and the tool reported "Assembled and
  wrote" because the output was empty.
* Split the assembly at `;` and newlines, assemble it with
  `pa "<insns>" @ <addr>` (so relative branches are encoded for the
  destination), and write the bytes with `wx`. The result now carries the
  bytes written.
* `wx` returns OK even when it cannot write (e.g. at an unmapped address),
  saying so only in an error message, so write_hex and write_string
  "succeeded" as well. All three tools now read the bytes back and fail
  with the new `WriteFailed` error (code -32001) otherwise.
* A pipe to Rizin before 0.10 sees a failed `pa` only as no output; its
  "Cannot assemble" message is then reported as the failure.
2026-10-30 18:35:59 +08:00
Anton Kochkov 6ab68b2f86 Fix search_rop for multi-instruction queries
* `/Rj pop rdi; ret` was split by Rizin's shell at `;`: the in-process
  backend failed the whole line, the pipe backend silently returned the
  gadgets of `/Rj pop rdi` alone. Quoted, `/Rj "pop rdi; ret"` returned
  nothing, because `/R` does not trim its `;`-separated fields and looked
  for " ret".
* Split the query on `;`, trim the instructions and collapse their spaces,
  and pass them as one double-quoted filter, `/Rj "pop rdi;ret"`.
  `quote_arg()` escapes the four characters that stay special inside
  double quotes (`\`, `"`, `$`, backtick), so any other text reaches the
  command verbatim; a test echoes such text through both backends.
* Describe what `/R` matches: the instructions in order, each as a
  substring, possibly with others in between. `exact: true` keeps only the
  gadgets made of exactly those instructions.
* Add MCP-level tests that drive the server through an rmcp client over an
  in-memory stream, on both backends, holding the same lock per in-process
  server as the integration tests.
2026-10-30 18:35:59 +08:00
Anton Kochkov abad61ab6c Add CI building Rizin into a user prefix
* Build Rizin `dev` and v0.9.1 into `~/.local/rizin` (cached by commit),
  the documented install path whose multiarch layout and missing rpath
  broke the build and the start of the binary, then lint, build, start
  the binary without LD_LIBRARY_PATH, and run the tests against each.
* Run nightly as well, so command or API drift on Rizin's `dev` branch
  shows up in CI rather than in users' sessions.
* A pipe-only job checks formatting and the build without librz.
* The integration tests hold a lock while an in-process session is open:
  Rizin 0.9 keeps its console in globals, so the cores of tests running in
  parallel must not overlap (opening a file then fails at random).
2026-10-30 18:35:59 +08:00
Anton Kochkov e1627ac036 Report the status and diagnostics of Rizin commands
* A failed command was indistinguishable from an empty success: in
  process, `rz_core_cmd_str()` returns NULL on failure and the FFI mapped
  it to ""; through the pipe, the error went to rizin's stderr. `command
  pdg` without rz-ghidra returned an empty success, `/Rj pop rdi; ret`
  failed with a misleading "no output for a command expecting JSON", and
  `write_assembly` reported success after `wa` failed.
* In process, run commands with `rz_core_cmd_str_status()` (Rizin 0.10,
  detected in the installed `rz_core.h`), which keeps the output of a
  failed line and returns its status. Rizin's log messages, which explain
  failures, are routed to the server through a small C shim, since stable
  Rust cannot define the variadic `RzLogCallback`, and collected per
  command. Before 0.10, `rz_core_cmd_str()` returning NULL is reported as
  `FAILED`.
* Through the pipe, query `_sj` after every command when the startup probe
  finds it (Rizin 0.10; about 50 µs more per command on `/bin/ls`), and on
  Unix read rizin's stderr alongside stdout with poll(), so the messages a
  command printed before its NUL are returned with it. Without `_sj` the
  status is `UNKNOWN` and commands are judged by their output, as before.
* A failure becomes `RizinError::CommandFailed`, a JSON-RPC error with the
  new code -32001 and `command`/`status`/`detail` in `data`; `EmptyJson`
  now only means a successful command printed nothing, and carries
  Rizin's messages. The raw `command` tool returns a tool error result
  that keeps the output printed before the failure.
* Optional metadata (`iCj` fails on an unsigned ELF) still maps a runtime
  failure to "absent", but not an unknown command or wrong arguments; a
  failed analysis at open time is reported in `analysis_error` instead of
  being ignored.
* Before Rizin 0.10, a missing command is still recognized from its
  "Command '...' does not exist." message, so a plugin command that is not
  installed reports NONEXISTINGCMD on both backends; the suite passes
  against v0.9.1 as well.
2026-10-30 18:35:59 +08:00
Anton Kochkov 340ebf080a Record Rizin's library directory as an rpath of the binary
* With Rizin in a custom prefix the binary failed to start with
  `librz_core.so.0.10: cannot open shared object file`, and an MCP client
  launching a bare command cannot set LD_LIBRARY_PATH. `cargo test` had
  the same problem.
* Add `-Wl,-rpath,<libdir>` for dynamic linking, except for the loader's
  default directories; `RIZIN_RPATH=0` opts out for packagers.
2026-10-30 18:35:59 +08:00
Anton Kochkov 5d79d9ec1e Resolve Rizin's library directory from the plain pkg-config libdir
* `pkg-config --define-prefix` rebuilds the prefix assuming a
  `<prefix>/lib/pkgconfig` layout, so for the multiarch
  `lib/x86_64-linux-gnu/pkgconfig` layout Meson uses even for a custom
  prefix it reports `<prefix>/lib/lib/x86_64-linux-gnu`. That query
  succeeded, so the correct plain `libdir` fallback never ran and the
  link failed with `unable to find library -lrz_core`.
* Query the plain `libdir` first and keep `--define-prefix` only for
  relocated installs; trust a directory only if it holds `librz_core`.
* Fail the build when the directory lacks `rz_core` or `rz_util`, instead
  of linking `rz_core` alone, which left `rz_mem_free` (in `rz_util`)
  undefined.
* Rerun the build script when `librz_core` or `rz_core.pc` change, and on
  the other pkg-config environment variables.
2026-10-30 18:35:59 +08:00
Anton Kochkov ad578ea359 Remove a redundant clone of the transport in main()
* `Transport` is `Copy`, so `cargo clippy -- -D warnings` rejects the
  `clone()` added for the startup log line (clippy::clone_on_copy).
2026-10-30 18:35:59 +08:00
Anton Kochkov 62fe8b824b Initial commit 2026-06-27 22:42:30 +04:00