Commit Graph

11814 Commits

Author SHA1 Message Date
Crowdin Bot 745f3781c3 New Crowdin translations by GitHub Action 2026-04-23 01:07:37 +00:00
Vitor Pamplona e12c7ca595 Merge pull request #2515 from vitorpamplona/claude/fix-marmot-reactions-gtBUg
Fix MIP-03 inner event verification in group messages
2026-04-22 21:06:04 -04:00
Vitor Pamplona f721142c73 Merge pull request #2514 from vitorpamplona/claude/grant-user-privileges-JxW5T
Add admin privilege management for Marmot group members
2026-04-22 20:49:15 -04:00
Claude 5106e572a5 fix: consume Marmot inner reactions/deletions as verified
MIP-03 inner events inside a kind:445 GroupEvent are unsigned (sig is
empty or placeholder), so LocalCache's `justVerify()` fails and
`consume(ReactionEvent)` skips the branch that attaches the reaction to
its target note via `addReaction()`. The same applies to kind:5
deletions. Side-channel inner events from other Marmot clients
(WhiteNoise in particular) were therefore silently dropped by
LocalCache, so an inbound emoji reaction never appeared on the target
chat bubble.

NIP-17 has the same "unsigned inner rumor" shape and already hands the
inner event to `justConsume(..., wasVerified = true)`. Align Marmot with
that contract at the two inbound call sites:
- `GroupEventHandler.add` (fresh kind:445 from relay)
- `Account` restore-from-disk loop (persisted plaintext at app start)

Authenticity is already guaranteed by the MLS layer:
`MarmotInboundProcessor.processPrivateMessage` rejects any inner event
whose `pubkey` doesn't match the MLS sender credential identity before
ever emitting an `ApplicationMessage`.

https://claude.ai/code/session_01KhVtLJAEkNCwpRWUUTqDFM
2026-04-23 00:34:51 +00:00
Claude e6e3120cd2 feat(marmot): let admins grant/revoke admin privileges from group info UI
Adds a star toggle next to each member in MarmotGroupInfoScreen that
admins can use to promote a non-admin to admin or demote a fellow
admin. Both paths show a confirmation dialog and publish a
GroupContextExtensions commit that rewrites admin_pubkeys. The toggle
is hidden for non-admin viewers and for the signer's own row (self
demotion must go through the leave flow). The demote button is also
hidden when the member is the only remaining admin so we never hit the
MIP-03 admin-depletion guard in MlsGroup at commit time.

Account.grantMarmotGroupAdmin / revokeMarmotGroupAdmin reuse
updateMarmotGroupMetadata so the new admin list ships alongside the
signer's outbox relays, keeping the canonical relay set consistent
with other metadata commits.
2026-04-23 00:28:54 +00:00
Vitor Pamplona 95f189338a Merge pull request #2513 from vitorpamplona/claude/fix-whitenoise-reactions-EWq2j
Filter non-displayable messages from Marmot group chat feed
2026-04-22 19:39:42 -04:00
Vitor Pamplona ca4459ca1d Merge pull request #2512 from vitorpamplona/claude/fix-marmot-relay-routing-VuVUp
Implement per-recipient relay routing for Marmot protocol
2026-04-22 19:38:58 -04:00
Claude 0657f6e1c7 fix(cli): apply Marmot relay-routing rules across every marmot command
The first pass only fixed `group add`. Auditing the rest of the CLI
against MIP-00..03 turned up three more spots where `amy` either
queried the wrong relays or silently skipped a required advertisement:

1. `marmot key-package check <npub>` used `anyRelays()` — i.e. the
   inviter's configured relays — so checking for a KeyPackage on a
   user who advertises `kind:10051` somewhere we don't know about
   always returned `not_found`. Now runs RecipientRelayFetcher against
   bootstrap seeds and fetches from the union of (target kind:10051,
   target kind:10002 write, bootstrap). Emits `found_on` so callers
   can see which relay served the hit.

2. `await key-package <npub>` had the same bug inside the poll loop.
   Resolved once up front, then the loop fetches from the target's
   advertised relays every tick. Throws an `AwaitTimeout` early if no
   relays can be discovered at all, instead of silently polling void.

3. `relay publish-lists` published kind:10002 + kind:10050 but never
   kind:10051. Per MIP-00 the KeyPackage Relay List is how other
   Marmot clients discover where our KPs live — without it the
   `key_package` bucket on disk is invisible to anyone else. Now also
   publishes kind:10051; falls back to the NIP-65 set if the bucket
   is empty so we never advertise an empty list. (`amy create`
   already publishes it via AccountBootstrapEvents.)

Docs: cli/README adds a "Relay routing" section that lists the exact
relay set used for publish vs fetch of every Marmot event kind, plus
the bootstrap-pool definition, so agents + interop-test authors can
reason about cross-user reachability without reading the code.
2026-04-22 22:15:10 +00:00
Claude c4e7e0d9b4 fix: don't render Marmot reactions/deletions as chat bubbles
WhiteNoise threads its kind:445 payloads across three inner kinds:
kind:9 for chat, kind:7 for emoji reactions, kind:5 for unreacts. The
Amethyst ingest pipeline was routing every inner event into the group
chatroom feed, so a kind:7 reaction rendered as a chat bubble whose
only content was the emoji, quoting the liked message via the target
`e` tag. That looked identical to a threaded reply. The actual kind:9
reply from `wn messages send --reply-to` never arrived, which made the
two look swapped in the UI.

Three changes to untangle this:

- MarmotGroupList.addMessage/restoreMessage now skip inner events with
  kind:5 and kind:7 before they enter `MarmotGroupChatroom.messages`.
  The reaction is still consumed by LocalCache so it attaches to the
  target note's reaction row, and the deletion still revokes that
  reaction — they just don't appear as standalone bubbles.
- LocalCache.computeReplyTo learned to derive thread parents for
  `ChatEvent` (kind:9) from plain NIP-10 `e` tags in addition to the
  existing NIP-18 `q` tag path. WhiteNoise emits `e`-tagged replies;
  without this the reply bubble had no quote context in the feed.
- tools/marmot-interop/marmot-interop.sh: `wn messages send` exposes
  its `reply_to` field through clap v4, which renames snake_case to
  kebab-case by default. The script was passing `--reply_to`, which
  clap rejected; the `|| true` + redirected stderr hid the error and
  no reply was ever published. Use `--reply-to`.

https://claude.ai/code/session_01K3g1uWLhByoEdBS77zdF32
2026-04-22 22:07:15 +00:00
Claude 5289fa3398 style(cli): import NormalizedRelayUrl + Amethyst defaults instead of inlining FQNs 2026-04-22 22:04:30 +00:00
Vitor Pamplona 7a932c03e3 Merge pull request #2511 from vitorpamplona/claude/fix-marmot-group-tabs-zLq2N
Implement known/new group classification based on follow set
2026-04-22 18:02:58 -04:00
Claude f6f2a34353 fix(cli): route Marmot welcome + KeyPackage fetch to the invitee's relays
`amy marmot group add` previously sent the Welcome gift wrap to the
inviter's own kind:10050 (ctx.inboxRelays()) and fetched the invitee's
KeyPackage from only the inviter's configured relays. Two users with
disjoint relay configurations could never successfully marmot each
other — the welcome landed somewhere the invitee never polled.

Mirror the Android Account.addMarmotGroupMember flow in the CLI:

- New RecipientRelayFetcher in quartz/marmot one-shot-drains a target
  user's kind:10050, kind:10051 and kind:10002 from a seed relay set.
  Replaceable-event semantics: newest created_at per kind wins. Guards
  against relays echoing events authored by someone else.
