Safety & Secrets

Some rules are not tunable. They hold regardless of your run policy, and each ships with a test that asserts it refuses.

The allowlist check after each settle runs after the other page has loaded. --block-off-allowlist-navigation (MCP run_exploration.blockOffAllowlistNavigation, suite option of the same name) stops it earlier, for mission runs only (goal, usability, adversarial, coverage, exploratory, --feature):

  • A page navigation the page starts itself to an origin outside the allowlist (a link, location.href, a form GET, a popup) is answered 204 in the mission's browser before it loads, so the page stays put.
  • The run treats it as a departure: a goal run stops blocked with the URL in the transcript, adversarial lists it in scope.departures and resets, coverage, exploratory and feature runs record it and never expand it.
  • Server redirects still pass, so the after-settle check stays the guard. Subresources and fetch/XHR load as usual, and the HTTP cache is off for a guarded run.
  • It never applies to login, journey run, source run, load, record or demo sessions. Without the flag nothing changes. allowedOrigins is the session's authorized origins (it scopes the geolocation grant), not a network guard.

Hard floors

  • Secrets & PII are redacted before any Jev or LLM call. The generative model never picks a real recipient for a real send.
  • Prompt-injection guard is always on — page text is untrusted data, never instructions.
  • Every autonomous run is bounded by action and decision ceilings, time budgets and a no-progress detector; a page that stops answering is closed by a 60 s watchdog. Never unbounded.
  • Secrets are masked in pixels in every video and screenshot, by a display-only layer that never changes the page, including secrets the app reveals mid-run (elements marked data-jevitate-mask or similar, and credential-shaped values). A capture whose mask can't be proven is not written. Since 0.15.0 the mask uses only the browser's own requestAnimationFrame, MutationObserver, getBoundingClientRect and Range.getClientRects, captured before any page script, so a page that replaces them (a polyfill, an animation library, test hooks) cannot make a frame show a secret. If one was already replaced before the mask loaded, it fails closed: the viewport is painted over and screenshots and capture sessions are refused with E_PIXEL_MASK (reason untrusted-primitive).
  • API keys are checked, never shown. A key is verified with its provider by a non-billable auth call; its value is never echoed, logged or sent to a model, and key entry is never offered over MCP.
  • Every executed step is recorded and auditable — even a full-autonomous run is replayable and reviewable after the fact.
  • Pacing is realism and politeness, never detection-evasion.

The secret invariant

A secret has exactly one permitted path:

external password manager (fetched on demand)
  → the browser input
    → the target site's own auth call

It is never placed in a model prompt, never persisted in a Journey, recording, or log, and never in any outbound request except the target's own auth call. It is origin-bound — a secret can never be typed into a different origin or an injected/redirected field. Nothing is stored at rest. Jevitate holds only a manager reference.

How the secret is supplied is configurable via SecretPolicy.secretMode:

ModeBehavior
visible-handbackHeaded browser, the human enters it, resume only on a passing postcondition
vault-autofillFetched from an external manager on demand, origin-bound, nothing stored
fail-closedStop rather than proceed without a secret

The containment and origin-binding above are not tunable — only how the secret is supplied.

Fail-fast invariants

These are permanent, named contracts — each with an "asserts-it-refuses" test:

  • No policy → throw. A missing or partial RunPolicy raises rather than defaulting permissively.
  • Secret never serialized. Serializing or logging a secret throws. Only a reference may be stored.
  • Origin mismatch → refuse. Before any fill, current origin must match the secret's bound origin.
  • Unknown journey/param → reject. Only published ids and schema-valid params run — no inline steps, ever.
  • Resume only on postcondition. Handback and self-heal resume require a passing check. A timeout fails closed.
  • Writes never auto-heal. Write, irreversible, or real-send steps fail closed for human-approved repair.

Safety boundaries

Jevitate assumes you're testing systems you're authorized to test. Every run is restricted to the origins you authorize, and session-ending, destructive and paid controls are refused unless you explicitly allow them. Every write request a run fires is listed in its result (sideEffects):

# only these origins (the --url's own origin when --allow is omitted)
jevitate explore --url http://localhost:3000/settings --allow http://localhost:3000 ...

# --allow REPLACES the start origin, so list it too. Since 0.13.0 the blocked-write hint prints
# the full list to set (start origin included), and E_UNAUTHORIZED_EXPLORE_TARGET says the start
# URL's origin must be in it. The list itself is exact: no implicit origins once --allow is given.

# sign-out, delete/revoke/rotate and paid controls are refused by default;
# add your own, or lift the default deliberately
jevitate explore ... --deny '/Archive/i' --deny 'role=button;name=Send invite'
jevitate explore ... --allow-destructive
  • Every refusal names its rule (0.8.0). A refused step's reason ends with the rule it matched, for example [rule builtin:may-cost-money, matched "Generate"], and the transcript entry carries safety: { ruleId, pattern, control, risk, waivable }. jevitate site policy rules (--json) lists every rule and whether it can be waived.
  • --allow-control <regex> (0.8.0). Exempts a control whose accessible name matches from the soft built-in "may cost money" heuristic only, for that run (--allow-control "^Generate Your First Key$"). It never lifts --deny, --paid, destructive, sign-out, read-only or origin rules, and never applies to native dialogs. An invalid, empty or match-everything regex is refused before any browser opens. Every click it permits is recorded in the result's safetyOverrides. It is available on explore, explore-author-journey, campaign run, sweep targets, suite items and MCP (allowControl).
  • Find-out goals are read-only. A goal with no --success refuses controls that start a write flow and blocks the app's own write requests, unless the goal asks for a change or you pass --allow-writes (--allow-write <glob> exempts one path). A goal with no --success that asks for a change still never performs a destructive write without --allow-writes or --allow-destructive: the control is refused and its destructive request blocked.
  • Security resets are destructive. "Reset authenticator", "Disable two-factor", "Turn off 2FA", "Reset password", "Unlink security key", "End all sessions" and similar controls are refused without --allow-destructive (or a goal that asks for that action), like "Delete" and "Revoke". A bare "Reset" or "Reset filters" is unaffected.
  • Dialogs are dismissed by default and logged. A native window.confirm or prompt an action raises is recorded on the step. --dialogs accept confirms it, but a dialog that names a destructive, session-ending or paid action the run may not take, or matches --deny, is still dismissed.
  • The usability guard probe is opt-in and fail-safe. --probe-guards clicks destructive controls only to see whether they ask first: it aborts every write and destructive-looking request, refuses pages with a WebSocket, EventSource or service worker, and never answers a dialog. Point it at staging.
  • Extensions widen nothing else. --extension allows only the loaded extensions' chrome-extension://<id> origins; other extension origins stay refused.
  • Resource governance only touches its own. Orphan cleanup signals only browsers jevitate launched (found by an owner marker), never another process.
  • Hang replays never re-send a paid or destructive write. Such a hang is reported inconclusive unless you pass --hang-replay-writes. --paid <pattern> declares your app's own paid controls.
  • Environments bound Journey runs. With --env, the run's allowlist is the environment's baseUrl plus allow; a step on another origin is refused before a browser opens. .jevitate/environments.json may not hold a secret or a session.
  • demo never runs on production. It needs a named environment and refuses one flagged "production": true, when drafting and again on demo approve.
  • MCP can't widen any of this. Raw browser tools are forbidden, missions only target promoted targets, every path argument must resolve inside the project or ~/.jevitate/, and operator-only settings (shell hooks, the browser binary, which environment variable a secret comes from, log sources) are never MCP arguments. Approving or cancelling an inbox item is human-only.
Runs change the app's state. Guardrails stop a run from clicking what it must not; they don't undo what it legitimately did. Point adversarial runs at disposable accounts or seeded data, and reset state between runs: goal runs can do it themselves with --fixtures or --before/--after hooks, which also run around every replay.
Load testing is gated too. Driving more than one virtual actor requires an explicit authorized-target assertion and stays within budget and quiet hours. Reported metrics are labeled measured or modeled and never silently downgraded.