From f6c943fcadd9212d5b3ae461af5b6f4db00afd0f Mon Sep 17 00:00:00 2001 From: didericis Date: Fri, 8 May 2026 01:23:42 -0400 Subject: [PATCH] fix(pipelock): write yaml to /etc/pipelock.yaml since image lacks /etc/pipelock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pipelock image is distroless and does not contain /etc/pipelock/, so docker cp to /etc/pipelock/pipelock.yaml fails with "Could not find the file /etc/pipelock in container" — docker cp does not create missing intermediate parent directories when targeting a stopped container, and no shell is available in the image for a mkdir shim. Move the config file to /etc/pipelock.yaml (directly under /etc, which always exists) and update the --config argv to match. Also surface docker cp stderr in the die message so future failures of this sort are debuggable. Assisted-by: Claude Code --- lib/pipelock.sh | 49 ++++++++++++++++++++----------------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/lib/pipelock.sh b/lib/pipelock.sh index ef9291e..0284dbf 100644 --- a/lib/pipelock.sh +++ b/lib/pipelock.sh @@ -271,7 +271,7 @@ pipelock_write_yaml() { # service name. The image runs `pipelock` as its CMD; we override # with `run --config ` and the listen address. # 2. `docker cp` the YAML config from the host mktemp dir into the -# container at /etc/pipelock/pipelock.yaml. +# container at /etc/pipelock.yaml. # # We use docker cp rather than `-v :` because Docker # Desktop bind mounts have ownership / case-sensitivity quirks on @@ -308,29 +308,15 @@ pipelock_start() { die "pipelock yaml not found at ${host_yaml}; pipelock_write_yaml must run first" fi - # Container layout: pipelock reads its config from /etc/pipelock/pipelock.yaml. - # We bring the container up with a `sleep` shim so we can `docker cp` - # the config in, then restart with the real command — this avoids a - # bind mount entirely and keeps the host file off the container's - # filesystem after the cp completes. - # - # Two-phase boot: - # phase 1: `docker run -d --entrypoint sh ... -c 'mkdir -p /etc/pipelock && sleep infinity'` - # so we can cp the YAML in before pipelock starts and - # tries to read it. We do NOT attach to internal_network - # here; we'll connect after the config is in place so the - # real pipelock process never sees a half-configured - # agent on the wire. - # phase 2: docker cp + docker restart with the real command via - # --entrypoint reset (handled below). + # Container layout: pipelock reads its config from /etc/pipelock.yaml. + # We `docker create` the sidecar, `docker cp` the YAML into the + # writable layer, then `docker start` it — no bind mount, no shell + # shim. The image is distroless (no `sh`), and `docker cp` to a + # stopped container does NOT create intermediate parent directories, + # so the YAML lives directly under /etc rather than in a /etc/pipelock + # subdirectory. info "starting pipelock sidecar ${name} on network ${internal_network}" - # We cannot easily restart a container with a different command - # using `docker restart`. Instead, run the container in two stages: - # boot it with `sh -c 'mkdir + sleep'`, cp the file in, then start - # the real pipelock by docker exec'ing it as PID-N. A simpler - # approach: `docker create` + `docker cp` + `docker start`. Use that. - # # Sidecar argv verification (PR #1 review). The pinned digest # (CLAUDE_BOTTLE_PIPELOCK_IMAGE above) has: # ENTRYPOINT ["/pipelock"] @@ -348,18 +334,23 @@ pipelock_start() { --name "$name" \ --network "$internal_network" \ "$CLAUDE_BOTTLE_PIPELOCK_IMAGE" \ - run --config /etc/pipelock/pipelock.yaml --listen "0.0.0.0:${CLAUDE_BOTTLE_PIPELOCK_PORT}" \ + run --config /etc/pipelock.yaml --listen "0.0.0.0:${CLAUDE_BOTTLE_PIPELOCK_PORT}" \ >/dev/null 2>&1; then die "failed to create pipelock sidecar ${name}" fi - # `docker cp` to a created-but-not-started container creates parent - # dirs as needed and works without the container running, since the - # cp is done against the writable layer directly. - if ! docker cp "$host_yaml" "${name}:/etc/pipelock/pipelock.yaml" >/dev/null 2>&1; then + # `docker cp` to a created-but-not-started container writes into the + # writable layer directly. The parent directory must already exist in + # the image — docker cp does NOT create missing intermediate dirs to + # a stopped container, contrary to a common assumption. The pipelock + # image is distroless (no `sh`), so we cannot prepopulate dirs with a + # shell shim either. We therefore put the config in /etc/pipelock.yaml + # (file directly under /etc) rather than /etc/pipelock/pipelock.yaml. + local cp_err + cp_err="$(docker cp "$host_yaml" "${name}:/etc/pipelock.yaml" 2>&1)" || { docker rm -f "$name" >/dev/null 2>&1 || true - die "failed to copy pipelock yaml into ${name}" - fi + die "failed to copy pipelock yaml into ${name}: ${cp_err}" + } # Attach to a per-agent user-defined bridge network for upstream # egress. The internal network has no gateway by definition, so