Skip to content

fix(mobile): read fresh exact-key agent authorization - #7393

Open
loganj wants to merge 4 commits into
fix/mobile-policy-20260905from
fix/mobile-authorization-20260905
Open

fix(mobile): read fresh exact-key agent authorization#7393
loganj wants to merge 4 commits into
fix/mobile-policy-20260905from
fix/mobile-authorization-20260905

Conversation

@loganj

@loganj loganj commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

🤖

Summary

Whether mobile let you mention an agent depended on stale, self-declared runtime claims: an advertised running instance could authorize a mention without real channel membership, membership was read from a contact field anyone can set, and an owned agent with no running instance couldn't be mentioned at all. This PR supplies the read-only fresh signed authority reader that resolves each requested agent key — the latest signed profile plus the owner's current policy (no runtime advertisement required), and channel membership signed by the relay itself rather than self-declared. It does not enforce this at send time: at this head the send path still trusts the persisted picker classification. Actual send-time enforcement lives in the unmerged stack — #7394 @2fd0d0bc (send-time fresh authorization on this reader), #7534 @85ddfafe (currentness fences at the compose boundary), #7536 @156b7ec3 (publication guard across the relay rate-limit wait), #7539 @38c76810 (observed-evidence checks into enqueue) — which must merge on top of this PR, in order and with the #7390 invitation rollout prerequisite, before widened discovery is enabled. No standalone-safety, whole-integration, or network-atomic claim is made here.

  • Membership is read from the relay's own signed identity, not the contact key; exact-destination queries include sender removals, so a newer removal or invalid snapshot can't revive an older permission.
  • Membership pagination is bounded, with same-second cursor ties handled deterministically; stalled pages are rejected.
  • Failed authority reads propagate — a mention never falls back to stale permissions — and account/community scope changes stop in-flight queries so results can't leak across scopes.
  • Directory entries remain hints; authorization answers come from this fresh read, not the directory. On this head that read is not yet enforced at send — send-time reauthorization is the unmerged stack's job (see above).

Desktop reference: commands/agent_discovery/relay_directory.rs, commands/identity_archive.rs.

Related issue

Testing

  • Regressions cover no-runtime ownership, ordinary owned membership, forged membership, wrong destination, latest removal/invalid snapshot, deterministic ties, stalled pagination, scope changes, relay-identity and authority failure.
  • At the branch head: just mobile-check, the full mobile test suite, and full local just ci all pass — receipts in the exact-head evidence comment.
  • Verification is widget-test level; no native device or simulator run is claimed. This reader slice changes no layout.

Screenshots

Flutter production-widget test renders — not native-device screenshots or acceptance captures.

Scenario Before After
Mention suggestions for an agent with only a self-declared runtime claim, no relay-signed membership Before: the runtime claim alone keeps the agent listed After: without relay-signed membership the agent is filtered out; the human member remains
Capture provenance

Rendered by the Flutter widget engine in a flutter test run (production widgets, production theme; no device or simulator). Before: this PR's declared base 804c07f016b7f73504b614d1bf81ef6f0120d83d. After: its head 882f69cbaf0f851717f071fba366b3b3a4a877d8.

…rship

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

loganj commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Independent authorization readiness evidence — 2026-09-07

Verified current stacked range: fix/mobile-policy-20260905 at 804c07f016b7f73504b614d1bf81ef6f0120d83d882f69cbaf0f851717f071fba366b3b3a4a877d8; +491/−11 = 502 across 7 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,094 passed); full just ci exit 0. Receipts: WORK_LOGS/OWNERSHIP_0324B6FF/7393-results.txt (mobile-check:0, mobile-test:0, ci:0) + 7393-envelope.log.
  • Remote: CI run 33993876124 completed success at this exact head. Rollup: 30 success / 25 intentional skips; nothing pending or failing.
  • Fresh review: NIP-11 self (not the contact pubkey) supplies the signed relay-membership signer; exact-destination queries omit #p so sender removal remains visible; the latest snapshot is selected before validation; requested exact keys, query-stage scope fences, bounded pagination, and a deterministic same-second cursor; the runtime cannot supply membership. No substantive defect found.
  • Advisory: intentionally ineligible on this stacked (non-main) base — skipped jobs are not required gates. Root fix(mobile): verify latest signed owner profiles consistently #7389's advisory is complete for its exact range (Overall Risk: NONE, zero findings).
  • Widget evidence only; no native device/simulator. Draft retained; no merge requested.

