Get bot-bottle.db off the data plane (supervise + egress proposals over RPC) #469
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?
Context
PRD 0070 states the rule plainly:
That rule is not in force. Two data-plane daemons hold a direct read-write handle on the shared SQLite file today:
supervise_server.py—_sv.write_proposal(L377) and_sv.read_response(L434) for MCP tool calls.egress_addon.py— the same two calls (L592, L645) on the DLPon_match: supervisepath.The PRD's own transitional caveat only names supervise. The egress addon — the most attack-exposed process in the system, TLS-bumping and body-scanning hostile traffic — is also a direct writer, and that isn't recorded anywhere.
The mount is the whole DB directory, read-write, via
orchestrator/lifecycle.py(_host_db_dir()→/run/supervise, plus the host root at/bot-bottle-root).Why it's like this
Purely historical. The supervise plane (PRD 0013) predates the orchestrator: in the per-bottle model there was no control plane, and a bind-mounted host DB was the only channel that got a proposal from inside the gateway to the operator on the host. Reasonable then.
PRD 0070 built the control plane but migrated only the operator half of the flow —
GET /supervise/proposalsandPOST /supervise/respondexist. The agent half (queue a proposal, poll for its response) was never added, so those four call sites kept doing what they always did.Why it matters
Security.
bot-bottle.dbco-tenants the orchestrator registry, andorchestrator_bottles.identity_tokenis a plaintext column. An RCE in egress therefore reads every bottle's identity token, then calls/resolveas any of them to collect that bottle's upstream credentials. This is the single step that converts one compromised process into fleet-wide credential access, and it's the concrete form of the "secret concentration" risk PRD 0070's security review flags. Note the daemons all run as root — noUSERdirective inDockerfile.gateway,Dockerfile.orchestrator, orDockerfile.infra.Scaling. TLS interception plus per-byte DLP scanning (gzip inflate, entropy, regex over bodies) is the expensive path, and mitmproxy is one GIL-bound asyncio process. The fix is N egress workers, which the resolver-only design otherwise permits almost for free — egress holds no policy and no cross-connection state, and source-IP attribution means bottles can be sharded across workers at launch rather than load-balanced, so attribution keeps working unchanged. The direct DB handle is what blocks it: N workers writing one SQLite file is survivable inside one kernel where
fcntlserializes them, and is exactly the incoherent-locking corruption case that forced the single-container design on macOS the moment those workers live in separate guests.The change
orchestrator/control_plane.py): add the agent-side half of the supervise flow — an endpoint to queue a proposal and one to poll for its response. Both must attribute the caller by(source_ip, identity_token)like/resolve, so a bottle can only ever queue or read its own proposals._sv.write_proposal/_sv.read_responsecall sites insupervise_server.pyandegress_addon.pyfor HTTP calls through the existing resolver client.SUPERVISE_DB_PATHfrom the data-plane env.The blocking-poll semantics already map cleanly onto a request: the existing contract is a ~30s grace window, then return
pendingwith aproposal_id, withcheck-proposalto resume. No new state machine.Notes
SecretProvider), which addresses the residual concentration risk that survives this fix./resolveonly, not the mutating routes), and running the daemons as distinct non-root UIDs.