From 4d22171d16cb36b8e5f4a2c585c62b2b24632ff1 Mon Sep 17 00:00:00 2001 From: Timmy Date: Wed, 15 Jul 2026 16:06:31 +0100 Subject: [PATCH] Install sled binary in huskies-owned dir so upgrades work without root /opt/huskies/bin/huskies (chowned to the huskies user) with a symlink from /usr/local/bin/huskies. Atomic replace needs write permission on the directory for the tmp-write + rename, which root-owned /usr/local/bin can't provide to the server process. resolve_target_path() now prefers /opt/huskies/bin/huskies over current_exe(), which can point at a stale location (e.g. /workspace/target/release/huskies after a historical in-container rebuild) that the entrypoint would never launch after a restart. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019fHdm92yjvguPi2LiXfLB9 --- docker/Dockerfile.base | 9 ++++++++- server/src/upgrade.rs | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base index 23ad9813..03093569 100644 --- a/docker/Dockerfile.base +++ b/docker/Dockerfile.base @@ -33,7 +33,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # Copy the huskies binary and entrypoint from the main image. -COPY --from=huskies-src /usr/local/bin/huskies /usr/local/bin/huskies +# The binary lives in /opt/huskies/bin (chowned to the huskies user below) so +# the in-process upgrade path (`POST /api/upgrade`) can atomically replace it +# without root: rename() needs write permission on the *directory*, not just +# the file. /usr/local/bin/huskies stays a symlink so PATH resolution and the +# entrypoint CMD are unchanged. +COPY --from=huskies-src /usr/local/bin/huskies /opt/huskies/bin/huskies COPY --from=huskies-src /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint.sh # Non-root user — Claude Code refuses --dangerously-skip-permissions as root. @@ -44,6 +49,8 @@ RUN groupadd -r huskies \ && mkdir -p /home/huskies/.ssh \ && chmod 700 /home/huskies/.ssh \ && chown -R huskies:huskies /home/huskies \ + && chown -R huskies:huskies /opt/huskies \ + && ln -s /opt/huskies/bin/huskies /usr/local/bin/huskies \ && mkdir -p /workspace \ && chown huskies:huskies /workspace \ && git config --global init.defaultBranch master \ diff --git a/server/src/upgrade.rs b/server/src/upgrade.rs index a78aacca..32aaa996 100644 --- a/server/src/upgrade.rs +++ b/server/src/upgrade.rs @@ -94,9 +94,22 @@ pub async fn run_cli_upgrade(source_url: &str, target: &Path) -> Result<(), Stri // ── Helpers ─────────────────────────────────────────────────────────────── -/// Resolve the path to replace: `current_exe()` if accessible, else -/// `/usr/local/bin/huskies`. +/// Resolve the path to replace with the new binary. +/// +/// Inside project containers the binary is installed at +/// `/opt/huskies/bin/huskies` (a huskies-owned directory, so the atomic +/// tmp-write + rename works without root) and `/usr/local/bin/huskies` is a +/// symlink to it. That path is preferred over `current_exe()` because +/// `current_exe()` can point at a stale location — e.g. +/// `/workspace/target/release/huskies` after a historical in-container +/// rebuild — which the entrypoint would never launch after a restart. +/// +/// Outside containers (no `/opt/huskies`), falls back to `current_exe()`. pub fn resolve_target_path() -> PathBuf { + let container_path = PathBuf::from("/opt/huskies/bin/huskies"); + if container_path.exists() { + return container_path; + } std::env::current_exe().unwrap_or_else(|_| PathBuf::from("/usr/local/bin/huskies")) }