Skip to content

Commit 0cfaf8f

Browse files
authored
fix(sandbox): add host-mediated gateway restart (#5874)
<!-- markdownlint-disable MD041 --> ## Summary Adds a supported host-mediated `gateway restart` command for NemoClaw-managed OpenClaw and Hermes gateways and routes automatic recovery through the same topology-specific authenticated controller or supervisor path. The lifecycle acts only on proven process and listener identity, applies configuration and state changes transactionally, and fails closed when privileged execution or built-in health probes are ambiguous. ## Related Issue Fixes #2426 Fixes #5253 Supersedes #5416 ## Changes - Added `sandbox:gateway:restart` plus public `nemoclaw <name> gateway restart` and `nemohermes <name> gateway restart` routing while keeping healthy `recover` idempotent. - Added managed gateway control for both supported topologies: a root PID 1 request channel for direct-root entrypoints and an authenticated root-owned controller paired with the nonroot supervisor in OpenShell-managed sandboxes. - Enforced exact PID and process-start identity checks, listener ownership validation, post-stop absence proof, exact reaping or respawn proof, health waits, and port-forward recovery. - Added descriptor-based, no-follow, atomic configuration and state guards, including bounded Hermes secret-boundary and configuration-hash validation. - Serialized shields mutations and recovery timers, and ordered snapshot, destroy, and inference transitions so stale callbacks cannot reapply state. - Made built-in OpenClaw and Hermes probes fail closed after trusted execution failure or timeout; unsupported privileged-exec drivers and ambiguous container selection are rejected while explicit custom gateway agents retain their compatibility path. - Wired runtime helpers into optimized build contexts with root-owned, non-writable permissions and preserved root PID 1 access to mutable sandbox-group state when hardened runtimes drop `CAP_DAC_OVERRIDE`. - Published a Trusted Computing Base page in both guide variants covering the direct-root and OpenShell-managed boundaries, shared-UID and mutable-config limits, JSON5 read compatibility, root group membership, and compatibility-removal conditions. - Restricted managed-controller procfs and filesystem overrides to source execution with the explicit test flag, and verified the pinned Node/JSON5 installation is root-owned and non-writable. - Added final-image proof for both built-in images covering root-only helper modes, required supplementary groups, root probe access, and sandbox-user refusal; made sandbox-operations assertions topology-aware for direct-root and OpenShell-managed runs. - Excluded authenticated, exact-identity Hermes controller replacements from crash quarantine with a root-only `0600` lifecycle lock and root-owned `0444` authorization bound to the live root controller; all orphaned, mismatched, unexpected, and failed replacement exits still count. - Tightened the OpenClaw image health fallback to read the tracked PID start identity before and after an exact installed gateway command line, while retaining known rewritten process-title compatibility. - Preserved the injected managed supervisor through the post-restart settle probe and locked the built-in completion protocol to `GATEWAY_PID=` rather than accepting the legacy custom-script `ALREADY_RUNNING` marker. - Migrated the branch's live recovery coverage onto the typed E2E workflow and removed the legacy shell lanes deleted on `main`. - Updated lifecycle, runtime-control, command, troubleshooting, security, and Hermes documentation. - Review scope: the existing OpenShell bridge-host allowance is preserved while adjacent private or internal endpoint shapes are DNS-validated and rejected. Broad module splitting and consumer-wide HTTPS binding remain separate architecture work rather than widening this security-sensitive recovery fix. ## Type of Change - [ ] Code change (feature, bug fix, or refactor) - [x] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Quality Gates - [x] Tests added or updated for changed behavior - [ ] Existing tests cover changed behavior — justification: - [ ] Tests not applicable — justification: - [x] Docs updated for user-facing behavior changes - [ ] Docs not applicable — justification: - [x] Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging) - [ ] Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: Independent security review found no blocking findings across privilege, process identity, filesystem, network, fail-closed, and regression-coverage boundaries; human security acceptance remains pending and required before merge. - [ ] Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue: ## Verification - [x] PR description includes the DCO sign-off declaration and every commit appears as `Verified` in GitHub - [ ] Git hooks passed during commit and push, or `npx prek run --from-ref main --to-ref HEAD` passes - [x] Targeted tests pass for changed behavior - [ ] Full `npm test` passes (broad runtime changes only) - [x] Quality Gates section completed with required justifications or waivers - [x] No secrets, API keys, or credentials committed - [ ] `npm run docs` builds without warnings (doc changes only) - [x] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [x] New doc pages include SPDX header and frontmatter (new pages only) Exact-head evidence for `8dbe420d3d835546683cbfa67fec0537791ad7c1`: - Signed, DCO-compliant commit `d86c60c325a3dbc74fc42f74447bc00f89be4400` addresses the current CodeRabbit correctness and documentation findings. Signed merge commit `c9061c68fd82b1b73044383bc4e8962808644744` incorporates `main` at `e10462ff3e2e0727350a2532fc7bb7edc64116b2` without rebasing or rewriting the verified history. Signed cleanup commit `8dbe420d3d835546683cbfa67fec0537791ad7c1` addresses the resulting CodeQL/code-quality diagnostics. - Post-merge controller, trust-contract, provisioning, managed-exit authorization, OpenClaw/Hermes config-guard, state-dir guard, endpoint-security, restart/boundary, and workflow-boundary suites pass: 15 files, 258 tests. - `npm run build:cli`, `npm run typecheck:cli`, Biome, ShellCheck, Python compilation, branch diff checks, test-size/project/title guards, and `npm run docs:check-agent-variants` pass. - `npm run docs` completes with 0 errors and two existing Fern warnings; the warning-free checkbox remains unchecked. - All 80 PR commits appear as GitHub `Verified`; all three new commits contain the DCO sign-off declaration. - The review-fix commit's normal formatting, lint, repository, secret-scan, commitlint, source-shape, and test-size hooks pass. The normal pre-push CLI typecheck and version-sync hooks pass. The full local `test-cli` hook and full-`npm test` boxes remain unchecked; exact-head CI is required for those broader platform lanes. - Standard CI, automated current-head review, human security acceptance, and exact-head E2E evidence are pending on this head and must settle before merge. --- Signed-off-by: Aaron Erickson <aerickson@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a `gateway restart` command for sandboxes with forced reload and post-restart health/forward verification. * **Bug Fixes** * Improved recovery so host forwards and supervised gateway processes are repaired more reliably. * Strengthened failure handling to be fail-closed when required Hermes secret-boundary validation or supervisor control is unavailable. * Corrected forward-health classification so stopped/occupied forwards are handled consistently. * **Documentation** * Expanded lifecycle, runtime-controls, command reference, and troubleshooting guidance for `recover`, `gateway restart`, shields windows, and Hermes recovery constraints. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
1 parent 539d0dd commit 0cfaf8f

160 files changed

Lines changed: 41114 additions & 6171 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Dockerfile

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,13 @@ ENV NPM_CONFIG_AUDIT=false \
9797
NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=20000 \
9898
NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=120000 \
9999
NPM_CONFIG_FETCH_TIMEOUT=300000
100-
RUN npm ci --omit=dev
100+
RUN npm ci --omit=dev \
101+
&& test -f /usr/local/bin/node \
102+
&& test -d /opt/nemoclaw/node_modules/json5 \
103+
&& node_unsafe="$(find -L /usr/local/bin/node -maxdepth 0 \( ! -user root -o -perm /022 \) -print -quit)" \
104+
&& test -z "$node_unsafe" \
105+
&& json5_unsafe="$(find -L /opt/nemoclaw/node_modules/json5 \( ! -user root -o -perm /022 \) -print -quit)" \
106+
&& test -z "$json5_unsafe"
101107
COPY scripts/patch-openclaw-tool-catalog.js /usr/local/lib/nemoclaw/patch-openclaw-tool-catalog.js
102108
COPY scripts/patch-openclaw-chat-send.js /usr/local/lib/nemoclaw/patch-openclaw-chat-send.js
103109
RUN chmod 755 /usr/local/lib/nemoclaw/patch-openclaw-tool-catalog.js \
@@ -529,10 +535,15 @@ RUN mkdir -p /sandbox/.nemoclaw/blueprints/0.1.0 \
529535

530536
# Copy startup script and shared sandbox initialisation library
531537
COPY scripts/lib/sandbox-init.sh /usr/local/lib/nemoclaw/sandbox-init.sh
538+
COPY scripts/lib/gateway-supervisor.sh /usr/local/lib/nemoclaw/gateway-supervisor.sh
532539
COPY scripts/lib/sandbox-rlimits.sh /usr/local/lib/nemoclaw/sandbox-rlimits.sh
533540
COPY scripts/lib/openclaw_device_approval_policy.py /usr/local/lib/nemoclaw/openclaw_device_approval_policy.py
534541
COPY scripts/lib/clean_runtime_shell_env_shim.py /usr/local/lib/nemoclaw/clean_runtime_shell_env_shim.py
542+
COPY scripts/state-dir-guard.py /usr/local/lib/nemoclaw/state-dir-guard.py
543+
COPY scripts/openclaw-config-guard.py /usr/local/lib/nemoclaw/openclaw-config-guard.py
544+
COPY scripts/managed-gateway-control.py /usr/local/lib/nemoclaw/managed-gateway-control.py
535545
COPY scripts/nemoclaw-start.sh /usr/local/bin/nemoclaw-start
546+
COPY scripts/gateway-control.sh /usr/local/bin/nemoclaw-gateway-control
536547
# Copy NODE_OPTIONS preload modules to a Landlock-accessible path. OpenShell ≥0.0.36
537548
# blocks /opt/nemoclaw-blueprint/ from non-root users, but the entrypoint
538549
# needs to read these files to install Node runtime preloads under /tmp.
@@ -549,7 +560,17 @@ RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/bin/nemoclaw-codex-acp \
549560
/scripts/generate-openclaw-config.mts \
550561
/src/lib/messaging/applier/build/messaging-build-applier.mts \
551562
&& chmod -R a+rX /src/lib/messaging \
552-
&& chmod 444 /usr/local/lib/nemoclaw/sandbox-rlimits.sh \
563+
&& chown root:root /usr/local/bin/nemoclaw-gateway-control \
564+
/usr/local/lib/nemoclaw/gateway-supervisor.sh \
565+
/usr/local/lib/nemoclaw/state-dir-guard.py \
566+
/usr/local/lib/nemoclaw/openclaw-config-guard.py \
567+
/usr/local/lib/nemoclaw/managed-gateway-control.py \
568+
&& chmod 700 /usr/local/bin/nemoclaw-gateway-control \
569+
&& chmod 500 /usr/local/lib/nemoclaw/state-dir-guard.py \
570+
/usr/local/lib/nemoclaw/openclaw-config-guard.py \
571+
/usr/local/lib/nemoclaw/managed-gateway-control.py \
572+
&& chmod 444 /usr/local/lib/nemoclaw/gateway-supervisor.sh \
573+
/usr/local/lib/nemoclaw/sandbox-rlimits.sh \
553574
&& chmod 644 /usr/local/lib/nemoclaw/openclaw_device_approval_policy.py \
554575
/usr/local/lib/nemoclaw/clean_runtime_shell_env_shim.py \
555576
&& if [ -d /usr/local/lib/nemoclaw/preloads-compiled-channels ]; then \
@@ -907,15 +928,24 @@ RUN set -eu; \
907928
done; \
908929
rm -rf /root/.npm /sandbox/.npm
909930

910-
# Stale-base fallback for the gateway-in-sandbox-group setup (#2681).
911-
# Newer base images already add the gateway user to the sandbox group, but
912-
# the derived image must remain build-clean against older sandbox-base:latest
913-
# tags too. The `id -nG` check makes this idempotent.
931+
# Stale-base fallback for the gateway/root-in-sandbox-group setup (#2681).
932+
# Newer base images already add both users to the sandbox group, but the
933+
# derived image must remain build-clean against older sandbox-base:latest
934+
# tags too. Root membership preserves PID 1 access when CAP_DAC_OVERRIDE is
935+
# dropped. The `id -nG` checks make this idempotent. Remove this block after
936+
# the minimum supported OpenClaw sandbox base tag is v0.0.71 or newer and
937+
# Dockerfile.base guarantees both memberships; keep that base contract covered
938+
# by test/sandbox-provisioning.test.ts.
914939
# hadolint ignore=DL4006
915940
RUN if id gateway >/dev/null 2>&1 && id sandbox >/dev/null 2>&1; then \
916941
if ! id -nG gateway | tr ' ' '\n' | grep -qx sandbox; then \
917942
usermod -aG sandbox gateway; \
918943
fi; \
944+
fi \
945+
&& if id root >/dev/null 2>&1 && id sandbox >/dev/null 2>&1; then \
946+
if ! id -nG root | tr ' ' '\n' | grep -qx sandbox; then \
947+
usermod -aG sandbox root; \
948+
fi; \
919949
fi
920950

921951
# Keep the image readable to the root entrypoint after capabilities are dropped.
@@ -1087,32 +1117,12 @@ RUN set -eu; \
10871117
# host-side delivery-chain monitoring (verify-deployment.ts, host
10881118
# port forward, sandbox status).
10891119
#
1090-
# The pgrep pattern matches both `openclaw gateway run` (the launcher
1091-
# command nemoclaw-start runs) and `openclaw-gateway` (the older re-execed
1092-
# binary form). Recent OpenClaw (v0.0.44 / 2026.5.18+) re-execs the
1093-
# long-running gateway into a process whose argv is plain `openclaw` with
1094-
# no `gateway` token at all (#4952), which that pattern cannot see — so on a
1095-
# marker-present container whose in-container curl probe failed, the stale
1096-
# pattern reported a live gateway as permanently unhealthy.
1097-
#
1098-
# When the pattern misses, fall back to the gateway PID that nemoclaw-start
1099-
# recorded in /tmp/nemoclaw-gateway.pid (record_gateway_pid, written for both
1100-
# the root and non-root launch paths and refreshed on every respawn) and
1101-
# confirm THAT pid is still a live `openclaw` process. This deliberately does
1102-
# not match any process merely named `openclaw`: a bare `pgrep -x openclaw`
1103-
# would keep Docker healthy when the real gateway has died but an unrelated
1104-
# `openclaw` one-shot (e.g. `openclaw agent ...`) happens to be running,
1105-
# defeating restart/self-healing. The recorded-pid check is gateway-specific
1106-
# and survives PID reuse via the comm prefix guard. `ps -o comm=` reads the
1107-
# (15-char) process name, which is `openclaw` for the re-execed gateway and
1108-
# `openclaw-gatewa(y)` for the legacy form — both match `openclaw*`.
1109-
#
1110-
# pgrep uses --ignore-ancestors so it cannot self-match the healthcheck
1111-
# shell that Docker spawns to run this CMD — that shell's argv contains
1112-
# the literal 'openclaw gateway' string we're searching for, and
1113-
# without --ignore-ancestors `pgrep -f` would happily report it as the
1114-
# live gateway even after the real process exited (procps 4.0+ supports
1115-
# this flag; the base image pins procps to 2:4.0.4-9).
1120+
# nemoclaw-start records `pid starttime` for the exact gateway process in
1121+
# /tmp/nemoclaw-gateway.pid on every launch. When curl sees connection
1122+
# refused, validate both values against `/proc/<pid>/stat` field 22 before
1123+
# accepting the exact OpenClaw gateway cmdline fallback. A numeric PID or
1124+
# OpenClaw-looking argv alone is insufficient because either can belong to a
1125+
# recycled process.
11161126
HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \
11171127
CMD port="${NEMOCLAW_DASHBOARD_PORT:-${OPENCLAW_GATEWAY_PORT:-}}"; \
11181128
if [ -z "$port" ]; then \
@@ -1123,11 +1133,12 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \
11231133
if [ "$rc" = 0 ]; then exit 0; fi; \
11241134
if [ "$rc" != 7 ]; then exit 1; fi; \
11251135
[ -f /tmp/nemoclaw-gateway-local ] || exit 0; \
1126-
if ! pgrep --ignore-ancestors -f 'openclaw[ -]gateway' > /dev/null 2>&1; then \
1127-
gwpid="$(cat /tmp/nemoclaw-gateway.pid 2>/dev/null)"; \
1128-
case "${gwpid:-x}" in *[!0-9]*) exit 1 ;; esac; \
1129-
case "$(ps -p "$gwpid" -o comm= 2>/dev/null)" in openclaw*) ;; *) exit 1 ;; esac; \
1130-
fi; \
1136+
gwpid=; gwstart=; gwextra=; \
1137+
IFS=' ' read -r gwpid gwstart gwextra </tmp/nemoclaw-gateway.pid 2>/dev/null || exit 1; \
1138+
case "${gwpid:-x}" in *[!0-9]*) exit 1 ;; esac; \
1139+
case "${gwstart:-x}" in *[!0-9]*) exit 1 ;; esac; \
1140+
[ -z "$gwextra" ] || exit 1; \
1141+
python3 -c 'import pathlib, sys; proc = pathlib.Path(sys.argv[1]); expected = sys.argv[2].encode("ascii"); port = sys.argv[3].encode(); parse = lambda data: (lambda fields: (fields[0], fields[19]))(data.rsplit(b") ", 1)[1].split()); before = parse((proc / "stat").read_bytes()); raw = (proc / "cmdline").read_bytes(); after = parse((proc / "stat").read_bytes()); trimmed = raw.rstrip(b"\0"); padding = len(raw) - len(trimmed); title = padding >= 1 and trimmed in (b"openclaw", b"openclaw-gateway"); argv = raw[:-1].split(b"\0") if padding == 1 else []; interpreters = (b"node", b"nodejs", b"/usr/local/bin/node", b"/usr/local/bin/nodejs", b"/usr/bin/node", b"/usr/bin/nodejs"); launchers = (b"/usr/local/bin/openclaw", b"/usr/local/lib/node_modules/openclaw/openclaw.mjs"); index = 1 if argv and argv[0] in interpreters else 0; command = index < len(argv) and argv[index] in launchers and argv[index + 1:] in ([b"gateway", b"run", b"--port", port], [b"gateway", b"run", b"--port=" + port]); identity = before[1] == expected == after[1] and before[0] != b"Z" and after[0] != b"Z"; raise SystemExit(not (identity and (title or command)))' "/proc/$gwpid" "$gwstart" "$port" 2>/dev/null || exit 1; \
11311142
[ -s /tmp/gateway.log ]
11321143

