Skip to content

Conversation

@LegendEvent
Copy link

@LegendEvent LegendEvent commented Jan 5, 2026

Summary

Adds GitHub Copilot authentication support with both interactive device flow and CI-friendly environment variables, along with CLI auth commands and a terminal renderer bug fix. Enables non-interactive usage of Copilot models in CI/CD pipelines.

Changes

New Features

  • strix/llm/copilot_auth.py (381 lines): Full OAuth device flow implementation

    • Interactive authentication via strix auth login
    • Environment variable support for CI:
      • STRIX_COPILOT_ACCESS - ready-to-use access token
      • STRIX_COPILOT_TOKEN - refresh token (automatically exchanged)
      • STRIX_COPILOT_ENTERPRISE - optional enterprise domain
    • Secure token storage with 0o600 permissions
    • Thread-safe token refresh with async lock
    • Automatic retry on 401/403 authentication errors
    • Enterprise domain support (github.com or custom domains)
  • CLI Authentication Commands (strix/interface/main.py)

    • New subcommands: strix auth login and strix auth logout
    • Interactive OAuth device flow with clear user prompts
    • Token file cleanup on logout

Integration Updates

  • strix/llm/llm.py: Copilot model integration
    • Model detection via github-copilot/ prefix
    • Automatic token retrieval with retry logic (max 1 retry)
    • Custom Copilot API headers for VSCode compatibility
    • Force refresh on authentication failures

Bug Fixes

  • strix/interface/tool_components/terminal_renderer.py: Fixed AttributeError when tool execution returns string error messages instead of dict results

Dependencies

  • Added httpx (async HTTP client) to pyproject.toml
  • Updated poetry.lock (vertex dependencies adjusted, other dependency updates)

Documentation

  • Updated README.md with GitHub Copilot authentication section
    • Interactive vs non-interactive usage examples
    • Environment variable documentation
    • Multi-target examples refined

Tests

  • strix/tests/test_copilot_auth.py (60 lines): Comprehensive test coverage
    • test_env_access_token_returns_immediately - validates CI env var usage
    • test_env_refresh_token_triggers_exchange - tests token exchange flow
    • test_non_interactive_without_token_raises - fail-fast behavior

Verification

  • ✅ New unit tests pass locally under Poetry
  • ✅ pre-commit hooks (ruff, mypy) run and fixes applied
  • ✅ Tested interactive flow with strix auth login
  • ✅ Tested non-interactive with --non-interactive flag
  • ✅ System respects STRIX_COPILOT_TOKEN/STRIX_COPILOT_ACCESS

Notes for Reviewers

  • Security: Tokens are sensitive; recommend storing them in CI secret stores (GitHub Actions Secrets, GitLab CI variables)
  • Backward Compatibility: No breaking changes. --target now optional to support auth subcommands
  • Token Storage: The token file is still written locally when a refresh exchange succeeds (preserves existing behavior)
  • Logging: Suppressed printing to stderr in login flow; now uses logging only. Clients relying on stderr prompts may need UX adjustments
  • Architecture: Follows existing upstream patterns; integrates cleanly with LLM request queue and retry logic

How to Test Locally

# Install and run tests
poetry install --with dev
poetry run pytest -q strix/tests/test_copilot_auth.py

# Interactive authentication
strix auth login
strix auth logout

# Non-interactive (with env vars)
export STRIX_COPILOT_ACCESS="<token>"
strix -n --target ./my-project

# Full pre-commit checks
poetry run pre-commit run --all-files

Related

  • Enables non-interactive/CI usage of GitHub Copilot models in Strix
  • Resolves tool execution crashes with string error messages

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 5, 2026

Greptile Summary

Added CI-friendly GitHub Copilot authentication with environment variable support (STRIX_COPILOT_ACCESS and STRIX_COPILOT_TOKEN) alongside existing interactive device flow authentication.

Key Changes:

  • New strix/llm/copilot_auth.py module handles device flow, token refresh, and environment variable overrides with secure disk storage (0600 permissions)
  • CLI commands strix auth login and strix auth logout provide user-friendly authentication management
  • LLM integration includes automatic retry with forced token refresh on 401/403 errors
  • Comprehensive test coverage for env token paths and non-interactive behavior
  • Documentation clearly explains interactive vs CI authentication patterns

Issues Found:

  • Duplicate code block in copilot_auth.py lines 310-324 (identical to lines 321-335)

