Skip to content

Docs support config view#124

Merged
touale merged 6 commits intomasterfrom
docs-support-config-view
Apr 17, 2026
Merged

Docs support config view#124
touale merged 6 commits intomasterfrom
docs-support-config-view

Conversation

@touale
Copy link
Copy Markdown
Owner

@touale touale commented Apr 16, 2026

Summary by CodeRabbit

  • New Features

    • Protected /docs/plugin-config page to view plugin config with sensitive-data masking.
    • /docs/plugin-release endpoint to check for plugin updates and show latest version status.
    • Repository version checks for GitHub/GitLab to detect releases and validate access.
  • Bug Fixes / Security

    • OAuth upgraded to session-backed tokens for stricter authenticated access.
  • Documentation

    • README tagline updated to emphasize scalable Python services with FastAPI + Ray.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

Warning

Rate limit exceeded

@touale has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 28 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 49 minutes and 28 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 38ee5aa0-0a45-41b4-8a51-0db2733c354b

📥 Commits

Reviewing files that changed from the base of the PR and between 411c241 and 36d58fe.

📒 Files selected for processing (2)
  • src/framex/driver/auth.py
  • tests/driver/test_auth.py

Walkthrough

Adds a repository versioning and access subsystem (GitHub/GitLab providers), session-backed OAuth/JWT authentication, protected plugin documentation endpoints, extended configuration for repository/docs, new utility modules (cache, config docs, common helpers), and expanded tests covering auth, docs, and repository behaviors.

Changes

Cohort / File(s) Summary
Repository System
src/framex/repository/__init__.py, src/framex/repository/versioning.py, src/framex/repository/providers/base.py, src/framex/repository/providers/github.py, src/framex/repository/providers/gitlab.py, src/framex/repository/providers/__init__.py
New provider framework and implementations for resolving latest versions and access checks (GitHub, GitLab). Adds provider selection, cached lookup, privacy detection, and token-based access APIs. Review provider matching, HTTP helpers, and URL parsing carefully.
Configuration
src/framex/config.py
Introduces repository auth/config models and DocsConfig; adds provider to OauthConfig and exposes settings.docs and settings.repository. Validate defaults and header-building logic.
Authentication & Sessions
src/framex/driver/auth.py
Switches to session-backed auth with in-memory sessions, session lifecycle management, updated JWT creation/decoding, and changed OAuth callback payload. Check session creation, expiry, and token contents.
Documentation Endpoints & App Wiring
src/framex/driver/application.py
Adds protected routes /docs/plugin-config and /docs/plugin-release integrated with plugin loading, repository privacy/access checks, embedded-config collection, and HTML rendering. Confirm auth gating and repo-access short-circuiting.
Plugin & Docs Integration
src/framex/plugin/on.py, src/framex/plugins/proxy/__init__.py, src/framex/utils/docs.py
Plugin descriptions now include plugin name; Swagger UI augmented with client-side logic to fetch release/config endpoints. Review changed description signatures and client JS behavior.
Utilities
src/framex/utils/__init__.py, src/framex/utils/cache.py, src/framex/utils/common.py, src/framex/utils/config_docs.py, src/framex/utils/docs.py
New utilities: cache encode/decode with XOR+zlib+base64, common helpers (stream events, path/module helpers, masking), and config-docs HTML builder with embedded-file discovery and sensitive-data masking. Pay attention to dynamic imports and masking edge cases.
Exports / Package Init
src/framex/repository/providers/__init__.py, src/framex/utils/__init__.py
New package-level re-exports to centralize provider and utils APIs. Ensure __all__ contents match public symbols.
Tests & Mocks
tests/api/test_proxy.py, tests/driver/test_auth.py, tests/test_config.py, tests/test_utils.py, tests/conftest.py, tests/mock.py
Extensive test additions/changes: OAuth session helpers, docs endpoint tests (auth + repo access), repository/versioning tests, cache/tests, and mock repository responses. Verify test patches for HTTP fetch and repository fetch stubbing.
Docs
README.md
Updated top tagline to reference “scalable Python services with plugins” and “FastAPI + Ray”.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant FastAPI as App /docs/plugin-config
    participant Auth as auth module
    participant Repo as repository.versioning
    participant Plugin as plugin loader
    participant Docs as config_docs builder

    Client->>FastAPI: GET /docs/plugin-config?plugin=X (cookie)
    FastAPI->>Auth: get_auth_payload(request)
    Auth-->>FastAPI: user payload or None
    alt OAuth enabled and unauthenticated
        FastAPI-->>Client: 403
    else
        FastAPI->>Plugin: get_plugin(plugin)
        Plugin-->>FastAPI: metadata/config or None
        FastAPI->>Repo: is_private_repository(repo_url)
        Repo-->>FastAPI: True/False/None
        alt private repo
            FastAPI->>Repo: can_access_repository(repo_url, provider, token)
            Repo-->>FastAPI: True/False/None
            alt access denied
                FastAPI-->>Client: 403
            end
        end
        FastAPI->>Docs: collect_embedded_config_files(config, whitelist)
        Docs-->>FastAPI: embedded files
        FastAPI->>Docs: build_plugin_config_html(config, embedded_files)
        Docs-->>FastAPI: HTMLResponse
        FastAPI-->>Client: 200 HTML
    end
