feat: one-click OAuth connect — consent flow, token persistence, Connect button#2
Merged
Conversation
…ect button - oauth.py: consent-URL builder (offline+consent forces a refresh token), single-use TTL-bound state nonce, code exchange with Google's error_description surfaced, save_secrets persistence (lazy host import, degrades cleanly host-free) - view: gated POST /oauth/start (operator-only, request-derived redirect_uri so :7870/:7871/LAN origins all work) + public GET /oauth/callback (manifest public_paths, nonce-gated); Connect/Reconnect button + connected-as email in the status card, with a poll that refreshes the panel once the grant lands - gmail.profile() for the connected-account display - refresh token takes effect immediately via an in-memory creds swap (no restart) - manifest: oauth_scopes config/setting (default gmail.modify + calendar + drive.readonly) - README: one-click connect + 5-minute Google Cloud client setup guide - tests: 15 new (33 total) — nonce lifecycle, exchange, callback wiring incl. persist-failure warning, manifest public path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
👀 Quinn is reviewing — verdict (PASS / WARN / FAIL) + findings to follow. |
There was a problem hiding this comment.
QA Audit — PR #2 | feat: one-click OAuth connect — consent flow, token persistence, Connect button
VERDICT: WARN (CI in-flight — test is in_progress; formal PASS/FAIL deferred)
CI Status
- test: in_progress ⏳
Diff Review
- New
oauth.py(~117 lines): authorization-code flow with single-use TTL state nonces,access_type=offline&prompt=consentfor refresh tokens, properhttpx.Clientlifecycle infinally, graceful no-host degradation via lazygraph.config_io.save_secretsimport. gmail.py: newprofile()helper for the "connected as <email>" display — clean, minimal.__init__.py: wiresbuild_routerwithscopes_fn+on_refresh_tokencallbacks;_set_refresh_tokenswaps the live_CREDSglobal in-place so the connect takes effect immediately.- README gains the one-click setup guide including Google Cloud Console steps, scope docs, and manual-fallback notes.
Observations
- LOW:
_PENDINGis a module-level in-memory dict inoauth.py. A host restart loses all in-flight state nonces (users see a "bad state" callback and re-click Connect — annoyance, not a defect). In a future multi-worker host this would need a shared store. - LOW: Clawpatch structural review unavailable for this repo (HTTP 500 during mapper init) — review is diff-only.
- GAP: Diff truncated at 200 of 600 lines; the remaining ~400 lines (view.py router, callback handler, tests, manifest) were not inspected. Core OAuth logic in the visible portion is sound.
— Quinn, QA Engineer
|
Submitted COMMENT review on #2. |
This was referenced Jul 3, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces manual refresh-token minting with a one-click connect: paste your OAuth client id + secret into Settings once, then Connect Google in the console view runs the whole authorization-code flow and stores the refresh token itself.
Flow
POST /api/plugins/google/oauth/start(operator bearer) mints a single-use, 10-min state nonce and returns Google's consent URL — redirect_uri derived from the live request origin, so :7870 / :7871 / a LAN hostname all work (must be registered on the OAuth client).window.open→ Google consent (access_type=offline&prompt=consentforces a refresh token every time).GET /plugins/google/oauth/callback(added to manifestpublic_paths— a redirect can't carry a bearer; the nonce gates it instead). Code is exchanged, the refresh token is merged into the untracked 0600secrets.yamlviagraph.config_io.save_secrets(refresh_tokenis already a declared plugin secret → redaction + uninstall-purge apply), and the in-memory creds swap immediately — no restart.gmail.profile()).Security posture
error_description) surface on the callback page instead of a bare status code.Config
New
oauth_scopessetting (space-separated override); default requestsgmail.modify calendar drive.readonly— the grow-into set. README gains the 5-minute Google Cloud client setup (Internal user type on Workspace = no verification / no 7-day token expiry; External+Testing caveat documented).Test
33 passed (15 new), ruff clean, host-free.
🤖 Generated with Claude Code