Skip to content

feat: add proxy function support#35

Merged
touale merged 10 commits intomasterfrom
feat/support-on-proxy
Dec 23, 2025
Merged

feat: add proxy function support#35
touale merged 10 commits intomasterfrom
feat/support-on-proxy

Conversation

@touale
Copy link
Copy Markdown
Owner

@touale touale commented Dec 22, 2025

Summary by CodeRabbit

  • New Features

    • Added proxy function decorator and runtime support to register/route functions through a proxy, plus a new HTTP endpoint for remote proxy invocation.
    • Added serialization utilities for caching (encode/decode).
  • Behavior Changes

    • Refined proxy API handling and result validation; clearer error/warning messages.
    • Auth and server defaults now normalize proxy auth paths and let Ray auto-detect CPUs when non-positive.
  • Bug Fixes

    • Non-dictionary proxy responses are returned instead of raising.
  • Tests

    • New/expanded tests covering proxy flows and cache encode/decode.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 22, 2025

Walkthrough

Adds a proxy function system: decorator-based registration and invocation routed through a new HTTP proxy endpoint, plus encode/decode caching utilities, config/constant updates, deferred auth/setting resolution, and accompanying tests and mocks.

Changes

Cohort / File(s) Summary
Proxy Plugin Core
src/framex/plugins/proxy/__init__.py, src/framex/plugins/proxy/config.py, src/framex/plugins/proxy/model.py
Introduces proxy function lifecycle: proxy_func_map, route registration at PROXY_FUNC_HTTP_PATH, methods register_proxy_function, call_proxy_function, HTTP route handler, proxy_functions config and validator, and ProxyFunc / ProxyFuncHttpBody models.
Plugin API & Decorator
src/framex/plugin/__init__.py, src/framex/plugin/on.py, src/framex/plugin/load.py
call_plugin_api accepts `PluginApi
Encoding / Cache Utilities
src/framex/utils.py, tests/test_utils.py
Adds xor_crypt, cache_encode, cache_decode (serialize/compress/encrypt/base64) and extends make_stream_event to accept BaseModel/None; unit tests added for encoding/decoding and model handling.
Config, Consts & Defaults
src/framex/config.py, src/framex/consts.py, pyproject.toml
Adds PROXY_PLUGIN_NAME and PROXY_FUNC_HTTP_PATH; renames/extends auth validation to auto-insert proxy path and generate a general auth key when missing; ServerConfig.num_cpus default → -1; adds pytest-order dev dependency.
Adapter / Ingress / Logging / Init
src/framex/adapter/base.py, src/framex/driver/ingress.py, src/framex/log.py, src/framex/__init__.py
Adjusts proxy check in BaseAdapter.call_func to require api.api; defers auth key lookup to APIIngress.register_route; moves settings import into log emitter; only pass positive num_cpus to Ray.
Tests, Mocks & CLI
pytest.ini, tests/mock.py, tests/test_plugins.py, tests/api/test_proxy.py, tests/test_plugin.py, tests/test_cli.py
Adds pytest proxy mappings and mock /proxy/remote POST handling (auth + decode); adds on_proxy tests and models; new proxy endpoint test; removes test expecting non-dict proxy error and removed CPU default assertion in CLI test.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Decorator as on_proxy
    participant Cache as cache_encode/cache_decode
    participant PluginAPI as call_plugin_api
    participant ProxyPlugin

    rect rgb(230,240,255)
    note over Client,ProxyPlugin: Registration (first call)
    Client->>Decorator: call decorated(...)
    Decorator->>Cache: cache_encode(func_name, kwargs)
    Cache-->>Decorator: encoded_payload
    Decorator->>PluginAPI: call_plugin_api(PROXY_PLUGIN_NAME, register_proxy_function, func_callable)
    PluginAPI->>ProxyPlugin: register_proxy_function(func_name, callable)
    ProxyPlugin-->>PluginAPI: status
    PluginAPI-->>Decorator: result
    Decorator-->>Client: return
    end

    rect rgb(230,255,230)
    note over Client,ProxyPlugin: Invocation (subsequent calls)
    Client->>Decorator: call decorated(...)
    Decorator->>Cache: cache_encode(func_name, kwargs)
    Cache-->>Decorator: encoded_payload
    Decorator->>PluginAPI: call_plugin_api(PROXY_PLUGIN_NAME, call_proxy_function, encoded_payload)
    PluginAPI->>ProxyPlugin: call_proxy_function(func_name, data)
    alt function is_remote
        ProxyPlugin->>ProxyPlugin: POST /proxy/remote (auth + data)
        ProxyPlugin-->>PluginAPI: serialized_result
    else local
        ProxyPlugin->>ProxyPlugin: invoke local callable
        ProxyPlugin-->>PluginAPI: serialized_result
    end
    PluginAPI-->>Decorator: serialized_result
    Decorator->>Cache: cache_decode(serialized_result)
    Cache-->>Decorator: decoded_result
    Decorator-->>Client: return decoded_result
    end
Loading
sequenceDiagram
    participant TestClient
    participant MockServer
    participant ProxyEndpoint as /api/v1/proxy/remote
    participant Cache as cache_decode

    TestClient->>MockServer: POST /api/v1/proxy/remote (Authorization + payload)
    MockServer->>ProxyEndpoint: route request
    ProxyEndpoint->>Cache: cache_decode(func_name)
    Cache-->>ProxyEndpoint: func_name
    ProxyEndpoint->>Cache: cache_decode(data)
    Cache-->>ProxyEndpoint: kwargs
    alt auth valid
        ProxyEndpoint->>ProxyEndpoint: execute target (local or simulated remote)
        ProxyEndpoint-->>TestClient: {"result":..., "data": cache_encode(returned)}
    else auth invalid
        ProxyEndpoint-->>TestClient: 401-like response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped a path through bytes and keys,
I wrapped and sent my funcs with ease,
I cached, I zipped, I hid the trace,
Local or remote — same warm place,
Hopping packets, carrots, and APIs. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.79% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 accurately describes the main feature addition: proxy function support. It directly relates to the substantial changes across the codebase for implementing proxy functionality.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/support-on-proxy

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 425984c and 8a1b9db.

📒 Files selected for processing (1)
  • src/framex/plugin/load.py
🔇 Additional comments (2)
src/framex/plugin/load.py (2)

1-1: LGTM!

The Callable import from collections.abc is appropriate and used for the type annotation in register_proxy_func.


43-44: This function is unused and should be removed or clarified as deprecated.

The register_proxy_func function in load.py (lines 43-44) is not actually used by the framework's proxy system. The actual proxy function registration occurs through the on_proxy() decorator pattern in on.py, which calls the register_proxy_function() method on the ProxyPlugin class via the plugin API. This register_proxy_func appears to be dead code or a deprecated placeholder from an earlier design.

Rather than adding documentation for a no-op function that serves no purpose, consider either removing it entirely or adding a comment explaining why it exists (if it's intentionally reserved for future use).

Likely an incorrect or invalid review 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.

@codecov
Copy link
Copy Markdown

codecov bot commented Dec 22, 2025

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

Caution

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

⚠️ Outside diff range comments (2)
src/framex/plugins/proxy/__init__.py (2)

65-69: httpx.AsyncClient not closed properly.

The AsyncClient is created but never closed, which can leak connections. Use a context manager or explicitly close the client.

🔎 Proposed fix
     async def _get_openai_docs(self, url: str) -> dict[str, Any]:
-        clent = httpx.AsyncClient(timeout=self.time_out)
-        response = await clent.get(f"{url}/api/v1/openapi.json")
-        response.raise_for_status()
-        return cast(dict[str, Any], response.json())
+        async with httpx.AsyncClient(timeout=self.time_out) as client:
+            response = await client.get(f"{url}/api/v1/openapi.json")
+            response.raise_for_status()
+            return cast(dict[str, Any], response.json())

191-201: httpx.AsyncClient not closed in non-streaming path.

Similar to _get_openai_docs, the client created on line 191 is not closed after use in the non-streaming branch (line 201).

🔎 Proposed fix
     async def fetch_response(
         self,
         stream: bool = False,
         **kwargs: Any,
     ) -> AsyncGenerator[str, None] | dict | str:
-        clent = httpx.AsyncClient(timeout=self.time_out)
         if stream:
+            client = httpx.AsyncClient(timeout=self.time_out)

             async def stream_generator() -> AsyncGenerator[str, None]:
-                async with clent.stream(**kwargs) as response:
+                async with client.stream(**kwargs) as response:
                     response.raise_for_status()
                     async for chunk in response.aiter_text():
                         yield chunk
+                await client.aclose()

             return stream_generator()
-        response = await clent.request(**kwargs)
-        response.raise_for_status()
-        try:
-            return cast(dict, response.json())
-        except json.JSONDecodeError:
-            return response.text
+        async with httpx.AsyncClient(timeout=self.time_out) as client:
+            response = await client.request(**kwargs)
+            response.raise_for_status()
+            try:
+                return cast(dict, response.json())
+            except json.JSONDecodeError:
+                return response.text
🧹 Nitpick comments (5)
src/framex/plugins/proxy/config.py (1)

32-37: LGTM! Validation ensures consistency.

The validator correctly ensures that every proxy function URL is covered by a proxy_urls entry, preventing configuration errors.

Optional: Consider extracting the error message to a constant

The static analysis tool suggests avoiding long messages in exception constructors. While this is a minor style concern, you could extract it if desired:

+    _INVALID_PROXY_URL_MSG = "proxy_functions url '{url}' is not covered by any proxy_urls rule"
+
     @model_validator(mode="after")
     def validate_proxy_functions(self) -> Self:
         for url in self.proxy_functions:
             if url not in self.proxy_urls:
-                raise ValueError(f"proxy_functions url '{url}' is not covered by any proxy_urls rule")
+                raise ValueError(self._INVALID_PROXY_URL_MSG.format(url=url))
         return self
src/framex/plugin/__init__.py (1)

120-123: Remove commented code.

The commented-out warning should be removed rather than left in the codebase.

Proposed cleanup
     if use_proxy:
         if not isinstance(result, dict):
-            # logger.warning(f"Proxy API {api_name} returned non-dict result: {type(result)}")
             return result
src/framex/utils.py (3)

61-62: Document that XOR is obfuscation, not encryption.

The xor_crypt function uses a fixed key for XOR operations, which provides obfuscation but not cryptographic security. If this is intentional (e.g., to prevent casual inspection of cached data), please add a docstring clarifying this is not secure encryption.

Suggested docstring
 def xor_crypt(data: bytes, key: str = "01234567890abcdefghijklmnopqrstuvwxyz") -> bytes:
+    """Apply XOR-based obfuscation to data.
+    
+    Note: This provides basic obfuscation only, NOT cryptographic security.
+    Do not rely on this for protecting sensitive data.
+    """
     return bytes(a ^ b for a, b in zip(data, cycle(key.encode())))

65-77: Add error handling for malformed input.

The cache_encode and cache_decode functions lack error handling for malformed input. Invalid hex strings, compression errors, or unpickling failures will raise unhandled exceptions.

Suggested error handling
 def cache_decode(data: str) -> Any:
-    raw = bytes.fromhex(data)
-    de_obfuscated = xor_crypt(raw)
-    decompressed = zlib.decompress(de_obfuscated)
-    return cloudpickle.loads(decompressed)  # nosec S301
+    try:
+        raw = bytes.fromhex(data)
+        de_obfuscated = xor_crypt(raw)
+        decompressed = zlib.decompress(de_obfuscated)
+        return cloudpickle.loads(decompressed)  # nosec S301
+    except (ValueError, zlib.error, Exception) as e:
+        raise ValueError(f"Failed to decode cached data: {e}") from e

61-77: Consider adding docstrings for new utility functions.

The new encoding/decoding utilities lack documentation explaining their purpose, parameters, and return values, which would improve maintainability.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83dc91e and 8b9e474.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • pyproject.toml
  • pytest.ini
  • src/framex/adapter/base.py
  • src/framex/plugin/__init__.py
  • src/framex/plugin/on.py
  • src/framex/plugins/proxy/__init__.py
  • src/framex/plugins/proxy/config.py
  • src/framex/plugins/proxy/model.py
  • src/framex/utils.py
  • tests/mock.py
  • tests/test_plugin.py
  • tests/test_plugins.py
💤 Files with no reviewable changes (1)
  • tests/test_plugin.py
🧰 Additional context used
🧬 Code graph analysis (5)
tests/mock.py (2)
src/framex/cli.py (1)
  • framex (11-12)
src/framex/utils.py (1)
  • cache_decode (73-77)
src/framex/plugin/on.py (3)
src/framex/plugin/model.py (3)
  • ApiType (22-26)
  • PluginApi (29-37)
  • PluginDeployment (41-43)
src/framex/utils.py (4)
  • cache_decode (73-77)
  • cache_encode (65-70)
  • extract_method_params (34-41)
  • plugin_to_deployment_name (15-16)
src/framex/plugin/__init__.py (1)
  • call_plugin_api (78-134)
src/framex/plugin/__init__.py (2)
src/framex/plugin/model.py (1)
  • PluginApi (29-37)
src/framex/plugin/manage.py (1)
  • get_api (196-201)
src/framex/adapter/base.py (1)
src/framex/plugin/model.py (1)
  • ApiType (22-26)
tests/test_plugins.py (1)
src/framex/plugin/on.py (1)
  • on_proxy (101-138)
🪛 Ruff (0.14.8)
src/framex/plugin/on.py

116-119: Avoid specifying long messages outside the exception class

(TRY003)

src/framex/plugins/proxy/__init__.py

283-283: Avoid specifying long messages outside the exception class

(TRY003)

src/framex/plugins/proxy/config.py

36-36: Avoid specifying long messages outside the exception class

(TRY003)

tests/test_plugins.py

171-171: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (20)
pytest.ini (1)

30-30: LGTM! Test configuration properly wired.

The proxy function mapping correctly links the test proxy URL to the test function, aligning with the new proxy function infrastructure.

src/framex/adapter/base.py (1)

32-34: LGTM! Conditional proxy handling is appropriate.

The added check ensures proxy_path is only set when an actual API endpoint exists. This correctly accommodates the new proxy function feature where proxy calls may occur without explicit API paths.

src/framex/plugins/proxy/config.py (1)

19-19: LGTM! New field properly typed.

The proxy_functions field correctly maps proxy URLs to lists of function names, supporting the new proxy function registration feature.

src/framex/plugins/proxy/model.py (2)

7-9: LGTM! Model structure is clean.

The ProxyFunc model appropriately captures function metadata. Note that Callable fields don't serialize with Pydantic's standard JSON serialization, but this is expected for in-memory proxy function registration.


12-14: LGTM! HTTP body model is appropriate.

The ProxyFuncHttpBody model correctly defines the structure for transporting encoded proxy function invocations.

src/framex/plugin/__init__.py (1)

78-86: LGTM! Enhanced API calling is more flexible.

The updated signature correctly supports both string API names and direct PluginApi instances, enabling the proxy function feature to pass pre-constructed API objects.

pyproject.toml (1)

11-11: LGTM! Dependency required for proxy function serialization.

The cloudpickle dependency is correctly added to support the new encoding/decoding utilities for proxy function payloads.

src/framex/utils.py (1)

51-58: LGTM! BaseModel handling is correctly implemented.

The expanded signature and BaseModel serialization logic properly supports streaming events with Pydantic models.

tests/test_plugins.py (5)

8-9: LGTM!

Import of the new on_proxy decorator is correctly added for testing the proxy functionality.


153-161: LGTM!

The SubModel and ExchangeModel Pydantic models are well-defined with proper type annotations for testing the proxy function serialization with nested models.


169-171: LGTM on the remote proxy function definition.

The proxy_only=True parameter correctly indicates this function should only be invoked remotely. The RuntimeError serves as a safeguard if local invocation occurs unexpectedly.


174-183: LGTM!

The test correctly verifies local proxy function invocation, including proper serialization/deserialization of nested Pydantic models. The assertions validate both primitive types and complex model structures.


186-194: LGTM!

The remote proxy test verifies that proxy_only=True functions are routed correctly and return the expected response structure with the function name and data.

src/framex/plugin/on.py (2)

1-1: LGTM!

The new imports are correctly added to support the on_proxy decorator functionality: functools for @wraps, PROXY_PLUGIN_NAME constant, encoding utilities, and call_plugin_api.

Also applies to: 10-10, 12-12, 14-14


128-134: LGTM on the proxy call logic.

The encoding of both function name and kwargs using cache_encode, along with the subsequent cache_decode of the result, correctly handles the serialization round-trip for proxy invocation.

src/framex/plugins/proxy/__init__.py (5)

19-20: LGTM!

Imports for the new proxy function models and encoding utilities are correctly added.


38-41: LGTM!

New instance attributes properly initialize the proxy function map, route initialization flag, and HTTP path.


142-163: LGTM!

The register_proxy_func_route method correctly sets up the HTTP route for proxy function invocation with the appropriate parameters and tags.


165-179: LGTM!

The _parse_proxy_function method properly configures remote proxy functions with authentication headers when required and registers them as remote handlers.


272-283: LGTM on the proxy function invocation logic.

The call_proxy_function method correctly handles both remote and local invocations, decoding the function name, selecting the appropriate kwargs format, and encoding the result.

Comment thread src/framex/plugin/on.py Outdated
Comment on lines +101 to +106
def on_proxy(proxy_only: bool = False) -> Callable:
def decorator(func: Callable) -> Callable:
from framex.config import settings

if not settings.server.enable_proxy:
return func
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the on.py file and examine the on_proxy decorator
find . -name "on.py" -path "*/plugin/*" -type f | head -5

Repository: touale/FrameX-kit

Length of output: 86


🏁 Script executed:

#!/bin/bash
# Get more context about the on_proxy decorator and enable_proxy
rg -n "on_proxy|enable_proxy|proxy_only" --type py -B2 -A5 | head -100

Repository: touale/FrameX-kit

Length of output: 6367


🏁 Script executed:

#!/bin/bash
# Read the on_proxy decorator implementation with full context
cat -n src/framex/plugin/on.py | head -150

Repository: touale/FrameX-kit

Length of output: 6670


🏁 Script executed:

#!/bin/bash
# Check if there's a test for proxy_only=True with enable_proxy=False
rg -n "proxy_only.*True" tests/ -A10 -B5 | grep -E "(enable_proxy|test_|def )" | head -30

Repository: touale/FrameX-kit

Length of output: 1114


🏁 Script executed:

#!/bin/bash
# Check for any test that covers proxy_only=True with enable_proxy=False
rg -n "proxy_only.*True" tests/ --type py -B15 | grep -E "(def test_|enable_proxy.*False|patch.*enable_proxy)"

Repository: touale/FrameX-kit

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Look at the full test_on_proxy_remote_call test to see how it's tested
sed -n '186,200p' tests/test_plugins.py

Repository: touale/FrameX-kit

Length of output: 513


🏁 Script executed:

#!/bin/bash
# Check test file for any test with proxy_only=True and enable_proxy=False
cat -n tests/test_plugins.py | grep -A30 "def test_on_proxy"

Repository: touale/FrameX-kit

Length of output: 1088


🏁 Script executed:

#!/bin/bash
# Look for any handling of proxy_only in the entire codebase
rg -n "proxy_only" --type py -B5 -A10

Repository: touale/FrameX-kit

Length of output: 2471


🏁 Script executed:

#!/bin/bash
# Check if there are any other proxy-related functions with proxy_only=True
rg -n "@on_proxy\(proxy_only" --type py

Repository: touale/FrameX-kit

Length of output: 113


🏁 Script executed:

#!/bin/bash
# Look at how enable_proxy is typically used in tests and if there's a default behavior
rg -n "enable_proxy" tests/ --type py | head -20

Repository: touale/FrameX-kit

Length of output: 731


🏁 Script executed:

#!/bin/bash
# Check if enable_proxy has a default value in settings
rg -n "enable_proxy" src/framex/config.py --type py -B3 -A3

Repository: touale/FrameX-kit

Length of output: 308


🏁 Script executed:

#!/bin/bash
# Look for any documentation or comments about proxy_only parameter
rg -n "proxy_only" . --type py -B5 | head -40

Repository: touale/FrameX-kit

Length of output: 1029


🏁 Script executed:

#!/bin/bash
# Check for pytest conftest that might set default enable_proxy
find tests -name "conftest.py" -type f -exec cat {} \;

Repository: touale/FrameX-kit

Length of output: 1802


🏁 Script executed:

#!/bin/bash
# Check if test_on_proxy_remote_call is being skipped or has fixtures that patch enable_proxy
sed -n '160,210p' tests/test_plugins.py

Repository: touale/FrameX-kit

Length of output: 1342


🏁 Script executed:

#!/bin/bash
# Check if framex.run with test_mode=True sets enable_proxy
rg -n "test_mode" src/ --type py -B3 -A10 | head -50

Repository: touale/FrameX-kit

Length of output: 1838


🏁 Script executed:

#!/bin/bash
# Check the full test setup and whether enable_proxy is patched for test_on_proxy_remote_call
sed -n '1,30p' tests/test_plugins.py

Repository: touale/FrameX-kit

Length of output: 1363


🏁 Script executed:

#!/bin/bash
# Check if the test fixture patches enable_proxy or if proxy is enabled in test mode
rg -n "enable_proxy" tests/test_plugins.py --type py -B5 -A5

Repository: touale/FrameX-kit

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Look at full test setup to understand how test_on_proxy_remote_call actually works
sed -n '1,100p' tests/conftest.py | cat -n

Repository: touale/FrameX-kit

Length of output: 2292


🏁 Script executed:

#!/bin/bash
# Check if test_on_proxy_remote_call has any pytest marks or fixtures
sed -n '160,196p' tests/test_plugins.py | cat -n

Repository: touale/FrameX-kit

Length of output: 1594


🏁 Script executed:

#!/bin/bash
# Check entire test_plugins.py for any imports or fixtures that might enable proxy
head -50 tests/test_plugins.py | cat -n

Repository: touale/FrameX-kit

Length of output: 2361


🏁 Script executed:

#!/bin/bash
# Check if there's a pytest.ini or setup that might configure enable_proxy
cat pytest.ini pyproject.toml 2>/dev/null | grep -A20 -B5 "pytest\|enable_proxy" || echo "No pytest config found"

Repository: touale/FrameX-kit

Length of output: 2061


🏁 Script executed:

#!/bin/bash
# Check if framex.run or test_mode enables proxy by default
rg -n "enable_proxy.*=" src/framex/ --type py -B2 -A2 | head -30

Repository: touale/FrameX-kit

Length of output: 1040


🏁 Script executed:

#!/bin/bash
# Check if there's any validation or handling for proxy_only when enable_proxy is False
rg -n "proxy_only" src/ --type py -B5 -A10

Repository: touale/FrameX-kit

Length of output: 1414


🏁 Script executed:

#!/bin/bash
# Look at the full on_proxy decorator once more to ensure we understand all code paths
sed -n '101,139p' src/framex/plugin/on.py | cat -n

Repository: touale/FrameX-kit

Length of output: 1819


Add defensive handling for proxy_only=True when proxy is disabled.

When proxy_only=True but settings.server.enable_proxy=False, the decorator returns the original function unchanged. For functions designed to run only through the proxy (like remote_exchange_key_value), this causes an unexpected RuntimeError instead of surfacing a clear configuration error.

Consider either:

  1. Raising a clear configuration error during decoration when proxy_only=True and proxy is disabled
  2. Returning a stub function that provides a helpful error message explaining the misconfiguration
🤖 Prompt for AI Agents
In src/framex/plugin/on.py around lines 101 to 106, the on_proxy decorator
silently returns the original function when settings.server.enable_proxy is
False, which causes functions intended to be proxy-only to fail later with a
confusing RuntimeError; update the decorator so that when proxy_only=True and
enable_proxy is False it either raises a clear ConfigurationError (preferred) at
decoration time or replaces the function with a stub that immediately raises a
descriptive error indicating that the proxy is disabled and the function
requires enable_proxy=True; implement one of these behaviors and include the
function name in the error message for clarity.

Comment thread src/framex/plugin/on.py Outdated
Comment on lines +108 to +126
is_registered = proxy_only
full_func_name = f"{func.__module__}.{func.__name__}"

@functools.wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
nonlocal is_registered

if args:
raise TypeError(
f"The proxy function '{func.__name__}' only supports keyword arguments. "
f"Please call it using the syntax {func.__name__}(param=value)."
)

if not is_registered:
api_reg = PluginApi(
deployment_name=PROXY_PLUGIN_NAME, call_type=ApiType.PROXY, func_name="register_proxy_function"
)
await call_plugin_api(api_reg, None, func_name=full_func_name, func_callable=func)
is_registered = True
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

Race condition on is_registered in concurrent scenarios.

The nonlocal is_registered flag is checked and set without synchronization. If multiple coroutines call the same proxy function concurrently before registration completes, multiple registration attempts could occur.

While duplicate registrations may be idempotent, consider using an asyncio.Lock if registration has side effects or costs.

🔎 Proposed fix using asyncio.Lock
+import asyncio
+
 def on_proxy(proxy_only: bool = False) -> Callable:
     def decorator(func: Callable) -> Callable:
         from framex.config import settings

         if not settings.server.enable_proxy:
             return func

         is_registered = proxy_only
         full_func_name = f"{func.__module__}.{func.__name__}"
+        registration_lock = asyncio.Lock()

         @functools.wraps(func)
         async def wrapper(*args: Any, **kwargs: Any) -> Any:
             nonlocal is_registered

             if args:
                 raise TypeError(
                     f"The proxy function '{func.__name__}' only supports keyword arguments. "
                     f"Please call it using the syntax {func.__name__}(param=value)."
                 )

             if not is_registered:
-                api_reg = PluginApi(
-                    deployment_name=PROXY_PLUGIN_NAME, call_type=ApiType.PROXY, func_name="register_proxy_function"
-                )
-                await call_plugin_api(api_reg, None, func_name=full_func_name, func_callable=func)
-                is_registered = True
+                async with registration_lock:
+                    if not is_registered:  # Double-check after acquiring lock
+                        api_reg = PluginApi(
+                            deployment_name=PROXY_PLUGIN_NAME, call_type=ApiType.PROXY, func_name="register_proxy_function"
+                        )
+                        await call_plugin_api(api_reg, None, func_name=full_func_name, func_callable=func)
+                        is_registered = True

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.8)

116-119: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In src/framex/plugin/on.py around lines 108 to 126, the nonlocal boolean
is_registered is read and set without synchronization which can cause concurrent
coroutines to trigger multiple registrations; introduce an asyncio.Lock defined
in the outer scope (next to is_registered), mark it nonlocal in the wrapper, and
wrap the check-and-register sequence in "async with lock:" so only one coroutine
performs the registration: inside the lock re-check is_registered, call
PluginApi/register only if still False, then set is_registered = True; other
calls awaiting the lock will see the updated flag and skip redundant
registration.

Comment thread src/framex/plugins/proxy/__init__.py Outdated
Comment on lines +261 to +270
async def register_proxy_function(
self, func_name: str, func_callable: Callable[..., Any], is_remote: bool = False
) -> bool:
if not self.init_proxy_func_route:
await self.register_proxy_func_route()
self.init_proxy_func_route = True

logger.info(f"Registering proxy function: {func_name}")
self.proxy_func_map[func_name] = ProxyFunc(func=func_callable, is_remote=is_remote)
return True
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

Race condition on init_proxy_func_route flag.

Similar to the is_registered issue in on.py, concurrent calls to register_proxy_function could trigger multiple route registrations before the flag is set. Consider using a lock or moving route registration to on_start.

🔎 Proposed fix using asyncio.Lock

Add a lock in __init__:

self._route_registration_lock = asyncio.Lock()

Then use it:

     async def register_proxy_function(
         self, func_name: str, func_callable: Callable[..., Any], is_remote: bool = False
     ) -> bool:
-        if not self.init_proxy_func_route:
-            await self.register_proxy_func_route()
-            self.init_proxy_func_route = True
+        if not self.init_proxy_func_route:
+            async with self._route_registration_lock:
+                if not self.init_proxy_func_route:
+                    await self.register_proxy_func_route()
+                    self.init_proxy_func_route = True

         logger.info(f"Registering proxy function: {func_name}")
         self.proxy_func_map[func_name] = ProxyFunc(func=func_callable, is_remote=is_remote)
         return True

Committable suggestion skipped: line range outside the PR's diff.

Comment thread src/framex/utils.py Outdated
Comment thread tests/mock.py Outdated
Comment thread tests/test_plugins.py
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
src/framex/plugin/on.py (1)

108-126: Note: Race condition on registration flag.

The is_registered flag is checked and set without synchronization (lines 121-126). While duplicate registrations may be idempotent, concurrent calls to the same proxy function before registration completes could trigger multiple registration attempts.

This issue was previously identified in past reviews. If registration has side effects or costs, consider using an asyncio.Lock to synchronize the check-and-register sequence.

tests/test_plugins.py (1)

164-166: Typo fixed from previous review.

The function name has been corrected from lcoal_exchange_key_value to local_exchange_key_value. Implementation looks good for testing local proxy execution.

src/framex/plugins/proxy/__init__.py (2)

52-57: Incorrect for-else logic persists from previous review.

The else clause on line 56 executes when the for loop completes normally (without break), so "No proxy functions to register" will be logged even when functions are successfully registered. This was flagged in a previous review but remains unfixed.

🔎 Proposed fix
-        if settings.proxy_functions:
-            for url, funcs in settings.proxy_functions.items():
-                for func in funcs:
-                    await self._parse_proxy_function(func, url)
-        else:  # pragma: no cover
+        if settings.proxy_functions:
+            for url, funcs in settings.proxy_functions.items():
+                for func in funcs:
+                    await self._parse_proxy_function(func, url)
+        else:
             logger.debug("No proxy functions to register")

263-275: Race condition on route registration persists from previous review.

Lines 266-268 have a time-of-check-to-time-of-use (TOCTOU) race: concurrent calls can both see init_proxy_func_route == False and attempt to register the route multiple times before the flag is set. While the duplicate function check (lines 269-271) prevents function re-registration, the route registration itself could still be invoked concurrently. This was flagged in a previous review suggesting an asyncio.Lock.

🧹 Nitpick comments (2)
pytest.ini (1)

36-37: Remove trailing empty lines.

Minor style issue: trailing empty lines at the end of the file.

🔎 Proposed fix
     auth__general_auth_keys=["i_am_general_auth_keys"]
     auth__auth_urls=["/api/v1/echo"]
-    
-
+
 asyncio_mode = auto
src/framex/plugins/proxy/__init__.py (1)

277-289: LGTM: Proxy function routing logic is correct.

The method properly decodes parameters, routes between local and remote execution paths based on the is_remote flag, and encodes the result. The logic for constructing kwargs for remote vs. local calls is sound.

Note: Static analysis flagged the error message on line 289 (TRY003). If desired, you could define a custom exception class, but this is a minor stylistic preference.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b9e474 and ffa9438.

📒 Files selected for processing (11)
  • pytest.ini
  • src/framex/config.py
  • src/framex/consts.py
  • src/framex/driver/ingress.py
  • src/framex/log.py
  • src/framex/plugin/__init__.py
  • src/framex/plugin/on.py
  • src/framex/plugins/proxy/__init__.py
  • src/framex/plugins/proxy/config.py
  • tests/mock.py
  • tests/test_plugins.py
🧰 Additional context used
🧬 Code graph analysis (6)
tests/test_plugins.py (1)
src/framex/plugin/on.py (1)
  • on_proxy (101-138)
src/framex/driver/ingress.py (1)
src/framex/config.py (1)
  • get_auth_keys (86-110)
src/framex/plugins/proxy/__init__.py (5)
src/framex/plugin/model.py (2)
  • PluginApi (29-37)
  • ApiType (22-26)
src/framex/plugins/proxy/builder.py (1)
  • create_pydantic_model (70-93)
src/framex/plugins/proxy/model.py (2)
  • ProxyFunc (7-9)
  • ProxyFuncHttpBody (12-14)
src/framex/utils.py (2)
  • cache_decode (73-77)
  • cache_encode (65-70)
src/framex/adapter/base.py (3)
  • BaseAdapter (20-68)
  • get_handle (53-53)
  • call_func (29-39)
src/framex/plugin/on.py (3)
src/framex/plugin/model.py (2)
  • ApiType (22-26)
  • PluginApi (29-37)
src/framex/utils.py (4)
  • cache_decode (73-77)
  • cache_encode (65-70)
  • extract_method_params (34-41)
  • plugin_to_deployment_name (15-16)
src/framex/plugin/__init__.py (1)
  • call_plugin_api (78-133)
src/framex/plugin/__init__.py (2)
src/framex/plugin/model.py (1)
  • PluginApi (29-37)
src/framex/plugin/manage.py (1)
  • get_api (196-201)
tests/mock.py (1)
src/framex/utils.py (1)
  • cache_decode (73-77)
🪛 Ruff (0.14.10)
tests/test_plugins.py

171-171: Avoid specifying long messages outside the exception class

(TRY003)

src/framex/plugins/proxy/config.py

36-36: Avoid specifying long messages outside the exception class

(TRY003)

src/framex/plugins/proxy/__init__.py

289-289: Avoid specifying long messages outside the exception class

(TRY003)

src/framex/plugin/on.py

116-119: Avoid specifying long messages outside the exception class

(TRY003)

tests/mock.py

79-79: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (26)
src/framex/consts.py (1)

8-9: LGTM!

The new proxy constants are well-defined and follow the existing naming conventions. The use of API_STR for path composition ensures consistency.

src/framex/log.py (1)

18-19: LGTM!

Deferring the settings import to emission time is a good practice that reduces import-time dependencies and helps avoid potential circular import issues.

tests/mock.py (1)

68-82: LGTM! Proper validation implemented.

The previous review concern about bare assert statements has been addressed. The code now uses explicit validation with a clear ValueError, ensuring the checks remain active even when Python runs with optimization flags.

src/framex/plugins/proxy/config.py (2)

19-19: LGTM!

The new proxy_functions field provides a clean way to configure proxy function mappings per URL.


32-37: LGTM!

The validator ensures configuration consistency by verifying that every proxy_functions URL has a corresponding entry in proxy_urls. This prevents misconfiguration at startup.

src/framex/driver/ingress.py (1)

70-78: LGTM!

Deferring auth_keys resolution to registration time is a good practice. The local logger import and debug logging provide better visibility into authentication requirements for each route.

src/framex/plugin/on.py (3)

101-106: LGTM!

The early return when proxy is disabled is straightforward and clean. Note that the previous review concern about proxy_only parameter handling is no longer applicable since that parameter has been removed from the decorator signature.


115-119: LGTM!

Good validation: enforcing keyword-only arguments prevents confusion and makes proxy function calls explicit and readable.


128-134: LGTM!

The proxy invocation flow correctly encodes the function name and arguments, calls the proxy, and decodes the result.

src/framex/config.py (1)

63-75: LGTM! Security-by-default approach.

The renamed validator now performs both normalization and validation:

  1. Automatically adds PROXY_FUNC_HTTP_PATH to auth_urls, ensuring the proxy endpoint is always protected.
  2. Generates a random authentication key if none is provided, logging a warning with the key for visibility.

This security-by-default approach is appropriate, though note that it introduces side effects (mutation) during validation, which is unconventional but pragmatic here.

tests/test_plugins.py (5)

8-9: LGTM: Import added for proxy testing.

The on_proxy import is correctly placed and aligns with the new proxy function system being tested.


153-162: LGTM: Test models are well-structured.

The SubModel and ExchangeModel classes provide good test coverage for complex nested Pydantic model serialization through the proxy system.


169-171: LGTM: Remote-only test function correctly structured.

The RuntimeError and unused parameters are intentional—this function validates that remote proxy calls bypass local execution. The static analysis warnings (ARG001, TRY003) are false positives in this test context.


174-183: LGTM: Comprehensive local proxy test.

The test properly validates local execution, including nested Pydantic model serialization and deserialization through the proxy system.


186-195: LGTM: Remote proxy test validates expected behavior.

The test correctly validates the remote proxy execution path, checking that the function name and data are properly encoded and returned in the expected response format.

src/framex/plugin/__init__.py (4)

79-86: LGTM: Flexible API parameter handling.

Accepting both str and PluginApi improves the API's flexibility for programmatic use while maintaining backward compatibility. The resolution logic correctly handles both cases.


89-102: LGTM: Robust proxy fallback mechanism.

The automatic proxy creation for missing HTTP endpoints (paths starting with "/") provides graceful degradation. The warning log helps with debugging, and the error message for non-proxy cases is clear.


103-104: LGTM: Correct proxy detection.

The use_proxy flag is properly set based on the API's call type.


120-132: LGTM: Comprehensive proxy response validation.

The response handling properly validates proxy responses:

  • Non-dict results pass through directly
  • Dict responses require status field and validation against legal_proxy_code
  • Appropriate logging for errors and edge cases
src/framex/plugins/proxy/__init__.py (7)

12-12: LGTM: Necessary imports for proxy function system.

The new imports (PROXY_FUNC_HTTP_PATH, ProxyFunc, ProxyFuncHttpBody, cache_decode, cache_encode) are all utilized in the proxy function registration and invocation logic.

Also applies to: 19-20


38-38: LGTM: Instance fields for proxy function management.

The proxy_func_map registry and init_proxy_func_route flag are appropriate for tracking registered proxy functions and route initialization state.

Also applies to: 40-40


66-69: LGTM: Proper async context manager usage.

The HTTP client is correctly managed with async with, ensuring proper resource cleanup.


142-163: LGTM: Proxy function route registration is well-structured.

The method correctly registers the HTTP endpoint for proxy function calls using the standardized path constant and appropriate parameters.


165-179: LGTM: Remote proxy function parsing is correctly implemented.

The method properly creates dynamic proxy methods for remote functions, uses ProxyFuncHttpBody for HTTP payloads, and registers them with the is_remote=True flag.


192-199: LGTM: Stream handling with proper cleanup.

The streaming logic correctly uses async with for the stream context and ensures the client is closed after the generator completes. The pattern is valid for streaming responses.


202-208: LGTM: Robust non-streaming response handling.

The method correctly uses an async context manager and gracefully handles both JSON and text responses with appropriate exception handling.

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: 1

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

63-64: XOR obfuscation is not cryptographically secure.

The xor_crypt function uses a fixed key for XOR-based transformation, which provides obfuscation rather than cryptographic security. If the encoded data includes sensitive information or is transmitted over untrusted channels, consider using proper encryption (e.g., cryptography library with AES-GCM).

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffa9438 and 7288013.

📒 Files selected for processing (4)
  • src/framex/plugin/on.py
  • src/framex/plugins/proxy/__init__.py
  • src/framex/utils.py
  • tests/test_plugins.py
🧰 Additional context used
🧬 Code graph analysis (2)
tests/test_plugins.py (1)
src/framex/plugin/on.py (1)
  • on_proxy (101-163)
src/framex/plugins/proxy/__init__.py (5)
src/framex/plugin/on.py (1)
  • on_request (63-98)
src/framex/plugins/proxy/config.py (1)
  • ProxyPluginConfig (9-37)
src/framex/plugins/proxy/model.py (2)
  • ProxyFunc (7-9)
  • ProxyFuncHttpBody (12-14)
src/framex/utils.py (2)
  • cache_decode (93-132)
  • cache_encode (67-90)
src/framex/adapter/base.py (2)
  • get_handle (53-53)
  • call_func (29-39)
🪛 Ruff (0.14.10)
src/framex/utils.py

99-99: Do not catch blind exception: Exception

(BLE001)


105-105: Do not catch blind exception: Exception

(BLE001)


123-123: Do not catch blind exception: Exception

(BLE001)

tests/test_plugins.py

171-171: Avoid specifying long messages outside the exception class

(TRY003)

src/framex/plugins/proxy/__init__.py

292-292: Avoid specifying long messages outside the exception class

(TRY003)

src/framex/plugin/on.py

114-114: Do not catch blind exception: Exception

(BLE001)


132-132: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (16)
src/framex/plugin/on.py (3)

101-110: Clean setup and lazy import pattern.

The decorator initialization is well-structured. The lazy import of settings and early return when proxy is disabled are appropriate design choices.


111-125: Resilient callable wrapper with intentional fallback.

The safe_callable wrapper appropriately handles various function wrapping scenarios (classmethod, staticmethod, wrapped functions) with a fallback mechanism. The broad exception handling on line 114 is intentional for this fallback pattern, so the static analysis warning can be safely ignored.


127-161: Well-structured proxy invocation pattern.

The wrapper correctly enforces keyword-only arguments (line 132), registers the function with the proxy deployment on first invocation, and routes subsequent calls through the proxy system with appropriate encoding. The registration and invocation flow integrates cleanly with the proxy infrastructure.

Note: A past review comment already identified a potential race condition with the is_registered flag (lines 134-146) and provided a detailed fix using asyncio.Lock. Consider addressing that if concurrent registration is a concern.

tests/test_plugins.py (4)

153-161: Test models appropriately structured.

The SubModel and ExchangeModel definitions effectively test nested Pydantic model serialization and deserialization through the proxy system. The varying field types validate the encoding utilities handle different data structures correctly.


164-171: Proxy function test fixtures appropriately designed.

The local_exchange_key_value and remote_exchange_key_value functions correctly model local vs. remote execution patterns. The RuntimeError in the remote function (line 171) is intentional—it validates that the function is invoked through the proxy system rather than directly. The static analysis warning about unused arguments (line 170) can be safely ignored.


174-184: Local proxy call test thoroughly validates nested structures.

The test correctly validates that the local proxy function preserves data integrity through the proxy system, including nested Pydantic models. The assertions (lines 178-184) appropriately verify both primitive types and nested model attributes.


187-196: Remote proxy call test validates routing and encoding.

The test appropriately validates that the remote proxy invocation correctly routes the function call, preserving the function name and argument data through the encoding/decoding pipeline. The assertions verify both the routing metadata and the transmitted data integrity.

src/framex/utils.py (2)

67-90: Encoding pipeline appropriately structures dynamic objects.

The cache_encode function correctly handles various Python types, including nested structures and Pydantic models. The exclusion of private attributes (line 70) and the multi-layer encoding pipeline (JSON → compression → XOR → base64) provide reasonable size reduction and obfuscation.

The dynamic_obj structure (lines 71-76) stores module and class information for later reconstruction—ensure that the decoding side validates this metadata against an allowlist if the data source is untrusted.


93-107: Decoding pipeline correctly reverses the encoding chain.

The multi-stage decoding loop (lines 95-106) appropriately handles the layered encoding (base64 → XOR → decompression → JSON). The broad exception handling on lines 99 and 105 is intentional to gracefully handle partially-encoded or plain data, making the static analysis warnings false positives in this context.

src/framex/plugins/proxy/__init__.py (7)

38-40: Proxy function infrastructure initialized.

The new fields proxy_func_map and init_proxy_func_route appropriately support the proxy function registration and routing system.


52-57: Proxy functions registration logic correctly structured.

The conditional check (line 52) and iteration properly handle the presence or absence of proxy functions, with appropriate debug logging. This addresses the for-else logic issue from previous review comments.


65-69: HTTP client management follows best practices.

The use of async with httpx.AsyncClient() context manager (lines 66-69) ensures proper resource cleanup for the OpenAPI docs fetch operation.


195-211: HTTP client lifecycle appropriately managed.

The fetch_response method correctly handles HTTP client lifecycle for both streaming (lines 195-202) and non-streaming (lines 205-211) scenarios. The streaming case creates a client that's explicitly closed after the generator completes, while the non-streaming case uses a context manager for automatic cleanup.


142-182: Proxy function routing infrastructure well-structured.

The proxy function routing implementation cleanly separates concerns:

  • register_proxy_func_route (lines 142-163) registers the HTTP endpoint
  • _proxy_func_route (lines 165-166) provides a simple forwarding handler
  • _parse_proxy_function (lines 168-182) creates remote proxy function wrappers

The integration with the adapter's route registration system is appropriate.


266-278: Proxy function registration with duplicate detection.

The method appropriately checks for duplicate registrations (lines 272-274) and logs function registration. The duplicate check prevents re-registering already-known functions in the proxy_func_map.

Note: A past review comment identified a potential race condition with the init_proxy_func_route flag (lines 269-271) and provided a detailed fix using asyncio.Lock. Consider addressing that if concurrent registration is a concern.


280-292: Proxy function invocation routing implemented correctly.

The method appropriately differentiates between remote (lines 284-285) and local (lines 286-287) proxy function invocations, with clear logging (line 289) and error handling for unregistered functions (line 292).

Note: This method uses cache_decode (lines 281-282) to deserialize the function name and arguments. As flagged in the src/framex/utils.py review, cache_decode has security implications when processing data from untrusted sources due to dynamic imports. Ensure appropriate authentication and validation are in place for the proxy endpoints.

Comment thread src/framex/utils.py
Comment on lines +108 to +131
def restore_models(item: Any) -> Any:
if isinstance(item, list):
return [restore_models(i) for i in item]

if isinstance(item, dict):
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()})

return {k: restore_models(v) for k, v in item.items()}

return item

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

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the broader context of the restore_models function
cat -n src/framex/utils.py | head -150

Repository: touale/FrameX-kit

Length of output: 5311


🏁 Script executed:

# Find where restore_models and cache_decode are used
rg -n "restore_models|cache_decode" --type py

Repository: touale/FrameX-kit

Length of output: 1302


🏁 Script executed:

# Check the on_proxy decorator and how the proxy system works
rg -n "on_proxy|@proxy" --type py -A 5

Repository: touale/FrameX-kit

Length of output: 2313


🏁 Script executed:

# Look at the on_proxy decorator implementation
sed -n '101,200p' src/framex/plugin/on.py

Repository: touale/FrameX-kit

Length of output: 3633


🏁 Script executed:

# Check the proxy plugin implementation around cache_decode usage
sed -n '270,290p' src/framex/plugins/proxy/__init__.py

Repository: touale/FrameX-kit

Length of output: 1130


🏁 Script executed:

# Check how proxy function results are handled - are they encoded back?
rg -n "return.*cache_encode|cache_encode.*return" --type py -B 2 -A 2

Repository: touale/FrameX-kit

Length of output: 496


🏁 Script executed:

# Check if there are any tests that show how proxy functions return data
cat tests/test_plugins.py | head -250

Repository: touale/FrameX-kit

Length of output: 7630


🏁 Script executed:

# Let me check what the xor_crypt function actually does and if it's used for security
sed -n '63,91p' src/framex/utils.py

Repository: touale/FrameX-kit

Length of output: 1205


Critical: Arbitrary code execution risk via dynamic imports on decryptable, untrusted data.

Lines 115-116 use importlib.import_module and getattr to dynamically reconstruct objects based on __module__ and __class__ fields from decoded data. The encoding uses xor_crypt with a hardcoded key visible in source code—this is obfuscation, not encryption. An attacker with access to the codebase can:

  1. Craft malicious JSON with arbitrary __module__ and __class__ values
  2. Encrypt it using the known hardcoded XOR key
  3. Inject the payload as a proxy function result or other data path
  4. Trigger importlib.import_module to load any installed module
  5. Use getattr to retrieve any class and instantiate it with attacker-controlled data, executing code via __init__

Required mitigation:

  • Replace XOR encryption with authenticated encryption (e.g., using secrets module with HMAC-SHA256)
  • Implement a strict allowlist of permitted modules and classes before importlib.import_module
  • Validate decoded dynamic_obj entries against the allowlist and reject unrecognized entries
  • Consider using JSON-only serialization for untrusted data paths without dynamic object restoration
🧰 Tools
🪛 Ruff (0.14.10)

123-123: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In src/framex/utils.py around lines 108 to 131, restore_models currently
performs dynamic import/module/class lookup using untrusted "__module__" and
"__class__" fields and relies on XOR obfuscation with a hardcoded key; replace
this with a safe flow: remove or disable automatic import/getattr for untrusted
data and enforce a strict allowlist mapping of permitted module+class names to
constructor callables, validate the decoded "__type__" entries against that
allowlist and reject or return a safe serialized fallback for anything not
explicitly allowed, and remove the hardcoded XOR approach in favor of
authenticated integrity checks (e.g., HMAC or AEAD) so only verified payloads
are processed; ensure any allowed class construction uses validated,
type-checked cleaned_data and avoid calling arbitrary __init__ on untrusted
input.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
tests/test_utils.py (1)

59-64: Remove duplicate imports.

Lines 59-63 duplicate imports already declared at lines 1-5 (json, Any, pytest, BaseModel). Remove these duplicate lines to improve code cleanliness.

🔎 Proposed fix
-
-
-from datetime import datetime
-from typing import Any
-
-import pytest
-from pydantic import BaseModel
-
-
 class SubModel(BaseModel):
     id: int
     name: str
src/framex/plugin/on.py (1)

134-146: Consider adding synchronization for registration.

The is_registered flag is checked and set without synchronization. If multiple coroutines invoke the same proxy function concurrently before registration completes, multiple registration attempts could occur.

While duplicate registrations may be idempotent, consider using an asyncio.Lock to ensure only one coroutine performs the registration.

🔎 Proposed fix using asyncio.Lock
+import asyncio
+
 def on_proxy() -> Callable:
     def decorator(func: Callable) -> Callable:
         from framex.config import settings
 
         if not settings.server.enable_proxy:
             return func
 
         is_registered = False
         full_func_name = f"{func.__module__}.{func.__name__}"
+        registration_lock = asyncio.Lock()
 
         # ... safe_callable definition ...
 
         @functools.wraps(func)
         async def wrapper(*args: Any, **kwargs: Any) -> Any:
             nonlocal is_registered
 
             if args:  # pragma: no cover
                 raise TypeError(f"The proxy function '{func.__name__}' only supports keyword arguments.")
 
-            if not is_registered:
-                api_reg = PluginApi(
-                    deployment_name=PROXY_PLUGIN_NAME,
-                    call_type=ApiType.PROXY,
-                    func_name="register_proxy_function",
-                )
-                await call_plugin_api(
-                    api_reg,
-                    None,
-                    func_name=full_func_name,
-                    func_callable=safe_callable,
-                )
-                is_registered = True
+            async with registration_lock:
+                if not is_registered:  # Double-check after acquiring lock
+                    api_reg = PluginApi(
+                        deployment_name=PROXY_PLUGIN_NAME,
+                        call_type=ApiType.PROXY,
+                        func_name="register_proxy_function",
+                    )
+                    await call_plugin_api(
+                        api_reg,
+                        None,
+                        func_name=full_func_name,
+                        func_callable=safe_callable,
+                    )
+                    is_registered = True

Based on previous review feedback.

tests/test_plugins.py (1)

154-163: Consider extracting shared test models.

SubModel and ExchangeModel are duplicated in tests/test_utils.py (lines 66-74). Consider extracting these to tests/conftest.py as shared fixtures to reduce duplication.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7288013 and 9488998.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • src/framex/__init__.py
  • src/framex/config.py
  • src/framex/plugin/on.py
  • tests/test_cli.py
  • tests/test_plugins.py
  • tests/test_utils.py
💤 Files with no reviewable changes (1)
  • tests/test_cli.py
🧰 Additional context used
🧬 Code graph analysis (3)
tests/test_utils.py (1)
src/framex/utils.py (4)
  • StreamEnventType (46-50)
  • cache_decode (93-132)
  • cache_encode (67-90)
  • make_stream_event (53-60)
src/framex/plugin/on.py (3)
src/framex/plugin/model.py (3)
  • ApiType (22-26)
  • PluginApi (29-37)
  • PluginDeployment (41-43)
src/framex/utils.py (3)
  • cache_decode (93-132)
  • cache_encode (67-90)
  • extract_method_params (36-43)
src/framex/plugin/__init__.py (1)
  • call_plugin_api (78-133)
tests/test_plugins.py (2)
src/framex/plugin/on.py (4)
  • on_proxy (101-163)
  • wrapper (72-96)
  • wrapper (128-159)
  • wrapper (167-224)
tests/test_utils.py (2)
  • SubModel (66-68)
  • ExchangeModel (71-75)
🪛 Ruff (0.14.10)
src/framex/plugin/on.py

114-114: Do not catch blind exception: Exception

(BLE001)


132-132: Avoid specifying long messages outside the exception class

(TRY003)

tests/test_plugins.py

168-168: Avoid specifying long messages outside the exception class

(TRY003)


181-181: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (8)
src/framex/__init__.py (1)

101-101: LGTM! CPU auto-detection logic is correct.

The conditional logic correctly passes None to Ray when num_cpus <= 0, enabling auto-detection. This aligns well with the ServerConfig.num_cpus default change to -1 in src/framex/config.py.

tests/test_utils.py (1)

78-127: LGTM! Comprehensive test coverage for encoding/decoding utilities.

The test suite thoroughly covers:

  • Basic data types and nested structures
  • Datetime serialization
  • Nested Pydantic models with proper reconstruction
  • Fallback behavior when class definitions are unavailable
src/framex/config.py (2)

47-47: LGTM! Default change enables CPU auto-detection.

Changing the default from 8 to -1 enables Ray's automatic CPU detection by default, which is more flexible for different deployment environments.


64-65: LGTM! Automatic proxy path protection is sensible.

Automatically ensuring PROXY_FUNC_HTTP_PATH is in auth_urls provides defense-in-depth for the proxy function feature.

src/framex/plugin/on.py (2)

111-125: LGTM! Safe callable unwrapping logic is sound.

The safe_callable helper correctly unwraps various decorator patterns (functools.wraps, classmethod, staticmethod) and handles both sync and async functions. The broad exception catch on line 114 is intentional as a fallback mechanism to ensure the raw function is accessible for proxy registration.

Note: The static analysis warning about catching blind Exception is a false positive in this context.


131-132: Keyword-only enforcement is correct.

The check ensures proxy functions only accept keyword arguments, which is necessary for proper serialization with cache_encode. The error message is clear and contextual.

tests/test_plugins.py (2)

173-181: LGTM! Test function design is clear.

The decorator stacking correctly exercises both local and remote proxy behavior. The supply_exception wrapper simulates the remote execution path where the local function body is not executed.


184-206: LGTM! Comprehensive proxy function tests.

Both test cases thoroughly validate:

  • Local execution with nested model serialization/deserialization
  • Remote proxy routing with proper data encoding
  • Nested Pydantic model reconstruction

Comment thread src/framex/config.py
Comment on lines +66 to +71
if not self.general_auth_keys: # pragma: no cover
from framex.log import logger

key = str(uuid4())
logger.warning(f"No general_auth_keys set, generate a random key: {key}")
self.general_auth_keys = [key]
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

Security concern: Auto-generated auth key is logged and may be weak.

Auto-generating a random authentication key and logging it has security implications:

  1. The key is exposed in logs, which may be collected/stored insecurely
  2. Users may not notice the warning and leave the weak default in place
  3. The auto-generation masks a configuration error

Consider either:

  • Requiring explicit configuration and raising a clear error when general_auth_keys is empty
  • Generating a strong key without logging it and providing it through a secure channel (e.g., startup output to stdout only, not logs)
🔎 Alternative approach: Require explicit configuration
         if not self.general_auth_keys:  # pragma: no cover
             from framex.log import logger
 
-            key = str(uuid4())
-            logger.warning(f"No general_auth_keys set, generate a random key: {key}")
-            self.general_auth_keys = [key]
+            raise ValueError(
+                "general_auth_keys must be explicitly configured for authentication. "
+                "Please set at least one auth key in your configuration."
+            )
🤖 Prompt for AI Agents
In src/framex/config.py around lines 66-71, the code auto-generates an auth key
and writes it to the logger which exposes secrets and masks a config error;
change behavior to require explicit configuration by raising a clear exception
when general_auth_keys is empty (include a message that instructs how to provide
keys via env/config) OR if you must auto-generate for convenience, generate a
cryptographically strong key (secure random, sufficient length) and do NOT log
it — instead print a single non-persisted startup notice to stdout informing the
operator that a key was generated and where to find it (or write it to a secure
file path), and set self.general_auth_keys to that value; remove logger.warning
that prints the secret.

Comment thread tests/test_plugins.py
Comment on lines +165 to +170
def supply_execption(func):
@wraps(func)
async def wrapper(*_: Any, **__: Any) -> None:
raise RuntimeError("I am def supply_execption(func): exception")

return wrapper
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

Typo in function name: supply_execption should be supply_exception.

The function name has a typo that should be corrected for clarity.

🔎 Proposed fix
-def supply_execption(func):
+def supply_exception(func):
     @wraps(func)
     async def wrapper(*_: Any, **__: Any) -> None:
         raise RuntimeError("I am def supply_execption(func): exception")
 
     return wrapper

Also update line 179:

 @on_proxy()
-@supply_execption
+@supply_exception
 async def remote_exchange_key_value(a_str: str, b_int: int, c_model: ExchangeModel) -> Any:  # noqa: ARG001

And update the error message on line 168:

-        raise RuntimeError("I am def supply_execption(func): exception")
+        raise RuntimeError("I am def supply_exception(func): exception")

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.10)

168-168: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In tests/test_plugins.py around lines 165 to 170, the helper function is
misspelled as supply_execption; rename it to supply_exception, update its
internal RuntimeError message to reflect the corrected name (line ~168), and
update any references/calls to this function (e.g., the use at line ~179) to use
supply_exception so names and messages are consistent.

@touale touale merged commit d68cc6b into master Dec 23, 2025
6 of 7 checks passed
@touale touale deleted the feat/support-on-proxy branch December 24, 2025 01:48
This was referenced Dec 25, 2025
This was referenced Mar 26, 2026
@coderabbitai coderabbitai bot mentioned this pull request Apr 17, 2026
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