Skip to content

feat: agentic cli qr login flow#30911

Merged
ieow merged 52 commits into
mainfrom
cw/abstarct-sdkconnect-v2
Jun 4, 2026
Merged

feat: agentic cli qr login flow#30911
ieow merged 52 commits into
mainfrom
cw/abstarct-sdkconnect-v2

Conversation

@ieow

@ieow ieow commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Description

Adds end-to-end support for linking MetaMask Agent CLI via QR scan on mobile, and refactors that flow out of generic SDK Connect V2 handling into a dedicated AgenticCli module.

Why: Agentic CLI connections differ from standard dApp MWP sessions—they are short-lived, use an OTP pairing step, require dashboard project selection, and must return an auth token to the CLI rather than persisting a wallet session in the connection registry.

This pr is based on the pr #30442 done by @tylerc-consensys

What changed:

  1. app/core/AgenticCli/ — New module that owns agentic CLI deeplink parsing (connectionType: { name: 'agentic-cli' }), MWP connection lifecycle (untrusted mode, no persistent save), OTP UI hooks, loading toasts, and the QR login pipeline:

    • Wait for keyring unlock
    • Fetch Hydra bearer token → exchange for dashboard access token via /api/v2/mm-qr-login/token
    • Open allowlisted dashboard WebView for project approval
    • Send auth-token message back to the CLI over MWP
    • Clean up the temporary connection
  2. Dashboard WebView (AgenticCliDashboardWebview) — Modal screen + service with build-type origin allowlists, auth token in URL hash + Authorization header, strict mm-agentic-cli postMessage parsing, pending-request resolve/reject with 5-minute timeout, and token redaction in logs. QR-supplied dashboardUrl / dashboardAuthUrl overrides are ignored in favor of configured endpoints.

  3. OTP modal (SDKConnectV2OtpModal) — Bottom sheet shown during MWP pairing with formatted OTP, countdown (1-minute display cap), and auto-dismiss on connect.

  4. SDK Connect V2 refactorConnectionRegistry routes agentic deeplinks to the new handler; MWP payload parsing and analytics move to shared utils (parseMwpConnectDeeplink, trackMwpEvent); relay URL respects dev vs prod; failed connects use cleanupConnection instead of always calling disconnect.

Changelog

CHANGELOG entry: Added QR login flow for MetaMask Agent CLI, including OTP pairing and dashboard project selection in a WebView.

Related issues

Fixes:

Manual testing steps

Feature: MetaMask Agent CLI QR login

  Scenario: user links CLI via QR scan (dev build)
    Given the app is unlocked on a dev build (main_dev / flask_dev)
    And MetaMask Agent CLI is running and displays a QR code

    When the user scans the QR code with MetaMask Mobile
    Then an OTP bottom sheet appears with a pairing code
    And a "Connecting to dApp" loading toast is shown

    When the user enters the OTP in the CLI and the MWP session connects
    Then the OTP sheet dismisses
    And a "Select project" dashboard WebView modal opens (test-dashboard host)

    When the user approves a project on the dashboard
    Then the WebView closes
    And a "MetaMask Agent CLI link successful" notification appears
    And the CLI receives the auth token and can continue

  Scenario: user closes dashboard WebView before approving
    Given the dashboard WebView is open after OTP pairing

    When the user taps Close or navigates back
    Then the connection fails gracefully with "Dashboard approval closed."
    And the temporary MWP connection is cleaned up

  Scenario: user on prod build uses prod dashboard
    Given the app is a prod build (main_prod / flask_prod)

    When the user completes the agentic CLI QR flow
    Then the dashboard WebView loads the prod dashboard origin (dashboard.w3a.io / dashboard.web3auth.io)
    And test-dashboard / dev-dashboard hosts are not allowlisted

Build-type dashboard URLs

Build type Dashboard WebView URL
*_dev https://test-dashboard.web3auth.io/agentic/login
*_uat https://dev-dashboard.web3auth.io/agentic/login
*_prod https://dashboard.w3a.io/agentic/login

Screenshots/Recordings

Before

After

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Test coverage added/updated

  • AgenticCliMwpConnectionService.test.ts — deeplink handling, event wiring, error/cleanup paths
  • AgenticCliQrLoginService.test.ts — Hydra token → dashboard token → WebView → send auth token
  • AgenticCliDashboardWebviewService.test.ts — origin allowlists, URL building, postMessage parsing, pending requests
  • AgenticCliDashboardWebview/index.test.tsx — WebView rendering, message handling, external link policy
  • SDKConnectV2OtpModal.test.tsx — OTP display and countdown
  • connection-registry.test.ts — agentic deeplink routing delegation

Security notes for reviewers

  • Dashboard WebView uses build-type origin allowlists; external top-frame navigations open in the system browser.
  • postMessage events require source: "mm-agentic-cli" and canonical result types (approved, rejected, close, error).
  • Auth tokens are redacted from error logs; QR-provided dashboard URLs are not trusted.

Note

High Risk
Touches authentication (Hydra bearer, dashboard tokens), WebView origin allowlists and postMessage handling, and MWP deeplink routing—security-sensitive paths with a large new surface area.

Overview
Adds MetaMask Agent CLI QR linking as a dedicated flow separate from normal SDK Connect V2 sessions: agentic MWP deeplinks (connectionType: agentic-cli) are handled in a new app/core/AgenticCli module instead of persisting through the generic connection registry.

The flow shows an OTP bottom sheet during pairing, runs a short untrusted MWP connect, then Hydra → dashboard token → allowlisted dashboard WebView for project approval, and returns an auth-token to the CLI before tearing down the temporary connection. QR-supplied dashboard URLs are ignored in favor of build-type config.

New UI includes SDKConnectV2OtpModal, AgenticCliDashboardWebview (origin allowlists, mm-agentic-cli postMessage contract, pending promise + timeout), and navigation routes. SDK Connect V2 now delegates agentic deeplinks, extracts parseMwpConnectPayload / trackMwpEvent, and uses cleanupConnection on failed connects.

Reviewed by Cursor Bugbot for commit 53522dd. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread app/core/SDKConnectV2/utils/relayUrl.ts Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we've actually ever used the dev relay. I'm not certain if it makes sense to do so now or not. I would actually make test-dapp testing a little more cumbersome if we did use the dev relay here.

@adonesky1

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.

+1, agreed — and I think this is actually a break, not just inconvenience. the dApp side mmconnect code hardcodes the prod relay with no env switch (connect-monorepo config), and the relay is a shared Centrifuge pub/sub broker — both ends have to be on the same server + channel or the handshake just times out. since *_dev builds set METAMASK_ENVIRONMENT='dev', a dev build would point at dev-api while every real dApp (and the test-dapp) stays on prod, so they'd never connect. also breaks resume() for connections that were established on prod.

this relayURL feeds handleConnectDeeplink too, so it hits the whole standard dApp path, not just agentic CLI. i'd keep the registry on prod (or have getMwpRelayUrl() return prod unconditionally) and scope any dev-relay override to the agentic flow if it actually needs one.

I would need to consult with @chakra-guy but IIRC last time I asked him he confirmed there is no active dev relay

Comment thread locales/languages/en.json
"expires_in": "Expires in {{time}}",
"expired": "Code expired",
"security_notice": "Securing the connection first. You'll authorize CLI access in the next step"
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should this have so much reference to CLI? I know the CLI is the only one triggering this flow by specifying an "untrusted" connection though

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.

Yeah agreed, maybe worth making this more generic but fine if we want to go with this since this is currently the only case where untrusted mode is used 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

reverted the relay_url here
db0be4b

}
}

private async cleanupConnection(conn: Connection): Promise<void> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this.disconnect already calls conn.disconnect. Is there a reason why the cleanupConnection helper is needed now?

@adonesky1 adonesky1 Jun 3, 2026

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.

on a failed connect, conn isn't in this.connections yet (we only add it after connect() succeeds), so old this.disconnect(conn.id)this.connections.get(id)?.disconnect() → no-op. that meant bridge.dispose() never ran, so we leaked the RPCBridgeAdapter (its global KeyringController:unlock sub + BackgroundBridge). cleanupConnection just calls conn.disconnect() directly so the bridge actually gets cleaned up. (transport's not the leak — WalletClient already tears that down itself on failure.) So that's all to say this is a good change for both the agentic and standard paths!

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

Comment on lines +11 to +31
export const showAgenticCliConnectionLoading = (
conninfo: ConnectionInfo,
): void => {
store.dispatch(
showSimpleNotification({
id: conninfo.id,
autodismiss: AGENTIC_CLI_LOADING_AUTODISMISS_MS,
title: strings('sdk_connect_v2.show_loading.title'),
description: strings('sdk_connect_v2.show_loading.description', {
dappName: conninfo.metadata.dapp.name,
}),
status: 'pending',
}),
);
};

