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
21 changes: 21 additions & 0 deletions .github/actions/resolve-hermes-base-image/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ runs:
[[ -n "$have" ]] && [[ "$(printf '%s\n%s\n' "$min_glibc" "$have" | sort -V | head -n 1)" == "$min_glibc" ]]
}

layout_ok() {
local ref="$1"
docker run --rm --entrypoint sh "$ref" -lc '
set -eu
for path in /sandbox/.openclaw /sandbox/.hermes-data; do
if [ -e "$path" ] || [ -L "$path" ]; then
echo "Hermes base image contains retired sandbox state: $path" >&2
exit 1
fi
done
'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

try_image() {
local ref="$1" version digest_ref
if ! docker pull "$ref" >/dev/null 2>&1; then
Expand All @@ -36,6 +49,10 @@ runs:
echo "::warning::Hermes sandbox base image ${ref} has glibc ${version:-unknown}; need >= ${min_glibc}"
return 1
fi
if ! layout_ok "$ref"; then
echo "::warning::Hermes sandbox base image ${ref} contains retired sandbox state; trying another candidate"
return 1
fi
digest_ref="$(docker image inspect "$ref" --format '{{range .RepoDigests}}{{println .}}{{end}}' | grep -F -m 1 "${image}@sha256:" || true)"
if [[ -z "$digest_ref" ]]; then
echo "::warning::Hermes sandbox base image ${ref} did not expose an immutable GHCR repo digest (may be a fresh tag); building locally"
Expand Down Expand Up @@ -64,4 +81,8 @@ runs:
echo "::error::Local Hermes sandbox base image has glibc ${version:-unknown}; need >= ${min_glibc}"
exit 1
fi
if ! layout_ok nemoclaw-hermes-base-local; then
echo "::error::Local Hermes sandbox base image contains retired sandbox state"
exit 1
fi
echo "HERMES_BASE_IMAGE=nemoclaw-hermes-base-local" >> "$GITHUB_ENV"
25 changes: 0 additions & 25 deletions .github/workflows/pr-self-hosted.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,31 +87,6 @@ jobs:
- name: Build sandbox test image on arm64
run: docker build -f test/Dockerfile.sandbox --build-arg BASE_IMAGE=nemoclaw-production-arm64 -t nemoclaw-sandbox-test-arm64 .

build-hermes-stale-openclaw-image:
runs-on: linux-amd64-cpu4
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Resolve Hermes base image
uses: ./.github/actions/resolve-hermes-base-image

- name: Verify Hermes stale OpenClaw final-image cleanup
env:
NEMOCLAW_HERMES_BASE_IMAGE: ${{ env.HERMES_BASE_IMAGE }}
run: bash scripts/verify-hermes-stale-openclaw-image.sh

- name: Upload Hermes stale OpenClaw image log on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: hermes-stale-openclaw-image-log
path: /tmp/nemoclaw-hermes-stale-openclaw-image.log
if-no-files-found: ignore

test-e2e-sandbox:
runs-on: linux-amd64-cpu4
timeout-minutes: 15
Expand Down
70 changes: 15 additions & 55 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@
# Layers PR-specific code (plugin, config, startup script) on top of the
# pre-built Hermes base image. Mirrors the OpenClaw Dockerfile structure.

ARG NEMOCLAW_STALE_OPENCLAW_BASE_DIGEST=sha256:60333c1982ad855d55887b4488e867eb343f3930a30aa8e0268e5397fc6f2926
ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@${NEMOCLAW_STALE_OPENCLAW_BASE_DIGEST}
ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:8dad3b989a9ed1e601743310b97be21be5f59f89f7913a47d04f3ec3c40b8ce6

# hadolint ignore=DL3006
FROM ${BASE_IMAGE}
ARG BASE_IMAGE
ARG NEMOCLAW_STALE_OPENCLAW_BASE_DIGEST

# Keep the final image contract explicit even when the published base image
# changes independently of this Dockerfile.
Expand Down Expand Up @@ -253,72 +250,35 @@ ENV HERMES_ENVIRONMENT_HINT="You are running inside an NVIDIA OpenShell sandbox,
# hadolint ignore=DL3002
USER root

# Flatten stale published base images that still contain /sandbox/.openclaw from
# older Hermes base layouts or the old .hermes-data symlink bridge. Dockerfile.base
# is the source of truth for new bases; this final-image repair only removes
# invalid state baked into already-published bases.
# NEMOCLAW_STALE_OPENCLAW_BASE_DIGEST is the removal trigger: update or remove
# this cleanup when the Dockerfile's default pinned Hermes base digest advances
# past the stale layout. Official GHCR bases must be digest-pinned while this
# temporary repair exists; local and verifier-built bases are intentionally
# exempt because their digest is not an upstream publication boundary. OpenShell
# starts the sandbox as the sandbox user, so runtime migration cannot rely on
# root privileges inside the pod. Regression coverage lives in
# scripts/verify-hermes-stale-openclaw-image.sh.
# REMOVAL_CHECKLIST:
# 1. BASE_IMAGE defaults to a digest newer than NEMOCLAW_STALE_OPENCLAW_BASE_DIGEST.
# 2. Supported GHCR Hermes base tags resolve to post-stale-layout digests.
# 3. Hermes stale-layout verifier coverage is kept or intentionally retired.
# 4. Remove this ARG and cleanup block in the same PR that advances the default.
# Refuse symlinked stale state before rm -rf so a compromised base cannot point
# cleanup outside the Hermes sandbox root.
# Docker image layers cannot persist bind mounts at /sandbox/.openclaw, so the
# build-time traversal threat here is a symlinked path baked into the base layer.
# Flatten stale published base images that still contain the old .hermes-data
# symlink bridge. The base resolver owns published-image provenance and exports
# immutable digests; this final-image boundary owns layout validation so local,
# rebuilt, and caller-selected bases remain supported. Retired OpenClaw state
# must never enter a Hermes final image. OpenShell starts the sandbox as the
# sandbox user, so the .hermes-data migration cannot rely on root privileges
# inside the pod.
# hadolint ignore=DL4006
RUN set -eu; \
stale_base_digest="${NEMOCLAW_STALE_OPENCLAW_BASE_DIGEST:?}"; \
case "${BASE_IMAGE:-}" in \
ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:*) \
if [ "${BASE_IMAGE##*@}" != "$stale_base_digest" ]; then \
echo "ERROR: remove stale Hermes .openclaw cleanup or update the tracked stale-base digest; BASE_IMAGE=$BASE_IMAGE tracked=$stale_base_digest" >&2; \
exit 1; \
fi; \
;; \
ghcr.io/nvidia/nemoclaw/hermes-sandbox-base:*) \
echo "ERROR: use an immutable Hermes base digest while stale .openclaw cleanup is present; BASE_IMAGE=$BASE_IMAGE" >&2; \
exit 1; \
;; \
nemoclaw-hermes-base-local | nemoclaw-hermes-stale-openclaw-dir-base:* | nemoclaw-hermes-stale-openclaw-link-base:*) \
;; \
*) \
echo "ERROR: unsupported Hermes BASE_IMAGE while stale .openclaw cleanup is present; BASE_IMAGE=${BASE_IMAGE:-unset}" >&2; \
exit 1; \
;; \
esac; \
config_dir=/sandbox/.hermes; \
data_dir=/sandbox/.hermes-data; \
openclaw_dir=/sandbox/.openclaw; \
mkdir -p "$config_dir"; \
if [ -L "$openclaw_dir" ]; then \
echo "ERROR: refusing Hermes layout cleanup because $openclaw_dir is a symlink" >&2; \
exit 1; \
fi; \
rm -rf "$openclaw_dir"; \
if [ -e "$openclaw_dir" ] || [ -L "$openclaw_dir" ]; then \
echo "ERROR: stale OpenClaw state dir still exists after Hermes cleanup: $openclaw_dir" >&2; \
echo "ERROR: Hermes base image contains retired OpenClaw state: $openclaw_dir" >&2; \
exit 1; \
fi; \
mkdir -p "$config_dir"; \
if [ -L "$data_dir" ]; then \
echo "ERROR: refusing legacy layout cleanup because $data_dir is a symlink" >&2; \
exit 1; \
fi; \
if [ -d "$data_dir" ]; then \
legacy_link="$(find "$data_dir" -type l -print -quit)"; \
if [ -n "$legacy_link" ]; then \
echo "ERROR: refusing legacy layout cleanup because $legacy_link is a symlink" >&2; \
exit 1; \
fi; \
for entry in "$data_dir"/*; do \
[ -e "$entry" ] || [ -L "$entry" ] || continue; \
if [ -L "$entry" ]; then \
echo "ERROR: refusing legacy layout cleanup because $entry is a symlink" >&2; \
exit 1; \
fi; \
name="$(basename "$entry")"; \
target="$config_dir/$name"; \
if [ -L "$target" ]; then \
Expand Down
190 changes: 0 additions & 190 deletions scripts/verify-hermes-stale-openclaw-image.sh

This file was deleted.

4 changes: 4 additions & 0 deletions test/e2e/test-hermes-sandbox-secret-boundary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ def parse_platform_toolsets(text: str) -> dict[str, list[str]]:

env_path = Path("/sandbox/.hermes/.env")
config_path = Path("/sandbox/.hermes/config.yaml")
for retired_path in (Path("/sandbox/.openclaw"), Path("/sandbox/.hermes-data")):
if retired_path.exists() or retired_path.is_symlink():
print(f"retired Hermes sandbox state is present: {retired_path}", file=sys.stderr)
sys.exit(1)
if env_path.is_symlink():
print(f"{env_path} is a symlink", file=sys.stderr)
sys.exit(1)
Expand Down
Loading
Loading