summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/hub_client.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-12 09:47:07 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-12 16:36:54 +0200
commit4cce50f09a73739387d5058a6f8183ebac65ae2c (patch)
tree30e9d72280295d913983910a956b0d3306696ec8 /packages/meshbay-node/src/meshbay_node/hub_client.py
parent9cc2909cb4a360c81b471ceab1d9578a7655a88e (diff)
downloadmeshbay-4cce50f09a73739387d5058a6f8183ebac65ae2c.tar.gz
fix: an empty group claim is a claim on nothing
A node that hosts no groups sends no `group_ids` on its hub socket, and the hub resolved the claim with `set(claimed_groups or authorized)` — so "I host nothing" arrived as "I host every group this account belongs to", other members' included. Such a node can serve none of them: it holds no GEK, and its own handshake refuses them with "Group not hosted on this node". `/v1/groups/{id}/nodes` answers in registration order and `_node_groups` is in-memory, so which node a client was sent to depended on who reconnected first after a hub restart. GroupPage took `nodes[0]` with no fallback. On 2026-09-11 a hub deploy at 20:14 reshuffled the registry, a second member's unconfigured node won the race, and a group stopped opening for everyone in it with its only real host online throughout. Any member could take one of their groups down, by accident, by leaving an empty node running. Four changes, because no one of them is sufficient: - the hub never widens an absent claim, and `update_groups` goes through the same ceiling as registration — it assigned its list verbatim, so the bound that makes C2 hold at authentication was one message wide - the node states the empty set rather than omitting the field - the refusal carries `not_hosted`, so a client can tell "try the next node" from "you, here, must do something first" - GroupPage walks the list instead of indexing into it The three lines involved date from 13, 20 and 23 August and each is defensible alone. The defect is in the seam, which is where the last two also were: a falsy empty collection must never mean "unspecified". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/hub_client.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/hub_client.py10
1 files changed, 8 insertions, 2 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py
index 8873958..ef12bb4 100644
--- a/packages/meshbay-node/src/meshbay_node/hub_client.py
+++ b/packages/meshbay-node/src/meshbay_node/hub_client.py
@@ -319,9 +319,15 @@ class HubClient:
"token": self._session.access_token,
"node_id": self._session.node_id,
}
+ # `if gids:` here, and the hub read a missing key as "claims
+ # every group this account belongs to" — so a node hosting
+ # nothing advertised itself for all of them and swallowed
+ # their traffic. The hub no longer widens an absent claim,
+ # and this says the empty set out loud rather than by
+ # omission: hosting nothing is a fact, not a missing field.
gids = group_ids() if callable(group_ids) else group_ids
- if gids:
- auth_msg["group_ids"] = gids
+ if gids is not None:
+ auth_msg["group_ids"] = list(gids)
await ws.send(json.dumps(auth_msg))
# Bounded: a hub that accepts the socket and then says nothing
# — which is what it does for a few seconds while restarting —