hermes: two-stage build, make /opt/hermes writable by uid 10000
All checks were successful
images / hermes (push) Successful in 1m55s
All checks were successful
images / hermes (push) Successful in 1m55s
Upstream ships /opt/hermes (app + .venv + scripts) read-only root, which blocks the agent self-modifying and the gateway auto-installing the WhatsApp bridge's node_modules in place. Add a derived Containerfile layer (FROM the upstream build) that chowns/chmods /opt/hermes writable by the runtime hermes user. Done in the image, not a volume: a volume over /opt/hermes copies-up once then freezes the app, silently defeating AutoUpdate=registry. Persistence stays on the /opt/data volume. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011D3YeWKpjg5bT488fVanCH
This commit is contained in:
@@ -59,15 +59,24 @@ jobs:
|
|||||||
IMAGE=git.lair.cafe/lair/hermes
|
IMAGE=git.lair.cafe/lair/hermes
|
||||||
# Self-healing: the source of truth is "is this version in the registry?"
|
# Self-healing: the source of truth is "is this version in the registry?"
|
||||||
# — not a committed pin that can desync if a prior build failed.
|
# — not a committed pin that can desync if a prior build failed.
|
||||||
|
# NB: when the *build definition* changes (e.g. the writable-tree
|
||||||
|
# layer), republish the same version with the `force` dispatch input.
|
||||||
if [ "$FORCE" != "true" ] && skopeo inspect "docker://${IMAGE}:${VERSION}" >/dev/null 2>&1; then
|
if [ "$FORCE" != "true" ] && skopeo inspect "docker://${IMAGE}:${VERSION}" >/dev/null 2>&1; then
|
||||||
echo "${IMAGE}:${VERSION} already published — nothing to build"
|
echo "${IMAGE}:${VERSION} already published — nothing to build"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "building ${IMAGE}:${VERSION} from NousResearch/hermes-agent#${TAG}"
|
# Two-stage: (1) build upstream from the git context into a local tag,
|
||||||
podman build --pull=newer \
|
# (2) derive our published image from it via images/hermes/Containerfile
|
||||||
|
# (makes /opt/hermes writable by uid 10000 — see that file).
|
||||||
|
BASE="localhost/hermes-upstream:${VERSION}"
|
||||||
|
echo "[1/2] building upstream ${BASE} from NousResearch/hermes-agent#${TAG}"
|
||||||
|
podman build --pull=newer -t "${BASE}" \
|
||||||
|
"https://github.com/NousResearch/hermes-agent.git#${TAG}"
|
||||||
|
echo "[2/2] building derived (writable /opt/hermes) -> ${IMAGE}:${VERSION}"
|
||||||
|
podman build --build-arg BASE="${BASE}" \
|
||||||
-t "${IMAGE}:${VERSION}" \
|
-t "${IMAGE}:${VERSION}" \
|
||||||
-t "${IMAGE}:latest" \
|
-t "${IMAGE}:latest" \
|
||||||
"https://github.com/NousResearch/hermes-agent.git#${TAG}"
|
images/hermes
|
||||||
podman push "${IMAGE}:${VERSION}"
|
podman push "${IMAGE}:${VERSION}"
|
||||||
podman push "${IMAGE}:latest"
|
podman push "${IMAGE}:latest"
|
||||||
echo "published ${IMAGE}:${VERSION} (and :latest)"
|
echo "published ${IMAGE}:${VERSION} (and :latest)"
|
||||||
|
|||||||
20
images/hermes/Containerfile
Normal file
20
images/hermes/Containerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Derived layer over the upstream NousResearch/hermes-agent image.
|
||||||
|
#
|
||||||
|
# Upstream ships /opt/hermes (the app: Python source + .venv + scripts) as a
|
||||||
|
# read-only tree owned by root (0555/0444). That stops the runtime `hermes`
|
||||||
|
# user (uid 10000) from self-modifying and, concretely, stops the gateway from
|
||||||
|
# auto-installing the WhatsApp Node bridge's node_modules in place.
|
||||||
|
#
|
||||||
|
# We make the whole app tree writable by uid 10000 so the agent has "untied
|
||||||
|
# hands". This is done in the IMAGE (not a volume) on purpose: a volume over
|
||||||
|
# /opt/hermes would copy-up once and then freeze the app, silently defeating
|
||||||
|
# AutoUpdate=registry. As a baked layer it ships hands-free on every pull AND
|
||||||
|
# refreshes cleanly on every update. Anything that must PERSIST across updates
|
||||||
|
# belongs on the /opt/data volume (skills, memory, sessions, the WhatsApp
|
||||||
|
# bridge via the gateway's `bridge_script` config), not in /opt/hermes.
|
||||||
|
#
|
||||||
|
# BASE is the upstream image the `images` workflow builds from the git context.
|
||||||
|
ARG BASE
|
||||||
|
FROM ${BASE}
|
||||||
|
USER root
|
||||||
|
RUN chown -R 10000:10000 /opt/hermes && chmod -R u+w /opt/hermes
|
||||||
@@ -17,11 +17,18 @@ fi
|
|||||||
[ -n "${HERMES_REF}" ] || { echo "could not resolve an upstream hermes ref"; exit 1; }
|
[ -n "${HERMES_REF}" ] || { echo "could not resolve an upstream hermes ref"; exit 1; }
|
||||||
VERSION="${HERMES_REF#v}"
|
VERSION="${HERMES_REF#v}"
|
||||||
|
|
||||||
echo "building ${IMAGE_NAME}:${VERSION} from NousResearch/hermes-agent#${HERMES_REF}"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
podman build --pull=newer \
|
BASE="localhost/hermes-upstream:${VERSION}"
|
||||||
|
|
||||||
|
echo "[1/2] building upstream ${BASE} from NousResearch/hermes-agent#${HERMES_REF}"
|
||||||
|
podman build --pull=newer -t "${BASE}" \
|
||||||
|
"https://github.com/NousResearch/hermes-agent.git#${HERMES_REF}"
|
||||||
|
|
||||||
|
echo "[2/2] building derived (writable /opt/hermes) -> ${IMAGE_NAME}:${VERSION}"
|
||||||
|
podman build --build-arg BASE="${BASE}" \
|
||||||
-t "${IMAGE_NAME}:${VERSION}" \
|
-t "${IMAGE_NAME}:${VERSION}" \
|
||||||
-t "${IMAGE_NAME}:latest" \
|
-t "${IMAGE_NAME}:latest" \
|
||||||
"https://github.com/NousResearch/hermes-agent.git#${HERMES_REF}"
|
"${SCRIPT_DIR}"
|
||||||
|
|
||||||
echo "built ${IMAGE_NAME}:${VERSION} and :latest"
|
echo "built ${IMAGE_NAME}:${VERSION} and :latest"
|
||||||
echo "push with: podman push ${IMAGE_NAME}:${VERSION} && podman push ${IMAGE_NAME}:latest"
|
echo "push with: podman push ${IMAGE_NAME}:${VERSION} && podman push ${IMAGE_NAME}:latest"
|
||||||
|
|||||||
@@ -6,22 +6,31 @@ self-improving AI agent — packaged for lair infra and published to
|
|||||||
|
|
||||||
## How it's built
|
## How it's built
|
||||||
|
|
||||||
Upstream ships its own `Dockerfile` (debian 13 + s6-overlay), so there is **no
|
Upstream ships its own `Dockerfile` (debian 13 + s6-overlay), so we build in two
|
||||||
vendored Containerfile here**. The `images` workflow (and `build.sh`) build
|
stages — `build.sh` and the `images` workflow both do this:
|
||||||
straight from the upstream git context at the latest release tag:
|
|
||||||
|
|
||||||
```
|
1. **Upstream** — build straight from the upstream git context at the release tag
|
||||||
podman build github.com/NousResearch/hermes-agent.git#<tag> \
|
into a local tag: `podman build github.com/NousResearch/hermes-agent.git#<tag>`.
|
||||||
-t git.lair.cafe/lair/hermes:<version> -t git.lair.cafe/lair/hermes:latest
|
2. **Derived** — [`Containerfile`](Containerfile) does `FROM` that and makes the
|
||||||
```
|
`/opt/hermes` app tree writable by the runtime `hermes` user (uid 10000), then
|
||||||
|
publishes `git.lair.cafe/lair/hermes:{<version>,latest}`.
|
||||||
|
|
||||||
|
The writable-tree layer gives the agent "untied hands" (self-modify; the gateway
|
||||||
|
can auto-install the WhatsApp bridge's `node_modules` in place) **without** the
|
||||||
|
volume trap: a volume over `/opt/hermes` would copy-up once and freeze the app,
|
||||||
|
silently defeating `AutoUpdate=registry`. Baked into the image, it ships
|
||||||
|
hands-free on every pull and refreshes cleanly on update. **Persistence belongs
|
||||||
|
on `/opt/data`** (skills, memory, sessions, the WhatsApp bridge via `bridge_script`),
|
||||||
|
never in `/opt/hermes`.
|
||||||
|
|
||||||
Builds are **release-triggered** (daily poll of the GitHub releases API; a build
|
Builds are **release-triggered** (daily poll of the GitHub releases API; a build
|
||||||
runs only when that version isn't already in our registry) and **self-healing**
|
runs only when that version isn't already in our registry) and **self-healing**
|
||||||
(a failed build leaves the version absent, so the next poll retries). Force a
|
(a failed build leaves the version absent, so the next poll retries). When the
|
||||||
rebuild via the workflow's `force` dispatch input, or locally:
|
*build definition* changes (e.g. this writable layer) for an already-published
|
||||||
|
version, republish with the workflow's `force` dispatch input. Locally:
|
||||||
|
|
||||||
```
|
```
|
||||||
HERMES_REF=v0.2.0 ./build.sh
|
HERMES_REF=v0.17.0 ./build.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## How it runs (single container, gateway + dashboard)
|
## How it runs (single container, gateway + dashboard)
|
||||||
|
|||||||
Reference in New Issue
Block a user