Skip to content

fix(bridge): VOX can be switched off with TX permission. Principle VIII. - #6375

Merged
NF0T merged 2 commits into
aethersdr:mainfrom
on8st:fix/bridge-vox-disarm
Oct 11, 2026
Merged

NF0T merged 2 commits into
aethersdr:mainfrom
on8st:fix/bridge-vox-disarm

Conversation

@on8st

@on8st on8st commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Refs #6053

What this does not do

This is the narrow part of #6053. It picks no policy.

  • Disarm without TX permission is still refused. invoke "VOX voice-operated transmit" setChecked false with "Allow TX via MCP" off is blocked exactly as on main. Whether an off-only action should be free of the gate is item 2 of the triage (a predicate on requiresTxPermission, or registerReceiveControlAction), and that is a design call for the maintainer.
  • The vox_toggle shortcut still arms VOX without the gate. It is registered with keysTx at its default false (src/gui/MainWindow_Shortcuts.cpp:1258). Item 3 of the triage; untouched.
  • MOX is untouched. setChecked false on MOX stays gated; the UNKEY asymmetry the triage describes is not addressed.
  • The gate itself and the name fallback are untouched. Any other control caught by the name fallback keeps the both-ways refusal (item 4).
  • The disarm needs the Phone applet on screen. invoke refuses a widget that is not visible (src/core/AutomationServer.cpp:4452), so a runner that armed VOX through the shortcut and never opened the applet still has no disarm path.
  • The bridge cannot arm VOX through the button, with or without TX permission, and pointer verbs on the button stay refused in both states, also when VOX is on.

One visible side effect: the button now carries the aetherTxKeying marker, so a widget dump reports "keying": true for it (src/core/AutomationServer.cpp:476-477). On main it is refused without saying so.

What was wrong

At main 16cfa80:

  • src/gui/PhoneApplet.cpp:159-167 creates the VOX button with the accessible name "VOX voice-operated transmit" and no TX-keying endpoint.
  • transmitControlMatch() (src/core/TxKeyingMarker.h:182-200) finds no marker and falls back to the name deny-list, which contains transmit (:168). The button classifies as NameFallback.
  • With no endpoint, txActionRequiresPermission() is true (TxKeyingMarker.h:68-73), so without TX permission doInvoke blocks it (src/core/AutomationServer.cpp:4477-4485).
  • With TX permission, invokeTxAction() (AutomationServer.cpp:7367-7386) asks prepareTxKeyingAction() for an action, and that returns nothing when there is no endpoint (TxKeyingMarker.h:83): transmit control has no scoped action for 'setChecked'.

So the bridge refuses every action on the VOX button, including switching it off, in both permission states. A runner that holds TX permission and sees VOX chattering cannot disarm it.

The change

PhoneApplet registers the VOX button with registerTxKeyingAction(). The prepare callback returns an action only for setChecked with false, 0, off, no or unchecked (the word the bridge itself reads back for an unchecked button); the action calls TransmitModel::setVoxEnable(false). Every other action and value returns no action.

The endpoint keeps requiresTxPermission at its default true, so the gate at AutomationServer.cpp:4478 and the keyboard guard in src/gui/TxKeyActivationGuard.h:25-26 see the same answer as before: a TX control that requires permission.

path request TX permission main this PR
bridge invoke setChecked false granted refused (no scoped action) accepted, VOX off
bridge invoke setChecked false not granted blocked blocked
bridge invoke setChecked true, click, toggle granted refused refused
bridge invoke setChecked true, click, toggle not granted blocked blocked
bridge clickAt on the button, VOX on or off granted refused refused
bridge clickAt on the button, VOX on or off not granted blocked blocked
operator mouse click arm / disarm n/a works works
operator Space / Enter, shortcuts on arm / disarm n/a not refused by the guard not refused by the guard
operator Space / Enter, shortcuts off, key bound arm / disarm n/a refused by the guard refused by the guard
operator Space / Enter, shortcuts off, key unbound arm / disarm n/a reaches the button reaches the button

