Skip to content

Threat model

Ony.ai sits on a privileged path: it decides whether an AI agent’s risky action runs. That makes the integrity of the decision the thing to protect. This page is the adversary-by-adversary view of how the design defends it. It is the reasoning behind the operational controls in hardening, and it pairs with data handling for what is stored and what is not.

  1. Decision integrity - an “allow” must reflect a real human approval of this action.
  2. The audit trail - a truthful, tamper-evident record of what was asked and decided.
  3. Secrets - the signing secret, provider credentials, TOTP secrets, session tokens.
  4. Tenant data - one org must never see or affect another’s.
  • The coding agent / connector is untrusted for risk. It sends a hint; the server re-derives the authoritative action_type to risk_level from a closed taxonomy. An unknown action fails closed to CRITICAL. See the risk and policy model.
  • The PSTN and inbound webhooks are untrusted. Caller ID is spoofable; webhook bodies can be forged.
  • The browser and dashboard are untrusted until a request carries a valid, unexpired bearer token bound to the user’s current credential version.

A compromised or malicious connector / agent tries to get a dangerous action auto-approved.

Risk is server-authoritative (the client hint is ignored); policy matches only server-derived values; unknown tools fail closed. A pushed verdict is a signed HMAC bound to handoff_id + session_id + tool_name + decision + expiry, so it cannot be replayed onto a different command, and the connector verifies that binding before it honors the verdict. The connector never gets a blanket “allow”.

A network or webhook forger posts fake telephony or chat callbacks to fabricate an approval.

Every inbound webhook is signature-verified per carrier (SignalWire, Twilio, Plivo HMAC; Telnyx Ed25519; Stripe HMAC; Telegram secret token; Slack signature) against the raw body, and fails closed with a 403. The boot guard refuses to start in production if a channel that can send has no verification secret configured. The ony_webhook_signature_failures_total metric surfaces spikes.

A caller spoofing the owner’s number dials IN (inbound “call your agent”) to snoop on status.

Caller ID is only a hint. The inbound flow ALWAYS requires a second factor (the authenticator TOTP, or an SMS one-time code) before it reveals anything, because the caller is unauthenticated. Attempts are capped and audited. This applies to the inbound path only; the outbound approval call is covered next.

An attacker who can intercept an OUTBOUND approval call (SIM-swap, call-forwarding, or a co-located person) approves a risky action Ony.ai dialed the owner about.

For an outbound call Ony.ai DIALS the org’s own verified number, so answering it is the possession factor, the same trust anchor as an SMS one-time code. That is why approval works out of the box with no authenticator enrolled: a mandatory TOTP made approval impossible for a fresh account and was absurd friction for a solo self-hoster. The layered controls:

  • the call speaks a match code shown on every other surface (a spoofer who cannot say it should be hung up on);
  • the decision is DTMF only, never inferred from speech or from any model;
  • high and medium approvals require a spoken read-back turn;
  • deny is always the cheapest gesture (never gated);
  • CRITICAL actions and production deploys cannot be approved by voice at all - they are dashboard-only, enforced by the server policy engine.

For stronger assurance an org can enable require_pin per policy rule, which additionally demands the caller’s TOTP before a high-risk approval finalizes.

Prompt-injection or secret exfiltration via spoken or texted content.

Voice, SMS, and chat carry only the closed taxonomy (action and risk) and a redacted gist, never raw client titles, commands, diffs, or the details payload. The payload summarizer strips secrets from its input before any LLM sees it, and redacts its output again.

A DB-write attacker or insider rewrites history to hide what an agent did.

Each audit event is HMAC-chained to the previous one with a server-held key that is never in the DB. Editing, reordering, or deleting a past event breaks the chain; verify_chain detects it and reports the first bad index. A unique (org, chain_index) constraint blocks fork races. Because the key is not in the DB, a DB-only attacker cannot recompute a valid chain.

Token theft or stale sessions.

Bearer tokens are signed, expire in 24h, and embed a credential_version. A password reset or a member deactivation bumps it and instantly revokes every prior token. Deactivated and unknown users are rejected even with a live token.

Brute force or abuse.

Per-IP sliding-window rate limits guard signup, login, OTP, reset, and place-call, with a spoof-resistant client-IP derivation (ONY_TRUSTED_PROXY_HOPS). See hardening for tuning.

Cross-tenant access.

Every query is scoped to organization_id; RBAC (admin, member, viewer) gates actions within an org.

A stolen backup or disk.

TOTP secrets are encrypted at rest; backups can be age-encrypted with a key held off-host; TLS in transit terminates at your reverse proxy.

  • The signing secret is the root of trust: auth tokens, decision signatures, the audit-chain key, and TOTP encryption. Protect and rotate it (the boot guard rejects a weak or default value in production; rotation is covered in hardening).
  • A compromised host running the API can read live secrets from process memory and environment. Ony.ai assumes the server itself is trusted. Least-privilege the container (it runs as non-root) and the host.
  • The configured LLM and telephony providers are trusted with what is documented in data handling. Choose providers accordingly, or run with ONY_LLM_PROVIDER=none.