Skip to content

fix: lazy-load Rich, thread-safe agents globals, DRY approval, proper exceptions#1155

Merged
MervinPraison merged 17 commits into
masterfrom
fix/lazy-rich-thread-safety-dry-approval
Mar 30, 2026
Merged

fix: lazy-load Rich, thread-safe agents globals, DRY approval, proper exceptions#1155
MervinPraison merged 17 commits into
masterfrom
fix/lazy-rich-thread-safety-dry-approval

Conversation

@MervinPraison

Copy link
Copy Markdown
Owner

Summary

Validates and fixes 4 real issues from the issue tracker, skipping 8+ that were architectural wishlists or already resolved.

Issues validated but skipped (not real bugs / would bloat SDK):

Test plan

  • All 6 modified files pass py_compile syntax check
  • 179 unit tests pass (6 pre-existing failures unrelated to changes)
  • Verified lazy loading: _rich_loaded is False after import, True only after first display call
  • Manual test: run an agent workflow in verbose mode to confirm Rich display still works
  • Manual test: concurrent Agents.launch(mode="http") calls to verify thread safety

https://claude.ai/code/session_01DUewsjMPzxcU6THm9wc56p

eltociear and others added 16 commits March 20, 2026 13:17
…nfig (CWE-78)

The --schedule-config YAML file allows setting arbitrary environment
variables via the "environment" section. An attacker who can supply or
modify this file could set keys like LD_PRELOAD, PATH, PYTHONPATH, etc.
to achieve arbitrary code execution.

Add _validate_env_key() with a blocklist of security-sensitive env var
names (dynamic linker, search paths, proxies) and call it before each
os.environ assignment in the schedule-config loading path.

Also add unit tests for the validation function.
…#1147, #1145)

Extract ~120 lines of duplicated approval code from _execute_tool_impl()
and execute_tool_async() into shared helpers _check_tool_approval_sync()
and _check_tool_approval_async(). Add threading.Lock to protect
_agent_counter, _server_started, _registered_agents, and _shared_apps
against concurrent access in multi-agent scenarios.

https://claude.ai/code/session_01WiGKiwTBj7xo5YoF3PYjWF
…ection

Merging β€” verified locally, both vulnerable occurrences are patched, all tests pass. Great contribution! πŸŽ‰
…y-thread-safety