- Context.bootstrapRelays() unions the inviter's configured relays with
  AmethystDefaults (DefaultNIP65RelaySet + DefaultDMRelayList) so we can
  discover strangers even when nothing overlaps with our own config.
- GroupAddMemberCommand now per-invitee: looks up their relay lists;
  passes kind:10051 + kind:10002 write + bootstrap to KeyPackageFetcher;
  publishes the welcome to kind:10050 (fallback to NIP-65 read, then
  DefaultDMRelayList, then our outbox as belt-and-braces).

Group commits/messages (kind:445) continue to use the group's MIP-01
relay set — those were already correct. Exposes welcome_targets and
key_package_relays in the JSON output so callers can verify routing.
2026-04-22 21:45:14 +00:00
Claude 11ba03334c fix: classify Marmot groups in Messages Known/New tabs by admin follow
Before, every Marmot group landed unconditionally under the Known tab of the
Messages screen while the New Requests tab ignored groups entirely, so an
invite from a stranger appeared next to real conversations. The dedicated
Marmot group list screen split by ownerSentMessage only, so unreplied groups
where an admin was followed still showed as New Requests.

Replace both splits with a shared MarmotGroupChatroom.isKnown(followingKeySet):
- user has replied -> Known
- no known members and no messages -> New Requests
- otherwise Known iff any admin is in the follow set

Wire the combined Messages feeds (ChatroomListKnownFeedFilter and
ChatroomListNewFeedFilter) through the helper, invalidate dmNew on group list
changes so empty groups flow into New Requests, and have
MarmotGroupListScreen observe kind3FollowList so newly followed admins move
groups between tabs immediately.
2026-04-22 21:40:23 +00:00
Vitor Pamplona 6c214b8c13 Merge pull request #2510 from vitorpamplona/claude/add-user-selection-button-L4Ndb
Refactor MarmotGroupInfoScreen layout for better UX
2026-04-22 17:28:15 -04:00
Claude 119959e942 feat: pin add-member field to bottom of Marmots group info with add button
Move the add-member search to a fixed position at the bottom of the screen
(matching the New Short Note pattern) so it stays visible regardless of how
far the user has scrolled through a large member list. Suggestions now render
above the input, and each suggestion row shows an explicit PersonAdd icon
button so the add action has a clear visual affordance.

Adds an optional trailingContent slot to ShowUserSuggestionList / UserLine
so callers can attach per-row actions without forking the component.

https://claude.ai/code/session_01JbRycm4g3LrqatnCVWVdGh
2026-04-22 21:16:25 +00:00
Vitor Pamplona c82f4342c4 Merge pull request #2509 from vitorpamplona/claude/fix-group-nav-stack-QioA1
Fix navigation stack handling when creating marmot groups
2026-04-22 16:56:28 -04:00
Claude 7cc53dc8ba fix: pop Create Group screen when navigating to new Marmot group
After creating a Marmot group, the Create Group screen stays on the
back stack, so pressing back from the new group chat returns to the
empty creation form instead of the Messages screen. Swap `nav.nav`
for `nav.popUpTo(..., Route.CreateMarmotGroup::class)` so the creation
screen is removed inclusively as we navigate into the group.
2026-04-22 20:40:53 +00:00
Vitor Pamplona fdcc86cedb Merge pull request #2508 from vitorpamplona/claude/inline-member-removal-wnenb
Inline member management in Marmot group info screen
2026-04-22 16:09:28 -04:00
Claude 32b0f41be9 test: move re-add-after-remove regression to Quartz MlsGroup test
The bug (re-added user rejoins without a usable own_leaf, so they
can neither decrypt new group messages nor send) reproduces entirely
with two in-process MlsGroup instances — no relay or second Nostr
client needed. A pure Quartz unit test runs in seconds and catches
the regression deterministically, where the bash interop version
depended on whitenoise-rs, wnd daemons and a local relay.

