Reference

The security model

The doctrine, in four sentences: the human's passkey signs the exact terms of what runs. Budgets and ledgers fail closed. A quote id alone moves nothing. Money is transit-only — buyer to the platform's own Stripe, with Bubblio holding neither funds nor card numbers, ever.

Who holds what

Every identity in the system, and the honest answer to “what does a leak cost?” — the question each one was designed around:

IdentityShapeWho holds itWhat a leak costs
door iddoor_ + 12 hexEveryone — it’s the address.Nothing. Public by design; minted once, survives off/on cycles.
API keybbl_…Your server’s environment. Bubblio stores only its SHA-256.Sessions, shape syncs, and key mints on your account — but never a capability: policy flips are dashboard-JWT-only, so a stolen key can’t open an action. Rotate it; the runbook keeps identity resolving.
webhook signing secretper-customerBubblio signs every callback with it (encrypted at rest); your route verifies.Forged “callbacks” to your route — bounded by the ±5-minute timestamped V2 signature. Rotate in dashboard → Settings.
agent keybak_…The agent holds the raw key (shown exactly once). Bubblio stores its SHA-256.Acting as that one user, at that one door, inside whatever policies, caps, approvals, and mandates the owner set. Revocation is immediate — in-flight quotes refuse to confirm.
ctx (identity token)JWT, ~90-daySigned by your server (secret derived from your API key); Bubblio stores it encrypted and can neither verify nor forge it.Impersonation of one user at your callback route — an identity, never money: spend authority lives only in the server-side ledger. Delivered inside the HMAC-signed body, never in URLs.
consent passkeyWebAuthnThe human’s device. The first Face ID is enrollment; no account exists.Nothing exportable — assertions sign the SHA-256 of the exact approved terms, so a stored assertion is evidence, not a credential.
quote idq_ + 16/32 hexThe agent — ordinarily ≤10 minutes; 30 while a human approval or a payment link is pending.Alone: nothing that executes. A keyed quote confirms only with the same agent_key that quoted — the id is deliberately not a bearer token.
poll tokenconnect quotes onlyThe keyless agent (once); Bubblio stores its hash.One key delivery at most — the completing poll trades it winner-only, and a replayed token reads as an unknown quote: nothing, not even proof the quote exists.
approval idapr_ + 32 hexWhoever the human’s chat shows it to.Reading the approval’s facts — 128-bit unguessable, and GETs write nothing (prefetchers are inert). Approving requires the passkey ceremony.
payment tokenrail-specificTransit only: quote → the one paymentIntents.create call on your Stripe.Never stored, logged, echoed, or handed to your handler. Amounts always derive from the quote’s pin; raw card numbers are hard-refused at both ends.

The through-line: no single party can act alone. Your server signs identity it never custodies; Bubblio custodies ciphertext it can neither read into being nor forge; the agent holds a key that policy, approvals, and revocation outrank.

Where it fails open, and where it fails closed

Fails closed — when in doubt, nothing runs

  • Execution: confirm consumes the quote with a winner-only conditional update; a duplicate replays the original receipt. Policy is re-read at execution — an action closed between quote and confirm refuses.
  • Money: the cents ledger's spend row is written before the consume; a budget sum that cannot be read refuses the confirm. Mandates are re-read live pre-flight. A settlement whose amount or args_hash mismatches the pin is refused — and refunded.
  • Syncs that would misprice: declaring a price against a payments-lagging API refuses the whole sync rather than serving a paid action free.
  • Key caps: racing mints can 409 spuriously (a retry resolves it) but can never overshoot the cap.
  • Consent: approvals authorize exactly one execution; declines are terminal; revocation at /approvals takes effect at the very next quote and confirm.

Fails open — availability over ceremony, stated honestly

  • Web Bot Auth verification: a presented Signature-Agent header is verified against the agent's published keys; absence or failure records verified: false and refuses nothing. Proven identity is stored separately from the raw claim and shown to the human as verified vs presented, unverified. Enforcement — caps that rise only for verified agents — is deliberately deferred and named below.
  • Discovery: a transient directory or feed read failure serves an honest empty list, never a 500 — and never gets cached.
  • Schema lag: a door on a lagging migration serves with fewer columns rather than failing — except where money is involved (see fail-closed).

The red-team rules that shipped

  • Prompt injection cannot fire an action. The ask brain's only tools are knowledge search and unmet-request capture; quote/confirm are protocol methods the model never holds. Handlers are tenant-closure-bound; knowledge search is tenant-scoped.
  • The replayable signature is refused. Callbacks require the timestamped V2 HMAC (${timestamp}.${rawBody}, ±5 min); the legacy body-only signature — valid forever once captured — is answered 401 by SDK ≥0.9 unless explicitly, temporarily allowed.
  • Uniform misses. Unknown, malformed, disabled, unapproved, and foreign resources answer identically per surface (“No agent door here.”) — a prober can't tell closed from nonexistent. Signature failures on the signed push endpoints answer that same 404, not 401.
  • Calm errors. Refusals are one plain sentence with a machine code; processor and decline internals never cross the wire.
  • Consent pages act only on POST. GETs render facts and write nothing — link scanners and chat prefetchers can't approve anything. The WebAuthn challenge is the terms (SHA-256 of the canonical platform + action + args_hash + amount), so every stored assertion covers exactly what was approved.
  • Winner-only writes everywhere it matters: quote consume, approval consume, connect-grant flip, mandate mint and revoke — races produce refusals or replays, never double effects.
  • Rate limits that bind. Buckets key on verified-agent-id else IP, with IP-independent per-door daily caps as the true cost ceiling (perfect IP attribution on a public endpoint is unachievable, so the design never depends on it). Every ceiling is published in the manifest.
  • Secrets refused by name. A Stripe sk_/rk_ value sent where the publishable key belongs is rejected before anything is stored; raw card numbers are hard-refused at both ends.

Deliberately deferred, so absence isn't mistaken for oversight

  • Web Bot Auth enforcement — capability tiers keyed on proven agent identity (higher caps for verified agents; doors that require verification for paid actions).
  • Network-wide mandates — standing consent stays per-platform, bound to a verified key; a network-wide grant is the prompt-injection drain vector.
  • Server-assisted ctx re-mint — today an API-key rotation window shorter than the ctx lifetime means pre-rotation users re-mint through your route (the runbook).

The full HTTP contract, probing-resistance notes included, is in the API reference and openapi.yaml.