registerReceiveControlAction() is deliberately not used: it sets requiresTxPermission to false, which would also take the button out of isTxKeyingButton() and change the keyboard rows above.

docs/architecture/aetherd-touchpoints.md is regenerated: the core/TxKeyingMarker.h include count goes from 15 to 16.

Tests

New phone_applet_vox_disarm_test (offscreen, no radio, no socket): a real PhoneApplet on a bare RadioModel, driven through AutomationServer::handleLine for invoke and clickAt. 36 checks:

  • classification by marker, and permission still required;
  • with TX permission: disarm accepted and landed in the model, for each "off" spelling, with the button out of step, and when already off; no name-fallback warning;
  • with TX permission: setChecked true, click, toggle and clickAt refused, VOX off and VOX on;
  • without TX permission: disarm, arm, click, toggle and clickAt all refused, model untouched;
  • the button's toggled signal, a mouse click and an unfiltered Space each drive the model both ways;
  • refuseTxKeyActivation() on the button: refused for a bound Space and a bound Enter with shortcuts off, not refused for an unbound Return, never refused with shortcuts on.

Fail-before. With only the test and its registration applied to main 16cfa80, the test failed 12 of the then 35 checks (run before the unchecked check was added, which makes 36): the marker classification, the name-fallback warning, and every disarm check (invoke setChecked false is accepted, the model follow-through, the four spellings, the out-of-step and already-off cases, and one knock-on check that finds VOX still armed). The 23 checks that pin unchanged behaviour pass on main and pass with the fix.

Full suite, macOS, Qt 6.12.0, -j8, failures rerun serially:

build tests failed which
main + test only 757 3 phone_applet_vox_disarm_test, peripheral_auth_dialog_interaction_test, unified_title_bar_test
this PR (34d81194) 757 2 peripheral_auth_dialog_interaction_test, unified_title_bar_test

The two remaining failures are not from this change and fail the same way on unchanged main on this machine:

This PR touches no dialog, title-bar or menu file.

tools/gen_touchpoint_manifest.py --check, check_test_registration.py, check_engine_boundary.py, check_ci_test_gate.py, check_wdsp_test_links.py and check_qsettings.py pass. check_a11y.py reports no finding on the changed lines.

73, ON8ST

🤖 Generated with Claude Code

The Phone applet's VOX button has no TX-keying endpoint, and its accessible
name ("VOX voice-operated transmit") matches the name fallback in
transmitControlMatch(). A name-matched control has no scoped action, so the
automation bridge refuses every action on it, including setChecked false,
with or without TX permission.

Register a disarm-only scoped action on the button. It stays a keying
control that requires bridge TX permission: arming (setChecked true, click,
toggle) and pointer verbs have no action and stay refused, and without TX
permission the gate blocks the button as before. The one new verb is
setChecked false with TX permission, which calls
TransmitModel::setVoxEnable(false).

The keyboard TX activation guard treats the button the same before and
after. The vox_toggle shortcut, MOX and the gate itself are not touched.

phone_applet_vox_disarm_test drives the real AutomationServer invoke and
clickAt paths and pins each unchanged refusal.

Refs aethersdr#6053

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st
on8st requested a review from a team as a code owner October 11, 2026 06:39

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

1. Issue fit

Partially, by design — and the body says so up front. This is Refs #6053, not Fixes, and the "What this does not do" section enumerates every part it leaves behind. I checked the body's claims against the code and they hold:

#6053 triage item this PR
1. Declare the button at its creation site done — registerTxKeyingAction at src/gui/PhoneApplet.cpp:174
2. Off-only must not need TX permission not done, disclosed; requiresTxPermission stays at its default true
3. Gate the ungated vox_toggle shortcut not done, disclosed; still keysTx=false at src/gui/MainWindow_Shortcuts.cpp:1258, firing tx.setVoxEnable(!tx.voxEnable()) at :1262
4. The general name-fallback dead end not done, disclosed

So the safety asymmetry in the issue's title survives: a bridge session without TX permission can still arm VOX via shortcut vox_toggle, and still cannot disarm it (src/core/AutomationServer.cpp:4478 blocks the disarm before preparation). The PR's own test pins that as "(unchanged)". That is the author's stated intent — #6053 says "this is a maintainer's call" and carries maintainer-review — so it is not a defect in the PR; it means #6053 must stay open after this merges.

One practical limit worth naming, which neither the issue nor the body states: doInvoke refuses an invisible widget at src/core/AutomationServer.cpp:4452, so the new disarm only works while the Phone applet is on screen. A runner that armed VOX through the ungated shortcut and never opened the applet still has no disarm path. The issue's own repro does invoke PHN click at step 2, so it is covered there — but it is a real caveat on "the bridge can now disarm VOX".

What this PR does not foreclose, which is why landing it ahead of the ruling is reasonable: whichever way item 2 is decided, the change is a one-line registration swap (registerReceiveControlAction, or a requiresTxPermission predicate), and the test's "TX not allowed: invoke setChecked false is refused" check simply flips to document the new policy.

2. Scope

File What it changes Claimed? Verdict
src/gui/PhoneApplet.cpp:174-190 the disarm-only scoped action, plus core/TxKeyingMarker.h and <QPointer> includes yes in scope
tests/phone_applet_vox_disarm_test.cpp new test yes in scope
tests/tests.cmake:8050-8066 registers the target yes in scope
tests/tests.cmake:8413 adds it to AETHER_AUTOMATION_SERVER_TESTS implied correct and required — the loop at :8423 links aetherdesktop_support, which owns AutomationServer.cpp
docs/architecture/aetherd-touchpoints.md:166 core/TxKeyingMarker.h count 15 → 16 yes in scope — generated, not hand-edited

Everything in the diff is explained by the issue. Checks I ran in the head checkout rather than taking on trust:

  • python3 tools/gen_touchpoint_manifest.py --check → manifest up to date. The generator only scans src/gui/ + src/main.cpp (tools/gen_touchpoint_manifest.py:41), so 16 is right even though 20 files tree-wide include the header.
  • tools/check_test_registration.py, check_ci_test_gate.py, check_engine_boundary.py, check_qsettings.py all exit 0.
  • No duplicate-symbol hazard: aetherdesktop_support (CMakeLists.txt:1682-1690) does not compile PhoneApplet.cpp.
  • target_include_directories(... PRIVATE src tests) — the added tests is needed for TestEventLoop.h / TestSettingsProfile.h; the sibling dexp target has only src because it uses neither. The omitted AETHER_SOURCE_DIR define is also correct: nothing in PhoneApplet.cpp, DragValuePopup.cpp or GuardedSlider.h references it.
  • Not in AETHER_SETTINGS_CONSUMERS, and doesn't need to be: aethercore links aether_sqlite3 PUBLIC (CMakeLists.txt:2446).

No new public surface, no deleted guard, no settings key, no default changed, no preference smuggled in — the one user-visible side effect ("keying": true in a widget dump, src/core/AutomationServer.cpp:476-477) is disclosed in the body. No CHANGELOG.md entry, correctly.

Socket tests: none. AutomationServer server; is constructed but never start()ed — the constructor at src/core/AutomationServer.cpp:2279-2282 is empty, and QLocalServer is created only in start() at :2304. The test drives handleLine(..., nullptr) through the AutomationServerTestAccess friend. Qt6::Network is linked for the bridge's headers, not for a socket. Nothing binds, listens or connects.

3. Blockers

None.

