Skip to content

Added GH Auth for use with GH and Copilot models. Uses GH cli first, …#251

Open
judggernaut wants to merge 3 commits intompfaffenberger:mainfrom
judggernaut:judggernaut/github_oauth
Open

Added GH Auth for use with GH and Copilot models. Uses GH cli first, …#251
judggernaut wants to merge 3 commits intompfaffenberger:mainfrom
judggernaut:judggernaut/github_oauth

Conversation

@judggernaut
Copy link
Copy Markdown

@judggernaut judggernaut commented Mar 28, 2026

Vibes are strong here. Adds ability for users to use GitHub and Copilot models as option if they have access to them via github-auth command. Github and Copilot models are not the same but we can share creds as needed to access the respective models.

Attempts to login with:

  1. GH CLI
  2. Provided PAT
  3. APP ID if the users have registered it, this is the last resort.

Summary by CodeRabbit

  • New Features

    • GitHub Models and Copilot integration with OAuth-based discovery and automatic config import
    • Multiple authentication methods: GitHub CLI, env vars, interactive paste, or device-flow (with browser prompt)
    • Slash commands to authenticate, view status, and logout; persistent token storage with secure file permissions
  • Tests

    • Comprehensive test suite covering auth flows, token management, model fetching, config changes, and commands

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 918ab47b-71da-4976-8282-46770e33a3fa

📥 Commits

Reviewing files that changed from the base of the PR and between 83f6a8f and b4f390a.

📒 Files selected for processing (1)
  • code_puppy/plugins/github_models_oauth/device_flow.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • code_puppy/plugins/github_models_oauth/device_flow.py

📝 Walkthrough

Walkthrough

Adds a GitHub Models OAuth plugin: new config constants and helpers, RFC 8628 device-flow implementation, token discovery and secure persistence, GitHub/Copilot model catalog fetching and local config management, callback wiring for auth/status/logout and model registration, tests, and integration of GitHub model overlays into model loading.

Changes

Cohort / File(s) Summary
Core config & factory
code_puppy/config.py, code_puppy/model_factory.py
Introduce GITHUB_MODELS_FILE constant and include it as an extra JSON overlay source when loading model configurations.
Plugin config & helpers
code_puppy/plugins/github_models_oauth/config.py
New GITHUB_MODELS_OAUTH_CONFIG plus helpers: get_client_id(), get_token_storage_path(), get_github_models_path() that ensure DATA_DIR with secure perms and provide paths for token and models files.
Device flow handler
code_puppy/plugins/github_models_oauth/device_flow.py
Implement device authorization flow: DeviceFlowResponse, start_device_flow(), poll_for_access_token(), and run_device_flow() with polling, error handling, and optional browser open.
Utilities
code_puppy/plugins/github_models_oauth/utils.py
Token discovery (gh CLI, env, interactive), token save/load with 0o600 perms, username validation, fetchers for GitHub/Copilot model catalogs with fallbacks, and read/write/remove local GitHub models config functions and defaults.
Callback registration & model types
code_puppy/plugins/github_models_oauth/register_callbacks.py
Register commands (/github-auth, /github-status, /github-logout), command routing, token acquisition sequence (gh CLI → env → prompt → device flow), token validation/storage, importing/removing models, and model-type factories for GitHub Models and Copilot.
Tests
code_puppy/plugins/github_models_oauth/test_plugin.py
New comprehensive pytest suite covering config, token sources, persistence, device flow behaviors, HTTP interactions, model management, callback routing, and auth control flow.

Sequence Diagrams

sequenceDiagram
    participant User
    participant Plugin as GitHub OAuth Plugin
    participant GH as GitHub Device<br/>Endpoints
    participant Browser
    participant API as GitHub API

    User->>Plugin: /github-auth
    Plugin->>Plugin: Check gh CLI token
    alt gh token found
        Plugin->>API: Validate token
        API-->>Plugin: Username
        Plugin->>Plugin: Save token & import models
    else No gh token
        Plugin->>Plugin: Check GITHUB_TOKEN env
        alt env token found
            Plugin->>API: Validate token
            API-->>Plugin: Username
            Plugin->>Plugin: Save token & import models
        else No env token
            Plugin->>User: Prompt for PAT
            User->>Plugin: Paste token
            Plugin->>API: Validate token
            API-->>Plugin: Username
            Plugin->>Plugin: Save token & import models
        else Invalid/empty PAT
            Plugin->>GH: Start device flow
            GH-->>Plugin: Device code + URI
            Plugin->>User: Display code & URL
            Plugin->>Browser: Open verification URI
            Browser->>GH: User authorizes
            Plugin->>GH: Poll for access token
            GH-->>Plugin: Access token
            Plugin->>API: Validate token
            API-->>Plugin: Username
            Plugin->>Plugin: Save token & import models
        end
    end
    Plugin-->>User: Auth success/failure
Loading
sequenceDiagram
    participant Poll as Polling Loop
    participant GH as GitHub API
    participant Plugin as Device Flow Handler

    loop until timeout
        Poll->>GH: POST access_token (device_code)
        alt access_token returned
            GH-->>Poll: access_token
            Poll->>Plugin: Return token
        else authorization_pending
            GH-->>Poll: error authorization_pending
            Poll->>Poll: Continue polling
        else slow_down
            GH-->>Poll: error slow_down
            Poll->>Poll: Increase interval, retry
        else expired_token
            GH-->>Poll: error expired_token
            Poll->>Plugin: Return None
        else other error
            GH-->>Poll: error
            Poll->>Plugin: Return None (abort)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

