From 2851b1a556bca2219bc504922db7688fefba87ee Mon Sep 17 00:00:00 2001 From: "Jeremy [KK7GWY]" Date: Sun, 12 Jul 2026 11:05:14 -0700 Subject: [PATCH] feat(automation): add observe-only bridge mode. Principle VII. 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) --- docs/assets/automation-observe-only.png | Bin 0 -> 59455 bytes docs/automation-bridge.md | 36 ++++++++++++++-- src/core/AutomationBridgeSettings.cpp | 9 +++- src/core/AutomationBridgeSettings.h | 3 ++ src/core/AutomationServer.cpp | 52 +++++++++++++++++++++++- src/core/AutomationServer.h | 9 ++++ src/gui/MainWindow.cpp | 2 + src/gui/MainWindow.h | 3 ++ src/gui/MainWindow_Session.cpp | 16 ++++++++ src/gui/RadioSetupDialog.cpp | 45 +++++++++++++++++--- src/gui/RadioSetupDialog.h | 4 ++ tools/aether_mcp.py | 10 +++++ tools/test_aether_mcp.py | 35 ++++++++++++++++ 13 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 docs/assets/automation-observe-only.png diff --git a/docs/assets/automation-observe-only.png b/docs/assets/automation-observe-only.png new file mode 100644 index 0000000000000000000000000000000000000000..1428e1bb9cd17b5236e308be35319950f8e43d55 GIT binary patch literal 59455 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 nQEGldP7OOSeIStkX4pScT49L-+%r;aytJM literal 0 HcmV?d00001 diff --git a/docs/automation-bridge.md b/docs/automation-bridge.md index 396dfb6f2..74aac11b5 100644 --- a/docs/automation-bridge.md +++ b/docs/automation-bridge.md @@ -186,6 +186,27 @@ emissions; once confirmed the choice persists. Toggling it drives the same `m_txAllowed` gate live (enabling arms the force-unkey watchdog; disabling force-unkeys immediately). +### Observe-only (read-only) mode + +For a look-but-don't-touch session — handing an assistant visibility +without letting it change anything — check **"Observe only"** in Radio +Setup → Network. The bridge then refuses **every** mutating verb and +answers only pure-introspection reads (`ping`, `verbs`, `whoami`, `get`, +`dumpTree`, `grab`, the read-only `log` actions, `floors`, the inventory-only +`streams` actions, and `hitTest`). In particular, it blocks `log set/reset` +and `streams reset/resync/refresh`; the latter two stream actions clear local +diagnostics or request a fresh radio inventory. It is +enforced in the app, not in the MCP server, so no client can flip it +off; the refusal message points the operator back to the checkbox. The +toggle takes effect immediately on a running bridge — no restart — so +the intended flow works: start the app with the bridge off, check +"Observe only", then start the bridge. `ping` and `whoami` report the +current state as `readOnly`, and the MCP server surfaces it in +`bridge_status` as `bridge_read_only`. Headless/CI runs can pin it with +`AETHER_AUTOMATION_READONLY=1`. + +![Observe only setting in Radio Setup → Network](assets/automation-observe-only.png) + --- ## How it works (the contract) @@ -235,7 +256,7 @@ transmit-gated verbs (refused unless `AETHER_AUTOMATION_ALLOW_TX=1` — see | | [`grab pan [path]`](#grab) | Raw spectrum surface of a specific pan. | | | [`grab pan-visible [path]`](#grab) | Pan applet incl. VFO/flag overlays (alias `pan-composite`). | | | [`floors`](#floors) | Per-pan measured noise + display floor (dBm). | -| | [`whoami`](#whoami) | This bridge instance: pid, socket, label, station, `txAllowed`. | +| | [`whoami`](#whoami) | This bridge instance: pid, socket, label, station, `txAllowed`, `readOnly`. | | **Drive** | [`invoke [v]`](#invoke) | Click/toggle/set/selectRow/submit/trigger a control (TX-guarded). | | | [`close `](#close) | Close the target's top-level window. | | | [`drag " "`](#drag-alias-mouse) | Synthesize press→move→release (alias `mouse`). | @@ -1588,8 +1609,11 @@ The `~500ms` is a **best-effort hint, not a contract** — the re-dump is async; `streams radio` still looks stale, poll again. Returns `not connected — cannot resync display inventory` with no radio. -All `streams` actions are read-only / RX; none keys the transmitter (`resync` -sends only the `sub pan all` subscription command). +None of the `streams` actions keys the transmitter. In **Observe only** mode, +the default Layer-A inventory and `radio`/`inventory` reads remain available; +`reset`, `resync`, and `refresh` are blocked. `reset` changes the local orphan +tally, while `resync`/`refresh` send the `sub pan all` subscription command to +the radio. ### `txwaterfall` Toggle the radio's **show-TX-in-waterfall** display flag (`transmit set @@ -1873,6 +1897,10 @@ push subscription — the observability suite. All diagnostic; nothing keys. oldest`, earlier matching events were evicted and the window is a truncated suffix, not a complete bracket. +In **Observe only** mode, `categories`, `get`, `tail`, `subscribe`, and +`unsubscribe` remain available. `set` and `reset` are blocked because they +change the app's logging state. + ### `record` Drive the client-side **QSO WAV recorder** (the same one behind the manual record button), so a live test can capture audio and verify SSB + CW/CWX is recorded. @@ -2209,7 +2237,7 @@ The complete registry, generated from the `add(...)` table in `AutomationServer. | `scale` | — | scale [pct] — report/persist the UI scale factor | | `panmessage` | — | panmessage [id timeout [tone=…] title\|detail] | | `dss` | — | dss [pan] [args] | -| `streams` | — | streams [radio\|reset] — stream diagnostics | +| `streams` | — | streams [radio\|inventory\|resync\|refresh\|reset] — stream diagnostics | | `tci` | — | tci start\|status\|stop — in-process TCI client simulator (JSON form only) | | `audioCapture` | — | audioCapture [args] | | `txwaterfall` | — | txwaterfall — show keyed TX in the waterfall | diff --git a/src/core/AutomationBridgeSettings.cpp b/src/core/AutomationBridgeSettings.cpp index 7f3ca4a04..f6985d95a 100644 --- a/src/core/AutomationBridgeSettings.cpp +++ b/src/core/AutomationBridgeSettings.cpp @@ -15,7 +15,7 @@ namespace AetherSDR { namespace { // Single nested-JSON key holding the bridge's non-secret config (Principle V). -// Shape: {"enabled":bool,"txAllowed":bool,"txAck":bool}. +// Shape: {"enabled":bool,"txAllowed":bool,"txAck":bool,"readOnly":bool}. const QString kRootKey = QStringLiteral("AutomationBridge"); // Legacy flat keys (pre-nesting). Migrated one-shot into kRootKey on first read. @@ -26,6 +26,7 @@ const QString kLegacyTxAck = QStringLiteral("AutomationBridgeTxAck"); constexpr const char* kFieldEnabled = "enabled"; constexpr const char* kFieldTxAllowed = "txAllowed"; constexpr const char* kFieldTxAck = "txAck"; +constexpr const char* kFieldReadOnly = "readOnly"; // Keychain coordinates for the token (analogous to MqttSettings). constexpr const char* kKeychainService = "AetherSDR"; @@ -91,6 +92,12 @@ bool AutomationBridgeSettings::txAck() } void AutomationBridgeSettings::setTxAck(bool on) { writeBool(kFieldTxAck, on); } +bool AutomationBridgeSettings::readOnly() +{ + return readObj().value(QLatin1String(kFieldReadOnly)).toBool(false); +} +void AutomationBridgeSettings::setReadOnly(bool on) { writeBool(kFieldReadOnly, on); } + QString AutomationBridgeSettings::keychainService() { return QString::fromLatin1(kKeychainService); diff --git a/src/core/AutomationBridgeSettings.h b/src/core/AutomationBridgeSettings.h index 296ab7d96..6303eb1e6 100644 --- a/src/core/AutomationBridgeSettings.h +++ b/src/core/AutomationBridgeSettings.h @@ -19,6 +19,7 @@ namespace AetherSDR { // enabled — the bridge runs at launch (Radio Setup → Network toggle) // txAllowed — an MCP client may key the transmitter (the TX guard) // txAck — the operator has acknowledged the TX warning at least once +// readOnly — observe-only: the bridge refuses every mutating verb (#4188) // // All accessors go through AppSettings and are process-wide. class AutomationBridgeSettings { @@ -29,6 +30,8 @@ class AutomationBridgeSettings { static void setTxAllowed(bool on); static bool txAck(); static void setTxAck(bool on); + static bool readOnly(); + static void setReadOnly(bool on); // Keychain coordinates for the bridge access token (see MqttSettings for // the analogous MQTT-password helpers). diff --git a/src/core/AutomationServer.cpp b/src/core/AutomationServer.cpp index 17b2701eb..20b0b89b1 100644 --- a/src/core/AutomationServer.cpp +++ b/src/core/AutomationServer.cpp @@ -2197,6 +2197,40 @@ struct AutomationServer::VerbSpec { namespace { +// Requests that are pure introspection — allowed even in observe-only mode +// (#4188 area 6). Some diagnostic verbs mix read and write actions, so the +// action must be checked as well as the canonical verb name. Everything else +// (drive/connect/capture/keying) is refused when m_readOnly is set. +bool isReadOnlyRequest(const QString& name, const QString& action) +{ + static const QSet kSafe = { + QStringLiteral("ping"), QStringLiteral("verbs"), + QStringLiteral("whoami"), QStringLiteral("dumpTree"), + QStringLiteral("grab"), QStringLiteral("get"), + QStringLiteral("floors"), QStringLiteral("hitTest"), + }; + if (kSafe.contains(name)) { + return true; + } + + const QString normalizedAction = action.trimmed().toLower(); + if (name == QLatin1String("log")) { + static const QSet kSafeLogActions = { + QString(), QStringLiteral("categories"), QStringLiteral("get"), + QStringLiteral("tail"), QStringLiteral("subscribe"), + QStringLiteral("unsubscribe"), + }; + return kSafeLogActions.contains(normalizedAction); + } + if (name == QLatin1String("streams")) { + static const QSet kSafeStreamActions = { + QString(), QStringLiteral("radio"), QStringLiteral("inventory"), + }; + return kSafeStreamActions.contains(normalizedAction); + } + return false; +} + QString vtok(const QList& p, int i) { return QString::fromUtf8(p.value(i)); @@ -2297,6 +2331,7 @@ const std::vector& AutomationServer::verbRegistry() {QStringLiteral("app"), QStringLiteral("AetherSDR")}, {QStringLiteral("version"), QCoreApplication::applicationVersion()}, {QStringLiteral("authRequired"), !self.m_authToken.isEmpty()}, + {QStringLiteral("readOnly"), self.m_readOnly}, }; }); @@ -2652,7 +2687,7 @@ const std::vector& AutomationServer::verbRegistry() a.value); }); - add("streams", {}, "streams [radio|reset] — stream diagnostics", + add("streams", {}, "streams [radio|inventory|resync|refresh|reset] — stream diagnostics", parseActionOnly, [](AutomationServer& s, A& a, QLocalSocket*) { return s.doStreams(a.action); @@ -2909,6 +2944,20 @@ QJsonObject AutomationServer::handleLine(const QByteArray& line, QLocalSocket* s } } + // Observe-only gate (#4188 area 6). When the operator has enabled + // read-only mode (Radio Setup -> Network -> "Observe only"), refuse any + // verb that isn't pure introspection — no driving, no connect/capture, no + // keying. Enforced HERE in the bridge (not the MCP client) so it can't be + // bypassed by talking to the socket directly. Uses the resolved canonical + // name so aliases are covered. + if (m_readOnly && !isReadOnlyRequest(spec->name, a.action)) { + qCWarning(lcAutomation) << "read-only mode: refused" << spec->name; + return err(QStringLiteral("read-only mode: '") + spec->name + + QStringLiteral("' is blocked. This bridge is observe-only " + "— uncheck \"Observe only\" in Radio Setup " + "-> Network to allow driving.")); + } + return spec->dispatch(*this, a, sock); } @@ -7484,6 +7533,7 @@ QJsonObject AutomationServer::doWhoami() const {QStringLiteral("guiClientIdTransient"), AppSettings::instance().guiClientIdentityIsTransient()}, {QStringLiteral("txAllowed"), m_txAllowed}, + {QStringLiteral("readOnly"), m_readOnly}, {QStringLiteral("version"), QCoreApplication::applicationVersion()}, }; } diff --git a/src/core/AutomationServer.h b/src/core/AutomationServer.h index fb95ec7f3..95ee1fb60 100644 --- a/src/core/AutomationServer.h +++ b/src/core/AutomationServer.h @@ -299,6 +299,14 @@ class AutomationServer : public QObject { void setTxAllowed(bool allowed); bool txAllowed() const { return m_txAllowed; } + // Observe-only gate (#4188 area 6). When true, the bridge refuses every + // verb that isn't pure introspection — no driving, connect, capture, or + // keying. Operator-driven from Radio Setup → Network; enforced in + // handleLine so a client can't bypass it. Safe to toggle live. `ping` and + // `whoami` report the current state. + void setReadOnly(bool readOnly) { m_readOnly = readOnly; } + bool readOnly() const { return m_readOnly; } + private slots: void onNewConnection(); void onReadyRead(); @@ -603,6 +611,7 @@ private slots: int m_txMaxKeyMs{20000}; // max continuous key time before force-unkey int m_txMaxPower{-1}; // power-ceiling clamp for invoke (-1 = off) bool m_txAllowed{false}; // AETHER_AUTOMATION_ALLOW_TX at start() + bool m_readOnly{false}; // observe-only gate (#4188 area 6) QString m_authToken; // shared-secret gate; empty = open (#3646) // Log/event channel (#3646 observability suite). The tap fills m_logRing // from arbitrary logging threads; the main thread reads it for tail/drain. diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index ddb0e7025..77011fd28 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -2665,6 +2665,8 @@ void MainWindow::wireRadioSetupDialogSignals(RadioSetupDialog* dlg, const QStrin [this](const QString& tok) { setAutomationBridgeToken(tok); }); connect(dlg, &RadioSetupDialog::automationBridgeTxAllowedChanged, this, [this](bool allowed) { setAutomationTxAllowed(allowed); }); + connect(dlg, &RadioSetupDialog::automationBridgeReadOnlyChanged, this, + [this](bool readOnly) { setAutomationReadOnly(readOnly); }); // serialSettingsChanged is the "external-device settings changed" signal in // practice — the dialog emits it for serial-port, FlexControl, Ulanzi-dial, // and HID-encoder edits. The Ulanzi/HID branches below run regardless of diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index ab9726bb9..a6561b950 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -230,6 +230,9 @@ class MainWindow : public QMainWindow { // Persist the TX-via-MCP opt-in and push it live (Radio Setup → Network). // Enabling arms the force-unkey watchdog; disabling force-unkeys the radio. void setAutomationTxAllowed(bool allowed); + // Persist the observe-only opt-in and push it live (Radio Setup → Network). + // When set, the bridge refuses every mutating verb (#4188 area 6). + void setAutomationReadOnly(bool readOnly); protected: void showEvent(QShowEvent* event) override; diff --git a/src/gui/MainWindow_Session.cpp b/src/gui/MainWindow_Session.cpp index 7bf3333c1..0e5591a70 100644 --- a/src/gui/MainWindow_Session.cpp +++ b/src/gui/MainWindow_Session.cpp @@ -1663,6 +1663,12 @@ bool MainWindow::startAutomationBridge(const QString& sockName) guard->setTxAllowed( qEnvironmentVariableIsSet("AETHER_AUTOMATION_ALLOW_TX") || AutomationBridgeSettings::txAllowed()); + // Observe-only gate (#4188) — apply the persisted operator opt-in after + // start() so the bridge comes up read-only if the box is checked. An env + // override lets headless/CI pin the bridge observe-only regardless. + guard->setReadOnly( + qEnvironmentVariableIsSet("AETHER_AUTOMATION_READONLY") + || AutomationBridgeSettings::readOnly()); }); return true; // start initiated; the socket begins listening once the token resolves } @@ -1696,4 +1702,14 @@ void MainWindow::setAutomationTxAllowed(bool allowed) m_automation->setTxAllowed(allowed); } +void MainWindow::setAutomationReadOnly(bool readOnly) +{ + // Persist the operator opt-in (nested config) so it survives restart. + AutomationBridgeSettings::setReadOnly(readOnly); + // Push live so toggling observe-only takes effect on a running bridge + // immediately — no restart needed to arm or lift the gate. + if (m_automation) + m_automation->setReadOnly(readOnly); +} + } // namespace AetherSDR diff --git a/src/gui/RadioSetupDialog.cpp b/src/gui/RadioSetupDialog.cpp index 86ef08855..6fcf15f64 100644 --- a/src/gui/RadioSetupDialog.cpp +++ b/src/gui/RadioSetupDialog.cpp @@ -1570,7 +1570,42 @@ QWidget* RadioSetupDialog::buildNetworkTab() grid->addWidget(txCheck, 3, 1); } - grid->addWidget(new QLabel("Network MTU:"), 4, 0); + // Observe only — the read-only gate (#4188 area 6). When checked, the + // bridge refuses every mutating verb (set/invoke/connect/tune/capture…) + // and answers only pure-introspection reads. Enforced in the bridge, so + // no MCP client can flip it off. Lets the operator start the app, arm + // observe-only, then start the MCP server for a look-but-don't-touch + // session. An env override (AETHER_AUTOMATION_READONLY) pins it for + // headless/CI runs. + { + grid->addWidget(new QLabel("Observe only:"), 4, 0); + auto* roCheck = new QCheckBox("Read-only (block all driving)"); + roCheck->setObjectName(QStringLiteral("automationReadOnlyCheck")); + const bool envForcesRo = qEnvironmentVariableIsSet("AETHER_AUTOMATION_READONLY"); + roCheck->setChecked(AutomationBridgeSettings::readOnly() || envForcesRo); + roCheck->setToolTip( + "Make the bridge observe-only: MCP clients can read state\n" + "(ping/whoami/get/dumpTree/grab, read-only log and streams\n" + "actions, floors, and hitTest)\n" + "but every mutating verb is refused. Enforced in the app, so a\n" + "client cannot bypass it. Toggle takes effect immediately on the\n" + "running bridge. See docs/automation-bridge.md."); + AetherSDR::ThemeManager::instance().applyStyleSheet(roCheck, + "QCheckBox { color: {{color.text.primary}}; font-size: 11px; }" + "QCheckBox::indicator { width: 14px; height: 14px; }"); + if (envForcesRo) { + roCheck->setEnabled(false); + roCheck->setToolTip(roCheck->toolTip() + + "\n\nForced on by the AETHER_AUTOMATION_READONLY launch variable."); + } + connect(roCheck, &QCheckBox::toggled, this, [this](bool on) { + AutomationBridgeSettings::setReadOnly(on); + emit automationBridgeReadOnlyChanged(on); + }); + grid->addWidget(roCheck, 4, 1); + } + + grid->addWidget(new QLabel("Network MTU:"), 5, 0); auto* mtuSpin = new QSpinBox; mtuSpin->setRange(576, 9000); mtuSpin->setValue(AppSettings::instance().value("NetworkMtu", "1450").toInt()); @@ -1582,7 +1617,7 @@ QWidget* RadioSetupDialog::buildNetworkTab() AppSettings::instance().setValue("NetworkMtu", QString::number(val)); AppSettings::instance().save(); }); - grid->addWidget(mtuSpin, 4, 1); + grid->addWidget(mtuSpin, 5, 1); // VITA-49 UDP receive buffer (SO_RCVBUF). Snap-to-preset slider; the // kernel clamps the grant at net.core.rmem_max, so we show the granted @@ -1599,7 +1634,7 @@ QWidget* RadioSetupDialog::buildNetworkTab() return QStringLiteral("%1 KB").arg(b / 1024); }; - grid->addWidget(new QLabel("VITA-49 RX buffer:"), 5, 0); + grid->addWidget(new QLabel("VITA-49 RX buffer:"), 6, 0); auto* bufRow = new QWidget; auto* bufLay = new QHBoxLayout(bufRow); bufLay->setContentsMargins(0, 0, 0, 0); @@ -1627,7 +1662,7 @@ QWidget* RadioSetupDialog::buildNetworkTab() bufValLabel->setMinimumWidth(48); bufLay->addWidget(bufSlider, 1); bufLay->addWidget(bufValLabel); - grid->addWidget(bufRow, 5, 1); + grid->addWidget(bufRow, 6, 1); auto* bufGrantedLabel = new QLabel; if (m_model && m_model->panStream()) { @@ -1635,7 +1670,7 @@ QWidget* RadioSetupDialog::buildNetworkTab() bufGrantedLabel->setText(g > 0 ? QString("granted: %1").arg(fmtBytes(g)) : QStringLiteral("granted: — (applies on connect)")); } - grid->addWidget(bufGrantedLabel, 6, 1); + grid->addWidget(bufGrantedLabel, 7, 1); connect(bufSlider, &QSlider::valueChanged, this, [this, bufValLabel, fmtBytes](int idx) { diff --git a/src/gui/RadioSetupDialog.h b/src/gui/RadioSetupDialog.h index 2755fb014..5a2539718 100644 --- a/src/gui/RadioSetupDialog.h +++ b/src/gui/RadioSetupDialog.h @@ -69,6 +69,10 @@ class RadioSetupDialog : public PersistentDialog { // one-time confirmation dialog). MainWindow persists it and pushes it to // the running bridge — enabling arms the force-unkey watchdog. void automationBridgeTxAllowedChanged(bool allowed); + // Fired when the user toggles "Observe only" in the Network tab. MainWindow + // persists it and pushes it to the running bridge, which then refuses every + // mutating verb (#4188 area 6) — MCP clients can read but not drive. + void automationBridgeReadOnlyChanged(bool readOnly); protected: void closeEvent(QCloseEvent* event) override; diff --git a/tools/aether_mcp.py b/tools/aether_mcp.py index 2b5da1539..9a9c92285 100644 --- a/tools/aether_mcp.py +++ b/tools/aether_mcp.py @@ -546,6 +546,10 @@ def handle_tool(name, args): try: pong = bridge_request({"cmd": "ping"}, timeout=10) status["bridge_auth_required"] = pong.get("authRequired") + # Observe-only gate (#4188 area 6). The bridge is authoritative; + # this just reflects it so a client knows up front that mutating + # verbs will be refused. Flip it in Radio Setup → Network. + status["bridge_read_only"] = pong.get("readOnly", False) except Exception as e: # noqa: BLE001 status["ping_error"] = str(e) # whoami is auth-gated — its success confirms our token is accepted. @@ -560,6 +564,12 @@ def handle_tool(name, args): "Copy the token from Radio Setup → Network → " "Access Token and set it in this MCP server's " "env config.") + if status.get("bridge_read_only"): + status["read_only_note"] = ( + "This bridge is observe-only. Read verbs work; every " + "mutating verb (set/invoke/connect/tune/capture…) is " + "refused by the app. Uncheck \"Observe only\" in Radio " + "Setup → Network to allow driving.") if not entries and not os.environ.get("AETHER_MCP_SOCKET"): status["hint"] = ("No bridge running. Launch AetherSDR with " "AETHER_AUTOMATION=1 or enable it in Radio Setup " diff --git a/tools/test_aether_mcp.py b/tools/test_aether_mcp.py index 6fe1ca991..0ffc39694 100644 --- a/tools/test_aether_mcp.py +++ b/tools/test_aether_mcp.py @@ -330,6 +330,40 @@ def test_prompts_and_resources(): check("unknown prompt → error", "error" in bad, str(bad)) +def test_read_only_reflected(): + # bridge_status must reflect the bridge's observe-only state (#4188 area 6) + # from the token-free ping — the bridge is authoritative; the server only + # mirrors it. readOnly:true → bridge_read_only + a read_only_note hint. + orig = aether_mcp.bridge_request + os.environ["AETHER_MCP_SOCKET"] = "/tmp/does-not-need-to-exist" + + def ro(obj, timeout=None): + if obj.get("cmd") == "ping": + return {"ok": True, "authRequired": False, "readOnly": True} + if obj.get("cmd") == "whoami": + return {"ok": True, "pid": 1, "readOnly": True} + return {"ok": True} + + aether_mcp.bridge_request = ro + try: + r = json.loads(aether_mcp.handle_tool("bridge_status", {})["content"][0]["text"]) + check("bridge_status reflects readOnly", r.get("bridge_read_only") is True, str(r)) + check("bridge_status adds read_only_note", "read_only_note" in r, str(r)) + + # readOnly absent/false → no note, mirroring an unlocked bridge. + def rw(obj, timeout=None): + if obj.get("cmd") == "ping": + return {"ok": True, "authRequired": False, "readOnly": False} + return {"ok": True} + aether_mcp.bridge_request = rw + r = json.loads(aether_mcp.handle_tool("bridge_status", {})["content"][0]["text"]) + check("bridge_status read-write has no note", + r.get("bridge_read_only") is False and "read_only_note" not in r, str(r)) + finally: + aether_mcp.bridge_request = orig + os.environ.pop("AETHER_MCP_SOCKET", None) + + if __name__ == "__main__": test_field_mapping() test_token_attached() @@ -337,6 +371,7 @@ def test_prompts_and_resources(): test_robustness_tools() test_fuzzy_suggest() test_prompts_and_resources() + test_read_only_reflected() if _failures: print(f"\n{len(_failures)} FAILED: {_failures}") sys.exit(1)