4. Nits (non-blocking)

  • src/gui/PhoneApplet.cpp:179-181 — the disarm allowlist refuses the bridge's own readback vocabulary. docs/automation-bridge.md:509 documents that a checkable button reads back "checked" / "unchecked", and parseBool (src/core/AutomationServer.cpp:1425-1431) treats "unchecked" as false. MOX's sibling lambda builds on from the truthy set and lets everything else mean off (src/gui/TxApplet.cpp:520-523), so setChecked unchecked unkeys MOX and unchecks every ordinary button — but on VOX:

                const bool disarm = action == QLatin1String("setChecked")
                    && (v == QLatin1String("false") || v == QLatin1String("0")
                        || v == QLatin1String("off") || v == QLatin1String("no"));

    it falls through to transmit control has no scoped action for 'setChecked' and VOX stays armed. A runner that echoes back the state it just read is exactly the client this action exists for, so refusing it is the wrong-direction failure. Drop-in:

                const bool disarm = action == QLatin1String("setChecked")
                    && (v == QLatin1String("false") || v == QLatin1String("0")
                        || v == QLatin1String("off") || v == QLatin1String("no")
                        || v == QLatin1String("unchecked"));

    (The broader alternative — mirror MOX and treat anything not in the truthy set as a disarm — is also defensible, since every such value only lowers TX risk. Either way the spellings loop in the test gains an entry.)

  • tests/phone_applet_vox_disarm_test.cpp:139-144 — the four-spelling loop reuses one check() label, so a failure on " FALSE " is indistinguishable from one on "0". Interpolating the spelling into the message would make the output self-locating.

  • Worth one line in the body: the disarm requires the Phone applet to be visible (src/core/AutomationServer.cpp:4452). The body's behaviour table reads as unconditional.

5. What I tried to break

Everything below is reasoned from the head checkout at /tmp/aetherclaude/pr-6375; I have no build and ran no test, so no runtime claim here was reproduced.

  • Lifetime. The Prepare lambda captures raw this, which I went after. It is stored as a property on m_voxBtn, a child of the applet, so its lifetime is bounded by the applet's; prepareTxKeyingAction (src/core/TxKeyingMarker.h:97) QPointer-guards the button, and the deferred Prepared re-checks QPointer<PhoneApplet> self. This is stronger than the MOX sibling at src/gui/TxApplet.cpp:525, which captures raw this into the deferred lambda. No dangling path found.
  • Accept-then-change-underneath. deferInvokeAction (src/core/AutomationServer.cpp:7435-7439) drops the queued action if permission, the permission epoch or the radio model changed between accept and fire, so a revoked-permission or swapped-radio disarm never lands. Survived.
  • Signal re-entrancy. setVoxEnable(false) → TransmitModel::phoneStateChanged → PhoneApplet::syncFromModel sets the button under QSignalBlocker (src/gui/PhoneApplet.cpp:561-563), so the toggled handler at :168 does not re-fire and there is no write-back loop. This is also what makes the test's "the button follows" assertion true.
  • The disabled-widget hole. prepareTxKeyingAction silently no-ops on a disabled widget (src/core/TxKeyingMarker.h:103), which would have been a silent-failure finding — but m_voxBtn is never disabled anywhere in the tree (only setCheckable/setFixedSize/setChecked touch it), and doInvoke refuses a disabled widget explicitly at :4466 first. Not reachable.
  • The claimed-unchanged keyboard guard. On main the button was NameFallback with no endpoint → txActionRequiresPermission true → isTxKeyingButton true. After the PR it is Marker with requiresTxPermission=true → still true. src/gui/TxKeyActivationGuard.h:25-26 gives the same answer, so the body's "unchanged" rows for Space/Enter are correct, not aspirational.
  • The pointer path. TxPointerAction::prepare (src/gui/TxPointerAction.cpp:23) prepares with action "click", which the lambda refuses, so clickAt cannot synthesise a press/release — matching the test and the body's table.
  • Is the test load-bearing, or does it assert the implementation back to itself? Load-bearing. Flip requiresTxPermission to false and txController() returns null without permission, prepareTxKeyingAction no longer bails on requiresTxPermission && !controller, and the disarm would be accepted with TX off — caught by "TX not allowed: invoke setChecked false is refused". The Marker-vs-NameFallback check and the nameMatchWarnings == 0 check both fail if the registration is removed.
  • The test's premise. The disarm needs TxController::valid() on a bare RadioModel with no backend. valid() (src/models/TxController.cpp:96-101) and Request::valid() (src/core/TxCoordinator.cpp:302-312) are pure producer/epoch/session bookkeeping — no connection requirement — so the premise holds from code.
  • Fail-before. I could not run it, but the claimed shape checks out: on main the button has no endpoint, so the classification check, the name-fallback-warning check, and every disarm check (prepareTxKeyingAction returns {} → "no scoped action") fail — the 12 the body reports.
  • What CI actually proved. tools/check_ci_test_gate.py reports 7 frozen -R tests out of 745 registered, and phone_applet_vox_disarm_test is not among them. All five checks on a1949f5 are green, including check-windows (the -DENABLE_ASR=OFF lane) — that proves the new target compiles and links on all three platforms, not that its 35 assertions pass. The 35/35 and 12-fail-before are the author's local macOS run.
  • security-audit (CodeGuard): no findings. The 383 hits on the listed files are lexical/fixture false positives — CS-VLN-CMDI-* on Markdown-quoted protocol verbs inside // comments with no shell anywhere in those files, CS-PII-* on RFC 5737 TEST-NET-1 and loopback literals in docs/user/screens.json, CS-SEC-PASSWORD on keychain-backed fields that never reach a log sink. It independently confirmed the direction of this change: the only behavioural delta is a reduction in transmit capability, with no fall-through to the generic setChecked handler (:4488 returns into invokeTxAction).
  • Sibling surfaces. One VOX button exists tree-wide. The other setVoxEnable writers are the MIDI/knob controller (src/gui/MainWindow_Controllers.cpp:2171), the PSK beacon save/restore (src/gui/PskReporterMapDialog.cpp:1960,1992) and the ungated shortcut (src/gui/MainWindow_Shortcuts.cpp:1262) — none is a bridge-reachable widget, and the last one is the disclosed item 3.

