bot-bottle-vault: secret custody + phone-to-host secret sync (VaultProvider interface) #328

Open
opened 2026-07-01 21:13:41 -04:00 by didericis-claude · 0 comments
Collaborator

Primary design goal

Set up a host machine once, then create, edit, and run bot-bottle agents from a phone — including changing, saving, and sharing secrets, and injecting them into bottles.

Everything below is in service of that goal. It splits into two independent problems that should not be solved by the same mechanism:

  1. Control — "create/edit/run this agent" from the phone. Already covered by the dashboard (#327): a web UI, reachable over a network you trust (e.g. Tailscale to the host), that authors account-scoped bottle/agent definitions and launches them.
  2. Custody — "change/save/share this secret" from the phone, and have it land in a bottle's sidecar without ever passing through the agent (or, ideally, without the phone needing to trust the dashboard host with plaintext at rest). That's this issue: bot-bottle-vault.

Conflating these two would mean either the dashboard becomes a secrets store (weakens the sidecar-only-injection invariant) or the vault becomes a command channel (expands its threat model for no reason). Keep them separate; the dashboard is a client of the vault, same as any host is.

Relationship to #327

#327's "store env vars / secrets" section defined secrets as custody, not storage: encrypted at rest, masked on every read API, delivered to the sidecar at launch, referenced by name in a definition. This issue is that backend, pulled out into its own component so it can:

  • have its own threat model and security review, independent of the dashboard's,
  • be swapped for an existing, audited provider instead of us building crypto/sync from scratch,
  • ship as a separate repo so bot-bottle core's dependency graph (stdlib-only, per #327) stays untouched by whatever the vault needs.

Goals

  • A VaultProvider contract that bot-bottle's sidecar/orchestrator calls to resolve secret-name -> value at launch time, entirely within the sidecar's existing injection boundary (agent never sees it, same as today's host-secret sidecar model).
  • A phone-usable client (mobile web via the dashboard, or a provider's own app) to create, edit, and share secret values, without the secret ever landing on a host in plaintext outside the sidecar.
  • Support sharing a secret across devices/operators (e.g. a team's shared deploy key), not just single-device sync.
  • Ship as a separate repo/app (bot-bottle-vault), interfacing with bot-bottle core over the VaultProvider contract — not a new core dependency.
  • Default to not building our own crypto/device-pairing — integrate an existing, audited secrets manager first; build bot-bottle-vault as an opinionated reference implementation second, once we know what it needs to do differently.

Non-goals (v1)

  • Not a general-purpose password manager UI — scope is "secrets an agent needs at launch."
  • Not solving the control-plane/launch problem — that's #327.
  • Not committing to a p2p/Holepunch transport for v1 — see options below.

Design: VaultProvider interface

A minimal contract the sidecar/orchestrator speaks, so the backend is swappable:

  • resolve(name) -> value — called by the sidecar at launch, over the sidecar's existing network boundary (never exposed to the agent).
  • list() -> [{name, updated_at}] — for the dashboard's secrets UI (#327's GET /api/secrets); values never included.
  • set(name, value) / rotate(name, value) / delete(name) — write-only from the client's perspective.
  • Auth/scoping: which account/definition is allowed to resolve which secret — mirrors #327's account-scoped bottle/agent definitions.

Any provider below can sit behind this contract. Bot-bottle core only ever talks to the contract.

Provider options (buy vs. build)

Provider Self-hostable OSS Phone app Fits "resolve by name" Notes
Bitwarden Secrets Manager / Vaultwarden Yes Yes Yes (mature) Yes — bws CLI/SDK, machine accounts Recommended first integration. Solves phone-side create/edit/share for free, audited by a much larger team than us. Vaultwarden's Secrets Manager API parity with Bitwarden's own self-host offering needs verification before committing.
Infisical Yes Yes Web / limited mobile Yes — has a pull-based agent for ephemeral environments, close to our sidecar-injection shape Good second option: lighter, more modern DX, OSS-first. Smaller audit trail than Bitwarden.
1Password Connect + Service Accounts Partial (Connect calls out to 1Password's cloud) No Yes (excellent) Yes Best-in-class crypto/device UX, but not fully self-hostable and not OSS — conflicts with the project's self-host/OSS posture. Useful comparator, probably not the pick.
HashiCorp Vault Yes Yes (core) No Yes (dynamic secrets, transit engine) Enterprise-grade, heavy ops burden, no phone story. Overkill for this shape.
DIY: age + Tailscale/Syncthing Yes Yes No polished app We build it Lowest dependency, closest to project ethos, but we own device-pairing/key-wrapping UX — the hard, security-critical part flagged above.
bot-bottle-vault (custom, Holepunch/Hyperbee-based) Yes Yes (ours) We build it Yes (native) Zero-coordination-server p2p is genuinely appealing for "no infra to run," but it's the most build-it-ourselves option along every axis (crypto, key mgmt, multi-writer, mobile client). Right long-term default if the reference implementation earns it; wrong place to start.

Recommendation: ship the VaultProvider contract first, wire it to Bitwarden Secrets Manager (self-hosted) as the initial backend, and treat bot-bottle-vault as a future reference implementation once we know the contract is right — not the v1 dependency.

Phone workflow (target end state)

  1. Operator sets up a host once (existing ./cli.py / bottle install flow).
  2. Operator installs/points the dashboard (#327) at that host, reachable from the phone over Tailscale (or equivalent).
  3. Operator pairs a vault client (Bitwarden mobile app, or later bot-bottle-vault's own client) to the same account.
  4. From the phone: author or edit a bottle/agent definition in the dashboard (#327); create/edit/share a secret in the vault client; reference the secret by name in the definition.
  5. Launch from the dashboard. The host's orchestrator resolves the definition, the sidecar calls the vault's resolve(name) at launch, injects the value — the phone and the agent both never see the value pass through the agent's environment.

Security notes

  • Storage layer must be zero-knowledge: whatever hosts the vault (self-hosted Bitwarden/Vaultwarden/Infisical instance, or later bot-bottle-vault) stores ciphertext only; it holds no keys capable of decrypting values at rest.
  • The sidecar-injection boundary is unchanged from today's host-secret model (#327, and the existing printenv-shows-proxy-URLs-only guarantee) — the vault only changes where secret values come from, not how they reach the agent.
  • Key management (root key location, per-device wrapping, revocation, recovery) is inherited from the chosen provider in v1 — deliberately not something we design from scratch yet.

Open questions

  • Does Vaultwarden's OSS build actually support the Secrets Manager API, or only the personal-vault/items API? Needs a spike before committing.
  • Where does the VaultProvider config live — per-account setting in the dashboard (#327), or a host-level config?
  • Sharing model: do we lean on the provider's own org/project sharing (Bitwarden orgs, Infisical projects), or build a thin sharing layer on top so switching providers doesn't lose that feature?
  • If/when we build bot-bottle-vault itself: Holepunch/Hyperbee vs. Tailscale+stdlib-HTTP for its internal transport — deferred until the contract proves out with an existing provider.
  • Recovery: what happens when the phone (the only device that ever unlocked the vault) is lost, before an org/second-device relationship exists?

Acceptance (v1)

  • VaultProvider contract defined and implemented against by the sidecar/orchestrator, gated behind the same account scoping as #327's definitions.
  • Bitwarden Secrets Manager (self-hosted) wired up as the first VaultProvider implementation.
  • A secret created/edited on the Bitwarden mobile app is resolvable by name and injected into a bottle's sidecar at launch, with no plaintext reaching the agent's environment.
  • Dashboard (#327) secrets list (GET /api/secrets) reflects names/metadata from the vault, never values.
  • End-to-end: from a phone, create a bottle/agent definition (#327), create a secret (this issue), launch, and confirm the running agent has the injected value available via its normal proxy path — without ever having touched the value directly.
## Primary design goal > Set up a host machine once, then create, edit, and run bot-bottle agents from a phone — including changing, saving, and sharing secrets, and injecting them into bottles. Everything below is in service of that goal. It splits into two independent problems that should **not** be solved by the same mechanism: 1. **Control** — "create/edit/run this agent" from the phone. Already covered by the dashboard (#327): a web UI, reachable over a network you trust (e.g. Tailscale to the host), that authors account-scoped bottle/agent definitions and launches them. 2. **Custody** — "change/save/share this secret" from the phone, and have it land in a bottle's sidecar without ever passing through the agent (or, ideally, without the phone needing to trust the dashboard host with plaintext at rest). That's this issue: **`bot-bottle-vault`**. Conflating these two would mean either the dashboard becomes a secrets store (weakens the sidecar-only-injection invariant) or the vault becomes a command channel (expands its threat model for no reason). Keep them separate; the dashboard is a *client* of the vault, same as any host is. ## Relationship to #327 #327's "store env vars / secrets" section defined secrets as **custody, not storage**: encrypted at rest, masked on every read API, delivered to the sidecar at launch, referenced by name in a definition. This issue is that backend, pulled out into its own component so it can: - have its own threat model and security review, independent of the dashboard's, - be swapped for an existing, audited provider instead of us building crypto/sync from scratch, - ship as a separate repo so bot-bottle core's dependency graph (stdlib-only, per #327) stays untouched by whatever the vault needs. ## Goals - A `VaultProvider` contract that bot-bottle's sidecar/orchestrator calls to resolve `secret-name -> value` at launch time, entirely within the sidecar's existing injection boundary (agent never sees it, same as today's host-secret sidecar model). - A phone-usable client (mobile web via the dashboard, or a provider's own app) to create, edit, and share secret values, without the secret ever landing on a host in plaintext outside the sidecar. - Support **sharing** a secret across devices/operators (e.g. a team's shared deploy key), not just single-device sync. - Ship as a **separate repo/app** (`bot-bottle-vault`), interfacing with bot-bottle core over the `VaultProvider` contract — not a new core dependency. - Default to **not building our own crypto/device-pairing** — integrate an existing, audited secrets manager first; build `bot-bottle-vault` as an opinionated reference implementation second, once we know what it needs to do differently. ## Non-goals (v1) - Not a general-purpose password manager UI — scope is "secrets an agent needs at launch." - Not solving the control-plane/launch problem — that's #327. - Not committing to a p2p/Holepunch transport for v1 — see options below. ## Design: `VaultProvider` interface A minimal contract the sidecar/orchestrator speaks, so the backend is swappable: - `resolve(name) -> value` — called by the sidecar at launch, over the sidecar's existing network boundary (never exposed to the agent). - `list() -> [{name, updated_at}]` — for the dashboard's secrets UI (#327's `GET /api/secrets`); values never included. - `set(name, value)` / `rotate(name, value)` / `delete(name)` — write-only from the client's perspective. - Auth/scoping: which account/definition is allowed to resolve which secret — mirrors #327's account-scoped bottle/agent definitions. Any provider below can sit behind this contract. Bot-bottle core only ever talks to the contract. ## Provider options (buy vs. build) | Provider | Self-hostable | OSS | Phone app | Fits "resolve by name" | Notes | |---|---|---|---|---|---| | **Bitwarden Secrets Manager / Vaultwarden** | Yes | Yes | Yes (mature) | Yes — `bws` CLI/SDK, machine accounts | **Recommended first integration.** Solves phone-side create/edit/share for free, audited by a much larger team than us. Vaultwarden's Secrets Manager API parity with Bitwarden's own self-host offering needs verification before committing. | | **Infisical** | Yes | Yes | Web / limited mobile | Yes — has a pull-based agent for ephemeral environments, close to our sidecar-injection shape | Good second option: lighter, more modern DX, OSS-first. Smaller audit trail than Bitwarden. | | **1Password Connect + Service Accounts** | Partial (Connect calls out to 1Password's cloud) | No | Yes (excellent) | Yes | Best-in-class crypto/device UX, but not fully self-hostable and not OSS — conflicts with the project's self-host/OSS posture. Useful comparator, probably not the pick. | | **HashiCorp Vault** | Yes | Yes (core) | No | Yes (dynamic secrets, transit engine) | Enterprise-grade, heavy ops burden, no phone story. Overkill for this shape. | | **DIY: age + Tailscale/Syncthing** | Yes | Yes | No polished app | We build it | Lowest dependency, closest to project ethos, but we own device-pairing/key-wrapping UX — the hard, security-critical part flagged above. | | **`bot-bottle-vault` (custom, Holepunch/Hyperbee-based)** | Yes | Yes (ours) | We build it | Yes (native) | Zero-coordination-server p2p is genuinely appealing for "no infra to run," but it's the most build-it-ourselves option along every axis (crypto, key mgmt, multi-writer, mobile client). Right long-term default if the reference implementation earns it; wrong place to start. | **Recommendation:** ship the `VaultProvider` contract first, wire it to **Bitwarden Secrets Manager** (self-hosted) as the initial backend, and treat `bot-bottle-vault` as a future reference implementation once we know the contract is right — not the v1 dependency. ## Phone workflow (target end state) 1. Operator sets up a host once (existing `./cli.py` / bottle install flow). 2. Operator installs/points the dashboard (#327) at that host, reachable from the phone over Tailscale (or equivalent). 3. Operator pairs a vault client (Bitwarden mobile app, or later `bot-bottle-vault`'s own client) to the same account. 4. From the phone: author or edit a bottle/agent definition in the dashboard (#327); create/edit/share a secret in the vault client; reference the secret by name in the definition. 5. Launch from the dashboard. The host's orchestrator resolves the definition, the sidecar calls the vault's `resolve(name)` at launch, injects the value — the phone and the agent both never see the value pass through the agent's environment. ## Security notes - Storage layer must be **zero-knowledge**: whatever hosts the vault (self-hosted Bitwarden/Vaultwarden/Infisical instance, or later `bot-bottle-vault`) stores ciphertext only; it holds no keys capable of decrypting values at rest. - The sidecar-injection boundary is unchanged from today's host-secret model (#327, and the existing `printenv`-shows-proxy-URLs-only guarantee) — the vault only changes *where secret values come from*, not *how* they reach the agent. - Key management (root key location, per-device wrapping, revocation, recovery) is inherited from the chosen provider in v1 — deliberately not something we design from scratch yet. ## Open questions - Does Vaultwarden's OSS build actually support the Secrets Manager API, or only the personal-vault/items API? Needs a spike before committing. - Where does the `VaultProvider` config live — per-account setting in the dashboard (#327), or a host-level config? - Sharing model: do we lean on the provider's own org/project sharing (Bitwarden orgs, Infisical projects), or build a thin sharing layer on top so switching providers doesn't lose that feature? - If/when we build `bot-bottle-vault` itself: Holepunch/Hyperbee vs. Tailscale+stdlib-HTTP for its internal transport — deferred until the contract proves out with an existing provider. - Recovery: what happens when the phone (the only device that ever unlocked the vault) is lost, before an org/second-device relationship exists? ## Acceptance (v1) - [ ] `VaultProvider` contract defined and implemented against by the sidecar/orchestrator, gated behind the same account scoping as #327's definitions. - [ ] Bitwarden Secrets Manager (self-hosted) wired up as the first `VaultProvider` implementation. - [ ] A secret created/edited on the Bitwarden mobile app is resolvable by name and injected into a bottle's sidecar at launch, with no plaintext reaching the agent's environment. - [ ] Dashboard (#327) secrets list (`GET /api/secrets`) reflects names/metadata from the vault, never values. - [ ] End-to-end: from a phone, create a bottle/agent definition (#327), create a secret (this issue), launch, and confirm the running agent has the injected value available via its normal proxy path — without ever having touched the value directly.
didericis added the Kind/Feature label 2026-07-06 13:06:22 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: didericis/bot-bottle#328