Skip to content

feat(sandbox): match sandbox refs by ID or name prefix - #73

Open
pratikbin wants to merge 1 commit into
mainfrom
feat/sandbox-ref-prefix-matching
Open

feat(sandbox): match sandbox refs by ID or name prefix#73
pratikbin wants to merge 1 commit into
mainfrom
feat/sandbox-ref-prefix-matching

Conversation

@pratikbin

Copy link
Copy Markdown
Contributor

What

Sandbox refs now resolve by prefix, the way Docker resolves container refs:

createos sandbox rm  sb-01243e     # enough of the ID to be unique
createos sandbox get my-box        # the name you gave it
createos sandbox exec my-b -- ls   # enough of the name to be unique

Why

Passing a partial ID silently did nothing useful. resolveSandboxRef returned
any sb- prefixed ref verbatim without checking it, so sandbox rm sb-01243e
reached the API as a literal string and came back "not found".

How

All 17 sandbox subcommands (rm, get, pause, resume, exec, shell,
push, pull, sync, network, disk, firewall, fork, edit, editor,
tunnel) already funnel through resolveSandboxRef, so the fix lands in one
place and no call sites change.

Precedence, most-specific first:

Input Resolves to
exact ID that sandbox
unique ID prefix that sandbox
exact name newest match (unchanged — the API does not enforce unique names)
unique name prefix that sandbox
prefix matching several refused, candidates listed newest-first (capped at 10)

Refusing beats guessing here because this resolver feeds rm.

The matching rules moved into a pure matchSandboxRef(rows, ref) so they are
unit-testable — SandboxClient wraps a live resty client and has no mock seam.

Two details worth reviewing

1. Errors are *api.APIError, not fmt.Errorf.
api.UserMessage rewrites any non-APIError into the generic
"something went wrong — please try again or contact support", and
rm.go:111 prints resolve failures through UserMessageVerbose. A plain
error would be swallowed on exactly the command where the message matters most.
Caught in live testing, not by unit tests. The pre-existing not-found error had
the same defect and is fixed too. Precedent for client-side construction:
internal/api/methods.go:755.

2. Unknown IDs still pass through to the API.
The listing is capped at 200 rows, so an ID matching nothing locally is
returned verbatim and the server stays the authority on whether it exists —
otherwise a valid ID outside that window would get a false CLI "not found".
Same reasoning for a failed list call: an ID-shaped ref falls through verbatim,
preserving today's behaviour where a full ID needed no list round-trip.
(Name-shaped refs have no such fallback — they cannot resolve without the list.)

Trade-off

An ID-shaped ref now costs one ListSandboxes call, where before it short-circuited.
Name resolution already paid this, and multi-ref commands already call the resolver
per ref, so it is not a new class of cost. Caching/batching was considered and
left out as out of scope.

Verification

Built the binary and exercised it against two real sandboxes deliberately
sharing both an ID prefix (sb-01m07v2) and a name prefix (prefixtest-):

Case Result
get <unique ID prefix> resolved correctly (the reported bug)
get <ambiguous ID prefix> refused, both listed
get <unique name prefix> resolved correctly
get <ambiguous name prefix> refused, both listed
get <unknown name> CLI not-found error
get <unknown ID> passed through; server returned "sandbox not found"
rm <ambiguous prefix> --force refused — re-listed after, both still running
rm <unique ID prefix> --force deleted the right one
rm <unique name prefix> --force deleted the right one

Test sandboxes cleaned up (0 remaining).

Static checks: go build ✅ · go vet ✅ · go test ✅ (10 new tests) ·
golangci-lint v2 repo-wide → 0 issues ✅ · gosec → 0 issues in changed files ✅

govulncheck reports 9 stdlib vulns — pre-existing, from the Go toolchain
(1.26.3, fixed in 1.26.6). Unrelated to this change; a toolchain bump is its own PR.

Not included

Per the cross-repo mesh protocol in CLAUDE.md, this is a shared-surface change,
so ../website-04 (Sandbox/CLI/Commands.md, Overview.md) should mirror it.
That is a public repo requiring explicit approval, so it is not in this PR.
Happy to prepare that diff separately.

Test plan

createos sandbox create --name demo-one
createos sandbox create --name demo-two
createos sandbox get demo-o          # unique name prefix → resolves
createos sandbox get demo-           # ambiguous → lists both, refuses
createos sandbox get sb-<first-6>    # unique ID prefix → resolves
createos sandbox rm demo- --force    # ambiguous → refuses, deletes nothing

Passing a partial sandbox ID did nothing useful: resolveSandboxRef
returned any `sb-` prefixed ref verbatim, so `sandbox rm sb-01243e`
reached the API as a literal and came back "not found".

Resolve refs the way Docker resolves container refs. Precedence runs
most-specific first: exact ID, unique ID prefix, exact name (newest
wins on duplicates, as before), then unique name prefix. A prefix
matching several sandboxes is refused with the candidates listed
rather than guessed at, which matters most for `rm`.

All 17 sandbox subcommands already funnel through resolveSandboxRef,
so no call sites change. The matching rules move into a pure
matchSandboxRef so they can be unit-tested: SandboxClient wraps a live
resty client and offers no mock seam.

Two details worth keeping:

Errors are *api.APIError, not fmt.Errorf. api.UserMessage rewrites
every other error type into a generic "something went wrong", and
rm.go prints resolve failures through it, so a plain error would have
been swallowed on exactly the command that needs it most. The
pre-existing not-found error had the same defect and is fixed too.

An ID that matches nothing is still returned verbatim, and an
ID-shaped ref survives a failed list call. The visible list is capped
at 200 rows, so the API must stay the authority on whether an ID
exists instead of the CLI inventing a not-found.
@pratikbin

Copy link
Copy Markdown
Contributor Author

This is very helpful since my Docker days.

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