Skip to content

feat: support Docker archive (.tar) as --from source on sandbox create #2175

Description

@feloy

Problem Statement

openshell sandbox create --from currently accepts three kinds of input:

  • A Dockerfile (or directory containing one) — built into the local Docker daemon, tag passed to the gateway
  • A fully-qualified registry image reference — gateway's compute driver pulls it at sandbox creation time
  • A community sandbox shorthand — expanded to a registry reference, same pull path

There is no path for a pre-built Docker archive (the .tar format produced by docker save or buildah push --format docker docker-archive:).

This matters for air-gapped environments and macOS VM-based build pipelines where a Docker archive is the natural hand-off artifact. Today, passing --from ./output.tar hits the error "local --from file is not a Dockerfile" because the existing file branch only recognises Dockerfile names.

Proposed Design

openshell sandbox create --from ./output.tar     # Docker archive (.tar)
openshell sandbox create --from ./output.tar.gz  # gzip-compressed Docker archive

How it fits the existing architecture

The Dockerfile path already establishes the right pattern to follow:

  1. CLI detects the local file, pre-processes it (Dockerfile → docker build; archive → docker load), and gets back a local image tag.
  2. CLI passes that tag as spec.image in CreateSandboxRequest — identical to a registry reference from the gateway's perspective.
  3. Gateway creates the sandbox using the tag, found in the shared local daemon. No new proto RPC is needed.

For a Docker archive the pre-processing step is:

  1. CLI detects .tar / .tar.gz extension → ResolvedSource::DockerArchive { path }.
  2. CLI calls the Docker daemon's POST /images/load via Bollard (the library already in use for Dockerfile builds), streaming the archive bytes.
  3. Docker loads the image and returns its embedded tag from manifest.json.
  4. CLI passes that tag to CreateSandboxRequest.spec.image.

This is the minimum-surface change: the gateway and proto are untouched; only the CLI gains a new ResolvedSource variant and a new pre-processing helper analogous to build_local_image.

Constraints

  • Local-gateway only — same guard as Dockerfile sources (dockerfile_sources_supported_for_gateway). The CLI and gateway must share the same Docker daemon for the loaded tag to be resolvable.
  • Extension-based detection — .tar and .tar.gz suffixes, consistent with how filename_looks_like_dockerfile works today. No magic-byte sniffing.
  • Tag extraction — the archive's embedded tag (from manifest.json → RepoTags[0]) is used as-is, or re-tagged to openshell/sandbox-from:{timestamp} for consistency with the Dockerfile flow.

Alternatives Considered

  • Stream archive bytes to the gateway over a new RPC: more complex, potentially supports remote gateways, but out of scope — remote Dockerfile sources are already unsupported, so there is no demand.
  • OCI image layout archive (oci-archive:): a distinct format; out of scope here, can be a follow-up.
  • Magic-byte sniffing: more robust but breaks consistency with the existing filename-heuristic approach and adds complexity for no real gain.

Agent Investigation

Traced the full Dockerfile pipeline:

  • crates/openshell-cli/src/run.rs — resolve_from() classifies input; build_from_dockerfile() drives Bollard; the resulting tag is passed directly to CreateSandboxRequest.
  • crates/openshell-bootstrap/src/build.rs — build_local_image() / build_image() use Bollard's build_image() with a tar-encoded context body. The archive load path (POST /images/load) is a parallel Bollard call with no build options — simpler than the build path.
  • proto/openshell.proto — CreateSandboxRequest carries only SandboxSpec.image: string. No changes required.
  • crates/openshell-server/ — gateway receives a tag and hands it to the active compute driver. No changes required.

Files likely affected:

  • crates/openshell-cli/src/run.rs — resolve_from(), ResolvedSource, sandbox_create()
  • crates/openshell-bootstrap/src/build.rs — new load_local_archive() helper (analogous to build_local_image())
  • architecture/ — CLI doc update

The key design insight the investigation confirmed: no new proto RPC is needed. Bollard's POST /images/load loads the archive into the local daemon exactly like Dockerfile builds do, and the embedded tag from manifest.json passes through to the gateway as a plain image reference.

Checklist

  • I've reviewed existing issues and the architecture docs
  • This is a design proposal, not a "please build this" request

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions