Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
efb5fa7
fix(onboard): grant Jetson nvmap group write access
sandl99 Aug 3, 2026
09bef64
docs(troubleshooting): simplify Jetson nvmap guidance
sandl99 Aug 3, 2026
632a5c9
test(onboard): cover failed nvmap permission verification
sandl99 Aug 3, 2026
0bb4a74
docs(troubleshooting): clarify nvmap rule lifecycle
sandl99 Aug 3, 2026
ea03eab
test(onboard): cover missing Jetson nvmap device
sandl99 Aug 3, 2026
c378434
Merge branch 'main' into fix/jetson-nvmap-nonroot
sandl99 Aug 4, 2026
29262e9
fix(onboard): preserve Jetson sandbox GPU access
sandl99 Aug 4, 2026
2257f36
Merge remote-tracking branch 'origin/main' into fix/jetson-nvmap-nonroot
sandl99 Aug 4, 2026
c9a80f8
fix(onboard): scope Jetson nvmap access to OpenClaw
sandl99 Aug 4, 2026
457d1f9
refactor(onboard): keep entrypoint line budget neutral
sandl99 Aug 4, 2026
9bd5a3a
test(images): stage Jetson bootstrap helper
sandl99 Aug 4, 2026
67a8579
fix(images): stage Jetson bootstrap script
sandl99 Aug 4, 2026
3690a12
Merge branch 'main' into fix/jetson-nvmap-nonroot
sandl99 Aug 4, 2026
ed7f1ec
fix(onboard): preserve managed Jetson groups
sandl99 Aug 4, 2026
1a17a1c
Merge branch 'main' into fix/jetson-nvmap-nonroot
apurvvkumaria Aug 4, 2026
8785891
Merge branch 'main' into fix/jetson-nvmap-nonroot
sandl99 Aug 5, 2026
5237552
Merge branch 'main' into fix/jetson-nvmap-nonroot
sandl99 Aug 5, 2026
92e10e2
Merge branch 'main' into fix/jetson-nvmap-nonroot
sandl99 Aug 5, 2026
5a92cb1
Merge remote-tracking branch 'origin/main' into fix/jetson-nvmap-nonroot
sandl99 Aug 5, 2026
70600b3
fix(installer): configure Jetson nvmap before L4T classification
sandl99 Aug 5, 2026
25d2c8b
fix(onboard): verify Jetson nvmap access before sandbox creation
sandl99 Aug 5, 2026
e9198bc
test(jetson): add nvmap boundary proof
sandl99 Aug 5, 2026
c7d6e05
fix(test): initialize Jetson POC readonly values
sandl99 Aug 5, 2026
f27f724
fix(test): make Jetson nvmap proof fail closed
apurvvkumaria Aug 5, 2026
8508897
Merge remote-tracking branch 'origin/main' into fix/jetson-nvmap-nonroot
sandl99 Aug 5, 2026
bf27905
fix(onboard): grant Jetson devices in sandbox policy
sandl99 Aug 5, 2026
98e392e
test(jetson): add CUDA boundary probe
sandl99 Aug 5, 2026
ba6080d
merge: resolve conflicts with main
github-actions[bot] Aug 6, 2026
06d7407
chore(onboard): revert prior Jetson GPU changes
sandl99 Aug 6, 2026
da3d48e
merge: sync main before focused Jetson fix
sandl99 Aug 6, 2026
000e88e
fix(onboard): permit Jetson GPU policy paths
sandl99 Aug 6, 2026
48338b7
fix(onboard): apply Jetson policy to default OpenClaw
sandl99 Aug 6, 2026
deee7ef
fix(onboard): preserve Jetson device groups
sandl99 Aug 6, 2026
a6bf7b5
fix(onboard): stage Jetson group bootstrap
sandl99 Aug 6, 2026
8255118
chore(onboard): add Jetson GPU diagnostic
sandl99 Aug 6, 2026
7fad34a
chore(onboard): add Jetson device-group proof
sandl99 Aug 6, 2026
0a65abe
fix(onboard): preserve Jetson groups in compatibility path
sandl99 Aug 6, 2026
4f65114
Merge remote-tracking branch 'origin/main' into fix/7610-pr-revert
sandl99 Aug 6, 2026
d6cfa87
refactor(onboard): carry agent scope with patch options
sandl99 Aug 6, 2026
03529c5
fix(images): audit Jetson wrapper metadata check
sandl99 Aug 6, 2026
7757675
chore(onboard): add cuInit boundary diagnostic
sandl99 Aug 6, 2026
f0a6720
chore(onboard): prove OpenRM policy boundary
sandl99 Aug 6, 2026
1964870
fix(onboard): make OpenRM proof non-interactive
sandl99 Aug 6, 2026
2b4a89a
fix(onboard): run OpenRM proof before rollback
sandl99 Aug 6, 2026
f06f365
fix(onboard): resolve Jetson review findings
apurvvkumaria Aug 6, 2026
b319e3f
Merge remote-tracking branch 'origin/main' into codex/pr8099-feedback…
apurvvkumaria Aug 6, 2026
cd42a42
test(images): cover Jetson helper permissions
apurvvkumaria Aug 6, 2026
0a0bc13
fix(onboard): recreate sandbox for OpenRM proof
sandl99 Aug 6, 2026
6e1cb76
fix(onboard): honor explicit sandbox recreation
sandl99 Aug 6, 2026
5a0fa51
chore(onboard): expand Jetson OpenRM proof
sandl99 Aug 6, 2026
d9cbe35
docs(onboard): describe OpenRM proof matrix
sandl99 Aug 6, 2026
f25c2b5
fix(onboard): validate Jetson groups before mutation
apurvvkumaria Aug 6, 2026
7b70ce2
fix(onboard): fail closed on OpenRM restore
apurvvkumaria Aug 6, 2026
e0e1e9c
Merge branch 'main' into fix/jetson-nvmap-nonroot
sandl99 Aug 7, 2026
8fde81f
test(onboard): make Jetson policy proof standalone
sandl99 Aug 7, 2026
72dd2f3
test(onboard): enable standalone OpenRM matrix
sandl99 Aug 7, 2026
21f21ee
test(onboard): cover invalid group database
apurvvkumaria Aug 7, 2026
ceff582
test(onboard): isolate Jetson process confinement
sandl99 Aug 7, 2026
3c81134
test(onboard): isolate remaining Jetson boundary
sandl99 Aug 7, 2026
60339b6
fix(onboard): allow Jetson NvSci IPC device
sandl99 Aug 7, 2026
250b7b4
fix(onboard): remove unproven NvSci policy
sandl99 Aug 7, 2026
f26b486
merge: resolve conflicts with main
github-actions[bot] Aug 8, 2026
65adec8
Merge branch 'main' into fix/jetson-nvmap-nonroot
apurvvkumaria Aug 10, 2026
a8ca0a0
Merge branch 'main' into fix/jetson-nvmap-nonroot
sandl99 Aug 11, 2026
1c723ba
test(onboard): expand Jetson CUDA diagnostics
sandl99 Aug 11, 2026
5e2459a
Revert "test(onboard): expand Jetson CUDA diagnostics"
sandl99 Aug 11, 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
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@
COPY scripts/lib/entrypoint-env-wrapper.sh /usr/local/lib/nemoclaw/entrypoint-env-wrapper.sh
COPY scripts/lib/gateway-supervisor.sh /usr/local/lib/nemoclaw/gateway-supervisor.sh
COPY scripts/lib/sandbox-rlimits.sh /usr/local/lib/nemoclaw/sandbox-rlimits.sh
COPY scripts/jetson-device-group-bootstrap.sh /usr/local/lib/nemoclaw/jetson-device-group-bootstrap.sh
COPY scripts/lib/openclaw_device_approval_policy.py /usr/local/lib/nemoclaw/openclaw_device_approval_policy.py
COPY scripts/lib/clean_runtime_shell_env_shim.py /usr/local/lib/nemoclaw/clean_runtime_shell_env_shim.py
COPY scripts/lib/normalize_mutable_config_perms.py /usr/local/lib/nemoclaw/normalize_mutable_config_perms.py
Expand Down Expand Up @@ -1553,12 +1554,14 @@
&& chmod -R a+rX /src/lib/messaging \
&& chown root:root /usr/local/bin/nemoclaw-gateway-control \
/usr/local/lib/nemoclaw/gateway-supervisor.sh \
/usr/local/lib/nemoclaw/jetson-device-group-bootstrap.sh \
/usr/local/lib/nemoclaw/state-dir-guard.py \
/usr/local/share/nemoclaw/state-lock-plan.json \
/usr/local/lib/nemoclaw/openclaw-config-guard.py \
/usr/local/lib/nemoclaw/managed-gateway-control.py \
&& chmod 700 /usr/local/bin/nemoclaw-gateway-control \
&& chmod 500 /usr/local/lib/nemoclaw/state-dir-guard.py \
&& chmod 500 /usr/local/lib/nemoclaw/jetson-device-group-bootstrap.sh \
/usr/local/lib/nemoclaw/state-dir-guard.py \
/usr/local/lib/nemoclaw/openclaw-config-guard.py \
/usr/local/lib/nemoclaw/managed-gateway-control.py \
&& chmod 444 /usr/local/share/nemoclaw/state-lock-plan.json \
Expand Down Expand Up @@ -1643,10 +1646,10 @@
# CHAT_UI_URL is a non-loopback address (Brev Launchable, remote deployments)
# since terminal-based pairing is impossible in those contexts.
# Default: "0" (device auth enabled for local deployments — secure by default).
ARG NEMOCLAW_DISABLE_DEVICE_AUTH=0

