Skip to content

fix(python): raise ExecError for command start failures#958

Open
G4614 wants to merge 13 commits into
mainfrom
g4614/pol-99-python-exec-error
Open

fix(python): raise ExecError for command start failures#958
G4614 wants to merge 13 commits into
mainfrom
g4614/pol-99-python-exec-error

Conversation

@G4614

@G4614 G4614 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Raise ExecError from Python SimpleBox when native exec startup fails before an execution handle exists.

Test plan:

  • cargo fmt --all --check
  • python3 -m py_compile sdks/python/boxlite/errors.py sdks/python/boxlite/simplebox.py sdks/python/boxlite/sync_api/_simplebox.py sdks/python/tests/test_errors.py sdks/python/tests/test_simplebox.py sdks/python/tests/test_sync_simplebox.py
  • BOXLITE_DEPS_STUB=1 cargo check -p boxlite-python
  • git diff --check
  • uv run --project sdks/python --group dev pytest tests/test_errors.py -q stopped because it began building the native Python package instead of running as a lightweight unit check

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Command-start failures are classified as execution errors across Rust, Go, and Python layers. Python wrappers translate them into ExecError with shell-compatible exit codes, while the runner returns structured HTTP responses. Tests cover missing and unexecutable commands.

Changes

Execution error handling

