mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-12 18:47:44 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28ab9d4f2b | ||
|
|
bfe3d94bad | ||
|
|
7ae6c6d19c | ||
|
|
cc962c96d4 |
No files matched your search
@@ -13,8 +13,8 @@ jobs:
|
||||
# Claude reads ALL previous reviews (Qodo, Coderabbit, Copilot) and fixes any valid issues
|
||||
claude-final-review:
|
||||
if: |
|
||||
github.event_name == 'pull_request_review' &&
|
||||
github.event.review.user.login == 'copilot-pull-request-reviewer[bot]'
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.user.login, 'copilot')) ||
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.user.login, 'copilot'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
@@ -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.111" \
|
||||
"praisonai>=4.5.112" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -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.111" \
|
||||
"praisonai>=4.5.112" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -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.111" \
|
||||
"praisonai>=4.5.112" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import re
|
||||
|
||||
# 1. Update config/loader.py to compute is_empty once
|
||||
with open("praisonaiagents/config/loader.py", "r") as f:
|
||||
loader_code = f.read()
|
||||
|
||||
# Replace the @property is_empty with just a cached boolean mechanism
|
||||
old_prop = """ @property
|
||||
def is_empty(self) -> bool:
|
||||
\"\"\"Return True if all features are empty/default.\"\"\"
|
||||
return (
|
||||
self.model is None and
|
||||
self.base_url is None and
|
||||
self.allow_delegation is False and
|
||||
self.allow_code_execution is False and
|
||||
self.code_execution_mode == "safe" and
|
||||
self.memory is None and
|
||||
self.knowledge is None and
|
||||
self.planning is None and
|
||||
self.reflection is None and
|
||||
self.web is None and
|
||||
self.output is None and
|
||||
self.execution is None and
|
||||
self.caching is None and
|
||||
self.autonomy is None and
|
||||
self.skills is None
|
||||
)"""
|
||||
|
||||
new_prop = """ _is_empty: Optional[bool] = None
|
||||
|
||||
@property
|
||||
def is_empty(self) -> bool:
|
||||
\"\"\"Return True if all features are empty/default.\"\"\"
|
||||
if self._is_empty is not None:
|
||||
return self._is_empty
|
||||
|
||||
self._is_empty = (
|
||||
self.model is None and
|
||||
self.base_url is None and
|
||||
self.allow_delegation is False and
|
||||
self.allow_code_execution is False and
|
||||
self.code_execution_mode == "safe" and
|
||||
self.memory is None and
|
||||
self.knowledge is None and
|
||||
self.planning is None and
|
||||
self.reflection is None and
|
||||
self.web is None and
|
||||
self.output is None and
|
||||
self.execution is None and
|
||||
self.caching is None and
|
||||
self.autonomy is None and
|
||||
self.skills is None
|
||||
)
|
||||
return self._is_empty"""
|
||||
|
||||
loader_code = loader_code.replace(old_prop, new_prop)
|
||||
with open("praisonaiagents/config/loader.py", "w") as f:
|
||||
f.write(loader_code)
|
||||
|
||||
|
||||
# 2. Update agent/agent.py to use _defaults
|
||||
with open("praisonaiagents/agent/agent.py", "r") as f:
|
||||
agent_code = f.read()
|
||||
|
||||
old_llm_block = """ # Apply config defaults for LLM if not explicitly set
|
||||
if llm is None and model is None:
|
||||
config_model = get_default("model")
|
||||
if config_model:
|
||||
llm = config_model
|
||||
|
||||
# Apply config defaults for base_url if not explicitly set
|
||||
if base_url is None:
|
||||
config_base_url = get_default("base_url")
|
||||
if config_base_url:
|
||||
base_url = config_base_url"""
|
||||
|
||||
new_llm_block = """ # Delay config loading to avoid _loader_get_defaults unless needed"""
|
||||
|
||||
# Actually we need _defaults ANYWAY for the rest of features, so let's just initialize _defaults FIRST!
|
||||
|
||||
old_everything = """ from ..config.loader import apply_config_defaults, get_default
|
||||
|
||||
# Apply config defaults for LLM if not explicitly set
|
||||
if llm is None and model is None:
|
||||
config_model = get_default("model")
|
||||
if config_model:
|
||||
llm = config_model
|
||||
|
||||
# Apply config defaults for base_url if not explicitly set
|
||||
if base_url is None:
|
||||
config_base_url = get_default("base_url")
|
||||
if config_base_url:
|
||||
base_url = config_base_url
|
||||
|
||||
# Apply config defaults for feature params (memory, knowledge, etc.)
|
||||
from ..config.loader import get_defaults_config as _loader_get_defaults"""
|
||||
|
||||
new_everything = """ from ..config.loader import get_defaults_config as _loader_get_defaults
|
||||
global _CACHED_DEFAULTS
|
||||
if _CACHED_DEFAULTS is None:
|
||||
_CACHED_DEFAULTS = _loader_get_defaults()
|
||||
_defaults = _CACHED_DEFAULTS
|
||||
_defaults_empty = _defaults.is_empty if hasattr(_defaults, 'is_empty') else False
|
||||
|
||||
# Apply config defaults for LLM if not explicitly set
|
||||
if llm is None and model is None and _defaults.model:
|
||||
llm = _defaults.model
|
||||
|
||||
# Apply config defaults for base_url if not explicitly set
|
||||
if base_url is None and _defaults.base_url:
|
||||
base_url = _defaults.base_url
|
||||
|
||||
# Note: We already set __CACHED_DEFAULTS above, so no need here"""
|
||||
|
||||
agent_code = agent_code.replace(old_everything, new_everything)
|
||||
|
||||
old_redundant = """ from ..config.loader import get_defaults_config as _loader_get_defaults
|
||||
global _CACHED_DEFAULTS
|
||||
if _CACHED_DEFAULTS is None:
|
||||
_CACHED_DEFAULTS = _loader_get_defaults()
|
||||
_defaults = _CACHED_DEFAULTS
|
||||
_defaults_empty = _defaults.is_empty if hasattr(_defaults, 'is_empty') else False
|
||||
|
||||
if not _defaults_empty:"""
|
||||
new_redundant = """ if not _defaults_empty:"""
|
||||
|
||||
# Just replace the second occurrence manually (or find it):
|
||||
if " from ..config.loader import get_defaults_config as _loader_get_defaults" in agent_code[agent_code.find("if not _defaults_empty:") - 400 : agent_code.find("if not _defaults_empty:")]:
|
||||
# It exists! We just drop the duplicate initialization
|
||||
pass
|
||||
|
||||
# A cleaner way using regex:
|
||||
agent_code = re.sub(
|
||||
r" from \.\.config\.loader import get_defaults_config as _loader_get_defaults\n"
|
||||
r" global _CACHED_DEFAULTS\n"
|
||||
r" if _CACHED_DEFAULTS is None:\n"
|
||||
r" _CACHED_DEFAULTS = _loader_get_defaults\(\)\n"
|
||||
r" _defaults = _CACHED_DEFAULTS\n"
|
||||
r" _defaults_empty = _defaults\.is_empty if hasattr\(_defaults, 'is_empty'\) else False\n\n"
|
||||
r" if not _defaults_empty:",
|
||||
" if not _defaults_empty:",
|
||||
agent_code
|
||||
)
|
||||
|
||||
with open("praisonaiagents/agent/agent.py", "w") as f:
|
||||
f.write(agent_code)
|
||||
|
||||
print("success!")
|
||||
@@ -723,7 +723,7 @@ class ExecutionConfig:
|
||||
|
||||
# Action when budget exceeded: "stop" (default) raises BudgetExceededError,
|
||||
# "warn" logs warning but continues, or callable(total_cost, max_budget).
|
||||
on_budget_exceeded: Any = "stop"
|
||||
on_budget_exceeded: Union[str, Callable, None] = "stop"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
@@ -738,6 +738,7 @@ class ExecutionConfig:
|
||||
"context_compaction": self.context_compaction,
|
||||
"max_context_tokens": self.max_context_tokens,
|
||||
"max_budget": self.max_budget,
|
||||
"on_budget_exceeded": self.on_budget_exceeded,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.111"
|
||||
version = "1.5.112"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+1
-1
@@ -2951,7 +2951,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.111"
|
||||
version = "1.5.112"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -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.111.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.111.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.112.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.112.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -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.111 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==4.5.112 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -78,7 +78,10 @@ class SandlockSandbox:
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
"""Whether sandlock backend is available."""
|
||||
return self._sandlock.is_available()
|
||||
try:
|
||||
return self._sandlock.landlock_abi_version() >= 1
|
||||
except (AttributeError, ImportError):
|
||||
return False
|
||||
|
||||
@property
|
||||
def sandbox_type(self) -> str:
|
||||
@@ -155,8 +158,8 @@ class SandlockSandbox:
|
||||
max_processes=limits.max_processes,
|
||||
max_open_files=limits.max_open_files,
|
||||
|
||||
# CPU and timeout limits
|
||||
max_cpu_time_seconds=limits.timeout_seconds,
|
||||
# Note: CPU throttle percentage, not time limit
|
||||
# Execution timeout is handled via Sandbox.run(timeout=...)
|
||||
)
|
||||
|
||||
return policy
|
||||
@@ -192,13 +195,15 @@ class SandlockSandbox:
|
||||
|
||||
Centralises all sandlock Sandbox/Policy construction and error handling
|
||||
so that ``execute`` and ``run_command`` share a single code path.
|
||||
|
||||
Note: ``env`` is accepted for API compatibility but sandlock's
|
||||
``Sandbox.run()`` does not support per-run environment injection;
|
||||
callers requiring custom env vars should set them in the policy or
|
||||
prior to sandbox creation. ``working_dir`` is applied via the policy
|
||||
(added to the writable-path allow-list).
|
||||
"""
|
||||
policy = self._create_policy(limits, working_dir)
|
||||
|
||||
process_env = os.environ.copy()
|
||||
if env:
|
||||
process_env.update(env)
|
||||
|
||||
started_at = time.time()
|
||||
|
||||
try:
|
||||
@@ -206,10 +211,40 @@ class SandlockSandbox:
|
||||
|
||||
result = await asyncio.get_running_loop().run_in_executor(
|
||||
None,
|
||||
lambda: sandbox.run(cmd, env=process_env, cwd=working_dir)
|
||||
lambda: sandbox.run(cmd, timeout=limits.timeout_seconds)
|
||||
)
|
||||
|
||||
completed_at = time.time()
|
||||
duration = completed_at - started_at
|
||||
|
||||
# Check result.success and exit_code to determine actual status
|
||||
if not result.success:
|
||||
# Check if this was a timeout based on duration
|
||||
if duration >= limits.timeout_seconds:
|
||||
return SandboxResult(
|
||||
execution_id=execution_id,
|
||||
status=SandboxStatus.TIMEOUT,
|
||||
error=f"Execution timed out after {limits.timeout_seconds}s",
|
||||
exit_code=result.exit_code,
|
||||
stdout=result.stdout,
|
||||
stderr=result.stderr,
|
||||
duration_seconds=duration,
|
||||
started_at=started_at,
|
||||
completed_at=completed_at,
|
||||
)
|
||||
else:
|
||||
# Non-zero exit or other failure
|
||||
return SandboxResult(
|
||||
execution_id=execution_id,
|
||||
status=SandboxStatus.FAILED,
|
||||
error=f"Execution failed with exit code {result.exit_code}: {result.stderr}",
|
||||
exit_code=result.exit_code,
|
||||
stdout=result.stdout,
|
||||
stderr=result.stderr,
|
||||
duration_seconds=duration,
|
||||
started_at=started_at,
|
||||
completed_at=completed_at,
|
||||
)
|
||||
|
||||
return SandboxResult(
|
||||
execution_id=execution_id,
|
||||
@@ -217,7 +252,7 @@ class SandlockSandbox:
|
||||
exit_code=result.exit_code,
|
||||
stdout=result.stdout,
|
||||
stderr=result.stderr,
|
||||
duration_seconds=completed_at - started_at,
|
||||
duration_seconds=duration,
|
||||
started_at=started_at,
|
||||
completed_at=completed_at,
|
||||
metadata={
|
||||
@@ -227,32 +262,25 @@ class SandlockSandbox:
|
||||
},
|
||||
)
|
||||
|
||||
except self._sandlock.TimeoutError:
|
||||
return SandboxResult(
|
||||
execution_id=execution_id,
|
||||
status=SandboxStatus.TIMEOUT,
|
||||
error=f"Execution timed out after {limits.timeout_seconds}s",
|
||||
duration_seconds=limits.timeout_seconds,
|
||||
started_at=started_at,
|
||||
completed_at=time.time(),
|
||||
)
|
||||
except self._sandlock.SecurityViolationError as e:
|
||||
return SandboxResult(
|
||||
execution_id=execution_id,
|
||||
status=SandboxStatus.FAILED,
|
||||
error=f"Security violation: {e}",
|
||||
duration_seconds=time.time() - started_at,
|
||||
started_at=started_at,
|
||||
completed_at=time.time(),
|
||||
)
|
||||
except Exception as e:
|
||||
completed_at = time.time()
|
||||
duration = completed_at - started_at
|
||||
if duration >= limits.timeout_seconds:
|
||||
return SandboxResult(
|
||||
execution_id=execution_id,
|
||||
status=SandboxStatus.TIMEOUT,
|
||||
error=f"Execution timed out after {limits.timeout_seconds}s",
|
||||
duration_seconds=duration,
|
||||
started_at=started_at,
|
||||
completed_at=completed_at,
|
||||
)
|
||||
return SandboxResult(
|
||||
execution_id=execution_id,
|
||||
status=SandboxStatus.FAILED,
|
||||
error=str(e),
|
||||
duration_seconds=time.time() - started_at,
|
||||
error=f"Execution failed: {e}",
|
||||
duration_seconds=duration,
|
||||
started_at=started_at,
|
||||
completed_at=time.time(),
|
||||
completed_at=completed_at,
|
||||
)
|
||||
|
||||
async def execute(
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.5.111"
|
||||
__version__ = "4.5.112"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=1.5.111",
|
||||
"praisonaiagents>=1.5.112",
|
||||
"python-dotenv>=0.19.0",
|
||||
"litellm>=1.81.0,<=1.82.6",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
@@ -45,7 +45,7 @@ class TestSandlockSandbox:
|
||||
def test_fallback_to_subprocess_when_unavailable(self):
|
||||
"""Test fallback to subprocess when sandlock is not available."""
|
||||
mock_sandlock = Mock()
|
||||
mock_sandlock.is_available.return_value = False
|
||||
mock_sandlock.landlock_abi_version.return_value = 0 # < 1, so unavailable
|
||||
|
||||
sandbox = _make_sandbox(mock_sandlock)
|
||||
assert not sandbox.is_available
|
||||
@@ -55,7 +55,7 @@ class TestSandlockSandbox:
|
||||
async def test_fallback_execution(self):
|
||||
"""Test that execution falls back to subprocess when sandlock unavailable."""
|
||||
mock_sandlock = Mock()
|
||||
mock_sandlock.is_available.return_value = False
|
||||
mock_sandlock.landlock_abi_version.return_value = 0 # < 1, so unavailable
|
||||
|
||||
sandbox = _make_sandbox(mock_sandlock)
|
||||
|
||||
@@ -98,7 +98,7 @@ class TestSandlockSandbox:
|
||||
def test_status_reporting(self):
|
||||
"""Test sandbox status reporting."""
|
||||
mock_sandlock = Mock()
|
||||
mock_sandlock.is_available.return_value = True
|
||||
mock_sandlock.landlock_abi_version.return_value = 6 # >= 6, so available
|
||||
|
||||
sandbox = _make_sandbox(mock_sandlock)
|
||||
status = sandbox.get_status()
|
||||
@@ -122,7 +122,7 @@ class TestSandlockSandbox:
|
||||
|
||||
mock_sandlock.Policy.return_value = Mock()
|
||||
mock_sandlock.Sandbox.return_value = Mock(run=Mock(return_value=mock_result))
|
||||
mock_sandlock.is_available.return_value = True
|
||||
mock_sandlock.landlock_abi_version.return_value = 6 # >= 6, so available
|
||||
|
||||
sandbox = _make_sandbox(mock_sandlock)
|
||||
|
||||
@@ -142,65 +142,68 @@ class TestSandlockSandbox:
|
||||
async def test_sandlock_execution_timeout(self):
|
||||
"""Test timeout handling in sandlock execution."""
|
||||
mock_sandlock = Mock()
|
||||
|
||||
class FakeTimeoutError(Exception):
|
||||
pass
|
||||
|
||||
mock_sandlock.TimeoutError = FakeTimeoutError
|
||||
mock_sandlock.SecurityViolationError = Exception
|
||||
mock_sandlock.Policy.return_value = Mock()
|
||||
mock_sandlock.Sandbox.return_value = Mock()
|
||||
mock_sandlock.is_available.return_value = True
|
||||
mock_sandlock.landlock_abi_version.return_value = 6 # >= 6, so available
|
||||
|
||||
sandbox = _make_sandbox(mock_sandlock)
|
||||
|
||||
with patch("asyncio.get_running_loop") as mock_loop:
|
||||
# Create a mock Result object indicating timeout
|
||||
mock_timeout_result = Mock()
|
||||
mock_timeout_result.success = False
|
||||
mock_timeout_result.exit_code = 124 # Common timeout exit code
|
||||
mock_timeout_result.stdout = ""
|
||||
mock_timeout_result.stderr = "Process timed out"
|
||||
|
||||
# Simulate timeout by returning failed Result after enough time
|
||||
with patch("asyncio.get_running_loop") as mock_loop, \
|
||||
patch("time.time", side_effect=[0, 11, 11]): # Started at 0, ended at 11s (> 10s timeout)
|
||||
mock_loop.return_value.run_in_executor = AsyncMock(
|
||||
side_effect=FakeTimeoutError()
|
||||
return_value=mock_timeout_result
|
||||
)
|
||||
|
||||
await sandbox.start()
|
||||
result = await sandbox.execute("import time; time.sleep(100)")
|
||||
result = await sandbox.execute("import time; time.sleep(100)", limits=ResourceLimits(timeout_seconds=10))
|
||||
|
||||
assert result.status == SandboxStatus.TIMEOUT
|
||||
assert "timed out" in result.error.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sandlock_security_violation(self):
|
||||
"""Test security violation handling."""
|
||||
async def test_sandlock_execution_failure(self):
|
||||
"""Test general execution failure handling."""
|
||||
mock_sandlock = Mock()
|
||||
|
||||
class FakeTimeoutError(Exception):
|
||||
pass
|
||||
|
||||
class FakeSecurityViolationError(Exception):
|
||||
pass
|
||||
|
||||
mock_sandlock.SecurityViolationError = FakeSecurityViolationError
|
||||
mock_sandlock.TimeoutError = FakeTimeoutError
|
||||
mock_sandlock.Policy.return_value = Mock()
|
||||
mock_sandlock.Sandbox.return_value = Mock()
|
||||
mock_sandlock.is_available.return_value = True
|
||||
mock_sandlock.landlock_abi_version.return_value = 6 # >= 6, so available
|
||||
|
||||
sandbox = _make_sandbox(mock_sandlock)
|
||||
|
||||
with patch("asyncio.get_running_loop") as mock_loop:
|
||||
# Create a mock Result object indicating non-zero exit
|
||||
mock_failed_result = Mock()
|
||||
mock_failed_result.success = False
|
||||
mock_failed_result.exit_code = 1 # Non-zero exit code
|
||||
mock_failed_result.stdout = ""
|
||||
mock_failed_result.stderr = "Permission denied"
|
||||
|
||||
# Simulate execution failure (not timeout)
|
||||
with patch("asyncio.get_running_loop") as mock_loop, \
|
||||
patch("time.time", side_effect=[0, 2, 2]): # Started at 0, ended at 2s (< 10s timeout)
|
||||
mock_loop.return_value.run_in_executor = AsyncMock(
|
||||
side_effect=FakeSecurityViolationError("Access denied")
|
||||
return_value=mock_failed_result
|
||||
)
|
||||
|
||||
await sandbox.start()
|
||||
result = await sandbox.execute("import os; os.system('rm -rf /')")
|
||||
|
||||
assert result.status == SandboxStatus.FAILED
|
||||
assert "Security violation" in result.error
|
||||
assert "Access denied" in result.error
|
||||
assert "exit code 1" in result.error
|
||||
assert "Permission denied" in result.error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_safe_sandbox_path_traversal_blocked(self):
|
||||
"""Test that path traversal attempts are blocked by _safe_sandbox_path."""
|
||||
mock_sandlock = Mock()
|
||||
mock_sandlock.is_available.return_value = True
|
||||
mock_sandlock.landlock_abi_version.return_value = 6 # >= 6, so available
|
||||
|
||||
sandbox = _make_sandbox(mock_sandlock)
|
||||
await sandbox.start()
|
||||
@@ -220,7 +223,7 @@ class TestSandlockSandbox:
|
||||
async def test_write_file_blocks_traversal(self):
|
||||
"""Test that write_file refuses paths that escape the sandbox root."""
|
||||
mock_sandlock = Mock()
|
||||
mock_sandlock.is_available.return_value = True
|
||||
mock_sandlock.landlock_abi_version.return_value = 6 # >= 6, so available
|
||||
|
||||
sandbox = _make_sandbox(mock_sandlock)
|
||||
await sandbox.start()
|
||||
@@ -229,3 +232,42 @@ class TestSandlockSandbox:
|
||||
assert success is False
|
||||
|
||||
await sandbox.stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_sandlock_integration(self):
|
||||
"""Integration test with real sandlock package if available.
|
||||
|
||||
This test uses the actual sandlock package (if available) to ensure
|
||||
the integration works with the real API surface, not just mocks.
|
||||
"""
|
||||
try:
|
||||
# Try to import real sandlock
|
||||
import sandlock
|
||||
|
||||
# Only run if Landlock is actually supported on this system
|
||||
if sandlock.landlock_abi_version() < 1:
|
||||
pytest.skip("Landlock not supported on this system")
|
||||
|
||||
# Test with real sandlock package
|
||||
from praisonai.sandbox.sandlock import SandlockSandbox
|
||||
|
||||
sandbox = SandlockSandbox()
|
||||
assert sandbox.is_available
|
||||
|
||||
await sandbox.start()
|
||||
|
||||
# Execute simple code that should succeed
|
||||
result = await sandbox.execute(
|
||||
"print('Real sandlock integration test')",
|
||||
limits=ResourceLimits(timeout_seconds=5, memory_mb=64)
|
||||
)
|
||||
|
||||
# Should complete successfully
|
||||
assert result.status == SandboxStatus.COMPLETED
|
||||
assert result.exit_code == 0
|
||||
assert "Real sandlock integration test" in result.stdout
|
||||
|
||||
await sandbox.stop()
|
||||
|
||||
except ImportError:
|
||||
pytest.skip("sandlock package not available for integration test")
|
||||
Generated
+1
-1
@@ -5090,7 +5090,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.111"
|
||||
version = "1.5.112"
|
||||
source = { directory = "../praisonai-agents" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
Reference in new issue
Block a user