Repository navigation
Description
Problem Statement
OpenShell's sandbox delete API takes only a mutable sandbox name (openshell sandbox delete -g <gateway> <sandbox-name>). There is no way for a client to bind a delete request to a specific sandbox's immutable identity (internal ID / resource version) and have OpenShell refuse the delete if the name now resolves to a different sandbox.
This means any client-side "read identity, then delete by name" pattern has an unavoidable TOCTOU race: another OpenShell client can delete the sandbox and create a replacement under the same name between the client's identity read and OpenShell processing the delete. No amount of re-checking identity immediately before issuing the delete closes this window, because the check and the delete are not atomic.
Impact
We hit this concretely in NVIDIA/NemoClaw#10863 / NVIDIA/NemoClaw#10867: nemoclaw destroy needed a safe way to clean up a sandbox left "retained" after an interrupted onboarding. Even with a durable identity fingerprint recorded for the retained sandbox, we could not safely automate the delete, because OpenShell could not guarantee the name still pointed at the same sandbox at the moment of deletion. The fix had to make deletion of a live retained sandbox permanently fail-closed (always require a human to run the delete manually after out-of-band identity confirmation), which is a worse operator experience than a race-free automatic cleanup would be.
Proposed Design
Add an atomic delete-by-identity primitive, e.g. a resource_version / immutable ID precondition on the delete RPC (compare-and-delete semantics): the delete succeeds only if the sandbox at that name still has the identity/resource-version the caller expects, and otherwise fails with a distinguishable "identity mismatch" error rather than deleting an unexpected sandbox.
This is related to #3050 (unifying sandbox references across gateway RPCs) — a canonical sandbox reference type that includes an immutable ID could be a natural carrier for this precondition, but #3050's stated scope does not currently call out atomic/conditional delete semantics, so filing this separately to track the specific capability gap.
Acceptance Criteria
- Sandbox delete RPC accepts an optional identity/resource-version precondition.
- Delete fails with a distinguishable error (not a silent no-op or wrong-sandbox deletion) when the precondition does not match the current sandbox at that name.
- Behavior documented for SDK/CLI consumers.
cc @jyaunches (raised during review of NVIDIA/NemoClaw#10867)
Activity
fix(sandbox): keep retained destroy fail closed (#10867)
github-actions commentedon Sep 21, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.
feat(cli): delete a sandbox by ID with sandbox delete --id
rodrigosetti commentedon Oct 1, 2026
I hit the same problem in my orchestration. If a sandbox becomes unhealthy, I recreate it under the same name. A delayed cleanup or retry can then end up deleting the replacement instead of the original sandbox.
I have a fix ready on my fork:
rodrigosetti#1
The main change in the API is that
DeleteSandboxRequestgets an (optional)expected_sandbox_id. The gateway only deletes the sandbox if the name still resolves to that ID. If it doesn’t, it returnsFAILED_PRECONDITION(SANDBOX_IDENTITY_MISMATCH) and leaves the sandbox untouched. Requests that don’t set the field keep the current behavior.I also added CLI support with
openshell sandbox delete --id <ID>andNAME --expected-id <ID>.The change is supported in the Rust, Python, and TypeScript SDKs, while Go gets the regenerated raw field. Python session cleanup,
sandbox create --no-keepcleanup, and TUI deletes now all pass the ID of the sandbox they intend to delete.Happy to adjust the design if we prefer a different approach.
I’ve requested a vouch: #4083
fix(sandbox): allow verified retained sandbox destruction (#12893)
github-actions commentedon Oct 17, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 14 days if there is no further activity. Comment or remove the state:stale label to keep it open.
antsok commentedon Oct 23, 2026
I would love to see some progress on this
fix(sandbox): report expected-ID delete mismatches as ABORTED
yanyunl1991 commentedon Oct 26, 2026
Thanks @antsok for the +1, and @rodrigosetti for sharing your implementation.
@rodrigosetti, adding an optional
expected_sandbox_idtoDeleteSandboxRequestmatches what we need on the NemoClaw side. If the gateway rejects the delete when the name resolves to a different ID, it closes the read-then-delete race described above. It is also backward compatible, because existing callers that send no ID keep the current behavior.Would you be willing to open that change as a PR against this repo? Once it lands and ships in a release, NemoClaw can switch
nemoclaw destroyfrom the current fail-closed path to an automatic, identity-bound cleanup of retained sandboxes (NVIDIA/NemoClaw#10863). I'm happy to test it against that flow.Maintainers: could this get a triage decision, so it does not close as stale while there are three independent use cases and a working implementation?
feat(sandbox): support deleting a sandbox only if it has an expected ID
fix(python): refuse to delete a sandbox session without an ID
rodrigosetti commentedon Oct 26, 2026
Hi @yanyunl1991, thanks for offering to test it.
Happy to open the PR but I don't have write access, and the contribution rules require this issue to carry
state:acceptedbefore I can open. I'll open it as soon as a maintainer accepts it. In the meantime, you can try the branch on my fork: rodrigosetti#1btw, I changed a mismatch to returns
ABORTED, notFAILED_PRECONDITIONto match how the API already reports conditional-write conflicts. TheErrorInforeason isSANDBOX_IDENTITY_MISMATCH. Please match on the reason rather than only the status code, since a delete can return otherABORTEDerrors.For
nemoclaw destroy, the CLI offers two forms:openshell sandbox delete --id <ID>deletes only the sandbox with that ID. If that sandbox is already gone, or its name now belongs to a replacement, the CLI reports it as already deleted, exits 0, and leaves the replacement alone. This looks like the best fit for automatic cleanup.openshell sandbox delete <NAME> --expected-id <ID>is the strict form. If the name now belongs to a different sandbox, it deletes nothing and exits non-zero, with the same exit code as other failures. If the name no longer exists, it reports the sandbox as already deleted and exits 0.The ID is the
Idthatopenshell sandbox getprints, ormetadata.idfromCreateSandbox/GetSandbox.Backwards compatibility note: please note that gateways that predate this field ignore it and delete by name (i.e. fallback to the unsafe case).