docs: renumber PRD 0081 -> 0083 across the reconcile chunk
test / unit (pull_request) Successful in 1m1s
lint / lint (push) Successful in 1m9s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / integration-docker (pull_request) Successful in 1m2s
test / image-input-builds (pull_request) Successful in 56s
test / coverage (pull_request) Successful in 17s

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>
This commit is contained in:
2026-07-27 15:15:44 +00:00
parent 343bf89f82
commit 606a57b7ed
21 changed files with 28 additions and 28 deletions
+1 -1
View File
@@ -528,7 +528,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT, AttachTargetT]):
def attach_bottled_agents_to_gateway(self) -> None: def attach_bottled_agents_to_gateway(self) -> None:
"""Reconcile every running bottle against the freshly-(re)booted gateway """Reconcile every running bottle against the freshly-(re)booted gateway
on the cold-boot bring-up path (PRD 0081). The shared flow + fail-hard on the cold-boot bring-up path (PRD 0083). The shared flow + fail-hard
policy live in `gateway_attach`; backends override only the three policy live in `gateway_attach`; backends override only the three
primitives below (ADR 0006).""" primitives below (ADR 0006)."""
from .gateway_attach import reconcile_running_bottles from .gateway_attach import reconcile_running_bottles
+1 -1
View File
@@ -54,7 +54,7 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
def __init__(self, *, infra: "DockerInfraService | None" = None) -> None: def __init__(self, *, infra: "DockerInfraService | None" = None) -> None:
# The infra service whose gateway just cold-booted, passed in on the # The infra service whose gateway just cold-booted, passed in on the
# bring-up reconcile path (PRD 0081) so `_gateway_attach_resources` # bring-up reconcile path (PRD 0083) so `_gateway_attach_resources`
# reads THAT gateway's CA rather than a fresh default-named service — # reads THAT gateway's CA rather than a fresh default-named service —
# otherwise a non-default instance (an isolated integration test's # otherwise a non-default instance (an isolated integration test's
# `-itest-` gateway) reads the default `bot-bottle-orch-gateway`, which # `-itest-` gateway) reads the default `bot-bottle-orch-gateway`, which
+1 -1
View File
@@ -272,7 +272,7 @@ class DockerGateway(Gateway):
if self._control_network: if self._control_network:
self._connect_control_network() self._connect_control_network()
# (Re)created a fresh container — signal a cold boot so the caller # (Re)created a fresh container — signal a cold boot so the caller
# reconciles running bottles against it (PRD 0081). # reconciles running bottles against it (PRD 0083).
return True return True
def _connect_control_network(self) -> None: def _connect_control_network(self) -> None:
+1 -1
View File
@@ -147,7 +147,7 @@ class DockerInfraService(InfraService):
) )
# A (re)created gateway container minted/mounted its CA afresh and lost # A (re)created gateway container minted/mounted its CA afresh and lost
# every bottle's per-bottle state; reconcile the already-running bottles # every bottle's per-bottle state; reconcile the already-running bottles
# against it (PRD 0081). Skipped when a healthy current gateway was left # against it (PRD 0083). Skipped when a healthy current gateway was left
# untouched — no cold boot, nothing to reconcile. # untouched — no cold boot, nothing to reconcile.
if cold_booted: if cold_booted:
from .backend import DockerBottleBackend from .backend import DockerBottleBackend
+2 -2
View File
@@ -83,7 +83,7 @@ def running_agent_containers(
) -> list[str]: ) -> list[str]:
"""Every running agent container on the gateway `network` (the gateway """Every running agent container on the gateway `network` (the gateway
itself excluded) — the bottles the bring-up reconcile attaches to the fresh itself excluded) — the bottles the bring-up reconcile attaches to the fresh
gateway (PRD 0081). Raises `OSError` if `docker` can't be run (fail hard: a gateway (PRD 0083). Raises `OSError` if `docker` can't be run (fail hard: a
reconcile that can't list its bottles must not look like "no bottles").""" reconcile that can't list its bottles must not look like "no bottles")."""
proc = run_docker([ proc = run_docker([
"docker", "network", "inspect", "--format", "docker", "network", "inspect", "--format",
@@ -98,7 +98,7 @@ def running_agent_containers(
def push_ca_to_container(container: str, ca_pem: str) -> None: def push_ca_to_container(container: str, ca_pem: str) -> None:
"""(Re)attach one running agent container to the current gateway: replace """(Re)attach one running agent container to the current gateway: replace
its trusted gateway CA with `ca_pem` and rebuild its trust store via its trusted gateway CA with `ca_pem` and rebuild its trust store via
`docker cp` + `docker exec` (PRD 0081). Unconditional install — there is one `docker cp` + `docker exec` (PRD 0083). Unconditional install — there is one
gateway, so no fingerprint match is needed. gateway, so no fingerprint match is needed.
Fail hard: raises `InfraLaunchError` (naming the container) on any failure — Fail hard: raises `InfraLaunchError` (naming the container) on any failure —
+1 -1
View File
@@ -73,7 +73,7 @@ class FirecrackerInfraService(InfraService):
# The fresh gateway rootfs minted a new CA and lost every bottle's # The fresh gateway rootfs minted a new CA and lost every bottle's
# git-gate/token state; reconcile the already-running bottles against # git-gate/token state; reconcile the already-running bottles against
# it so a gateway rebuild doesn't silently break their egress # it so a gateway rebuild doesn't silently break their egress
# (PRD 0081). Cold-boot only — the adopt fast-paths above never reach # (PRD 0083). Cold-boot only — the adopt fast-paths above never reach
# here, so a healthy current gateway is left untouched. # here, so a healthy current gateway is left untouched.
if cold_booted: if cold_booted:
from .backend import FirecrackerBottleBackend from .backend import FirecrackerBottleBackend
@@ -90,7 +90,7 @@ def persist_env_var_secret(private_key: Path, guest_ip: str, secret: str) -> Non
def attach_ca_to_agent_vm(run_dir: Path, ca_pem: str) -> None: def attach_ca_to_agent_vm(run_dir: Path, ca_pem: str) -> None:
"""(Re)attach one running agent VM (identified by its `run_dir`) to the """(Re)attach one running agent VM (identified by its `run_dir`) to the
current gateway: replace its trusted gateway CA with `ca_pem` and rebuild current gateway: replace its trusted gateway CA with `ca_pem` and rebuild
its trust store over SSH (PRD 0081). Unconditional install there is one its trust store over SSH (PRD 0083). Unconditional install there is one
gateway, so no fingerprint match is needed. Bare-pipe input keeps the cert gateway, so no fingerprint match is needed. Bare-pipe input keeps the cert
off argv. off argv.
+2 -2
View File
@@ -1,4 +1,4 @@
"""The shared gateway-attach reconcile flow (PRD 0081). """The shared gateway-attach reconcile flow (PRD 0083).
`BottleBackend.attach_bottled_agents_to_gateway()` delegates here so every `BottleBackend.attach_bottled_agents_to_gateway()` delegates here so every
backend reconciles running bottles against a freshly-booted gateway *the same backend reconciles running bottles against a freshly-booted gateway *the same
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
@dataclass(frozen=True) @dataclass(frozen=True)
class GatewayAttachResources: class GatewayAttachResources:
"""Everything a running bottle needs to (re)attach to the current gateway on """Everything a running bottle needs to (re)attach to the current gateway on
a cold boot (PRD 0081). Gathered once per reconcile and handed to every a cold boot (PRD 0083). Gathered once per reconcile and handed to every
bottle. The CA now; egress secrets and git-gate state join as the reconcile bottle. The CA now; egress secrets and git-gate state join as the reconcile
grows (#516).""" grows (#516)."""
@@ -112,7 +112,7 @@ class MacosGateway(Gateway):
Always returns True: this recreates the gateway container unconditionally Always returns True: this recreates the gateway container unconditionally
(a cold boot), so the caller reconciles running bottles against it (a cold boot), so the caller reconciles running bottles against it
(PRD 0081).""" (PRD 0083)."""
self._orchestrator_url = orchestrator_url self._orchestrator_url = orchestrator_url
self._gateway_token = gateway_token self._gateway_token = gateway_token
# Fail closed on a missing policy source or token: the resolver-only data # Fail closed on a missing policy source or token: the resolver-only data
+1 -1
View File
@@ -142,7 +142,7 @@ class MacosInfraService(InfraService):
url, orchestrator.mint_gateway_token()) url, orchestrator.mint_gateway_token())
# A (re)created gateway container minted/mounted its CA afresh and lost # A (re)created gateway container minted/mounted its CA afresh and lost
# every bottle's per-bottle state; reconcile the already-running bottles # every bottle's per-bottle state; reconcile the already-running bottles
# against it (PRD 0081). Skipped when a healthy current gateway was left # against it (PRD 0083). Skipped when a healthy current gateway was left
# untouched — no cold boot, nothing to reconcile. # untouched — no cold boot, nothing to reconcile.
if cold_booted: if cold_booted:
from .backend import MacosContainerBottleBackend from .backend import MacosContainerBottleBackend
@@ -99,7 +99,7 @@ def ensure_gateway(
def running_agent_containers() -> list[str]: def running_agent_containers() -> list[str]:
"""Every running agent container's name — the bottles the bring-up reconcile """Every running agent container's name — the bottles the bring-up reconcile
attaches to the fresh gateway (PRD 0081). Raises `EnumerationError` if the attaches to the fresh gateway (PRD 0083). Raises `EnumerationError` if the
live set can't be determined (fail hard rather than reconcile a partial live set can't be determined (fail hard rather than reconcile a partial
set).""" set)."""
return [f"{CONTAINER_NAME_PREFIX}{agent.slug}" for agent in enumerate_active()] return [f"{CONTAINER_NAME_PREFIX}{agent.slug}" for agent in enumerate_active()]
@@ -108,7 +108,7 @@ def running_agent_containers() -> list[str]:
def push_ca_to_container(name: str, ca_pem: str) -> None: def push_ca_to_container(name: str, ca_pem: str) -> None:
"""(Re)attach one running agent container to the current gateway: replace """(Re)attach one running agent container to the current gateway: replace
its trusted gateway CA with `ca_pem` and rebuild its trust store via its trusted gateway CA with `ca_pem` and rebuild its trust store via
`container cp` + `container exec` (PRD 0081). Unconditional install there `container cp` + `container exec` (PRD 0083). Unconditional install there
is one gateway, so no fingerprint match is needed. is one gateway, so no fingerprint match is needed.
Fail hard: raises `InfraLaunchError` (naming the container) on any failure Fail hard: raises `InfraLaunchError` (naming the container) on any failure
+1 -1
View File
@@ -133,7 +133,7 @@ class Gateway(abc.ABC):
so its mitmproxy minted a fresh CA and lost its per-bottle state), False so its mitmproxy minted a fresh CA and lost its per-bottle state), False
when a healthy current gateway was left untouched. The bring-up flow when a healthy current gateway was left untouched. The bring-up flow
uses this as the cold-boot signal that gates the running-bottle uses this as the cold-boot signal that gates the running-bottle
reconcile (`attach_bottled_agents_to_gateway`, PRD 0081).""" reconcile (`attach_bottled_agents_to_gateway`, PRD 0083)."""
@abc.abstractmethod @abc.abstractmethod
def is_running(self) -> bool: def is_running(self) -> bool:
@@ -15,7 +15,7 @@ macOS-container). Cross-backend operations can be expressed two ways:
concrete method and each backend overrides small **primitives** it calls. concrete method and each backend overrides small **primitives** it calls.
Form 1 gives each backend total freedom, which is exactly the problem: the three 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 implementations drift. The bring-up reconcile (PRD 0083) first shipped as form 1
and immediately diverged — each backend re-implemented "gather CA → list running and immediately diverged — each backend re-implemented "gather CA → list running
bottles → push to each", and one backend's copy silently skipped failures while 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 another's aborted, an inconsistency caught only in review (#519). The behaviour
@@ -37,7 +37,7 @@ method that:
- delegates only the irreducibly backend-specific steps to `@abstractmethod` - delegates only the irreducibly backend-specific steps to `@abstractmethod`
primitives with narrow, well-typed signatures. primitives with narrow, well-typed signatures.
The first application is `attach_bottled_agents_to_gateway()` (PRD 0081): the The first application is `attach_bottled_agents_to_gateway()` (PRD 0083): the
base gathers resources once, attempts every running bottle, and raises an base gathers resources once, attempts every running bottle, and raises an
aggregate `InfraLaunchError` if any failed; backends supply only aggregate `InfraLaunchError` if any failed; backends supply only
`_gateway_attach_resources()`, `_running_bottles()`, and `_gateway_attach_resources()`, `_running_bottles()`, and
@@ -60,6 +60,6 @@ operations whose *shape* should be uniform, not about banning abstract methods.
## Links ## Links
- PRD 0081 (`docs/prds/0081-reprovision-gateway-state-on-bringup.md`). - PRD 0083 (`docs/prds/0083-reprovision-gateway-state-on-bringup.md`).
- Review that prompted this: PR #519. - Review that prompted this: PR #519.
- `bot_bottle/backend/base.py` (`BottleBackend.attach_bottled_agents_to_gateway`). - `bot_bottle/backend/base.py` (`BottleBackend.attach_bottled_agents_to_gateway`).
+1 -1
View File
@@ -85,7 +85,7 @@ class TestRuntimeModuleSizes(unittest.TestCase):
def test_backend_contract_does_not_absorb_preparation_logic(self) -> None: def test_backend_contract_does_not_absorb_preparation_logic(self) -> None:
# base.py holds the backend *contract*; implementation lives elsewhere. # base.py holds the backend *contract*; implementation lives elsewhere.
# The cap is a tripwire against absorbing preparation/impl logic, not a # The cap is a tripwire against absorbing preparation/impl logic, not a
# ban on new contract surface — bumped 580->615 for the PRD 0081 # ban on new contract surface — bumped 580->615 for the PRD 0083
# gateway-attach contract (delegator + 3 abstract primitives; the flow # gateway-attach contract (delegator + 3 abstract primitives; the flow
# itself lives in backend/gateway_attach.py, not here). # itself lives in backend/gateway_attach.py, not here).
caps = { caps = {
@@ -162,7 +162,7 @@ class TestFirecrackerReprovision(unittest.TestCase):
class TestAttachFlow(unittest.TestCase): class TestAttachFlow(unittest.TestCase):
"""PRD 0081 / #519 review: the base backend owns the reconcile flow and """PRD 0083 / #519 review: the base backend owns the reconcile flow and
fails hard gather resources, attempt every running bottle, then raise an fails hard gather resources, attempt every running bottle, then raise an
aggregate if any failed (never silently skip).""" aggregate if any failed (never silently skip)."""
@@ -208,7 +208,7 @@ class TestAttachFlow(unittest.TestCase):
class TestFirecrackerAttachPrimitive(unittest.TestCase): class TestFirecrackerAttachPrimitive(unittest.TestCase):
"""PRD 0081: attach_ca_to_agent_vm pushes the current gateway CA into one """PRD 0083: attach_ca_to_agent_vm pushes the current gateway CA into one
running agent VM over SSH, failing hard.""" running agent VM over SSH, failing hard."""
def _run_dir(self, root: Path, ip: str = "10.243.0.3") -> Path: def _run_dir(self, root: Path, ip: str = "10.243.0.3") -> Path:
+1 -1
View File
@@ -70,7 +70,7 @@ class TestDockerInfraService(unittest.TestCase):
def test_cold_boot_reconciles_running_bottles(self) -> None: def test_cold_boot_reconciles_running_bottles(self) -> None:
# A (re)created gateway (connect returns True) triggers the bring-up # A (re)created gateway (connect returns True) triggers the bring-up
# reconcile so running bottles re-trust the fresh gateway (PRD 0081). # reconcile so running bottles re-trust the fresh gateway (PRD 0083).
self.gw.connect_to_orchestrator.return_value = True self.gw.connect_to_orchestrator.return_value = True
self.svc.ensure_running() self.svc.ensure_running()
self.attach.assert_called_once_with() self.attach.assert_called_once_with()
+1 -1
View File
@@ -63,7 +63,7 @@ class TestFirecrackerGatewayConnect(unittest.TestCase):
patch.object(infra_vm, "push_secret") as push: patch.object(infra_vm, "push_secret") as push:
cold_booted = gw.connect_to_orchestrator(_ORCH_URL, _TOKEN) cold_booted = gw.connect_to_orchestrator(_ORCH_URL, _TOKEN)
# The VM is booted fresh here (cold-boot path only), so it always # The VM is booted fresh here (cold-boot path only), so it always
# signals a cold boot → the caller reconciles running bottles (PRD 0081). # signals a cold boot → the caller reconciles running bottles (PRD 0083).
self.assertTrue(cold_booted) self.assertTrue(cold_booted)
# Booted on the gateway link with the gateway role; the orchestrator's # Booted on the gateway link with the gateway role; the orchestrator's
# guest IP (parsed off the URL) rides the cmdline as bb_orch. # guest IP (parsed off the URL) rides the cmdline as bb_orch.
+1 -1
View File
@@ -49,7 +49,7 @@ _ATTACH = (
class TestEnsureRunning(unittest.TestCase): class TestEnsureRunning(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
# The cold-boot branch reconciles running bottles against the fresh # The cold-boot branch reconciles running bottles against the fresh
# gateway (PRD 0081). Stub it so these composition tests stay isolated # gateway (PRD 0083). Stub it so these composition tests stay isolated
# from SSH; the wiring itself is asserted in test_cold_boot_reconciles*. # from SSH; the wiring itself is asserted in test_cold_boot_reconciles*.
ap = patch(_ATTACH) ap = patch(_ATTACH)
self.attach = ap.start() self.attach = ap.start()
+1 -1
View File
@@ -46,7 +46,7 @@ class TestMacosGatewayConnect(unittest.TestCase):
def test_connect_signals_cold_boot(self) -> None: def test_connect_signals_cold_boot(self) -> None:
# The container is recreated unconditionally, so connect always signals # The container is recreated unconditionally, so connect always signals
# a cold boot → the caller reconciles running bottles (PRD 0081). # a cold boot → the caller reconciles running bottles (PRD 0083).
run = Mock(return_value=_proc()) run = Mock(return_value=_proc())
with patch(f"{_GW}.container_mod") as mod, \ with patch(f"{_GW}.container_mod") as mod, \
patch(f"{_GW}.host_gateway_ca_dir", return_value=Path("/ca")): patch(f"{_GW}.host_gateway_ca_dir", return_value=Path("/ca")):
+1 -1
View File
@@ -58,7 +58,7 @@ class TestInfraEnsureRunning(unittest.TestCase):
def test_cold_boot_reconciles_running_bottles(self) -> None: def test_cold_boot_reconciles_running_bottles(self) -> None:
# A (re)created gateway (connect returns True) triggers the bring-up # A (re)created gateway (connect returns True) triggers the bring-up
# reconcile so running bottles re-trust the fresh gateway (PRD 0081). # reconcile so running bottles re-trust the fresh gateway (PRD 0083).
self.gw.connect_to_orchestrator.return_value = True self.gw.connect_to_orchestrator.return_value = True
with patch(f"{_INFRA}.ensure_networks"): with patch(f"{_INFRA}.ensure_networks"):
self.svc.ensure_running() self.svc.ensure_running()
+2 -2
View File
@@ -90,7 +90,7 @@ class TestDockerGateway(unittest.TestCase):
self.assertEqual([], [c for c in calls if c[:2] == ["docker", "run"]]) self.assertEqual([], [c for c in calls if c[:2] == ["docker", "run"]])
self.assertEqual([], [c for c in calls if c[:2] == ["docker", "rm"]]) self.assertEqual([], [c for c in calls if c[:2] == ["docker", "rm"]])
# A healthy current gateway left untouched is not a cold boot — the # A healthy current gateway left untouched is not a cold boot — the
# bring-up flow skips the running-bottle reconcile (PRD 0081). # bring-up flow skips the running-bottle reconcile (PRD 0083).
self.assertFalse(cold_booted) self.assertFalse(cold_booted)
def test_ensure_running_recreates_when_image_is_stale(self) -> None: def test_ensure_running_recreates_when_image_is_stale(self) -> None:
@@ -113,7 +113,7 @@ class TestDockerGateway(unittest.TestCase):
self.assertEqual(1, len([c for c in calls if c[:2] == ["docker", "run"]])) self.assertEqual(1, len([c for c in calls if c[:2] == ["docker", "run"]]))
self.assertTrue(any(c[:2] == ["docker", "rm"] for c in calls)) self.assertTrue(any(c[:2] == ["docker", "rm"] for c in calls))
# A recreated container minted/mounted its CA afresh — a cold boot that # A recreated container minted/mounted its CA afresh — a cold boot that
# triggers the running-bottle reconcile (PRD 0081). # triggers the running-bottle reconcile (PRD 0083).
self.assertTrue(cold_booted) self.assertTrue(cold_booted)
def test_ensure_running_starts_the_singleton_when_absent(self) -> None: def test_ensure_running_starts_the_singleton_when_absent(self) -> None: