Skip to content

Add support for Apple's container CLI runtime - #577

Open
Victor Manuel Puga Ruiz (VictorPuga) wants to merge 12 commits into
microsoft:mainfrom
victorpuga-forks:apple-container
Open

Add support for Apple's container CLI runtime#577
Victor Manuel Puga Ruiz (VictorPuga) wants to merge 12 commits into
microsoft:mainfrom
victorpuga-forks:apple-container

Conversation

@VictorPuga

@VictorPuga Victor Manuel Puga Ruiz (VictorPuga) commented Aug 4, 2026

Copy link
Copy Markdown

Relates to #143.

Summary

Adds AppleContainerClient, an IContainersClient implementation for Apple's container CLI (macOS 26+, Apple Silicon only). Extends DockerClientBase but overrides nearly every command builder, since the CLI's surface (bare verbs like run/list/stop, no --filter, non-Docker JSON shapes) isn't Docker-compatible enough to inherit as-is. Registered behind isMac() && isArm64(), no compose counterpart (none exists), same as wslc.

All behavior below was verified against a real CLI 1.2.0 install, not inferred from docs.

Supported

checkInstall, version, info (synthesized), run, list, start, stop, delete, exec, logs, inspectContainers, stats, build, prune (container/image/volume/network), login/logout, readFile/writeFile (via exec+tar, since cp has no stdin/stdout support), inspectImages, image pull (pinned to --arch arm64), image list, full volume and network support (list/inspect/create/remove/prune).

Notable gotchas handled

  • No ID-based image addressing at all — only name:tag resolves, so ListImagesItem.id is the reference, not the digest.
  • Container state is just running/stopped; mapped to Docker's running/exited so tree icons and start/stop visibility read correctly.
  • container start only accepts one ID at a time — multi-select throws instead of silently starting one.
  • logs has no --timestamps/--since/--until, tails via -n not --tail.
  • cp doesn't support - for stdin/stdout, so readFile/writeFile go through exec ... tar instead (mirrors the same gap in WslcClient).
  • build and stats are top-level verbs (build, stats), not image build/container stats; the latter would double the noun prefix and fail to route.
  • prune needed per-resource handling: the base class's container container prune double-noun bug, plus an unconditional --force that none of container/image/volume/network prune actually accept.
  • login/logout route through registry login/registry logout, not a nonexistent top-level login/logout.
  • Network names must be lowercase; events/restart don't exist and are rejected explicitly.

Out of scope

  • Pre-existing, unrelated gap: "View Logs"/"Build Image" tree commands use a hardcoded Docker-syntax template (containers.commands.* in package.json) that bypasses IContainersClient entirely, with no per-runtime override. Confirmed broken for this runtime; workaround is a custom containers.commands.logs setting. Real fix is a separate, bigger change.
  • No Compose equivalent exists for this CLI, so no orchestrator client (same as wslc).

Testing

330 unit tests passing (full package suite), lint clean. E2E suite (ContainersClientE2E.test.ts) now wired up for this client: 38 passing / 8 pending against the real container CLI on Apple Silicon, and the same suite re-run against Docker (44 passing / 2 pending) to confirm the shared fixture changes didn't regress other runtimes. Manually verified end-to-end on real hardware (pull/run/list/stop/start/inspect/exec/logs/remove/build/volumes/networks) plus the Images/Containers tree views and tooltips in the running extension.

Adds a container-client for Apple's `container` CLI (macOS 26+, Apple
Silicon only), gated on isMac() && isArm64() in
officialRuntimeRegistrations.ts and exposed via the
containers.containerClient setting.

Extends DockerClientBase but overrides most command builders, since
the CLI's surface diverges from Docker's beyond what Finch/Podman
needed: container-object verbs are top-level (`run`/`list`/`stop`/
`delete`, not `container run`-style), `list`/`image list` accept no
`--filter` flag at all (filtering is done client-side instead), and
`list`/`image list` JSON is a nested, non-Docker-shaped record, so it
gets its own schema/normalizer files. `image pull` is pinned to
`--arch arm64` since it otherwise fetches every platform in a
multi-arch manifest by default. `events`/`restart` are rejected since
the CLI has no equivalent subcommand.

