Skip to content

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

Merged
MervinPraison merged 6 commits into
MervinPraison:mainfrom
congwang-mk:fix/sandlock-integration
Jun 24, 2026
Merged

fix(sandbox): correct sandlock integration semantics and fail loud#1367
MervinPraison merged 6 commits into
MervinPraison:mainfrom
congwang-mk:fix/sandlock-integration

Conversation

@congwang-mk

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

Copy link
Copy Markdown
Contributor

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.

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor
πŸ“ 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

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

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.

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

greptile-apps Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR corrects the SandlockSandbox wrapper to match sandlock's current API: it replaces the old Policy object with Sandbox(**kwargs), fixes network deny-all semantics, decodes bytes stdout/stderr to str, adds max_cpu to the policy, passes scripts by path in execute_file, uses exit_code == -1 as the timeout sentinel, and removes the silent subprocess fallback in favour of a fail-loud RuntimeError at construction time.

  • Core semantics fixed: network policy (net_allow), clean_env=True baseline, max_cpu, context-manager sandbox construction, and exit_code == -1 timeout detection all now match sandlock's real API.
  • Security footgun removed: __init__ raises RuntimeError immediately when Landlock ABI is below the SDK minimum, preventing silent degradation to subprocess isolation.
  • Tests updated: fallback tests replaced with fail-loud assertion, new tests cover env isolation, network rules, and bytes decoding.

Confidence Score: 4/5

Safe to merge with a small follow-up: the integration test exception guard is incomplete and can hard-fail in certain sandlock version/kernel combinations instead of skipping cleanly.

The core implementation changes look correct and well-motivated. The one gap is in the integration test: the except ImportError clause does not cover AttributeError or RuntimeError, so the test can hard-fail in CI where sandlock is installed but at an unexpected version. The stale _used_subprocess_fallback field is cosmetic. Neither is a production bug, but the test gap can produce misleading CI failures.

The exception handler at the bottom of test_real_sandlock_integration in test_sandlock_sandbox.py needs broadening.

Important Files Changed

Filename Overview
src/praisonai/praisonai/sandbox/sandlock.py Significant rework of sandlock integration: fail-loud constructor, correct env isolation, context manager, timeout via exit_code sentinel, _decode for bytes, and path filtering. Stale _used_subprocess_fallback field remains in get_status() and security_policy.allowed_paths are not mirrored to the read allowlist unlike working_dir/temp_dir.
src/praisonai/tests/unit/sandbox/test_sandlock_sandbox.py Test suite updated to match new fail-loud semantics, adds env isolation and network tests. Integration test skip threshold is now correct (min_landlock_abi()), but except clause only catches ImportError, leaving AttributeError and RuntimeError paths unguarded.

Comments Outside Diff (1)

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

    P1 Incomplete exception guard β€” AttributeError and RuntimeError escape

    The except ImportError only catches a missing sandlock package. Two new failure modes introduced in this PR are not covered:

    1. If the installed sandlock version does not export min_landlock_abi, the pre-check on line 297 (sandlock.min_landlock_abi()) raises AttributeError, which propagates uncaught and hard-fails the test instead of skipping.
    2. If the SandlockSandbox() constructor on line 303 raises RuntimeError for any reason not covered by the pre-check, the test likewise hard-fails rather than skipping cleanly.

    Broadening the catch to except (ImportError, RuntimeError, AttributeError) β€” or simply except Exception β€” would make the test resilient to all construction-time failures.

Reviews (7): Last reviewed commit: "fix(sandbox): isolate host environment f..." | 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
Contributor

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."
)

@coderabbitai coderabbitai Bot left a comment

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.

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

praisonai-triage-agent Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

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
congwang-mk added a commit to congwang-mk/PraisonAI that referenced this pull request Jun 3, 2026
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.
@MervinPraison

Copy link
Copy Markdown
Owner

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

@MervinPraison

Copy link
Copy Markdown
Owner

@claude

Merge review β€” blocked (fork PR, stale CI)

  • mergeStateStatus: UNSTABLE
  • Only CodeRabbit + GitGuardian checks present β€” no review or test-windows on current head
  • Branch fix/sandlock-integration is 2 months stale vs main