fix: deduplicate approval logic and add thread-safety (#1147, #1145)
…on, root/deployment type guards

- Reject empty strings, keys containing "=" or NUL in _validate_env_key()
  (CodeRabbit: malformed keys should fail during validation, not at os.environ)
- Validate root config is a dict after yaml.safe_load() (handles list/scalar/None)
- Validate deployment section is a dict before .get() calls
- Both raise ValueError caught by the existing handler β†’ clean CLI error
- Add test_malformed_env_keys_rejected, test_non_dict_root_config_rejected,
  test_non_dict_deployment_rejected (7 tests total, all passing)
#1134: Decouple execute_code from optional deps (black/pylint/autopep8)
- execute_code is now a standalone module-level function
- PythonTools class retained for analyze/format/lint (lazy-init)
- Agents can now use execute_code without heavy optional deps

#1129: Thread-safety for remaining global mutable state
- Add _lazy_import_lock with double-checked locking for all 6 lazy-import
  caches (_rich_console, _rich_live, _llm_module, _main_module, etc.)
- Add _env_cache_lock for class-level env var caches
  (_env_output_mode, _default_model)

#1137: Clarify misleading TODO comment
- Updated TODO in llm.py to clarify this is code-level DRY, not
  duplicate API calls per request
fix: Tier 1 security & concurrency fixes (#1134, #1129, #1137)
LiteLLM v1.82.7 and v1.82.8 contained a credential stealer from a
supply chain attack (compromised PyPI maintainer via Trivy CI/CD).
Both versions removed from PyPI.

PraisonAI was NOT affected (installed 1.81.1), but as a defensive
measure, cap the upper bound at 1.82.6 (latest safe) until LiteLLM
completes their supply-chain review and resumes releases.

Ref: https://docs.litellm.ai/blog/security-update-march-2026
security: cap litellm<=1.82.6 (supply chain incident)
… guards

fix: harden env key validation β€” malformed keys, root/deployment type guards
Updated the version number in pyproject.toml and uv.lock to reflect the new release.
… exceptions (#1154, #1145, #1147, #1128)

- Lazy-load Rich imports in main.py, agents.py, knowledge.py, video_agent.py
  to avoid ~50-100ms import overhead when display isn't used
- Add _agents_server_lock to protect shared mutable state in agents.py
  (mirrors existing pattern in agent.py)
- Extract _build_approval_request() and _finalize_approval() to deduplicate
  sync/async approval logic
- Replace bare except: with except Exception: + logging in llm.py

https://claude.ai/code/session_01DUewsjMPzxcU6THm9wc56p
@coderabbitai

coderabbitai Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c601347-dd19-445b-b2f0-1ce40de83874

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review
✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/lazy-rich-thread-safety-dry-approval

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.

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Thread-safe lazy imports, DRY approval logic, Rich lazy-loading, and CWE-78 env-var validation

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
β€’ Add thread-safe locks to protect shared mutable state in agent/agents APIs
  - _lazy_import_lock for lazy-loaded Rich/LLM modules
  - _server_lock for HTTP server registration and startup
  - _agent_counter_lock for unique agent display name generation
  - _env_cache_lock for environment variable caching
β€’ Deduplicate ~120 lines of approval logic into shared helpers
  - Extract _build_approval_request(), _finalize_approval(), _check_tool_approval_sync(),
  _check_tool_approval_async()
  - Reduce code duplication between sync/async tool execution paths
β€’ Implement lazy-loading for Rich display library in main.py
  - Add _ensure_rich() function to defer Rich imports until first display call
  - Reduces import overhead from ~420ms to ~20ms for silent mode
β€’ Fix CWE-78 environment variable injection vulnerability in schedule config
  - Add _validate_env_key() blocklist for dangerous keys (LD_PRELOAD, PATH, PYTHONPATH, etc.)
  - Validate all env vars before applying (fail-closed pattern)
  - Add comprehensive unit tests for validation
β€’ Refactor python_tools.py to make execute_code standalone
  - Move execute_code() outside PythonTools class (no optional deps required)
  - Keep analyze/format/lint functions in class (requires black/pylint/autopep8)
  - Lazy-initialize PythonTools only when optional tools are needed
β€’ Update version to 4.5.85 and fix litellm dependency constraint
Diagram
flowchart LR
  A["Shared State<br/>Mutable Dicts"] -->|"Protected by<br/>threading.Lock"| B["Thread-Safe<br/>Access"]
  C["Rich/LLM<br/>Imports"] -->|"Lazy-loaded<br/>on first use"| D["Reduced<br/>Import Time"]
  E["Sync/Async<br/>Approval Logic"] -->|"Extracted to<br/>Shared Helpers"| F["DRY Code<br/>120 lines saved"]
  G["Schedule Config<br/>YAML"] -->|"Validated by<br/>_validate_env_key"| H["CWE-78<br/>Blocked"]
  I["execute_code<br/>Function"] -->|"Standalone<br/>No deps"| J["Always Available"]
Loading

Grey Divider

File Changes

1. src/praisonai-agents/praisonaiagents/agent/agent.py Thread safety, lazy loading, code deduplication +261/-246

Thread-safe lazy imports and approval logic deduplication

src/praisonai-agents/praisonaiagents/agent/agent.py


2. src/praisonai-agents/praisonaiagents/agent/video_agent.py Lazy loading, performance optimization +5/-3

Lazy-load Rich console and progress components

src/praisonai-agents/praisonaiagents/agent/video_agent.py


3. src/praisonai-agents/praisonaiagents/agents/agents.py Thread safety, concurrency protection +60/-55

Add thread-safe lock for shared server state

src/praisonai-agents/praisonaiagents/agents/agents.py


View more (17)
4. src/praisonai-agents/praisonaiagents/knowledge/knowledge.py Lazy loading, performance optimization +3/-2

Lazy-load Rich progress components in store method

src/praisonai-agents/praisonaiagents/knowledge/knowledge.py


5. src/praisonai-agents/praisonaiagents/llm/llm.py Error handling, bug fix +7/-3

Replace bare except with proper exception handling

src/praisonai-agents/praisonaiagents/llm/llm.py


6. src/praisonai-agents/praisonaiagents/main.py Lazy loading, performance optimization +45/-16

Lazy-load all Rich dependencies on first display call

src/praisonai-agents/praisonaiagents/main.py


7. src/praisonai-agents/praisonaiagents/tools/python_tools.py Code refactoring, dependency management +299/-257

Refactor execute_code as standalone function, lazy-init optional tools

src/praisonai-agents/praisonaiagents/tools/python_tools.py


8. src/praisonai-agents/tests/unit/tools/test_python_tools_sandbox.py Tests, refactoring +5/-5

Update test harness for standalone execute_code function

src/praisonai-agents/tests/unit/tools/test_python_tools_sandbox.py


9. src/praisonai/praisonai/cli/main.py Security fix, input validation +68/-1

Add CWE-78 environment variable validation for schedule config

src/praisonai/praisonai/cli/main.py


10. src/praisonai/praisonai/deploy.py Version bump, configuration changes +1/-1

Update version to 4.5.85 in Dockerfile

src/praisonai/praisonai/deploy.py


11. src/praisonai/praisonai/version.py Version bump +1/-1

Update version from 4.5.83 to 4.5.85

src/praisonai/praisonai/version.py


12. src/praisonai/tests/unit/test_cwe78_env_injection.py Tests, security validation +193/-0

Add comprehensive unit tests for env-var validation

src/praisonai/tests/unit/test_cwe78_env_injection.py


13. src/praisonai/praisonai.rb Version bump, configuration changes +2/-2

Update Homebrew formula version to 4.5.85

src/praisonai/praisonai.rb


14. README.md πŸ“ Documentation +1/-1

Fix navigation link from Development to Contributing

README.md


15. docker/Dockerfile.chat Version bump, configuration changes +1/-1

Update praisonai version constraint to 4.5.85

docker/Dockerfile.chat


16. docker/Dockerfile.dev Version bump, configuration changes +1/-1

Update praisonai version constraint to 4.5.85

docker/Dockerfile.dev


17. docker/Dockerfile.ui Version bump, configuration changes +1/-1

Update praisonai version constraint to 4.5.85

docker/Dockerfile.ui


18. src/praisonai-agents/pyproject.toml Version bump, dependencies +3/-3

Update version and add litellm upper bound constraint

src/praisonai-agents/pyproject.toml


19. src/praisonai/README.md πŸ“ Documentation +1/-1

Fix navigation link from Development to Contributing

src/praisonai/README.md


20. src/praisonai/pyproject.toml Version bump, dependencies +2/-2

Update version and add litellm upper bound constraint

src/praisonai/pyproject.toml


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Mar 26, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) πŸ“˜ Rule violations (0) πŸ“Ž Requirement gaps (4) πŸ“ Spec deviations (0)

Grey Divider


Action required

1. execute_code calls exec πŸ“Ž Requirement gap ⛨ Security
Description
execute_code still executes user-supplied code in-process via exec()/eval() and is not
integrated with a SandboxProtocol implementation. This fails the requirement for
sandboxed-by-default execution and leaves the host process exposed if the in-process controls are
bypassed.
Code

src/praisonai-agents/praisonaiagents/tools/python_tools.py[R203-218]

+            # Compile code with restricted mode
+            compiled_code = compile(code, '<string>', 'exec')
+
+            # Execute with output capture
+            with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
+                exec(compiled_code, globals_dict, locals_dict)
+
+                # Get last expression value if any
+                import ast
+                tree = ast.parse(code)
+                if tree.body and isinstance(tree.body[-1], ast.Expr):
+                    result = eval(
+                        compile(ast.Expression(tree.body[-1].value), '<string>', 'eval'),
+                        globals_dict,
+                        locals_dict
+                    )
Evidence
Compliance ID 10 prohibits in-process exec() without routing execution through an explicit sandbox
mechanism integrated with SandboxProtocol; the PR’s updated execute_code compiles and executes
untrusted code directly using exec(...) and then evaluates expressions with eval(...).

Python exec tool is sandboxed by default and integrated with SandboxProtocol
src/praisonai-agents/praisonaiagents/tools/python_tools.py[85-218]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`execute_code()` runs arbitrary code using in-process `exec()`/`eval()` rather than routing execution through a `SandboxProtocol`-backed sandbox. This violates the sandboxing-by-default requirement.

## Issue Context
Even with restricted `__builtins__` and AST checks, in-process execution remains risky and does not satisfy the protocol-integration requirement.

## Fix Focus Areas
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[85-218]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. execute_code ignores timeout πŸ“Ž Requirement gap ⛨ Security
Description
The execute_code API exposes a timeout parameter but does not enforce any timeout or resource
limits on execution. This allows untrusted code to run indefinitely (e.g., infinite loops) and can
cause denial-of-service of the host process.
Code

src/praisonai-agents/praisonaiagents/tools/python_tools.py[R86-92]

+def execute_code(
+    code: str,
+    globals_dict: Optional[Dict[str, Any]] = None,
+    locals_dict: Optional[Dict[str, Any]] = None,
+    timeout: int = 30,
+    max_output_size: int = 10000
+) -> Dict[str, Any]:
Evidence
Compliance ID 11 requires sandbox execution to enforce timeouts/resource limits; execute_code
defines timeout: int = 30 but the implementation proceeds to compile/execute code without using
the timeout value or any limiting mechanism.

Sandboxed Python execution enforces resource limits and timeouts
src/praisonai-agents/praisonaiagents/tools/python_tools.py[86-92]
src/praisonai-agents/praisonaiagents/tools/python_tools.py[202-218]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`execute_code()` accepts a `timeout` parameter but does not actually enforce it, nor does it enforce CPU/memory limits.

## Issue Context
Without enforcement, user code can hang indefinitely or consume unbounded resources, which violates the sandbox resource-limits requirement.

## Fix Focus Areas
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[86-92]
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[202-218]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. PythonTools init returns None 🐞 Bug βœ“ Correctness
Description
_get_python_tools() returns _python_tools_instance even when it is initialized to None, so
analyze_code/format_code/lint_code/disassemble_code crash with AttributeError on first call.
This makes the new lazy optional-dependency wrappers unusable.
Code

src/praisonai-agents/praisonaiagents/tools/python_tools.py[R525-550]

+def _get_python_tools():
+    """Lazy-init PythonTools (requires black/pylint/autopep8)."""
+    global _python_tools_instance
+    try:
+        return _python_tools_instance
+    except NameError:
+        _python_tools_instance = PythonTools()
+        return _python_tools_instance
+
+_python_tools_instance = None
+
+def analyze_code(code: str) -> Optional[Dict[str, Any]]:
+    """Analyze Python code structure and quality. Requires: pip install black pylint autopep8"""
+    return _get_python_tools().analyze_code(code)
+
+def format_code(code: str, style: str = 'black', line_length: int = 88) -> Optional[str]:
+    """Format Python code. Requires: pip install black pylint autopep8"""
+    return _get_python_tools().format_code(code, style, line_length)
+
+def lint_code(code: str) -> Optional[Dict[str, List[Dict[str, Any]]]]:
+    """Lint Python code. Requires: pip install black pylint autopep8"""
+    return _get_python_tools().lint_code(code)
+
+def disassemble_code(code: str) -> Optional[str]:
+    """Disassemble Python code to bytecode. Requires: pip install black pylint autopep8"""
+    return _get_python_tools().disassemble_code(code)
Evidence
_python_tools_instance is explicitly set to None, and _get_python_tools() only constructs
PythonTools() on NameError (which will never occur), so wrapper functions dereference None.

src/praisonai-agents/praisonaiagents/tools/python_tools.py[523-550]
src/praisonai-agents/praisonaiagents/tools/python_tools.py[534-550]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`_get_python_tools()` never instantiates `PythonTools()` because `_python_tools_instance` is defined as `None` and the function only instantiates on `NameError`. As a result, `analyze_code/format_code/lint_code/disassemble_code` call methods on `None`.

### Issue Context
This was introduced as part of the refactor to make optional-dependency tools lazy.

### Fix Focus Areas
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[525-550]

### What to change
- Replace the `try/except NameError` pattern with an explicit `if _python_tools_instance is None:` check.
- Instantiate and cache `PythonTools()` when the cached instance is `None`.
- (Optional) add a small lock if these wrappers can be called concurrently from multiple threads.

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. execute_code export broken 🐞 Bug βœ“ Correctness
Description
praisonaiagents.tools.__getattr__ still resolves execute_code as a PythonTools instance
method, but execute_code was moved to a module-level function and PythonTools no longer defines
it. Importing execute_code from praisonaiagents.tools will now raise AttributeError (and may
also trigger unwanted optional-dependency checks).
Code

src/praisonai-agents/praisonaiagents/tools/python_tools.py[R267-289]

+class PythonTools:
+    """Tools for Python code analysis, formatting, and linting.
+
+    Requires: pip install black pylint autopep8
+    For code execution only, use the standalone execute_code() function.
+    """
+
+    def __init__(self):
+        """Initialize PythonTools β€” checks for required packages."""
+        self._check_dependencies()
+
+    def _check_dependencies(self):
+        """Check if required packages are installed."""
+        missing = []
+        for package in ['black', 'pylint', 'autopep8']:
+            if util.find_spec(package) is None:
+                missing.append(package)
+
+        if missing:
+            raise ImportError(
+                f"Required packages not available. Please install: {', '.join(missing)}\n"
+                f"Run: pip install {' '.join(missing)}"
+            )
Evidence
After the refactor, execute_code exists only as a top-level function while PythonTools contains
analysis/format/lint methods only. However, tools/__init__.py still maps execute_code to the
PythonTools class and __getattr__ retrieves it via getattr(instance, name), which will fail.
The repo also contains code importing execute_code from praisonaiagents.tools, which will break
at runtime.

src/praisonai-agents/praisonaiagents/tools/python_tools.py[85-110]
src/praisonai-agents/praisonaiagents/tools/python_tools.py[262-320]
src/praisonai-agents/praisonaiagents/tools/init.py[60-69]
src/praisonai-agents/praisonaiagents/tools/init.py[254-263]
src/praisonai-agents/tests/programming-agent.py[1-9]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`from praisonaiagents.tools import execute_code` is now broken because `TOOL_MAPPINGS` still points `execute_code` to the `PythonTools` class, but `execute_code` is no longer a `PythonTools` method.

### Issue Context
The PR refactored `execute_code` into a standalone function to avoid optional deps. The tools package lazy loader still assumes Python tools are class methods.

### Fix Focus Areas
- src/praisonai-agents/praisonaiagents/tools/__init__.py[60-69]
- src/praisonai-agents/praisonaiagents/tools/__init__.py[254-263]

### What to change
- In `TOOL_MAPPINGS`, map `execute_code` to `('.python_tools', None)` so `__getattr__` returns the module-level function.
- Consider mapping `analyze_code/format_code/lint_code/disassemble_code` to `('.python_tools', None)` as well, so the module-level wrappers control lazy instantiation (instead of instantiating `PythonTools()` during attribute access).
- Add/adjust a small unit test that `from praisonaiagents.tools import execute_code` succeeds without black/pylint/autopep8 installed.

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. _get_default_model uses os.getenv πŸ“Ž Requirement gap βš™ Maintainability
Description
The modified Agent env-cache methods still read configuration directly from
os.environ/os.getenv instead of using a unified configuration resolver entry point. This
perpetuates scattered config resolution logic and makes consistent precedence/validation difficult.
Code

src/praisonai-agents/praisonaiagents/agent/agent.py[R206-223]

    def _get_env_output_mode(cls):
-        """Get cached PRAISONAI_OUTPUT env var value."""
+        """Get cached PRAISONAI_OUTPUT env var value (thread-safe)."""
        if not cls._env_output_checked:
-            cls._env_output_mode = os.environ.get('PRAISONAI_OUTPUT', '').lower()
-            cls._env_output_checked = True
+            with cls._env_cache_lock:
+                if not cls._env_output_checked:
+                    cls._env_output_mode = os.environ.get('PRAISONAI_OUTPUT', '').lower()
+                    cls._env_output_checked = True
        return cls._env_output_mode
    
    @classmethod
    def _get_default_model(cls):
-        """Get cached default model name from OPENAI_MODEL_NAME env var."""
+        """Get cached default model name from OPENAI_MODEL_NAME env var (thread-safe)."""
        if not cls._default_model_checked:
-            cls._default_model = os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
-            cls._default_model_checked = True
+            with cls._env_cache_lock:
+                if not cls._default_model_checked:
+                    cls._default_model = os.getenv('OPENAI_MODEL_NAME', 'gpt-4o-mini')
+                    cls._default_model_checked = True
        return cls._default_model
Evidence
Compliance ID 1 requires a single configuration resolver and forbids direct environment variable
reads outside it; the changed Agent methods still call os.environ.get('PRAISONAI_OUTPUT', ...)
and os.getenv('OPENAI_MODEL_NAME', ...).

Unified configuration resolver is the single entry point for env/config resolution
src/praisonai-agents/praisonaiagents/agent/agent.py[206-223]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Agent._get_env_output_mode()` and `Agent._get_default_model()` directly read environment variables (`os.environ.get` / `os.getenv`) on a hot path. This violates the requirement that env/config resolution goes through a single unified resolver.

## Issue Context
Even though the PR improves thread-safety around these reads, it still preserves scattered configuration resolution logic.

## Fix Focus Areas
- src/praisonai-agents/praisonaiagents/agent/agent.py[206-223]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. rich imports remain ad-hoc πŸ“Ž Requirement gap βš™ Maintainability
Description
Rich lazy-loading is still implemented separately across modules instead of being centralized in
shared display utilities. This increases the risk of inconsistent behavior and regressions in Rich
import/loading semantics across subsystems.
Code

src/praisonai-agents/praisonaiagents/agents/agents.py[R1289-1293]

        if show_verbose and is_tty:
+            from rich.console import Console
            from rich.panel import Panel
            console = Console()
            import time as time_module
Evidence
Compliance ID 9 requires centralizing Rich lazy-load access patterns in a shared module reused by
other modules; the PR keeps module-specific Rich imports (e.g., agents.py, video_agent.py,
knowledge.py) while also adding a separate _ensure_rich() loader in main.py, demonstrating
non-centralized, duplicated patterns.

Shared lazy-loaded display utilities centralize Rich access patterns
src/praisonai-agents/praisonaiagents/main.py[9-29]
src/praisonai-agents/praisonaiagents/agents/agents.py[1289-1293]
src/praisonai-agents/praisonaiagents/agent/video_agent.py[213-219]
src/praisonai-agents/praisonaiagents/knowledge/knowledge.py[788-790]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Rich lazy-loading is duplicated across multiple modules (`main.py`, `agents.py`, `video_agent.py`, `knowledge.py`) rather than being centralized behind shared display utilities.

## Issue Context
The compliance requirement expects a shared module (e.g., `praisonaiagents/display.py`) that encapsulates Rich lazy-loading and is reused by other modules to avoid drift.

## Fix Focus Areas
- src/praisonai-agents/praisonaiagents/main.py[9-29]
- src/praisonai-agents/praisonaiagents/agents/agents.py[1289-1293]
- src/praisonai-agents/praisonaiagents/agent/video_agent.py[213-219]
- src/praisonai-agents/praisonaiagents/knowledge/knowledge.py[788-790]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Unlocked endpoint list iteration 🐞 Bug β›― Reliability
Description
Agent.launch() and Agents.launch() root/health endpoints iterate *_registered_*[port].keys()
without holding the corresponding server lock, while other threads can mutate those dicts during
concurrent launch() calls. This can raise RuntimeError: dictionary changed size during iteration
and intermittently break / or /health responses.
Code

src/praisonai-agents/praisonaiagents/agent/agent.py[R8430-8443]

+                    @_shared_apps[port].get("/")
+                    async def root():
+                        return {
+                            "message": f"Welcome to PraisonAI Agents API on port {port}. See /docs for usage.",
+                            "endpoints": list(_registered_agents[port].keys())
+                        }
+
+                    # Add healthcheck endpoint
+                    @_shared_apps[port].get("/health")
+                    async def healthcheck():
+                        return {
+                            "status": "ok",
+                            "endpoints": list(_registered_agents[port].keys())
+                        }
Evidence
Although writes/registrations are protected by _server_lock/_agents_server_lock, the FastAPI
handlers read and iterate the same dicts without any lock. Concurrent launch() calls (main thread)
can mutate while the server thread handles a request, causing iteration failures.

src/praisonai-agents/praisonaiagents/agent/agent.py[8417-8460]
src/praisonai-agents/praisonaiagents/agent/agent.py[8429-8444]
src/praisonai-agents/praisonaiagents/agents/agents.py[1650-1675]
src/praisonai-agents/praisonaiagents/agents/agents.py[1680-1694]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The `/` and `/health` handlers build endpoint lists from mutable dicts (`_registered_agents[port]`, `_agents_registered_endpoints[port]`) without acquiring the corresponding lock, while other threads can mutate these dicts during endpoint registration.

### Issue Context
The PR introduced `_server_lock`/`_agents_server_lock` for write-side thread safety, but read-side iteration is still unprotected.

### Fix Focus Areas
- src/praisonai-agents/praisonaiagents/agent/agent.py[8429-8444]
- src/praisonai-agents/praisonaiagents/agent/agent.py[8459-8460]
- src/praisonai-agents/praisonaiagents/agents/agents.py[1660-1674]
- src/praisonai-agents/praisonaiagents/agents/agents.py[1692-1694]

### What to change
- Inside each root/health handler, take a snapshot under the lock (e.g., `with _server_lock: endpoints = list(_registered_agents[port].keys())`) and then return that snapshot.
- Do the same for any other `list(...keys())` / `join(list(...keys()))` diagnostics that can run concurrently with registrations.
- Keep lock hold times minimal (copy keys then release).

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

β“˜ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces thread-safe lazy loading for modules and shared server states, refactors tool approval logic, and decouples core code execution from optional dependencies. It also includes a critical security fix to prevent environment variable injection (CWE-78) via YAML configurations and bumps the package version to 1.5.85. Feedback was provided to address potential race conditions in the lazy-loading mechanisms of main.py and python_tools.py by implementing proper locking patterns.

Comment on lines +17 to +28

def _ensure_rich():
"""Lazy-load all Rich dependencies on first display call."""
global _rich_loaded, Console, Panel, Text, Markdown, Live
if _rich_loaded:
return
from rich.console import Console as _C
from rich.panel import Panel as _P
from rich.text import Text as _T
from rich.markdown import Markdown as _M
from rich.live import Live as _L
Console, Panel, Text, Markdown, Live = _C, _P, _T, _M, _L

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The lazy-loading mechanism for rich is not thread-safe. In a multi-threaded environment, multiple threads could concurrently check _rich_loaded, find it False, and then race to import and assign the global variables. This is inconsistent with the thread-safe lazy-loading patterns used elsewhere in this PR (e.g., in agent.py).

To ensure thread safety, you should use a lock. You will also need to add import threading at the top of the file.

Suggested change
def _ensure_rich():
"""Lazy-load all Rich dependencies on first display call."""
global _rich_loaded, Console, Panel, Text, Markdown, Live
if _rich_loaded:
return
from rich.console import Console as _C
from rich.panel import Panel as _P
from rich.text import Text as _T
from rich.markdown import Markdown as _M
from rich.live import Live as _L
Console, Panel, Text, Markdown, Live = _C, _P, _T, _M, _L
import threading
_rich_lock = threading.Lock()
def _ensure_rich():
"""Lazy-load all Rich dependencies on first display call (thread-safe)."""
global _rich_loaded, Console, Panel, Text, Markdown, Live
if not _rich_loaded:
with _rich_lock:
if not _rich_loaded:
from rich.console import Console as _C
from rich.panel import Panel as _P
from rich.text import Text as _T
from rich.markdown import Markdown as _M
from rich.live import Live as _L
Console, Panel, Text, Markdown, Live = _C, _P, _T, _M, _L
_rich_loaded = True

Comment on lines +525 to +534
def _get_python_tools():
"""Lazy-init PythonTools (requires black/pylint/autopep8)."""
global _python_tools_instance
try:
return _python_tools_instance
except NameError:
_python_tools_instance = PythonTools()
return _python_tools_instance

_python_tools_instance = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The lazy initializer _get_python_tools is not thread-safe. A race condition can occur if multiple threads call it simultaneously when _python_tools_instance is None. Additionally, using try...except NameError is less clear than a direct if _python_tools_instance is None: check, especially since the variable is explicitly initialized to None.

For robust thread-safe initialization, a double-checked locking pattern should be used.

_python_tools_lock = threading.Lock()

def _get_python_tools():
    """Lazy-init PythonTools (requires black/pylint/autopep8) in a thread-safe way."""
    global _python_tools_instance
    if _python_tools_instance is None:
        with _python_tools_lock:
            if _python_tools_instance is None:
                _python_tools_instance = PythonTools()
    return _python_tools_instance

_python_tools_instance = None

@qodo-code-review

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: quick-test

Failed stage: Run Fast Tests [❌]

Failed test name: ""

Failure summary:

  • The workflow ran ad-hoc Python debug commands that attempted to open YAML configs using paths that
    do not exist from the current working directory, causing hard failures:
    - FileNotFoundError:
    [Errno 2] No such file or directory: src/praisonai/tests/autogen-agents.yaml (e.g., log lines
    858-864, 1367-1370).
    - FileNotFoundError: [Errno 2] No such file or directory:
    tests/autogen-agents.yaml (log lines 991-997).
    - Later, pytest reports rootdir:
    /home/runner/work/PraisonAI/PraisonAI/src/praisonai (line 1447), so relative paths like
    src/praisonai/tests/... will not resolve unless run from repo root (path usage is inconsistent with
    the chosen rootdir/working directory).
  • The OpenAI credentials used in CI are invalid, and API calls fail with authentication errors:
    -
    Error code: 401 ... 'Incorrect API key provided' ... 'invalid_api_key' (lines 1085-1087 and
    1244-1245), which would break any tests that hit the OpenAI API.
  • The job ultimately fails at the test step because pytest collected no runnable tests and exits
    non-zero:
    - collected 0 items / 1 skipped followed by Process completed with exit code 5 (lines
    1456-1462). This indicates the test suite was fully skipped or nothing was discovered under the
    current configuration/working directory, and the pipeline treats that as a failure.
Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

723:  οΏ½[36;1m        with open(yaml_file, 'r') as f:οΏ½[0m
724:  οΏ½[36;1m            config = yaml.safe_load(f)οΏ½[0m
725:  οΏ½[36;1m        οΏ½[0m
726:  οΏ½[36;1m        # Check if any role contains 'researcher'οΏ½[0m
727:  οΏ½[36;1m        roles = config.get('roles', {})οΏ½[0m
728:  οΏ½[36;1m        for role_key, role_data in roles.items():οΏ½[0m
729:  οΏ½[36;1m            role_name = role_data.get('role', '')οΏ½[0m
730:  οΏ½[36;1m            if 'researcher' in role_key.lower() or 'researcher' in role_name.lower():οΏ½[0m
731:  �[36;1m                print(f'  🎯 FOUND in {yaml_file}:')�[0m
732:  οΏ½[36;1m                print(f'    Framework: {config.get(\"framework\", \"NOT_SET\")}')οΏ½[0m
733:  οΏ½[36;1m                print(f'    Role key: {role_key}')οΏ½[0m
734:  οΏ½[36;1m                print(f'    Role name: {role_name}')οΏ½[0m
735:  οΏ½[36;1m                print(f'    All roles: {list(roles.keys())}')οΏ½[0m
736:  οΏ½[36;1m                print()οΏ½[0m
737:  οΏ½[36;1m    except Exception as e:οΏ½[0m
738:  �[36;1m        print(f'  ❌ Error reading {yaml_file}: {e}')�[0m
739:  οΏ½[36;1mοΏ½[0m
740:  οΏ½[36;1mprint('πŸ” Checking for default configurations...')οΏ½[0m
741:  οΏ½[36;1m# Check if there are any default configs or hardcoded rolesοΏ½[0m
742:  οΏ½[36;1mtry:οΏ½[0m
743:  οΏ½[36;1m    import praisonaiοΏ½[0m
744:  οΏ½[36;1m    print(f'  PraisonAI package location: {praisonai.__file__}')οΏ½[0m
745:  οΏ½[36;1m    οΏ½[0m
746:  οΏ½[36;1m    # Check if there are any example YAML files in the packageοΏ½[0m
747:  οΏ½[36;1m    package_dir = os.path.dirname(praisonai.__file__)οΏ½[0m
748:  οΏ½[36;1m    for root, dirs, files in os.walk(package_dir):οΏ½[0m
749:  οΏ½[36;1m        for file in files:οΏ½[0m
750:  οΏ½[36;1m            if file.endswith(('.yaml', '.yml')):οΏ½[0m
751:  οΏ½[36;1m                file_path = os.path.join(root, file)οΏ½[0m
752:  οΏ½[36;1m                print(f'  πŸ“ Found YAML in package: {file_path}')οΏ½[0m
753:  οΏ½[36;1mexcept Exception as e:οΏ½[0m
754:  �[36;1m    print(f'  ❌ Error checking package: {e}')�[0m
755:  οΏ½[36;1m"οΏ½[0m
...

798:  οΏ½[36;1m    print(f'  2. AgentsGenerator framework: \"{agents_gen.framework}\"')οΏ½[0m
799:  οΏ½[36;1m    οΏ½[0m
800:  οΏ½[36;1m    # Load the YAML to check what it containsοΏ½[0m
801:  οΏ½[36;1m    import yamlοΏ½[0m
802:  οΏ½[36;1m    with open('src/praisonai/tests/autogen-agents.yaml', 'r') as f:οΏ½[0m
803:  οΏ½[36;1m        config = yaml.safe_load(f)οΏ½[0m
804:  οΏ½[36;1m    οΏ½[0m
805:  οΏ½[36;1m    framework = agents_gen.framework or config.get('framework')οΏ½[0m
806:  οΏ½[36;1m    print(f'  3. Final framework decision: \"{framework}\"')οΏ½[0m
807:  οΏ½[36;1m    print(f'  4. Available frameworks:')οΏ½[0m
808:  οΏ½[36;1m    οΏ½[0m
809:  οΏ½[36;1m    # Check framework availabilityοΏ½[0m
810:  οΏ½[36;1m    try:οΏ½[0m
811:  οΏ½[36;1m        import autogenοΏ½[0m
812:  οΏ½[36;1m        print(f'    βœ… AutoGen available')οΏ½[0m
813:  οΏ½[36;1m    except ImportError:οΏ½[0m
814:  �[36;1m        print(f'    ❌ AutoGen NOT available')�[0m
815:  οΏ½[36;1m        οΏ½[0m
816:  οΏ½[36;1m    try:οΏ½[0m
817:  οΏ½[36;1m        from praisonaiagents import AgentοΏ½[0m
818:  οΏ½[36;1m        print(f'    βœ… PraisonAI agents available')οΏ½[0m
819:  οΏ½[36;1m    except ImportError:οΏ½[0m
820:  �[36;1m        print(f'    ❌ PraisonAI agents NOT available')�[0m
821:  οΏ½[36;1m        οΏ½[0m
822:  οΏ½[36;1m    try:οΏ½[0m
823:  οΏ½[36;1m        from crewai import AgentοΏ½[0m
824:  οΏ½[36;1m        print(f'    βœ… CrewAI available')οΏ½[0m
825:  οΏ½[36;1m    except ImportError:οΏ½[0m
826:  �[36;1m        print(f'    ❌ CrewAI NOT available')�[0m
827:  οΏ½[36;1m    οΏ½[0m
828:  οΏ½[36;1m    print(f'  5. Roles in YAML: {list(config.get(\"roles\", {}).keys())}')οΏ½[0m
829:  οΏ½[36;1m    οΏ½[0m
830:  οΏ½[36;1m    # Now test the actual framework executionοΏ½[0m
831:  οΏ½[36;1m    if framework == 'autogen':οΏ½[0m
832:  οΏ½[36;1m        print(f'  6. βœ… Should execute _run_autogen')οΏ½[0m
833:  οΏ½[36;1m    elif framework == 'praisonai':οΏ½[0m
834:  �[36;1m        print(f'  6. ❌ Would execute _run_praisonai (WRONG!)')�[0m
835:  οΏ½[36;1m    else:οΏ½[0m
836:  �[36;1m        print(f'  6. ❌ Would execute _run_crewai (DEFAULT FALLBACK)')�[0m
837:  οΏ½[36;1m        οΏ½[0m
838:  οΏ½[36;1mexcept Exception as e:οΏ½[0m
839:  �[36;1m    print(f'❌ Error tracing execution: {e}')�[0m
840:  οΏ½[36;1m    import tracebackοΏ½[0m
...

845:  pythonLocation: /opt/hostedtoolcache/Python/3.11.15/x64
846:  PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib/pkgconfig
847:  Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
848:  Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
849:  Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
850:  LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib
851:  OPENAI_API_KEY: ***
852:  OPENAI_API_BASE: ***
853:  OPENAI_MODEL_NAME: ***
854:  LOGLEVEL: DEBUG
855:  PYTHONPATH: /home/runner/work/PraisonAI/PraisonAI/src/praisonai-agents:
856:  ##[endgroup]
857:  πŸ” Tracing AutoGen execution to find where it diverges...
858:  Traceback (most recent call last):
859:  File "<string>", line 25, in <module>
860:  FileNotFoundError: [Errno 2] No such file or directory: 'src/praisonai/tests/autogen-agents.yaml'
861:  🎯 Testing AutoGen execution path:
862:  1. PraisonAI framework: ""
863:  2. AgentsGenerator framework: ""
864:  ❌ Error tracing execution: [Errno 2] No such file or directory: 'src/praisonai/tests/autogen-agents.yaml'
865:  ##[group]Run echo "πŸ” Tracing YAML file loading and role creation..."
...

960:  οΏ½[36;1m    οΏ½[0m
961:  οΏ½[36;1m    # Test AgentsGenerator initializationοΏ½[0m
962:  οΏ½[36;1m    agents_gen = AgentsGenerator(οΏ½[0m
963:  οΏ½[36;1m        agent_file='tests/autogen-agents.yaml',οΏ½[0m
964:  οΏ½[36;1m        framework=praisonai.framework,οΏ½[0m
965:  οΏ½[36;1m        config_list=praisonai.config_listοΏ½[0m
966:  οΏ½[36;1m    )οΏ½[0m
967:  οΏ½[36;1m    print(f'  βš™οΈ AgentsGenerator framework: {agents_gen.framework}')οΏ½[0m
968:  οΏ½[36;1m    print(f'  βš™οΈ Final framework decision: {agents_gen.framework or config.get(\"framework\")}')οΏ½[0m
969:  οΏ½[36;1m    οΏ½[0m
970:  οΏ½[36;1m    # Check config_listοΏ½[0m
971:  οΏ½[36;1m    print(f'  πŸ”‘ Config list model: {praisonai.config_list[0].get(\"model\")}')οΏ½[0m
972:  οΏ½[36;1m    print(f'  πŸ”‘ Config list API key: {praisonai.config_list[0].get(\"api_key\", \"NOT_SET\")[:10]}...')οΏ½[0m
973:  οΏ½[36;1m    οΏ½[0m
974:  οΏ½[36;1mexcept Exception as e:οΏ½[0m
975:  �[36;1m    print(f'❌ Error in framework detection: {e}')�[0m
976:  οΏ½[36;1m"οΏ½[0m
...

979:  pythonLocation: /opt/hostedtoolcache/Python/3.11.15/x64
980:  PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib/pkgconfig
981:  Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
982:  Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
983:  Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
984:  LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib
985:  OPENAI_API_KEY: ***
986:  OPENAI_API_BASE: ***
987:  OPENAI_MODEL_NAME: ***
988:  LOGLEVEL: DEBUG
989:  PYTHONPATH: /home/runner/work/PraisonAI/PraisonAI/src/praisonai-agents:
990:  ##[endgroup]
991:  πŸ” Testing framework detection and config flow...
992:  Traceback (most recent call last):
993:  File "<string>", line 10, in <module>
994:  FileNotFoundError: [Errno 2] No such file or directory: 'tests/autogen-agents.yaml'
995:  πŸ”§ Testing framework detection:
996:  ##[error]Process completed with exit code 1.
997:  ##[group]Run echo "πŸ” Testing PraisonAIModel API key handling..."
...

1013:  οΏ½[36;1m    model = PraisonAIModel(model='openai/***')οΏ½[0m
1014:  οΏ½[36;1m    οΏ½[0m
1015:  οΏ½[36;1m    print('πŸ€– PraisonAIModel Configuration:')οΏ½[0m
1016:  οΏ½[36;1m    print(f'  model: {model.model}')οΏ½[0m
1017:  οΏ½[36;1m    print(f'  model_name: {model.model_name}')οΏ½[0m
1018:  οΏ½[36;1m    print(f'  api_key_var: {model.api_key_var}')οΏ½[0m
1019:  οΏ½[36;1m    print(f'  api_key: {model.api_key[:10] if model.api_key != \"nokey\" else \"DEFAULT_NOKEY\"}...')οΏ½[0m
1020:  οΏ½[36;1m    print(f'  base_url: {model.base_url}')οΏ½[0m
1021:  οΏ½[36;1m    οΏ½[0m
1022:  οΏ½[36;1m    if model.api_key == 'nokey':οΏ½[0m
1023:  �[36;1m        print('❌ FOUND THE ISSUE: PraisonAIModel is using default \"nokey\" instead of environment variable!')�[0m
1024:  οΏ½[36;1m    else:οΏ½[0m
1025:  οΏ½[36;1m        print('βœ… PraisonAIModel has valid API key from environment')οΏ½[0m
1026:  οΏ½[36;1m        οΏ½[0m
1027:  οΏ½[36;1mexcept Exception as e:οΏ½[0m
1028:  �[36;1m    print(f'❌ Error testing PraisonAIModel: {e}')�[0m
1029:  οΏ½[36;1m"οΏ½[0m
...

1052:  api_key: sk-proj-hw...
1053:  base_url: ***
1054:  βœ… PraisonAIModel has valid API key from environment
1055:  ##[group]Run echo "πŸ”‘ Testing API key validity with minimal OpenAI call..."
1056:  οΏ½[36;1mecho "πŸ”‘ Testing API key validity with minimal OpenAI call..."οΏ½[0m
1057:  οΏ½[36;1mpython -c "οΏ½[0m
1058:  οΏ½[36;1mimport osοΏ½[0m
1059:  οΏ½[36;1mtry:οΏ½[0m
1060:  οΏ½[36;1m    from openai import OpenAIοΏ½[0m
1061:  οΏ½[36;1m    client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY'))οΏ½[0m
1062:  οΏ½[36;1m    # Make a minimal API call to test key validityοΏ½[0m
1063:  οΏ½[36;1m    response = client.models.list()οΏ½[0m
1064:  οΏ½[36;1m    print('βœ… API Key is VALID - OpenAI responded successfully')οΏ½[0m
1065:  οΏ½[36;1m    print(f'πŸ“Š Available models: {len(list(response.data))} models found')οΏ½[0m
1066:  οΏ½[36;1mexcept Exception as e:οΏ½[0m
1067:  �[36;1m    print(f'❌ API Key is INVALID - Error: {e}')�[0m
1068:  οΏ½[36;1m    print('πŸ” This explains why all API-dependent tests are failing')οΏ½[0m
1069:  οΏ½[36;1m    print('πŸ’‘ The GitHub secret OPENAI_API_KEY needs to be updated with a valid key')οΏ½[0m
...

1071:  shell: /usr/bin/bash -e {0}
1072:  env:
1073:  pythonLocation: /opt/hostedtoolcache/Python/3.11.15/x64
1074:  PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib/pkgconfig
1075:  Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
1076:  Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
1077:  Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
1078:  LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib
1079:  OPENAI_API_KEY: ***
1080:  OPENAI_API_BASE: ***
1081:  OPENAI_MODEL_NAME: ***
1082:  LOGLEVEL: DEBUG
1083:  PYTHONPATH: /home/runner/work/PraisonAI/PraisonAI/src/praisonai-agents:
1084:  ##[endgroup]
1085:  πŸ”‘ Testing API key validity with minimal OpenAI call...
1086:  ❌ API Key is INVALID - Error: Error code: 401 - {'error': {'message': 'Incorrect API key provided: sk-proj-********************************************************************************************************************************************************q-EA. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}
1087:  πŸ” This explains why all API-dependent tests are failing
1088:  πŸ’‘ The GitHub secret OPENAI_API_KEY needs to be updated with a valid key
...

1228:  response = client.create(params)
1229:  ^^^^^^^^^^^^^^^^^^^^^
1230:  File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/autogen/oai/client.py", line 285, in create
1231:  response = completions.create(**params)
1232:  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1233:  File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/openai/_utils/_utils.py", line 286, in wrapper
1234:  return func(*args, **kwargs)
1235:  ^^^^^^^^^^^^^^^^^^^^^
1236:  File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 1211, in create
1237:  return self._post(
1238:  ^^^^^^^^^^^
1239:  File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/openai/_base_client.py", line 1297, in post
1240:  return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
1241:  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1242:  File "/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/openai/_base_client.py", line 1070, in request
1243:  raise self._make_status_error_from_response(err.response) from None
1244:  openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Incorrect API key provided: sk-proj-********************************************************************************************************************************************************q-EA. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'code': 'invalid_api_key', 'param': None}, 'status': 401}
1245:  ##[error]Process completed with exit code 1.
1246:  ##[group]Run echo "πŸ” Comprehensive debugging of PraisonAI execution path..."
...

1318:  οΏ½[36;1m    print()οΏ½[0m
1319:  οΏ½[36;1m    print('πŸ”§ Testing generate_crew_and_kickoff logic:')οΏ½[0m
1320:  οΏ½[36;1m    οΏ½[0m
1321:  οΏ½[36;1m    # Simulate the loading logicοΏ½[0m
1322:  οΏ½[36;1m    if agents_gen.agent_yaml:οΏ½[0m
1323:  οΏ½[36;1m        loaded_config = yaml.safe_load(agents_gen.agent_yaml)οΏ½[0m
1324:  οΏ½[36;1m        print('  Would load from agent_yaml')οΏ½[0m
1325:  οΏ½[36;1m    else:οΏ½[0m
1326:  οΏ½[36;1m        if agents_gen.agent_file == '/app/api:app' or agents_gen.agent_file == 'api:app':οΏ½[0m
1327:  οΏ½[36;1m            agents_gen.agent_file = 'agents.yaml'οΏ½[0m
1328:  οΏ½[36;1m            print(f'  Would change agent_file to: {agents_gen.agent_file}')οΏ½[0m
1329:  οΏ½[36;1m        try:οΏ½[0m
1330:  οΏ½[36;1m            with open(agents_gen.agent_file, 'r') as f:οΏ½[0m
1331:  οΏ½[36;1m                loaded_config = yaml.safe_load(f)οΏ½[0m
1332:  οΏ½[36;1m            print(f'  Successfully loaded: {agents_gen.agent_file}')οΏ½[0m
1333:  οΏ½[36;1m        except FileNotFoundError:οΏ½[0m
1334:  οΏ½[36;1m            print(f'  FileNotFoundError: {agents_gen.agent_file}')οΏ½[0m
1335:  οΏ½[36;1m            loaded_config = NoneοΏ½[0m
1336:  οΏ½[36;1m    οΏ½[0m
1337:  οΏ½[36;1m    if loaded_config:οΏ½[0m
1338:  οΏ½[36;1m        final_framework = agents_gen.framework or loaded_config.get('framework')οΏ½[0m
1339:  οΏ½[36;1m        print(f'  Final framework decision: {final_framework}')οΏ½[0m
1340:  οΏ½[36;1m        print(f'  Loaded roles: {list(loaded_config.get(\"roles\", {}).keys())}')οΏ½[0m
1341:  οΏ½[36;1m        οΏ½[0m
1342:  οΏ½[36;1m        if 'researcher' in loaded_config.get('roles', {}):οΏ½[0m
1343:  �[36;1m            print('  ❌ FOUND Researcher role in loaded config!')�[0m
1344:  οΏ½[36;1m        else:οΏ½[0m
1345:  οΏ½[36;1m            print('  βœ… No Researcher role in loaded config')οΏ½[0m
1346:  οΏ½[36;1m            οΏ½[0m
1347:  οΏ½[36;1mexcept Exception as e:οΏ½[0m
1348:  �[36;1m    print(f'❌ Error during execution debug: {e}')�[0m
1349:  οΏ½[36;1m    import tracebackοΏ½[0m
...

1354:  pythonLocation: /opt/hostedtoolcache/Python/3.11.15/x64
1355:  PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib/pkgconfig
1356:  Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
1357:  Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
1358:  Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
1359:  LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib
1360:  OPENAI_API_KEY: ***
1361:  OPENAI_API_BASE: ***
1362:  OPENAI_MODEL_NAME: ***
1363:  LOGLEVEL: DEBUG
1364:  PYTHONPATH: /home/runner/work/PraisonAI/PraisonAI/src/praisonai-agents:
1365:  ##[endgroup]
1366:  πŸ” Comprehensive debugging of PraisonAI execution path...
1367:  Traceback (most recent call last):
1368:  File "<string>", line 50, in <module>
1369:  FileNotFoundError: [Errno 2] No such file or directory: 'src/praisonai/tests/autogen-agents.yaml'
1370:  ============================================================
...

1411:  README.md
1412:  package.json
1413:  πŸ“‹ Files in src/praisonai/tests/ directory:
1414:  src/praisonai/tests/crewai-agents.yaml
1415:  src/praisonai/tests/autogen-agents.yaml
1416:  src/praisonai/tests/search-tool-agents.yaml
1417:  src/praisonai/tests/agents-advanced.yaml
1418:  src/praisonai/tests/agents.yaml
1419:  src/praisonai/tests/inbuilt-tool-agents.yaml
1420:  ❌ ROOT agents.yaml EXISTS (this is the problem!)
1421:  Root framework: praisonai
1422:  Root roles: ['file_reader', 'command_executor']
1423:  🎯 Testing EXACT execution path:
1424:  Using test file: src/praisonai/tests/autogen-agents.yaml
1425:  File exists: False
1426:  ❌ Error during execution debug: [Errno 2] No such file or directory: 'src/praisonai/tests/autogen-agents.yaml'
1427:  ##[group]Run # Run the fastest, most essential tests with coverage
...

1446:  cachedir: .pytest_cache
1447:  rootdir: /home/runner/work/PraisonAI/PraisonAI/src/praisonai
1448:  configfile: pytest.ini
1449:  plugins: timeout-2.4.0, cov-7.1.0, anyio-4.13.0, asyncio-1.3.0
1450:  timeout: 60.0s
1451:  timeout method: thread
1452:  timeout func_only: False
1453:  asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
1454:  /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/coverage/control.py:497: CoverageWarning: No data was collected. (no-data-collected); see https://coverage.readthedocs.io/en/7.13.5/messages.html#warning-no-data-collected
1455:  warnings.warn(msg, category=CoverageWarning, stacklevel=2)
1456:  collecting ... collected 0 items / 1 skipped
1457:  ================================ tests coverage ================================
1458:  _______________ coverage: platform linux, python 3.11.15-final-0 _______________
1459:  Coverage XML written to file coverage.xml
1460:  ============================= 1 skipped in 18.21s ==============================
1461:  ##[error]Process completed with exit code 5.
1462:  ##[group]Run echo "πŸ”„ Restoring root configuration files..."

Comment on lines +203 to +218
# Compile code with restricted mode
compiled_code = compile(code, '<string>', 'exec')

# Execute with output capture
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
exec(compiled_code, globals_dict, locals_dict)

# Get last expression value if any
import ast
tree = ast.parse(code)
if tree.body and isinstance(tree.body[-1], ast.Expr):
result = eval(
compile(ast.Expression(tree.body[-1].value), '<string>', 'eval'),
globals_dict,
locals_dict
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. execute_code calls exec πŸ“Ž Requirement gap ⛨ Security

execute_code still executes user-supplied code in-process via exec()/eval() and is not
integrated with a SandboxProtocol implementation. This fails the requirement for
sandboxed-by-default execution and leaves the host process exposed if the in-process controls are
bypassed.
Agent Prompt
## Issue description
`execute_code()` runs arbitrary code using in-process `exec()`/`eval()` rather than routing execution through a `SandboxProtocol`-backed sandbox. This violates the sandboxing-by-default requirement.

## Issue Context
Even with restricted `__builtins__` and AST checks, in-process execution remains risky and does not satisfy the protocol-integration requirement.

## Fix Focus Areas
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[85-218]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +86 to +92
def execute_code(
code: str,
globals_dict: Optional[Dict[str, Any]] = None,
locals_dict: Optional[Dict[str, Any]] = None,
timeout: int = 30,
max_output_size: int = 10000
) -> 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.

Action required

2. execute_code ignores timeout πŸ“Ž Requirement gap ⛨ Security

The execute_code API exposes a timeout parameter but does not enforce any timeout or resource
limits on execution. This allows untrusted code to run indefinitely (e.g., infinite loops) and can
cause denial-of-service of the host process.
Agent Prompt
## Issue description
`execute_code()` accepts a `timeout` parameter but does not actually enforce it, nor does it enforce CPU/memory limits.

## Issue Context
Without enforcement, user code can hang indefinitely or consume unbounded resources, which violates the sandbox resource-limits requirement.

## Fix Focus Areas
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[86-92]
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[202-218]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +525 to +550
def _get_python_tools():
"""Lazy-init PythonTools (requires black/pylint/autopep8)."""
global _python_tools_instance
try:
return _python_tools_instance
except NameError:
_python_tools_instance = PythonTools()
return _python_tools_instance

_python_tools_instance = None

def analyze_code(code: str) -> Optional[Dict[str, Any]]:
"""Analyze Python code structure and quality. Requires: pip install black pylint autopep8"""
return _get_python_tools().analyze_code(code)

def format_code(code: str, style: str = 'black', line_length: int = 88) -> Optional[str]:
"""Format Python code. Requires: pip install black pylint autopep8"""
return _get_python_tools().format_code(code, style, line_length)

def lint_code(code: str) -> Optional[Dict[str, List[Dict[str, Any]]]]:
"""Lint Python code. Requires: pip install black pylint autopep8"""
return _get_python_tools().lint_code(code)

def disassemble_code(code: str) -> Optional[str]:
"""Disassemble Python code to bytecode. Requires: pip install black pylint autopep8"""
return _get_python_tools().disassemble_code(code)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Pythontools init returns none 🐞 Bug βœ“ Correctness

_get_python_tools() returns _python_tools_instance even when it is initialized to None, so
analyze_code/format_code/lint_code/disassemble_code crash with AttributeError on first call.
This makes the new lazy optional-dependency wrappers unusable.
Agent Prompt
### Issue description
`_get_python_tools()` never instantiates `PythonTools()` because `_python_tools_instance` is defined as `None` and the function only instantiates on `NameError`. As a result, `analyze_code/format_code/lint_code/disassemble_code` call methods on `None`.

### Issue Context
This was introduced as part of the refactor to make optional-dependency tools lazy.

### Fix Focus Areas
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[525-550]

### What to change
- Replace the `try/except NameError` pattern with an explicit `if _python_tools_instance is None:` check.
- Instantiate and cache `PythonTools()` when the cached instance is `None`.
- (Optional) add a small lock if these wrappers can be called concurrently from multiple threads.

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +267 to +289
class PythonTools:
"""Tools for Python code analysis, formatting, and linting.

Requires: pip install black pylint autopep8
For code execution only, use the standalone execute_code() function.
"""

def __init__(self):
"""Initialize PythonTools β€” checks for required packages."""
self._check_dependencies()

def _check_dependencies(self):
"""Check if required packages are installed."""
missing = []
for package in ['black', 'pylint', 'autopep8']:
if util.find_spec(package) is None:
missing.append(package)

if missing:
raise ImportError(
f"Required packages not available. Please install: {', '.join(missing)}\n"
f"Run: pip install {' '.join(missing)}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Execute_code export broken 🐞 Bug βœ“ Correctness

praisonaiagents.tools.__getattr__ still resolves execute_code as a PythonTools instance
method, but execute_code was moved to a module-level function and PythonTools no longer defines
it. Importing execute_code from praisonaiagents.tools will now raise AttributeError (and may
also trigger unwanted optional-dependency checks).
Agent Prompt
### Issue description
`from praisonaiagents.tools import execute_code` is now broken because `TOOL_MAPPINGS` still points `execute_code` to the `PythonTools` class, but `execute_code` is no longer a `PythonTools` method.

### Issue Context
The PR refactored `execute_code` into a standalone function to avoid optional deps. The tools package lazy loader still assumes Python tools are class methods.

### Fix Focus Areas
- src/praisonai-agents/praisonaiagents/tools/__init__.py[60-69]
- src/praisonai-agents/praisonaiagents/tools/__init__.py[254-263]

### What to change
- In `TOOL_MAPPINGS`, map `execute_code` to `('.python_tools', None)` so `__getattr__` returns the module-level function.
- Consider mapping `analyze_code/format_code/lint_code/disassemble_code` to `('.python_tools', None)` as well, so the module-level wrappers control lazy instantiation (instead of instantiating `PythonTools()` during attribute access).
- Add/adjust a small unit test that `from praisonaiagents.tools import execute_code` succeeds without black/pylint/autopep8 installed.

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@MervinPraison

Copy link
Copy Markdown
Owner Author

@copilot

Do a thorough triage and analysis of this issue:

Classification: Is this a bug, feature request, question, or documentation gap?
Severity: Critical / High / Medium / Low β€” explain why
Affected area: Which modules/files in the codebase are relevant?
Root cause analysis: Search the codebase and identify the likely root cause
Impact assessment: What else could be affected? Any related issues?
Suggested fix: Propose a concrete solution approach with file paths
Effort estimate: Small (hours) / Medium (day) / Large (days+)

Copilot AI commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

@copilot

Do a thorough triage and analysis of this issue:
...

Here's a thorough triage and analysis of the 4 issues addressed in this PR:


Issue #1128 β€” Bare except: in LLM capability checks (llm.py)

Classification: Bug
Severity: Medium
Affected area: src/praisonai-agents/praisonaiagents/llm/llm.py β€” can_use_tools() and can_use_stop_words()
Root cause: Both functions used bare except: which catches SystemExit and KeyboardInterrupt in addition to real errors. If the user pressed Ctrl+C or the process was asked to exit during a litellm.get_supported_openai_params() call, the signal was silently swallowed and False was returned instead of propagating. The methods also gave zero diagnostic info on what went wrong.
Impact: Silent incorrect fallback values during agent capability detection; may cause wrong model behavior (e.g., disabling JSON response format when it was actually available). Could mask Ctrl+C during long-running agent runs.
Fix applied: Replaced with except Exception: and added logging.debug(..., exc_info=True) so the real error is recorded.
Effort: Small (< 1 hour)


Issue #1145 β€” Race condition in shared server dicts (agents.py)

Classification: Bug
Severity: High
Affected area: src/praisonai-agents/praisonaiagents/agents/agents.py β€” module-level _agents_server_started, _agents_registered_endpoints, _agents_shared_apps
Root cause: Three module-level dict objects used for shared server state were mutated from multiple threads (each Agents.launch(mode="http") call runs in its own thread) with no synchronization. Python's GIL protects single bytecode operations but not compound read-modify-write sequences like if port not in dict: dict[port] = .... Two threads could both see the key missing and each create a duplicate FastAPI app, leading to duplicate route registration errors or silent data loss.
Impact: Intermittent crashes or incorrect routing when multiple agents share a port β€” only reproducible under concurrent load (e.g., test suites, multi-agent HTTP servers).
Fix applied: Added _agents_server_lock = threading.Lock() and wrapped all check-then-mutate blocks with with _agents_server_lock:. Mirrors the identical _server_lock pattern already in agent.py.
Effort: Small (< 1 hour)


Issue #1147 β€” Duplicated sync/async approval logic (agent.py)

Classification: Bug / Code Quality
Severity: Low-Medium
Affected area: src/praisonai-agents/praisonaiagents/agent/agent.py β€” _check_tool_approval_sync() and _check_tool_approval_async()
Root cause: Both methods contained an identical 30-line block to build ApprovalRequest, determine needs_approval, and handle _approval_timeout. Any fix or enhancement to approval logic required editing two places. The async path also had a subtle behavioral difference: it didn't apply orig_timeout restoration on the backend in some branches.
Impact: High maintenance risk β€” a future change to approval logic will likely fix one path but not the other, creating a latent divergence bug.
Fix applied: Extracted _build_approval_request() (shared setup) and _finalize_approval() (shared decision processing) as private helpers. Both sync/async paths now call these helpers. Net result: ~30 lines of duplication removed, single source of truth for approval logic.
Effort: Small (2–3 hours including testing)


Issue #1154 β€” Eager Rich imports at module level (main.py, agents.py, knowledge.py, video_agent.py)

Classification: Performance issue
Severity: Low
Affected area: src/praisonai-agents/praisonaiagents/main.py, agents/agents.py, knowledge/knowledge.py, agent/video_agent.py
Root cause: from rich.console import Console (and Panel, Text, Markdown, Live) were at module top-level. Rich is a large library (~2 MB) and importing it unconditionally adds 50–100 ms to every import praisonaiagents call β€” even in headless/non-interactive contexts (API servers, batch jobs, CI) where no terminal output is ever rendered.
Impact: Measurable startup slowdown for all users; particularly costly in serverless/Lambda environments billed per millisecond. agent.py already used lazy imports as a deliberate pattern β€” the other files were inconsistent.
Fix applied: Introduced _ensure_rich() in main.py (following the same pattern from agent.py) that lazily imports all Rich classes on first display call. Moved from rich import ... inside the relevant if show_verbose and is_tty: blocks in the other files.
Effort: Small (1–2 hours)


CI Failure note

The Quick Validation Tests failure (exit code 5 β€” no tests collected) is pre-existing on master (run 23484173105 on master also fails identically). It is not caused by this PR.

@MervinPraison MervinPraison merged commit c932a74 into master Mar 30, 2026
20 of 22 checks passed
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.

5 participants