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:
| Identity | Shape | Who holds it | What a leak costs |
|---|---|---|---|
| door id | door_ + 12 hex | Everyone — it’s the address. | Nothing. Public by design; minted once, survives off/on cycles. |
| API key | bbl_… | 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 secret | per-customer | Bubblio 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 key | bak_… | 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-day | Signed 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 passkey | WebAuthn | The 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 id | q_ + 16/32 hex | The 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 token | connect quotes only | The 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 id | apr_ + 32 hex | Whoever 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 token | rail-specific | Transit 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
/approvalstakes effect at the very next quote and confirm.
Fails open — availability over ceremony, stated honestly
- Web Bot Auth verification: a presented
Signature-Agentheader is verified against the agent's published keys; absence or failure recordsverified: falseand 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
askbrain's only tools are knowledge search and unmet-request capture;quote/confirmare 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.