Fix list:

  1. Rebase onto latest main and resolve conflicts in sandlock.py.
  2. Trigger review + test-windows CI (maintainer may need to approve workflow for fork).
  3. Confirm unit tests in test_sandlock_sandbox.py still pass after rebase.

Wrapper/sandbox layer βœ… | unit tests present βœ…

@praisonai-triage-agent

praisonai-triage-agent Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

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


PR Merge Review - Sandlock Integration ❌

CRITICAL FINDING: Multiple Unresolved Issues from April Review

  • Read AGENTS.md architecture guidelines
  • Check branch conflicts with main
  • Analyze existing reviewer feedback (CodeRabbit, Greptile, Gemini, Copilot)
  • Review sandlock integration against previous Claude feedback
  • Assess critical bugs from April review
  • Provide final merge recommendation

RECOMMENDATION: DO NOT MERGE

Critical Issues (BLOCKING)

1. 🚨 INCORRECT KERNEL VERSION (sandlock.py:92)

Status: UNFIXED from April review

# Line 92 - WRONG kernel version
">= 6.12 with CONFIG_SECURITY_LANDLOCK=y"
# Should be:  
">= 5.13 with CONFIG_SECURITY_LANDLOCK=y"
  • Impact: Users on kernels 5.13-6.11 get misleading error messages
  • All reviewers flagged this: CodeRabbit, Greptile, previous Claude review
  • Still not fixed 2 months later

2. πŸ”’ SECURITY VULNERABILITY (sandlock.py:395)

Status: PARTIALLY FIXED but still vulnerable

# Line 395 - Still passes abs_path directly
extra_readable=[abs_path],
  • Original issue: execute_file passed entire parent directory to readable allowlist
  • Partial fix: Now passes file path instead of directory
  • Remaining vulnerability: No validation against filesystem root exposure
  • Should check: if os.path.dirname(abs_path) == "/" then handle specially

3. πŸ“Š TEST COVERAGE GAP (test_sandlock_sandbox.py:253)

Status: LOGIC ERROR in integration test

# Lines 253-254 - Wrong ABI threshold  
if sandlock.landlock_abi_version() < 1:
    pytest.skip("Landlock not supported")
  • Issue: Test skips on ABI < 1, but constructor requires ABI >= 6
  • Result: Test may hard-fail on intermediate-ABI kernels instead of clean skip
  • Greptile identified this mismatch

Secondary Issues

4. Exception Handling (sandlock.py:282)

  • Broad except Exception without logging - acceptable but not ideal
  • CodeRabbit suggested narrowing to specific exceptions

5. Mock Test Consistency (test_sandlock_sandbox.py:123-124)

  • Mock uses bytes (correct) but doesn't set success=True explicitly
  • Minor test hygiene issue

What Was Fixed βœ…

  • Network policy semantics: Correctly uses net_allow array format
  • Timeout detection: Uses exit_code == -1 sentinel correctly
  • Bytes decoding: Added _decode() helper for UTF-8 conversion
  • Context manager: Added proper with Sandbox() cleanup
  • CPU limits: Now passes max_cpu parameter
  • Path filtering: Filters fs_readable to existing paths only
  • Fail-loud behavior: Removed silent fallback to subprocess

Branch Status Assessment

  • 80+ files diverged from main (massive scope creep)
  • 2 months stale - significant rebase conflicts expected
  • No CI runs on current head (fork limitations)
  • mergeStateStatus: UNSTABLE

Next Steps Required

  1. Author must fix the 3 critical issues above
  2. Rebase onto latest main and resolve conflicts
  3. Trigger CI workflows (maintainer approval needed for fork)
  4. Address all reviewer feedback from April

This is quality defensive security work with excellent fail-loud architecture, but the critical bugs make it unsafe to merge in current state.

Final Verdict: ❌ BLOCK MERGE until critical issues resolved

@MervinPraison

Copy link
Copy Markdown
Owner

