Skip to content

fix: Tier 1 security & concurrency fixes (#1134, #1129, #1137)#1150

Merged
MervinPraison merged 1 commit intomainfrom
fix/tier1-issues
Mar 26, 2026
Merged

fix: Tier 1 security & concurrency fixes (#1134, #1129, #1137)#1150
MervinPraison merged 1 commit intomainfrom
fix/tier1-issues

Conversation

@MervinPraison
Copy link
Copy Markdown
Owner

@MervinPraison MervinPraison commented Mar 26, 2026

Summary

Addresses Tier 1 issues identified in the security audit: #1134, #1129, #1137.

#1134 β€” Decouple execute_code from optional deps

  • execute_code is now a standalone module-level function β€” no black/pylint/autopep8 needed
  • PythonTools class retained for analyze_code/format_code/lint_code (lazy-init)
  • Agents can now use code execution without installing heavy optional dependencies
  • All 3 sandbox layers preserved (AST validation + text patterns + restricted builtins)

#1129 β€” Thread-safety for remaining global mutable state

#1137 β€” Clarify misleading TODO

  • Updated TODO at llm.py:73-74: this is a DRY/maintenance issue, NOT duplicate API calls

Testing

  • βœ… 23/23 sandbox tests pass (14 exploit vectors blocked, 9 legitimate code passes)
  • βœ… 815/816 full test suite pass (1 pre-existing flaky memory test unrelated to changes)
  • βœ… from praisonaiagents.tools.python_tools import execute_code works without optional deps

Summary by CodeRabbit

  • New Features

    • Code execution, analysis, formatting, linting, and disassembly functionality now available as standalone functions without requiring class instantiation.
    • Code execution now requires explicit approval for enhanced security.
  • Documentation

    • Clarified API call flow documentation.
  • Refactor

    • Improved thread safety in agent initialization with locking mechanisms.
  • Tests

    • Updated test suite for new function signatures.

#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
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 26, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bf40e6f1-ae8a-4640-9f87-61854ccfed66

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 78d7883 and 9ef5391.

πŸ“’ Files selected for processing (4)
  • src/praisonai-agents/praisonaiagents/agent/agent.py
  • src/praisonai-agents/praisonaiagents/llm/llm.py
  • src/praisonai-agents/praisonaiagents/tools/python_tools.py
  • src/praisonai-agents/tests/unit/tools/test_python_tools_sandbox.py

πŸ“ Walkthrough

Walkthrough

The PR introduces thread-safe lazy initialization in agent.py using double-checked locking for import caches and environment variable lookups. In python_tools.py, the execute_code method is refactored from a class method into a module-level function decorated with @require_approval, while helper methods become standalone functions. Clarifying comment updates are made to llm.py, and tests are updated to reflect the new module-level function structure.

Changes

Cohort / File(s) Summary
Thread-safe lazy initialization
src/praisonai-agents/praisonaiagents/agent/agent.py
Added module-level and class-level threading locks with double-checked locking pattern around lazy imports (_get_console, _get_live, _get_llm_functions, etc.) and environment cache lookups to ensure thread-safe single initialization.
Documentation clarification
src/praisonai-agents/praisonaiagents/llm/llm.py
Updated comments above LLMContextLengthExceededException to clarify that custom-LLM and OpenAI request flows are separate and do not result in duplicate API calls.
Code execution refactoring
src/praisonai-agents/praisonaiagents/tools/python_tools.py
Moved execute_code from instance method to module-level function with @require_approval(risk_level="critical") decorator; converted _safe_getattr and _validate_code_ast to standalone helpers; refactored public analysis/formatting functions (analyze_code, format_code, lint_code, disassemble_code) as lazy-initialized wrappers; enhanced AST-based and pattern-based code validation for security.
Test infrastructure update
src/praisonai-agents/tests/unit/tools/test_python_tools_sandbox.py
Updated test harness to invoke standalone execute_code function directly via __wrapped__ attribute instead of instantiating PythonTools class.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Review effort 4/5, Possible security concern

Poem

🐰 With locks in hand and threads aligned,
Code execution's tightly twined,
We sandbox scripts with care and might,
No lazy mishaps in the nightβ€”
Approval gates keep risks in sight! ✨

✨ Finishing Touches
πŸ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tier1-issues

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

Decouple execute_code from optional deps and add thread-safety

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
β€’ Decouple execute_code from optional dependencies (black/pylint/autopep8)
  - execute_code now standalone module-level function with 3-layer sandbox
  - PythonTools class retained for analyze/format/lint with lazy initialization
β€’ Add thread-safety for all lazy-import caches and environment variable caches
  - Implement double-checked locking pattern with _lazy_import_lock
  - Protect class-level env caches with _env_cache_lock
β€’ Clarify misleading TODO comment in llm.py about duplicate API calls
Diagram
flowchart LR
  A["execute_code<br/>standalone function"] -->|"3-layer sandbox<br/>AST+patterns+builtins"| B["Safe code execution<br/>no deps required"]
  C["PythonTools class<br/>analyze/format/lint"] -->|"lazy-init<br/>requires black/pylint"| D["Optional tools<br/>on-demand"]
  E["Lazy imports"] -->|"_lazy_import_lock<br/>double-checked"| F["Thread-safe caching"]
  G["Env var caches"] -->|"_env_cache_lock"| F
Loading

Grey Divider

File Changes

1. src/praisonai-agents/praisonaiagents/agent/agent.py Thread-safety +68/-49

Add thread-safety to lazy imports and env caches

β€’ Add _lazy_import_lock threading.Lock for protecting 6 lazy-import caches
β€’ Implement double-checked locking pattern in all 6 lazy-loader functions
β€’ Add _env_cache_lock for protecting class-level environment variable caches
β€’ Apply double-checked locking to _get_env_output_mode() and _get_default_model()

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


2. src/praisonai-agents/praisonaiagents/llm/llm.py πŸ“ Documentation +3/-1

Clarify TODO about separate code paths

β€’ Replace misleading TODO comment about duplicate API calls with clarifying NOTE
β€’ Explain that custom-LLM and OpenAI paths are separate code paths, not duplicate requests
β€’ Reframe as DRY/maintenance concern rather than billing issue

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


3. src/praisonai-agents/praisonaiagents/tools/python_tools.py ✨ Enhancement +299/-257

Decouple execute_code from optional dependencies

β€’ Extract execute_code as standalone module-level function (no class dependency)
β€’ Move _safe_getattr and _validate_code_ast to module-level functions
β€’ Implement 3-layer sandbox: AST validation + text patterns + restricted builtins
β€’ Refactor PythonTools class to require optional deps only for analyze/format/lint
β€’ Add lazy-initialization wrapper functions for optional-dep tools
β€’ Update docstrings to clarify dependency requirements

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


View more (1)
4. src/praisonai-agents/tests/unit/tools/test_python_tools_sandbox.py πŸ§ͺ Tests +5/-5

Update sandbox tests for standalone execute_code

β€’ Update _SandboxHarness to use standalone execute_code function
β€’ Remove PythonTools class instantiation bypass logic
β€’ Simplify test harness to directly call module-level execute_code

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


Grey Divider

Qodo Logo

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

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

This pull request significantly enhances the system's robustness by addressing critical security and concurrency issues. It refactors the Python code execution mechanism to be dependency-free, improving flexibility for agents. Concurrently, it introduces comprehensive thread-safety measures for lazy-loaded modules and environment variable caches, ensuring stable and predictable behavior in multi-threaded environments.

Highlights

  • Decoupled Code Execution: The execute_code function is now a standalone, module-level function, removing its dependency on optional packages like black, pylint, and autopep8. This allows agents to use code execution without installing heavy optional dependencies.
  • Enhanced Thread-Safety for Lazy Imports: Implemented _lazy_import_lock with double-checked locking for all six lazy-import caches, ensuring thread-safe access and initialization of modules like rich.console, rich.live, and various internal LLM and display functions.
  • Thread-Safety for Environment Variable Caches: Added _env_cache_lock to protect class-level environment variable caches (_env_output_mode, _default_model), completing the thread-safety work for mutable global state.
  • Clarified Misleading TODO: Updated a TODO comment in llm.py to clarify that separate LLM code paths are a DRY/maintenance concern, not an issue of duplicate API calls.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

Footnotes

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

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Mar 26, 2026

Code Review by Qodo

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

Grey Divider


Action required

1. execute_code runs in-process exec πŸ“Ž Requirement gap ⛨ Security
Description
The new execute_code executes untrusted code via in-process exec()/eval() and does not enforce
the timeout parameter or any subprocess/resource isolation. This fails the requirement to sandbox
or restrict Python execution by default to prevent filesystem/network/secret access and DoS risks.
Code

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

+        try:
+            # 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 3 requires sandboxed (or otherwise strongly restricted) execution by default with
enforced timeouts/resource limits; the added standalone execute_code still uses in-process
exec() and accepts timeout without enforcing it.

Sandbox or restrict Python exec() tool execution by default
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()` still runs untrusted code via in-process `exec()`/`eval()` and does not enforce the `timeout` parameter, violating the requirement that Python execution be sandboxed/restricted by default with resource/time limits.

## Issue Context
Compliance requires integrating a sandbox mechanism (e.g., existing sandbox protocol and/or subprocess isolation) and enforcing timeout/resource limits to prevent filesystem/network/environment access and denial-of-service.

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


2. _python_tools_instance global not locked πŸ“Ž Requirement gap β›― Reliability
Description
The new module-level _python_tools_instance lazy cache is global mutable state with no locking,
creating race conditions under multi-agent/threaded use. It also returns _python_tools_instance
even when it is None, risking inconsistent behavior across threads.
Code

src/praisonai-agents/praisonaiagents/tools/python_tools.py[R525-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
Evidence
Compliance ID 2 requires thread/async safety for global mutable SDK state; _python_tools_instance
is a shared global cache initialized without a lock (and returned even when None), which can lead
to concurrent initialization/mutation issues in multi-agent execution.

Ensure multi-agent thread/async safety for global mutable SDK state
src/praisonai-agents/praisonaiagents/tools/python_tools.py[525-534]

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

## Issue description
`_python_tools_instance` is global mutable state lazily initialized without any lock, which is not thread-safe for multi-agent use. Additionally, `_get_python_tools()` returns `_python_tools_instance` even when it is `None`.

## Issue Context
The SDK must be safe by default under concurrency; global caches should use locks (e.g., double-checked locking) or other safe initialization patterns.

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

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


3. execute_code still requires deps 🐞 Bug βœ“ Correctness
Description
Importing execute_code from praisonaiagents.tools still instantiates PythonTools via
TOOL_MAPPINGS/__getattr__, which triggers _check_dependencies and fails when black/pylint/autopep8
aren’t installedβ€”contradicting the new standalone execute_code behavior.
Code

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

+# ──────────────────────────────────────────────────────────────────────
+# Standalone execute_code β€” NO optional deps required (no black/pylint)
+# ──────────────────────────────────────────────────────────────────────
+
+@require_approval(risk_level="critical")
+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
This PR makes execute_code a standalone module-level function intended to work without optional
deps. However, praisonaiagents.tools maps execute_code to the PythonTools class and its __getattr__
path instantiates PythonTools, whose __init__ enforces optional deps via _check_dependencies;
therefore from praisonaiagents.tools import execute_code will raise ImportError when those deps
are absent. The repository’s programming-agent example imports execute_code/analyze_code/etc from
praisonaiagents.tools, so this is a real breakage path.

src/praisonai-agents/praisonaiagents/tools/python_tools.py[81-92]
src/praisonai-agents/praisonaiagents/tools/python_tools.py[267-289]
src/praisonai-agents/praisonaiagents/tools/init.py[62-68]
src/praisonai-agents/praisonaiagents/tools/init.py[215-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
`praisonaiagents.tools` still lazily resolves `execute_code` (and other python tool functions) as *PythonTools instance methods*, which forces optional dependency checks and breaks imports when `black/pylint/autopep8` are not installed.

### Issue Context
This PR intentionally made `execute_code` a standalone function that should be importable/usable without optional deps.

### Fix Focus Areas
- src/praisonai-agents/praisonaiagents/tools/__init__.py[62-68]
- src/praisonai-agents/praisonaiagents/tools/__init__.py[215-263]
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[81-92]
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[525-550]

### Implementation notes
- Change `TOOL_MAPPINGS['execute_code']` to point to a direct function import (class_name = `None`), e.g. `(' .python_tools', None)`.
- Prefer also mapping `analyze_code/format_code/lint_code/disassemble_code` as direct functions (class_name = `None`) so they go through the module-level wrappers that perform lazy instantiation.
- Consider updating the `python_tools` mapping (if used) to return the module rather than a class-method binding.

β“˜ 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

@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 action failed due to incorrect working directory / relative paths when trying to open YAML
    fixtures:
    - FileNotFoundError: [Errno 2] No such file or directory:
    src/praisonai/tests/autogen-agents.yaml (see around lines 858-864 and 1366-1370).
    -
    FileNotFoundError: [Errno 2] No such file or directory: tests/autogen-agents.yaml (see around lines
    991-996).
    These paths were evaluated relative to the job’s current directory (later shown by
    pytest as rootdir: /home/runner/work/PraisonAI/PraisonAI/src/praisonai), so the referenced files
    were not found at those relative locations.
  • A separate step confirms the configured OPENAI_API_KEY secret is invalid (OpenAI returns 401 /
    invalid_api_key, around lines 1085-1087), which would cause any API-dependent tests to fail once
    reached.
  • The final test run also fails with pytest exit code 5 because no tests were collected (collected 0
    items / 1 skipped, lines 1456-1462), which indicates the test discovery/run configuration resulted
    in zero runnable tests in this CI context.
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
...

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:  πŸ”§ Testing framework detection:
994:  File "<string>", line 10, in <module>
995:  FileNotFoundError: [Errno 2] No such file or directory: 'tests/autogen-agents.yaml'
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:  examples
1412:  server.json
1413:  πŸ“‹ Files in src/praisonai/tests/ directory:
1414:  src/praisonai/tests/autogen-agents.yaml
1415:  src/praisonai/tests/agents-advanced.yaml
1416:  src/praisonai/tests/search-tool-agents.yaml
1417:  src/praisonai/tests/inbuilt-tool-agents.yaml
1418:  src/praisonai/tests/agents.yaml
1419:  src/praisonai/tests/crewai-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: anyio-4.13.0, cov-7.1.0, asyncio-1.3.0, timeout-2.4.0
1450:  asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
1451:  timeout: 60.0s
1452:  timeout method: thread
1453:  timeout func_only: False
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 20.97s ==============================
1461:  ##[error]Process completed with exit code 5.
1462:  ##[group]Run echo "πŸ”„ Restoring root configuration files..."

@MervinPraison MervinPraison merged commit dbcd36e into main Mar 26, 2026
11 of 17 checks passed
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This pull request focuses on improving thread-safety for lazy-loaded modules and cached environment variables. It introduces threading.Lock for double-checked locking in agent.py to ensure thread-safe initialization of various components and environment variable access. Additionally, python_tools.py has been refactored to separate the core execute_code function and its security mechanisms into standalone, dependency-free functions, while tools requiring optional dependencies are now part of a PythonTools class accessed via lazy-initialized functions. A minor documentation update was also made in llm.py. A critical issue was identified in python_tools.py regarding the non-thread-safe lazy initialization of the PythonTools instance, which could lead to AttributeError due to incorrect NameError handling and lack of proper locking.

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.

critical

The lazy initialization for PythonTools is not thread-safe and appears to be buggy. The try...except NameError block will not work as intended because _python_tools_instance is defined at the module level, so a NameError will never be raised. This will cause _get_python_tools() to return None on its first call, leading to an AttributeError in functions like analyze_code.

Given that this pull request focuses on concurrency fixes, this should be updated to use a thread-safe double-checked locking pattern, similar to what's used for lazy-loading in agent.py.

You'll also need to add import threading at the top of the file.

Suggested change
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
_python_tools_lock = threading.Lock()
_python_tools_instance = None
def _get_python_tools():
"""Lazy-init PythonTools (requires black/pylint/autopep8)."""
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

Comment on lines +202 to +218
try:
# 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 runs in-process exec πŸ“Ž Requirement gap ⛨ Security

The new execute_code executes untrusted code via in-process exec()/eval() and does not enforce
the timeout parameter or any subprocess/resource isolation. This fails the requirement to sandbox
or restrict Python execution by default to prevent filesystem/network/secret access and DoS risks.
Agent Prompt
## Issue description
`execute_code()` still runs untrusted code via in-process `exec()`/`eval()` and does not enforce the `timeout` parameter, violating the requirement that Python execution be sandboxed/restricted by default with resource/time limits.

## Issue Context
Compliance requires integrating a sandbox mechanism (e.g., existing sandbox protocol and/or subprocess isolation) and enforcing timeout/resource limits to prevent filesystem/network/environment access and denial-of-service.

## 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 +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

Choose a reason for hiding this comment

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

Action required

2. _python_tools_instance global not locked πŸ“Ž Requirement gap β›― Reliability

The new module-level _python_tools_instance lazy cache is global mutable state with no locking,
creating race conditions under multi-agent/threaded use. It also returns _python_tools_instance
even when it is None, risking inconsistent behavior across threads.
Agent Prompt
## Issue description
`_python_tools_instance` is global mutable state lazily initialized without any lock, which is not thread-safe for multi-agent use. Additionally, `_get_python_tools()` returns `_python_tools_instance` even when it is `None`.

## Issue Context
The SDK must be safe by default under concurrency; global caches should use locks (e.g., double-checked locking) or other safe initialization patterns.

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

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

Comment on lines +81 to +92
# ──────────────────────────────────────────────────────────────────────
# Standalone execute_code β€” NO optional deps required (no black/pylint)
# ──────────────────────────────────────────────────────────────────────

@require_approval(risk_level="critical")
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

3. Execute_code still requires deps 🐞 Bug βœ“ Correctness

Importing execute_code from praisonaiagents.tools still instantiates PythonTools via
TOOL_MAPPINGS/__getattr__, which triggers _check_dependencies and fails when black/pylint/autopep8
aren’t installedβ€”contradicting the new standalone execute_code behavior.
Agent Prompt
### Issue description
`praisonaiagents.tools` still lazily resolves `execute_code` (and other python tool functions) as *PythonTools instance methods*, which forces optional dependency checks and breaks imports when `black/pylint/autopep8` are not installed.

### Issue Context
This PR intentionally made `execute_code` a standalone function that should be importable/usable without optional deps.

### Fix Focus Areas
- src/praisonai-agents/praisonaiagents/tools/__init__.py[62-68]
- src/praisonai-agents/praisonaiagents/tools/__init__.py[215-263]
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[81-92]
- src/praisonai-agents/praisonaiagents/tools/python_tools.py[525-550]

### Implementation notes
- Change `TOOL_MAPPINGS['execute_code']` to point to a direct function import (class_name = `None`), e.g. `(' .python_tools', None)`.
- Prefer also mapping `analyze_code/format_code/lint_code/disassemble_code` as direct functions (class_name = `None`) so they go through the module-level wrappers that perform lazy instantiation.
- Consider updating the `python_tools` mapping (if used) to return the module rather than a class-method binding.

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

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