Skip to content

Fix per-model DAX IQ rate limits and safe restoration - #6492

Merged
jensenpat merged 4 commits into
aethersdr:mainfrom
rfoust:codex/fix-6437-dax-iq-rates
Oct 18, 2026
Merged

jensenpat merged 4 commits into
aethersdr:mainfrom
rfoust:codex/fix-6437-dax-iq-rates

Conversation

@rfoust

@rfoust rfoust commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #6486. Implements the per-model DAX IQ rate-table item from #6437; the parent checklist remains open.

FLEX-6400M offered 192 kHz despite supporting only 24/48/96 kHz, and the request model accepted arbitrary rates. The existing model table and Flex capability payload now publish supported rates. Production requests, stream recreation, profile recovery, and TCI consumers enforce those limits.

Unsupported choices remain visible and disabled with accessible reasons. A saved 192 kHz preference uses 96 kHz on a limited model without overwriting the preference, then returns to 192 kHz on a capable model. Reconnect/model replacement cancel stale restore timers and seed confirmed stream state. TCI retains valid negotiation received before model identification, issues no IQ commands while unidentified, and reconciles the preference when capabilities arrive. Known-model unsupported SETs preserve the current preference. ThemeManager tracks the On/Off stylesheet through availability transitions.

The rate table follows the official published FlexLib v4.2.20 source, FlexLib/ModelInfo.cs. Unknown models have an empty rate list. The existing case-insensitive and suffixed-model matching contract is preserved. docs/architecture/radio-capabilities-map.md records the capability and its consumers; user DAX IQ and TCI pages describe the behavior.

Principle I — FlexLib is the protocol authority. Radio-reported stream state remains authoritative.

Validation

  • Rebased onto main 1dd498a76; the generated Log Analyzer references were regenerated to resolve the conflict. The source fixes were preserved unchanged.
  • Landing pass: native macOS build of model_capabilities_test, dax_iq_rate_limits_test, control_availability_registry_test, and tci_server_review_test. Three socket-free CTests passed. The broader TCI target was built but not executed because its current suite includes socket tests.
  • Landing pass: compared all 19 model rows with the local official FlexLib v4.2.18 reference. Engine-boundary, touchpoint-manifest, capability-record, and command-plane gates passed on base and head with no growth. Test registration, frozen CI gate, QSettings, accessibility, color, generated references, and Log Analyzer checks passed.
  • Existing author evidence: authenticated, TX-disabled automation bridge exercised the production applet/model with injected capabilities and no radio transport; mutations caught rate-table, request, restore, early-TCI, and button-style regressions. The Windows reviewer independently reproduced both earlier blockers with mutations and confirmed the clean baseline. Those sessions were not rerun in this landing pass.
  • Hosted Linux, macOS, Windows, Static checks, sanitizer configuration, and Docs checks passed on final head 4b815a608. No local docs-site or PDF rebuild in the landing pass.

The default DAX regression test is socket-free. Opt-in dax_iq_rate_limits_test --bridge-proof binds the production automation bridge's owner-only local socket, requires a non-empty token, and pins TX off. It was not executed in the landing pass.

Existing bridge artifacts: request/response evidence, limited-model applet, disabled 192k choice, capable-model applet. Assets reside on a separate fork branch.

Live radio, RF/TX, and live profile-load behavior remain unverified. The MainWindow profile recovery order was source-reviewed; the production model sequence is exercised by the DAX regression. A separate automated code-review tool was unavailable; the landing review was manual.

Generated with OpenAI Codex (GPT-6 Astra)

@rfoust
rfoust marked this pull request as ready for review October 14, 2026 23:44
@rfoust
rfoust requested a review from a team as a code owner October 14, 2026 23:44
Copilot AI balanced review requested due to automatic review settings October 14, 2026 23:44

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.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@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 ⚠️ Failed 2026-10-14T23:52:43.837916Z 02e75de 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.

@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.

Issue fit

