Host-side control server: broker bottle launches and own host-durable state #468
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Currently container launches are done directly via the CLI. We want to move this so both the CLI and the orchestrator communicate with a host-side control server, which becomes the single privileged component on the host: it brokers launches, owns orchestrator lifecycle, and is the sole writer of host-durable state.
Launch flows
Web console
CLI
Bootstrap / recovery
The host controller owns orchestrator lifecycle, so orchestrator startup cannot route through the orchestrator. The CLI needs a direct channel for bootstrap and recovery even though all bottle operations funnel through the orchestrator:
This is the path #391 (backend-agnostic orchestrator restart) would target.
The prize is that after this, the CLI no longer needs the Docker socket. That is what lets a dedicated Gitea runner user drop the root-equivalent
dockergroup (called out in PRD 0070 under "Relationship to other work").Existing scaffold
orchestrator/broker.pyhas a well-formed contract —LaunchRequest(ids + static flags only, never argv), HS256 sign/verify, fixed-schema validation, fail-closedBrokerAuthError, and aStubBrokerthat exercises the full sign → verify → act path in-process.DockerBrokerexists but is on no production path; every backend starts the orchestrator with--broker stub.Four gaps between that and a real host service:
LaunchBroker.submit(token)is an in-process method call fromOrchestrator.launch_bottle(service.pyL115). Needs aBrokerClientthat POSTs the signed token and a host-side HTTP server that verifies and acts.orchestrator/__main__.pyL58 doessecrets.token_bytes(32)and hands the same value to signer and verifier — only viable because they share a process. A separate daemon needs it provisioned out of band and durable across orchestrator restarts.sign_requestemitsjtiandiat;verify_requestreads neither. No expiry window, nojticache. Harmless in-process; over a wire a captured launch token replays indefinitely.launch/teardownonly. Everything else host-privileged is still in the CLI (see below), so the schema has to grow — carefully, since PRD 0070's security argument rests on "structured requests only, static flags + ids."What moves off the CLI
Host-privileged operations currently scattered across
backend/*/consolidated_launch.pyand driven by a short-lived CLI process:OrchestratorService.ensure_running()— starting the infra containerdocker run/ VM boot itselfnext_free_ip,_network_container_ips)provision_git_gate— dockercp/execof per-bottle deploy keys into the gateway_reprovision_running_bottles—docker exec printenv ENV_VAR_SECRETagainst live agentsOrchestrator.reconcileis the tell: it takeslive_source_ipsas a parameter only because the orchestrator cannot see the backend. With a host service that enumeration becomes internal and the parameter goes away.Database / state ownership
We originally wanted a single unified DB for all state mounted on the host. That isn't possible — SQLite locking is not coherent across guest kernels over a share, which is why the macOS backend already uses a container-only volume (
INFRA_DB_VOLUME = "bot-bottle-mac-db"). Rather than working around it with a mounted volume for one shared DB, split the state by owner and lifetime. Depends on #469, which gets the DB off the data plane first.Orchestrator
Owns all operational state on a volume nothing else mounts (generalizing the existing macOS design to every backend). Durable across orchestrator restarts so re-adoption works and running bottles are not bricked by a restart:
orchestrator_bottles— registry:bottle_id,source_ip,identity_token,metadata,policybottled_agent_secrets— encrypted per-bottle egress tokenssupervise_proposals/supervise_responses— the approval queueStays SQLite: mutable, transactional, queried. The orchestrator is sole mounter as well as sole writer.
Host
Owns the historical record, written only by the host controller over the authenticated channel:
sys.stderr(the container log) byegress_addon.py; both halves of "the audit record" should land in one placebot_bottle_config,orchestrator_config), or moved out of the DB entirely per PRD 0070's three-tier tableAppend-only, never updated, never transactionally queried — so this should be a JSONL log, not SQLite.
O_APPENDwrites are atomic and there is no locking protocol to get wrong, and hash-chaining for tamper-evidence becomes cheap. It also survives orchestrator destruction and container-runtime volume pruning (the #450 lesson), and stays readable without the orchestrator running.Note what host-location does not buy: integrity against a live compromised orchestrator, which makes the decisions being audited and can forge or omit entries wherever the file lives. An off-box copy is the answer to that, separately.
Gateway
None. After #469 the data plane holds no database state at all and reaches everything over RPC.
Open decisions
Cost
This converts on-demand privilege into standing privilege: today the Docker socket is exercised by a CLI the user invokes, afterwards a privileged daemon runs continuously under launchd/systemd. PRD 0070's argument is that the broker's privilege is narrower (structured requests vs. a raw socket), which holds — but "always running" is a new property, and the install/upgrade story grows. Precedent exists in the Firecracker one-time privileged pool setup.
Related
bot-bottle.dboff the data plane (lands underneath this)SecretProviderImplementation will need a PRD.
WIP - Create host side control server to prepare for console controlled launchto Host-side control server: broker bottle launches and own host-durable stateI did a code pass over the current CLI and backend interfaces. My first-pass minimum is two deliberately separate HTTP surfaces on the same process.
CLI/bootstrap surface (host bearer auth)
GET /v1/statusbackend status.POST /v1/orchestrator/ensure-runningstartandsupervise.POST /v1/orchestrator/restartGET /v1/bottleslist activeand supplies the selection data forcommit/cleanup.POST /v1/bottles/{identity}/attach-agentinteractiveorheadless.POST /v1/bottles/{identity}/snapshotcommit; the controller selects the backend freezer and records the resulting resumable artifact.GET /v1/cleanup-planPOST /v1/cleanupI would keep
backend setup/backend teardownout of the daemon API for v1. They install or remove the daemon's own host prerequisites and are better treated as install/uninstall operations.init,edit,info,list available, andloginare also non-privileged/local and do not need controller endpoints. The existing supervise proposal/response API can remain on the orchestrator.Orchestrator/broker surface (signed request auth)
POST /v1/broker/operationsOne transport endpoint, but a closed tagged union rather than a generic command:
provision-bottleteardown-bottlereconcile-hostprovision-bottleshould carry a versioned, fully declarative launch spec: bottle/identity ids, backend, approved image reference, network allocation intent, policy/metadata references, and static feature flags. It must not accept an executable, argv, shell text, arbitrary runtime flags, arbitrary mounts, arbitrary devices, or caller-chosen container names.resumeis the same operation with an existing identity; image/snapshot selection stays controller-owned. Image building can initially be an internal step ofprovision-bottle, then become only fixed-ref validation once #386 lands.teardown-bottleis idempotent and identity-scoped.reconcile-hosttakes no caller-supplied live-IP list: the controller enumerates the runtimes itself, reconciles against orchestrator records, and returns the result.Separately, the audit handler should be
POST /v1/audit/eventswith its plain bearer credential, as proposed in the issue. It should share neither request parsing nor dispatch with/v1/broker/operations.That maps the existing commands as follows:
start= local manifest/UI resolution → ensure orchestrator → provision → attach → teardownresume= provision existing identity → attach → teardownlist active= list bottlescommit= snapshotcleanup= cleanup plan → confirm → executesupervise= ensure orchestrator, then existing orchestrator APIOne likely implementation clarification: the attach endpoint is required to actually remove Docker/VM control from the CLI. Leaving
docker exec -it(or its Firecracker/macOS equivalents) in the CLI would retain the privilege we are trying to eliminate. The transport can be settled during the PRD, but the capability boundary belongs in v1.@didericis-claude should cleanup the cli a little bit before this:
infolist activetoactivelist availabletolistFile a new issue and implement a new PR to clean up the cli, and do this.