Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
92 changes: 92 additions & 0 deletions .github/workflows/nightly-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
# agent state, and the same agent type running.
# hermes-e2e Hermes Agent E2E — install → onboard --agent hermes → health
# probe → live inference. Validates the multi-agent architecture.
# openclaw-onboard-security-posture-e2e
# Full OpenClaw onboard on a non-root host user with
# post-drop capability and trusted rc-file assertions.
# hermes-onboard-security-posture-e2e
# Full Hermes onboard on a non-root host user with
# post-drop capability and trusted rc-file assertions.
# hermes-inference-switch-e2e
# Switches a running Hermes sandbox with `nemohermes inference set`
# and verifies route, config.yaml, hashes, and live requests.
Expand Down Expand Up @@ -79,6 +85,8 @@ on:
token-rotation-e2e, sandbox-survival-e2e,
openshell-gateway-upgrade-e2e,
issue-2478-crash-loop-recovery-e2e, hermes-e2e,
openclaw-onboard-security-posture-e2e,
hermes-onboard-security-posture-e2e,
hermes-inference-switch-e2e, hermes-discord-e2e,
hermes-slack-e2e, sandbox-operations-e2e, inference-routing-e2e,
openclaw-inference-switch-e2e,
Expand Down Expand Up @@ -781,6 +789,84 @@ jobs:
path: /tmp/nemoclaw-e2e-hermes-install.log
if-no-files-found: ignore

# ── OpenClaw onboard security posture E2E ───────────────────────
# Full OpenClaw install/onboard/inference path, then asserts the Linux
# Docker-driver security posture that #3891 needed: non-root host user,
# post-drop PID 1 bounding caps without CAP_DAC_OVERRIDE, static root-owned
# rc shims, and dynamic configure guards only in /tmp/nemoclaw-proxy-env.sh.
openclaw-onboard-security-posture-e2e:
if: >-
github.repository == 'NVIDIA/NemoClaw' &&
(github.event_name != 'workflow_dispatch' ||
inputs.jobs == '' ||
contains(format(',{0},', inputs.jobs), ',openclaw-onboard-security-posture-e2e,'))
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.target_ref || github.ref }}

- name: Run OpenClaw onboard security posture E2E test
env:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-openclaw-security-posture"
NEMOCLAW_RECREATE_SANDBOX: "1"
NEMOCLAW_E2E_SECURITY_POSTURE: "1"
NEMOCLAW_E2E_EXPECT_NON_ROOT_HOST: "1"
GITHUB_TOKEN: ${{ github.token }}
run: bash test/e2e/test-full-e2e.sh

- name: Upload install log on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: openclaw-onboard-security-posture-install-log
path: /tmp/nemoclaw-e2e-install.log
if-no-files-found: ignore

# ── Hermes onboard security posture E2E ─────────────────────────
# Full Hermes install/onboard/health/inference path with the same posture
# assertions as OpenClaw. This specifically catches the #3891 class where
# startup rewrites /sandbox rc files after CAP_DAC_OVERRIDE is gone.
hermes-onboard-security-posture-e2e:
if: >-
github.repository == 'NVIDIA/NemoClaw' &&
(github.event_name != 'workflow_dispatch' ||
inputs.jobs == '' ||
contains(format(',{0},', inputs.jobs), ',hermes-onboard-security-posture-e2e,'))
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.target_ref || github.ref }}

- name: Run Hermes onboard security posture E2E test
env:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-hermes-security-posture"
NEMOCLAW_RECREATE_SANDBOX: "1"
NEMOCLAW_AGENT: "hermes"
NEMOCLAW_E2E_SECURITY_POSTURE: "1"
NEMOCLAW_E2E_EXPECT_NON_ROOT_HOST: "1"
GITHUB_TOKEN: ${{ github.token }}
run: bash test/e2e/test-hermes-e2e.sh

- name: Upload install log on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: hermes-onboard-security-posture-install-log
path: /tmp/nemoclaw-e2e-hermes-install.log
if-no-files-found: ignore

# ── Hermes inference switch E2E ─────────────────────────────────
# Validates `nemohermes inference set` against a running Hermes sandbox:
# OpenShell route, config.yaml patch, config hashes, no automatic restart,
Expand Down Expand Up @@ -2204,6 +2290,8 @@ jobs:
sandbox-survival-e2e,
issue-2478-crash-loop-recovery-e2e,
hermes-e2e,
openclaw-onboard-security-posture-e2e,
hermes-onboard-security-posture-e2e,
hermes-inference-switch-e2e,
hermes-discord-e2e,
hermes-slack-e2e,
Expand Down Expand Up @@ -2300,6 +2388,8 @@ jobs:
sandbox-survival-e2e,
issue-2478-crash-loop-recovery-e2e,
hermes-e2e,
openclaw-onboard-security-posture-e2e,
hermes-onboard-security-posture-e2e,
hermes-inference-switch-e2e,
hermes-discord-e2e,
hermes-slack-e2e,
Expand Down Expand Up @@ -2453,6 +2543,8 @@ jobs:
sandbox-survival-e2e,
issue-2478-crash-loop-recovery-e2e,
hermes-e2e,
openclaw-onboard-security-posture-e2e,
hermes-onboard-security-posture-e2e,
hermes-inference-switch-e2e,
hermes-discord-e2e,
hermes-slack-e2e,
Expand Down
140 changes: 36 additions & 104 deletions agents/hermes/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -124,97 +124,6 @@ HERMES_HASH_FILE="/etc/nemoclaw/hermes.config-hash"

# verify_config_integrity is provided by sandbox-init.sh (parameterized).

rewrite_rc_marker_block() {
local rc_file="$1"
local marker_begin="$2"
local marker_end="$3"
local snippet="${4:-}"
local dir base tmp

[ -e "$rc_file" ] || return 0
if [ -L "$rc_file" ] || [ ! -f "$rc_file" ]; then
echo "[SECURITY] refusing unsafe rc file: $rc_file" >&2
return 1
fi

dir="$(dirname "$rc_file")"
base="$(basename "$rc_file")"
tmp="$(mktemp "${dir}/.${base}.tmp.XXXXXX")" || return 1

awk -v b="$marker_begin" -v e="$marker_end" \
'$0==b{s=1;next} $0==e{s=0;next} !s' "$rc_file" >"$tmp" 2>/dev/null || {
rm -f "$tmp"
return 1
}

if [ -n "$snippet" ]; then
printf '%s\n' "$snippet" >>"$tmp" || {
rm -f "$tmp"
return 1
}
fi

if [ "$(id -u)" -eq 0 ] && ! chown root:root "$tmp"; then
rm -f "$tmp"
return 1
fi
chmod 644 "$tmp" 2>/dev/null || true

if [ -L "$rc_file" ]; then
echo "[SECURITY] refusing symlinked rc file during replace: $rc_file" >&2
rm -f "$tmp"
return 1
fi
mv -f "$tmp" "$rc_file" 2>/dev/null || {
rm -f "$tmp"
return 1
}
}

rewrite_rc_marker_block_or_fail_in_root() {
local rc_file="$1"
if rewrite_rc_marker_block "$@"; then
return 0
fi
if [ "$(id -u)" -eq 0 ]; then
return 1
fi
echo "[setup] could not update rc file ${rc_file}; continuing in non-root mode" >&2
return 0
}

install_configure_guard() {
local marker_begin="# nemoclaw-configure-guard begin"
local marker_end="# nemoclaw-configure-guard end"
local snippet
read -r -d '' snippet <<'GUARD' || true
# nemoclaw-configure-guard begin
hermes() {
case "$1" in
setup|doctor)
echo "Error: 'hermes $1' cannot modify config inside the sandbox." >&2
echo "NemoClaw manages sandbox config from the host for integrity checks." >&2
echo "" >&2
echo "To change your configuration, exit the sandbox and run:" >&2
echo " nemoclaw onboard --resume" >&2
return 1
;;
esac
command hermes "$@"
}
# nemoclaw-configure-guard end
GUARD

for rc_file in "${_SANDBOX_HOME}/.bashrc" "${_SANDBOX_HOME}/.profile"; do
[ -f "$rc_file" ] || continue
rewrite_rc_marker_block_or_fail_in_root "$rc_file" "$marker_begin" "$marker_end" "$snippet"
done
# SECURITY FIX: Lock .bashrc/.profile after all mutations are complete.
# This was missing in Hermes (unlike OpenClaw which had it via #2125),
# leaving rc files writable by the sandbox user. Ref: #2277
lock_rc_files "$_SANDBOX_HOME"
}

# configure_messaging_channels is provided by sandbox-init.sh (shared).

print_dashboard_urls() {
Expand Down Expand Up @@ -308,8 +217,8 @@ if [ -n "${SSL_CERT_FILE:-}" ] && [ -f "${SSL_CERT_FILE}" ]; then
export GIT_SSL_CAINFO="${GIT_SSL_CAINFO:-$SSL_CERT_FILE}"
fi

# Resolve sandbox home dir early — used by proxy-env writing and
# install_configure_guard before the non-root/root branch below.
# Resolve sandbox home dir early — used by proxy-env writing before the
# non-root/root branch below.
if [ "$(id -u)" -eq 0 ]; then
_SANDBOX_HOME=$(getent passwd sandbox 2>/dev/null | cut -d: -f6)
_SANDBOX_HOME="${_SANDBOX_HOME:-/sandbox}"
Expand All @@ -323,8 +232,9 @@ fi
# by the sandbox user — same vulnerability class as #2181.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/2277
_PROXY_ENV_FILE="/tmp/nemoclaw-proxy-env.sh"
{
cat <<PROXYEOF
write_runtime_shell_env() {
{
cat <<PROXYEOF
# Proxy configuration (overrides narrow OpenShell defaults on connect)
export HTTP_PROXY="$_PROXY_URL"
export HTTPS_PROXY="$_PROXY_URL"
Expand All @@ -334,13 +244,37 @@ export https_proxy="$_PROXY_URL"
export no_proxy="$_NO_PROXY_VAL"
export HERMES_HOME="${HERMES_DIR}"
PROXYEOF
for _ca_env_name in SSL_CERT_FILE CURL_CA_BUNDLE REQUESTS_CA_BUNDLE GIT_SSL_CAINFO; do
_ca_env_value="${!_ca_env_name:-}"
if [ -n "$_ca_env_value" ]; then
printf 'export %s=%q\n' "$_ca_env_name" "$_ca_env_value"
fi
done
} | emit_sandbox_sourced_file "$_PROXY_ENV_FILE"
for _ca_env_name in SSL_CERT_FILE CURL_CA_BUNDLE REQUESTS_CA_BUNDLE GIT_SSL_CAINFO; do
_ca_env_value="${!_ca_env_name:-}"
if [ -n "$_ca_env_value" ]; then
printf 'export %s=%q\n' "$_ca_env_name" "$_ca_env_value"
fi
done
cat <<'GUARDENVEOF'
# nemoclaw-configure-guard begin
hermes() {
case "$1" in
setup|doctor)
echo "Error: 'hermes $1' cannot modify config inside the sandbox." >&2
echo "NemoClaw manages sandbox config from the host for integrity checks." >&2
echo "" >&2
echo "To change your configuration, exit the sandbox and run:" >&2
echo " nemoclaw onboard --resume" >&2
return 1
;;
esac
command hermes "$@"
}
# nemoclaw-configure-guard end
GUARDENVEOF
} | emit_sandbox_sourced_file "$_PROXY_ENV_FILE"
}

write_runtime_shell_env
# SECURITY FIX: Lock .bashrc/.profile after all static shims are in place.
# Hermes connect sessions source the dynamic guard from /tmp/nemoclaw-proxy-env.sh
# so startup never needs to rewrite files directly under /sandbox after caps drop.
lock_rc_files "$_SANDBOX_HOME"

# ── Legacy layout migration ──────────────────────────────────────
path_has_immutable_bit() {
Expand Down Expand Up @@ -617,7 +551,6 @@ if [ "$(id -u)" -ne 0 ]; then
exit 1
fi
refresh_hermes_provider_placeholders
install_configure_guard
configure_messaging_channels

if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then
Expand Down Expand Up @@ -660,7 +593,6 @@ fi
export HERMES_HOME="${HERMES_DIR}"
verify_config_integrity "${HERMES_DIR}" "${HERMES_HASH_FILE}"
refresh_hermes_provider_placeholders
install_configure_guard
configure_messaging_channels

if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then
Expand Down
Loading
Loading