🐰✨ I hopped in quick with a token bright,
Device codes flashed under moonlit night.
I fetched models, saved secrets snug and tight,
Opened browsers, polled until the light—
Hooray! Now GitHub models join the flight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main objective: adding GitHub authentication support for GitHub and Copilot models with a specific authentication order (GH CLI first).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code_puppy/plugins/github_models_oauth/device_flow.py`:
- Around line 46-53: The POSTs in device_flow.py are sending JSON but GitHub
requires form-encoded bodies; update both requests.post calls (the one that
initiates the device flow and the one that polls/exchanges the device code) to
pass form data via data= instead of json= and remove the explicit "Content-Type:
application/json" header so requests will set application/x-www-form-urlencoded
automatically; locate the two calls by the requests.post invocations used in the
device flow initiation and token polling/exchange functions and change their
payload handling and headers accordingly.

In `@code_puppy/plugins/github_models_oauth/register_callbacks.py`:
- Around line 104-114: The code currently proceeds to call
set_model_and_reload_agent even when discovery produced zero models because
_handle_auth() returns True; change the logic so that after discovery/config
writes you only call set_model_and_reload_agent when total > 0 (i.e., inside the
same branch that emits emit_success), and pick a model from the actual
discovered list (or verify existence) rather than hard-coding
"github-openai-gpt-4.1"; apply the same guard to the other occurrence referenced
(the block around set_model_and_reload_agent at the second location).
- Around line 145-154: Wrap token_path.unlink() in a try/except that catches
OSError and logs the error via emit_info/emit_error instead of allowing it to
raise, track a boolean indicating whether token and model removals succeeded,
call remove_github_models() as before but after it ensure the running
session/cache is invalidated or switched away from any plugin-managed model
(e.g., if the active model id startswith "github-" or "copilot-", clear or set a
safe default), and only call emit_success("👋 GitHub Models logout complete")
when all cleanup steps succeeded; if any step failed emit an error/info message
and do not emit success.

In `@code_puppy/plugins/github_models_oauth/test_plugin.py`:
- Around line 561-596: The test calls _handle_auth which always invokes
fetch_copilot_models()/add_copilot_models_to_config in addition to
fetch_github_models/add_models_to_config, so the test must stub the Copilot
discovery path to avoid live network/filesystem side effects; update the test
patches to also patch
"code_puppy.plugins.github_models_oauth.register_callbacks.fetch_copilot_models"
(returning an empty list or expected stubbed models) and
"code_puppy.plugins.github_models_oauth.register_callbacks.add_copilot_models_to_config"
(return_value=True) in the same with(...) block so _handle_auth only exercises
the GitHub-models branch and does not perform real requests or writes.

In `@code_puppy/plugins/github_models_oauth/utils.py`:
- Around line 175-188: The current fetch logic treats HTTP 401/403 as a generic
catalog outage and falls back to DEFAULT_GITHUB_MODELS, which masks auth
failures; change the behavior in the function that calls response.status_code
(the block using _parse_model_list, emit_info, emit_warning, logger and
DEFAULT_GITHUB_MODELS) to detect 401 and 403 explicitly and raise or return a
clear auth error (or re-raise the response/Exception) instead of emitting the
generic warning and returning built-ins; apply the same explicit 401/403
handling to the other identical block referenced (lines around where
DEFAULT_GITHUB_MODELS is returned again) so _handle_auth() can surface
permission problems rather than registering inaccessible models.
- Around line 55-59: The message emitted by emit_info currently recommends
creating a classic PAT with 'read:user' scope which is misleading for the GitHub
Models API; update the emitted text in the emit_info call to recommend creating
a fine-grained Personal Access Token with the "models:read" permission (and note
that classic PATs may still work) and remove the emphasis on 'read:user'; also
add a brief note that this token is required for endpoints like /catalog/models
and /inference/chat/completions to make the guidance accurate and secure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1cda3ec-e6fd-4ac7-bcd4-9597a84e06ea

📥 Commits

Reviewing files that changed from the base of the PR and between dc6f435 and be44b20.

📒 Files selected for processing (8)
  • code_puppy/config.py
  • code_puppy/model_factory.py
  • code_puppy/plugins/github_models_oauth/__init__.py
  • code_puppy/plugins/github_models_oauth/config.py
  • code_puppy/plugins/github_models_oauth/device_flow.py
  • code_puppy/plugins/github_models_oauth/register_callbacks.py
  • code_puppy/plugins/github_models_oauth/test_plugin.py
  • code_puppy/plugins/github_models_oauth/utils.py

Comment thread code_puppy/plugins/github_models_oauth/device_flow.py
Comment thread code_puppy/plugins/github_models_oauth/register_callbacks.py
Comment on lines +145 to +154
token_path = get_token_storage_path()
if token_path.exists():
token_path.unlink()
emit_info("✓ Removed GitHub OAuth tokens")

removed = remove_github_models()
if removed:
emit_info(f"✓ Removed {removed} GitHub models from configuration")

emit_success("👋 GitHub Models logout complete")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Harden /github-logout against cleanup failures and stale model state.

token_path.unlink() can still raise out of this callback, and after remove_github_models() the session/cache can keep pointing at deleted github-* / copilot-* entries. This path should catch OSError, avoid emitting success on partial cleanup, and invalidate/switch away from plugin-managed models before returning.

As per coding guidelines, "Fail gracefully in plugin code — never crash the app, especially in callback handlers".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code_puppy/plugins/github_models_oauth/register_callbacks.py` around lines
145 - 154, Wrap token_path.unlink() in a try/except that catches OSError and
logs the error via emit_info/emit_error instead of allowing it to raise, track a
boolean indicating whether token and model removals succeeded, call
remove_github_models() as before but after it ensure the running session/cache
is invalidated or switched away from any plugin-managed model (e.g., if the
active model id startswith "github-" or "copilot-", clear or set a safe
default), and only call emit_success("👋 GitHub Models logout complete") when
all cleanup steps succeeded; if any step failed emit an error/info message and
do not emit success.

Comment thread code_puppy/plugins/github_models_oauth/test_plugin.py
Comment thread code_puppy/plugins/github_models_oauth/utils.py
Comment thread code_puppy/plugins/github_models_oauth/utils.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
code_puppy/plugins/github_models_oauth/register_callbacks.py (1)

146-159: ⚠️ Potential issue | 🟠 Major

/github-logout reports success even when cleanup fails.

