Skip to content

Matas: Minor overall refactor and fixed bugs#38

Closed
MT-superdev wants to merge 1 commit intowarestack:mainfrom
MT-superdev:matas/fix/pr-36-audit
Closed

Matas: Minor overall refactor and fixed bugs#38
MT-superdev wants to merge 1 commit intowarestack:mainfrom
MT-superdev:matas/fix/pr-36-audit

Conversation

@MT-superdev
Copy link
Contributor

@MT-superdev MT-superdev commented Jan 21, 2026

  • Resolved failing pre-commit hooks and unit tests from legacy PR docs: feedback-from-Martas #36 to ensure a clean, passing build.
  • Refactored core modules to follow the "Thomas Standard" of strict Pydantic models and type hints for better data integrity.
  • Enabled unauthenticated access for public repositories and implemented an IP-based rate-limiting layer (5 req/hr for anonymous users).
  • Activated the LLM-powered reasoning engine, replacing static templates with context-aware recommendations.
  • Added logic to scan and analyze real signals such as CODEOWNERS and GitHub Action workflows for patterns like pytest or deployment jobs.
  • Purged all mock data from the landing page and established a direct connection to the live backend API with new loading states.
  • Introduced a suite of integration tests covering public, private, and authenticated repository scenarios.
  • Conducted a self-audit identifying Redis migration and improved exception handling as the next immediate priorities for production hardening.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added rate limiting to the repository analysis API: 5 requests/hour for anonymous users, 100 requests/hour for authenticated users.
    • Enabled public repository analysis without authentication.
    • Added Retry-After headers for rate-limited responses (HTTP 429).
    • Frontend now connected to live backend data.
  • Documentation

    • Added guidance on unauthenticated analysis and rate limiting.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

This pull request refactors the agent architecture to use LangGraph state-based orchestration, adds rate limiting and optional authentication to the API, extends GitHub integration to support public/anonymous access, updates pre-commit tooling and configuration, and introduces new models for API requests/responses and analysis workflows.

Changes

Cohort / File(s) Summary
Configuration & Build Tooling
.pre-commit-config.yaml, pyproject.toml, linting_commands.txt
Updated pre-commit hooks from v5.0.0 to v6.0.0; consolidated Ruff configuration under [tool.ruff]; simplified tooling with fewer separate hooks (removed separate ruff-format, PyUpgrade); adjusted isort integration and mypy settings.
Documentation
README.md, docs/features.md
Added "Unauthenticated Analysis & Rate Limiting" section describing public access limits (5 req/hour anonymous, 100 req/hour authenticated), 429 responses with Retry-After header, and private repo authentication requirements.
Agent Orchestration (LangGraph)
src/agents/repository_analysis_agent/agent.py, src/agents/repository_analysis_agent/nodes.py, src/agents/repository_analysis_agent/models.py, src/agents/repository_analysis_agent/prompts.py
Replaced procedural multi-step analysis with graph-based orchestration; introduced AnalysisState and RuleRecommendation models; refactored nodes to fetch_repository_metadata() and generate_rule_recommendations() using LLM-driven rule generation with fallback logic; updated prompts to SYSTEM/USER format for JSON-based recommendations.
API Layer & Rate Limiting
src/api/dependencies.py, src/api/rate_limit.py, src/api/recommendations.py, src/api/rules.py
Added get_github_service(), get_current_user_optional(), and get_current_user() dependencies; introduced in-memory rate limiter with per-IP (5/hr) and per-user (100/hr) limits; refactored recommendations endpoint with AnalyzeRepoRequest and AnalysisResponse models; added parse_repo_from_url() helper; updated rules evaluation response with feedback field.
GitHub Integration
src/integrations/github/api.py, src/integrations/github/service.py
Expanded GitHub API client with public/anonymous access support via allow_anonymous flag; added methods get_repository(), list_directory_any_auth(), get_file_content(), create_check_run(), get_pull_request(), list_pull_requests(); introduced _get_session(), _generate_jwt(), _decode_private_key(); replaced httpx with aiohttp. New GitHubService class with get_repo_metadata() and analyze_repository_rules() methods; added custom exceptions GitHubRateLimitError, GitHubResourceNotFoundError.
Core Models & Utilities
src/core/models.py, src/core/utils/caching.py, src/core/utils/logging.py
Added new User model with id, username, email, avatar_url, github_token; changed EventType to string-based enum; added public attributes to WebhookEvent (init, event_type, payload, repository, sender, installation_id); simplified cache initialization logic with ternary expressions; moved Callable to TYPE_CHECKING for deferred evaluation.
Configuration & Provider Integration
src/core/config/settings.py, src/integrations/providers/bedrock_provider.py, src/integrations/providers/factory.py
Refactored Bedrock model ID validation logic with normalized lowercase comparison and provider alias set matching; added from __future__ import annotations to factory; simplified token/temperature assignment with inline ternary expressions.
Webhook Handlers & Dispatching
src/webhooks/router.py, src/webhooks/handlers/deployment.py, src/webhooks/handlers/deployment_status.py, src/webhooks/handlers/issue_comment.py, src/webhooks/handlers/pull_request.py, src/webhooks/dispatcher.py, src/webhooks/auth.py
Updated deployment_status handler to pass installation_id and payload to task queue; refactored issue_comment handler with any() construct for pattern matching and new guard branch; improved error handling for unknown events in router to return 202; updated comments for clarity around payload fragility and timing-attack prevention.
Task Scheduling & Queue
src/tasks/scheduler/deployment_scheduler.py, src/tasks/task_queue.py
Replaced explicit try/except in deployment_scheduler with contextlib.suppress(asyncio.CancelledError) context manager; updated docstrings in task_queue to clarify deduplication behavior and event hashing fields.
Rules & Utilities
src/rules/validators.py, src/rules/utils/contributors.py
Simplified contributor checking with compound condition in _has_recent_activity(); refactored condition validation logic with improved comments; updated validator docstrings to clarify placeholder/demo behavior.
Application Initialization
src/main.py, src/__init__.py
Replaced print statements with logging.info; changed asyncio scheduler startup to direct await; added explicit webhook handler imports; swapped config-based CORS to development values (["\*"]); updated recommendations router prefix to /api/rules; simplified task counting logic. Updated src/__init__.py package comment.
Testing
tests/conftest.py, tests/integration/test_recommendations.py, tests/integration/test_rules_api.py
Added Helpers utility class with mock_env() static method; introduced autouse mock_settings fixture and event_loop fixture (session scope) for async testing; added new integration tests for anonymous/authenticated access to recommendations endpoint; refactored conditional checks in rules_api tests.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Request
    participant API as Recommendations<br/>Endpoint
    participant RateLimit as Rate Limiter
    participant Auth as Auth<br/>Dependency
    participant Agent as Repository<br/>Analysis Agent
    participant Graph as LangGraph<br/>Workflow
    participant Metadata as fetch_repository<br/>_metadata Node
    participant LLM as generate_rule<br/>_recommendations Node

    Client->>API: POST /rules/recommend
    API->>RateLimit: rate_limiter(request, user)
    RateLimit->>RateLimit: Check IP/User limits
    alt Limit Exceeded
        RateLimit-->>API: HTTPException 429
        API-->>Client: 429 Too Many Requests
    else Within Limits
        RateLimit-->>API: Allow
        API->>Auth: get_current_user_optional()
        Auth-->>API: User | None
        API->>Agent: RepositoryAnalysisAgent.execute()
        Agent->>Graph: graph.invoke(initial_state)
        Graph->>Metadata: fetch_repository_metadata(state)
        Metadata->>Metadata: Gather file tree, languages<br/>CI, docs, CODEOWNERS
        Metadata-->>Graph: Return enriched state
        Graph->>LLM: generate_rule_recommendations(state)
        LLM->>LLM: Construct prompt from signals
        LLM->>LLM: Call LLM for rules
        LLM-->>Graph: RuleRecommendation list | fallback
        Graph-->>Agent: Final AnalysisState
        Agent-->>API: AgentResult
        API->>API: Map to AnalysisResponse
        API-->>Client: 200 AnalysisResponse
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A graph of nodes, a flow so clean,
Rate limits keep the chaos lean,
From public repos, rules now spring,
LangGraph orchestrates each thing,
Auth and safety, side by side—
The agent's got nothing left to hide! 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is extremely vague and generic, using 'Minor overall refactor and fixed bugs' which provides no meaningful information about the substantial changes in the pull request. Replace with a specific title that reflects the main changes, such as 'Add unauthenticated API access with rate limiting and LLM-powered rule recommendations' or 'Refactor repository analysis agent with graph-based orchestration and public API support'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @MT-superdev, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant refactor to the repository analysis and rule recommendation system, shifting towards a more robust, AI-driven, and accessible architecture. The core agent now utilizes a graph-based workflow for enhanced flexibility and maintainability. Key changes include enabling unauthenticated access for public repositories with integrated rate limiting, streamlining the development toolchain with a consolidated linter, and improving the GitHub API client for better authentication handling. These updates aim to provide more accurate and context-aware governance rule recommendations while making the service more widely available.

Highlights

  • Architectural Refactor for Repository Analysis Agent: The core RepositoryAnalysisAgent has been completely re-architected to leverage langgraph for defining its workflow, moving from a procedural execution to a graph-based state machine. This enhances modularity, testability, and the ability to orchestrate complex AI agent flows.
  • Simplified Data Models and LLM Integration: The data models for repository analysis (RepositoryAnalysisRequest, RepositoryAnalysisState, etc.) have been significantly simplified and consolidated into a new AnalysisState Pydantic model. The rule generation logic now directly uses an LLM via langchain's structured output capabilities, replacing previous static and hybrid rule generation methods.
  • Unauthenticated Access and Rate Limiting for Public Repositories: The /v1/rules/recommend endpoint now supports unauthenticated access for public GitHub repositories. An IP-based rate-limiting layer has been introduced, allowing 5 requests per hour for anonymous users and 100 for authenticated users, with Retry-After headers for exceeding limits. This is reflected in updated README.md and docs/features.md.
  • Pre-commit Hook and Linting Toolchain Overhaul: The project's pre-commit configuration has been updated to ruff v0.3.0, consolidating linting, formatting, and import sorting. pyupgrade, black, isort, and flake8 have been removed, streamlining the development environment. A new linting_commands.txt file provides quick references for linting tasks.
  • Enhanced GitHub API Client with Anonymous Support: The GitHubClient now includes an allow_anonymous parameter in its authentication header logic, enabling read-only access to public repository data without requiring an installation ID or user token. Several GitHub API interaction methods have been refactored or moved to a new GitHubService for better abstraction.
  • New FastAPI Dependencies for Authentication and Rate Limiting: New FastAPI dependencies (src/api/dependencies.py and src/api/rate_limit.py) have been added to manage user authentication (optional and required) and enforce the new rate-limiting policies across API endpoints.
  • Integration Tests for Recommendations API: A new suite of integration tests (tests/integration/test_recommendations.py) has been added to verify the functionality of the recommendations API, covering anonymous access to public/private repositories and authenticated access to private repositories.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a major refactoring that modernizes the development tooling, overhauls the repository analysis agent to be LLM-powered using LangGraph, and enables unauthenticated access for public repositories with rate limiting. The changes are extensive and generally improve the codebase's structure, clarity, and features. Key improvements include consolidating linting/formatting under Ruff, adopting a stateful agent architecture, and adding new API dependencies for auth and rate limiting.