Layer / File(s) Summary
Command-start error classification
src/boxlite/src/portal/interfaces/exec.rs, sdks/go/errors.go, sdks/go/boxlite_test.go
spawn_failed responses become execution errors, and the Go SDK adds and tests IsExecution.
Runner HTTP error mapping
apps/runner/pkg/api/controllers/boxlite_exec.go, apps/runner/pkg/api/controllers/boxlite_exec_test.go
Exec-start failures are centralized into structured 422 execution responses, 409 state responses, and 500 fallbacks.
Python binding error propagation
sdks/python/src/util.rs, sdks/python/src/box_handle.rs, sdks/python/src/lib.rs
Execution errors map to the exported CommandStartError, while other errors retain generic mapping.
SimpleBox exception translation
sdks/python/boxlite/errors.py, sdks/python/boxlite/simplebox.py, sdks/python/boxlite/sync_api/_simplebox.py, sdks/python/boxlite/exec.py, sdks/python/tests/*, scripts/test/e2e/cases/*
Async and sync wrappers convert command-start failures into ExecError, derive exit codes 126 or 127, and test typed start failures and HTTP 422 responses.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ExecutionInterface
  participant PythonBinding
  participant SimpleBox
  ExecutionInterface->>PythonBinding: return BoxliteError::Execution(detail)
  PythonBinding->>SimpleBox: raise CommandStartError(detail)
  SimpleBox->>SimpleBox: derive exit code 126 or 127
  SimpleBox-->>PythonBinding: raise ExecError(command, exit_code, detail)
Loading

Possibly related PRs

Suggested labels: bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is missing the required Summary, Changes, and How to verify sections from the template. Add the template sections with a 1-2 sentence Summary, a Changes bullet list, a concrete How to verify step, and optional Risks / rollout.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: Python SimpleBox now raises ExecError on command start failures.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch g4614/pol-99-python-exec-error

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.

Comment thread sdks/python/boxlite/errors.py Fixed
@G4614
G4614 marked this pull request as ready for review July 15, 2026 07:45
@G4614
G4614 requested a review from a team as a code owner July 15, 2026 07:45
@boxlite-agent

boxlite-agent Bot commented Jul 15, 2026

Copy link
Copy Markdown

📦 BoxLite review — looks good · b02456d

Review evidence

  • git diff --numstat origin/main...HEAD && git diff origin/main...HEAD — 9 files, exec-start error mapping Rust+Python
  • grep spawn_error/reason chain in guest exec + portal/interfaces/exec.rs — format!("{}: {}",reason,detail) matches util.rs prefix check
  • grep 'Failed to spawn' in executor.rs spawn_with_pipes — ENOENT/EACCES strerror text matches 127/126 mapping
  • grep '.exec(' src/*.rs for other unmapped call sites — only one call site, now uses map_exec_start_err
  • cd sdks/python && python3 -m pytest tests/test_errors.py -q — 21 passed
  • pytest tests/test_simplebox.py / test_sync_simplebox.py (new exec-error tests) — needs built native ext + KVM microVM+alpine image, unavailable here
  • cargo check on box_handle.rs/util.rs/lib.rs — cargo not installed in sandbox

Risk notes

  • error classification correctness — traced guest spawn_error -> BoxliteError::Internal("spawn_failed: {detail}") -> util.rs prefix match -> ExecStartError; string match is exact, not fragile given fixed format string
  • exit code heuristic — 'no such file or directory'/'permission denied' are POSIX strerror text on both Linux and macOS, consistent with 127/126 shell convention
  • fallback placeholder class — _ExecStartError placeholder (RuntimeError subclass) duplicated in simplebox.py and sync_api/_simplebox.py mirrors real pyo3 create_exception! base, so except clause behaves correctly whether or not native ext is present
  • test coverage — new tests only exercised via syntax/logic tracing, not executed (no build toolchain/VM in sandbox); rust src changes not compiled
sdks/python/boxlite/errors.py
  _exec_start_exit_code  +8/-0  maps stderr text to 126/127
sdks/python/boxlite/simplebox.py
  SimpleBox.exec  +22/-3  catches _ExecStartError, raises ExecError
sdks/python/boxlite/sync_api/_simplebox.py
  SyncSimpleBox.exec  +21/-3  mirrors async fix for sync API
sdks/python/src/util.rs
  map_exec_start_err/is_exec_start_failure  +23/-1  new ExecStartError exception + prefix match
sdks/python/src/box_handle.rs
  PyBox exec  +2/-2  swaps map_err for map_exec_start_err
sdks/python/src/lib.rs
  boxlite_python module init  +2/-0  registers _ExecStartError in module
sdks/python/tests/test_simplebox.py
  test_simplebox_*exec*  +21/-0  new ENOENT/EACCES exec tests
sdks/python/tests/test_sync_simplebox.py
  test_*exec_error*  +20/-1  sync counterparts of new tests

reviewed b02456d in a BoxLite microVM · @boxlite-agent review to re-run · powered by BoxLite

@G4614
G4614 enabled auto-merge July 15, 2026 07:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
sdks/python/src/util.rs (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a docstring to the exported exception.

As per coding guidelines, comprehensive docstrings should be written for all public classes. Since CommandStartError is exported to Python as a public exception class, consider providing a docstring.

💡 Proposed refactor
-create_exception!(boxlite_python, CommandStartError, PyRuntimeError);
+create_exception!(boxlite_python, CommandStartError, PyRuntimeError, "Raised when a command fails to start.");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/python/src/util.rs` at line 4, Add a descriptive docstring to the
exported Python exception created by CommandStartError, using the
create_exception! declaration in boxlite_python. Ensure the resulting public
exception class exposes documentation describing when command startup failures
are raised.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@sdks/python/src/util.rs`:
- Line 4: Add a descriptive docstring to the exported Python exception created
by CommandStartError, using the create_exception! declaration in boxlite_python.
Ensure the resulting public exception class exposes documentation describing
when command startup failures are raised.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ae3f57a8-52f6-4b37-b2ef-f22532b656fc

📥 Commits

Reviewing files that changed from the base of the PR and between 08de7ba and 0146f67.

📒 Files selected for processing (10)
  • sdks/python/boxlite/errors.py
  • sdks/python/boxlite/exec.py
  • sdks/python/boxlite/simplebox.py
  • sdks/python/boxlite/sync_api/_simplebox.py
  • sdks/python/src/box_handle.rs
  • sdks/python/src/lib.rs
  • sdks/python/src/util.rs
  • sdks/python/tests/test_simplebox.py
  • sdks/python/tests/test_sync_simplebox.py
  • src/boxlite/src/portal/interfaces/exec.rs

@boxlite-agent boxlite-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📦 BoxLite review — 1 issue

Comment thread sdks/python/boxlite/sync_api/_simplebox.py

@boxlite-agent boxlite-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📦 BoxLite review — 1 issue

Comment thread sdks/python/tests/test_sync_simplebox.py Outdated
@G4614
G4614 force-pushed the g4614/pol-99-python-exec-error branch from 0e56100 to e348e9f Compare July 15, 2026 09:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
sdks/python/boxlite/simplebox.py (1)

234-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Revise the comment to explain why the code exists, not what it does.

The comment # Execute via Rust (returns PyExecution) describes what the code does rather than why it exists. As per coding guidelines, write clear and concise comments explaining why code exists, not what it does. You can either remove it if the code is self-explanatory or update it to clarify the design intent (e.g., # Delegate to the native extension for isolated execution).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/python/boxlite/simplebox.py` at line 234, Update or remove the comment
immediately before the native execution call in the SimpleBox execution flow; if
retained, state the design intent—delegating execution to the native extension
for isolation—rather than describing the return type or implementation
mechanics.

Source: Coding guidelines

sdks/python/tests/test_errors.py (1)

26-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a docstring to the test function.

As per coding guidelines, write comprehensive docstrings for all public functions and classes. Although this is a test function, it acts as a public API for the Pytest runner and falls under this requirement.

📝 Proposed fix
 def test_command_start_exit_code(message, expected):
+    """Verify that command start failure messages map to the correct exit codes."""
     assert _command_start_exit_code(message) == expected
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/python/tests/test_errors.py` around lines 26 - 27, Add a concise,
comprehensive docstring to the test_command_start_exit_code test function
describing its inputs and the expected exit-code assertion, while leaving the
test logic unchanged.

Source: Coding guidelines

sdks/python/tests/test_sync_simplebox.py (1)

85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add missing assertion for stderr.

For completeness and consistency with the async equivalent test_simplebox_command_not_found_raises_exec_error, consider asserting that the stderr contains an indicator that the command was not found.

💡 Proposed fix
     def test_command_not_found_raises_exec_error(self, shared_sync_runtime):
         """Direct command start failures raise ExecError, not bare RuntimeError."""
         with SyncSimpleBox(image="alpine:latest", runtime=shared_sync_runtime) as box:
             with pytest.raises(ExecError) as exc:
                 box.exec("definitely-not-a-boxlite-command-xyz")
             assert exc.value.exit_code == 127
+            assert "not found" in exc.value.stderr.lower()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/python/tests/test_sync_simplebox.py` around lines 85 - 90, Extend
test_command_not_found_raises_exec_error to assert that the captured ExecError
stderr indicates the command was not found, matching the coverage in
test_simplebox_command_not_found_raises_exec_error while preserving the existing
exit_code assertion.
sdks/python/tests/test_simplebox.py (1)

30-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add missing docstrings to test methods.

As per coding guidelines, write comprehensive docstrings for all public functions and classes. Although these are test methods, providing docstrings ensures consistency with the rest of the test suite.

  • sdks/python/tests/test_simplebox.py#L30-L36: Add a docstring to test_simplebox_unexecutable_command_raises_exit_code_126.
  • sdks/python/tests/test_sync_simplebox.py#L92-L103: Add a docstring to test_unexecutable_command_raises_exit_code_126.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdks/python/tests/test_simplebox.py` around lines 30 - 36, Add comprehensive
docstrings describing the expected exit code and permission-denied behavior to
test_simplebox_unexecutable_command_raises_exit_code_126 in
sdks/python/tests/test_simplebox.py (lines 30-36) and
test_unexecutable_command_raises_exit_code_126 in
sdks/python/tests/test_sync_simplebox.py (lines 92-103), without changing their
test logic.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@sdks/python/boxlite/simplebox.py`:
- Line 234: Update or remove the comment immediately before the native execution
call in the SimpleBox execution flow; if retained, state the design
intent—delegating execution to the native extension for isolation—rather than
describing the return type or implementation mechanics.

In `@sdks/python/tests/test_errors.py`:
- Around line 26-27: Add a concise, comprehensive docstring to the
test_command_start_exit_code test function describing its inputs and the
expected exit-code assertion, while leaving the test logic unchanged.

In `@sdks/python/tests/test_simplebox.py`:
- Around line 30-36: Add comprehensive docstrings describing the expected exit
code and permission-denied behavior to
test_simplebox_unexecutable_command_raises_exit_code_126 in
sdks/python/tests/test_simplebox.py (lines 30-36) and
test_unexecutable_command_raises_exit_code_126 in
sdks/python/tests/test_sync_simplebox.py (lines 92-103), without changing their
test logic.

In `@sdks/python/tests/test_sync_simplebox.py`:
- Around line 85-90: Extend test_command_not_found_raises_exec_error to assert
that the captured ExecError stderr indicates the command was not found, matching
the coverage in test_simplebox_command_not_found_raises_exec_error while
preserving the existing exit_code assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 80696e48-763f-4161-8f07-d51b7631aaac

📥 Commits

Reviewing files that changed from the base of the PR and between c6deec0 and e348e9f.

📒 Files selected for processing (15)
  • apps/runner/pkg/api/controllers/boxlite_exec.go
  • apps/runner/pkg/api/controllers/boxlite_exec_test.go
  • sdks/go/boxlite_test.go
  • sdks/go/errors.go
  • sdks/python/boxlite/errors.py
  • sdks/python/boxlite/exec.py
  • sdks/python/boxlite/simplebox.py
  • sdks/python/boxlite/sync_api/_simplebox.py
  • sdks/python/src/box_handle.rs
  • sdks/python/src/lib.rs
  • sdks/python/src/util.rs
  • sdks/python/tests/test_errors.py
  • sdks/python/tests/test_simplebox.py
  • sdks/python/tests/test_sync_simplebox.py
  • src/boxlite/src/portal/interfaces/exec.rs
🚧 Files skipped from review as they are similar to previous changes (9)
  • sdks/go/boxlite_test.go
  • sdks/go/errors.go
  • src/boxlite/src/portal/interfaces/exec.rs
  • sdks/python/src/lib.rs
  • sdks/python/src/box_handle.rs
  • apps/runner/pkg/api/controllers/boxlite_exec.go
  • sdks/python/boxlite/sync_api/_simplebox.py
  • sdks/python/boxlite/exec.py
  • sdks/python/src/util.rs

@boxlite-agent boxlite-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📦 BoxLite review — 1 issues

Comment thread sdks/python/tests/test_simplebox.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/test/e2e/cases/test_exec_comprehensive.py`:
- Around line 256-258: Update
scripts/test/e2e/cases/test_exec_comprehensive.py:256-258 to expect ExecError
from SimpleBox.exec instead of CommandStartError. Also update
scripts/test/e2e/cases/test_exec_comprehensive.py:278-281 to capture ExecError
as exc_info, optionally asserting its exit_code is 127.
🪄 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 Plus

Run ID: acdc4783-64a2-4ae9-b90d-d6b95a395468

📥 Commits

Reviewing files that changed from the base of the PR and between 4f1233e and 04b0d32.

📒 Files selected for processing (2)
  • scripts/test/e2e/cases/test_error_code_mapping.py
  • scripts/test/e2e/cases/test_exec_comprehensive.py

Comment thread scripts/test/e2e/cases/test_exec_comprehensive.py Outdated

@boxlite-agent boxlite-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📦 BoxLite review — 2 issues

Comment thread sdks/python/boxlite/errors.py Outdated
Comment on lines +10 to +18
def _command_start_exit_code(message: str) -> int:
"""Translate a pre-exec spawn failure to the conventional shell code."""
detail = message.casefold()
not_found_markers = (
"not found in $path",
"no such file or directory",
"os error 2",
)
return 127 if any(marker in detail for marker in not_found_markers) else 126

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 exit-code heuristic can't distinguish cwd vs binary
guest spawn errors for missing cwd and missing binary both render as 'No such file or directory (os error 2)', so a nonexistent cwd is reported as ExecError exit_code=127 (command-not-found) even though the command exists.

@boxlite-agent boxlite-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📦 BoxLite review — 1 issue

Comment thread sdks/python/src/util.rs Outdated
Comment on lines +18 to +23
fn is_exec_start_failure(err: &BoxliteError) -> bool {
let BoxliteError::Internal(message) = err else {
return false;
};
let detail = message.to_ascii_lowercase();
detail.contains("spawn_failed") && detail.contains("failed to spawn")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Exec-start detection relies on brittle string match
is_exec_start_failure matches lowercase substrings 'spawn_failed' and 'failed to spawn' from the formatted error text built in portal/interfaces/exec.rs and guest/service/exec/executor.rs; a future wording change in either unrelated file silently downgrades ExecError back to a bare RuntimeError with no compile-time link or shared constant.

Comment thread sdks/python/boxlite/simplebox.py Outdated
)
except _ExecStartError as e:
message = str(e)
raise ExecError(command_display, 126, message) from e

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 All exec-start failures hardcoded to exit_code 126
Command-not-found (ENOENT) and permission-denied (EACCES) both map to exit_code=126, diverging from POSIX convention (127 vs 126) that users may rely on when branching on exit codes; confirmed by tests asserting 126 for both cases in test_simplebox.py.

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.

1 participant