Line 158 emits success unconditionally, so token unlink errors (Lines 150-153) still end with a “complete” message.

🔧 Proposed fix
 def _handle_logout() -> None:
     token_path = get_token_storage_path()
+    cleanup_ok = True
     try:
         if token_path.exists():
             token_path.unlink()
             emit_info("✓ Removed GitHub OAuth tokens")
     except OSError as exc:
         logger.error("Failed to remove token file: %s", exc)
         emit_error(f"Failed to remove token file: {exc}")
+        cleanup_ok = False
 
     removed = remove_github_models()
     if removed:
         emit_info(f"✓ Removed {removed} GitHub models from configuration")
+    elif token_path.exists():
+        cleanup_ok = False
 
-    emit_success("👋 GitHub Models logout complete")
+    if cleanup_ok:
+        emit_success("👋 GitHub Models logout complete")
+    else:
+        emit_error("GitHub logout completed with errors. Check logs and retry.")

As per coding guidelines, "Fail gracefully in plugin code — never crash the app, especially in callback handlers".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code_puppy/plugins/github_models_oauth/register_callbacks.py` around lines
146 - 159, The logout flow currently always calls emit_success("👋 GitHub Models
logout complete") even if token removal failed; change the logic in
register_callbacks.py around token_path handling and remove_github_models so
failures prevent the final success message: detect errors when unlinking
token_path (token_path.exists() / token_path.unlink()) by setting a failure flag
or returning early after logger.error/emit_error, and only call emit_success if
no failure occurred (and still run remove_github_models but treat its failure
similarly); reference token_path, remove_github_models, emit_error, logger, and
emit_success to locate and update the code accordingly.
code_puppy/plugins/github_models_oauth/utils.py (1)

188-191: ⚠️ Potential issue | 🟠 Major

Fallback guidance still points to read:user, which is misleading for Models access.

Line 190 suggests gh auth login -s read:user, which can send users down the wrong path when troubleshooting model catalog/inference access.

🔧 Proposed fix
-        emit_warning(
-            f"   Catalog returned HTTP {response.status_code}; using {len(DEFAULT_GITHUB_MODELS)} built-in models.\n"
-            "   💡 For the full list, use a PAT or re-run: gh auth login -s read:user"
-        )
+        emit_warning(
+            f"   Catalog returned HTTP {response.status_code}; using {len(DEFAULT_GITHUB_MODELS)} built-in models.\n"
+            "   💡 For full catalog access, use a token with GitHub Models access "
+            "(fine-grained PATs need models:read)."
+        )
GitHub REST API models inference and catalog token requirements for fine-grained PATs; confirm whether models:read is required and whether read:user is sufficient.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code_puppy/plugins/github_models_oauth/utils.py` around lines 188 - 191, The
fallback warning currently suggests using "gh auth login -s read:user", which is
misleading for model/catalog access; update the message in the emit_warning call
(the block that references response.status_code and DEFAULT_GITHUB_MODELS) to
recommend the correct PAT scopes for Models access (e.g., "models:read" or the
exact fine-grained PAT scope required) and/or instruct users to create a PAT
with model/catalog access rather than read:user; verify the precise GitHub scope
required and replace the "gh auth login -s read:user" substring with the
accurate scope suggestion and a short action (e.g., "gh auth login -s
models:read" or "create a PAT with the models:read scope").
🧹 Nitpick comments (1)
code_puppy/plugins/github_models_oauth/test_plugin.py (1)

476-485: Add a regression test for logout partial-failure behavior.

Current command-routing tests assert return values, but there’s no assertion that /github-logout suppresses success messaging when unlink fails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code_puppy/plugins/github_models_oauth/test_plugin.py` around lines 476 -
485, Update the test for /github-logout to cover the partial-failure case: patch
register_callbacks._handle_logout to simulate an unlink failure (raise an
exception or return False) and assert that
_handle_custom_command("/github-logout", "github-logout") still returns True but
does not emit the success message by verifying the success-message sender is not
called (mock the module's message-sending function, e.g.,
send_message/send_response used by register_callbacks). Ensure you reference and
patch _handle_logout and _handle_custom_command in the test and assert the
message-sender mock has zero calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code_puppy/plugins/github_models_oauth/device_flow.py`:
- Around line 84-90: The polling loop currently increments elapsed only by sleep
(using variables delay, elapsed, interval and while elapsed < timeout) which
ignores the time spent performing the HTTP request (the request with its own
timeout), so change the loop to use time.monotonic() to measure real elapsed
time: record a loop_start = time.monotonic() before entering the while, compute
elapsed = time.monotonic() - loop_start each iteration (instead of adding
delay), and when waiting between polls compute sleep time from the configured
interval minus the time already spent in that iteration (or cap sleep by
remaining timeout) so total elapsed accounts for both request duration and sleep
and the loop respects timeout correctly.
- Around line 166-169: Wrap the blocking call to
poll_for_access_token(device.device_code, device.interval) in a try/except that
catches KeyboardInterrupt so the Ctrl+C path is handled gracefully; after
emit_info("⏳ Waiting for authorization (press Ctrl+C to cancel)…") surrounding
that call, catch KeyboardInterrupt and perform a clean abort flow (e.g., emit an
informational cancellation message and return/exit the function without
raising), ensuring any cleanup or state rollback is executed before returning.

---

Duplicate comments:
In `@code_puppy/plugins/github_models_oauth/register_callbacks.py`:
- Around line 146-159: The logout flow currently always calls emit_success("👋
GitHub Models logout complete") even if token removal failed; change the logic
in register_callbacks.py around token_path handling and remove_github_models so
failures prevent the final success message: detect errors when unlinking
token_path (token_path.exists() / token_path.unlink()) by setting a failure flag
or returning early after logger.error/emit_error, and only call emit_success if
no failure occurred (and still run remove_github_models but treat its failure
similarly); reference token_path, remove_github_models, emit_error, logger, and
emit_success to locate and update the code accordingly.

In `@code_puppy/plugins/github_models_oauth/utils.py`:
- Around line 188-191: The fallback warning currently suggests using "gh auth
login -s read:user", which is misleading for model/catalog access; update the
message in the emit_warning call (the block that references response.status_code
and DEFAULT_GITHUB_MODELS) to recommend the correct PAT scopes for Models access
(e.g., "models:read" or the exact fine-grained PAT scope required) and/or
instruct users to create a PAT with model/catalog access rather than read:user;
verify the precise GitHub scope required and replace the "gh auth login -s
read:user" substring with the accurate scope suggestion and a short action
(e.g., "gh auth login -s models:read" or "create a PAT with the models:read
scope").

---

Nitpick comments:
In `@code_puppy/plugins/github_models_oauth/test_plugin.py`:
- Around line 476-485: Update the test for /github-logout to cover the
partial-failure case: patch register_callbacks._handle_logout to simulate an
unlink failure (raise an exception or return False) and assert that
_handle_custom_command("/github-logout", "github-logout") still returns True but
does not emit the success message by verifying the success-message sender is not
called (mock the module's message-sending function, e.g.,
send_message/send_response used by register_callbacks). Ensure you reference and
patch _handle_logout and _handle_custom_command in the test and assert the
message-sender mock has zero calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1743bda8-09c5-4e29-a947-58d3f37d50d4

📥 Commits

Reviewing files that changed from the base of the PR and between be44b20 and 83f6a8f.

📒 Files selected for processing (4)
  • code_puppy/plugins/github_models_oauth/device_flow.py
  • code_puppy/plugins/github_models_oauth/register_callbacks.py
  • code_puppy/plugins/github_models_oauth/test_plugin.py
  • code_puppy/plugins/github_models_oauth/utils.py

Comment thread code_puppy/plugins/github_models_oauth/device_flow.py Outdated
Comment thread code_puppy/plugins/github_models_oauth/device_flow.py
@mpfaffenberger mpfaffenberger force-pushed the main branch 2 times, most recently from c1f396b to daa401a Compare April 23, 2026 22:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant