From 9d4360b74fa47c0fc9e86597d25199e67e830b4e Mon Sep 17 00:00:00 2001 From: Vyncint Ng Date: Sat, 22 Aug 2026 12:10:40 +0700 Subject: [PATCH] fix: normalise the OpenShell version pin to a release tag --openshell-version has never worked. OpenShell tags every release vX.Y.Z and its installer uses OPENSHELL_VERSION verbatim as the tag in the asset URL, so the value we documented -- `--openshell-version 0.0.97` -- asked for a release that does not exist: https://github.com/NVIDIA/OpenShell/releases/download/0.0.97/openshell.rb 404 https://github.com/NVIDIA/OpenShell/releases/download/v0.0.97/openshell.rb 200 and the install died with "the selected release may not include a Homebrew formula". Broken since the flag shipped in v0.2.7; found while testing against newer upstreams. Normalise a bare X.Y.Z to vX.Y.Z in both the Go path and install.sh. An explicit vX.Y.Z passes through, as does OpenShell's documented `dev` literal for the rolling build. Also record what the compatibility testing found. Verified live on OpenShell 0.0.111 and apple/container 1.2.2: create -> Ready -> exec -> delete, egress still blocked by policy, restart adoption intact. The contract gained four RPCs after v0.0.96 -- GetGatewayListenerRequirements, StartSandbox, EnsureWorkspace, DeleteWorkspace -- none of which this driver implements. Three are optional by construction: the gateway maps their Unimplemented back to Ok. StopSandbox/StartSandbox are not, but are reached only by the new `openshell sandbox stop`/`start` commands, or by lifecycle sweeps gated on the gateway_manages_lifecycle capability this driver does not advertise. README, STATUS.md and docs/CONTRACT.md now say so instead of implying the v0.0.96 contract is still current. Signed-off-by: Vyncint Ng --- .goreleaser.yaml | 3 +- CHANGELOG.md | 20 +++++++++++++ README.md | 29 +++++++++++++++---- STATUS.md | 3 +- cmd/openshell-driver-applecontainer/update.go | 17 +++++++++-- .../update_test.go | 18 ++++++++++++ docs/CONTRACT.md | 9 ++++++ install.sh | 20 +++++++++++-- 8 files changed, 106 insertions(+), 13 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7a096d8..6ff9b04 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -63,7 +63,8 @@ release: Unpacking the archive by hand instead? The binary is unsigned — run `xattr -d com.apple.quarantine openshell-driver-applecontainer` after extracting. - Compatibility: OpenShell v0.0.96, apple/container 1.2.0, Apple silicon macOS 26. + Compatibility: OpenShell 0.0.96 – 0.0.111, apple/container 1.2.0 – 1.2.2, Apple silicon macOS 26. + `openshell sandbox stop`/`start` are unimplemented by this driver. # Published to https://github.com/vyncint/homebrew-tap as a cask, not a formula: # Homebrew treats pre-built binaries as casks, and a cask handles the quarantine diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cade84..bff25f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,26 @@ All notable changes to this project are documented here. The format follows ## [Unreleased] +### Fixed + +- **`--openshell-version` never worked.** OpenShell tags every release `vX.Y.Z` and its installer + uses `OPENSHELL_VERSION` verbatim as the tag in the asset URL, so the documented + `--openshell-version 0.0.97` asked for a release that does not exist and the install died with + "the selected release may not include a Homebrew formula". Both the flag (`update --all + --openshell-version`) and `install.sh --openshell-version` now normalise a bare `X.Y.Z` to + `vX.Y.Z`; an explicit `vX.Y.Z` and OpenShell's `dev` literal pass through untouched. Broken + since the flag shipped in v0.2.7. + +### Changed + +- **Compatibility verified against OpenShell 0.0.111 and apple/container 1.2.2** (previously + 0.0.97 and 1.2.0), live on the reference machine: create → Ready → exec → delete, egress still + blocked by policy, and restart adoption. The contract gained four RPCs after v0.0.96 that this + driver does not implement; three are optional by design (the gateway maps `Unimplemented` to + success) and `StopSandbox`/`StartSandbox` are reached only by the new + `openshell sandbox stop`/`start` commands, which fail with a clear message. README, STATUS and + `docs/CONTRACT.md` now spell this out rather than implying the v0.0.96 contract is current. + ## [0.2.11] - 2026-08-17 ### Security diff --git a/README.md b/README.md index 3eb1e31..20d29a5 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ 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 +openshell-driver-applecontainer update --all --openshell-version 0.0.111 --container-version 1.2.2 # pin the prerequisites too (reproducible / rollback) openshell-driver-applecontainer cleanup # remove the driver service + gateway wiring (data kept) @@ -351,8 +351,10 @@ recon in `docs/CONTRACT.md`. - **No host-side nftables defense layer** — that upstream mechanism is Linux-only. On macOS, vmnet NAT already blocks inbound traffic from off the Mac; a pf anchor reproducing the "guests may only reach the gateway port" rule is possible future work. -- `StopSandbox` returns `Unimplemented` (the gateway never calls it in v0.0.96; the managed - VM driver does the same). +- `StopSandbox` returns `Unimplemented`. The v0.0.96 gateway never called it; newer ones do, for + the `openshell sandbox stop` / `start` commands, so those two commands fail cleanly against this + driver ("StopSandbox is not implemented yet") while everything else works. See + [Compatibility](#compatibility). - GPU sandboxes are rejected (`ValidateSandboxCreate` fails them explicitly). - One `cpuOverhead` vCPU is added by apple/container on top of the requested count. @@ -360,7 +362,22 @@ recon in `docs/CONTRACT.md`. | driver | OpenShell | apple/container | host | |---|---|---|---| -| 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 | +| v0.2.12+ | contract derived from v0.0.96 (`5541398ccbda`); **verified against 0.0.96, 0.0.97 and 0.0.111** | **1.2.0 and 1.2.2** | Apple silicon, macOS 26 | +| v0.1.x – v0.2.11 | contract derived from v0.0.96; verified against 0.0.96 and 0.0.97 | 1.2.0 | Apple silicon, macOS 26 | + +**Newer gateways stay compatible by design, not by luck.** The contract grew four RPCs after +v0.0.96 — `GetGatewayListenerRequirements`, `StartSandbox`, `EnsureWorkspace`, `DeleteWorkspace` — +and this driver implements none of them. Three are explicitly optional: the gateway maps their +`Unimplemented` back to success (`Err(status) if status.code() == Code::Unimplemented => Ok(())` +in `compute/mod.rs`), which is upstream's stated forward-compatibility contract for independently +versioned external drivers. `StopSandbox`/`StartSandbox` are the exception, and are reached only +by an explicit `openshell sandbox stop`/`start`, or by lifecycle sweeps a driver opts into with +the `gateway_manages_lifecycle` capability, which this driver does not advertise. So on 0.0.111 +everything works except those two commands, which fail with a clear message rather than damaging +anything. + +Newer spec fields are likewise ignored rather than honoured: `DriverSandboxSpec.command` and +`.tty`, added after v0.0.96, do not reach the guest. 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 @@ -372,8 +389,8 @@ matching tag is unpublished the driver falls back to the pinned one rather than 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 +curl -LsSf …/install.sh | sh -s -- --version v0.2.12 --openshell-version 0.0.111 --container-version 1.2.2 +openshell-driver-applecontainer update --all --openshell-version 0.0.111 --container-version 1.2.2 ``` ## Install from a release (manual) diff --git a/STATUS.md b/STATUS.md index 5bcc51f..ac380d2 100644 --- a/STATUS.md +++ b/STATUS.md @@ -3,7 +3,8 @@ Single source of truth for milestone state. Updated at every milestone boundary. Pinned upstream: OpenShell **v0.0.96** (`5541398ccbda05fd951e08e5741b9ca090717f3a`). -Host: Apple silicon, macOS 26.6, apple/container 1.2.0, Go 1.26.5. +Verified against OpenShell 0.0.96, 0.0.97 and 0.0.111. +Host: Apple silicon, macOS 26.6, apple/container 1.2.0 and 1.2.2, Go 1.26.6. | Milestone | State | Notes | |---|---|---| diff --git a/cmd/openshell-driver-applecontainer/update.go b/cmd/openshell-driver-applecontainer/update.go index b6017f8..6e15af3 100644 --- a/cmd/openshell-driver-applecontainer/update.go +++ b/cmd/openshell-driver-applecontainer/update.go @@ -36,7 +36,7 @@ func runUpdate(args []string) int { 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") + openshellVersion := fs.String("openshell-version", "", "with --all: pin OpenShell to this release (e.g. 0.0.111 or v0.0.111); 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 @@ -285,6 +285,19 @@ func updatePrerequisites(log *slog.Logger, openshellVersion, containerVersion st // OPENSHELL_VERSION to select a release. const openShellInstallURL = "https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh" +// openShellReleaseTag turns a user-supplied OpenShell version into the release +// tag its installer expects. OpenShell tags every release `vX.Y.Z` and uses +// OPENSHELL_VERSION verbatim as the tag in the asset URL, so a bare `0.0.111` +// asks for a release that does not exist and the install fails with "the +// selected release may not include a Homebrew formula". Accept both spellings. +// `dev` is OpenShell's documented literal for the rolling build; never touch it. +func openShellReleaseTag(version string) string { + if version == "" || version == "dev" || strings.HasPrefix(version, "v") { + return version + } + return "v" + version +} + // runOpenShellInstaller installs a specific OpenShell release. The script is // downloaded to a file and executed with `sh ` rather than piped straight // into a shell, so a truncated download cannot execute as a partial script. @@ -302,7 +315,7 @@ func runOpenShellInstaller(version string) error { return fmt.Errorf("download the OpenShell installer: %w", err) } cmd := exec.Command("/bin/sh", script) - cmd.Env = append(os.Environ(), "OPENSHELL_VERSION="+version) + cmd.Env = append(os.Environ(), "OPENSHELL_VERSION="+openShellReleaseTag(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 { diff --git a/cmd/openshell-driver-applecontainer/update_test.go b/cmd/openshell-driver-applecontainer/update_test.go index 0299504..3f940e4 100644 --- a/cmd/openshell-driver-applecontainer/update_test.go +++ b/cmd/openshell-driver-applecontainer/update_test.go @@ -21,6 +21,24 @@ func TestReleaseArchiveName(t *testing.T) { } } +func TestOpenShellReleaseTag(t *testing.T) { + cases := map[string]string{ + // OpenShell tags releases vX.Y.Z; a bare version 404s on the asset URL. + "0.0.111": "v0.0.111", + "0.0.97": "v0.0.97", + "v0.0.111": "v0.0.111", + // Empty means "latest" — the installer resolves it itself. + "": "", + // OpenShell's documented literal for the rolling build. + "dev": "dev", + } + for in, want := range cases { + if got := openShellReleaseTag(in); got != want { + t.Errorf("openShellReleaseTag(%q) = %q, want %q", in, got, want) + } + } +} + // makeTarGz writes a gzipped tar of name->content and returns its path. func makeTarGz(t *testing.T, dir string, entries map[string][]byte) string { t.Helper() diff --git a/docs/CONTRACT.md b/docs/CONTRACT.md index 9fb3a2c..f3c9f4d 100644 --- a/docs/CONTRACT.md +++ b/docs/CONTRACT.md @@ -2,6 +2,15 @@ Everything below was derived by reading NVIDIA/OpenShell at tag **v0.0.96** (commit `5541398ccbda05fd951e08e5741b9ca090717f3a`). File:line references are into that tree. + +> **The upstream contract has grown since.** As of OpenShell v0.0.111 it adds four RPCs — +> `GetGatewayListenerRequirements`, `StartSandbox`, `EnsureWorkspace`, `DeleteWorkspace` — plus +> `GetCapabilitiesResponse.gateway_manages_lifecycle` and `DriverSandboxSpec.command` / `.tty`. +> This driver implements none of them and remains compatible: the gateway maps `Unimplemented` +> to success for the three optional RPCs, and reaches `StopSandbox`/`StartSandbox` only through +> the explicit `openshell sandbox stop`/`start` commands or lifecycle sweeps gated on the +> `gateway_manages_lifecycle` capability this driver does not advertise. Verified live on +> 0.0.111. The sections below still describe the v0.0.96 baseline this driver was written to. The two contract protos are vendored verbatim under `proto/` (see NOTICE). ## 1. RPC surface (proto/compute_driver.proto, package openshell.compute.v1) diff --git a/install.sh b/install.sh index 3a52e86..6c520fa 100755 --- a/install.sh +++ b/install.sh @@ -16,7 +16,8 @@ # -y, --yes assume "yes" to every prompt (non-interactive) # --no-setup install the binary but do not run `setup` # --version install a specific driver release (default: latest) -# --openshell-version pin OpenShell (default: its latest release) +# --openshell-version pin OpenShell, with or without the leading +# "v" (default: its latest release) # --container-version pin apple/container (default: its latest release) # --prefix install prefix (default: /opt/homebrew) # OSHL_AC_VERSION, OSHL_AC_OPENSHELL_VERSION, OSHL_AC_CONTAINER_VERSION, @@ -44,6 +45,18 @@ err() { need() { command -v "$1" >/dev/null 2>&1; } +# openshell_release_tag turns a user-supplied OpenShell version into the release +# tag its installer expects. OpenShell tags every release vX.Y.Z and uses +# OPENSHELL_VERSION verbatim as the tag in the asset URL, so a bare X.Y.Z asks +# for a release that does not exist. Accept both spellings; `dev` is +# OpenShell's documented literal for the rolling build and passes through. +openshell_release_tag() { + case "$1" in + "" | dev | v*) printf '%s' "$1" ;; + *) printf 'v%s' "$1" ;; + esac +} + # confirm asks a yes/no question, reading from the controlling terminal so # it works under `curl … | sh`. Returns non-zero for "no" / no terminal. confirm() { @@ -167,8 +180,9 @@ check_openshell() { # non-zero exit here is expected. Tolerate it and verify the binary # landed instead; `setup` (run later) brings the gateway up. 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 + tag=$(openshell_release_tag "$OPENSHELL_VERSION_PIN") + info "installing OpenShell $tag (pinned)" + curl -LsSf "$OPENSHELL_INSTALL_URL" | OPENSHELL_VERSION="$tag" sh || true else curl -LsSf "$OPENSHELL_INSTALL_URL" | sh || true fi