Confidence Score: 4/5

  • This PR is safe to merge after fixing the duplicate code block
  • Score reflects solid implementation with good security practices (secure token storage, async locks, proper error handling) and comprehensive testing. One logical error (duplicate code) needs fixing before merge. The feature is well-integrated and documented.
  • Pay attention to strix/llm/copilot_auth.py to fix the duplicate code block at lines 310-324

Important Files Changed

Filename Overview
strix/llm/copilot_auth.py Implements CI-friendly Copilot auth with env overrides. Has duplicate code block (lines 310-324) that needs removal.
strix/tests/test_copilot_auth.py Good test coverage for env tokens and non-interactive behavior. Uses proper mocking with respx.
strix/llm/llm.py Integrates Copilot auth with retry logic for 401/403 errors. Clean implementation with proper error handling.
strix/interface/main.py Added auth login/logout subcommands with proper error handling. Integrated warmup with non-interactive flag.
README.md Clear documentation for interactive and CI authentication with security guidance.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI
    participant LLM
    participant Auth
    participant GitHub
    participant Copilot

    alt Interactive Authentication
        User->>CLI: strix auth login
        CLI->>Auth: Start device flow
        Auth->>GitHub: Request device code
        GitHub-->>Auth: Returns flow data
        Auth-->>User: Display verification URL
        User->>GitHub: Complete browser auth
        loop Poll authorization
            Auth->>GitHub: Check status
            GitHub-->>Auth: Status response
        end
        Auth->>Auth: Save credentials locally
        Auth-->>CLI: Success
    end

    alt Environment Direct Access
        User->>LLM: Request with copilot model
        LLM->>Auth: Request credentials
        Auth->>Auth: Read env variable
        Auth-->>LLM: Return immediately
        LLM->>Copilot: API request
        Copilot-->>LLM: Response
    end

    alt Environment Refresh Exchange
        User->>LLM: Request with copilot model
        LLM->>Auth: Request credentials
        Auth->>Auth: Use refresh from env
        Auth->>GitHub: Exchange for access
        GitHub-->>Auth: Returns access with expiry
        Auth->>Auth: Save locally
        Auth-->>LLM: Return result
        LLM->>Copilot: API request
        Copilot-->>LLM: Response
    end

    alt Expired Credential Refresh
        User->>LLM: Request with copilot model
        LLM->>Auth: Request credentials
        Auth->>Auth: Check expiry
        Auth->>Auth: Acquire lock
        Auth->>GitHub: Exchange for new access
        GitHub-->>Auth: New credentials
        Auth->>Auth: Save locally
        Auth-->>LLM: Return refreshed
        LLM->>Copilot: API attempt 1
        Copilot-->>LLM: 401 Error
        LLM->>Auth: Force refresh
        Auth->>GitHub: Exchange again
        GitHub-->>Auth: Fresh credentials
        Auth-->>LLM: Return result
        LLM->>Copilot: API attempt 2
        Copilot-->>LLM: Success
    end
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (4)

  1. strix/llm/copilot_auth.py, line 272-278 (link)

    style: when STRIX_COPILOT_TOKEN is provided via environment variable, tokens are still written to disk at line 317 even though they may be ephemeral CI credentials

  2. strix/llm/copilot_auth.py, line 297 (link)

    style: unnecessary parentheses around effective_enterprise

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  3. README.md, line 221 (link)

    style: missing code block syntax highlighting

    bash
    export STRIX_LLM="github-copilot/<model>"

    
    <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
    
    
    
  4. README.md, line 239-243 (link)

    style: example should use proper code block formatting with bash syntax highlighting

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

7 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@LegendEvent
Copy link
Author

Related to issue #225

@LegendEvent
Copy link
Author

Related to issue #226

@LegendEvent
Copy link
Author

This feature was added with the assistance of AI (LLM).

@LegendEvent LegendEvent marked this pull request as draft January 7, 2026 18:43
…tecture

- Add httpx dependency to pyproject.toml
- Create strix/llm/copilot_auth.py with OAuth device-flow implementation
  - Token storage with secure permissions (0o600)
  - CI authentication via environment variables
  - Enterprise domain support
  - Thread-safe token refresh with async lock

