diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-12 10:08:36 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-12 16:36:54 +0200 |
| commit | 28548dcbde50f7cee471d6962e16115bb87b056f (patch) | |
| tree | 0ba9e8c85ef77d327d8b5bcaee4b64d518e71ebe | |
| parent | 02f061ee2c1824734bf63c91d39b47848926f59c (diff) | |
| download | meshbay-28548dcbde50f7cee471d6962e16115bb87b056f.tar.gz | |
docs: AV9–AV12, and the two shapes that keep producing them
The register grows by the four findings from users.py, and gains the pattern
underneath several of them: a limit written on one of several equivalent paths
(three endpoints send mail, one had no limit; every list in admin.py is
bounded, the two outside it were not; the group claim was bounded at
registration and not on reload), and a bound that counts the wrong thing (a
per-IP rate limit bounds a caller, never the mailbox that receives what they
cause).
Also records that admin.py was read under the lens and needed nothing, so the
next reader does not re-derive that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
| -rw-r--r-- | CLAUDE.md | 11 | ||||
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 22 |
2 files changed, 31 insertions, 2 deletions
@@ -122,7 +122,7 @@ that produced it. | Looking for | Read | |---|---| | 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 | +| What one member can cost the others (`AV1`–`AV12`) | §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 | @@ -148,6 +148,15 @@ These are about working on the tree rather than about the design: 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** +- **A blocking call in an async handler is the whole instance's problem.** + `mail._send` is `smtplib` with a ten-second timeout, and it was called + straight from four handlers: while the MTA thought about it, nothing else was + served, no node socket read, no offer relayed. It has no symptom a test + catches — everything works, slowly, for everyone, whenever the mail server is + having a bad day. `mail.send_off_loop` is the door, and + `test_no_mail_is_sent_from_the_event_loop` reads the source for direct calls, + because there is nothing else to read + - **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 diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index bda33f1..16e728f 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -2473,7 +2473,8 @@ Two structural recommendations from that review stand as rules: ### 13.5b Availability between members (`AV`) -**Added 2026-09-12, after the finding that produced it.** The first three +**Added 2026-09-12, after the finding that produced it, and extended the same +day when `users.py` and `admin.py` were read under it.** 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** — and that is the question a group platform lives @@ -2489,6 +2490,21 @@ else bears the cost?** Where the answer is "someone other than the sender", there must be a ceiling, and it must apply on every path that writes the state — not only the one where the ceiling was first thought of. +Two shapes recur, and are worth naming because each accounts for several +entries. **A limit written on one of several equivalent paths** — three +endpoints send mail and one of them had neither a rate limit nor a captcha; +every list in `admin.py` is bounded and the two outside it were not; the group +claim was bounded at registration and not on reload. And **a bound that counts +the wrong thing** — a per-IP rate limit bounds a caller, never the mailbox that +receives what they cause, which is why `AV10` is a cooldown per *account* under +a limit per IP rather than a tighter limit. + +`admin.py` was read under this lens and needed nothing. Its moderator/admin +line is drawn explicitly — a moderator may not change a role, may not revoke, +and may not touch an admin's account at all — self-modification is refused, and +every list it serves is bounded. It is the part of the hub where the question +had already been asked. + | Label | The rule it names | |---|---| | **AV1** | **An empty claim is a claim on nothing.** A node's group set is `authorized ∩ claimed`, and an absent or empty `group_ids` registers it for no group rather than all of its owner's — on registration and on `update_groups` alike (§7.2) | @@ -2499,6 +2515,10 @@ there must be a ceiling, and it must apply on every path that writes the state | **AV6** | **A relay proves possession of its approved key.** A public key is not a password, and the register call is unauthenticated by design — it is not a user — so the proof is the only thing standing between a stranger and where nodes send relayed traffic | | **AV7** | **A node bounds how many peers it holds and how long an unproven one lasts.** The hub's cap is per calling account, which is a limit on each member and not on the machine, so without this an operator's exposure grew with the size of their groups | | **AV8** | **One account cannot make the hub mail another at will.** The invitation email's subject comes from the group row, never from the request, and the endpoint is metered | +| **AV9** | **No mail is sent from the event loop.** `smtplib` is synchronous and waits up to ten seconds; called from an async handler that wait is the whole instance's, not one request's. Every send goes through `mail.send_off_loop` | +| **AV10** | **Every path that makes the hub send mail is metered, per account.** A rate limit that counts by IP bounds a caller, not an inbox. Changing one's address mails an arbitrary stranger, so it carries a cooldown as well as a limit; so does a reset request | +| **AV11** | **A namespace a client writes into is closed, and its rows are capped.** The preference key space is an allow-list plus `default_tab:<group_id>` checked as a group id, the value is length-bounded, and the row count per account is bounded | +| **AV12** | **Every list has an upper bound on `limit` and a floor under `offset`.** Including the ones that take no authentication at all | ### 13.6 Chat design findings |