@loganj
loganj marked this pull request as ready for review September 8, 2026 13:54
@loganj
loganj requested a review from a team as a code owner September 8, 2026 13:54

@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.

Verdict: REQUEST CHANGES

Reviewed: 804c07f016b7f73504b614d1bf81ef6f0120d83d..882f69cbaf0f851717f071fba366b3b3a4a877d8 (exact head 882f69cbaf0f851717f071fba366b3b3a4a877d8)

Risk: high — this changes mobile's relay-derived agent authorization, membership provenance, freshness, and user-visible mention eligibility.

Blocking finding

[P1] The global authorization query excludes the revocation snapshot it must observe.

agentDirectoryProvider invokes readAgentAuthorization without an exact channelId (mobile/lib/shared/mentions/agent_identity_provider.dart:68-86). That path adds #p: [viewer] to its kind 39002 query (mobile/lib/shared/mentions/agent_authorization.dart:101-124). When the relay publishes a newer replacement roster removing the viewer, that authoritative head no longer contains the viewer's p tag and therefore cannot match the query. The fold at agent_authorization.dart:47-85 sees only the older viewer-present roster and retains its channel as current authorization.

That stale channel is consumed by mention eligibility at mobile/lib/features/channels/mentions/mention_candidates_provider.dart:80-104 and mention_candidates.dart:91-118. A non-owned respond_to:anyone agent can consequently remain visible/selectable based on a shared membership that the relay has revoked. The implementation's exact-destination path correctly avoids this trap, but the production directory uses the global path.

A filter-faithful temporary regression with an older viewer+agent roster and a newer viewer-removed replacement reproduced the defect at this head:

Expected: empty
Actual: ['room']
probe_rc=1

The committed removal test does not detect it because its fake query returns supplied events without applying the emitted #p filter.

Author action: make the global read removal-aware. Do not use #p=viewer as the sole source of roster heads when absence from the latest replacement is the revocation signal. A bounded option is to derive current candidate destinations independently and query their exact #d heads in batches, then select the latest relay-signed snapshot per destination before requiring both viewer and agent membership. Add a filter-faithful regression bound through agentDirectoryProvider into autocomplete eligibility, not only the helper.

Verification owner: author for the patch, production-seam regression, and full mobile gates; reviewer for exact-new-head delta and adversarial rerun.

Validation

At matching clean local HEAD 882f69cbaf0f851717f071fba366b3b3a4a877d8:

  • git diff --check 804c07f016b7f73504b614d1bf81ef6f0120d83d..HEAD — pass.
  • flutter test test/shared/mentions/agent_authorization_test.dart test/shared/mentions/agent_policy_test.dart — 17 passed.
  • just mobile-check — pass; 551 files formatted/0 changed, Flutter analyze reported no issues.
  • Filter-faithful viewer-removal causal probe — failed as above, then temporary changes were removed.
  • GitHub CI's Mobile job and aggregate checks are green at this exact head. Those gates do not cover the missing filter-faithful regression.

A separate full just mobile-test run reported 2,094 passing tests, but its checkout metadata was removed externally before a post-run SHA/status recheck; I therefore do not treat that run as independently exact-head-pinned evidence. This does not affect the source-backed defect or the clean exact-head checks above.

Manual/native evidence: no native iOS device/simulator journey was run. The PR changes no layout, but live relay/autocomplete behavior remains unobserved.

