Skip to content

fix(core): remove only a container the handle created - #25

Merged
amondnet merged 3 commits into
mainfrom
fix/container-ownership
Aug 29, 2026
Merged

fix(core): remove only a container the handle created#25
amondnet merged 3 commits into
mainfrom
fix/container-ownership

Conversation

@amondnet

@amondnet amondnet commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

ContainerHandle.remove() issued docker rm --force --volumes <name> addressed by name and blind to ownership. Container names are shared across processes by design, so the name alone was never authorisation for a destructive call:

The fix is ownership tracking inside packages/core/src/sandbox/docker/container.ts only:

  1. acquire() already branches on create vs. adopt, so it now returns that distinction ({ name, created }) instead of leaving callers to re-derive it.
  2. The handle keeps it alongside the memoised acquisition, so it follows the existing latch semantics — a failed acquisition is not memoised, and remove() clears the latch so the handle may create again afterwards.
  3. remove() removes only a container this handle created. An adopted one, or a handle that never acquired anything, does nothing and resolves.
  4. remove() stays idempotent; a create still in flight is settled first, so an abandoned create is not leaked.

Adoption itself is unchanged. harness/provider.ts documents cross-process adoption as the feature that makes a sandbox id resumable, so what acquire() adopts — and whether createSession should refuse a taken name — stays with #16. This PR fixes only the destructive half of #18.

New tests in packages/core/test/sandbox/docker/container-ownership.test.ts use the fake-docker-binary technique already established by provider.test.ts (tmpdir script + PLEASE_DOCKER_PATH + a child-process driver, because DOCKER_BIN is read once at import). They cover: adopted → no rm; created → rm --force --volumes <name>; never-acquired → no docker call at all; and idempotency. Removing the guard fails 3 of the 4.

Related issue

Closes #18

Checklist

  • PR title follows Conventional Commits
  • Tests added or updated, and the suite passes (bun run test)
  • Lint and type-check pass (bun run lint, bun run type-check)
  • Documentation updated if behavior changed
  • No breaking change, or a BREAKING CHANGE: note is included

Summary by cubic

Fixes ContainerHandle.remove() so it only removes a container this handle created, instead of force-removing any container with the same name. Previously, adopting another process's container (or never acquiring one) still ran docker rm --force --volumes, which could kill a live session in another process. Now adopted or never-acquired containers are left untouched, and remove() remains idempotent, even when a create is still in flight. Closes #18.

Written for commit 06a283c. Summary will update on new commits.

`ContainerHandle.remove()` ran `docker rm --force --volumes <name>`, addressed by name and
blind to ownership. A container name is shared across processes by design, so carrying the
name was never evidence the handle was entitled to a destructive call on it: a handle that
adopted another process's live container — or one that never acquired anything — would
force-remove it anyway. A failed `onCreate` in one process could take down a live session
in another.

`acquire()` already branches on create and adopt, so it now reports which path it took, and
the handle keeps that alongside its memoised acquisition. Ownership follows the existing
latch resets — a failed acquisition is not memoised, and `remove()` clears the latch so the
handle may create again — rather than being set once. `remove()` stays idempotent, and an
adopted or untouched container resolves without reaching the daemon.

Adoption itself is deliberately unchanged: `harness/provider.ts` documents cross-process
adoption as the feature that makes a sandbox id resumable. What `acquire()` adopts is a
separate decision, owned by #16.
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.81818% with 2 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/core/src/sandbox/docker/container.ts 81.81% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@amondnet
amondnet marked this pull request as ready for review August 28, 2026 19:04

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/core/test/sandbox/docker/container-ownership.test.ts
Comment thread packages/core/test/sandbox/docker/container-ownership.test.ts Outdated
…f pinning it

`remove()` settles an acquisition still in flight before deciding ownership, and no test
reached that path: every case awaited `ready()` first, and a synchronous fake daemon
leaves no window to observe. A `docker` that sleeps on `run` opens one, so abandoning a
create mid-flight is now pinned to remove what it produced.

The expected `rm` argv is derived from `containerName` rather than spelling out its
current FNV-1a digest, so a change to the hash reads as a change to the hash instead of
as an ownership regression.
@amondnet
amondnet added this pull request to the merge queue Aug 29, 2026
Merged via the queue into main with commit cdd91d0 Aug 29, 2026
7 checks passed
@amondnet
amondnet deleted the fix/container-ownership branch August 29, 2026 14:31
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.

createSession adopts a running container, so a failed start can kill another process's live session

1 participant