v0.4.0
⚠️ Behavior change — read before upgrading
Failed sandbox update_* and delete calls now raise SandboxAPIError instead of silently returning a malformed object.
Every generated control-plane API function returns Union[Error, T] | None, so an error status is a return value, not an exception. SandboxInstance.update_metadata, update_ttl, update_lifecycle, update_network and delete passed that response straight into cls(response), which does not type-check. A 403/404/500 therefore produced an instance wrapping an Error.
If you ignored the return value — the common shape — a failed write looked like a success:
update_ttlreturned normally and the TTL was never applieddeletereturned normally and the sandbox stayed alive and kept billing
If you used the return value, you hit AttributeError: 'Error' object has no attribute 'metadata' far from the real cause.
What this means for you
| you | impact |
|---|---|
| Your calls succeed | No change. Same objects, same types, same behavior. |
| Your calls have been failing silently | You now get exceptions. You were already broken — the update never applied, the delete never happened. |
You wrote a workaround checking isinstance(result.sandbox, Error) |
This breaks. The call now raises before returning, so your check never runs. |
Most notably: delete() on an already-deleted sandbox now raises where it previously returned quietly. If you delete in a finally: or a cleanup path, wrap it:
try:
await sandbox.delete()
except SandboxAPIError:
pass # already goneAlso: delete() on an empty response now raises SandboxAPIError rather than ValueError.
This brings Python in line with the TypeScript SDK, which has always used throwOnError: true on these calls, and with Go, which returns idiomatic (res, err). Python was the only SDK swallowing these errors.
Fixes
-
fix(sandbox): propagate control-plane errors fromupdate_*/delete(#206, ENG-4244)- all four
update_*helpers anddelete, in both the async and sync (SyncSandboxInstance) trees deleteno longer raises a misleadingValueError("Sandbox X not found")for what may have been a 500VolumeInstance.deletehad the same gap and now raises on bothErrorand empty responses- Reported by Albert at EvenUp, who also identified the correct fix: routing through the existing
_unwrap_response()helper.
- all four
-
fix(deps): capmcpbelow2.0.0(#207, ENG-4250)mcp 2.0.0removedmcp.server.fastmcp, whichblaxel.core.mcp.serversubclasses, breaking every fresh install- migrating off that surface and lifting the cap is tracked in ENG-4255
Verification
- Red/green: with the source fix reverted and only the new tests applied, 18 tests fail; with it restored, all pass
- 423 unit tests passing; full integration suite green
- One integration test was found to be passing for the wrong reason — it built a lifecycle with a duplicate
ttl-idlepolicy that the control plane rejects with a 400, and the swallowed error meant the update never applied. Fixed.
Full diff: v0.3.5...v0.4.0