@claude Stale CI β€” required workflows missing on HEAD (2026-06-12 audit)

Status: mergeStateStatus=UNSTABLE; head commit (ec8b347) only shows GitGuardian (pass). No recent review, test-windows, or other required PR workflows on this SHA.

Fix:

  1. Rebase onto current origin/main and push to trigger a full workflow matrix:
    git fetch origin && git rebase origin/main && git push --force-with-lease
  2. If rebase is clean but jobs still skip: use Actions β†’ Re-run all jobs on the latest workflow run for this branch, or push an empty commit.
  3. After CI runs, fix any sandbox/sandlock test failures in fix(sandbox) scope and ensure all required checks green before merge.

Do not merge until standard PR checks have run and passed on the latest commit.

@praisonai-triage-agent

praisonai-triage-agent Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

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


PR Status Assessment - Sandlock Integration ❌

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:

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

RECOMMENDATION: DO NOT MERGE

Critical Issues (BLOCKING)

1. 🚨 INCORRECT KERNEL VERSION (sandlock.py:92)

Status: UNFIXED from April review

# Line 92 - WRONG kernel version
">= 6.12 with CONFIG_SECURITY_LANDLOCK=y"
# Should be:  
">= 5.13 with CONFIG_SECURITY_LANDLOCK=y"
  • Impact: Users on kernels 5.13-6.11 get misleading error messages
  • All reviewers flagged this: CodeRabbit, Greptile, previous Claude review
  • Still not fixed 2 months later

2. πŸ”’ SECURITY VULNERABILITY (sandlock.py:395)

Status: PARTIALLY FIXED but still vulnerable

# Line 395 - Still passes abs_path directly
extra_readable=[abs_path],
  • Original issue: execute_file passed entire parent directory to readable allowlist
  • Partial fix: Now passes file path instead of directory
  • Remaining vulnerability: No validation against filesystem root exposure
  • Should check: if os.path.dirname(abs_path) == "/" then handle specially

3. πŸ“Š TEST COVERAGE GAP (test_sandlock_sandbox.py:253)

Status: LOGIC ERROR in integration test

# Lines 253-254 - Wrong ABI threshold  
if sandlock.landlock_abi_version() < 1:
    pytest.skip("Landlock not supported")
  • Issue: Test skips on ABI < 1, but constructor requires ABI >= 6
  • Result: Test may hard-fail on intermediate-ABI kernels instead of clean skip
  • Greptile identified this mismatch

Secondary Issues

4. Exception Handling (sandlock.py:282)

  • Broad except Exception without logging - acceptable but not ideal
  • CodeRabbit suggested narrowing to specific exceptions

5. Mock Test Consistency (test_sandlock_sandbox.py:123-124)

  • Mock uses bytes (correct) but doesn't set success=True explicitly
  • Minor test hygiene issue

What Was Fixed βœ…

  • Network policy semantics: Correctly uses net_allow array format
  • Timeout detection: Uses exit_code == -1 sentinel correctly
  • Bytes decoding: Added _decode() helper for UTF-8 conversion
  • Context manager: Added proper with Sandbox() cleanup
  • CPU limits: Now passes max_cpu parameter
  • Path filtering: Filters fs_readable to existing paths only
  • Fail-loud behavior: Removed silent fallback to subprocess

Branch Status Assessment

  • 57 commits diverged from main (significant scope creep)
  • 2 months stale - significant rebase conflicts expected
  • No CI runs on current head (fork limitations)
  • mergeStateStatus: UNSTABLE

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

Next Steps Required

  1. Author must fix the 3 critical issues above
  2. Rebase onto latest main and resolve conflicts
  3. Trigger CI workflows (maintainer approval needed for fork)
  4. Address all reviewer feedback from April

This is quality defensive security work with excellent fail-loud architecture, but the critical bugs make it unsafe to merge in current state.

Final Verdict: ❌ BLOCK MERGE until critical issues resolved


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

@congwang-mk congwang-mk force-pushed the fix/sandlock-integration branch from ec8b347 to 3cece7f Compare June 13, 2026 00:34
congwang-mk added a commit to congwang-mk/PraisonAI that referenced this pull request Jun 13, 2026
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.
@congwang-mk

Copy link
Copy Markdown
Contributor Author

@MervinPraison β€” rebased onto latest main (was 74 commits behind, clean rebase) and addressed the reviewer feedback. Full breakdown of what changed and, importantly, what I deliberately did not change and why.

Fixed

  • Test ABI-skip mismatch (Greptile) β€” test_real_sandlock_integration skipped on landlock_abi_version() < 1, but the constructor raises when abi < min_landlock_abi() (= 6). On an intermediate-ABI kernel (1–5) the test wouldn't skip and would hard-fail in __init__ instead of skipping cleanly. Now skips below sandlock.min_landlock_abi().
  • Write-dir not readable (Gemini, MEDIUM) β€” Landlock does not grant read from write. The sandboxed process couldn't read its own working/temp dir. Writable dirs are now mirrored into the read allowlist. Verified with a real-sandlock write-then-read-back smoke test.
  • Broad except swallowed the traceback (CodeRabbit, BLE001) β€” added logger.exception(...) so the underlying cause is logged before returning a FAILED result.

Not changed β€” and why

  • Kernel version in the error message (>= 6.12, flagged as "should be 5.13") β€” this is now correct and I've left it. The flag refers to an earlier revision that gated on abi < 1; ABI v1 shipped in kernel 5.13, so back then 5.13 was right. The code was since changed to gate on sandlock.min_landlock_abi(), which returns 6 β€” and Landlock ABI v6 shipped in Linux 6.12. So 6.12 matches the actual requirement; changing it to 5.13 would understate the real minimum and mislead users on 5.13–6.11 kernels into thinking SandlockSandbox will work for them when the constructor will reject it. (Verified: min_landlock_abi() == 6 on this host.)
  • execute_file "root exposure" β€” already addressed in an earlier commit: it passes the single resolved script path to the read allowlist (extra_readable=[abs_path]), not the parent directory, so a script at /script.py can never expose /. No dirname == "/" guard is needed because no dirname is added.
  • Mock success=True/bytes β€” already present in the success test (the result mock sets success = True and byte-valued stdout/stderr).

Verification

  • pytest tests/unit/sandbox/test_sandlock_sandbox.py β†’ 11/11 pass on the rebased tree.
  • Real-sandlock smoke test (ABI 8 kernel): execute, execute_file with args, and write-then-read in the working dir all return COMPLETED.

@MervinPraison

Copy link
Copy Markdown
Owner

@claude

Merge conflict resolution (automated analysis vs origin/main)

PR #1367: fix(sandbox): correct sandlock integration semantics and fail loud
Branch: fix/sandlock-integration

You have direct access to this branch β€” checkout, compare with origin/main, edit conflicted files, commit, and push with --force-with-lease after rebase.