- Modify strix/llm/llm.py to integrate Copilot authentication
  - Add Copilot model detection (github-copilot/* prefix)
  - Integrate token fetching in _stream_request() method
  - Automatic token refresh on 401/403 authentication errors
  - Add Copilot-specific headers for VS Code compatibility
  - Preserve all upstream architectural patterns (streaming, retry logic)

- Modify strix/interface/main.py to add authentication commands
  - Add handle_auth_login() and handle_auth_logout() async functions
  - Add auth subparsers (login/logout) in CLI
  - Add Copilot detection in warm_up_llm() for non-interactive mode
  - Make --target argument optional (required=False)
  - Remove deprecated HOST_GATEWAY_HOSTNAME import

- Create strix/tests/test_copilot_auth.py with comprehensive test coverage
  - test_env_access_token_returns_immediately
  - test_env_refresh_token_triggers_exchange
  - test_non_interactive_without_token_raises

- Update README.md with Copilot authentication documentation
  - Add GitHub Copilot models section with usage examples
  - Add interactive authentication examples (strix auth login/logout)
  - Add non-interactive/CI authentication examples
  - Update CI/CD workflow documentation

All changes preserve upstream main architecture and improvements.
No merge conflicts - clean integration.
All tests passing (100%).
Fixed AttributeError when tool execution returns string error messages
instead of dict results. The method now checks result type and
displays string errors formatted as error messages.

Fixes crashes when tool server fails or returns error strings.
@LegendEvent LegendEvent force-pushed the feature/copilot-device-flow branch from c7d1b0c to 74ad166 Compare January 8, 2026 00:24
Remove temporary development files and directories that should
not be in the repository.
@LegendEvent LegendEvent marked this pull request as ready for review January 8, 2026 00:44
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +310 to +324
base_url = copilot_openai_base_url(enterprise_domain=effective_enterprise)

# If forcing refresh or token is expired, perform refresh with lock
if force_refresh or not (info.access and info.expires and info.expires > _now_ms()):
async with _refresh_lock:
if force_refresh:
logger.info("Forcing Copilot access token refresh due to API failure")
else:
logger.info("Refreshing expired Copilot access token")

info = _load_oauth_info_from_disk(token_path) if not env_refresh else info
base_url = copilot_openai_base_url(enterprise_domain=effective_enterprise)

# If forcing refresh or token is expired, perform refresh with lock
if force_refresh or not (info.access and info.expires and info.expires > _now_ms()):
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: duplicate code block - lines 310-324 are identical to lines 321-335 inside the lock

Suggested change
base_url = copilot_openai_base_url(enterprise_domain=effective_enterprise)
# If forcing refresh or token is expired, perform refresh with lock
if force_refresh or not (info.access and info.expires and info.expires > _now_ms()):
async with _refresh_lock:
if force_refresh:
logger.info("Forcing Copilot access token refresh due to API failure")
else:
logger.info("Refreshing expired Copilot access token")
info = _load_oauth_info_from_disk(token_path) if not env_refresh else info
base_url = copilot_openai_base_url(enterprise_domain=effective_enterprise)
# If forcing refresh or token is expired, perform refresh with lock
if force_refresh or not (info.access and info.expires and info.expires > _now_ms()):
# Compute base_url/domain (recompute if enterprise_url argument used and env not set)
effective_enterprise = enterprise_domain or (
_normalize_domain(enterprise_url) if enterprise_url else None
)
base_url = copilot_openai_base_url(enterprise_domain=effective_enterprise)
# If forcing refresh or token is expired, perform refresh with lock
if force_refresh or not (info.access and info.expires and info.expires > _now_ms()):
async with _refresh_lock:
if force_refresh:
logger.info("Forcing Copilot access token refresh due to API failure")
else:
logger.info("Refreshing expired Copilot access token")
# Reload token info to ensure we have latest state
Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/llm/copilot_auth.py
Line: 310:324

Comment:
**logic:** duplicate code block - lines 310-324 are identical to lines 321-335 inside the lock

```suggestion
    # Compute base_url/domain (recompute if enterprise_url argument used and env not set)
    effective_enterprise = enterprise_domain or (
        _normalize_domain(enterprise_url) if enterprise_url else None
    )
    base_url = copilot_openai_base_url(enterprise_domain=effective_enterprise)

    # If forcing refresh or token is expired, perform refresh with lock
    if force_refresh or not (info.access and info.expires and info.expires > _now_ms()):
        async with _refresh_lock:
            if force_refresh:
                logger.info("Forcing Copilot access token refresh due to API failure")
            else:
                logger.info("Refreshing expired Copilot access token")

            # Reload token info to ensure we have latest state
```

How can I resolve this? If you propose a fix, please make it concise.

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