All behavior was verified against a real CLI 1.2.0 install rather than
assumed from docs, including a stdout/stderr split test confirming
progress output never pollutes stdout, so the base class's stdout-only
output parsing for run/stop/remove needed no override.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
configuration.name is a sibling of configuration.descriptor in real
`container image list --format json` output, not nested inside it.
The schema had it nested, so zod/mini silently dropped the field
instead of erroring, and every image parsed as <none>:<none> --
visible in the Images tree as a <none> parent with <none> children.

Also exclude platform.architecture "unknown" variants from the size
sum. Each real platform in a multi-arch pull is paired with a ~86KB
attestation/provenance blob reported as its own "unknown/unknown"
variant; summing those in inflated the reported image size.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`image inspect` and `inspect` accept no --format flag (confirmed:
"Unknown option '--format'"); JSON is their only output. `inspect`
(containers) is also a bare verb like run/list/stop/delete, not
`container inspect`. Both were left un-overridden and inherited
DockerClientBase's --format-based defaults, breaking "Run Interactive"
and the image/container hover tooltips entirely.

Adds AppleContainerInspectImageRecord.ts and
AppleContainerInspectContainerRecord.ts, following the same
real-JSON-shape-first approach as the list records.

Also fixes ListImagesItem.id/InspectImagesItem.id: `container` has no
ID-based image addressing at all -- image inspect/rm/run reject a bare
digest, a sha256:-prefixed digest, and even name@sha256:digest, only a
name:tag reference resolves. id was set to the manifest digest, which
this CLI can never look up, breaking every downstream call that reuses
it (tooltip inspection, image-ancestor container filtering). id is now
the name:tag reference itself, the only value actually usable as a CLI
argument for this runtime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds 34 unit tests covering every overridden command: checkInstall,
version, info, pullImage, listImages, inspectImages, runContainer,
listContainers, stopContainers, removeContainers, inspectContainers,
and the unsupported getEventStream/restartContainers rejections.

Fixtures are trimmed from real `container` CLI 1.2.0 output captured
on Apple Silicon hardware (see apple-container-poc-plan.md), not
hand-guessed shapes, and assert the specific quirks already found and
fixed: configuration.name vs configuration.descriptor.name, the
"unknown" attestation variant excluded from image size, id being the
name:tag reference rather than the digest, bare verbs (run/list/stop/
delete/inspect) instead of container-prefixed ones, and --mount using
target= instead of destination=.

Full package suite (269 tests) and lint pass with this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
State-icon bug: getContainerStateIcon (extension-side) only recognizes
Docker's state vocabulary (exited, dead, created, etc.). Apple's own
"stopped" string was passed through unmapped and fell into that
switch's default arm, which renders the *running* icon -- so every
container looked like it needed "start" regardless of actual state.
Confirmed container's vocabulary is just running/stopped, even for a
created-but-never-started container (no separate "created" state).
Maps "stopped" to Docker's "exited" so state-dependent UI reads
correctly.

Start-command bug: getStartContainersCommandArgs was never overridden,
so it inherited "container container start <id>" -- wrong noun
prefix, same class of bug already fixed for run/list/stop/delete/
inspect. Also discovered `container start` only accepts one container
ID at a time (confirmed: a second ID errors and neither container
starts, unlike stop/delete which accept multiple), so a multi-select
"Start" request now throws CommandNotSupportedError explicitly rather
than silently starting only the first container or none.

Both verified against the real CLI and covered by 3 new unit tests
(272 passing, lint clean).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
getExecContainerCommandArgs / getLogsForContainerCommandArgs were unoverridden and
inherited container container exec / container container logs from
DockerClientBase -- same wrong-noun-prefix bug already fixed for
run/list/stop/start/delete/inspect. Real command is bare exec/logs.

logs also needed a flag fix: container logs has no --tail (tailing is
-n <count> instead), and no --timestamps/--since/--until support at
all. Those now throw CommandNotSupportedError when explicitly
requested rather than being silently dropped or erroring at the CLI
layer.

Both verified against the real CLI. 8 new unit tests (280 passing
total across the package, lint clean).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 4, 2026 21:37
@VictorPuga Victor Manuel Puga Ruiz (VictorPuga) changed the title Apple container Add support for Apple's container CLI runtime Aug 4, 2026
@VictorPuga

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

This comment was marked as resolved.

Fixes 3 issues flagged in PR microsoft#577 review:
- ListImageRecord: a legitimate 0-byte size no longer collapses to undefined;
  only an empty (non-"unknown") variant list does.
