Follows the base PR's renumber (0081 was already taken; main is on 0082). Update every `PRD 0081` reference in the gateway-attach code, tests, and ADR 0006, plus ADR 0006's link to the PRD file. No behaviour change. 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 0083) 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 0083): 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 0083 (
docs/prds/0083-reprovision-gateway-state-on-bringup.md). - Review that prompted this: PR #519.
bot_bottle/backend/base.py(BottleBackend.attach_bottled_agents_to_gateway).