diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-07 18:03:52 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-07 18:03:52 +0200 |
| commit | e1383e1d545b994f4ad61694f868339defb0bdef (patch) | |
| tree | 67a3b1933f2a9c5caf107d01d9ff91c44a975df6 /CLAUDE.md | |
| parent | 36cebf25d0e0f24cf63be4380ccb5d03da726a74 (diff) | |
| parent | 8980a8e42d94ab7c0bc9739283d39f938f8402b0 (diff) | |
| download | meshbay-e1383e1d545b994f4ad61694f868339defb0bdef.tar.gz | |
Merge origin/main into the chat encryption work
Both sides landed a breaking MNP change and both called it 2.0, which is right:
the sealed upload, the removal of `stream_seg` and mandatory chat encryption
share one flag day. They are recorded as one version in `__init__.py` rather
than as a race between two.
The resolutions that were decisions rather than mechanics:
* **`MNP_MIN_SUPPORTED` moves to "2.0".** The sealed upload alone was a
*confined* break — a 1.x peer could still connect, browse, download, stream
and chat, with only its uploads refused by `upload_not_sealed` — so the floor
deliberately stayed at "1.0". Mandatory chat encryption ends that
confinement: a 1.x peer can neither produce a sealed chat message nor read
one, so it would connect, look fine, and be unable to say anything. Refusing
it at the handshake is the honest form. The per-message `upload_not_sealed`
path is untouched and still right if the floor is ever lowered.
* **`sendChat` throws on an `error` reply**, from origin, applied to the sealed
send. It matters more after this change, not less: the node now refuses a
stale epoch, a malformed envelope and a device claim that is not the
connection's own, so there are three new ways for a message to be rejected
and none of them may look like a message that was sent.
* **`req_id` supersedes the per-type routing** this branch added for
`chat_keys_resp` and `device_hello_ack`. Both blocks are kept beside the
existing `chat_hist_resp` one, for the same stated reason — a node too old to
stamp — and their comments no longer claim to be the mechanism that closes
the class. `req_id` is.
* **`chat_send_probe.py` is rebuilt on origin's structure**, not beside it: two
scenarios, a stub that stamps `req_id`, `music_meta_req` as the older pending
request. The encrypted path is layered on — a real Ed25519 device key
generated in the page, and a `chat_keys_resp` sealed by the shipped Python,
because a payload the page built itself would prove only that the page agrees
with the page.
* **`test_reply_correlation.py` now sends a sealed message.** Its subject is
which of the two messages leaving that handler carries the id; plaintext chat
was only the fixture, and the node refuses one now.
* `groupbox` keeps both new purposes (`upload`, `chat_keys`); `protocol.py`
keeps origin's removal of `STREAM_SEGMENT` and this branch's correction of
the "Double Ratchet message" comment on `CHAT_MESSAGE`, which was wrong when
it was written and is wrong differently now.
Full suite on the merged tree: 1993 passed, 11 failed — the same 11 that fail
on a pristine checkout (2 Windows service tests, 1 apps-enabled policy, 7
transcode tests that pass in isolation, and the WebRTC invite test that hangs
on its own).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZZxYjz8YeWRz13xDi8LJr
Diffstat (limited to 'CLAUDE.md')
| -rw-r--r-- | CLAUDE.md | 27 |
1 files changed, 22 insertions, 5 deletions
@@ -471,11 +471,28 @@ anything that assumes one key per person. next visit to the tab, since the node had stored it and answered. Every line of `chat-app.js` is correct and every routed message in `transport.js` is routed correctly; the defect is in the seam, which is why - `tests/harness/chat_send_probe.py` drives the two together. `ack` is now - matched by request type (`chat_msg`, or the keypair-bundle store/delete that - name themselves in `detail`). Anything left to the "oldest pending" guess is - a latent version of this bug: a request type deserves a key, and a reply - deserves something to key it by + `tests/harness/chat_send_probe.py` drives the two together. `ack` was matched + by request type (`chat_msg`, or the keypair-bundle store/delete that name + themselves in `detail`), and that closed the instance — **but it left the + class open, and it came back on 2026-09-06 through the other door.** A + refusal has no type of its own to key on: `_dispatch_message`'s catch-all + answers every unforeseen failure with `{"type": "error", "detail": "Request + failed"}`, and 238 of `webrtc_server.py`'s 240 error sends name nothing + either. So a chat send the node refused was routed by luck all over again — + same frozen composer, same 30 s, and rare enough (it needs an older request + still waiting, which a `music_meta_req` behind a failing third-party lookup + supplies for over a hundred seconds) to look like once every couple of days. + The keys were never the fix, only a workaround for a protocol that carried no + correlation id at all: `_seqId` existed, indexed `_pending`, and was never put + on the wire. It is now (`req_id`, see `protocol.py`) — the node stamps it on + the reply from `_send`, via a ContextVar so a handler's spawned work still + answers under the right id, and never on a broadcast, which answers nothing. + With that, the arrival-order fallback is gone for any node that stamps. + The lesson is not "key the replies": it is that **matching by arrival order + is a guess that fails silently and asymmetrically** — the victim is never + the request that was answered wrongly, it is the unrelated one that now + waits for a reply already delivered elsewhere. A reply needs an identifier + the protocol guarantees, not a field it happens to have - **A refusal that never rejects.** Denying Chromium's `fullscreen` permission does not make `requestFullscreen()` throw — the promise never settles. The |