Registering a target
POST /api/v1/targets (targets:write) takes { "target": "https://staging.example.com" }
and returns the target plus a verification token. Registration normalizes the URL
and rejects private addresses before anything is stored.
Only explicitly registered hosts are in scope. A workspace can also allowlist hosts it owns, which skips the verification step for those hosts.
Verifying control
POST /api/v1/targets/:id/verify (targets:write) checks the token you served
at /.well-known/offense-verify.txt.
What the check does and does not do:
- resolves the host through DNS-over-HTTPS at
cloudflare-dns.com, - rejects private answers, including after redirects,
- caps the response at 4 KB and times out after 5 seconds,
- rejects redirects instead of following them.
Verification covers only the hosts you registered. It is not a statement that you are authorized to test anything else.
Identities
PUT /api/v1/targets/:id/identities (targets:write) attaches the two logins
used for the tenant-isolation check. Each identity belongs to a different
organization and carries one sample record.
- Passwords are encrypted with AES-GCM under
CREDENTIALS_KEY(base64, 32 bytes) and are readable only by the runner through a runner-only credential endpoint. - They never enter findings, evidence, the run log, or a model prompt.
- For SSO, MFA or CAPTCHA, supply a Playwright
storageStateinstead. The app rejects identical states for both identities, validates cookie expiry and size, and encrypts each state. An expired session produces an explicit/app/onboarding/accounts?error=sessionstate. - Revoking a credential removes it from future runs. It is not retroactively removed from evidence already collected.
Swarms
A swarm launches several independent assessments against the same verified target and returns one combined report.
- In the browser, choose Agents on the review screen.
1launches a normal assessment;2or more launches a swarm. Each agent gets a distinct focus area and testing approach fromsrc/swarm.js, so the group does not spend every budget on the same boundary. MAX_SWARM_AGENTScaps the size (default100). The per-agent budget ismin(ASSESSMENT_BUDGET_MICROS, available / count), andMIN_ASSESSMENT_BUDGET_MICROSis required for each member.- Each member reserves and settles its own budget, so stopping a swarm refunds every unused member reservation.
- The combined report at
/app/swarms/:idmerges duplicate findings across agents, shows which agents reproduced each finding, lists each assignment, and links back to the individual evidence bundles.
Over the API (POST /api/v1/swarms, scope assessments:create):
{ "count": 3, "budgetMicros": 2000000, "instruction": "…",
"model": "…", "idempotencyKey": "…" }
budgetMicros is per agent. The response returns the combined findings and
a reportUrl. GET /api/v1/swarms/:id reads it; POST /api/v1/swarms/:id/stop (assessments:cancel) stops every active member.
Swarm members still use the normal durable job path. MAX_ACTIVE_SANDBOXES
limits an ordinary batch; a swarm may create more queued members than that,
while MAX_GLOBAL_ACTIVE and the queue continue to bound real concurrency.
Restricting a key to one host
An API key can be limited to a set of hostnames via allowedTargets. Any route
that addresses a target, sandbox, assessment or swarm by id is checked against
that set before the handler runs:
{
"error": "api_key_target_forbidden",
"allowedTargets": ["staging.example.com"],
"message": "This key is restricted to staging.example.com."
}
An empty allowlist means the key reaches every target the workspace owns.
POST /api/v1/sandboxes/:id/config is exempt because it is the call that
attaches a target in the first place — the target it sets is still checked.
Scheduled loops
POST /api/v1/loops (loops:write) runs an assessment on a schedule.
intervalMinuteshas a 5-minute floor and defaults to 1440.concurrencyis 1–10.409 target_not_readyif the loop's target is not verified.POST /api/v1/loops/:id/stopdisables it.
A loop is dispatched by the Worker cron (*/5 * * * *). The window is claimed
with a compare-and-set on next_run_at before any work, so two ticks cannot run
the same window, and budget and per-loop concurrency are checked first. Every
skip is recorded as last_error rather than dropped.
A loop stores and shows the source addresses it probes from
(OFFENSE_PROBE_ADDRESSES, comma-separated) and any declared test windows.
Run web/db/migrations/0035-loop-schedules.sql before deploying loops.