export const hideAgenticCliConnectionLoading = (
conninfo: ConnectionInfo,
): void => {
store.dispatch(hideNotificationById(conninfo.id));
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thoughts on adding an overridable autodismiss timeout to app/core/SDKConnectV2/adapters/host-application-adapter.ts  showConnectionLoading work instead of having the dispatch logic in two different places?

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.

Agreed, looks like the loading toast here is the same as the adapter's showConnectionLoading (identical strings/shape, just a 15s vs 10s autodismiss), so it might be worth consolidating rather than keeping a near-duplicate. Could be as simple as letting the adapter method take an optional timeout, but I'll leave the approach to you. The OTP UI feels genuinely agentic-specific, so that's fine to keep separate. Non-blocking either way.

@ieow ieow dismissed stale reviews from tommasini and himanshuchawla009 via db0be4b June 4, 2026 03:50
Comment thread app/core/AgenticCli/AgenticCliMwpConnectionService.ts
agenticCliStage,
url: redactUrl(url),
});
deps.hostapp.showConnectionError();

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.

Error toast on dashboard close

Medium Severity

Closing or rejecting dashboard approval rejects open() with a user-facing closed message, but the agentic deeplink catch still calls showConnectionError(), so users see the generic “failed to establish connection” toast after an intentional dismiss.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit db0be4b. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is fine to show error toast when user dismiss or reject webview screen

Comment thread app/core/AgenticCli/agenticCliOtpUi.ts
Comment thread app/core/AgenticCli/AgenticCliQrLoginService.ts

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 53522dd. Configure here.

await this.handleAgenticCliDeeplink(url, agenticCliReq);
} else {
await this.handleConnectDeeplink(url);
}

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.

Invalid agentic-cli uses standard MWP

High Severity

When a connect deeplink declares connectionType.name of agentic-cli but fails agentic validation (e.g. non-HTTP dashboardUrl), routing treats it as a normal MWP session. That runs the trusted connect path and can persist the connection instead of the untrusted Agent CLI QR flow.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 53522dd. Configure here.

@ieow ieow Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it is expected to re-route to normal mwp session on failing of parsing agentic link

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeAccounts, SmokeConfirmations, SmokeIdentity, SmokeNetworkAbstractions, SmokeNetworkExpansion, SmokeSwap, SmokeStake, SmokeWalletPlatform, SmokeMoney, SmokePerps, SmokeMultiChainAPI, SmokePredictions, SmokeSeedlessOnboarding, SmokeBrowser, SmokeSnaps
  • Selected Performance tags: @PerformanceAccountList, @PerformanceOnboarding, @PerformanceLogin, @PerformanceSwaps, @PerformanceLaunch, @PerformanceAssetLoading, @PerformancePredict, @PerformancePreps
  • Risk Level: high
  • AI Confidence: %
click to see 🤖 AI reasoning details

E2E Test Selection:
Fallback: AI analysis did not complete successfully. Running all tests.

Performance Test Selection:
Fallback: AI analysis did not complete successfully. Running all performance tests.

View GitHub Actions results

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.81773% with 86 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.84%. Comparing base (1096b41) to head (53522dd).
⚠️ Report is 89 commits behind head on main.

Files with missing lines Patch % Lines
...ponents/Views/AgenticCliDashboardWebview/index.tsx 69.51% 16 Missing and 9 partials ⚠️
.../core/AgenticCli/AgenticCliMwpConnectionService.ts 72.41% 8 Missing and 8 partials ⚠️
app/core/AgenticCli/AgenticCliQrLoginService.ts 76.08% 6 Missing and 5 partials ⚠️
...hboardWebview/AgenticCliDashboardWebviewService.ts 90.21% 0 Missing and 9 partials ⚠️
app/core/AgenticCli/agenticCliConnectionRequest.ts 70.83% 4 Missing and 3 partials ⚠️
.../core/SDKConnectV2/services/connection-registry.ts 64.70% 4 Missing and 2 partials ⚠️
.../SDK/SDKConnectV2OtpModal/SDKConnectV2OtpModal.tsx 89.18% 1 Missing and 3 partials ⚠️
app/core/AgenticCli/agenticCliLoading.ts 60.00% 2 Missing ⚠️
app/core/AgenticCli/sendAuthToken.ts 66.66% 1 Missing and 1 partial ⚠️
...core/SDKConnectV2/utils/parseMwpConnectDeeplink.ts 83.33% 1 Missing and 1 partial ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #30911      +/-   ##
==========================================
+ Coverage   82.73%   82.84%   +0.11%     
==========================================
  Files        5576     5596      +20     
  Lines      143554   144561    +1007     
  Branches    33190    33593     +403     
==========================================
+ Hits       118774   119768     +994     
+ Misses      16874    16723     -151     
- Partials     7906     8070     +164     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

lgtm

@ieow ieow enabled auto-merge June 4, 2026 13:15

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

One more non-blocking comment (agreement w/ @jiexi 's suggestion) otherwise LGTM!

@ieow ieow added this pull request to the merge queue Jun 4, 2026
Merged via the queue into main with commit e986a55 Jun 4, 2026
210 of 213 checks passed
@ieow ieow deleted the cw/abstarct-sdkconnect-v2 branch June 4, 2026 15:29
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 4, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.81.0 Issue or pull request that will be included in release 7.81.0 label Jun 4, 2026
@ieow

ieow commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

add follow up pr to address non blocking comment
#31123

pull Bot pushed a commit to Dustin4444/metamask-mobile that referenced this pull request Jun 10, 2026
…taMask#31123)

<!--
Please submit this PR as a draft initially.

Do not mark it as "Ready for review" until this PR meets the canonical
Definition of Ready For Review in `docs/readme/ready-for-review.md`.

In short: the template must be materially complete (not just section
titles
present), all status checks must be currently passing, and the only
expected
follow-up commits must be reviewer-driven.

address comment :

MetaMask#30911 (comment)
-->

## **Description**

Agentic CLI connection loading duplicated the same notification logic
already implemented in the SDK Connect V2 host application adapter
(`agenticCliLoading.ts` dispatched `showSimpleNotification` /
`hideNotificationById` directly).

This PR removes that duplicate module and routes Agentic CLI loading
through `deps.hostapp.showConnectionLoading` / `hideConnectionLoading`,
keeping a single code path for connection-loading UI.

`showConnectionLoading` now accepts optional
`ShowConnectionLoadingOptions` with `autodismissMs`. The default remains
**10s** for standard SDK Connect flows; Agentic CLI passes **15s** via
`AGENTIC_CLI_CONNECTION_LOADING_AUTODISMISS_MS` to accommodate the
longer MWP → OTP → dashboard connect sequence.

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Fixes:
MetaMask#30911 (comment)
## **Manual testing steps**

```gherkin
Feature: Agentic CLI connection loading

  Scenario: user connects via Agentic CLI deeplink
    Given the app is unlocked and Agentic CLI connect is available
    When user opens an Agentic CLI connect deeplink
    Then a connection-loading notification appears with the dApp name
    And the notification auto-dismisses after ~15s if the flow stalls
    And the notification is hidden when the connect flow completes or fails

  Scenario: standard SDK Connect V2 connection loading is unchanged
    Given a non–Agentic CLI SDK Connect V2 session is establishing
    When connection loading is shown via the host adapter
    Then the loading notification still auto-dismisses after ~10s by default
```

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->
N/A

## **Pre-merge author checklist**

<!--
Every checklist item must be consciously assessed before marking this PR
as
"Ready for review". A checked box means you deliberately considered that
responsibility, not that you literally performed every action listed.

Unchecked boxes are ambiguous: they are not an implicit "N/A" and they
are not
a silent "skip". See `docs/readme/ready-for-review.md` for the full
checklist
semantics.
-->

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

#### Performance checks (if applicable)

- [x] I've tested on Android
  - Ideally on a mid-range device; emulator is acceptable
- [x] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [x] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example

For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).

## **Pre-merge reviewer checklist**

<!--
Reviewer checklist items follow the same semantics as the author
checklist: an
unchecked box is ambiguous, a checked box means the reviewer consciously
assessed that responsibility. See `docs/readme/ready-for-review.md`.
-->

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> UI notification plumbing refactor with no auth or data-path changes;
default 10s autodismiss behavior is preserved for non–Agentic CLI
callers.
> 
> **Overview**
> Removes the Agentic CLI–specific loading helpers in
`agenticCliLoading.ts` and routes connection loading through the shared
SDK Connect V2 **host application adapter** instead.
> 
> `showConnectionLoading` / `hideConnectionLoading` on
`IHostApplicationAdapter` now accept optional
`ShowConnectionLoadingOptions` with `autodismissMs` (default **10s**;
Agentic CLI uses exported **15s** for longer MWP → OTP → dashboard
flows). `AgenticCliMwpConnectionService` calls
`deps.hostapp.showConnectionLoading` / `hideConnectionLoading` with that
timeout rather than dispatching notifications directly. Tests were
updated accordingly and adapter coverage was added for custom
autodismiss.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
aa778c8. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.81.0 Issue or pull request that will be included in release 7.81.0 risk:medium AI analysis: medium risk size-XL team-onboarding Onboarding team

Projects

None yet

Development

Successfully merging this pull request may close these issues.