Interop driver: report the session version a responder settled on

`session_respond` returned no version at all, so the cross-engine test
could not assert the one thing that mattered — that a session this
device ANSWERS agrees with the initiator about which protocol it runs.
`session_initiate` and `session_encrypt` report it too, so a version
that changes across a store-and-reopen is visible rather than inferred.

Sealed groups: file the message instead of dropping it in silence.
`groupRoster` asked the relay for the membership of a group the relay
cannot know — §8.9b exists so it holds opaque tags and no member list —
so the sender was never "in the roster" and EVERY message in EVERY
sealed group was discarded on arrival by the check meant to protect it.
The sealed roster is the authoritative list and the only one that
exists. A refusal now says which group and which sender.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-01 09:21:19 -04:00
co-authored by Claude Opus 4.8
parent ee17d773b7
commit 2402f7a665
2 changed files with 30 additions and 3 deletions
@@ -762,6 +762,13 @@ class Session(
if (gid != null) {
val known = groupRoster(gid)
if (known == null || message.fromUser !in known) {
// ⚠️ Refusing is correct — but refusing in silence is what
// made a sealed group look like a group nobody could talk
// in. Say which group and which sender, never the message.
System.err.println(
"fnmsg-recv: NOT FILED into group ${gid.take(8)} — " +
"sender ${message.fromUser} is not in the roster " +
"(roster ${known?.size ?: -1} member(s))")
continue
}
// ⚠️ Every group envelope carries the group's name, and this is
@@ -1244,6 +1251,16 @@ class Session(
@Volatile var groupNamesUpdated: Boolean = false
private fun groupRoster(gid: String): Set<Long>? {
// 🔴 **A SEALED group's roster is not the relay's to give.** §8.9b
// exists so the relay holds opaque tags and no membership at all, so
// `listGroups()` returns an EMPTY member list for one — and the caller
// reads that as "the sender is not a member" and drops the message.
// Every message in every sealed group was discarded on arrival, in
// silence, by the check meant to protect it. The sealed roster is the
// authoritative list here and the only one that exists.
if (vault.groupSecrets(gid) != null) {
return sealedGroups().firstOrNull { it.id == gid }?.memberIds?.toSet()
}
val now = System.currentTimeMillis()
val cached = groupRosterCache
if (cached != null && now - cached.first < 30_000) return cached.second[gid]