Skip to content

fix(sdk): SimpleBox auto_remove leaks boxes over REST (Python + Node) - #1186

Open
G4614 wants to merge 3 commits into
boxlite-ai:mainfrom
G4614:fix/simplebox-auto-remove-rest-leak
Open

fix(sdk): SimpleBox auto_remove leaks boxes over REST (Python + Node)#1186
G4614 wants to merge 3 commits into
boxlite-ai:mainfrom
G4614:fix/simplebox-auto-remove-rest-leak

Conversation

@G4614

@G4614 G4614 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

SimpleBox (Python and Node) now deletes the box on exit when auto_remove/autoRemove is true, instead of only stopping it (a no-op over REST that left every such box Stopped forever on the dev cloud environment).

Test plan:

  • apps/e2e/cases/test_simplebox_lifecycle.py against the dev cloud API — fails on the pre-fix Python code, passes after
  • apps/e2e/cases/test_node_simplebox_lifecycle.py against the dev cloud API — fails on the pre-fix Node code, passes after
  • apps/e2e/cases/test_sdk_tunnel.py against the dev cloud API — dev box count returns to 0 after the run
  • cd sdks/node && npx tsc --noEmit

Summary by CodeRabbit

  • New Features

    • Added automatic cleanup for boxes created with auto-removal enabled when their lifecycle ends.
    • Added explicit box removal support to the Node SDK.
    • Preserved boxes configured with auto-removal disabled for inspection and manual cleanup.
    • Reused boxes are not removed unintentionally.
  • Bug Fixes

    • Improved cleanup reliability by retrying removal during shutdown and tolerating already-removed boxes.
  • Tests

    • Added end-to-end coverage for Python and Node box lifecycle and cleanup behavior.

`Box.__aexit__` (box_handle.rs) only ever called `handle.stop()`. Deletion
was assumed to come from `BoxOptions.auto_remove`, but that field is a
deprecated, REST-silent no-op (rest/types.rs never transmits it) - local
runtimes self-delete on stop internally, which is why this only leaks
remotely. Every box `apps/e2e/cases/test_sdk_tunnel.py` created via
SimpleBox was left `Stopped` forever on the dev cloud environment; a full
e2e run there confirmed exactly 5 leaked boxes, matching that file's 5
SimpleBox call sites 1:1.

SimpleBox.__aexit__ / SyncSimpleBox.__exit__ now explicitly call
runtime.remove() after stop() when auto_remove was requested and this
instance created the box (a box reused via reuse_existing=True may still
be open in an outer session, so an inner exit must not delete it). The
remove call retries once after a 1s backoff, since stop() can leave the
box briefly `pending` server-side and force=True does not bypass that
guard (the REST controller never reads the `force` query param on delete).

Before:
  SimpleBox.__aexit__ (simplebox.py)
    -> Box.__aexit__ (box_handle.rs)        <- BUG: only stops, never deletes
      -> LiteBox.stop -> RestBox::stop        (REST: auto_remove field is dropped)

After:
  SimpleBox.__aexit__ (simplebox.py)
    -> Box.__aexit__ (box_handle.rs)          - still just stops the VM
    -> SimpleBox._remove_after_stop (simplebox.py)   [new]
      -> Boxlite.remove (runtime.rs) -> DELETE /boxes/{id}
         retries once (1s) past the post-stop `pending` window

Regression coverage: apps/e2e/cases/test_simplebox_lifecycle.py, verified
failing against the pre-fix code and passing after, against the dev cloud
API. Re-ran test_sdk_tunnel.py (the file that originally leaked) after the
fix: dev cloud box count returned to 0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 672634c0-9b97-4ef2-92ac-2319bb5aab59

📥 Commits

Reviewing files that changed from the base of the PR and between 494aeaf and 2ab039d.

📒 Files selected for processing (2)
  • apps/e2e/cases/test_simplebox_lifecycle.py
  • apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/e2e/cases/test_simplebox_lifecycle.py
  • apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts

📝 Walkthrough

Walkthrough

SimpleBox context exits now remove newly created boxes when automatic removal is enabled. Python and Node APIs retry cleanup and log final failures. REST end-to-end tests cover removal and retention behavior.

Changes

SimpleBox cleanup lifecycle

Layer / File(s) Summary
Context exit cleanup
sdks/python/boxlite/simplebox.py, sdks/python/boxlite/sync_api/_simplebox.py
Both Python wrappers store auto_remove, stop boxes on exit, remove newly created boxes when enabled, retry once, and log final cleanup failures.
Node stop cleanup
sdks/node/lib/simplebox.ts
The Node runtime adds remove. SimpleBox.stop() removes newly created boxes when autoRemove is enabled, retries once, and logs final failures.
REST lifecycle validation
apps/e2e/cases/test_simplebox_lifecycle.py, apps/e2e/cases/test_node_simplebox_lifecycle.py, apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts
Python and Node tests verify automatic deletion, retention with removal disabled, polling, and explicit cleanup.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant SimpleBox
  participant Runtime
  Test->>SimpleBox: Exit context or call stop
  SimpleBox->>Runtime: Stop box
  SimpleBox->>Runtime: Retry removal for newly created box
  Runtime-->>SimpleBox: Removal result
  SimpleBox-->>Test: Return after cleanup
Loading

Possibly related PRs

Suggested reviewers: dorianzheng

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the fix and lists verification steps, but it omits the required Call graph and Changes sections. Add the required Summary, Call graph with Before and After hops, Changes, and any applicable issue or rollout details.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the REST auto-removal leak fix for the Python and Node SimpleBox SDKs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@G4614
G4614 marked this pull request as ready for review August 10, 2026 05:57
@G4614
G4614 requested a review from a team as a code owner August 10, 2026 05:57
@boxlite-agent

boxlite-agent Bot commented Aug 10, 2026

Copy link
Copy Markdown

📦 BoxLite review — couldn't complete

claude exited 1

stdout:
{"is_error":true,"duration_api_ms":0,"num_turns":1,"stop_reason":"stop_sequence","session_id":"8289a2fa-d223-4770-83c9-6da37f23eea8","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"terminal_reason":"api_error","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subtype":"success","api_error_status":403,"result":"Your organization has disabled Claude subscription access for Claude Code · Use an Anthropic API key instead, or ask your admin to enable access","type":"result","duration_ms":292,"uuid":"1336d166-3349-4bdd-b2f0-d3f0a6fcd9cb"}

stderr:
<empty>

powered by BoxLite

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/e2e/cases/test_simplebox_lifecycle.py`:
- Around line 31-35: Update the get_info polling loop in the lifecycle deletion
helper to return true only for the REST not-found condition (HTTP
404/not_found). Add or propagate a typed not-found signal through the REST
runtime and Python SDK, then retry transient failures and re-raise all other
errors instead of treating every exception as deletion.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b2277052-1249-4382-a740-606c93733719

📥 Commits

Reviewing files that changed from the base of the PR and between 19e4d8e and bc4cce2.

📒 Files selected for processing (3)
  • apps/e2e/cases/test_simplebox_lifecycle.py
  • sdks/python/boxlite/simplebox.py
  • sdks/python/boxlite/sync_api/_simplebox.py

Comment thread apps/e2e/cases/test_simplebox_lifecycle.py Outdated
Same bug as the Python side, same fix. SimpleBox.stop() only ever called
this._box.stop(); autoRemove on JsBoxOptions is a deprecated field REST
runtimes silently ignore (local runtimes self-delete on stop internally,
which is why this only leaks remotely). Confirmed with a live repro
against api.dev.boxlite.ai: the pre-fix box stayed Stopped forever after
stop(); with the fix it's gone within the same 15s window.

stop() now explicitly calls runtime.remove() after stopping when
autoRemove was requested and this instance created the box (one reused
via reuseExisting may still be open in an outer session). Retries once
after a 1s backoff for the same post-stop `pending` race the Python fix
retries.

Added apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts + cases/test_node_simplebox_lifecycle.py,
mirroring test_simplebox_lifecycle.py through the existing cross-language
Node e2e driver pattern (cases/test_node_tunnel.py's).

Test plan:
- [x] Two-side verified against the dev cloud API via
      test_node_simplebox_lifecycle.py: fails on the pre-fix code
      ("box ... still present after stop() with autoRemove=true"),
      passes after.
- [x] cd sdks/node && npx tsc --noEmit

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@G4614 G4614 changed the title fix(python-sdk): SimpleBox auto_remove leaks boxes over REST fix(sdk): SimpleBox auto_remove leaks boxes over REST (Python + Node) Aug 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts (1)

49-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a reused-box retention case.

The test does not verify the reuseExisting: true contract from this PR. Create an outer named box. Create an inner SimpleBox with the same name, reuseExisting: true, and autoRemove: true. After inner.stop(), assert that the outer box still exists. Clean up the outer box explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts` around lines 49 - 60, Extend
the lifecycle test around the existing keptBox scenario with a reused-box
retention case: create an outer named box, then an inner SimpleBox using the
same name with reuseExisting: true and autoRemove: true; stop the inner box and
assert the outer box still exists. Explicitly remove the outer box during
cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts`:
- Around line 20-24: Update the cleanup helper around runtime.getInfo() so it
returns true only when getInfo(id) returns null and propagates transport,
authorization, and other unexpected errors; at
apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts lines 20-24, remove the catch-all
success path. At lines 55-60, retry only the expected transient or pending-state
failure from remove(), then throw the final failure instead of swallowing it.

---

Nitpick comments:
In `@apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts`:
- Around line 49-60: Extend the lifecycle test around the existing keptBox
scenario with a reused-box retention case: create an outer named box, then an
inner SimpleBox using the same name with reuseExisting: true and autoRemove:
true; stop the inner box and assert the outer box still exists. Explicitly
remove the outer box during cleanup.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a7fd773c-b4a6-4afc-b3fd-dd89fc92f10c

📥 Commits

Reviewing files that changed from the base of the PR and between bc4cce2 and 494aeaf.

📒 Files selected for processing (3)
  • apps/e2e/cases/test_node_simplebox_lifecycle.py
  • apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts
  • sdks/node/lib/simplebox.ts

Comment thread apps/e2e/sdks/node/e2e_simplebox_lifecycle.ts Outdated
CodeRabbit on boxlite-ai#1186: both _box_gone() (Python) and boxGone() (Node) caught
*any* get_info failure and returned "gone" - a pending-state error,
transport blip, or unrelated server error would make the deletion
assertion pass while the box still exists, silently hiding a real leak
instead of catching one.

Both now only treat a REST not-found (matched by message, since neither
SDK exposes a typed not-found error over REST) as deletion; every other
failure is retried until the timeout, then re-raised/rethrown instead of
being swallowed. The Node driver's own cleanup remove() at exit no longer
swallows its error either, for the same reason.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@DorianZheng

Copy link
Copy Markdown
Member

this should be implemented in core side. seems cloud didn't honor the flag

@DorianZheng DorianZheng left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please check the root cause

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.

2 participants