- quartz: add MlsGroupLifecycleTest.testReaddAfterRemove_RejoinerCanEncryptAndDecrypt
  covering create → add → round-trip → remove → re-add with a fresh
  KeyPackage → round-trip again, asserting leafIndex and both
  encrypt/decrypt directions on the rejoined instance.
- interop: drop test_17_readd_after_remove from the headless suite and
  its registration, since the unit test supersedes it.

https://claude.ai/code/session_01JaeqZwVNLKvUUvXWRzPmRP
2026-04-22 19:52:30 +00:00
Claude a10fdb934a fix: route Leave Group back to the Messages screen
After leaving a Marmot group the user was dropped on the group list;
send them to the main Messages tab instead, which is the correct
starting point for picking another conversation.

https://claude.ai/code/session_01JaeqZwVNLKvUUvXWRzPmRP
2026-04-22 19:36:44 +00:00
Claude 65c56d0035 feat: inline member management on Marmot group info screen
- Fold the Remove Member screen into the info screen: each member row
  now has an inline remove icon that opens a confirmation dialog.
- Fold the Add Member screen into the info screen: a search field with
  a user-suggestion list lives directly above the member list, so
  adding a member never requires leaving the screen.
- Move Leave Group out of the scrolling body into a top-bar action so
  it is reachable regardless of member count.
- Shrink the relay strip (35dp tiles with a small activity dot) and
  tuck it next to the group header; drop the "Relays" label and the
  MLS epoch readout, which were visual clutter.
- Route the chat screen's Add Member button to the info screen and
  delete the two standalone screens and their routes.
- Add headless interop test 17: A creates a group, adds B, removes B,
  re-adds B, and verifies B can both receive and send messages. Guards
  the reported regression where a re-added member came back without a
  usable leaf and silently lost the ability to post.

https://claude.ai/code/session_01JaeqZwVNLKvUUvXWRzPmRP
2026-04-22 19:28:25 +00:00
Vitor Pamplona 2d6f626ee3 Merge pull request #2502 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-22 14:44:42 -04:00
Vitor Pamplona 21c367302b Merge pull request #2506 from vitorpamplona/claude/fix-scaffold-spacing-NDAkw
Fix disappearing bars hiding on pure overscroll for non-scrollable lists
2026-04-22 14:44:35 -04:00
Crowdin Bot 642aa6fe1f New Crowdin translations by GitHub Action 2026-04-22 18:44:32 +00:00
Vitor Pamplona 74aa940098 Merge pull request #2504 from vitorpamplona/claude/republish-relay-events-mo64n
Auto-republish account settings when relay lists change
2026-04-22 14:42:52 -04:00
Vitor Pamplona 4b52b2702d Merge pull request #2505 from vitorpamplona/claude/fix-marmot-account-isolation-4Ysv5
Isolate MLS and Marmot stores per account
2026-04-22 14:42:10 -04:00
Claude 36eda1cdea feat(relays): republish account settings when AllRelay lists change
When the user updates their relay configuration on the AllRelay screen,
the newly-selected relays often have no copy of the account's existing
settings (profile, follow list, mute list, other relay lists, etc.), so
the user appears to "lose" their account to anyone reading from those
relays until the next time each list is edited.

After saving each relay list, re-publish the relevant existing events
to the newly-selected relays (only when the set actually differs):

- Outbox/inbox (NIP-65) or private-storage or broadcast changes
  republish every known account-settings event to the new relay set.
- KeyPackage relay-list changes republish all active kind:30443
  KeyPackage events authored by this account.
- Local relay-list changes republish account-settings events locally.
- Indexer relay-list changes publish kind:0 and kind:3.
- Search relay-list changes publish kind:0.

Local relays now flow through a new Account.saveLocalRelayList so the
republish hook lives alongside every other relay-list save.

https://claude.ai/code/session_01PcvDaNyzT6Tk4D7jn75Jbm
2026-04-22 18:32:28 +00:00
Claude bd2fae9ca5 fix(marmot): scope Marmot storage per-account to prevent chat leakage
The three Marmot stores (MLS group state, messages, KeyPackage bundles)
were all initialized with the global app filesDir, so chats and MLS
state from one account were visible to any other logged-in account.

Pass each store a per-account subdirectory (accounts/<pubkey>) derived
from the signer pubkey, so paths become:

  files/accounts/<pubkey>/mls_groups/<groupId>/{state,retained,messages}
  files/accounts/<pubkey>/marmot_keypackages/state

Existing in-memory state was already scoped per-Account; this closes the
last remaining cross-account leak on disk.
2026-04-22 18:30:24 +00:00
Claude b5b01f6eda fix(scaffold): keep bars visible on non-scrollable lists
The DisappearingScaffold hid its top/bottom bars purely from overscroll
gestures, so a feed with too few items to scroll would end up with two
empty bands where the chrome used to be. Pure overscroll (consumed.y == 0)
from the fully-visible state is now ignored; once the bars have started
moving we know the list is scrollable and edge overscroll keeps feeding
bar motion as before.
2026-04-22 18:28:41 +00:00
Vitor Pamplona e452cf8abd Merge pull request #2501 from vitorpamplona/claude/debug-marmot-harness-oCU2g
Claude/debug marmot harness o cu2g
2026-04-22 13:39:15 -04:00
Claude c85848968c fix(marmot-interop): recover from stale MLS DB in interactive harness
The interactive harness was failing with the same
KeyringEntryMissingForExistingDatabase error as the headless one —
the MLS SQLite DB on disk references a keyring entry that no longer
exists (keychain entry pruned, data dir restored out of band, or a
previous run used the mock keyring). wnd can't open the DB in that
state, so the daemon exits before its socket appears.

Unlike the headless harness (which wipes unconditionally because its
mock keyring is always ephemeral), the interactive harness uses the
real OS keyring and benefits from preserving B/C identities across
runs. So: try once, and only wipe + retry when stderr actually shows
the keyring-missing error. Also dump stderr/stdout tails inline on
final failure so the operator doesn't have to chase a log path.
2026-04-22 17:38:08 +00:00
Claude ac72262acc fix(marmot-interop): clean up daemons on Ctrl+C in interactive harness
Apply the same SIGINT/SIGTERM/SIGHUP trap hardening to the interactive
harness that just landed on the headless one. Without it, killing the
script mid-run leaves the nohup'd wn daemons alive and the next run
fails in preflight because the sockets are still held.
2026-04-22 17:33:27 +00:00
Claude b21f8677ce fix(marmot-interop): clean up daemons on Ctrl+C / SIGTERM / SIGHUP
The previous trap only fired on the EXIT pseudo-signal, which doesn't
always run cleanly when the script is killed mid-flight — leaving the
nohup'd wnd daemons (and the local relay) alive and still holding the
port. Route SIGINT/SIGTERM/SIGHUP through an explicit `exit`, use a
single idempotent cleanup handler, and preserve the exit code so the
harness still reports failure when killed.
2026-04-22 17:30:33 +00:00
Vitor Pamplona cadb9ebec9 Merge pull request #2500 from vitorpamplona/claude/debug-marmot-harness-oCU2g
Improve wnd daemon startup reliability and error diagnostics
2026-04-22 13:00:24 -04:00
Claude 0f032e39d9 fix(marmot-interop): wipe stale wnd state when mock keyring is in use
The headless harness runs wnd with WHITENOISE_MOCK_KEYRING=1, but the
mock keyring is in-memory only — it starts empty on every wnd restart.
Meanwhile the SQLite databases under $data_dir persist across runs and
reference keys that no longer exist, so the second run onward fails at
startup with KeyringEntryMissingForExistingDatabase before wnd can even
open its control socket. Wipe everything under the daemon's data dir
(except logs and pid) before each start so wnd always comes up cold and
consistent with its ephemeral keyring.

