From 566ccd95861b0f2e1d85ddf368c319b5c7eb29ec Mon Sep 17 00:00:00 2001 From: Ho1yShif Date: Tue, 7 Jul 2026 11:47:42 -0700 Subject: [PATCH 1/2] =?UTF-8?q?fix(render):=20resilient=20entrypoint=20?= =?UTF-8?q?=E2=80=94=20drop=20privileges=20only=20when=20permitted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior fix used `setpriv` to drop from root to the nanobot user, but the Render deploy still exited with status 128 and zero output. Reproduced locally: `setpriv --reuid` needs CAP_SETUID/CAP_SETGID, and when those are not granted it fails ("setresuid failed: Operation not permitted") and exits before ever exec'ing the app — matching the silent pre-Python exit seen on Render. Make the entrypoint resilient and self-diagnosing: - Log the effective user at startup (and each decision) so a failed start is visible in logs instead of a silent 128. - If already non-root, just run the app. - If root, chown the mounted disk, then probe whether setpriv can drop privileges. If it can, drop to nanobot (hardening preserved). If not, run the app as root so it can still write the root-owned disk. Verified locally (amd64) against a root-owned tmpfs disk in both runtimes: full-caps root drops to nanobot; caps-dropped root falls back to root. Both boot clean, ws://0.0.0.0:8765, GET / = 200. Co-Authored-By: Claude Opus 4.8 (1M context) --- entrypoint.sh | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index e0a39acb..d801326e 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,10 +1,29 @@ #!/bin/sh -# Runs as root so we can fix ownership of a freshly-mounted persistent disk -# (Render mounts new disks root-owned) before dropping to the non-root app user. +# Prepares the (possibly root-owned, freshly-mounted) data disk and runs the app +# as the non-root nanobot user when the platform allows it, falling back to root +# only when privilege-dropping is not permitted. Logs each decision so a failed +# start is diagnosable in the platform's logs instead of a silent exit. +echo "[entrypoint] starting as $(id)" + dir="$HOME/.nanobot" -mkdir -p "$dir" -chown -R nanobot:nanobot "$dir" +mkdir -p "$dir" 2>&1 || echo "[entrypoint] warning: mkdir $dir failed" + +if [ "$(id -u)" != "0" ]; then + # Already non-root (platform forced a user). Can't chown; just run. + echo "[entrypoint] not root — running app as $(id -un 2>/dev/null || id -u)" + exec nanobot "$@" +fi + +# Running as root: make the mounted disk writable by the app user. +chown -R nanobot:nanobot "$dir" 2>&1 || echo "[entrypoint] warning: chown $dir failed" + +# Drop to the non-root user if the runtime grants the capability to do so +# (setpriv needs CAP_SETUID/CAP_SETGID). Otherwise stay root so the app can +# still write the root-owned disk. +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 -# Drop privileges to the non-root user and exec the app as PID 1 so signals -# (SIGTERM / graceful shutdown) reach it directly. -exec setpriv --reuid=nanobot --regid=nanobot --init-groups nanobot "$@" +echo "[entrypoint] setpriv privilege-drop not permitted — running app as root" +exec nanobot "$@" From 988a9e2a9c6f08714a4ffbc56bac624d57fe098f Mon Sep 17 00:00:00 2001 From: Ho1yShif Date: Tue, 7 Jul 2026 11:52:47 -0700 Subject: [PATCH 2/2] fix(render): route Docker Command through entrypoint.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render's Docker Command replaces the Dockerfile ENTRYPOINT rather than being appended to it. render.yaml set `dockerCommand: gateway --config ...` assuming it would become `nanobot gateway ...` via the entrypoint, but Render ran `gateway` directly — not a binary (it is the `nanobot gateway` subcommand) — so the container failed at init ("executable file not found in $PATH") and exited with the silent status 128. The entrypoint (and all disk/privilege prep) never ran on any prior deploy. Invoke the entrypoint explicitly in the Docker Command so it runs, prepares the mounted disk, and execs the app. Reproduced locally: `--entrypoint gateway` fails at container init exactly as on Render; routing through `/usr/local/bin/entrypoint.sh gateway --config ...` boots clean, drops to nanobot, GET / = 200. Co-Authored-By: Claude Opus 4.8 (1M context) --- render.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/render.yaml b/render.yaml index 996b1e7d..70abbc2d 100644 --- a/render.yaml +++ b/render.yaml @@ -8,9 +8,11 @@ services: runtime: docker dockerfilePath: ./Dockerfile dockerContext: . - # entrypoint.sh execs `nanobot "$@"`, so this becomes: - # nanobot gateway --config /app/render-config.json - dockerCommand: gateway --config /app/render-config.json + # Render's Docker Command REPLACES the Dockerfile ENTRYPOINT (it is not + # appended to it), so invoke the entrypoint explicitly. The entrypoint + # prepares the mounted disk, drops to the non-root user when permitted, then + # execs `nanobot gateway --config /app/render-config.json`. + dockerCommand: /usr/local/bin/entrypoint.sh gateway --config /app/render-config.json plan: starter healthCheckPath: / envVars: