diff --git a/Dockerfile b/Dockerfile index c0690f9a..3078be2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,9 +42,11 @@ RUN useradd -m -u 1000 -s /bin/bash nanobot && \ COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN sed -i 's/\r$//' /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh -# Start as root so the Render entrypoint can chown the freshly-mounted -# (root-owned) persistent disk, then drop to the non-root nanobot user via -# setpriv (see entrypoint.sh). Local runs without a disk are unaffected. +# Start as root so the entrypoint can chown the data dir (on Render, the +# freshly-mounted root-owned persistent disk) before dropping to the non-root +# nanobot user via setpriv. The entrypoint drops privileges on every root start +# and fails closed if it cannot, so the agent never runs as root (see +# entrypoint.sh). USER root ENV HOME=/home/nanobot # Ensure crash output reaches Render logs (app output is otherwise swallowed on diff --git a/README.md b/README.md index 0c2b94db..634eb884 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ Deploy nanobot's gateway and bundled WebUI as a single web service with persistent memory. Render reads [`render.yaml`](./render.yaml) and prompts for two secrets on deploy: `ANTHROPIC_API_KEY` and `NANOBOT_WEB_TOKEN` (the password that gates the public WebUI — generate a strong random value, e.g. `openssl rand -hex 32`). +> **Note:** The blueprint attaches a persistent disk so sessions, memory, and WebUI history survive restarts. Persistent disks require a paid service (they are not available on Render's free tier). + [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/HKUDS/nanobot) ## What can nanobot do? diff --git a/entrypoint.sh b/entrypoint.sh index 2de8ce73..1a4c181d 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,26 +3,42 @@ dir="$HOME/.nanobot" # Render deploy path (see render.yaml + render-config.json). Gated on Render's # automatic RENDER=true env var so local Docker/podman usage is unaffected. -# Copies the committed config template onto the mounted disk (wiring secrets via -# ${VAR} env vars and keeping runtime data on the persistent disk), chowns the -# root-owned mount, then drops to the non-root nanobot user. Logs each decision -# so a failed start is diagnosable in Render's logs. +# Initializes the on-disk config from the committed template (wiring secrets via +# ${VAR} env vars, keeping runtime data on the persistent disk) and appends the +# --config flag. Logs each decision so a failed start is diagnosable in Render's +# logs. Privilege dropping is handled below, for every root start (not just here). if [ "$RENDER" = "true" ]; then echo "[entrypoint] Render deploy — starting as $(id)" mkdir -p "$dir" || echo "[entrypoint] warning: mkdir $dir failed" config="$dir/config.json" - cp /app/render-config.json "$config" || echo "[entrypoint] warning: cp config failed" - if [ "$(id -u)" = "0" ]; then - chown -R nanobot:nanobot "$dir" || echo "[entrypoint] warning: chown $dir failed" - if setpriv --reuid=nanobot --regid=nanobot --init-groups true 2>/dev/null; then - echo "[entrypoint] dropping privileges to nanobot via setpriv" - exec setpriv --reuid=nanobot --regid=nanobot --init-groups nanobot "$@" --config "$config" - fi - echo "[entrypoint] setpriv privilege-drop not permitted — running as root" + # Initialize config only when it does not already exist, so WebUI/provider + # settings edited at runtime survive restarts. The disk persists config.json + # across deploys; overwriting it every boot would discard those changes. + if [ ! -f "$config" ]; then + echo "[entrypoint] initializing $config from render-config.json" + cp /app/render-config.json "$config" || echo "[entrypoint] warning: cp config failed" + else + echo "[entrypoint] existing $config found — leaving it in place" fi - exec nanobot "$@" --config "$config" + set -- "$@" --config "$config" fi +# Drop privileges whenever the container starts as root. Render mounts the +# persistent disk root-owned, and a plain `docker run` also defaults to root now, +# so this covers both. Chown the data dir so the non-root user can write it, then +# re-exec as nanobot. Fail closed: if the privilege drop cannot be performed, +# exit rather than run the agent as root. +if [ "$(id -u)" = "0" ]; then + chown -R nanobot:nanobot "$dir" 2>/dev/null || echo "[entrypoint] warning: chown $dir failed" + if setpriv --reuid=nanobot --regid=nanobot --init-groups true 2>/dev/null; then + echo "[entrypoint] dropping privileges to nanobot via setpriv" + exec setpriv --reuid=nanobot --regid=nanobot --init-groups nanobot "$@" + fi + echo "[entrypoint] error: started as root but setpriv privilege drop failed — refusing to run as root" >&2 + exit 1 +fi + +# Already non-root: make sure the data dir is writable before starting. if [ -d "$dir" ] && [ ! -w "$dir" ]; then owner_uid=$(stat -c %u "$dir" 2>/dev/null || stat -f %u "$dir" 2>/dev/null) cat >&2 <