forked from NVIDIA/NemoClaw
-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathDockerfile
More file actions
270 lines (250 loc) · 12 KB
/
Copy pathDockerfile
File metadata and controls
270 lines (250 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Hermes sandbox image — Hermes Agent + NemoClaw plugin inside OpenShell
#
# Layers PR-specific code (plugin, config, startup script) on top of the
# pre-built Hermes base image. Mirrors the OpenClaw Dockerfile structure.
ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/hermes-sandbox-base:latest
# hadolint ignore=DL3006
FROM ${BASE_IMAGE}
# Keep the final image contract explicit even when the published base image
# changes independently of this Dockerfile.
RUN set -eu; \
hermes_path="$(command -v hermes 2>/dev/null || true)"; \
if [ "$hermes_path" != "/usr/local/bin/hermes" ]; then \
echo "ERROR: expected hermes at /usr/local/bin/hermes, got ${hermes_path:-missing}" >&2; \
exit 1; \
fi; \
test -x /usr/local/bin/hermes; \
/usr/local/bin/hermes --version
# Harden: remove unnecessary build tools and network probes
RUN (apt-get remove --purge -y gcc gcc-12 g++ g++-12 cpp cpp-12 make \
netcat-openbsd netcat-traditional ncat 2>/dev/null || true) \
&& apt-get autoremove --purge -y \
&& rm -rf /var/lib/apt/lists/*
# Hermes v2026.4.13+ auto-detects HTTPS_PROXY and skips fallback-IP
# transport when a proxy is present. The sandbox proxy chain
# (decode-proxy → OpenShell L7 proxy) handles credential placeholder
# rewriting and hostname-based policy enforcement. No monkey patch needed.
ENV HERMES_TELEGRAM_DISABLE_FALLBACK_IPS=1
# Copy NemoClaw plugin for Hermes (Python-based)
COPY agents/hermes/plugin/ /opt/nemoclaw-hermes-plugin/
RUN chmod -R a+rX /opt/nemoclaw-hermes-plugin/
# Copy config generator and URL-decode proxy
COPY agents/hermes/generate-config.ts /opt/nemoclaw-hermes-config/generate-config.ts
COPY agents/hermes/config/ /opt/nemoclaw-hermes-config/config/
COPY scripts/hermes-managed-tool-gateway-matrix.json /opt/nemoclaw-hermes-config/hermes-managed-tool-gateway-matrix.json
RUN find /opt/nemoclaw-hermes-config -type d -exec chmod 755 {} + \
&& find /opt/nemoclaw-hermes-config -type f -exec chmod 444 {} +
COPY agents/hermes/decode-proxy.py /usr/local/bin/nemoclaw-decode-proxy
RUN chmod 755 /usr/local/bin/nemoclaw-decode-proxy
# Copy blueprint (shared infrastructure)
COPY nemoclaw-blueprint/ /opt/nemoclaw-blueprint/
# Ensure sandbox user can read blueprint files copied as root
RUN chmod -R a+rX /opt/nemoclaw-blueprint/
# Copy startup script
COPY scripts/lib/sandbox-init.sh /usr/local/lib/nemoclaw/sandbox-init.sh
COPY agents/hermes/start.sh /usr/local/bin/nemoclaw-start
RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/lib/nemoclaw/sandbox-init.sh
# Build args for config that varies per deployment.
ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b
ARG NEMOCLAW_PROVIDER_KEY=custom
ARG NEMOCLAW_PRIMARY_MODEL_REF=inference/claude-opus-4-7
ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1
ARG NEMOCLAW_INFERENCE_API=openai-completions
# CHAT_UI_URL is a legacy name shared with the OpenClaw build arg. For
# Hermes this URL points at the OpenAI-compatible API server (port 8642,
# exposing /v1 and /health), NOT a browser chat UI. Callers authenticate
# via a bearer token in the Authorization header. See
# agents/hermes/manifest.yaml (dashboard.kind: api).
ARG CHAT_UI_URL=http://127.0.0.1:8642
ARG NEMOCLAW_MESSAGING_CHANNELS_B64=W10=
ARG NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=e30=
ARG NEMOCLAW_DISCORD_GUILDS_B64=e30=
ARG NEMOCLAW_TELEGRAM_CONFIG_B64=e30=
ARG NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64=W10=
ARG NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=0
ARG NEMOCLAW_BUILD_ID=default
# Promote build-args to env vars for the config generation script.
ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_PROVIDER_KEY=${NEMOCLAW_PROVIDER_KEY} \
NEMOCLAW_PRIMARY_MODEL_REF=${NEMOCLAW_PRIMARY_MODEL_REF} \
NEMOCLAW_INFERENCE_BASE_URL=${NEMOCLAW_INFERENCE_BASE_URL} \
NEMOCLAW_INFERENCE_API=${NEMOCLAW_INFERENCE_API} \
CHAT_UI_URL=${CHAT_UI_URL} \
NEMOCLAW_MESSAGING_CHANNELS_B64=${NEMOCLAW_MESSAGING_CHANNELS_B64} \
NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=${NEMOCLAW_MESSAGING_ALLOWED_IDS_B64} \
NEMOCLAW_DISCORD_GUILDS_B64=${NEMOCLAW_DISCORD_GUILDS_B64} \
NEMOCLAW_TELEGRAM_CONFIG_B64=${NEMOCLAW_TELEGRAM_CONFIG_B64} \
NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64=${NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64} \
NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=${NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER}
WORKDIR /sandbox
USER sandbox
# Set up blueprint for local resolution
RUN mkdir -p /sandbox/.nemoclaw/blueprints/0.1.0 \
&& cp -r /opt/nemoclaw-blueprint/* /sandbox/.nemoclaw/blueprints/0.1.0/
# Generate Hermes config.yaml and .env from build args.
# Config is mutable by default (600 sandbox:sandbox). Immutability is opt-in
# via `shields up`. .env holds API key placeholders for OpenShell provider pipeline.
# SECURITY: Uses a separate script file instead of inline code to avoid
# code injection via build-arg interpolation (same concern as OpenClaw C-2).
RUN node --experimental-strip-types /opt/nemoclaw-hermes-config/generate-config.ts
# Install NemoClaw plugin into Hermes
RUN mkdir -p /sandbox/.hermes/plugins/nemoclaw \
&& cp -r /opt/nemoclaw-hermes-plugin/* /sandbox/.hermes/plugins/nemoclaw/
# Write a default SOUL.md for the sandboxed agent.
RUN mkdir -p /sandbox/.hermes/memories \
&& printf '%s\n' \
'You are a helpful AI assistant running inside an NVIDIA OpenShell sandbox.' \
'Your inference is routed through NemoClaw. You have access to terminal,' \
'file, and web tools. Be concise and helpful.' \
> /sandbox/.hermes/SOUL.md
# Set mutable-default permissions (640/750 sandbox:sandbox).
# The sandbox user can write config; the gateway user can read it via
# supplementary sandbox-group membership. Shields-up applies 444 root:root +
# chattr +i.
# hadolint ignore=DL3002
USER root
# Flatten stale published base images that still contain the old .hermes-data
# symlink bridge. OpenShell starts the sandbox as the sandbox user, so runtime
# migration cannot rely on root privileges inside the pod.
# hadolint ignore=DL4006
RUN set -eu; \
config_dir=/sandbox/.hermes; \
data_dir=/sandbox/.hermes-data; \
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 \
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 \
rm -f "$target"; \
fi; \
if [ -d "$entry" ]; then \
mkdir -p "$target"; \
cp -a "$entry"/. "$target"/; \
elif [ ! -e "$target" ]; then \
cp -a "$entry" "$target"; \
fi; \
done; \
data_real="$(readlink -f "$data_dir" 2>/dev/null || printf '%s' "$data_dir")"; \
while :; do \
replaced_marker="$(mktemp)"; \
rm -f "$replaced_marker"; \
find "$config_dir" -type l -print | while IFS= read -r link; do \
raw_target="$(readlink "$link" 2>/dev/null || true)"; \
resolved_target="$(readlink -f "$link" 2>/dev/null || true)"; \
legacy_target=0; \
case "$raw_target" in "$data_real"/* | "$data_dir"/*) legacy_target=1 ;; esac; \
case "$resolved_target" in "$data_real"/* | "$data_dir"/*) legacy_target=1 ;; esac; \
if [ "$legacy_target" -eq 1 ]; then \
copy_target="$resolved_target"; \
if [ -z "$copy_target" ] || { [ ! -e "$copy_target" ] && [ ! -L "$copy_target" ]; }; then \
copy_target="$raw_target"; \
fi; \
if [ -d "$copy_target" ] && [ ! -L "$copy_target" ]; then \
rm -f "$link"; \
mkdir -p "$link"; \
cp -a "$copy_target"/. "$link"/; \
elif [ -e "$copy_target" ] || [ -L "$copy_target" ]; then \
rm -f "$link"; \
cp -a "$copy_target" "$link"; \
else \
echo "ERROR: legacy symlink target missing: $link -> ${raw_target:-$resolved_target}" >&2; \
exit 1; \
fi; \
: > "$replaced_marker"; \
fi; \
done; \
if [ ! -e "$replaced_marker" ]; then \
rm -f "$replaced_marker"; \
break; \
fi; \
rm -f "$replaced_marker"; \
done; \
rm -rf "$data_dir"; \
fi; \
mkdir -p "$config_dir/memories" \
"$config_dir/sessions" \
"$config_dir/skills" \
"$config_dir/plugins" \
"$config_dir/cron" \
"$config_dir/logs" \
"$config_dir/skins" \
"$config_dir/plans" \
"$config_dir/workspace" \
"$config_dir/profiles" \
"$config_dir/cache" \
"$config_dir/pairing" \
"$config_dir/runtime"; \
if [ -e "$data_dir" ] || [ -L "$data_dir" ]; then \
echo "ERROR: legacy data dir still exists after cleanup: $data_dir" >&2; \
exit 1; \
fi; \
data_real="$(readlink -f "$data_dir" 2>/dev/null || printf '%s' "$data_dir")"; \
find "$config_dir" -type l -print | while IFS= read -r link; do \
raw_target="$(readlink "$link" 2>/dev/null || true)"; \
resolved_target="$(readlink -f "$link" 2>/dev/null || true)"; \
case "$raw_target" in \
"$data_real"/* | "$data_dir"/*) \
echo "ERROR: legacy symlink remains after cleanup: $link -> $raw_target" >&2; \
exit 1; \
;; \
esac; \
case "$resolved_target" in \
"$data_real"/* | "$data_dir"/*) \
echo "ERROR: legacy symlink remains after cleanup: $link -> $resolved_target" >&2; \
exit 1; \
;; \
esac; \
done; \
rm -rf /root/.cache/pip /sandbox/.cache \
&& chown -R sandbox:sandbox /sandbox/.hermes \
&& chown gateway:sandbox /sandbox/.hermes/runtime \
&& chmod 750 /sandbox/.hermes \
&& chmod 770 \
/sandbox/.hermes/memories \
/sandbox/.hermes/sessions \
/sandbox/.hermes/skills \
/sandbox/.hermes/plugins \
/sandbox/.hermes/cron \
/sandbox/.hermes/logs \
/sandbox/.hermes/skins \
/sandbox/.hermes/plans \
/sandbox/.hermes/workspace \
/sandbox/.hermes/profiles \
/sandbox/.hermes/cache \
/sandbox/.hermes/pairing \
/sandbox/.hermes/runtime \
&& chmod 2770 /sandbox/.hermes/runtime \
&& chmod 640 /sandbox/.hermes/config.yaml \
&& chmod 640 /sandbox/.hermes/.env \
&& for name in state.db state.db-wal state.db-shm gateway.pid gateway.lock gateway_state.json channel_directory.json; do \
rm -f "/sandbox/.hermes/${name}"; \
ln -s "runtime/${name}" "/sandbox/.hermes/${name}"; \
done
# Pin config hash at build time for integrity verification at startup.
RUN mkdir -p /etc/nemoclaw \
&& sha256sum /sandbox/.hermes/config.yaml /sandbox/.hermes/.env \
> /etc/nemoclaw/hermes.config-hash \
&& chown root:root /etc/nemoclaw/hermes.config-hash \
&& chmod 444 /etc/nemoclaw/hermes.config-hash
# Backward-compatible marker for host-side shields logic on older sandboxes.
RUN sha256sum /sandbox/.hermes/config.yaml /sandbox/.hermes/.env \
> /sandbox/.hermes/.config-hash \
&& chmod 600 /sandbox/.hermes/.config-hash \
&& chown sandbox:sandbox /sandbox/.hermes/.config-hash
# start.sh handles privilege separation: runs as root initially, then drops
# to 'gateway' user via gosu for the agent process. See start.sh.
ENTRYPOINT ["/usr/local/bin/nemoclaw-start"]
CMD ["/bin/bash"]