feat(automation): validation hardening for the agent automation bridge#3722
Conversation
… guard, scoped targeting, wider get coverage
Four agent-automation-bridge improvements from dogfooding the control-surface
QA sweep, plus a reusable validation tool:
1. Circular/wrapping controls — dumpTree now reports `range` {min,max} for
sliders/spinboxes. A driver classifies a control as wrapping (not broken)
when setValue(max) doesn't stick but a mid value does — e.g. the 0-360 phase
slider where step 72 == 0. The linear-clamp assumption was wrong for angular
controls (it false-flagged "ESC phase" as stuck).
2. Refuse invoke on disabled controls — Qt mutates a disabled widget on
setValue()/setChecked(), so the bridge used to report a happy newValue while
the radio never saw the change. invoke now returns
{ok:false, disabled:true, error:"refused: … is disabled"}, turning a silent
no-op into an assertable signal.
3. Scoped targeting — "<scope>/<name>" disambiguates duplicate accessibleNames
across applets ("AF gain" / "Squelch threshold" exist in both RxApplet and
PanadapterApplet). <scope> matches an ancestor by objectName/class/
accessibleName; falls through to flat matching so a literal '/' still works.
4. Wider get coverage — new `equalizer` (8-band RX+TX) model snapshot, and
slice snapshot gains squelch/squelchLevel, agcMode/agcThreshold, apf/apfLevel
so a sweep can model-check those too.
tools/automation_validate.py is the reusable form of the QA sweep: record →
3-value scale probe with wrapping classification → model cross-check → restore,
over every value-bearing applet control; scoped-targets duplicates, skips
disabled + keying controls, prints a findings table with timing.
Verified live on a FLEX-8400M (no transmission): full sweep of 31 numeric
controls; "ESC phase" classified `wrapping`; disabled "Squelch threshold"
refused; "RxApplet/AF gain" vs "PanadapterApplet/AF gain" resolve to distinct
widgets; `get equalizer` and the new slice fields return correctly; every
control restored, transmitting=false throughout. (Squelch threshold is
deliberately not model-cross-checked — its level is cached by RxApplet's
Manual/Auto branching until squelch is enabled, a mode-gated control, not a
desync.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NF0T
left a comment
There was a problem hiding this comment.
Human-directed independent review (Claude / @NF0T)
Four improvements to the automation bridge. Code is sound — flagging for @ten9876 to approve due to AGENTS.md (Tier 1).
Claims independently verified against current codebase:
EqualizerModel::BandCount,bandLabel(),rxBand(),txBand(),rxEnabled(),txEnabled()confirmed atsrc/models/EqualizerModel.h:18–42✓RadioModel::equalizerModel()atsrc/models/RadioModel.h:65returnsEqualizerModel&;&radio->equalizerModel()→const EqualizerModel*— correct C++ ✓- All new
SliceModelfields —apfLevel(),squelchOn(),squelchLevel(),agcMode(),agcThreshold()— confirmed atsrc/models/SliceModel.h:69–328✓ QDoubleSpinBoxalready in use at line 687 ofAutomationServer.cpp— no missing include ✓matchRecursive()(line 120) andmatchByButtonText()(line 141) pre-exist; scoped lookup correctly calls both within the ancestor subtree; if scope string resolves no ancestorscstays null and falls through to steps 1–N as documented ✓doInvokedisabled check fires before the TX-keying guard — correct ordering ✓Bridgeanddiscover_socketconfirmed intools/automation_probe.py:33,45—automation_validate.pyimport is valid ✓- Squelch threshold MODEL exclusion correctly documented (mode-gated, not a desync) ✓
No blocking issues found. Tier 1 (AGENTS.md) — @ten9876 to approve.
There was a problem hiding this comment.
Reviewed the full diff against the source tree. This is clean, well-scoped work — thanks @jensenpat.
Verified
- Model APIs all resolve.
EqualizerModel::{rxBand,txBand,rxEnabled,txEnabled,bandLabel,BandCount},RadioModel::equalizerModel(), and the newSliceModel::{apfLevel,squelchOn,squelchLevel,agcMode,agcThreshold}getters all exist, andRadioModel.hincludes bothSliceModel.h/EqualizerModel.h, soequalizerSnapshot()compiles against full types (not forward decls). rangecast order is correct.QAbstractSlider→QSpinBox→QDoubleSpinBoxis right, sinceQDoubleSpinBoxdoes not derive fromQSpinBox;QDialcorrectly inherits the slider branch.- Scoped resolver matches scope by objectName/findChild/class/accessibleName and falls through to flat resolution when either half doesn't resolve, so a literal
/in a control name still works (good call documenting that). - Disabled refusal uses
w->isEnabled(), which also returns false when an ancestor is disabled — that's the right semantics for "the radio won't accept this." - Conventions honored (QStringLiteral, qobject_cast, no QSettings), Python imports (
Bridge/discover_socket) and thetree["roots"]key check out, and all 6 CI checks are green.
Minor (non-blocking, optional)
- In
doInvoke, the disabled check runs before the TX-safety guard, so a disabled TX-keying control returnsdisabled:truerather thanblocked. Harmless — both refuse — but worth being aware of if any caller keys off the specific error. - In
automation_validate.py,classify_numericcomputesmid = int((lo+hi)/2). For aQDoubleSpinBoxwith a sub-unit range (e.g.[0,1]) mid collapses tolo, making the mid-probe and model cross-check degenerate. None of the mappedMODELcontrols are double-valued, so this is purely defensive — around()or float mid would cover it if such a control is ever added.
LGTM as a COMMENT review. Nice that the validation tool encodes the wrapping/disabled/duplicate-name lessons directly rather than leaving them as tribal knowledge.
🤖 aethersdr-agent · cost: $2.8373 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Approving (Tier 1 / automation-bridge area, per @NF0T's routing).
Verified the security-relevant points for a TX-reachable bridge:
- Disabled-refusal doesn't regress the TX guard.
!w->isEnabled()runs beforeisTransmitControl(), so a disabled keying control returnsdisabled:truerather thanblocked— both refuse; no path drives a keying control. It's a net safety win (silent no-op → assertable error), andisEnabled()correctly accounts for a disabled ancestor. - Scoped targeting can't bypass guards.
<scope>/<name>only changes resolution; the resolved widget still passes throughdoInvoke's disabled check and theaetherTxKeyingmarker guard. Falls through to flat matching so a literal/in a name still works. range/equalizer/sliceadditions are read-only (dumpTree/get) — no write path.rangecast order (QAbstractSlider→QSpinBox→QDoubleSpinBox) is correct sinceQDoubleSpinBoxdoesn't derive fromQSpinBox.automation_validate.pyskips disabled + keying controls, and the server enforces both guards regardless — defense-in-depth.
Model APIs (EqualizerModel, new SliceModel getters) all resolve, conventions honored, CI green on all six. The two bot nits (disabled-vs-blocked error string for disabled TX controls; classify_numeric mid collapsing for sub-unit double ranges) are non-blocking and correctly noted as defensive. Nice that the QA sweep's wrapping/disabled/duplicate-name lessons are encoded in the tool rather than left tribal. Thanks @jensenpat, @NF0T.
Four improvements to the agent automation bridge, all surfaced by dogfooding the control-surface QA sweep — plus the reusable validation tool that operationalizes it. Independent of the password-redaction fix (#3717); branched off current
main.1. Circular/wrapping control detection
dumpTreenow reportsrange: {min, max}for sliders/spinboxes. A driver classifies a control as wrapping (not broken) whensetValue(max)doesn't stick but a mid value does — e.g. the 0–360° phase slider where step 72 ≡ 0°. The old linear-clamp assumption false-flaggedESC phaseas stuck.2. Refuse
invokeon disabled controlsQt mutates a disabled widget on
setValue()/setChecked(), so the bridge used to report a happynewValuewhile the radio never saw the change.invokenow returns{"ok":false,"disabled":true,"error":"refused: … is disabled"}— a silent no-op becomes an assertable signal.3. Scoped targeting for duplicate names
"<scope>/<name>"disambiguates anaccessibleNamethat appears in more than one applet —"AF gain"and"Squelch threshold"exist in bothRxAppletandPanadapterApplet.<scope>matches an ancestor by objectName/class/accessibleName; falls through to flat matching, so a literal/in a name still works."RxApplet/AF gain"vs"PanadapterApplet/AF gain".4. Wider
getmodel coverageequalizer(aliaseq) snapshot — 8-band RX+TX graphic EQ.slicesnapshot gainssquelch/squelchLevel,agcMode/agcThreshold,apf/apfLevel.So a sweep can model-check controls that were widget-level-only before.
Tooling
tools/automation_validate.py— the reusable form of the QA sweep: record → 3-value scale probe with wrapping classification → model cross-check → restore, over every value-bearing applet control; scoped-targets duplicates, skips disabled + keying controls, prints a findings table with timing.Verification (live, FLEX-8400M, no transmission)
transmitting:falsethroughout.ESC phase(range[0,72]) → classifiedwrapping(72→0, 36 sticks);ESC gain→linear.RxApplet/Squelch threshold→invokerefused (disabled:true).RxApplet/AF gainandPanadapterApplet/AF gainresolve to distinct widgets.get equalizer→ RX/TX bands;get slice→ new squelch/AGC/APF fields.💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat