Skip to content

fix(backups): secure pCloud setup and durable conversation exports - #22826

Merged
aryanorastar merged 41 commits into
mainfrom
fix/pcloud-backup-safety
Oct 25, 2026
Merged

aryanorastar merged 41 commits into
mainfrom
fix/pcloud-backup-safety

Conversation

@aryanorastar

@aryanorastar aryanorastar commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Refs #20213. Builds on #20498 while preserving its contributor commits; do not merge the unsafe foundation first.

This PR completes the account/consent/durable-delivery boundary rather than adding another UID-only observer or uncorrelated audio buffer. Maintainer authorization now covers merging verified source and the repository's normal automatic development-deployment/app-release pipelines. No manual deployment, pCloud credential provisioning, provider activation, remote-data mutation or issue closure is requested.

Changes

  • Opt-in per-app backend transport: account-bound one-use setup capabilities, exact origin checks, scoped completion reads, signed finalized delivery, unchanged idempotency/SSRF/redaction for existing apps.
  • Secure cookie sessions, CSRF-protected POST settings/disconnect, explicit transcript/audio opt-in, authoritative US/EU OAuth callback routing and truthful pending revocation.
  • Encrypted transactional jobs with per-file receipts, stable account/conversation paths, bounded retries, restart leases, expiry scrubbing, key rotation and account/consent/generation fences.
  • Saved audio uses the canonical Omi playback path, not UID-only live buffers. Current app/account/conversation access is rechecked before provider writes. Missing/unready audio is visible, not reported as uploaded.
  • Real FastAPI/provider/store tests are registered in both deterministic manifest lanes; no production framework/model shims or public/default encryption key.

Architecture and rollout constraints

The standalone service requires one replica on an explicitly configured persistent volume. It does not silently fall back from Redis to JSON or deploy onto ephemeral Cloud Run disk. No existing Redis/file data is read, deleted or automatically migrated. Any existing installation must be inventoried and migrated explicitly before rollout.

The shared plugin SDK remains models-only. Transport and job architecture/checklist are documented in backend/docs/plugin-transport.md and plugins/omi-pcloud-app/README.md; capture/playback ownership is recorded in the pipeline guide. User-facing setup/status remains the existing mobile/desktop integration flow.

OMI_PLUGIN_TRANSPORT_JSON and the matching standalone configuration are intentionally unprovisioned. Reviewed runtime secret bindings on every finalization host, a pCloud developer app/test account, minimal provider permissions/callbacks, persistent-volume backup/restore, real connected US/EU export/retry/revocation and separately authorized rollout remain release gates. Live provider configuration has not been verified; local fixtures are not provider credentials. Code readiness and activation are separate. The new registered pCloud transport stays inactive until provisioned; activation remains blocked on the release gates above. Existing-client fresh-grant and credential-isolation fixes do affect later app builds, so the whole PR is not described as inactive. Keep #20213 open until they are verified.

Line-Count-Exception: backend/utils/apps.py | 1716 -> 1731 | Re-enable validation owns the probe list, so the registered-transport branch (trusted-origin check plus non-mutating health probe instead of an unsigned delivery) belongs inside validate_app_endpoints_for_reenable rather than a parallel validator.

UI evidence

UI-Evidence: none

Both app-home and setup-step actions fetch fresh launch grants. Flutter app_detail.dart, macOS AppsPage.swift, Windows AppDetailSheet.tsx and web AppDetail.tsx retain the existing layout. Web uses a same-styled button, reserves a tab synchronously, then navigates it to the fresh URL. A failed refresh closes the reserved tab and shows a retryable inline error. Local browser evidence below uses synthetic loopback grants, not a connected provider.

Review follow-up

  • Launch grants on every client: macOS, Windows and web reused the grant loaded with the sheet, so a second Open (or one after the grant expired) failed. Each now refetches app details right before opening, as Flutter already did.
  • Folder changes for unfinished uploads: a queued or retrying job kept the folder captured at enqueue. It now adopts the current destination before uploading and restarts its receipts there; a folder change mid-upload fails the job with a retry prompt instead of splitting it across folders.
  • Completed receipts after expiry: expiry scrubbed completed and canceled jobs too, wiping their per-file receipts. It now scrubs only unfinished payloads; rows still go after the 30-day purge.
  • Failed backups beyond 30 entries: the list was the newest 30 rows, so older failed jobs lost their retry. It now returns the newest 30 plus every failed or pending job (bounded by the existing 100 unfinished-job cap).
  • Each item has a test that failed before the fix: three plugin tests, plus a Windows and a web test that open twice and expect a new grant each time.

Second review follow-up (2026-10-23)

  • Disconnect retires pending OAuth attempts. Previously, with attempts A and B pending, completing A, disconnecting, then completing B restored an active connection. Disconnect now rotates a durable per-account authorization epoch (also when no connection exists). Connect stamps the epoch on the OAuth state; the callback rechecks it under the account lock before the token exchange (a retired code is never exchanged) and again after it (a token issued to a retired attempt is revoked, not stored). If pCloud does not confirm that revocation, the page says so and points to removing the app in pCloud; a newer connection is never overwritten.
  • Windows/Linux first-time Install uses fresh grants. Apps.tsx beginSetupFlow built setup and completion URLs from the undecorated catalog item. It now fetches this account's app details first, keeps signed query bytes via urlWithUidParam, and launches, polls or enables nothing after a failed read, an account change or an unmount. The Install button stays busy during the read, so a repeated attempt gets a new grant.
  • Regressions that failed before the fix: four plugin route tests (two pending attempts then Disconnect; Disconnect with no connection; Disconnect during a paused token exchange, with revocation confirmed and unconfirmed) and six Windows page-level Install tests. Guard tests also cover a newer connection surviving a stale exchange, a new Connect after Disconnect, another account staying connected, and canceled jobs staying canceled.
  • Evidence on 137509030a: plugin suite 73 passed (real FastAPI routes and SQLite, synthetic provider I/O, network denied); Windows Apps/AppDetailSheet/appInstall 61 passed, typecheck clean; isolated loopback service smoke (boot and restart) passed. No live pCloud OAuth: the developer console is signed in, but app creation awaits pCloud approval, so client credentials and a real token exchange remain unverified.

Earlier verification (before the 2026-10-23 update)

  • bash scripts/run-plugin-pcloud-tests.sh: 67 real installed-service/provider/storage/signing contract tests passed with live provider requests blocked, including completed/partial receipt preservation on disconnect.
  • Selected backend runner: 132 tests passed in ten isolated files at the unchanged local 0.30s CPU guard, covering app setup/install, real finalized fanout composition, saved-audio access and existing finalization/playback behavior.
  • Route-policy inventory: 27 tests passed at the same unchanged CPU guard; GET/HEAD declare scoped plugin authority without expanding the legacy missing-route baseline.
  • Pinned backend type check: 0 errors, 41,730 warnings; existing warning debt remains.
  • Actual local browser/HTTP path with synthetic provider IO: account-bound link consumption/cookie handoff, capability URL scrubbing, opt-in defaults, saved preferences, retry admission/completion and failed revocation checked. Desktop and 390px embedded production-page rendering inspected; no live pCloud account or native-client build was used.
  • Local PR preflight: 38 selected manifest checks passed. The failure-class recurrence/history audit explicitly skips this shallow checkout; full-history CI remains authoritative. Normal pre-push results are recorded separately below.
  • Normal pre-push gate passed without bypasses, including the same 60-test plugin suite, pinned backend type check, OpenAPI checks and three changed backend test files. Its broad 2,064-file selector was bounded by the repository's unchanged 40-file cap; this is not a claim that the complete backend/platform suite ran locally.

Local source verification is on bbace15c92, based on main c84b9138f4c46b76728465656e4653308c64a289. Main advanced ten non-overlapping commits during this work; final GitHub merge-tree checks remain a gate before any merge.

Expected consent behavior comes from #20213. Region routing, OAuth token behavior and revocation are defined by pCloud authorization, OAuth tokens and logout. Existing mock metadata/response conventions are not treated as authority over those contracts.

Failure-Class: FC-unsigned-disconnect-deletes-tokens

Line-Count-Exception: desktop/macos/Desktop/Sources/MainWindow/Pages/AppsPage.swift | 3748 -> 3759 | Fresh home and setup-step launches and page/account retirement stay in the existing detail-sheet owner; AppSetupURL owns fresh-read fencing and signed-query preservation.

Line-Count-Exception: backend/routers/apps.py | 2673 -> 2696 | Existing marketplace integration entry points need setup-capability decoration and one GET/HEAD handoff; signing, authorization and audio logic are extracted into focused utility modules, with route-composition tests.

Why not a new one-off guard: this PR provides the account-bound transport primitive and durable state owner, then exercises production HTTP/state transitions; the existing unsigned-disconnect class already records this trust-boundary failure. The new manifest entry runs that behavioral suite rather than scraping source for a preferred implementation.

UI-Evidence: none — Python-rendered standalone setup is outside released-client UI paths; local synthetic screenshots and browser observations are recorded in this task, not advertised as deployed/customer proof.

View guided diff Turn on auto-fix

2026-10-23 maintainer review and merge gates

