Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

### Fixed

- **The supervisor image tag now tracks the installed gateway.** It was hardcoded to `0.0.96`
while the OpenShell installer resolves its own latest release, so a host running gateway
0.0.97 booted every sandbox with a 0.0.96 supervisor — a silent version mismatch, since the
supervisor runs inside the sandbox and speaks to the gateway. The driver now reads
`openshell-gateway --version` and uses the matching `supervisor:<version>`; `--supervisor-image`
(or `OSHL_AC_SUPERVISOR_IMAGE`) still pins it explicitly, and an unpublished matching tag falls
back to the pinned one instead of failing every create.

### Added

- Version pinning for the prerequisites, for reproducible installs and upstream rollbacks:
`install.sh --openshell-version X.Y.Z --container-version X.Y.Z` (env
`OSHL_AC_OPENSHELL_VERSION`, `OSHL_AC_CONTAINER_VERSION`) and
`update --all --openshell-version … --container-version …`. Previously only the driver's own
release was selectable; OpenShell and apple/container always resolved to latest. OpenShell
pinning goes through its official installer (which honors `OPENSHELL_VERSION`), apple/container
through `update-container.sh -v`.
- `setup` reports the resolved driver / gateway / apple-container versions and the supervisor
image, and warns when the supervisor tag does not match the gateway.

## [0.2.6] - 2026-08-02

### Fixed
Expand Down
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ Both services start at login and restart on failure — nothing to launch by han
to repair the installation). To upgrade or remove the stack later, see
[Update and remove](#update-and-remove).

The installer is non-interactive with `-y`, and takes `--no-setup`, `--version vX.Y.Z`, and
`--prefix <dir>` (or the `OSHL_AC_YES`, `OSHL_AC_VERSION`, `OSHL_AC_PREFIX` env vars).
The installer is non-interactive with `-y`, and takes `--no-setup`, `--version vX.Y.Z`,
`--openshell-version X.Y.Z`, `--container-version X.Y.Z`, and `--prefix <dir>` (or the
`OSHL_AC_YES`, `OSHL_AC_VERSION`, `OSHL_AC_OPENSHELL_VERSION`, `OSHL_AC_CONTAINER_VERSION`,
`OSHL_AC_PREFIX` env vars). Without the pins each component resolves to its latest release.

### Update and remove

Expand All @@ -47,6 +49,8 @@ Two commands manage the stack's lifecycle, mirroring apple/container's own
openshell-driver-applecontainer update # update the driver to the latest release, then re-setup
openshell-driver-applecontainer update --all # also update OpenShell (brew) and apple/container
openshell-driver-applecontainer update --version vX.Y.Z # pin a specific driver release
openshell-driver-applecontainer update --all --openshell-version 0.0.97 --container-version 1.2.0
# pin the prerequisites too (reproducible / rollback)

openshell-driver-applecontainer cleanup # remove the driver service + gateway wiring (data kept)
openshell-driver-applecontainer cleanup -d # also remove driver state, vmnet network and pulled images
Expand Down Expand Up @@ -172,7 +176,7 @@ changing anything here so the launchd service picks it up:
| `--state-dir` | `~/.local/state/openshell-applecontainer` | sandbox records, seed dirs, supervisor cache |
| `--network` | `oshl` | vmnet network for sandbox VMs (auto-created) |
| `--default-image` | `ghcr.io/nvidia/openshell-community/sandboxes/base:latest` | advertised via GetCapabilities |
| `--supervisor-image` | `ghcr.io/nvidia/openshell/supervisor:0.0.96` | release-matched supervisor source |
| `--supervisor-image` | matched to the installed gateway (falls back to `…/supervisor:0.0.96`) | release-matched supervisor source |
| `--grpc-endpoint` | **auto-derived** from the vmnet network (`https://<gateway-ip>:17670`) | gateway endpoint as reachable from inside guests |
| `--guest-tls-ca/cert/key` | auto-detected (`$OPENSHELL_LOCAL_TLS_DIR`, XDG state, or the Homebrew TLS dir) | client TLS triple handed to sandboxes |
| `--namespace` | `default` | namespace reported on sandboxes |
Expand Down Expand Up @@ -302,9 +306,23 @@ recon in `docs/CONTRACT.md`.

## Compatibility

| driver | OpenShell (pinned tag) | apple/container | host |
| driver | OpenShell | apple/container | host |
|---|---|---|---|
| v0.1.x – v0.2.x | v0.0.96 (`5541398ccbda`) | 1.2.0 | Apple silicon, macOS 26 |
| v0.1.x – v0.2.x | contract derived from v0.0.96 (`5541398ccbda`); verified against 0.0.96 and 0.0.97 | 1.2.0 | Apple silicon, macOS 26 |

The supervisor runs **inside** every sandbox and speaks to the gateway, so its image tag must
track the gateway's version. The driver reads the installed gateway's version
(`openshell-gateway --version`) and pulls the matching `supervisor:<version>` automatically —
`setup` prints the resolved driver / gateway / apple-container versions and warns on a mismatch.
Pin it yourself with `--supervisor-image` (or `OSHL_AC_SUPERVISOR_IMAGE`) to opt out; if the
matching tag is unpublished the driver falls back to the pinned one rather than failing.

Pin the whole stack for a reproducible install (or to roll back a bad upstream release):

```sh
curl -LsSf …/install.sh | sh -s -- --version v0.2.6 --openshell-version 0.0.97 --container-version 1.2.0
openshell-driver-applecontainer update --all --openshell-version 0.0.97 --container-version 1.2.0
```

## Install from a release (manual)

Expand Down
3 changes: 3 additions & 0 deletions cmd/openshell-driver-applecontainer/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func runCleanup(args []string) int {
return 1
}
log := newLogger("info")
// Remove the supervisor image the driver actually uses, not just the
// pinned default.
defaults.ResolveSupervisorImage(log)
s, err := hostsetup.New(backend.NewCLI(log), log)
if err != nil {
log.Error("cleanup failed", "err", err)
Expand Down
10 changes: 9 additions & 1 deletion cmd/openshell-driver-applecontainer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,14 @@ Usage:
Idempotent — re-run any time to repair the installation.

openshell-driver-applecontainer update [--version vX.Y.Z] [--all] [--no-setup]
[--openshell-version X.Y.Z]
[--container-version X.Y.Z]
Updates the driver to the latest release (verifying its checksum)
and re-runs setup so the service restarts on the new binary. --all
also updates the prerequisites (OpenShell via brew, apple/container
via its own updater). --no-setup replaces the binary only.
via its own updater); with --all, --openshell-version and
--container-version pin those to an exact release instead of latest.
--no-setup replaces the binary only.

openshell-driver-applecontainer cleanup [-d | -k] [--all]
Reverses setup. By default removes only the driver's service and
Expand Down Expand Up @@ -101,6 +105,10 @@ func run(args []string) error {
log := newLogger(cfg.LogLevel)
slog.SetDefault(log)

// Match the supervisor image to the gateway installed on this host unless
// the operator pinned it; a lagging tag is a silent protocol mismatch.
cfg.ResolveSupervisorImage(log)

store, err := state.NewStore(cfg.StateDir)
if err != nil {
return err
Expand Down
6 changes: 5 additions & 1 deletion cmd/openshell-driver-applecontainer/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ func runSetup(args []string) int {
slog.Error("resolve defaults", "err", err)
return 1
}
log := newLogger("info")
// Resolve before the flag default is captured, so the pre-pull grabs the
// supervisor image the driver will actually use.
defaults.ResolveSupervisorImage(log)

fs := flag.NewFlagSet("setup", flag.ContinueOnError)
network := fs.String("network", defaults.Network, "vmnet network for sandbox VMs")
Expand All @@ -32,7 +36,6 @@ func runSetup(args []string) int {
return 2
}

log := newLogger("info")
s, err := hostsetup.New(backend.NewCLI(log), log)
if err != nil {
log.Error("setup failed", "err", err)
Expand All @@ -47,6 +50,7 @@ func runSetup(args []string) int {
DefaultImage: *defaultImage,
SupervisorImage: *supervisorImage,
PullImages: !*noPull,
DriverVersion: version,
}); err != nil {
log.Error("setup failed", "err", err)
return 1
Expand Down
73 changes: 64 additions & 9 deletions cmd/openshell-driver-applecontainer/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,18 @@ const (
// service restarts on it. With --all it also updates the prerequisites.
func runUpdate(args []string) int {
fs := flag.NewFlagSet("update", flag.ContinueOnError)
targetVersion := fs.String("version", "", "install a specific release (e.g. v0.2.4); default: latest")
targetVersion := fs.String("version", "", "install a specific driver release (e.g. v0.2.4); default: latest")
noSetup := fs.Bool("no-setup", false, "replace the binary but do not re-run setup")
all := fs.Bool("all", false, "also update the prerequisites: OpenShell (brew) and apple/container")
openshellVersion := fs.String("openshell-version", "", "with --all: pin OpenShell to this release (e.g. 0.0.97); default: its latest")
containerVersion := fs.String("container-version", "", "with --all: pin apple/container to this release (e.g. 1.2.0); default: its latest")
if err := fs.Parse(args); err != nil {
return 2
}
if !*all && (*openshellVersion != "" || *containerVersion != "") {
slog.Error("update: --openshell-version/--container-version only apply with --all")
return 2
}
log := newLogger("info")

want := *targetVersion
Expand All @@ -58,7 +64,7 @@ func runUpdate(args []string) int {
}

if *all {
updatePrerequisites(log)
updatePrerequisites(log, *openshellVersion, *containerVersion)
}

if *noSetup {
Expand Down Expand Up @@ -213,11 +219,18 @@ func replaceBinary(target, newBin string) error {
return streamCmd("sudo", "install", "-m", "0755", newBin, target)
}

// updatePrerequisites updates OpenShell (brew) and apple/container (via its
// own installed updater). Best-effort and terminal-attached (brew output,
// sudo prompts).
func updatePrerequisites(log *slog.Logger) {
if _, err := exec.LookPath("brew"); err == nil {
// updatePrerequisites updates OpenShell and apple/container. Best-effort and
// terminal-attached (brew output, sudo prompts). Empty version strings mean
// "latest"; a pinned version reproduces an exact stack (or rolls one back).
func updatePrerequisites(log *slog.Logger, openshellVersion, containerVersion string) {
if openshellVersion != "" {
// brew cannot install an arbitrary tap version, so pinning goes
// through OpenShell's own installer, which honors OPENSHELL_VERSION.
log.Info("update: installing the pinned OpenShell release", "version", openshellVersion)
if err := runOpenShellInstaller(openshellVersion); err != nil {
log.Warn("pinned OpenShell install failed", "version", openshellVersion, "err", err)
}
} else if _, err := exec.LookPath("brew"); err == nil {
log.Info("update: upgrading OpenShell (brew)")
if err := streamCmd("brew", "upgrade", "openshell"); err != nil {
log.Warn("brew upgrade openshell failed (it may already be current)", "err", err)
Expand All @@ -230,8 +243,13 @@ func updatePrerequisites(log *slog.Logger) {
if err := streamCmd("container", "system", "stop"); err != nil {
log.Debug("container system stop", "err", err)
}
log.Info("update: updating apple/container (its updater needs sudo)")
if err := streamCmd(acUpdater); err != nil {
acArgs := []string{}
if containerVersion != "" {
acArgs = append(acArgs, "-v", containerVersion)
}
log.Info("update: updating apple/container (its updater needs sudo)",
"version", orLatest(containerVersion))
if err := streamCmd(acUpdater, acArgs...); err != nil {
log.Warn("apple/container updater failed", "err", err)
}
// Bring the runtime back up; the driver needs it. (setup would also
Expand All @@ -242,6 +260,43 @@ func updatePrerequisites(log *slog.Logger) {
}
}

// openShellInstallURL is OpenShell's official installer; it honors
// OPENSHELL_VERSION to select a release.
const openShellInstallURL = "https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh"

// runOpenShellInstaller installs a specific OpenShell release. The script is
// downloaded to a file and executed with `sh <file>` rather than piped straight
// into a shell, so a truncated download cannot execute as a partial script.
// Its gateway health-check cannot pass until setup runs afterwards, so a
// non-zero exit is expected and not treated as failure — the caller re-runs
// setup, and the version probe afterwards reveals what actually landed.
func runOpenShellInstaller(version string) error {
tmp, err := os.MkdirTemp("", "oshl-installer-")
if err != nil {
return err
}
defer func() { _ = os.RemoveAll(tmp) }()
script := filepath.Join(tmp, "install.sh")
if err := downloadTo(openShellInstallURL, script); err != nil {
return fmt.Errorf("download the OpenShell installer: %w", err)
}
cmd := exec.Command("/bin/sh", script)
cmd.Env = append(os.Environ(), "OPENSHELL_VERSION="+version)
cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin
_ = cmd.Run() // expected non-zero: the gateway has no driver until setup
if _, err := exec.LookPath("openshell"); err != nil {
return fmt.Errorf("openshell binary not present after install: %w", err)
}
return nil
}

func orLatest(v string) string {
if v == "" {
return "latest"
}
return v
}

// currentBinaryPath is the real (symlink-resolved) path of the running binary.
func currentBinaryPath() (string, error) {
self, err := os.Executable()
Expand Down
38 changes: 32 additions & 6 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@
# Environment / flags:
# -y, --yes assume "yes" to every prompt (non-interactive)
# --no-setup install the binary but do not run `setup`
# --version <vX.Y.Z> install a specific release (default: latest)
# --version <vX.Y.Z> install a specific driver release (default: latest)
# --openshell-version <X.Y.Z> pin OpenShell (default: its latest release)
# --container-version <X.Y.Z> pin apple/container (default: its latest release)
# --prefix <dir> install prefix (default: /opt/homebrew)
# OSHL_AC_VERSION, OSHL_AC_PREFIX, OSHL_AC_YES=1 mirror the flags.
# OSHL_AC_VERSION, OSHL_AC_OPENSHELL_VERSION, OSHL_AC_CONTAINER_VERSION,
# OSHL_AC_PREFIX, OSHL_AC_YES=1 mirror the flags.
set -eu

REPO="vyncint/openshell-driver-applecontainer"
BINARY="openshell-driver-applecontainer"
PREFIX="${OSHL_AC_PREFIX:-/opt/homebrew}"
VERSION="${OSHL_AC_VERSION:-}"
OPENSHELL_VERSION_PIN="${OSHL_AC_OPENSHELL_VERSION:-}"
CONTAINER_VERSION_PIN="${OSHL_AC_CONTAINER_VERSION:-}"
ASSUME_YES="${OSHL_AC_YES:-0}"
RUN_SETUP=1

Expand Down Expand Up @@ -63,13 +68,25 @@ parse_args() {
VERSION="$2"
shift
;;
--openshell-version)
[ "$#" -ge 2 ] || err "--openshell-version needs a value"
OPENSHELL_VERSION_PIN="$2"
shift
;;
--container-version)
[ "$#" -ge 2 ] || err "--container-version needs a value"
CONTAINER_VERSION_PIN="$2"
shift
;;
--prefix)
[ "$#" -ge 2 ] || err "--prefix needs a value"
PREFIX="$2"
shift
;;
-h | --help)
sed -n '2,20p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//'
# Print the header comment block, stopping at the first code line
# (robust to the header growing).
awk 'NR>1 && /^#/ { sub(/^# ?/, ""); print; next } NR>1 { exit }' "$0"
exit 0
;;
*) err "unknown option: $1" ;;
Expand Down Expand Up @@ -103,8 +120,12 @@ check_homebrew() {

# install_container downloads and runs Apple's signed installer package.
install_container() {
tag=$(curl -sSf "https://api.github.com/repos/$CONTAINER_REPO/releases/latest" |
grep '"tag_name"' | head -1 | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
if [ -n "$CONTAINER_VERSION_PIN" ]; then
tag="$CONTAINER_VERSION_PIN"
else
tag=$(curl -sSf "https://api.github.com/repos/$CONTAINER_REPO/releases/latest" |
grep '"tag_name"' | head -1 | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
fi
[ -n "$tag" ] || err "could not determine the latest apple/container release"
pkg="container-${tag}-installer-signed.pkg"
url="https://github.com/$CONTAINER_REPO/releases/download/$tag/$pkg"
Expand Down Expand Up @@ -141,7 +162,12 @@ check_openshell() {
# cannot pass until this driver's `setup` runs afterwards — so a
# non-zero exit here is expected. Tolerate it and verify the binary
# landed instead; `setup` (run later) brings the gateway up.
curl -LsSf "$OPENSHELL_INSTALL_URL" | sh || true
if [ -n "$OPENSHELL_VERSION_PIN" ]; then
info "installing OpenShell $OPENSHELL_VERSION_PIN (pinned)"
curl -LsSf "$OPENSHELL_INSTALL_URL" | OPENSHELL_VERSION="$OPENSHELL_VERSION_PIN" sh || true
else
curl -LsSf "$OPENSHELL_INSTALL_URL" | sh || true
fi
need openshell || err "OpenShell installation failed"
else
err "OpenShell is required"
Expand Down
Loading