From 32a86417d0edc3bf5c4cf859243c9e34b29bf1ef Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 10 Sep 2026 19:26:10 +0200 Subject: fix(node): the handshake ack dropped one app's directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ack was assembled from its own tuple of application names, a copy of the daemon's `APP_DIR_KEYS`, and the two had drifted: the copy was missing `helloworld`. So the reference application — the one that exists to prove a new application needs no special-casing — was the single application whose configured folders never reached a client, which made the plugin claim false exactly where it is demonstrated. Fixed by removing the copy rather than syncing it. The ack now emits whatever `_directories` the group context carries, and `_app_directories_ctx` is the only thing that puts one there, so the two cannot disagree again. The transport names an application in one place, `ALLOWED_APPS`, which is enforcement rather than a directory list. The client had the same fault one layer up: `group-page.js` read three names by hand from the ack while the live-update path beside it was already generic. It derives the map from the ack's own keys now, so the fix reaches the settings pane instead of stopping at the wire. A first attempt moved the list to `roster.py`, where directory *storage* lives, and `test_helloworld_proves_the_plugin_claim.py` refused it: the roster, the ops, the config and the root set must name no application at all. That test is the architecture's own guard and it was right — the list belongs on the daemon, which is what wires a group's context, and everything downstream is derived from it. Two new tests, both verified to fail against the previous shape: the ack carries an application the node names nowhere else, and the ack keeps no list of its own. `test_the_lists_are_read_under_one_name_each` now asserts the shell names no application rather than that it names exactly three. Two stale comments went with it — the ack's, which described scalars removed in 07ff8b4, and the client's, which said those scalars still rode the wire for MNP 1.0 peers that can no longer connect. Full suite: 2258 passed, 4 skipped. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YVoHVCcfBqud6ZjG4db3y7 --- .../src/meshbay_node/transport/webrtc_server.py | 24 +++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py') diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 54f1541..a0776d2 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -897,9 +897,7 @@ class WebRTCPeerSession: # safe for the reason those were not: it is *derived* from this list # on every build rather than stored beside it, so the two cannot # drift apart. - **{f"{app}_directories": - list(self._group_ctx().get(f"{app}_directories") or []) - for app in ("video", "music", "photo", "chat")}, + **self._app_directories_ack(), # Where chat attachments are written — the singular form, because # Chat genuinely has one destination. "" means the operator has not # chosen, and the paperclip says so. @@ -3182,6 +3180,26 @@ class WebRTCPeerSession: task.add_done_callback(_on_done) return task + def _app_directories_ack(self) -> dict: + """ + Every application's configured folders, for the handshake ack. + + Read off the group context rather than from a list of applications kept + here, so this cannot name an application the node knows nothing else + about — and cannot fail to name one the daemon does. A copy of the + daemon's `APP_DIR_KEYS` lived here until 2026-09-10 and had already lost + an entry, which made the app that entry belonged to the single one whose + directories never reached a client. This module names an application in + exactly one place, and it is `ALLOWED_APPS`. + + `_app_directories_ctx` is the only thing that puts a `*_directories` key + in that context, and an absent one reads as none configured — never as + "the whole group index". + """ + return {key: list(value or []) + for key, value in self._group_ctx().items() + if key.endswith("_directories")} + def _group_ctx(self) -> dict: if "groups" in self._ctx and self._group_id: # `.get`, not a bare subscript. A config reload removes a group -- cgit v1.2.3