Security model

Disclosure policy and the summary posture live in the root SECURITY.md. This document is the engineering view: assets, actors, controls, and the tests that keep them honest.

Assets

  1. Customer feedback and vote data (tenant-private).
  2. Developer credentials: secret keys, MCP keys, studio tokens, OIDC sessions.
  3. Tester identity and reputation.
  4. The developer's coding agent — the most valuable target, because it can change the customer's code. ADapptive never runs it, but it consumes what ADapptive emits.
  5. Reward budgets and payout instructions (once the economy is live).

Actors and boundaries

flowchart TB
  subgraph untrusted [Untrusted]
    T[Testers and anonymous sessions]
    W[The public web and crawlers]
  end
  subgraph platform [ADapptive]
    API[API]
    DB[(Postgres)]
    DASH[Dashboard]
    MCPH[Remote MCP]
  end
  subgraph dev [Developer's trust domain]
    AGENT[Coding agent]
    REPO[Source code]
  end
  T -->|publishable key + tester JWT| API
  W --> DASH
  API --> DB
  MCPH -->|mcp key or OIDC| API
  AGENT --> MCPH
  AGENT --> REPO

Three boundaries matter: tester → platform (input validation, redaction, screening, rate limits), platform → agent (envelope, CLEARED-only, least privilege key), and tenant → tenant (every query scoped by customerId).

Controls

Keys and tokens

Kind Storage Grants Regression tests
adp_pk_* publishable plaintext (public by design) submit, vote, read zones key-kind permission matrix (integration.test.ts)
adp_sk_* secret SHA-256 hash everything in the tenant same
adp_mk_* MCP SHA-256 hash read feedback/zones/campaigns; write feedback status, analysis, deployments, logs, stages, rollback mcp-key.test.ts: cannot delete zones, list testers, grant rewards, rotate keys
studio token HMAC, TTL zone CRUD cannot manage people or rewards
pairing code SHA-256 hash, 10-minute TTL, single use redeem for one studio token replay rejected
OIDC JWT verified against issuer JWKS (OIDC_ISSUER, OIDC_AUDIENCE) workspace by role; tester surface invalid token fails closed, never downgrades to anonymous

Secrets are shown once and never persisted in plaintext. Rotation invalidates the previous hash atomically.

Tenant isolation

Every read and write includes customerId; Socket.IO rooms are zone:<customerId>:<zoneId>. Tested with two tenants sharing a zoneId.

Voting integrity

Weight is resolved server-side from the voter's pool tier; anonymous and account votes count once. Votes upsert in one transaction with a row lock and a counter recompute; dedupe per session and per tester identity. The threshold flip requires the zone's weighted threshold, minParticipation, and minTrustedVoters distinct trusted voters (ACTIVE pool members or accounts older than TRUSTED_ACCOUNT_MIN_AGE_HOURS with at least one cleared contribution).

Safety pipeline (tester → agent)

Runs synchronously on POST /api/feedback and on poll options; results in Feedback.safetyStatus and Feedback.safetyReport.

Layer What it does Outcome
Redaction emails, phone numbers, card numbers (Luhn), SSNs, IBANs, IPv4/IPv6, JWT and API-key shapes → [redacted:type] in redactedTitle/redactedDescription always applied; count recorded
Normalisation strips C0/C1 control characters, bidi overrides, zero-width characters; collapses whitespace always applied
Injection screening scores imperative agent-directed phrasing ("ignore previous instructions", "you are now", "run", "execute", "add to .env", tool-call syntax, shell pipelines, base64 blobs, excessive URLs) score ≥ flag threshold → FLAGGED; hard patterns → BLOCKED
Classifier (optional, `SAFETY_CLASSIFIER=anthropic openrouter off`)
Moderation humans approve, edit-and-approve, or block from /moderation; each decision writes AuditEvent FLAGGED → CLEARED or BLOCKED

Only CLEARED items are returned by get_ready_feedback and by the THRESHOLD_MET filter. Agent-facing text is the redacted copy, wrapped in <untrusted_tester_content> with attributes for id, field and source.

Sybil and abuse controls

Per-key rate limits (existing), per-tester and per-session submission and vote limits, account-age gating for vote weight, burst detection on ipHash clusters recorded in safetyReport.signals, invite usage caps, and House Pool tiers that start at zero weight.

Extension

Mandatory permissions are localhost-only; live-site access is an optional_host_permissions grant per origin (enforced by a manifest unit test). No credentials ship with the extension. Developer mode stores only expiring studio tokens obtained through a pairing code; tester sign-in is OIDC PKCE against a public client.

Remote MCP

Streamable HTTP behind Traefik; bearer adp_mk_/adp_sk_ or Authentik access tokens; /.well-known/oauth-protected-resource for discovery; per-key rate limits; the transport forwards the caller's credential to the API and holds no credentials of its own.

Transport, storage, infrastructure

TLS at Traefik; Postgres and MinIO on the private compose network; secrets only in Coolify's store; IPs stored as salted SHA-256; screenshots private and served through authenticated routes with signature validation; no .env in images.

What we deliberately do not do

  • Run customer code or a hosted AI worker.
  • Store plaintext GitHub or integration tokens.
  • Trust client-supplied roles, weights, or safety verdicts.
  • Let studio tokens or MCP keys manage people or money.

Verification

CI runs lint, build, type-check, and the integration suite against Postgres on Node 20. Safety-pipeline tests assert: injection samples are FLAGGED or BLOCKED and absent from agent-facing lists; PII is redacted in agent-facing fields; quorum blocks a threshold flip; adp_mk_ is denied on forbidden routes; pairing codes are single-use. Manual: extension E2E in Chrome, Playwright golden paths (see operations).