Loading
sequenceDiagram
    participant Client
    participant FastAPI as App /docs/plugin-release
    participant Repo as repository.versioning
    participant Provider as GitHub/GitLab provider

    Client->>FastAPI: GET /docs/plugin-release?plugin=X
    FastAPI->>Repo: get_latest_repository_version(repo_url)
    Repo->>Provider: resolve & get_latest_version(parsed_url)
    Provider->>Provider: fetch_json(api_endpoint, headers)
    Provider-->>Repo: latest_version or None
    Repo-->>FastAPI: latest_version
    FastAPI->>Repo: has_newer_release_version(current, latest)
    Repo-->>FastAPI: boolean
    FastAPI-->>Client: 200 JSON {has_update, latest_version}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
Through repos and tokens I hop with glee,
Guarding configs and versions for thee,
Sessions snug, docs rendered neat,
Plugins and releases all in one seat.
Hooray for code that helps us be free!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Docs support config view' directly describes the main feature added: plugin configuration documentation viewing capability via new /docs/plugin-config endpoint and related infrastructure.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs-support-config-view

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (2)
src/framex/utils/__init__.py (1)

3-3: Typo in re-exported symbol: StreamEnventType.

The symbol StreamEnventType originates from src/framex/utils/common.py and contains a typo ("Envent" instead of "Event"). Consider fixing the class definition to StreamEventType in a follow-up to prevent further propagation through the public API.

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

In `@src/framex/utils/__init__.py` at line 3, The public re-export mistakenly uses
the misspelled symbol StreamEnventType; update the original class/enum
definition in common.py to the correct name StreamEventType and then update the
re-export list in __init__.py to export StreamEventType instead of
StreamEnventType, ensuring all internal references (e.g., imports, type
annotations, and usages) are updated to the new StreamEventType identifier to
avoid breaking imports.
src/framex/repository/versioning.py (1)

20-25: Consider a TTL or explicit invalidation for release lookups.

This cache is process-lifetime, so /docs/plugin-release can keep serving an old latest version indefinitely after an upstream release is published. A short TTL cache or an explicit invalidation hook would make the docs view much less stale.

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