Ready for code review at Aryan's request; the gates below block merge, not review. The receipt/folder/retry-history fixes from the prior review are present. This pass resolves the current-main manifest conflicts, refreshes both home and setup-step grants on every client, preserves signed query parameters on Windows, rejects stale/failed launch reads, and removes mobile vendor completion bearer forwarding, signed-URL logging and app-detail body logging. The existing catalog owner also rejects retired account/ABA detail reads. Existing legacy apps without a completion endpoint keep optional setup. Local evidence: 67 installed plugin/provider/store tests, 78 isolated backend tests, 25 Flutter launch/authorization/privacy tests, 46 Windows renderer/install tests, 10 native Swift URL/launch-boundary tests, final Flutter analyzer, and the complete web check (6 Bun smoke + 876 Vitest tests) pass. The full macOS debug build also passed in 619.16 seconds with two jobs; this is not macOS 14 runtime acceptance or the full desktop test suite. A real browser with the production AppDetail and synthetic loopback HTTP grants accepted distinct grants on repeated setup clicks, closed a reserved tab after a forced 503, and recovered on retry. These are synthetic provider boundaries, not live pCloud OAuth/US/EU exports or installed-client acceptance. The source fix is c43b217; documentation-only follow-ups 7f34a3c and 9deac74 repair the backend instruction budget and pin a removed historical page reference to its measured source snapshot. At that historical checkpoint those items remained open. The current code-readiness and separate activation decisions are recorded in the 2026-10-25 update below. The mobile vendor isolation intentionally overlaps #21827; retain that boundary when integrating the two PRs. No deployment or credentials provisioned.

Maintainer readiness and scope update (2026-10-25)

  • Final reviewed head: 922986c34d2f8e86595c48c48e7616d2c5fb7e46; no new blocking code defect identified in the current follow-up. Fresh local bash scripts/run-plugin-pcloud-tests.sh: 73 passed with synthetic provider IO and network denied. The changed transport implementation is unchanged from the previously reviewed head; prior valid browser, client-launch, storage, account/epoch and revocation regression evidence is reused, not presented as a fresh live-provider run.
  • Current-head CI is green: 46 successful checks, 15 appropriately skipped, none pending/failing before readiness. All six backend shards and aggregate, required Mobile Release Eligibility, backend hermetic gate, Windows renderer/build smoke, Swift contracts, Flutter checks, Android emulator acceptance and iOS compile passed. The earlier head's backend wall-budget failure is not a failure of this head.
  • Local metadata-only preflight: five selected checks passed; final diff check and merge-tree against current main are clean. No baseline, timing budget, branch protection or required check was bypassed. The earlier shallow local history skip is not represented as a full-history ratchet pass.
  • The maintainer explicitly accepted the repository's normal automatic development deployments and app release train accompanying this merge. This is not authorization to create/configure the pCloud developer app or provision its transport. Those remain separate operator work.
  • Keep OMI_PLUGIN_TRANSPORT_JSON and the standalone provider configuration unprovisioned. Before pCloud activation, verify approved company/provider ownership, minimal grants and exact callbacks, real US/EU export/retry/revocation, and persistent-volume backup/restore. Keep feat(backups): add pCloud and extensible cloud-backup destinations #20213 open; a source merge is not customer recovery or proof of live-provider operation.
  • This PR overlaps fix: complete referral and integration authorization recovery #21827 in mobile app setup and completion handling. Integrate the second PR against the merged first PR, retain vendor-bearer isolation/fresh grants and account/attempt ownership, regenerate API clients after schema merges, and rerun the affected tests.

ahmedcompk and others added 29 commits October 4, 2026 00:55
…versation export

- Fix ensure_folder sequential parent creation to prevent pCloud API error 2002
- Enforce path traversal rejection (. and ..) in upload_file
- Add db.py with authenticated SHA256-CTR token encryption and account isolation
- Add main.py FastAPI app with OAuth connect/disconnect and setup webview
- Add /conversation webhook with explicit privacy consent gating (summary, transcript, audio)
- Add 25 hermetic unit/integration tests running in 0.19s without external dependencies
- Add Procfile and railway.toml deployment configurations

Failure-Class: none
…and logout revocation

- Normalize test response accessors (_get_body_text, _get_redirect_location) and expose Starlette-compatible body bytes and location headers in fallback shims for 100% environment parity
- Make _get_encryption_key fail fast with RuntimeError when PCLOUD_TOKEN_ENCRYPTION_KEY is unset
- Implement PCloudClient.logout() calling pCloud /logout and invoke it in /disconnect
- Add .gitignore for plugins/omi-pcloud-app ignoring data/ token store, pycache, and env files
- Pin requirements.txt to exact == dependencies
- Add unit tests for logout endpoint and crypto key fail-fast (28 tests total, all passing)

Failure-Class: none
…ocation

- Normalize FastAPI Query default sentinel objects in auth_pcloud_callback when called directly as a coroutine
- Pass error=None explicitly in test_app.py callback tests for 100% test pass rate with installed FastAPI dependencies
- Add unit test verifying direct coroutine invocation unwraps Query sentinels safely (29/29 tests passing)

Failure-Class: none
… in callback

- Drop unused tenacity==8.2.3 from requirements.txt
- Remove test-accommodation hasattr(param, 'default') unwrap in auth_pcloud_callback (main.py) to restore pure framework typing
- Use TestClient wrapper when available and direct invocation fallback in test_app.py
- Replace sentinel-normalization unit test with dedicated test_auth_pcloud_callback_error_param asserting 400 Bad Request on provider errors
- Verified 29/29 tests pass hermetically; 100% Black 120 and byte-level diff hygiene compliant

Failure-Class: none
…eipts

Failure-Class: FC-unsigned-disconnect-deletes-tokens

Preserve the #20498 foundation commits. Add authenticated setup/delivery, explicit private export consent, canonical conversation audio reads, encrypted durable retries, truthful revocation, and manifest-backed regression coverage. Local-only verification; keep #20213 open pending live account, runtime binding and rollout proof.
Declare scoped audio route policies and cover completed/partial receipt preservation in the production store and HTTP tests.

Failure-Class: FC-unsigned-disconnect-deletes-tokens
@mintlify

mintlify Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
omi 🟢 Ready View Preview Oct 25, 2026, 5:20 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9514483528

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/utils/plugin_transport.py
Comment thread desktop/windows/src/renderer/src/components/apps/AppDetailSheet.tsx Outdated
@cubic-dev-ai

cubic-dev-ai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Auto-fix isn't included in your plan ↗

Upgrade to Team

@Git-on-my-level

Copy link
Copy Markdown
Member

{"body": "Since my last review: addressed (verified on this head \u2014 fresh launch grants on every client: app_detail.dart refreshes details before auto-open and routes opens through the injectable openAppHome; AppsPage.swift openExternalApp() now awaits loadAppDetails() before opening; AppDetailSheet.tsx onOpenApp refetches details each Open and falls back to current fields; AppDetail.tsx opens the tab synchronously so it is not popup-blocked, then navigates it to the fresh URL. Unfinished jobs now adopt the current destination folder before uploading, and a folder change mid-upload fails the job with a retry prompt instead of splitting it across folders (worker.py run_next_job). Expiry scrubs only unfinished payloads; completed/canceled per-file receipts survive to the 30-day purge (store.py claim()). The settings list keeps every failed/pending job reachable beyond the newest 30 (store.py jobs()), bounded by the 100-unfinished cap) \u00b7 still open (nothing code-side from my review; the two pre-merge asks below are maintainer decisions, not rework) \u00b7 newly introduced (nothing material).\n\nOne note on backend/AGENTS.md: the Service Map addition points coding agents at docs/plugin-transport.md for the authenticated standalone-plugin handoff. The pointer matches the shipped code (utils/plugin_transport.py), so the guidance is accurate \u2014 flagging it since it steers future AI-contributed backend work toward that contract.\n\nThe four failing checks reduce to two test failures in files this PR does not touch \u2014 capture_live_recovery_test.dart (BLE fault-injection timing, expected 210 vs 200) and MeetingMemoryBriefRenderingTests.swift (brief-card width) \u2014 which look like pre-existing flakes rather than regressions from this diff. They still need to be green (or confirmed flaky on main) before merge.\n\nUSER IMPACT: none until the plugin transport is provisioned (opt-in, server-side config only); once live, opening a backup app always gets a fresh sign-in link, and backup jobs resume per file with honest failure states and reachable retries.\n\nBefore merge I'd still want: maintainer sign-off on the new plugin_capability auth mechanism itself (a new trust boundary, inert until provisioned), and confirmation of the single-replica persistent-volume deployment plan.\n\n---\nby AI on behalf of David \u2014 human maintainer sign-off needed for the new plugin_capability auth mechanism before merge."}

@aryanorastar
aryanorastar marked this pull request as draft October 23, 2026 05:55
Retain both the plugin tests and nightly CI contract registrations, and both plugin capability and capture-policy route declarations. Installed-service suite: 67 passed. Four isolated backend files: 78 passed. Client refinements remain separate working changes; keep draft pending provider and volume verification.
Failure-Class: FC-unsigned-disconnect-deletes-tokens

Fetch fresh grants for both settings and setup steps on every client; preserve signed query bytes, reject failed/stale account reads, and never forward Omi bearer credentials to vendors or log capability-bearing details. Reuse the existing app-detail owner and the shared transport. Verified: 25 focused Flutter tests, 10 native Swift boundary tests, 46 Windows tests, full web check (6 Bun + 876 Vitest), 67 plugin tests, 78 isolated backend tests, plus real browser synthetic HTTP retry. Keep draft for live provider, installed-client, volume, security acceptance and final-head CI. No deployment or customer writes.
Keep the plugin transport link, move its operational detail to the owning contract, and remove a duplicate general documentation reminder already enforced by root AGENTS.md. Lean-guide check passes at 38,988 bytes; no runtime behavior changes.
The global agent-doc reference gate exposed a historical onboarding path that is no longer in main. Verified the source at b0f8455 through GitHub, linked that immutable snapshot and explicitly qualified the old graph measurements. Reference and lean-guide checks pass; no app code or measured values changed.
@aryanorastar

Copy link
Copy Markdown
Collaborator Author

Non-device verification follow-up — 2026-10-23

All checks on current head 9deac7421dae859a2f5a7cbdc204f1a3cad89088 are now successful or intentionally skipped, including Desktop Swift Static & Test Contracts. The older Oct 21 mobile/native failures are not the current-head result.

Fresh local verification:

  • bash scripts/run-plugin-pcloud-tests.sh: 67 passed, one existing Starlette/AnyIO warning.
  • Actual loopback Uvicorn service, two separate process starts sharing one isolated database: health 200 and UID-only setup 401 on both; both owned processes stopped.
  • Current production Store with synthetic records: restart preserves partial receipts; consistent SQLite backup/restore preserves credentials/preferences and retry state; key rotation on the restored copy permits the new key and rejects the retired key; integrity and plaintext-absence checks pass.

These are local synthetic tests, not live pCloud OAuth/export or hosted-volume proof. The existing developer console is accessible, but New app explicitly requires provider approval; no approved test OAuth application was available in that view. No application request, new grant, credential creation, live file write or deployment was performed.

Still draft: approved minimal-scope test app/callback, live US/EU connect/export/retry/revoke, hosted-volume acceptance and independent security/operator acceptance remain gates. Pairwise integration with #21827 also has conflicts in app/lib/backend/http/api/apps.dart, app/lib/pages/apps/app_detail/app_detail.dart and app/test/widgets/app_install_flow_test.dart. Reconcile after the first PR lands and rerun shared setup/account-fencing regressions; do not drop either PR's trust-boundary changes. This pairwise check did not modify either branch or checkout. No merge performed.

@Git-on-my-level

Copy link
Copy Markdown
Member

Verdict: strong, security-first hardening of the pCloud backup foundation — no blocking correctness issue found in review, but two design decisions need maintainer sign-off before this leaves draft.

What I verified in the largest surfaces:

  • backend/utils/plugin_transport.py — HS256 with pinned algorithm, iss/aud/scope/jti required, HTTPS origin pinning rejecting userinfo/fragments, malformed OMI_PLUGIN_TRANSPORT_JSON fails closed, and authorize_app_setup decorates a deep copy so cached marketplace records never carry launch grants. update_app in backend/routers/apps.py strips uid/omi_token before persisting owner edits.
  • backend/utils/plugin_backup_audio.py — conversation-scoped bearer, account-deletion enforcement, app availability + enablement recheck, discarded/locked/deleted conversations return 404.
  • backend/utils/app_integrations.py — the delivery signature binds sub + path + method + sha256 of the canonical payload, and token injection leaves the existing SSRF pinning, circuit breaker and idempotency key intact.
  • plugins/omi-pcloud-app (main.py, store.py, worker.py, security.py) — hashed HttpOnly session with CSRF on every mutation, one-use setup nonces, uid-bound encrypted SQLite envelopes with key rotation, lease-based worker, consent_version fence that forces transcript/audio off, per-file consent recheck under the same account lock as settings/disconnect, generation fences on disconnect, and GCS-only no-redirect audio downloads capped at 512 MB / 120 s. Implementation matches the README and backend/docs/plugin-transport.md claims.
  • Clients (app_detail.dart, isAppSetupCompleted in apps.dart, Windows AppDetailSheet.tsx, web AppDetail.tsx, macOS AppSetupURL) — fresh fetch before every launch, no cached-grant fallback, account-change fences. The Flutter change also stops forwarding Omi's bearer token to non-Omi origins and stops logging signed URLs (covered by api_secret_log_safety_test.dart) — a real security improvement.

Two points for maintainer sign-off:

  1. backend/utils/app_integrations.py mints a 7-day, conversation-scoped audio-read token into every finalized-conversation delivery to a registered transport app, regardless of the user's audio-export consent on the plugin side. The read handler rechecks account/app/conversation eligibility, and consent is enforced plugin-side at upload time, but the capability itself is issued unconditionally. Registration-is-operator-trust is a reasonable design, but it deserves an explicit human yes.
  2. backend/AGENTS.md drops the sentence "Update this map for inter-service call changes." while adding the plugin-transport link. The added pointer is accurate; if the map-update instruction is meant to survive, please restore or relocate it.

Agent-behavior note: this PR edits backend/AGENTS.md, an agent instruction file. The plugin-transport pointer matches the shipped code and is safe to follow; the dropped map-update sentence (point 2) is the part that changes what AI coding agents will do in this repo, so a human maintainer should confirm that edit.

Still wanted as a reviewer:

  • Confirmation that the new pcloud-backup-contract-tests checks-manifest lane actually executes on PRs — it did not appear in this PR's check rollup.
  • A staged end-to-end run against a real pCloud app before rollout (the suite is hermetic by design; provider network is denied).

Performance: one extra authenticated GET per app-home/setup open on each client — acceptable for one-use grants. Blast radius: legacy unsigned integrations are unchanged by construction (no transport registration → original behavior), pinned by tests.

USER IMPACT: none until the pCloud backup app is registered and rolled out; afterwards, opening an app's settings or home always uses a fresh single-use sign-in link instead of a possibly expired or already-used one.

Security-sensitive auth/consent design (point 1) needs maintainer sign-off; leaving for human maintainer review under security-review.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@aryanorastar
aryanorastar marked this pull request as ready for review October 23, 2026 14:11

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9deac7421d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/utils/plugin_transport.py
Comment thread plugins/omi-pcloud-app/main.py
@aryanorastar
aryanorastar marked this pull request as draft October 23, 2026 16:32
…resh grants

- Disconnect rotates a durable per-account authorization epoch, even with no
  connection. The callback checks the epoch before and after the token
  exchange under the account lock: a retired attempt never exchanges its code,
  and one already exchanging revokes its new token instead of storing it.
- The Windows/Linux Apps page fetches fresh account-bound app details before
  launching setup and polling completion, and launches, polls or enables
  nothing after a failed read, an account change or an unmount.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aryanorastar

Copy link
Copy Markdown
Collaborator Author

Pushed 137509030a with two fixes:

  • Disconnect now retires pending pCloud authorizations. Before, starting two connection attempts, finishing the first, disconnecting and then finishing the second reconnected the account. Disconnect now rotates a stored per-account authorization generation, even when no account is connected. The callback checks that generation under the account lock both before and after the token exchange. A retired attempt never exchanges its code. An attempt already mid-exchange revokes the token it was just issued instead of saving it. If pCloud doesn't confirm that revocation, the page says so. A newer connection is never overwritten.
  • Windows/Linux first-time Install uses fresh grants. Apps.tsx beginSetupFlow now fetches this account's app details before opening setup or polling for completion, and keeps the signed URL parameters intact. It opens, polls or enables nothing if that fetch fails, the account changes, or the page closes.

Evidence:

  • Plugin suite: 73 passed (real routes and SQLite, simulated pCloud responses, network blocked). Four of the new tests fail on the previous head.
  • Windows Apps, AppDetailSheet and appInstall tests: 61 passed. All six new Install tests fail on the previous head.
  • Windows typecheck is clean, and the local service smoke test (boot and restart) passed.
  • scripts/pr-preflight: 63 checks passed.

Not verified: a live pCloud OAuth exchange. pCloud hasn't approved the developer app yet, so there are no real client credentials. This PR stays in draft while the release prerequisites in the description are open.

@aryanorastar

Copy link
Copy Markdown
Collaborator Author

Merged current main (df89f72d6c) to clear the conflict. The only conflicting file was .github/agent-docs/hot-reload-invalidation.md, where both sides had fixed the same stale link; I kept main's wording.

About the earlier backend CI failure: the first run timed out installing system packages. On the re-run, the backend shards failed on seven pyright "unnecessary isinstance" errors in utils/retrieval/tools/edit_tools.py and utils/retrieval/turn_refs.py. This PR doesn't touch those files, recent main runs report 0 pyright errors, and pyright on both files in this merged tree reports none either. This push should show whether they're gone.

On the merged head: plugin suite 73 passed, Windows Apps/AppDetailSheet/appInstall 61, Flutter install-flow and setup-authorization 23, and the PR's backend tests (transport, routes, route policy, auto-disable recovery) all passed.

@aryanorastar

Copy link
Copy Markdown
Collaborator Author

Merged current main (1f18e24f29, no conflicts) to get a fresh backend run.

