Addresses review on #519 (@didericis 6143 + the fail-hard direction over the codex skip). Base owns the flow (6143 / ADR 0006). `attach_bottled_agents_to_gateway()` is now a concrete method on `BottleBackend` that delegates to `gateway_attach.reconcile_running_bottles`; backends override only three primitives — `_gateway_attach_resources()`, `_running_bottles()`, `_attach_bottle_to_gateway()`. The shared control flow + error policy live in one place so a backend can't drift onto a bespoke loop or a silent skip. Keeps the reconcile flow + `GatewayAttachResources` out of `backend/base.py` (the size guardrail) in a dedicated `backend/gateway_attach.py`. New ADR 0006 records the "shared behaviour in the base backend, subclasses override primitives" theme. Fail hard, never skip (the maintainer's direction over the codex review's skip). Any attach failure now aborts bring-up instead of being logged and skipped: a bottle that silently can't reach the fresh gateway (its egress just starts failing TLS) is worse than a loud failure. Every bottle is attempted and the failures are raised together (aggregate `InfraLaunchError`) so one bring-up surfaces the full blast radius. Resource gathering (the CA fetch) also fails hard. PRD 0081 goal + design updated to match (reversed from the earlier "tolerate per-bottle failures" draft). Bumps the base.py guardrail cap 580->600 for the new contract surface (the delegator + 3 abstract primitives); the flow itself lives in gateway_attach.py. Refs #516, #519. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2.9 KiB
ADR 0006: Shared behaviour lives in the base backend
- Status: Accepted
- Date: 2026-07-27
- Deciders: didericis
Context
BottleBackend has three concrete implementations (Firecracker, docker,
macOS-container). Cross-backend operations can be expressed two ways:
- Abstract method per backend — the base declares
@abstractmethod foo()and each backend writes its ownfoo()end to end. - Template method — the base owns the control flow of
foo()as a concrete method and each backend overrides small primitives it calls.
Form 1 gives each backend total freedom, which is exactly the problem: the three implementations drift. The bring-up reconcile (PRD 0081) first shipped as form 1 and immediately diverged — each backend re-implemented "gather CA → list running bottles → push to each", and one backend's copy silently skipped failures while another's aborted, an inconsistency caught only in review (#519). The behaviour that must be identical (the loop, the error policy, the ordering) was duplicated in the place least likely to stay in sync.
Decision
Prefer the template-method form for cross-backend behaviour: encode shared control flow and policy in the base backend; backends override primitives, not control flow. Lean toward less freedom in the concrete backends and more consistency in the base.
Concretely, a cross-backend operation on BottleBackend should be a concrete
method that:
- owns the sequencing, iteration, and error policy (e.g. fail-hard vs. best-effort, fail-fast vs. attempt-all-then-aggregate); and
- delegates only the irreducibly backend-specific steps to
@abstractmethodprimitives with narrow, well-typed signatures.
The first application is attach_bottled_agents_to_gateway() (PRD 0081): the
base gathers resources once, attempts every running bottle, and raises an
aggregate InfraLaunchError if any failed; backends supply only
_gateway_attach_resources(), _running_bottles(), and
_attach_bottle_to_gateway().
A backend-specific @abstractmethod with no shared control flow (e.g.
_launch_impl, enumerate_active) stays as form 1 — this ADR is about
operations whose shape should be uniform, not about banning abstract methods.
Consequences
- The error policy, ordering, and loop for a cross-backend operation have one source of truth; a new backend cannot forget them or diverge.
- New backends implement smaller, more focused primitives instead of re-deriving a whole flow.
- The base carries more logic and needs generic-enough primitive signatures
(e.g. a backend-specific attach-target type parameter on
BottleBackend). - Genuinely backend-specific one-offs still use a plain abstract method; the guidance is a default, not an absolute.
Links
- PRD 0081 (
docs/prds/0081-reprovision-gateway-state-on-bringup.md). - Review that prompted this: PR #519.
bot_bottle/backend/base.py(BottleBackend.attach_bottled_agents_to_gateway).