a42499435b
When a group installs a `required_capabilities` extension (every Marmot group does, listing MarmotGroupData=0xF2EE, SelfRemove=0x000A, Basic credential), every member's leaf MUST advertise those types in its own [Capabilities]. We were validating none of that: - `applyProposalAdd` checked version + ciphersuite but never matched the new KP's capabilities against the group's required_capabilities. A non-conformant member silently joined; the next commit that touched their leaf got rejected by spec-conformant peers, splitting the group. - `processWelcome` similarly never checked the joiner's own KP against the group's required set, nor did it sweep existing members' leaves. A misconfigured GroupInfo signer could invite us into an incoherent group whose first commit we'd silently reject forever. Adds two helpers in `MlsGroup.Companion`: - `findRequiredCapabilities(extensions)`: decodes the §7.2 struct from a GroupContext extension list, or returns null if absent. - `requireCapabilitiesMeetRequirements(caps, req, who)`: throws with a specific (extensions=, proposals=, credentials=) diff naming the missing types — turns silent interop breaks into one debuggable line. Wires the gate in two places: - `applyProposalAdd` rejects the Add proposal if the new leaf falls short of the group's required set. - `processWelcome` rejects the join if either (a) our own KP doesn't meet the group's requirements or (b) any existing member's leaf doesn't — the latter catches a malformed GroupInfo at join time. 5 new tests: round-trip decoding, rejection on missing extension / proposal, acceptance when caps are a superset, and an end-to-end `addMember` rejection of a hand-crafted KP with SelfRemove stripped (re-signed so the rejection is from the capability gate, not the signature check). Quartz test suite green; marmot interop 16/16. Closes audit gaps #4, #5, #10. https://claude.ai/code/session_013VYkpz8P1mPh9Ejxy9anhJ