Also surface failure diagnostics inline instead of burying them in a log
file path: print the last 40 lines of stderr and 20 of stdout, report
whether wnd is still alive or already exited, and break out of the 30s
wait loop the moment the daemon dies.
2026-04-22 16:59:07 +00:00
Vitor Pamplona 9de2399cb8 Merge pull request #2499 from vitorpamplona/claude/animate-drawer-items-azHFq
Replace newStack with navBottomBar for bottom navigation
2026-04-22 12:37:17 -04:00
Claude f59597c55f refactor: move skip-slide hint to NavBackStackEntry.savedStateHandle
The prior change tracked the "no slide" intent via a mutable flag on Nav
that every navigate method had to reset. Replace it with a per-entry
hint: navBottomBar writes SKIP_SLIDE_ANIMATION_KEY=true to the new
destination's savedStateHandle, and composableFromEnd reads it off
targetState (forward) / initialState (pop).

Benefits:
- No shared mutable state; no reset bookkeeping in nav/newStack/popUpTo.
- No race between interleaved navigate calls.
- Pop animations are automatically consistent with how the entry was
  entered - backing out of a bottom-bar tab now also fades.
2026-04-22 16:24:17 +00:00
Vitor Pamplona 4892eedd80 Merge pull request #2498 from vitorpamplona/claude/fix-marmot-mip-tests-F1mPy
Use processFramedCommit for RFC 9420 §6.1 signature verification
2026-04-22 12:08:34 -04:00
Claude ed68b79938 fix: skip slide animation for user-pinned bottom bar routes
Drawer routes are registered with composableFromEnd (slide-from-right +
scale-out) because that matches the expected drawer push animation.
When a user pins one of those routes onto the bottom navigation bar
(feature added in 376e404c), the bottom-bar tap still replayed the
slide animation because the transition is bound to the Route type at
composable<> registration time, not to the caller.

Introduce a third navigation verb, INav.navBottomBar(route), that sets
a skipSlideAnimation flag on Nav before performing the same newStack-
style stack reset (popUpTo + launchSingleTop). composableFromEnd /
composableFromEndArgs read the flag in their enter/exit/popEnter/
popExit lambdas and return null when set, inheriting the NavHost-level
fade (matching what the five default bottom-bar routes already do).

nav.newStack behaviour and all drawer navigation paths are unchanged,
so non-bottom-bar callers (intent redirects in AppNavigation.kt and
every DrawerContent click) keep their slide.
2026-04-22 16:08:04 +00:00
Claude 64ab67a6fc fix(marmot): align MIP compliance tests with held-back v2 and signed commits
Two drifts surfaced as 7 failing tests on the jvmTest run:

1. MarmotGroupData CURRENT_VERSION is held at 2 for mdk-core interop
   (see the const-doc block), but three tests still assumed v3:
   - marmotGroupData_defaultVersionIsThree asserted the constant directly.
   - marmotGroupData_roundTripWithDisappearingSecs and
     buildGroupEvent_appendsExpirationTagWhenDisappearingConfigured relied
     on the default version emitting the v3-only disappearing_message_secs
     field, which the encoder correctly skips for v2.

   Rename the first to `currentVersionIsHeldAtTwoForMdkInterop` with a
   spec-cross-reference comment and an extra assertion that v3 is still
   in SUPPORTED_VERSIONS. Pin version=3 explicitly in the other two so
   they exercise the v3 path regardless of the interop hold-back.

2. MlsGroup.processCommit now requires a non-empty FramedContentTBS
   signature on non-external commits (per RFC 9420 §6.1, introduced in
   0c970945). Four pipeline tests still called the raw processCommit
   overload with signature=ByteArray(0), so they all failed with
   "FramedContentTBS signature missing on commit from leaf 0".

   Add a `processFramedCommit` wrapper on MlsGroupManager that mirrors
   MlsGroup.processFramedCommit + the retention/persistence bookkeeping
   from processCommit, and switch the four tests to feed framedCommitBytes
   through it. MarmotInboundProcessor is unaffected (it already has the
   decoded PublicMessage fields).