In `@src/framex/repository/versioning.py` around lines 20 - 25, The
get_latest_repository_version function is using functools.lru_cache without
expiration so docs can serve stale release data; replace the process-lifetime
lru_cache with a TTL-backed cache or add an explicit invalidation hook: either
swap the decorator for a TTL cache (e.g., cachetools.TTLCache or an equivalent
cached decorator) around get_latest_repository_version(repo_url) with a short
TTL (minutes) or keep lru_cache but add an exported invalidation function (e.g.,
invalidate_latest_repository_version) that calls
get_latest_repository_version.cache_clear() or a key-targeted clear
implementation so the docs view can refresh after upstream releases. Ensure
references to _get_provider_for_url and provider.get_latest_version(parsed_url)
remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/framex/config.py`:
- Around line 107-115: The current matches method (matches) uses
path.startswith(normalized_prefix) which incorrectly matches sibling segments
(e.g., "/team-alpha" for prefix "/team-a"); update matches to only accept an
empty prefix or exact match or a segment-prefix match by checking path ==
normalized_prefix or path.startswith(normalized_prefix + "/"); keep
normalized_path_prefix and path_prefix logic as-is but reference
normalized_path_prefix in the new checks to ensure correct leading-slash
handling.
- Around line 118-124: The GitLabRepositoryAuthConfig.configured_hosts method
only returns hosts from endpoints so a global token (set on the provider config)
never applies to self-hosted GitLab instances; update configured_hosts in class
GitLabRepositoryAuthConfig to include a wildcard/fallback host (for example an
empty string or "*" entry) when the provider-level token is set (check the
inherited token field on RepositoryProviderAuthConfig) so that the matching
logic in src/framex/repository/providers/gitlab.py will consider the global
token for self-hosted hosts as well.

In `@src/framex/driver/application.py`:
- Around line 173-183: The fallback that reads config from settings.plugins (the
settings.plugins.get(plugin) branch) bypasses repository access checks; update
the handler so that before returning build_plugin_config_html for the
settings-based config you perform the same repository access validation used
when loaded_plugin is present (reuse the same access-check logic/function used
for loaded_plugin), or explicitly gate this path behind the same permission
check and only proceed if the request has access; ensure you still call
collect_embedded_config_files with workspace_root=Path.cwd().resolve() and the
same whitelist, and if access is denied raise
HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Plugin config not
found: {plugin}") as in the other branch so settings-based configs are not
exposed without validation.

In `@src/framex/driver/auth.py`:
- Line 17: The in-memory session store _AUTH_SESSIONS makes auth process-local
and volatile; replace it with a shared persistent store or switch to stateless
tokens: implement a session backend using Redis/DB (e.g., wrap get/set/delete
operations used by the auth functions that reference _AUTH_SESSIONS) and update
all places that read/write _AUTH_SESSIONS to call that backend, or alternatively
encode the complete auth state into the signed token and remove reliance on
_AUTH_SESSIONS; update the functions/methods that currently interact with
_AUTH_SESSIONS (and the related code referenced around lines 46-55) to use the
chosen shared/session-backend API.
- Around line 74-76: The handler currently returns payload when session_id is
missing which lets sessionless JWTs authenticate; change the logic so that if
session_id is not a non-empty str (the check around payload.get("session_id")),
the function rejects the token instead of returning payload—e.g. return an
authentication failure/None or raise the appropriate auth exception so
downstream code treats it as unauthenticated; if you need legacy behavior, add
an explicit compatibility flag to gate accepting sessionless tokens rather than
falling back to returning payload.

In `@src/framex/repository/providers/gitlab.py`:
- Around line 64-74: The current _iter_project_path_candidates uses
GITLAB_RESERVED_PATH_MARKERS to cut off candidate generation early, which
prevents valid repos like "/tree/platform" from being tried; change
_iter_project_path_candidates so it does not treat reserved markers as hard
stops—remove the marker_index-based truncation and set max_length to len(parts)
(or otherwise generate all prefix candidates from len(parts) down to 2),
allowing _resolve_project_path to disambiguate via API probes; keep using
extract_repository_parts(parsed_url) and return the same list comprehension join
logic for candidates.

In `@src/framex/utils/cache.py`:
- Around line 61-74: The current restore_models branch for items with "__type__"
== "dynamic_obj" unsafely imports and instantiates classes from untrusted
payloads; update restore_models to stop dynamic import/instantiation and instead
map allowed (module, class) pairs to safe constructors via an explicit allowlist
(e.g., ALLOWED_DYNAMIC_TYPES) and only call import/getattr or model_validate
when the (item["__module__"], item["__class__"]) tuple is in that allowlist; for
any other values, return the cleaned_data as a plain dict (not SimpleNamespace)
to avoid executing arbitrary code, and ensure cache_decode-related callers
(restore_models) handle these plain dicts correctly.

In `@src/framex/utils/common.py`:
- Around line 17-24: path_to_module_name currently uses rel_path =
path.resolve().relative_to(Path.cwd()) which fails for plugin files outside the
CWD; update path_to_module_name to avoid relative_to(Path.cwd()) and instead
work from the absolute path parts: resolve the path, inspect its parts, detect
if the file is an __init__ and drop the last component, then locate a package
root marker (e.g. "src") in the parts and join the parts after that with dots to
form the module name (if "src" not found, join the parts from the first
directory that should be treated as the package root or use the full dotted path
from the filename without suffix); keep the removeprefix("src.") behavior by
removing a leading "src" segment if present and ensure module_name handles both
module files and package __init__.py correctly in the path_to_module_name
function.
- Around line 32-39: extract_method_params currently returns inspect._empty for
unannotated parameters which breaks downstream validation expecting real types;
update extract_method_params to map param.annotation == inspect._empty to
typing.Any before appending so every tuple uses a concrete type (e.g., change
the value appended in extract_method_params to (param.name, Any) when
param.annotation is inspect._empty), ensure Any is imported from typing if not
already, and keep skipping "self" unchanged.

In `@src/framex/utils/config_docs.py`:
- Around line 230-249: _normalize_display_config_paths currently rewrites any
string-looking path with supported suffixes into an absolute/display path
without checking workspace membership or the whitelist, which leaks blocked
embedded file paths; update _normalize_display_config_paths to apply the same
gating used by collect_embedded_config_files(): verify that the candidate path
is inside the resolved workspace_root and/or present in the allowed/whitelist
before calling _to_display_embedded_config_path (or else leave the original
string untouched), reusing or mirroring collect_embedded_config_files() checks
(and the SUPPORTED_EMBEDDED_CONFIG_SUFFIXES guard) so out-of-workspace or
non-whitelisted files are not converted to display/absolute paths.

---

Nitpick comments:
In `@src/framex/repository/versioning.py`:
- Around line 20-25: The get_latest_repository_version function is using
functools.lru_cache without expiration so docs can serve stale release data;
replace the process-lifetime lru_cache with a TTL-backed cache or add an
explicit invalidation hook: either swap the decorator for a TTL cache (e.g.,
cachetools.TTLCache or an equivalent cached decorator) around
get_latest_repository_version(repo_url) with a short TTL (minutes) or keep
lru_cache but add an exported invalidation function (e.g.,
invalidate_latest_repository_version) that calls
get_latest_repository_version.cache_clear() or a key-targeted clear
implementation so the docs view can refresh after upstream releases. Ensure
references to _get_provider_for_url and provider.get_latest_version(parsed_url)
remain unchanged.

In `@src/framex/utils/__init__.py`:
- Line 3: The public re-export mistakenly uses the misspelled symbol
StreamEnventType; update the original class/enum definition in common.py to the
correct name StreamEventType and then update the re-export list in __init__.py
to export StreamEventType instead of StreamEnventType, ensuring all internal
references (e.g., imports, type annotations, and usages) are updated to the new
StreamEventType identifier to avoid breaking imports.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c56ec41f-e29d-4d83-9bca-d847151dee33

📥 Commits

Reviewing files that changed from the base of the PR and between fc18a6f and b9499ac.

📒 Files selected for processing (23)
  • README.md
  • src/framex/config.py
  • src/framex/driver/application.py
  • src/framex/driver/auth.py
  • src/framex/plugin/on.py
  • src/framex/plugins/proxy/__init__.py
  • src/framex/repository/__init__.py
  • src/framex/repository/providers/__init__.py
  • src/framex/repository/providers/base.py
  • src/framex/repository/providers/github.py
  • src/framex/repository/providers/gitlab.py
  • src/framex/repository/versioning.py
  • src/framex/utils/__init__.py
  • src/framex/utils/cache.py
  • src/framex/utils/common.py
  • src/framex/utils/config_docs.py
  • src/framex/utils/docs.py
  • tests/api/test_proxy.py
  • tests/conftest.py
  • tests/driver/test_auth.py
  • tests/mock.py
  • tests/test_config.py
  • tests/test_utils.py

Comment thread src/framex/config.py
Comment thread src/framex/config.py
Comment on lines +118 to +124
class GitLabRepositoryAuthConfig(RepositoryProviderAuthConfig):
token_header: str = "PRIVATE-TOKEN" # noqa
token_scheme: str = ""
endpoints: list[GitLabRepositoryAuthEndpointConfig] = Field(default_factory=list)

def configured_hosts(self) -> set[str]:
return {endpoint.host.lower() for endpoint in self.endpoints}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Global GitLab auth cannot target a self-hosted instance by itself.

configured_hosts() only returns hosts from endpoints, and src/framex/repository/providers/gitlab.py only matches self-hosted GitLab URLs when the host appears there. If a deployment sets only the global repository.auth.gitlab.token, version/access checks for gitlab.internal.example never run.

One way to model the missing host mapping
 class GitLabRepositoryAuthConfig(RepositoryProviderAuthConfig):
+    hosts: list[str] = Field(default_factory=list)
     token_header: str = "PRIVATE-TOKEN"  # noqa
     token_scheme: str = ""
     endpoints: list[GitLabRepositoryAuthEndpointConfig] = Field(default_factory=list)

     def configured_hosts(self) -> set[str]:
-        return {endpoint.host.lower() for endpoint in self.endpoints}
+        return {host.lower() for host in self.hosts} | {endpoint.host.lower() for endpoint in self.endpoints}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/config.py` around lines 118 - 124, The
GitLabRepositoryAuthConfig.configured_hosts method only returns hosts from
endpoints so a global token (set on the provider config) never applies to
self-hosted GitLab instances; update configured_hosts in class
GitLabRepositoryAuthConfig to include a wildcard/fallback host (for example an
empty string or "*" entry) when the provider-level token is set (check the
inherited token field on RepositoryProviderAuthConfig) so that the matching
logic in src/framex/repository/providers/gitlab.py will consider the global
token for self-hosted hosts as well.

Comment thread src/framex/driver/application.py Outdated
Comment thread src/framex/driver/auth.py

api_key_header = APIKeyHeader(name="Authorization", auto_error=False)
SESSION_LIFETIME = timedelta(hours=24)
_AUTH_SESSIONS: dict[str, dict[str, Any]] = {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This session store only works on one process.

_AUTH_SESSIONS lives in module memory, so a login created on worker A is invisible to worker B, and every restart drops all active sessions. That will make docs auth flaky as soon as this runs with multiple workers/instances. Please move the session state to shared storage (for example Redis/DB), or keep the full auth state in the signed token if stateless auth is the goal.

Also applies to: 46-55

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

In `@src/framex/driver/auth.py` at line 17, The in-memory session store
_AUTH_SESSIONS makes auth process-local and volatile; replace it with a shared
persistent store or switch to stateless tokens: implement a session backend
using Redis/DB (e.g., wrap get/set/delete operations used by the auth functions
that reference _AUTH_SESSIONS) and update all places that read/write
_AUTH_SESSIONS to call that backend, or alternatively encode the complete auth
state into the signed token and remove reliance on _AUTH_SESSIONS; update the
functions/methods that currently interact with _AUTH_SESSIONS (and the related
code referenced around lines 46-55) to use the chosen shared/session-backend
API.

Comment thread src/framex/driver/auth.py Outdated
Comment on lines +64 to +74
def _iter_project_path_candidates(self, parsed_url: ParseResult) -> list[str]:
parts = self.extract_repository_parts(parsed_url)
if len(parts) < 2:
return []

marker_index = next((index for index, part in enumerate(parts) if part in GITLAB_RESERVED_PATH_MARKERS), None)
max_length = marker_index if marker_index is not None else len(parts)
if max_length < 2:
return []

return ["/".join(parts[:length]) for length in range(max_length, 1, -1)]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t treat reserved markers as hard stops inside namespace/project names.

This truncates candidates as soon as any segment equals tree, blob, tags, etc. A valid repo like /tree/platform or /group/tags-service can never resolve even though _resolve_project_path() already has the API probe needed to disambiguate.

Safer candidate generation
     def _iter_project_path_candidates(self, parsed_url: ParseResult) -> list[str]:
         parts = self.extract_repository_parts(parsed_url)
         if len(parts) < 2:
             return []

-        marker_index = next((index for index, part in enumerate(parts) if part in GITLAB_RESERVED_PATH_MARKERS), None)
-        max_length = marker_index if marker_index is not None else len(parts)
-        if max_length < 2:
-            return []
-
-        return ["/".join(parts[:length]) for length in range(max_length, 1, -1)]
+        return ["/".join(parts[:length]) for length in range(len(parts), 1, -1)]
📝 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
def _iter_project_path_candidates(self, parsed_url: ParseResult) -> list[str]:
parts = self.extract_repository_parts(parsed_url)
if len(parts) < 2:
return []
marker_index = next((index for index, part in enumerate(parts) if part in GITLAB_RESERVED_PATH_MARKERS), None)
max_length = marker_index if marker_index is not None else len(parts)
if max_length < 2:
return []
return ["/".join(parts[:length]) for length in range(max_length, 1, -1)]
def _iter_project_path_candidates(self, parsed_url: ParseResult) -> list[str]:
parts = self.extract_repository_parts(parsed_url)
if len(parts) < 2:
return []
return ["/".join(parts[:length]) for length in range(len(parts), 1, -1)]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/repository/providers/gitlab.py` around lines 64 - 74, The current
_iter_project_path_candidates uses GITLAB_RESERVED_PATH_MARKERS to cut off
candidate generation early, which prevents valid repos like "/tree/platform"
from being tried; change _iter_project_path_candidates so it does not treat
reserved markers as hard stops—remove the marker_index-based truncation and set
max_length to len(parts) (or otherwise generate all prefix candidates from
len(parts) down to 2), allowing _resolve_project_path to disambiguate via API
probes; keep using extract_repository_parts(parsed_url) and return the same list
comprehension join logic for candidates.

Comment thread src/framex/utils/cache.py
Comment on lines +61 to +74
if item.get("__type__") == "dynamic_obj":
try:
module = importlib.import_module(item["__module__"])
cls = getattr(module, item["__class__"])

cleaned_data = {k: restore_models(v) for k, v in item["data"].items()}

if hasattr(cls, "model_validate"):
return cls.model_validate(cleaned_data)
return cls(**cleaned_data)
except Exception:
from types import SimpleNamespace

return SimpleNamespace(**{k: restore_models(v) for k, v in item["data"].items()})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Stop restoring arbitrary classes from decoded payloads.

This branch trusts __module__ / __class__ from the serialized data and then imports + instantiates that type. Because cache_decode() is later used on request-derived proxy payloads, a crafted body can drive arbitrary imports/constructors here. Please restrict restoration to an explicit allowlist of cache-safe types, or leave decoded values as plain dicts.

🧰 Tools
🪛 Ruff (0.15.10)

[warning] 71-71: Do not catch blind exception: Exception

(BLE001)

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

In `@src/framex/utils/cache.py` around lines 61 - 74, The current restore_models
branch for items with "__type__" == "dynamic_obj" unsafely imports and
instantiates classes from untrusted payloads; update restore_models to stop
dynamic import/instantiation and instead map allowed (module, class) pairs to
safe constructors via an explicit allowlist (e.g., ALLOWED_DYNAMIC_TYPES) and
only call import/getattr or model_validate when the (item["__module__"],
item["__class__"]) tuple is in that allowlist; for any other values, return the
cleaned_data as a plain dict (not SimpleNamespace) to avoid executing arbitrary
code, and ensure cache_decode-related callers (restore_models) handle these
plain dicts correctly.

Comment on lines +17 to +24
def path_to_module_name(path: Path) -> str:
"""Convert path to module name."""
rel_path = path.resolve().relative_to(Path.cwd().resolve())
if rel_path.stem == "__init__":
module_name = ".".join(rel_path.parts[:-1])
else:
module_name = ".".join([*rel_path.parts[:-1], rel_path.stem]) # type: ignore[arg-type]
return module_name.removeprefix("src.")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t derive plugin module names relative to Path.cwd().

src/framex/plugin/manage.py passes absolute module_spec.origin paths from discovered plugin search roots. If a plugin lives outside the current working directory, relative_to(Path.cwd()) raises and plugin discovery fails.

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

In `@src/framex/utils/common.py` around lines 17 - 24, path_to_module_name
currently uses rel_path = path.resolve().relative_to(Path.cwd()) which fails for
plugin files outside the CWD; update path_to_module_name to avoid
relative_to(Path.cwd()) and instead work from the absolute path parts: resolve
the path, inspect its parts, detect if the file is an __init__ and drop the last
component, then locate a package root marker (e.g. "src") in the parts and join
the parts after that with dots to form the module name (if "src" not found, join
the parts from the first directory that should be treated as the package root or
use the full dotted path from the filename without suffix); keep the
removeprefix("src.") behavior by removing a leading "src" segment if present and
ensure module_name handles both module files and package __init__.py correctly
in the path_to_module_name function.

Comment on lines +32 to +39
def extract_method_params(func: Callable) -> list[tuple[str, Any]]:
sig = inspect.signature(func)
params: list[tuple[str, Any]] = []
for param in sig.parameters.values():
if param.name == "self":
continue
params.append((param.name, param.annotation))
return params
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Map missing annotations to a real type before storing params.

For unannotated arguments, inspect.signature() returns inspect._empty, but PluginApi.params expects type[Any] | Callable[..., Any]. Passing the sentinel through here can break model validation or downstream schema generation.

Proposed fix
 def extract_method_params(func: Callable) -> list[tuple[str, Any]]:
     sig = inspect.signature(func)
     params: list[tuple[str, Any]] = []
     for param in sig.parameters.values():
         if param.name == "self":
             continue
-        params.append((param.name, param.annotation))
+        annotation = Any if param.annotation is inspect._empty else param.annotation
+        params.append((param.name, annotation))
     return params
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/utils/common.py` around lines 32 - 39, extract_method_params
currently returns inspect._empty for unannotated parameters which breaks
downstream validation expecting real types; update extract_method_params to map
param.annotation == inspect._empty to typing.Any before appending so every tuple
uses a concrete type (e.g., change the value appended in extract_method_params
to (param.name, Any) when param.annotation is inspect._empty), ensure Any is
imported from typing if not already, and keep skipping "self" unchanged.

Comment thread src/framex/utils/config_docs.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/framex/utils/config_docs.py (1)

282-293: Narrow the blind except Exception.

Ruff flags BLE001 here. The try block only runs tomllib.loads, yaml.safe_load, and yaml.safe_dump — catching tomllib.TOMLDecodeError, yaml.YAMLError, and ValueError/UnicodeDecodeError keeps the fallback intentional and avoids swallowing unrelated bugs (e.g., a KeyError or RecursionError in mask_sensitive_config_data would be silently hidden today).

🔧 Suggested change
-    try:
+    try:
         if suffix == ".toml":
             parsed = tomllib.loads(content)
             return _format_plugin_config_toml(mask_sensitive_config_data(parsed))
         if suffix in {".yaml", ".yml"}:
             parsed = yaml.safe_load(content)
             masked = mask_sensitive_config_data(parsed)
             return yaml.safe_dump(masked, allow_unicode=True, sort_keys=False).rstrip()
-    except Exception:
+    except (tomllib.TOMLDecodeError, yaml.YAMLError, ValueError, UnicodeDecodeError):
         return mask_sensitive_config_text(content)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framex/utils/config_docs.py` around lines 282 - 293, The current broad
except hides unrelated errors; narrow it to only handle parsing/serialization
failures by catching tomllib.TOMLDecodeError for toml, yaml.YAMLError for yaml,
and also include ValueError/UnicodeDecodeError around yaml.safe_dump/loads; keep
the same fallback to mask_sensitive_config_text. Update the try/except around
tomllib.loads, yaml.safe_load, and yaml.safe_dump so _format_plugin_config_toml
and mask_sensitive_config_data still run normally but only parsing/serialization
exceptions are caught and routed to the fallback, referencing tomllib.loads,
yaml.safe_load, yaml.safe_dump, _format_plugin_config_toml, and
mask_sensitive_config_data.
tests/test_utils.py (1)

568-598: Avoid importlib.reload in tests — it corrupts module identity across the suite.

Reloading framex.repository.providers.base replaces the module object in sys.modules. Other tests that imported classes from this module before this test runs (e.g., the provider tests above) will still hold references to the old module objects, leading to flaky behavior depending on test ordering. monkeypatch.setattr alone on base_module.httpx.Client is sufficient and properly restored.

🧹 Suggested simplification
 def test_repository_fetch_json_follows_redirects(monkeypatch):
     import framex.repository.providers.base as base_module

-    base_module = importlib.reload(base_module)
     captured: dict[str, bool] = {}

And drop the import importlib at line 2 if no longer used elsewhere.

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

In `@tests/test_utils.py` around lines 568 - 598, The test
test_repository_fetch_json_follows_redirects should not call importlib.reload;
remove the importlib.reload(base_module) call and just import
framex.repository.providers.base once into base_module, then use
monkeypatch.setattr(base_module.httpx, "Client", FakeClient) as already done;
also remove the now-unused import importlib if it exists, and keep assertions
that RepositoryVersionProvider.fetch_json(...) and captured["follow_redirects"]
are True to verify behavior.
tests/api/test_proxy.py (1)

12-43: Consider SimpleNamespace for the OAuth test stub.

Creating a class on the fly via type("OAuthConfig", (), {...})() works but is noisier than the idiomatic types.SimpleNamespace(**{...}) already used in tests/driver/test_auth.py::fake_oauth. Reusing the existing fake_oauth helper (exported or duplicated) would also keep the test stubs consistent.

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

In `@tests/api/test_proxy.py` around lines 12 - 43, Replace the ad-hoc dynamic
class in _set_oauth_session with the idiomatic SimpleNamespace (or reuse the
existing fake_oauth helper from tests/driver/test_auth.py) so the test stub is
consistent; specifically, create a SimpleNamespace with keys provider,
jwt_secret, jwt_algorithm, authorization_url, client_id, call_back_url and then
monkeypatch.setattr(settings.auth, "oauth", that SimpleNamespace instead of
using type("OAuthConfig", ...), keeping the rest of _set_oauth_session
(create_auth_session, create_jwt, client.cookies.set) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/framex/config.py`:
- Line 58: OauthConfig.provider currently defaults to an empty string which ends
up stored as oauth_provider and can cause can_access_repository to silently
fail; fix by making provider explicit or validating it: change the
OauthConfig.provider field to a stricter type (e.g., Literal["github","gitlab"])
or add a model_post_init on OauthConfig that asserts provider is non-empty and
either sets a detected provider (from redirect/URL) or raises a clear validation
error; alternatively, update oauth_callback to avoid stamping an empty string
into the session/JWT and ensure
can_access_repository(auth_payload.get("oauth_provider"), ...) has a fallback
path when provider is missing.

In `@src/framex/driver/auth.py`:
- Around line 119-122: OAuth scope is hardcoded to GitLab-only values which
breaks multi-provider support; update the code to make scope provider-aware by
adding a scope field to OauthConfig (with provider-specific defaults in
model_post_init) and use that field instead of the hardcoded string in the OAuth
URL construction, and likewise parameterize provider-dependent values: switch
username extraction (e.g., use "username" vs "login" in the token/profile
parsing code around the existing username extraction) and adjust
provider-specific error detail strings (the error messages at/around the current
checks) to depend on settings.auth.oauth.provider or the new
OauthConfig.provider so the flow works for GitLab, GitHub, etc.

In `@src/framex/utils/config_docs.py`:
- Around line 113-118: _mask_sensitive_string currently reveals the first and
last two chars for most values which can expose a large fraction of short
secrets; modify _mask_sensitive_string so that if len(value) < 8 it returns a
complete mask (e.g., '*' repeated len(value)) instead of leaking characters,
otherwise keep the existing partial-mask behavior (first 2 + middle stars + last
2); preserve the current empty-value handling and ensure the star count for long
values is adjusted to exactly len(value)-4.

---

Nitpick comments:
In `@src/framex/utils/config_docs.py`:
- Around line 282-293: The current broad except hides unrelated errors; narrow
it to only handle parsing/serialization failures by catching
tomllib.TOMLDecodeError for toml, yaml.YAMLError for yaml, and also include
ValueError/UnicodeDecodeError around yaml.safe_dump/loads; keep the same
fallback to mask_sensitive_config_text. Update the try/except around
tomllib.loads, yaml.safe_load, and yaml.safe_dump so _format_plugin_config_toml
and mask_sensitive_config_data still run normally but only parsing/serialization
exceptions are caught and routed to the fallback, referencing tomllib.loads,
yaml.safe_load, yaml.safe_dump, _format_plugin_config_toml, and
mask_sensitive_config_data.

In `@tests/api/test_proxy.py`:
- Around line 12-43: Replace the ad-hoc dynamic class in _set_oauth_session with
the idiomatic SimpleNamespace (or reuse the existing fake_oauth helper from
tests/driver/test_auth.py) so the test stub is consistent; specifically, create
a SimpleNamespace with keys provider, jwt_secret, jwt_algorithm,
authorization_url, client_id, call_back_url and then
monkeypatch.setattr(settings.auth, "oauth", that SimpleNamespace instead of
using type("OAuthConfig", ...), keeping the rest of _set_oauth_session
(create_auth_session, create_jwt, client.cookies.set) unchanged.

In `@tests/test_utils.py`:
- Around line 568-598: The test test_repository_fetch_json_follows_redirects
should not call importlib.reload; remove the importlib.reload(base_module) call
and just import framex.repository.providers.base once into base_module, then use
monkeypatch.setattr(base_module.httpx, "Client", FakeClient) as already done;
also remove the now-unused import importlib if it exists, and keep assertions
that RepositoryVersionProvider.fetch_json(...) and captured["follow_redirects"]
are True to verify behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87f6eea0-4050-4793-9151-cb99964fa0cb

📥 Commits

Reviewing files that changed from the base of the PR and between b9499ac and 411c241.

📒 Files selected for processing (8)
  • src/framex/config.py
  • src/framex/driver/application.py
  • src/framex/driver/auth.py
  • src/framex/utils/config_docs.py
  • tests/api/test_proxy.py
  • tests/driver/test_auth.py
  • tests/mock.py
  • tests/test_utils.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/mock.py
  • src/framex/driver/application.py

Comment thread src/framex/config.py


class OauthConfig(BaseModel):
provider: str = ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

provider defaults to empty string — verify downstream tolerance.

OauthConfig.provider defaults to "", which means oauth_callback will stamp an empty string into the session/JWT (oauth_provider). can_access_repository(..., auth_payload.get("oauth_provider"), ...) must handle empty/unknown providers gracefully (e.g., fall back to URL-based detection), otherwise misconfigured deployments silently fail the repo access check. Consider making provider required via Literal["github", "gitlab"] or validating it in model_post_init.

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

In `@src/framex/config.py` at line 58, OauthConfig.provider currently defaults to
an empty string which ends up stored as oauth_provider and can cause
can_access_repository to silently fail; fix by making provider explicit or
validating it: change the OauthConfig.provider field to a stricter type (e.g.,
Literal["github","gitlab"]) or add a model_post_init on OauthConfig that asserts
provider is non-empty and either sets a detected provider (from redirect/URL) or
raises a clear validation error; alternatively, update oauth_callback to avoid
stamping an empty string into the session/JWT and ensure
can_access_repository(auth_payload.get("oauth_provider"), ...) has a fallback
path when provider is missing.

Comment thread src/framex/driver/auth.py Outdated
Comment on lines 119 to 122
"&scope=read_user%20read_api%20api%20ai_features"
)
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hardcoded GitLab OAuth scope breaks multi-provider support.

read_user, read_api, api, and ai_features are GitLab-only scope names; they're invalid for GitHub (which uses user:email, repo, etc.). Since OauthConfig.provider was introduced in this PR, the scope (and likely the username field extraction at line 154 and the GitLab-specific error strings at lines 144/155) should be provider-aware or driven from config. Otherwise the OAuth flow only works for GitLab despite the new provider field.

🔧 Suggested direction

Add a scope field to OauthConfig (with provider-appropriate defaults via model_post_init) and reference it here, e.g.:

-                    "&scope=read_user%20read_api%20api%20ai_features"
+                    f"&scope={quote(settings.auth.oauth.scope)}"

Similarly, parameterize username vs login extraction and the error detail strings based on settings.auth.oauth.provider.

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

In `@src/framex/driver/auth.py` around lines 119 - 122, OAuth scope is hardcoded
to GitLab-only values which breaks multi-provider support; update the code to
make scope provider-aware by adding a scope field to OauthConfig (with
provider-specific defaults in model_post_init) and use that field instead of the
hardcoded string in the OAuth URL construction, and likewise parameterize
provider-dependent values: switch username extraction (e.g., use "username" vs
"login" in the token/profile parsing code around the existing username
extraction) and adjust provider-specific error detail strings (the error
messages at/around the current checks) to depend on settings.auth.oauth.provider
or the new OauthConfig.provider so the flow works for GitLab, GitHub, etc.

Comment on lines +113 to +118
def _mask_sensitive_string(value: str) -> str:
if not value:
return value
if len(value) <= 4:
return "****"
return f"{value[:2]}{'*' * max(len(value) - 4, 4)}{value[-2:]}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Partial-leak masking preserves the first and last two characters.

f"{value[:2]}{'*' * max(len(value) - 4, 4)}{value[-2:]}" deliberately reveals the first and last 2 characters. That's fine for identifying which token is set, but for very short secrets (≤ ~8 chars) it exposes a substantial fraction. If any of the masked fields here can legitimately hold short secrets, consider a hard floor (e.g., mask entirely when len(value) < 8).

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

In `@src/framex/utils/config_docs.py` around lines 113 - 118,
_mask_sensitive_string currently reveals the first and last two chars for most
values which can expose a large fraction of short secrets; modify
_mask_sensitive_string so that if len(value) < 8 it returns a complete mask
(e.g., '*' repeated len(value)) instead of leaking characters, otherwise keep
the existing partial-mask behavior (first 2 + middle stars + last 2); preserve
the current empty-value handling and ensure the star count for long values is
adjusted to exactly len(value)-4.

@touale touale merged commit cf4f8d3 into master Apr 17, 2026
8 of 9 checks passed
@touale touale deleted the docs-support-config-view branch April 17, 2026 02:37
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