Files
amethyst/commons
Claude 025116acc8 fix(marmot): five defects from the pre-merge audit
A review of the full branch diff turned up five, all verified against the code
before changing anything.

**A departure gate nothing ever cleared.** `leaveGroup` raises a durable
`LEAVING` gate and the only `clearGate` calls were inside
`resolveDisbandRequest`, so a member who left and was invited back held a gate
against a membership that no longer existed. Harmless until this branch, where
gates began blocking outbound work AND surviving restarts — which turned it
into a group that reads fine and can never be written to again, permanently. An
authenticated re-join now clears it, which is the rule `REMOVED` already
stated.

**An SSRF hole in IPv6 avatar hosts.** `isNonRoutableIpv6` compared TEXT, so
`::1` was caught and `0:0:0:0:0:0:0:1` — the same address, expanded — was not,
and `::ffff:127.0.0.1` shares no prefix with anything it looked for. A group
avatar URL could make every member fetch from their own machine. Addresses are
now parsed to their 16 bytes and judged numerically, with IPv4-mapped and
-compatible forms delegated to the existing IPv4 rules and an unparseable
literal refused rather than waved through.

**A message on a branch the group then adopted was never rendered.** An app
payload that decrypted only on a candidate branch had its id recorded as
processed, so a later redelivery hit the `Duplicate` early-return — even though
that result is itself a witness FOR the branch, which convergence may go on to
select. It is now retryable like `UndecryptableOuterLayer`. Safe to
re-process: witnesses are a set keyed by sender, so a resent payload adds
nothing to a branch's standing.

**One dropped socket wedged a group until app restart.** An unconfirmed publish
pins `PendingPublish`, and the only caller of `retryPendingPublishObligations`
was `restoreAll`. A blocked commit now retries that group's obligations on its
way through, so the next attempt is the recovery.

`MarmotPublishBeforeApplyTest` measured "no replacement commit" by counting
sends, which the retry breaks without violating anything: the re-send carries
the SAME event id, and a fork means a second DIFFERENT commit for the held
epoch. It now counts distinct ids, which is the property it always meant.

**`forget()` left two of the gate's three copies behind.** It dropped the map
but not the snapshot non-suspending readers see, nor the record on disk, so
`restore()` resurrected a gate for a forgotten group. Latent — no production
caller yet.

11,001 tests green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kCuA6tc4JQzHPCDd39GHq
2026-09-11 02:53:37 +00:00
..