feat(automation): operator-controlled observe-only bridge mode (#4188)#4208
Conversation
There was a problem hiding this comment.
Clean, well-scoped change. The design is right: the gate is enforced in handleLine (server-side, after the auth check, before dispatch) rather than in the MCP client, it's operator-only, and it reuses the established nested-JSON AutomationBridgeSettings pattern (Principle V) and the live-push idiom that mirrors the existing TX gate. CI is green and the commit is signed. The Python test is a good addition and correctly cleans up the env var it sets. Just one non-blocking note about allowlist granularity below.
Non-blocking notes
logis on the observe-only allowlist, but itsset/resetsubcommands mutate process-wideLogManagerstate — whilemark(also a log-ring writer) is correctly blocked. Slight tension with the "refuses every mutating verb" wording. Detail inline.- Minor:
audioCaptureis a pure-observe verb but isn't allowlisted, so it's refused in observe-only mode. Probably fine as a conservative default, but you may want it available for a genuine look-only session. (Body note — the verb isn't in this diff.)
Nice work, @ten9876 — the enforcement placement and the env-override for headless/CI are exactly right.
🤖 aethersdr-agent · cost: $4.5147 · model: claude-opus-4-8
There was a problem hiding this comment.
Nicely done — this cleanly mirrors the existing TX gate and gets the important things right: enforcement lives in handleLine (not the MCP server) so a socket-direct client can't bypass it, it's deny-by-default (any verb not explicitly allowlisted is refused, so future verbs are safe automatically), it gates on the resolved canonical spec->name so aliases are covered, and the gate sits after the auth check. The env override deliberately doesn't write the persisted value, and the live-toggle path matches the TX idiom. Persistence stays in the nested AutomationBridge key (Principle V), and the Python test covers both the reflected state and the absence of the note. All 9 checks are green and the commit is signed.
Non-blocking notes
- The observe-only allowlist is verb-level, but two allowlisted verbs (
log,streams) have mutating sub-actions that slip through what the comment/docs call "pure introspection" — see inline. Low severity (neither drives the radio), but worth a wording tweak or a sub-action guard. - Mirror-image, and I think correctly the safe side:
connect list/show,scale(report),menu list,dss snapshot,qrz statusare read sub-actions that are now fully blocked because their parent verb mutates. That's the right fail-safe default — just flagging it's intentional so it isn't a surprise later.
No changes required from my side; take or leave the note above.
🤖 aethersdr-agent · cost: $4.6173 · model: claude-opus-4-8
There was a problem hiding this comment.
Nice work — this is a clean, tightly-scoped feature that mirrors the existing TX-gate pattern faithfully. The design decisions are all the right ones: enforcement in the bridge (handleLine) rather than the MCP server so a client can't bypass it, operator-only control, a default-deny allowlist, persistence via the nested AutomationBridge config key (Principle V), and a live toggle that matches the txAllowed path exactly. I verified the read-only gate sits after the token check and operates on the resolved canonical verb name, so there's no auth-handshake lockout and aliases are covered. ping stays open on both the auth and read-only paths. I also cross-checked the allowlist against the full verb registry: every excluded verb is either mutating or mixed-mode, and no mutating verb leaked into the safe set. Docs, a Python regression test, and bridge_status reflection are all present. CI is green (9/9) and the commit is signed.
Non-blocking notes
- The allowlist correctly errs on the side of default-deny; a couple of mixed-mode verbs (
layout get,scalereport) lose their read sub-action in observe-only. Worth a one-line confirm that's intended (inline).
No changes required from me — happy to see this merge. Thanks for the thorough PR description and the offscreen verification.
🤖 aethersdr-agent · cost: $6.0403 · model: claude-opus-4-8
|
Holding for next weeks release. |
Add an operator-controlled, bridge-enforced read-only profile for automation clients, persist it in the nested bridge configuration, expose it in bridge status, and provide a live Radio Setup toggle. Apply action-aware admission to mixed diagnostic verbs: observe-only clients may read log state and stream inventories, but cannot change log categories, clear stream diagnostics, or request a radio inventory resync. Document and screenshot the operator control. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aa15dcb to
2851b1a
Compare
|
First-pass triage review (not exhaustive — surfacing blockers to keep this moving): Blockers None spotted in a first pass. The observe-only gate is soundly designed:
Secondary notes
Gating logic looks correct and bypass-resistant; the only real gap is test coverage of the gate itself, which is a follow-up rather than a merge blocker. |
jensenpat
left a comment
There was a problem hiding this comment.
Approved after cleanup and verification. The observe-only gate is now action-aware: log mutations (set/reset), stream mutations (reset/resync/refresh), and radio-affecting verbs are rejected, while diagnostic log and stream reads remain available. Verified through both JSON and raw bridge requests with autoconnect disabled, plus the full 128-test local suite, bridge checks, strict engine-boundary check, macOS build, and all GitHub checks including C++ CodeQL. The branch is rebased onto current main as one signed commit (2851b1a). Principles VII and VIII.
Observe-only bridge profile — #4188 area 6
Adds a look-but-don’t-touch mode for the agent automation bridge so an operator can hand an MCP client visibility without letting it drive the app or radio.
Design
Read-only is operator-controlled (never client-flippable) and enforced in the bridge after authentication but before dispatch, so a client talking to the socket directly cannot bypass it.
AutomationServer— reportsreadOnlyfromping/whoamiand defaults to denying every verb outside the introspection policy.log categories/get/tail/subscribe/unsubscribeandstreamsinventory reads remain available;log set/resetandstreams reset/resync/refreshare blocked. This prevents observe-only clients from changing runtime diagnostics or sending a radio inventory resync.AutomationBridgeSettings— persistsreadOnlyin the nested{enabled,txAllowed,txAck,readOnly}bridge configuration (Principle V).AETHER_AUTOMATION_READONLY=1pins the gate for headless/CI use.bridge_read_onlyplus aread_only_note.Screenshot
Verification
origin/main(05a2222c).tools/test_aether_mcp.pypassed.tools/check_engine_boundary.py --strictpassed with no blocking findings.log categories/tailandstreamsdefault/radio/inventory;log set/reset,streams reset/resync/refresh, andtune;🤖 Original implementation generated with Claude Code.
👨🏼💻 Generated with OpenAI Codex (GPT-5.6 Sol) and tested by @jensenpat