11331144
# Entrypoint runs as root to start the gateway as the gateway user,

Dockerfile.base

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,16 @@ RUN arch="$(dpkg --print-architecture)" \
9797
#
9898
# `gateway` is also a member of the `sandbox` group so both users can write
9999
# to the mutable-default OpenClaw config tree (chmod g+w + setgid below).
100+
# Keep root in that group too: hardened runtimes may drop CAP_DAC_OVERRIDE,
101+
# but the root PID 1 lifecycle guard still needs descriptor-safe access to the
102+
# sandbox-owned mutable tree before it can validate or transition it.
100103
# This replaces the previous EACCES-swallow approach for control-UI config
101104
# mutations — see #2681. UIDs stay distinct (security separation preserved);
102105
# the shared group only governs the mutable-default state directory.
103106
RUN groupadd -r gateway && useradd -r -g gateway -d /sandbox -s /usr/sbin/nologin gateway \
104107
&& groupadd -r sandbox && useradd -r -g sandbox -d /sandbox -s /bin/bash sandbox \
105108
&& usermod -aG sandbox gateway \
109+
&& usermod -aG sandbox root \
106110
&& mkdir -p /sandbox/.nemoclaw \
107111
&& chown -R sandbox:sandbox /sandbox
108112

agents/hermes/Dockerfile

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,35 @@ COPY nemoclaw-blueprint/ /opt/nemoclaw-blueprint/
107107
RUN chmod -R a+rX /opt/nemoclaw-blueprint/
108108

109109
# Copy startup script and the secret-boundary validator. The validator is the
110-
# single source of truth shared between start.sh (cold start) and the host-side
111-
# gateway recovery script (src/lib/agent/runtime.ts) so `sandbox recover` enforces
112-
# the same boundary as the initial entrypoint.
110+
# single source of truth used by start.sh at cold start and by its root-owned
111+
# PID 1 lifecycle handler, so `recover` enforces the same boundary as startup.
113112
COPY scripts/lib/sandbox-init.sh /usr/local/lib/nemoclaw/sandbox-init.sh
113+
COPY scripts/lib/gateway-supervisor.sh /usr/local/lib/nemoclaw/gateway-supervisor.sh
114114
COPY scripts/lib/sandbox-rlimits.sh /usr/local/lib/nemoclaw/sandbox-rlimits.sh
115115
COPY agents/hermes/start.sh /usr/local/bin/nemoclaw-start
116+
COPY scripts/gateway-control.sh /usr/local/bin/nemoclaw-gateway-control
117+
COPY scripts/managed-gateway-control.py /usr/local/lib/nemoclaw/managed-gateway-control.py
116118
COPY agents/hermes/validate-env-secret-boundary.py /usr/local/lib/nemoclaw/validate-hermes-env-secret-boundary.py
117119
COPY agents/hermes/seed-dashboard-config.py /usr/local/lib/nemoclaw/seed-hermes-dashboard-config.py
118120
COPY agents/hermes/runtime-config-guard.py /usr/local/lib/nemoclaw/hermes-runtime-config-guard.py
121+
COPY scripts/state-dir-guard.py /usr/local/lib/nemoclaw/state-dir-guard.py
122+
COPY nemoclaw-blueprint/scripts/*.js /usr/local/lib/nemoclaw/preloads/
119123
# Dockerfile.base is the source of truth for rlimit hooks. This Hermes replay
120124
# only repairs stale bases predating the v0.0.69 base layer, which may lack the
121125
# profile hook, bashrc hook, or root-owned helper mode. Remove it once the
122126
# minimum supported Hermes sandbox base tag guarantees those artifacts and
123127
# test/sandbox-rlimit-hooks.test.ts covers that base.
124128
RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/lib/nemoclaw/sandbox-init.sh /usr/local/lib/nemoclaw/validate-hermes-env-secret-boundary.py /usr/local/lib/nemoclaw/seed-hermes-dashboard-config.py /usr/local/lib/nemoclaw/hermes-runtime-config-guard.py \
129+
&& chown root:root /usr/local/bin/nemoclaw-gateway-control /usr/local/lib/nemoclaw/gateway-supervisor.sh /usr/local/lib/nemoclaw/state-dir-guard.py /usr/local/lib/nemoclaw/managed-gateway-control.py \
130+
&& chmod 700 /usr/local/bin/nemoclaw-gateway-control \
131+
&& chmod 500 /usr/local/lib/nemoclaw/state-dir-guard.py /usr/local/lib/nemoclaw/managed-gateway-control.py \
132+
&& chmod 444 /usr/local/lib/nemoclaw/gateway-supervisor.sh \
133+
&& if [ -d /usr/local/lib/nemoclaw/preloads ]; then \
134+
chown -R 0:0 /usr/local/lib/nemoclaw/preloads \
135+
&& find /usr/local/lib/nemoclaw/preloads -type f -exec chmod 444 {} + \
136+
&& find /usr/local/lib/nemoclaw/preloads -type d -exec chmod 755 {} + \
137+
; \
138+
fi \
125139
&& chmod 444 /usr/local/lib/nemoclaw/sandbox-rlimits.sh \
126140
&& mkdir -p /etc/profile.d \
127141
&& printf '%s\n' \
@@ -434,7 +448,7 @@ RUN mkdir -p /etc/nemoclaw \
434448
# Backward-compatible marker for host-side shields logic on older sandboxes.
435449
RUN sha256sum /sandbox/.hermes/config.yaml /sandbox/.hermes/.env \
436450
> /sandbox/.hermes/.config-hash \
437-
&& chmod 600 /sandbox/.hermes/.config-hash \
451+
&& chmod 640 /sandbox/.hermes/.config-hash \
438452
&& chown sandbox:sandbox /sandbox/.hermes/.config-hash
439453

440454
# OpenShell's macOS VM backend currently remaps extracted rootfs ownership to

0 commit comments

Comments
 (0)