Skip to content

Commit 534b7c3

Browse files
lpcoxCopilot
andauthored
feat: add the selectable Apple Container runtime backend
Layer 3 of the Apple Container backend stack. Layers 1 and 2 built the primitives and the no-NIC capability transport but registered nothing; this makes `--container-runtime apple-container` a real, user-selectable runtime and wires the complete AWF lifecycle around it. Docker Compose keeps owning infrastructure (Squid, the API proxy, the CLI proxy); only the agent crosses the hypervisor boundary. The guest has zero NICs. `--network none` is not a hardening flag that can be traded away — omitting `--network` attaches Apple's default vmnet network — so layer 1 always emits it, layer 2's merge re-asserts it, and the backend checks it once more immediately before `container create`. Direct IP egress, DNS, DoH, IPv6, raw sockets, the metadata address, and every host network path are unreachable by construction rather than by rule. Runtime selection: - `apple-container` is a `microvm` execution-model entry in the runtime registry and a factory in the external backend resolver, so the agent is excluded from docker-compose.yml while infrastructure is still generated. - `--apple-container-preview` (required opt-in), `--apple-container-cpus`, `--apple-container-memory`, `--apple-container-init-image`, and `--apple-container-cli`, with matching `appleContainer` config, schema, and spec sections. - runtime-validation.ts is an exhaustive, fail-closed compatibility matrix, wired both before and after `applySecurityMode` and re-asserted in the backend's preflight — the latter is load-bearing, because main-action auto-applies `dockerHostPathPrefix` after config validation. Docker-in-Docker, ARC split filesystems, legacy iptables security, host access, enclaves, topology peers, DoH, `filesystem.allowWrite`, extra volumes, TTY, ssl-bump, the chroot sysroot, non-default agent images, `--build-local`, and Vertex are each refused with a message naming the reason. Vertex specifically: its provider port is not in the transport allowlist, so it is rejected rather than silently losing its endpoint. Infrastructure bridging: On macOS the sidecars live inside the Docker Desktop VM, so their 172.30.0.x addresses are unreachable from the host as well; two hops are required. AWF publishes exactly the ports backing an allowlisted capability, to 127.0.0.1 only. That *replaces* rather than supplements the service builders' mappings — `buildSquidService` emits `3128:3128`, which binds 0.0.0.0 and would leave an open forward proxy and unauthenticated credential-injecting endpoints on every interface the runner sits on. Fixed ports are validated free before Compose starts, so a collision is a named conflict instead of a Docker bind error. Agent launch: - The agent image's entrypoint is Docker-specific end to end (iptables-init handshake, `awfuser` remap, resolv.conf rewrite, chroot into /host). It is bypassed, not adapted, so no half-applicable assumption runs. - Mounts are a short explicit list: the workspace and `${RUNNER_TEMP}/gh-aw` at their own absolute host paths, plus run-scoped `/tmp`, `$HOME`, and the two `.copilot` directories. Host credential stores are absent because they were never mounted — there is nothing to shadow, so no credential-hiding overlay that temporarily moves live host files is needed. - The workload runs as the host uid:gid on a read-only rootfs. - Reusing AWF's own environment builders with a loopback "network" yields exactly the endpoints the transport plan publishes, so the plan's conflict check passes on identical values rather than being bypassed. A value that cannot survive a single `--env` argv token fails the run with the variable named instead of being silently dropped. - Capability sockets live under /tmp, deliberately not workDir: macOS caps sun_path at 104 bytes and a realistic `${RUNNER_TEMP}/awf-<ts>/...` path exceeds it, which would make the relay unbindable. - Images must be digest-pinned, and `--skip-pull` is honoured by verification: Apple Container's image store is independent of Docker's, so a Docker pre-pull does not populate it. - `container inspect` output carries the guest environment, and the audit directory is widened to a+rX for artifact upload, so environment blocks are reduced to variable names before being persisted — matching the "no env vars" contract the Docker and Cloud Hypervisor collectors already keep. An unparseable capture is dropped rather than written through. - Teardown quiesces the guest before removing the transport. `preserve` keeps the VM for inspection but still unlinks every socket, so a preserved run never leaves a live path into the credential-injecting sidecar. Init image, docs, CI: containers/apple-init/ takes Apple's digest-pinned vminit, moves /sbin/vminitd to /sbin/vminitd.apple, and installs the layer-2 shim in its place — nothing else is added, removed, or rewritten. The supported `container` CLI range is recorded as labels and asserted against the compiled-in host contract by a test that parses the Dockerfile. The release job is gated on an `APPLE_VMINIT_IMAGE` repository variable; when unset, no apple-init digest is published and the runtime simply cannot be selected, rather than falling back to an unknown init. action.yml gains macOS arm64 installation without weakening Linux behaviour, and does not claim hosted macOS can run the runtime — hosted macOS reports kern.hv_support=0 and fails preflight. The live smoke workflow is `workflow_dispatch`-only against a maintainer-reviewed SHA in a protected environment. It deliberately has no pull request trigger, not even a label-gated one: this repository is public, the runner is persistent bare metal, and a label does not stop commits landing between the label and the checkout of `refs/pull/N/merge`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent ef14f22 commit 534b7c3

36 files changed

Lines changed: 4434 additions & 48 deletions

.github/workflows/release.yml

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,129 @@ jobs:
350350
--type spdxjson \
351351
ghcr.io/${{ github.repository }}/cli-proxy@${{ steps.build_cli_proxy.outputs.digest }}
352352
353+
# Build the AWF Apple Container init image (Apple's vminit with the AWF guest
354+
# capability relay installed at /sbin/vminitd).
355+
#
356+
# Gated on the `APPLE_VMINIT_IMAGE` repository variable, which must hold a
357+
# digest-pinned reference to Apple's own vminit image for the supported
358+
# `container` CLI range. There is deliberately no default: a floating base
359+
# would let Apple's init move underneath a shim that hard-codes where that
360+
# init lives. When the variable is unset the job is skipped and no
361+
# apple-init digest is published — and because the Apple Container backend
362+
# refuses to launch without a digest-pinned init image, the runtime fails
363+
# closed at preflight rather than running with an unknown guest init.
364+
build-apple-init:
365+
name: Build Apple Container Init Image
366+
runs-on: ubuntu-latest
367+
needs: bump-version
368+
if: vars.APPLE_VMINIT_IMAGE != ''
369+
outputs:
370+
digest: ${{ steps.build_apple_init.outputs.digest }}
371+
steps:
372+
- name: Checkout code
373+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
374+
with:
375+
ref: ${{ needs.bump-version.outputs.version }}
376+
377+
- name: Validate the Apple base image is digest-pinned
378+
env:
379+
AWF_VMINIT_IMAGE: ${{ vars.APPLE_VMINIT_IMAGE }}
380+
run: |
381+
set -euo pipefail
382+
case "$AWF_VMINIT_IMAGE" in
383+
*@sha256:*) echo "Apple vminit base: $AWF_VMINIT_IMAGE" ;;
384+
*) echo "::error::APPLE_VMINIT_IMAGE must be digest-pinned"; exit 1 ;;
385+
esac
386+
387+
- name: Read the transport contract constants
388+
id: contract
389+
run: |
390+
set -euo pipefail
391+
src=src/apple-container/transport-capabilities.ts
392+
min=$(sed -n "s/^export const APPLE_CONTAINER_TRANSPORT_MIN_CLI_VERSION = '\\([^']*\\)';$/\\1/p" "$src")
393+
max=$(sed -n "s/^export const APPLE_CONTAINER_TRANSPORT_MAX_CLI_VERSION_EXCLUSIVE = '\\([^']*\\)';$/\\1/p" "$src")
394+
contract=$(sed -n 's/^export const APPLE_CONTAINER_TRANSPORT_CONTRACT_VERSION = \([0-9]*\);$/\1/p' "$src")
395+
test -n "$min" && test -n "$max" && test -n "$contract"
396+
echo "min=$min" >> "$GITHUB_OUTPUT"
397+
echo "max=$max" >> "$GITHUB_OUTPUT"
398+
echo "contract=$contract" >> "$GITHUB_OUTPUT"
399+
400+
- name: Log in to GitHub Container Registry
401+
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
402+
with:
403+
registry: ghcr.io
404+
username: ${{ github.actor }}
405+
password: ${{ secrets.GITHUB_TOKEN }}
406+
407+
- name: Set up Docker Buildx
408+
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
409+
410+
- name: Set up QEMU
411+
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
412+
with:
413+
platforms: arm64
414+
415+
- name: Install cosign
416+
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
417+
418+
# arm64 only: Apple Container guests are native arm64 and Rosetta
419+
# translation is never used, so an amd64 variant would be a silently
420+
# unusable artifact rather than a useful one.
421+
- name: Build and push Apple init image
422+
id: build_apple_init
423+
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
424+
with:
425+
context: .
426+
file: ./containers/apple-init/Dockerfile
427+
push: true
428+
platforms: linux/arm64
429+
build-args: |
430+
AWF_VMINIT_IMAGE=${{ vars.APPLE_VMINIT_IMAGE }}
431+
AWF_CLI_MIN_VERSION=${{ steps.contract.outputs.min }}
432+
AWF_CLI_MAX_VERSION_EXCLUSIVE=${{ steps.contract.outputs.max }}
433+
AWF_TRANSPORT_CONTRACT_VERSION=${{ steps.contract.outputs.contract }}
434+
tags: |
435+
ghcr.io/${{ github.repository }}/apple-init:${{ needs.bump-version.outputs.version_number }}
436+
ghcr.io/${{ github.repository }}/apple-init:latest
437+
cache-from: type=gha,scope=apple-init
438+
cache-to: type=gha,mode=max,scope=apple-init
439+
440+
- name: Verify the published init image layout
441+
run: |
442+
set -euo pipefail
443+
ref="ghcr.io/${{ github.repository }}/apple-init@${{ steps.build_apple_init.outputs.digest }}"
444+
# Extracting the image is the only way to prove the relocation
445+
# happened; the image has no shell, so it cannot be probed by running it.
446+
id=$(docker create --platform linux/arm64 "$ref")
447+
trap 'docker rm -f "$id" >/dev/null 2>&1 || true' EXIT
448+
docker export "$id" > init.tar
449+
tar -tf init.tar | grep -qx 'sbin/vminitd'
450+
tar -tf init.tar | grep -qx 'sbin/vminitd.apple'
451+
mkdir -p extracted
452+
tar -xf init.tar -C extracted sbin/vminitd
453+
file extracted/sbin/vminitd | tee /dev/stderr | \
454+
grep -q 'ELF 64-bit LSB executable, ARM aarch64'
455+
file extracted/sbin/vminitd | grep -q 'statically linked'
456+
457+
- name: Sign Apple init image with cosign
458+
run: |
459+
cosign sign --yes \
460+
ghcr.io/${{ github.repository }}/apple-init@${{ steps.build_apple_init.outputs.digest }}
461+
462+
- name: Generate SBOM for Apple init image
463+
uses: anchore/sbom-action@28d71544de8eaf1b958d335707167c5f783590ad # v0.22.2
464+
with:
465+
image: ghcr.io/${{ github.repository }}/apple-init@${{ steps.build_apple_init.outputs.digest }}
466+
format: spdx-json
467+
output-file: apple-init-sbom.spdx.json
468+
469+
- name: Attest SBOM for Apple init image
470+
run: |
471+
cosign attest --yes \
472+
--predicate apple-init-sbom.spdx.json \
473+
--type spdxjson \
474+
ghcr.io/${{ github.repository }}/apple-init@${{ steps.build_apple_init.outputs.digest }}
475+
353476
# Build the unified enclave images from containers/enclave/Dockerfile.
354477
build-enclaves:
355478
name: Build Enclave Images
@@ -721,7 +844,7 @@ jobs:
721844
release:
722845
name: Create Release
723846
runs-on: ubuntu-latest
724-
needs: [bump-version, build-squid, build-agent, build-api-proxy, build-cli-proxy, build-agent-act, build-build-tools, build-enclaves, build-gh-aw-node, build-cloud-hypervisor-test-artifacts]
847+
needs: [bump-version, build-squid, build-agent, build-api-proxy, build-cli-proxy, build-agent-act, build-build-tools, build-enclaves, build-gh-aw-node, build-cloud-hypervisor-test-artifacts, build-apple-init]
725848
steps:
726849
- name: Checkout code
727850
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
@@ -830,6 +953,8 @@ jobs:
830953
release/cloud-hypervisor-test-x86_64.sbom.spdx.json
831954
832955
- name: Generate containers list
956+
env:
957+
APPLE_INIT_DIGEST: ${{ needs['build-apple-init'].outputs.digest }}
833958
run: |
834959
mkdir -p release
835960
printf '%s\n' \
@@ -843,6 +968,17 @@ jobs:
843968
"ghcr.io/${{ github.repository }}/enclave-mcp-server@${{ needs['build-enclaves'].outputs.enclave_mcp_server_digest }}" \
844969
"ghcr.io/${{ github.repository }}/gh-aw-node@${{ needs['build-gh-aw-node'].outputs.digest }}" \
845970
> release/containers.txt
971+
# The Apple Container init image is only built when APPLE_VMINIT_IMAGE
972+
# is configured. Its absence is not silently equivalent to "use latest":
973+
# the Apple Container backend refuses any init image that is not
974+
# digest-pinned, so a release without this line simply cannot select
975+
# that preview runtime.
976+
if [ -n "${APPLE_INIT_DIGEST:-}" ]; then
977+
echo "ghcr.io/${{ github.repository }}/apple-init@${APPLE_INIT_DIGEST}" \
978+
>> release/containers.txt
979+
else
980+
echo "No apple-init digest; the Apple Container preview runtime is not selectable in this release."
981+
fi
846982
echo "Generated containers.txt:"
847983
cat release/containers.txt
848984

0 commit comments

Comments
 (0)