Multi-agent analysis (do this first)

  1. Compare this PR diff with current origin/main β€” several areas (CLI session store, tool registry, doctor/cli_backend) may already be landed via other PRs (fix: consolidate tool registries, move framework dispatch to adapters, and fix lazy loadingΒ #2003, fix(cli): wire praison run session continuity to project session storeΒ #1963, fix: wire CLI run session continuity to project session storeΒ #1949, etc.).
  2. If the behavioural fix is already on main, close this PR as superseded and link the landing PR.
  3. If still needed, rebase onto latest origin/main and resolve only the hunks below (prefer main for unrelated refactors; keep this PR intent for its scoped fix).

Conflicting files (1)

  • src/praisonai/praisonai/sandbox/sandlock.py

Per-file conflict regions

src/praisonai/praisonai/sandbox/sandlock.py

<<<<<<< HEAD
        completed_at = time.time()
        duration = completed_at - started_at
        stdout = self._decode(result.stdout)
        stderr = self._decode(result.stderr)

        # sandlock uses exit_code == -1 as the ExitStatus::Timeout sentinel
        # (see sandlock's python/src/sandlock/_sdk.py).  This is a
        # structural signal β€” Sandbox.run() doesn't populate result.error
        # for timeouts, so string-matching on it is unreliable.
        if result.success:
            status = SandboxStatus.COMPLETED
            error = None
        elif result.exit_code == -1:
            status = SandboxStatus.TIMEOUT
            error = f"Execution timed out after {limits.timeout_seconds}s"
        else:
            status = SandboxStatus.FAILED
            error = f"Execution failed with exit code {result.exit_code}: {stderr}"

        return SandboxResult(
            execution_id=execution_id,
            status=status,
            exit_code=result.exit_code,
            stdout=stdout,
            stderr=stderr,
            duration_seconds=duration,
            started_at=started_at,
            completed_at=completed_at,
            error=error,
            metadata={
                "sandbox_type": "sandlock",
                "landlock_enabled": True,
                "seccomp_enabled": True,
            },
... (truncated)

Step-by-step rebase / fix

git fetch origin main
git checkout fix/sandlock-integration
git rebase origin/main
# For each file above: remove <<<<<<< / ======= / >>>>>>> markers
# Keep correct combined logic; run targeted tests for touched modules
git add <resolved-files>
git rebase --continue
git push --force-with-lease origin HEAD

Suggested verification (adjust per PR):

  • pytest on paths listed in conflicting files
  • praisonai doctor / CLI smoke if CLI files conflicted

Posted by conflict bot: exact regions from git merge-tree + worktree merge simulation vs origin/main.

@praisonai-triage-agent

praisonai-triage-agent Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

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


PR Analysis: Sandlock Integration Fixes

Todo List:

  • Read AGENTS.md architecture guidelines
  • Analyze current sandlock.py implementation on this branch
  • Check main branch's sandlock.py for existing fixes
  • Review all previous reviewer feedback
  • Determine if PR is still needed or has been superseded
  • Provide conflict resolution guidance if needed

Summary

This PR significantly improves the SandlockSandbox wrapper with critical security and correctness fixes. The changes are still needed - the main branch contains the older, vulnerable implementation with silent fallback behavior.

Key Improvements in This PR

βœ… What Was Fixed

  1. Fail-loud behavior: Constructor now raises RuntimeError when Landlock is unavailable (no silent fallback)
  2. Network policy: Correctly uses net_allow array format instead of incorrect net_allow_hosts
  3. Timeout detection: Uses sandlock's exit_code == -1 sentinel (not error string matching)
  4. Bytes decoding: Added _decode() helper for proper UTF-8 conversion
  5. Context manager: Proper with Sandbox() cleanup
  6. CPU limits: Now passes max_cpu parameter correctly
  7. Path filtering: Filters fs_readable to existing paths only
  8. Execute file: Direct script execution without shell interpretation
  9. Writable-to-readable mirroring: Ensures sandbox can read its own working directory

Critical Issues Still Present

πŸ”΄ BLOCKING Issues

  1. Incorrect kernel version in error message (line 92)

    • Current: ">= 6.12"
    • The code checks min_landlock_abi() which returns 6
    • Landlock ABI v6 was introduced in Linux 6.12, so the message is CORRECT
    • Previous reviewers incorrectly flagged this as wrong (they confused ABI v1 with ABI v6)
  2. Execute_file passes file path, not directory (line 409)

    • Current implementation passes [abs_path] to extra_readable
    • This is CORRECT - it only exposes the single file, not its parent directory
    • No root filesystem exposure vulnerability exists
  3. Test ABI skip mismatch (test file line 257)

    • Test skips when landlock_abi_version() < min_landlock_abi()
    • This is CORRECT - matches the constructor's requirement

Conflict Resolution

The PR has a merge conflict in src/praisonai/praisonai/sandbox/sandlock.py. The conflict appears to be in the _run_sandlocked method area.

Recommended Resolution:

  1. Keep this PR's implementation - it has all the security fixes
  2. The main branch still has the vulnerable version with:
    • Silent fallback to SubprocessSandbox (security risk)
    • Incorrect network policy (net_allow_hosts instead of net_allow)
    • Missing bytes decoding
    • No Landlock path existence filtering

Architecture Assessment βœ…

The changes comply with PraisonAI architecture:

  • Protocol-driven: Properly placed in wrapper layer (praisonai/), not core SDK
  • Backward compatible: Breaking change is intentional and documented for security
  • Performance: No hot-path impact, lazy imports maintained
  • Security-first: Fails loud on missing kernel support (correct approach)

Recommendation

MERGE THIS PR after rebasing to resolve conflicts. The security improvements are critical and the implementation is sound.

The previous reviewers' concerns about:

  • Kernel version "6.12" - is actually correct for ABI v6
  • Root filesystem exposure - already fixed (passes file, not directory)
  • Test coverage - integration test properly skips on unsupported systems

@congwang-mk Your latest changes from June properly addressed the legitimate issues. The PR is ready to merge once rebased.


congwang and others added 4 commits June 19, 2026 21:25
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).
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.
… skip, and error logging

- Mirror writable dirs into the Landlock read allowlist. Landlock does not
  grant read from write, so the sandboxed process could not read its own
  working/temp directory (Gemini MEDIUM).
- Fix test_real_sandlock_integration to skip below sandlock.min_landlock_abi()
  instead of ABI < 1, so it cleanly skips on intermediate-ABI kernels rather
  than hard-failing in the constructor (Greptile).
- Log the full traceback in the broad except around sandlock execution so the
  underlying cause is not lost (CodeRabbit BLE001).
@congwang-mk congwang-mk force-pushed the fix/sandlock-integration branch from 3cece7f to 4a1f94e Compare June 20, 2026 04:27
Comment on lines 367 to 379
@@ -320,10 +377,10 @@ async def execute(
self._used_subprocess_fallback = True
result = await fallback.execute(code, language, limits, env, working_dir)
return self._tag_subprocess_fallback(result)

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.

P1 Stale fallback branches contradict fail-loud guarantee

execute() and run_command() (lines 367–379 and 449–461) still contain the full subprocess-fallback blocks the PR description says were removed. Since __init__ now raises RuntimeError before the object can be constructed without Landlock, these branches are dead code in production β€” but they create an inconsistency with execute_file(), which was refactored to have no fallback at all. If a future caller or test reaches execute() in a state where is_available returns False (e.g. mocked incorrectly, or AttributeError swallowed by the is_available exception handler), execute() and run_command() silently degrade to subprocess isolation while execute_file() would propagate the error. The three execution methods should behave uniformly.

execute() and run_command() still carried subprocess-fallback branches
(inherited from main during the rebase) while execute_file() had none.
Since __init__ now raises RuntimeError when Landlock is unavailable, the
object can't be constructed without kernel isolation, making those
branches dead code β€” and an inconsistency: if is_available ever returned
False, execute()/run_command() would silently degrade to subprocess
isolation while execute_file() propagated the error.

Remove the fallback blocks and the now-orphaned _tag_subprocess_fallback
helper so all three execution methods behave uniformly and fail loud.
The _used_subprocess_fallback / metadata fields are retained: they now
report a constant "not fallen back / kernel-isolated", keeping main's
metadata schema intact and truthful.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +280 to +281
if env:
sandbox_kwargs["env"] = dict(env)

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.

P1 if env: treats an explicitly-passed empty dict (env={}) as falsy and skips injecting the env key into sandbox_kwargs. This means a caller who passes env={} intending to run the sandboxed process with a clean, secrets-free environment will instead inherit the full parent process environment β€” silently leaking any SECRET_KEY, DATABASE_URL, or similar vars present in the parent. The correct guard is if env is not None:; the empty-dict case should still set sandbox_kwargs["env"] = {} to honour the caller's intent.

Suggested change
if env:
sandbox_kwargs["env"] = dict(env)
if env is not None:
sandbox_kwargs["env"] = dict(env)

…=True)

The sandlock child inherited the parent process's FULL environment on
every run: _build_sandbox_kwargs never set clean_env, so sandlock used
its default (clean_env=False = inherit everything), leaking host secrets
(SECRET_KEY, DATABASE_URL, ...) into untrusted sandboxed code. This also
diverged from SubprocessSandbox, which deliberately builds a minimal,
policy-driven env instead of copying the host's.

The reviewer-reported symptom (env={} skipped by `if env:`) was real but
its proposed fix was insufficient: with clean_env=False, setting env={}
overrides nothing and the full host environment still leaks. The actual
lever is clean_env.

- Set clean_env=True so the child starts from a minimal baseline
  (PATH/HOME/USER/TERM/LANG); per-call env is layered on top.
- Change the guard to `if env is not None:` so an explicit empty dict is
  honoured rather than skipped.
- Add a regression test asserting clean_env=True and that env={} /
  populated / None are all handled without host-env inheritance.

Verified behaviorally: with SECRET_KEY set in the parent, the sandboxed
process sees it as absent for env=None and env={}, while an explicit
override still applies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MervinPraison

Copy link
Copy Markdown
Owner

@claude

Automated merge readiness report (local git rebase origin/main scan, 2026-06-22 22:03 UTC)

GitHub status

CI (key jobs: test-core, smoke, test-windows, main (3.11), review)

  • Key CI gaps: test-core: missing; smoke: missing; test-windows: missing; main (3.11): missing; review: missing

Guidance

  1. git fetch origin && git rebase origin/main on the PR branch.
  2. Resolve conflicts aligning with latest main unless intentional.
  3. git push --force-with-lease after rebase.
  4. Ensure key CI jobs pass.

Final verdict after fix: reply merge-ready (CLEAN + green CI) or list remaining blockers.

@praisonai-triage-agent

praisonai-triage-agent Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

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


PR Merge Readiness Assessment - Sandlock Integration

Todo List:

  • Read AGENTS.md architecture guidelines
  • Review sandlock.py changes and test file changes
  • Analyze previous reviewer feedback (CodeRabbit, Greptile, Gemini, Copilot)
  • Check architecture compliance
  • Assess if critical issues are resolved
  • Provide final merge verdict

Summary

This PR significantly improves the SandlockSandbox wrapper with critical security and correctness fixes. The changes properly implement fail-loud behavior, fix network policy semantics, add proper UTF-8 decoding, and ensure correct timeout detection.

Architecture Compliance βœ…

The changes comply with PraisonAI architecture principles per AGENTS.md:

  • Protocol-driven: Properly placed in wrapper layer (praisonai/), not core SDK
  • Backward compatible: Breaking change is intentional and documented for security
  • Performance: No hot-path impact, lazy imports maintained

Key Improvements βœ…

  1. Fail-loud behavior: Constructor raises RuntimeError when Landlock unavailable (no silent fallback)
  2. Network policy: Correctly uses net_allow array format
  3. Timeout detection: Uses sandlock's exit_code == -1 sentinel
  4. Bytes decoding: Added _decode() helper for UTF-8 conversion
  5. Context manager: Proper with Sandbox() cleanup
  6. CPU limits: Now passes max_cpu parameter
  7. Path filtering: Filters fs_readable to existing paths only
  8. Execute file: Direct script execution via path, not shell interpretation
  9. Environment isolation: clean_env=True prevents host secret leakage
  10. Writable-to-readable mirroring: Ensures sandbox can read its own working directory

Critical Issues Assessment

Previously Flagged Issues - Status:

  1. Kernel version in error message (Line 92-94)

    • Current: ">= 6.12" for minimum ABI requirement
    • CORRECT βœ… - The code checks min_landlock_abi() which returns 6. Landlock ABI v6 was introduced in Linux 6.12, so the message is accurate
    • Previous reviewers incorrectly conflated ABI v1 (Linux 5.13) with ABI v6 (Linux 6.12)
  2. Execute_file root exposure (Line 423)

    • Current: Passes [abs_path] (single file) to extra_readable
    • CORRECT βœ… - Only exposes the single script file, not its parent directory
    • No root filesystem exposure vulnerability exists
  3. Test ABI skip mismatch (test file line 297)

    • Test skips when landlock_abi_version() < min_landlock_abi()
    • FIXED βœ… - Properly matches constructor's requirement
  4. Exception handling (Line 306)

    • Broad except Exception with logger.exception(...)
    • FIXED βœ… - Now logs full traceback before returning FAILED result
  5. Environment handling (Line 293)

    • Uses if env is not None: guard
    • FIXED βœ… - Correctly honors explicit empty dict env={}

Remaining Minor Issues

  • Stale field: _used_subprocess_fallback in get_status() (line 516) is never set to True since fallback was removed - cosmetic issue only
  • Mock test consistency: Test mocks could set success=True explicitly for clarity

CI Status ⚠️

BLOCKING: No CI runs on current head

  • Missing: test-core, smoke, test-windows, main (3.11), review
  • This is a fork PR - maintainer needs to approve workflow runs

Final Verdict

βœ… APPROVE WITH CONDITIONS

The sandlock integration improvements are architecturally sound and critically important for security. All major issues have been properly addressed:

  1. Security improvements are excellent - fail-loud behavior prevents silent security degradation
  2. Implementation is correct - kernel version, path exposure, and environment handling are all properly implemented
  3. Test coverage is comprehensive - 11 tests pass, including integration test

Required Before Merge:

  1. CI must pass - Maintainer needs to approve workflow runs for this fork PR
  2. Rebase on main if conflicts exist (currently shows as MERGEABLE)
  3. Optional cleanup: Remove unused _used_subprocess_fallback field

The previous reviewers' concerns about kernel version and root exposure were based on misunderstandings - the current implementation is correct.


Recommendation: Once CI passes, this PR should be merged. The security improvements are critical and the implementation quality is high.

@MervinPraison MervinPraison merged commit 55dc751 into MervinPraison:main Jun 24, 2026
2 checks passed
@MervinPraison

Copy link
Copy Markdown
Owner

@claude if any gaps or issues, create a new branch, implement the fix and then create pr

@praisonai-triage-agent

praisonai-triage-agent Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MervinPraison's task β€”β€” View job β€’ claude/pr-1367-20260624-2048 β€’ Create PR βž”


Sandlock integration β€” remaining gap cleanup βœ…

PR #1367 is already merged and resolved most reviewer concerns. I audited the current merged code for the gaps reviewers left open, fixed the two valid ones, and confirmed the rest were already addressed.

  • Read current merged sandlock.py + tests
  • Validate reviewer concerns against current code
  • Implement valid fixes
  • Run unit tests (11 passed, 1 skipped)
  • Commit, push, create PR

Fixed (2 valid Greptile P1 findings)

  1. Stale _used_subprocess_fallback field β€” the silent SubprocessSandbox fallback was removed, but the flag lingered at sandlock.py:67,516,523, always False, leaking a dead/misleading value through get_status(). Removed the field; since __init__ now fails loud when Landlock is unavailable, a constructed instance is always kernel-isolated and get_status() reports that directly (subprocess_fallback: False).
  2. Integration test exception guard too narrow β€” test_real_sandlock_integration only caught ImportError. sandlock.min_landlock_abi() can raise AttributeError (older sandlock) and the constructor can raise RuntimeError, hard-failing CI instead of skipping. Broadened the guard to skip cleanly on both.

Confirmed NOT bugs (already correct in merged code)

  • Kernel >= 6.12 β€” correct for ABI v6. min_landlock_abi() returns 6, and ABI v6 shipped in Linux 6.12. Earlier "should be 5.13" flags predate the switch to min_landlock_abi().
  • execute_file root exposure β€” uses extra_readable=[abs_path] (single resolved file, never a parent dir), so /script.py can't expose /.
  • env={} empty-dict leak β€” already fixed: guard is if env is not None: (line 293).
  • Broad except β€” already logs the traceback via logger.exception(...).

Verification

pytest tests/unit/sandbox/test_sandlock_sandbox.py
β†’ 11 passed, 1 skipped (real-sandlock integration; sandlock not installed in CI)

--- β€’ Branch: claude/pr-1367-20260624-2048

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