All 7 originally-failing Marmot tests now pass; the 4 unrelated
NostrClient network tests remain independently flaky.

https://claude.ai/code/session_01XQNAmwn1y87GAoK94QWgyn
2026-04-22 16:04:59 +00:00
Vitor Pamplona 491c58dbfb Merge pull request #2497 from vitorpamplona/claude/review-marmot-giftwraps-xdwC2
Fix external join path encryption and add processFramedCommit
2026-04-22 11:31:22 -04:00
Vitor Pamplona 12630d06d3 Merge pull request #2496 from vitorpamplona/claude/fix-amethyst-relay-tests-EqhSz
Improve sanity checks and build resilience in marmot-interop
2026-04-22 11:30:14 -04:00
Vitor Pamplona 57ff68895f Fixes iconography inconsistency 2026-04-22 11:22:01 -04:00
Claude f9c6a4711f fix(quartz/mls): correct MLS commit framing on send and receive
Two bugs in MlsGroup were blocking 12 of 209 jvmTest cases (all RFC 9420
interop vectors already passed; only our higher-level lifecycle code
diverged from the spec).

Bug A — write/read asymmetry on member commits. processCommit requires
signature + confirmation_tag as separate parameters, but CommitResult
only exposed framedCommitBytes (the full PublicMessage envelope). Tests
called processCommit(commitBytes, ..., ByteArray(0)) and immediately
hit "FramedContentTBS signature missing on commit from leaf N".

Add MlsGroup.processFramedCommit(framedCommitBytes) that unpacks the
MlsMessage(PublicMessage(Commit)) envelope, verifies membership_tag for
member senders (RFC 9420 §6.2), and delegates to processCommit. Mirrors
the unwrap MarmotInboundProcessor already does in production. Updates
the 9 affected tests to use the new entry point.

Bug B — externalJoin used the wrong HPKE info bytes. The joiner
encrypted UpdatePath path-secrets against groupContext.toTlsBytes()
(pre-mutation), but receivers HPKE-Open against the post-mutation
context (epoch+1, new tree_hash) per RFC 9420 §7.6. Result:
AEADBadTagException on every external join.

Refactor externalJoin to follow the same staging pattern as commit():
stage public keys, applyUpdatePath, patch parent_hash, replace
placeholder leaf — then compute pathEncContextBytes from the post-
mutation tree, then HPKE-encrypt path-secrets with that context.

To make the framed pipeline available to external commits as well,
introduce ExternalJoinResult exposing both commitBytes and
framedCommitBytes (PublicMessage with sender = new_member_commit, no
membership_tag). processFramedCommit resolves NEW_MEMBER_COMMIT senders
to the receiver-side leaf index using the same first-blank-or-append
algorithm as RatchetTree.addLeaf, so wire decoding doesn't need to
carry the joiner's chosen index in the empty Sender field.

After: 209/209 MLS tests pass.
2026-04-22 15:20:29 +00:00
Claude fc38c4cb5d fix(marmot-interop): repair hunk count in skip-retry patch + harden preflight
Two harness-side bugs that made a broken wn silently look like a working one:

1. whitenoise-skip-unprocessable-retry.patch's hunk header declared
   `@@ -178,7 +178,23 @@` but the new side only has 22 lines. GNU patch
   bails with "malformed patch at line 26" and leaves the target file
   untouched. Fix the count to +178,22 so the patch actually applies.

2. setup.sh's patch-apply loop piped `patch` through `tee`, which
   masked patch's exit code behind tee's. A miscounted hunk therefore
   still touched the `.headless-patched-*` marker and the next run
   skipped the "patch" step entirely, so the resulting wn ran the
   stock 10-retry exponential backoff on every MlsMessageUnprocessable
   — ~17min per event, which pegs the per-account serial event
   processor and makes every later test timeout "just because".
   Check patch's real exit status; fail preflight loudly instead.

Same class of bug was also hiding cargo build failures: the
`cargo build ... | tee` pipeline reported success even when rustup
couldn't fetch the toolchain manifest (503 from static.rust-lang.org)
or cargo couldn't hit the crates.io index (503 from fastly). setup.sh
then `info`-logged the expected binary paths and happily moved on —
until the daemon launch tripped over `nohup: No such file or directory`.

Now each cargo build runs in a 4-attempt retry loop that terminates
only when the binary actually exists on disk, and fails preflight
loudly if it never materialises. Matches the jitpack 503 retry
behaviour already in place for `:cli:installDist`.

https://claude.ai/code/session_018kSBco5VVfctkW6vAm7NzA
2026-04-22 15:14:04 +00:00
Claude d65b4b0e5d fix(marmot-interop): retry gradle :cli:installDist on transient 503s
jitpack.io and dl.google.com both return 503 on a non-trivial fraction
of cold-cache fetches, and Gradle disables the whole repository for the
rest of the build the moment a single 503 lands — so one bad roll
aborts the entire harness before any test gets to run. Wrap the gradle
invocation in a 4-attempt retry loop; each attempt resumes from
Gradle's local cache so only the still-missing artifacts get re-fetched.

In practice the retry completes in seconds when jitpack is the only
flake, and the whole preflight still wall-clocks well under the first
attempt's runtime. Existing success path is unchanged.

https://claude.ai/code/session_018kSBco5VVfctkW6vAm7NzA
2026-04-22 15:06:40 +00:00
Claude d8d094db48 fix(marmot-interop): parse post-v0.2 wn --json shape in interactive harness
The `.result` wrapper fix landed in lib.sh's pollers and the headless tests
back in f08b010f, but the 8 inline `jq` reads in marmot-interop.sh were
missed — which silently returned empty against current `wn` and made every
UI-verification test look like "Amethyst didn't propagate X" when in fact
the underlying wn state was correct and the script just couldn't see it.

All 8 reads now peel `(.result // .)` the same way the lib.sh helpers do,
staying backwards-compatible with the pre-v0.2 flat shape:
  * test 02, 04 member lists
  * test 06 member-removed check
  * test 07 group-name poll
  * test 08 admin list (promote + demote)
  * test 09 anchor message lookup
  * test 11 leave-group member check

Also split the configure_relays sanity check into three relay-level
surfaces so a broken relay set points at the specific kind it can't
handle, instead of 10 later tests all timing out with the same "no
invite" symptom:

  * kind:30443  (KeyPackage)       — both sides publish + symmetric discover
  * kind:10050  (Inbox advertise)  — C reachable as giftwrap target
  * kind:1059   (Gift wrap)        — B->C welcome delivery
  * kind:445    (MLS commit/msg)   — real group-message round-trip

And drop the orphan whitenoise-mdk-plaintext-policy.patch: it lived in
`headless/patches/` but was never referenced by setup.sh's patch array,
and if it ever were applied it would flip mdk-core's MLS wire-format
from MIXED_CIPHERTEXT to MIXED_PLAINTEXT — a protocol-breaking
divergence from every stock whitenoise-rs / White Noise build. Removing
it eliminates the trap of a future contributor "enabling" it.

https://claude.ai/code/session_018kSBco5VVfctkW6vAm7NzA
2026-04-22 14:59:44 +00:00
Vitor Pamplona 4499114ab2 Merge pull request #2495 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-22 09:58:48 -04:00
Crowdin Bot 20cf8fb3bf New Crowdin translations by GitHub Action 2026-04-22 13:56:50 +00:00
Vitor Pamplona c21831bd3e Merge pull request #2481 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-22 09:56:01 -04:00