Desktop migration: Rust backend → Python backend (#5302)#5374
Desktop migration: Rust backend → Python backend (#5302)#5374
Conversation
Replaces the hardcoded omi-desktop-auth Cloud Run URL with the OMI_API_URL environment variable, matching APIClient.baseURL resolution. Python backend already has identical /v1/auth/* endpoints. Closes #5359
Pass redirect_uri from the auth session to the callback HTML template instead of hardcoding omi://auth/callback. This enables desktop apps (which use omi-computer://auth/callback) to receive OAuth callbacks correctly when authenticating through the Python backend.
Both Google and Apple callback endpoints now pass the session's redirect_uri to the auth_callback.html template, enabling dynamic custom URL scheme redirects per client (mobile vs desktop).
Add server-side validation at /v1/auth/authorize to reject redirect_uri values that don't match allowed app schemes (omi://, omi-computer://, omi-computer-dev://). Also fix empty string fallback with 'or' operator.
Use |tojson filter for safe template variable serialization. Add defense-in-depth scheme validation in JavaScript before redirect. Block redirect and manual link for disallowed schemes.
…ering 15 tests covering: - Redirect_uri allowlist validation (rejects https, javascript, data, ftp, empty) - Allowed schemes pass (omi://, omi-computer://, omi-computer-dev://) - Google/Apple callback uses session redirect_uri in template - Fallback to default omi://auth/callback when missing - XSS safety: JSON-escaped redirect_uri prevents script injection
When FIREBASE_API_KEY has app restrictions (e.g. Android-only), the signInWithIdp REST API returns 403. Fall back to decoding the Google id_token JWT, looking up the user via Admin SDK (get_user_by_email), and creating a custom token directly. This makes auth work regardless of API key restrictions.
Desktop app uploads transcriptions via this endpoint but Python backend only had it in the developer API (API key auth). This adds a user-auth version to conversations router, reusing the same process_conversation pipeline. Defaults source to 'desktop', accepts timezone and input_device_name fields sent by Swift client.
New router for desktop app's session-based chat: - GET/POST/GET/:id/PATCH/:id/DELETE /v2/chat-sessions - POST /v2/desktop/messages (simple save, not streaming) - PATCH /v2/messages/:id/rating
…sage_rating, add data protection decorators to save_message, fix get_chat_sessions filtering and pagination
…lete for session messages
…nds, thumbs-down rating
New router for desktop screen activity sync. Accepts up to 100 screenshot rows per batch, writes to Firestore via existing database/screen_activity.py, and upserts Pinecone ns3 vectors in a background thread. Matches Rust backend contract. Closes part of #5302 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up the new screen_activity router for desktop migration. Part of #5302 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per-section merge for assistant_settings preserves sibling sections. AI profile uses full-replace semantics. Both use Firestore set(merge=True) for document-creation safety. Part of #5302 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GET/PATCH /v1/users/assistant-settings with per-section merge, validation (prompt length, list caps, confidence range). GET/PATCH /v1/users/ai-profile with RFC3339 timestamp validation and 10KB profile_text truncation. Matches Rust backend contracts. Part of #5302 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap upsert in try/except and return 500 with controlled message on failure. Matches Rust error handling behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents stale nested keys from persisting. Falls back to set(merge=True) when document doesn't exist yet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Require T separator and timezone in generated_at. Store as parsed datetime for Firestore timestampValue compatibility with Rust. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rejects non-standard offsets like +00 or +0000. Requires full YYYY-MM-DDTHH:MM:SS(Z|+HH:MM|-HH:MM) format matching Rust. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents unrelated update errors from silently converting to merge-write path. Only falls back on document-not-found. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Avoids adding google.api_core import to database/users.py. Checks e.code == 404 to narrow fallback, re-raises other errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create endpoints return 200 (not 201) matching Rust default - Invalid date on GET skips filter instead of 400 (Rust ignores bad dates) - Invalid category on GET skips filter instead of 400 (Rust accepts any) - PATCH empty body updates updated_at only (Rust behavior), not 400 - Missing advice on PATCH returns 500 (Rust), not 404 - duration_seconds=0 preserved (not replaced with 60 default) - mark_all_read ignores per-item failures (match Rust let _ = pattern) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…boundary caps (50 total)
## Summary Port staged tasks (7 endpoints) and daily scores (2 endpoints) from Rust desktop backend to Python, as part of Day 2 migration (#5302). ### Endpoints added - `POST /v1/staged-tasks` — create with case-insensitive dedup - `GET /v1/staged-tasks` — list filtered by completed=false, ordered by relevance_score ASC + created_at DESC tie-break - `DELETE /v1/staged-tasks/{id}` — idempotent hard-delete (matches Rust) - `PATCH /v1/staged-tasks/batch-scores` — batch update relevance scores - `POST /v1/staged-tasks/promote` — promote top-ranked task to action_items (max 5 active AI tasks, [screen] prefix/suffix dedup) - `GET /v1/daily-score` — daily completion score (due_at filter) - `GET /v1/scores` — daily + weekly + overall scores with default_tab selection ### Key behaviors matching Rust - Create dedup: case-insensitive description match, skips deleted tasks - List: `completed=false` Firestore filter, client-side deleted skip, created_at DESC tie-break - Delete: idempotent (no 404 for missing items) - Weekly score: uses `created_at` range (not `due_at`) - Promote: strips `[screen]` prefix/suffix for dedup comparison ### Review cycle changes - Round 1: Fixed 6 Rust-parity issues (dedup, filters, idempotent delete, weekly field) - Round 2: Added created_at DESC tie-break ordering, moved in-function imports to top level - Round 3: Approved (PR_APPROVED_LGTM) ### Test coverage (50 tests) - 8 model validation tests - 16 staged task endpoint tests (incl. [screen] dedup, boundary caps, promote at 4 active) - 8 score endpoint tests - 18 DB-layer unit tests (dedup logic, completed/deleted filtering, scoring field verification, idempotent delete) All 50 tests pass. Tests are registered in `test.sh`. ## Test plan - [x] 50 unit tests pass (`pytest tests/unit/test_staged_tasks.py -v`) - [x] DB-layer tests verify Firestore query fields (daily=due_at, weekly=created_at) - [x] DB-layer tests verify dedup logic (case-insensitive, whitespace trim, skip deleted) - [x] Boundary tests: description 2000/2001, limit 0/1/501, offset -1 - [x] [screen] prefix/suffix normalization tested in promote flow - [x] Codex reviewer approved (3 rounds) Closes part of #5302 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary - Port focus sessions and advice endpoints from Rust to Python backend (issue #5302, Day 2) - 9 new endpoints matching Rust contract for desktop macOS app migration ## Endpoints | Method | Path | Purpose | |--------|------|---------| | POST | `/v1/focus-sessions` | Create focus session (focused/distracted) | | GET | `/v1/focus-sessions` | List with date filter + pagination | | DELETE | `/v1/focus-sessions/{id}` | Delete session | | GET | `/v1/focus-stats` | Daily stats with top 5 distractions | | POST | `/v1/advice` | Create advice with category/confidence validation | | GET | `/v1/advice` | List with category filter, dismissed toggle | | PATCH | `/v1/advice/{id}` | Update is_read/is_dismissed | | DELETE | `/v1/advice/{id}` | Delete advice | | POST | `/v1/advice/mark-all-read` | Batch mark unread as read | ## Files changed | File | Lines | What | |------|-------|------| | `database/focus_sessions.py` | +76 | Firestore CRUD | | `database/advice.py` | +114 | Firestore CRUD + mark-all-read | | `routers/focus_sessions.py` | +154 | 4 endpoints with stats computation | | `routers/advice.py` | +139 | 5 endpoints with validation | | `main.py` | +4 | Router registration | | `tests/unit/test_focus_sessions.py` | +170 | 21 unit tests | | `tests/unit/test_advice.py` | +214 | 24 unit tests | | `test.sh` | +2 | Test registration | ## Verification **Unit tests:** 45/45 passing **Live Firestore tests:** 23/23 passing against dev backend (port 8791, based-hardware-dev) **Note:** `GET /v1/advice` default query (filters `is_dismissed=false`) requires a Firestore composite index (`is_dismissed` + `created_at` DESC). Works with `include_dismissed=true`. Index URL in server logs for prod setup. ## Codex review - CP4 approach review identified 2 bugs, both fixed: - `update_advice` 404 path was unreachable (Firestore throws before returning None) - Date filter `<= 23:59:59` excluded sub-second timestamps, fixed to `< next_day` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
E2E Live Firestore Test — Day 2 Endpoints (21/21 PASS)Local backend on port 8789 running trunk Staged Tasks (8/8)Daily Scores (3/3)Focus Sessions (5/5)Advice (5/5)All 21 endpoints hit real Firestore and return expected responses. by AI for @beastoin |
## Summary
Eliminates the dedicated `omi-desktop-auth` Cloud Run service by
pointing the desktop macOS app's auth flow at the Python backend
(`api.omi.me`), which already has identical `/v1/auth/*` endpoints.
### Changes
**Swift (`AuthService.swift`)**
- Replace hardcoded `omi-desktop-auth` Cloud Run URL with `OMI_API_URL`
env var
- Uses same resolution as `APIClient.baseURL`: `getenv()` →
`ProcessInfo` → fatal
- Added log line showing which auth host is used per sign-in attempt
**Python (`auth.py` + `auth_callback.html`)**
- Pass `redirect_uri` from auth session to callback HTML template
- Template now uses dynamic `{{ redirect_uri }}` with `|tojson` safe
serialization
- Enables desktop apps (`omi-computer://auth/callback`) to receive OAuth
callbacks correctly
- Falls back to `omi://auth/callback` for mobile compatibility
**Security hardening**
- Server-side validation: `redirect_uri` must match allowed app URL
schemes (`omi://`, `omi-computer://`, `omi-computer-dev://`)
- Client-side defense-in-depth: JavaScript validates scheme before
redirect
- Empty string fallback: uses `or` operator instead of `.get()` default
- Template uses `|tojson` filter to prevent XSS from malformed URIs
**Tests (15 new)**
- `test_auth_routes.py`: redirect_uri validation, callback template
rendering, XSS safety
- Added to `test.sh`
### Why
- Desktop auth used a **separate Cloud Run service** with identical code
to the Python backend
- One less service to maintain, deploy, and monitor
- Aligns with migration plan (#5302): Python backend is source of truth
### Risk
- **Low**: Python backend auth endpoints are identical to the dedicated
service
- **Mitigated**: Dynamic `redirect_uri` with dual validation ensures
both mobile (`omi://`) and desktop (`omi-computer://`) schemes work
safely
- **Deploy order**: Python backend changes must ship before Swift client
update
- **Rollback**: Revert single Swift file to restore dedicated service
URL
## Testing
- [x] Backend unit tests: 15 new tests all passing
- [x] Backend tests in test.sh
- [x] Python backend auth endpoints accept desktop redirect_uri (307 →
Google/Apple OAuth)
- [x] Clean build on Mac Mini (1072 objects)
- [x] Source verification: zero references to omi-desktop-auth
- [x] [Live test
evidence](https://storage.googleapis.com/omi-pr-assets/pr-5360/live-test-evidence.md)
## Checkpoints
- [x] CP0: Skills discovery
- [x] CP1: Issue #5359 understood
- [x] CP2: Workspace setup
- [x] CP3: Exploration complete
- [x] CP4: Codex consult (3 turns)
- [x] CP5: Implementation + commits
- [x] CP6: PR created
- [x] CP7: Review approved (iteration 3)
- [x] CP8: Tests approved (iteration 2)
- [x] CP9: Live backend validation
Closes #5359
Part of #5302
_by AI for @beastoin_
E2E Live Test — Full Trunk (Day 1 + Day 2 + Auth)Local backend (23/23 PASS)Backend on port 8789 running trunk Mac Mini E2E (13/13 PASS)Python backend tunneled to Mac Mini ( All endpoints hit real Firestore. Integration PR ready for merge. by AI for @beastoin |
…ions Path updates (5 endpoints): - v2/chat/initial-message → v2/initial-message - v2/agent/provision → v1/agent/vm-ensure - v2/agent/status → v1/agent/vm-status - v1/personas/check-username → v1/apps/check-username - v1/personas/generate-prompt → v1/app/generate-prompts (POST→GET) Decoder hardening: - ServerConversation.createdAt: use decodeIfPresent with Date() fallback - ActionItemsListResponse: try "action_items" then "items" key (Python vs staged-tasks) - AgentProvisionResponse/AgentStatusResponse: make fields optional, add hasVm - UsernameAvailableResponse: support both is_taken (Python) and available (Rust) Graceful no-ops: - recordLlmUsage(): no-op with log (endpoint removed) - fetchTotalOmiAICost(): return nil immediately (endpoint removed) - getChatMessageCount(): return 0 immediately (endpoint removed) Remove staged-tasks migration: - Remove migrateStagedTasks() and migrateConversationItemsToStaged() from APIClient - Remove migration callers and functions from TasksStore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary Sub-PR A for #5302 Day 3: Python backend endpoints needed by desktop app. **Endpoints added:** - `POST /v2/chat/generate-title` — LLM-powered chat session title generation with graceful fallback - `GET /v1/conversations/count` — Firestore aggregation count with stream fallback **Database layer:** - `count_conversations()` — Firestore count() aggregation - `stream_conversations()` — generator for unbounded counting fallback ## Test plan - 23 unit tests across 2 test files (12 generate-title + 11 conversations-count) - Both files in `test.sh` - Coverage: success paths, auth, LLM fallback, boundary truncation (50/100/500 chars), status parsing/normalization, aggregation fallback parity, validation (empty, too many statuses) - All 23 tests passing ## Review cycle - CP7 reviewer: approved (3 rounds — fixed mutable default arg, statuses validation, stream fallback) - CP8 tester: approved (4 boundary tests added for coverage gaps) Closes part of #5302
…5381) ## Summary Sub-PR for #5302 desktop migration Day 3 — Swift client adaptation for Python backend. **Path updates (5 endpoints):** - `v2/chat/initial-message` → `v2/initial-message` - `v2/agent/provision` → `v1/agent/vm-ensure` - `v2/agent/status` → `v1/agent/vm-status` - `v1/personas/check-username` → `v1/apps/check-username` - `v1/personas/generate-prompt` → `v1/app/generate-prompts` (POST→GET) **Decoder hardening:** - `ServerConversation.createdAt`: `decodeIfPresent` with `Date()` fallback - `ActionItemsListResponse`: custom decoder tries `"action_items"` then `"items"` key (Python action-items vs staged-tasks endpoints) - `AgentProvisionResponse`/`AgentStatusResponse`: fields made optional, added `hasVm` field for Python's minimal response shape - `UsernameAvailableResponse`: supports both `is_taken` (Python) and `available` (Rust) via custom decoder **Graceful no-ops (3 endpoints removed from Python):** - `recordLlmUsage()` → no-op with log - `fetchTotalOmiAICost()` → returns nil immediately - `getChatMessageCount()` → returns 0 immediately **Remove staged-tasks migration (no longer needed):** - Removed `migrateStagedTasks()` and `migrateConversationItemsToStaged()` from APIClient - Removed migration callers and helper functions from TasksStore ## Files changed - `desktop/Desktop/Sources/APIClient.swift` — all path/decoder/no-op changes - `desktop/Desktop/Sources/AgentVMService.swift` — adapt to optional AgentVM response fields - `desktop/Desktop/Sources/Stores/TasksStore.swift` — remove migration functions and callers ## Notes - `regeneratePersonaPrompt()` now points to `v1/app/generate-prompts` (GET). The response shape differs from the old `v1/personas/generate-prompt` — may need attention if called. - AgentVM endpoints (`vm-ensure`, `vm-status`) return minimal `{has_vm, status}` from Python vs full VM details from Rust. The service gracefully degrades but won't have `authToken`/`ip` until those are added to Python responses. ## Test plan - [ ] Verify Swift compiles (no build errors from optional changes) - [ ] Mac Mini desktop test: app launches, no crash on startup - [ ] Verify action items load (ActionItemsListResponse decoder) - [ ] Verify username check works on persona page 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Day 3 Integration CompleteBoth sub-PRs merged into trunk:
Integration trunk status
Remaining before merge
by AI for @beastoin |
E2E Live Test Evidence (CP9) — kaiTested against local Python backend (localhost:8789) on A. New Desktop Endpoints (4/4 pass)
B. Adapted/Path-Changed Endpoints (4/4 pass)
C. Desktop Chat Session Flow (7/7 pass)
D. Mobile Regression Check (8/8 pass — no breakage)
Ren's Mac Mini Evidence (7/7 pass)OAuth sign-in OK, 7 endpoints verified via curl, desktop app launched successfully. All checkpoints passed (CP0-CP9). PR ready for merge. by AI for @beastoin |
Mac Mini Live Test — Python Backend Only (CP9)CONFIRMED: Desktop app runs fully on Python backend with zero Rust dependency. Evidence1. Authenticated app loading real data from Python backend: App shows beastoin's conversations loaded from dev Firestore via Python backend. Sidebar navigation, tasks, conversations all functional. 2. Backend logs — authenticated API calls from Mac Mini (100.126.187.125): All for UID R2IxlZVs8sRU20j9jLNTBiiFAoO2 (beastoin), all 200 OK. 3. OAuth flow verified through Python backend: 4. No Rust backend running:
5. Setup:
SummaryAll Day 1-3 endpoints work. Desktop app is fully operational on Python backend. Rust backend can be safely decommissioned. by AI for @beastoin |

Summary
Complete desktop migration from Rust backend to Python backend for issue #5302.
Day 1-2: Auth + Desktop Chat
omi-desktop-authCloud Run → Python backend/v1/auth/*endpoints (PR Migrate desktop auth from dedicated Cloud Run to Python backend #5360)routers/chat.py(PR Backend-listen memory: fix false-positive alert + reduce memory footprint #5371)Day 3: Endpoint Parity + Swift Adaptation
POST /v2/chat/generate-title— LLM-powered session title generation with fallbackGET /v1/conversations/count— Firestore aggregation with stream fallbackaction_items/items, optional Date fields, UsernameAvailableResponse handlesis_taken/availableRemaining (deferred — fail-soft)
authToken/ipfields — Python returns minimal{has_vm, status}, service degrades gracefullyTest plan
test.shpasses (pre-existing failures in unrelatedtest_process_conversation_usage_context.pyonly)Files changed
backend/routers/chat.py— generate-title endpointbackend/routers/conversations.py— count endpointbackend/database/conversations.py— count/stream functionsdesktop/Desktop/Sources/APIClient.swift— 5 path fixes + decoder hardeningdesktop/Desktop/Sources/AgentVMService.swift— optional authTokendesktop/Desktop/Sources/Stores/TasksStore.swift— migration removalCloses #5302