From 9906f2c37b81cbadb6beb1b317fb432de32f0386 Mon Sep 17 00:00:00 2001 From: grunch Date: Wed, 17 Jun 2026 09:37:07 -0300 Subject: [PATCH 1/4] =?UTF-8?q?feat(transport):=20Phase=203=20=E2=80=94=20?= =?UTF-8?q?transport=20auto-detection=20+=20get-dm=20v2=20+=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of docs/TRANSPORT_V2_SPEC.md — completes the CLI's transport-v2 support. - Auto-detection: `events::fetch_protocol_version_with` reads the node's `protocol_versions` tag (kind-38385 info event) with a short INFO_PROBE_TIMEOUT. `init_context` → `resolve_transport` runs it once at startup when `--transport`/`TRANSPORT` is unset: "2" → TRANSPORT=nip44; "1"/absent/unreachable → leave unset (messaging defaults to gift-wrap). An explicit `--transport` is authoritative and skips the probe. A pre-v2 daemon publishes no tag, so the CLI stays on v1 instead of mis-pairing (backward-compat guard for the version-skew risk Phase 1 flagged). - get-dm listing is transport-aware: `create_filter` for the DirectMessages* kinds now uses `transport.event_kind()` and pins `author = mostro_pubkey` on v2 (new `mostro_pubkey` param threaded through call sites), closing the Phase 2 gap. - Verbose: `resolve_transport` logs the active transport and how it was chosen. - Docs: docs/commands.md gains a Global options section documenting `--transport` + auto-detection; spec marked complete. Tests: deterministic `protocol_versions` tag read/parse. Full suite green; clippy --all-targets --all-features -D warnings and fmt clean. Based on Phase 2 (#177). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/TRANSPORT_V2_SPEC.md | 44 ++++++++++++++++++++------------- docs/commands.md | 18 ++++++++++++++ src/cli.rs | 35 ++++++++++++++++++++++++++ src/util/events.rs | 52 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 17 deletions(-) diff --git a/docs/TRANSPORT_V2_SPEC.md b/docs/TRANSPORT_V2_SPEC.md index 60ddb6d..fdfd92f 100644 --- a/docs/TRANSPORT_V2_SPEC.md +++ b/docs/TRANSPORT_V2_SPEC.md @@ -1,6 +1,6 @@ # mostro-cli — Transport v2 (NIP-44 Direct) client support -**Status:** Phases 1–2 implemented · Phase 3 pending +**Status:** Phases 1–3 implemented **Daemon spec:** `MostroP2P/mostro` → `docs/TRANSPORT_V2_SPEC.md` **Issue:** [#626 — Messaging Transport Abstraction Layer](https://github.com/MostroP2P/mostro/issues/626) **Core:** `transport` module shipped in **mostro-core 0.13.0** @@ -140,27 +140,37 @@ gates. > to 2. Phase 1 (gift-wrap, kind 1059, `version = 2`) never hits the gate, so > its backward-compatibility is unaffected. -### Phase 3 — Capability auto-detection + docs/UX — PENDING - -- Read the node's `protocol_versions` tag from its kind-`38385` info event - (same fetch path as the existing `pow` probe) and, when `--transport` is not - given, auto-select the matching transport — warning on a mismatch - ("this node speaks v2; re-run with --transport nip44") instead of silently - timing out. This `protocol_versions` probe is also the **backward-compat - guard** for the version-skew risk Phase 1 flagged: a pre-0.13 daemon (or a - misconfigured pairing) advertises no `protocol_versions` tag, so the CLI - treats it as v1/gift-wrap and warns rather than silently failing. Until this - lands, the explicit `--transport` flag is the only negotiation — an operator - pointing a 0.13 CLI at an older daemon must match transports manually. -- Surface the active transport in verbose output. -- Update `docs/architecture.md`, `docs/commands.md`, and the README. +### Phase 3 — Capability auto-detection + docs/UX — IMPLEMENTED + +- **Auto-detection.** `events::fetch_protocol_version_with` reads the node's + `protocol_versions` tag from its kind-38385 info event (short + `INFO_PROBE_TIMEOUT` so a node without one degrades fast). `init_context` → + `resolve_transport` runs it **once at startup** when `--transport` / + `TRANSPORT` is unset: `2` → set `TRANSPORT=nip44`, `1` / absent / unreachable + → leave it unset so the messaging layer defaults to gift-wrap. An explicit + `--transport` is authoritative and skips the probe entirely. +- **Backward-compat guard.** Because a pre-v2 daemon publishes no + `protocol_versions` tag, auto-detect leaves the CLI on gift-wrap (v1) rather + than silently mis-pairing — addressing the version-skew risk Phase 1 flagged. + An operator can still force either transport with `--transport`. +- **Verbose surface.** `resolve_transport` logs the active transport and how it + was chosen (`explicit` / `auto-detected protocol vN` / default fallback) at + `info` (shown with `-v`). +- **Docs.** `docs/commands.md` documents the global `--transport` flag and the + auto-detection; this spec is marked complete. (The `get-dm` listing and + range-order follow-up became transport-aware in Phase 2 via `create_filter`.) + +Tests: `protocol_versions` tag read + parse (deterministic, offline). The +auto-detect wiring is exercised end to end by the manual checks below (they +depend on a live relay/node). ## 5. Testing notes - The daemon under test (`MostroP2P/mostro` PR #780) defaults to `transport = "gift-wrap"`; set `transport = "nip44"` in its `settings.toml` to exercise v2 + the anti-spam gate. -- After Phase 2, run the CLI with `--transport nip44` (or `TRANSPORT=nip44`) - against that node. +- Run the CLI with `--transport nip44` (or `TRANSPORT=nip44`) against that + node. As of Phase 3 you can also omit it: the CLI auto-detects the node's + transport from its `protocol_versions` info tag at startup. - The daemon's first-contact PoW lane (`pow_first_contact`) is testable by combining `--transport nip44` with `--pow ` on the CLI. diff --git a/docs/commands.md b/docs/commands.md index fdf6d1a..3d3a171 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -4,6 +4,24 @@ This document lists all `mostro-cli` subcommands defined in `src/cli.rs`, their All commands are part of the `Commands` enum and are dispatched via `Commands::run(&self, ctx: &Context)`. +### Global options + +These flags apply to every subcommand (parsed on the top-level `Cli`; each also +settable via the matching env var): + +- `-m, --mostropubkey ` (`MOSTRO_PUBKEY`): the Mostro node to talk to. Required. +- `-r, --relays ` (`RELAYS`): comma-separated relay URLs. +- `-p, --pow ` (`POW`): NIP-13 proof-of-work difficulty mined on outgoing events. +- `-s, --secret` (`SECRET=true`): full-privacy mode (unsigned inner tuple; identity = trade key). +- `-t, --transport ` (`TRANSPORT`): wire transport to speak — + `gift-wrap` (protocol v1, kind 1059) or `nip44` (protocol v2, signed kind 14). + **Optional**: when omitted, the CLI auto-detects it at startup from the node's + `protocol_versions` tag on its kind-38385 info event (a node that advertises + nothing — e.g. a pre-v2 daemon — is treated as `gift-wrap`). Pass the flag to + override auto-detection. Must match the node's `transport` setting. See + `docs/TRANSPORT_V2_SPEC.md`. +- `-v, --verbose`: enable info-level logging (also surfaces the resolved transport). + ### Orders - **`listorders`** diff --git a/src/cli.rs b/src/cli.rs index 5c0383d..797cf1b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -479,6 +479,12 @@ async fn init_context(cli: &Cli) -> Result { // Connect to Nostr relays let client = util::connect_nostr().await?; + // Resolve the wire transport once, at startup. An explicit + // `--transport`/`TRANSPORT` wins; otherwise auto-detect from the node's + // advertised `protocol_versions` so the operator need not match it by hand + // (docs/TRANSPORT_V2_SPEC.md Phase 3). + resolve_transport(&client, mostro_pubkey).await; + Ok(Context { client, identity_keys, @@ -490,6 +496,35 @@ async fn init_context(cli: &Cli) -> Result { }) } +/// Resolve the wire transport into the `TRANSPORT` env var the messaging layer +/// reads (`parse_transport_env`). An explicit `--transport` / `TRANSPORT` is +/// authoritative and skips the network probe; otherwise the node's advertised +/// `protocol_versions` tag (kind-38385 info event) selects it. A node that +/// publishes nothing — a pre-v2 daemon, or an unreachable relay — leaves the +/// var unset, so the messaging layer falls back to the gift-wrap default. This +/// also guards against accidentally pairing a v2-capable CLI with an older +/// daemon: absent the tag, the CLI stays on v1. +async fn resolve_transport(client: &Client, mostro_pubkey: PublicKey) { + if let Ok(explicit) = std::env::var("TRANSPORT") { + log::info!("Transport: {explicit} (explicit)"); + return; + } + match util::events::fetch_protocol_version_with(client.clone(), mostro_pubkey).await { + Some(2) => { + set_var("TRANSPORT", "nip44"); + log::info!("Transport: nip44 (auto-detected protocol v2)"); + } + Some(1) => log::info!("Transport: gift-wrap (auto-detected protocol v1)"), + Some(other) => { + log::warn!("Node advertised unknown protocol_versions={other}; defaulting to gift-wrap") + } + None => log::info!( + "Could not detect node transport (no kind-38385 protocol_versions tag); \ + defaulting to gift-wrap" + ), + } +} + fn is_admin_command(command: &Option) -> bool { matches!( command, diff --git a/src/util/events.rs b/src/util/events.rs index 7da5f7b..0fcfcd8 100644 --- a/src/util/events.rs +++ b/src/util/events.rs @@ -175,6 +175,30 @@ pub async fn fetch_required_pow_with(client: Client, mostro_pubkey: PublicKey) - read_info_tag_from_event(event, "pow").and_then(|v| v.parse::().ok()) } +/// Timeout for the startup transport-capability probe. Deliberately short: it +/// runs before every command when `--transport`/`TRANSPORT` is unset, so a +/// node that publishes no info event must degrade to the gift-wrap default +/// quickly rather than blocking the command for the full +/// [`FETCH_EVENTS_TIMEOUT`]. +pub const INFO_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); + +/// Fetch the node's advertised protocol version from the kind-38385 info +/// event's `protocol_versions` tag (`"1"` = gift wrap, `"2"` = NIP-44 direct; +/// see the daemon's docs/TRANSPORT_V2_SPEC.md §4). +/// +/// `None` when the node publishes no info event, the tag is absent (a pre-v2 +/// daemon), or the value is unparseable — every such case is treated by the +/// caller as "assume v1/gift-wrap". Used by the CLI's startup auto-detection +/// when the operator didn't pick a transport explicitly. +pub async fn fetch_protocol_version_with(client: Client, mostro_pubkey: PublicKey) -> Option { + let filter = Filter::new() + .author(mostro_pubkey) + .kind(nostr_sdk::Kind::Custom(NOSTR_INFO_EVENT_KIND)); + let events = client.fetch_events(filter, INFO_PROBE_TIMEOUT).await.ok()?; + let event = events.iter().max_by_key(|e| e.created_at)?; + read_info_tag_from_event(event, "protocol_versions").and_then(|v| v.trim().parse::().ok()) +} + #[allow(clippy::too_many_arguments)] pub async fn fetch_events_list( list_kind: ListKind, @@ -314,6 +338,34 @@ mod tests { assert_eq!(read_info_tag_from_event(&event, "pow"), None); } + #[tokio::test] + async fn protocol_versions_tag_reads_and_parses() { + // Mirrors how `fetch_protocol_version_with` extracts the daemon's + // single-value `protocol_versions` tag ("1" = gift wrap, "2" = nip44) + // and parses it to the u8 the CLI's auto-detect maps to a Transport. + let keys = Keys::generate(); + let event = make_info_event( + &keys, + vec![ + pow_tag("0"), + Tag::parse(["protocol_versions", "2"]).unwrap(), + ], + ) + .await; + assert_eq!( + read_info_tag_from_event(&event, "protocol_versions").as_deref(), + Some("2") + ); + assert_eq!( + read_info_tag_from_event(&event, "protocol_versions") + .and_then(|v| v.trim().parse::().ok()), + Some(2) + ); + // Absent tag (a pre-v2 daemon) → None → caller assumes gift-wrap. + let bare = make_info_event(&keys, vec![pow_tag("0")]).await; + assert_eq!(read_info_tag_from_event(&bare, "protocol_versions"), None); + } + #[tokio::test] async fn pow_tag_parses_as_u8() { // u8 parse is what fetch_required_pow chains after the helper. From bcdc10c2c6e46066c8d4dce4633750b317869fd7 Mon Sep 17 00:00:00 2001 From: grunch Date: Wed, 17 Jun 2026 16:20:47 -0300 Subject: [PATCH 2/4] fix(transport): wait for relay connection before first request connect() only spawns background connection tasks; firing subscribe/ send_dm immediately raced the handshake and dropped Mostro's reply, surfacing as "Timeout waiting for DM". Block on wait_for_connection so auto-detection and the DM round-trip run against a live relay. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/util/net.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/util/net.rs b/src/util/net.rs index 0bf2409..4d46eda 100644 --- a/src/util/net.rs +++ b/src/util/net.rs @@ -1,6 +1,15 @@ use anyhow::Result; use nostr_sdk::prelude::*; use std::env::var; +use std::time::Duration; + +/// Upper bound on how long [`connect_nostr`] blocks waiting for the relay +/// handshakes to complete. `Client::connect` only *spawns* background +/// connection tasks and returns immediately, so without this wait the very +/// next network op (the transport probe, then `subscribe` + `send_dm`) races +/// the still-in-progress handshake. On a fast/local relay this returns in +/// milliseconds; it only blocks the full budget when a relay is unreachable. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); pub async fn connect_nostr() -> Result { let my_keys = Keys::generate(); @@ -12,5 +21,12 @@ pub async fn connect_nostr() -> Result { client.add_relay(r).await?; } client.connect().await; + // `connect` is fire-and-forget: it doesn't wait for the sockets to come up. + // Block until the relays are actually connected so the immediately + // following transport auto-detection, subscription and DM publish don't + // race the handshake — otherwise a fast (e.g. local) Mostro can reply + // before our subscription lands and, with `limit(0)`, the live-only + // subscription never sees the stored reply → `wait_for_dm` times out. + client.wait_for_connection(CONNECT_TIMEOUT).await; Ok(client) } From 7e47097b3cb7de30ba74db2107ec930e26cf2c7c Mon Sep 17 00:00:00 2001 From: grunch Date: Wed, 17 Jun 2026 16:35:02 -0300 Subject: [PATCH 3/4] refactor(transport): rename info tag protocol_versions -> protocol_version Client and node are compatible only when they run the same protocol version, so the kind-38385 info tag carries a single value ("1" or "2"), not a list. Rename the tag everywhere the CLI reads/documents it to match the daemon and protocol spec. NOTE: must land together with the matching mostrod + protocol changes; the daemon still emits the old tag name until then. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/TRANSPORT_V2_SPEC.md | 10 +++++----- docs/commands.md | 2 +- src/cli.rs | 8 ++++---- src/util/events.rs | 19 ++++++++----------- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/docs/TRANSPORT_V2_SPEC.md b/docs/TRANSPORT_V2_SPEC.md index fdfd92f..c801840 100644 --- a/docs/TRANSPORT_V2_SPEC.md +++ b/docs/TRANSPORT_V2_SPEC.md @@ -17,7 +17,7 @@ transport). The daemon now speaks one of two wire transports per node, selected by its `[mostro] transport` setting and advertised on the kind-`38385` instance-info -event via a `protocol_versions` tag (`"1"` or `"2"`). A v1-only client cannot +event via a `protocol_version` tag (`"1"` or `"2"`). A v1-only client cannot talk to a `transport = "nip44"` node at all (it never sees kind-14 traffic and its gift wraps are ignored). To test and use v2 nodes, the CLI must: @@ -143,14 +143,14 @@ gates. ### Phase 3 — Capability auto-detection + docs/UX — IMPLEMENTED - **Auto-detection.** `events::fetch_protocol_version_with` reads the node's - `protocol_versions` tag from its kind-38385 info event (short + `protocol_version` tag from its kind-38385 info event (short `INFO_PROBE_TIMEOUT` so a node without one degrades fast). `init_context` → `resolve_transport` runs it **once at startup** when `--transport` / `TRANSPORT` is unset: `2` → set `TRANSPORT=nip44`, `1` / absent / unreachable → leave it unset so the messaging layer defaults to gift-wrap. An explicit `--transport` is authoritative and skips the probe entirely. - **Backward-compat guard.** Because a pre-v2 daemon publishes no - `protocol_versions` tag, auto-detect leaves the CLI on gift-wrap (v1) rather + `protocol_version` tag, auto-detect leaves the CLI on gift-wrap (v1) rather than silently mis-pairing — addressing the version-skew risk Phase 1 flagged. An operator can still force either transport with `--transport`. - **Verbose surface.** `resolve_transport` logs the active transport and how it @@ -160,7 +160,7 @@ gates. auto-detection; this spec is marked complete. (The `get-dm` listing and range-order follow-up became transport-aware in Phase 2 via `create_filter`.) -Tests: `protocol_versions` tag read + parse (deterministic, offline). The +Tests: `protocol_version` tag read + parse (deterministic, offline). The auto-detect wiring is exercised end to end by the manual checks below (they depend on a live relay/node). @@ -171,6 +171,6 @@ depend on a live relay/node). to exercise v2 + the anti-spam gate. - Run the CLI with `--transport nip44` (or `TRANSPORT=nip44`) against that node. As of Phase 3 you can also omit it: the CLI auto-detects the node's - transport from its `protocol_versions` info tag at startup. + transport from its `protocol_version` info tag at startup. - The daemon's first-contact PoW lane (`pow_first_contact`) is testable by combining `--transport nip44` with `--pow ` on the CLI. diff --git a/docs/commands.md b/docs/commands.md index 3d3a171..4316c82 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -16,7 +16,7 @@ settable via the matching env var): - `-t, --transport ` (`TRANSPORT`): wire transport to speak — `gift-wrap` (protocol v1, kind 1059) or `nip44` (protocol v2, signed kind 14). **Optional**: when omitted, the CLI auto-detects it at startup from the node's - `protocol_versions` tag on its kind-38385 info event (a node that advertises + `protocol_version` tag on its kind-38385 info event (a node that advertises nothing — e.g. a pre-v2 daemon — is treated as `gift-wrap`). Pass the flag to override auto-detection. Must match the node's `transport` setting. See `docs/TRANSPORT_V2_SPEC.md`. diff --git a/src/cli.rs b/src/cli.rs index 797cf1b..9d82023 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -481,7 +481,7 @@ async fn init_context(cli: &Cli) -> Result { // Resolve the wire transport once, at startup. An explicit // `--transport`/`TRANSPORT` wins; otherwise auto-detect from the node's - // advertised `protocol_versions` so the operator need not match it by hand + // advertised `protocol_version` so the operator need not match it by hand // (docs/TRANSPORT_V2_SPEC.md Phase 3). resolve_transport(&client, mostro_pubkey).await; @@ -499,7 +499,7 @@ async fn init_context(cli: &Cli) -> Result { /// Resolve the wire transport into the `TRANSPORT` env var the messaging layer /// reads (`parse_transport_env`). An explicit `--transport` / `TRANSPORT` is /// authoritative and skips the network probe; otherwise the node's advertised -/// `protocol_versions` tag (kind-38385 info event) selects it. A node that +/// `protocol_version` tag (kind-38385 info event) selects it. A node that /// publishes nothing — a pre-v2 daemon, or an unreachable relay — leaves the /// var unset, so the messaging layer falls back to the gift-wrap default. This /// also guards against accidentally pairing a v2-capable CLI with an older @@ -516,10 +516,10 @@ async fn resolve_transport(client: &Client, mostro_pubkey: PublicKey) { } Some(1) => log::info!("Transport: gift-wrap (auto-detected protocol v1)"), Some(other) => { - log::warn!("Node advertised unknown protocol_versions={other}; defaulting to gift-wrap") + log::warn!("Node advertised unknown protocol_version={other}; defaulting to gift-wrap") } None => log::info!( - "Could not detect node transport (no kind-38385 protocol_versions tag); \ + "Could not detect node transport (no kind-38385 protocol_version tag); \ defaulting to gift-wrap" ), } diff --git a/src/util/events.rs b/src/util/events.rs index 0fcfcd8..a910984 100644 --- a/src/util/events.rs +++ b/src/util/events.rs @@ -183,7 +183,7 @@ pub async fn fetch_required_pow_with(client: Client, mostro_pubkey: PublicKey) - pub const INFO_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); /// Fetch the node's advertised protocol version from the kind-38385 info -/// event's `protocol_versions` tag (`"1"` = gift wrap, `"2"` = NIP-44 direct; +/// event's `protocol_version` tag (`"1"` = gift wrap, `"2"` = NIP-44 direct; /// see the daemon's docs/TRANSPORT_V2_SPEC.md §4). /// /// `None` when the node publishes no info event, the tag is absent (a pre-v2 @@ -196,7 +196,7 @@ pub async fn fetch_protocol_version_with(client: Client, mostro_pubkey: PublicKe .kind(nostr_sdk::Kind::Custom(NOSTR_INFO_EVENT_KIND)); let events = client.fetch_events(filter, INFO_PROBE_TIMEOUT).await.ok()?; let event = events.iter().max_by_key(|e| e.created_at)?; - read_info_tag_from_event(event, "protocol_versions").and_then(|v| v.trim().parse::().ok()) + read_info_tag_from_event(event, "protocol_version").and_then(|v| v.trim().parse::().ok()) } #[allow(clippy::too_many_arguments)] @@ -339,31 +339,28 @@ mod tests { } #[tokio::test] - async fn protocol_versions_tag_reads_and_parses() { + async fn protocol_version_tag_reads_and_parses() { // Mirrors how `fetch_protocol_version_with` extracts the daemon's - // single-value `protocol_versions` tag ("1" = gift wrap, "2" = nip44) + // single-value `protocol_version` tag ("1" = gift wrap, "2" = nip44) // and parses it to the u8 the CLI's auto-detect maps to a Transport. let keys = Keys::generate(); let event = make_info_event( &keys, - vec![ - pow_tag("0"), - Tag::parse(["protocol_versions", "2"]).unwrap(), - ], + vec![pow_tag("0"), Tag::parse(["protocol_version", "2"]).unwrap()], ) .await; assert_eq!( - read_info_tag_from_event(&event, "protocol_versions").as_deref(), + read_info_tag_from_event(&event, "protocol_version").as_deref(), Some("2") ); assert_eq!( - read_info_tag_from_event(&event, "protocol_versions") + read_info_tag_from_event(&event, "protocol_version") .and_then(|v| v.trim().parse::().ok()), Some(2) ); // Absent tag (a pre-v2 daemon) → None → caller assumes gift-wrap. let bare = make_info_event(&keys, vec![pow_tag("0")]).await; - assert_eq!(read_info_tag_from_event(&bare, "protocol_versions"), None); + assert_eq!(read_info_tag_from_event(&bare, "protocol_version"), None); } #[tokio::test] From a66f54d3a2f4720e71c62d6c246c56b7d3403823 Mon Sep 17 00:00:00 2001 From: grunch Date: Wed, 17 Jun 2026 16:35:34 -0300 Subject: [PATCH 4/4] fix(transport): treat empty TRANSPORT as unset in resolve_transport resolve_transport short-circuited on any present TRANSPORT value, but parse_transport_env only honors a non-empty, trimmed value. So `TRANSPORT=""` (e.g. `--transport ""`) skipped auto-detection and left the var empty, silently pairing a v2 node to the gift-wrap default. Only short-circuit on a meaningful value; fall through to auto-detect otherwise. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/cli.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 9d82023..834401d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -505,9 +505,16 @@ async fn init_context(cli: &Cli) -> Result { /// also guards against accidentally pairing a v2-capable CLI with an older /// daemon: absent the tag, the CLI stays on v1. async fn resolve_transport(client: &Client, mostro_pubkey: PublicKey) { + // Only an explicit, non-empty value is authoritative — mirror + // `parse_transport_env`, which treats empty/whitespace as unset and falls + // back to the default. Otherwise `TRANSPORT=""` (e.g. `--transport ""`) + // would skip auto-detection and leave the var empty, silently pairing a v2 + // node to the gift-wrap default. if let Ok(explicit) = std::env::var("TRANSPORT") { - log::info!("Transport: {explicit} (explicit)"); - return; + if !explicit.trim().is_empty() { + log::info!("Transport: {explicit} (explicit)"); + return; + } } match util::events::fetch_protocol_version_with(client.clone(), mostro_pubkey).await { Some(2) => {