feat(orchestrator): slice 4 — consolidated per-host sidecar #358

Open
didericis-claude wants to merge 2 commits from orchestrator-slice4 into orchestrator-slice3
Collaborator

Slice 4 of PRD 0070, stacked on #357. The core consolidation win: one persistent sidecar per host, shared by every bottle, instead of a sidecar bundle per bottle — safe because the attribution invariant (source IP + identity token) lets the sidecar map each request to the right bottle.

  • sidecar.py — a backend-neutral Sidecar lifecycle contract (mirrors LaunchBroker) + a DockerSidecar. Defining behaviour is idempotent singleton: ensure_running starts the instance if absent and is a no-op if it's already up (N launches never spawn N sidecars); stop is idempotent.
  • dockerutil.py — a shared run_docker helper; DockerBroker now uses it too (DRY with slice 3).
  • service.py — the Orchestrator holds an optional Sidecar, exposes ensure_sidecar() + sidecar_status(); GET /sidecar reports it; __main__ --sidecar-image ensures the single sidecar on startup.

Tests: unit (docker mocked) — is_running, ensure idempotent (no-op when up / starts when absent), failure raises, stop idempotent; Orchestrator sidecar wiring/status; control-plane /sidecar; integration (gated) — a real idempotent singleton (one container after two ensures), stop removes. Full suite green (only pre-existing /bin/sleep errors); integration verified locally against real docker.

Merge order: #352#356#357 → this.

🤖 Generated with Claude Code

