fix(v1): typed sandbox fault codes - #2574
Conversation
`SandboxError(message, *, code=None)` names the fault from typed evidence only (exception type, errno, HTTP/RPC status): `not_found`, `timeout`, `disk_full`, `unavailable`, `provisioning`, `denied`. `trace.Error.code` records it next to `status_code`. The prime runtime maps SDK exception types in one place, including the HTTP status the SDK's base `APIError` carries only in its text (read from the chained httpx error, not parsed).
…Error connectrpc turns the CancelledError raised inside a VM RPC into ConnectError(CANCELED); prime-sandboxes re-wraps it as APIError and the runtime wrapped that as SandboxError, so a caller retrying on sandbox faults swallowed the cancel. `_failure` re-raises the cancellation when the task is cancelling or the failure carries the CANCELED code; the SDK's live-process stream pump no longer re-attaches on its own cancel.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a2882cb. Configure here.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — Beyond adding typed fault codes, this PR changes cancellation, timeout, error-wrapping, and live-process reconnection behavior across existing Prime runtime paths, including an import-time patch to a private SDK method. The resulting blast radius is materially wider than a small additive trace-schema change. Notes:
You can add or adjust custom eligibility rules. Learn more. |
An `asyncio.timeout` deadline cancels the task the same way a caller does, and connectrpc swallows either into ConnectError(CANCELED). `prepare_execution` mapped the error outside its scope: the ordinary error escaped the `async with`, `Timeout.__aexit__` uncancelled the task without converting it, and `_failure` raised CancelledError from a task nobody cancelled. `_faults` now wraps each SDK call directly, inside any timeout scope over it, and re-raises the swallowed cancel while the task still counts as cancelling, so the owning scope turns its own deadline into TimeoutError (-> SandboxError code "timeout") and lets an external cancel through. `_cancelled` consults only `Task.cancelling()`; the error chain cannot tell a fired deadline (or another task's cancel batched in by the SDK) from this task's cancel.
|
Superseded by #2580, which reworks this in the shape the error module already uses: typed |

What
SandboxError(message, *, code: str | None = None).codenames the fault from typed evidence only — an exception type, an errno, or an HTTP/RPC status, never the message text:not_found,timeout,disk_full,unavailable,provisioning,denied;Nonewhen nothing typed says.trace.Error.code: str | None, recorded byTrace.record_errornext tostatus_code.errors.sandbox_fault_code(e)maps the Python-level sources (FileNotFoundError,TimeoutError,OSErrorwithENOSPC,httpxstatus/transport errors).runtimes/prime.pymaps the SDK's exception types and Connect RPC codes in one function,_fault_code. The SDK's baseAPIErrorcarries the HTTP status only in its text ("HTTP 503: ...") and is raised inside thehttpxhandler withoutfrom; that function reads the typed status off the chainedhttpx.HTTPStatusErrorinstead of parsing the message.start()falls back toprovisioning.runtimes/subprocess.py: a bounded read of a missing path carriesnot_found.Cancellation is not a fault
A cancelled task's RPC comes back from the SDK as an ordinary error: connectrpc turns the
CancelledErrorraised inside a VM RPC intoConnectError(CANCELED, "Request was cancelled"), prime-sandboxes re-wraps it asAPIError, and the runtime used to wrap that asSandboxError("prime exec failed: Connect RPC failed (canceled): ...")— so any caller that retries on sandbox faults (a harness turn, a rollout re-attempt) swallowed the cancel and kept working; a run failed to exit on SIGINT for 11 minutes and leaked 21 sandboxes. The same seam now decides:_failure(message, e)re-raisesasyncio.CancelledErrorwhen the current task is being cancelled (Task.cancelling()) or the failure carries the cancel (theCANCELEDcode, or theCancelledErrorchained under it), else theSandboxErrorwith its typed code. No path returns aSandboxErrorfor a cancel, so there is nocanceledcode. The SDK's live-process stream pump (AsyncSandboxProcess._pump, its own task) meets the same swallow and re-attaches on it (live process stream dropped (Request was cancelled); re-attaching 1/5), so a cancelled pump streamed on until the remote process exited andasyncio.runnever returned;_can_reconnectis wrapped (guarded bygetattr, a stopgap until the SDK owns the cancel — connectrpc re-raisingCancelledError, or prime-sandboxes listingCANCELEDas fatal) so the pump ends instead.Why
A pipeline running ~90 concurrent rollouts on prime sandboxes classifies sandbox faults (box gone, API unavailable, disk full, quota) by regex over
SandboxErrortext, because the error carries no typed code. The runtime already knows the type of the failure at the point it wraps it; this records it once so consumers branch ontrace.last_error.codeinstead of on prose.Validation
tests/v1/test_errors.py: the code survivesrecord_errorand ato_recordround trip; typed sources map, text-only ones stayNone(including the SDK'sAPIError("HTTP 404: ...")shape); a runtime read of a missing path raisesSandboxError(code="not_found").APIError←ConnectError(CANCELED)←CancelledError), or with no typed trace at all, insideruntime.run()while the awaiting task is cancelled →CancelledErrorpropagates and the task ends cancelled, notSandboxError; a fault without a cancel stays aSandboxErrorwith its code. The SDK pump, cancelled while its stream reportsCANCELED, ends without re-attaching; a dropped link (UNAVAILABLE) still re-attaches.uv run pytest tests/v1 -m "not e2e",ruff,ty check,pre-commitpass.Not wired: the modal runtime (its SDK types are not audited here),
harness._check_result's "runtime died"SandboxError(the alive probe is structural, not an exception type — leftNonefor now), andPrimeProcess.write/terminate/kill, which never mapped toSandboxErrorand still surface a swallowed cancel as the SDK's rawAPIError.Note
Add typed fault codes to
SandboxErrorand Prime runtime error handlingcodefield toSandboxErrorandtrace.Errorso sandbox failures carry a named fault code (e.g.not_found,timeout,disk_full,denied,unavailable) alongside their messagesandbox_fault_codeand_http_fault_codeclassifiers in errors.py that map typed localOSErrorand HTTP status evidence to codes; unknown exception types and message-only strings produce no code_faultscontext manager in prime.py and wraps all Prime runtime operations (start,run,read,write,expose,run_background,open_process,prepare_execution) so provider failures become codedSandboxErrorand swallowed task cancellation is re-raised asasyncio.CancelledErrorAsyncSandboxProcess._can_reconnectpredicate with a cancellation-aware wrapper so a cancelled stream pump does not attempt reconnectionsandbox_fault_codeto subprocess reads in subprocess.py and recordscodeon trace errors in trace.pyEGRESS_APPLY_TIMEOUT; deadline expiry raises atimeout-codedSandboxErrorinstead of propagating as cancellation. The import-time replacement ofAsyncSandboxProcess._can_reconnectis guarded but modifies a SDK private method at module load.Macroscope summarized 9671970.
Note
High Risk
Changes Prime/async cancellation and error classification across all sandbox RPC paths; misclassification could break retries or leave sandboxes leaking, though behavior is heavily tested.
Overview
Adds typed sandbox fault codes so pipelines can branch on
trace.last_error.codeinstead of parsing error messages.SandboxErrornow accepts an optionalcode(not_found,timeout,disk_full,unavailable,provisioning,denied) derived only from typed evidence.sandbox_fault_codeand Prime’s_fault_code(walking SDK/Connect/httpx exception chains, including HTTP status on chainedAPIError) classify failures;Trace.record_errorpersistscodeon trace errors.Prime runtime routes SDK calls through
_faults, which re-raisesasyncio.CancelledErrorwhenTask.cancelling()is set (so retry-on-SandboxErrorcannot swallow SIGINT/cancel) and otherwise raisesSandboxErrorwith the mapped code. Egress apply timeouts explicitly usecode="timeout".AsyncSandboxProcess._can_reconnectis wrapped so a cancelled stream pump does not reconnect forever. Subprocess bounded reads attach codes fromOSError.New
tests/v1/test_errors.pycovers trace round-trips, classification, cancel vs deadline behavior, and stream pump reconnect rules.Reviewed by Cursor Bugbot for commit 9671970. Bugbot is set up for automated code reviews on this repo. Configure here.