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
- Customer feedback and vote data (tenant-private).
- Developer credentials: secret keys, MCP keys, studio tokens, OIDC sessions.
- Tester identity and reputation.
- 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.
- 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).