Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
45e3077
fix: finish referral and integration connection recovery
aryanorastar Oct 16, 2026
d096aa8
test: use a valid MCP detail in account-switch regression
aryanorastar Oct 16, 2026
a56f1f7
fix: ignore browser receipts after authorization owner changes
aryanorastar Oct 16, 2026
482a6c1
fix: retire pending authorization when task selection changes
aryanorastar Oct 16, 2026
55f85de
fix(app): let the latest task selection own its whole authorization
aryanorastar Oct 16, 2026
d9fa6f5
fix(app): tie MCP registration to its URL and serialize task default …
aryanorastar Oct 18, 2026
a22b2e3
fix(app): retire a whole MCP connect attempt when its URL is edited
aryanorastar Oct 19, 2026
5a002b7
Merge remote-tracking branch 'origin/main' into fix/connection-comple…
aryanorastar Oct 19, 2026
0f5dc4c
fix(app): release MCP polling for its own wait even after an account …
aryanorastar Oct 19, 2026
1bb10cc
Merge remote-tracking branch 'origin/main' into fix/connection-comple…
aryanorastar Oct 20, 2026
a762d47
Merge origin/main into connection recovery
aryanorastar Oct 23, 2026
2cc2f0e
test(app): align task-delivery fixtures with recovery reads
aryanorastar Oct 23, 2026
283c148
fix(app,web): finish non-OAuth MCP setup, fence registrations, keep r…
aryanorastar Oct 23, 2026
70b84d7
fix(app,backend): fence MCP retry cache, fresh OAuth on retry, align …
aryanorastar Oct 24, 2026
bf87146
Merge origin/main into fix/connection-completion-recovery
aryanorastar Oct 24, 2026
b98b032
chore(desktop): regenerate Swift API DTOs for the MCP authorize route
aryanorastar Oct 24, 2026
fbc7212
fix(app,backend): serialize MCP authorization retries; rotate only th…
aryanorastar Oct 25, 2026
09acc4d
Merge current main and preserve fresh-grant connection recovery
aryanorastar Oct 25, 2026
51cdfd3
test(app): migrate plugin setup security checks to typed verdicts
aryanorastar Oct 25, 2026
ed6cd0f
fix(auth): fence MCP callback commits against newer retries
aryanorastar Oct 25, 2026
a6c4d7b
test(backend): isolate app visibility guards from module stubs
aryanorastar Oct 25, 2026
c846bbb
docs(auth): separate recovery readiness from Firebase helper rollout
aryanorastar Oct 25, 2026
223d4c6
ci: refresh security analysis after GitHub API timeout
aryanorastar Oct 25, 2026
918e2b6
test(auth): constrain local OAuth redirect headers
aryanorastar Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": 1,
"images": [
{
"file": "001-web-referral-retry.png",
"platform": "web",
"description": "Real LoginClient with synthetic auth and local failing/successful claim responses; no production account changed.",
"captured_by": "agent",
"source": "screencapture"
},
{
"file": "002-web-referral-retried.png",
"platform": "web",
"description": "Real LoginClient with synthetic auth and local failing/successful claim responses; no production account changed.",
"captured_by": "agent",
"source": "screencapture"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"change":"Integration setup keeps checking after sign-in, offers retry when a check fails, and opens external settings in your browser. Delayed replies cannot change a page or account you have left."}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"change": "Adding an MCP server that does not need sign-in now opens its page so you can enable it. Retrying a sign-in for an MCP server now starts a fresh authorization instead of reopening an expired link, and slow integration status checks no longer cut a connection short"
}
76 changes: 75 additions & 1 deletion app/docs/mobile-browser-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ statuses for other integrations. Task snapshots validate before cache adoption.
checks use that verdict, never the presence of a serialized access token.

Task authorization keeps the prior default until the server confirms the new
grant, then persists the selection. Failure to persist the default remains
grant, then persists the selection. A selection owns its attempt from the
confirmation dialog through launch, polling and save; choosing another app while
an earlier authorization is still launching retires it (latest selection wins).
A launch that throws reports failure and leaves no loading state behind. Failure to persist the default remains
retryable. An unfinished browser flow shows instructions and a manual status
check; it never becomes an endless spinner or a confirmed grant.

Expand All @@ -39,12 +42,17 @@ check; it never becomes an endless spinner or a confirmed grant.
- [x] Terminal, retryable read failures preserve saved connection information.
- [x] Fresh post-auth reads fence old reads and account changes.
- [x] Task selection follows server-confirmed authorization.
- [x] Latest task selection wins even while an older authorization is still
launching; late receipts after page or account changes do nothing.
- [x] Overlapping default saves land in choice order; only the latest publishes.
- [x] An MCP registration is reused only for its own URL, never after an edit.
- [x] Hermetic production-widget, native-bridge and provider regressions.
- [ ] Merge the fix and ship an Android store release (also validate iOS).
- [ ] On Samsung S26 Ultra, complete referral Google signup and return to Omi.
- [ ] Verify the reported integration(s) connect and display the server result.
- [ ] Identify the separate repeated Try Again screen with app version and a
screenshot or exact screen name; investigate any remaining failure.
- [ ] Keep #21378 open until the release ships and the customer confirms.

Featurebase incident: https://github.com/BasedHardware/omi/issues/21378.
The report does not identify the failing integration(s), app version or general
Expand All @@ -54,3 +62,69 @@ not. No account contents or recordings are changed by this fix.
References:
- https://pub.dev/packages/url_launcher#checking-supported-schemes
- https://developers.googleblog.com/upcoming-security-changes-to-googles-oauth-20-authorization-endpoint-in-embedded-webviews/

## Completion audit after #21381

The first PR was merged while the audit continued. The follow-up closes the
remaining mobile and web client gaps:

- Native Calendar/Gmail and task authorization keep checking when the grant is
saved after foreground return; default selection follows the confirmed grant.
- Marketplace setup uses a typed successful boolean verdict, serial probes,
bounded initial/detail reads and explicit retry. Failed responses and absent
endpoints cannot claim setup complete. Only explicit setup/enable intent may
auto-install after confirmation.
- MCP waits for approval AND the server's enable receipt, bounds creation/status
calls, retries an existing registration only for the URL it was made for,
stops waiting when the URL is edited, cancels on leaving and cannot pop a
different route from a late callback.
- External settings/home pages join affiliate signup in the system browser;
embedded Google popup handling and spoofed user agents are retired. URL/uid
construction, server-stored grants and conversation data are preserved.
- Vendor completion probes use the documented uid contract without forwarding
Omi's Firebase bearer token; vendor failures cannot expire the Omi session.
- Web referral activation has one token/request/body deadline, a working manual
retry and account/route ownership fences. It does not automatically retry an
eligibility rejection or alter the desktop referral destination.

These recovery changes do not change Firebase's auth domain, provider callback
URIs or auth-helper proxy, and can be merged independently of #21355. The new
MCP reauthorization route must be available on the target backend before clients
using fresh OAuth retries are released; merging code does not deploy that route.

Complete resolution of #21378 also needs #21355's separate web sign-in fix.
That PR changes the Firebase helper origin: its web client and generated Bun
proxy are one coordinated release unit, and its exact Google/Apple callback
settings must be verified before that change is rolled out. Follow the
`web/app/docs/firebase-auth.md` from #21355, including authorized-domain coverage
(a configured parent domain can cover its subdomains). Verify Google/Apple
sign-in, referral return, cancellation and destination preservation in the
provider-connected integration environment. Simulator/browser acceptance is
sufficient for code verification; the reporter's Samsung outcome is separate
release/customer follow-up. Local provider fixtures do not verify live settings.
The unspecified general Try Again screen still requires the reporter's screen,
version and integration names; this audit does not invent its cause or close the
customer incident from code merge.

## MCP retry and callback commit ownership

An explicit retry uses the existing registration and requests a fresh single-use
OAuth state and PKCE verifier. Verifier rotation updates only that nested field,
so a callback finishing during provider discovery cannot have its tokens erased.
The callback exchanges its code and discovers tools outside Firestore, then
atomically compares the app owner and verifier before committing credentials,
tools and approval. A newer retry, changed owner or deleted registration retires
that callback: it returns 409 and cannot enable the app or replace newer tokens.
Successful current callbacks retain the existing automatic enable behavior.

Verification checklist:

- [x] Held token-exchange and tool-discovery callbacks cannot overwrite a newer
completed authorization; only the current callback enables the app.
- [x] Strict transaction tests reject changed owner/verifier and missing records,
preserving unrelated integration fields on a successful commit.
- [x] Local HTTP OAuth/MCP provider validates PKCE; real isolated Redis checks
single-use state/replay and enablement, with Firestore emulator persistence.
- [ ] For #21355 and complete incident resolution, verify real provider
configuration and Google/Apple sign-in/referral return. The local MCP
provider is synthetic, not proof of those settings or customer recovery.
2 changes: 2 additions & 0 deletions app/e2e/flows/app-detail.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ name: app-detail
description: Apps → App Detail — view app info, reviews, capabilities, install/enable toggle
covers:
- app/lib/pages/apps/app_detail/app_detail.dart
- app/lib/pages/apps/app_detail/app_setup_owner.dart
- app/lib/pages/apps/widgets/omi_external_browser_page.dart
- app/lib/pages/apps/app_detail/reviews_section.dart
- app/lib/pages/apps/app_detail/reviews_list_page.dart
- app/lib/pages/apps/page.dart
Expand Down
1 change: 1 addition & 0 deletions app/e2e/flows/referral-program.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ name: referral-program
description: Settings → Referral Program → system browser signup → return to Omi
covers:
- app/lib/pages/referral/referral_page.dart
- app/lib/pages/apps/widgets/omi_external_browser_page.dart
- app/lib/pages/settings/settings_drawer.dart
prerequisites:
- auth_ready # User completed real sign-in (Google/Apple) and onboarding. Android: `bash setup.sh android`. iOS: `bash setup.sh ios`. App must show home screen.
Expand Down
2 changes: 1 addition & 1 deletion app/integration_test/task_delivery_capture.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class _LoadedIntegrations extends TaskIntegrationProvider {
@override
bool get hasLoaded => true;
@override
Future<void> loadFromBackend({bool forceRefresh = false}) async {}
Future<void> loadFromBackend({bool silent = false, bool forceRefresh = false}) async {}
}