- InspectContainerRecord: network attachments with no name are dropped
  instead of surfacing as an empty-string network name.
- InspectContainerRecord: imageId keeps its sha256: prefix so downstream
  slicing (ImageTreeItem, ContainerTreeItem, askCopilot) still works.
Comment thread extensions/vscode-containers/package.json Outdated
* - No `events`, `restart`, or `info` subcommand exists.
* - `--version`/`-v` only accepts the long form; the short form errors.
*/
export class AppleContainerClient extends DockerClientBase implements IContainersClient {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's on me to look into this (or Copilot lol), but we should see to what degree we can inherit from the base client. I imagine not a ton since Apple Containers seem to be substantially different from Docker/Podman/Nerdctl/Finch...more like WSLC.

…/login/ports/mounts

Addresses the remaining review feedback on PR microsoft#577 (Apple container CLI runtime
client), all re-verified against a real CLI 1.2.0 install:

- Add full Volume and Network command support (list/inspect/create/remove/prune),
  with new Zod schemas built from real captured JSON -- these were entirely
  unimplemented before.
- Fix prune: the base class emitted `container container prune` (duplicate noun)
  and unconditionally passed `--force`, which none of container/image/volume/
  network prune actually support. Each of the four has a distinct real output
  format, now parsed correctly instead of guessed.
- Fix login/logout: route through `registry login`/`registry logout`, the real
  command path, instead of a nonexistent top-level `login`/`logout`.
- Wire up real ports/mounts in container list/inspect from
  configuration.publishedPorts/.mounts, including port-range expansion and
  volume-vs-bind mount detection (previously hardcoded empty).
- Wire up real imageAncestors/volumes/networks list-filters using raw record
  fields confirmed to be present (previously left unfiltered).
- Fix repoDigests to the repository@sha256:... form used elsewhere.
- Drop the now-unnecessary --mount destination= override (confirmed the real
  CLI accepts it directly via the shared withDockerMountsArg helper).
- Remove stray references to the AI-assisted planning doc from code comments.
- Add an AppleContainerCanary integration test (23 assertions, gated on
  CONTAINER_CLIENT_TYPE=applecontainer) so a future CLI release that adds any
  of the capabilities worked around here gets caught automatically.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

hope this is a fix.

@VictorPuga

Copy link
Copy Markdown
Author

Hi, Patrick Verbrugge (@patverb). The code review comments were addressed.

* - No `events`, `restart`, or `info` subcommand exists.
* - `--version`/`-v` only accepts the long form; the short form errors.
*/
export class AppleContainerClient extends DockerClientBase implements IContainersClient {

@bwateratmsft Brandon Waterloo [MSFT] (bwateratmsft) Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This client should be wired in to the Container Client E2E tests, and those should be run to ensure that coverage is maximized.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

}

// There is no `restart` subcommand.
public override restartContainers(options: RestartContainersCommandOptions): Promise<PromiseCommandResponse<Array<string>>> {

@bwateratmsft Brandon Waterloo [MSFT] (bwateratmsft) Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 While you're overriding the inherited container-noun commands here, statsContainers needs the same treatment — it's currently broken as inherited. The base builds container stats (Docker's docker container stats), which for this client becomes container container stats and fails with Error: Plugin 'container-container' not found. On top of that, container stats has no --all flag at all — it shows all running containers by default and errors with Unknown option '--all'.

It needs a getStatsContainersCommandArgs override that returns a bare stats with no --all. Verified against CLI 1.2.2.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed

// `image prune` accepts `--all` but not `--force` (confirmed: errors with "Unknown option
// '--force'"), unlike the base which always passes `--force`. Real output: a "Reclaimed X
// in disk space" summary line, then one `deleted <digest>` line per removed image.
protected override getPruneImagesCommandArgs(options: PruneImagesCommandOptions): CommandLineArgs {

@bwateratmsft Brandon Waterloo [MSFT] (bwateratmsft) Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 buildImage is also broken as inherited. The base builds image build, but Apple's container has no image build subcommand — build is a top-level verb, so container image build ... errors (Unknown option '-t', and it just routes to the image help without building anything).

This needs a getBuildImageCommandArgs override that uses a bare build, and drops --iidfile and --disable-content-trust (neither exists on container build). Everything else it accepts maps over: --file/--target/--tag/--label/--platform/--build-arg/--pull plus the positional context dir. Verified against CLI 1.2.2 — a real container build --tag x . succeeds where container image build fails.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed

strict: boolean,
): Promise<Array<InspectNetworksItem>> {
return this.parseInspectJson(output, strict, (item) =>
normalizeAppleContainerInspectNetworkRecord(AppleContainerListNetworkRecordSchema.parse(item), JSON.stringify(item)));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 readFile and writeFile are also broken as inherited, and need overrides at the end of the class (a new File Commands region).

The base's readFile streams via cp CONTAINER:PATH -, and writeFile streams via cp - CONTAINER:DIR. But Apple's container cp supports neither stdin nor stdout - — a literal - is just treated as a local filename (verified: container cp c:/etc/hostname - writes a file named - in the cwd and emits nothing; the stdin write form silently doesn't write the piped content).

Suggested fix, mirroring what WslcClient does for reads:

  • getReadFileCommandArgsexec <container> tar -cf - -C <dir> <file>, so the caller still receives the single-entry tarball stream it expects (requires tar in the image; Linux containers only).
  • getWriteFileCommandArgs → for the streamed case (no inputFile), exec -i <container> tar -xf - -C <path> to extract the incoming tar; when a host inputFile is given, fall back to Apple's normal working cp <inputFile> <container>:<path>.

Note this diverges from WslcClient.getWriteFileCommandArgs, which can still use cp - CONTAINER:DIR because wslc's cp does accept stdin -; Apple's does not, so the streamed write has to go through exec. All verified against CLI 1.2.2.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed

…s overrides

Both commands were broken as inherited from DockerClientBase. buildImage built
`image build`, but Apple's CLI has `build` as a top-level verb with no `image
build` subcommand; it also drops --iidfile/--disable-content-trust, which
don't exist on `container build`. statsContainers built `container stats`,
which becomes `container container stats` here and fails to route; the real
verb is bare `stats` with no --all flag at all.

Addresses review feedback from bwateratmsft on PR microsoft#577. Verified against real
CLI 1.2.0 output on Apple Silicon per the repo's AppleContainerClient
verification convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both were broken as inherited from DockerClientBase. `container cp` doesn't
accept a stdin or stdout `-`: `container cp CONTAINER:PATH -` just writes a
local file literally named `-` and streams nothing, and the equivalent stdin
write silently drops the piped content.

readFile now tars the target file inside the container via `exec ... tar -cf
- -C <dir> <file>`, mirroring the same gap in WslcClient. writeFile extracts
a streamed tar via `exec -i ... tar -xf -` when no host inputFile is given,
and falls back to the base's plain `cp <file> CONTAINER:DIR` (which works
fine for host-to-container copies) when one is.

Addresses review feedback from bwateratmsft on PR microsoft#577. Verified against a
real running container on Apple Silicon per the repo's AppleContainerClient
verification convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t E2E suite

Adds 'applecontainer' as a ClientType and instantiates AppleContainerClient in
ContainersClientE2E.test.ts, skipping paths the CLI genuinely lacks (restart,
events, --expose/--publish-all, and the whole orchestrator suite, since there
is no Compose equivalent).

Running the suite against real hardware surfaced three runtime differences
that only show up under live execution, not static review:

- Network names must be lowercase (confirmed: `container network create
  testNetworkCamel` errors with "invalid network name"). The shared
  `testContainerNetworkName` fixture had mixed case, breaking container setup
  for every runtime that reached it; renamed to an all-lowercase name.
- `container build` always auto-tags with a random UUID even without --tag,
  so it never produces a dangling image the way Docker's untagged builds do.
  Bare `image prune` reclaims nothing as a result; PruneImagesCommand now
  passes `{ all: true }` for this client so the test still proves prune works.
- The first `container run` in a session fetches a per-machine kernel + init
  VM image (~20s cold, ~1s once cached), which blew the suite's 10s default
  timeout. Bumped to 60s for the Containers `before` hook, Apple-only.

Verified: 38 passing / 8 pending against the real `container` CLI, and
re-ran the same suite against docker (44 passing / 2 pending) to confirm the
shared network-name fixture fix didn't regress other runtimes.

Addresses review feedback from bwateratmsft on PR microsoft#577.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

Support for coming macOS container runtime

5 participants