Slice 4 of PRD 0070, **stacked on #357**. The core consolidation win: **one persistent sidecar per host**, shared by every bottle, instead of a sidecar bundle per bottle — safe because the attribution invariant (source IP + identity token) lets the sidecar map each request to the right bottle. - **`sidecar.py`** — a backend-neutral `Sidecar` lifecycle contract (mirrors `LaunchBroker`) + a `DockerSidecar`. Defining behaviour is **idempotent singleton**: `ensure_running` starts the instance if absent and is a no-op if it's already up (N launches never spawn N sidecars); `stop` is idempotent. - **`dockerutil.py`** — a shared `run_docker` helper; `DockerBroker` now uses it too (DRY with slice 3). - **`service.py`** — the Orchestrator holds an optional `Sidecar`, exposes `ensure_sidecar()` + `sidecar_status()`; **`GET /sidecar`** reports it; `__main__ --sidecar-image` ensures the single sidecar on startup. **Tests:** unit (docker mocked) — is_running, ensure idempotent (no-op when up / starts when absent), failure raises, stop idempotent; Orchestrator sidecar wiring/status; control-plane `/sidecar`; integration (gated) — a real idempotent singleton (one container after two ensures), stop removes. Full suite green (only pre-existing `/bin/sleep` errors); **integration verified locally against real docker**. Merge order: #352 → #356 → #357 → this. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
didericis-claude added 1 commit 2026-07-13 15:28:44 -04:00
feat(orchestrator): slice 4 — consolidated per-host sidecar (#352)
lint / lint (push) Successful in 1m58s
test / unit (pull_request) Successful in 58s
test / integration (pull_request) Successful in 18s
test / coverage (pull_request) Successful in 1m7s
f85cbdeebf
The core consolidation win: one persistent sidecar per host, shared by
every bottle, instead of a sidecar bundle per bottle. Safe to share
because the attribution invariant (source IP + identity token) lets the
sidecar map each request to the right bottle.

  * orchestrator/sidecar.py — a backend-neutral `Sidecar` lifecycle
    contract (mirrors LaunchBroker) + a `DockerSidecar` impl. The defining
    behaviour is idempotent singleton: `ensure_running` starts the instance
    if absent and is a no-op if it's already up, so N launches never spawn
    N sidecars; `stop` is idempotent.
  * orchestrator/dockerutil.py — a shared `run_docker` helper; DockerBroker
    now uses it too (DRY with slice 3).
  * service.py — the Orchestrator holds an optional `Sidecar`, exposes
    `ensure_sidecar()` + `sidecar_status()`.
  * control_plane.py — `GET /sidecar` reports it; __main__ gains
    `--sidecar-image` and ensures the single sidecar on startup.

Tests: unit (docker mocked) — is_running, ensure idempotent (no-op when up,
starts when absent), failure raises, stop idempotent; Orchestrator sidecar
wiring/status; control-plane /sidecar; integration (gated) — ensure is a
real idempotent singleton (one container after two ensures), stop removes.
Full suite green (only pre-existing /bin/sleep errors); integration
verified locally against real docker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
didericis reviewed 2026-07-13 16:18:55 -04:00
@@ -0,0 +6,4 @@
import subprocess
def run_docker(argv: list[str]) -> subprocess.CompletedProcess[str]:
Owner

Why not use the docker_mod util? Are there too many deps in it/does it create circular deps? If so that's a bit of a smell, this should really be placed there.

Why not use the docker_mod util? Are there too many deps in it/does it create circular deps? If so that's a bit of a smell, this should really be placed there.
didericis added 1 commit 2026-07-13 16:24:40 -04:00
refactor(orchestrator): move run_docker to a lean top-level docker_cmd (#352)
lint / lint (push) Successful in 2m2s
test / unit (pull_request) Successful in 1m0s
test / integration (pull_request) Successful in 18s
test / coverage (pull_request) Successful in 1m7s
67652899eb
Review feedback: don't bury a parallel docker util in the orchestrator.
But reusing backend.docker.util (docker_mod) isn't right either — importing
it runs backend/__init__.py, which eagerly loads all three backends
(docker + firecracker + macos) plus the manifest/egress/git-gate/supervise
framework (~76 modules), so every orchestrator import would drag the whole
backend layer in.

Compromise: promote the helper to a top-level, framework-free
bot_bottle/docker_cmd.py (single stdlib import), a proper shared home the
orchestrator's docker components use now and backend.docker.util can adopt
later. Verified `import bot_bottle.orchestrator` stays lean (12 modules, no
firecracker/macos backends).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
Author
Collaborator

Good instinct, and I dug in. docker_mod (backend.docker.util) is lean itself (only imports ...log), but importing it isn't cheap: it runs backend/__init__.py, which eagerly imports all three bottle backends (docker + firecracker + macos) plus the manifest/egress/git-gate/supervise/workspace framework. Measured: from bot_bottle.backend.docker import util pulls in 76 bot_bottle modules, including the entire firecracker and macos backends.

Since orchestrator/__init__ imports DockerBroker/DockerSidecar, routing them through docker_mod would make every orchestrator import (even the lean registry/control-plane tests) load all three backends — coupling the deliberately-lean orchestrator to the whole backend layer for a 3-line subprocess wrapper. No circular dep, but real weight.

So you're right it's a smell — but the smell is the eager all-backends import in backend/__init__.py, not the helper. Rather than pick between "parallel util" and "drag in the backend framework", I promoted it to a top-level, framework-free bot_bottle/docker_cmd.py (single stdlib import) — a proper shared home the orchestrator uses now and backend.docker.util can adopt later (6765289). Verified import bot_bottle.orchestrator stays lean afterward: 12 modules, no firecracker/macos backends.

Two follow-ups worth a separate cleanup if you want them: (a) make backend/__init__.py lazy-load the backends so docker_mod is cheap to import anywhere, and (b) then thread backend.docker.util's own subprocess.run(["docker", ...]) calls through docker_cmd.run_docker for full DRY.

Good instinct, and I dug in. `docker_mod` (`backend.docker.util`) is lean *itself* (only imports `...log`), but importing it isn't cheap: it runs `backend/__init__.py`, which **eagerly imports all three bottle backends** (docker + firecracker + macos) plus the manifest/egress/git-gate/supervise/workspace framework. Measured: `from bot_bottle.backend.docker import util` pulls in **76 `bot_bottle` modules**, including the entire firecracker and macos backends. Since `orchestrator/__init__` imports `DockerBroker`/`DockerSidecar`, routing them through `docker_mod` would make *every* orchestrator import (even the lean registry/control-plane tests) load all three backends — coupling the deliberately-lean orchestrator to the whole backend layer for a 3-line `subprocess` wrapper. No circular dep, but real weight. So you're right it's a smell — but the smell is the eager all-backends import in `backend/__init__.py`, not the helper. Rather than pick between "parallel util" and "drag in the backend framework", I promoted it to a **top-level, framework-free `bot_bottle/docker_cmd.py`** (single stdlib import) — a proper shared home the orchestrator uses now and `backend.docker.util` can adopt later (`6765289`). Verified `import bot_bottle.orchestrator` stays lean afterward: **12 modules, no firecracker/macos backends**. Two follow-ups worth a separate cleanup if you want them: (a) make `backend/__init__.py` lazy-load the backends so `docker_mod` is cheap to import anywhere, and (b) then thread `backend.docker.util`'s own `subprocess.run(["docker", ...])` calls through `docker_cmd.run_docker` for full DRY.
Owner

@didericis-claude file an issue for those follow ups (can put in the same issue)

@didericis-claude file an issue for those follow ups (can put in the same issue)
Author
Collaborator

Filed both in #359 (one issue, two ordered parts): (1) make backend/__init__.py lazy-load the backends so importing a backend util doesn't drag in all three backends (~76 modules), then (2) DRY backend.docker.util's docker calls onto docker_cmd.run_docker. Neither blocks the orchestrator stack.

Filed both in **#359** (one issue, two ordered parts): (1) make `backend/__init__.py` lazy-load the backends so importing a backend util doesn't drag in all three backends (~76 modules), then (2) DRY `backend.docker.util`'s docker calls onto `docker_cmd.run_docker`. Neither blocks the orchestrator stack.
didericis approved these changes 2026-07-13 16:46:47 -04:00
Some checks are pending
lint / lint (push) Successful in 2m2s
test / unit (pull_request) Successful in 1m0s
test / integration (pull_request) Successful in 18s
test / coverage (pull_request) Successful in 1m7s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin orchestrator-slice4:orchestrator-slice4
git checkout orchestrator-slice4
Sign in to join this conversation.