Skip to content

Fix radio nickname updates after acknowledged writes - #6470

Merged
jensenpat merged 5 commits into
aethersdr:mainfrom
rfoust:codex/fix-6469-nickname-confirmation
Oct 17, 2026
Merged

jensenpat merged 5 commits into
aethersdr:mainfrom
rfoust:codex/fix-6469-nickname-confirmation

Conversation

@rfoust

@rfoust rfoust commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

SmartSDR 4.3.16 can acknowledge a nickname clear or rename with R|0| without sending a nickname status echo. Apply the sanitized nickname only after a valid successful reply so the model, radio tab, station label and status bar converge without reconnecting. Consume the field edit before dispatch, preserve newer uncommitted text, reconcile rejected values, and release pending state on disconnect.

An unrelated full radio status can carry the old nickname between dispatch and R|0. The successful reply confirms the requested name despite that earlier status; a nickname status received after confirmation remains authoritative. Request ordering, session reset and backend destruction still fence stale replies.

Addresses #6469. The original second clear 24 seconds later remains unproven, so this PR does not close the issue. Related: #6460, #6437 and the bridge editing gap in #6451.

The Flex backend uses the existing guarded reply-capable command path and LAN/SmartLink transport, preserving foreign-pan ownership checks. Current main's answered-transmit and Radio Setup sinks are retained. Malformed or missing result codes now fail closed as -1 for all Flex reply consumers, rather than defaulting to success; valid unsigned 32-bit error codes retain their wire bit pattern. The unified sink uses sendCmdPublic, which lacks sendCommand's additional protocol debug line; transport TX: logging remains, and current main already routes the backend through sendCmdPublic.

The bridge adds explicit invoke editText and invoke commit actions. Only radioNicknameEdit opts in. Changed text, including empty text, arms the existing modified guard; commit queues editingFinished without re-arming repeated commits. Other fields, invalid input, hidden/disabled/read-only edits and observe-only requests are refused. Deferred commits recheck authorization and field state. Existing setText and submit behavior is preserved. User and bridge documentation and MCP descriptions are updated.

Validation

  • Rebased onto bccd4b719; signed head 2d3a2147361d47952a59e523b86cf5a75cef3844. GitHub verifies all five commits. Mechanical conflict resolution retains current main's answered-transmit and setup sinks; generated Log Analyzer references were refreshed.
  • Six socket-free focused tests passed headless: radio_nickname_test, automation_boundary_widgets_test, model_control_routing_test, backend_receive_contract_test, aetherd_amp_tuner_encode_test, and radio_setup_control_routing_test.
  • The old status fence fails the new rename/clear ordering assertions. The fixed tests also require later status authority, preserved pending field text, exactly one identity notification, request/session/lifetime fencing and malformed-reply rejection. The fixture uses main's existing backend wiring without an extra test-only identity connection.
  • ARM64 Qt 6.12 app build passed with RADE enabled and REQUIRE_KEYCHAIN=ON; no RNNoise x86 sources. The authenticated isolated DEMO-0001 bridge identified the clean head and passed production-field rename, repeated commit and empty commit; txAllowed:false and transmitting:false. The owned instance was stopped. The final local build and repeat of all six tests and the bridge proof use a Qt 6.12-matched QtKeychain library; the runtime reports no duplicate Qt classes.
  • Engine boundary, command-plane, capability-record and touchpoint checks pass with identical findings to current main and no baseline growth. Generated user-reference, bridge docs, Log Analyzer (707 checks) and diff checks pass. Hosted Docs (site and PDF) and Static checks pass on the new head; platform CI gates the merge. No local docs/PDF build was run for the landing.

Existing hardware evidence

The author tested original head c5ef0abf9 on a FLEX-6700 running 4.3.16.42034: bridge-only clear and sanitized rename received R|0|, all identity surfaces converged without reconnecting, repeated commits sent no extra commands, and the rename survived reconnect. The original nickname was restored; TX remained disabled. Earlier 4.2.18 evidence exercised native Return/focus loss.

NF0T independently tested that original head on a FLEX-8400 running 4.3.16.42034 and reproduced the old-name status race documented in the review. The landing regression injects that exact status-before-reply ordering. The final rebased change has not been retested on live hardware.

Limits

Generated with OpenAI Codex (GPT-6 Astra)

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Thanks for this — the completion-callback nickname round-trip is a real improvement, and the failure here is a single line, not a design problem.

What failed

Static checks → step Command plane above the seam (run 38588841668). Everything else in that job passed, and Build docs site and Sanitizer option configures were green. build, check-macos and check-windows were still in progress when I looked, so I can't say anything about them yet.

This is your code, not CI infrastructure — the #5262 M4 ratchet is working as designed.

Reproduced locally at 5c532a7 with python tools/check_command_plane.py --strict:

::error file=src/models/RadioModel.cpp,title=command-plane-ratchet::src/models/RadioModel.cpp
grew from 134 to 135 raw Flex command(s) above the seam. This baseline may only shrink (#5262 M4).
command-plane: 30 file(s), 297 raw Flex command(s) above the seam; baseline 296 across 30 file(s)

Why

src/models/RadioModel.cpp is listed in PLANE_HELPER_FILES (tools/check_command_plane.py), so inside that one file unqualified sendCmd( / sendCommand( calls are counted as command-plane sites — it's the sink itself, so there's no receiver to resolve. Your new sink forwarder in setupBackend() adds one:

flex->setNicknameCommandSink([this](const QString& cmd, FlexBackend::CommandReply reply) {
    return sendCmd(cmd, std::move(reply)) != 0;   // ← site #135
});

Worth saying plainly: that lambda isn't wire text. The encode still lives in FlexBackend::requestRadioNickname(), below the seam, which is exactly where M4 wants it. The checker can't distinguish "produces a command" from "forwards one" inside the sink file, so the rule it actually enforces is don't add a second dispatch funnel — the same reasoning the script applies to m_wanConn in its FOREIGN_RECEIVERS note ("transport dispatch of a command some other site already produced; counting them would double-count").

Two dead ends, so you don't spend time on them:

  • Bumping BASELINE["src/models/RadioModel.cpp"] to 135 won't fly — the baseline is SHRINK ONLY, and static-checks.yml:320-326 explains why a file absent from or at its baseline must not grow.
  • this->sendCmd(...) doesn't dodge it either; this is in RADIO_MODEL_RECEIVERS.

Suggested fix — widen the existing sink instead of adding a second one

RadioModel::sendCmd() already takes an optional ResponseCallback (RadioModel.h:1548), and the existing setCommandSink lambda's sendCommand(cmd) is itself one of the counted 134. Fold the two together and the count is unchanged:

  1. FlexBackend::setCommandSink → std::function<bool(const QString&, CommandReply)>, and FlexBackend::send() calls it with {}.
  2. Drop setNicknameCommandSink / m_nicknameSink; requestRadioNickname() uses m_sink with the completion reply.
  3. In RadioModel::setupBackend(), one lambda:
    flex->setCommandSink([this](const QString& cmd, FlexBackend::CommandReply reply) {
        return sendCmd(cmd, std::move(reply)) != 0;
    });
  4. Update both setNicknameCommandSink call sites in tests/radio_nickname_test.cpp (encoding() and modelAndDialog()) to the unified sink.

I applied steps 1–3 to a scratch copy of your branch and re-ran the checker: RadioModel.cpp back to 134, total 296, exit 0. (Scratch only — I haven't pushed anything.)

If you'd rather keep the two sinks separate for clarity, that's a reasonable argument to make, but it needs a maintainer call on the checker rather than a baseline bump — the script documents its curation criterion precisely so that kind of change can be reviewed on its merits.

No reviewer or Copilot comments were attached to this run, so nothing else to fold in. Nice work on the generation-guard in RadioSetupDialog — the stale-completion case is easy to miss.


🤖 aethersdr-agent · cost: $6.1460 · model: claude-opus-5

@rfoust
rfoust marked this pull request as ready for review October 14, 2026 15:38
@rfoust
rfoust requested a review from a team as a code owner October 14, 2026 15:38
Copilot AI balanced review requested due to automatic review settings October 14, 2026 15:38
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T15:46:28.177652Z 62c3982 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62c3982b1f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/core/backends/flex/FlexBackend.cpp

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed against #6469, the project canon, and the head checkout at 62c3982. Findings are reasoned from code — I have no build, no test run and no way to drive the GUI here, so nothing below was reproduced at runtime.

1. Issue fit — Partially, and the body says so

#6469 item Diff Verdict
1. Model/label/status bar stale after an R|0-only clear or rename FlexBackend.cpp:197-211 publishes RadioDelta{nickname} on a current-session code == 0; RadioModel.cpp:11859 commits it and infoChanged carries it to the label and status bar Addressed
2. One edit sent radio clear_name twice, 24 s apart RadioSetupDialog.cpp:2123 consumes the edit and clears modified before dispatch, and :2182 adds !m_nicknamePending to the infoChanged re-seed Mechanically guarded, root cause still unknown
3. No visible acknowledgment of a commit Explicitly deferred as a cross-settings pattern Out of scope, correctly

Item 2 deserves a note rather than a demand. The issue's own hypothesis — that the infoChanged re-seed at :2168-2172 re-armed the field — can't be the mechanism: QLineEdit::setText() resets modified to false, so that path could never have made isModified() true for a second editingFinished. Something else did, and this PR doesn't find it. The body is straight about that ("Addresses #6469", "has not been reproduced or root-caused"), and the new "Return again and focus loss after an acknowledged edit send no second command" check pins the mechanical case. Leaving #6469 open is the right call; no honesty problem here.

2. Scope

File(s) Change Claimed? Verdict
IRadioBackend.h, FlexBackend.{h,cpp} NicknameCompletion + reply-carrying command sink Yes In scope. New seam surface (a defaulted param on a virtual); I grepped the head checkout — no other backend overrides requestRadioNickname, so there is no sibling implementation left behind
RadioModel.{h,cpp} Sink swapped from sendCommand() to sendCmdPublic() Partly — the body says "the existing sink now carries optional replies through sendCmdPublic" In scope, one undisclosed side effect (nit 4)
RadioSetupDialog.{h,cpp} Pending-edit latch, generation counter, disconnect reconciliation Yes In scope
tests/radio_nickname_test.cpp Reply/ordering/teardown coverage Yes In scope
docs/user/docs/radio-setup.md Documents the confirmed-update behavior Yes In scope
docs/user/src/components/LogAnalyzer/rules.json "line" fields shifted +4 / +2 Yes ("Log Analyzer line references regenerated") In scope, and verified: RadioModel.cpp:7637 is else if (key == "model") and :7646 is else if (key == "software_ver") in the head checkout, matching the new numbers

No file is unexplained by the issue, no guard or early return is removed, no CHANGELOG.md entry, no new flat AppSettings key, no new raw Flex command verb (radio name / radio clear_name already existed on this path), no capability field. I found nothing to unbundle.

3. Blockers

None.

4. Nits (all non-blocking)

1. src/core/backends/flex/FlexBackend.cpp:199 — a malformed reply code reads as success. Confirmed from the parser; flagged by chatgpt-codex-connector and independently by security-audit (CodeGuard).

            const bool accepted = current && code == 0;

CommandParser.cpp:91-95 writes msg.resultCode only when the hex parse succeeds:

            const quint32 resultCode = parts[1].toUInt(&ok, 16);
            if (ok) {
                msg.resultCode = static_cast<int>(resultCode);
            }

so R<seq> with no code field, or R<seq>|garbage|, leaves resultCode at its {0} default (CommandParser.h:22) and this gate reads it as an acknowledgment.

Why I'm calling it a nit and not a blocker, so the sizing is visible: the if (ok) gap is pre-existing and every code == 0 consumer in the tree shares it — sendCmd("info", …) at RadioModel.cpp:7620 and the client gui registration callback among them; the string published is the operator's own sanitized text, not anything the peer chooses; and an on-path party able to forge an R-line can forge a well-formed R<seq>|0| just as easily, so the parser default is no privilege gain. A conformant radio emits neither shape. The right fix is in CommandParser — a resultCodeValid flag, or a non-zero sentinel on an unparseable code — which closes it for all consumers at once rather than hardening one call site. Worth its own follow-up issue; I'd not hold this PR for it.

2. src/core/backends/flex/FlexBackend.cpp:206 — the status fence watches only one of the two nickname writers.

                && statusRevision == self->m_nicknameStatusRevision) {

m_nicknameStatusRevision is bumped in decodeRadioStatus (FlexBackend.cpp:1685-1687), but that is not the only path that sets RadioModel::m_nickname: the info reply handler at RadioModel.cpp:7634 (else if (key == "name") m_nickname = val;) writes it directly, bypassing RadioDelta and so bypassing the revision counter. An info reply landing between dispatch and our R\|0 therefore does not fence the publish. In practice our write is the newer intent so the outcome is usually right — worth a comment saying the fence covers the status path only, or routing the info nickname through the same counter.

3. src/core/backends/flex/FlexBackend.cpp:1619 — the session fence rides a method about something else.

     ++m_nicknameSession;
     // #4198: forget the amp/tuner encode handles on disconnect/reset so a stale
     // handle can't survive into a reconnect (possibly a different radio).

clearExtensionHandles() is named and documented for the #4198 amp/tuner encode handles; it now also fences nickname replies, with the bump placed above the comment that explains the rest of the body. onRadioSessionEstablished() (RadioModel.cpp:6822) is the declared per-session backend hook. A one-line comment at the bump, or a dedicated reset, would keep the next reader from deleting it as stray.

4. src/models/RadioModel.cpp:875-877 — the sink swap drops one debug line for every Flex verb.

            flex->setCommandSink([this](const QString& cmd, FlexBackend::CommandReply reply) {
                return sendCmdPublic(cmd, std::move(reply));
            });

sendCommand() logs qCDebug(lcProtocol) << "RadioModel::sendCommand:" << cmd << "connected:" … "wan:"; sendCmdPublic() does not. Both apply rejectsForeignPanWrite, so the pan-ownership guard the body claims is genuinely retained — only the log line differs, and it now goes missing for every FlexBackend-encoded command, not just the nickname. I grepped docs/, tools/ and .github/ for RadioModel::sendCommand and nothing consumes it, and RadioConnection.cpp:573 still logs TX: at lcConnection, so nothing diagnostic is actually lost. Flagging it only because it's a tree-wide behavior change the body doesn't mention.

5. tests/radio_nickname_test.cpp:341 — the last replyOrdering() assertion cannot fail.

    check(!accepted, "retained reply callback survives backend destruction without applying state");

accepted is already false from the preceding "a late reply from an ended session" check, so this passes whether the completion runs with false or never runs at all. Re-arm it so the check distinguishes the two:

    accepted = true;
    backend->requestRadioNickname(QStringLiteral("Destroyed"), [&](bool ok) { accepted = ok; });
    backend.reset();
    replies.last()(0, QString());
    check(!accepted, "retained reply callback survives backend destruction without applying state");

6. src/gui/RadioSetupDialog.cpp:2192 — guard asymmetry. The infoChanged lambda two lines up tests if (m_nicknameEdit && …); the new connectionStateChanged lambda dereferences it bare. Not reachable as a null deref — m_nicknameEdit is assigned unconditionally at :2097, before both connect calls, and never nulled — so this is consistency only.

7. src/gui/RadioSetupDialog.cpp:2097 — no setMaxLength on the nickname field (raised by security-audit), unlike the sibling edits elsewhere in this dialog. It matters a little more now than before: the R\|0 path publishes the client-sanitized string, so if the radio bounds the name server-side and still answers R\|0, the model and the status bar show the untruncated value until some later status or info refresh happens to carry the real one. That's the one Principle II edge an ack-without-echo approach can't close. Worth checking whether FlexLib's Nickname setter bounds length and mirroring it if so — I'm not asserting a number.

5. What I tried to break, and didn't

  • Teardown ordering. onDisconnected() runs expirePendingCallbacks (RadioModel.cpp:7887) before the session bump in clearExtensionHandles (:7980), so I checked whether a reply could slip through on the way down. It can't: the drain passes kNoCommandPlaneCode (0x50000063, RadioModel.cpp:76), which fails code == 0. The other synthetic drop, kProfileLoadSuppressedCommandCode (0x50000061), is likewise non-zero. The dialog then converges through infoChanged (:8146) and connectionStateChanged(false) (:8147), both after m_nickname.clear() at :8056. There is a brief window where the expiry completion re-seeds the field from the not-yet-cleared nickname, but the two later emits settle it.
  • A dispatch that returns false. WanConnection::sendCommand returns 0 without invoking the callback when m_connected is false, and the one-arg setCommandSink wrapper returns false without invoking it — in both cases the completion never fires, so I checked the dialog can't latch forever: RadioSetupDialog.cpp:2144-2147 clears m_nicknamePending on the Unsupported return. Covered.
  • Thread affinity on the retained callback. RadioConnection::commandResponse is emitted on the worker thread, but RadioModel.cpp:1408 connects it with this as the context object, so the reply — and the m_nickname* counter reads, the emit radioChanged and the QLineEdit::setText — all run on the GUI thread. WanConnection invokes inline but is a direct GUI-thread member and is never moveToThread-ed. No race on the four new counters.
  • Lifetime. QPointer<FlexBackend> is tested before every deref (:198), QPointer<RadioSetupDialog> before self->m_nicknameEdit (:2136), and the generation counter discards a completion whose edit has been superseded. replyOrdering() exercises reordered success, an intervening nickname= status, an ended session and a destroyed backend — the destroyed-backend assertion is weak (nit 5) but the guard it targets is correct.
  • Overload ambiguity on the two setCommandSink signatures. I checked all 19 call sites, including the explicitly-typed std::function<void(const QString&)> in tests/ModelControlWireSpy.h:17; every one is a one-arg or two-arg callable, none passes {}/nullptr, which would be the ambiguous case. build, check-macos and check-windows are all green on 62c3982, which confirms it on three compilers.
  • The pending latch with no timeout. A radio that stays connected and answers pings but loses this one reply leaves m_nicknamePending set, which suppresses infoChanged refreshes of the field (model and status bar still track) until the next edit or disconnect. Bounded in the usual case by the ping watchdog; I don't think it's worth a timer.
  • Sibling surfaces. Nothing else in src/ calls requestRadioNickname, and no other IRadioBackend subclass overrides it, so there is no second nickname write path left on the old behavior.
  • Scanner findings I could not confirm. All four rules.json hits are the regex/explanation text itself, untouched by this diff: :88-89 (CS-PII-DL-1, CS-VLN-SSRF-HEX) is the string 0xf3000001 in a user-facing error explanation; :744 is sm_\w+/denoiser_48k\.trtpkg; :1003 is version [\']GLIBC_2.\d+' not found. The CS-VLN-CMDI-BACKTICKhits inIRadioBackend.handFlexBackend.cppare Markdown-quoted Flex command words in comments, the known lexical false positive here. TheCS-VLN-SSRF-HEXhits atFlexBackend.cpp:1523-1566` are pre-existing handle parsing outside the diff. Dropping all of them.
  • What I could not check. No runtime: the live 4.3 clear, the 24 s duplicate, and the "R|0 with no status echo" firmware behavior all rest on the trace in #6469 and on the author's hardware run. And radio_nickname_test appears in no ctest -R filter in ci.yml, so green CI proves it compiles, not that it passes — the 4/4 is the author's local claim.

6. Recommendation

Approve with nits. The design is the right one for the reported defect: R\|0 really is the radio's confirmation, so taking it as authority is Principle II-compliant rather than optimistic, and the three fences (session, request ordering, status revision) plus the dialog's edit generation are each exercised by a test that would fail without them. Nothing I threw at it — teardown ordering, reordered replies, dropped dispatches, cross-thread replies, destroyed objects, overload ambiguity — produced a failure. Nits 1 and 5 are the two I'd most like to see land, nit 1 as its own CommandParser follow-up since it's tree-wide rather than this PR's doing. Worth saying plainly: the body's disclosure of what it did not reproduce, and its decision to leave #6469 open, is exactly the right shape for a change like this. Thanks for the thorough write-up and the wire trace.


🤖 aethersdr-agent · cost: $17.3347 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (195 finding(s))

  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6470/docs/user/src/components/LogAnalyzer/rules.json:89
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6470/docs/user/src/components/LogAnalyzer/rules.json:744
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6470/docs/user/src/components/LogAnalyzer/rules.json:1003
  • [HIGH] CS-PII-DL-1 — PII detected: drivers_license in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6470/docs/user/src/components/LogAnalyzer/rules.json:88
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:49
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:96
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:184
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:339
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:347
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:363
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:424
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:463
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:480
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:513
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:638
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:685
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:698
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:711
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:736
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:817
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:830
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:843
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:873
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:905
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:1005
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:1010
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:1094
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:1144
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/IRadioBackend.h /tmp/aetherclaude/pr-6470/src/core/backends/IRadioBackend.h:1150
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:1523
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:1558
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:1560
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:1566
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:449
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:578
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:630
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:860
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:865
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:888
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6470/src/core/backends/flex/FlexBackend.cpp:1278

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $17.5203 · model: claude-opus-5

@rfoust

rfoust commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

Additional changes and live testing are complete at signed head c5ef0abf9. The PR description has also been updated.

Agent automation bridge: added explicit invoke editText and invoke commit actions, with only radioNicknameEdit opted in. Empty text can be edited and committed through the production dialog handler. Repeated commits do not re-arm an unchanged field. Existing setText and submit behavior is preserved. The actions enforce owner opt-in, authentication, observe-only, field visibility/enabled/read-only state and validator acceptance. Deferred commits are cancelled on authorization changes, newer text or widget destruction. MCP descriptions and bridge/user docs are updated.

Verification:

  • Native ARM64 app build passed with RADE enabled; executable and CMake processors are ARM64, with no RNNoise x86 sources.
  • Five focused regression tests passed. Removing the bridge's modified-state arming failed 12 assertions; restoration passed.
  • User docs build, bridge/generated-reference checks, Log Analyzer, engine-boundary, command-plane and diff checks passed. Independent scoped review reported no actionable findings after a documentation-label correction.
  • On FLEX-6700 firmware 4.3.16.42034, bridge-only clear produced C69|radio clear_name → R69|0|; model, field, status label and discovery became empty without reconnecting, with the normal model-name fallback on the radio tab.
  • Bridge-only rename from Proof 43_Name! produced C115|radio name Proof43Name → R115|0|; model, field, status label, radio tab and discovery agreed. No nickname status echo appeared in either edit window.
  • Repeated commits, same-value edits and dialog close/reopen sent no extra nickname command: 38 seconds of clear observation and 50 seconds of rename observation before reconnect. The accepted rename survived disconnect/reconnect.
  • The original nickname was restored and confirmed. TX remained disabled; the owned instance, radio lock and temporary build alias were cleaned up.

Remaining limits: the original second clear 24 seconds after the reported single edit was not reproduced or root-caused. This run tested the fixed head, without reconstructing the old revision's interaction. Native focus/Return triggers on the locked Mac were not exercised in this 4.3 run; explicit bridge commits exercised the production handler, while native trigger coverage comes from regression tests and the earlier live 4.2 rename test. Optional shared Saved/error and screen-reader feedback remains deferred.

The unresolved reproduction gap is also recorded in the issue comment. The PR remains ready for review; build CI is still running on this head.

NF0T
NF0T previously requested changes Oct 17, 2026

@NF0T NF0T left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks, @rfoust. Taking R|0 as the confirmation is the right design for this firmware, and the write-up made it easy to check. FlexLib 4.3.16 applies the nickname at send time (Radio.cs Nickname setter) and never waits for a status. This PR goes further and waits for the radio's reply, which is more conservative than SmartSDR itself and fits Principle II ("where a command has no status echo the client may update optimistically, but the radio's subsequent status is the truth"). You also answered the earlier review cleanly. Nits 1, 2, 3, 5 and 6 from aethersdr-agent are all addressed in code, and nit 7 is answered with the FlexLib StringHelper.Sanitize reference (no length cap there; I checked).

I drove the PR head on a FLEX-8400, firmware 4.3.16.42034, through the agent automation bridge. Everything the body claims held. I also found one ordering case where the new status fence does the opposite of what it intends, and I reproduced it on the radio. That is the one thing I'm asking to change, below. The branch also conflicts with main now and needs a rebase either way.

Blocker: the status-revision fence discards an accepted rename

FlexBackend.cpp:203 skips the R|0 apply if any nickname= status arrived between dispatch and the reply. The assumption is that such a status is newer than our write. On this firmware that assumption fails in a common case.

Every full radio status from 4.3.16 carries nickname=. I don't mean a rename echo. I mean the routine S…|radio slices=… panadapters=… lineout_gain=… nickname=NF0T-MAIN callsign=NF0T … status the radio sends whenever any MultiFLEX client creates or removes a slice or panadapter. When one of those is generated just before the radio processes radio name, it reaches us after dispatch but before the R|0, and it carries the old name.

Reproduced on the 8400 with slice add and a nickname commit sent back-to-back. Verbatim wire, from the instance's aether.connection log:

[13:54:00.652] TX: "C293|slice create pan=0x40000000 freq=14.200000"
[13:54:00.653] TX: "C294|radio name RaceName"
[13:54:00.677] RX: "S76BB182D|radio slices=0 panadapters=1 … nickname=Proof6470x …"
[13:54:00.697] RX: "R293|0|1"
[13:54:00.697] RX: "R294|0|"

The radio accepted RaceName. Afterwards the app disagreed with itself:

Surface Value
Radio discovery broadcast (connect list) RaceName
Radio tab (radioTab_1025-…) RaceName
get radio → nickname Proof6470x
Status-bar label Proof6470x
Radio Setup nickname field Proof6470x. The field reverted after an accepted edit.

The last row is the important one. A field snapping back to the old name right after the operator committed a new one is exactly the sequence the triage analysis on #6469 gave for the 24-second duplicate clear: the name reappears, so the operator enters it again. The state healed only at the radio's next full radio status (here, when I removed the slice). In a quiet single-client session that may not come for a long time.

Why I think the rule is inverted, not just racy. SmartSDR processes one client's commands in order on the TCP stream, and the R for a command follows its processing. So a status that arrives before our R|0 was generated either before the radio applied our write (stale, as above) or as an echo of it (same value as ours). Either way, applying our sanitized value on R|0 is correct. A status that arrives after the R|0 is applied after it anyway, so it still wins without any fence. That includes another client renaming the radio right after us: their command is processed after ours, so its status follows our reply. I couldn't construct an ordering where the fence helps, and I could make one where it hurts.

If that holds up, the fix makes the code simpler. Drop m_nicknameStatusRevision and its check, and keep the request-ordering and session fences, which are both sound. replyOrdering()'s "nickname status after dispatch supersedes the command reply" then flips: the sanitized value should win. Ideally add a case that mirrors the wire above, with a status carrying the old name between dispatch and R|0. Question for you: did you see a radio emit a nickname status after processing the write but before its R? If there's firmware that does, I'd like to understand it before suggesting the fence go.

Must-do before merge: rebase

mergeStateStatus is DIRTY. git merge-tree against current main conflicts in src/core/backends/flex/FlexBackend.h, src/models/RadioModel.cpp and docs/user/src/components/LogAnalyzer/rules.json. The rules.json line numbers are generated, so please regenerate them after the rebase rather than hand-merging. The green CI is from 2026-10-14, against a base 9 commits behind main.

What I tried to break, and what held

Live on the FLEX-8400 / 4.3.16.42034. Isolated offscreen instance, its own settings dir, AETHER_AUTOMATION_NO_TX=1, transmitting: false throughout. All edits went through your new editText / commit verbs on radioNicknameEdit.

Attack Wire Result
commit on an untouched field none ✅ No write.
Clear (editText "", commit) C131|radio clear_name → R131|0| (44 ms) ✅ Model, field and discovery all "" with no reconnect. A tree search found no widget still showing the old name.
Repeat commit; same-value editText "" + commit; dialog close and reopen none ✅ Still exactly one command.
Sanitized rename Proof 6470_x! C209|radio name Proof6470x → R209|0| (97 ms) ✅ Model, field, status label and radio tab all Proof6470x. No nickname status echo followed.
Rename racing a slice add see blocker ❌ Split state until the next full radio status.
Disconnect and reconnect (new handle 1024bae7) ✅ Rename persisted on the radio.
Restore NF0T-MAIN C472|radio name NF0T-MAIN ✅ Confirmed in the model and discovery.

Four edits produced four commands over the whole session, so "one edit sends one command" holds on 4.3.

Locally (Windows, MSVC, Qt 6.12.0, PR head c5ef0abf9):

  • radio_nickname_test, automation_boundary_widgets_test, model_control_routing_test, backend_receive_contract_test and aetherd_amp_tuner_encode_test all pass. So do 46 neighbouring tests: the DVK / GUI-client registration / CWX consumers of CommandParser, radio_setup_*, and automation_*. automation_sensitive_line_edit_test and automation_sensitive_grab_command_test fail identically on a base build, so they are environmental and not from this PR.
  • Mutations, each restored afterwards:
    • Parser default put back to 0: all 9 malformed-reply cases fail.
    • !m_nicknamePending removed from the infoChanged refresh: "unrelated identity refresh cannot replace a pending clear" fails.
    • The R|0 radioChanged emit removed: 6 assertions fail.
      The tests pin what they claim.
  • Step 0: both test files drive AutomationServer in-process. No sockets.

Read and checked:

  • The sendCommand → sendCmdPublic sink swap keeps rejectsForeignPanWrite.
  • A synchronous failure callback from sendCmd's profile-load or no-command-plane drops runs the completion and then the Unsupported branch. That is harmless.
  • QPointer guards sit on both the backend and the dialog.
  • The deferInvokeAction commit rechecks the opt-in, visibility, enabled and read-only state, observe-only, and the authorization epoch before it emits.
  • No #5554 prohibited pattern. The dynamic_cast<FlexBackend*> in the diff is pre-existing context, and the command-plane ratchet is unchanged.

Scope

Files Claimed Verdict
IRadioBackend.h, FlexBackend.{h,cpp}, RadioModel.{h,cpp} yes In scope.
CommandParser.{h,cpp} yes (commit 71bd7ab) In scope, but tree-wide (see guidance 1).
RadioSetupDialog.{h,cpp} yes In scope.
AutomationServer.cpp, tools/aether_mcp.py, both bridge docs yes Bundled but justified. ten9876 asked for exactly this commit path in #6451, and the live proof depends on it. New bridge surface, so see guidance 3.
docs/user/docs/radio-setup.md yes In scope. Satisfies the user-docs rule.
rules.json yes Generated. Regenerate after the rebase.
tests/* yes In scope.

Preference vs fix: this is a fix. Every behavior change traces to #6469's acceptance criteria or to #6451. No default, layout or workflow changes. No CHANGELOG.md entry, correctly. All four commits are signed and verified. CODEOWNERS: everything here is @aethersdr/reviewers.

Forward guidance (non-blocking)

  1. The parser change affects every Flex reply, not just nickname. A malformed or missing result code now reaches every callback as -1 (logged as ffffffff) instead of 0. That fails closed, and it's the parser-level fix the earlier review asked for, so I'm glad it's here. Two notes. FlexLib's ParseReply drops such a reply outright, so its callback never fires, whereas ours fires a failure. And GUI-client registration treats any non-zero code as DisconnectAndWaitForUser. A conformant radio never sends these shapes, so it's fine, but please give it a sentence in the body so a maintainer sees it isn't nickname-only.
  2. The sink swap drops the lcProtocol RadioModel::sendCommand: debug line for every FlexBackend verb. That was aethersdr-agent's nit 4, still unmentioned. RadioConnection still logs TX:, so nothing is really lost. One line in the body would do.
  3. Bridge verbs. ten9876's #6451 proposal was a single commitEdit for "most settings fields". This PR ships editText + commit behind a per-widget opt-in, used by one field. I think the opt-in is the safer shape. Since third-party agents will bind to these names, it's worth a nod from him on the naming before more fields opt in.

Once the fence question is settled and the branch is rebased, I expect this to be an approval. The squash subject I'd use: fix(flex): apply radio-confirmed nickname clears and renames without a status echo (#6470). Principle II.

Really nice work, especially the mutation testing and the plain statement of what you did not reproduce.

73, Ryan NF0T


👨🏼‍💻 Co-authored by Claude Opus 5.5

Comment thread src/core/backends/flex/FlexBackend.cpp Outdated
Comment thread tests/radio_nickname_test.cpp Outdated
@jensenpat jensenpat self-assigned this Oct 17, 2026
@jensenpat
jensenpat force-pushed the codex/fix-6469-nickname-confirmation branch from c5ef0ab to 2d3a214 Compare October 17, 2026 21:50
@jensenpat
jensenpat dismissed NF0T’s stale review October 17, 2026 21:51

Fixed in 2d3a214: removed the nickname status-revision fence, retained request/session/lifetime guards, added old-name full-status-before-R|0 rename and clear regressions plus a later-status authority case. The old fence fails those checks; radio_nickname_test and all six focused tests pass on the rebased head. Rebased onto bccd4b7 and regenerated Log Analyzer references; merge-tree is conflict-free. No live-hardware retest is claimed.

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved at 2d3a2147361d47952a59e523b86cf5a75cef3844 after a manual adversarial review; the automated code-review skill is unavailable in this session.

  • Fixed NF0T's reproduced status-before-reply race in 2d3a2147361d47952a59e523b86cf5a75cef3844: an accepted rename/clear now supersedes the earlier full status carrying the old name. Request ordering, session reset and destroyed-backend guards remain; a later nickname status still wins.
  • Updated replyOrdering() and the production-dialog test for that race. The old fence fails the new ordering assertions; the fixed test passes. Removed a duplicate fixture identity connection because main now wires injected backend state itself, retaining the exactly-one identity-notification check.
  • Rebased onto bccd4b719, preserving upstream answered-transmit and setup sinks, regenerated Log Analyzer references, and checked the merge result. All five commits are GitHub-verified.
  • Current-head evidence: Qt 6.12 ARM64 app build with matching QtKeychain and RADE; 6/6 headless socket-free tests (radio_nickname_test, automation_boundary_widgets_test, model_control_routing_test, backend_receive_contract_test, aetherd_amp_tuner_encode_test, radio_setup_control_routing_test). Isolated authenticated DEMO-0001 bridge exercised nickname edit, repeated commit and empty commit on the production field, identified this clean head, and reported TX disabled / not transmitting; owned PID 35975 was stopped.
  • Boundary/command-plane/capability/touchpoint findings are identical to main, with no baseline growth. Bridge/generated-reference checks, 707 Log Analyzer checks and diff checks pass. Hosted Docs (site/PDF) and Static checks pass. Platform CI is still running and remains a merge gate.

The prior parser finding is fixed at the shared boundary (invalid codes fail with -1 for every Flex reply consumer). The PR body now discloses that scope and the command-path logging behavior. The early command-plane finding is retired by the unified existing sink, not an exemption; the scanner's backtick/hex hits are comment/regex literals or unchanged handle parsing, with no executable command or URL sink added. All review threads have evidence replies and are resolved.

No new live-radio run: the ordering regression is grounded in NF0T's FLEX-8400 wire trace. The original 24-second duplicate cause remains unproven and #6469 stays open. No remaining actionable findings.

@jensenpat
jensenpat enabled auto-merge (squash) October 17, 2026 21:55
@jensenpat
jensenpat merged commit 52b8cac into aethersdr:main Oct 17, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants