Skip to content

cubeapi: accept e2b flattened autoPause/autoResume on sandbox create - #1254

Merged
fslongjin merged 2 commits into
TencentCloud:masterfrom
Arpit-Ahuja-293:fix/cubeapi-e2b-autopause-wire-fields
Aug 11, 2026
Merged

cubeapi: accept e2b flattened autoPause/autoResume on sandbox create#1254
fslongjin merged 2 commits into
TencentCloud:masterfrom
Arpit-Ahuja-293:fix/cubeapi-e2b-autopause-wire-fields

Conversation

@Arpit-Ahuja-293

Copy link
Copy Markdown
Contributor

Problem

The e2b Python SDK does not put lifecycle on the wire. Sandbox.create(lifecycle={"on_timeout": "pause", "auto_resume": True}) is flattened client-side into top-level fields before POST /sandboxes:

{ "templateID": "<id>", "timeout": 30, "autoPause": true, "autoResume": { "enabled": true } }

NewSandbox bound neither field, so serde dropped them silently. CubeAPI then resolved lifecycle = None and forwarded auto_pause=false to CubeMaster — the sandbox was killed at timeout instead of paused. The setting appeared to succeed at the SDK layer and did nothing.

Fix

  • NewSandbox binds autoPause and autoResume.
  • New SandboxAutoResume untagged enum accepts both {"enabled": true} (current SDK) and bare true (older releases, hand-rolled clients) — same meaning, so no reason to 400 on shape.
  • Lifecycle derivation extracted from the inline closure in create_sandbox into resolve_lifecycle_flags(), a pure function that unit tests can reach.

Design decision worth reviewing

When a request carries both lifecycle and the flattened fields, the nested lifecycle object wins.

Reasoning: the e2b SDK never sends both, so this only disambiguates direct API callers — and an explicit lifecycle can only come from someone who meant it. Happy to invert this if maintainers prefer the flat fields taking precedence.

Tests

7 lifecycle tests, 5 of them new, written test-first:

  • new_sandbox_deserializes_e2b_flat_lifecycle_fields — the exact e2b wire body
  • new_sandbox_accepts_bare_bool_auto_resume
  • flat_lifecycle_fields_enable_pause_and_resume
  • nested_lifecycle_wins_over_flat_lifecycle_fields
  • absent_lifecycle_fields_keep_kill_behaviour — no behaviour change for callers that don't opt in

Verification

Run in the project builder image (rustc 1.89, aarch64-unknown-linux-musl):

  • make cube-api-test → 108 passed, 0 failed
  • make cubeapi → clean release build
  • cargo fmt --check → clean
  • cargo clippy --all-targets → warning set identical to the pre-change baseline; none in new code
  • cargo build --locked passes — no dependency changes, Cargo.lock untouched

Also updated

  • openapi.yml — regenerated via cube-api --export-openapi, applying only the additive schema hunks so the hand-maintained license header and servers: block survive.
  • docs/guide/lifecycle.md and docs/zh/guide/lifecycle.md — new "Wire format" section covering both accepted shapes and the precedence rule, EN and ZH in sync.

Closes #965

}

#[test]
fn flat_lifecycle_fields_enable_pause_and_resume() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These tests cover deserialization and the pure resolve_lifecycle_flags function separately, but nothing drives create_sandbox with flat autoPause/autoResume and asserts the flags actually reach the CubeMaster request. That's precisely the failure mode this PR fixes — before the change, the fields were silently dropped somewhere between inbound JSON and CreateSandboxRequest, and a future regression (e.g. someone removing the auto_pause: flat_auto_pause binding from the destructuring at line ~158, which still compiles because of ..) would not be caught by any of these unit tests.

The existing mock-CubeMaster integration tests (e.g. create_sandbox_forwards_create_time_env_vars_to_cubemaster, which captures the outbound request body) are the right template: a parallel test sending autoPause: true / autoResume: {"enabled": true} and asserting create_body["auto_pause"] == true / create_body["auto_resume"] == true would lock the full path down. The destructuring binding → resolve call → request field assignment is currently only covered by code review, not a test.

/// explicitly it wins over the flattened compatibility fields. The e2b SDK
/// never sends both, so this only disambiguates direct API callers.
#[test]
fn nested_lifecycle_wins_over_flat_lifecycle_fields() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since resolve_lifecycle_flags is now extracted as a pub(crate) function, the pre-existing lifecycle_object_translates_to_cubemaster_bools test (just above these additions) still carries its own inline translate helper that re-implements the nested-lifecycle branch of the new function. It could call the real code directly, e.g. resolve_lifecycle_flags(body.lifecycle.as_ref(), None, None). As-is, the test and the production logic are two copies that can silently drift — the exact kind of stale-duplication hazard the pure function was extracted to remove. Consider updating that test while this is fresh.

@cubesandboxbot

cubesandboxbot Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review: cubeapi — accept e2b flattened autoPause/autoResume on sandbox create

AI-generated review. This is a machine assessment; please treat it as input, not a human approval.

Verdict: Looks correct — approve with minor nits

The fix is sound. NewSandbox previously dropped the e2b SDK's flattened autoPause/autoResume fields silently, so lifecycle resolved to None and CubeMaster always received auto_pause=false — the sandbox was killed at timeout instead of paused. This PR binds both fields, extracts the lifecycle→flag derivation into a pure resolve_lifecycle_flags(), and covers it with focused unit tests plus a full-service integration test.

Things I verified against the base tree:

  • End-to-end flow is wired correctly. NewSandbox destructuring binds auto_pause/auto_resume (all other fields still fall through ..), and the resolver output feeds CreateSandboxRequest.auto_pause/auto_resume, whose CubeMaster wire names are confirmed at CubeAPI/src/cubemaster/mod.rs:757-765. The integration test asserts the outbound auto_pause/auto_resume booleans, so a regression that unbinds the fields (which .. would otherwise tolerate) is caught.
  • No behavior change for the nested lifecycle path. resolve_lifecycle_flags is a line-for-line extraction of the old inline closure, and the extended lifecycle_object_translates_to_cubemaster_bools test re-asserts all pre-existing combinations (kill/pause × resume, snake_case alias, empty object).
  • No serialization leak. NewSandbox derives Deserialize only (no Serialize), so the new fields can't appear in responses; the missing skip_serializing_if on them is a non-issue.
  • The untagged enum is defensive in the right way. SandboxAutoResume accepts {"enabled": bool} and bare bool, and the camelCase rename + snake_case alias on both new fields means the fix works regardless of whether the real e2b client emits autoPause or auto_pause.
  • Test plumbing compiles/checks out. Arc, tokio::sync::Mutex, serde_json::Value, axum::serve are available in the test module; the mock CubeMaster response shape matches CreateSandboxResponse (accepts requestID, sandbox_id, ret).

Should address

  1. PR body vs. diff mismatch — docs were not updated. The description claims docs/guide/lifecycle.md and docs/zh/guide/lifecycle.md gained a "Wire format" section, but neither file is in the diff (only CubeAPI/src/models/mod.rs, CubeAPI/src/services/sandboxes.rs, openapi.yml changed; both docs files in the base tree have no such section). Since this PR changes the accepted wire shapes, either include the docs or correct the description — otherwise the compatibility surface this PR adds is undocumented for users.

Design points raised inline

  1. Precedence edge with an empty lifecycle object (services/sandboxes.rs): lifecycle: {} deserializes to Some(Default::default()), so it shadows an explicit autoPause: true and yields (false, false). Not a bug per the documented "nested wins" rule, but it's the one case where the rule drops a non-default flat value; worth confirming that's intended since the rule is being introduced here.

  2. Doc-comment precision (models/mod.rs): "so lifecycle never arrives from an SDK caller" is only true of the e2b SDK — CubeSandbox's own Python SDK sends the nested lifecycle object. The code handles both correctly; the comment just overgeneralizes.

Minor

  1. OpenAPI description placement. The autoResume field's doc comment landed as a sibling of $ref inside the oneOf item rather than on the property itself. Valid in OAS 3.1, but consumers that resolve only the property-level description will miss it.

  2. PR body says "5 of them new"; the diff adds 6 new tests (plus extends lifecycle_object_translates_to_cubemaster_bools). Cosmetic count discrepancy.

  3. The deserialization tests encode the assumed e2b wire shape (hand-built JSON). The field aliasing mitigates the risk, but a golden request captured from the real e2b SDK (or an e2e) would lock the assumption in.

@luzhixing12345

Copy link
Copy Markdown
Collaborator

Thanks for your contribution.
LGTM. Some local tests have passed.

@luzhixing12345

Copy link
Copy Markdown
Collaborator

please squash all commits into a single commit. @Arpit-Ahuja-293

Comment thread docs/guide/lifecycle.md Outdated
- **`auto_resume=True`**: when any request next arrives for a `paused` sandbox (HTTP, `run_code`, file I/O, …), the platform wakes it up before the request lands. Callers never see the pause; typical resume latency is sub-second to a few seconds.
- If `auto_resume=False` (or unset), the sandbox stays paused until you explicitly `Sandbox.connect(sandbox_id=...)`. Useful for "wait for the user" workflows.

### Wire format

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think there is no need to update the docs. This is implementation detail, not user-facing APIs.

@luzhixing12345

Copy link
Copy Markdown
Collaborator

@Arpit-Ahuja-293 please remove the lifecycle doc change commit

Arpit-Ahuja-293 and others added 2 commits August 11, 2026 20:05
The e2b SDK flattens its user-facing lifecycle object into top-level
autoPause / autoResume before the request goes out. NewSandbox bound
neither field, so serde dropped them and CubeAPI forwarded
auto_pause=false — sandboxes were killed at timeout instead of paused.

Bind both fields and resolve them alongside the nested lifecycle object,
which keeps precedence when a caller sends it. autoResume accepts both
the object and bare-bool wire shapes.

Closes TencentCloud#965

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Arpit Ahuja <iamarpitahuja@gmail.com>
Signed-off-by: jinlong <jinlong@tencent.com>
@fslongjin
fslongjin force-pushed the fix/cubeapi-e2b-autopause-wire-fields branch from 478b4d2 to a616958 Compare August 11, 2026 12:08
@fslongjin
fslongjin merged commit 9f1d914 into TencentCloud:master Aug 11, 2026
22 checks passed
Comment thread CubeAPI/src/models/mod.rs

/// e2b SDK compatibility: the SDK flattens its user-facing `lifecycle`
/// object into top-level `autoPause` / `autoResume` before it hits the
/// wire, so `lifecycle` never arrives from an SDK caller. Ignored when

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit on the doc comment: "so lifecycle never arrives from an SDK caller" is only true of the e2b SDK. CubeSandbox's own Python SDK (sdk/python/cubesandbox/sandbox.py:_serialize_lifecycle) sends the nested lifecycle object (onTimeout/autoResume) on every create, and the existing lifecycle_object_translates_to_cubemaster_bools test covers exactly that path — which is precisely why the "nested wins" precedence matters. Suggest narrowing to something like "the e2b SDK flattens this client-side, so the nested lifecycle is what CubeSandbox's own SDK and direct API callers send."

auto_pause: Option<bool>,
auto_resume: Option<&SandboxAutoResume>,
) -> (bool, bool) {
if let Some(lc) = lifecycle {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Precedence edge worth a decision since the rule is introduced here: lifecycle deserializes to Some(SandboxLifecycleConfig::default()) for an empty object {} (both fields have #[serde(default)]). So a request carrying lifecycle: {} plus an explicit autoPause: true resolves to (false, false) and the flat flag is silently dropped — even though {} reads more like "caller didn't actually specify a policy" than an intentional Kill choice. Not a bug per the documented rule (the SDK never sends both), but it's the one case where "nested wins" drops a non-default flat value; worth confirming that's intended.

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.

[Bug Report]CubeAPI should accept E2B lifecycle wire fields for auto-pause

5 participants