Skip to content

feat(automation): validation hardening for the agent automation bridge#3722

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:feat/automation-validation-hardening
Jun 22, 2026
Merged

feat(automation): validation hardening for the agent automation bridge#3722
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:feat/automation-validation-hardening

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

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

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 old linear-clamp assumption 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"} — a silent no-op becomes an assertable signal.

3. Scoped targeting for duplicate names

"<scope>/<name>" disambiguates an accessibleName that appears in more than one applet — "AF gain" and "Squelch threshold" exist in both RxApplet and PanadapterApplet. <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 get model coverage

  • New equalizer (alias eq) snapshot — 8-band RX+TX graphic EQ.
  • slice snapshot gains squelch/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)

  • Full sweep of 31 numeric controls in ~40 ms, all restored, transmitting:false throughout.
  • ESC phase (range [0,72]) → classified wrapping (72→0, 36 sticks); ESC gainlinear.
  • Disabled RxApplet/Squelch thresholdinvoke refused (disabled:true).
  • RxApplet/AF gain and PanadapterApplet/AF gain resolve to distinct widgets.
  • get equalizer → RX/TX bands; get slice → new squelch/AGC/APF fields.
  • Only flagged issues were the two genuinely-disabled controls (correct). 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.

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

… 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>
@jensenpat jensenpat marked this pull request as ready for review June 21, 2026 16:16
@jensenpat jensenpat requested review from a team as code owners June 21, 2026 16:16

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

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 at src/models/EqualizerModel.h:18–42
  • RadioModel::equalizerModel() at src/models/RadioModel.h:65 returns EqualizerModel&; &radio->equalizerModel()const EqualizerModel* — correct C++ ✓
  • All new SliceModel fields — apfLevel(), squelchOn(), squelchLevel(), agcMode(), agcThreshold() — confirmed at src/models/SliceModel.h:69–328
  • QDoubleSpinBox already in use at line 687 of AutomationServer.cpp — no missing include ✓
  • matchRecursive() (line 120) and matchByButtonText() (line 141) pre-exist; scoped lookup correctly calls both within the ancestor subtree; if scope string resolves no ancestor sc stays null and falls through to steps 1–N as documented ✓
  • doInvoke disabled check fires before the TX-keying guard — correct ordering ✓
  • Bridge and discover_socket confirmed in tools/automation_probe.py:33,45automation_validate.py import is valid ✓
  • Squelch threshold MODEL exclusion correctly documented (mode-gated, not a desync) ✓

No blocking issues found. Tier 1 (AGENTS.md) — @ten9876 to approve.

@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 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 new SliceModel::{apfLevel,squelchOn,squelchLevel,agcMode,agcThreshold} getters all exist, and RadioModel.h includes both SliceModel.h/EqualizerModel.h, so equalizerSnapshot() compiles against full types (not forward decls).
  • range cast order is correct. QAbstractSliderQSpinBoxQDoubleSpinBox is right, since QDoubleSpinBox does not derive from QSpinBox; QDial correctly 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 the tree["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 returns disabled:true rather than blocked. Harmless — both refuse — but worth being aware of if any caller keys off the specific error.
  • In automation_validate.py, classify_numeric computes mid = int((lo+hi)/2). For a QDoubleSpinBox with a sub-unit range (e.g. [0,1]) mid collapses to lo, making the mid-probe and model cross-check degenerate. None of the mapped MODEL controls are double-valued, so this is purely defensive — a round() 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 ten9876 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.

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 before isTransmitControl(), so a disabled keying control returns disabled:true rather than blocked — both refuse; no path drives a keying control. It's a net safety win (silent no-op → assertable error), and isEnabled() correctly accounts for a disabled ancestor.
  • Scoped targeting can't bypass guards. <scope>/<name> only changes resolution; the resolved widget still passes through doInvoke's disabled check and the aetherTxKeying marker guard. Falls through to flat matching so a literal / in a name still works.
  • range / equalizer / slice additions are read-only (dumpTree/get) — no write path. range cast order (QAbstractSliderQSpinBoxQDoubleSpinBox) is correct since QDoubleSpinBox doesn't derive from QSpinBox.
  • automation_validate.py skips 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.

@ten9876 ten9876 merged commit f190643 into aethersdr:main Jun 22, 2026
6 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.

3 participants