feat(launch): switch start to docker compose project per bottle #35
Reference in New Issue
Block a user
Delete Branch "chunk-3-compose-lifecycle"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
PRD 0018 chunk 3. Each
claude-bottle startinvocation is now onedocker composeproject per slug.Flow in
launch.py:state/<slug>/{pipelock,egress}/.bottle_plan_to_compose, write tostate/<slug>/docker-compose.yml.docker compose up -d— token + OAuth values flow through subprocess env soenvironment: [NAME]bare-name entries inherit without rendering values into the file.docker exec.DockerBottle.exec_claudestill runsdocker exec -it(resolved TTY question).Teardown:
docker compose logs --no-color --timestampstostate/<slug>/compose.log(best-effort).docker compose downfor the project.metadata.jsongrows acompose_projectfield so dashboard / cleanup / resume tooling can derivedocker compose -p <project> ...invocations without re-deriving the slug.Security follow-ups from chunk-2 review
(b) CA private keys at 0o600. pipelock + egress
ca-key.pemland at 0o600 explicitly. The mitmproxy cert+key concat (mitmproxy-ca.pem) stays 0o644 because the egress container's uid-1000 user reads it through the bind mount; parent dir at 0o700 still restricts host-side reach.(c) Apply atomicity.
egress_apply+pipelock_applyswitch fromdocker cpto host-side write-temp-then-rename on the bind-mount source. POSIX rename is atomic on the same filesystem, so a sidecar SIGHUP racing the apply can't see a half-writtenroutes.yaml/pipelock.yaml.Per-sidecar
start/stopThe PRD planned to delete these in this chunk; in practice the integration test suite drives them directly to validate each sidecar image in isolation, which is still useful coverage.
launch.pyno longer calls them — they're test utilities now. A follow-up chunk can prune if the integration tests move to the compose lifecycle.git-gate chmod fix
The entrypoint's
chmod 600on the keyfile + known_hosts now tolerates EROFS via|| true. SSH already refuses to load keys at anything other than 0600 on the host, so the inside-container chmod was already a no-op in the docker-cp path; it just needs to not error on the read-only bind mount.Status
./cli.py start implementerbrings up the project, attaches, captures full merged logs on teardown tostate/<slug>/compose.log, and reaps all containers + networksPRD 0018 chunk 3. Each instance is now one `docker compose` project: - launch.py renders the compose spec via chunk-1's bottle_plan_to_compose, writes it to state/<slug>/docker-compose.yml, `docker compose up -d`s, and (on teardown) dumps `docker compose logs --no-color --timestamps` to state/<slug>/compose.log before `docker compose down`. - Networks are pre-created (`docker network create --internal` + user-defined bridge) so pipelock yaml can know the internal CIDR before compose-up. Compose references them with `external: true`; the launch step's ExitStack still owns network removal. - Agent still runs `sleep infinity`; claude reaches it via `docker exec -it` exactly like before (per the PRD's resolved TTY question). - metadata.json grows a `compose_project` field so dashboard / cleanup tooling can derive compose invocations without re-deriving the slug. Security follow-ups from chunk-2 review: (b) CA private keys: pipelock + egress ca-key.pem land at 0o600 explicitly. The mitmproxy cert+key concat stays 0o644 because the egress container's uid-1000 user reads it through the bind mount; parent dir at 0o700 still restricts host-side reach. (c) Apply atomicity: egress_apply + pipelock_apply switch from `docker cp` to host-side write-temp-then-rename on the bind-mount source. POSIX rename is atomic on the same filesystem, so a sidecar SIGHUP racing the apply can't see a half-written routes.yaml / pipelock.yaml. Per-sidecar Docker{Sidecar}.start/stop methods stay in place — the integration test suite drives them directly to validate each image in isolation, which is still useful. launch.py no longer calls them; a follow-up chunk can prune if the integration tests move to the compose lifecycle. git-gate entrypoint's chmod 600 on the keyfile + known_hosts now tolerates EROFS (`|| true`) — the host SSH key is already 0600 (SSH refuses to load otherwise), so the inside-container chmod was already a no-op in the docker-cp path and now just needs to not error on the read-only bind mount. 422 unit tests pass; supervise integration test passes; end-to-end `./cli.py start implementer` brings up the project, attaches, captures full merged logs on teardown, and reaps all containers + networks.