Future<void> main() async {
Expand Down
34 changes: 28 additions & 6 deletions app/lib/backend/http/api/apps.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'dart:convert';

Check warning on line 1 in app/lib/backend/http/api/apps.dart

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

app/lib/backend/http/api/apps.dart is 860 lines; consider splitting files over 800 lines.
import 'dart:developer';
import 'dart:io';

Expand Down Expand Up @@ -410,8 +410,10 @@
return '${head.substring(0, queryIndex)}?${[...kept, uidParam].join('&')}$fragment';
}

Future<bool> isAppSetupCompleted(String? url, {ApiSend? send, String? uid}) async {
if (url == null || url.isEmpty) return true;
enum AppSetupStatus { completed, pending, notRequired }

Future<ApiResult<AppSetupStatus>> getAppSetupStatus(String? url, {ApiSend? send, String? uid}) {
if (url == null || url.isEmpty) return Future.value(const ApiSuccess(AppSetupStatus.notRequired));
final target = Uri.tryParse(url);
final omi = send == null ? Uri.tryParse(Env.apiBaseUrl ?? '') : null;
final isOmiOrigin = target != null &&
Expand All @@ -428,17 +430,18 @@
: (request) => HttpPoolManager.instance.send(() => http.Request(request.method, Uri.parse(request.url)),
timeout: const Duration(seconds: 15), retries: 0));
final account = uid ?? SharedPreferencesUtil().uid;
final result = await executeApi<bool>(
return executeApi(
canSend: send == null ? () => SharedPreferencesUtil().uid == account : null,
request: ApiRequest(url: appSetupUrlWithUid(url, account), method: 'GET'),
send: transport,
decode: (body) {
final data = jsonDecode(body);
if (data is! Map || data['is_setup_completed'] is! bool) throw const FormatException('Invalid setup status');
return data['is_setup_completed'] == true;
if (data is! Map || data['is_setup_completed'] is! bool) {
throw const FormatException('Invalid setup status');
}
return data['is_setup_completed'] == true ? AppSetupStatus.completed : AppSetupStatus.pending;
},
);
return result is ApiSuccess<bool> && result.data;
}

Future<(bool, String, String?)> submitAppServer(File file, Map<String, dynamic> appData) async {
Expand Down Expand Up @@ -805,6 +808,25 @@

/// Add an MCP server as a private app with chat tools.
/// Returns {app_id, requires_oauth, auth_url?, tools_count?, tool_names?}
/// Fresh OAuth authorization for an existing MCP registration. The original URL's state
/// is single-use and expires, so an explicit retry needs a new one. Null on any failure.
Future<String?> authorizeMcpServer(String appId) async {
final response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/apps/${Uri.encodeComponent(appId)}/mcp/authorize',
headers: {},
body: '',
method: 'POST',
);
// The body carries a state token; log only the status.
Logger.debug('authorizeMcpServer: ${response?.statusCode}');
if (response == null || response.statusCode != 200) return null;
try {
return wire.GeneratedMcpAuthorizeResponse.fromJson(jsonDecode(response.body) as Map<String, dynamic>).authUrl;
} catch (_) {
return null;
}
}

Future<Map<String, dynamic>?> addMcpServer(String name, String serverUrl, {String? description}) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/apps/mcp',
Expand Down
20 changes: 20 additions & 0 deletions app/lib/backend/schema/gen/apps_wire.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,26 @@ class GeneratedMcpAddServerResponse {
}
}

class GeneratedMcpAuthorizeResponse {
final String authUrl;

const GeneratedMcpAuthorizeResponse({
required this.authUrl,
});

factory GeneratedMcpAuthorizeResponse.fromJson(Map<String, dynamic> json) {
return GeneratedMcpAuthorizeResponse(
authUrl: _required(_readFieldValue<String>(_readField(json, const ["auth_url"]), "auth_url", _readString, requiredField: true, nullable: false), "auth_url"),
);
}

Map<String, dynamic> toJson() {
return {
'auth_url': authUrl,
};
}
}

class GeneratedAppDescriptionGenerationResponse {
final String description;

Expand Down
Loading
Loading