Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f80407e
fix(onboard): use docker gateway on macOS OpenShell 0.0.37
ericksoa May 12, 2026
19d7a59
test(e2e): prove OpenShell upgrade preserves running agent
ericksoa May 12, 2026
0785611
fix(onboard): repair OpenShell before gateway upgrade
ericksoa May 12, 2026
a24a0c8
test(e2e): start OpenShell upgrade from real 0.36 CLI
ericksoa May 12, 2026
827e24a
test(e2e): seed upgrade sandbox with docker-driver harness
ericksoa May 12, 2026
db59075
test(e2e): upgrade from real NemoClaw 0.0.36 install
ericksoa May 12, 2026
323258b
test(e2e): use old curl installer for OpenShell upgrade
ericksoa May 12, 2026
186b8ce
test(e2e): avoid multiline exec for old OpenShell
ericksoa May 12, 2026
9a252f5
fix(onboard): retire legacy gateway before docker-driver upgrade
ericksoa May 12, 2026
ee54a42
fix(installer): preserve state across OpenShell gateway upgrade
ericksoa May 12, 2026
02451f8
test(e2e): describe OpenShell upgrade restore gate
ericksoa May 12, 2026
1ade44b
fix(onboard): use OpenShell VM gateway on macOS
ericksoa May 12, 2026
514a9ee
Merge remote-tracking branch 'origin/main' into fix/darwin-openshell-…
ericksoa May 12, 2026
4abb7f5
test(onboard): stabilize Darwin gateway repair checks
ericksoa May 12, 2026
75ac0e2
test(e2e): apply gateway upgrade shell formatting
ericksoa May 12, 2026
bc375a8
fix(installer): upgrade active OpenShell path
ericksoa May 12, 2026
31b92ec
fix(installer): gate legacy OpenShell upgrade
ericksoa May 12, 2026
cc49148
fix(installer): require backup support for gateway upgrade
ericksoa May 12, 2026
68a1882
fix(installer): make gateway upgrade wording agent-neutral
ericksoa May 12, 2026
06c315b
fix(onboard): preserve sandbox create diagnostics
ericksoa May 12, 2026
21a9c2c
fix(onboard): repair macos openshell vm upgrade
ericksoa May 12, 2026
6024aee
test(e2e): fix openshell upgrade shellcheck
ericksoa May 12, 2026
31b362d
Merge remote-tracking branch 'origin/main' into fix/darwin-openshell-…
ericksoa May 12, 2026
698ae05
Merge remote-tracking branch 'origin/main' into fix/darwin-openshell-…
ericksoa May 12, 2026
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
17 changes: 11 additions & 6 deletions .github/workflows/nightly-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
# sandbox-survival-e2e Sandbox survival across gateway restarts (onboard, inference,
# gateway stop/start, verify sandbox + workspace + inference).
# openshell-gateway-upgrade-e2e
# Validates stale Linux Docker-driver OpenShell gateway
# processes are restarted after an OpenShell upgrade.
# Validates real v0.0.36 curl install upgrade into
# OpenShell 0.0.37 with pre-upgrade backup, restored
# agent state, and the same agent type running.
# gateway-health-honest-e2e
# Coverage guard for #3111: onboard must not log
# "Docker-driver gateway is healthy" when the gateway
Expand Down Expand Up @@ -1257,17 +1258,17 @@ jobs:
if-no-files-found: ignore

# ── OpenShell gateway upgrade E2E ────────────────────────────
# Reproduces the old-install upgrade edge case for Linux Docker-driver
# gateways: a healthy gateway process with stale supervisor/runtime env must
# be restarted rather than reused after the current OpenShell install.
# Reproduces the old-install upgrade edge case: a working claw on the previous
# NemoClaw/OpenShell release must run through current curl-style install/onboard
# and keep the same in-sandbox agent process alive under the upgraded gateway.
openshell-gateway-upgrade-e2e:
if: >-
github.repository == 'NVIDIA/NemoClaw' &&
(github.event_name != 'workflow_dispatch' ||
inputs.jobs == '' ||
contains(format(',{0},', inputs.jobs), ',openshell-gateway-upgrade-e2e,'))
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
Expand All @@ -1291,8 +1292,12 @@ jobs:
name: openshell-gateway-upgrade-logs
path: |
/tmp/nemoclaw-e2e-openshell-gateway-upgrade.log
/tmp/nemoclaw-e2e-openshell-gateway-install.log
/tmp/nemoclaw-e2e-openshell-gateway-old-install.log
/tmp/nemoclaw-e2e-openshell-gateway-current-install.log
/tmp/nemoclaw-e2e-openshell-gateway-start.log
/tmp/nemoclaw-e2e-openshell-gateway-process.log
/tmp/nemoclaw-e2e-openshell-gateway-compatible-mock.log
if-no-files-found: ignore

# ── Gateway health-honesty E2E ──────────────────────────────
Expand Down
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ ARG NEMOCLAW_DISABLE_DEVICE_AUTH=0
# so each image gets a fresh gateway auth token.
# Pass --build-arg NEMOCLAW_BUILD_ID=$(date +%s) to bust the cache.
ARG NEMOCLAW_BUILD_ID=default
# macOS OpenShell VM backend imports the Docker image into a virtiofs rootfs
# where image uid/gid ownership is presented as the host user. The VM also
# starts NemoClaw as the non-root sandbox user, so uid-owned 770/660 paths
# become unreadable unless this Darwin-only compatibility mode is enabled.
ARG NEMOCLAW_DARWIN_VM_COMPAT=0
# Sandbox egress proxy host/port. Defaults match the OpenShell-injected
# gateway (10.200.0.1:3128). Operators on non-default networks can override
# at sandbox creation time by exporting NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT
Expand Down Expand Up @@ -592,6 +597,21 @@ RUN chown root:root /sandbox/.nemoclaw \
&& touch /sandbox/.nemoclaw/config.json \
&& chown sandbox:sandbox /sandbox/.nemoclaw/config.json

# OpenShell 0.0.37's macOS VM backend currently remaps rootfs ownership to the
# host uid/gid inside the guest, while the entrypoint runs as non-root sandbox.
# Enable this only for Darwin VM builds so Linux Docker-driver sandboxes keep
# the tighter group-only mutable-default permissions.
RUN if [ "$NEMOCLAW_DARWIN_VM_COMPAT" = "1" ]; then \
chmod -R a+rwX /sandbox/.openclaw; \
find /sandbox/.openclaw -type d -exec chmod a+rwx {} +; \
chmod a+rw /sandbox/.openclaw/openclaw.json /sandbox/.openclaw/.config-hash; \
for p in /sandbox/.nemoclaw/state /sandbox/.nemoclaw/migration /sandbox/.nemoclaw/snapshots /sandbox/.nemoclaw/staging; do \
chmod -R a+rwX "$p"; \
find "$p" -type d -exec chmod a+rwx {} +; \
done; \
chmod a+rw /sandbox/.nemoclaw/config.json; \
fi

# Entrypoint runs as root to start the gateway as the gateway user,
# then drops to sandbox for agent commands. See nemoclaw-start.sh.
ENTRYPOINT ["/usr/local/bin/nemoclaw-start"]
Expand Down
4 changes: 4 additions & 0 deletions ci/env-var-doc-allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"name": "NEMOCLAW_MODEL_ROUTER_VENV",
"reason": "Internal developer/test override for the managed Model Router virtualenv path. Production users should use the default ~/.nemoclaw/model-router-venv."
},
{
"name": "NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE",
"reason": "Internal installer sentinel exported only during OpenShell gateway replacement so onboard restores the pre-upgrade sandbox backup. Not user-facing."
},
{
"name": "NEMOCLAW_TEST_NO_SLEEP",
"reason": "Test sentinel that bypasses real-time sleep() calls in onboard inference probes. Set to '1' only by Vitest tests; never user-set."
Expand Down
3 changes: 3 additions & 0 deletions docs/manage-sandboxes/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ When a new NemoClaw release becomes available, update the `nemoclaw` CLI on your

Re-run the installer.
Before it onboards anything, the installer calls [`nemoclaw backup-all`](../reference/commands.md#nemoclaw-backup-all) automatically, storing a snapshot of each running sandbox in `~/.nemoclaw/rebuild-backups/` as a safety net.
If your existing gateway is from OpenShell earlier than `0.0.37`, the installer prompts before it runs the new automatic gateway upgrade path.
The automatic path is offered only when the existing `nemoclaw` CLI supports `backup-all`; older installs must preserve sandbox state manually before retiring the gateway.
For unattended installs, set `NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE=1`, or manually run `nemoclaw backup-all` and `openshell gateway destroy -g nemoclaw || openshell gateway destroy` before rerunning the installer as `curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 bash`.

```console
$ curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ To make the installer abort instead of continuing, set `NEMOCLAW_SINGLE_SESSION=
$ NEMOCLAW_SINGLE_SESSION=1 curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
```

When existing sandboxes were created with OpenShell earlier than `0.0.37`, the installer prompts before running the new automatic gateway upgrade path.
For scripted installs, set `NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE=1` to allow the installer to back up registered sandbox state, retire the old gateway, install OpenShell `0.0.37`, and restore state during onboarding.
The automatic path is disabled if the existing `nemoclaw` CLI does not advertise `backup-all`; preserve sandbox state manually before retiring the old gateway in that case.
To perform those steps manually, run `nemoclaw backup-all`, retire the old gateway with `openshell gateway destroy -g nemoclaw || openshell gateway destroy`, then rerun the installer as `curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 bash`.

The wizard prompts for a provider first, then collects the provider credential if needed.
Supported non-experimental choices include NVIDIA Endpoints, OpenAI, Anthropic, Google Gemini, and compatible OpenAI or Anthropic endpoints.
Credentials are registered with the OpenShell gateway and never persisted to host disk. See [Credential Storage](../security/credential-storage.md) for details on inspection, rotation, and migration from earlier releases.
Expand Down
4 changes: 4 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ bootstrap_usage() {
printf " NEMOCLAW_FRESH=1 Same as --fresh\n"
printf " NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 Same as --yes-i-accept-third-party-software\n"
printf " NEMOCLAW_SANDBOX_NAME Sandbox name to create/use\n"
printf " NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE=1\n"
printf " Allow automatic pre-0.0.37 OpenShell gateway upgrade\n"
printf " NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1\n"
printf " Continue after manually backing up and retiring old gateway\n"
printf " NEMOCLAW_PROVIDER build | openai | anthropic | anthropicCompatible\n"
printf " | gemini | ollama | custom | nim-local | vllm | routed\n"
printf " | hermes-provider\n"
Expand Down
92 changes: 92 additions & 0 deletions scripts/install-openshell.sh
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,89 @@ required_driver_bins_present() {
esac
}

macos_vm_driver_bin() {
command -v openshell-driver-vm 2>/dev/null || true
}

macos_vm_driver_has_hypervisor_entitlement() {
local bin="$1"
[ "$OS" = "Darwin" ] || return 0
[ -n "$bin" ] && [ -x "$bin" ] || return 1
command -v codesign >/dev/null 2>&1 || return 1
codesign -d --entitlements :- "$bin" 2>/dev/null \
| grep -q "com.apple.security.hypervisor"
}

sign_macos_vm_driver() {
local bin="$1"
local use_sudo="${2:-0}"
local entitlements

[ "$OS" = "Darwin" ] || return 0
[ -n "$bin" ] && [ -x "$bin" ] || return 0

if macos_vm_driver_has_hypervisor_entitlement "$bin"; then
return 0
fi
command -v codesign >/dev/null 2>&1 \
|| fail "codesign is required to prepare openshell-driver-vm for macOS Hypervisor.framework."

entitlements="$(mktemp "${TMPDIR:-/tmp}/nemoclaw-openshell-driver-vm-entitlements.XXXXXX.plist")"
cat >"$entitlements" <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.hypervisor</key>
<true/>
</dict>
</plist>
EOF

info "Signing openshell-driver-vm with the macOS Hypervisor entitlement..."
if [ "$use_sudo" = "1" ]; then
sudo codesign --force --sign - --entitlements "$entitlements" "$bin" \
|| {
rm -f "$entitlements"
fail "Failed to sign openshell-driver-vm with the macOS Hypervisor entitlement."
}
else
codesign --force --sign - --entitlements "$entitlements" "$bin" \
|| {
rm -f "$entitlements"
fail "Failed to sign openshell-driver-vm with the macOS Hypervisor entitlement."
}
fi
rm -f "$entitlements"

macos_vm_driver_has_hypervisor_entitlement "$bin" \
|| fail "openshell-driver-vm was signed but the macOS Hypervisor entitlement was not present afterward."
}

repair_existing_macos_vm_driver() {
local bin
[ "$OS" = "Darwin" ] || return 0
bin="$(macos_vm_driver_bin)"
[ -n "$bin" ] && [ -x "$bin" ] || return 1
if macos_vm_driver_has_hypervisor_entitlement "$bin"; then
return 0
fi

warn "openshell-driver-vm is missing the macOS Hypervisor entitlement — repairing..."
if [ -w "$bin" ]; then
sign_macos_vm_driver "$bin" 0
return 0
fi
if [ "${NEMOCLAW_NON_INTERACTIVE:-}" != "1" ] && [ -t 0 ] && command -v sudo >/dev/null 2>&1; then
sign_macos_vm_driver "$bin" 1
return 0
fi
return 1
}

ACTIVE_OPENSHELL_BIN=""
if command -v openshell >/dev/null 2>&1; then
ACTIVE_OPENSHELL_BIN="$(command -v openshell 2>/dev/null || true)"
INSTALLED_VERSION_OUTPUT="$(openshell --version 2>&1 || true)"
INSTALLED_VERSION="$(printf '%s\n' "$INSTALLED_VERSION_OUTPUT" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
[ -n "$INSTALLED_VERSION" ] || INSTALLED_VERSION="0.0.0"
Expand All @@ -106,6 +188,8 @@ if command -v openshell >/dev/null 2>&1; then
fi
if ! required_driver_bins_present; then
warn "openshell $INSTALLED_VERSION is missing Docker-driver binaries — reinstalling pinned OpenShell ${PIN_VERSION}..."
elif ! repair_existing_macos_vm_driver; then
warn "openshell $INSTALLED_VERSION has an unsigned macOS VM driver that could not be repaired in place — reinstalling pinned OpenShell ${PIN_VERSION}..."
else
info "openshell already installed: $INSTALLED_VERSION (>= $MIN_VERSION, <= $MAX_VERSION)"
exit 0
Expand Down Expand Up @@ -209,6 +293,12 @@ for asset_name in "${ASSETS[@]}"; do
done

target_dir="/usr/local/bin"
if [[ -n "$ACTIVE_OPENSHELL_BIN" && "$ACTIVE_OPENSHELL_BIN" = /* ]]; then
active_dir="$(dirname "$ACTIVE_OPENSHELL_BIN")"
if [ -d "$active_dir" ] && [ -w "$active_dir" ]; then
target_dir="$active_dir"
fi
fi

install_bins() {
local dir="$1"
Expand All @@ -221,6 +311,7 @@ install_bins() {
fi
if [ -x "$tmpdir/openshell-driver-vm" ]; then
install -m 755 "$tmpdir/openshell-driver-vm" "$dir/openshell-driver-vm"
sign_macos_vm_driver "$dir/openshell-driver-vm" 0
fi
}

Expand All @@ -243,6 +334,7 @@ else
fi
if [ -x "$tmpdir/openshell-driver-vm" ]; then
sudo install -m 755 "$tmpdir/openshell-driver-vm" "$target_dir/openshell-driver-vm"
sign_macos_vm_driver "$target_dir/openshell-driver-vm" 1
fi
fi

Expand Down
Loading
Loading