Skip to content

Expose the sandbox infrastructure errors reported by the compute plane - #223

Merged
drappier-charles merged 4 commits into
mainfrom
cdrappier/devin/sandbox-errors-sdk
Aug 20, 2026
Merged

Expose the sandbox infrastructure errors reported by the compute plane#223
drappier-charles merged 4 commits into
mainfrom
cdrappier/devin/sandbox-errors-sdk

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Fixes ENG-5027

Summary

Regenerates the controlplane client from main (make sdk-controlplane) so the new Sandbox.errors history is typed, and surfaces it on both the async and sync sandbox wrappers:

@property
def errors(self) -> list[SandboxInfrastructureError]:  # code, fatal, instance, message, time
    errors = self.sandbox.errors
    return list(errors) if not isinstance(errors, Unset) and errors else []

Entries are oldest first; fatal marks the failure that moved the sandbox to FAILED (a plain VM_EXITED restart is informational). errors is only returned when a single sandbox is read — list/find-all projections don't carry it. message is a normalized reason; raw VM/VMM log lines never leave the compute plane.

tests/manual/sandbox_errors.py prints the history for a sandbox (NAME=<sandbox> uv run python tests/manual/sandbox_errors.py, otherwise it creates and deletes a throwaway one).

The regeneration also dropped the public DriveSpec.size (the control plane no longer exposes it — drives have no size limit), which broke the drive wrapper and its tests. DriveCreateConfiguration.size stays accepted but is now deprecated and no longer serialized into create/update bodies, and DriveInstance.size / SyncDriveInstance.size read the legacy value off spec.additional_properties["size"]. The drive unit tests build their DriveSpec fixtures without size accordingly.

Link to Devin session: https://app.devin.ai/sessions/37e2b2b146c04e8daf2812d81fa0e2e4
Requested by: @drappier-charles


Note

Two new commits since the previous review: drops the now-removed DriveSpec.size from the drive ACL integration fixture, and adds an integration test covering the sandbox.errors accessor on both a single-get and a listing.

Written by Mendral for commit aab7db2.


Note

Cursor Bugbot is generating a summary for commit 60aaf6f. Configure here.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@drappier-charles drappier-charles self-assigned this Aug 20, 2026
@drappier-charles
drappier-charles self-requested a review August 20, 2026 02:14
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

mendral-app[bot]

This comment was marked as outdated.

@mendral-app

mendral-app Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🔀 Component Interaction Diagram

Based on the changes in this PR, here's how the sandbox infrastructure error flow works end-to-end:

sequenceDiagram
    participant User as User Application
    participant Wrapper as SandboxInstance<br/>(async/sync wrapper)
    participant API as Generated API Layer<br/>(list_sandboxes, etc.)
    participant Client as HTTP Client (httpx)
    participant CP as Control Plane API
    participant Compute as Compute Plane

    Note over Compute,CP: Error originates in infrastructure
    Compute->>CP: Report infrastructure error<br/>(code, fatal, instance, message, time)
    CP->>CP: Append to sandbox.errors[]

    Note over User,CP: User retrieves sandbox
    User->>Wrapper: sandbox.errors
    Wrapper->>API: get_sandbox(name)
    API->>Client: GET /api/sandboxes/{name}<br/>+ optional ?status= filter
    Client->>CP: HTTP Request
    CP-->>Client: Sandbox JSON (with errors[])
    Client-->>API: Response
    API-->>Wrapper: Sandbox model<br/>(SandboxInfrastructureError[] deserialized)
    Wrapper-->>User: list[SandboxInfrastructureError]<br/>(always a list, never Unset)

    Note over User,CP: Listing (projection excludes errors)
    User->>Wrapper: SandboxInstance.list(status="RUNNING")
    Wrapper->>API: list_sandboxes(status=...)
    API->>Client: GET /api/sandboxes?status=RUNNING
    Client->>CP: HTTP Request
    CP-->>Client: Sandbox[] (errors omitted)
    Client-->>API: Response
    API-->>Wrapper: Sandbox models (errors=UNSET)
    Wrapper-->>User: sandbox.errors → [] (empty list)
Loading

Summary of Key Flows

Flow Description
Error surfacing New SandboxInfrastructureError model propagates compute plane failures through the generated client to user-facing .errors property
Status filtering 8 list endpoints gain a status query param for server-side filtering
Wrapper contract .errors always returns list (never Unset/None) — listings return [], single gets return actual errors
Drive deprecation size param removed from creation; legacy reads via additional_properties (separate from error flow)

New model: SandboxInfrastructureError with fields: code, fatal, instance, message, time

Note

Posted by PR Sequence Diagram · Tag @mendral-app with feedback.

@mendral-app

mendral-app Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📋 Created Linear issue ENG-5027 — status: In Progress

  • Assignee: Charles Drappier (reviewer — bot PR)
  • Labels: SDK, Sandbox
  • Project: Sandbox MK3.1 Release
  • Estimate: L (391 additions, 58 deletions, 20 files)
  • PR linked: ✅ Issue will auto-close when this PR merges

Auto-created because no Linear reference was found in the PR title, description, or branch name.

Note

Posted by Linear Issue Enforcer · Tag @mendral-app with feedback.

drappier-charles and others added 2 commits August 20, 2026 02:23
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
mendral-app[bot]

This comment was marked as outdated.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@mendral-app

mendral-app Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🧪 Testing Guide

What this PR addresses

This PR does three things:

  1. Exposes sandbox infrastructure errors — adds a new errors property to both async and sync sandbox wrappers, returning a list of SandboxInfrastructureError (code, fatal, instance, message, time). These are infrastructure failures reported by the compute plane (e.g., VM_EXITED).
  2. Deprecates DriveSpec.size — the control plane no longer exposes a size limit on drives. DriveCreateConfiguration.size is still accepted but no longer serialized. DriveInstance.size reads a legacy value from spec.additional_properties["size"].
  3. Adds status filter parameter to all list endpoints (agents, applications, sandboxes, drives, functions, jobs, models, volumes) via a controlplane client regeneration.

Fixes ENG-5027.

Steps to reproduce / exercise the new behavior

Sandbox errors:

  1. Set up a Blaxel workspace with sandbox access.
  2. Run the manual test: NAME=<existing-sandbox> uv run python tests/manual/sandbox_errors.py (or omit NAME= to have it create/delete a throwaway sandbox).
  3. To see actual errors, provoke a sandbox failure (e.g., use a sandbox that has previously failed or trigger a VM exit condition).
  4. Alternatively, run the integration test: pytest tests/integration/core/sandbox/test_sandbox_errors.py — this verifies .errors returns a list on both a single-get and a list-all response.

Drive size deprecation:

  1. Run existing drive unit tests: pytest tests/core/test_drive.py
  2. Run the drive ACL integration test: pytest tests/integration/core/sandbox/test_drive_acl.py
  3. Verify drives can still be created without specifying a size.

What to verify (expected behavior)

  • sandbox.errors returns list[SandboxInfrastructureError] (possibly empty) when reading a single sandbox.
  • sandbox.errors returns [] when accessed from a list/find-all response (errors aren't projected on listings).
  • Each error entry has: code, fatal (bool), instance, message, time.
  • fatal=True marks the failure that moved the sandbox to FAILED state; non-fatal entries (e.g., VM_EXITED restarts) are informational.
  • Drive creation no longer sends size in the request body, even if DriveCreateConfiguration.size is set.
  • DriveInstance.size still returns the legacy value (from additional_properties) for old drives.
  • All list endpoints accept an optional status query parameter for filtering.
  • Existing unit tests pass: pytest tests/core/test_drive.py
  • No regressions in sandbox or drive functionality.

Note

Posted by PR Testing Guide · Tag @mendral-app with feedback.

@drappier-charles
drappier-charles marked this pull request as ready for review August 20, 2026 03:48
@drappier-charles
drappier-charles merged commit 90210d6 into main Aug 20, 2026
19 of 21 checks passed
@drappier-charles
drappier-charles deleted the cdrappier/devin/sandbox-errors-sdk branch August 20, 2026 03:48

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 4 potential issues.

Open in Devin Review

Comment on lines +20 to +41
async def test_reads_empty_error_history_on_healthy_sandbox(self):
"""A sandbox that never hit an infrastructure failure reports no error."""
name = unique_name("errors")
await SandboxInstance.create(
{
"name": name,
"image": default_image,
"region": default_region,
"labels": default_labels,
}
)

try:
sandbox = await SandboxInstance.get(name)

# The shape of an entry (code, fatal, instance, message, time) is
# not exercisable here: covering it would mean provoking a real
# infrastructure failure on the compute plane.
assert isinstance(sandbox.errors, list)
assert sandbox.errors == []
finally:
await SandboxInstance.delete(name)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟡 New sandbox test cleans up outside the required class-level fixture

The new sandbox test deletes its sandbox inline in a try/finally block (tests/integration/core/sandbox/test_sandbox_errors.py:41) instead of the class-level cleanup fixture the repository conventions mandate, so a hard failure or interruption before the finally block leaves a sandbox behind.
Impact: Leftover test sandboxes can accumulate in the workspace.

Repository convention on integration test cleanup

AGENTS.md (Testing section) states: "Always clean up sandboxes in class-level cleanup fixtures". Other suites follow this, e.g. tests/integration/core/sandbox/test_drive_acl.py:118-130 defines an autouse class-scoped cleanup fixture tracking created sandboxes/drives. The new TestSandboxErrors class has no such fixture.

Prompt for agents
TestSandboxErrors in tests/integration/core/sandbox/test_sandbox_errors.py creates a sandbox and deletes it inline via try/finally. Per AGENTS.md, integration tests must clean up sandboxes in a class-level (autouse, class-scoped) cleanup fixture that tracks the created resource names, as done in tests/integration/core/sandbox/test_drive_acl.py. Refactor the test to register created sandbox names on the class and delete them in such a fixture.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid — fixed in #224 (this PR was already merged): an autouse class-scoped cleanup fixture now deletes every sandbox the class created, mirroring test_drive_acl.py.

display_name: str | None = None,
labels: Dict[str, str] | None = None,
size: int | None = None, # Size in GB
size: int | None = None, # Deprecated: drives have no size limit, this is ignored

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔍 Deprecated drive size is now silently ignored

DriveCreateConfiguration(size=...) is still accepted and stored on the config object, but is no longer serialized into create or update bodies (src/blaxel/core/drive/drive.py:206-224, src/blaxel/core/drive/drive.py:566-582). Callers who previously relied on setting a size will get no error and no warning, and after an update round-trip the legacy size value read from spec.additional_properties will disappear from the wrapper's view (the merged spec never carries the old value forward). Consider emitting a DeprecationWarning when a non-None size is passed so silent behavior change is visible.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deliberate: the control plane dropped the field from the public spec, so there is nothing to send and an update round-trip legitimately loses the legacy value. I left size accepted so existing callers keep compiling rather than breaking at the call site. Adding a DeprecationWarning would make it noisier for callers who can't do anything about it, so I'd rather not do it without @drappier-charles asking for it.

Comment on lines +43 to +48
async def test_listing_does_not_carry_the_error_history(self):
"""Listings project the field out, so they must not be read for it."""
page = await SandboxInstance.list(limit=1)

assert len(page.data) > 0
assert page.data[0].errors == []

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔍 Listing assertion depends on pre-existing workspace state

assert len(page.data) > 0 requires that the workspace already contains at least one sandbox; the test creates none of its own. In a clean workspace (or one where the only sandbox is created/deleted by the sibling test) this will fail spuriously. Creating a labeled sandbox in this test (and cleaning it up) would make the assertion deterministic.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid too, fixed in the same PR (#224): the listing test now creates its own labeled sandbox before listing, so len(page.data) > 0 no longer depends on pre-existing workspace state.

Comment on lines +139 to +145
errors = []
_errors = d.pop("errors", UNSET)
for errors_item_data in _errors or []:
errors_item = SandboxInfrastructureError.from_dict(errors_item_data)

errors.append(errors_item)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔍 errors defaults to empty list rather than UNSET after deserialization

Like the existing events handling, errors is initialized to [] and only filled when the key is present, so a payload without errors yields errors=[] instead of UNSET. Round-tripping such a Sandbox through to_dict() will now emit "errors": [], which the control plane may interpret as an explicit (read-only) value on update bodies. This mirrors pre-existing behavior for events, so it is likely tolerated server-side, but worth confirming that update endpoints ignore the field.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Generated by openapi-python-client, so not editable here, and it mirrors the pre-existing events handling. Server side it's harmless: errors is readOnly in the spec and the controlplane update path never reads it off the body (the array is only ever appended to by the workload-error workflow), so an incoming "errors": [] is ignored.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 60aaf6f. Configure here.

page = await SandboxInstance.list(limit=1)

assert len(page.data) > 0
assert page.data[0].errors == []

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.

Listing errors test assumes existing sandboxes

Medium Severity

test_listing_does_not_carry_the_error_history lists one sandbox and requires page.data to be non-empty. The sibling test creates a sandbox and deletes it in finally, so a clean workspace or this file run alone leaves nothing to list and the assertion fails independently of the errors accessor.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 60aaf6f. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same finding as Devin Review's, already fixed in #224: the listing test creates its own labeled sandbox, and cleanup moved to a class-scoped fixture so nothing is deleted mid-class.

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