Why the previous head failed: every test in backend shard 1 passed, but the shard's total time ran 740–748 s, over the unchanged 720 s budget. The cause was the timing, not a test:

  • CI gives each shard every sixth file from the sorted file list. This PR's four backend test files therefore reshuffle every shard assignment, and shard 1 drew a heavier set: 639 s of test time, versus 538 s for main's shard 1 at the same hour.
  • main itself went over budget at 04:07 (shard 4, 727 s), so runners were slow across the board then. main has passed consistently since 09:09.
  • This PR's backend tests take 1–3 s per file locally, and its new modules add about 10 ms to the shared import.

I didn't raise the budget. On the merged head: plugin suite 73, the PR's backend tests (transport, routes, route policy, auto-disable recovery) all passed, and the OpenAPI, TS-client and Dart-DTO checks are clean.

@Git-on-my-level

Copy link
Copy Markdown
Member

Verdict: the security architecture here is sound and unusually well-tested for this size — no blocking code findings from me — but this stays a human-maintainer decision, and one CI wiring gap should be fixed before this leaves draft.

What I verified on the current head:

  • backend/utils/plugin_transport.py: registration fails closed on malformed OMI_PLUGIN_TRANSPORT_JSON; HS256 tokens carry iss/aud/sub/scope/jti/exp; setup_url() strips any pre-existing uid/omi_token query params before appending fresh ones, so an owner-edited URL can't inherit or smuggle a grant; delivery headers sign the SHA-256 of the canonical payload, and the canonicalization matches plugins/omi-pcloud-app/security.py::verify_delivery exactly on both sides.
  • backend/routers/apps.py: get_app_details() decorates a deep copy, so cached/public records never carry grants, and update_app() strips launch credentials before persistence. backend/utils/plugin_backup_audio.py: the new backup-audio route rechecks registration, scope, conversation binding, account-deletion access, app-enabled state, and conversation visibility — a query UID never authorizes.
  • backend/utils/apps.py: re-enabling a transport app probes GET /health instead of an unsigned webhook POST, and rejects a webhook moved off the trusted origin.
  • app/lib/backend/http/api/apps.dart: vendor completion checks no longer forward the Firebase bearer off-origin, and log lines no longer embed signed URLs. The fresh-grant launch flows (app_detail.dart/app_provider.dart, Windows Apps.tsx/AppDetailSheet.tsx, web AppDetail.tsx, macOS AppSetupURL.freshDetails) each fetch fresh details per open, refuse cached fallback, and stop on account change or unmount — with tests on every surface.
  • plugins/omi-pcloud-app: single-use setup nonce consumed atomically in SQLite, Secure/HttpOnly/sameSite=lax session, compare_digest CSRF on every POST form, OAuth state bound to the session, MultiFernet-encrypted jobs with leases and expiry scrubbing.

