Skip to content

Sandbox layer: sync/async parity fixes and hardening - #70

Merged
sfroment merged 14 commits into
mainfrom
feature/sdk-parity-fixes
Sep 25, 2026
Merged

sfroment merged 14 commits into
mainfrom
feature/sdk-parity-fixes

Conversation

@sfroment

@sfroment sfroment commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

The sandbox layer's sync and async twins had shipped drift, plus a hardening
batch: service pools, lazy list handles, opt-in exec errors, fail-closed
readiness, and create-failure cleanup. Examples cover every new public
surface (28–31), porting the pool/claim walkthroughs from #64 onto the
spec-based pool API.

 AsyncSandbox.create(          # main
-  idle_timeout: int = 0,        # always-on — drifted from sync's 300
-  enable_mesh: bool = False,   # mesh disabled — drifted from sync's auto
+  idle_timeout: int = 300,      # scale-to-zero, identical to sync
+  enable_mesh: Optional[bool] = None,  # mesh tri-state: auto
 )
commits (each green, vertical slice):
├── fix: fail closed on terminal deployment states
├── feat: typed errors for token, port, secret gates
├── feat: retry executor 5xx/network errors, wrap 4xx
├── feat: clean up sandbox when create fails or times out
├── feat: service pools and sandbox claims   ← supersedes #64
├── feat: Sandbox.list with lazy handles
├── feat: opt-in raise_on_error for exec
├── fix: align async create defaults + guard caller apps  ← parity pins
├── refactor: build deployment definition once per create twin
├── fix: tell the truth when cleanup fails
├── test: pin file IO twins and matrix corners
├── docs: regenerate reference docs, scope out test modules
└── docs: add examples for pools, claims, list, raise_on_error

Evidence

  • Before: main silently builds different sandboxes per flavor:

    main:sandbox.py:1721  idle_timeout: int = 0      # sync: 300
    main:sandbox.py:1730  enable_mesh: bool = False  # sync: None (auto)
    

    The parity guard written first fails exactly on this:

    FAILED test_async_create_matches_sync_defaults
      AssertionError: 0 != 300 — idle_timeout default drifted from sync
    
  • After: 160 passed; two fresh-context review rounds verified
    sync/async CreateService payloads identical (mesh auto, deep sleep 300)
    and that the JS mirror expects exactly these defaults (JS mirrors sync:
    DEFAULT_IDLE_TIMEOUT = 300).

Merge Danger

Door: two-way — behavior changes are deliberate and revertible; public
interfaces unchanged and pinned.

Blast Radius: SDK-wide.

Observable changes: (1) async-create defaults now match sync (scale-to-zero,
mesh auto); (2) is_healthy()/wait_ready() raise SandboxDeploymentError
on terminal states instead of polling to timeout (fail closed); (3)
cleanup_on_failure deletes only the service when the app is
caller-provided; (4) pools work supersedes #64 (this branch's version is the
newer, spec-integrated one — close #64 on merge). Release-note the
scaled-to-zero raise for 1.5.x monitors.

@sfroment
sfroment marked this pull request as ready for review September 25, 2026 12:16
@sfroment
sfroment added this pull request to stack #72 September 25, 2026 13:03
STOPPED/SLEEPING/UNHEALTHY previously polled until wait_ready
timeout; now raises SandboxDeploymentError naming the status.
DEGRADED counts as ready (JS classifyServiceStatus parity).
MissingApiTokenError/InvalidPortError keep ValueError parents so
published 1.5.x except-clauses still catch them. get_from_id now
fails fast with NoSandboxSecretError instead of returning a
handle that dies on first connected op.
4xx leaked raw httpx.HTTPStatusError; now SandboxRequestError with
status and body. SandboxServiceError subclasses it, so 5xx remains
catchable under both names. Network errors retry with the same
exponential backoff (Go CLI parity) and wrap as SandboxError.
Default cleanup_on_failure=True mirrors JS: a failed wait deletes
the orphaned sandbox (which otherwise burns instance-hours) and the
error says so; False restores the old keep-and-retry behavior.
Service-creation failures wrap as SandboxError and delete the app
this call created, never a caller-provided app.
ServicePool CRUD + claim/get_claim/list_claims/wait_claim_ready,
fully mirrored sync+async over the existing generated
ServicePoolsApi/PoolClaimsApi. Claims are idempotent by
request_id (UUID, preserved across retries) and retry 429/5xx
with linear backoff, matching the JS SDK.
Lists type-SANDBOX services with CLI-parity pagination (100 per
page). Handles carry no executor secret; the first connected op
raises NoSandboxSecretError naming the get_from_id fix instead of
a generic SandboxError.
SandboxCommandError was exported but never raised. exec(..., raise_on_error=True) now raises it (carrying the CommandResult); default keeps returning failed results.
The async twin silently built a different sandbox than sync:
always-on (idle_timeout 0) with mesh disabled, where sync scales
to zero (300) with mesh auto. Aligned, and pinned by a parity
suite that compares every sync/async twin's signatures,
defaults, order, and kind. cleanup_on_failure also stops
deleting a caller-provided app whole — only the service this
call created — and sync update_network_policy passes self.host
like its async twin.
_create_sync built the definition twice and the async twin
rebuilt it inside every snapshot branch; the builders are pure,
so one build per twin is behavior-identical.
Cleanup helpers now report whether the delete happened so
failure errors stop claiming the sandbox was deleted. Also
document delete()'s app-vs-service semantics and pin the
unknown-provenance (get_from_id/list) whole-app default.
SandboxFileIO joins the class pairs; the sync-only allowlist
now covers every pair; light-sleep override cells pinned.
Regenerated to match the current API surface (the earlier
regen claim was empty), with an explicit module list in
generate_docs.sh so test doubles never leak into the reference
docs.
Ports the pool and claim examples from #64 onto the spec-based
pool API and covers the two other new public surfaces: lazy
Sandbox.list handles and opt-in exec errors.
A claimed service is detached from the pool and caller-owned:
delete it when done. The example now spawns its own pool, shows
get_claim, and keeps the request_id replay demo.
@sfroment
sfroment force-pushed the feature/sdk-parity-fixes branch from ec06567 to 0186953 Compare September 25, 2026 14:11
@sfroment
sfroment merged commit f785bbf into main Sep 25, 2026
10 checks passed
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