6. Recommendation

Approve with nits. The change is narrow, monotonically safety-reducing (it can only switch VOX off), every refusal it claims to leave alone I verified is genuinely unchanged, and the body is unusually scrupulous — it claims Refs rather than Fixes, enumerates the four things it does not do, and discloses the widget-dump side effect. Nothing here forecloses either item-2 design.

Concrete next step: take the "unchecked" spelling, add one line to the body about the applet needing to be visible, and keep #6053 open — items 2 (off-only free of the gate) and 3 (gating vox_toggle) still need the maintainer ruling the issue asks for, and until item 3 lands an unpermissioned session can still arm VOX with no sanctioned way back.

Nice work on the fail-before table and on running the static checks — it made this a verify rather than an investigation.


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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

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

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6375/docs/architecture/aetherd-touchpoint-tags.json:10
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6375/docs/architecture/aetherd-touchpoint-tags.json:1145
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6375/docs/user/screens.json:2579
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6375/docs/user/screens.json:4219
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6375/docs/user/screens.json:3877
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6375/docs/user/screens.json:1122
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6375/docs/user/screens.json:2579
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6375/docs/user/screens.json:2625
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6375/docs/user/screens.json:2659
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6375/docs/user/screens.json:2756
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6375/docs/user/screens.json:2796
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6375/docs/user/screens.json:4219
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:101
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:183
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:308
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:418
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:441
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:504
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:541
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:604
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:676
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:738
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/RadioCapabilities.h /tmp/aetherclaude/pr-6375/src/core/backends/RadioCapabilities.h:796
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/anan/AnanBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/anan/AnanBackend.cpp:1030
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/anan/AnanRxDsp.cpp /tmp/aetherclaude/pr-6375/src/core/backends/anan/AnanRxDsp.cpp:451
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/anan/AnanRxDsp.h /tmp/aetherclaude/pr-6375/src/core/backends/anan/AnanRxDsp.h:34
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/anan/AnanRxDsp.h /tmp/aetherclaude/pr-6375/src/core/backends/anan/AnanRxDsp.h:106
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/anan/AnanRxDsp.h /tmp/aetherclaude/pr-6375/src/core/backends/anan/AnanRxDsp.h:122
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/anan/AnanRxDsp.h /tmp/aetherclaude/pr-6375/src/core/backends/anan/AnanRxDsp.h:200
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/anan/AnanRxDsp.h /tmp/aetherclaude/pr-6375/src/core/backends/anan/AnanRxDsp.h:226
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/flex/FlexBackend.cpp:1257
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/flex/FlexBackend.cpp:1292
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/flex/FlexBackend.cpp:1294
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/flex/FlexBackend.cpp:1300
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/flex/FlexBackend.cpp:222
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/flex/FlexBackend.cpp:333
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/flex/FlexBackend.cpp:384
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/flex/FlexBackend.cpp:611
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/flex/FlexBackend.cpp:616
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/FlexBackend.cpp /tmp/aetherclaude/pr-6375/src/core/backends/flex/FlexBackend.cpp:639

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

Review of aethersdr#6375: a checkable button reads back "checked" / "unchecked"
(docs/automation-bridge.md), and a client that echoes the state it read
was refused: setChecked unchecked left VOX armed. The disarm accepts it.
The test names the spelling in each check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st

on8st commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Fixed in 34d81194, a new commit on top (no rebase).

  • unchecked now disarms. You are right: it is the bridge's own readback word for an unchecked button (docs/automation-bridge.md), and refusing it left VOX armed. I added that one spelling and did not take the wider form (anything not truthy disarms); say if you prefer that.
  • The test names the spelling in each check, and has a fifth one for unchecked: 36 checks.
  • The body now says the disarm needs the Phone applet on screen, with your :4452 reference.

Agreed that #6053 stays open: items 2 and 3 need the ruling, and until item 3 lands a session without TX permission can still arm VOX through vox_toggle with no way back.

Full ctest at 34d81194: 755 of 757; the two that fail are the clean-main failures #6008 and #6224. The fail-before figure in the body (12 of 35) was measured before the fifth spelling was added and is marked so.

73, ON8ST

@NF0T NF0T self-assigned this Oct 11, 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.

Approve

Thanks, @on8st. This is a clean, honestly scoped piece of work. Where #6053 asked for "declare the button at its creation site" and a disarm that works, you delivered exactly that and nothing else, and the "What this does not do" section tells the reader which three of the four triage items are still open. That is what lets a Tier 3 reviewer approve it without pre-empting the maintainer ruling.

Why it is right

The incident behind #6053 was a keyed session that already held TX permission. The runner's setChecked false died at prepareTxKeyingAction() because the VOX button had no registered endpoint (TxKeyingMarker.h), so invokeTxAction() returned "no scoped action". Registering a disarm-only endpoint fixes that path and no other. I checked the paths that could have widened:

  • requiresTxPermission stays true, so doInvoke still blocks every verb when m_txAllowed is false (AutomationServer.cpp:4478).
  • With m_readOnly set, txController() returns null, and prepareTxKeyingAction() returns empty for a permission-requiring endpoint with no controller. A read-only session still cannot disarm.
  • TxPointerAction::prepare prepares with action "click", which the lambda refuses, so clickAt cannot synthesise a press.
  • deferInvokeAction drops the queued disarm if permission, epoch or radio model changed in between. It fails closed, which in this one case is the "wrong" direction for a disarm, but it is the existing gate and not something this PR should change.
  • JSON false and 0 reach the lambda as "false" and "0" (AutomationServer.cpp:3841-3846), so a client that sends a bool is covered.

Scope table

File Change Claimed Verdict
src/gui/PhoneApplet.cpp disarm-only registerTxKeyingAction + two includes yes in scope
tests/phone_applet_vox_disarm_test.cpp new test yes in scope
tests/tests.cmake target + AETHER_AUTOMATION_SERVER_TESTS entry yes required, in scope
docs/architecture/aetherd-touchpoints.md TxKeyingMarker.h 15 to 16 yes generated, in scope