However, I've identified several critical and high-severity issues that need to be addressed. These include a bug in the new rate-limiting logic, regressions in the project and test configuration (pyproject.toml), duplicated code in the GitHub API client, and a misconfiguration in the FastAPI router setup. There are also some inconsistencies and potentially misleading tests that should be reviewed.

Overall, this is a great step forward, but the identified issues should be fixed to ensure the application is stable and maintainable.

I am having trouble creating individual review comments. Click here to see my feedback.

src/api/rate_limit.py (20)

critical

The rate_limiter dependency is not correctly wired to receive the authenticated user. It uses Depends(lambda: None), which will always result in user being None. This means the rate limit for authenticated users (AUTH_LIMIT) will never be applied, and all users will be subject to the anonymous IP-based limit. To fix this, you should depend on get_current_user_optional from src.api.dependencies.

async def rate_limiter(request: Request, user: User | None = Depends(get_current_user_optional)):

pyproject.toml (141-147)

high

The docs dependencies have been removed from [project.optional-dependencies]. Since the project still contains documentation files that are being updated in this PR, removing the dependencies required to build the documentation (like mkdocs) will break the doc generation process. Please re-add these dependencies if documentation is still intended to be built from this repository.

pyproject.toml (191-194)

high

The mypy configuration has been weakened by removing warn_redundant_casts, warn_unused_ignores, warn_no_return, and warn_unreachable. These flags help enforce stricter type checking and catch potential bugs. Was there a specific reason for removing them? If not, I recommend re-enabling them to maintain a high level of static analysis quality.

pyproject.toml (213-234)

high

The [tool.coverage.run] and [tool.coverage.report] sections have been completely removed. This removes important configurations, such as omit patterns for excluding test files from coverage metrics and exclude_lines for ignoring specific lines (like pragma: no cover). This is likely to result in inaccurate coverage reports. Please restore this configuration.

src/main.py (103)

high

The recommendations_api_router is being included with the prefix /api/rules. However, the router defined in src/api/recommendations.py already has a prefix of /rules. This will result in a duplicated path, making the final endpoint URL /api/rules/rules/recommend, which is likely not intended. Please remove the prefix from either the router definition or its inclusion here to fix the path.

app.include_router(recommendations_api_router, prefix="/api/v1", tags=["Recommendations API"])

src/integrations/github/api.py (279-291)

high

The logic inside get_pr_checks to fetch check runs for a commit SHA is a duplication of the get_check_runs method. To adhere to the DRY principle and improve maintainability, get_pr_checks should call get_check_runs after retrieving the head_sha, as it did in the previous version.

pyproject.toml (99-108)

medium

The keywords, classifiers, and [project.urls] sections have been removed. This metadata is valuable for package discovery on PyPI and for providing users with important links to the homepage, documentation, and issue tracker. Was their removal intentional? If not, please consider re-adding them.

pyproject.toml (207-208)

medium

The pytest addopts for coverage reports have been changed to only include term-missing. The html and xml report formats were removed. These formats are often crucial for CI pipelines to archive and display coverage results. Please consider if this was intentional, and if not, re-add the desired report formats.

src/api/dependencies.py (9)

medium

The logger is initialized twice. You can remove this duplicate line.

src/main.py (93)

medium

The CORS allow_origins is hardcoded to ["*"]. While the comment mentions this is for development, it's a security risk if this configuration makes it to production. It would be safer to make this configurable via environment variables, with a restrictive default for production environments, as was done previously.

    allow_origins=config.cors.origins,

src/integrations/github/service.py (19-115)

medium

This new GitHubService appears to be introducing inconsistencies. It uses httpx for API calls, whereas the existing GitHubClient was just refactored to consistently use aiohttp. Furthermore, the logic in analyze_repository_rules seems to be a placeholder and duplicates the purpose of the new RepositoryAnalysisAgent. Could you clarify the role of this service? If it's intended for future use, it might be better to align its implementation (e.g., use aiohttp and the shared GitHubClient) or remove it for now to avoid confusion.

tests/integration/test_recommendations.py (48-71)

medium

The test test_authenticated_access_private_repo seems to be testing analysis of a private repository. However, the endpoint implementation currently hardcodes is_public=True when calling the agent, and the PR description indicates private repo support is a future feature. This test passes only because the GitHub API calls are mocked, but it doesn't accurately reflect the current capabilities of the application. This could lead to a false sense of security about this feature. It might be better to mark this test with @pytest.mark.skip until the feature is actually implemented.

Copy link

@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: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/rules/utils/contributors.py (1)

55-55: TypeError when comparing naive and timezone-aware datetimes.

datetime.now() creates a naive datetime, but _has_recent_activity parses GitHub timestamps into timezone-aware datetimes (lines 196, 206). Comparing these raises TypeError: can't compare offset-naive and offset-aware datetimes.

🐛 Proposed fix: Use timezone-aware datetime
-            cutoff_date = datetime.now() - timedelta(days=days_back)
+            cutoff_date = datetime.now(tz=datetime.UTC) - timedelta(days=days_back)

Or import and use timezone.utc:

-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
-            cutoff_date = datetime.now() - timedelta(days=days_back)
+            cutoff_date = datetime.now(tz=timezone.utc) - timedelta(days=days_back)
src/core/utils/caching.py (1)

129-135: Global cache TTL mutation causes unintended side effects.

Modifying _GLOBAL_CACHE.ttl affects all cached entries, not just the one being set. If one caller sets ttl=60, subsequent callers expecting the default 3600s TTL will have their entries expire prematurely.

Either remove the TTL override capability from set_cache, or use per-entry TTL tracking.

Suggested fix: remove global TTL mutation
 async def set_cache(key: str, value: Any, ttl: int | None = None) -> None:
     """
     Async helper to store into the module-level cache.
+    Note: ttl parameter is ignored; entries use the global cache TTL.
     """
-    if ttl and ttl != _GLOBAL_CACHE.ttl:
-        _GLOBAL_CACHE.ttl = ttl
     _GLOBAL_CACHE.set(key, value)
src/integrations/providers/bedrock_provider.py (3)

202-204: with_structured_output is a no-op stub.

Returning self without implementing structured output will silently ignore any output schema passed by callers. This could lead to unexpected behavior when callers expect structured responses.

Consider either implementing actual structured output support or raising NotImplementedError to fail explicitly.

Suggested fix: fail explicitly
             def with_structured_output(self, output_model: Any) -> Any:
                 """Add structured output support."""
-                return self
+                raise NotImplementedError(
+                    "Structured output is not supported for Anthropic Bedrock inference profiles. "
+                    "Use the standard Bedrock client instead."
+                )

239-246: Async method calls synchronous implementation.

_agenerate directly calls the synchronous _generate, which performs a blocking HTTP call. This blocks the event loop and defeats the purpose of async.

Suggested fix: use asyncio.to_thread for true async
             async def _agenerate(
                 self,
                 messages: list[BaseMessage],
                 stop: list[str] | None = None,
                 run_manager: Any | None = None,
             ) -> ChatResult:
                 """Async generate using the Anthropic client."""
-                return self._generate(messages, stop, run_manager)
+                import asyncio
+                return await asyncio.to_thread(self._generate, messages, stop, run_manager)

215-222: System messages must be passed via the system parameter, not as user messages in the messages array.

Anthropic's Bedrock API requires system messages to be passed as a top-level system field in the request body (alongside the messages array), not as messages with a role in the messages array. The current code silently converts system messages to "user" role, which breaks system prompt functionality and produces unexpected model behavior.

Update the message construction to extract system messages and pass them separately:

  • Check for msg.type == "system" and collect the content
  • Pass it as the system parameter to messages.create()
  • Only include "user" and "assistant" roles in the messages array
src/api/rules.py (1)

14-27: Missing error handling for agent execution.

The comment on line 19 acknowledges the agent may throw, but there's no try/except. Additionally, result.data.get() assumes result has a data attribute. If the agent fails or returns an unexpected format, clients receive an opaque 500 error.

Suggested fix: add error handling
 `@router.post`("/rules/evaluate")
 async def evaluate_rule(request: RuleEvaluationRequest):
     # Agent: uses central config—change here affects all rule evals.
     agent = get_agent("feasibility")

-    # Async call—agent may throw if rule malformed.
-    result = await agent.execute(rule_description=request.rule_text)
+    try:
+        result = await agent.execute(rule_description=request.rule_text)
+    except Exception as e:
+        return {
+            "supported": False,
+            "snippet": "",
+            "feedback": f"Rule evaluation failed: {str(e)}",
+        }

     # Output: keep format stable for frontend. Brittle if agent changes keys.
     return {
         "supported": result.data.get("is_feasible", False),
         "snippet": result.data.get("yaml_content", ""),
         "feedback": result.message,
     }
🤖 Fix all issues with AI agents
In `@src/agents/repository_analysis_agent/nodes.py`:
- Around line 130-151: The structured output model RecommendationsList currently
inherits AnalysisState which requires fields like repo_full_name and causes
Pydantic validation failures; change RecommendationsList to inherit from
pydantic.BaseModel (or BaseModel alias used in the project) and define only
recommendations: list[RuleRecommendation] so
llm.with_structured_output(RecommendationsList) returns a Pydantic object that
matches the LLM payload; update any imports if necessary and keep the rest of
the flow (structured_llm, response.recommendations) unchanged.

In `@src/api/dependencies.py`:
- Around line 24-41: The get_current_user_optional function currently treats any
Bearer token as authenticated; change it so that when a Bearer token is present
it validates the token signature with the configured IdP/JWT verification (or
calls your existing token validation helper) and only returns a User when
validation succeeds, otherwise return None; alternatively, gate the existing
“fake user” behavior behind an explicit dev-only flag (e.g., a CONFIG_DEV_AUTH
or ENABLE_FAKE_AUTH env var) and ensure production defaults to real validation
and None on failure; update error handling in get_current_user_optional to catch
validation errors, log them, and return None instead of authenticating every
token.

In `@src/api/rate_limit.py`:
- Around line 20-27: The rate_limiter function can raise AttributeError when
request.client is None; update rate_limiter to safely derive the IP by checking
request.client first (use request.client.host when request.client is not None)
and otherwise fall back to a header or placeholder (e.g.,
request.headers.get("x-forwarded-for") or "unknown") before building the key
variable; ensure the key assignment (key = f"ip:{...}") and limit selection
logic remain unchanged and still use AUTH_LIMIT/ANON_LIMIT.
- Around line 20-24: The rate_limiter dependency is using Depends(lambda: None)
so it always gets None; replace that with the optional auth dependency so
authenticated users are recognized: import and use get_current_user_optional and
change the parameter in async def rate_limiter(request: Request, user: User |
None = Depends(get_current_user_optional)) so the function receives the actual
user when present (refer to rate_limiter and get_current_user_optional).

In `@src/api/recommendations.py`:
- Around line 102-106: Replace logging of raw user email in the Analysis request
log: do not use user.email directly in logger.info; instead derive a stable
non-PII identifier (e.g., user.id or a hashed/redacted token) and log that.
Update the code around repo_url_str, client_ip, user_id and the logger.info call
so user_id is set from user.id (or a sanitized/hash of user.email) and then
include that identifier in logger.info(f"Analysis requested for {repo_url_str}
by {user_id} (IP: {client_ip})").
- Around line 82-92: The rate limiter dependency currently uses a lambda
returning None which prevents authenticated detection; update the limiter in
rate_limit.py to depend on get_current_user_optional instead of the lambda
(import get_current_user_optional from src.api.dependencies at the top of
rate_limit.py) so rate_limiter can check the passed User (user and user.email)
and apply the authenticated (100 req/hr) vs anonymous (5 req/hr) limits; no
change needed to recommend_rules other than ensuring it still uses
Depends(rate_limiter).

In `@src/integrations/github/api.py`:
- Around line 168-179: The create_check_run method currently always includes
"conclusion" in the payload, which GitHub rejects unless status == "completed";
update the payload construction in create_check_run so that data = {"name":
name, "head_sha": sha, "status": status, "output": output} and only add
data["conclusion"] = conclusion when status == "completed" (use the existing
function name create_check_run and variables url, headers, data to locate the
change), preserving the early-return when headers is falsy and leaving the rest
of the request flow unchanged.

In `@src/integrations/github/service.py`:
- Around line 40-48: The rate-limit handling in the async HTTP fetch needs to
also treat HTTP 429 as a rate-limit error: inside the block using
httpx.AsyncClient() where response is checked (the existing status_code checks
and raises for GitHubResourceNotFoundError and GitHubRateLimitError), add a
condition that treats response.status_code == 429 (or response.status_code ==
403 with "rate limit" in response.text) as a GitHubRateLimitError and raise
GitHubRateLimitError accordingly; ensure this check occurs before calling
response.raise_for_status() so the custom error is raised for 429/403-rate-limit
cases.
- Around line 66-73: The loop that checks files (iterating files_to_check and
calling client.get on
f"{self.BASE_URL}/repos/{owner}/{repo}/contents/{filepath}") treats any non-200
as "not found" and thus silently masks rate limits, auth failures, and server
errors; update the logic inside that async with block to inspect
resp.status_code: if 404 add to found_files logic as before, if 401/403/429/5xx
log an informative message via the service logger (include resp.status_code,
resp.text or headers) and raise or return a distinct error so callers can handle
auth or rate-limit cases, and if 429 include retry-after handling (sleep/retry
or surface the rate-limit error); keep existing behavior only for genuine 404s.
- Around line 107-114: The _parse_url function currently splits the raw string
and can return invalid owner/repo for inputs like "https://github.com"; update
_parse_url to robustly extract and validate owner and repo by parsing the URL
path (e.g. using urllib.parse.urlparse) or by supporting plain "owner/repo"
input, split the path into segments, ensure there are at least two non-empty
segments (owner and repo), and raise a ValueError with a clear message if they
are missing or empty; adjust the return to explicitly return (owner, repo) only
after validation.

In `@src/main.py`:
- Around line 88-97: Replace the hardcoded CORS settings in the
app.add_middleware(CORSMiddleware, ...) call by importing and using the
environment-aware settings from config (e.g., config.cors.origins and
config.cors.headers) instead of ["*"], and set allow_credentials to False by
default or conditionally (e.g., only True in non-production/dev) so credentials
are not enabled in production; update the CORSMiddleware call to reference
config.cors.origins, config.cors.headers and a config-driven allow_credentials
flag.

In `@tests/conftest.py`:
- Around line 20-34: The mock_settings fixture currently calls the non-existent
pytest.helpers.mock_env and will fail at collection; replace that usage by
applying unittest.mock.patch.dict directly in the fixture (or register the local
Helpers class as a pytest plugin via pytest-helpers-namespace if you prefer
reuse). Specifically, update the mock_settings fixture to use
unittest.mock.patch.dict(os.environ, {...}, clear=False) as a context manager
around yield, and remove or re-register references to pytest.helpers.mock_env
and any direct calls to pytest.helpers in tests; if keeping the Helpers class,
ensure it is exposed to pytest (e.g., via pytest_plugins or
pytest-helpers-namespace) so code referencing pytest.helpers.mock_env resolves.
🧹 Nitpick comments (15)
src/core/config/settings.py (1)

141-150: Consider whether the "gcp" alias is too broad.

The alias "gcp" is generic and could cause confusion or conflicts if other GCP-based AI services are added in the future (e.g., Gemini API direct access vs. Vertex AI). Consider removing it or documenting why it's intentionally mapped to Vertex AI.

♻️ Suggested refinement
         vertex_aliases = {
             "vertex_ai",
             "garden",
             "vertex",
             "vertexai",
             "model_garden",
-            "gcp",
         }
src/core/utils/logging.py (1)

148-149: Consider simplifying the comment.

The comment is slightly imprecise—we're not using a string name for the type; we're relying on PEP 563's postponed evaluation via __future__. A clearer comment might be:

✏️ Suggested clarification
-    # Callable is now only needed for typing, so it's safe to use the string name or handled by __future__
+    # Callable is imported under TYPE_CHECKING; PEP 563 ensures this annotation isn't evaluated at runtime
src/rules/utils/contributors.py (1)

247-252: Singleton ignores subsequent github_client arguments.

Once created, get_contributor_analyzer always returns the same instance regardless of the github_client passed. If different clients are used (e.g., for different authentication contexts), this could cause unexpected behavior.

♻️ Consider validating client consistency or using a registry
 def get_contributor_analyzer(github_client) -> ContributorAnalyzer:
     """Get or create the global contributor analyzer instance."""
     global _contributor_analyzer
-    if _contributor_analyzer is None:
+    if _contributor_analyzer is None or _contributor_analyzer.github_client is not github_client:
         _contributor_analyzer = ContributorAnalyzer(github_client)
     return _contributor_analyzer

Alternatively, document that the first client "wins" if this is intentional.

linting_commands.txt (1)

5-9: Consider including tests/ directory in linting commands.

The tests/ directory exists in the project, and while pre-commit hooks already lint it via pre-commit run --all-files, the manual linting commands target only src/. For consistency and to ensure tests follow the same linting standards as production code, consider extending the paths:

# Run ruff linter and formatter directly (if needed)
-ruff check src/
-ruff format src/
+ruff check src/ tests/
+ruff format src/ tests/

# Run mypy for type checking
-mypy src/
+mypy src/ tests/
src/core/utils/caching.py (1)

161-163: Confusing cache type selection based on ttl parameter.

The ternary selects AsyncCache when ttl is provided, otherwise TTLCache. This coupling between TTL presence and cache implementation type is unclear. Both cache types support TTL, so the selection criteria seems arbitrary.

Consider making the cache type selection explicit via a separate parameter or always using the same cache type.

src/tasks/task_queue.py (1)

114-127: Deduplication logic is defined but not used.

_create_event_hash is implemented (lines 47-112) but enqueue always sets event_hash=None and never calls it. The event_hashes dict and cleanup logic exist but serve no purpose.

Either remove the dead code or complete the deduplication implementation.

Option: Enable deduplication
     async def enqueue(self, event_type: str, repo_full_name: str, installation_id: int, payload: dict[str, Any]) -> str:
         """Enqueue a new task for background processing."""
+        # Check for duplicate events
+        event_hash = self._create_event_hash(event_type, repo_full_name, payload)
+        if event_hash in self.event_hashes:
+            existing_task_id = self.event_hashes[event_hash]
+            logger.info(f"Duplicate event detected, returning existing task {existing_task_id}")
+            return existing_task_id
+
         task_id = f"{event_type}_{repo_full_name}_{datetime.now().timestamp()}"

         task = Task(
             id=task_id,
             event_type=event_type,
             repo_full_name=repo_full_name,
             installation_id=installation_id,
             payload=payload,
             status=TaskStatus.PENDING,
             created_at=datetime.now(),
-            event_hash=None,  # No deduplication for now
+            event_hash=event_hash,
         )

         self.tasks[task_id] = task
+        self.event_hashes[event_hash] = task_id

         logger.info(f"Enqueued task {task_id} for {repo_full_name}")

         return task_id
src/webhooks/handlers/deployment_status.py (1)

36-42: Comment is misleading - queue has no overflow protection.

The comment states "may fail if queue overloaded," but the in-memory TaskQueue has no size limits or backpressure mechanism. Consider either:

  1. Removing the misleading comment, or
  2. Adding actual queue capacity limits to TaskQueue
pyproject.toml (1)

37-37: Consider aligning the Ruff version floor with pre-commit.

The dev dependency specifies ruff>=0.1.0, but .pre-commit-config.yaml pins v0.3.0. Consider ruff>=0.3.0 to ensure local dev environments match the pre-commit behavior.

src/integrations/github/service.py (1)

40-41: Consider adding request timeouts to prevent indefinite hangs.

Neither get_repo_metadata nor analyze_repository_rules specifies a timeout. Network issues could cause requests to hang indefinitely.

♻️ Suggested improvement
-        async with httpx.AsyncClient() as client:
-            response = await client.get(api_url)
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            response = await client.get(api_url)

Alternatively, define a class-level timeout constant for consistency across methods.

Also applies to: 66-67

src/core/models.py (1)

7-18: Consider excluding github_token from default serialization.

Storing the OAuth token in the User model is a valid pattern, but the token could be accidentally exposed in logs or API responses if the model is serialized without care. Pydantic's Field supports repr=False to hide it from string representations.

♻️ Suggested improvement
     # storing the token allows the service layer to make requests on behalf of the user
-    github_token: str | None = Field(None, description="OAuth token for GitHub API access")
+    github_token: str | None = Field(
+        None,
+        description="OAuth token for GitHub API access",
+        repr=False,  # Prevent token from appearing in repr/logs
+    )

Additionally, when returning User data in API responses, explicitly exclude the token:

user.model_dump(exclude={"github_token"})
tests/conftest.py (1)

49-58: Remove the custom event_loop fixture—it conflicts with asyncio_mode = "auto".

With asyncio_mode = "auto" configured in pyproject.toml (line 133), pytest-asyncio automatically manages event loop creation and cleanup. The custom session-scoped event_loop fixture is redundant and can cause conflicts or deprecation warnings in pytest-asyncio ≥0.21. Simply delete the fixture and let pytest-asyncio handle loop management.

Removal diff
-# 4. Async Support (Essential for FastAPI)
-# Note: 'asyncio_mode = "auto"' in pyproject.toml handles the loop,
-# but this fixture ensures scope cleanliness if needed.
-@pytest.fixture(scope="session")
-def event_loop():
-    import asyncio
-
-    policy = asyncio.get_event_loop_policy()
-    loop = policy.new_event_loop()
-    yield loop
-    loop.close()
src/api/rate_limit.py (1)

29-40: Memory leak: stale rate-limit keys are never purged.

The store grows unbounded as new IPs/users make requests. While timestamps within each key are pruned, keys for inactive clients persist indefinitely.

Consider adding periodic cleanup or TTL-based key expiration. Given the PR notes Redis migration as an immediate priority, this may be acceptable short-term.

♻️ Suggested improvement: prune empty keys
     timestamps = [ts for ts in timestamps if now - ts < WINDOW]
     if len(timestamps) >= limit:
         retry_after = int(WINDOW - (now - min(timestamps)))
         raise HTTPException(
             status_code=status.HTTP_429_TOO_MANY_REQUESTS,
             detail=f"Rate limit exceeded. Try again in {retry_after} seconds.",
             headers={"Retry-After": str(retry_after)},
         )
     timestamps.append(now)
-    _RATE_LIMIT_STORE[key] = timestamps
+    if timestamps:
+        _RATE_LIMIT_STORE[key] = timestamps
+    elif key in _RATE_LIMIT_STORE:
+        del _RATE_LIMIT_STORE[key]
src/rules/validators.py (1)

246-260: Consider timezone handling for consistency.

WeekendCondition uses datetime.now() (local time), while AllowedHoursCondition supports explicit timezones via pytz. For consistency and predictable behavior across deployment environments, consider adding timezone support here as well.

src/api/dependencies.py (1)

8-9: Remove duplicate logger assignment.

♻️ Proposed fix
-logger = logging.getLogger(__name__)  # Logger: keep at module level for reuse.
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__name__)  # Logger: keep at module level for reuse.
src/agents/repository_analysis_agent/models.py (1)

6-16: Constrain severity/category to known values.

Right now any string passes validation; using Literal/Enum helps prevent invalid values and keeps docs aligned with actual outputs (e.g., the fallback uses system).

♻️ Suggested typing constraints
-from pydantic import BaseModel, Field
+from typing import Literal
+from pydantic import BaseModel, Field
@@
-    severity: str = Field("medium", description="low, medium, high, or critical")
-    category: str = Field("quality", description="security, quality, compliance, or velocity")
+    severity: Literal["low", "medium", "high", "critical"] = Field(
+        "medium", description="low, medium, high, or critical"
+    )
+    category: Literal["security", "quality", "compliance", "velocity", "system"] = Field(
+        "quality", description="security, quality, compliance, velocity, or system"
+    )
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5a3be1 and 4bfd1c6.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (37)
  • .pre-commit-config.yaml
  • README.md
  • docs/features.md
  • linting_commands.txt
  • pyproject.toml
  • src/__init__.py
  • src/agents/repository_analysis_agent/agent.py
  • src/agents/repository_analysis_agent/models.py
  • src/agents/repository_analysis_agent/nodes.py
  • src/agents/repository_analysis_agent/prompts.py
  • src/api/dependencies.py
  • src/api/rate_limit.py
  • src/api/recommendations.py
  • src/api/rules.py
  • src/core/config/settings.py
  • src/core/models.py
  • src/core/utils/caching.py
  • src/core/utils/logging.py
  • src/integrations/github/api.py
  • src/integrations/github/service.py
  • src/integrations/providers/bedrock_provider.py
  • src/integrations/providers/factory.py
  • src/main.py
  • src/rules/utils/contributors.py
  • src/rules/validators.py
  • src/tasks/scheduler/deployment_scheduler.py
  • src/tasks/task_queue.py
  • src/webhooks/auth.py
  • src/webhooks/dispatcher.py
  • src/webhooks/handlers/deployment.py
  • src/webhooks/handlers/deployment_status.py
  • src/webhooks/handlers/issue_comment.py
  • src/webhooks/handlers/pull_request.py
  • src/webhooks/router.py
  • tests/conftest.py
  • tests/integration/test_recommendations.py
  • tests/integration/test_rules_api.py
🧰 Additional context used
🧬 Code graph analysis (15)
src/webhooks/dispatcher.py (9)
src/webhooks/handlers/deployment.py (1)
  • handle (17-46)
src/webhooks/handlers/deployment_status.py (1)
  • handle (17-50)
src/webhooks/handlers/issue_comment.py (2)
  • handle (25-147)
  • event_type (19-20)
src/webhooks/handlers/pull_request.py (1)
  • handle (18-45)
src/webhooks/handlers/check_run.py (1)
  • handle (16-29)
src/webhooks/handlers/push.py (1)
  • handle (16-29)
src/webhooks/handlers/deployment_protection_rule.py (1)
  • handle (16-33)
src/webhooks/handlers/deployment_review.py (1)
  • handle (16-33)
src/webhooks/handlers/base.py (1)
  • handle (16-26)
src/integrations/github/service.py (1)
src/core/utils/caching.py (1)
  • get (44-66)
src/integrations/providers/factory.py (1)
src/core/config/provider_config.py (2)
  • get_max_tokens_for_agent (57-63)
  • get_temperature_for_agent (65-71)
src/webhooks/router.py (2)
src/webhooks/handlers/issue_comment.py (1)
  • event_type (19-20)
src/core/models.py (1)
  • EventType (24-38)
src/api/dependencies.py (2)
src/core/models.py (1)
  • User (7-18)
src/integrations/github/service.py (1)
  • GitHubService (19-115)
src/agents/repository_analysis_agent/nodes.py (7)
src/agents/repository_analysis_agent/models.py (2)
  • AnalysisState (19-44)
  • RuleRecommendation (6-16)
src/integrations/providers/bedrock_provider.py (2)
  • get_chat_model (24-48)
  • with_structured_output (202-204)
src/integrations/providers/factory.py (1)
  • get_chat_model (97-130)
src/core/utils/caching.py (1)
  • get (44-66)
src/core/models.py (1)
  • repo_full_name (56-58)
src/agents/repository_analysis_agent/test_agent.py (1)
  • agent (18-20)
src/integrations/providers/vertex_ai_provider.py (1)
  • ainvoke (109-131)
src/api/rate_limit.py (1)
src/core/models.py (1)
  • User (7-18)
src/agents/repository_analysis_agent/models.py (1)
src/core/models.py (1)
  • repo_full_name (56-58)
tests/integration/test_recommendations.py (1)
src/core/utils/caching.py (1)
  • get (44-66)
src/api/recommendations.py (6)
src/agents/repository_analysis_agent/agent.py (2)
  • RepositoryAnalysisAgent (13-62)
  • execute (39-62)
src/agents/repository_analysis_agent/models.py (1)
  • RuleRecommendation (6-16)
src/api/dependencies.py (1)
  • get_current_user_optional (24-43)
src/api/rate_limit.py (1)
  • rate_limiter (20-40)
src/core/models.py (2)
  • User (7-18)
  • repo_full_name (56-58)
tests/integration/test_rules_api.py (1)
  • client (21-23)
src/webhooks/handlers/deployment.py (2)
src/core/utils/caching.py (1)
  • get (44-66)
src/core/models.py (1)
  • repo_full_name (56-58)
src/main.py (3)
src/webhooks/handlers/deployment_protection_rule.py (1)
  • DeploymentProtectionRuleEventHandler (10-33)
src/tasks/scheduler/deployment_scheduler.py (2)
  • get_deployment_scheduler (364-369)
  • stop (40-48)
src/tasks/task_queue.py (1)
  • stop_workers (143-149)
src/tasks/task_queue.py (3)
src/webhooks/handlers/issue_comment.py (1)
  • event_type (19-20)
src/core/models.py (1)
  • repo_full_name (56-58)
src/core/utils/caching.py (1)
  • get (44-66)
src/integrations/github/api.py (2)
src/core/models.py (1)
  • repo_full_name (56-58)
src/core/utils/caching.py (1)
  • get (44-66)
tests/conftest.py (1)
src/integrations/github/api.py (1)
  • close (163-166)
🪛 Gitleaks (8.30.0)
tests/conftest.py

[high] 28-28: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 markdownlint-cli2 (0.18.1)
README.md

238-238: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


244-244: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🔇 Additional comments (49)
src/core/config/settings.py (1)

136-138: LGTM!

The simplified Bedrock validation is clear and the comment helpfully explains why only the model ID is required (credentials come from AWS environment/IMDS).

src/core/utils/logging.py (1)

8-18: Clean modernization of type imports.

Good use of PEP 563 (from __future__ import annotations) combined with the TYPE_CHECKING guard pattern. This correctly avoids runtime import of Callable while maintaining full type checker support. Using collections.abc.Callable over the deprecated typing.Callable is the right choice.

src/rules/utils/contributors.py (1)

61-65: LGTM!

The refactor correctly combines nested conditionals while preserving short-circuit evaluation—_has_recent_activity is only called when the contribution threshold is met, avoiding unnecessary API calls.

src/__init__.py (1)

1-1: LGTM!

The updated docstring appropriately signals the intent to keep this root package file minimal.

docs/features.md (1)

279-287: LGTM!

The new documentation section clearly communicates the rate limiting behavior and authentication requirements. This aligns well with the API changes in this PR.

src/tasks/scheduler/deployment_scheduler.py (1)

2-2: LGTM!

Good refactor using contextlib.suppress() instead of a bare try/except/pass block, following the SIM105 recommendation. The explicit CancelledError handling in _scheduler_loop (line 100) is correctly preserved since it has actual logic (logging and breaking the loop).

Also applies to: 45-47

README.md (1)

238-245: LGTM!

The new section clearly documents the rate limiting feature for users. The static analysis warnings (MD036) appear to be false positives—line 239 uses proper heading syntax (##), and line 244 is a list item, not misused emphasis.

src/integrations/providers/factory.py (1)

8-9: LGTM!

Clean refactor:

  • Adding from __future__ import annotations enables postponed annotation evaluation.
  • The inline conditional assignments maintain the same precedence logic (explicit params → agent config → global config) in a more concise form.

Also applies to: 78-79

src/webhooks/auth.py (1)

32-44: LGTM!

The clarifying comments improve code readability without altering the secure implementation. The constant-time comparison and HMAC-SHA256 usage are correctly documented.

src/core/utils/caching.py (1)

169-175: LGTM!

The ternary for key generation and unified .get() call simplify the code while maintaining correct behavior for both cache types.

src/integrations/providers/bedrock_provider.py (1)

151-167: LGTM on the profile matching refactor.

The lowercase normalization and grouped provider checks improve readability and correctness. The explicit keyword lists make the matching criteria clear.

src/tasks/task_queue.py (1)

35-57: LGTM on the comment clarifications.

The updated comments improve understanding of the deduplication mechanism and event-specific hash fields.

src/webhooks/handlers/deployment_status.py (1)

17-50: LGTM on the handler implementation.

The handler correctly validates the installation ID, extracts deployment info, and enqueues with all required parameters. The logging provides good visibility into the event processing.

.pre-commit-config.yaml (1)

7-7: LGTM! Clean pre-commit configuration update.

Good additions:

  • check-added-large-files helps prevent accidental large file commits
  • --exit-non-zero-on-fix ensures CI fails when auto-fixes are applied, enforcing clean commits
  • Consolidating linting and formatting under Ruff simplifies the toolchain

Also applies to: 14-14, 18-24

tests/conftest.py (1)

28-28: Gitleaks false positive — this is intentional mock data.

The base64 string bW9jay1rZXk= decodes to mock-key, and the surrounding comments clearly indicate these are dummy test values. This is not a real secret.

pyproject.toml (1)

43-76: LGTM! Well-structured Ruff configuration.

Good choices:

  • Rule selection covers core errors, import sorting, bug detection, and modernization
  • B008 ignore is correctly documented for FastAPI's dependency injection pattern
  • E501 delegated to formatter is the right approach
  • docstring-code-format = true improves docstring readability
src/core/models.py (1)

24-38: LGTM! Clean model definitions.

  • EventType(str, Enum) improves JSON serialization compatibility
  • WebhookEvent property accessors provide safe access with sensible defaults
  • The efficiency comment explaining why WebhookEvent isn't a Pydantic model is helpful

Also applies to: 41-62

src/webhooks/handlers/pull_request.py (1)

22-23: LGTM! Helpful documentation of a known fragility.

The comment appropriately warns about the string-based action comparison. While this is a standard pattern for GitHub webhooks, the note helps future maintainers understand the coupling.

src/webhooks/handlers/deployment.py (1)

27-38: LGTM!

The updated comments appropriately document the fragility of payload structure dependencies and the async nature of the enqueue operation. The implementation remains sound.

src/webhooks/dispatcher.py (2)

16-17: LGTM!

The comment clarifies the registry's purpose for hot-swap and test injection patterns.


60-60: LGTM!

The singleton comment appropriately documents the lifecycle and thread-safety considerations. For the current async FastAPI usage where handlers are registered at startup, this pattern is acceptable.

src/webhooks/handlers/issue_comment.py (3)

35-39: LGTM!

The bot self-reply guard with comprehensive username variants is well-implemented. The comment clarifies the purpose.


155-169: LGTM!

The pattern hierarchy (double quotes → single quotes → unquoted fallback) is sensible, and the defensive empty-reason check at line 168 prevents accidental matches from the greedy unquoted pattern.


189-194: LGTM!

The any() pattern is idiomatic and allows easy extension if more help triggers are needed.

src/webhooks/router.py (3)

13-16: LGTM!

The dependency injection pattern for the dispatcher improves testability and lifecycle control.


31-34: Unconventional but acceptable pattern for unsupported events.

Using HTTPException with 202 for control flow is non-standard, but the design intent is clear: accept unknown events gracefully to prevent GitHub retry storms. The comment documents this well.


58-66: LGTM!

The exception handling correctly distinguishes 202 (unsupported event) from other HTTP errors, ensuring graceful degradation for unknown GitHub events while preserving error semantics for actual failures.

src/rules/validators.py (5)

85-91: LGTM!

The metadata comment clarifies the purpose of these class attributes for dynamic validator selection.


238-243: Placeholder logic documented appropriately.

The TODO and comment clearly indicate this is demo-only behavior. The PR description notes these as known gaps for production hardening.


622-630: LGTM!

The lazy import with graceful fallback for missing pytz is a reasonable pattern for an optional dependency. The warning log helps diagnose misconfiguration.


181-187: LGTM!

The conditional logic is clear: match presence for files_match_pattern, match absence for files_not_match_pattern. The comment documents the intent well.


980-1007: LGTM!

The validator registry provides a clean mapping from condition names to validator instances, and the get_validator_descriptions() function enables dynamic introspection for the LLM-powered reasoning engine mentioned in the PR objectives.

src/integrations/github/api.py (5)

22-27: Anonymous/public auth flow is clear and well‑scoped.

Priority order is explicit and the public repo helpers consistently reuse it.

Also applies to: 36-64, 98-129, 135-142


189-228: PR retrieval and check‑run lookup via head SHA look solid.

The flow is clear and correctly reuses the PR metadata for check‑runs.

Also applies to: 266-293


236-243: JWT generation and org team lookup adjustments are straightforward.

Also applies to: 297-304


245-252: The code correctly assumes base64-encoded private key per documented configuration.

The environment variable PRIVATE_KEY_BASE64_GITHUB and setup documentation (LOCAL_SETUP.md, DEVELOPMENT.md) explicitly instruct users to base64-encode the .pem file before providing it to the application. The unconditional base64.b64decode() is correct by design and should not include a fallback for raw PEM, as that would contradict the documented configuration contract and could mask user configuration errors.


230-234: [rewritten review comment]
[classification tag]

tests/integration/test_rules_api.py (1)

28-28: Conditional toggle reads clearer with != "true" check.

Also applies to: 71-71, 106-106

tests/integration/test_recommendations.py (1)

13-71: Good coverage for anonymous vs authenticated access paths.

src/main.py (3)

16-18: Lifecycle logging and handler wiring look good.

Also applies to: 38-38, 65-65, 70-70, 78-78


121-125: Task status aggregation is clearer and consistent.

Also applies to: 135-135


103-103: Verify router prefix alignment with recommendations routes and tests.

The new prefix /api/rules may change the public path; confirm it still matches /v1/rules/recommend (tests and clients). Inspect recommendations router definitions and test expectations to ensure path consistency.

src/api/dependencies.py (1)

14-18: DI helper and strict auth dependency are clean and straightforward.

Also applies to: 46-54

src/agents/repository_analysis_agent/prompts.py (1)

3-35: Prompts are concise and focused on JSON output.

src/agents/repository_analysis_agent/agent.py (2)

22-37: Clean two‑step LangGraph flow.

Node registration and edge wiring are straightforward and easy to follow.


39-58: Solid state rehydration and error handling.

The model_dump → graph → rehydrate pattern is clear and keeps the happy path tight.

src/agents/repository_analysis_agent/models.py (1)

19-44: AnalysisState shape looks coherent.

Inputs, signals, outputs, and metadata are clearly separated and easy to extend.

src/api/recommendations.py (1)

45-76: Good fast‑fail URL parsing.

Nice touch allowing owner/repo shorthand and providing a clear validation error.

src/agents/repository_analysis_agent/nodes.py (1)

13-104: Signal collection is clear and defensive.

Graceful handling of missing files and workflow parsing keeps this resilient.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +130 to +151
# 2. Initialize LLM
# We use the factory to respect project settings (provider, temperature)
try:
llm = get_chat_model(agent="repository_analysis")

# Build YAML rule with proper indentation
# parameters: is at column 0, source_patterns: at column 2, list items at column 4
source_patterns_yaml = "\n".join(f' - "{pattern}"' for pattern in source_patterns)
test_patterns_yaml = "\n".join(f' - "{pattern}"' for pattern in test_patterns)
# 3. Structured Output Enforcement
# We define a wrapper model to ensure we get a list of recommendations
# Note: LangChain's with_structured_output is preferred over raw JSON parsing
class RecommendationsList(AnalysisState):
# We strictly want the list, reusing the model definition
recommendations: list[RuleRecommendation]

yaml_content = f"""description: "Require tests when code changes"
enabled: true
severity: medium
event_types:
- pull_request
parameters:
source_patterns:
{source_patterns_yaml}
test_patterns:
{test_patterns_yaml}
"""
structured_llm = llm.with_structured_output(RecommendationsList)

confidence = 0.74
if pr_issues.get("missing_tests", 0) > 0:
confidence = 0.85
if state.contributing_analysis.content and state.contributing_analysis.requires_tests:
confidence = min(0.95, confidence + 0.1)

recommendations.append(
RuleRecommendation(
yaml_rule=yaml_content.strip(),
confidence=confidence,
reasoning=test_reasoning,
strategy_used="hybrid",
response = await structured_llm.ainvoke(
[SystemMessage(content=REPOSITORY_ANALYSIS_SYSTEM_PROMPT), HumanMessage(content=user_prompt)]
)
)

# Require description in PR body.
# Increase confidence if we detect short titles in PR history (indicator of missing context)
desc_reasoning = f"Repository analysis for {state.repository_full_name}."
if pr_issues.get("short_titles", 0) > 0:
desc_reasoning += f" Detected {pr_issues['short_titles']} PRs with very short titles (likely missing context)."
else:
desc_reasoning += " Encourages context for reviewers; lightweight default."

desc_confidence = 0.68
if pr_issues.get("short_titles", 0) > 0:
desc_confidence = 0.80

recommendations.append(
RuleRecommendation(
yaml_rule="""description: "Ensure PRs include context"
enabled: true
severity: low
event_types:
- pull_request
parameters:
min_description_length: 50
""".strip(),
confidence=desc_confidence,
reasoning=desc_reasoning,
strategy_used="static",
# The response is already a Pydantic object (RecommendationsList or similar)
# We extract the list of recommendations
valid_recs = response.recommendations if hasattr(response, "recommendations") else []

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/agents/repository_analysis_agent/nodes.py | head -200

Repository: warestack/watchflow

Length of output: 8012


🏁 Script executed:

cat src/agents/repository_analysis_agent/models.py

Repository: warestack/watchflow

Length of output: 1664


Structured output schema needs BaseModel instead of AnalysisState to prevent validation failures.

RecommendationsList inherits AnalysisState, which has a required repo_full_name: str field with no default. LLM responses will omit this field and other metadata, causing Pydantic validation to fail and triggering the exception handler's fallback on every successful LLM call. Use a minimal BaseModel with only the recommendations field.

🛠️ Corrected structured-output model
+from pydantic import BaseModel, Field, ConfigDict
 from langchain_core.messages import HumanMessage, SystemMessage
@@
-        class RecommendationsList(AnalysisState):
-            # We strictly want the list, reusing the model definition
-            recommendations: list[RuleRecommendation]
+        class RecommendationsList(BaseModel):
+            recommendations: list[RuleRecommendation] = Field(default_factory=list)
+            model_config = ConfigDict(extra="ignore")
@@
-        valid_recs = response.recommendations if hasattr(response, "recommendations") else []
+        if isinstance(response, RecommendationsList):
+            valid_recs = response.recommendations
+        else:
+            raise ValueError("Structured output missing recommendations")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 2. Initialize LLM
# We use the factory to respect project settings (provider, temperature)
try:
llm = get_chat_model(agent="repository_analysis")
# Build YAML rule with proper indentation
# parameters: is at column 0, source_patterns: at column 2, list items at column 4
source_patterns_yaml = "\n".join(f' - "{pattern}"' for pattern in source_patterns)
test_patterns_yaml = "\n".join(f' - "{pattern}"' for pattern in test_patterns)
# 3. Structured Output Enforcement
# We define a wrapper model to ensure we get a list of recommendations
# Note: LangChain's with_structured_output is preferred over raw JSON parsing
class RecommendationsList(AnalysisState):
# We strictly want the list, reusing the model definition
recommendations: list[RuleRecommendation]
yaml_content = f"""description: "Require tests when code changes"
enabled: true
severity: medium
event_types:
- pull_request
parameters:
source_patterns:
{source_patterns_yaml}
test_patterns:
{test_patterns_yaml}
"""
structured_llm = llm.with_structured_output(RecommendationsList)
confidence = 0.74
if pr_issues.get("missing_tests", 0) > 0:
confidence = 0.85
if state.contributing_analysis.content and state.contributing_analysis.requires_tests:
confidence = min(0.95, confidence + 0.1)
recommendations.append(
RuleRecommendation(
yaml_rule=yaml_content.strip(),
confidence=confidence,
reasoning=test_reasoning,
strategy_used="hybrid",
response = await structured_llm.ainvoke(
[SystemMessage(content=REPOSITORY_ANALYSIS_SYSTEM_PROMPT), HumanMessage(content=user_prompt)]
)
)
# Require description in PR body.
# Increase confidence if we detect short titles in PR history (indicator of missing context)
desc_reasoning = f"Repository analysis for {state.repository_full_name}."
if pr_issues.get("short_titles", 0) > 0:
desc_reasoning += f" Detected {pr_issues['short_titles']} PRs with very short titles (likely missing context)."
else:
desc_reasoning += " Encourages context for reviewers; lightweight default."
desc_confidence = 0.68
if pr_issues.get("short_titles", 0) > 0:
desc_confidence = 0.80
recommendations.append(
RuleRecommendation(
yaml_rule="""description: "Ensure PRs include context"
enabled: true
severity: low
event_types:
- pull_request
parameters:
min_description_length: 50
""".strip(),
confidence=desc_confidence,
reasoning=desc_reasoning,
strategy_used="static",
# The response is already a Pydantic object (RecommendationsList or similar)
# We extract the list of recommendations
valid_recs = response.recommendations if hasattr(response, "recommendations") else []
# 2. Initialize LLM
# We use the factory to respect project settings (provider, temperature)
try:
llm = get_chat_model(agent="repository_analysis")
# 3. Structured Output Enforcement
# We define a wrapper model to ensure we get a list of recommendations
# Note: LangChain's with_structured_output is preferred over raw JSON parsing
class RecommendationsList(BaseModel):
recommendations: list[RuleRecommendation] = Field(default_factory=list)
model_config = ConfigDict(extra="ignore")
structured_llm = llm.with_structured_output(RecommendationsList)
response = await structured_llm.ainvoke(
[SystemMessage(content=REPOSITORY_ANALYSIS_SYSTEM_PROMPT), HumanMessage(content=user_prompt)]
)
# The response is already a Pydantic object (RecommendationsList or similar)
# We extract the list of recommendations
if isinstance(response, RecommendationsList):
valid_recs = response.recommendations
else:
raise ValueError("Structured output missing recommendations")
🤖 Prompt for AI Agents
In `@src/agents/repository_analysis_agent/nodes.py` around lines 130 - 151, The
structured output model RecommendationsList currently inherits AnalysisState
which requires fields like repo_full_name and causes Pydantic validation
failures; change RecommendationsList to inherit from pydantic.BaseModel (or
BaseModel alias used in the project) and define only recommendations:
list[RuleRecommendation] so llm.with_structured_output(RecommendationsList)
returns a Pydantic object that matches the LLM payload; update any imports if
necessary and keep the rest of the flow (structured_llm,
response.recommendations) unchanged.

Comment on lines +24 to +41
async def get_current_user_optional(request: Request) -> User | None:
"""
Auth check—don't fail if missing. Critical for public repo support (Phase 1).
"""
auth_header = request.headers.get("Authorization")

if not auth_header:
return None

try:
# Token extraction—fragile if header format changes.
scheme, token = auth_header.split()
if scheme.lower() != "bearer":
return None

# TODO: Wire to real IdP (Supabase/Auth0). For now, fake user if token present. WARNING: Must verify signature in prod.
return User(id=123, username="authenticated_user", email="user@example.com", github_token=token)
except Exception as e:
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Any Bearer token currently authenticates a user.

This is an auth‑bypass for any endpoint using get_current_user and allows arbitrary access to private flows. Please validate tokens (IdP/JWT signature) or gate this behavior behind an explicit dev‑only flag and return None otherwise.

🤖 Prompt for AI Agents
In `@src/api/dependencies.py` around lines 24 - 41, The get_current_user_optional
function currently treats any Bearer token as authenticated; change it so that
when a Bearer token is present it validates the token signature with the
configured IdP/JWT verification (or calls your existing token validation helper)
and only returns a User when validation succeeds, otherwise return None;
alternatively, gate the existing “fake user” behavior behind an explicit
dev-only flag (e.g., a CONFIG_DEV_AUTH or ENABLE_FAKE_AUTH env var) and ensure
production defaults to real validation and None on failure; update error
handling in get_current_user_optional to catch validation errors, log them, and
return None instead of authenticating every token.

Comment on lines +20 to +27
async def rate_limiter(request: Request, user: User | None = Depends(lambda: None)):
now = time.time()
if user and user.email:
key = f"user:{user.email}"
limit = AUTH_LIMIT
else:
key = f"ip:{request.client.host}"
limit = ANON_LIMIT
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential AttributeError when request.client is None.

request.client can be None when the app runs behind certain proxies or during testing. Accessing .host directly will raise an AttributeError.

🐛 Proposed fix
     if user and user.email:
         key = f"user:{user.email}"
         limit = AUTH_LIMIT
     else:
-        key = f"ip:{request.client.host}"
+        client_host = request.client.host if request.client else "unknown"
+        key = f"ip:{client_host}"
         limit = ANON_LIMIT
🤖 Prompt for AI Agents
In `@src/api/rate_limit.py` around lines 20 - 27, The rate_limiter function can
raise AttributeError when request.client is None; update rate_limiter to safely
derive the IP by checking request.client first (use request.client.host when
request.client is not None) and otherwise fall back to a header or placeholder
(e.g., request.headers.get("x-forwarded-for") or "unknown") before building the
key variable; ensure the key assignment (key = f"ip:{...}") and limit selection
logic remain unchanged and still use AUTH_LIMIT/ANON_LIMIT.

Comment on lines +20 to +24
async def rate_limiter(request: Request, user: User | None = Depends(lambda: None)):
now = time.time()
if user and user.email:
key = f"user:{user.email}"
limit = AUTH_LIMIT
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how rate_limiter is used in recommendations.py
rg -n -A10 'rate_limiter' src/api/recommendations.py

Repository: warestack/watchflow

Length of output: 805


🏁 Script executed:

#!/bin/bash
# Read the rate_limiter function implementation
cat -n src/api/rate_limit.py

Repository: warestack/watchflow

Length of output: 1582


Fix user dependency injection in rate_limiter.

The Depends(lambda: None) default means rate_limiter always receives None for user, regardless of whether the endpoint has authenticated the request. FastAPI does not automatically share dependencies between the route's dependencies list and the endpoint parameters. As a result, all requests are treated as anonymous and limited to 5/hour, contradicting the docstring's stated behavior. Change line 20 to use Depends(get_current_user_optional) instead:

Suggested fix
from src.core.auth import get_current_user_optional

async def rate_limiter(request: Request, user: User | None = Depends(get_current_user_optional)):
🤖 Prompt for AI Agents
In `@src/api/rate_limit.py` around lines 20 - 24, The rate_limiter dependency is
using Depends(lambda: None) so it always gets None; replace that with the
optional auth dependency so authenticated users are recognized: import and use
get_current_user_optional and change the parameter in async def
rate_limiter(request: Request, user: User | None =
Depends(get_current_user_optional)) so the function receives the actual user
when present (refer to rate_limiter and get_current_user_optional).

Comment on lines 82 to +92
@router.post(
"/v1/rules/recommend/proceed-with-pr",
response_model=ProceedWithPullRequestResponse,
summary="Create a PR with generated Watchflow rules",
description="Creates a branch, commits rules.yaml, and opens a PR using either installation or user token.",
"/recommend",
response_model=AnalysisResponse,
status_code=status.HTTP_200_OK,
summary="Analyze Repository for Governance Rules",
description="Analyzes a public or private repository using AI agents. Public repos allow anonymous access.",
dependencies=[Depends(rate_limiter)],
)
async def proceed_with_pr(request: ProceedWithPullRequestRequest) -> ProceedWithPullRequestResponse:
if not request.repository_full_name:
raise HTTPException(status_code=400, detail="repository_full_name or repository_url is required")
if not request.installation_id and not request.user_token:
raise HTTPException(status_code=400, detail="installation_id or user_token is required")

repo = request.repository_full_name
auth_ctx = {"installation_id": request.installation_id, "user_token": request.user_token}

repo_data = await github_client.get_repository(repo, **auth_ctx)
base_branch = request.base_branch or (repo_data or {}).get("default_branch", "main")

base_sha = await github_client.get_git_ref_sha(repo, base_branch, **auth_ctx)
if not base_sha:
log_structured(
logger,
"base_branch_resolution_failed",
operation="proceed_with_pr",
subject_ids=[repo],
base_branch=base_branch,
error="Unable to resolve base branch SHA",
)
raise HTTPException(status_code=400, detail=f"Unable to resolve base branch '{base_branch}'")

# Check if branch already exists
existing_branch_sha = await github_client.get_git_ref_sha(repo, request.branch_name, **auth_ctx)
if existing_branch_sha:
# Branch exists - use it
log_structured(
logger,
"branch_already_exists",
operation="proceed_with_pr",
subject_ids=[repo],
branch=request.branch_name,
existing_sha=existing_branch_sha,
)
# Verify the branch points to the correct base
if existing_branch_sha != base_sha:
log_structured(
logger,
"branch_sha_mismatch",
operation="proceed_with_pr",
subject_ids=[repo],
branch=request.branch_name,
existing_sha=existing_branch_sha,
expected_sha=base_sha,
warning="Branch exists but points to different SHA than base branch",
)
else:
# Create new branch
created_ref = await github_client.create_git_ref(repo, request.branch_name, base_sha, **auth_ctx)
if not created_ref:
log_structured(
logger,
"branch_creation_failed",
operation="proceed_with_pr",
subject_ids=[repo],
branch=request.branch_name,
base_branch=base_branch,
base_sha=base_sha,
error="Failed to create branch - check logs for GitHub API error details",
)
raise HTTPException(
status_code=400,
detail=(
f"Failed to create branch '{request.branch_name}' from '{base_branch}'. "
"The branch may already exist or you may not have permission to create branches."
),
)
log_structured(
logger,
"branch_created",
operation="proceed_with_pr",
subject_ids=[repo],
branch=request.branch_name,
base_branch=base_branch,
new_sha=created_ref.get("object", {}).get("sha"),
)

file_result = await github_client.create_or_update_file(
repo_full_name=repo,
path=request.file_path,
content=request.rules_yaml,
message=request.commit_message,
branch=request.branch_name,
**auth_ctx,
)
if not file_result:
log_structured(
logger,
"file_creation_failed",
operation="proceed_with_pr",
subject_ids=[repo],
branch=request.branch_name,
file_path=request.file_path,
error="Failed to create or update file - check logs for GitHub API error details",
)
raise HTTPException(
status_code=400,
detail=(
f"Failed to create or update file '{request.file_path}' on branch '{request.branch_name}'. "
"Check server logs for detailed error information."
),
)

commit_sha = (file_result.get("commit") or {}).get("sha")
log_structured(
logger,
"file_created",
operation="proceed_with_pr",
subject_ids=[repo],
branch=request.branch_name,
file_path=request.file_path,
commit_sha=commit_sha,
)
async def recommend_rules(
request: Request, payload: AnalyzeRepoRequest, user: User | None = Depends(get_current_user_optional)
) -> AnalysisResponse:
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find rate_limiter definition and usage
rg -n "def rate_limiter" -C3

Repository: warestack/watchflow

Length of output: 182


🏁 Script executed:

#!/bin/bash
# Find dependencies with rate_limiter
rg -n "Depends\(rate_limiter\)" -C2

Repository: warestack/watchflow

Length of output: 451


🏁 Script executed:

#!/bin/bash
# Find get_current_user_optional definition and usage
rg -n "def get_current_user_optional" -C2

Repository: warestack/watchflow

Length of output: 359


🏁 Script executed:

#!/bin/bash
# Check rate_limit.py file structure
fd -t f "rate_limit" --extension py

Repository: warestack/watchflow

Length of output: 84


🏁 Script executed:

#!/bin/bash
# Read rate_limit.py to see current implementation
cat -n src/api/rate_limit.py

Repository: warestack/watchflow

Length of output: 1582


🏁 Script executed:

#!/bin/bash
# Read dependencies.py to see get_current_user_optional
cat -n src/api/dependencies.py

Repository: warestack/watchflow

Length of output: 2193


🏁 Script executed:

#!/bin/bash
# Search for rate_limiter function definition
rg -n "^(async )?def rate_limiter" -A 5

Repository: warestack/watchflow

Length of output: 419


Authenticated requests are rate-limited as anonymous.

The route uses Depends(rate_limiter), but the limiter currently depends on lambda: None, which always returns None. This means the if user and user.email: check will never be true, and all requests get the anonymous limit (5 req/hr) instead of the authenticated limit (100 req/hr).

Change the limiter dependency to:

🛠️ Fix (rate_limit.py, line 20)
-async def rate_limiter(request: Request, user: User | None = Depends(lambda: None)):
+async def rate_limiter(request: Request, user: User | None = Depends(get_current_user_optional)):

Also add the import at the top of rate_limit.py:

from src.api.dependencies import get_current_user_optional
🤖 Prompt for AI Agents
In `@src/api/recommendations.py` around lines 82 - 92, The rate limiter dependency
currently uses a lambda returning None which prevents authenticated detection;
update the limiter in rate_limit.py to depend on get_current_user_optional
instead of the lambda (import get_current_user_optional from
src.api.dependencies at the top of rate_limit.py) so rate_limiter can check the
passed User (user and user.email) and apply the authenticated (100 req/hr) vs
anonymous (5 req/hr) limits; no change needed to recommend_rules other than
ensuring it still uses Depends(rate_limiter).

Comment on lines +40 to +48
async with httpx.AsyncClient() as client:
response = await client.get(api_url)

if response.status_code == 404:
raise GitHubResourceNotFoundError(f"Repo {owner}/{repo} not found")
if response.status_code == 403 and "rate limit" in response.text.lower():
raise GitHubRateLimitError("GitHub API rate limit exceeded")

response.raise_for_status()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rate limit detection misses HTTP 429 status code.

GitHub returns both 403 with rate-limit text and 429 Too Many Requests for rate limiting. The current check only handles the former.

🐛 Proposed fix
         async with httpx.AsyncClient() as client:
             response = await client.get(api_url)

             if response.status_code == 404:
                 raise GitHubResourceNotFoundError(f"Repo {owner}/{repo} not found")
-            if response.status_code == 403 and "rate limit" in response.text.lower():
+            if response.status_code == 429 or (
+                response.status_code == 403 and "rate limit" in response.text.lower()
+            ):
                 raise GitHubRateLimitError("GitHub API rate limit exceeded")

             response.raise_for_status()
             return response.json()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async with httpx.AsyncClient() as client:
response = await client.get(api_url)
if response.status_code == 404:
raise GitHubResourceNotFoundError(f"Repo {owner}/{repo} not found")
if response.status_code == 403 and "rate limit" in response.text.lower():
raise GitHubRateLimitError("GitHub API rate limit exceeded")
response.raise_for_status()
async with httpx.AsyncClient() as client:
response = await client.get(api_url)
if response.status_code == 404:
raise GitHubResourceNotFoundError(f"Repo {owner}/{repo} not found")
if response.status_code == 429 or (
response.status_code == 403 and "rate limit" in response.text.lower()
):
raise GitHubRateLimitError("GitHub API rate limit exceeded")
response.raise_for_status()
🤖 Prompt for AI Agents
In `@src/integrations/github/service.py` around lines 40 - 48, The rate-limit
handling in the async HTTP fetch needs to also treat HTTP 429 as a rate-limit
error: inside the block using httpx.AsyncClient() where response is checked (the
existing status_code checks and raises for GitHubResourceNotFoundError and
GitHubRateLimitError), add a condition that treats response.status_code == 429
(or response.status_code == 403 with "rate limit" in response.text) as a
GitHubRateLimitError and raise GitHubRateLimitError accordingly; ensure this
check occurs before calling response.raise_for_status() so the custom error is
raised for 429/403-rate-limit cases.

Comment on lines +66 to +73
async with httpx.AsyncClient() as client:
for filepath in files_to_check:
# Tricky: Public repos can be read without auth, Private need auth
# We use the 'contents' API
check_url = f"{self.BASE_URL}/repos/{owner}/{repo}/contents/{filepath}"
resp = await client.get(check_url, headers=headers)
if resp.status_code == 200:
found_files.append(filepath)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent failure masks errors beyond "file not found".

All non-200 responses are treated as "file doesn't exist," including rate limits, server errors, and auth failures on private repos. This could lead to misleading recommendations.

🐛 Proposed fix: Log unexpected responses and handle rate limits
         async with httpx.AsyncClient() as client:
             for filepath in files_to_check:
                 # Tricky: Public repos can be read without auth, Private need auth
                 # We use the 'contents' API
                 check_url = f"{self.BASE_URL}/repos/{owner}/{repo}/contents/{filepath}"
                 resp = await client.get(check_url, headers=headers)
                 if resp.status_code == 200:
                     found_files.append(filepath)
+                elif resp.status_code == 429 or (
+                    resp.status_code == 403 and "rate limit" in resp.text.lower()
+                ):
+                    raise GitHubRateLimitError("GitHub API rate limit exceeded")
+                elif resp.status_code not in (404, 403):
+                    # 404 = file not found, 403 = private repo without auth
+                    logger.warning(f"Unexpected response {resp.status_code} for {filepath}")
🤖 Prompt for AI Agents
In `@src/integrations/github/service.py` around lines 66 - 73, The loop that
checks files (iterating files_to_check and calling client.get on
f"{self.BASE_URL}/repos/{owner}/{repo}/contents/{filepath}") treats any non-200
as "not found" and thus silently masks rate limits, auth failures, and server
errors; update the logic inside that async with block to inspect
resp.status_code: if 404 add to found_files logic as before, if 401/403/429/5xx
log an informative message via the service logger (include resp.status_code,
resp.text or headers) and raise or return a distinct error so callers can handle
auth or rate-limit cases, and if 429 include retry-after handling (sleep/retry
or surface the rate-limit error); keep existing behavior only for genuine 404s.

Comment on lines +107 to +114
def _parse_url(self, url: str) -> tuple[str, str]:
"""
Extracts owner and repo from https://github.com/owner/repo
"""
clean_url = str(url).rstrip("/")
parts = clean_url.split("/")
if len(parts) < 2:
raise ValueError("Invalid GitHub URL")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

URL parsing is fragile and accepts invalid inputs.

The validation len(parts) < 2 passes for malformed URLs like "https://github.com" (parts: ['https:', '', 'github.com']), returning invalid owner/repo values.

🐛 Proposed fix: Validate extracted values
     def _parse_url(self, url: str) -> tuple[str, str]:
         """
         Extracts owner and repo from https://github.com/owner/repo
         """
         clean_url = str(url).rstrip("/")
         parts = clean_url.split("/")
-        if len(parts) < 2:
+        if len(parts) < 2 or not parts[-2] or not parts[-1]:
             raise ValueError("Invalid GitHub URL")
+        owner, repo = parts[-2], parts[-1]
+        # Reject URL fragments that aren't valid GitHub identifiers
+        if owner in ("github.com", "") or repo == "":
+            raise ValueError("Invalid GitHub URL: could not extract owner/repo")
-        return parts[-2], parts[-1]
+        return owner, repo
🤖 Prompt for AI Agents
In `@src/integrations/github/service.py` around lines 107 - 114, The _parse_url
function currently splits the raw string and can return invalid owner/repo for
inputs like "https://github.com"; update _parse_url to robustly extract and
validate owner and repo by parsing the URL path (e.g. using
urllib.parse.urlparse) or by supporting plain "owner/repo" input, split the path
into segments, ensure there are at least two non-empty segments (owner and
repo), and raise a ValueError with a clear message if they are missing or empty;
adjust the return to explicitly return (owner, repo) only after validation.

Comment on lines +88 to 97
# We explicitly allow all origins ("*") to prevent the browser from blocking requests
# from your local file system or different localhost ports.

app.add_middleware(
CORSMiddleware,
allow_origins=config.cors.origins,
allow_origins=["*"], # Explicitly allow all for development
allow_credentials=True,
allow_methods=["*"],
allow_headers=config.cors.headers,
allow_headers=["*"],
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the full src/main.py file to understand the context
wc -l src/main.py

Repository: warestack/watchflow

Length of output: 78


🏁 Script executed:

# Read the src/main.py file
cat -n src/main.py

Repository: warestack/watchflow

Length of output: 6387


🏁 Script executed:

# Search for environment-related configurations and CORS-related code
rg -i "environ|cors|allow_credentials|allow_origins" --type py -A 3 -B 3

Repository: warestack/watchflow

Length of output: 33632


🏁 Script executed:

# Check if there are config files or environment setup
fd -t f -e "env" -e "config" -e "yml" -e "yaml" -e "toml" -e "ini"

Repository: warestack/watchflow

Length of output: 510


🏁 Script executed:

# Look for any development/production distinctions
rg -i "development|production|is_prod|debug|env\.get" --type py

Repository: warestack/watchflow

Length of output: 4977


🏁 Script executed:

# Search for usage of settings.cors or CORSConfig
rg "settings\.cors|from.*cors_config|CORSConfig" --type py -B 2 -A 2

Repository: warestack/watchflow

Length of output: 1249


🏁 Script executed:

# Check if there are any other CORS-related configurations or middleware additions
rg "add_middleware.*CORS|CORSMiddleware" --type py -B 2 -A 5

Repository: warestack/watchflow

Length of output: 749


🏁 Script executed:

# Look at the cors_config.py file directly
cat -n src/core/config/cors_config.py

Repository: warestack/watchflow

Length of output: 324


🏁 Script executed:

# Search for any usage of settings.cors in the application code
rg "settings\.cors|config\.cors" --type py -B 3 -A 3

Repository: warestack/watchflow

Length of output: 515


🏁 Script executed:

# Search for where the Settings class is imported and instantiated
rg "from.*settings import|Settings\(\)" --type py -B 2 -A 2

Repository: warestack/watchflow

Length of output: 272


🏁 Script executed:

# Get full context of settings.py CORS configuration
sed -n '1,150p' src/core/config/settings.py | tail -80

Repository: warestack/watchflow

Length of output: 3099


🏁 Script executed:

# Check if the config is instantiated globally
cat -n src/core/config/__init__.py

Repository: warestack/watchflow

Length of output: 377


🏁 Script executed:

# Search for any usage of config in main.py or app initialization
grep -n "config\|settings\|import.*core" src/main.py

Repository: warestack/watchflow

Length of output: 45


🏁 Script executed:

# Check if config is imported anywhere in main.py or if there's a reference to it
head -30 src/main.py | grep -i "config\|import"

Repository: warestack/watchflow

Length of output: 1229


🏁 Script executed:

# Search the entire codebase for where the hardcoded CORS should be replaced
rg "allow_origins.*\[" --type py | head -20

Repository: warestack/watchflow

Length of output: 139


🏁 Script executed:

# Check if there's a main() function or if config gets initialized elsewhere
cat src/main.py | grep -A 5 "if __name__"

Repository: warestack/watchflow

Length of output: 45


🏁 Script executed:

# Check if there are any environment variables set in docker-compose or helm charts
grep -i "cors\|environment\|debug" docker-compose.yaml helm-chart/values.yaml helm-chart/templates/deployment.yaml

Repository: warestack/watchflow

Length of output: 99


🏁 Script executed:

# Look for any production/staging configuration references
rg "production|staging|deploy" --type yaml -B 2 -A 2 | head -40

Repository: warestack/watchflow

Length of output: 1581


🏁 Script executed:

# Check README or deployment documentation
find . -name "README*" -o -name "DEPLOY*" -o -name "*.md" | xargs grep -l -i "cors\|environment\|config" 2>/dev/null | head -5

Repository: warestack/watchflow

Length of output: 152


🏁 Script executed:

# Final verification: confirm the default origins are safe (not wildcard)
cat -n src/core/config/settings.py | grep -A 12 "CORS_ORIGINS"

Repository: warestack/watchflow

Length of output: 669


Import and use the environment-aware CORS configuration instead of hardcoding values.

The hardcoded allow_origins=["*"] with allow_credentials=True violates the CORS specification and will be active in production. A proper, environment-aware configuration system already exists in src/core/config/settings.py with safe defaults (localhost + warestack domains only), but it's being bypassed. Import config and use config.cors.origins and config.cors.headers. Additionally, set allow_credentials=False by default or make it conditional on the environment (disable for production). Credentials should only be enabled if explicitly needed.

🤖 Prompt for AI Agents
In `@src/main.py` around lines 88 - 97, Replace the hardcoded CORS settings in the
app.add_middleware(CORSMiddleware, ...) call by importing and using the
environment-aware settings from config (e.g., config.cors.origins and
config.cors.headers) instead of ["*"], and set allow_credentials to False by
default or conditionally (e.g., only True in non-production/dev) so credentials
are not enabled in production; update the CORSMiddleware call to reference
config.cors.origins, config.cors.headers and a config-driven allow_credentials
flag.

Comment on lines +20 to +34
@pytest.fixture(autouse=True)
def mock_settings():
"""Forces the test environment to use dummy values."""
with pytest.helpers.mock_env(
{
"APP_CLIENT_ID_GITHUB": "mock-client-id",
"APP_CLIENT_SECRET_GITHUB": "mock-client-secret",
"WEBHOOK_SECRET_GITHUB": "mock-webhook-secret",
"PRIVATE_KEY_BASE64_GITHUB": "bW9jay1rZXk=", # "mock-key" in base64
"AI_PROVIDER": "openai",
"OPENAI_API_KEY": "sk-mock-key",
"ENVIRONMENT": "test",
}
):
yield
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: pytest.helpers does not exist — this will fail at test collection.

The mock_settings fixture references pytest.helpers.mock_env(), but pytest has no built-in helpers attribute. The Helpers class is defined below but not registered as a pytest plugin.

🐛 Proposed fix

Use unittest.mock.patch.dict directly instead of the undefined pytest.helpers:

+from unittest.mock import patch
+
+
 # 2. Mock Environment Variables (Security First)
 # We do this BEFORE importing app code to ensure no real secrets are read
 `@pytest.fixture`(autouse=True)
 def mock_settings():
     """Forces the test environment to use dummy values."""
-    with pytest.helpers.mock_env(
-        {
+    with patch.dict(
+        os.environ,
+        {
             "APP_CLIENT_ID_GITHUB": "mock-client-id",
             "APP_CLIENT_SECRET_GITHUB": "mock-client-secret",
             "WEBHOOK_SECRET_GITHUB": "mock-webhook-secret",
             "PRIVATE_KEY_BASE64_GITHUB": "bW9jay1rZXk=",  # "mock-key" in base64
             "AI_PROVIDER": "openai",
             "OPENAI_API_KEY": "sk-mock-key",
             "ENVIRONMENT": "test",
         }
     ):
         yield

If you want to keep the Helpers class for reuse, consider using the pytest-helpers-namespace plugin or simply import the helper directly.

Also applies to: 38-41

🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 28-28: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In `@tests/conftest.py` around lines 20 - 34, The mock_settings fixture currently
calls the non-existent pytest.helpers.mock_env and will fail at collection;
replace that usage by applying unittest.mock.patch.dict directly in the fixture
(or register the local Helpers class as a pytest plugin via
pytest-helpers-namespace if you prefer reuse). Specifically, update the
mock_settings fixture to use unittest.mock.patch.dict(os.environ, {...},
clear=False) as a context manager around yield, and remove or re-register
references to pytest.helpers.mock_env and any direct calls to pytest.helpers in
tests; if keeping the Helpers class, ensure it is exposed to pytest (e.g., via
pytest_plugins or pytest-helpers-namespace) so code referencing
pytest.helpers.mock_env resolves.

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