mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Co-authored-by: Cyrus Gray <cyrus@tinyhumans.ai>
This commit is contained in:
@@ -37,3 +37,6 @@ Thumbs.db
|
||||
# Tests (not needed in build context)
|
||||
tests/
|
||||
scripts/
|
||||
# Re-include the Docker entrypoint for the core image (Dockerfile COPYs it).
|
||||
# The negation must come after the broad exclusion above to take effect.
|
||||
!scripts/docker-entrypoint-core.sh
|
||||
|
||||
@@ -121,3 +121,84 @@ jobs:
|
||||
- name: Tear down
|
||||
if: always()
|
||||
run: docker rm -f oh-smoke || true
|
||||
|
||||
# Regression gate for issue #2065: named volume starts root-owned; the
|
||||
# entrypoint must chown it before dropping to the openhuman user, otherwise
|
||||
# the first disk write (init_rpc_token → write_token_file) raises EACCES
|
||||
# and the process exits with code 1.
|
||||
#
|
||||
# This job deliberately omits OPENHUMAN_CORE_TOKEN (so the core WILL attempt
|
||||
# to write core.token) and mounts a fresh anonymous volume at the workspace
|
||||
# path (so Docker creates it root:root). The existing job above always sets
|
||||
# the token and therefore short-circuits the write — it cannot catch this bug.
|
||||
docker-volume-permissions:
|
||||
name: Smoke-test core with fresh volume and no pre-set token
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 45
|
||||
needs: docker-image
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build openhuman-core image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: false
|
||||
load: true
|
||||
tags: openhuman-core:smoke
|
||||
cache-from: type=gha,scope=deploy-smoke
|
||||
cache-to: type=gha,scope=deploy-smoke,mode=max
|
||||
|
||||
- name: Run container with fresh anonymous volume and no token
|
||||
run: |
|
||||
docker run -d \
|
||||
--name oh-vol-smoke \
|
||||
-p 7789:7788 \
|
||||
-v oh-vol-smoke-workspace:/home/openhuman/.openhuman \
|
||||
-e OPENHUMAN_APP_ENV=staging \
|
||||
-e BACKEND_URL=https://staging-api.tinyhumans.ai \
|
||||
openhuman-core:smoke
|
||||
|
||||
- name: Wait for /health (volume-permissions path)
|
||||
run: |
|
||||
set -e
|
||||
for i in $(seq 1 30); do
|
||||
if curl -fsS http://localhost:7789/health > /tmp/vol-health.json; then
|
||||
echo "Healthy on attempt $i"
|
||||
cat /tmp/vol-health.json
|
||||
exit 0
|
||||
fi
|
||||
echo "attempt $i: not ready, sleeping..."
|
||||
sleep 2
|
||||
done
|
||||
echo "Container never became healthy. Logs:"
|
||||
docker logs oh-vol-smoke || true
|
||||
exit 1
|
||||
|
||||
- name: Assert no permission-denied errors in logs
|
||||
run: |
|
||||
set -e
|
||||
if docker logs oh-vol-smoke 2>&1 | grep -q "Permission denied (os error 13)"; then
|
||||
echo "FAIL: 'Permission denied (os error 13)' found in container logs"
|
||||
docker logs oh-vol-smoke || true
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: no permission-denied errors in container logs"
|
||||
|
||||
- name: Container logs (always)
|
||||
if: always()
|
||||
run: docker logs oh-vol-smoke || true
|
||||
|
||||
- name: Tear down
|
||||
if: always()
|
||||
run: |
|
||||
docker rm -f oh-vol-smoke || true
|
||||
docker volume rm oh-vol-smoke-workspace || true
|
||||
|
||||
+23
-5
@@ -68,16 +68,34 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libx11-6 \
|
||||
libevdev2 \
|
||||
curl \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Non-root user for security
|
||||
RUN useradd --create-home --shell /bin/bash openhuman
|
||||
USER openhuman
|
||||
WORKDIR /home/openhuman
|
||||
# Non-root user for security — fixed UID/GID so volume ownership is stable
|
||||
# across image rebuilds.
|
||||
RUN groupadd --gid 10001 openhuman \
|
||||
&& useradd --uid 10001 --gid 10001 --create-home --shell /bin/bash openhuman
|
||||
|
||||
# Pre-create and own the workspace directory inside the image so the
|
||||
# entrypoint chown is a no-op on a fresh (root-owned) named volume and on
|
||||
# first-time anonymous volume mounts.
|
||||
ENV HOME=/home/openhuman
|
||||
RUN mkdir -p /home/openhuman/.openhuman \
|
||||
&& chown -R openhuman:openhuman /home/openhuman
|
||||
|
||||
# Copy the built binary
|
||||
COPY --from=builder /build/target/release/openhuman-core /usr/local/bin/openhuman-core
|
||||
|
||||
# Copy the entrypoint script that chowns the workspace volume before dropping
|
||||
# privileges. The script is a separate file so the E2E entrypoint
|
||||
# (e2e/docker-entrypoint.sh) is not affected.
|
||||
COPY scripts/docker-entrypoint-core.sh /usr/local/bin/docker-entrypoint-core.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint-core.sh
|
||||
|
||||
# The entrypoint runs as root so it can chown the mounted volume, then execs
|
||||
# gosu to drop to the openhuman user before starting the binary.
|
||||
USER root
|
||||
|
||||
# Default workspace directory
|
||||
ENV OPENHUMAN_WORKSPACE=/home/openhuman/.openhuman
|
||||
# Bind to all interfaces so the container is reachable
|
||||
@@ -91,5 +109,5 @@ EXPOSE 7788
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD curl -sf http://localhost:7788/health || exit 1
|
||||
|
||||
ENTRYPOINT ["openhuman-core"]
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint-core.sh"]
|
||||
CMD ["serve"]
|
||||
|
||||
@@ -354,6 +354,35 @@ rejected by the app picker; use HTTPS for any publicly reachable core.
|
||||
|
||||
---
|
||||
|
||||
## Named-volume ownership and the Docker entrypoint
|
||||
|
||||
Docker creates named volumes owned `root:root` by default. Because the core
|
||||
runs as the non-root `openhuman` user (UID 10001), the first write after the
|
||||
banner — `init_rpc_token → write_token_file` into `$OPENHUMAN_WORKSPACE` —
|
||||
would raise `Permission denied (os error 13)` if nothing fixes the ownership
|
||||
first.
|
||||
|
||||
The image ships a dedicated entrypoint at
|
||||
`/usr/local/bin/docker-entrypoint-core.sh` that:
|
||||
|
||||
1. Starts as `root`.
|
||||
2. Runs `mkdir -p` + `chown openhuman:openhuman` on both `$OPENHUMAN_WORKSPACE`
|
||||
and `$HOME/.openhuman` (the directory `core.token` is written to when
|
||||
`OPENHUMAN_CORE_TOKEN` is unset).
|
||||
3. Calls `exec gosu openhuman openhuman-core "$@"` to drop privileges and
|
||||
hand off to the binary.
|
||||
|
||||
This is **idempotent**: on a freshly-created volume the chown heals the
|
||||
root-owned directory; on a volume that was already healed the chown is a
|
||||
no-op. No manual `docker volume rm` is required when upgrading from images
|
||||
predating this fix.
|
||||
|
||||
The entrypoint is named `docker-entrypoint-core.sh` and wired **only** into
|
||||
the root `Dockerfile`. The E2E image (`e2e/docker-entrypoint.sh`) is
|
||||
unaffected.
|
||||
|
||||
---
|
||||
|
||||
## Smoke test
|
||||
|
||||
The repo ships [`.github/workflows/deploy-smoke.yml`](../../.github/workflows/deploy-smoke.yml),
|
||||
@@ -361,14 +390,34 @@ which runs on every PR that touches the deploy artifacts. It builds the
|
||||
Docker image, boots it, and polls `/health`, so a regression in the cloud
|
||||
deploy path fails CI before it lands on `main`.
|
||||
|
||||
The workflow contains two jobs:
|
||||
|
||||
- **`docker-image`** — sets `OPENHUMAN_CORE_TOKEN` and mounts no volume.
|
||||
Protects the DigitalOcean App Platform path (`.do/app.yaml`) where the
|
||||
token is always pre-set and no persistent volume is used.
|
||||
- **`docker-volume-permissions`** — omits `OPENHUMAN_CORE_TOKEN` and mounts
|
||||
a fresh anonymous volume at `/home/openhuman/.openhuman`. Reproduces the
|
||||
exact failure mode of issue #2065 and asserts that `/health` returns 200
|
||||
and that `Permission denied (os error 13)` is absent from the logs.
|
||||
|
||||
To run the same check locally:
|
||||
|
||||
```bash
|
||||
docker build -t openhuman-core:smoke .
|
||||
|
||||
# Token-set path (App Platform):
|
||||
docker run -d --name oh-smoke -p 7788:7788 \
|
||||
-e OPENHUMAN_CORE_TOKEN=smoke-test-token \
|
||||
openhuman-core:smoke
|
||||
# Wait ~15s for the binary to come up, then:
|
||||
curl -fsS http://localhost:7788/health
|
||||
docker rm -f oh-smoke
|
||||
|
||||
# Fresh-volume / no-token path (Docker Compose, VPS):
|
||||
docker volume create oh-vol-test
|
||||
docker run -d --name oh-vol-smoke -p 7789:7788 \
|
||||
-v oh-vol-test:/home/openhuman/.openhuman \
|
||||
openhuman-core:smoke
|
||||
curl -fsS http://localhost:7789/health
|
||||
docker rm -f oh-vol-smoke
|
||||
docker volume rm oh-vol-test
|
||||
```
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
# docker-entrypoint-core.sh — runtime entrypoint for the openhuman-core container.
|
||||
#
|
||||
# Problem: Docker named volumes are created owned root:root, even when the image
|
||||
# has a non-root USER. The first write openhuman-core makes after the banner
|
||||
# (init_rpc_token → write_token_file → create_dir_all) hits EACCES and the
|
||||
# process exits with code 1.
|
||||
#
|
||||
# Fix (gosu pattern):
|
||||
# 1. Start as root so we can chown the mount point(s).
|
||||
# 2. mkdir -p + chown the workspace directory *before* any application code
|
||||
# runs, so the openhuman user owns it regardless of whether Docker created
|
||||
# the volume as root.
|
||||
# 3. exec gosu openhuman to drop privileges and hand off to the binary.
|
||||
#
|
||||
# This is idempotent: if the directory already exists with the right ownership
|
||||
# (image-baked or a re-used volume that was healed on a previous run) the chown
|
||||
# is a no-op. No manual "docker volume rm" is required when upgrading from a
|
||||
# previously broken image.
|
||||
#
|
||||
# Requirements: gosu must be installed in the image (see Dockerfile).
|
||||
# POSIX sh — no bashisms.
|
||||
set -e
|
||||
|
||||
OPENHUMAN_USER="openhuman"
|
||||
OPENHUMAN_UID="$(id -u "${OPENHUMAN_USER}" 2>/dev/null || echo '')"
|
||||
|
||||
# The workspace path the core will actually write to.
|
||||
# Prefer the env var if set; otherwise fall back to the image default.
|
||||
WORKSPACE_DIR="${OPENHUMAN_WORKSPACE:-/home/openhuman/.openhuman}"
|
||||
# The home directory (where core.token is written when OPENHUMAN_CORE_TOKEN is
|
||||
# unset — see src/core/auth.rs default_root_openhuman_dir()).
|
||||
HOME_OPENHUMAN_DIR="/home/openhuman/.openhuman"
|
||||
|
||||
echo "[docker-entrypoint] uid=$(id -u), gid=$(id -g), user=$(id -un 2>/dev/null || echo unknown)"
|
||||
echo "[docker-entrypoint] chowning workspace dirs for ${OPENHUMAN_USER} (uid=${OPENHUMAN_UID})"
|
||||
echo "[docker-entrypoint] WORKSPACE_DIR=${WORKSPACE_DIR}"
|
||||
echo "[docker-entrypoint] HOME_OPENHUMAN_DIR=${HOME_OPENHUMAN_DIR}"
|
||||
|
||||
# Ensure workspace dir exists and is owned by the openhuman user.
|
||||
mkdir -p "${WORKSPACE_DIR}"
|
||||
chown "${OPENHUMAN_USER}:${OPENHUMAN_USER}" "${WORKSPACE_DIR}"
|
||||
echo "[docker-entrypoint] chown ${WORKSPACE_DIR} -> ${OPENHUMAN_USER}:${OPENHUMAN_USER} done"
|
||||
|
||||
# If WORKSPACE_DIR and HOME_OPENHUMAN_DIR differ, heal the home dir too
|
||||
# (core.token always lands in $HOME/.openhuman regardless of OPENHUMAN_WORKSPACE).
|
||||
if [ "${WORKSPACE_DIR}" != "${HOME_OPENHUMAN_DIR}" ]; then
|
||||
mkdir -p "${HOME_OPENHUMAN_DIR}"
|
||||
chown "${OPENHUMAN_USER}:${OPENHUMAN_USER}" "${HOME_OPENHUMAN_DIR}"
|
||||
echo "[docker-entrypoint] chown ${HOME_OPENHUMAN_DIR} -> ${OPENHUMAN_USER}:${OPENHUMAN_USER} done"
|
||||
fi
|
||||
|
||||
echo "[docker-entrypoint] dropping privileges -> exec gosu ${OPENHUMAN_USER} openhuman-core $*"
|
||||
exec gosu "${OPENHUMAN_USER}" openhuman-core "$@"
|
||||
Reference in New Issue
Block a user