No CHANGELOG entry (correct), no new public surface, no deleted guard, no preference change. The one visible side effect, "keying": true in a widget dump, is disclosed. I saw it live: the VOX button now reports "keying": true, "checked": true, "visible": true.

CODEOWNERS: every file is Tier 3 (*, docs/, tests/). @aethersdr/reviewers satisfies the gate.

What I tried to break

Built the PR head in an isolated worktree (MinGW 13.1, Qt 6.12.0, Windows 11, ENABLE_ASR=OFF).

  • The new test on the PR head: 36 of 36 pass, 0 failures. CI built and linked this target on all three platforms but does not execute it (it is not in the frozen -R list), so this is the first execution I know of outside the author's macOS run.
  • Fail-before, by reverting PhoneApplet.cpp to main: 13 failures. That is your 12 of 35 plus the unchecked check added afterwards. The failures are the marker classification, the name-fallback warning, every disarm spelling, and the knock-on "nothing armed VOX behind a refusal".
  • Mutation A, registerReceiveControlAction instead of registerTxKeyingAction: 6 failures. "requires bridge TX permission", "TX not allowed: setChecked false is refused", "an armed VOX stays armed", and the three keyboard-guard rows. So the test pins the permission requirement and the keyboard rows you said registerReceiveControlAction would change.
  • Mutation B, letting setChecked true through to setVoxEnable(true): 2 failures. "setChecked true is refused" and "nothing armed VOX behind a refusal". The arming direction is pinned.
  • Live bridge against the built-in Demo radio (DEMO-0001, family sim, isolated AETHER_SETTINGS_DIR, own socket, txAllowed:false):
    • shortcut vox_toggle returned {"fired":true,"ok":true} and get transmit read voxEnable: true. This is item 3 from #6053, untouched, as the body says.
    • invoke "VOX voice-operated transmit" with setChecked false, setChecked true, click and toggle were all blocked: ... transmit-keying control (TX-safety guard). These are the unchanged rows.
    • A widget dump showed "keying": true on the VOX button, as disclosed.

Not driven live: the permission-granted half. I tried to relaunch the Demo instance with AETHER_AUTOMATION_ALLOW_TX=1 and the session's auto-mode classifier declined it as a safety-bypass flag, so I did not pursue it. That half is covered by the unit test above, which drives the real PhoneApplet through AutomationServer::handleLine. I also did not test the "applet hidden" case live; that one I confirm from code only: the visibility check at AutomationServer.cpp:4452 runs before the TX guard.

Socket-test preflight (Step 0): clean. AutomationServer server; is constructed but never start()ed, and the QLocalServer is created only in start(). Nothing binds, listens or connects, and there is no fake peer.

Non-blocking, for future reference

  • Principle citation. The branch commits cite Principle VIII (Evidence Over Assertion). That is defensible given the fail-before table, but the sentence this change leans on is Principle VI's "Any code path that can transmit fails closed", since the PR keeps arming refused. I used VI in the squash subject below; VIII on the branch commits is fine.
  • #6053 must stay open. Items 2 (off-only without the gate) and 3 (gating vox_toggle) still need the maintainer ruling, and until item 3 lands a session without TX permission can arm VOX with no way back. This PR says Refs, not Fixes, and the squash body below keeps it that way.
  • The disarm only works while the Phone applet is visible. You added that to the body, thank you.

Squash subject

fix(bridge): VOX can be switched off with TX permission. Principle VI. (#6375)

Nice work on the fail-before table and on running the static checks. It made this a verification and not an investigation.

73, NF0T


👨🏼‍💻 Co-authored by Claude Sonnet 5.5

@NF0T
NF0T merged commit 0625d24 into aethersdr:main Oct 11, 2026
5 checks passed
@NF0T NF0T removed their assignment Oct 11, 2026
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.

2 participants