Auth Flows
When a skill needs credentials from a web dashboard (API keys, session tokens), the flow is: discover with Playwright, implement with agentos.http. For steps that agentos.http can’t replay (native form POSTs, complex redirect chains), the agent uses Playwright for that step and agentos.http for everything after.
The pattern
- Discover — use the Playwright skill interactively to walk through the login/signup flow.
capture_networkreveals endpoints,cookiesshows what session cookies get set,inspectshows form structure. - Implement — write the login flow as Python +
agentos.httpin the skill’s.pyfile. Usehttp.headers()for WAF bypass and inject cookies fromparams.auth.cookiesor_callto other skills (e.g. Gmail for magic links,brave-browserfor Google session cookies). - Store — return extracted credentials via
__secrets__so the engine stores them securely. The LLM never sees raw secret values. - Test —
test-skills.cjsshould work without a running browser. If your skill needs Playwright at runtime, rethink the approach.
Dashboard connections
Skills with web dashboards declare a dashboard connection alongside their api connection:
connections:
api:
base_url: "https://api.example.com"
auth:
type: api_key
header: { x-api-key: .auth.key }
dashboard:
base_url: "https://dashboard.example.com"
auth:
type: cookies
domain: ".example.com"
login:
- sso: google
- email_link: true
All auth goes under a single auth: key with a type discriminator (api_key, cookies, oauth). The login block declares available login methods. Login operations are Python functions that execute the flow with agentos.http. See specs/auth-model.md in the engine repo for the unified auth model, and specs/sso-credential-bootstrap.md for the end-to-end bootstrap flow.
Secret-safe credential return
Login and API key extraction operations return credentials via __secrets__:
def get_api_key(*, _call=None, **params):
# ... HTTPX calls to get the key ...
return {
"__secrets__": [{
"issuer": "api.example.com",
"identifier": "user@example.com",
"item_type": "api_key",
"label": "Example API Key",
"source": "example",
"value": {"key": api_key},
"metadata": {"masked": {"key": "••••" + api_key[-4:]}}
}],
"__result__": {"status": "authenticated", "identifier": "user@example.com"}
}
The engine writes __secrets__ to the credential store, creates an account entity on the graph, and strips the secrets before the MCP response reaches the agent.
Cookie resolution chain
The engine uses timestamp-based resolution — all cookie sources are checked, and the one with the newest cookies wins. There’s no fixed priority order. See connections.md → Resolution Algorithm for the full explanation with worked examples.
Sources (all checked on every resolve):
- In-memory cache — cookies from the last extraction, updated by
Set-Cookieresponses from our own HTTP requests (writeback). Can be newer than the browser when a server rotates tokens. - Browser providers (Brave, Firefox) — fresh extraction from the browser’s local cookie database (~20ms). Reflects the user’s latest browsing.
- Credential store (
credentials.sqlite) — persistent copy, also updated by writeback. Survives engine restart.
The candidate with the highest newest_cookie_at (latest per-cookie timestamp) wins. On ties, the cache wins (first candidate). No TTL — timestamps are the only arbiter.
Playwright is always skipped unless explicitly requested via the provider parameter. It’s used for reverse engineering and login automation, not runtime auth.
Provider scoring (within the provider tier)
When multiple browser providers return cookies for the same domain:
- Required names — providers with all cookies listed in
auth.namesscore highest - Creation timestamp — most recently created cookies win
- Cookie count — final tiebreaker
Retry on auth failure
On SESSION_EXPIRED: prefix (or Python exceptions containing 401, 403,
unauthorized, forbidden), the engine:
- Marks the current provider as failed
- Excludes it from the candidate list
- Re-runs provider selection — next-best provider wins
- Retries the operation once with the new provider’s cookies
This means a skill with stale Brave cookies and fresh Playwright cookies will automatically fall back to Playwright after Brave fails. One retry only — no infinite loops.
Explicit provider override
For testing or when auto-selection picks wrong:
run({ skill: "amazon", tool: "list_orders", provider: "playwright" })
The provider argument bypasses the selection heuristic entirely.
Providers always return the full cookie jar
The names field in connection auth is purely a selection hint — it helps
the engine choose the right provider. Providers always return all cookies for the
domain, never a filtered subset. Skills that need the full cookie jar (which is
most of them) work correctly regardless of whether names is declared.
Key rules
- Never import Playwright in skill Python code. Playwright is a separate skill for investigation. Skill operations use
agentos.http. - All I/O through SDK modules.
http.get/post,shell.run,sql.query. Neverurllib,subprocess,sqlite3,requests,httpx. - Never expose secrets in
__result__. Secrets go in__secrets__only. The agent sees masked versions viametadata.masked. _callis same-skill only. It dispatches to sibling operations within the same skill (e.g. Gmail’slist_emailscallingget_email). It cannot call operations in other skills.- Cross-skill coordination goes through the agent. If a login flow needs email access, the operation yields back to the agent (see below), and the agent uses whatever email capability is available.
Agent-in-the-loop auth flows
Some login flows require input the skill can’t obtain on its own — a verification code from email, an SMS code, or user approval. These flows must yield back to the agent rather than trying to handle the dependency internally.
Why not handle it in Python?
_callis same-skill only — Python can’t callgmail.search_emailsfrom insideexa.py- Hardcoding a specific email skill (Gmail) couples the skill to that provider — what if the user uses Mimestream?
- Blocking in Python for 60 seconds while polling gives the agent no visibility or control
The multi-step pattern
Split the flow so the agent orchestrates between agentos.http operations and Playwright when needed:
Agent calls skill.send_login_code({ email })
→ Python/agentos.http: CSRF + trigger verification email
→ Returns: { status: "code_sent", hint: "..." }
Agent checks email (any provider) and extracts the code
Agent uses Playwright to complete login (if `agentos.http` can't replay the code submission)
→ Navigate to login page, type email, submit, type code, submit
→ Extract cookies from browser
Agent calls skill.store_session_cookies({ email, session_token, ... })
→ Python/agentos.http: validates session, stores via __secrets__
The hint field tells the agent what to search for (e.g. “subject ‘Sign in to Exa Dashboard’ from exa.ai”). The agent knows how to search email — it picks the right provider and extracts the code.
Why Playwright for the code submission? Some auth implementations (e.g. Exa’s NextAuth) submit verification codes via a native HTML form POST that HTTPX cannot replay — the server-side handling differs from a programmatic POST. The fetch interceptor captures nothing, but the browser navigates successfully. When this happens, use Playwright for the form submission step and agentos.http for everything else.
When to use this pattern
- Email verification codes (Exa, any NextAuth email provider)
- SMS/TOTP verification
- OAuth consent that requires user approval
- Any flow where the skill needs external input it can’t obtain via
_call - Any step where
agentos.httpreplay fails but the browser works (native form POSTs, complex redirect chains)
Example: Exa
See skills/exa/exa.py:
send_login_code— triggers the verification email (HTTPX)store_session_cookies— validates and stores browser-extracted session cookies (HTTPX)- The agent uses Playwright between these two operations to enter the code and complete login
Future: session-scoped state
Passing CSRF tokens through params works but is noisy. The target is session-scoped temporary storage (tied to the MCP/agent session) so Python can write state in step 1 and read it in step 2 without the agent seeing the plumbing. See the engine roadmap for “Session-scoped state for auth flows.”
For the full reverse engineering methodology, see:
- Auth & Runtime — credential bootstrap lifecycle, network interception, cookie mechanics, CSRF patterns, web navigation
- NextAuth.js guide — vendor-specific patterns for NextAuth/Auth.js sites
- WorkOS guide — vendor-specific patterns for WorkOS-based auth