-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile
More file actions
159 lines (139 loc) · 7.8 KB
/
Copy pathDockerfile
File metadata and controls
159 lines (139 loc) · 7.8 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
# HermesShell + OpenShell: based on official Hermes Agent image
# hermes binary: /opt/hermes/.venv/bin/hermes (accessible to all users)
# hermes data: /opt/data/ (config, skills, memories)
FROM nousresearch/hermes-agent:v2026.5.29.2 AS base
ARG HERMES_VERSION="v2026.5.29.2"
ARG HERMESSHELL_MODEL=""
ARG HERMESSHELL_INFERENCE_BASE_URL="https://inference.local/v1"
ARG HERMESSHELL_INFERENCE_API="openai-completions"
ARG HERMESSHELL_TZ="UTC"
USER root
# OpenShell requires a 'sandbox' user/group; iproute2 for network namespace
# setup; curl for the nvm installer.
RUN apt-get update && \
apt-get install -y --no-install-recommends iproute2 curl ca-certificates python3-venv php-cli php-curl php-gd php-mbstring php-xml php-zip composer universal-ctags global python3-pygments && \
rm -rf /var/lib/apt/lists/* && \
groupadd -r sandbox && useradd -r -g sandbox -m -d /sandbox -s /bin/bash sandbox && \
usermod -aG hermes sandbox 2>/dev/null || true && \
chown -R sandbox:sandbox /sandbox
# Configure timezone from host (defaults to UTC if not provided)
RUN ln -sf "/usr/share/zoneinfo/${HERMESSHELL_TZ}" /etc/localtime && \
echo "${HERMESSHELL_TZ}" > /etc/timezone
ENV TZ="${HERMESSHELL_TZ}"
# Login-time init: lives in the image layer (/etc/profile.d/) so it
# survives the VOLUME mount that replaces /sandbox/ at runtime.
COPY hermes-init.sh /etc/profile.d/hermes.sh
RUN chmod +x /etc/profile.d/hermes.sh
# MCP server configurator (optional) — generated by step 4 for deployments
# that use MCP servers (GitLab, Jira, etc.). If absent, the build succeeds
# and MCP servers are simply not configured.
COPY configure-mcp.sh* /usr/local/bin/
RUN chmod +x /usr/local/bin/configure-mcp.sh 2>/dev/null || true
# Install nvm system-wide and use it to install Node.js 22 + npm 11.
# Replaces the Debian Node.js 20 + npm 9 that ships in the base image.
# nvm lives at /opt/nvm/ (persistent in image layer, not under any VOLUME).
ENV NVM_DIR="/opt/nvm"
RUN mkdir -p "$NVM_DIR" && \
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && \
. "$NVM_DIR/nvm.sh" && \
nvm install 22 && \
nvm alias default 22 && \
ln -sf "$(which node)" /usr/local/bin/node && \
ln -sf "$(which npm)" /usr/local/bin/npm && \
ln -sf "$(which npx)" /usr/local/bin/npx && \
chmod -R a+rX "$NVM_DIR" && \
printf '\nexport NVM_DIR="/opt/nvm"\nnvm() { unset -f nvm; . "$NVM_DIR/nvm.sh"; nvm "$@"; }\nexport OPENAI_BASE_URL="${OPENAI_BASE_URL:-https://inference.local/v1}"\nexport OPENAI_API_KEY="${OPENAI_API_KEY:-not-needed}"\n' >> /etc/bash.bashrc
# Pre-install mcp-atlassian (Jira/Confluence MCP server) in a dedicated venv.
# uvx can't run inside the sandbox (Landlock blocks libc detection), so we
# install at build time and reference the binary directly.
RUN python3 -m venv /opt/mcp-atlassian && \
/opt/mcp-atlassian/bin/pip install --no-cache-dir mcp-atlassian && \
chmod -R a+rX /opt/mcp-atlassian
# Skip the hermes-ink TUI bundle staleness check by forcing Hermes onto its
# prebuilt-bundle shortcut. _hermes_ink_bundle_stale() looks for
# dist/ink-bundle.js, but the upstream build script outputs entry-exports.js,
# so the check always returns stale and triggers a synchronous npm rebuild
# at every TUI launch (which then fails under OpenShell's Landlock read_only
# policy on /opt/hermes). Setting HERMES_TUI_DIR makes _make_tui_argv take
# the shortcut that checks dist/entry.js instead, bypassing the loop.
ENV HERMES_TUI_DIR="/opt/hermes/ui-tui"
# Ensure the hermes installation is world-readable/executable so the
# OpenShell sandbox user can run hermes from SSH sessions
RUN chmod -R a+rX /opt/hermes && \
find /opt/hermes/.venv/bin -type f -executable -exec chmod 755 {} +
# Symlink hermes to /usr/local/bin so it's on the standard PATH used by
# OpenShell SSH sessions (/sandbox/.venv/bin:/usr/local/bin:/usr/bin:/bin).
# The Docker ENV PATH doesn't carry into SSH sessions.
RUN ln -sf /opt/hermes/.venv/bin/hermes /usr/local/bin/hermes
# Sync bundled skills from the image
RUN cd /opt/hermes && \
. .venv/bin/activate && \
python3 tools/skills_sync.py 2>/dev/null || true
ENV PATH="/opt/hermes/.venv/bin:${PATH}"
ENV HERMES_HOME="/sandbox/.hermes"
ENV HERMES_WEB_DIST="/opt/hermes/hermes_cli/web_dist"
# Stash default configs directly from /opt/hermes/ source files into
# /usr/local/share/hermes-defaults/. We CANNOT go through /opt/data/
# because the base image declares it as a VOLUME — writes to it during
# build are discarded between RUN steps. Apply inference config (sed)
# to the stashed copy so it's ready to use at login time.
RUN mkdir -p /usr/local/share/hermes-defaults && \
cp /opt/hermes/cli-config.yaml.example /usr/local/share/hermes-defaults/config.yaml && \
cp /opt/hermes/.env.example /usr/local/share/hermes-defaults/.env && \
cp /opt/hermes/docker/SOUL.md /usr/local/share/hermes-defaults/SOUL.md && \
CONFIG="/usr/local/share/hermes-defaults/config.yaml" && \
sed -i \
-e 's|^\( *\)provider: .*|\1provider: "custom"|' \
-e 's|^\( *\)base_url: .*|\1base_url: "https://inference.local/v1"|' \
-e 's|^\( *\)default: .*|\1default: "local"|' \
"$CONFIG"
ENV OPENAI_BASE_URL="https://inference.local/v1"
RUN echo "${HERMES_VERSION}" > /etc/hermes-version
WORKDIR /sandbox
VOLUME ["/opt/data", "/sandbox"]
# Bake build-time inference config into the image for entrypoint patching
ENV HERMESSHELL_MODEL_DEFAULT="${HERMESSHELL_MODEL}"
ENV HERMESSHELL_INFERENCE_BASE_URL_DEFAULT="${HERMESSHELL_INFERENCE_BASE_URL}"
ENV HERMESSHELL_INFERENCE_API_DEFAULT="${HERMESSHELL_INFERENCE_API}"
# Init script: fix PVC ownership, seed config files, patch inference, generate MCP config
RUN printf '#!/bin/sh\n\
chown sandbox:sandbox /sandbox /opt/data 2>/dev/null\n\
chmod g+rwx /opt/data 2>/dev/null\n\
mkdir -p /opt/data/cron /opt/data/sessions /opt/data/logs /opt/data/hooks \\\n\
/opt/data/memories /opt/data/skills /opt/data/skins /opt/data/plans \\\n\
/opt/data/workspace /opt/data/home 2>/dev/null\n\
for f in config.yaml .env SOUL.md; do\n\
[ ! -f "/opt/data/$f" ] && [ -f "/usr/local/share/hermes-defaults/$f" ] && \\\n\
cp "/usr/local/share/hermes-defaults/$f" "/opt/data/$f"\n\
done\n\
# Patch inference config from build ARGs or runtime overrides\n\
MODEL="${HERMESSHELL_MODEL_OVERRIDE:-$HERMESSHELL_MODEL_DEFAULT}"\n\
BASE_URL="${HERMESSHELL_INFERENCE_BASE_URL_OVERRIDE:-$HERMESSHELL_INFERENCE_BASE_URL_DEFAULT}"\n\
if [ -n "$MODEL" ] && [ -f /opt/data/config.yaml ]; then\n\
sed -i "s|^\\( *\\)model: .*|\\1model: \\"$MODEL\\"|" /opt/data/config.yaml\n\
fi\n\
if [ -n "$BASE_URL" ] && [ -f /opt/data/config.yaml ]; then\n\
sed -i "s|^\\( *\\)base_url: .*|\\1base_url: \\"$BASE_URL\\"|" /opt/data/config.yaml\n\
fi\n\
[ -x /usr/local/bin/configure-mcp.sh ] && /usr/local/bin/configure-mcp.sh 2>/dev/null\n\
# Pre-resolve Jira credentials in config.yaml. mcp-atlassian uses Basic auth\n\
# which base64-encodes placeholders — the L7 proxy cannot resolve them.\n\
# The real values are available as env vars injected by OpenShell providers.\n\
if [ -f /opt/data/config.yaml ]; then\n\
_cfg=/opt/data/config.yaml\n\
for _placeholder in JIRA_USER JIRA_API_TOKEN GITLAB_TOKEN; do\n\
_val=$(eval echo "\\${${_placeholder}:-}")\n\
if [ -n "$_val" ] && echo "$_val" | grep -qv "^openshell:resolve:"; then\n\
sed -i "s|openshell:resolve:env:${_placeholder}|${_val}|g" "$_cfg"\n\
fi\n\
done\n\
unset _cfg _placeholder _val\n\
fi\n\
chown -R sandbox:sandbox /opt/data 2>/dev/null\n\
chmod -R g+rwX /opt/data 2>/dev/null\n\
exec "$@"\n' > /usr/local/bin/entrypoint.sh && \
chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# OpenShell typically overrides CMD with its own SSH init process. The
# gateway is reliably started at login time via /etc/profile.d/hermes.sh.
CMD ["hermes", "gateway"]