Partially. The capability table, the Flex publisher, the applet's disabled-with-reason choices, the setSampleRate/createStream guards, the profile-recovery ordering and the "fallback does not overwrite the saved preference" behaviour all land as #6486 asks, and the test coverage for them is unusually thorough. I verified the Principle I claim directly against the local FlexLib reference (~/reference/FlexLib/FlexLib/ModelInfo.cs, v4.2.18 — the PR cites 4.2.20, but the field and every value match): DaxIqSampleRates is {24000,48000,96000} for exactly FLEX-6300, FLEX-6400, FLEX-6400M, FLEX-8400, FLEX-8400M, AU-510 and AU-510M; {24000,48000,96000,192000} elsewhere including RT-2122; ImmutableList<int>.Empty on DEFAULT. The table in ModelCapabilities.cpp reproduces that exactly, and the struct's new field sits in the position the positional kTable initializers use.

Two defects below, one of which is a protocol-side regression against the issue's own "Guard production rate requests … including non-GUI consumers" line — it guards them a little too hard.

Scope

File / group What it changes Claimed? Verdict
ModelCapabilities.{h,cpp} maxDaxIqRateHz + daxIqSampleRates() from FlexLib Yes In scope
RadioCapabilities.h, FlexBackend.cpp, RadioModel.cpp Publish/consume the rate list; setSampleRateProvider Yes In scope — new capability surface, see note
DaxIqModel.{h,cpp} Provider, supportsSampleRate/sampleRateForPreference/requestedSampleRate, request guards Yes In scope
DaxIqApplet.{h,cpp} Disabled choices, availability registry, restore timer, syncStreamState Yes In scope
TciServer.{h,cpp} Rate admission + effective-rate announce/reconcile Yes In scope
MainWindow_Wiring.cpp Profile-load recovery applies the supported rate before create Yes In scope
tests/*, tests.cmake Socket-free regression target + opt-in bridge proof Yes In scope
docs/user/docs/{dax-iq-streaming,tci-server}.md Limits documented Yes In scope
docs/user/src/components/LogAnalyzer/rules.json Line-number refresh only (+5 in RadioModel.cpp, +46 in DaxIqModel.cpp) Yes In scope — mechanical, consistent with the diff

Everything in the diff is explained by #6486. No CHANGELOG entry, no new flat AppSettings keys (confirmed — only existing DaxIqRate%1/DaxIqEnabled%1 are read/written), no preference changes smuggled in: every UI change is "show the unsupported rate, disabled, with a reason", which the issue asks for in those words.

One note rather than a finding: RadioCapabilities::daxIqSampleRates is new public capability surface. It's QList<int>, so per the bools-only capability ratchet it needs no FROZEN entry, and the issue explicitly asks for "the existing model/backend capability design". Flagging it only so the maintainer sees it consciously.

Blockers

1. src/core/TciServer.cpp:1485-1488 — an unknown model silently discards a TCI client's rate request instead of deferring it

            const bool supported = ok
                && (rate == 24000 || rate == 48000
                    || rate == 96000 || rate == 192000)
                && (!m_model || m_model->daxIqModel().supportsSampleRate(rate));
            if (!supported) {
                replyText(ws, QStringLiteral("iq_samplerate:%1;")
                                  .arg(achievedIqSampleRate()));
                continue;
            }

supportsSampleRate() is false for every rate while daxIqSampleRates is empty, and empty is the state for an unknown or not-yet-reported model — FlexBackend::capabilities() builds the list from capabilitiesFor(m_modelProvider()), and m_modelProvider is [this]{ return m_model; } (RadioModel.cpp:884), which is empty until the radio's model= status arrives. The TCI server also runs whenever the TCI applet is enabled, radio or not.

Failure scenario: SDC or CW Skimmer is already connected to the TCI port (the documented topology — the server outlives radio connections). The operator starts AetherSDR, or connects the radio by IP. The skimmer negotiates iq_samplerate:96000; once, at its own connect, before model= lands. Today that request is whitelisted and stored in m_iqSampleRate, and applied when the streams come up. After this change it is refused, m_iqSampleRate stays at the 48000 default, and nothing recovers it: when the model does resolve, announceEffectiveIqSampleRate() computes sampleRateForPreference(48000) == 48000 == m_announcedIqSampleRate and returns early, so there is no re-announce and no cue for the skimmer to retry. The skimmer runs the whole session at 48 kHz on a radio that supports 192.

The PR documents the behaviour ("Until a supported model is identified … TCI rate requests follow the same model limits"), but documenting it doesn't stop it being a regression for a non-GUI consumer that only asks once. The applet path doesn't have this problem because it re-arms on capabilitiesChanged; only the TCI SET is lossy.

An empty list means unknown, not nothing is allowed — and the rest of the machinery you added already clamps correctly at apply time (ensureIqStream() bails on rate == 0, effectiveIqSampleRate() downgrades). So store the whitelisted preference and let it clamp:

            const QList<int> modelRates =
                m_model ? m_model->daxIqModel().supportedSampleRates() : QList<int>{};
            // An empty list is "model not identified yet", not "no rate is
            // legal": a skimmer negotiates its rate once, at connect, which can
            // precede the radio's `model=` status. Refusing then would strand it
            // at 48k for the session. Keep the preference; effectiveIqSampleRate()
            // clamps it the moment the model is known.
            const bool supported = ok
                && (rate == 24000 || rate == 48000
                    || rate == 96000 || rate == 192000)
                && (modelRates.isEmpty() || modelRates.contains(rate));

A known-but-incapable model (the FLEX-6400 case the PR is actually about) still refuses, which is the behaviour the test asserts.

2. src/gui/DaxIqApplet.cpp:268 + :296 — the On/Off pill's own styling is immediately reverted by the new availability registry

    m_iqEnable[idx]->setText(exists ? "On" : "Off");
    m_iqEnable[idx]->setStyleSheet(exists ? kIqBtnOn : kIqBtnOff);
    ...
    if (m_model && m_availability) {
        m_availability->refreshEngaged();
    }

refreshEngaged() → ControlAvailabilityRegistry::applyOne → ThemeManager::setWidgetForegroundToken(w, …) (ThemeManager.cpp:1632-1653), which ends in applyStyleSheet(widget, it->stylesheetTemplate) — the template cached when the registry first tracked the widget, not the sheet the widget has now. Unlike eventFilter and the theme-switch path, which both guard with w->styleSheet() == it.value().appliedStylesheet ("somebody set their own sheet afterwards"; ThemeManager.h:524-529 names "TX indicator state" as exactly this case), setWidgetForegroundToken has no such guard.

Because setWidgetForegroundToken early-returns when the token is unchanged, the clobber lands precisely on the transitions where the engaged state flips — which is exactly when syncStreamState just changed the stylesheet:

  • Registered while the channel is Off (template = kIqBtnOff). Stream comes up → setStyleSheet(kIqBtnOn) → refreshEngaged() → Inactive→Active, token "color.control.inactive" → "" → re-pushes kIqBtnOff. Button reads "On" in the Off colours.
  • Registered while the channel is already On (the late-binding path your own test exercises, template = kIqBtnOn). Turning it off → setStyleSheet(kIqBtnOff) → Active→Inactive → re-pushes kIqBtnOn. Button reads "Off" in the On colours, and that template never updates.

This is reasoned from the code, not observed running — but it is the first time this applet has had a ControlAvailabilityRegistry, so it is introduced here. tests/dax_iq_rate_limits_test.cpp:318 and :331 assert text() == "On" / "Off" and the accessible description, never styleSheet(), so the suite passes straight through it.

The fix is to stop fighting over the sheet: route the state styling through the theme manager so the cached template is the current one (also the house rule for setStyleSheet escapes):

    m_iqEnable[idx]->setText(exists ? "On" : "Off");
    ThemeManager::instance().applyStyleSheet(m_iqEnable[idx], exists ? kIqBtnOn : kIqBtnOff);

Please add an assertion on the rendered sheet (or on stateOf() plus styleSheet()) so this can't silently come back.

Nits (non-blocking)

  • src/models/DaxIqModel.cpp:160 — sampleRateForPreference()'s final fallback is rates.contains(48000) ? 48000 : 0. Harmless today (every non-empty list is a prefix of {24,48,96,192}), but a future list without 48000, or a 96k preference on a 24/48-only model, lands on a rate nobody asked for rather than the highest supported one ≤ the preference. std::upper_bound over the sorted list would be the honest shape.
  • src/gui/DaxIqApplet.cpp:313-342 — refreshRateChoices() writes combo->setAccessibleDescription() directly while ControlAvailabilityRegistry::applyText also owns that channel through its aetherAvailabilityDescription hold property. The hold mechanism is designed to tolerate a second writer, but the resulting text is now ordering-dependent between refreshRateChoices() and the next refreshEngaged(). Worth a comment at least.
  • src/gui/DaxIqApplet.cpp:313-342 — reasons is accumulated only on ch == 0 and then applied to all four combos. Correct (the list is per-radio, not per-channel) but it reads like a loop-variable bug; hoist it out of the channel loop.
  • src/core/TciServer.cpp:1494 — on a successful SET the reply echoes rate, the preference, while achievedIqSampleRate() is used everywhere else. Consistent today only because the SET is refused whenever the two could differ; if Blocker 1 is fixed as suggested, echo the effective rate here too.
  • tests/dax_iq_rate_limits_test.cpp:88 — ok = ok && server.m_iqSampleRate == 192000 after the rejected 192 kHz SET is vacuous: m_iqSampleRate was already 192000 from the accepted SET and the reject path never writes it, so that clause passes even with the whole reconcile path deleted. The sent.last() == "iq_samplerate:96000;" clause next to it is the one doing the work.
  • tests/dax_iq_rate_limits_test.cpp — spin(1600) against a 1500 ms QTimer is 100 ms of slack; that will flake under TSan/ASan. 2500 ms costs nothing here.
  • tests/dax_iq_rate_limits_test.cpp:281,284 — --capture writes fixed /private/tmp/dax-iq-*.png paths in a shared directory. Opt-in and harmless, but QTemporaryDir is one line.
  • From the security audit (CodeGuard): tests/dax_iq_rate_limits_test.cpp:197 — bridge.setAuthToken(qEnvironmentVariable("AETHER_MCP_TOKEN")). If that variable is unset the token is empty and AutomationServer's gate (if (!m_authToken.isEmpty() && cmd != "ping")) is skipped, so --bridge-proof runs the bridge unauthenticated for its 60 s window. Mitigated by the owner-only socket, AETHER_AUTOMATION_NO_TX=1 and setTxAllowed(false), so not a finding — but failing fast on an empty token would be better than silently dropping auth.

Socket test disclosure

tests/dax_iq_rate_limits_test.cpp adds a socket path: under the opt-in --bridge-proof argument it starts the production AutomationServer on the owner-only local socket named by AETHER_AUTOMATION_SOCKET (QLocalServer; no radio transport, no firmware peer, TX pinned off). The default run is socket-free, tests.cmake:6493-6504 registers dax_iq_rate_limits_test with no arguments so CI never reaches that branch, and the tests.cmake comment names the socket. The PR body discloses it. Recording per the socket-test rule; not a finding.

What I tried to break

  • The capability table itself. Checked row-by-row against the local FlexLib ModelInfo.cs (v4.2.18) rather than the PR's citation: the 96 kHz set, the 192 kHz set, RT-2122 and the empty DEFAULT all match, and the new maxDaxIqRateHz field's position in the struct matches the positional kTable initializers, so no row is silently off by one.
  • Other backends losing DAX IQ. daxIqSampleRates is set only by FlexBackend, and hasDaxStreams is false on HL2, Icom and Sim — so the new "empty list ⇒ no streams" rule cannot regress a non-Flex or simulator path.
  • Double-create on restore. The deleted m_restorePending comment named a real symptom; the single QTimer + isActive() guard is strictly stronger (one timer object cannot double-schedule), and restoreChannelsNow() keeps the stream(...).exists skip. I also checked the new capabilitiesChanged → restoreEnabledChannels() edge: publishCapabilities has four callers and fires on capability revisions, not per status line, so it cannot become a 1.5 s create loop.
  • DaxIqApplet lifetime. ~DaxIqApplet() = default; is out-of-line in the TU that includes ControlAvailabilityRegistry.h, so the unique_ptr to the forward-declared type is fine; the registry being both unique_ptr-owned and QObject-parented to this is redundant but not a double free (~QObject deparents). m_model is a QPointer and every lambda re-checks m_model != model, so a stale connection from a replaced model is inert.
  • MainWindow_Wiring.cpp:3150-3165 ordering. setSampleRate records m_desiredRate before its exists/streamId early return, and the subsequent applyStreamStatus(isNew) reapply uses sampleRateForPreference(m_desiredRate) — so dropping the 600 ms delayed setSampleRate genuinely does restore at the right rate without racing a later session.
  • Injection through the new rate path. The TCI value goes through toInt() and a closed whitelist; both QString::arg substitutions in the emitted stream set 0x%1 daxiq_rate=%2 are integers, so no client text reaches a Flex command string. Confirmed independently by the security-audit (CodeGuard) pass, which also refuted every CS-VLN-CMDI-BACKTICK hit as Markdown-quoted wire literals in comments, the SSRF-LOCALHOST/PII-IP hits as loopback-normalisation comments, and the PATH-UNC/PII hits as JSON-escaped regex bodies in rules.json. No confirmed security finding.
  • What I could not check: nothing was built or run — every finding above is reasoned from the code in /tmp/aetherclaude/pr-6492. The PR's bridge-proof screenshots and the "live 96 kHz radio validation" box are unverifiable from here, and CI being green covers only the -R-filtered subset.

Recommendation

Request changes — on Blocker 1 primarily. The capability work underneath is accurate and well tested (the mutation list and the replacement/reconnect cases are better than most PRs in this area get), but the TCI admission check turns "model not known yet" into a permanent silent downgrade for exactly the consumer class the issue names, and the applet's On/Off pill loses its colour on every state flip. Both are small, local fixes to code already in the diff. Concrete next step: apply the two suggested edits, add an assertion on the enable button's rendered stylesheet across one On→Off→On cycle, and loosen the spin(1600) slack.

Thanks for the FlexLib citation with line-level detail — being able to diff your table against the reference row-by-row is what made the Principle I half of this review quick and conclusive.


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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

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

  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6492/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-6492/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-6492/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-6492/docs/user/src/components/LogAnalyzer/rules.json:88
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:113
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:114
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:1098
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:64
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:265
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:276
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:752
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:910
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:1538
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:1681
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:1812
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:1852
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:1905
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:2593
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:3020
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:3061
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:3090
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:3271
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:3854
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:3931
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:113
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:114
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:991
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/TciServer.cpp /tmp/aetherclaude/pr-6492/src/core/TciServer.cpp:1098
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.h /tmp/aetherclaude/pr-6492/src/core/TciServer.h:92
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.h /tmp/aetherclaude/pr-6492/src/core/TciServer.h:183
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.h /tmp/aetherclaude/pr-6492/src/core/TciServer.h:333
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/TciServer.h /tmp/aetherclaude/pr-6492/src/core/TciServer.h:388
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6492/src/core/backends/RadioCapabilities.h:108
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6492/src/core/backends/RadioCapabilities.h:255
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6492/src/core/backends/RadioCapabilities.h:390
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6492/src/core/backends/RadioCapabilities.h:525
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6492/src/core/backends/RadioCapabilities.h:548
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6492/src/core/backends/RadioCapabilities.h:611
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6492/src/core/backends/RadioCapabilities.h:648
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6492/src/core/backends/RadioCapabilities.h:711

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: $13.8265 · model: claude-opus-5

@rfoust

rfoust commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review in signed commit a630422.

Both blockers reproduced and fixed.

  • TCI now remembers whitelisted 96/192 kHz negotiation received before model identification, without issuing any IQ command or creating a stream while the rate list is empty. Capability publication applies the supported effective rate, announces a 192→96 kHz fallback on FLEX-6400M, and restores 192 kHz on FLEX-6700. Known-model unsupported SETs still leave the preference unchanged. Replies and init-burst state use the effective rate when available; before identification they echo the deferred preference.
  • Every IQ button state update now uses ThemeManager::applyStyleSheet, keeping its tracked template current. The regression test checks the rendered background through late binding and On→Off→On. The Off→On assertion failed before the fix and now passes.

Nits addressed: shared unavailable-rate reasons are calculated outside the channel loop; rate descriptions are supplied before a registry refresh so per-rate reasons and active/inactive announcements compose consistently; the rejected-SET test now starts from a different stored preference; restore tests wait for the expected signal with a 2.5 s deadline (negative checks also allow 2.5 s); optional captures use a unique QTemporaryDir; and --bridge-proof fails before opening its socket if AETHER_MCP_TOKEN is empty.

I left the generic upper_bound fallback suggestion out. The current FlexLib lists are exactly 24/48/96 and 24/48/96/192 kHz, both containing 48 kHz; the explicit saved-192→96 fallback implements this issue without changing behavior for malformed saved preferences or hypothetical future capability lists.

Verification: the two new regression scenarios failed before the fixes and pass afterward. Native ARM64 macOS application build with RADE enabled, all four focused CTests, docs build, architecture/capability/test/theme/accessibility checks, missing-token refusal, and the authenticated TX-disabled agent automation bridge proof passed. The default test remains socket-free. No radio transport or RF transmission was used; live radio/profile-load validation remains unperformed. CI will rerun on this commit; the preceding commit had all required checks green.

CodeGuard: I inspected the 40 individual alerts shown in the advisory comment. The backticks and loopback addresses are in C++ comments; F3000001/0xf3000001 are documented protocol error-code examples; the UNC/GLIBC hits are JSON regex text. Those cited locations do not establish command execution, SSRF, a path operation, or personal data exposure. The new rate path parses an integer and admits a closed whitelist before any command formatting. I found no security fix needed for the listed alerts. The comment reports 154 total but exposes only 40; please attach the full report if any of the unlisted alerts need separate triage.

The PR remains ready for review.

@nigelfenton nigelfenton 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 at the request of the maintainers. Both blockers from the earlier review are properly fixed rather than demoted — I checked the current head (a630422e) and proved each fix with a mutation rather than reading the diff and believing it.

Everything below was built and run on Windows/MSVC (Qt 6.12.0, RelWithDebInfo) in a scratch worktree at the PR head.

Issue fit

Yes. I verified the Principle I claim independently: all 19 rows of the rate table match FlexLib's DaxIqSampleRates, and the case-insensitive prefix contract resolves correctly — AU-510 catches AU-510M, AU-520 catches AU-520M, and AU-510 does not swallow AU-520. Deriving the list from maxDaxIqRateHz rather than hand-listing four values per row is the right call; it makes a wrong row impossible to express as an inconsistent set.

Scope

File / group Claimed by the body? Verdict
ModelCapabilities.{h,cpp} Yes In scope; table verified against FlexLib
RadioCapabilities.h, FlexBackend.cpp, RadioModel.cpp Yes In scope — new capability surface, see nit 1
DaxIqModel.{h,cpp}, DaxIqApplet.{h,cpp} Yes In scope
TciServer.{h,cpp} Yes In scope
MainWindow_Wiring.cpp Yes In scope
tests/*, tests.cmake Yes In scope; preflight cleared (below)
docs/user/docs/*, rules.json Yes In scope; rules.json is mechanical line-number refresh

No CHANGELOG.md entry, no new flat settings keys, no smuggled preference — every UI change is "show the unsupported rate, disabled, with a reason", which #6486 asks for in those words.

The one thing blocking merge

CONFLICTING on docs/user/src/components/LogAnalyzer/rules.json — the only conflicting file; everything else auto-merges cleanly. Worth mentioning once rather than twice: #6491 conflicts on this same generated file. If you are rebasing both, that is one regeneration each, and whichever lands second will need it again.

Two non-blocking observations

1. daxIqSampleRates is not recorded in docs/architecture/radio-capabilities-map.md. See the inline comment — the header's own ADDING A FIELD contract asks for it, and the sibling hasDaxStreams is recorded there at line 239.

2. The TCI fix is pinned, but not by the TCI test. When I reverted the blocker-1 fix, tci_server_review_test passed unchanged — the catch came from dax_iq_rate_limits_test ("TCI defers pre-identification rates without commands, then clamps and restores intent"). Coverage exists and works, so this is not a gap. It is only that the assertion guarding TciServer.cpp lives in the DAX target, so a future TCI-only change could move that line with its own test still green. Entirely your call whether that is worth a mirrored assertion.

What I tried to break

  • Reverted the blocker-1 fix (removed modelRates.isEmpty() ||, restoring "unknown model refuses every rate"): caught, 1 failure. The skimmer-stranded-at-48k regression is genuinely pinned, not just described.
  • Reverted the blocker-2 fix (raw setStyleSheet back at the On/Off pill): caught, 1 failure — "Off to On updates the rendered background after availability refresh".
  • Clean baseline after restoring both: dax_iq_rate_limits_test 51 assertions / 0 failures; model_capabilities_test all OK; both exit 0 with real stdout.
  • Traced the zero-rate path. sampleRateForPreference() returns 0 for an unknown model, so I checked every consumer rather than assuming: ensureIqStream() bails on rate == 0, achievedIqSampleRate() falls back to m_iqSampleRate, announceEffectiveIqSampleRate() returns early. Every known model includes 48000, so the 0 is reachable only in the unknown case, which is handled throughout.
  • Re-walked the skimmer scenario end to end. capabilitiesChanged fires announceEffectiveIqSampleRate() and reconcileIqStreams() once model= lands, so a preference retained before identification is reconciled rather than stranded. The earlier failure path is closed.
  • Preflight cleared. The socket path is behind an explicit --bridge-proof argument and a non-empty AETHER_MCP_TOKEN, and add_test passes no arguments, so the default CTest run is socket-free. I did not exercise the bridge-proof mode.
  • Not verified: the live profile-load path (source-reviewed per your own note, not exercised through a real load), and the bridge screenshots, which are on a fork branch outside this diff.

Recommendation: rebase to clear the rules.json conflict, and consider folding the map-doc row into the same push. Good piece of work — the mutation evidence in your test plan held up when I re-ran it independently.

73, Nigel G0JKN

Comment thread src/core/backends/RadioCapabilities.h
Comment thread src/core/TciServer.cpp
@jensenpat jensenpat self-assigned this Oct 18, 2026
@jensenpat
jensenpat force-pushed the codex/fix-6437-dax-iq-rates branch from a630422 to 81d56bc Compare October 18, 2026 01:02
jensenpat
jensenpat previously approved these changes Oct 18, 2026

@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.

Reviewed head 81d56bc after the requested rebase onto main 5b43981. No remaining blocking findings.

  • Added the missing daxIqSampleRates capability-map contract in 81d56bc (docs/architecture/radio-capabilities-map.md:265); replied to and resolved both review threads with evidence.
  • Regenerated the conflicted Log Analyzer references. git range-diff confirms the source fixes survived the rebase unchanged.
  • Independently matched all 19 model-rate rows and unknown-model behavior against the local official FlexLib reference. Reviewed request validation, pending TCI negotiation, supported-rate reapplication, model replacement, restore timer cancellation, radio-confirmed stream state, and ThemeManager ownership.
  • Native macOS build passed for model_capabilities_test, dax_iq_rate_limits_test, control_availability_registry_test, and tci_server_review_test. Three selected socket-free CTests passed (0 failures, 12.74 s). The DAX target exercises the early TCI and On/Off styling regressions. Reused the author's and Windows reviewer's mutation/bridge evidence; did not claim those sessions were rerun here.
  • Base/head engine-boundary, manifest, capability-record and command-plane gates passed with no growth; test registration, frozen CI gate, QSettings, accessibility, color, generated reference and 707 Log Analyzer checks passed.

Limits: the broader TCI target was not run because its current suite includes socket tests. No fresh full desktop application build, optional bridge socket run, live profile load, radio or RF/TX testing in this pass. The MainWindow recovery order was source-reviewed. Automated code-review tooling was unavailable; this was a manual review. Hosted CI and Docs are still running and must pass before landing; documentation was not rebuilt locally.

rfoust and others added 4 commits October 17, 2026 18:23
Apply the published FlexLib model table to DAX IQ requests, restores,
and TCI subscriptions. Preserve valid 192 kHz preferences on capable
models and explain unsupported choices in the applet.

Fixes aethersdr#6486
Refs aethersdr#6437

Generated with OpenAI Codex.
Seed confirmed stream state and clear meter levels when the applet binds
or replaces its radio model. Track accepted pending sample-rate intent
through refreshes and announce confirmed streams as engaged.

Resolve remembered rates before profile recovery creates streams, so a
saved 192k preference restores at 96k on limited models without a delayed
request targeting a later session.

Regression checks failed before the fixes and now pass. Three additional
mutations were caught; the native ARM64 application with RADE, four focused
CTest targets, isolated TX-disabled bridge proof, docs build and static
checks pass. Live radio/profile-load validation remains unperformed.

Refs aethersdr#6486, aethersdr#6437.
Remember whitelisted TCI rates negotiated before model identification,
without issuing IQ commands until capabilities are known. Announce the
effective rate when available and retain the preference across models.

Apply IQ button state sheets through ThemeManager so availability updates
cannot restore a stale On/Off template. Compose accessible rate reasons
before registry state text and calculate shared reasons once per radio.

Regressions first failed for early negotiation and Off-to-On styling, then
passed after these fixes. Strengthen rejection and accessibility coverage,
wait for restore signals with more timer slack, isolate optional captures,
and require an auth token before starting the opt-in bridge fixture.

Native ARM64+RADE application build, four focused tests, docs/static checks,
missing-token refusal and the TX-disabled bridge proof passed. Live radio
validation remains unperformed.

Refs aethersdr#6486, aethersdr#6437.

@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 exact head 4b815a6 after resolving the new generated-reference conflict against main 1dd498a.

The earlier review remains applicable: git range-diff confirms the DAX source fixes and capability-map entry are unchanged; only generated Log Analyzer line references changed during rebase. All four commits are verified. Both review threads remain answered and resolved.

Fresh proof on this head: incremental native macOS build passed for model_capabilities_test, dax_iq_rate_limits_test, control_availability_registry_test, and tci_server_review_test; three selected socket-free CTests passed, 0 failures, 11.48 s. The broader TCI target was built but not executed because its current suite includes socket tests. Base/head boundary, touchpoint, capability-record and command-plane checks match without growth; generated references and all 707 Log Analyzer checks pass. All hosted checks passed on the preceding rebased head; they must pass again on this new head before landing.

No new correctness findings. Existing author/Windows mutation and bridge evidence is reused, not claimed as a fresh session. No optional bridge socket session, live radio, RF/TX or live profile-load test was performed. A separate automated review tool was unavailable; review was manual. Hosted Docs handles the site build.

@jensenpat
jensenpat merged commit d2a94bc into aethersdr:main Oct 18, 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.

DAX IQ: enforce the FlexLib per-model sample-rate limits

4 participants