Material notes:

  • CI gap (top action item): the new pcloud-backup-contract-tests entry in .github/checks-manifest.yaml produced no check-run on this head (verified via the check-runs API), so the plugin's ~1,200 lines of tests in test_app.py/test_pcloud_client.py have not executed in CI on this PR — the exact failure mode the check's own reason string cites for feat(backups): add turnkey pCloud cloud-backup plugin service (#20213) #20498. Please confirm the manifest runner picks the entry up on pull_request events (or adjust it) so the suite actually gates this PR.
  • Security: audio bearer tokens embedded in finalized deliveries (backend/utils/app_integrations.py, ttl=7*86400) are long-lived credentials inside a webhook payload. The mitigations are real (per-conversation scope, full server-side recheck at read time, 7-day job expiry), but the lifetime is a maintainer call.
  • Agent guidance: backend/AGENTS.md drops the sentence "Update this map for inter-service call changes." while adding the plugin-transport link — coding agents working in backend/ will no longer be told to update the Service Map when inter-service calls change. If that convention still holds, worth restoring the sentence.
  • Blast radius: legacy/unregistered apps are unchanged by construction (plugin_transport → None passes through), which the tests pin.

USER IMPACT: when this ships, pCloud backup setup links become single-use and per-account — reopening settings always gets a fresh link — and conversation exports become durable, retryable jobs with transcript/audio consent both defaulting to off.

What I'd still want as a reviewer: (1) the pCloud contract check running green on a pushed head, (2) maintainer sign-off on the 7-day audio-token TTL and on merge ordering vs #20498, (3) confirmation that the AGENTS.md Service-Map instruction removal is intentional.

Leaving for human maintainer review: security-sensitive new auth surface and audio-read routes need maintainer sign-off, plus a merge-ordering decision against the stacked #20498.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@aryanorastar

Copy link
Copy Markdown
Collaborator Author

Pushed 922986c34d: merged current main and restored the backend/AGENTS.md instruction.

  • Merge conflict: .github/checks-manifest.yaml. This PR's pcloud-backup-contract-tests entry and main's two speaker-label entries were independent additions, so all three are kept. The manifest contract tests pass.
  • CI wiring: the pCloud suite already runs in CI, but not as its own check-run. On pull requests, manifest entries run as steps of scripts/pr-preflight inside the repo-checks Hygiene job, so the check-runs API never lists them separately. On 1f18e24f29 (run 40087831984, Hygiene job 120856194583) the log shows the suite selected, then ==> pcloud-backup-contract-tests, 73 passed, <== PASS pcloud-backup-contract-tests. It will run again on this head.
  • backend/AGENTS.md: I restored "Update this map for inter-service call changes." in the Service Map. Removing it wasn't intended; the file is still within the agents-md-lean budget.
  • For maintainers to decide: the 7-day lifetime of the audio bearer in finalized deliveries, and the merge order against feat(backups): add turnkey pCloud cloud-backup plugin service (#20213) #20498. I haven't changed either.

Plugin suite 73 passed on this head.

@aryanorastar
aryanorastar marked this pull request as ready for review October 25, 2026 06:12
@aryanorastar
aryanorastar merged commit 8fe094d into main Oct 25, 2026
66 checks passed
@aryanorastar
aryanorastar deleted the fix/pcloud-backup-safety branch October 25, 2026 06:14

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 922986c34d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +28 to +29
if not get_available_app_by_id(app_id, uid) or not is_user_app_enabled(uid, app_id):
raise HTTPException(status_code=403, detail='This backup app is no longer enabled')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject globally disabled apps in the backup access check

If an app is globally disabled while backups are queued, its existing seven-day grants still pass both HEAD and GET here, allowing further uploads and audio downloads. get_available_app_by_id() checks private-app visibility but returns records with disabled=True, while is_user_app_enabled() only checks the user's Redis installation set; disable_app_in_firestore() does not remove those installations. This bypasses the disabled-app filter used by get_available_apps() for conversation fanout. Check the returned app's disabled state before authorizing pending backup writes or issuing audio URLs.

Useful? React with 👍 / 👎.

: step?.url
? urlWithUidParam(step.url, uid)
: null;
if (url) tab.location.href = url;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allowlist navigation schemes before redirecting the reserved tab

For an unregistered app, a developer can supply a javascript: home URL: ExternalIntegration.app_home_url in backend/models/app.py is an unrestricted string, and the create/update handlers do not validate its scheme. Assigning that URL here executes developer-controlled JavaScript in the reserved blank tab, which inherits Omi's origin; setting opener = null does not sandbox its storage access. The previous React-rendered href passed through React 19's JavaScript-URL sanitizer, whereas this imperative assignment bypasses it. Parse and allowlist safe navigation protocols before assigning either home or setup URLs.

Useful? React with 👍 / 👎.

const uid = getCacheUid()
try {
const res = await omiApi.get<App>(`/v1/apps/${app.id}`)
if (!mounted.current || getCacheUid() !== uid) return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore the mounted ref during effect setup

In the Windows/Linux development renderer, main.tsx wraps the app in StrictMode, which runs an extra effect setup/cleanup cycle. This component's existing cleanup sets mounted.current = false, but its effect setup never restores it to true. Consequently, this newly added guard silently rejects every Open App and setup-step launch in development, even after a successful details fetch. Restore the ref in effect setup and cover these launchers with a StrictMode regression test.

Useful? React with 👍 / 👎.

Comment on lines +169 to +171
@app.get('/setup-completed')
def setup_completed(uid: str, omi_token: str):
claims = verify(omi_token, 'status')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Migrate the backend OAuth flow to registered-plugin grants

For a registered plugin using the documented /v1/oauth/authorize flow, the remaining caller in backend/routers/oauth.py still reads undecorated database metadata. Its /v1/oauth/token handler sends only uid to the completion endpoint (lines 213–226), so this newly required omi_token produces 422 and the OAuth exchange reports 503 even when setup is complete. Already-enabled users skip that probe but receive an undecorated home URL (line 270), which likewise cannot establish a pCloud session. Mint status and launch grants in this OAuth handler too, and preserve their query parameters in backend/templates/oauth_authenticate.html, which currently appends a literal ?uid=.

AGENTS.md reference: AGENTS.md:L34-L34

Useful? React with 👍 / 👎.

This branch was successfully deployed

1 active deployment
staging - docs — 922986c3 Deployed Oct 25, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Backend Task (python) needs-maintainer-review Needs a human maintainer to sign off before merge security-review Touches auth, provider routing, secrets, or security-sensitive surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants