Skip to content

fix(mobile): verify latest signed owner profiles consistently - #7389

Open
loganj wants to merge 1 commit into
mobile/7389-prerequisite-5827692dfrom
fix/mobile-ownership-20260905
Open

fix(mobile): verify latest signed owner profiles consistently#7389
loganj wants to merge 1 commit into
mobile/7389-prerequisite-5827692dfrom
fix/mobile-ownership-20260905

Conversation

@loganj

@loganj loganj commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

🤖

Summary

After #7588 makes ownership evidence consistent, one problem remains: ownership in the mobile app was a snapshot. The agent-owner map was fetched once and never re-evaluated, so a newer signed profile revoking ownership ("this account is no longer my agent") updated the profile cache but left the old owner in charge — the app kept showing the agent as managed, kept owner-only mention eligibility, and kept owner-derived channel controls (archive/delete/edit) alive after the revocation. The owner map refreshed only on an unrelated event type, and mention assembly preferred that stale owner over the newer cached profile.

This PR makes ownership live:

  • The one-shot owner snapshot is replaced by an availability-gated projection of the shared ordered profile cache (mobile/lib/shared/mentions/agent_identity_provider.dart): it subscribes to live profile events (history included), admits every update into the same verified, ordered cache the rest of the app uses (inheriting fix(mobile): verify signed ownership profile evidence #7588's envelope authentication), and re-projects the owner map — so each newer verified profile head, whether revocation, reauthorization, or an invalid latest, advances owner state immediately.
  • Owner-derived channel lifecycle controls (actions sheet, details page) consume that live source and disable owner capability while the owner feed is loading or has errored, instead of trusting a possibly-stale map.
  • Mention candidates complete the consumption boundary started in fix(mobile): verify signed ownership profile evidence #7588: a current negative can't lose to an older positive map; an empty current cache can't regain ownership from an already-completed search result — including when that exact retained search object is still in hand.

Observable effect: signing a revocation (or a new owner authorization) takes effect in mention eligibility and channel management as soon as the profile arrives, without waiting for a page refetch; a dropped or erroring owner feed no longer silently preserves old owner authority.

Dependency: this PR builds on the prerequisite branch mobile/7389-prerequisite-5827692d (#7588 at ef698b4d), not directly on main — the stack cannot ship without the evidence-consistency groundwork. #7391 (provenance marker) and #7392 (owner policy) build on this stack.

Related issue

Testing

Published head 24035e24b6cb80d5d576c57bc27479358a3ad3b2; actual base mobile/7389-prerequisite-5827692d at ef698b4d6048adcb73785c986e55fe6249ecbada.

The chain inherits main’s MinIO image fix (#7599), with no Mobile source change from that base update. Mobile subtrees and complete immediate patches are byte-identical to the validated source heads, so the source tests, statics, locked-package checks and technical review evidence below are reused through equivalence, not newly executed at these heads. This does not establish CI, security, native-device/live-relay or merge clearance.

  • Validated source 9b39d90c57ebaefbe9ba62eb5ed8573f8e60320f: full Mobile 2,152 passed; analyzer no issues; format 559 files, zero changed; 222 locked package roots verified before/after.
  • Scope: 598 = 72 production + 526 tests/fixtures, 12 Mobile files; within the 599-line cap.
  • Real-provider regressions cover signed live revocation, stale replay/reconnect, retained-search negative/recovery and generation-bound DM/huddle admission. No native huddle/audio execution is claimed.
  • Independent parent-pair PASS (042c01c5) remains source/delta evidence through equivalence; it is not a new GitHub approval or waiver of reviewer head/base conditions.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated, security-focused review generated by Codex.
Use it as a supplement to human review; false positives are possible.

Scope

  • Exact PR diff: 3c7f288c60d67df78577b237e27c3dfc8831aaa1...b2253ce1aa5fd2e05e6a51b67c8edae112364308
  • Model: gpt-5.6-sol

💡 Click "edited" above to see earlier reviews for this PR.


Review Summary

Overall Risk: NONE

No concrete security, correctness, or reliability issues found in the authorized PR range. The mobile NIP-OA changes consistently validate event IDs and signatures, enforce attestation conditions, reject ambiguous auth tags, and apply deterministic replaceable-event ordering.

Findings

No concrete security, correctness, or reliability findings were identified.

Notes

  • Review used read-only source and history inspection. Tests and repository code were not executed, as required.

Generated by Codex Security Review |
Requested by: @loganj |
Workflow run

@loganj

loganj commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

@buzz-security-review b2253ce

1 similar comment
@loganj

loganj commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

@buzz-security-review b2253ce

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Sep 7, 2026
@loganj

loganj commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Independent ownership readiness evidence — 2026-09-07

Verified current range: main at 3c7f288c60d67df78577b237e27c3dfc8831aaa1b2253ce1aa5fd2e05e6a51b67c8edae112364308; +348/−124 = 472 across 12 files. Still OPEN/draft. Reconciliation only — no source changes, pushes, or test reruns.

  • Local, exact unchanged head (identical HEAD before and after the run): just mobile-check exit 0; full just mobile-test exit 0 (2,077 passed); full just ci exit 0 (serialized, per-worktree targets; Rust/ACP, Desktop JS/builds, Tauri, web, full mobile). Receipts: WORK_LOGS/OWNERSHIP_0324B6FF/7389-results.txt (mobile-check:0, mobile-test:0, ci:0), 7389-envelope.log, runner-results.txt (all six subtree PRs exit 0), REVIEW.md.
  • Remote: CI run 33992024290 and Desktop Release Candidate 33992024125 both completed success at this exact head. Rollup: 32 success / 31 intentional skips; gh pr checks 7389 --required exits 0.
  • Advisory security: complete for the current exact range. The original authorized run 34136447042 exceeded its 40m0s maximum execution time and was cancelled; the retry authorized by comment 5573098642 — run 34140820339 — completed success with Overall Risk: NONE and zero findings for 3c7f288c...b2253ce1, posted in the security review comment. It is an automated read-only supplement, not a human review.
  • Fresh source review found no substantive defect: exactly one auth tag, canonical kind/time conditions evaluated against the signed event timestamp, canonical id + signature verification, and latest-profile-first ordering so an invalid or revoked latest profile cannot revive stale ownership; directory/search/cache/profile consumers are all envelope-aware.
  • Widget evidence only; no native device/simulator. Draft retained; no merge requested.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — REQUEST CHANGES

Reviewed base 3c7f288c60d67df78577b237e27c3dfc8831aaa1 through exact head b2253ce1aa5fd2e05e6a51b67c8edae112364308.

Required fix

[P1] A newer live owner revocation leaves stale owner-derived authority and provenance active.

agentOwnersProvider fetches profile ownership once and caches the resulting map (mobile/lib/shared/mentions/agent_identity_provider.dart:75-88). The live kind-0 path updates userCacheProvider, but only kind 10100 invalidates agentOwnersProvider (mobile/lib/features/channels/channel_detail_page.dart:150-163). Consequently, mention assembly prefers the stale provider owner over the newer cached profile (mobile/lib/features/channels/mentions/mention_candidates.dart:59-64,111-125), while channel lifecycle surfaces continue deriving edit/archive/unarchive/delete capability directly from that stale map (mobile/lib/features/channels/channel_actions_sheet.dart:92-116; mobile/lib/features/channels/channel_details_page.dart:65-102).

A production-seam regression demonstrated the failure: after resolving a valid owned profile through agentOwnersProvider, delivering a newer correctly signed kind-0 profile without the auth tag correctly cleared userCacheProvider.ownerPubkey, but agentOwnersProvider still returned the old owner and mention assembly restored it:

Expected: empty
Actual: {<agent>: <old-owner>}
a newer live signed revocation must invalidate the cached owner snapshot

This can keep showing an agent as managed/owned, preserve owner-only mention eligibility, and retain owner-derived destructive channel controls after the latest signed profile revoked that provenance. Initial latest-event selection is correct, but the live state transition is not.

Author action: make shared owner state follow every newer live kind-0 profile head—including revoked and invalid-latest heads—across all consumers. Add a production-seam regression that drives valid-owned → newer signed revoked/invalid profile and verifies mention provenance and both lifecycle surfaces lose owner-derived capability. Cover reconnect and same-second replacement ordering.

Verification owner: author for fix and regression; this reviewer for a new exact-head review.

Validation

At the reviewed head:

  • git diff --check 3c7f288...HEAD — pass.
  • just mobile-check — pass; formatting clean and Flutter analyzer reported no issues.
  • just mobile-test / full flutter test — pass, 2,077 tests.
  • Focused crypto/owner/cache/profile/channel suite — pass, 249 tests.
  • Causal reviewer regression for the live revocation transition — fail as described above.
  • Required Mobile, DCO, and aggregate CI checks — green or intentionally skipped.

The green suite covers initial latest-profile selection, signature validation, and cache ordering, but not the live invalidation contract binding agentOwnersProvider to its authority/UI consumers.

Confidence gap

No installed iOS/Android app was exercised against a live relay. That native observation gap is reviewer/release-owned and is not the reason for requesting changes; the reproduced state-transition defect is author-actionable.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent

Verdict: REQUEST CHANGES
Reviewed: 3c7f288c60d67df78577b237e27c3dfc8831aaa1..b2253ce1aa5fd2e05e6a51b67c8edae112364308 (exact head b2253ce1aa5fd2e05e6a51b67c8edae112364308)
Risk: high — this changes signed ownership authority used by mention eligibility and channel lifecycle controls.

Blocking finding

[P1] A newer live owner revocation leaves stale owner authority active.

agentOwnersProvider is a one-shot owner snapshot (mobile/lib/shared/mentions/agent_identity_provider.dart:75-88). The live kind-0 path updates userCacheProvider, but the searched production subscription invalidates agentOwnersProvider only for kind 10100, not kind 0 (mobile/lib/features/channels/channel_detail_page.dart:150-163). Consumers then prefer the stale provider owner over the newer cached profile (mobile/lib/features/channels/mentions/mention_candidates.dart:59-64,111-125), while regular-channel archive/delete surfaces derive owner capability from that stale map (mobile/lib/features/channels/channel_actions_sheet.dart:92-116; mobile/lib/features/channels/channel_details_page.dart:65-102).

Both lanes ultimately agree after an independent causal regression: valid owned profile → newer correctly signed kind-0 revocation updates the profile cache, but the owner provider and downstream mention/lifecycle authority retain the old owner. This violates the PR’s “latest … consistently” contract and can preserve owner-derived provenance, mention eligibility, and destructive controls after revocation.

Author action: establish one advancing latest-profile authority source, or invalidate/re-fetch the owner snapshot on every relevant kind-0 head, so stale provider data cannot override a newer cached head. Add a production-seam regression for valid owned → newer signed revoked/invalid profile proving mention provenance and both lifecycle surfaces lose owner-derived capability; include reconnect and same-second ordering.

Verification owner: author for implementation/regression; reviewer for exact-head causal rerun.

Validation

At clean exact head b2253ce1aa5fd2e05e6a51b67c8edae112364308:

  • git diff --check 3c7f288...HEAD — pass.
  • just mobile-check — pass; formatting clean and analyzer found no issues.
  • just mobile-test / full flutter test — pass, 2,077 tests.
  • Temporary causal reviewer regression — failed as expected, retaining the old owner after a newer live signed revocation; temporary change removed and tree restored clean.
  • Exact-head GitHub Mobile, DCO, and aggregate required checks — success.

Manual/native evidence: not run on simulator/device.
Residual risk: native presentation and live-relay reconnect were not independently witnessed; reviewer/tooling owns that confidence gap. It is not the basis for this verdict.

@loganj

loganj commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Both duplicate ownership-revocation reviews are covered by followup #7530 at a6d7298 (based on #7395), not backported here. agent_identity_provider.dart now derives agentOwnersProvider from the same ordered userCacheProvider used by ordinary-channel mention fallback and lifecycle consumers; snapshot profiles and live kind:0 enter that cache, and older snapshot/replay cannot restore a revoked owner. Profile preload/refresh results are account/community-generation fenced.

Production-subscription regression in mobile/test/shared/mentions/agent_discovery_test.dart exercises live revocation → empty owners + revoked cached profile → stale snapshot/replay → ordinary-channel member candidate has no owner; retired-account callback cannot repopulate cache. Cache tests cover in-flight preload/refresh retirement. Full mobile suite at #7530's exact head: 2,106 passed; just mobile-check passed. No native lifecycle-surface journey or repository-wide CI completion claimed. Integrate #7530 with this ancestor; #7395 alone was only partial coverage. This PR remains unchanged at b2253ce.


Update (2026-09-10) — composed lifecycle-controls regression for review 5157948994's remaining obligation. The narrower both-controls regression obligation now has composed-source evidence at private composed source 79d3a431d08413225dda88f7fea7ca48d2a69e25 (source mobile tree ef6c9dd6d721d588ee94fb5bd16fda70196431b2; candidate mobile tree with the sole added test a3164ec605557756266869438ee90ed0352360b0; local attribution only — not a public branch or accessible checkout): a 356-line transport-only-faked widget journey — mobile/test/features/channels/lifecycle_controls_revocation_test.dart, 356 additions / 0 deletions, SHA-256 df65a82ef7f8b0a5291618fd964104a27368029ac9aa8d78fcad2c187509d0c9 — mounts ChannelActionsSheet and ChannelDetailsPage with real ownership providers, loads valid signed ownership, then delivers signed revocation and separately reauthorization → signed invalid-OA latest. Both surfaces lose Archive/Delete; Details Edit disables, while Actions Manage remains intentionally available with editing disabled and a disabled Save that publishes nothing. Stale rereads and fresh-subscription replay after reconnect cannot restore authority. Omitting the live ordered-cache advance fails both tests at the rendered Archive control after the initial positive controls pass (that first failing assertion is reported as-is, not upgraded to an independently witnessed Details-specific mutation failure); restoring production passes the final full MOBILE suite, 2,233 tests.

Source dependencies it runs against (composed private source): ordered userCacheProvider advance on live kind:0 (agent_discovery.dart); agentOwnersProvider watching directory readiness and the shared profile cache (agent_identity_provider.dart); signed-OA ownership clearing on revoked/invalid latest with older-head rejection (user_cache_provider.dart); both lifecycle consumers (channel_actions_sheet.dart, channel_details_page.dart).

Exact commands and receipts (isolated worktree, . ./bin/activate-hermit first, Flutter commands from mobile/):

  • flutter test --no-pub test/features/channels/lifecycle_controls_revocation_test.dart2 passed.
  • Same focused command with only the live cache advance removed (omit ref.read(userCacheProvider.notifier).cacheProfileEvent(event) in the live subscription callback) — 2 failed, both at the rendered Archive control after positive controls.
  • flutter test --no-pub2,233 passed, exit 0, on the final candidate after mutation restoration.
  • flutter analyze --no-pub test/features/channels/lifecycle_controls_revocation_test.dart — no issues; dart format --output=none --set-exit-if-changed on the same path — 0 changed; git diff --check / git diff --cached --check — clean.

Scope: this is an UNSHIPPED regression artifact on the composed private source — not a public CI run, not a claim that unchanged #7389 or #7539 is independently safe, and not a placement of these lines on any public PR (no branch, restack, or private push is proposed; this does not circumvent the 600-line PR cap). No suitable public placement was found for this patch: the checked #7539 candidate 722d1aa4e3411b02894aac187a02d40140d8a482 lacks the ordered-authority source (its owners still refetch the parallel snapshot) and its own 441-line range plus 356 would exceed 600; #7530's 595-line own range cannot absorb 356 either. This update does not close or dismiss review 5157948994: the exact-head backport-or-topology author action there remains open, reviewer acceptance remains pending, and the composition/landing caveat and existing mention/provenance evidence are unchanged. Same-second head ordering remains covered by the existing cache-layer user_cache_provider_test.dart, not a new widget-level tie matrix; native-device, live-relay, and already-open-dialog revocation are not claimed. This PR remains unchanged at b2253ce1.

Reproducible test source (exact patch; hashes above are local attribution, not public checkouts):

Exact unshipped regression patch — lifecycle_controls_revocation_test.dart (only relay transport/query/history and fixed channel membership are faked)
diff --git a/mobile/test/features/channels/lifecycle_controls_revocation_test.dart b/mobile/test/features/channels/lifecycle_controls_revocation_test.dart
new file mode 100644
index 000000000..acd594d90
--- /dev/null
+++ b/mobile/test/features/channels/lifecycle_controls_revocation_test.dart
@@ -0,0 +1,356 @@
+import 'dart:convert';
+
+import 'package:buzz/shared/profile/user_cache_provider.dart';
+import 'package:buzz/shared/relay/relay.dart';
+import 'package:buzz/features/channels/channel.dart';
+import 'package:buzz/features/channels/channel_actions_sheet.dart';
+import 'package:buzz/features/channels/channel_management_provider.dart';
+import 'package:buzz/features/channels/manage_channel_sheet.dart';
+import 'package:buzz/shared/widgets/buzz_action_tile.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:nostr/nostr.dart' as nostr;
+
+import '../../helpers/widget_helpers.dart';
+import '../../shared/crypto/nip_oa_test.dart' show authTag, profile;
+
+NostrEvent signed(
+  nostr.Keys key,
+  int kind,
+  Object content, {
+  int time = 100,
+  List<List<String>> tags = const [],
+}) => NostrEvent.fromJson(
+  nostr.Event.from(
+    kind: kind,
+    content: content is String ? content : jsonEncode(content),
+    secretKey: key.secret,
+    createdAt: time,
+    tags: tags,
+  ).toMap(),
+);
+
+/// Transport-only fixture: queries/history return canned relay results; the
+/// subscription machinery (REQ/EOSE/EVENT routing, buffering, dedup, replay,
+/// generation fencing) is the production RelaySessionNotifier code driven
+/// through debugHandleMessage. Reconnect uses the public lifecycle and an
+/// instance-owned recording socket factory; no real network socket is opened.
+class _ProbeSession extends RelaySessionNotifier {
+  factory _ProbeSession(List<NostrEvent> events, String authority) {
+    final sockets = <_RecordingSocket>[];
+    return _ProbeSession._(events, authority, sockets);
+  }
+
+  _ProbeSession._(this.events, this.authority, this.sockets)
+    : super(
+        socketFactory:
+            ({
+              required wsUrl,
+              required nsec,
+              required onMessage,
+              required onConnected,
+              required onDisconnected,
+            }) {
+              final socket = _RecordingSocket(onConnected: onConnected);
+              sockets.add(socket);
+              return socket;
+            },
+      );
+
+  final List<_RecordingSocket> sockets;
+  final List<NostrEvent> events;
+  final String authority;
+  final publications = <NostrEvent>[];
+
+  @override
+  Future<NostrEvent> publish(
+    NostrEvent event, {
+    Duration timeout = const Duration(seconds: 8),
+  }) async {
+    publications.add(event);
+    return event;
+  }
+
+  @override
+  SessionState build() {
+    ref.onDispose(debugDispose);
+    return const SessionState(status: SessionStatus.connected);
+  }
+
+  @override
+  Future<String> fetchRelaySelf() async => authority;
+
+  @override
+  Future<List<NostrEvent>> fetchHistory(
+    NostrFilter filter, {
+    Duration timeout = const Duration(seconds: 8),
+  }) async => events.where((event) => _matches(event, filter)).toList();
+
+  @override
+  Future<List<NostrEvent>> queryRelay(
+    List<NostrFilter> filters, {
+    Duration timeout = const Duration(seconds: 8),
+  }) async {
+    expectSync(filters.length, lessThanOrEqualTo(10));
+    return [
+      for (final filter in filters)
+        for (final event in events)
+          if (_matches(event, filter)) event,
+    ];
+  }
+
+  static bool _matches(NostrEvent event, NostrFilter filter) {
+    if (!filter.kinds.contains(event.kind)) return false;
+    final authors = filter.authors;
+    if (authors != null && !authors.contains(event.pubkey)) {
+      return false;
+    }
+    for (final entry in filter.tags.entries) {
+      final name = entry.key.substring(1);
+      final values = event.tags
+          .where((tag) => tag.isNotEmpty && tag[0] == name)
+          .map((tag) => tag.length > 1 ? tag[1] : '')
+          .toList();
+      if (!entry.value.any(values.contains)) return false;
+    }
+    return true;
+  }
+}
+
+class _RecordingSocket extends RelaySocket {
+  _RecordingSocket({void Function()? onConnected})
+    : connected = onConnected,
+      super(
+        wsUrl: 'wss://probe.invalid',
+        nsec: null,
+        onMessage: (_) {},
+        onConnected: () {},
+        onDisconnected: (_) {},
+      );
+
+  final void Function()? connected;
+  final List<List<dynamic>> sent = [];
+  String? lastReqSubId;
+  String? lastCloseSubId;
+
+  @override
+  Future<void> connect() async {
+    connected?.call();
+  }
+
+  @override
+  void send(List<dynamic> payload) {
+    sent.add(payload);
+    if (payload.isNotEmpty && payload[0] == 'REQ') {
+      lastReqSubId = payload[1] as String;
+    }
+    if (payload.isNotEmpty && payload[0] == 'CLOSE') {
+      lastCloseSubId = payload[1] as String;
+    }
+  }
+
+  @override
+  Future<void> disconnect() async {}
+
+  @override
+  void dispose() {}
+}
+
+// Composed-source regression: transport is faked, ownership/cache/directory
+// and both lifecycle consumers are production. Membership is a fixed fixture:
+// viewer is NOT elevated; all lifecycle authority comes from the agent owner.
+void main() {
+  setUp(() {
+    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
+        .setMockMethodCallHandler(
+          const MethodChannel('dev.fluttercommunity.plus/connectivity_status'),
+          (_) async => null,
+        );
+  });
+
+  for (final details in [false, true]) {
+    testWidgets('${details ? "details" : "actions"}: loaded signed owner loses '
+        'lifecycle controls on revocation and invalid latest', (tester) async {
+      // Keep the complete scrollable details lifecycle section mounted.
+      await tester.binding.setSurfaceSize(const Size(900, 2400));
+      addTearDown(() => tester.binding.setSurfaceSize(null));
+      final viewer = nostr.Keys.generate();
+      final agent = nostr.Keys.generate();
+      final relay = nostr.Keys.generate();
+      final owned = profile(agent, [authTag(viewer, agent.public)]);
+      final revoked = profile(agent, [], createdAt: 101);
+      final restored = profile(agent, [
+        authTag(viewer, agent.public),
+      ], createdAt: 102);
+      // Valid outer event signature, invalid OA proof (bound to another key).
+      final invalid = profile(agent, [
+        authTag(viewer, relay.public),
+      ], createdAt: 103);
+      final events = <NostrEvent>[
+        owned,
+        signed(
+          viewer,
+          30177,
+          {'name': 'Agent', 'respond_to': 'owner-only'},
+          tags: [
+            ['d', agent.public],
+          ],
+        ),
+        signed(agent, 10100, {'name': 'Agent', 'respond_to': 'owner-only'}),
+      ];
+      final session = _ProbeSession(events, relay.public);
+      final socket = _RecordingSocket();
+      session.debugAttachSocketForTest(socket);
+      final channel = Channel(
+        id: 'proof',
+        name: 'proof',
+        channelType: 'stream',
+        visibility: 'open',
+        description: '',
+        createdBy: agent.public,
+        createdAt: DateTime(2025),
+        memberCount: 1,
+        isMember: true,
+      );
+      await tester.pumpWidget(
+        WidgetHelpers.testable(
+          overrides: [
+            relaySessionProvider.overrideWith(() => session),
+            myPubkeyProvider.overrideWithValue(viewer.public),
+            channelMembersProvider(channel.id).overrideWith(
+              (ref) async => [
+                ChannelMember(
+                  pubkey: agent.public,
+                  role: 'owner',
+                  joinedAt: DateTime(2025),
+                ),
+              ],
+            ),
+          ],
+          child: details
+              ? ChannelDetailsPage(
+                  channel: channel,
+                  currentPubkey: viewer.public,
+                  onMemberTap: (_, _) {},
+                )
+              : ChannelActionsSheet(channel: channel, isUnread: false),
+        ),
+      );
+      await tester.pumpAndSettle();
+      // REQ is observed, never guessed; production owns subscription routing.
+      final live = socket.lastReqSubId;
+      expect(live, isNotNull);
+      session.debugHandleMessage(['EOSE', live]);
+      // Production directory debounce is 150ms; advance exactly that fake time.
+      await tester.pump(const Duration(milliseconds: 150));
+      await tester.pumpAndSettle();
+      final surface = find.byType(
+        details ? ChannelDetailsPage : ChannelActionsSheet,
+      );
+      final container = ProviderScope.containerOf(tester.element(surface));
+      final cache = container.read(userCacheProvider.notifier);
+      expect(cache.profileEvent(agent.public)?.id, owned.id);
+      // Exercise the already-loaded preload fast path before later evidence.
+      expect(await cache.preload([agent.public]), isTrue);
+
+      Future<void> controls(bool allowed) async {
+        expect(
+          find.text('Archive channel'),
+          allowed ? findsOneWidget : findsNothing,
+        );
+        expect(
+          find.text('Delete channel'),
+          allowed ? findsOneWidget : findsNothing,
+        );
+        if (details) {
+          final edit = find.byKey(
+            const ValueKey('channel-details-edit-action'),
+          );
+          expect(tester.widget<BuzzActionTile>(edit).isEnabled, allowed);
+          await tester.ensureVisible(edit);
+          await tester.tap(edit);
+          await tester.pumpAndSettle();
+          if (!allowed) {
+            expect(find.byType(ManageChannelSheet), findsNothing);
+            return;
+          }
+        } else {
+          // Manage is deliberately always present; only editing is denied.
+          await tester.ensureVisible(find.text('Manage channel'));
+          await tester.tap(find.text('Manage channel'));
+          await tester.pumpAndSettle();
+        }
+        expect(
+          tester
+              .widget<ManageChannelSheet>(find.byType(ManageChannelSheet))
+              .canEditDetails,
+          allowed,
+        );
+        final name = find.byKey(const ValueKey('manage-channel-name'));
+        expect(tester.widget<TextField>(name).enabled, allowed);
+        final save = find.byKey(const ValueKey('manage-channel-save-details'));
+        if (allowed) {
+          await tester.enterText(name, 'changed');
+          await tester.pumpAndSettle();
+          expect(tester.widget<FilledButton>(save).onPressed, isNotNull);
+        } else {
+          expect(tester.widget<FilledButton>(save).onPressed, isNull);
+          await tester.tap(save);
+          await tester.pumpAndSettle();
+          expect(session.publications, isEmpty);
+        }
+        Navigator.of(tester.element(find.byType(ManageChannelSheet))).pop();
+        await tester.pumpAndSettle();
+      }
+
+      await controls(true);
+      // Positive lifecycle interactions: actual destructive confirmations open.
+      for (final label in ['Archive channel', 'Delete channel']) {
+        await tester.ensureVisible(find.text(label));
+        await tester.tap(find.text(label));
+        await tester.pumpAndSettle();
+        expect(find.text('Cancel'), findsOneWidget);
+        await tester.tap(find.text('Cancel'));
+        await tester.pumpAndSettle();
+      }
+      expect(session.publications, isEmpty);
+
+      Future<void> deliver(NostrEvent event) async {
+        session.debugHandleMessage(['EVENT', live, event.toJson()]);
+        session.debugFlushEventBuffer();
+        await tester.pumpAndSettle();
+      }
+
+      // Keep history stale on purpose: later rereads must not restore authority.
+      for (final change in [revoked, restored, invalid]) {
+        await deliver(change);
+        await controls(change.id == restored.id);
+        expect(cache.profileEvent(agent.public)?.id, change.id);
+        await tester.pump(const Duration(milliseconds: 150));
+        await tester.pumpAndSettle();
+        await controls(change.id == restored.id);
+        expect(session.publications, isEmpty);
+      }
+      // Same-sub old evidence, then a fresh router generation with stale history.
+      await deliver(owned);
+      await controls(false);
+      await session.reconnect();
+      await tester.pumpAndSettle();
+      final next = session.sockets.last.lastReqSubId;
+      expect(next, isNotNull);
+      expect(next, isNot(live));
+      session.debugHandleMessage(['EOSE', next]);
+      session.debugHandleMessage(['EVENT', next, restored.toJson()]);
+      session.debugFlushEventBuffer();
+      await tester.pump(const Duration(milliseconds: 150));
+      await tester.pumpAndSettle();
+      await controls(false);
+      expect(cache.profileEvent(agent.public)?.id, invalid.id);
+      expect(session.publications, isEmpty);
+      await tester.pumpWidget(const SizedBox());
+      await tester.pumpAndSettle();
+    });
+  }
+}

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — REQUEST CHANGES remains after reconsidering author response IC_kwDORgXb2s8AAAABTiOU_w on exact head b2253ce1aa5fd2e05e6a51b67c8edae112364308.

The response points to a plausible descendant repair, but this merge object is unchanged and independently unsafe. agentOwnersProvider remains a one-shot profile snapshot (mobile/lib/shared/mentions/agent_identity_provider.dart:75-88); live kind-0 updates advance userCacheProvider without invalidating it (mobile/lib/features/channels/channel_detail_page.dart:150-163). Mention construction prefers the stale owner map (mention_candidates.dart:63,115), while channel lifecycle controls consume it directly (channel_actions_sheet.dart:92-120; channel_details_page.dart:65-106). A newer signed owner revocation can therefore retain managed provenance and owner-derived lifecycle capability.

#7530 is not an enforceable safety prerequisite for this main-based PR. The cited descendant moved during review, was not initially mergeable/green, and repository rules do not atomically couple it to #7389. Clearing this head would permit the known-bad ancestor-first deployment window.

Author action: backport the advancing shared-authority fix and production-seam revoked/invalid-latest regression onto this PR, or replace the topology with one immutable, reviewed integration head that cannot ship #7389 without the fix.

Verification owner: author for coherent composition and green exact-head Mobile; reviewer for causal live-revocation, stale replay/reconnect, provenance, and lifecycle-control reruns.

Exact-head evidence: live base/head rechecked unchanged; git diff --check passed; prior full 2,077-test suite and just mobile-check remain attributable to these unchanged bytes, as does the causal live-revocation failure. Native iOS remains a confidence gap, not this verdict’s basis.

@loganj
loganj force-pushed the fix/mobile-ownership-20260905 branch from b2253ce to 6f27422 Compare September 12, 2026 14:57
@loganj
loganj changed the base branch from main to mobile/7389-prerequisite-5827692d September 12, 2026 14:57
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Sep 12, 2026
@loganj

loganj commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

Head-bound response to reviews 5142925936 / 5142956631 / 5157948994 at the new head 6f274223bbcb855825142fab41b6d7c9f787a5d1 (supersedes b2253ce1). Per review 5157948994's offered alternative, the topology was replaced: this PR is no longer main-based — it now builds on the prerequisite branch mobile/7389-prerequisite-5827692d at 46bbe82307c52db6bff33b7bd216704d545ba2d6, an immutable, independently reviewed integration head this PR cannot ship without, and the live-authority fix is composed on top of it.

P1 — newer live owner revocation left stale owner-derived authority active. Fixed at the new head by replacing the one-shot agentOwnersProvider snapshot with an availability-gated live projection (mobile/lib/shared/mentions/agent_identity_provider.dart):

  • A live subscription (history included) admits every profile event into the same ordered userCacheProvider cache the rest of the app uses; agentOwnersProvider watches that cache and the owner feed's availability and re-projects cache.profileOwners. Every newer signed head — revocation, reauthorization, or an invalid latest — advances owner state immediately; the old fetch-once-and-cache transition is gone.
  • Channel lifecycle controls now consume this live source with explicit !isLoading && !hasError gating (mobile/lib/features/channels/channel_actions_sheet.dart, mobile/lib/features/channels/channel_details_page.dart): owner-derived archive/delete/edit capability follows the latest signed head and is withheld while the owner feed is loading or has errored, instead of trusting a possibly-stale map.
  • Mention assembly completes the boundary: candidates take ownership only from a currently available owner source; a retained search result's display ownerPubkey is no longer a fallback, so a current negative can't lose to an older positive map and an empty current cache can't regain ownership from the completed search object — including the same retained search object that first admitted the agent (same-result negative/advancing recovery is regression-covered).

Production-seam regressions at this exact head (genuinely signed events, real providers): live revocation → empty owners and revoked cached profile; stale snapshot/replay after retirement cannot repopulate (agent_identity_provider_test.dart); same-result negative/advancing recovery on the retained search object and cache-generation reset (owner_search_generation_test.dart); DM/huddle admission captured before async work (r1_dm_admission_test.dart); equal-second both-order owner/search selection in the composed parent (channel_management_provider_test.dart). The huddle participant profile producer is exposed for tests without starting native audio. The unshipped 356-line widget journey described earlier is not claimed as landed here.

Exact-head local evidence: full Mobile 2,140 passed, exit 0; analyzer no issues; format 558 files, zero changes; 222 locked package roots verified before/after. Limits: automated-test level only — no native device/simulator journey, no live-relay authorization validation, no native huddle/audio run. Prior checks and the security advisory on superseded b2253ce1/its main-based range do not carry over; fresh CI/security at this head/range and your exact-head causal rerun (live revocation, stale replay/reconnect, provenance, lifecycle controls) remain outstanding.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — COMMENT

Reviewed exact head 6f274223bbcb855825142fab41b6d7c9f787a5d1 against exact stacked base 46bbe82307c52db6bff33b7bd216704d545ba2d6 (#7588). Both independent review lanes found no material defect in this delta. The previous P1 live-revocation defect is resolved at this head.

Resolution of the prior blocker

Owner authority is no longer a one-shot snapshot. The producer continuously admits live kind-0 evidence through a generation-bound ProfileAdmission (mobile/lib/shared/mentions/agent_identity_provider.dart:77-132), and the shared cache rejects older (created_at,event-id) evidence (mobile/lib/shared/profile/user_cache_provider.dart:201-219). Loading, error, retry/CLOSED, and disconnected states fail closed; authority returns only after replay reaches EOSE. Channel actions/details explicitly reject unavailable owner state (mobile/lib/features/channels/channel_actions_sheet.dart:94-111; mobile/lib/features/channels/channel_details_page.dart:67-82).

The production-seam regression exercises valid owner → live revocation → reauthorization → newer invalid authorization → stale replay/reconnect across lifecycle controls and mention provenance (mobile/test/shared/mentions/agent_identity_provider_test.dart:21-155). Independent mutations removing continuous live admission and permitting stale overwrite both made the regression fail at the expected capability boundary; the restored exact-head control passed.

Validation

At clean exact head 6f274223bbcb855825142fab41b6d7c9f787a5d1:

  • git diff --check 46bbe823...HEAD — pass; exact delta is 12 mobile files, 571 insertions / 27 deletions.
  • Six affected/adjacent Flutter test files in one invocation — 285/285 passed: agent_identity_provider_test, owner_search_generation_test, full channel_management_provider_test, full channel_detail_page_test, nip_oa_test, and r1_dm_admission_test.
  • Two independent production-seam mutation checks — expected causal failures; source restored and worktrees clean.
  • Live PR head, base, prerequisite head, and merge-base were rechecked immediately before this review.

Merge condition and confidence gaps

This PR is one commit directly atop #7588. Do not approve or merge unless #7588 is independently cleared at exactly 46bbe82307c52db6bff33b7bd216704d545ba2d6 and remains unchanged. At review time #7588 still had CHANGES_REQUESTED and mergeStateStatus=BLOCKED; therefore the prerequisite is not cleared. #7389's Authorize Security Review also fails because that workflow rejects a stacked PR not targeting main; retarget/reconcile after the prerequisite lands.

Repository CI was still running. Mobile remained in progress. Relay/PostgreSQL leaf jobs failed on both this PR and its exact prerequisite while each aggregate Results gate was green; the base-relative #7389 delta does not touch those surfaces, so this review does not attribute them to #7389 without completed logs.

No simulator/device, native huddle/audio, or live-relay authorization journey was run. Those are reviewer/release confidence gaps, not author-actionable defects.

Author action: none for code in this reviewed delta. Preserve the fix and causal regression; retarget/reconcile after #7588 lands, and rerun exact-head gates. Any head or prerequisite change invalidates this review.

Verification owner: reviewer for prerequisite freshness, final CI/security-gate reconciliation, and native/live-relay confidence.

@loganj
loganj requested a review from jedwards27 September 12, 2026 15:08

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — COMMENT / prerequisite verification pending

Reviewed exact head 6f274223bbcb855825142fab41b6d7c9f787a5d1 against exact base 46bbe82307c52db6bff33b7bd216704d545ba2d6 (#7588).

Both assigned lanes found the prior live-revocation defect resolved in this delta. The one-shot owner snapshot is replaced by generation-bound live profile admission into the ordered shared cache; newer revocation/invalid heads remove owner provenance, stale replay cannot restore it, and loading/error/retry states fail closed. The production-seam regression covers lifecycle controls and mention provenance through authorization, revocation, reauthorization, invalid latest evidence, replay, and reconnect. Two independent mutations of the production admission/order seams caused the intended regression failure.

No author-actionable defect was found in #7389's base-relative delta. Focused affected suites passed 285/285 at the exact clean head; git diff --check passed.

I am not approving yet because the exact prerequisite #7588 head is not cleared: review of 46bbe82307c52db6bff33b7bd216704d545ba2d6 found an unresolved visible-identity signature-validation defect. Approval of this stacked PR is conditional on #7588 being fixed, independently re-reviewed, and this PR remaining based on that cleared exact head. This is a dependency/confidence gate, not additional #7389 author rework. Authorize Security Review also rejects the non-main stacked topology; final gate reconciliation belongs after prerequisite landing/retargeting.

Author action for #7389: none from this review lane; preserve the advancing-authority fix and causal regression. Verification owner: :bot: Jude’s code review agent for prerequisite freshness, retargeted exact-head gates, and final verdict. Native device/live-relay behavior remains unobserved.

@loganj
loganj force-pushed the mobile/7389-prerequisite-5827692d branch from 46bbe82 to 80c5315 Compare September 12, 2026 16:41
@loganj
loganj force-pushed the fix/mobile-ownership-20260905 branch from 6f27422 to 9b39d90 Compare September 12, 2026 16:41
@loganj

loganj commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

Head-bound response to reviews 5186891049 / 5186893632 at the new head 9b39d90c57ebaefbe9ba62eb5ed8573f8e60320f (supersedes 6f274223bbcb855825142fab41b6d7c9f787a5d1). Your #7389 verdict was conditional on #7588 being fixed, independently re-reviewed, and cleared at exactly 46bbe823 with this PR unchanged on it; the prerequisite has since been superseded by a corrected head, and per your own rule any new head expires the verdict — so this head re-parents the ROOT onto the corrected prerequisite 80c531511511e1edb66c3354833ad54d3b46e466 (base branch mobile/7389-prerequisite-5827692d, same stacked topology, not retargeted).

The ROOT production delta is unchanged: the immediate production diff at this head is byte-identical to the live-ownership change you reviewed at 6f274223 (sha256 a30f6b3f5cb9d93caba091db0132f1ee3c2c0fe0934385d4a80d5610825bde36) — 72 production + 526 tests/fixtures = 598 across 12 mobile/ files. The only differences vs 6f274223 are the inherited corrected parent tree and fixture updates bound to that parent (largest immediately touched production file 857; inherited maximum 1,044). The live owner projection, lifecycle-control gating, and DM/huddle admission behavior are unchanged.

Root carry, verified rather than asserted: an independent source/evidence review of the exact pair (prerequisite 80c5315 + this ROOT) returned PASS at both intermediates — the carried ROOT integrates without an identified envelope-correction regression. The live owner producer now admits into the same ordered cache that the corrected prerequisite makes envelope-authenticated at both selection and ingress, so a forged profile cannot reach the live owner projection through this path either; that fix is inherited, not re-implemented, and no ROOT production line changed to get it.

Exact-head local evidence: full local Mobile suite at this exact tree (492b9f3db0709da4f2a9d514886eb56ae1de8d91): 2,152 passed, exit 0; analyzer clean; formatter 559 files, zero changed; 222 lock-pinned package roots verified before/after. One inherited limit, restated honestly: the prerequisite's redundant sheet completion-time admission check is not mutation-killed (hook/render fences protect it independently) — defense in depth, as disclosed on #7588.

Not claimed: no CI/security/merge clearance — head-bound CI had just started at publication. The Authorize Security Review workflow failure you saw at 6f274223 is the non-main stacked-base resolution gate (that workflow only resolves open PRs targeting main); the same topology applies at this head, so the same artifact is expected there, and the security disposition for the new range remains owner-owned. At the superseded head, one desktop Tauri discovery test (probe_codex_acp_version_returns_version_when_descendant_holds_pipe_open) failed on a single runner while the same suite passed on another runner in the same window (same source; mechanism unknown) — desktop code outside this mobile-only delta; this new head re-tests that boundary and no flakiness/attribution claim is made. A fresh review at this exact head would be appreciated: https://github.com/block/buzz/pull/7389/files

@loganj
loganj requested a review from jedwards27 September 12, 2026 16:43
Carry the immediate ROOT ownership readiness, consumer, queued-live and DM/huddle generation corrections on the authenticated profile prerequisite. Preserve the original ROOT production delta and bind its signed fixtures to the corrected parent.

Signed-off-by: Logan Johnson <loganj@squareup.com>

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — APPROVE

Reviewed exact head 9b39d90c57ebaefbe9ba62eb5ed8573f8e60320f against exact prerequisite/base 80c531511511e1edb66c3354833ad54d3b46e466 (#7588). Both assigned review lanes independently recommend approval, and #7588 is independently approved unchanged at that exact head. No unresolved author-actionable defect was found.

Result

The prior live-revocation defect remains fixed. Owner authority now follows generation-bound, authenticated, advancing kind-0 evidence and becomes authoritative only after EOSE; acquisition, retry/CLOSED, disconnect, and error states fail closed (mobile/lib/shared/mentions/agent_identity_provider.dart:77-132; mobile/lib/shared/profile/user_cache_provider.dart:12-26,133-154,205-223). Newer revocation or invalid owner evidence removes the owner projection, while stale replay cannot overwrite the ordered head.

Lifecycle actions/details and mention classification use the same availability fence rather than stale retained AsyncValue data (mobile/lib/features/channels/channel_actions_sheet.dart:100-122; mobile/lib/features/channels/channel_details_page.dart:76-108; mobile/lib/features/channels/mentions/mention_candidates_provider.dart:89-111). The production-seam tests exercise authorization, revocation, reauthorization, invalid evidence, unavailable/retry states, reconnect, stale replay, and retired DM/huddle admission through actual consumers (mobile/test/shared/mentions/agent_identity_provider_test.dart:27-157; mobile/test/shared/mentions/r1_dm_admission_test.dart:25-142).

The #7389 production patch is byte-identical to the previously reviewed patch; this head refreshes the prerequisite and rebinds tests to its authenticated ingress contract. The exact old/new production diffs compared equal with SHA-256 a30f6b3f5cb9d93caba091db0132f1ee3c2c0fe0934385d4a80d5610825bde36.

Validation

At clean exact head 9b39d90c57ebaefbe9ba62eb5ed8573f8e60320f:

  • just mobile-test / full touched package: 2,152/2,152 passed.
  • just mobile-check: 559 files formatted, zero changed; Flutter analyzer reported no issues.
  • git diff --check: passed.
  • Independent causal mutations removing live admission and lifecycle availability guards failed the intended production-seam tests, then sources were restored and worktrees rechecked clean.
  • Exact live head/base and #7588 approval/head were rechecked immediately before submission.

CI and residual risk

Exact-head Mobile CI remained in progress. Desktop integration jobs fail before tests while pulling removed MinIO images; those failures also concern infrastructure outside this mobile-only delta, while the aggregate relay result is green. CI owns final gate reconciliation.

No native iOS/Android accessibility/device journey or live-relay authorization journey was run. No layout/input component changed; these are reviewer/release confidence gaps, not author rework.

Author action: none.
Verification owner: CI/release owners for remaining exact-head jobs and optional native/live-relay observation. Any head or base change invalidates this approval.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — APPROVED

Reviewed exact head 9b39d90c57ebaefbe9ba62eb5ed8573f8e60320f against exact stacked base 80c531511511e1edb66c3354833ad54d3b46e466 (#7588). The prerequisite was independently approved at that exact SHA; any head or base movement expires this approval.

Both assigned review lanes found no material defect. The base-relative production patch is byte-for-byte unchanged from the previously reviewed delta. It replaces the one-shot owner snapshot with generation-bound live kind-0 admission, denies authority before EOSE and during retry/error/disconnect, rejects retired-generation callbacks and stale replay, and makes lifecycle controls plus mention provenance consume the same availability-fenced authority (mobile/lib/shared/mentions/agent_identity_provider.dart:77-132; mobile/lib/shared/profile/user_cache_provider.dart:12-26,133-154,205-223; mobile/lib/features/channels/channel_actions_sheet.dart:100-122; mobile/lib/features/channels/channel_details_page.dart:76-108; mobile/lib/features/channels/mentions/mention_candidates_provider.dart:89-111).

The prior live-revocation defect remains resolved through authorization → revocation → reauthorization → invalid evidence → reconnect/replay. #7588 now rejects invalid signed envelopes before profile ordering/cache admission, so this delta's owner projection rests on authenticated, deterministic evidence.

Author action: none.

Verification owner: CI for completion of the exact-head Mobile gate; reviewer/release tooling for optional native iOS, VoiceOver, and live-relay observation.

Validation at exact clean head: both independent lanes passed just mobile-check, full Mobile 2,152/2,152, and git diff --check. Production-seam mutations removing live admission and lifecycle availability guards failed the intended owner/lifecycle assertions, then were restored clean. Tests cover real owner, mention, action-sheet/details, DM, huddle, cache-generation, and reconnect consumers.

Confidence gaps: no native simulator/device or live-relay journey was run; this changes state/authority plumbing rather than layout or input structure. GitHub Mobile was still running at verdict time. Red Desktop integration leaf jobs fail while pulling removed MinIO images, are outside this Mobile-only delta, and roll into a green aggregate Results gate; they are not attributed to this PR. The non-main stacked security authorization limitation remains an external topology/gate concern, not author rework.

@loganj
loganj force-pushed the mobile/7389-prerequisite-5827692d branch from 80c5315 to ef698b4 Compare September 12, 2026 17:55
@loganj
loganj force-pushed the fix/mobile-ownership-20260905 branch from 9b39d90 to 24035e2 Compare September 12, 2026 17:55
@loganj

loganj commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

🤖
Final-head base/equivalence update to the source response: #7389 is now 24035e24b6cb80d5d576c57bc27479358a3ad3b2, based on mobile/7389-prerequisite-5827692d at ef698b4d6048adcb73785c986e55fe6249ecbada. The chain inherits main #7599’s MinIO fix. Mobile subtree and immediate patch are byte-identical to 9b39d90c (598 lines, unchanged ROOT delta). Full Mobile 2,152 / statics / 222 locked roots and independent pair PASS 042c01c5 are reused through equivalence, not rerun. The sheet completion-only mutation survivor and readback/invite selection-only causality limits remain as disclosed. No CI/security/native/merge clearance claim. GitHub currently shows APPROVED, but reviews 5187253564/5187266226 explicitly expire on head/base movement. Please refresh that head/base disposition for this proven-equivalent delta, rather than repeat the source explanation or commission a new technical cycle solely for the base update.

@loganj
loganj requested a review from jedwards27 September 12, 2026 18:00

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — APPROVE

Reviewed exact head 24035e24b6cb80d5d576c57bc27479358a3ad3b2 against exact prerequisite/base ef698b4d6048adcb73785c986e55fe6249ecbada (#7588). Both assigned review lanes independently recommend approval, and #7588 remains independently approved at this exact refreshed head. No unresolved author-actionable defect was found.

Changed-head reconciliation

The complete #7389 base-relative patch is byte-for-byte equal to the previously approved 80c53151…9b39d90c patch (SHA-256 16b492be68e54c4d83e862aedba29dd11a6880ad97accdc28fa188cf84b89d86). The Mobile trees are identical (a660aa4b736b0013d45a4e49c93145626ebe49d9), the stable Mobile patch-id is unchanged (4fbbeb2d3ac1cf965ff835e6aa066eeb88230342), and the refreshed prerequisite differs only in five non-Mobile MinIO/architecture/deploy files.

Result

Owner authority remains generation-bound, authenticated, and ordered by (createdAt, lowest eventId); it becomes authoritative only after EOSE and fails closed during acquisition, disconnect, CLOSED/retry, or error (mobile/lib/shared/profile/user_cache_provider.dart:12-27,133-154,205-223; mobile/lib/shared/mentions/agent_identity_provider.dart:77-133). Newer signed revocation or invalid owner evidence removes authority, stale replay cannot restore it, and newer signed reauthorization can.

Lifecycle controls and mention classification reject unavailable owner state rather than consuming retained stale data (mobile/lib/features/channels/channel_actions_sheet.dart:92-122; mobile/lib/features/channels/channel_details_page.dart:67-108; mobile/lib/features/channels/mentions/mention_candidates_provider.dart:89-111). DM/huddle admission separately requires ready identity, membership, role, and preload sources (mobile/lib/features/channels/channel_detail_page.dart:431-494). The production-seam regressions cover authorization, revocation, reauthorization, invalid advancing evidence, reconnect/stale replay, and retired admission.

Validation

At clean exact head 24035e24b6cb80d5d576c57bc27479358a3ad3b2:

  • just mobile-check: passed; 559 files formatted with zero changes and Flutter analyzer reported no issues.
  • Full Mobile: one lane passed 2,152/2,152. The other independently reached 2,151 passed / 1 unrelated failure in voice_note_recording_test.dart:529, where teardown double-deleted a temporary directory; #7389 changes neither that file nor path, and the focused touched-path suite passed.
  • Six affected/adjacent test files: 285/285 passed.
  • git diff --check: passed.
  • Fresh causal mutations removing live profile admission and the Details loading/error authority fence failed the intended production regression, then sources were restored clean. The Actions rendering branch independently suppresses lifecycle actions during loading/error, making its additional predicate defense-in-depth.
  • Exact live head/base, prerequisite head, and prerequisite approval were rechecked immediately before submission.

CI and residual risk

Exact-head Mobile CI for #7389 and #7588 remained in progress; completed DCO, Semgrep, zizmor, macOS/Windows builds, Mobile Swift, and integration aggregate jobs were green or path-skipped. CI owns required-job completion.

No fresh native iOS/Android accessibility/device or live-relay huddle/audio journey was run. Those are reviewer/release confidence gaps, not author rework.

Author action: none.
Verification owner: CI for final required-job completion; release/mobile QA for optional native/live-relay observation. Any head or base change invalidates this approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants