refactor: repo reorganization for clearer separation of concerns #477
Reference in New Issue
Block a user
Delete Branch "refactor/repo-reorg"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #444
Umbrella branch for a broad, behavior-preserving reorganization of the repo. The goal is to make the tree easier to parse and to make separation of concerns something you can see in the layout and enforce, rather than infer from scattered modules.
Why
The package root has accumulated loose modules that belong to distinct concerns (persistence, gateway data plane, orchestration), and some concerns have drifted into more than one shape. Recent review churn on #471 made the cost concrete: the control-plane auth provisioning was re-derived per backend and failed review three times (see #476). Before the substantive backend-uniformity work, group the code so each concern has one obvious home and cross-concern coupling is visible at the import site.
Landed so far
DockerGatewaynow lives inbackend/docker/gateway.py— the shape the other backends' gateway classes will share — whileorchestrator/gateway.pykeeps only the backend-neutralGatewaylifecycle ABC + constants. Deleted the dead standalone-gateway path (--gateway,Orchestrator.ensure_gateway, thegatewayctor arg) that this class was the sole consumer of, and fixed a latent bug it surfaced: the shared-CA read targeted a container name (bot-bottle-orch-gateway) the consolidated flow never creates — now reads frombot-bottle-infra.bot_bottle.storepackage. Consolidated the SQLite store family — theDbStorebase, themigrationsschema base, the concreteaudit/config/queuestores, andStoreManager— out of the package root intobot_bottle/store/. Callers use direct submodule imports; the orchestrator's own stores stay inorchestrator/and repoint to the moved base.Planned (this branch)
gatewayJWT → data-plane daemons) so docker / macOS / firecracker stop hand-rolling it, and realGatewayimpls for the VM backends.No behavior change; full unit suite green (2251 tests).
🤖 Generated with Claude Code
Separate the gateway (data plane) from the orchestrator (control plane) at the module level. The gateway runtime files move out of the package root — and the backend-neutral Gateway lifecycle ABC + GATEWAY_* constants move out of orchestrator/ — into a new bot_bottle/gateway/ package: gateway/__init__.py (was orchestrator/gateway.py: Gateway ABC + consts + rotate_gateway_ca) gateway/gateway_init.py (the PID-1 daemon supervisor) gateway/egress_addon.py, egress_addon_core.py, egress_dlp_config.py, dlp_detectors.py (the egress mitmproxy daemon) gateway/git_http_backend.py (the git-http daemon) gateway/git_gate_render.py (the git-gate pre-receive rendering) gateway/supervise_server.py (the supervise MCP daemon) gateway/policy_resolver.py (the data-plane control-plane RPC client) orchestrator/ now holds only control-plane files. The shared plan/types/auth layer (egress.py=EgressPlan, git_gate.py=GitGatePlan, supervise.py, supervise_types.py, control_auth.py) and the launch-time git-gate provisioning helpers stay at root, so orchestrator/ and backend/ still own them. Because these daemons are invoked as `python3 -m bot_bottle.<name>`, loaded flat by mitmproxy, and referenced in Dockerfile.gateway, the move updates more than Python imports: the `-m` invocations (firecracker/macOS infra scripts), the Dockerfile.gateway addon shim + ENTRYPOINT, gateway_init's _DAEMONS module paths, and the git-gate CGI heredocs all now point at bot_bottle.gateway.*. No behavior change; full unit suite green (2251). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>Give each service its own store package + manager, and cut the supervise module along the control/data-plane boundary so nothing in the shared layer reaches up into the orchestrator. Stores, by owner: - bot_bottle/store/ keeps only the shared base (DbStore, migrations) and the concrete stores that aren't service-owned (audit_store, config_store). - bot_bottle/orchestrator/store/ now houses the orchestrator-owned stores — queue_store (supervise queue), secret_store, config_store — plus a new orchestrator store_manager that migrates them (composing audit/config downward from the base). The old shared store_manager is gone. Supervise plane, by tier: - bot_bottle/supervisor/ (NEUTRAL, importable by every tier including the gateway): types.py (the Proposal/Response/AuditEntry wire types + the tool/ status/poll constants + the shared daemon constants moved out of supervise.py) and plan.py (SupervisePlan, a pure DTO). - bot_bottle/orchestrator/supervisor/ (orchestrator-only): queue.py (the queue/audit I/O wrappers + render_diff + sha256_hex) and supervise.py (the Supervise lifecycle that stages the DB via the store manager). Its __init__ re-exports the neutral vocabulary so orchestrator-side callers import from one place. The gateway now imports only bot_bottle.supervisor.types (never bot_bottle.supervise), so the data plane holds no code dependency on the orchestrator — it reaches the queue over the control-plane RPC. This removes the circular import that moving queue_store under orchestrator introduced (supervise -> orchestrator -> service -> supervise). supervise_types.py -> supervisor/types.py; supervise.py deleted (split). Full unit suite green (2251). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>Remove the supervise queue helpers that no live path uses anymore — they're mechanisms the PRD-0070 / idempotent-poll migration superseded: - archive_proposal (+ QueueStore.archive_proposal): the poll stopped archiving on read (#469 review), so single-proposal archiving has no caller; decided proposals drop off the pending list via their response row and are reaped in bulk by archive_all_proposals on teardown/reconcile. - wait_for_response: the data plane polls the queue over the control-plane RPC now, so nothing block-waits on it. - audit_dir / audit_log_path: file-based audit paths, replaced by the SQLite AuditStore. Also drops the tests that covered those obsolete helpers. Kept the prod-unused read accessors list_pending_proposals and read_audit_entries: those aren't dead mechanisms — they're the read side of live write paths that many tests use to verify real queue/audit behavior. Full unit suite green (2243). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>Turn the loose queue/audit functions in orchestrator/supervisor/queue.py into methods on a concrete Supervisor service. The Orchestrator now owns an injectable `self._supervisor` and calls `self._supervisor.write_proposal(...)` etc., instead of module-level free functions — the supervise dependency is explicit and mockable, and a Supervisor can be scoped to a `db_path` (defaults to the host DB) so tests can point it at a temp database. - Supervisor (in the package __init__) drops the ABC and gains write_proposal, read_proposal, list_pending_proposals, list_all_pending_proposals, write_response, read_response, archive_all_proposals, write_audit_entry, read_audit_entries, plus the launch-time prepare(). - render_diff / sha256_hex are pure and stateless, so they move to orchestrator/supervisor/util.py (re-exported from the facade) rather than becoming methods. - queue.py is deleted; service.py + the supervise tests call through a Supervisor instance. Full unit suite green (2243). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>backend/__init__.py eagerly imported the whole framework (manifest, egress, git-gate, env, workspace, agent-provider), so importing ANY backend.* submodule paid ~32 modules just to run the package init. Split it: - backend/base.py — the abstract contract (BottleSpec, BottlePlan, BottleCleanupPlan, ExecResult, ActiveAgent, Bottle, BottleImages, BottleBackend). Carries the framework imports; loaded only when a caller needs the contract. - backend/selection.py — backend registry / selection / enumeration (get_bottle_backend, _get_backends, _auto_select_backend, has_backend, known_backend_names, enumerate_active_agents). Concrete backends still imported lazily inside. - backend/__init__.py — thin: a __getattr__ that resolves the public names from those submodules on first access (+ a TYPE_CHECKING block for checkers). Existing `from bot_bottle.backend import X` and patch.object call-sites keep working. `import bot_bottle.backend` drops from 32 -> 2 bot_bottle modules. Repointed the backend-selection test's patch targets to the `selection` module (the functions reference each other there now; the FirecrackerBottleBackend class-method patches are unchanged — same class object). Note: backend.docker.util is still heavy because backend/docker/__init__ eagerly imports DockerBottleBackend — slimming the three sub-package inits is the follow-up that makes the leaves cheap. Full unit suite green (2243). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>Turn the git_gate module into a package with GitGate as a concrete service class (dropping the ABC), mirroring the Supervisor shape. The host-side git-gate operations are now methods the backend drives: git_gate/ __init__.py — thin __getattr__ facade (keeps `from bot_bottle.git_gate import …` working; render/hook names lazily forwarded to gateway.git_gate_render) plan.py — GitGatePlan (the launch DTO the backend contract references) service.py — GitGate: prepare / provision_dynamic_keys / revoke_provisioned_keys / preflight_host_keys provision.py — deploy-key lifecycle (was git_gate_provision.py) host_key.py — host-key preflight (was git_gate_host_key.py) The backend launch + base.py preflight now call GitGate() methods instead of the free functions. The runtime rendering / in-gateway hook execution stay in gateway/git_gate_render.py (data plane) — only the host-side service moved. Because the facade forwards names lazily, the ~25 `from bot_bottle.git_gate import GitGatePlan` call-sites are unchanged, and importing git_gate.plan (the contract's dependency) no longer drags in the provisioning / forge-API code. Full unit suite green (2243). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>_common had rotted into a junk drawer. Trim it to what actually justifies a shared leaf module and rename for legibility: * REPO_DIR: deleted (dead — nothing imported it). * read_tty_line: dropped the pointless re-export; cleanup/start/init now import it straight from bot_bottle.util. * USER_CWD: deleted. It captured os.getcwd() at import time, but nothing chdirs and no test patched it, so it was equivalent to a live os.getcwd() — inlined at the six call sites. * PROG: kept, now the sole member. It's still a leaf (both the dispatcher and the commands it imports need it) so it can't move into __init__ without a circular import. _common.py -> constants.py. Full unit suite green (2243); CLI dispatch verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>Split the cli package's two responsibilities out of __init__: * commands/__init__.py now assembles the COMMANDS registry and NO_MIGRATION_COMMANDS from the per-command modules — the command surface lives entirely under commands/. * __main__.py now owns main() (dispatch + migration gate + exit-code mapping) alongside the runnable entry guard. cli/__init__.py shrinks to a shim that re-exports main / COMMANDS / NO_MIGRATION_COMMANDS, so bot_bottle.cli.main (repo-root cli.py entry) and the tests' bot_bottle.cli.COMMANDS keep working unchanged. Verified `python -m bot_bottle.cli` (runpy) and `cli.py` both dispatch; full unit suite green (2243). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>The codebase used "control plane" both as an architectural role term AND as an identifier alias for the orchestrator component, producing duplicate names for one thing (control_plane_url vs orchestrator_url, CONTROL_PLANE_PORT, host_control_plane_token, …). Going forward the concrete component is always named for what it is — Gateway or Orchestrator — and the plane vocabulary is reserved for prose (module descriptions, the security argument). Renamed (identifiers + the in-repo env/wire/file string values, all setters/getters are in this repo so the change is atomic): ControlPlaneServer -> OrchestratorServer control_plane_url -> orchestrator_url probe_control_plane_url -> probe_orchestrator_url host_control_plane_token -> host_orchestrator_token CONTROL_PLANE_PORT -> ORCHESTRATOR_PORT CONTROL_PLANE_TOKEN_ENV/FILE -> ORCHESTRATOR_TOKEN_ENV/FILENAME BOT_BOTTLE_CONTROL_PLANE_TOKEN-> BOT_BOTTLE_ORCHESTRATOR_TOKEN control-plane-token (file) -> orchestrator-token control_auth (module) -> orchestrator_auth (stays top-level; the gateway imports it and must not import the orchestrator/ package) CONTROL_AUTH_HEADER -> ORCHESTRATOR_AUTH_HEADER x-bot-bottle-control-auth -> x-bot-bottle-orchestrator-auth CONTROL_AUTH_JWT_ENV -> ORCHESTRATOR_AUTH_JWT_ENV BOT_BOTTLE_CONTROL_AUTH_JWT -> BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT _control_auth_headers -> _orchestrator_auth_headers Prose plane-terms ("control plane", "data plane") are preserved, including the test name test_data_plane_daemons_get_jwt_not_key (it names the security invariant). Gateway and orchestrator verified to agree on the renamed wire header; full unit suite green (2243). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>Two facade issues from the reorg: * orchestrator/__init__.py used `__all__ = list(_LAZY)`, which pyright strict can't analyze (reportUnsupportedDunderAll) — and because it isn't a static literal, the TYPE_CHECKING re-export imports read as unused. Replaced with the explicit literal list the other lazy facades already use. * manifest/index.py imported six piece types (ManifestAgentProvider, EGRESS_AUTH_SCHEMES, ManifestEgressConfig/Route, ManifestGitEntry, ManifestKeyConfig) it never referenced — the package facade re-exports those straight from their submodules, so index.py needn't import them. Dropped the dead imports. pyright: 0 errors (was 24). Full unit suite green (2243). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>