Skip to content

fix(sandbox): correct sandlock integration semantics and fail loud#1367

Open
congwang-mk wants to merge 2 commits intoMervinPraison:mainfrom
congwang-mk:fix/sandlock-integration
Open

fix(sandbox): correct sandlock integration semantics and fail loud#1367
congwang-mk wants to merge 2 commits intoMervinPraison:mainfrom
congwang-mk:fix/sandlock-integration

Conversation

@congwang-mk
Copy link
Copy Markdown

@congwang-mk congwang-mk commented Apr 13, 2026

Summary

The SandlockSandbox wrapper in src/praisonai/praisonai/sandbox/sandlock.py has several latent correctness and security issues. A caller who asks for kernel-level isolation could silently get weaker isolation, resource limits get dropped on the floor, and the timeout classifier never actually fires for real timeouts.

This PR fixes the integration to match sandlock's actual API, removes the silent fallback that violated the caller's explicit security choice, and strengthens the test suite accordingly.

What's fixed

  • Network policy intent is now explicit. sandlock uses tri-state semantics for net_allow_hosts (None = unrestricted, [] = deny all, [...] = allowlist). The previous code passed None to a Sequence[str] field when network was enabled and [] when disabled β€” semantically backwards and type-invalid. Rewritten to pass net_connect=[\"0-65535\"] when enabled or net_allow_hosts=[] when disabled. TCP-level deny-all defaults handle the rest.

  • stdout / stderr are now str, not bytes. sandlock returns bytes from Sandbox.run(); PraisonAI's SandboxResult is typed str. Added a _decode() helper with errors=\"replace\" so downstream consumers never see binary artefacts or crash on .lower() / .split().

  • max_cpu is now actually passed. limits.cpu_percent was silently ignored. Added max_cpu=limits.cpu_percent to the Policy construction.

  • execute_file() passes the script by path. The old implementation slurped the file into python3 -c <code> which is subject to ARG_MAX. Now it invokes [interp, abs_path, *args] directly and adds the script's parent directory to the Landlock read allowlist via a new extra_readable parameter on _create_policy.

  • Timeout detection uses sandlock's structural sentinel. sandlock exposes ExitStatus::Timeout as exit_code == -1 (see sandlock/_sdk.py around line 1475). Sandbox.run() does not populate result.error on timeout β€” only pipelines do β€” so string-matching is unreliable. Switching to exit_code == -1 works uniformly and matches how sandlock itself detects timeouts.

  • Context manager around Sandbox. with self._sandlock.Sandbox(policy) as sb: guarantees cleanup even if .run() raises mid-flight.

  • fs_readable is filtered to paths that exist. Landlock fails at spawn time if any allowlisted path is missing, so the hardcoded list β€” which included /usr/local/lib/python3 β€” caused sandlock_spawn failed on most hosts. The test_real_sandlock_integration test was silently failing on baseline for this reason; it now passes.

Breaking change β€” silent fallback removed

SandlockSandbox.execute/run_command/execute_file used to fall back to SubprocessSandbox whenever landlock_abi_version() < 1, logging only a warning. This violates the caller's explicit choice of kernel-level isolation: a SandlockSandbox that isn't actually using Landlock is a security footgun, and a warning in the logs is not a consent mechanism.

__init__ now raises RuntimeError if Landlock support is missing. The three fallback branches in execute(), run_command(), and execute_file() are removed.

Callers who want graceful degradation should catch ImportError / RuntimeError and construct SubprocessSandbox explicitly:

```python
try:
sb = SandlockSandbox(cfg)
except (ImportError, RuntimeError):
sb = SubprocessSandbox(cfg)
```

This is a deliberate fail-loud change β€” the previous behavior would silently weaken the security guarantees of any caller running on a kernel <5.13 or in a container with seccomp stripped.

Test plan

  • test_raises_when_landlock_unavailable replaces the two old fallback tests and asserts RuntimeError at construction time.
  • test_sandlock_execution_timeout mocks exit_code = -1 (the sentinel) instead of patching time.time.
  • test_sandlock_execution_failure sets result.error = None explicitly (reflects real Sandbox.run() behavior).
  • test_policy_creation_with_minimal_limits strengthened to verify max_cpu, net_allow_hosts=[] deny-all, and that net_connect is left unset so TCP deny-all defaults apply.
  • test_real_sandlock_integration now actually passes (previously failed on baseline due to the /usr/local/lib/python3 hardcoded path).
  • 10/10 unit tests pass: `pytest src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py`
  • End-to-end smoke test against real sandlock confirmed: basic execute, network-blocked execute, execute_file with args, and a forced real timeout all behave correctly.

Files changed

  • `src/praisonai/praisonai/sandbox/sandlock.py` β€” all fixes
  • `src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py` β€” updated tests

πŸ€– Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added startup-time Landlock support validation with explicit error messaging when unsupported.
  • Improvements

    • Enhanced timeout detection for sandboxed processes.
    • Improved stdout/stderr handling with proper UTF-8 decoding.
    • Refined network isolation policy configuration.
    • Changed script execution method for improved compatibility and access control.

The SandlockSandbox wrapper had several latent correctness issues that
could cause it to silently run with weaker isolation than intended, or
drop resource limits on the floor.

Fixes:

* Network policy intent is now explicit.  sandlock's net_allow_hosts
  uses tri-state semantics (None=unrestricted, []=deny all, [..]=
  allowlist) and network-enabled=True previously passed None to a
  Sequence[str] field.  Rewritten to pass net_connect=["0-65535"]
  when network is enabled, or net_allow_hosts=[] to block DNS when
  disabled.  TCP defaults ([] = deny all) handle the rest.

* stdout/stderr are now str, not bytes.  sandlock returns bytes from
  Sandbox.run(); PraisonAI's SandboxResult is typed as str.  Added a
  _decode() helper with errors="replace" so downstream consumers never
  see binary artefacts or crash on .lower() / .split().

* max_cpu is now actually passed to the Policy.  Previously
  limits.cpu_percent was silently ignored.

* execute_file() passes the script by path, not via `python3 -c <code>`.
  Large scripts no longer hit ARG_MAX, and the script's parent
  directory is added to the Landlock read allowlist via the new
  extra_readable parameter on _create_policy.

* Timeout detection is authoritative: we now inspect result.error for
  "timed out" rather than heuristically comparing wall-clock duration
  against limits.timeout_seconds.  A process that happens to finish
  just under the timeout no longer gets mis-classified.

* Sandbox handles are now managed via `with ... as sb:` so cleanup
  runs on exception.

* fs_readable is filtered to paths that actually exist.  Landlock
  fails at spawn time if any allowlisted path is missing, so the
  hardcoded list (which included /usr/local/lib/python3) caused
  sandlock_spawn failures on most hosts.  Now we filter with
  os.path.isdir before constructing the policy.

Breaking change β€” silent fallback removed:

  SandlockSandbox used to fall back to SubprocessSandbox whenever
  landlock_abi_version() < 1, logging only a warning.  This violates
  the caller's explicit choice of kernel-level isolation: a
  SandlockSandbox that isn't actually using Landlock is a security
  footgun, and a warning in the logs is not a consent mechanism.

  __init__ now raises RuntimeError if Landlock support is missing.
  Callers who want graceful degradation should catch ImportError /
  RuntimeError and construct SubprocessSandbox explicitly, e.g.:

      try:
          sb = SandlockSandbox(cfg)
      except (ImportError, RuntimeError):
          sb = SubprocessSandbox(cfg)

  The equivalent fallback branches in execute(), run_command(), and
  execute_file() are removed.

Tests updated:

  - test_raises_when_landlock_unavailable replaces the two fallback
    tests and asserts RuntimeError is raised at construction time.
  - test_sandlock_execution_timeout now mocks result.error instead
    of patching time.time.
  - test_sandlock_execution_failure sets result.error=None explicitly.
  - test_policy_creation_with_minimal_limits strengthened to check
    max_cpu, the new net_allow_hosts=[] deny-all semantics, and that
    net_connect is left unset (defaults to deny-all).

All 10 unit tests pass, including the real-sandlock integration test
(which was failing on baseline because of the /usr/local/lib/python3
hardcoded path).
Sandbox.run() does not populate result.error on timeout β€” only the
Pipeline path does.  String-matching on result.error was therefore
unreliable for the common single-sandbox case: a real timeout from
Sandbox.run() returns success=False, exit_code=-1, empty stderr, and
error=None, which my previous logic mis-classified as FAILED.

Switch to the structural signal: sandlock's ExitStatus::Timeout is
exposed as exit_code == -1 (see sandlock's _sdk.py around line 1475).
This matches how sandlock itself detects pipeline timeouts and works
uniformly across Sandbox.run() and any future execution paths.

Verified end-to-end with a real forced timeout against real sandlock:
  status: SandboxStatus.TIMEOUT
  exit: -1
  error: Execution timed out after 1s

Test updated to match: mock_timeout_result.exit_code = -1 and
error = None (reflecting actual Sandbox.run() behavior).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

πŸ“ Walkthrough

Walkthrough

The changes enhance the Sandlock sandbox implementation with runtime Landlock availability validation, extended filesystem allowlisting via extra_readable paths, restructured network policy logic, UTF-8 I/O decoding, and exit-code-based timeout detection. Helper functions are modified to execute scripts via interpreter instead of -c mode and no longer fall back to subprocess when sandlock is unavailable.

Changes

Cohort / File(s) Summary
Sandlock Sandbox Core Implementation
src/praisonai/praisonai/sandbox/sandlock.py
Added landlock_abi_version() check in __init__ to raise RuntimeError when unsupported; extended _create_policy() and _run_sandlocked() to accept extra_readable paths; refactored filesystem allowlist to filter existing directories; reworked network policy to use net_connect for enabled networks and net_allow_hosts=[] for denial; added _decode() for UTF-8 I/O conversion; replaced timeout detection with exit_code == -1 sentinel; updated execute(), execute_file(), and run_command() to remove subprocess fallback and use interpreter-based execution with script parent directory read access.
Sandlock Sandbox Tests
src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py
Updated initialization test to expect RuntimeError when Landlock ABI version is unsupported; revised policy assertions to expect max_cpu and strict network isolation (net_allow_hosts == [], no net_connect); modified timeout and failure tests to mock exit_code = -1 for timeout detection and use byte-valued stdout/stderr with error = None.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A sandbox strengthened, Landlock now checked,
Paths filtered clean, no surprises wrecked,
Exit codes whisper when timeouts arise,
Scripts run safer 'neath security's eyes. πŸ”’βœ¨

πŸš₯ Pre-merge checks | βœ… 3
βœ… Passed checks (3 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title accurately describes the main fix: correcting sandlock integration semantics and enforcing early failure when support is missing.
Docstring Coverage βœ… Passed Docstring coverage is 94.12% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests

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.

@MervinPraison
Copy link
Copy Markdown
Owner

@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first β€” incorporate their findings.

Review areas:

  1. Bloat check: Are changes minimal and focused? Any unnecessary code or scope creep?
  2. Security: Any hardcoded secrets, unsafe eval/exec, missing input validation?
  3. Performance: Any module-level heavy imports? Hot-path regressions?
  4. Tests: Are tests included? Do they cover the changes adequately?
  5. Backward compat: Any public API changes without deprecation?
  6. Code quality: DRY violations, naming conventions, error handling?
  7. Address reviewer feedback: If Qodo, Coderabbit, or Gemini flagged valid issues, include them in your review
  8. Suggest specific improvements with code examples where possible

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 enhances the SandlockSandbox by enforcing Landlock support at instantiation and removing the silent fallback to SubprocessSandbox. Key improvements include refined network policy logic, better timeout detection using sandlock's exit code sentinels, and a more robust execute_file implementation that passes script paths directly to interpreters. Feedback focuses on tightening the filesystem security policy: specifically, ensuring that the sandbox's temporary and working directories are explicitly granted read access (as required by Landlock) and narrowing the scope of extra_readable paths to individual files rather than entire parent directories to prevent unintended data exposure.

limits=limits,
env=env,
working_dir=self._temp_dir,
extra_readable=[os.path.dirname(abs_path)],
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.

security-high high

Allowlisting the entire parent directory of the script is overly permissive and could expose sensitive files on the host if the script is located outside the sandbox's temporary directory. It is safer to only allowlist the script file itself. Note that this requires updating _create_policy to support files in extra_readable (by using os.path.exists instead of os.path.isdir).

Suggested change
extra_readable=[os.path.dirname(abs_path)],
extra_readable=[abs_path],

Comment on lines +155 to +159
allowed_read_paths = [p for p in _candidate_read_paths if os.path.isdir(p)]
if extra_readable:
allowed_read_paths.extend(
p for p in extra_readable if os.path.isdir(p)
)
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 current implementation of _create_policy has two issues regarding the read allowlist:

  1. It only allows directories in extra_readable due to the os.path.isdir check. This prevents allowlisting individual files, which is necessary for the execute_file method to work securely.
  2. The sandbox's temporary directory (self._temp_dir) and the working_dir are added to the writable allowlist but not the readable one. In Landlock, write access does not implicitly grant read access. The sandboxed process will likely fail to read its own working directory or files written to the sandbox unless they are explicitly added to fs_readable.
Suggested change
allowed_read_paths = [p for p in _candidate_read_paths if os.path.isdir(p)]
if extra_readable:
allowed_read_paths.extend(
p for p in extra_readable if os.path.isdir(p)
)
allowed_read_paths = [p for p in _candidate_read_paths if os.path.isdir(p)]
if extra_readable:
allowed_read_paths.extend(p for p in extra_readable if os.path.exists(p))
for p in [self._temp_dir, working_dir]:
if p and os.path.isdir(p) and p not in allowed_read_paths:
allowed_read_paths.append(p)

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 13, 2026

Greptile Summary

This PR hardens the SandlockSandbox integration by correcting sandlock's actual API semantics, removing a silent security downgrade fallback, and fixing several latent bugs. The changes cover: fail-loud RuntimeError when Landlock is not available at construction time (instead of silently falling back to SubprocessSandbox), correct tri-state network policy construction, bytes-to-str decoding for stdout/stderr, passing max_cpu to the Policy, executing files by path rather than reading them into -c (avoiding ARG_MAX), filtering non-existent Landlock paths that would cause spawn failures, and using exit_code == -1 as the structural timeout sentinel instead of wall-clock time or string matching. The test suite is updated to match.

Key changes:

  • __init__ now raises RuntimeError if landlock_abi_version() < 1; callers wanting graceful degradation must handle it explicitly
  • Network policy correctly uses net_connect=["0-65535"] for enabled and net_allow_hosts=[] for disabled, reflecting sandlock's tri-state semantics
  • _decode() helper added to safely convert bytes sandlock output to str with errors="replace"
  • _create_policy filters candidate read paths to those that os.path.isdir() returns True for, preventing sandlock_spawn failures on hosts missing /usr/local/lib/python3
  • execute_file now invokes [interp, abs_path, *args] and adds the script's parent directory to the Landlock read allowlist via extra_readable
  • Context manager (with Sandbox(...) as sb) guarantees cleanup even if .run() raises
  • Test coverage updated: test_raises_when_landlock_unavailable, timeout mock uses exit_code=-1, failure mock sets error=None to match real API

Confidence Score: 4/5

Safe to merge after correcting the misleading kernel version in the RuntimeError message; all logic changes are sound.

The PR correctly fixes all stated bugs: network policy semantics, bytes/str decoding, max_cpu propagation, file-by-path execution, Landlock path filtering, timeout sentinel, and context-manager cleanup. The breaking change (fail-loud vs. silent fallback) is intentional, well-documented, and the right security call. The only factual error is the kernel version in the error message ('>= 6.12' vs. the correct '>= 5.13'), which is a non-functional P2. The test quality issue (missing explicit success=True) is minor.

src/praisonai/praisonai/sandbox/sandlock.py line 88 (kernel version string); src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py line 104 (missing success=True in mock)

Important Files Changed

Filename Overview
src/praisonai/praisonai/sandbox/sandlock.py Core sandbox implementation: fail-loud init, corrected network policy, bytes decoding, path filtering, context manager, and timeout detection β€” all logically sound; one misleading error message (kernel version stated as β‰₯ 6.12 instead of β‰₯ 5.13)
src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py Tests updated to match new API semantics; timeout and failure mocks correctly use exit_code=-1 and bytes; success test omits explicit mock_result.success=True, relying on Mock() truthiness (fragile)

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["SandlockSandbox.__init__"] --> B{landlock_abi_version?}
    B -->|exception| C["raise RuntimeError\n(query failed)"]
    B -->|abi < 1| D["raise RuntimeError\n(Landlock unsupported)"]
    B -->|abi >= 1| E["Sandbox ready"]

    E --> F["execute / run_command / execute_file"]
    F --> G["_create_policy(limits, working_dir, extra_readable)"]
    G --> H["Filter candidate read paths\n(os.path.isdir)"]
    H --> I{network_enabled?}
    I -->|Yes| J["net_connect=['0-65535']"]
    I -->|No| K["net_allow_hosts=[]"]
    J & K --> L["Policy(fs_readable, fs_writable,\nmax_memory, max_processes,\nmax_open_files, max_cpu, **net_kwargs)"]

    L --> M["_run_sandlocked(cmd)"]
    M --> N["run_in_executor β†’ _run()\nwith Sandbox(policy) as sb:\n  sb.run(cmd, timeout=...)"]
    N -->|exception| O["SandboxResult(FAILED)"]
    N -->|result| P{result.success?}
    P -->|True| Q["SandboxResult(COMPLETED)"]
    P -->|exit_code == -1| R["SandboxResult(TIMEOUT)"]
    P -->|else| S["SandboxResult(FAILED)"]

    N --> T["_decode(stdout/stderr)\nbytes β†’ str via UTF-8 replace"]
    T --> P
Loading

Comments Outside Diff (1)

  1. src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py, line 100-124 (link)

    P2 Success test relies on Mock() truthiness instead of explicitly setting success=True

    mock_result.success is never set, so accessing it returns a new Mock() object. Mock() is always truthy, which happens to trigger the if result.success: branch in _run_sandlocked β€” making the test pass correctly β€” but this is coincidental and fragile. If the condition were changed to if result.success is True: or if result.success == True:, the test would silently start returning a FAILED status while still asserting SandboxStatus.COMPLETED.

Reviews (1): Last reviewed commit: "fix(sandbox): use sandlock's exit_code =..." | Re-trigger Greptile

Comment on lines +86 to +91
raise RuntimeError(
"SandlockSandbox requires Landlock support (Linux kernel "
">= 6.12 with CONFIG_SECURITY_LANDLOCK=y). This kernel "
f"reports Landlock ABI version {abi}. Use SubprocessSandbox "
"explicitly if weaker isolation is acceptable."
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Incorrect minimum kernel version in error message

The error message states Linux kernel >= 6.12, but Landlock ABI version 1 was introduced in Linux 5.13 (not 6.12). The PR description itself mentions "running on a kernel <5.13" in the breaking-change section, so the discrepancy is likely a typo. Users on kernels between 5.13 and 6.12 who encounter this error will be told their kernel is too old when it is actually sufficient.

Suggested change
raise RuntimeError(
"SandlockSandbox requires Landlock support (Linux kernel "
">= 6.12 with CONFIG_SECURITY_LANDLOCK=y). This kernel "
f"reports Landlock ABI version {abi}. Use SubprocessSandbox "
"explicitly if weaker isolation is acceptable."
)
if abi < 1:
raise RuntimeError(
"SandlockSandbox requires Landlock support (Linux kernel "
">= 5.13 with CONFIG_SECURITY_LANDLOCK=y). This kernel "
f"reports Landlock ABI version {abi}. Use SubprocessSandbox "
"explicitly if weaker isolation is acceptable."
)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py (1)

104-106: Mock stdout/stderr types don't match real sandlock behavior.

The mock uses str for stdout/stderr, but real sandlock returns bytes. While _decode() handles both types, this test doesn't exercise the byte decoding path. For consistency with the timeout/failure tests (which use b""), consider using bytes here too.

πŸ“ Suggested fix
         mock_result = Mock()
         mock_result.exit_code = 0
-        mock_result.stdout = "Hello, World!"
-        mock_result.stderr = ""
+        mock_result.stdout = b"Hello, World!"
+        mock_result.stderr = b""
+        mock_result.success = True

Note: You'll also need to add mock_result.success = True since the implementation checks result.success first.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py` around lines 104 -
106, Update the test's mock_result to mirror real sandlock behavior by setting
mock_result.stdout and mock_result.stderr to bytes (e.g., b"Hello, World!" and
b"") instead of str, and also set mock_result.success = True so the code path
that checks result.success is exercised; this ensures the _decode()
byte-handling branch and success branch are covered when tests run.
src/praisonai/praisonai/sandbox/sandlock.py (1)

275-286: Broad exception catch is acceptable here but could be narrowed.

The static analysis tool flags except Exception (BLE001). While catching broad exceptions is generally discouraged, this usage is reasonable for wrapping sandlock execution where multiple exception types could occur. The exception details are preserved in the error field rather than silently swallowed.

Consider narrowing to specific expected exceptions if sandlock documents them, but the current approach is pragmatic for resilience.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/praisonai/praisonai/sandbox/sandlock.py` around lines 275 - 286, The
catch-all "except Exception" in the try block that awaits
asyncio.get_running_loop().run_in_executor(None, _run) is triggering BLE001;
either narrow it to the specific exceptions sandlock can raise (e.g.,
TimeoutError, RuntimeError, OSError) by replacing "except Exception as e:" with
"except (TimeoutError, RuntimeError, OSError) as e:" in the surrounding
function, or if you intentionally want the broad catch for sandbox resilience,
keep it but add logging.exception(...) to record the traceback and add a concise
lint justification comment (e.g., "# broad except to ensure sandbox resilience
(BLE001)"), or alternatively suppress the linter for that line with a targeted
noqa, while still returning the SandboxResult using SandboxResult,
SandboxStatus, execution_id and started_at as currently done.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/praisonai/praisonai/sandbox/sandlock.py`:
- Around line 378-391: The code unconditionally adds os.path.dirname(abs_path)
to extra_readable which can be "/" for files at filesystem root and would expose
the entire root; change the logic before calling _run_sandlocked to detect this
edge case (abs_path from os.path.realpath) and if os.path.dirname(abs_path) ==
os.path.sep, pass the file path itself (abs_path) or a more restrictive path
list instead of "/" as the extra_readable entry; update the call site that
constructs extra_readable (in the method that builds cmd and calls
self._run_sandlocked) to use this conditional selection so only the file (or a
non-root directory) is added.
- Around line 85-91: The RuntimeError message raised in SandlockSandbox when abi
< 1 contains an incorrect kernel version; update the text in the raise
RuntimeError (in sandlock.py around the SandlockSandbox check using the variable
abi) to state the correct minimum kernel that introduced Landlock ABI v1 (Linux
kernel >= 5.13) while preserving the rest of the guidance
(CONFIG_SECURITY_LANDLOCK=y and suggestion to use SubprocessSandbox).

---

Nitpick comments:
In `@src/praisonai/praisonai/sandbox/sandlock.py`:
- Around line 275-286: The catch-all "except Exception" in the try block that
awaits asyncio.get_running_loop().run_in_executor(None, _run) is triggering
BLE001; either narrow it to the specific exceptions sandlock can raise (e.g.,
TimeoutError, RuntimeError, OSError) by replacing "except Exception as e:" with
"except (TimeoutError, RuntimeError, OSError) as e:" in the surrounding
function, or if you intentionally want the broad catch for sandbox resilience,
keep it but add logging.exception(...) to record the traceback and add a concise
lint justification comment (e.g., "# broad except to ensure sandbox resilience
(BLE001)"), or alternatively suppress the linter for that line with a targeted
noqa, while still returning the SandboxResult using SandboxResult,
SandboxStatus, execution_id and started_at as currently done.

In `@src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py`:
- Around line 104-106: Update the test's mock_result to mirror real sandlock
behavior by setting mock_result.stdout and mock_result.stderr to bytes (e.g.,
b"Hello, World!" and b"") instead of str, and also set mock_result.success =
True so the code path that checks result.success is exercised; this ensures the
_decode() byte-handling branch and success branch are covered when tests run.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2ced78a2-08ac-444c-aaf3-c98f7e9529d1

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 6693a75 and 8b959b3.

πŸ“’ Files selected for processing (2)
  • src/praisonai/praisonai/sandbox/sandlock.py
  • src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py

Comment on lines +85 to +91
if abi < 1:
raise RuntimeError(
"SandlockSandbox requires Landlock support (Linux kernel "
">= 6.12 with CONFIG_SECURITY_LANDLOCK=y). This kernel "
f"reports Landlock ABI version {abi}. Use SubprocessSandbox "
"explicitly if weaker isolation is acceptable."
)
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.

⚠️ Potential issue | 🟑 Minor

Incorrect kernel version in error message.

The error message states Landlock requires "Linux kernel >= 6.12", but Landlock ABI v1 was introduced in Linux kernel 5.13 (released 2021). Kernel 6.12 is not accurate and may mislead users into thinking they need a much newer kernel than actually required.

πŸ“ Suggested fix
         if abi < 1:
             raise RuntimeError(
                 "SandlockSandbox requires Landlock support (Linux kernel "
-                ">= 6.12 with CONFIG_SECURITY_LANDLOCK=y).  This kernel "
+                ">= 5.13 with CONFIG_SECURITY_LANDLOCK=y).  This kernel "
                 f"reports Landlock ABI version {abi}.  Use SubprocessSandbox "
                 "explicitly if weaker isolation is acceptable."
             )
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if abi < 1:
raise RuntimeError(
"SandlockSandbox requires Landlock support (Linux kernel "
">= 6.12 with CONFIG_SECURITY_LANDLOCK=y). This kernel "
f"reports Landlock ABI version {abi}. Use SubprocessSandbox "
"explicitly if weaker isolation is acceptable."
)
if abi < 1:
raise RuntimeError(
"SandlockSandbox requires Landlock support (Linux kernel "
">= 5.13 with CONFIG_SECURITY_LANDLOCK=y). This kernel "
f"reports Landlock ABI version {abi}. Use SubprocessSandbox "
"explicitly if weaker isolation is acceptable."
)
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/praisonai/praisonai/sandbox/sandlock.py` around lines 85 - 91, The
RuntimeError message raised in SandlockSandbox when abi < 1 contains an
incorrect kernel version; update the text in the raise RuntimeError (in
sandlock.py around the SandlockSandbox check using the variable abi) to state
the correct minimum kernel that introduced Landlock ABI v1 (Linux kernel >=
5.13) while preserving the rest of the guidance (CONFIG_SECURITY_LANDLOCK=y and
suggestion to use SubprocessSandbox).

Comment on lines +378 to +391
abs_path = os.path.realpath(file_path)
interp = "bash" if file_path.endswith((".sh", ".bash")) else "python3"
cmd: List[str] = [interp, abs_path]
if args:
cmd.extend(args)

return await self._run_sandlocked(
cmd,
execution_id=execution_id,
limits=limits,
env=env,
working_dir=self._temp_dir,
extra_readable=[os.path.dirname(abs_path)],
)
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.

⚠️ Potential issue | 🟑 Minor

Consider edge case when script is at filesystem root.

If abs_path resolves to a file directly under / (e.g., /script.py), then os.path.dirname(abs_path) returns "/", which would add the entire root filesystem to the read allowlist. While this is an unlikely edge case, it could weaken isolation.

πŸ›‘οΈ Suggested guard
         abs_path = os.path.realpath(file_path)
+        script_dir = os.path.dirname(abs_path)
+        extra_read = [script_dir] if script_dir and script_dir != "/" else []
         interp = "bash" if file_path.endswith((".sh", ".bash")) else "python3"
         cmd: List[str] = [interp, abs_path]
         if args:
             cmd.extend(args)
 
         return await self._run_sandlocked(
             cmd,
             execution_id=execution_id,
             limits=limits,
             env=env,
             working_dir=self._temp_dir,
-            extra_readable=[os.path.dirname(abs_path)],
+            extra_readable=extra_read,
         )
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
abs_path = os.path.realpath(file_path)
interp = "bash" if file_path.endswith((".sh", ".bash")) else "python3"
cmd: List[str] = [interp, abs_path]
if args:
cmd.extend(args)
return await self._run_sandlocked(
cmd,
execution_id=execution_id,
limits=limits,
env=env,
working_dir=self._temp_dir,
extra_readable=[os.path.dirname(abs_path)],
)
abs_path = os.path.realpath(file_path)
script_dir = os.path.dirname(abs_path)
extra_read = [script_dir] if script_dir and script_dir != "/" else []
interp = "bash" if file_path.endswith((".sh", ".bash")) else "python3"
cmd: List[str] = [interp, abs_path]
if args:
cmd.extend(args)
return await self._run_sandlocked(
cmd,
execution_id=execution_id,
limits=limits,
env=env,
working_dir=self._temp_dir,
extra_readable=extra_read,
)
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/praisonai/praisonai/sandbox/sandlock.py` around lines 378 - 391, The code
unconditionally adds os.path.dirname(abs_path) to extra_readable which can be
"/" for files at filesystem root and would expose the entire root; change the
logic before calling _run_sandlocked to detect this edge case (abs_path from
os.path.realpath) and if os.path.dirname(abs_path) == os.path.sep, pass the file
path itself (abs_path) or a more restrictive path list instead of "/" as the
extra_readable entry; update the call site that constructs extra_readable (in
the method that builds cmd and calls self._run_sandlocked) to use this
conditional selection so only the file (or a non-root directory) is added.

@MervinPraison
Copy link
Copy Markdown
Owner

@claude You are the FINAL architecture reviewer. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.

Phase 1: Review per AGENTS.md

  1. Protocol-driven: check heavy implementations vs core SDK
  2. Backward compatible: ensure zero feature regressions
  3. Performance: no hot-path regressions

Phase 2: FIX Valid Issues
4. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
5. Push all code fixes directly to THIS branch (do NOT create a new PR)
6. Comment a summary of exact files modified and what you skipped

Phase 3: Final Verdict
7. If all issues are resolved, approve the PR / close the Issue
8. If blocking issues remain, request changes / leave clear action items

@praisonai-triage-agent
Copy link
Copy Markdown
Contributor

praisonai-triage-agent bot commented Apr 13, 2026

Claude finished @MervinPraison's task β€”β€” View job


Architecture Review and Analysis

CRITICAL LIMITATION: This is a PR from a FORK - I cannot push fixes directly to this branch. I can only provide review feedback.

Todo List:

  • Read AGENTS.md architecture guidelines
  • Review sandlock integration changes for architectural compliance
  • Analyze all reviewer feedback (Gemini, CodeRabbit, Qodo, Copilot)
  • Assess security and performance implications
  • Provide final verdict and recommendations

Phase 1: Architecture Review per AGENTS.md

βœ… Protocol-driven Design: This change is appropriately placed in the wrapper (praisonai/) not the core SDK (praisonaiagents/). The sandbox implementation is a heavy integration that belongs in the wrapper layer per AGENTS.md guidelines.

βœ… Backward Compatible: The changes don't break public APIs. The breaking change (removing silent fallback) is intentional and documented.

βœ… Performance: No hot-path regressions - sandlock is lazy-imported and only affects those explicitly choosing SandlockSandbox.

Phase 2: Valid Issues Found by Reviewers

I've analyzed all reviewer feedback and identified 2 CRITICAL BUGS that need immediate attention:

1. CRITICAL: Incorrect Kernel Version (src/praisonai/praisonai/sandbox/sandlock.py:88)

  • Issue: Error message claims >= 6.12 but Landlock ABI v1 was introduced in Linux 5.13
  • Impact: Users on kernels 5.13-6.12 will get misleading error messages
  • Fix Required:
# Line 88: Change this
">= 6.12 with CONFIG_SECURITY_LANDLOCK=y"
# To this  
">= 5.13 with CONFIG_SECURITY_LANDLOCK=y"

2. HIGH: Filesystem Root Exposure Risk (src/praisonai/praisonai/sandbox/sandlock.py:390)

  • Issue: execute_file unconditionally adds os.path.dirname(abs_path) to readable paths
  • Impact: If script is at filesystem root (/script.py), this exposes entire root filesystem (/)
  • Fix Required:
# Lines 378-391: Replace with this logic
abs_path = os.path.realpath(file_path)
script_dir = os.path.dirname(abs_path)
extra_read = [script_dir] if script_dir and script_dir != "/" else []
interp = "bash" if file_path.endswith((".sh", ".bash")) else "python3"
cmd: List[str] = [interp, abs_path]
if args:
    cmd.extend(args)

return await self._run_sandlocked(
    cmd,
    execution_id=execution_id,
    limits=limits,
    env=env,
    working_dir=self._temp_dir,
    extra_readable=extra_read,  # Changed from [os.path.dirname(abs_path)]
)

3. MEDIUM: Test Fragility (src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py:104-106)

  • Issue: Mock tests don't set success=True explicitly, relying on Mock() truthiness
  • Fix Required:
# Lines 104-106: Add explicit success flag
mock_result.exit_code = 0
mock_result.stdout = b"Hello, World!"  # Use bytes like real sandlock
mock_result.stderr = b""
mock_result.success = True  # Add this line

Phase 3: Final Verdict

❌ REQUEST CHANGES - The PR has 2 critical bugs that must be fixed before merge:

  1. Kernel version misinformation - This is factually incorrect and will confuse users
  2. Security vulnerability - Root filesystem exposure is a serious security issue

Recommendations for PR Author (@congwang-mk)

Please address these issues:

  1. Fix the kernel version in the error message (line 88)
  2. Add filesystem root protection in execute_file (line 390)
  3. Improve test robustness by setting explicit success=True (line 106)

The overall approach and architecture are sound - this is quality defensive security work that properly fails loud instead of silently degrading security. The fixes needed are straightforward but critical for safety.

Architecture Assessment: βœ… APPROVED with required fixes

The sandlock integration follows PraisonAI architectural principles:

  • Proper layer separation (wrapper vs core)
  • Fail-loud security philosophy
  • Protocol-driven design
  • No performance regressions
  • Comprehensive test coverage

Note: Since this is a fork PR, I cannot push the fixes directly. The PR author must implement these changes.

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.

3 participants