aboutsummaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-12 09:47:46 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-12 16:36:54 +0200
commit7c3a1d6fd765ef0421d0f3d85e512e10ea2f6f87 (patch)
tree00d05d94cbe1216085ffa94173b6643cd50c8b1e /CLAUDE.md
parentbf7ff9ec311660318c8562abe03ef4db62475c98 (diff)
downloadmeshbay-7c3a1d6fd765ef0421d0f3d85e512e10ea2f6f87.tar.gz
docs: availability between members is a finding category
The first three reviews asked who can read what, who can impersonate whom, and what a hostile node can forge. None asked what a legitimate but misconfigured or careless member costs everyone else — which is the question a group platform lives on, because every member was invited by someone who trusted them and none of them is an attacker. C2 had asked "can a node claim a group its owner is not in?" and the answer was correctly no. Nobody had asked what happens when a node claims one its owner *is* in but does not host, which is how a group went dark for all of its members with its real host online throughout. §13.5b is the register, AV1 to AV8. The lens, for anything reviewed from here: a participant supplies input; if anyone other than the sender bears the cost, there is a ceiling to write, and it goes on every path that writes the state. CLAUDE.md gets the working rule and the incident as a lesson, and the stale path to sync-ui.js corrected — it lives under scripts/, not build/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md48
1 files changed, 44 insertions, 4 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index f2a3363..b6fbd4c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -121,7 +121,8 @@ that produced it.
| Looking for | Read |
|---|---|
-| What a label means (`C1`, `H3`, `NS6`, `T3`, `C5b`, `W2`, `E9`, `F1`, …) | `docs/MESHBAY_DESIGN.md` §13 |
+| What a label means (`C1`, `H3`, `NS6`, `T3`, `C5b`, `W2`, `E9`, `F1`, `AV4`, …) | `docs/MESHBAY_DESIGN.md` §13 |
+| What one member can cost the others (`AV1`–`AV8`) | §13.5b — the newest category, and the one the first three reviews had no question for |
| Trust model, and what the project may and may not claim | §2 |
| Identity, devices, admission, recovery, the keypair bundle | §3 |
| Cryptography, key hierarchy, the group and chat envelopes | §4 |
@@ -144,9 +145,16 @@ These are about working on the tree rather than about the design:
`/a/<hash>/` against `meshbay_hub.api.webapp.ASSET_V` before concluding a fix
is missing. `site/` and the Caddy config have never been deployed
- **One UI source.** `packages/meshbay-hub/src/meshbay_hub/static/` is the
- interface, for the web and the app alike; `packages/meshbay-client/build/
- sync-ui.js` copies it and CI fails if the copy drifts — **never edit
- `packages/meshbay-client/ui/` by hand**
+ interface, for the web and the app alike; `packages/meshbay-client/scripts/
+ sync-ui.js` copies it (`npm run sync-ui`) and CI fails if the copy drifts —
+ **never edit `packages/meshbay-client/ui/` by hand**
+- **Before adding an endpoint or a message, ask who pays.** A participant
+ supplies input; if anyone other than the sender bears the cost, there is a
+ ceiling to write, and it goes on **every** path that writes the state — the
+ gap between `_authorize_node_ws` and `update_groups` was one message wide.
+ `docs/MESHBAY_DESIGN.md` §13.5b is the register; `test_availability_between_
+ members.py` is where a new case goes, and every test in it is two accounts,
+ because a one-member test proves a one-member property
- **Never change the KDF parameters in one place.** `keyderive.js`,
`keyderive.py`, the QE harness and `test_bundle_kdf_parity.py` are held
byte-identical by that test, and a mismatch does not look like an error — it
@@ -590,6 +598,38 @@ do. Read them before writing anything that touches the same mechanism.
needs a person with a large file. Confirmed by the operator on 2026-08-15:
Firefox, 180 MB, written to disk. Nothing multi-gigabyte has been measured
+- **An empty claim read as a claim on everything, and a list taken as a
+ ranking.** Two defaults, in two codebases, a month old and harmless
+ separately. The hub resolved a node's group claim with `set(claimed_groups or
+ authorized)`, and the node omitted `group_ids` entirely when it hosted
+ nothing — so "I host no groups" arrived as "I host all of this account's
+ groups", other members' included. Such a node can serve none of them: it has
+ no GEK, and its own handshake refuses them. But `/v1/groups/{id}/nodes`
+ answers in **registration order**, `_node_groups` is in-memory, and
+ GroupPage took `nodes[0]` with no fallback — so the group opened or did not
+ depending on who reconnected first after a hub restart. On 2026-09-11 a hub
+ deploy at 20:14 did exactly that: this node needed 16 s to come back (502
+ while the hub restarts, then its access token had expired during the outage,
+ so a refusal and two 5 s backoffs), a second member's unconfigured node won
+ the race, and a group went dark for everyone with its only real host online
+ the whole time. **A member could take any of their groups down, by accident,
+ by leaving an empty node running.**
+ Four things worth keeping. The node's own logs and audit database were the
+ evidence that cleared it — every handshake OK, no refusal recorded since
+ August — which is what said the refusal came from somewhere else entirely;
+ believing the error message names the machine you are standing on is how an
+ hour goes. `git log -S` on the three suspect lines dated them all to August,
+ which is what turned "what did I just break" into "what changed around it":
+ the answer was a restart, not a commit. The read-only probe that settled it
+ is four lines — the node's own keystore, a node-scoped token, `/v1/groups/mine`
+ and `/v1/groups/{id}/nodes` — and node-scoped tokens are accepted by
+ `get_current_user`, so a node can ask the hub what the hub thinks of it.
+ And the rule under all of it: **a falsy empty collection must never mean
+ "unspecified"** — `if gids:` and `or authorized` are the same mistake written
+ twice, and the ceiling that made C2 hold on connection was stepped over one
+ message later by `update_groups`, which assigned its list verbatim. A limit
+ enforced on one path is not enforced
+
**Corrections that used to live here** — `punch_nat()` is not a traversal stack,
the node keystore's Argon2id parameters, what group chat actually uses, and what
is sealed on the wire — are now design statements in `docs/MESHBAY_DESIGN.md`