Skip to content

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

Open
congwang-mk wants to merge 3 commits into
MervinPraison:mainfrom
congwang-mk:fix/sandlock-integration
Open

fix(sandbox): correct sandlock integration semantics and fail loud#1367
congwang-mk wants to merge 3 commits into
MervinPraison: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.  Written against the sandlock 0.8.1
API, which replaces the standalone Policy object with direct
Sandbox(**kwargs) configuration and collapses the network knobs into a
single net_allow endpoint allowlist.

Fixes:

* Configuration uses the current sandlock API.  sandlock 0.8.1 dropped
  the Policy object; config is passed straight to Sandbox(**kwargs).
  _build_sandbox_kwargs() assembles that dict.

* Network policy intent is now explicit via net_allow.  An empty
  allowlist denies all outbound (the default); network-enabled=True
  opens all TCP plus UDP DNS (["*:*", "udp://*:53"]) so the sandboxed
  process can actually resolve hostnames and connect.

* 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 sandbox.  Previously
  limits.cpu_percent was silently ignored.

* env variables are injected via the Sandbox env field (set/overridden
  in the child), which the current API supports natively.

* 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 _build_sandbox_kwargs.

* Timeout detection is authoritative: we inspect result.error for
  "timed out" rather than heuristically comparing wall-clock duration
  against limits.timeout_seconds.  (Refined in the following commit to
  use sandlock's exit_code == -1 sentinel.)

* 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
  sandbox creation failures on most hosts.  Now we filter with
  os.path.isdir before constructing the sandbox.

Breaking change — silent fallback removed:

  SandlockSandbox used to fall back to SubprocessSandbox whenever
  Landlock support was too low, 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 —
  i.e. landlock_abi_version() < min_landlock_abi() (currently ABI 6,
  Linux 6.7+).  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_sandbox_kwargs_with_minimal_limits / _network_enabled check
    max_cpu and the net_allow deny-all / allow-TCP+DNS semantics.
  - test_sandlock_execution_timeout mocks result.error to assert the
    timeout path.
  - test_sandlock_execution_failure sets result.error=None explicitly.

All unit tests pass, including the real-sandlock integration test.
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 corrects several latent integration bugs in SandlockSandbox: the wrong network-policy field and semantics, missing bytes-to-str decoding, a dropped max_cpu limit, a timing-based timeout classifier that sandlock's own API never populates, and hardcoded read-allowlist paths that caused sandlock_spawn failed on most hosts. execute_file is also reworked to invoke the interpreter by path rather than via -c, avoiding ARG_MAX. The fail-loud RuntimeError on missing Landlock support is a deliberate breaking change that replaces a silent fallback that could weaken the caller's security contract without consent.

  • sandlock.py: Replaces _create_policy() with _build_sandbox_kwargs(), adds _decode() for safe bytes→str conversion, detects timeout via exit_code == -1, wraps Sandbox in a context manager, and filters fs_readable paths to those that actually exist.
  • test_sandlock_sandbox.py: Removes fallback tests, adds test_raises_when_landlock_unavailable, strengthens policy/kwargs assertions (max_cpu, net_allow), and aligns the timeout test with sandlock's structural sentinel.

Confidence Score: 4/5

The sandlock.py production changes are well-reasoned and internally consistent; the main risk is a mismatch in the integration test that could cause a hard failure on machines with an intermediate Landlock ABI rather than a clean skip.

The core sandlock.py fixes are correct: network policy, bytes decoding, CPU limits, timeout sentinel, context manager, and path filtering all align with the stated API semantics. The integration test at line 253 skips on abi < 1 but the constructor raises on abi < min_landlock_abi() (6), and the outer except ImportError won't catch that RuntimeError.

src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py — specifically the test_real_sandlock_integration skip condition and exception handler

Important Files Changed

Filename Overview
src/praisonai/praisonai/sandbox/sandlock.py Rewrites sandlock integration with correct network policy, bytes decoding, CPU limit propagation, context-manager cleanup, and fail-loud ABI check; the logic changes are well-reasoned and internally consistent
src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py Unit tests are substantially improved, but the real-sandlock integration test uses an ABI skip threshold (< 1) that no longer matches the constructor guard (< min_landlock_abi()), and the except clause doesn't catch RuntimeError, leaving the test able to hard-fail on intermediate-ABI systems

Sequence Diagram

sequenceDiagram
    participant Caller
    participant SandlockSandbox
    participant _run_sandlocked
    participant Executor
    participant sandlock.Sandbox

    Caller->>SandlockSandbox: execute(code) / execute_file(path) / run_command(cmd)
    SandlockSandbox->>SandlockSandbox: _build_sandbox_kwargs(limits, working_dir, extra_readable)
    Note over SandlockSandbox: Filter fs_readable to existing paths
    SandlockSandbox->>_run_sandlocked: cmd, limits, env, extra_readable
    _run_sandlocked->>Executor: run_in_executor(None, _run)
    Executor->>sandlock.Sandbox: __enter__() → sb
    sandlock.Sandbox->>sandlock.Sandbox: "sb.run(cmd, timeout=N)"
    alt success
        sandlock.Sandbox-->>_run_sandlocked: "result (exit_code=0)"
        _run_sandlocked-->>Caller: SandboxResult(COMPLETED)
    else timeout
        sandlock.Sandbox-->>_run_sandlocked: "result (exit_code=-1)"
        _run_sandlocked-->>Caller: SandboxResult(TIMEOUT)
    else failure
        sandlock.Sandbox-->>_run_sandlocked: "result (exit_code=N)"
        _run_sandlocked-->>Caller: SandboxResult(FAILED)
    else exception
        sandlock.Sandbox-->>_run_sandlocked: raises Exception
        _run_sandlocked-->>Caller: SandboxResult(FAILED)
    end
    sandlock.Sandbox->>sandlock.Sandbox: __exit__() cleanup
Loading

Reviews (3): Last reviewed commit: "fix(sandbox): address review feedback on..." | 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.

@MervinPraison
Copy link
Copy Markdown
Owner

@congwang-mk just a gentle reminder to fix this issue. Thank you

@congwang-mk congwang-mk force-pushed the fix/sandlock-integration branch from 8b959b3 to c0c4eee Compare June 3, 2026 05:18
Code-review fixes from PR MervinPraison#1367:

* execute_file no longer allowlists the script's entire parent
  directory for Landlock read.  Only the script file itself is granted
  read access, so sibling files on the host are never exposed (gemini
  flagged this as high severity).  Verified end-to-end against real
  sandlock: a script that tries to read a sibling file is denied.

* _build_sandbox_kwargs filters extra_readable with os.path.exists
  instead of os.path.isdir, so individual files (not just directories)
  can be allowlisted — required for the file-only execute_file path.

* Corrected the Landlock kernel version in the fail-loud error message.
  The wrapper now requires ABI >= min_landlock_abi() (currently v6),
  which shipped in Linux 6.12 — not 6.7.  The message is phrased around
  the ABI version so it stays correct as the SDK's minimum changes.

* test_sandlock_execution_success now uses bytes for stdout/stderr (to
  exercise the _decode() byte path, matching real sandlock) and sets
  result.success = True explicitly.

Note: the suggestion to also add temp_dir/working_dir to fs_readable
was not applied — sandlock's fs_writable already grants read access
("fs_readable ... in addition to writable paths"), confirmed by test.
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