`apply_allowlist_change` used `docker restart <bundle>` to make
pipelock reload, which bounced ALL four daemons — including
supervise, whose MCP socket the agent's claude-code client had
open. That dropped the connection. A second apply works because
supervise has come back up by then.
Fix: per-daemon restart via SIGUSR1.
- New `_Supervisor.restart_daemon(name)` terminates one named
child and spawns a replacement in place. Other daemons keep
running.
- main() wires SIGUSR1 → `restart_daemon("pipelock")`. Pipelock
has no in-process reload, so this is its analog of egress's
SIGHUP-reload-addon path. Pipelock is the only daemon that
currently needs hot-config reload via restart; if others
acquire the need, add a new signal.
- `apply_allowlist_change` now `docker kill --signal USR1
<bundle>` instead of `docker restart`. Supervise / egress /
git-gate keep running across the apply.
Tests:
- New `_Supervisor.restart_daemon` cases: replaces in place
(different pid post-restart, sibling daemon unchanged),
unknown name is a no-op, restart-during-shutdown is a no-op.
- `test_pipelock_apply` rewritten to bring up the bundle image
with `CLAUDE_BOTTLE_SIDECAR_DAEMONS=pipelock` so the
supervisor is PID 1 and handles SIGUSR1. The previous
standalone-pipelock setup wouldn't survive SIGUSR1 (pipelock
default disposition is terminate). Test builds the bundle
image in setUpClass (cached layers make repeat runs fast).
531 tests passing locally (unit + integration).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two bugs surfaced when applying an egress route change:
1. egress_apply.py still targeted claude-bottle-egress-<slug> —
the legacy per-sidecar container that no longer exists (it's
a docker-network alias on the bundle now). Switched it to
sidecar_bundle_container_name(slug), matching the chunk-5
fix already made to pipelock_apply.py.
2. `docker kill --signal HUP <bundle>` lands SIGHUP on the
supervisor (PID 1 in the bundle), which previously had no
SIGHUP handler — the signal was ignored. Added
`_Supervisor.forward_signal(sig, daemon_name)` and a SIGHUP
handler in main() that forwards to the egress daemon so
mitmdump's addon reload still works under the bundle.
Tests:
- New _Supervisor.forward_signal cases: forwards to the named
child (Python subprocess as the SIGHUP target — bash trap +
stdout=PIPE deferral interferes with the production-style
test); unknown-daemon name is a no-op.
Stale-reference cleanup (separate issue surfaced while looking
at this):
- claude_bottle/{egress,git_gate,egress_addon,
egress_addon_core,supervise_server}.py: Dockerfile.egress /
Dockerfile.git-gate / Dockerfile.supervise references updated
to Dockerfile.sidecars (the old per-sidecar Dockerfiles were
deleted in PRD 0024 chunk 5).
- tests/README.md: dropped the entry for
test_pipelock_sidecar_smoke (deleted in chunk 3) and added
the new bundle integration tests.
- git_gate.py: stale `DockerGitGate.start via docker cp`
reference (the method was deleted in chunk 3) rewritten to
the bind-mount path the renderer uses now.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reverses chunk 1's "any unexpected child death tears down the
rest" policy. New behavior: a daemon dying is logged but does
NOT initiate shutdown — the surviving daemons keep running and
whatever the dead one served starts failing visibly on the
agent side. The supervisor exits only when (a) it receives
SIGTERM/SIGINT, or (b) every child has died on its own.
Eventual design is restart-the-dead-daemon plus a notification
to the supervise sidecar so the operator sees the event
explicitly; this commit ships only the "log and leave alone"
half. PRD 0024 open question 1 updated to reflect the new
intent.
Tests updated: replaced "crash propagates exit code via
auto-teardown" with three cases that exercise the new policy
(crash without shutdown leaves survivors up, crash-then-signal
surfaces the nonzero code, all-children-die-unattended still
converges the loop).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New Dockerfile.sidecars multi-stage build: pulls the pinned
pipelock and gitleaks binaries into a mitmproxy-base final image,
installs git + openssh-client, and ships the project's egress
addon + supervise server alongside a stdlib-Python init at
/app/sidecar_init.py.
The init supervisor (claude_bottle/sidecar_init.py) is PID 1 in
the bundle. It spawns the daemons named in
CLAUDE_BOTTLE_SIDECAR_DAEMONS (or all four by default),
propagates SIGTERM/SIGINT to children with an 8s grace before
SIGKILL, and exits with the first-unexpected-child exit code so
a daemon crash tears down the bundle (per PRD 0024 open
question 1's default).
claude_bottle/egress_entrypoint.sh extracted verbatim from
Dockerfile.egress's prior inline sh -c so the supervisor can
call it as a normal child.
Tests:
- unit: _selected_daemons env-var subset behavior (7 cases),
_Supervisor signal/exit-code semantics including SIGKILL
escalation, and end-to-end main() via subprocess.
- integration: builds the image and probes that pipelock,
gitleaks, mitmdump, and the supervise Python module are
present + executable, plus a no-daemons-selected smoke test
of the entrypoint wiring. Skipped under act_runner (200+MB
base pulls + multi-stage build).
Renderer collapse and the deletion of Dockerfile.{egress,git-gate,
supervise} land in chunk 2 + 3.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>