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
3 changes: 2 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -351,16 +351,33 @@ 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.

## Compatibility

| 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
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|---|
Expand Down
17 changes: 15 additions & 2 deletions cmd/openshell-driver-applecontainer/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <file>` rather than piped straight
// into a shell, so a truncated download cannot execute as a partial script.
Expand All @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions cmd/openshell-driver-applecontainer/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 9 additions & 0 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 17 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
# -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 driver release (default: latest)
# --openshell-version <X.Y.Z> pin OpenShell (default: its latest release)
# --openshell-version <X.Y.Z> pin OpenShell, with or without the leading
# "v" (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_OPENSHELL_VERSION, OSHL_AC_CONTAINER_VERSION,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down