Residual risk: async scope fencing, NIP-11 self authority selection, bounded pagination, and failure propagation appear covered by focused tests, but the authorization revocation defect is unresolved. A new head invalidates this verdict until its delta and regression are reviewed.

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

loganj commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review #7393 (review) in c73ff57: #p is now destination discovery only; bounded exact relay-author #d head reads (without #p) replace all seed snapshots before eligibility. The filter-faithful regression exercises viewer-removal through autocomplete, and exact-destination reader API is unchanged. Full mobile suite on this head: 2,095 passed; flutter analyze clean. First full run had one unrelated voice-note temporary-directory cleanup failure; unchanged-head full repeat passed. No native-device validation or repository-wide just ci claim. PR remains ready, 570 additions + 11 deletions against 804c07f.

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.

Verdict: REQUEST CHANGES

Reviewed: 804c07f016b7f73504b614d1bf81ef6f0120d83d..df61d54cb2e4f534a19ce8782a7fd6ccd165b467 (exact live head df61d54cb2e4f534a19ce8782a7fd6ccd165b467)

Risk: critical — this is authorization state used by a user-visible path that can silently add and notify an agent.

Blocking finding

[P1] A relay-signed roster removal does not invalidate the cached directory, and send trusts the stale selection.

The new exact-key reread fixes freshness when a directory read occurs: mobile/lib/shared/mentions/agent_authorization.dart:25-55 now uses the #p result only for destination discovery and reads each current exact #d head before authorizing it. The production lifecycle does not cause that read after a roster replacement, however.

agentDirectoryProvider performs the authorization read only when its non-auto-disposed FutureProvider is constructed and watches only relay session state (mobile/lib/shared/mentions/agent_identity_provider.dart:68-90). Autocomplete keeps consuming that cached success (mobile/lib/features/channels/mentions/mention_candidates_provider.dart:80-104). The kind 39002 live path refreshes current-channel membership and bot-role state, but no membership-triggered invalidation of agentDirectoryProvider was found in the mobile source paths reviewed; the explicit production invalidation found is the special kind 10100 DM identity path at mobile/lib/features/channels/channel_detail_page.dart:150-163.

That stale value has a concrete consequence: after a replacement roster removes an agent while the viewer remains, the picker can reclassify the removed agent as an eligible non-member (mobile/lib/features/channels/mentions/mention_candidates.dart:91-118). Selection persists the stale MentionCandidate, and _scanNonMemberMentions refreshes ordinary members but trusts candidate.isAgent without reauthorizing the exact agent/destination key (mobile/lib/features/channels/compose_bar/helpers.dart:521-558). Send can therefore silently add the revoked agent back and notify it (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:472-508). This defeats revocation in an already-open compose journey.

The committed regression does not protect this lifecycle: it calls readAgentAuthorization directly and then the pure candidate builder (mobile/test/shared/mentions/agent_authorization_test.dart:153-201), bypassing the cached provider and send seam. An independent temporary provider-lifecycle probe at this exact head read an included agent, changed the fake relay to a newer exact #d head removing the viewer, and reproduced the stale result on the second provider read (expected empty; received one entry). Removing the new exact-#d reread separately made the committed removal test fail causally, confirming that narrower fix works but is not reached by production refresh.

Author action: connect authoritative kind 39002 replacement updates to a scope-fenced directory refresh so autocomplete drops revoked entries. Also reauthorize selected non-member agents at the exact destination/key before add/send, fail closed while preserving the draft on query failure or scope change, and add production-seam regressions proving (1) one mounted provider/autocomplete instance observes a later signed removal and (2) a candidate selected before revocation is neither added nor p-tagged afterward.

Verification owner: author for the refresh/send fence, regressions, and full mobile gates; :bot: Jude’s code review agent for exact-new-head adversarial rerun.

Validation at the reviewed head

  • PASS — focused authorization/policy suites: 18 tests.
  • PASS — just mobile-check: 551 files checked, no formatting changes; analyzer reported no issues.
  • PASS — full just mobile-test: 2,095 tests.
  • PASS — git diff --check 804c07f016b7f73504b614d1bf81ef6f0120d83d...HEAD.
  • PASS — causal exact-head mutation of the new exact-#d reread made its removal regression fail, then pass after restoration.
  • PASS — GitHub Clients / Mobile and aggregate Mobile checks at this head.
  • PASS — final local/live head match and clean reviewer trees reported by both independent lanes.

Manual/native evidence: no native iOS/live-relay journey was run. Loading and errors currently fail closed to no relay-agent suggestions, but explicit loading/error/retry UX was not observed; that is a reviewer/tooling confidence gap, not additional author action beyond the established stale-success defect.

Residual risk: native relay/autocomplete timing remains unobserved. The source trace, full mobile suite, and causal provider probe establish the blocking cache/send defect despite that gap.

:bot: Jude’s code review agent

@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: 804c07f016b7f73504b614d1bf81ef6f0120d83d..df61d54cb2e4f534a19ce8782a7fd6ccd165b467.

The changed head fixes refresh-time #p removal blindness by using #p only for destination discovery and then reading exact relay-signed #d heads. The new helper regression is mutation-causal.

[P1] Live roster revocation never refreshes the cached production directory

agentDirectoryProvider performs its fresh authorization read only at construction (agent_identity_provider.dart:68-90); autocomplete consumes cached data (mention_candidates_provider.dart:80-104). Live kind 39002 updates refresh channel member/bot providers, but do not invalidate the directory. A replacement roster removing an agent can therefore leave it visible as an eligible non-member; a previously selected stale candidate can be silently re-added because send trusts candidate.isAgent rather than reauthorizing.

The committed test calls readAgentAuthorization directly with fresh sessions and then a pure candidate helper, bypassing provider cache/lifecycle. A production-provider probe reproduced stale output after a newer exact-#d removal.

Author action: invalidate/recompute directory authorization on authoritative roster-head updates with generation fencing, and preferably reauthorize selected destination/key at send. Add production-seam tests proving one mounted provider/autocomplete drops a later removal and a pre-revocation selection cannot auto-add or recipient-tag afterward.

Verification owner: author for lifecycle/send fixes and full Mobile gates; reviewer for causal exact-head rerun.

Validation: focused authorization/policy suites passed 18; full Mobile passed 2,095; just mobile-check, mutation of exact-#d reread, policy audit, and git diff --check passed at clean exact head. Loading/error currently fails closed to no suggestions but lacks explicit affordance; native iOS timing was not observed. Those are confidence gaps, not extra author work.

@loganj

loganj commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Coverage for new reviews 5157594911 / 5157603700 and the reconsideration 5158416346: this PR remains frozen at df61d54cb2e4f534a19ce8782a7fd6ccd165b467; no backport or standalone-safety claim.

The cached-directory/stale-send finding is covered across sibling descendants:

  1. fix(mobile): revalidate exact agent intent before publication #7394 — send: fresh exact-recipient/destination authorization before prepare and delivery rejects removed non-owned agents rather than trusting saved picker classification. Send regressions cover revoked/removed recipients, draft retention and no publication. These links pin the previously published send evidence; that lane is independently owned and was not changed here.
  2. Corrected fix(mobile): discover owned agents without runtime advertisements #7395 — picker: subscription owner refreshes directory authority on membership events and now recovers terminal CLOSED under a bounded generation budget. The new mounted ComposeBar regression drives production SDK closure/replacement, then a signed 39002 replacement removing the viewer through the live subscription: directory empties and the already-open autocomplete drops the agent. Exact fix(mobile): discover owned agents without runtime advertisements #7395 HEAD b829496e0: 2,103 mobile tests passed; analyzer clean.

#7394 and #7395 are still unmerged siblings. Neither this frozen parent nor either sibling alone supplies both protections. Integrate both (and the existing #7390 invitation rollout prerequisite) before enabling widened discovery. #7530 carries the corrected discovery parent without changing its own authority/cache patch. Native integrated select/add/send validation and repository-wide CI remain separate integration evidence, not asserted by this provider/widget result.

Update at the unchanged head: the send-time body overclaim quoted in review 5158416346 is now corrected in the PR body — two hunks (summary paragraph and the directory-entries bullet), no source or head change, head still df61d54c. The body now states this PR supplies the read-only fresh signed authority reader, does not enforce it at send time on this head, and attributes send-time enforcement and the remaining lifecycle/freshness corrections to the unmerged stack; it makes no standalone-safety, whole-integration, or network-atomic claim.

One review-scope question, so the next pass can be decisive: at this unchanged reader-only head, is there a remaining defect in the reader itself (fresh signed exact-key resolution, bounded pagination, failure propagation), or is the CHANGES_REQUESTED verdict a requirement that this intermediate PR be standalone-safe at its own head — that it independently carry the send-time reauthorization and mounted directory refresh its children add? The answer determines the remedy: a reader fix here versus the inert-parent/immutable-integration-head option the review describes.

@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 author-response reconsideration

Reviewed: 804c07f016b7f73504b614d1bf81ef6f0120d83d..df61d54cb2e4f534a19ce8782a7fd6ccd165b467 (unchanged exact head). Reconsidered against author response 5606140554.

Risk: critical — relay-derived authorization controls a user-visible path that can add and notify an agent.

[P1] The production directory/send path is still not standalone-safe

The response usefully makes the stack dependency explicit, but it also confirms that neither this parent nor either unmerged sibling alone supplies both required protections. That does not resolve this head's production contract:

  • agentDirectoryProvider performs one non-auto-disposed authorization future and watches only session state (mobile/lib/shared/mentions/agent_identity_provider.dart:68-90); autocomplete consumes that cached success (mobile/lib/features/channels/mentions/mention_candidates_provider.dart:80-104). A later signed roster replacement does not refresh the mounted directory.
  • The committed removal regression creates a fresh reader/session rather than exercising one mounted provider (mobile/test/shared/mentions/agent_authorization_test.dart:153-200).
  • Send still trusts persisted MentionCandidate.isAgent (mobile/lib/features/channels/compose_bar/helpers.dart:521-558) and proceeds to add/send (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:472-508) without exact destination/key reauthorization.

The PR body still says resolution uses fresh signed data “at send time” and that only this fresh read authorizes agent mentions. This head has no send-time authorization call site and no live directory refresh. #7394 and #7395 are open parallel descendants based on this head; future code and a deployment-order instruction do not make this independently mergeable production wiring safe. The response's “no standalone-safety claim” narrows the author's assertion, but does not narrow the code or existing PR contract.

Author action: either (1) make this head standalone-safe with scope-fenced authoritative kind-39002 directory refresh and exact destination/key reauthorization before add/send, plus mounted-autocomplete and selected-before-revocation publication regressions; or (2) make this parent genuinely inert/primitive-only and move production adoption and its contract to one immutable integration head containing both protections. Naming separate open siblings is insufficient.

Verification owner: author for the standalone patch or immutable integrated adoption head, causal production-seam regressions, and full Mobile gates; reviewer for exact-new-head delta, mutation/adversarial rerun, and lifecycle/send verification.

Validation: exact local/live head equality and clean reviewer tree; git diff --check passed. Prior same-head evidence remains applicable because source bytes are unchanged: focused 18 tests, just mobile-check, full 2,095-test Mobile suite, and exact-#d mutation passed; the mounted-provider lifecycle probe reproduced stale output. Authenticated reviewer jedwards27 differs from PR author loganj. At reconsideration, #7394/#7395 Mobile lanes passed, while some sibling CI jobs remained in progress; sibling CI does not certify this parent's standalone contract.

Confidence gap / residual risk: no native integrated select → revoke → add/send journey was run. That is not an additional defect and is not the basis for this verdict.

:bot: Jude’s code review agent

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