fix(gateway): persist git-gate state across gateway restarts
tracker-policy-pr / check-pr (pull_request) Successful in 12s
lint / lint (push) Successful in 59s
test / unit (pull_request) Successful in 53s
test / integration-docker (pull_request) Successful in 1m7s
test / coverage (pull_request) Successful in 19s

Per-bottle git-gate state (bare repos under /git/<id>, deploy creds under
/git-gate/creds/<id>) was provisioned once at bottle launch and lived only
in the gateway's ephemeral storage. A gateway rebuild/restart wiped it and
nothing re-provisioned already-running bottles, so their agents 404'd on
fetch/push. Same class of bug as the CA (#510); the orchestrator restores
only egress tokens, not git-gate declarations.

Persist the state on both backends, mirroring the CA-persistence approach:

- firecracker: attach a second persistent data drive (/dev/vdc) to the
  gateway VM and bind-mount its git/ + creds/ subdirs onto /git and
  /git-gate/creds in the gateway guest init, before the data plane starts.
  Generalize the VM config to a stable-ordered data_drives tuple (CA=vdb,
  git=vdc; orchestrator registry stays vdb).
- docker: bind-mount host dirs (host_gateway_git_dir / creds_dir, under the
  never-pruned app-data root) onto /git and /git-gate/creds, with
  BOT_BOTTLE_DOCKER_GIT_MOUNT / _CREDS_MOUNT env overrides so CI isolates
  them to per-run volumes it cleans up.

Teardown already rm -rf's /git/<id> + creds, so the persistent store
self-cleans over the normal lifecycle.

Closes #512

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 17:34:22 -04:00
parent 0ddaa95c14
commit dff811f190
13 changed files with 213 additions and 27 deletions
+34
View File
@@ -53,6 +53,15 @@ ORCHESTRATOR_AUTH_JWT_ENV = "BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT"
# the shared gateway's TLS interception, so it must not rotate on restart. See
# host_gateway_ca_dir() for why this is a host bind-mount, not a named volume.
GATEWAY_CA_DIRNAME = "gateway-ca"
# The host directories holding the gateway's persistent git-gate state — the
# per-bottle bare repos (`gateway-git`) and deploy creds (`gateway-creds`).
# Bind-mounted into the gateway container at /git and /git-gate/creds so they
# survive container recreation; without it a gateway restart drops every
# already-running bottle's git-gate state and its agent 404s on fetch/push
# (issue #512). Host bind-mounts (not named volumes) for the same reason as the
# CA dir — see host_gateway_ca_dir().
GATEWAY_GIT_DIRNAME = "gateway-git"
GATEWAY_CREDS_DIRNAME = "gateway-creds"
def bot_bottle_root() -> Path:
@@ -97,6 +106,27 @@ def host_gateway_ca_dir() -> Path:
return ca_dir
def host_gateway_git_dir() -> Path:
"""The directory holding the gateway's persistent per-bottle bare repos,
created if missing. Bind-mounted into the gateway container at /git so the
repos survive container recreation (issue #512). A host bind-mount under the
app-data root, never pruned — same rationale as host_gateway_ca_dir()."""
git_dir = bot_bottle_root() / GATEWAY_GIT_DIRNAME
git_dir.mkdir(parents=True, exist_ok=True)
return git_dir
def host_gateway_creds_dir() -> Path:
"""The directory holding the gateway's persistent per-bottle git-gate deploy
creds, created if missing. Bind-mounted into the gateway container at
/git-gate/creds so the creds survive container recreation (issue #512). A
host bind-mount under the app-data root — same rationale as
host_gateway_ca_dir()."""
creds_dir = bot_bottle_root() / GATEWAY_CREDS_DIRNAME
creds_dir.mkdir(parents=True, exist_ok=True)
return creds_dir
def host_signing_key(filename: str) -> str:
"""A per-host signing key at `<root>/<filename>`, minted (256-bit, url-safe)
and persisted 0600 on first use, then reused.
@@ -143,10 +173,14 @@ __all__ = [
"ORCHESTRATOR_TOKEN_ENV",
"ORCHESTRATOR_AUTH_JWT_ENV",
"GATEWAY_CA_DIRNAME",
"GATEWAY_GIT_DIRNAME",
"GATEWAY_CREDS_DIRNAME",
"bot_bottle_root",
"host_db_path",
"host_db_dir",
"host_gateway_ca_dir",
"host_gateway_git_dir",
"host_gateway_creds_dir",
"host_signing_key",
"host_orchestrator_token",
]