Check warning on line 1649 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1649 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
# Internal audit provenance for the opt-out above. Standard onboarding rewrites
# this to managed-onboard; direct image builders retain operator provenance.
ARG NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE=operator

Check warning on line 1652 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1652 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
# Compatibility build arg for older custom Dockerfiles and rebuild tooling.
# NemoClaw-managed images intentionally do not consume it; gateway auth tokens
# are generated at container startup and are never baked into image layers.
Expand Down Expand Up @@ -1680,7 +1683,7 @@
# NEMOCLAW_MESSAGING_PLAN_B64 intentionally remains ARG-only: Docker exposes it
# to build RUN processes without retaining the full plan in the final image env.
# Direct ARG interpolation into inline source is a code injection vector (C-2).
ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \

Check warning on line 1686 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1686 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1686 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1686 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
NEMOCLAW_INFERENCE_PROVIDER_ID=${NEMOCLAW_INFERENCE_PROVIDER_ID} \
NEMOCLAW_UPSTREAM_PROVIDER=${NEMOCLAW_UPSTREAM_PROVIDER} \
NEMOCLAW_PRIMARY_MODEL_REF=${NEMOCLAW_PRIMARY_MODEL_REF} \
Expand Down Expand Up @@ -2306,6 +2309,8 @@
&& check_metadata /usr/local/bin/nemoclaw-managed-bootstrap 'root:root:755' \
&& test ! -L /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \
&& check_metadata /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh 'root:root:444' \
&& test ! -L /usr/local/lib/nemoclaw/jetson-device-group-bootstrap.sh \
&& check_metadata /usr/local/lib/nemoclaw/jetson-device-group-bootstrap.sh 'root:root:500' \
&& check_metadata /usr/local/bin/nemoclaw-gateway-control 'root:root:700' \
&& check_metadata /usr/local/lib/nemoclaw/state-dir-guard.py 'root:root:500' \
&& check_metadata /usr/local/share/nemoclaw/state-lock-plan.json 'root:root:444' \
Expand Down
4 changes: 4 additions & 0 deletions ci/env-var-doc-allowlist.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
[
{
"name": "NEMOCLAW_DIAGNOSE_JETSON_OPENRM_POLICY",
"reason": "Temporary maintainer-only Jetson hardware A/B for issue #7610; remove after the exact policy boundary is proven."
},
{
"name": "NEMOCLAW_DISABLE_AUTO_DISPATCH",
"reason": "Test harness sentinel set to '1' so test files can import src/nemoclaw.ts without triggering main(). Never user-set in production."
Expand Down
22 changes: 22 additions & 0 deletions docs/reference/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2832,12 +2832,34 @@ The path creates the sandbox and then recreates the OpenShell-managed Docker con
`NEMOCLAW_DOCKER_GPU_PATCH=0` is ignored because this runtime requires the compatibility patch for GPU passthrough, and onboarding logs a warning when it is set.
To skip GPU passthrough entirely, rerun with `--no-gpu` or set `NEMOCLAW_SANDBOX_GPU=0`.

<AgentOnly variant="openclaw">

#### Jetson and Tegra compatibility default

Automatic GPU onboarding uses the compatibility path directly; it does not make a native attempt first.
The path recreates the OpenShell-managed Docker container with NVIDIA GPU flags and propagates eligible host group IDs for the supported Jetson GPU device nodes.

For legacy OpenClaw Jetson compatibility recreation, Docker passes those group IDs with `--group-add`.
OpenShell 0.0.85 calls `initgroups()` from the unchanged container group database before it starts the sandbox account.
That call replaces the inherited supplementary groups, so `--group-add` alone does not preserve Jetson device access.
Before the fixed OpenShell supervisor starts, NemoClaw runs a bounded wrapper from the sandbox image as root.
The image owns the wrapper as `root:root` with mode `0500`.
The wrapper can hand off only to `/opt/openshell/bin/openshell-sandbox`.
The wrapper validates the complete supplied list for count, group-ID format, range, and duplicates before it changes the container account.
The wrapper adds only the validated Jetson device GIDs that onboarding detected to the existing sandbox account in `/etc/group`.
It verifies the resulting membership before handoff.
OpenShell then rebuilds the account's group list from the updated database, preserving access to the detected device nodes.
This wrapper runs only when legacy OpenClaw Jetson recreation preserves the fixed supervisor entrypoint.

The creation-time filesystem policy adds no Jetson entries unless `/dev/nvmap` is an existing, non-symlink character device.
When that condition is met, the policy adds `/opt/nvidia` as read-only.
It adds each existing, non-symlink character device on the eligible GPU path list as read-write.
Generic GPU and CPU-only policies do not receive these Jetson entries.

Use `NEMOCLAW_DOCKER_GPU_PATCH=0` only for troubleshooting because it bypasses that group propagation and CUDA may not initialize.

</AgentOnly>

#### Common compatibility-path recovery

After compatibility recreation starts, onboarding keeps the pre-patch container as a rollback backup until the replacement passes the Ready, direct GPU, and applicable local-inference checks.
Expand Down
4 changes: 4 additions & 0 deletions scripts/checks/openshell-policy-mutation-read.mts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export const MUTATION_READS: readonly AuditedPolicyReadFile[] = [
preservingBase("getPresetContentGatewayState/readPolicy"),
],
},
{
relativePath: "src/lib/onboard/diagnostics/jetson-openrm-proof.ts",
expectedReads: [preservingBase("maybeRunJetsonOpenRmPolicyProof")],
},
{
relativePath: "nemoclaw/src/blueprint/runner.ts",
expectedReads: [unclassifiedBase("actionApply")],
Expand Down
164 changes: 164 additions & 0 deletions scripts/diagnose-jetson-cuinit-boundary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

set -uo pipefail

sandbox_name="${1:-tm}"
if [[ ! "$sandbox_name" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then
printf 'Invalid sandbox name: %s\n' "$sandbox_name" >&2
exit 2
fi

container_ids=()
while IFS=$'\t' read -r container_id container_name; do
if [[ "$container_name" == "openshell-${sandbox_name}-"* ]] \
&& [[ "$container_name" != *-nemoclaw-gpu-backup-* ]]; then
container_ids+=("$container_id")
fi
done < <(docker ps --no-trunc --format '{{.ID}}\t{{.Names}}')

if ((${#container_ids[@]} != 1)); then
printf 'Expected one running non-backup Docker container for sandbox %s; found %d.\n' \
"$sandbox_name" "${#container_ids[@]}" >&2
docker ps --no-trunc --format 'ID={{.ID}} NAME={{.Names}} STATUS={{.Status}}' >&2
exit 1
fi

container_id="${container_ids[0]}"

cuda_probe='import ctypes
import glob
import os
import stat

print(f"identity=uid={os.getuid()} gid={os.getgid()} groups={os.getgroups()}")
for key in ("LD_LIBRARY_PATH", "NVIDIA_VISIBLE_DEVICES", "NVIDIA_DRIVER_CAPABILITIES"):
print("env_{}={}".format(key, os.environ.get(key, "")))

patterns = (
"/dev/nvidia*",
"/dev/nvhost-*",
"/dev/nvmap",
"/dev/nvgpu/igpu0/*",
"/dev/dri/renderD*",
)
paths = sorted({path for pattern in patterns for path in glob.glob(pattern)})
for path in paths:
try:
info = os.stat(path)
except Exception as error:
print(f"device_stat={path} {type(error).__name__}: {error}")
continue
if not stat.S_ISCHR(info.st_mode):
continue
access = []
for label, flags in (("r", os.O_RDONLY), ("rw", os.O_RDWR)):
try:
fd = os.open(path, flags)
os.close(fd)
access.append(f"{label}=ok")
except Exception as error:
error_number = getattr(error, "errno", "")
access.append(f"{label}={type(error).__name__}:{error_number}")
print(
f"device={path} mode={info.st_mode & 0o777:o} uid={info.st_uid} gid={info.st_gid} "
+ " ".join(access)
)

try:
cuda = ctypes.CDLL("libcuda.so.1")
except OSError as error:
print(f"libcuda_load=OSError: {error}")
raise SystemExit(11)

cuda.cuInit.argtypes = [ctypes.c_uint]
cuda.cuInit.restype = ctypes.c_int
result = cuda.cuInit(0)
print("libcuda_load=ok")
try:
names = sorted(
{
line.split()[-1]
for line in open("/proc/self/maps", encoding="utf-8")
if "libcuda.so" in line and line.split()[-1].startswith("/")
}
)
print(f"libcuda_maps={names}")
except Exception as error:
print(f"libcuda_maps={type(error).__name__}: {error}")

error_name = ctypes.c_char_p()
try:
cuda.cuGetErrorName.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)]
cuda.cuGetErrorName.restype = ctypes.c_int
name_result = cuda.cuGetErrorName(result, ctypes.byref(error_name))
decoded_name = error_name.value.decode() if name_result == 0 and error_name.value else "unknown"
except Exception as error:
decoded_name = f"unavailable:{type(error).__name__}"
print(f"cuInit(0)={result} name={decoded_name}")
raise SystemExit(0 if result == 0 else 10)'

run_probe() {
local label="$1"
shift
local output status
printf '\n=== %s ===\n' "$label"
if output="$("$@" 2>&1)"; then
status=0
else
status=$?
fi
printf '%s\nprobe_exit=%d\n' "$output" "$status"
probe_output="$output"
}

extract_cuinit() {
sed -n -E 's/^cuInit\(0\)=([0-9]+).*$/\1/p' <<<"$1" | tail -1
}

printf 'sandbox=%s\ncontainer=%s\n' "$sandbox_name" "$container_id"
printf 'This diagnostic is read-only. It does not create, restart, rename, or remove a sandbox or container.\n'
printf 'git_head=%s\n' "$(git rev-parse HEAD 2>/dev/null || printf unknown)"
printf 'openshell_version=%s\n' "$(openshell --version 2>&1 || printf unknown)"

printf '\n=== OpenShell policy paths ===\n'
openshell policy get --base "$sandbox_name" 2>&1 \
| grep -E 'read_only:|read_write:|/opt/nvidia|/dev/nv|/dev/dri' || true

printf '\n=== Active container configuration ===\n'
docker inspect --format \
'image={{.Image}} runtime={{.HostConfig.Runtime}} user={{json .Config.User}} group_add={{json .HostConfig.GroupAdd}} entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} devices={{json .HostConfig.Devices}} device_requests={{json .HostConfig.DeviceRequests}}' \
"$container_id" 2>&1 || true
docker exec --user 0 "$container_id" /usr/bin/id sandbox 2>&1 || true
docker exec --user 0 "$container_id" /usr/bin/stat -Lc \
'wrapper=type=%F mode=%a uid=%u gid=%g path=%n' \
/usr/local/lib/nemoclaw/jetson-device-group-bootstrap.sh 2>&1 || true

run_probe "Host account" python3 -c "$cuda_probe"
host_output="$probe_output"
run_probe "Direct Docker as root" docker exec --user 0 "$container_id" python3 -c "$cuda_probe"
docker_root_output="$probe_output"
run_probe "Direct Docker as sandbox" docker exec --user sandbox "$container_id" python3 -c "$cuda_probe"
docker_sandbox_output="$probe_output"
run_probe "OpenShell sandbox execution" openshell sandbox exec -n "$sandbox_name" -- python3 -c "$cuda_probe"
openshell_output="$probe_output"

host_cuinit="$(extract_cuinit "$host_output")"
docker_root_cuinit="$(extract_cuinit "$docker_root_output")"
docker_sandbox_cuinit="$(extract_cuinit "$docker_sandbox_output")"
openshell_cuinit="$(extract_cuinit "$openshell_output")"

printf '\n=== Boundary result ===\n'
printf 'host_cuInit=%s docker_root_cuInit=%s docker_sandbox_cuInit=%s openshell_cuInit=%s\n' \
"${host_cuinit:-missing}" "${docker_root_cuinit:-missing}" \
"${docker_sandbox_cuinit:-missing}" "${openshell_cuinit:-missing}"
if [[ "$docker_sandbox_cuinit" == "0" && "$openshell_cuinit" != "0" ]]; then
printf 'ISOLATED: CUDA works in the running container as sandbox but fails through OpenShell execution. Investigate the OpenShell filesystem/device policy boundary.\n'
elif [[ "$docker_root_cuinit" == "0" && "$docker_sandbox_cuinit" != "0" ]]; then
printf 'ISOLATED: CUDA works as root in the running container but fails as sandbox. Investigate identity, group, or device permission differences.\n'
elif [[ "$docker_root_cuinit" != "0" && "$docker_sandbox_cuinit" != "0" ]]; then
printf 'ISOLATED: CUDA already fails in direct Docker execution. Investigate the recreated container runtime, injected driver libraries, and device set before changing OpenShell policy.\n'
else
printf 'INCONCLUSIVE: preserve this output; the four boundaries did not produce a single failing transition.\n'
fi
60 changes: 60 additions & 0 deletions scripts/jetson-device-group-bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Compatibility bridge for #7610. Remove when the minimum supported OpenShell
# release natively preserves Jetson device groups across the sandbox-user handoff.

set -euo pipefail

fail() {
printf 'Jetson device-group bootstrap: %s\n' "$*" >&2
exit 1
}

[ "$(/usr/bin/id -u)" -eq 0 ] || fail "must run as root"
[ "${1:-}" = "--device-group-gids" ] || fail "device group argument is missing"
group_gids="${2:-}"
[ "${3:-}" = "--" ] || fail "supervisor delimiter is missing"
shift 3
[ "${1:-}" = "/opt/openshell/bin/openshell-sandbox" ] \
|| fail "OpenShell supervisor entrypoint is invalid"
/usr/bin/id sandbox >/dev/null 2>&1 || fail "sandbox user is missing"
if [ ! -f /etc/group ] || [ -L /etc/group ]; then
fail "container group database is invalid"
fi

IFS=',' read -r -a gids <<<"$group_gids"
if [ "${#gids[@]}" -eq 0 ] || [ "${#gids[@]}" -gt 16 ]; then
fail "device group count is invalid"
fi

declare -A seen=()
for gid in "${gids[@]}"; do
[[ "$gid" =~ ^[1-9][0-9]{0,9}$ ]] || fail "device group ID is invalid"
[ "$gid" -le 2147483647 ] || fail "device group ID is out of range"
[ -z "${seen[$gid]:-}" ] || fail "device group ID is duplicated"
seen[$gid]=1
done

for gid in "${gids[@]}"; do
group_record="$(/usr/bin/getent group "$gid" || true)"
if [ -z "$group_record" ]; then
group_name="nemoclaw_gpu_$gid"
/usr/sbin/groupadd --gid "$gid" "$group_name"
else
IFS=':' read -r group_name _ resolved_gid _ <<<"$group_record"
if [ -z "$group_name" ] || [ "$resolved_gid" != "$gid" ]; then
fail "device group record is invalid"
fi
fi
/usr/sbin/usermod --append --groups "$group_name" sandbox
done

sandbox_groups=" $(/usr/bin/id -G sandbox) "
for gid in "${gids[@]}"; do
[[ "$sandbox_groups" == *" $gid "* ]] \
|| fail "sandbox membership verification failed"
done

exec "$@"
35 changes: 35 additions & 0 deletions scripts/prove-jetson-openrm-policy-boundary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

sandbox_name="${1:-tm}"
if [[ ! "$sandbox_name" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then
printf 'Invalid sandbox name: %s\n' "$sandbox_name" >&2
exit 2
fi

printf 'Running the standalone Jetson OpenRM policy proof for sandbox %s.\n' "$sandbox_name"
printf 'This bypasses onboarding and its resume checkpoints.\n'
printf 'The current container is preserved as a rollback backup before the production recreation and policy matrix run.\n'
printf 'The matrix briefly widens only the replacement sandbox policy, restores the baseline policy, then restores the original container.\n'

npm run build:cli
exec node - "$sandbox_name" <<'NODE'
const {
createDockerGpuDiagnosticRedactor,
} = require("./dist/lib/onboard/docker-gpu-diagnostic-redaction");
const {
runStandaloneJetsonOpenRmPolicyProof,
} = require("./dist/lib/onboard/diagnostics/jetson-openrm-standalone");

runStandaloneJetsonOpenRmPolicyProof(process.argv[2]).catch((error) => {
const message = error instanceof Error ? error.message : String(error);
const redacted = createDockerGpuDiagnosticRedactor()
.redactText(message)
.replace(/[\r\n]+/gu, " ");
console.error(`Error: ${redacted}`);
process.exitCode = 1;
});
NODE
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const GPU_IMAGE_ID = `sha256:${"a".repeat(64)}`;
export function createGpuFlowInput(): SandboxGpuCreateFlowInput {
return {
sandboxName: "alpha",
agentName: "openclaw",
provider: "nim",
sandboxGpuConfig: {
mode: "1",
Expand Down
32 changes: 32 additions & 0 deletions src/lib/onboard/diagnostics/jetson-openrm-namespace-proof.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";
import { runJetsonOpenRmNamespaceProof } from "./jetson-openrm-namespace-proof";

describe("Jetson OpenRM namespace proof", () => {
it("isolates the OpenShell network namespace from direct Docker execution (#7610)", () => {
const dockerRun = vi.fn((args: readonly string[]) => ({
status: 0,
stdout: `cuInit(0)=${args.at(-1) === "net-namespace" ? "801" : "0"}`,
}));
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);

runJetsonOpenRmNamespaceProof("a".repeat(64), dockerRun);

expect(dockerRun).toHaveBeenCalledTimes(8);
expect(log).toHaveBeenCalledWith(expect.stringContaining("net-namespace"));
});

it("reports missing namespace probes with their exact mode (#7610)", () => {
const dockerRun = vi.fn(() => ({ status: 1, stderr: "setns denied" }));
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);

runJetsonOpenRmNamespaceProof("b".repeat(64), dockerRun);

expect(error).toHaveBeenCalledWith(
expect.stringContaining("namespace_case_error[net-namespace]"),
);
expect(error).toHaveBeenCalledWith(expect.stringContaining("baseline did not pass"));
});
});
Loading
Loading