Compare commits

...
2 Commits
Author SHA1 Message Date
MervinPraison 3929d4fbce Release v4.5.127 2026-04-09 10:34:44 +01:00
MervinPraison c9e5023832 feat: PraisonAI PR Reviewer Integration setup 2026-04-09 10:33:35 +01:00
25 changed files with 241 additions and 35 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
framework: praisonai
topic: Pull Request Review
roles:
lead_reviewer:
role: Lead Code Reviewer
goal: Provide a comprehensive and constructive review of the pull request
backstory: You are an expert software engineer with deep knowledge of best practices, security, and performance. You meticulously review code for bugs and maintainability.
tasks:
code_review:
description: Review the code changes in the pull request. Look for logic errors, formatting issues, security flaws, and performance bottlenecks. Output the review as a detailed markdown comment.
expected_output: A detailed PR review in markdown format.
+40
View File
@@ -0,0 +1,40 @@
name: PraisonAI PR Reviewer
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
issue_comment:
types: [created]
jobs:
review:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false && (github.event_name == 'pull_request' || contains(github.event.comment.body, '@praisonai'))
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate GitHub App Token
id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PraisonAI
run: pip install praisonaiagents[all]
- name: Run PraisonAI PR Review
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
praisonai agents --file .github/praisonai-reviewer.yaml
+25
View File
@@ -0,0 +1,25 @@
# PraisonAI PR Reviewer Setup Guide
This guide explains how to set up PraisonAI as an automated pull request reviewer using GitHub Actions and GitHub Apps.
## Prerequisites
1. A GitHub App created within your organization or account.
2. The App must have the following permissions:
- Pull Requests: Read & Write
- Issues: Read & Write
- Contents: Read
3. Generate a Private Key for the GitHub App.
## Setup Steps
1. Configure GitHub Secrets for your repository:
- `APP_ID`: The App ID of your GitHub App.
- `PRIVATE_KEY`: The generated Private Key (PEM format).
- `OPENAI_API_KEY` (or other LLM key) for PraisonAI to use.
2. Ensure `.github/workflows/praisonai-pr-review.yml` is present in your default branch.
3. Customize `.github/praisonai-reviewer.yaml` to configure the reviewing agents with specific roles.
## Triggering the Review
The review will run automatically upon PR creation and synchronization. You can also trigger it manually by commenting `@praisonai` on any pull request or issue.
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.5.126" \
"praisonai>=4.5.127" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.5.126" \
"praisonai>=4.5.127" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.5.126" \
"praisonai>=4.5.127" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -0,0 +1,41 @@
name: PraisonAI PR Reviewer
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
issue_comment:
types: [created]
jobs:
review:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false && (github.event_name == 'pull_request' || contains(github.event.comment.body, '@praisonai'))
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate GitHub App Token
id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PraisonAI
run: pip install praisonaiagents[all]
- name: Run PraisonAI PR Review
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
# Use PraisonAI to review the PR
praisonai agents --file .github/praisonai-reviewer.yaml
@@ -329,6 +329,9 @@ class MCP:
env = kwargs.get('env', {})
if not env:
env = os.environ.copy()
# Sanitize environment to prevent leaking caller credentials
sensitive_pattern = re.compile(r'(?i)(api_key|token|secret|password|credential)')
env = {k: v for k, v in env.items() if not sensitive_pattern.search(k)}
# Always set Python encoding
env['PYTHONIOENCODING'] = 'utf-8'
@@ -126,13 +126,24 @@ class FileTools:
# Validate directory path
safe_dir = FileTools._validate_path(directory)
path = Path(safe_dir)
resolved_base = path.resolve()
if pattern:
if '..' in pattern:
raise ValueError("Pattern cannot contain '..'")
if Path(pattern).is_absolute() or pattern.startswith('/'):
raise ValueError("Pattern cannot be an absolute path")
files = path.glob(pattern)
else:
files = path.iterdir()
result = []
for file in files:
try:
file.resolve().relative_to(resolved_base)
except ValueError:
continue
if file.is_file():
stat = file.stat()
result.append({
@@ -59,14 +59,13 @@ class ShellTools:
else:
command = shlex.split(command)
# Expand tilde and environment variables in command arguments
# Expand tilde in command arguments
# (shell=False means the shell won't do this for us)
command = [os.path.expanduser(os.path.expandvars(arg)) for arg in command]
command = [os.path.expanduser(arg) for arg in command]
# Expand tilde in cwd (subprocess doesn't do this)
if cwd:
cwd = os.path.expanduser(cwd)
cwd = os.path.expandvars(cwd) # Also expand $HOME, $USER, etc.
if not os.path.isdir(cwd):
# Fallback: try home directory, then current working directory
fallback = os.path.expanduser("~") if os.path.isdir(os.path.expanduser("~")) else os.getcwd()
@@ -139,14 +139,18 @@ def _crawl_with_httpx(urls: List[str]) -> List[Dict[str, Any]]:
# Try httpx first
try:
import httpx
with httpx.Client(follow_redirects=True, timeout=30.0) as client:
with httpx.Client(follow_redirects=False, timeout=30.0) as client:
response = client.get(url)
response.raise_for_status()
content = response.text
except ImportError:
# Fallback to urllib
import urllib.request
with urllib.request.urlopen(url, timeout=30) as response:
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None # Disable redirects for security
opener = urllib.request.build_opener(NoRedirectHandler())
with opener.open(url, timeout=30) as response:
content = response.read().decode('utf-8', errors='ignore')
# Basic HTML to text extraction
@@ -117,13 +117,30 @@ class AGUI:
def _attach_routes(self, router: "APIRouter") -> None:
"""Attach AG-UI routes to the router."""
from fastapi import Request, HTTPException
from fastapi.responses import StreamingResponse
import os
encoder = EventEncoder()
def _check_auth(request: Request):
token = os.environ.get("AGUI_AUTH_TOKEN") or os.environ.get("PRAISONAI_AUTH_TOKEN")
if token:
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer ") or auth_header.split(" ")[1] != token:
raise HTTPException(status_code=401, detail="Unauthorized")
else:
client_host = request.client.host if request.client else ""
if client_host not in ("127.0.0.1", "::1", "localhost"):
raise HTTPException(
status_code=403,
detail="Access denied. Configure AGUI_AUTH_TOKEN for remote access."
)
@router.post("/agui")
async def run_agent_agui(run_input: RunAgentInput):
async def run_agent_agui(request: Request, run_input: RunAgentInput):
"""Run the agent via AG-UI protocol."""
_check_auth(request)
async def event_generator():
async for event in self._run_agent(run_input):
yield encoder.encode(event)
@@ -134,15 +151,13 @@ class AGUI:
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
"Access-Control-Allow-Headers": "*",
},
)
@router.get("/status")
async def get_status():
async def get_status(request: Request):
"""Get agent status."""
_check_auth(request)
return {"status": "available"}
async def _run_agent(self, run_input: RunAgentInput) -> AsyncIterator[BaseEvent]:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "1.5.126"
version = "1.5.127"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
@@ -206,16 +206,16 @@ class TestApprovalRegistry:
def test_mark_and_check_approved(self):
from praisonaiagents.approval.registry import ApprovalRegistry
reg = ApprovalRegistry()
assert not reg.is_already_approved("execute_command")
reg.mark_approved("execute_command")
assert reg.is_already_approved("execute_command")
assert not reg.is_already_approved("write_file")
reg.mark_approved("write_file")
assert reg.is_already_approved("write_file")
def test_already_approved_skips_backend(self):
"""Once approved, no backend call needed."""
from praisonaiagents.approval.registry import ApprovalRegistry
reg = ApprovalRegistry()
reg.mark_approved("execute_command")
decision = reg.approve_sync("agent", "execute_command", {})
reg.mark_approved("write_file")
decision = reg.approve_sync("agent", "write_file", {})
assert decision.approved is True
assert "already" in decision.reason.lower()
+1 -1
View File
@@ -2951,7 +2951,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "1.5.126"
version = "1.5.127"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.126.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.126.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.127.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.127.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+2 -2
View File
@@ -68,12 +68,12 @@ def import_tools_from_file(file_path):
return custom_tools_module
try:
if os.path.exists(tools_path):
if tools_path and os.path.exists(tools_path):
# tools.py exists in the root directory, import from file
custom_tools_module = import_tools_from_file(tools_path)
logger.debug("Successfully imported custom tools from root tools.py")
else:
logger.debug("No custom tools.py file found in the root directory")
logger.debug("No custom tools file found or specified")
custom_tools_module = None
if custom_tools_module:
+24 -7
View File
@@ -117,8 +117,23 @@ class AgentOS:
def _register_routes(self, app: Any) -> None:
"""Register API routes."""
from fastapi import HTTPException
from fastapi import HTTPException, Request
from pydantic import BaseModel
import os
def _check_auth(request: Request):
token = os.environ.get("AGENTOS_AUTH_TOKEN") or os.environ.get("PRAISONAI_AUTH_TOKEN")
if token:
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer ") or auth_header.split(" ")[1] != token:
raise HTTPException(status_code=401, detail="Unauthorized")
else:
client_host = request.client.host if request.client else ""
if client_host not in ("127.0.0.1", "::1", "localhost"):
raise HTTPException(
status_code=403,
detail="Access denied. Configure AGENTOS_AUTH_TOKEN for remote access."
)
class ChatRequest(BaseModel):
message: str
@@ -145,7 +160,8 @@ class AgentOS:
return {"status": "healthy"}
@app.get(f"{self.config.api_prefix}/agents")
async def list_agents():
async def list_agents(request: Request):
_check_auth(request)
return {
"agents": [
{
@@ -160,16 +176,17 @@ class AgentOS:
}
@app.post(f"{self.config.api_prefix}/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
async def chat(request_obj: ChatRequest, request: Request):
_check_auth(request)
# Find the agent
agent = None
if request.agent_name:
if request_obj.agent_name:
for a in self.agents:
if getattr(a, 'name', None) == request.agent_name:
if getattr(a, 'name', None) == request_obj.agent_name:
agent = a
break
if agent is None:
raise HTTPException(status_code=404, detail=f"Agent '{request.agent_name}' not found")
raise HTTPException(status_code=404, detail=f"Agent '{request_obj.agent_name}' not found")
elif self.agents:
agent = self.agents[0]
else:
@@ -177,7 +194,7 @@ class AgentOS:
# Call the agent
try:
response = agent.chat(request.message)
response = agent.chat(request_obj.message)
return ChatResponse(
response=str(response),
agent_name=getattr(agent, 'name', 'unknown'),
@@ -282,6 +282,33 @@ class JobWorkflowExecutor:
def _exec_inline_python(self, code: str, step: Dict, flags: Dict) -> Dict:
"""Execute inline Python code in an isolated namespace."""
import ast
# Security validation (prevent sandbox escape)
try:
tree = ast.parse(code)
blocked_attrs = {
'__subclasses__', '__bases__', '__mro__', '__globals__',
'__code__', '__class__', '__dict__', '__builtins__',
'__import__', '__loader__', '__spec__', '__init_subclass__',
'__set_name__', '__reduce__', '__reduce_ex__',
'__traceback__', '__qualname__', '__module__',
}
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
return {"ok": False, "error": "Import statements are not allowed in script steps"}
if isinstance(node, ast.Attribute) and node.attr in blocked_attrs:
return {"ok": False, "error": f"Access to attribute '{node.attr}' is restricted"}
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
if node.func.id in ('exec', 'eval', 'compile', '__import__', 'open', 'input', 'breakpoint', 'setattr', 'getattr', 'delattr'):
return {"ok": False, "error": f"Call to '{node.func.id}' is not allowed"}
if isinstance(node, ast.Constant) and isinstance(node.value, str):
if any(attr in node.value for attr in blocked_attrs):
return {"ok": False, "error": "String constant contains restricted attribute name"}
except SyntaxError as e:
return {"ok": False, "error": f"Syntax Error: {str(e)}"}
except Exception as e:
return {"ok": False, "error": f"Validation Error: {str(e)}"}
_safe_builtins = {
"True": True, "False": False, "None": None,
"int": int, "float": float, "str": str, "bool": bool,
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==4.5.126 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==4.5.127 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
@@ -243,7 +243,15 @@ class WebSocketGateway:
def _check_auth(request) -> Optional[JSONResponse]:
"""Validate auth token if configured. Returns error response or None."""
if not self.config.auth_token:
# If no token is configured, restrict sensitive routes to localhost
client_ip = request.client.host if request.client else "127.0.0.1"
if client_ip not in ("127.0.0.1", "::1", "localhost"):
return JSONResponse(
{"error": "Authentication token not configured. Remote access denied."},
status_code=403,
)
return None
auth_header = request.headers.get("authorization", "")
if not auth_header.startswith("Bearer "):
return JSONResponse(
@@ -158,6 +158,11 @@ def _safe_extractall(tar: tarfile.TarFile, dest_dir: Path) -> None:
raise RegistryError(f"Archive is too large uncompressed (>{MAX_SIZE} bytes)")
member_path = Path(member.name)
# Protect against symlink attacks
if member.issym() or member.islnk():
raise RegistryError(
f"Refusing to extract symlink/hardlink in archive: {member.name}"
)
# Reject absolute paths
if member_path.is_absolute():
raise RegistryError(
+1 -1
View File
@@ -1 +1 @@
__version__ = "4.5.126"
__version__ = "4.5.127"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=1.5.126",
"praisonaiagents>=1.5.127",
"python-dotenv>=0.19.0",
"litellm>=1.81.0,<=1.82.6",
"PyYAML>=6.0",
+1 -1
View File
@@ -5117,7 +5117,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "1.5.126"
version = "1.5.127"
source = { directory = "../praisonai-agents" }
dependencies = [
{ name = "aiohttp" },