The core design: your agent never holds the credential
The point of the product is that credentials move out of your prompts, your.env, and your
agent’s context into a server-side vault. Your agent asks Sente for a logged-in account and gets
back one of:
- an interactive live view URL (human-watchable, take-over capable),
- a CDP URL to drive with your own Playwright or Puppeteer,
- an exported Playwright
storageStateto load into your own browser stack.
GET /v1/registrations/:id/credentials returns
403 CREDENTIALS_WRITE_ONLY, permanently, with no override. Rotation is re-submitting the
credentials. For a Sente-created account the generated password is retrievable through that
endpoint — we generated it for you and you need it — and every read is written to the audit log.
At rest
- Passwords and TOTP seeds are encrypted with AES-256-GCM before they reach the database: a fresh 12-byte IV per value plus the GCM authentication tag, stored as ciphertext only.
- Generated passwords are 20 characters from a CSPRNG, mixed character classes.
- The key is a 32-byte value held in the API and worker process environment on our production host. It is an application-level key — not a KMS- or HSM-held key. That upgrade is on the roadmap; this page will say so when it ships, not before.
- Exactly two code paths call the decrypt function: the run controller, at the moment a
register/login/connect run needs to type the credential, and the credentials endpoint above —
which is
403for connected accounts. So for a connected account, only the run controller ever decrypts it. - API keys (
sk_sente_…) are stored as SHA-256 hashes, never in plaintext, are scoped to one organization, and are revocable from the dashboard. Every identity, account, run, message, and audit row is org-scoped at the query level.
During a run: what the models see
Two different models touch your data, with very different exposure.1
The browser-driving agent gets the password
It is Claude, via the Anthropic API. When it must type your password into a login form, it receives
it — the same way a human contractor would have to. Mitigations:
- The run executes in an isolated remote browser session on a dedicated per-identity browser profile — persistent across that identity’s own runs so logins survive, never on your machine, never shared with another identity.
- The password is registered in the agent’s run-lifetime redaction vault before the session starts, so it is value-matched and scrubbed out of the agent’s logs and the persisted run record (step text, tool-call arguments, selector and semantic hints, captured URLs). The vault is cleared at run end.
- Anthropic does not train on API traffic.
2
The TOTP seed never reaches any model
When a site asks for an authenticator code, the driving agent stops and reports that it is blocked.
Our server computes the current RFC-6238 code from the vaulted seed and injects only that six-digit
code into the session. The seed itself is never in a prompt, a log, or a response. Wrong-seed or
clock-skew loops are capped at 3 injections per run, after which the run hands off to a human.
3
The email classifier treats mail as hostile input
Anyone on the internet can send mail to an
@sente.run address, so the model that classifies
inbound email (OTP / magic link / other) runs with a fixed system prompt, the email passed as
labelled untrusted data, no tools, and schema-constrained output that is re-validated
server-side. A hostile email can at worst be classified wrong — it cannot make the extractor act.Session handoff
- One live session per identity. Requesting a second returns
409rather than opening a parallel browser on the same profile. - Freshness, not hope.
getSessionhands the session back only if a login was confirmed within the freshness window (10 minutes by default); otherwise it re-logs in first, from the vault plus a fresh code from the account’s own inbox, so what you get was just confirmed logged in. - The CDP URL is a pass-through of the upstream browser provider’s session endpoint. It is session-scoped, expires with the session, carries no account key, and is never stored on our side — a new session mints a new one.
- A
session.exportis written to the audit log every time astorageStateis exported.
Audit trail
Credential and session access is written to an insert-only audit table, readable atGET /v1/audit-events and in the dashboard’s Security tab. The recorded actions are:
registration.create · connection.create · connection.revoke · connection.delete ·
credentials.read · credentials.write · session.open · session.close · session.export ·
run.resume · run.intervene · run.abort · identity.delete
Each row carries the action, the subject, a timestamp, and a small meta object. Writers never put
secret values in meta, and the endpoint never returns a credential. See
Audit trail.
Blast-radius controls
- Per-organization caps on identities, outbound sends, and runs — see Limits.
- Webhook URLs pass an SSRF guard: http(s) only, and the host must resolve exclusively to public addresses. Loopback, RFC-1918, CGNAT, and link-local ranges (including the cloud metadata endpoint) are denied for both IPv4 and IPv6, and the check runs again immediately before every dispatch, not just at registration.
- Runs are bounded in time: a 20-minute cap on the browser session, an 8-minute wait for a verification email, a ~10-minute hold when a run blocks for a human, and a 5-minute stale-heartbeat recovery that fails a run whose driver died.
- A global kill switch pauses all run claiming platform-wide in one operation; queued runs stay queued rather than being lost.
Revocation and deletion
What we don’t have yet
Read this before trusting us with production.- No SOC 2. Small team; happy to walk through the posture on a call and answer a questionnaire.
- No KMS-held keys — the vault key is application-level, as described above.
- Webhook deliveries are authenticated with a shared secret header (
X-Sente-Secret, one secret per organization), not an HMAC signature over the request body. Verify it with a timing-safe compare, and treat the payload as a notification to fetch by id rather than as trusted data. - The SSRF guard resolves, then checks. A DNS-rebinding race between the check and the request is theoretically possible; pinning the resolved address is a known follow-up.
- SMS 2FA and CAPTCHAs stop the run. It blocks, pages you, and waits for a human in an interactive live view. Sente does not attempt to complete a CAPTCHA. See Human takeover.
- SSO-only accounts (Google/Okta sign-in, passkeys) cannot be connected — there is no password to vault.
- Subprocessors: Anthropic (the models), a third-party remote-browser provider (the browser the agent drives), and AWS (hosting, database, inbound and outbound email). The current list in full is available on request.
If you’re the RBAC-minded reviewer
Don’t connect your root or production account. Create a scoped account or token for the agent in the target app’s own permission system — staging scope, or a non-destructive production role — connect that, and rotate it on your schedule. Rotation is one call and invalidates nothing else. Your model never sees the secret at any point in that loop, and your.env stays empty.
Next steps
Audit trail
Every credential and session access, queryable.
Limits
The caps, and which error code each one returns.
Acceptable use
What Sente is for, and what we refuse.
