Expose the sandbox infrastructure errors reported by the compute plane - #223
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
🔀 Component Interaction DiagramBased 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)
Summary of Key Flows
New model: Note Posted by PR Sequence Diagram · Tag @mendral-app with feedback. |
|
📋 Created Linear issue ENG-5027 — status: In Progress
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. |
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>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🧪 Testing GuideWhat this PR addressesThis PR does three things:
Fixes ENG-5027. Steps to reproduce / exercise the new behaviorSandbox errors:
Drive size deprecation:
What to verify (expected behavior)
Note Posted by PR Testing Guide · Tag @mendral-app with feedback. |
| 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) |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
🔍 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
| 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 == [] |
There was a problem hiding this comment.
🔍 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
| 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) | ||
|
|
There was a problem hiding this comment.
🔍 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ 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 == [] |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 60aaf6f. Configure here.
There was a problem hiding this comment.
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.


Fixes ENG-5027
Summary
Regenerates the controlplane client from
main(make sdk-controlplane) so the newSandbox.errorshistory is typed, and surfaces it on both the async and sync sandbox wrappers:Entries are oldest first;
fatalmarks the failure that moved the sandbox toFAILED(a plainVM_EXITEDrestart is informational).errorsis only returned when a single sandbox is read — list/find-all projections don't carry it.messageis a normalized reason; raw VM/VMM log lines never leave the compute plane.tests/manual/sandbox_errors.pyprints 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.sizestays accepted but is now deprecated and no longer serialized into create/update bodies, andDriveInstance.size/SyncDriveInstance.sizeread the legacy value offspec.additional_properties["size"]. The drive unit tests build theirDriveSpecfixtures withoutsizeaccordingly.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.sizefrom the drive ACL integration fixture, and adds an integration test covering thesandbox.errorsaccessor 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.