From 61a0d122e3c796a759e6bc1574777d083ab2e1a2 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Thu, 18 Dec 2025 18:19:44 -0500 Subject: [PATCH 01/12] Add Agent Auth documentation Add documentation for the Agent Auth feature including overview, hosted UI, programmatic usage, credentials management, session monitoring, and early preview information. Also adds MCP CLI reference to docs.json. --- agent-auth/credentials.mdx | 342 +++++++ agent-auth/early-preview.mdx | 1383 +++++++++++++++++++++++++++++ agent-auth/hosted-ui.mdx | 355 ++++++++ agent-auth/overview.mdx | 210 +++++ agent-auth/programmatic.mdx | 519 +++++++++++ agent-auth/session-monitoring.mdx | 402 +++++++++ docs.json | 14 +- 7 files changed, 3224 insertions(+), 1 deletion(-) create mode 100644 agent-auth/credentials.mdx create mode 100644 agent-auth/early-preview.mdx create mode 100644 agent-auth/hosted-ui.mdx create mode 100644 agent-auth/overview.mdx create mode 100644 agent-auth/programmatic.mdx create mode 100644 agent-auth/session-monitoring.mdx diff --git a/agent-auth/credentials.mdx b/agent-auth/credentials.mdx new file mode 100644 index 0000000..7673b58 --- /dev/null +++ b/agent-auth/credentials.mdx @@ -0,0 +1,342 @@ +--- +title: "Credentials" +description: "Securely store login credentials for automated re-authentication" +--- + +Credentials allow you to securely store login information that enables fully automated authentication flows. When linked to an Auth Agent, credentials enable automatic re-authentication when sessions expire—without any user interaction. + +## Why Use Credentials? + +Without stored credentials, every time a session expires, you need to redirect users back through the login flow. With credentials: + +- **Automated re-auth** - Sessions can be refreshed automatically in the background +- **No user interaction** - Re-authentication happens without user involvement +- **Secure storage** - Credentials are encrypted at rest using per-organization keys +- **Never exposed** - Values are never returned in API responses or shared with LLMs + +## How Credentials Work + + + + Create a credential with the login values for a specific domain + + + Associate the credential with an Auth Agent + + + Run one successful authentication to save form selectors + + + When sessions expire, Agent Auth uses stored credentials and selectors to re-authenticate automatically + + + +## Creating Credentials + +Store credentials using the Credentials API. Values are encrypted immediately and cannot be retrieved. + + +```typescript TypeScript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// Create a credential +const credential = await kernel.credentials.create({ + name: 'my-netflix-login', // Unique name within your org + domain: 'netflix.com', // Target domain + values: { // Key-value pairs of login fields + email: 'user@example.com', + password: 'secretpassword123', + }, +}); + +console.log('Credential created:', credential.id); +// Note: values are NOT returned - only metadata +``` + +```python Python +from kernel import Kernel + +kernel = Kernel() + +# Create a credential +credential = await kernel.credentials.create( + name="my-netflix-login", + domain="netflix.com", + values={ + "email": "user@example.com", + "password": "secretpassword123", + }, +) + +print(f"Credential created: {credential.id}") +``` + + +**Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `name` | Yes | Unique name for the credential within your organization | +| `domain` | Yes | Target domain this credential is for (e.g., `netflix.com`) | +| `values` | Yes | Object containing field name → value pairs | + + +Credential values are write-only. Once stored, they cannot be retrieved via the API. Only metadata (name, domain, created_at) is returned. + + +## Linking Credentials to Auth Agents + +There are two ways to link credentials to an Auth Agent: + +### Option 1: Link During Auth Agent Creation + +```typescript +// Create auth agent with credential link +const agent = await kernel.agents.auth.create({ + target_domain: 'netflix.com', + profile_name: 'my-profile', + credential_id: credential.id, // Link the credential +}); +``` + +### Option 2: Save Credentials During Auth Flow + +During an authentication invocation, you can save the entered credentials: + +```typescript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'my-netflix-login', // Save credentials when login succeeds +}); +``` + +This approach: +- Creates the credential automatically when login succeeds +- Links it to the Auth Agent +- Saves the form selectors for future re-auth + +## Using Stored Credentials + +Once credentials are linked and an initial login has completed (capturing form selectors), the Auth Agent can re-authenticate automatically. + +### Checking Re-auth Capability + +```typescript +const agent = await kernel.agents.auth.retrieve(agentId); + +console.log('Status:', agent.status); // AUTHENTICATED or NEEDS_AUTH +console.log('Can Reauth:', agent.can_reauth); // true if credentials + selectors exist +console.log('Credential ID:', agent.credential_id); +console.log('Has Selectors:', agent.has_selectors); +``` + +An Auth Agent `can_reauth` when: +1. It has a linked credential (`credential_id` is set) +2. It has saved form selectors from a previous successful login (`has_selectors` is true) + +### Triggering Re-authentication + +When sessions expire, trigger re-authentication: + +```typescript +const reauth = await kernel.agents.auth.reauth(agent.id); + +switch (reauth.status) { + case 'reauth_started': + console.log('Re-auth started, invocation:', reauth.invocation_id); + // Poll for completion + break; + case 'already_authenticated': + console.log('Session is still valid'); + break; + case 'cannot_reauth': + console.log('Cannot re-auth:', reauth.message); + // Missing credentials or selectors - need manual login + break; +} +``` + +See [Session Monitoring](/agent-auth/session-monitoring) for automated session management. + +## Managing Credentials + +### List Credentials + +```typescript +const credentials = await kernel.credentials.list({ + domain: 'netflix.com', // Optional: filter by domain +}); + +for (const cred of credentials) { + console.log(`${cred.name} (${cred.domain}) - Created: ${cred.created_at}`); +} +``` + +### Get Credential Details + +```typescript +const credential = await kernel.credentials.retrieve(credentialId); + +console.log('Name:', credential.name); +console.log('Domain:', credential.domain); +console.log('Created:', credential.created_at); +console.log('Updated:', credential.updated_at); +// Note: values are never returned +``` + +### Update Credentials + +```typescript +await kernel.credentials.update(credentialId, { + name: 'updated-name', // Optional: update name + values: { // Optional: update values + email: 'newemail@example.com', + password: 'newpassword', + }, +}); +``` + +### Delete Credentials + +```typescript +await kernel.credentials.delete(credentialId); +``` + + +Deleting a credential unlinks it from any associated Auth Agents. Those agents will no longer be able to re-authenticate automatically. + + +## Credential Values Schema + +The `values` object is flexible—it stores whatever key-value pairs you provide. Common patterns: + +### Basic Email/Password + +```typescript +values: { + email: 'user@example.com', + password: 'secretpassword', +} +``` + +### Username/Password + +```typescript +values: { + username: 'myusername', + password: 'secretpassword', +} +``` + +### Multiple Fields + +Some sites have additional fields (company ID, account number, etc.): + +```typescript +values: { + company_id: 'ACME123', + username: 'jsmith', + password: 'secretpassword', +} +``` + +## Complete Example: Automated Auth Flow + +Here's a complete example setting up fully automated authentication: + +```typescript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +async function setupAutomatedAuth() { + // 1. Create credential + const credential = await kernel.credentials.create({ + name: 'acme-portal-login', + domain: 'portal.acme.com', + values: { + email: 'agent@company.com', + password: 'secure-password-123', + }, + }); + + // 2. Create auth agent with credential + const agent = await kernel.agents.auth.create({ + target_domain: 'portal.acme.com', + profile_name: 'acme-agent-profile', + credential_id: credential.id, + login_url: 'https://portal.acme.com/login', + }); + + // 3. Complete initial login to capture selectors + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); + + console.log('Complete initial login at:', invocation.hosted_url); + // User completes login once... + + // After initial login, agent.can_reauth will be true +} + +async function useAuthenticatedBrowser(agentId: string) { + // Check if re-auth is needed + const agent = await kernel.agents.auth.retrieve(agentId); + + if (agent.status === 'NEEDS_AUTH') { + if (agent.can_reauth) { + // Automated re-auth + const reauth = await kernel.agents.auth.reauth(agentId); + if (reauth.status === 'reauth_started') { + // Wait for re-auth to complete + await pollForCompletion(reauth.invocation_id); + } + } else { + throw new Error('Cannot re-auth - manual login required'); + } + } + + // Use the authenticated profile + const browser = await kernel.browsers.create({ + profile: { name: agent.profile_name }, + stealth: true, + }); + + return browser; +} +``` + +## Security + +Credentials are designed with security as the top priority: + +| Security Feature | Description | +|-----------------|-------------| +| **Encryption at rest** | All credential values are encrypted using per-organization keys | +| **Write-only values** | Values cannot be retrieved via the API after creation | +| **Never logged** | Credential values are never written to logs | +| **Never shared with LLMs** | Values are injected directly into form fields, never passed through AI models | +| **Isolated execution** | Authentication runs in an isolated browser environment | + +## Best Practices + +1. **Use descriptive names** - Name credentials clearly (e.g., `production-crm-login`, `staging-portal-admin`) + +2. **One credential per account** - Create separate credentials for different user accounts + +3. **Monitor auth status** - Use [Session Monitoring](/agent-auth/session-monitoring) to detect when re-auth is needed + +4. **Handle re-auth failures** - If automated re-auth fails (password changed, new 2FA method), fall back to manual login + +## Next Steps + + + + Automatically detect and handle expired sessions + + + Use the hosted UI for initial login + + diff --git a/agent-auth/early-preview.mdx b/agent-auth/early-preview.mdx new file mode 100644 index 0000000..dfca036 --- /dev/null +++ b/agent-auth/early-preview.mdx @@ -0,0 +1,1383 @@ +--- +title: "Early Preview" +description: "Agent Auth early preview documentation for testers" +--- + + +This is early preview documentation for Agent Auth testers. Features and APIs may change before general availability. + + +## What is Agent Auth? + +Agent Auth is an AI-powered authentication system that helps you log users into any website and save their authenticated browser session (profile) for future automations. It uses web agents to intelligently discover login forms, dynamically generate credential inputs, and handle multi-step authentication flows like 2FA/OTP. + +**Key features:** + +- Automatically detects login form fields on any website +- Dynamically generates forms based on discovered fields +- Handles multi-step auth (2FA, OTP, magic links) +- Saves authenticated browser profiles for reuse +- Hourly session checks to detect when re-authentication is needed +- Credentials are encrypted and submitted programmatically—never stored in plaintext or shared with LLMs +- **NEW:** Store credentials for fully automated re-authentication +- **NEW:** `reauth()` endpoint for programmatic session refresh + +## Choose Your Integration + +| Flow | Who enters credentials? | Best for | +|------|------------------------|----------| +| **Hosted UI** | User, in Kernel's hosted page | Simplest integration, minimal code | +| **Custom UI** | User, in YOUR app's UI | Full control over login UX | +| **Programmatic** | No one (you provide them in code) | Automated/headless, bot accounts | + +## How It Works + +1. Your backend calls `kernel.agents.auth.create()` to setup the agent, then calls `kernel.agents.auth.invocations.create()` to start the flow → Returns `invocation_id`, `hosted_url`, `handoff_code`, and `expires_at` +2. User is redirected to `hosted_url` → Enters credentials when prompted → Handles 2FA/OTP if needed → Profile saved on success +3. Your backend polls invocation status → Uses saved profile in future browser automations +4. After authentication completes, Agent Auth automatically monitors the session every hour and updates the status if re-authentication is required +5. **NEW:** If credentials are stored, Agent Auth can automatically re-authenticate when sessions expire + + +**Plan for re-auth from the start.** If you want automated re-authentication later, use `save_credential_as` during the initial login. This saves credentials and form selectors, enabling `can_reauth` for future sessions. + + +## Key Concepts + +### The Three Primitives + +| Concept | What it is | Lifespan | +|---------|-----------|----------| +| **Profile** | A saved browser session (cookies, localStorage). Once authenticated, use it to get logged-in browsers. | Permanent | +| **Auth Agent** | Binds a profile to a domain. Tracks auth status, credentials, and selectors. | Permanent | +| **Invocation** | A single login attempt. Created each time you need to authenticate. | 5 minutes | + +### Auth Agent + +An Auth Agent is a persistent entity that manages authentication for a specific domain + profile combination. You create or retrieve Auth Agents using `kernel.agents.auth.create()`. This method is idempotent—if an agent already exists for the domain and profile, it returns the existing one. + +The Auth Agent stores: + +- The target domain (e.g., `doordash.com`) +- The linked browser profile +- The login URL (learned during discovery, used to speed up future invocations) +- The auth check URL (where to verify logged-in state) +- Current authentication status (`AUTHENTICATED` or `NEEDS_AUTH`) +- **NEW:** Linked credential ID for automated re-auth +- **NEW:** Saved form selectors for deterministic re-auth + +**Auth Agent fields:** + +| Field | Description | +|-------|-------------| +| `id` | Unique identifier for the agent | +| `status` | `AUTHENTICATED` or `NEEDS_AUTH` | +| `credential_id` | ID of linked credential (if any) | +| `credential_name` | Name of linked credential (if any) | +| `can_reauth` | `true` if agent has credentials AND selectors for automated re-auth | +| `has_selectors` | `true` if form selectors were saved from a previous login | +| `last_auth_check_at` | When the last session health check was performed | + + +**When is `can_reauth` true?** The agent must have BOTH: +1. A linked credential (`credential_id` is set) +2. Saved form selectors from a previous successful login (`has_selectors` is true) + +If you want automated re-auth later, use `save_credential_as` during your first login flow. + + +### Auth Agent Invocation + +An Invocation is a single authentication attempt that belongs to an Auth Agent. You start a new Invocation by calling `kernel.agents.auth.invocations.create(agent.id)`. Over time, an Auth Agent accumulates multiple Invocations as sessions need to be established or refreshed. + +**Invocation expires in 5 minutes.** If the user doesn't complete login in time, the invocation status becomes `EXPIRED`. + +**Invocation Types:** + +- `login` - Authentication flow (used for both first-time login and re-authentication) +- `step_up` - Real-time authentication challenge (coming soon) + +### Status Reference + +Different endpoints return different status values. Here's the complete reference: + +**Invocation status** (from `invocations.retrieve()`): + +| Status | Description | +|--------|-------------| +| `IN_PROGRESS` | Invocation is active, user is completing login | +| `SUCCESS` | Login completed successfully, profile is saved | +| `EXPIRED` | Invocation timed out (5 minutes) | +| `CANCELED` | Invocation was explicitly canceled | + +**Invocation create response** (from `invocations.create()`): + +| Status | Description | +|--------|-------------| +| `invocation_created` | New invocation started, redirect user to `hosted_url` | +| `already_authenticated` | Profile is already logged in, no invocation needed | + +**Auth Agent status** (from `agents.auth.retrieve()`): + +| Status | Description | +|--------|-------------| +| `AUTHENTICATED` | Profile has a valid logged-in session | +| `NEEDS_AUTH` | Session expired or never logged in | + +**Reauth response** (from `agents.auth.reauth()`): + +| Status | Description | +|--------|-------------| +| `reauth_started` | Automated re-auth started, poll `invocation_id` for completion | +| `already_authenticated` | Session is still valid, no action needed | +| `cannot_reauth` | Missing credentials or selectors, manual login required | + +### Automatic Session Monitoring + +Auth Agents with `AUTHENTICATED` status are automatically checked hourly to verify the session is still valid. If the check detects the user has been logged out (session expired, cookies cleared by the site, etc.), the Auth Agent status is updated to `NEEDS_AUTH`. + +**How to use this:** + +- Poll `kernel.agents.auth.retrieve(id)` periodically to check if re-authentication is needed +- When status changes from `AUTHENTICATED` to `NEEDS_AUTH`: + - If `can_reauth` is `true`: Call `kernel.agents.auth.reauth(id)` for automated re-auth + - Otherwise: Trigger a new auth flow with `kernel.agents.auth.invocations.create(agent.id)` +- The check runs passively and doesn't modify the saved profile + +### What Discover Does + +When you call the discover endpoint on an Invocation, the Auth Agent: + +1. Navigates to the target domain (or directly to `login_url` if provided) +2. Finds the login page by looking for sign-in links/buttons +3. Extracts all form input fields on the page (email, password, etc.) +4. Returns the discovered fields with their names, types, labels, and selectors + +If the profile is already logged in from a previous session, discover returns `logged_in: true`. At that point, discovery is complete, no fields will be returned, and you should simply proceed to use the associated profile. No credentials or further steps are required. + +### What Submit Does + +When you call the submit endpoint on an Invocation, the Auth Agent: + +1. Fills in the form fields using the `field_values` you provide (matched by field name) +2. Clicks the submit button +3. Waits for the page to respond +4. Analyzes what happened next + +The AI determines the outcome by looking at the page after submission: + +- **Logged in** - The page shows a logged-in state (dashboard, account page, etc.) → Returns `logged_in: true` +- **Error displayed** - The page shows an error message → Returns `success: false` with `error_message` +- **New fields appeared** - The page now shows different form fields (e.g., OTP input after email/password) → Returns `needs_additional_auth: true` with `additional_fields` + +When you get `additional_fields`, collect those values from the user and call submit again. This loop continues until you get `logged_in: true` or an error. + +--- + +## Tips for Early Preview Testing + +- **Start simple** - Use the Hosted UI (Option 1) and test with sites that have basic email/password login +- **Provide login_url when known** - Speeds up discovery by skipping the login page search +- **Handle 2FA** - Test flows with 2FA to ensure your integration handles `additional_fields` +- **Poll with backoff** - Start at 2s intervals, increase to 5s. Max timeout is 5 minutes. See polling example below. +- **Clean up profiles** - Delete test profiles with `kernel.profiles.delete(profileName)` when done to avoid clutter +- **Plan for re-auth** - Use `save_credential_as` during initial login to enable automated re-auth later +- **Don't log credentials** - Never log `field_values` or credential data + +--- + +## Support + +Questions or issues? Reach out to us on Slack! + +--- + +## Installation (Early Preview) + +Before using Agent Auth, install the early preview SDK: + +**TypeScript/Node.js:** + +```json +{ + "dependencies": { + "@onkernel/sdk": "https://pkg.stainless.com/s/kernel-typescript/5ace6e2da99e73ad2d863be786213bb9fd28ce53/dist.tar.gz" + } +} +``` + +**Python (requirements.txt):** + +``` +kernel @ https://pkg.stainless.com/s/kernel-python/e7cab450cadd635e0d94db9618e4d729f5d97bcf/kernel-0.23.0-py3-none-any.whl +``` + +Or in pyproject.toml: + +```toml +[project] +dependencies = [ + "kernel @ https://pkg.stainless.com/s/kernel-python/e7cab450cadd635e0d94db9618e4d729f5d97bcf/kernel-0.23.0-py3-none-any.whl" +] +``` + +--- + +## Integration Options + + +**Quick Start Examples:** We have standalone example scripts for each integration pattern: +- **Developer Pre-creates Credential** - When you already have user credentials stored +- **Hosted UI Flow** - Redirect users to complete login themselves +- **Programmatic Flow** - Full control over the login process + + +### Option 1: Hosted UI (Recommended) + +The simplest approach—redirect users to our hosted authentication UI, then poll for completion. + +#### Happy Path in 30 Seconds + +```typescript +// 1. Create agent + invocation +const agent = await kernel.agents.auth.create({ target_domain: 'example.com', profile_name: 'user-123' }); +const invocation = await kernel.agents.auth.invocations.create({ auth_agent_id: agent.id }); + +// 2. Redirect user (or skip if already_authenticated) +if (invocation.status !== 'already_authenticated') { + redirect(invocation.hosted_url); +} + +// 3. Poll for completion, then use the profile +const browser = await kernel.browsers.create({ profile: { name: 'user-123' }, stealth: true }); +``` + +#### Hosted UI Redirect Contract + +| Question | Answer | +|----------|--------| +| Is `hosted_url` single-use? | No, user can refresh the page | +| Does Kernel redirect back to my app? | No, you must poll for completion | +| How do I detect user abandoned? | Invocation status becomes `EXPIRED` after 5 minutes | +| Can I pass state/return_url? | Not currently supported | + +#### Step 1: Start the Auth Flow + +```typescript +import { Kernel } from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// Step 1: Create or Find Auth Agent +const agent = await kernel.agents.auth.create({ + target_domain: 'netflix.com', + profile_name: 'netflix-user-123', + login_url: 'https://netflix.com/login', // Optional: speeds up discovery + proxy: { proxy_id: 'proxy_abc123' }, // Optional: use a proxy +}); + +// Step 2: Start the Auth Flow +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'netflix-creds', // NEW: Save credentials for future re-auth +}); + +// invocation returns: status, invocation_id, hosted_url, handoff_code, expires_at +``` + +#### Step 2: Redirect User + +```typescript +// Check if already logged in +if (invocation.status === 'already_authenticated') { + console.log('Already logged in! Profile is ready to use.'); + // Skip to Step 4 - Use the Profile +} else { + // Redirect to hosted flow + window.location.href = invocation.hosted_url; +} +``` + +#### Step 3: Poll for Completion + +```typescript +// Recommended: Poll with backoff (2s → 3s → 5s), max 5 minutes +async function pollForCompletion(invocationId: string) { + const maxWaitMs = 5 * 60 * 1000; // 5 minutes + const start = Date.now(); + let delay = 2000; + + while (Date.now() - start < maxWaitMs) { + const status = await kernel.agents.auth.invocations.retrieve(invocationId); + + if (status.status === 'SUCCESS') { + return { success: true, status }; + } + if (status.status === 'EXPIRED' || status.status === 'CANCELED') { + return { success: false, reason: status.status }; + } + + await new Promise(r => setTimeout(r, delay)); + delay = Math.min(delay * 1.5, 5000); // Cap at 5s + } + + return { success: false, reason: 'TIMEOUT' }; +} + +const result = await pollForCompletion(invocation.invocation_id); + +if (result.success) { + console.log('Success! Profile is ready.'); + + // Verify auth agent status + const authAgent = await kernel.agents.auth.retrieve(agent.id); + console.log(authAgent.status); // "AUTHENTICATED" + console.log(authAgent.can_reauth); // true if credentials were saved +} +``` + + +**Don't poll too aggressively.** Use exponential backoff starting at 2 seconds. Invocations typically complete in 10-60 seconds depending on the site and user speed. + + +#### Step 4: Use the Profile + +```typescript +// Create a browser with a saved profile - the user is already logged in! +const browser = await kernel.browsers.create({ + stealth: true, + profile: { + name: 'netflix-user-123', // Use the profile name from Step 1 + }, + proxy_id: 'proxy_abc123', // If you used a proxy in Step 1, use the same one here +}); + +// browser.session_id - Session ID +// browser.cdp_ws_url - CDP URL for Playwright/Puppeteer +// Run your automation - the browser is already authenticated! + +// Cleanup when done +await kernel.browsers.deleteByID(browser.session_id); +``` + + +Use `stealth: true` and the same proxy configuration you passed to `kernel.agents.auth.create()`. Agent Auth runs the authentication browser session with stealth mode enabled—mismatched settings may cause unexpected behavior with bot detection or session issues. + + +#### Step 5: Handle Re-authentication + +When sessions expire, check if automated re-auth is available: + +```typescript +// Check auth agent status +const agent = await kernel.agents.auth.retrieve(agentId); + +if (agent.status === 'NEEDS_AUTH') { + if (agent.can_reauth) { + // Automated re-auth (no user interaction needed) + const reauth = await kernel.agents.auth.reauth(agent.id); + + if (reauth.status === 'reauth_started') { + // Poll for completion using the same backoff pattern + const result = await pollForCompletion(reauth.invocation_id); + console.log('Re-auth complete:', result); + } else if (reauth.status === 'already_authenticated') { + console.log('Session is still valid'); + } else if (reauth.status === 'cannot_reauth') { + // Selectors may have drifted - fall back to manual login + console.log('Cannot reauth:', reauth.message); + } + } else { + // Manual re-auth required - redirect user to hosted UI + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'netflix-creds', // Save credentials this time! + }); + window.location.href = invocation.hosted_url; + } +} +``` + + +**If reauth fails:** The site's login form may have changed (selectors drifted). Fall back to creating a new invocation for manual login—this will re-capture the updated selectors. + + +--- + +### Option 2: Custom UI with Discover/Submit APIs + +Build your own authentication UI using the lower-level discover and submit APIs. You control the UI, but need to handle the multi-step flow yourself. + +#### When to Use Custom UI + +- You want login to match your app's design +- You need to collect credentials in your own form +- You're building a native mobile app + +#### Step 1: Start the Auth Invocation + +```typescript +import { Kernel } from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// Step 1: Create Agent & Start Invocation +const agent = await kernel.agents.auth.create({ + target_domain: 'doordash.com', + profile_name: 'my-profile-123', +}); + +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'doordash-creds', // Optional: save for re-auth +}); +``` + +#### Step 2: Exchange Handoff Code for JWT + +```typescript +const exchangeResponse = await kernel.agents.auth.invocations.exchange( + invocation.invocation_id, + { code: invocation.handoff_code } +); + +const jwt = exchangeResponse.jwt; // 30 min TTL +``` + +#### Step 3: Create JWT-Authenticated Client + +```typescript +const jwtKernel = new Kernel({ apiKey: jwt }); +``` + +#### Step 4: Discover Login Fields + +```typescript +const discoverResponse = await jwtKernel.agents.auth.invocations.discover( + invocation.invocation_id, + {} // Optional: { login_url: 'https://example.com/login' } +); + +if (discoverResponse.logged_in) { + // Already logged in! Profile saved. Skip to using the profile. +} + +if (discoverResponse.success && discoverResponse.fields) { + // Render these fields in your UI + // Fields are returned in DOM order—display them as-is +} +``` + + +**Discover is idempotent.** If the user refreshes or you call discover again, it returns the cached fields from the same invocation. You don't need to store fields—just call discover again to resume. + + +**About discovered fields:** + +- Fields are returned in DOM order from the login page +- Currently supports: `text`, `email`, `password`, `tel`, `number`, `code` +- Does NOT include CAPTCHAs, SSO buttons, or "remember me" checkboxes +- `additional_fields` (from submit) may include 2FA codes, security questions, or "select account" prompts—treat them generically as "more fields to fill" + +#### Step 5: Map Discovered Fields to Credentials + +The discover response returns an array of fields. Each field has a `name` property that you use as the key when submitting credentials. + +**Example discover response:** + +```json +{ + "success": true, + "fields": [ + { "name": "email", "type": "email", "label": "Email Address" }, + { "name": "password", "type": "password", "label": "Password" } + ] +} +``` + +**To submit, use each field's `name` as the key in `field_values`:** + +```json +{ + "field_values": { + "email": "user@example.com", + "password": "secretpassword" + } +} +``` + +**Recommended mapping strategy:** + +```typescript +// Mapping priority: +// 1. Match by field.type (most reliable) +// 2. Match by field.name patterns (fallback) +// 3. Log unmapped fields for debugging + +function mapCredentialsToFields( + fields: Array<{ name: string; type: string }>, + credentials: { email: string; password: string; code?: string } +): Record { + const fieldValues: Record = {}; + const unmapped: string[] = []; + + for (const field of fields) { + const name = field.name.toLowerCase(); + const type = field.type.toLowerCase(); + + // Priority 1: Match by type + if (type === 'email') { + fieldValues[field.name] = credentials.email; + } else if (type === 'password') { + fieldValues[field.name] = credentials.password; + } else if (type === 'code') { + fieldValues[field.name] = credentials.code || ''; + } + // Priority 2: Match by name patterns + else if (name.includes('email') || name.includes('user')) { + fieldValues[field.name] = credentials.email; + } else if (name.includes('pass')) { + fieldValues[field.name] = credentials.password; + } else { + unmapped.push(field.name); + } + } + + // Log unmapped fields for debugging (but never log actual values!) + if (unmapped.length > 0) { + console.warn('Unmapped fields:', unmapped); + } + + return fieldValues; +} + +const fieldValues = mapCredentialsToFields(discoverResponse.fields, userCredentials); +``` + + +**Never log `field_values` or credential data.** Only log field names for debugging. + + +#### Step 6: Submit Credentials (Loop Until Logged In) + +```typescript +let submitResponse = await jwtKernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } +); + +// Handle multi-step auth flows +while (submitResponse.needs_additional_auth && submitResponse.additional_fields) { + // Map the additional fields the same way + const additionalValues: Record = {}; + + for (const field of submitResponse.additional_fields) { + if (field.type === 'code' || field.name.includes('code') || field.name.includes('otp')) { + // Prompt user for their 2FA code + additionalValues[field.name] = await promptUserForOTP(); + } else if (field.type === 'password' || field.name.includes('password')) { + additionalValues[field.name] = userCredentials.password; + } + // Add more mappings as needed + } + + // Submit again with the new fields + submitResponse = await jwtKernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: additionalValues } + ); +} + +// Check final result +if (submitResponse.logged_in) { + // Success! Profile saved. +} + +if (submitResponse.error_message) { + // Login failed, show error to user + console.error(submitResponse.error_message); +} +``` + +**Field mapping tips:** + +- Always use `field.name` as the key in `field_values`—this is what the agent uses to match values to form inputs +- Check both `field.type` and `field.name` for matching—some sites use generic types but descriptive names +- Common patterns: `type: "email"` → user's email, `type: "password"` → user's password, `type: "code"` → 2FA/OTP code +- For `additional_fields` with type `code`, you'll typically need to prompt the user in real-time for their 2FA code + +--- + +### Option 3: Programmatic (Headless/Automated) + +For bots, service accounts, or automated testing where you provide credentials in code and no user is involved. + +#### When to Use Programmatic + +- Bot or service accounts with known credentials +- Automated testing/CI pipelines +- Backend-only flows with no user interaction + +#### Key Differences from Custom UI + +- You provide credentials from environment variables or a secrets manager +- No user prompt for 2FA—you need a TOTP generator or alternate method +- Use API key auth directly (no JWT exchange needed) + +```typescript +import { Kernel } from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// Credentials from your secrets manager +const CREDS = { + email: process.env.BOT_EMAIL!, + password: process.env.BOT_PASSWORD!, +}; + +// 1. Create agent + invocation +const agent = await kernel.agents.auth.create({ + target_domain: 'example.com', + profile_name: 'bot-profile', + login_url: 'https://example.com/login', // Recommended for bots +}); + +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'bot-creds', // Enable reauth +}); + +if (invocation.status === 'already_authenticated') { + console.log('Already logged in!'); +} else { + // 2. Discover fields (API key auth works, no JWT needed) + const discover = await kernel.agents.auth.invocations.discover( + invocation.invocation_id, + {} + ); + + if (discover.logged_in) { + console.log('Already logged in during discover!'); + } else if (discover.fields) { + // 3. Map credentials to fields + const fieldValues = mapCredentialsToFields(discover.fields, CREDS); + + // 4. Submit + const result = await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } + ); + + if (result.logged_in) { + console.log('Login successful!'); + } else if (result.needs_additional_auth) { + // Handle 2FA - see note below + console.log('2FA required:', result.additional_fields); + } + } +} + +// 5. Use the profile +const browser = await kernel.browsers.create({ + profile: { name: 'bot-profile' }, + stealth: true, +}); +``` + + +**2FA with bots:** If the target site requires 2FA, you'll need one of: +- A TOTP generator library (for authenticator app codes) +- Access to the account's email API (for email codes) +- App-specific passwords (if the service supports them) +- A service account without 2FA + +If you can't automate 2FA, the programmatic flow won't work for that site. + + +--- + +## End-to-End Example: The Complete Lifecycle + +Here's a complete example showing the full lifecycle: initial auth, using the profile, detecting session expiry, and re-auth. + +```typescript +import { Kernel } from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// === INITIAL SETUP (run once per user) === +async function setupAuth(userId: string, targetDomain: string) { + const profileName = `user-${userId}-${targetDomain}`; + + // 1. Create agent + const agent = await kernel.agents.auth.create({ + target_domain: targetDomain, + profile_name: profileName, + }); + + // 2. Start auth flow + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: `${profileName}-creds`, // Enable future reauth! + }); + + if (invocation.status === 'already_authenticated') { + return { agentId: agent.id, profileName, ready: true }; + } + + // 3. Return hosted URL for user to complete login + return { + agentId: agent.id, + profileName, + ready: false, + hostedUrl: invocation.hosted_url, + invocationId: invocation.invocation_id, + }; +} + +// === USE THE PROFILE (run for each automation) === +async function runAutomation(agentId: string, profileName: string) { + // 1. Check if we need to re-auth + const agent = await kernel.agents.auth.retrieve(agentId); + + if (agent.status === 'NEEDS_AUTH') { + if (agent.can_reauth) { + // Automated re-auth + const reauth = await kernel.agents.auth.reauth(agentId); + if (reauth.status === 'reauth_started') { + await pollForCompletion(reauth.invocation_id); + } + } else { + throw new Error('Session expired and cannot reauth - user must log in again'); + } + } + + // 2. Create browser with authenticated profile + const browser = await kernel.browsers.create({ + profile: { name: profileName }, + stealth: true, + }); + + try { + // 3. Run your automation + // const page = await connectPlaywright(browser.cdp_ws_url); + // await page.goto('https://example.com/dashboard'); + // ... do your work ... + + return { success: true }; + } finally { + // 4. Always clean up + await kernel.browsers.deleteByID(browser.session_id); + } +} + +// === HELPER === +async function pollForCompletion(invocationId: string) { + const maxWaitMs = 5 * 60 * 1000; + const start = Date.now(); + let delay = 2000; + + while (Date.now() - start < maxWaitMs) { + const status = await kernel.agents.auth.invocations.retrieve(invocationId); + if (status.status !== 'IN_PROGRESS') return status; + await new Promise(r => setTimeout(r, delay)); + delay = Math.min(delay * 1.5, 5000); + } + throw new Error('Timeout waiting for auth'); +} +``` + +**What to store in your database:** + +| Data | Permanent? | Purpose | +|------|------------|---------| +| `agentId` | Yes | For reauth, status checks | +| `profileName` | Yes | For creating browsers | +| `invocationId` | No (5 min TTL) | Only while polling for completion | + +--- + +## Credentials API + +Store credentials for fully automated re-authentication. Credentials are encrypted at rest and never exposed in API responses. + +### Create Credential + +```typescript +const credential = await kernel.credentials.create({ + name: 'my-netflix-login', + domain: 'netflix.com', + values: { + email: 'user@example.com', + password: 'secretpassword', + }, +}); +``` + +### Link Credential to Auth Agent + +```typescript +const agent = await kernel.agents.auth.create({ + target_domain: 'netflix.com', + profile_name: 'my-profile', + credential_name: credential.name, // Link the credential by name +}); +``` + +### List Credentials + +```typescript +const response = await kernel.credentials.list({ + domain: 'netflix.com', // Optional filter +}); + +// Response is paginated - access items via .items +for (const credential of response.items) { + console.log(credential.id, credential.name); +} +``` + +### Delete Credential + +```typescript +await kernel.credentials.delete(credentialId); +``` + +--- + +## Reauth API + +Trigger automated re-authentication when sessions expire. + +```typescript +// Check if re-auth is needed +const agent = await kernel.agents.auth.retrieve(agentId); + +if (agent.status === 'NEEDS_AUTH' && agent.can_reauth) { + const reauth = await kernel.agents.auth.reauth(agent.id); + + switch (reauth.status) { + case 'reauth_started': + console.log('Re-auth in progress:', reauth.invocation_id); + // Poll for completion... + break; + case 'already_authenticated': + console.log('Session is still valid'); + break; + case 'cannot_reauth': + console.log('Cannot reauth:', reauth.message); + // Fall back to manual login + break; + } +} +``` + +**Requirements for `can_reauth`:** + +- Auth Agent must have a linked credential (`credential_id`) +- Auth Agent must have saved form selectors (`has_selectors`) from a previous successful login + +--- + +## API Reference + +### Auth Agents + +#### POST /agents/auth + +Create or find an auth agent for a domain/profile combination. This is idempotent—calling with the same domain and profile returns the existing agent. + +**Auth:** API key + +**Request:** + +```json +{ + "target_domain": "netflix.com", + "profile_name": "netflix-user-123", + "login_url": "https://netflix.com/login", + "credential_name": "my-netflix-login", + "proxy": { + "proxy_id": "proxy_abc123" + } +} +``` + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `target_domain` | Yes | Target domain for authentication | +| `profile_name` | Yes | Name of the profile to use | +| `login_url` | No | Login page URL to skip discovery | +| `credential_name` | No | Name of an existing credential to link for auto-fill and re-auth | +| `proxy` | No | Proxy configuration | + +**Response:** + +```json +{ + "id": "abc123", + "profile_name": "netflix-user-123", + "domain": "netflix.com", + "status": "NEEDS_AUTH", + "credential_id": "cred_abc123", + "credential_name": "my-netflix-login", + "can_reauth": false, + "has_selectors": false, + "last_auth_check_at": null +} +``` + +#### GET /agents/auth/{id} + +Get auth agent details and current authentication status. + +**Auth:** API key + +**Response:** + +```json +{ + "id": "abc123", + "profile_name": "netflix-user-123", + "domain": "netflix.com", + "status": "AUTHENTICATED", + "credential_id": "cred_abc123", + "credential_name": "my-netflix-login", + "can_reauth": true, + "has_selectors": true, + "last_auth_check_at": "2025-01-15T10:30:00Z" +} +``` + +#### DELETE /agents/auth/{id} + +Delete an auth agent. + +**Auth:** API key + +**Response:** 204 No Content + + +Deleting an auth agent does not delete the associated browser profile. Use `kernel.profiles.delete(profileName)` to clean up profiles separately. + + +#### GET /agents/auth + +List auth agents with optional filters. + +**Auth:** API key + +**Query Parameters:** + +| Parameter | Description | +|-----------|-------------| +| `profile_name` | Filter by profile name | +| `target_domain` | Filter by target domain | +| `limit` | Maximum results (default: 20, max: 100) | +| `offset` | Number of results to skip | + +**Response:** + +The SDK returns a paginated response object. Access items via the `items` property: + +```typescript +const response = await kernel.agents.auth.list({ target_domain: 'netflix.com' }); + +for (const agent of response.items) { + console.log(agent.id, agent.status); +} +``` + +**Raw API Response:** + +```json +[ + { + "id": "abc123", + "profile_name": "netflix-user-123", + "domain": "netflix.com", + "status": "AUTHENTICATED", + "credential_id": "cred_abc123", + "credential_name": "my-netflix-login", + "has_selectors": true, + "can_reauth": true, + "last_auth_check_at": "2025-01-15T10:30:00Z" + } +] +``` + +**Headers:** +- `X-Has-More`: Whether there are more results +- `X-Next-Offset`: Offset for next page + +#### POST /agents/auth/{id}/reauth + +Trigger automated re-authentication. + +**Auth:** API key + +**Response (reauth started):** + +```json +{ + "status": "reauth_started", + "invocation_id": "inv_xyz789" +} +``` + +**Response (already authenticated):** + +```json +{ + "status": "already_authenticated" +} +``` + +**Response (cannot reauth):** + +```json +{ + "status": "cannot_reauth", + "message": "Missing stored credential or form selectors" +} +``` + +--- + +### Auth Agent Invocations + +#### POST /agents/auth/invocations + +Create an invocation to start an auth flow for an existing auth agent. + +**Auth:** API key + +**Request:** + +```json +{ + "auth_agent_id": "abc123", + "save_credential_as": "my-saved-creds" +} +``` + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `auth_agent_id` | Yes | ID of the auth agent | +| `save_credential_as` | No | Save credentials with this name for future re-auth | + +**Response (invocation created):** + +```json +{ + "status": "invocation_created", + "invocation_id": "inv_xyz789", + "handoff_code": "aBcD123EfGh456IjKl789", + "hosted_url": "https://agent-auth.onkernel.com/agents/auth/invocations/inv_xyz789?code=aBcD123", + "expires_at": "2025-12-01T12:00:00Z" +} +``` + +**Response (already logged in - no invocation needed):** + +```json +{ + "status": "already_authenticated" +} +``` + + +When `status` is `already_authenticated`, the agent is already logged in and no invocation was created. You can proceed directly to using the profile. + + +#### GET /agents/auth/invocations/{invocation_id} + +Get invocation status and details. + +**Auth:** API key or JWT (from exchange) + +**Response:** + +```json +{ + "status": "IN_PROGRESS", + "app_name": "My App", + "target_domain": "doordash.com", + "expires_at": "2025-12-01T12:00:00Z" +} +``` + +| Status | Description | +|--------|-------------| +| `IN_PROGRESS` | User is completing login | +| `SUCCESS` | Login successful, profile saved | +| `EXPIRED` | Invocation timed out | +| `CANCELED` | Invocation was canceled | + +#### POST /agents/auth/invocations/{invocation_id}/exchange + +Exchange a handoff code for a JWT token. + +**Auth:** None (handoff code is the credential) + +**Request:** + +```json +{ + "code": "aBcD123EfGh456IjKl789" +} +``` + +**Response:** + +```json +{ + "invocation_id": "inv_xyz789", + "jwt": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +#### POST /agents/auth/invocations/{invocation_id}/discover + +Discover login form fields on the target site. + +**Auth:** API key or JWT (from exchange) + +**Request:** + +```json +{ + "login_url": "https://doordash.com/login" +} +``` + +| Parameter | Description | +|-----------|-------------| +| `login_url` | Override the stored login URL | + +**Response (fields found):** + +```json +{ + "success": true, + "logged_in": false, + "login_url": "https://identity.doordash.com/auth", + "page_title": "Sign In - DoorDash", + "fields": [ + { + "name": "email", + "type": "email", + "label": "Email", + "placeholder": "Enter your email", + "required": true, + "selector": "//input[@id='email']" + }, + { + "name": "password", + "type": "password", + "label": "Password", + "required": true, + "selector": "//input[@id='password']" + } + ] +} +``` + +**Response (already logged in):** + +```json +{ + "success": true, + "logged_in": true +} +``` + +#### POST /agents/auth/invocations/{invocation_id}/submit + +Submit credentials and attempt login. + +**Auth:** API key or JWT (from exchange) + +**Request:** + +```json +{ + "field_values": { + "email": "user@example.com", + "password": "********" + } +} +``` + +**Response (success):** + +```json +{ + "success": true, + "logged_in": true, + "app_name": "My App", + "target_domain": "doordash.com" +} +``` + +**Response (needs 2FA):** + +```json +{ + "success": true, + "logged_in": false, + "needs_additional_auth": true, + "additional_fields": [ + { + "name": "code", + "type": "code", + "label": "Verification Code", + "required": true, + "selector": "//input[@name='code']" + } + ] +} +``` + +**Response (error):** + +```json +{ + "success": false, + "logged_in": false, + "error_message": "Incorrect email or password" +} +``` + +--- + +### Credentials + +#### POST /credentials + +Create a new credential. + +**Auth:** API key + +**Request:** + +```json +{ + "name": "my-netflix-login", + "domain": "netflix.com", + "values": { + "email": "user@example.com", + "password": "secretpassword" + } +} +``` + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `name` | Yes | Unique name within your organization | +| `domain` | Yes | Target domain | +| `values` | Yes | Key-value pairs of credential fields | + +**Response:** + +```json +{ + "id": "cred_abc123", + "name": "my-netflix-login", + "domain": "netflix.com", + "created_at": "2025-01-15T10:30:00Z", + "updated_at": "2025-01-15T10:30:00Z" +} +``` + + +Credential values are write-only and never returned in API responses. + + +#### GET /credentials + +List credentials. + +**Auth:** API key + +**Query Parameters:** + +| Parameter | Description | +|-----------|-------------| +| `domain` | Filter by domain | +| `limit` | Maximum results (default: 20, max: 100) | +| `offset` | Number of results to skip | + +#### GET /credentials/{id} + +Get credential metadata (values not returned). + +#### PATCH /credentials/{id} + +Update a credential. + +**Request:** + +```json +{ + "name": "updated-name", + "values": { + "email": "newemail@example.com", + "password": "newpassword" + } +} +``` + +#### DELETE /credentials/{id} + +Delete a credential. + +--- + +### Field Types + +| Type | Description | +|------|-------------| +| `text` | Generic text input | +| `email` | Email address | +| `password` | Password (masked) | +| `tel` | Phone number | +| `number` | Numeric input | +| `url` | URL input | +| `code` | Verification code (OTP, 2FA) | + +--- + +## Security + +| Feature | Description | +|---------|-------------| +| **Credentials encrypted at rest** | All credential values are encrypted using per-organization keys | +| **Credentials never exposed** | Values are write-only and never returned in API responses | +| **Credentials never shared with LLMs** | Values are submitted programmatically directly to the target site | +| **Short-lived tokens** | JWT tokens expire after 30 minutes | +| **Handoff codes expire** | One-time use, expire after 5 minutes | +| **Profile encryption** | Browser profiles are encrypted at rest | +| **Isolated execution** | Each auth flow runs in an isolated browser environment | + +### FAQ + +**Does Kernel see my users' credentials?** + +Yes, when you use `submit()`, credentials pass through Kernel's servers to fill the form on the target site. They are: +- Encrypted in transit (TLS) +- Never logged or persisted beyond the immediate use +- Never passed to AI/LLM models +- Used only to fill form fields and click submit + +**Can I delete or rotate credentials?** + +Yes. Use `kernel.credentials.delete(id)` to delete, or `kernel.credentials.update(id, { values: {...} })` to rotate. + +**What if `save_credential_as` captures the wrong fields?** + +The credential captures whatever `field_values` you submit. If you submit extra fields or wrong mappings, those get saved. You can update or delete the credential and try again. + +**Should I log `field_values` for debugging?** + +Never log credential values. Log only field names if you need to debug mapping issues. diff --git a/agent-auth/hosted-ui.mdx b/agent-auth/hosted-ui.mdx new file mode 100644 index 0000000..4e0a670 --- /dev/null +++ b/agent-auth/hosted-ui.mdx @@ -0,0 +1,355 @@ +--- +title: "Hosted UI" +description: "The simplest way to authenticate users - redirect to our hosted authentication UI" +--- + +The Hosted UI flow is the recommended approach for most applications. You redirect users to a Kernel-hosted authentication page where they complete the login process, then poll for completion. + +## When to Use Hosted UI + +Use the Hosted UI when: +- Building user-facing applications where the user can complete login +- You want Kernel to handle the login UI and form discovery +- You need a quick integration with minimal code +- You want to avoid handling multi-step auth flows yourself + +## Complete Example + + +```typescript TypeScript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// Step 1: Create or get existing Auth Agent +const agent = await kernel.agents.auth.create({ + target_domain: 'doordash.com', + profile_name: 'doordash-user-123', + login_url: 'https://identity.doordash.com/auth', // Optional: speeds up discovery +}); + +// Step 2: Start the auth flow +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); + +// Step 3: Check if already logged in +if (invocation.status === 'already_authenticated') { + console.log('Already authenticated! Profile is ready to use.'); + // Skip to Step 6: Use the Profile +} else { + // Redirect user to hosted UI + console.log('Redirect user to:', invocation.hosted_url); + // In a web app: window.location.href = invocation.hosted_url; +} + +// Step 4: Poll for completion (do this on your backend) +const pollForCompletion = async (invocationId: string) => { + const maxWaitMs = 5 * 60 * 1000; // 5 minutes + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitMs) { + const status = await kernel.agents.auth.invocations.retrieve(invocationId); + + if (status.status === 'SUCCESS') { + return { success: true }; + } + if (status.status === 'EXPIRED' || status.status === 'CANCELED') { + return { success: false, reason: status.status }; + } + + // Poll every 2 seconds + await new Promise(r => setTimeout(r, 2000)); + } + + return { success: false, reason: 'TIMEOUT' }; +}; + +const result = await pollForCompletion(invocation.invocation_id); + +// Step 5: Verify the auth agent status +if (result.success) { + const authAgent = await kernel.agents.auth.retrieve(agent.id); + console.log('Auth status:', authAgent.status); // "AUTHENTICATED" +} + +// Step 6: Use the authenticated profile +const browser = await kernel.browsers.create({ + profile: { name: 'doordash-user-123' }, + stealth: true, +}); + +console.log('Browser ready:', browser.cdp_ws_url); +// Run your automation - the browser is already logged in! + +// Clean up when done +await kernel.browsers.deleteByID(browser.session_id); +``` + +```python Python +from kernel import Kernel +import asyncio + +kernel = Kernel() + +# Step 1: Create or get existing Auth Agent +agent = await kernel.agents.auth.create( + target_domain="doordash.com", + profile_name="doordash-user-123", + login_url="https://identity.doordash.com/auth", # Optional +) + +# Step 2: Start the auth flow +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, +) + +# Step 3: Check if already logged in +if invocation.status == "already_authenticated": + print("Already authenticated! Profile is ready to use.") +else: + print(f"Redirect user to: {invocation.hosted_url}") + +# Step 4: Poll for completion +async def poll_for_completion(invocation_id: str): + max_wait_seconds = 5 * 60 + start_time = asyncio.get_event_loop().time() + + while asyncio.get_event_loop().time() - start_time < max_wait_seconds: + status = await kernel.agents.auth.invocations.retrieve(invocation_id) + + if status.status == "SUCCESS": + return {"success": True} + if status.status in ("EXPIRED", "CANCELED"): + return {"success": False, "reason": status.status} + + await asyncio.sleep(2) + + return {"success": False, "reason": "TIMEOUT"} + +result = await poll_for_completion(invocation.invocation_id) + +# Step 5: Verify auth agent status +if result["success"]: + auth_agent = await kernel.agents.auth.retrieve(agent.id) + print(f"Auth status: {auth_agent.status}") + +# Step 6: Use the authenticated profile +browser = await kernel.browsers.create( + profile={"name": "doordash-user-123"}, + stealth=True, +) + +print(f"Browser ready: {browser.cdp_ws_url}") + +# Clean up when done +await kernel.browsers.delete_by_id(browser.session_id) +``` + + +## Step-by-Step Breakdown + +### 1. Create an Auth Agent + +An Auth Agent represents a (domain, profile) pair. Creating one is idempotent—if an agent already exists for the domain and profile, it returns the existing one. + +```typescript +const agent = await kernel.agents.auth.create({ + target_domain: 'example.com', // Required: domain to authenticate + profile_name: 'my-profile', // Required: profile to store session + login_url: 'https://example.com/login', // Optional: speeds up discovery +}); +``` + +**Parameters:** +| Parameter | Required | Description | +|-----------|----------|-------------| +| `target_domain` | Yes | The domain to authenticate (e.g., `netflix.com`) | +| `profile_name` | Yes | Name of the profile to store the authenticated session | +| `login_url` | No | Direct URL to the login page. Providing this speeds up discovery. | +| `proxy` | No | Proxy configuration (see [Proxies](/proxies/overview)) | + +### 2. Start an Invocation + +An invocation starts a new authentication attempt. It returns a `hosted_url` where the user completes login. + +```typescript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); +``` + +**Response fields:** +| Field | Description | +|-------|-------------| +| `invocation_id` | Unique ID for this auth attempt | +| `hosted_url` | URL to redirect the user to | +| `handoff_code` | One-time code for programmatic flows | +| `expires_at` | When the invocation expires (5 minutes) | +| `logged_in` | If `true`, already authenticated—no login needed | + + +If `logged_in` is `true`, the profile is already authenticated. Skip the redirect and go straight to using the profile. + + +### 3. Redirect the User + +In a web application, redirect the user to the hosted URL: + +```typescript +// Frontend code +window.location.href = invocation.hosted_url; +``` + +The hosted UI will: +1. Navigate to the target domain +2. Find and display the login form +3. Let the user enter credentials +4. Handle multi-step auth (2FA, OTP, etc.) +5. Save the authenticated session to the profile + +### 4. Poll for Completion + +On your backend, poll the invocation status until it completes: + +```typescript +let status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +while (status.status === 'IN_PROGRESS') { + await new Promise(r => setTimeout(r, 2000)); // Poll every 2 seconds + status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +switch (status.status) { + case 'SUCCESS': + console.log('Authentication successful!'); + break; + case 'EXPIRED': + console.log('User did not complete login in time'); + break; + case 'CANCELED': + console.log('Authentication was canceled'); + break; +} +``` + + +Poll every 2 seconds with a maximum timeout of 5 minutes. The invocation expires after 5 minutes if not completed. + + +### 5. Use the Authenticated Profile + +Once authentication succeeds, create browsers with the profile to get an already-logged-in session: + +```typescript +const browser = await kernel.browsers.create({ + profile: { name: 'my-profile' }, + stealth: true, +}); + +// Connect with Playwright +import { chromium } from 'playwright'; +const playwrightBrowser = await chromium.connectOverCDP(browser.cdp_ws_url); +const page = playwrightBrowser.contexts()[0].pages()[0]; + +// Navigate - you're already logged in! +await page.goto('https://example.com/dashboard'); +``` + + +Use `stealth: true` when creating browsers for authenticated sessions. Agent Auth runs authentication with stealth mode enabled, and mismatched settings may cause issues with bot detection. + + +## Using Proxies + +If the target site requires a specific IP or region, configure a proxy: + +```typescript +// Create auth agent with proxy +const agent = await kernel.agents.auth.create({ + target_domain: 'region-locked-site.com', + profile_name: 'my-profile', + proxy: { proxy_id: 'proxy_abc123' }, +}); + +// Use the same proxy when creating browsers +const browser = await kernel.browsers.create({ + profile: { name: 'my-profile' }, + proxy_id: 'proxy_abc123', + stealth: true, +}); +``` + + +Use the same proxy configuration for both the Auth Agent and subsequent browser sessions. Different IPs may trigger security measures on the target site. + + +## Saving Credentials for Re-auth + +You can save credentials during the hosted flow to enable automatic re-authentication when sessions expire: + +```typescript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'my-saved-creds', // Save credentials for future re-auth +}); +``` + +When credentials are saved, the Auth Agent can automatically re-authenticate without user interaction. See [Session Monitoring](/agent-auth/session-monitoring) for details. + +## Handling Callback URLs + +For web applications, you may want to redirect users back to your app after authentication completes. Use the invocation's `hosted_url` as a starting point and implement webhook or polling patterns: + +```typescript +// Backend: Start auth flow +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); + +// Frontend: Redirect to hosted UI +window.location.href = invocation.hosted_url; + +// Backend: Poll for completion and notify frontend via WebSocket or SSE +// OR: Use a webhook URL (coming soon) +``` + +## Error Handling + +Handle common error scenarios: + +```typescript +try { + const agent = await kernel.agents.auth.create({ + target_domain: 'example.com', + profile_name: 'my-profile', + }); + + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); + + // ... polling logic ... +} catch (error) { + if (error.status === 401) { + console.error('Invalid API key'); + } else if (error.status === 404) { + console.error('Auth agent not found'); + } else if (error.status === 409) { + console.error('Conflict - invocation already in progress'); + } else { + console.error('Unexpected error:', error.message); + } +} +``` + +## Next Steps + + + + Build custom auth flows with full control + + + Store credentials for automated re-auth + + diff --git a/agent-auth/overview.mdx b/agent-auth/overview.mdx new file mode 100644 index 0000000..e82e09a --- /dev/null +++ b/agent-auth/overview.mdx @@ -0,0 +1,210 @@ +--- +title: "Overview" +description: "Secure authentication for AI agents - log users into any website and save authenticated browser sessions" +--- + +Agent Auth is an AI-powered authentication system that helps you log users into any website and save their authenticated browser session (profile) for future automations. It uses web agents to intelligently discover login forms, dynamically generate credential inputs, and handle multi-step authentication flows like 2FA/OTP. + +## Why Agent Auth? + +AI agents can navigate the open web with ease, but the most valuable workflows live behind logins. To perform tasks on behalf of real users, agents need a secure way to access authenticated sessions. + +Agent Auth solves this by providing: + +- **Unified access to logged-in websites** - One API to log agents into any site with user consent +- **Automatic form discovery** - Detects login form fields on any website +- **Multi-step auth handling** - Handles 2FA, OTP, magic links, and CAPTCHAs +- **Credential security** - Credentials are encrypted and submitted programmatically—never stored in plaintext or shared with LLMs +- **Session persistence** - Saves authenticated browser profiles for reuse +- **Automatic session monitoring** - Hourly checks detect when re-authentication is needed + +## How It Works + + + + Your backend calls `kernel.agents.auth.create()` to create an Auth Agent for a specific domain and profile combination. + + + Call `kernel.agents.auth.invocations.create()` to start the auth flow. This returns a `hosted_url` where users complete login, or you can use the programmatic API for headless flows. + + + The user is redirected to the hosted UI where they enter credentials and handle 2FA if needed. The profile is saved on success. + + + Create browsers with the saved profile using `kernel.browsers.create({ profile: { name: 'my-profile' } })`. The browser is already logged in. + + + +## Key Concepts + +### Auth Agent + +An **Auth Agent** is a persistent entity that manages authentication for a specific domain + profile combination. It stores: + +- The target domain (e.g., `netflix.com`) +- The linked browser profile +- The login URL (learned during discovery) +- Current authentication status (`AUTHENTICATED` or `NEEDS_AUTH`) +- Optional linked credentials for automated re-auth + +Auth Agents are **idempotent**—if an agent already exists for the domain and profile, `kernel.agents.auth.create()` returns the existing one. + +### Invocation + +An **Invocation** is a single authentication attempt. You start one by calling `kernel.agents.auth.invocations.create()`. Over time, an Auth Agent accumulates multiple invocations as sessions need to be established or refreshed. + +**Invocation statuses:** +- `IN_PROGRESS` - User is completing the auth flow +- `SUCCESS` - Authentication completed, profile saved +- `EXPIRED` - Invocation timed out (5 minutes) +- `CANCELED` - Invocation was explicitly canceled + +### Profile + +A **Profile** stores browser session state (cookies, localStorage) that persists across browser sessions. Once authenticated via Agent Auth, use the profile with `kernel.browsers.create()` to get an already-logged-in browser. + +See [Profiles](/browsers/profiles) for more details on working with profiles. + +### Credentials + +**Credentials** are securely stored login credentials that enable automated re-authentication when sessions expire. Values are encrypted at rest and never exposed to LLMs or API responses. + +See [Credentials](/agent-auth/credentials) for details on storing and using credentials. + +## Integration Options + +Agent Auth offers two integration approaches: + +### Hosted UI (Recommended) + +The simplest approach—redirect users to our hosted authentication UI. Best for: +- User-facing applications +- When you want Kernel to handle the login UI +- Quick integration with minimal code + +See [Hosted UI Guide](/agent-auth/hosted-ui) for implementation details. + +### Programmatic API + +Build your own authentication UI using the lower-level discover and submit APIs. Best for: +- Custom UI requirements +- Automated/headless scenarios +- When you have credentials stored + +See [Programmatic Guide](/agent-auth/programmatic) for implementation details. + +## Security + +Agent Auth is designed with security as a first principle: + +| Security Feature | Description | +|-----------------|-------------| +| **Credentials never stored in plaintext** | All credential values are encrypted at rest using per-organization encryption keys | +| **Credentials never shared with LLMs** | Credentials are submitted programmatically to target sites, never passed through AI models | +| **Short-lived tokens** | JWT tokens for programmatic flows expire after 30 minutes | +| **One-time handoff codes** | Handoff codes can only be exchanged once and expire after 5 minutes | +| **Profile encryption** | Browser profiles are encrypted end-to-end | +| **Isolated execution** | Each auth flow runs in an isolated browser environment | + +## Quick Example + +Here's a complete example using the Hosted UI flow: + + +```typescript TypeScript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// 1. Create or get existing Auth Agent +const agent = await kernel.agents.auth.create({ + target_domain: 'netflix.com', + profile_name: 'netflix-user-123', +}); + +// 2. Start the auth flow +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); + +// 3. Check if already logged in +if (invocation.status === 'already_authenticated') { + console.log('Already authenticated!'); +} else { + // Redirect user to hosted UI + console.log('Redirect to:', invocation.hosted_url); +} + +// 4. Poll for completion +let status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +while (status.status === 'IN_PROGRESS') { + await new Promise(r => setTimeout(r, 2000)); + status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +if (status.status === 'SUCCESS') { + // 5. Use the authenticated profile + const browser = await kernel.browsers.create({ + profile: { name: 'netflix-user-123' }, + stealth: true, + }); + console.log('Browser ready:', browser.cdp_ws_url); +} +``` + +```python Python +from kernel import Kernel +import asyncio + +kernel = Kernel() + +# 1. Create or get existing Auth Agent +agent = await kernel.agents.auth.create( + target_domain="netflix.com", + profile_name="netflix-user-123", +) + +# 2. Start the auth flow +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, +) + +# 3. Check if already logged in +if invocation.status == "already_authenticated": + print("Already authenticated!") +else: + # Redirect user to hosted UI + print(f"Redirect to: {invocation.hosted_url}") + +# 4. Poll for completion +status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) +while status.status == "IN_PROGRESS": + await asyncio.sleep(2) + status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +if status.status == "SUCCESS": + # 5. Use the authenticated profile + browser = await kernel.browsers.create( + profile={"name": "netflix-user-123"}, + stealth=True, + ) + print(f"Browser ready: {browser.cdp_ws_url}") +``` + + +## Next Steps + + + + Get started with the simplest integration path + + + Build custom auth flows with full control + + + Store credentials for automated re-auth + + + Keep sessions alive automatically + + diff --git a/agent-auth/programmatic.mdx b/agent-auth/programmatic.mdx new file mode 100644 index 0000000..9421565 --- /dev/null +++ b/agent-auth/programmatic.mdx @@ -0,0 +1,519 @@ +--- +title: "Programmatic Flow" +description: "Build custom authentication UIs with full control using discover and submit APIs" +--- + +The Programmatic flow gives you complete control over the authentication process. Instead of redirecting users to the hosted UI, you build your own UI and use the discover/submit APIs to drive the authentication. + +## When to Use Programmatic Flow + +Use the Programmatic flow when: +- You need a custom authentication UI that matches your app's design +- You're building automated/headless authentication scenarios +- You have credentials stored and want to authenticate without user interaction +- You need fine-grained control over the auth flow + +## How It Works + + + + Same as Hosted UI - create an agent and start an invocation + + + Exchange the one-time handoff code for a scoped JWT token + + + Call `discover()` to get the form fields on the login page + + + Call `submit()` with the field values to attempt login + + + If additional fields are returned (2FA, OTP), collect and submit again + + + +## Complete Example + + +```typescript TypeScript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// Step 1: Create Auth Agent and Invocation +const agent = await kernel.agents.auth.create({ + target_domain: 'example.com', + profile_name: 'my-profile', +}); + +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); + +if (invocation.status === 'already_authenticated') { + console.log('Already authenticated!'); + // Profile is ready to use +} else { + // Step 2: Exchange handoff code for JWT + const exchange = await kernel.agents.auth.invocations.exchange( + invocation.invocation_id, + { code: invocation.handoff_code } + ); + + // Step 3: Create JWT-authenticated client + const jwtKernel = new Kernel({ apiKey: exchange.jwt }); + + // Step 4: Discover login fields + const discoverResponse = await jwtKernel.agents.auth.invocations.discover( + invocation.invocation_id, + {} + ); + + if (discoverResponse.logged_in) { + console.log('Already logged in during discovery!'); + } else if (discoverResponse.success && discoverResponse.fields) { + console.log('Discovered fields:', discoverResponse.fields); + + // Step 5: Map credentials to discovered fields + const fieldValues = mapCredentialsToFields(discoverResponse.fields, { + email: 'user@example.com', + password: 'secretpassword', + }); + + // Step 6: Submit credentials + let submitResponse = await jwtKernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } + ); + + // Step 7: Handle multi-step auth + while (submitResponse.needs_additional_auth && submitResponse.additional_fields) { + console.log('Additional auth required:', submitResponse.additional_fields); + + // Collect 2FA code from user + const otpCode = await promptUserForOTP(); + + const additionalValues = mapCredentialsToFields( + submitResponse.additional_fields, + { code: otpCode } + ); + + submitResponse = await jwtKernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: additionalValues } + ); + } + + if (submitResponse.logged_in) { + console.log('Authentication successful!'); + } else if (submitResponse.error_message) { + console.error('Login failed:', submitResponse.error_message); + } + } +} + +// Helper function to map credentials to discovered fields +function mapCredentialsToFields( + fields: Array<{ name: string; type: string; label?: string }>, + credentials: Record +): Record { + const fieldValues: Record = {}; + + for (const field of fields) { + const name = field.name.toLowerCase(); + const type = field.type.toLowerCase(); + const label = (field.label || '').toLowerCase(); + + if (type === 'email' || name.includes('email') || label.includes('email')) { + fieldValues[field.name] = credentials.email || ''; + } else if (type === 'password' || name.includes('password')) { + fieldValues[field.name] = credentials.password || ''; + } else if (type === 'code' || name.includes('code') || name.includes('otp')) { + fieldValues[field.name] = credentials.code || ''; + } else if (name.includes('username')) { + fieldValues[field.name] = credentials.email || credentials.username || ''; + } + } + + return fieldValues; +} +``` + +```python Python +from kernel import Kernel + +kernel = Kernel() + +# Step 1: Create Auth Agent and Invocation +agent = await kernel.agents.auth.create( + target_domain="example.com", + profile_name="my-profile", +) + +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, +) + +if invocation.status == "already_authenticated": + print("Already authenticated!") +else: + # Step 2: Exchange handoff code for JWT + exchange = await kernel.agents.auth.invocations.exchange( + invocation.invocation_id, + code=invocation.handoff_code, + ) + + # Step 3: Create JWT-authenticated client + jwt_kernel = Kernel(api_key=exchange.jwt) + + # Step 4: Discover login fields + discover_response = await jwt_kernel.agents.auth.invocations.discover( + invocation.invocation_id, + ) + + if discover_response.logged_in: + print("Already logged in during discovery!") + elif discover_response.success and discover_response.fields: + print(f"Discovered fields: {discover_response.fields}") + + # Step 5: Map credentials to discovered fields + field_values = map_credentials_to_fields( + discover_response.fields, + {"email": "user@example.com", "password": "secretpassword"}, + ) + + # Step 6: Submit credentials + submit_response = await jwt_kernel.agents.auth.invocations.submit( + invocation.invocation_id, + field_values=field_values, + ) + + # Step 7: Handle multi-step auth + while ( + submit_response.needs_additional_auth + and submit_response.additional_fields + ): + print(f"Additional auth required: {submit_response.additional_fields}") + otp_code = input("Enter 2FA code: ") + + additional_values = map_credentials_to_fields( + submit_response.additional_fields, + {"code": otp_code}, + ) + + submit_response = await jwt_kernel.agents.auth.invocations.submit( + invocation.invocation_id, + field_values=additional_values, + ) + + if submit_response.logged_in: + print("Authentication successful!") + elif submit_response.error_message: + print(f"Login failed: {submit_response.error_message}") + + +def map_credentials_to_fields(fields, credentials): + field_values = {} + for field in fields: + name = field.name.lower() + field_type = field.type.lower() + label = (field.label or "").lower() + + if field_type == "email" or "email" in name or "email" in label: + field_values[field.name] = credentials.get("email", "") + elif field_type == "password" or "password" in name: + field_values[field.name] = credentials.get("password", "") + elif field_type == "code" or "code" in name or "otp" in name: + field_values[field.name] = credentials.get("code", "") + elif "username" in name: + field_values[field.name] = credentials.get("email") or credentials.get("username", "") + + return field_values +``` + + +## Step-by-Step Breakdown + +### 1. Exchange Handoff Code for JWT + +The handoff code is a one-time credential that you exchange for a JWT token. This JWT is scoped to the specific invocation and grants permission to control the browser session. + +```typescript +const exchange = await kernel.agents.auth.invocations.exchange( + invocation.invocation_id, + { code: invocation.handoff_code } +); + +const jwt = exchange.jwt; // Valid for 30 minutes +``` + + +The handoff code can only be used once and expires after 5 minutes. The JWT is valid for 30 minutes. + + +### 2. Create JWT-Authenticated Client + +Create a new Kernel client using the JWT for subsequent API calls: + +```typescript +const jwtKernel = new Kernel({ apiKey: exchange.jwt }); +``` + +This client has limited permissions—it can only call the discover and submit endpoints for this specific invocation. + +### 3. Discover Login Fields + +Call `discover()` to navigate to the login page and extract form fields: + +```typescript +const discoverResponse = await jwtKernel.agents.auth.invocations.discover( + invocation.invocation_id, + { login_url: 'https://example.com/login' } // Optional: override login URL +); +``` + +**Discover response:** + +```json +{ + "success": true, + "logged_in": false, + "login_url": "https://identity.example.com/login", + "page_title": "Sign In - Example", + "fields": [ + { + "name": "email", + "type": "email", + "label": "Email Address", + "placeholder": "Enter your email", + "required": true, + "selector": "//input[@id='email']" + }, + { + "name": "password", + "type": "password", + "label": "Password", + "required": true, + "selector": "//input[@id='password']" + } + ] +} +``` + + +If `discover()` returns `logged_in: true`, the profile is already authenticated from a previous session. No credentials are needed—proceed to using the profile. + + +### 4. Map Credentials to Fields + +The discover response returns an array of fields with `name`, `type`, and `label` properties. Use the `name` as the key when submitting credentials: + +```typescript +// Discovered fields +const fields = [ + { name: 'email', type: 'email', label: 'Email Address' }, + { name: 'password', type: 'password', label: 'Password' } +]; + +// Your stored credentials +const credentials = { + email: 'user@example.com', + password: 'secretpassword' +}; + +// Map to field_values using field.name as key +const fieldValues = { + 'email': credentials.email, // field.name → credential value + 'password': credentials.password +}; +``` + +**Field type reference:** + +| Type | Description | Example Values | +|------|-------------|----------------| +| `text` | Generic text input | Username, name | +| `email` | Email address | user@example.com | +| `password` | Password (masked) | ••••••••• | +| `tel` | Phone number | +1-555-0123 | +| `number` | Numeric input | 12345 | +| `code` | Verification code (OTP/2FA) | 123456 | + +### 5. Submit Credentials + +Submit the mapped field values to attempt login: + +```typescript +const submitResponse = await jwtKernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } +); +``` + +**Possible submit responses:** + +**Success:** +```json +{ + "success": true, + "logged_in": true, + "app_name": "My App", + "target_domain": "example.com" +} +``` + +**Needs additional auth (2FA):** +```json +{ + "success": true, + "logged_in": false, + "needs_additional_auth": true, + "additional_fields": [ + { + "name": "code", + "type": "code", + "label": "Verification Code", + "required": true, + "selector": "//input[@name='code']" + } + ] +} +``` + +**Error:** +```json +{ + "success": false, + "logged_in": false, + "error_message": "Incorrect email or password" +} +``` + +### 6. Handle Multi-Step Auth + +When `needs_additional_auth` is `true`, the page is showing new fields (typically 2FA/OTP). Collect the additional values and submit again: + +```typescript +while (submitResponse.needs_additional_auth && submitResponse.additional_fields) { + // Show the additional fields to the user + console.log('Additional fields needed:', submitResponse.additional_fields); + + // Collect values (e.g., prompt user for 2FA code) + const additionalValues: Record = {}; + for (const field of submitResponse.additional_fields) { + if (field.type === 'code' || field.name.includes('code')) { + additionalValues[field.name] = await promptUserForOTP(); + } + } + + // Submit again + submitResponse = await jwtKernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: additionalValues } + ); +} +``` + +## Building a Custom Login UI + +Here's an example of rendering discovered fields in a React component: + +```tsx +function LoginForm({ fields, onSubmit }) { + const [values, setValues] = useState>({}); + + return ( +
{ + e.preventDefault(); + onSubmit(values); + }}> + {fields.map((field) => ( +
+ + setValues({ + ...values, + [field.name]: e.target.value + })} + /> +
+ ))} + +
+ ); +} +``` + +## Saving Credentials During Programmatic Flow + +You can save credentials during the invocation for future automated re-auth: + +```typescript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'my-login-creds', // Credentials will be saved +}); + +// ... complete the programmatic flow ... +// Credentials are automatically saved when login succeeds +``` + +See [Credentials](/agent-auth/credentials) for details on pre-storing credentials. + +## Error Handling + +```typescript +try { + const discoverResponse = await jwtKernel.agents.auth.invocations.discover( + invocation.invocation_id, + {} + ); + + if (!discoverResponse.success) { + console.error('Discovery failed:', discoverResponse.error_message); + return; + } + + const submitResponse = await jwtKernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } + ); + + if (!submitResponse.success) { + console.error('Login failed:', submitResponse.error_message); + // Show error to user, let them retry + } +} catch (error) { + if (error.status === 401) { + console.error('JWT expired - exchange a new handoff code'); + } else if (error.status === 404) { + console.error('Invocation not found or expired'); + } else { + console.error('Unexpected error:', error.message); + } +} +``` + +## Security Considerations + +- The JWT is scoped to the specific invocation and cannot access other resources +- Credentials submitted via `submit()` are sent directly to the target site +- Credentials are never logged, stored in plaintext, or passed to LLMs +- The browser session is isolated and destroyed after the invocation completes + +## Next Steps + + + + Pre-store credentials for fully automated auth + + + Keep sessions alive automatically + + diff --git a/agent-auth/session-monitoring.mdx b/agent-auth/session-monitoring.mdx new file mode 100644 index 0000000..18fec2d --- /dev/null +++ b/agent-auth/session-monitoring.mdx @@ -0,0 +1,402 @@ +--- +title: "Session Monitoring" +description: "Automatic session health checks and re-authentication" +--- + +Agent Auth automatically monitors authenticated sessions to detect when they expire. This enables proactive re-authentication before your workflows fail. + +## How Session Monitoring Works + +Auth Agents with `AUTHENTICATED` status are automatically checked hourly to verify the session is still valid. The check: + +1. Opens a browser with the authenticated profile +2. Navigates to the auth check URL (usually the main site) +3. Determines if the user is still logged in +4. Updates the Auth Agent status if the session has expired + +``` +┌─────────────────────┐ +│ Auth Agent │ +│ status: AUTH │ +└─────────┬───────────┘ + │ hourly check + ▼ +┌─────────────────────┐ +│ Check Session │ +│ - Load profile │ +│ - Navigate to site│ +│ - Detect login │ +└─────────┬───────────┘ + │ + ┌─────┴─────┐ + ▼ ▼ + Still Session + logged expired + in + │ │ + ▼ ▼ + No change Update status + → NEEDS_AUTH +``` + +## Detecting Expired Sessions + +Check the Auth Agent status to determine if re-authentication is needed: + + +```typescript TypeScript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +async function checkAndRefreshAuth(agentId: string) { + const agent = await kernel.agents.auth.retrieve(agentId); + + console.log('Status:', agent.status); + console.log('Last check:', agent.last_auth_check_at); + + if (agent.status === 'NEEDS_AUTH') { + console.log('Session expired - re-authentication needed'); + return await triggerReauth(agent); + } + + console.log('Session is valid'); + return agent; +} +``` + +```python Python +from kernel import Kernel + +kernel = Kernel() + +async def check_and_refresh_auth(agent_id: str): + agent = await kernel.agents.auth.retrieve(agent_id) + + print(f"Status: {agent.status}") + print(f"Last check: {agent.last_auth_check_at}") + + if agent.status == "NEEDS_AUTH": + print("Session expired - re-authentication needed") + return await trigger_reauth(agent) + + print("Session is valid") + return agent +``` + + +**Auth Agent status values:** + +| Status | Description | +|--------|-------------| +| `AUTHENTICATED` | Session is valid and ready to use | +| `NEEDS_AUTH` | Session has expired, re-authentication required | + +## Triggering Re-authentication + +When a session expires, you have two options: + +### Option 1: Automated Re-auth (Requires Credentials) + +If the Auth Agent has linked credentials and saved selectors, trigger automated re-auth: + +```typescript +async function triggerReauth(agent) { + if (!agent.can_reauth) { + console.log('Cannot auto-reauth - manual login required'); + // Fall back to hosted UI flow + return await manualReauth(agent.id); + } + + const reauth = await kernel.agents.auth.reauth(agent.id); + + switch (reauth.status) { + case 'reauth_started': + console.log('Re-auth started:', reauth.invocation_id); + // Poll for completion + return await pollForCompletion(reauth.invocation_id); + + case 'already_authenticated': + console.log('Already authenticated'); + return { success: true }; + + case 'cannot_reauth': + console.log('Cannot reauth:', reauth.message); + return await manualReauth(agent.id); + } +} +``` + +### Option 2: Manual Re-auth (Hosted UI) + +If credentials aren't stored, redirect the user to complete login: + +```typescript +async function manualReauth(agentId: string) { + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agentId, + save_credential_as: 'my-saved-creds', // Save for future auto-reauth + }); + + console.log('Redirect user to:', invocation.hosted_url); + // User completes login... + + return await pollForCompletion(invocation.invocation_id); +} +``` + +## Polling for Re-auth Completion + +After triggering re-auth, poll for completion: + +```typescript +async function pollForCompletion(invocationId: string) { + const maxWaitMs = 5 * 60 * 1000; // 5 minutes + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitMs) { + const status = await kernel.agents.auth.invocations.retrieve(invocationId); + + if (status.status === 'SUCCESS') { + console.log('Re-authentication successful!'); + return { success: true }; + } + + if (status.status === 'EXPIRED' || status.status === 'CANCELED') { + console.log('Re-auth failed:', status.status); + return { success: false, reason: status.status }; + } + + await new Promise(r => setTimeout(r, 2000)); + } + + return { success: false, reason: 'TIMEOUT' }; +} +``` + +## Proactive Session Management + +Instead of waiting for automations to fail, proactively check and refresh sessions: + +```typescript +async function ensureAuthenticated(agentId: string) { + const agent = await kernel.agents.auth.retrieve(agentId); + + // Already authenticated + if (agent.status === 'AUTHENTICATED') { + return agent; + } + + // Need to re-auth + if (agent.can_reauth) { + const reauth = await kernel.agents.auth.reauth(agentId); + if (reauth.status === 'reauth_started') { + await pollForCompletion(reauth.invocation_id); + } + } else { + throw new Error('Session expired and cannot auto-reauth'); + } + + // Return refreshed agent + return await kernel.agents.auth.retrieve(agentId); +} + +// Use before running automations +async function runAutomation(agentId: string, profileName: string) { + // Ensure authenticated before starting + await ensureAuthenticated(agentId); + + // Create browser with authenticated profile + const browser = await kernel.browsers.create({ + profile: { name: profileName }, + stealth: true, + }); + + // Run your automation... +} +``` + +## Monitoring Multiple Auth Agents + +For applications with many authenticated sessions, batch-check auth status: + +```typescript +async function checkAllSessions() { + const agents = await kernel.agents.auth.list(); + const needsReauth = []; + const canAutoReauth = []; + const needsManualReauth = []; + + for (const agent of agents.items) { + if (agent.status === 'NEEDS_AUTH') { + needsReauth.push(agent); + if (agent.can_reauth) { + canAutoReauth.push(agent); + } else { + needsManualReauth.push(agent); + } + } + } + + console.log(`Total agents: ${agents.items.length}`); + console.log(`Needs re-auth: ${needsReauth.length}`); + console.log(`Can auto-reauth: ${canAutoReauth.length}`); + console.log(`Needs manual login: ${needsManualReauth.length}`); + + // Auto-reauth those that can be automated + for (const agent of canAutoReauth) { + await kernel.agents.auth.reauth(agent.id); + } + + return { needsManualReauth }; +} +``` + +## Complete Example: Robust Session Management + +Here's a complete pattern for robust session handling: + +```typescript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +class AuthSessionManager { + async getAuthenticatedBrowser( + agentId: string, + profileName: string, + options?: { forceRefresh?: boolean } + ) { + // Check current status + let agent = await kernel.agents.auth.retrieve(agentId); + + // Force refresh if requested + if (options?.forceRefresh && agent.can_reauth) { + await this.triggerReauth(agent); + agent = await kernel.agents.auth.retrieve(agentId); + } + + // Handle expired sessions + if (agent.status === 'NEEDS_AUTH') { + if (agent.can_reauth) { + await this.triggerReauth(agent); + agent = await kernel.agents.auth.retrieve(agentId); + } else { + throw new AuthRequiredError( + 'Session expired - manual login required', + agentId + ); + } + } + + // Verify authentication succeeded + if (agent.status !== 'AUTHENTICATED') { + throw new AuthFailedError('Authentication failed', agentId); + } + + // Create and return authenticated browser + return await kernel.browsers.create({ + profile: { name: profileName }, + stealth: true, + }); + } + + private async triggerReauth(agent: AuthAgent) { + const reauth = await kernel.agents.auth.reauth(agent.id); + + if (reauth.status === 'reauth_started' && reauth.invocation_id) { + await this.pollForCompletion(reauth.invocation_id); + } else if (reauth.status === 'cannot_reauth') { + throw new CannotReauthError(reauth.message, agent.id); + } + } + + private async pollForCompletion(invocationId: string, timeoutMs = 300000) { + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + const status = await kernel.agents.auth.invocations.retrieve(invocationId); + + if (status.status === 'SUCCESS') return; + if (status.status === 'EXPIRED') throw new Error('Re-auth expired'); + if (status.status === 'CANCELED') throw new Error('Re-auth canceled'); + + await new Promise(r => setTimeout(r, 2000)); + } + + throw new Error('Re-auth timeout'); + } +} + +// Custom error types +class AuthRequiredError extends Error { + constructor(message: string, public agentId: string) { + super(message); + this.name = 'AuthRequiredError'; + } +} + +class AuthFailedError extends Error { + constructor(message: string, public agentId: string) { + super(message); + this.name = 'AuthFailedError'; + } +} + +class CannotReauthError extends Error { + constructor(message: string, public agentId: string) { + super(message); + this.name = 'CannotReauthError'; + } +} + +// Usage +const sessionManager = new AuthSessionManager(); + +try { + const browser = await sessionManager.getAuthenticatedBrowser( + 'agent_abc123', + 'my-profile' + ); + // Use the browser... +} catch (error) { + if (error instanceof AuthRequiredError) { + // Redirect user to complete login + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: error.agentId, + }); + console.log('Login required:', invocation.hosted_url); + } else { + throw error; + } +} +``` + +## Best Practices + +1. **Check before automation** - Always verify auth status before starting long-running automations + +2. **Enable auto-reauth** - Store credentials to enable automated re-authentication + +3. **Handle failures gracefully** - Have a fallback to manual login when auto-reauth fails + +4. **Monitor auth status** - Periodically check auth agents to proactively handle expirations + +5. **Log auth events** - Track when re-auth happens for debugging and auditing + +## Session Check Frequency + +- Auth checks run approximately **every hour** for authenticated agents +- Checks are passive and don't modify the saved profile +- The `last_auth_check_at` field shows when the last check occurred + +## Next Steps + + + + Store credentials to enable auto-reauth + + + Review Agent Auth concepts + + diff --git a/docs.json b/docs.json index dedd1b8..3406726 100644 --- a/docs.json +++ b/docs.json @@ -62,6 +62,17 @@ "quickstart" ] }, + { + "group": "Agent Auth", + "pages": [ + "agent-auth/overview", + "agent-auth/hosted-ui", + "agent-auth/programmatic", + "agent-auth/credentials", + "agent-auth/session-monitoring", + "agent-auth/early-preview" + ] + }, { "group": "Browsers", "pages": [ @@ -182,7 +193,8 @@ "reference/cli/create", "reference/cli/auth", "reference/cli/browsers", - "reference/cli/apps" + "reference/cli/apps", + "reference/cli/mcp" ] }, { From 67ad13252ee9935640665ba1981424e1e9448b17 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Thu, 18 Dec 2025 18:27:28 -0500 Subject: [PATCH 02/12] Update status values in Agent Auth documentation to use uppercase constants for consistency and clarity --- agent-auth/early-preview.mdx | 46 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/agent-auth/early-preview.mdx b/agent-auth/early-preview.mdx index dfca036..3308f0c 100644 --- a/agent-auth/early-preview.mdx +++ b/agent-auth/early-preview.mdx @@ -114,8 +114,8 @@ Different endpoints return different status values. Here's the complete referenc | Status | Description | |--------|-------------| -| `invocation_created` | New invocation started, redirect user to `hosted_url` | -| `already_authenticated` | Profile is already logged in, no invocation needed | +| `INVOCATION_CREATED` | New invocation started, redirect user to `hosted_url` | +| `ALREADY_AUTHENTICATED` | Profile is already logged in, no invocation needed | **Auth Agent status** (from `agents.auth.retrieve()`): @@ -128,9 +128,9 @@ Different endpoints return different status values. Here's the complete referenc | Status | Description | |--------|-------------| -| `reauth_started` | Automated re-auth started, poll `invocation_id` for completion | -| `already_authenticated` | Session is still valid, no action needed | -| `cannot_reauth` | Missing credentials or selectors, manual login required | +| `REAUTH_STARTED` | Automated re-auth started, poll `invocation_id` for completion | +| `ALREADY_AUTHENTICATED` | Session is still valid, no action needed | +| `CANNOT_REAUTH` | Missing credentials or selectors, manual login required | ### Automatic Session Monitoring @@ -243,8 +243,8 @@ The simplest approach—redirect users to our hosted authentication UI, then pol const agent = await kernel.agents.auth.create({ target_domain: 'example.com', profile_name: 'user-123' }); const invocation = await kernel.agents.auth.invocations.create({ auth_agent_id: agent.id }); -// 2. Redirect user (or skip if already_authenticated) -if (invocation.status !== 'already_authenticated') { +// 2. Redirect user (or skip if ALREADY_AUTHENTICATED) +if (invocation.status !== 'ALREADY_AUTHENTICATED') { redirect(invocation.hosted_url); } @@ -289,7 +289,7 @@ const invocation = await kernel.agents.auth.invocations.create({ ```typescript // Check if already logged in -if (invocation.status === 'already_authenticated') { +if (invocation.status === 'ALREADY_AUTHENTICATED') { console.log('Already logged in! Profile is ready to use.'); // Skip to Step 4 - Use the Profile } else { @@ -377,13 +377,13 @@ if (agent.status === 'NEEDS_AUTH') { // Automated re-auth (no user interaction needed) const reauth = await kernel.agents.auth.reauth(agent.id); - if (reauth.status === 'reauth_started') { + if (reauth.status === 'REAUTH_STARTED') { // Poll for completion using the same backoff pattern const result = await pollForCompletion(reauth.invocation_id); console.log('Re-auth complete:', result); - } else if (reauth.status === 'already_authenticated') { + } else if (reauth.status === 'ALREADY_AUTHENTICATED') { console.log('Session is still valid'); - } else if (reauth.status === 'cannot_reauth') { + } else if (reauth.status === 'CANNOT_REAUTH') { // Selectors may have drifted - fall back to manual login console.log('Cannot reauth:', reauth.message); } @@ -647,7 +647,7 @@ const invocation = await kernel.agents.auth.invocations.create({ save_credential_as: 'bot-creds', // Enable reauth }); -if (invocation.status === 'already_authenticated') { +if (invocation.status === 'ALREADY_AUTHENTICATED') { console.log('Already logged in!'); } else { // 2. Discover fields (API key auth works, no JWT needed) @@ -721,7 +721,7 @@ async function setupAuth(userId: string, targetDomain: string) { save_credential_as: `${profileName}-creds`, // Enable future reauth! }); - if (invocation.status === 'already_authenticated') { + if (invocation.status === 'ALREADY_AUTHENTICATED') { return { agentId: agent.id, profileName, ready: true }; } @@ -744,7 +744,7 @@ async function runAutomation(agentId: string, profileName: string) { if (agent.can_reauth) { // Automated re-auth const reauth = await kernel.agents.auth.reauth(agentId); - if (reauth.status === 'reauth_started') { + if (reauth.status === 'REAUTH_STARTED') { await pollForCompletion(reauth.invocation_id); } } else { @@ -857,14 +857,14 @@ if (agent.status === 'NEEDS_AUTH' && agent.can_reauth) { const reauth = await kernel.agents.auth.reauth(agent.id); switch (reauth.status) { - case 'reauth_started': + case 'REAUTH_STARTED': console.log('Re-auth in progress:', reauth.invocation_id); // Poll for completion... break; - case 'already_authenticated': + case 'ALREADY_AUTHENTICATED': console.log('Session is still valid'); break; - case 'cannot_reauth': + case 'CANNOT_REAUTH': console.log('Cannot reauth:', reauth.message); // Fall back to manual login break; @@ -1020,7 +1020,7 @@ Trigger automated re-authentication. ```json { - "status": "reauth_started", + "status": "REAUTH_STARTED", "invocation_id": "inv_xyz789" } ``` @@ -1029,7 +1029,7 @@ Trigger automated re-authentication. ```json { - "status": "already_authenticated" + "status": "ALREADY_AUTHENTICATED" } ``` @@ -1037,7 +1037,7 @@ Trigger automated re-authentication. ```json { - "status": "cannot_reauth", + "status": "CANNOT_REAUTH", "message": "Missing stored credential or form selectors" } ``` @@ -1070,7 +1070,7 @@ Create an invocation to start an auth flow for an existing auth agent. ```json { - "status": "invocation_created", + "status": "INVOCATION_CREATED", "invocation_id": "inv_xyz789", "handoff_code": "aBcD123EfGh456IjKl789", "hosted_url": "https://agent-auth.onkernel.com/agents/auth/invocations/inv_xyz789?code=aBcD123", @@ -1082,12 +1082,12 @@ Create an invocation to start an auth flow for an existing auth agent. ```json { - "status": "already_authenticated" + "status": "ALREADY_AUTHENTICATED" } ``` -When `status` is `already_authenticated`, the agent is already logged in and no invocation was created. You can proceed directly to using the profile. +When `status` is `ALREADY_AUTHENTICATED`, the agent is already logged in and no invocation was created. You can proceed directly to using the profile. #### GET /agents/auth/invocations/{invocation_id} From 023ab1a167d196c390dbaace9cdcff4e8af6b523 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Thu, 18 Dec 2025 18:30:33 -0500 Subject: [PATCH 03/12] docs: Remove "NEW" badges from feature list --- agent-auth/early-preview.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/agent-auth/early-preview.mdx b/agent-auth/early-preview.mdx index 3308f0c..42339c9 100644 --- a/agent-auth/early-preview.mdx +++ b/agent-auth/early-preview.mdx @@ -19,8 +19,8 @@ Agent Auth is an AI-powered authentication system that helps you log users into - Saves authenticated browser profiles for reuse - Hourly session checks to detect when re-authentication is needed - Credentials are encrypted and submitted programmatically—never stored in plaintext or shared with LLMs -- **NEW:** Store credentials for fully automated re-authentication -- **NEW:** `reauth()` endpoint for programmatic session refresh +- Store credentials for fully automated re-authentication +- `reauth()` endpoint for programmatic session refresh ## Choose Your Integration @@ -36,7 +36,7 @@ Agent Auth is an AI-powered authentication system that helps you log users into 2. User is redirected to `hosted_url` → Enters credentials when prompted → Handles 2FA/OTP if needed → Profile saved on success 3. Your backend polls invocation status → Uses saved profile in future browser automations 4. After authentication completes, Agent Auth automatically monitors the session every hour and updates the status if re-authentication is required -5. **NEW:** If credentials are stored, Agent Auth can automatically re-authenticate when sessions expire +5. If credentials are stored, Agent Auth can automatically re-authenticate when sessions expire **Plan for re-auth from the start.** If you want automated re-authentication later, use `save_credential_as` during the initial login. This saves credentials and form selectors, enabling `can_reauth` for future sessions. @@ -63,8 +63,8 @@ The Auth Agent stores: - The login URL (learned during discovery, used to speed up future invocations) - The auth check URL (where to verify logged-in state) - Current authentication status (`AUTHENTICATED` or `NEEDS_AUTH`) -- **NEW:** Linked credential ID for automated re-auth -- **NEW:** Saved form selectors for deterministic re-auth +- Linked credential ID for automated re-auth +- Saved form selectors for deterministic re-auth **Auth Agent fields:** @@ -279,7 +279,7 @@ const agent = await kernel.agents.auth.create({ // Step 2: Start the Auth Flow const invocation = await kernel.agents.auth.invocations.create({ auth_agent_id: agent.id, - save_credential_as: 'netflix-creds', // NEW: Save credentials for future re-auth + save_credential_as: 'netflix-creds', // Save credentials for future re-auth }); // invocation returns: status, invocation_id, hosted_url, handoff_code, expires_at From dd8482c679736ba7dfb3d73b82dd28d6d07249c7 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Thu, 18 Dec 2025 18:35:03 -0500 Subject: [PATCH 04/12] Update Agent Auth documentation to use uppercase status constants for consistency across all sections --- agent-auth/credentials.mdx | 8 ++++---- agent-auth/hosted-ui.mdx | 4 ++-- agent-auth/overview.mdx | 4 ++-- agent-auth/programmatic.mdx | 4 ++-- agent-auth/session-monitoring.mdx | 12 ++++++------ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/agent-auth/credentials.mdx b/agent-auth/credentials.mdx index 7673b58..3f33a44 100644 --- a/agent-auth/credentials.mdx +++ b/agent-auth/credentials.mdx @@ -144,14 +144,14 @@ When sessions expire, trigger re-authentication: const reauth = await kernel.agents.auth.reauth(agent.id); switch (reauth.status) { - case 'reauth_started': + case 'REAUTH_STARTED': console.log('Re-auth started, invocation:', reauth.invocation_id); // Poll for completion break; - case 'already_authenticated': + case 'ALREADY_AUTHENTICATED': console.log('Session is still valid'); break; - case 'cannot_reauth': + case 'CANNOT_REAUTH': console.log('Cannot re-auth:', reauth.message); // Missing credentials or selectors - need manual login break; @@ -289,7 +289,7 @@ async function useAuthenticatedBrowser(agentId: string) { if (agent.can_reauth) { // Automated re-auth const reauth = await kernel.agents.auth.reauth(agentId); - if (reauth.status === 'reauth_started') { + if (reauth.status === 'REAUTH_STARTED') { // Wait for re-auth to complete await pollForCompletion(reauth.invocation_id); } diff --git a/agent-auth/hosted-ui.mdx b/agent-auth/hosted-ui.mdx index 4e0a670..de66c66 100644 --- a/agent-auth/hosted-ui.mdx +++ b/agent-auth/hosted-ui.mdx @@ -34,7 +34,7 @@ const invocation = await kernel.agents.auth.invocations.create({ }); // Step 3: Check if already logged in -if (invocation.status === 'already_authenticated') { +if (invocation.status === 'ALREADY_AUTHENTICATED') { console.log('Already authenticated! Profile is ready to use.'); // Skip to Step 6: Use the Profile } else { @@ -105,7 +105,7 @@ invocation = await kernel.agents.auth.invocations.create( ) # Step 3: Check if already logged in -if invocation.status == "already_authenticated": +if invocation.status == "ALREADY_AUTHENTICATED": print("Already authenticated! Profile is ready to use.") else: print(f"Redirect user to: {invocation.hosted_url}") diff --git a/agent-auth/overview.mdx b/agent-auth/overview.mdx index e82e09a..cf66ef5 100644 --- a/agent-auth/overview.mdx +++ b/agent-auth/overview.mdx @@ -128,7 +128,7 @@ const invocation = await kernel.agents.auth.invocations.create({ }); // 3. Check if already logged in -if (invocation.status === 'already_authenticated') { +if (invocation.status === 'ALREADY_AUTHENTICATED') { console.log('Already authenticated!'); } else { // Redirect user to hosted UI @@ -170,7 +170,7 @@ invocation = await kernel.agents.auth.invocations.create( ) # 3. Check if already logged in -if invocation.status == "already_authenticated": +if invocation.status == "ALREADY_AUTHENTICATED": print("Already authenticated!") else: # Redirect user to hosted UI diff --git a/agent-auth/programmatic.mdx b/agent-auth/programmatic.mdx index 9421565..e44c3bd 100644 --- a/agent-auth/programmatic.mdx +++ b/agent-auth/programmatic.mdx @@ -51,7 +51,7 @@ const invocation = await kernel.agents.auth.invocations.create({ auth_agent_id: agent.id, }); -if (invocation.status === 'already_authenticated') { +if (invocation.status === 'ALREADY_AUTHENTICATED') { console.log('Already authenticated!'); // Profile is ready to use } else { @@ -155,7 +155,7 @@ invocation = await kernel.agents.auth.invocations.create( auth_agent_id=agent.id, ) -if invocation.status == "already_authenticated": +if invocation.status == "ALREADY_AUTHENTICATED": print("Already authenticated!") else: # Step 2: Exchange handoff code for JWT diff --git a/agent-auth/session-monitoring.mdx b/agent-auth/session-monitoring.mdx index 18fec2d..009e93b 100644 --- a/agent-auth/session-monitoring.mdx +++ b/agent-auth/session-monitoring.mdx @@ -111,16 +111,16 @@ async function triggerReauth(agent) { const reauth = await kernel.agents.auth.reauth(agent.id); switch (reauth.status) { - case 'reauth_started': + case 'REAUTH_STARTED': console.log('Re-auth started:', reauth.invocation_id); // Poll for completion return await pollForCompletion(reauth.invocation_id); - case 'already_authenticated': + case 'ALREADY_AUTHENTICATED': console.log('Already authenticated'); return { success: true }; - case 'cannot_reauth': + case 'CANNOT_REAUTH': console.log('Cannot reauth:', reauth.message); return await manualReauth(agent.id); } @@ -190,7 +190,7 @@ async function ensureAuthenticated(agentId: string) { // Need to re-auth if (agent.can_reauth) { const reauth = await kernel.agents.auth.reauth(agentId); - if (reauth.status === 'reauth_started') { + if (reauth.status === 'REAUTH_STARTED') { await pollForCompletion(reauth.invocation_id); } } else { @@ -304,9 +304,9 @@ class AuthSessionManager { private async triggerReauth(agent: AuthAgent) { const reauth = await kernel.agents.auth.reauth(agent.id); - if (reauth.status === 'reauth_started' && reauth.invocation_id) { + if (reauth.status === 'REAUTH_STARTED' && reauth.invocation_id) { await this.pollForCompletion(reauth.invocation_id); - } else if (reauth.status === 'cannot_reauth') { + } else if (reauth.status === 'CANNOT_REAUTH') { throw new CannotReauthError(reauth.message, agent.id); } } From e433d7203f2e3ba240f594743cec37af6fa54939 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Sat, 20 Dec 2025 17:37:24 -0500 Subject: [PATCH 05/12] docs(agent-auth): add auto-login and update flows --- agent-auth/auto-login.mdx | 387 ++++++++++ agent-auth/credentials.mdx | 60 +- agent-auth/early-preview.mdx | 1387 +--------------------------------- agent-auth/hosted-ui.mdx | 42 +- agent-auth/overview.mdx | 83 +- agent-auth/programmatic.mdx | 144 ++-- docs.json | 22 +- 7 files changed, 619 insertions(+), 1506 deletions(-) create mode 100644 agent-auth/auto-login.mdx diff --git a/agent-auth/auto-login.mdx b/agent-auth/auto-login.mdx new file mode 100644 index 0000000..8dcf5be --- /dev/null +++ b/agent-auth/auto-login.mdx @@ -0,0 +1,387 @@ +--- +title: "Auto-Login" +description: "Fully automated authentication with pre-linked credentials" +--- + +Auto-Login is the simplest approach for automated flows—pre-create credentials, link them to the auth agent, and the system handles everything automatically. Just poll for completion. + +## When to Use Auto-Login + +Use Auto-Login when: +- You already have the user's credentials (from your secrets manager, user input collected earlier, etc.) +- You want fully automated login with minimal code +- You're building headless/bot workflows that don't require user interaction + + +If you need users to enter their own credentials, use the [Hosted UI](/agent-auth/hosted-ui) or [Programmatic](/agent-auth/programmatic) flows instead. + + +## How It Works + + + + ```typescript + const credential = await kernel.credentials.create({ + name: 'user-123-netflix', + domain: 'netflix.com', + values: { username: 'user@example.com', password: 'secretpassword' }, + }); + ``` + + + ```typescript + const agent = await kernel.agents.auth.create({ + target_domain: 'netflix.com', + profile_name: 'netflix-user-123', + credential_name: credential.name, + }); + ``` + + + ```typescript + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); + // System automatically discovers fields, maps credentials, and submits + ``` + + + ```typescript + const status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + // Poll until status.status === 'SUCCESS' + ``` + + + ```typescript + const browser = await kernel.browsers.create({ + profile: { name: 'netflix-user-123' }, + stealth: true, + }); + ``` + + + +## Complete Example + + +```typescript TypeScript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// Step 1: Create credential with login values +const credential = await kernel.credentials.create({ + name: 'user-123-netflix', + domain: 'netflix.com', + values: { + username: 'user@example.com', + password: 'secretpassword', + }, +}); + +// Step 2: Create auth agent with credential linked +const agent = await kernel.agents.auth.create({ + target_domain: 'netflix.com', + profile_name: 'netflix-user-123', + credential_name: credential.name, + login_url: 'https://netflix.com/login', // Optional: speeds up discovery +}); + +// Step 3: Start invocation - auto-login kicks in automatically +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); + +// Check if already authenticated +if (invocation.status === 'ALREADY_AUTHENTICATED') { + console.log('Already logged in! Profile ready.'); +} else { + // Step 4: Poll for completion + console.log(`Invocation type: ${invocation.type}`); // "auto_login" + + const result = await pollForCompletion(invocation.invocation_id); + + if (result.success) { + console.log('Auto-login successful!'); + } +} + +// Step 5: Use the profile +const browser = await kernel.browsers.create({ + profile: { name: 'netflix-user-123' }, + stealth: true, +}); + +// Run your automation... +await kernel.browsers.deleteByID(browser.session_id); +``` + +```python Python +from kernel import Kernel +import asyncio + +kernel = Kernel() + +# Step 1: Create credential with login values +credential = await kernel.credentials.create( + name="user-123-netflix", + domain="netflix.com", + values={ + "username": "user@example.com", + "password": "secretpassword", + }, +) + +# Step 2: Create auth agent with credential linked +agent = await kernel.agents.auth.create( + target_domain="netflix.com", + profile_name="netflix-user-123", + credential_name=credential.name, + login_url="https://netflix.com/login", # Optional: speeds up discovery +) + +# Step 3: Start invocation - auto-login kicks in automatically +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, +) + +# Check if already authenticated +if invocation.status == "ALREADY_AUTHENTICATED": + print("Already logged in! Profile ready.") +else: + # Step 4: Poll for completion + print(f"Invocation type: {invocation.type}") # "auto_login" + + result = await poll_for_completion(invocation.invocation_id) + + if result["success"]: + print("Auto-login successful!") + +# Step 5: Use the profile +browser = await kernel.browsers.create( + profile={"name": "netflix-user-123"}, + stealth=True, +) + +# Run your automation... +await kernel.browsers.delete_by_id(browser.session_id) +``` + + +## Polling for Completion + +Auto-login runs asynchronously. Poll the invocation status to know when it completes: + + +```typescript TypeScript +async function pollForCompletion(invocationId: string) { + const maxWaitMs = 5 * 60 * 1000; // 5 minutes + const start = Date.now(); + let delay = 3000; + + while (Date.now() - start < maxWaitMs) { + const status = await kernel.agents.auth.invocations.retrieve(invocationId); + + console.log(`Status: ${status.status}, Step: ${status.step}`); + + if (status.status === 'SUCCESS') { + return { success: true, status }; + } + if (status.status === 'EXPIRED' || status.status === 'CANCELED') { + return { success: false, reason: status.status }; + } + + // Check if manual input needed (e.g., MFA code not in credentials) + if (status.step === 'awaiting_input') { + return { success: false, reason: 'MANUAL_INPUT_REQUIRED', status }; + } + + await new Promise(r => setTimeout(r, delay)); + delay = Math.min(delay * 1.5, 5000); + } + + return { success: false, reason: 'TIMEOUT' }; +} +``` + +```python Python +async def poll_for_completion(invocation_id: str): + max_wait_seconds = 5 * 60 + start_time = asyncio.get_event_loop().time() + delay = 3 + + while asyncio.get_event_loop().time() - start_time < max_wait_seconds: + status = await kernel.agents.auth.invocations.retrieve(invocation_id) + + print(f"Status: {status.status}, Step: {status.step}") + + if status.status == "SUCCESS": + return {"success": True, "status": status} + if status.status in ("EXPIRED", "CANCELED"): + return {"success": False, "reason": status.status} + + # Check if manual input needed (e.g., MFA code not in credentials) + if status.step == "awaiting_input": + return {"success": False, "reason": "MANUAL_INPUT_REQUIRED", "status": status} + + await asyncio.sleep(delay) + delay = min(delay * 1.5, 5) + + return {"success": False, "reason": "TIMEOUT"} +``` + + +**Invocation step values** (useful for monitoring progress): + +| Step | Description | +|------|-------------| +| `initialized` | Invocation just started | +| `discovering` | Currently discovering login fields | +| `awaiting_input` | Waiting for input (manual intervention needed) | +| `submitting` | Currently submitting credentials | +| `completed` | Login flow finished | + +## Credential Field Mapping + +Auto-login maps your credential values to discovered form fields: + +1. **Direct name match** - If your credential has `username` and the form has a field named `username`, they match +2. **Type-based fallback** - If no name match, maps by field type: + - `email` type → uses `email` or `username` from credentials + - `password` type → uses `password` from credentials + - `tel` type → uses `phone` from credentials + - `text` type → uses `username` or `email` as fallback + +**Recommended credential structure:** + +```typescript +{ + values: { + username: 'user@example.com', // Works for email and text fields + password: 'secretpassword', + // Optional for sites that need them: + phone: '+1234567890', + code: '123456', // For TOTP if you have a generator + } +} +``` + +## Multi-Step Form Handling + +Auto-login automatically handles multi-step login forms: + +- **Step 1**: Discovers username/email field → maps and submits +- **Step 2**: New password field appears → maps and submits +- **Step 3**: 2FA code field appears → maps if `code` is in credentials, otherwise pauses + +If the system encounters fields it can't map from your credentials (like a 2FA code field without a `code` value), it sets `step: 'awaiting_input'` and pauses. + +### Handling Paused Flows + +When auto-login pauses, you can continue manually: + + +```typescript TypeScript +const result = await pollForCompletion(invocation.invocation_id); + +if (result.reason === 'MANUAL_INPUT_REQUIRED') { + // Get the 2FA code from user or TOTP generator + const otpCode = await getOTPCode(); + + // Call discover to see current fields + const discover = await kernel.agents.auth.invocations.discover( + invocation.invocation_id, + {} + ); + + // Submit the missing value + const submit = await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: { code: otpCode } } + ); + + if (submit.logged_in) { + console.log('Login completed!'); + } +} +``` + +```python Python +result = await poll_for_completion(invocation.invocation_id) + +if result["reason"] == "MANUAL_INPUT_REQUIRED": + # Get the 2FA code from user or TOTP generator + otp_code = await get_otp_code() + + # Call discover to see current fields + discover = await kernel.agents.auth.invocations.discover( + invocation.invocation_id, + ) + + # Submit the missing value + submit = await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + field_values={"code": otp_code}, + ) + + if submit.logged_in: + print("Login completed!") +``` + + + +**2FA handling:** If the site requires 2FA, include a `code` value in your credentials using a TOTP generator library. If you can't provide the code upfront, auto-login will pause at `step: 'awaiting_input'` and you can continue manually. + + +## Using Proxies + +If the target site requires a specific IP or region, configure a proxy: + + +```typescript TypeScript +const agent = await kernel.agents.auth.create({ + target_domain: 'region-locked-site.com', + profile_name: 'my-profile', + credential_name: 'my-credential', + proxy: { proxy_id: 'proxy_abc123' }, +}); + +// Use the same proxy when creating browsers +const browser = await kernel.browsers.create({ + profile: { name: 'my-profile' }, + proxy_id: 'proxy_abc123', + stealth: true, +}); +``` + +```python Python +agent = await kernel.agents.auth.create( + target_domain="region-locked-site.com", + profile_name="my-profile", + credential_name="my-credential", + proxy={"proxy_id": "proxy_abc123"}, +) + +# Use the same proxy when creating browsers +browser = await kernel.browsers.create( + profile={"name": "my-profile"}, + proxy_id="proxy_abc123", + stealth=True, +) +``` + + + +Use the same proxy configuration for both the Auth Agent and subsequent browser sessions. Different IPs may trigger security measures on the target site. + + +## Next Steps + + + + Store and manage login credentials + + + Automatic session health checks and re-auth + + diff --git a/agent-auth/credentials.mdx b/agent-auth/credentials.mdx index 3f33a44..fb10ed0 100644 --- a/agent-auth/credentials.mdx +++ b/agent-auth/credentials.mdx @@ -18,16 +18,31 @@ Without stored credentials, every time a session expires, you need to redirect u - Create a credential with the login values for a specific domain + ```typescript + const credential = await kernel.credentials.create({ + name: 'my-login', + domain: 'example.com', + values: { email: 'user@example.com', password: 'secret' }, + }); + ``` - Associate the credential with an Auth Agent + ```typescript + const agent = await kernel.agents.auth.create({ + target_domain: 'example.com', + profile_name: 'my-profile', + credential_name: 'my-login', + }); + ``` - Run one successful authentication to save form selectors + Run one successful authentication to save form selectors (via [Auto-Login](/agent-auth/auto-login) or [Hosted UI](/agent-auth/hosted-ui)). - When sessions expire, Agent Auth uses stored credentials and selectors to re-authenticate automatically + ```typescript + // When session expires, trigger re-auth + const reauth = await kernel.agents.auth.reauth(agent.id); + ``` @@ -158,7 +173,7 @@ switch (reauth.status) { } ``` -See [Session Monitoring](/agent-auth/session-monitoring) for automated session management. +Poll for completion, then use [Session Monitoring](/agent-auth/session-monitoring) to detect future expirations. ## Managing Credentials @@ -230,6 +245,22 @@ values: { } ``` +### With 2FA/TOTP Code + +For sites requiring 2FA, include the `code` field with a TOTP generator: + +```typescript +values: { + email: 'user@example.com', + password: 'secretpassword', + code: '123456', // Generate with a TOTP library +} +``` + + +When using [Auto-Login](/agent-auth/auto-login) with 2FA, the system will automatically use the `code` value when a 2FA field is detected. Use a TOTP generator library to provide the current code. + + ### Multiple Fields Some sites have additional fields (company ID, account number, etc.): @@ -242,6 +273,17 @@ values: { } ``` +### Phone Number + +For sites that require phone-based login: + +```typescript +values: { + phone: '+1234567890', + password: 'secretpassword', +} +``` + ## Complete Example: Automated Auth Flow Here's a complete example setting up fully automated authentication: @@ -333,10 +375,10 @@ Credentials are designed with security as the top priority: ## Next Steps - - Automatically detect and handle expired sessions + + Fully automated login with pre-linked credentials - - Use the hosted UI for initial login + + Automatic session health checks and re-auth diff --git a/agent-auth/early-preview.mdx b/agent-auth/early-preview.mdx index 42339c9..502326b 100644 --- a/agent-auth/early-preview.mdx +++ b/agent-auth/early-preview.mdx @@ -1,200 +1,36 @@ --- title: "Early Preview" -description: "Agent Auth early preview documentation for testers" ---- - - -This is early preview documentation for Agent Auth testers. Features and APIs may change before general availability. - - -## What is Agent Auth? - -Agent Auth is an AI-powered authentication system that helps you log users into any website and save their authenticated browser session (profile) for future automations. It uses web agents to intelligently discover login forms, dynamically generate credential inputs, and handle multi-step authentication flows like 2FA/OTP. - -**Key features:** - -- Automatically detects login form fields on any website -- Dynamically generates forms based on discovered fields -- Handles multi-step auth (2FA, OTP, magic links) -- Saves authenticated browser profiles for reuse -- Hourly session checks to detect when re-authentication is needed -- Credentials are encrypted and submitted programmatically—never stored in plaintext or shared with LLMs -- Store credentials for fully automated re-authentication -- `reauth()` endpoint for programmatic session refresh - -## Choose Your Integration - -| Flow | Who enters credentials? | Best for | -|------|------------------------|----------| -| **Hosted UI** | User, in Kernel's hosted page | Simplest integration, minimal code | -| **Custom UI** | User, in YOUR app's UI | Full control over login UX | -| **Programmatic** | No one (you provide them in code) | Automated/headless, bot accounts | - -## How It Works - -1. Your backend calls `kernel.agents.auth.create()` to setup the agent, then calls `kernel.agents.auth.invocations.create()` to start the flow → Returns `invocation_id`, `hosted_url`, `handoff_code`, and `expires_at` -2. User is redirected to `hosted_url` → Enters credentials when prompted → Handles 2FA/OTP if needed → Profile saved on success -3. Your backend polls invocation status → Uses saved profile in future browser automations -4. After authentication completes, Agent Auth automatically monitors the session every hour and updates the status if re-authentication is required -5. If credentials are stored, Agent Auth can automatically re-authenticate when sessions expire - - -**Plan for re-auth from the start.** If you want automated re-authentication later, use `save_credential_as` during the initial login. This saves credentials and form selectors, enabling `can_reauth` for future sessions. - - -## Key Concepts - -### The Three Primitives - -| Concept | What it is | Lifespan | -|---------|-----------|----------| -| **Profile** | A saved browser session (cookies, localStorage). Once authenticated, use it to get logged-in browsers. | Permanent | -| **Auth Agent** | Binds a profile to a domain. Tracks auth status, credentials, and selectors. | Permanent | -| **Invocation** | A single login attempt. Created each time you need to authenticate. | 5 minutes | - -### Auth Agent - -An Auth Agent is a persistent entity that manages authentication for a specific domain + profile combination. You create or retrieve Auth Agents using `kernel.agents.auth.create()`. This method is idempotent—if an agent already exists for the domain and profile, it returns the existing one. - -The Auth Agent stores: - -- The target domain (e.g., `doordash.com`) -- The linked browser profile -- The login URL (learned during discovery, used to speed up future invocations) -- The auth check URL (where to verify logged-in state) -- Current authentication status (`AUTHENTICATED` or `NEEDS_AUTH`) -- Linked credential ID for automated re-auth -- Saved form selectors for deterministic re-auth - -**Auth Agent fields:** - -| Field | Description | -|-------|-------------| -| `id` | Unique identifier for the agent | -| `status` | `AUTHENTICATED` or `NEEDS_AUTH` | -| `credential_id` | ID of linked credential (if any) | -| `credential_name` | Name of linked credential (if any) | -| `can_reauth` | `true` if agent has credentials AND selectors for automated re-auth | -| `has_selectors` | `true` if form selectors were saved from a previous login | -| `last_auth_check_at` | When the last session health check was performed | - - -**When is `can_reauth` true?** The agent must have BOTH: -1. A linked credential (`credential_id` is set) -2. Saved form selectors from a previous successful login (`has_selectors` is true) - -If you want automated re-auth later, use `save_credential_as` during your first login flow. - - -### Auth Agent Invocation - -An Invocation is a single authentication attempt that belongs to an Auth Agent. You start a new Invocation by calling `kernel.agents.auth.invocations.create(agent.id)`. Over time, an Auth Agent accumulates multiple Invocations as sessions need to be established or refreshed. - -**Invocation expires in 5 minutes.** If the user doesn't complete login in time, the invocation status becomes `EXPIRED`. - -**Invocation Types:** - -- `login` - Authentication flow (used for both first-time login and re-authentication) -- `step_up` - Real-time authentication challenge (coming soon) - -### Status Reference - -Different endpoints return different status values. Here's the complete reference: - -**Invocation status** (from `invocations.retrieve()`): - -| Status | Description | -|--------|-------------| -| `IN_PROGRESS` | Invocation is active, user is completing login | -| `SUCCESS` | Login completed successfully, profile is saved | -| `EXPIRED` | Invocation timed out (5 minutes) | -| `CANCELED` | Invocation was explicitly canceled | - -**Invocation create response** (from `invocations.create()`): - -| Status | Description | -|--------|-------------| -| `INVOCATION_CREATED` | New invocation started, redirect user to `hosted_url` | -| `ALREADY_AUTHENTICATED` | Profile is already logged in, no invocation needed | - -**Auth Agent status** (from `agents.auth.retrieve()`): - -| Status | Description | -|--------|-------------| -| `AUTHENTICATED` | Profile has a valid logged-in session | -| `NEEDS_AUTH` | Session expired or never logged in | - -**Reauth response** (from `agents.auth.reauth()`): - -| Status | Description | -|--------|-------------| -| `REAUTH_STARTED` | Automated re-auth started, poll `invocation_id` for completion | -| `ALREADY_AUTHENTICATED` | Session is still valid, no action needed | -| `CANNOT_REAUTH` | Missing credentials or selectors, manual login required | - -### Automatic Session Monitoring - -Auth Agents with `AUTHENTICATED` status are automatically checked hourly to verify the session is still valid. If the check detects the user has been logged out (session expired, cookies cleared by the site, etc.), the Auth Agent status is updated to `NEEDS_AUTH`. - -**How to use this:** - -- Poll `kernel.agents.auth.retrieve(id)` periodically to check if re-authentication is needed -- When status changes from `AUTHENTICATED` to `NEEDS_AUTH`: - - If `can_reauth` is `true`: Call `kernel.agents.auth.reauth(id)` for automated re-auth - - Otherwise: Trigger a new auth flow with `kernel.agents.auth.invocations.create(agent.id)` -- The check runs passively and doesn't modify the saved profile - -### What Discover Does - -When you call the discover endpoint on an Invocation, the Auth Agent: - -1. Navigates to the target domain (or directly to `login_url` if provided) -2. Finds the login page by looking for sign-in links/buttons -3. Extracts all form input fields on the page (email, password, etc.) -4. Returns the discovered fields with their names, types, labels, and selectors - -If the profile is already logged in from a previous session, discover returns `logged_in: true`. At that point, discovery is complete, no fields will be returned, and you should simply proceed to use the associated profile. No credentials or further steps are required. - -### What Submit Does - -When you call the submit endpoint on an Invocation, the Auth Agent: - -1. Fills in the form fields using the `field_values` you provide (matched by field name) -2. Clicks the submit button -3. Waits for the page to respond -4. Analyzes what happened next - -The AI determines the outcome by looking at the page after submission: - -- **Logged in** - The page shows a logged-in state (dashboard, account page, etc.) → Returns `logged_in: true` -- **Error displayed** - The page shows an error message → Returns `success: false` with `error_message` -- **New fields appeared** - The page now shows different form fields (e.g., OTP input after email/password) → Returns `needs_additional_auth: true` with `additional_fields` - -When you get `additional_fields`, collect those values from the user and call submit again. This loop continues until you get `logged_in: true` or an error. - ---- - -## Tips for Early Preview Testing - -- **Start simple** - Use the Hosted UI (Option 1) and test with sites that have basic email/password login -- **Provide login_url when known** - Speeds up discovery by skipping the login page search -- **Handle 2FA** - Test flows with 2FA to ensure your integration handles `additional_fields` -- **Poll with backoff** - Start at 2s intervals, increase to 5s. Max timeout is 5 minutes. See polling example below. -- **Clean up profiles** - Delete test profiles with `kernel.profiles.delete(profileName)` when done to avoid clutter -- **Plan for re-auth** - Use `save_credential_as` during initial login to enable automated re-auth later -- **Don't log credentials** - Never log `field_values` or credential data - ---- - -## Support - -Questions or issues? Reach out to us on Slack! - ---- - -## Installation (Early Preview) - -Before using Agent Auth, install the early preview SDK: +description: "Agent Auth early preview documentation" +--- + + +Agent Auth is now documented in the main docs. See the pages below for current documentation. + + + + + Introduction to Agent Auth and key concepts + + + Fully automated login with pre-linked credentials + + + Redirect users to complete login themselves + + + Build custom auth flows with full control + + + Store credentials for automated re-auth + + + Keep sessions alive automatically + + + +## Early Preview SDK Installation + +For early preview testers, install the preview SDK: **TypeScript/Node.js:** @@ -221,1163 +57,6 @@ dependencies = [ ] ``` ---- - -## Integration Options - - -**Quick Start Examples:** We have standalone example scripts for each integration pattern: -- **Developer Pre-creates Credential** - When you already have user credentials stored -- **Hosted UI Flow** - Redirect users to complete login themselves -- **Programmatic Flow** - Full control over the login process - - -### Option 1: Hosted UI (Recommended) - -The simplest approach—redirect users to our hosted authentication UI, then poll for completion. - -#### Happy Path in 30 Seconds - -```typescript -// 1. Create agent + invocation -const agent = await kernel.agents.auth.create({ target_domain: 'example.com', profile_name: 'user-123' }); -const invocation = await kernel.agents.auth.invocations.create({ auth_agent_id: agent.id }); - -// 2. Redirect user (or skip if ALREADY_AUTHENTICATED) -if (invocation.status !== 'ALREADY_AUTHENTICATED') { - redirect(invocation.hosted_url); -} - -// 3. Poll for completion, then use the profile -const browser = await kernel.browsers.create({ profile: { name: 'user-123' }, stealth: true }); -``` - -#### Hosted UI Redirect Contract - -| Question | Answer | -|----------|--------| -| Is `hosted_url` single-use? | No, user can refresh the page | -| Does Kernel redirect back to my app? | No, you must poll for completion | -| How do I detect user abandoned? | Invocation status becomes `EXPIRED` after 5 minutes | -| Can I pass state/return_url? | Not currently supported | - -#### Step 1: Start the Auth Flow - -```typescript -import { Kernel } from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// Step 1: Create or Find Auth Agent -const agent = await kernel.agents.auth.create({ - target_domain: 'netflix.com', - profile_name: 'netflix-user-123', - login_url: 'https://netflix.com/login', // Optional: speeds up discovery - proxy: { proxy_id: 'proxy_abc123' }, // Optional: use a proxy -}); - -// Step 2: Start the Auth Flow -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'netflix-creds', // Save credentials for future re-auth -}); - -// invocation returns: status, invocation_id, hosted_url, handoff_code, expires_at -``` - -#### Step 2: Redirect User - -```typescript -// Check if already logged in -if (invocation.status === 'ALREADY_AUTHENTICATED') { - console.log('Already logged in! Profile is ready to use.'); - // Skip to Step 4 - Use the Profile -} else { - // Redirect to hosted flow - window.location.href = invocation.hosted_url; -} -``` - -#### Step 3: Poll for Completion - -```typescript -// Recommended: Poll with backoff (2s → 3s → 5s), max 5 minutes -async function pollForCompletion(invocationId: string) { - const maxWaitMs = 5 * 60 * 1000; // 5 minutes - const start = Date.now(); - let delay = 2000; - - while (Date.now() - start < maxWaitMs) { - const status = await kernel.agents.auth.invocations.retrieve(invocationId); - - if (status.status === 'SUCCESS') { - return { success: true, status }; - } - if (status.status === 'EXPIRED' || status.status === 'CANCELED') { - return { success: false, reason: status.status }; - } - - await new Promise(r => setTimeout(r, delay)); - delay = Math.min(delay * 1.5, 5000); // Cap at 5s - } - - return { success: false, reason: 'TIMEOUT' }; -} - -const result = await pollForCompletion(invocation.invocation_id); - -if (result.success) { - console.log('Success! Profile is ready.'); - - // Verify auth agent status - const authAgent = await kernel.agents.auth.retrieve(agent.id); - console.log(authAgent.status); // "AUTHENTICATED" - console.log(authAgent.can_reauth); // true if credentials were saved -} -``` - - -**Don't poll too aggressively.** Use exponential backoff starting at 2 seconds. Invocations typically complete in 10-60 seconds depending on the site and user speed. - - -#### Step 4: Use the Profile - -```typescript -// Create a browser with a saved profile - the user is already logged in! -const browser = await kernel.browsers.create({ - stealth: true, - profile: { - name: 'netflix-user-123', // Use the profile name from Step 1 - }, - proxy_id: 'proxy_abc123', // If you used a proxy in Step 1, use the same one here -}); - -// browser.session_id - Session ID -// browser.cdp_ws_url - CDP URL for Playwright/Puppeteer -// Run your automation - the browser is already authenticated! - -// Cleanup when done -await kernel.browsers.deleteByID(browser.session_id); -``` - - -Use `stealth: true` and the same proxy configuration you passed to `kernel.agents.auth.create()`. Agent Auth runs the authentication browser session with stealth mode enabled—mismatched settings may cause unexpected behavior with bot detection or session issues. - - -#### Step 5: Handle Re-authentication - -When sessions expire, check if automated re-auth is available: - -```typescript -// Check auth agent status -const agent = await kernel.agents.auth.retrieve(agentId); - -if (agent.status === 'NEEDS_AUTH') { - if (agent.can_reauth) { - // Automated re-auth (no user interaction needed) - const reauth = await kernel.agents.auth.reauth(agent.id); - - if (reauth.status === 'REAUTH_STARTED') { - // Poll for completion using the same backoff pattern - const result = await pollForCompletion(reauth.invocation_id); - console.log('Re-auth complete:', result); - } else if (reauth.status === 'ALREADY_AUTHENTICATED') { - console.log('Session is still valid'); - } else if (reauth.status === 'CANNOT_REAUTH') { - // Selectors may have drifted - fall back to manual login - console.log('Cannot reauth:', reauth.message); - } - } else { - // Manual re-auth required - redirect user to hosted UI - const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'netflix-creds', // Save credentials this time! - }); - window.location.href = invocation.hosted_url; - } -} -``` - - -**If reauth fails:** The site's login form may have changed (selectors drifted). Fall back to creating a new invocation for manual login—this will re-capture the updated selectors. - - ---- - -### Option 2: Custom UI with Discover/Submit APIs - -Build your own authentication UI using the lower-level discover and submit APIs. You control the UI, but need to handle the multi-step flow yourself. - -#### When to Use Custom UI - -- You want login to match your app's design -- You need to collect credentials in your own form -- You're building a native mobile app - -#### Step 1: Start the Auth Invocation - -```typescript -import { Kernel } from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// Step 1: Create Agent & Start Invocation -const agent = await kernel.agents.auth.create({ - target_domain: 'doordash.com', - profile_name: 'my-profile-123', -}); - -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'doordash-creds', // Optional: save for re-auth -}); -``` - -#### Step 2: Exchange Handoff Code for JWT - -```typescript -const exchangeResponse = await kernel.agents.auth.invocations.exchange( - invocation.invocation_id, - { code: invocation.handoff_code } -); - -const jwt = exchangeResponse.jwt; // 30 min TTL -``` - -#### Step 3: Create JWT-Authenticated Client - -```typescript -const jwtKernel = new Kernel({ apiKey: jwt }); -``` - -#### Step 4: Discover Login Fields - -```typescript -const discoverResponse = await jwtKernel.agents.auth.invocations.discover( - invocation.invocation_id, - {} // Optional: { login_url: 'https://example.com/login' } -); - -if (discoverResponse.logged_in) { - // Already logged in! Profile saved. Skip to using the profile. -} - -if (discoverResponse.success && discoverResponse.fields) { - // Render these fields in your UI - // Fields are returned in DOM order—display them as-is -} -``` - - -**Discover is idempotent.** If the user refreshes or you call discover again, it returns the cached fields from the same invocation. You don't need to store fields—just call discover again to resume. - - -**About discovered fields:** - -- Fields are returned in DOM order from the login page -- Currently supports: `text`, `email`, `password`, `tel`, `number`, `code` -- Does NOT include CAPTCHAs, SSO buttons, or "remember me" checkboxes -- `additional_fields` (from submit) may include 2FA codes, security questions, or "select account" prompts—treat them generically as "more fields to fill" - -#### Step 5: Map Discovered Fields to Credentials - -The discover response returns an array of fields. Each field has a `name` property that you use as the key when submitting credentials. - -**Example discover response:** - -```json -{ - "success": true, - "fields": [ - { "name": "email", "type": "email", "label": "Email Address" }, - { "name": "password", "type": "password", "label": "Password" } - ] -} -``` - -**To submit, use each field's `name` as the key in `field_values`:** - -```json -{ - "field_values": { - "email": "user@example.com", - "password": "secretpassword" - } -} -``` - -**Recommended mapping strategy:** - -```typescript -// Mapping priority: -// 1. Match by field.type (most reliable) -// 2. Match by field.name patterns (fallback) -// 3. Log unmapped fields for debugging - -function mapCredentialsToFields( - fields: Array<{ name: string; type: string }>, - credentials: { email: string; password: string; code?: string } -): Record { - const fieldValues: Record = {}; - const unmapped: string[] = []; - - for (const field of fields) { - const name = field.name.toLowerCase(); - const type = field.type.toLowerCase(); - - // Priority 1: Match by type - if (type === 'email') { - fieldValues[field.name] = credentials.email; - } else if (type === 'password') { - fieldValues[field.name] = credentials.password; - } else if (type === 'code') { - fieldValues[field.name] = credentials.code || ''; - } - // Priority 2: Match by name patterns - else if (name.includes('email') || name.includes('user')) { - fieldValues[field.name] = credentials.email; - } else if (name.includes('pass')) { - fieldValues[field.name] = credentials.password; - } else { - unmapped.push(field.name); - } - } - - // Log unmapped fields for debugging (but never log actual values!) - if (unmapped.length > 0) { - console.warn('Unmapped fields:', unmapped); - } - - return fieldValues; -} - -const fieldValues = mapCredentialsToFields(discoverResponse.fields, userCredentials); -``` - - -**Never log `field_values` or credential data.** Only log field names for debugging. - - -#### Step 6: Submit Credentials (Loop Until Logged In) - -```typescript -let submitResponse = await jwtKernel.agents.auth.invocations.submit( - invocation.invocation_id, - { field_values: fieldValues } -); - -// Handle multi-step auth flows -while (submitResponse.needs_additional_auth && submitResponse.additional_fields) { - // Map the additional fields the same way - const additionalValues: Record = {}; - - for (const field of submitResponse.additional_fields) { - if (field.type === 'code' || field.name.includes('code') || field.name.includes('otp')) { - // Prompt user for their 2FA code - additionalValues[field.name] = await promptUserForOTP(); - } else if (field.type === 'password' || field.name.includes('password')) { - additionalValues[field.name] = userCredentials.password; - } - // Add more mappings as needed - } - - // Submit again with the new fields - submitResponse = await jwtKernel.agents.auth.invocations.submit( - invocation.invocation_id, - { field_values: additionalValues } - ); -} - -// Check final result -if (submitResponse.logged_in) { - // Success! Profile saved. -} - -if (submitResponse.error_message) { - // Login failed, show error to user - console.error(submitResponse.error_message); -} -``` - -**Field mapping tips:** - -- Always use `field.name` as the key in `field_values`—this is what the agent uses to match values to form inputs -- Check both `field.type` and `field.name` for matching—some sites use generic types but descriptive names -- Common patterns: `type: "email"` → user's email, `type: "password"` → user's password, `type: "code"` → 2FA/OTP code -- For `additional_fields` with type `code`, you'll typically need to prompt the user in real-time for their 2FA code - ---- - -### Option 3: Programmatic (Headless/Automated) - -For bots, service accounts, or automated testing where you provide credentials in code and no user is involved. - -#### When to Use Programmatic - -- Bot or service accounts with known credentials -- Automated testing/CI pipelines -- Backend-only flows with no user interaction - -#### Key Differences from Custom UI - -- You provide credentials from environment variables or a secrets manager -- No user prompt for 2FA—you need a TOTP generator or alternate method -- Use API key auth directly (no JWT exchange needed) - -```typescript -import { Kernel } from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// Credentials from your secrets manager -const CREDS = { - email: process.env.BOT_EMAIL!, - password: process.env.BOT_PASSWORD!, -}; - -// 1. Create agent + invocation -const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', - profile_name: 'bot-profile', - login_url: 'https://example.com/login', // Recommended for bots -}); - -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'bot-creds', // Enable reauth -}); - -if (invocation.status === 'ALREADY_AUTHENTICATED') { - console.log('Already logged in!'); -} else { - // 2. Discover fields (API key auth works, no JWT needed) - const discover = await kernel.agents.auth.invocations.discover( - invocation.invocation_id, - {} - ); - - if (discover.logged_in) { - console.log('Already logged in during discover!'); - } else if (discover.fields) { - // 3. Map credentials to fields - const fieldValues = mapCredentialsToFields(discover.fields, CREDS); - - // 4. Submit - const result = await kernel.agents.auth.invocations.submit( - invocation.invocation_id, - { field_values: fieldValues } - ); - - if (result.logged_in) { - console.log('Login successful!'); - } else if (result.needs_additional_auth) { - // Handle 2FA - see note below - console.log('2FA required:', result.additional_fields); - } - } -} - -// 5. Use the profile -const browser = await kernel.browsers.create({ - profile: { name: 'bot-profile' }, - stealth: true, -}); -``` - - -**2FA with bots:** If the target site requires 2FA, you'll need one of: -- A TOTP generator library (for authenticator app codes) -- Access to the account's email API (for email codes) -- App-specific passwords (if the service supports them) -- A service account without 2FA - -If you can't automate 2FA, the programmatic flow won't work for that site. - - ---- - -## End-to-End Example: The Complete Lifecycle - -Here's a complete example showing the full lifecycle: initial auth, using the profile, detecting session expiry, and re-auth. - -```typescript -import { Kernel } from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// === INITIAL SETUP (run once per user) === -async function setupAuth(userId: string, targetDomain: string) { - const profileName = `user-${userId}-${targetDomain}`; - - // 1. Create agent - const agent = await kernel.agents.auth.create({ - target_domain: targetDomain, - profile_name: profileName, - }); - - // 2. Start auth flow - const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: `${profileName}-creds`, // Enable future reauth! - }); - - if (invocation.status === 'ALREADY_AUTHENTICATED') { - return { agentId: agent.id, profileName, ready: true }; - } - - // 3. Return hosted URL for user to complete login - return { - agentId: agent.id, - profileName, - ready: false, - hostedUrl: invocation.hosted_url, - invocationId: invocation.invocation_id, - }; -} - -// === USE THE PROFILE (run for each automation) === -async function runAutomation(agentId: string, profileName: string) { - // 1. Check if we need to re-auth - const agent = await kernel.agents.auth.retrieve(agentId); - - if (agent.status === 'NEEDS_AUTH') { - if (agent.can_reauth) { - // Automated re-auth - const reauth = await kernel.agents.auth.reauth(agentId); - if (reauth.status === 'REAUTH_STARTED') { - await pollForCompletion(reauth.invocation_id); - } - } else { - throw new Error('Session expired and cannot reauth - user must log in again'); - } - } - - // 2. Create browser with authenticated profile - const browser = await kernel.browsers.create({ - profile: { name: profileName }, - stealth: true, - }); - - try { - // 3. Run your automation - // const page = await connectPlaywright(browser.cdp_ws_url); - // await page.goto('https://example.com/dashboard'); - // ... do your work ... - - return { success: true }; - } finally { - // 4. Always clean up - await kernel.browsers.deleteByID(browser.session_id); - } -} - -// === HELPER === -async function pollForCompletion(invocationId: string) { - const maxWaitMs = 5 * 60 * 1000; - const start = Date.now(); - let delay = 2000; - - while (Date.now() - start < maxWaitMs) { - const status = await kernel.agents.auth.invocations.retrieve(invocationId); - if (status.status !== 'IN_PROGRESS') return status; - await new Promise(r => setTimeout(r, delay)); - delay = Math.min(delay * 1.5, 5000); - } - throw new Error('Timeout waiting for auth'); -} -``` - -**What to store in your database:** - -| Data | Permanent? | Purpose | -|------|------------|---------| -| `agentId` | Yes | For reauth, status checks | -| `profileName` | Yes | For creating browsers | -| `invocationId` | No (5 min TTL) | Only while polling for completion | - ---- - -## Credentials API - -Store credentials for fully automated re-authentication. Credentials are encrypted at rest and never exposed in API responses. - -### Create Credential - -```typescript -const credential = await kernel.credentials.create({ - name: 'my-netflix-login', - domain: 'netflix.com', - values: { - email: 'user@example.com', - password: 'secretpassword', - }, -}); -``` - -### Link Credential to Auth Agent - -```typescript -const agent = await kernel.agents.auth.create({ - target_domain: 'netflix.com', - profile_name: 'my-profile', - credential_name: credential.name, // Link the credential by name -}); -``` - -### List Credentials - -```typescript -const response = await kernel.credentials.list({ - domain: 'netflix.com', // Optional filter -}); - -// Response is paginated - access items via .items -for (const credential of response.items) { - console.log(credential.id, credential.name); -} -``` - -### Delete Credential - -```typescript -await kernel.credentials.delete(credentialId); -``` - ---- - -## Reauth API - -Trigger automated re-authentication when sessions expire. - -```typescript -// Check if re-auth is needed -const agent = await kernel.agents.auth.retrieve(agentId); - -if (agent.status === 'NEEDS_AUTH' && agent.can_reauth) { - const reauth = await kernel.agents.auth.reauth(agent.id); - - switch (reauth.status) { - case 'REAUTH_STARTED': - console.log('Re-auth in progress:', reauth.invocation_id); - // Poll for completion... - break; - case 'ALREADY_AUTHENTICATED': - console.log('Session is still valid'); - break; - case 'CANNOT_REAUTH': - console.log('Cannot reauth:', reauth.message); - // Fall back to manual login - break; - } -} -``` - -**Requirements for `can_reauth`:** - -- Auth Agent must have a linked credential (`credential_id`) -- Auth Agent must have saved form selectors (`has_selectors`) from a previous successful login - ---- - -## API Reference - -### Auth Agents - -#### POST /agents/auth - -Create or find an auth agent for a domain/profile combination. This is idempotent—calling with the same domain and profile returns the existing agent. - -**Auth:** API key - -**Request:** - -```json -{ - "target_domain": "netflix.com", - "profile_name": "netflix-user-123", - "login_url": "https://netflix.com/login", - "credential_name": "my-netflix-login", - "proxy": { - "proxy_id": "proxy_abc123" - } -} -``` - -| Parameter | Required | Description | -|-----------|----------|-------------| -| `target_domain` | Yes | Target domain for authentication | -| `profile_name` | Yes | Name of the profile to use | -| `login_url` | No | Login page URL to skip discovery | -| `credential_name` | No | Name of an existing credential to link for auto-fill and re-auth | -| `proxy` | No | Proxy configuration | - -**Response:** - -```json -{ - "id": "abc123", - "profile_name": "netflix-user-123", - "domain": "netflix.com", - "status": "NEEDS_AUTH", - "credential_id": "cred_abc123", - "credential_name": "my-netflix-login", - "can_reauth": false, - "has_selectors": false, - "last_auth_check_at": null -} -``` - -#### GET /agents/auth/{id} - -Get auth agent details and current authentication status. - -**Auth:** API key - -**Response:** - -```json -{ - "id": "abc123", - "profile_name": "netflix-user-123", - "domain": "netflix.com", - "status": "AUTHENTICATED", - "credential_id": "cred_abc123", - "credential_name": "my-netflix-login", - "can_reauth": true, - "has_selectors": true, - "last_auth_check_at": "2025-01-15T10:30:00Z" -} -``` - -#### DELETE /agents/auth/{id} - -Delete an auth agent. - -**Auth:** API key - -**Response:** 204 No Content - - -Deleting an auth agent does not delete the associated browser profile. Use `kernel.profiles.delete(profileName)` to clean up profiles separately. - - -#### GET /agents/auth - -List auth agents with optional filters. - -**Auth:** API key - -**Query Parameters:** - -| Parameter | Description | -|-----------|-------------| -| `profile_name` | Filter by profile name | -| `target_domain` | Filter by target domain | -| `limit` | Maximum results (default: 20, max: 100) | -| `offset` | Number of results to skip | - -**Response:** - -The SDK returns a paginated response object. Access items via the `items` property: - -```typescript -const response = await kernel.agents.auth.list({ target_domain: 'netflix.com' }); - -for (const agent of response.items) { - console.log(agent.id, agent.status); -} -``` - -**Raw API Response:** - -```json -[ - { - "id": "abc123", - "profile_name": "netflix-user-123", - "domain": "netflix.com", - "status": "AUTHENTICATED", - "credential_id": "cred_abc123", - "credential_name": "my-netflix-login", - "has_selectors": true, - "can_reauth": true, - "last_auth_check_at": "2025-01-15T10:30:00Z" - } -] -``` - -**Headers:** -- `X-Has-More`: Whether there are more results -- `X-Next-Offset`: Offset for next page - -#### POST /agents/auth/{id}/reauth - -Trigger automated re-authentication. - -**Auth:** API key - -**Response (reauth started):** - -```json -{ - "status": "REAUTH_STARTED", - "invocation_id": "inv_xyz789" -} -``` - -**Response (already authenticated):** - -```json -{ - "status": "ALREADY_AUTHENTICATED" -} -``` - -**Response (cannot reauth):** - -```json -{ - "status": "CANNOT_REAUTH", - "message": "Missing stored credential or form selectors" -} -``` - ---- - -### Auth Agent Invocations - -#### POST /agents/auth/invocations - -Create an invocation to start an auth flow for an existing auth agent. - -**Auth:** API key - -**Request:** - -```json -{ - "auth_agent_id": "abc123", - "save_credential_as": "my-saved-creds" -} -``` - -| Parameter | Required | Description | -|-----------|----------|-------------| -| `auth_agent_id` | Yes | ID of the auth agent | -| `save_credential_as` | No | Save credentials with this name for future re-auth | - -**Response (invocation created):** - -```json -{ - "status": "INVOCATION_CREATED", - "invocation_id": "inv_xyz789", - "handoff_code": "aBcD123EfGh456IjKl789", - "hosted_url": "https://agent-auth.onkernel.com/agents/auth/invocations/inv_xyz789?code=aBcD123", - "expires_at": "2025-12-01T12:00:00Z" -} -``` - -**Response (already logged in - no invocation needed):** - -```json -{ - "status": "ALREADY_AUTHENTICATED" -} -``` - - -When `status` is `ALREADY_AUTHENTICATED`, the agent is already logged in and no invocation was created. You can proceed directly to using the profile. - - -#### GET /agents/auth/invocations/{invocation_id} - -Get invocation status and details. - -**Auth:** API key or JWT (from exchange) - -**Response:** - -```json -{ - "status": "IN_PROGRESS", - "app_name": "My App", - "target_domain": "doordash.com", - "expires_at": "2025-12-01T12:00:00Z" -} -``` - -| Status | Description | -|--------|-------------| -| `IN_PROGRESS` | User is completing login | -| `SUCCESS` | Login successful, profile saved | -| `EXPIRED` | Invocation timed out | -| `CANCELED` | Invocation was canceled | - -#### POST /agents/auth/invocations/{invocation_id}/exchange - -Exchange a handoff code for a JWT token. - -**Auth:** None (handoff code is the credential) - -**Request:** - -```json -{ - "code": "aBcD123EfGh456IjKl789" -} -``` - -**Response:** - -```json -{ - "invocation_id": "inv_xyz789", - "jwt": "eyJhbGciOiJIUzI1NiIs..." -} -``` - -#### POST /agents/auth/invocations/{invocation_id}/discover - -Discover login form fields on the target site. - -**Auth:** API key or JWT (from exchange) - -**Request:** - -```json -{ - "login_url": "https://doordash.com/login" -} -``` - -| Parameter | Description | -|-----------|-------------| -| `login_url` | Override the stored login URL | - -**Response (fields found):** - -```json -{ - "success": true, - "logged_in": false, - "login_url": "https://identity.doordash.com/auth", - "page_title": "Sign In - DoorDash", - "fields": [ - { - "name": "email", - "type": "email", - "label": "Email", - "placeholder": "Enter your email", - "required": true, - "selector": "//input[@id='email']" - }, - { - "name": "password", - "type": "password", - "label": "Password", - "required": true, - "selector": "//input[@id='password']" - } - ] -} -``` - -**Response (already logged in):** - -```json -{ - "success": true, - "logged_in": true -} -``` - -#### POST /agents/auth/invocations/{invocation_id}/submit - -Submit credentials and attempt login. - -**Auth:** API key or JWT (from exchange) - -**Request:** - -```json -{ - "field_values": { - "email": "user@example.com", - "password": "********" - } -} -``` - -**Response (success):** - -```json -{ - "success": true, - "logged_in": true, - "app_name": "My App", - "target_domain": "doordash.com" -} -``` - -**Response (needs 2FA):** - -```json -{ - "success": true, - "logged_in": false, - "needs_additional_auth": true, - "additional_fields": [ - { - "name": "code", - "type": "code", - "label": "Verification Code", - "required": true, - "selector": "//input[@name='code']" - } - ] -} -``` - -**Response (error):** - -```json -{ - "success": false, - "logged_in": false, - "error_message": "Incorrect email or password" -} -``` - ---- - -### Credentials - -#### POST /credentials - -Create a new credential. - -**Auth:** API key - -**Request:** - -```json -{ - "name": "my-netflix-login", - "domain": "netflix.com", - "values": { - "email": "user@example.com", - "password": "secretpassword" - } -} -``` - -| Parameter | Required | Description | -|-----------|----------|-------------| -| `name` | Yes | Unique name within your organization | -| `domain` | Yes | Target domain | -| `values` | Yes | Key-value pairs of credential fields | - -**Response:** - -```json -{ - "id": "cred_abc123", - "name": "my-netflix-login", - "domain": "netflix.com", - "created_at": "2025-01-15T10:30:00Z", - "updated_at": "2025-01-15T10:30:00Z" -} -``` - - -Credential values are write-only and never returned in API responses. - - -#### GET /credentials - -List credentials. - -**Auth:** API key - -**Query Parameters:** - -| Parameter | Description | -|-----------|-------------| -| `domain` | Filter by domain | -| `limit` | Maximum results (default: 20, max: 100) | -| `offset` | Number of results to skip | - -#### GET /credentials/{id} - -Get credential metadata (values not returned). - -#### PATCH /credentials/{id} - -Update a credential. - -**Request:** - -```json -{ - "name": "updated-name", - "values": { - "email": "newemail@example.com", - "password": "newpassword" - } -} -``` - -#### DELETE /credentials/{id} - -Delete a credential. - ---- - -### Field Types - -| Type | Description | -|------|-------------| -| `text` | Generic text input | -| `email` | Email address | -| `password` | Password (masked) | -| `tel` | Phone number | -| `number` | Numeric input | -| `url` | URL input | -| `code` | Verification code (OTP, 2FA) | - ---- - -## Security - -| Feature | Description | -|---------|-------------| -| **Credentials encrypted at rest** | All credential values are encrypted using per-organization keys | -| **Credentials never exposed** | Values are write-only and never returned in API responses | -| **Credentials never shared with LLMs** | Values are submitted programmatically directly to the target site | -| **Short-lived tokens** | JWT tokens expire after 30 minutes | -| **Handoff codes expire** | One-time use, expire after 5 minutes | -| **Profile encryption** | Browser profiles are encrypted at rest | -| **Isolated execution** | Each auth flow runs in an isolated browser environment | - -### FAQ - -**Does Kernel see my users' credentials?** - -Yes, when you use `submit()`, credentials pass through Kernel's servers to fill the form on the target site. They are: -- Encrypted in transit (TLS) -- Never logged or persisted beyond the immediate use -- Never passed to AI/LLM models -- Used only to fill form fields and click submit - -**Can I delete or rotate credentials?** - -Yes. Use `kernel.credentials.delete(id)` to delete, or `kernel.credentials.update(id, { values: {...} })` to rotate. - -**What if `save_credential_as` captures the wrong fields?** - -The credential captures whatever `field_values` you submit. If you submit extra fields or wrong mappings, those get saved. You can update or delete the credential and try again. - -**Should I log `field_values` for debugging?** +## Support -Never log credential values. Log only field names if you need to debug mapping issues. +Questions or issues? Reach out to us on Slack! diff --git a/agent-auth/hosted-ui.mdx b/agent-auth/hosted-ui.mdx index de66c66..5ddfdfe 100644 --- a/agent-auth/hosted-ui.mdx +++ b/agent-auth/hosted-ui.mdx @@ -13,6 +13,43 @@ Use the Hosted UI when: - You need a quick integration with minimal code - You want to avoid handling multi-step auth flows yourself +## How It Works + + + + ```typescript + const agent = await kernel.agents.auth.create({ + target_domain: 'example.com', + profile_name: 'my-profile', + }); + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); + ``` + + + ```typescript + if (invocation.status !== 'ALREADY_AUTHENTICATED') { + window.location.href = invocation.hosted_url; + } + ``` + + + ```typescript + const status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + // Poll until status.status === 'SUCCESS' + ``` + + + ```typescript + const browser = await kernel.browsers.create({ + profile: { name: 'my-profile' }, + stealth: true, + }); + ``` + + + ## Complete Example @@ -184,12 +221,11 @@ const invocation = await kernel.agents.auth.invocations.create({ |-------|-------------| | `invocation_id` | Unique ID for this auth attempt | | `hosted_url` | URL to redirect the user to | -| `handoff_code` | One-time code for programmatic flows | | `expires_at` | When the invocation expires (5 minutes) | -| `logged_in` | If `true`, already authenticated—no login needed | +| `status` | If `ALREADY_AUTHENTICATED`, profile is already logged in—no redirect needed | -If `logged_in` is `true`, the profile is already authenticated. Skip the redirect and go straight to using the profile. +If `status` is `ALREADY_AUTHENTICATED`, the profile is already logged in. Skip the redirect and go straight to using the profile. ### 3. Redirect the User diff --git a/agent-auth/overview.mdx b/agent-auth/overview.mdx index cf66ef5..b55e9f2 100644 --- a/agent-auth/overview.mdx +++ b/agent-auth/overview.mdx @@ -22,16 +22,32 @@ Agent Auth solves this by providing: - Your backend calls `kernel.agents.auth.create()` to create an Auth Agent for a specific domain and profile combination. + ```typescript + const agent = await kernel.agents.auth.create({ + target_domain: 'example.com', + profile_name: 'my-profile', + }); + ``` - Call `kernel.agents.auth.invocations.create()` to start the auth flow. This returns a `hosted_url` where users complete login, or you can use the programmatic API for headless flows. + ```typescript + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); + // Returns hosted_url for user login, or use programmatic API + ``` - The user is redirected to the hosted UI where they enter credentials and handle 2FA if needed. The profile is saved on success. + Redirect the user to `invocation.hosted_url` where they enter credentials and handle 2FA. The profile is saved on success. - Create browsers with the saved profile using `kernel.browsers.create({ profile: { name: 'my-profile' } })`. The browser is already logged in. + ```typescript + const browser = await kernel.browsers.create({ + profile: { name: 'my-profile' }, + stealth: true, + }); + // Browser is already logged in! + ``` @@ -61,37 +77,31 @@ An **Invocation** is a single authentication attempt. You start one by calling ` ### Profile -A **Profile** stores browser session state (cookies, localStorage) that persists across browser sessions. Once authenticated via Agent Auth, use the profile with `kernel.browsers.create()` to get an already-logged-in browser. - -See [Profiles](/browsers/profiles) for more details on working with profiles. +A **Profile** stores browser session state (cookies, localStorage) that persists across browser sessions. Once authenticated via Agent Auth, use the profile with `kernel.browsers.create()` to get an already-logged-in browser. See [Profiles](/browsers/profiles) for more. ### Credentials -**Credentials** are securely stored login credentials that enable automated re-authentication when sessions expire. Values are encrypted at rest and never exposed to LLMs or API responses. - -See [Credentials](/agent-auth/credentials) for details on storing and using credentials. - -## Integration Options - -Agent Auth offers two integration approaches: +**Credentials** are securely stored login credentials that enable automated re-authentication when sessions expire. Values are encrypted at rest and never exposed to LLMs or API responses. See [Credentials](/agent-auth/credentials) for more. -### Hosted UI (Recommended) +## Choose Your Integration -The simplest approach—redirect users to our hosted authentication UI. Best for: -- User-facing applications -- When you want Kernel to handle the login UI -- Quick integration with minimal code + + + **Best for:** Fully automated flows -See [Hosted UI Guide](/agent-auth/hosted-ui) for implementation details. - -### Programmatic API + Credentials are pre-linked. System handles everything automatically. + + + **Best for:** User-facing apps -Build your own authentication UI using the lower-level discover and submit APIs. Best for: -- Custom UI requirements -- Automated/headless scenarios -- When you have credentials stored + Redirect users to Kernel's hosted login page. + + + **Best for:** Custom UI or headless -See [Programmatic Guide](/agent-auth/programmatic) for implementation details. + Build your own UI with discover/submit APIs. + + ## Security @@ -101,8 +111,6 @@ Agent Auth is designed with security as a first principle: |-----------------|-------------| | **Credentials never stored in plaintext** | All credential values are encrypted at rest using per-organization encryption keys | | **Credentials never shared with LLMs** | Credentials are submitted programmatically to target sites, never passed through AI models | -| **Short-lived tokens** | JWT tokens for programmatic flows expire after 30 minutes | -| **One-time handoff codes** | Handoff codes can only be exchanged once and expire after 5 minutes | | **Profile encryption** | Browser profiles are encrypted end-to-end | | **Isolated execution** | Each auth flow runs in an isolated browser environment | @@ -191,20 +199,3 @@ if status.status == "SUCCESS": print(f"Browser ready: {browser.cdp_ws_url}") ``` - -## Next Steps - - - - Get started with the simplest integration path - - - Build custom auth flows with full control - - - Store credentials for automated re-auth - - - Keep sessions alive automatically - - diff --git a/agent-auth/programmatic.mdx b/agent-auth/programmatic.mdx index e44c3bd..eb3d022 100644 --- a/agent-auth/programmatic.mdx +++ b/agent-auth/programmatic.mdx @@ -9,27 +9,54 @@ The Programmatic flow gives you complete control over the authentication process Use the Programmatic flow when: - You need a custom authentication UI that matches your app's design -- You're building automated/headless authentication scenarios -- You have credentials stored and want to authenticate without user interaction -- You need fine-grained control over the auth flow +- You need fine-grained control over the discover/submit flow +- You want to build your own credential mapping logic + + +**Have credentials stored?** If you just want automated login with pre-linked credentials, [Auto-Login](/agent-auth/auto-login) is simpler—it handles discover/submit automatically. Use Programmatic when you need custom control over each step. + ## How It Works - Same as Hosted UI - create an agent and start an invocation - - - Exchange the one-time handoff code for a scoped JWT token + ```typescript + const agent = await kernel.agents.auth.create({ + target_domain: 'example.com', + profile_name: 'my-profile', + }); + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); + ``` - Call `discover()` to get the form fields on the login page + ```typescript + const discover = await kernel.agents.auth.invocations.discover( + invocation.invocation_id, + {} + ); + // discover.fields contains the form fields + ``` - Call `submit()` with the field values to attempt login + ```typescript + const submit = await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: { email: 'user@example.com', password: 'secret' } } + ); + ``` - If additional fields are returned (2FA, OTP), collect and submit again + ```typescript + // If submit.needs_additional_auth, collect the new fields and submit again + if (submit.needs_additional_auth) { + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: { code: '123456' } } + ); + } + ``` @@ -53,19 +80,9 @@ const invocation = await kernel.agents.auth.invocations.create({ if (invocation.status === 'ALREADY_AUTHENTICATED') { console.log('Already authenticated!'); - // Profile is ready to use } else { - // Step 2: Exchange handoff code for JWT - const exchange = await kernel.agents.auth.invocations.exchange( - invocation.invocation_id, - { code: invocation.handoff_code } - ); - - // Step 3: Create JWT-authenticated client - const jwtKernel = new Kernel({ apiKey: exchange.jwt }); - - // Step 4: Discover login fields - const discoverResponse = await jwtKernel.agents.auth.invocations.discover( + // Step 2: Discover login fields + const discoverResponse = await kernel.agents.auth.invocations.discover( invocation.invocation_id, {} ); @@ -75,19 +92,19 @@ if (invocation.status === 'ALREADY_AUTHENTICATED') { } else if (discoverResponse.success && discoverResponse.fields) { console.log('Discovered fields:', discoverResponse.fields); - // Step 5: Map credentials to discovered fields + // Step 3: Map credentials to discovered fields const fieldValues = mapCredentialsToFields(discoverResponse.fields, { email: 'user@example.com', password: 'secretpassword', }); - // Step 6: Submit credentials - let submitResponse = await jwtKernel.agents.auth.invocations.submit( + // Step 4: Submit credentials + let submitResponse = await kernel.agents.auth.invocations.submit( invocation.invocation_id, { field_values: fieldValues } ); - // Step 7: Handle multi-step auth + // Step 5: Handle multi-step auth while (submitResponse.needs_additional_auth && submitResponse.additional_fields) { console.log('Additional auth required:', submitResponse.additional_fields); @@ -99,7 +116,7 @@ if (invocation.status === 'ALREADY_AUTHENTICATED') { { code: otpCode } ); - submitResponse = await jwtKernel.agents.auth.invocations.submit( + submitResponse = await kernel.agents.auth.invocations.submit( invocation.invocation_id, { field_values: additionalValues } ); @@ -158,17 +175,8 @@ invocation = await kernel.agents.auth.invocations.create( if invocation.status == "ALREADY_AUTHENTICATED": print("Already authenticated!") else: - # Step 2: Exchange handoff code for JWT - exchange = await kernel.agents.auth.invocations.exchange( - invocation.invocation_id, - code=invocation.handoff_code, - ) - - # Step 3: Create JWT-authenticated client - jwt_kernel = Kernel(api_key=exchange.jwt) - - # Step 4: Discover login fields - discover_response = await jwt_kernel.agents.auth.invocations.discover( + # Step 2: Discover login fields + discover_response = await kernel.agents.auth.invocations.discover( invocation.invocation_id, ) @@ -177,19 +185,19 @@ else: elif discover_response.success and discover_response.fields: print(f"Discovered fields: {discover_response.fields}") - # Step 5: Map credentials to discovered fields + # Step 3: Map credentials to discovered fields field_values = map_credentials_to_fields( discover_response.fields, {"email": "user@example.com", "password": "secretpassword"}, ) - # Step 6: Submit credentials - submit_response = await jwt_kernel.agents.auth.invocations.submit( + # Step 4: Submit credentials + submit_response = await kernel.agents.auth.invocations.submit( invocation.invocation_id, field_values=field_values, ) - # Step 7: Handle multi-step auth + # Step 5: Handle multi-step auth while ( submit_response.needs_additional_auth and submit_response.additional_fields @@ -202,7 +210,7 @@ else: {"code": otp_code}, ) - submit_response = await jwt_kernel.agents.auth.invocations.submit( + submit_response = await kernel.agents.auth.invocations.submit( invocation.invocation_id, field_values=additional_values, ) @@ -235,39 +243,12 @@ def map_credentials_to_fields(fields, credentials): ## Step-by-Step Breakdown -### 1. Exchange Handoff Code for JWT - -The handoff code is a one-time credential that you exchange for a JWT token. This JWT is scoped to the specific invocation and grants permission to control the browser session. - -```typescript -const exchange = await kernel.agents.auth.invocations.exchange( - invocation.invocation_id, - { code: invocation.handoff_code } -); - -const jwt = exchange.jwt; // Valid for 30 minutes -``` - - -The handoff code can only be used once and expires after 5 minutes. The JWT is valid for 30 minutes. - - -### 2. Create JWT-Authenticated Client - -Create a new Kernel client using the JWT for subsequent API calls: - -```typescript -const jwtKernel = new Kernel({ apiKey: exchange.jwt }); -``` - -This client has limited permissions—it can only call the discover and submit endpoints for this specific invocation. - -### 3. Discover Login Fields +### 1. Discover Login Fields Call `discover()` to navigate to the login page and extract form fields: ```typescript -const discoverResponse = await jwtKernel.agents.auth.invocations.discover( +const discoverResponse = await kernel.agents.auth.invocations.discover( invocation.invocation_id, { login_url: 'https://example.com/login' } // Optional: override login URL ); @@ -305,7 +286,7 @@ const discoverResponse = await jwtKernel.agents.auth.invocations.discover( If `discover()` returns `logged_in: true`, the profile is already authenticated from a previous session. No credentials are needed—proceed to using the profile. -### 4. Map Credentials to Fields +### 2. Map Credentials to Fields The discover response returns an array of fields with `name`, `type`, and `label` properties. Use the `name` as the key when submitting credentials: @@ -340,12 +321,12 @@ const fieldValues = { | `number` | Numeric input | 12345 | | `code` | Verification code (OTP/2FA) | 123456 | -### 5. Submit Credentials +### 3. Submit Credentials Submit the mapped field values to attempt login: ```typescript -const submitResponse = await jwtKernel.agents.auth.invocations.submit( +const submitResponse = await kernel.agents.auth.invocations.submit( invocation.invocation_id, { field_values: fieldValues } ); @@ -390,7 +371,7 @@ const submitResponse = await jwtKernel.agents.auth.invocations.submit( } ``` -### 6. Handle Multi-Step Auth +### 4. Handle Multi-Step Auth When `needs_additional_auth` is `true`, the page is showing new fields (typically 2FA/OTP). Collect the additional values and submit again: @@ -408,7 +389,7 @@ while (submitResponse.needs_additional_auth && submitResponse.additional_fields) } // Submit again - submitResponse = await jwtKernel.agents.auth.invocations.submit( + submitResponse = await kernel.agents.auth.invocations.submit( invocation.invocation_id, { field_values: additionalValues } ); @@ -470,7 +451,7 @@ See [Credentials](/agent-auth/credentials) for details on pre-storing credential ```typescript try { - const discoverResponse = await jwtKernel.agents.auth.invocations.discover( + const discoverResponse = await kernel.agents.auth.invocations.discover( invocation.invocation_id, {} ); @@ -480,7 +461,7 @@ try { return; } - const submitResponse = await jwtKernel.agents.auth.invocations.submit( + const submitResponse = await kernel.agents.auth.invocations.submit( invocation.invocation_id, { field_values: fieldValues } ); @@ -490,9 +471,7 @@ try { // Show error to user, let them retry } } catch (error) { - if (error.status === 401) { - console.error('JWT expired - exchange a new handoff code'); - } else if (error.status === 404) { + if (error.status === 404) { console.error('Invocation not found or expired'); } else { console.error('Unexpected error:', error.message); @@ -502,7 +481,6 @@ try { ## Security Considerations -- The JWT is scoped to the specific invocation and cannot access other resources - Credentials submitted via `submit()` are sent directly to the target site - Credentials are never logged, stored in plaintext, or passed to LLMs - The browser session is isolated and destroyed after the invocation completes diff --git a/docs.json b/docs.json index 3406726..9a62632 100644 --- a/docs.json +++ b/docs.json @@ -62,17 +62,6 @@ "quickstart" ] }, - { - "group": "Agent Auth", - "pages": [ - "agent-auth/overview", - "agent-auth/hosted-ui", - "agent-auth/programmatic", - "agent-auth/credentials", - "agent-auth/session-monitoring", - "agent-auth/early-preview" - ] - }, { "group": "Browsers", "pages": [ @@ -83,6 +72,17 @@ "browsers/standby", "browsers/profiles", "browsers/termination", + { + "group": "Agent Auth", + "pages": [ + "agent-auth/overview", + "agent-auth/auto-login", + "agent-auth/hosted-ui", + "agent-auth/programmatic", + "agent-auth/credentials", + "agent-auth/session-monitoring" + ] + }, { "group": "Bot Anti-Detection", "pages": [ From 1bd28aab04cfe3c0394e2975f76a7aea4a303c46 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Mon, 22 Dec 2025 10:13:35 -0500 Subject: [PATCH 06/12] docs(agent-auth): update target_domain to domain param --- agent-auth/auto-login.mdx | 10 +++++----- agent-auth/credentials.mdx | 6 +++--- agent-auth/hosted-ui.mdx | 14 +++++++------- agent-auth/overview.mdx | 6 +++--- agent-auth/programmatic.mdx | 8 ++++---- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/agent-auth/auto-login.mdx b/agent-auth/auto-login.mdx index 8dcf5be..260100d 100644 --- a/agent-auth/auto-login.mdx +++ b/agent-auth/auto-login.mdx @@ -31,7 +31,7 @@ If you need users to enter their own credentials, use the [Hosted UI](/agent-aut ```typescript const agent = await kernel.agents.auth.create({ - target_domain: 'netflix.com', + domain: 'netflix.com', profile_name: 'netflix-user-123', credential_name: credential.name, }); @@ -81,7 +81,7 @@ const credential = await kernel.credentials.create({ // Step 2: Create auth agent with credential linked const agent = await kernel.agents.auth.create({ - target_domain: 'netflix.com', + domain: 'netflix.com', profile_name: 'netflix-user-123', credential_name: credential.name, login_url: 'https://netflix.com/login', // Optional: speeds up discovery @@ -134,7 +134,7 @@ credential = await kernel.credentials.create( # Step 2: Create auth agent with credential linked agent = await kernel.agents.auth.create( - target_domain="netflix.com", + domain="netflix.com", profile_name="netflix-user-123", credential_name=credential.name, login_url="https://netflix.com/login", # Optional: speeds up discovery @@ -340,7 +340,7 @@ If the target site requires a specific IP or region, configure a proxy: ```typescript TypeScript const agent = await kernel.agents.auth.create({ - target_domain: 'region-locked-site.com', + domain: 'region-locked-site.com', profile_name: 'my-profile', credential_name: 'my-credential', proxy: { proxy_id: 'proxy_abc123' }, @@ -356,7 +356,7 @@ const browser = await kernel.browsers.create({ ```python Python agent = await kernel.agents.auth.create( - target_domain="region-locked-site.com", + domain="region-locked-site.com", profile_name="my-profile", credential_name="my-credential", proxy={"proxy_id": "proxy_abc123"}, diff --git a/agent-auth/credentials.mdx b/agent-auth/credentials.mdx index fb10ed0..b6007b9 100644 --- a/agent-auth/credentials.mdx +++ b/agent-auth/credentials.mdx @@ -29,7 +29,7 @@ Without stored credentials, every time a session expires, you need to redirect u ```typescript const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', + domain: 'example.com', profile_name: 'my-profile', credential_name: 'my-login', }); @@ -110,7 +110,7 @@ There are two ways to link credentials to an Auth Agent: ```typescript // Create auth agent with credential link const agent = await kernel.agents.auth.create({ - target_domain: 'netflix.com', + domain: 'netflix.com', profile_name: 'my-profile', credential_id: credential.id, // Link the credential }); @@ -306,7 +306,7 @@ async function setupAutomatedAuth() { // 2. Create auth agent with credential const agent = await kernel.agents.auth.create({ - target_domain: 'portal.acme.com', + domain: 'portal.acme.com', profile_name: 'acme-agent-profile', credential_id: credential.id, login_url: 'https://portal.acme.com/login', diff --git a/agent-auth/hosted-ui.mdx b/agent-auth/hosted-ui.mdx index 5ddfdfe..ebe4383 100644 --- a/agent-auth/hosted-ui.mdx +++ b/agent-auth/hosted-ui.mdx @@ -19,7 +19,7 @@ Use the Hosted UI when: ```typescript const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', + domain: 'example.com', profile_name: 'my-profile', }); const invocation = await kernel.agents.auth.invocations.create({ @@ -60,7 +60,7 @@ const kernel = new Kernel(); // Step 1: Create or get existing Auth Agent const agent = await kernel.agents.auth.create({ - target_domain: 'doordash.com', + domain: 'doordash.com', profile_name: 'doordash-user-123', login_url: 'https://identity.doordash.com/auth', // Optional: speeds up discovery }); @@ -131,7 +131,7 @@ kernel = Kernel() # Step 1: Create or get existing Auth Agent agent = await kernel.agents.auth.create( - target_domain="doordash.com", + domain="doordash.com", profile_name="doordash-user-123", login_url="https://identity.doordash.com/auth", # Optional ) @@ -192,7 +192,7 @@ An Auth Agent represents a (domain, profile) pair. Creating one is idempotent— ```typescript const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', // Required: domain to authenticate + domain: 'example.com', // Required: domain to authenticate profile_name: 'my-profile', // Required: profile to store session login_url: 'https://example.com/login', // Optional: speeds up discovery }); @@ -201,7 +201,7 @@ const agent = await kernel.agents.auth.create({ **Parameters:** | Parameter | Required | Description | |-----------|----------|-------------| -| `target_domain` | Yes | The domain to authenticate (e.g., `netflix.com`) | +| `domain` | Yes | The domain to authenticate (e.g., `netflix.com`) | | `profile_name` | Yes | Name of the profile to store the authenticated session | | `login_url` | No | Direct URL to the login page. Providing this speeds up discovery. | | `proxy` | No | Proxy configuration (see [Proxies](/proxies/overview)) | @@ -303,7 +303,7 @@ If the target site requires a specific IP or region, configure a proxy: ```typescript // Create auth agent with proxy const agent = await kernel.agents.auth.create({ - target_domain: 'region-locked-site.com', + domain: 'region-locked-site.com', profile_name: 'my-profile', proxy: { proxy_id: 'proxy_abc123' }, }); @@ -357,7 +357,7 @@ Handle common error scenarios: ```typescript try { const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', + domain: 'example.com', profile_name: 'my-profile', }); diff --git a/agent-auth/overview.mdx b/agent-auth/overview.mdx index b55e9f2..fd583cb 100644 --- a/agent-auth/overview.mdx +++ b/agent-auth/overview.mdx @@ -24,7 +24,7 @@ Agent Auth solves this by providing: ```typescript const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', + domain: 'example.com', profile_name: 'my-profile', }); ``` @@ -126,7 +126,7 @@ const kernel = new Kernel(); // 1. Create or get existing Auth Agent const agent = await kernel.agents.auth.create({ - target_domain: 'netflix.com', + domain: 'netflix.com', profile_name: 'netflix-user-123', }); @@ -168,7 +168,7 @@ kernel = Kernel() # 1. Create or get existing Auth Agent agent = await kernel.agents.auth.create( - target_domain="netflix.com", + domain="netflix.com", profile_name="netflix-user-123", ) diff --git a/agent-auth/programmatic.mdx b/agent-auth/programmatic.mdx index eb3d022..ca0a9dd 100644 --- a/agent-auth/programmatic.mdx +++ b/agent-auth/programmatic.mdx @@ -22,7 +22,7 @@ Use the Programmatic flow when: ```typescript const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', + domain: 'example.com', profile_name: 'my-profile', }); const invocation = await kernel.agents.auth.invocations.create({ @@ -70,7 +70,7 @@ const kernel = new Kernel(); // Step 1: Create Auth Agent and Invocation const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', + domain: 'example.com', profile_name: 'my-profile', }); @@ -164,7 +164,7 @@ kernel = Kernel() # Step 1: Create Auth Agent and Invocation agent = await kernel.agents.auth.create( - target_domain="example.com", + domain="example.com", profile_name="my-profile", ) @@ -340,7 +340,7 @@ const submitResponse = await kernel.agents.auth.invocations.submit( "success": true, "logged_in": true, "app_name": "My App", - "target_domain": "example.com" + "domain": "example.com" } ``` From 17164514970d2da4748aa4d0a7af4155aa60088b Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Mon, 22 Dec 2025 10:20:01 -0500 Subject: [PATCH 07/12] docs(agent-auth): update wording on credential exposure --- agent-auth/credentials.mdx | 4 ++-- agent-auth/overview.mdx | 14 +++++++------- agent-auth/programmatic.mdx | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/agent-auth/credentials.mdx b/agent-auth/credentials.mdx index b6007b9..3a34c56 100644 --- a/agent-auth/credentials.mdx +++ b/agent-auth/credentials.mdx @@ -12,7 +12,7 @@ Without stored credentials, every time a session expires, you need to redirect u - **Automated re-auth** - Sessions can be refreshed automatically in the background - **No user interaction** - Re-authentication happens without user involvement - **Secure storage** - Credentials are encrypted at rest using per-organization keys -- **Never exposed** - Values are never returned in API responses or shared with LLMs +- **Never exposed** - Values are never returned in API responses ## How Credentials Work @@ -359,7 +359,7 @@ Credentials are designed with security as the top priority: | **Encryption at rest** | All credential values are encrypted using per-organization keys | | **Write-only values** | Values cannot be retrieved via the API after creation | | **Never logged** | Credential values are never written to logs | -| **Never shared with LLMs** | Values are injected directly into form fields, never passed through AI models | +| **Never exposed** | Values are injected directly into form fields, never returned in API responses | | **Isolated execution** | Authentication runs in an isolated browser environment | ## Best Practices diff --git a/agent-auth/overview.mdx b/agent-auth/overview.mdx index fd583cb..7efd711 100644 --- a/agent-auth/overview.mdx +++ b/agent-auth/overview.mdx @@ -1,20 +1,20 @@ --- title: "Overview" -description: "Secure authentication for AI agents - log users into any website and save authenticated browser sessions" +description: "Secure authentication system - log users into any website and save authenticated browser sessions" --- -Agent Auth is an AI-powered authentication system that helps you log users into any website and save their authenticated browser session (profile) for future automations. It uses web agents to intelligently discover login forms, dynamically generate credential inputs, and handle multi-step authentication flows like 2FA/OTP. +Agent Auth is an authentication system that helps you log users into any website and save their authenticated browser session (profile) for future automations. It automatically discovers login forms, maps credential inputs, and handles multi-step authentication flows like 2FA/OTP. ## Why Agent Auth? -AI agents can navigate the open web with ease, but the most valuable workflows live behind logins. To perform tasks on behalf of real users, agents need a secure way to access authenticated sessions. +The most valuable workflows live behind logins. To perform tasks on behalf of real users, your applications need a secure way to access authenticated sessions. Agent Auth solves this by providing: -- **Unified access to logged-in websites** - One API to log agents into any site with user consent +- **Unified access to logged-in websites** - One API to log into any site with user consent - **Automatic form discovery** - Detects login form fields on any website - **Multi-step auth handling** - Handles 2FA, OTP, magic links, and CAPTCHAs -- **Credential security** - Credentials are encrypted and submitted programmatically—never stored in plaintext or shared with LLMs +- **Credential security** - Credentials are encrypted at rest and submitted programmatically—never stored in plaintext - **Session persistence** - Saves authenticated browser profiles for reuse - **Automatic session monitoring** - Hourly checks detect when re-authentication is needed @@ -81,7 +81,7 @@ A **Profile** stores browser session state (cookies, localStorage) that persists ### Credentials -**Credentials** are securely stored login credentials that enable automated re-authentication when sessions expire. Values are encrypted at rest and never exposed to LLMs or API responses. See [Credentials](/agent-auth/credentials) for more. +**Credentials** are securely stored login credentials that enable automated re-authentication when sessions expire. Values are encrypted at rest and never exposed in API responses. See [Credentials](/agent-auth/credentials) for more. ## Choose Your Integration @@ -110,7 +110,7 @@ Agent Auth is designed with security as a first principle: | Security Feature | Description | |-----------------|-------------| | **Credentials never stored in plaintext** | All credential values are encrypted at rest using per-organization encryption keys | -| **Credentials never shared with LLMs** | Credentials are submitted programmatically to target sites, never passed through AI models | +| **Credentials never exposed** | Credentials are submitted programmatically to target sites, never returned in API responses | | **Profile encryption** | Browser profiles are encrypted end-to-end | | **Isolated execution** | Each auth flow runs in an isolated browser environment | diff --git a/agent-auth/programmatic.mdx b/agent-auth/programmatic.mdx index ca0a9dd..e1bdb33 100644 --- a/agent-auth/programmatic.mdx +++ b/agent-auth/programmatic.mdx @@ -482,7 +482,7 @@ try { ## Security Considerations - Credentials submitted via `submit()` are sent directly to the target site -- Credentials are never logged, stored in plaintext, or passed to LLMs +- Credentials are never logged or stored in plaintext - The browser session is isolated and destroyed after the invocation completes ## Next Steps From cd44ae2f8c178267cff6b490fabd7ac17cbc46e8 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Mon, 22 Dec 2025 11:52:15 -0500 Subject: [PATCH 08/12] docs(session-monitoring): update diagrams and tips --- agent-auth/session-monitoring.mdx | 72 ++++++++++++++++--------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/agent-auth/session-monitoring.mdx b/agent-auth/session-monitoring.mdx index 009e93b..026a381 100644 --- a/agent-auth/session-monitoring.mdx +++ b/agent-auth/session-monitoring.mdx @@ -14,29 +14,19 @@ Auth Agents with `AUTHENTICATED` status are automatically checked hourly to veri 3. Determines if the user is still logged in 4. Updates the Auth Agent status if the session has expired -``` -┌─────────────────────┐ -│ Auth Agent │ -│ status: AUTH │ -└─────────┬───────────┘ - │ hourly check - ▼ -┌─────────────────────┐ -│ Check Session │ -│ - Load profile │ -│ - Navigate to site│ -│ - Detect login │ -└─────────┬───────────┘ - │ - ┌─────┴─────┐ - ▼ ▼ - Still Session - logged expired - in - │ │ - ▼ ▼ - No change Update status - → NEEDS_AUTH +```mermaid +flowchart TD + A[Auth Agent
status: AUTHENTICATED] --> B[Hourly Check] + B --> C[Check Session] + C --> D{Still logged in?} + D -->|Yes| E[No change] + D -->|No| F[Update status
→ NEEDS_AUTH] + + subgraph Check Session + C1[Load profile] + C2[Navigate to site] + C3[Detect login state] + end ``` ## Detecting Expired Sessions @@ -96,6 +86,10 @@ async def check_and_refresh_auth(agent_id: str): When a session expires, you have two options: + +Check `agent.can_reauth` to determine which option is available. If `true`, automated re-auth is possible. If `false`, you'll need to redirect the user to complete login manually. + + ### Option 1: Automated Re-auth (Requires Credentials) If the Auth Agent has linked credentials and saved selectors, trigger automated re-auth: @@ -374,21 +368,29 @@ try { ## Best Practices -1. **Check before automation** - Always verify auth status before starting long-running automations - -2. **Enable auto-reauth** - Store credentials to enable automated re-authentication - -3. **Handle failures gracefully** - Have a fallback to manual login when auto-reauth fails - -4. **Monitor auth status** - Periodically check auth agents to proactively handle expirations - -5. **Log auth events** - Track when re-auth happens for debugging and auditing + + + Always verify auth status before starting long-running automations. This prevents failures mid-workflow and allows you to handle re-authentication before it becomes blocking. + + + Store credentials to enable automated re-authentication. When sessions expire, the system can automatically log back in without user intervention. + + + Have a fallback to manual login when auto-reauth fails. Some sites may require CAPTCHA or additional verification that can't be automated. + + + Periodically check auth agents to proactively handle expirations. Don't wait for automations to fail—check status and refresh sessions ahead of time. + + + Track when re-auth happens for debugging and auditing. This helps identify patterns like which sites expire frequently or which users need attention. + + ## Session Check Frequency -- Auth checks run approximately **every hour** for authenticated agents -- Checks are passive and don't modify the saved profile -- The `last_auth_check_at` field shows when the last check occurred + +Auth checks run approximately **every hour** for authenticated agents. Checks are passive and don't modify the saved profile. Use the `last_auth_check_at` field to see when the last check occurred. + ## Next Steps From 61c49d5ddcc32c553019c5aad785de5033d86740 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Mon, 22 Dec 2025 13:55:39 -0500 Subject: [PATCH 09/12] docs(credentials): document TOTP secret for 2FA --- agent-auth/credentials.mdx | 60 ++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/agent-auth/credentials.mdx b/agent-auth/credentials.mdx index 3a34c56..b35b4c7 100644 --- a/agent-auth/credentials.mdx +++ b/agent-auth/credentials.mdx @@ -96,6 +96,7 @@ print(f"Credential created: {credential.id}") | `name` | Yes | Unique name for the credential within your organization | | `domain` | Yes | Target domain this credential is for (e.g., `netflix.com`) | | `values` | Yes | Object containing field name → value pairs | +| `totp_secret` | No | Base32-encoded TOTP secret for automatic 2FA code generation | Credential values are write-only. Once stored, they cannot be retrieved via the API. Only metadata (name, domain, created_at) is returned. @@ -245,20 +246,52 @@ values: { } ``` -### With 2FA/TOTP Code +### With TOTP-based 2FA -For sites requiring 2FA, include the `code` field with a TOTP generator: +For sites that use authenticator apps (Google Authenticator, Authy, 1Password, etc.) for two-factor authentication, you can provide the TOTP secret to enable fully automated login—including the 2FA step. ```typescript -values: { - email: 'user@example.com', - password: 'secretpassword', - code: '123456', // Generate with a TOTP library -} +const credential = await kernel.credentials.create({ + name: 'my-login', + domain: 'example.com', + values: { + email: 'user@example.com', + password: 'secretpassword', + }, + totp_secret: 'JBSWY3DPEHPK3PXP', // Base32-encoded TOTP secret +}); ``` +When the Auth Agent detects a 2FA code field during login, it automatically generates the current 6-digit code using the provided secret—no manual intervention required. + +#### What is a TOTP Secret? + +A TOTP (Time-based One-Time Password) secret is a base32-encoded key shared between you and the target site. It's the same secret your authenticator app uses to generate 6-digit codes that change every 30 seconds. + +#### How to Obtain Your TOTP Secret + +1. Go to the target site's security or 2FA settings +2. Choose "Set up authenticator app" (or re-setup if already configured) +3. Look for a link like "Can't scan the code?" or "Enter manually" +4. Copy the text secret shown (e.g., `JBSW Y3DP EHPK 3PXP`) +5. Remove any spaces and use it as the `totp_secret` + + +Store your TOTP secret securely. If you only configure the authenticator app by scanning the QR code, you may not have access to the text secret later. + + +#### Supported 2FA Types + +| Type | Supported | Notes | +|------|-----------|-------| +| **TOTP (Authenticator apps)** | ✅ Yes | Google Authenticator, Authy, 1Password, etc. | +| **SMS OTP** | ❌ No | Requires receiving a text message | +| **Email OTP** | ❌ No | Requires checking email | +| **Push notifications** | ❌ No | Requires mobile app approval | +| **Hardware keys (FIDO/U2F)** | ❌ No | Requires physical device | + -When using [Auto-Login](/agent-auth/auto-login) with 2FA, the system will automatically use the `code` value when a 2FA field is detected. Use a TOTP generator library to provide the current code. +If `totp_secret` is not configured and the site requires 2FA, you'll be prompted to enter the code manually via the Hosted UI. ### Multiple Fields @@ -294,7 +327,7 @@ import Kernel from '@onkernel/sdk'; const kernel = new Kernel(); async function setupAutomatedAuth() { - // 1. Create credential + // 1. Create credential (with optional TOTP secret for 2FA sites) const credential = await kernel.credentials.create({ name: 'acme-portal-login', domain: 'portal.acme.com', @@ -302,6 +335,8 @@ async function setupAutomatedAuth() { email: 'agent@company.com', password: 'secure-password-123', }, + // Optional: provide TOTP secret for fully automated 2FA + totp_secret: 'JBSWY3DPEHPK3PXP', }); // 2. Create auth agent with credential @@ -356,11 +391,12 @@ Credentials are designed with security as the top priority: | Security Feature | Description | |-----------------|-------------| -| **Encryption at rest** | All credential values are encrypted using per-organization keys | -| **Write-only values** | Values cannot be retrieved via the API after creation | -| **Never logged** | Credential values are never written to logs | +| **Encryption at rest** | All credential values and TOTP secrets are encrypted using per-organization keys | +| **Write-only values** | Values and TOTP secrets cannot be retrieved via the API after creation | +| **Never logged** | Credential values and TOTP secrets are never written to logs | | **Never exposed** | Values are injected directly into form fields, never returned in API responses | | **Isolated execution** | Authentication runs in an isolated browser environment | +| **TOTP status only** | API only returns `has_totp_secret` (boolean), never the secret itself | ## Best Practices From 0f0944fb9b69dc7f34d03e13b9e4feb2567f35b4 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Mon, 22 Dec 2025 15:38:02 -0500 Subject: [PATCH 10/12] docs(credentials): Add 2FA setup instructions with TOTP --- agent-auth/credentials.mdx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/agent-auth/credentials.mdx b/agent-auth/credentials.mdx index b35b4c7..4c8d90f 100644 --- a/agent-auth/credentials.mdx +++ b/agent-auth/credentials.mdx @@ -276,6 +276,33 @@ A TOTP (Time-based One-Time Password) secret is a base32-encoded key shared betw 4. Copy the text secret shown (e.g., `JBSW Y3DP EHPK 3PXP`) 5. Remove any spaces and use it as the `totp_secret` +#### Completing 2FA Setup with Kernel + +When setting up 2FA on a site, you typically need to enter a verification code to confirm the setup. When you store your TOTP secret in Kernel, we return the current code so you can complete setup immediately: + +```typescript +// Create credential with TOTP secret +const credential = await kernel.credentials.create({ + name: 'my-login', + domain: 'example.com', + values: { email: 'user@example.com', password: 'secret' }, + totp_secret: 'JBSWY3DPEHPK3PXP', +}); + +// Use the returned code to complete 2FA setup on the site +console.log('Enter this code:', credential.totp_code); // e.g., "847291" +console.log('Expires at:', credential.totp_code_expires_at); // ISO 8601 timestamp +``` + +If you need a fresh code later (e.g., for manual login or the first code expired), use the TOTP code endpoint: + +```typescript +const { code, expires_at } = await kernel.credentials.totpCode(credential.id); +console.log('Current code:', code); // Fresh 6-digit code +``` + +This means you can store your TOTP secrets **only in Kernel**—no need to also add them to a personal authenticator app. + Store your TOTP secret securely. If you only configure the authenticator app by scanning the QR code, you may not have access to the text secret later. @@ -397,6 +424,7 @@ Credentials are designed with security as the top priority: | **Never exposed** | Values are injected directly into form fields, never returned in API responses | | **Isolated execution** | Authentication runs in an isolated browser environment | | **TOTP status only** | API only returns `has_totp_secret` (boolean), never the secret itself | +| **Ephemeral TOTP codes** | Generated codes expire every 30 seconds and are only returned when needed | ## Best Practices From 1de9365a12866bfd067f0dbb371252fb74a6e34a Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Mon, 22 Dec 2025 19:31:11 -0500 Subject: [PATCH 11/12] docs: Update agent auth docs structure --- docs.json | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs.json b/docs.json index 9a62632..df13dbe 100644 --- a/docs.json +++ b/docs.json @@ -72,17 +72,6 @@ "browsers/standby", "browsers/profiles", "browsers/termination", - { - "group": "Agent Auth", - "pages": [ - "agent-auth/overview", - "agent-auth/auto-login", - "agent-auth/hosted-ui", - "agent-auth/programmatic", - "agent-auth/credentials", - "agent-auth/session-monitoring" - ] - }, { "group": "Bot Anti-Detection", "pages": [ @@ -109,6 +98,22 @@ "browsers/faq" ] }, + { + "group": "Agents", + "pages": [ + { + "group": "Auth", + "pages": [ + "agents/auth/overview", + "agents/auth/auto-login", + "agents/auth/hosted-ui", + "agents/auth/programmatic", + "agents/auth/credentials", + "agents/auth/session-monitoring" + ] + } + ] + }, { "group": "App Platform", "pages": [ From 313b7abc1ced161db0a332d202ba8a81abe8f721 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Tue, 23 Dec 2025 12:50:46 -0500 Subject: [PATCH 12/12] docs: Rewrite and condense Agent Auth overview --- agent-auth/overview.mdx | 189 +++++++++------------------------------- 1 file changed, 43 insertions(+), 146 deletions(-) diff --git a/agent-auth/overview.mdx b/agent-auth/overview.mdx index 7efd711..57f9103 100644 --- a/agent-auth/overview.mdx +++ b/agent-auth/overview.mdx @@ -3,86 +3,62 @@ title: "Overview" description: "Secure authentication system - log users into any website and save authenticated browser sessions" --- -Agent Auth is an authentication system that helps you log users into any website and save their authenticated browser session (profile) for future automations. It automatically discovers login forms, maps credential inputs, and handles multi-step authentication flows like 2FA/OTP. - -## Why Agent Auth? - -The most valuable workflows live behind logins. To perform tasks on behalf of real users, your applications need a secure way to access authenticated sessions. - -Agent Auth solves this by providing: - -- **Unified access to logged-in websites** - One API to log into any site with user consent -- **Automatic form discovery** - Detects login form fields on any website -- **Multi-step auth handling** - Handles 2FA, OTP, magic links, and CAPTCHAs -- **Credential security** - Credentials are encrypted at rest and submitted programmatically—never stored in plaintext -- **Session persistence** - Saves authenticated browser profiles for reuse -- **Automatic session monitoring** - Hourly checks detect when re-authentication is needed +Agent Auth logs users into any website and saves their authenticated browser session for future automations—handling form discovery, 2FA/OTP, and session persistence automatically. ## How It Works + An **Auth Agent** manages authentication for a specific domain + profile combination. It tracks the target site, login URL, and auth status. Agents are idempotent—creating one for an existing domain/profile returns the existing agent. + ```typescript const agent = await kernel.agents.auth.create({ - domain: 'example.com', - profile_name: 'my-profile', + domain: 'netflix.com', + profile_name: 'netflix-user-123', }); ``` - + + An **Invocation** is a single authentication attempt. Each invocation returns a `hosted_url` where users complete login, or you can use the [programmatic API](/agent-auth/programmatic) to build your own UI. + ```typescript const invocation = await kernel.agents.auth.invocations.create({ auth_agent_id: agent.id, }); - // Returns hosted_url for user login, or use programmatic API + + // If already logged in, skip the flow + if (invocation.status === 'ALREADY_AUTHENTICATED') { + console.log('Session still valid!'); + } else { + // Redirect user to complete login + console.log('Login URL:', invocation.hosted_url); + } ``` + + Invocation statuses: `IN_PROGRESS` → `SUCCESS` | `EXPIRED` | `CANCELED` - Redirect the user to `invocation.hosted_url` where they enter credentials and handle 2FA. The profile is saved on success. + {/* TODO: Add image showing the hosted UI login flow */} + + The user enters credentials, handles 2FA/OTP if needed, and the authenticated session is saved to a **Profile**—encrypted browser state (cookies, localStorage) that persists across sessions. + + For fully automated flows, link [Credentials](/agent-auth/credentials) to enable re-authentication without user intervention. + Launch a browser with the saved profile—it's already logged in. Profiles work with any Kernel browser operation. + ```typescript const browser = await kernel.browsers.create({ - profile: { name: 'my-profile' }, + profile: { name: 'netflix-user-123' }, stealth: true, }); - // Browser is already logged in! + // Browser is logged into Netflix! ``` + + See [Profiles](/browsers/profiles) to learn more about session persistence. -## Key Concepts - -### Auth Agent - -An **Auth Agent** is a persistent entity that manages authentication for a specific domain + profile combination. It stores: - -- The target domain (e.g., `netflix.com`) -- The linked browser profile -- The login URL (learned during discovery) -- Current authentication status (`AUTHENTICATED` or `NEEDS_AUTH`) -- Optional linked credentials for automated re-auth - -Auth Agents are **idempotent**—if an agent already exists for the domain and profile, `kernel.agents.auth.create()` returns the existing one. - -### Invocation - -An **Invocation** is a single authentication attempt. You start one by calling `kernel.agents.auth.invocations.create()`. Over time, an Auth Agent accumulates multiple invocations as sessions need to be established or refreshed. - -**Invocation statuses:** -- `IN_PROGRESS` - User is completing the auth flow -- `SUCCESS` - Authentication completed, profile saved -- `EXPIRED` - Invocation timed out (5 minutes) -- `CANCELED` - Invocation was explicitly canceled - -### Profile - -A **Profile** stores browser session state (cookies, localStorage) that persists across browser sessions. Once authenticated via Agent Auth, use the profile with `kernel.browsers.create()` to get an already-logged-in browser. See [Profiles](/browsers/profiles) for more. - -### Credentials - -**Credentials** are securely stored login credentials that enable automated re-authentication when sessions expire. Values are encrypted at rest and never exposed in API responses. See [Credentials](/agent-auth/credentials) for more. - ## Choose Your Integration @@ -103,99 +79,20 @@ A **Profile** stores browser session state (cookies, localStorage) that persists -## Security +## Why Agent Auth? -Agent Auth is designed with security as a first principle: +The most valuable workflows live behind logins. Agent Auth provides: -| Security Feature | Description | -|-----------------|-------------| -| **Credentials never stored in plaintext** | All credential values are encrypted at rest using per-organization encryption keys | -| **Credentials never exposed** | Credentials are submitted programmatically to target sites, never returned in API responses | -| **Profile encryption** | Browser profiles are encrypted end-to-end | -| **Isolated execution** | Each auth flow runs in an isolated browser environment | +- **One API for any login flow** - Automatic form discovery works on any website +- **2FA/OTP handling** - Supports TOTP, magic links, SMS codes, and CAPTCHAs +- **Session monitoring** - Hourly checks detect when re-authentication is needed +- **Security-first design** - Credentials encrypted at rest, never exposed in API responses -## Quick Example - -Here's a complete example using the Hosted UI flow: - - -```typescript TypeScript -import Kernel from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// 1. Create or get existing Auth Agent -const agent = await kernel.agents.auth.create({ - domain: 'netflix.com', - profile_name: 'netflix-user-123', -}); - -// 2. Start the auth flow -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, -}); - -// 3. Check if already logged in -if (invocation.status === 'ALREADY_AUTHENTICATED') { - console.log('Already authenticated!'); -} else { - // Redirect user to hosted UI - console.log('Redirect to:', invocation.hosted_url); -} - -// 4. Poll for completion -let status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); -while (status.status === 'IN_PROGRESS') { - await new Promise(r => setTimeout(r, 2000)); - status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); -} - -if (status.status === 'SUCCESS') { - // 5. Use the authenticated profile - const browser = await kernel.browsers.create({ - profile: { name: 'netflix-user-123' }, - stealth: true, - }); - console.log('Browser ready:', browser.cdp_ws_url); -} -``` - -```python Python -from kernel import Kernel -import asyncio - -kernel = Kernel() - -# 1. Create or get existing Auth Agent -agent = await kernel.agents.auth.create( - domain="netflix.com", - profile_name="netflix-user-123", -) - -# 2. Start the auth flow -invocation = await kernel.agents.auth.invocations.create( - auth_agent_id=agent.id, -) - -# 3. Check if already logged in -if invocation.status == "ALREADY_AUTHENTICATED": - print("Already authenticated!") -else: - # Redirect user to hosted UI - print(f"Redirect to: {invocation.hosted_url}") - -# 4. Poll for completion -status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) -while status.status == "IN_PROGRESS": - await asyncio.sleep(2) - status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) - -if status.status == "SUCCESS": - # 5. Use the authenticated profile - browser = await kernel.browsers.create( - profile={"name": "netflix-user-123"}, - stealth=True, - ) - print(f"Browser ready: {browser.cdp_ws_url}") -``` - +## Security + +| Feature | Description | +|---------|-------------| +| **Encrypted credentials** | Values encrypted at rest with per-organization keys, never stored in plaintext | +| **No credential exposure** | Credentials submitted programmatically to target sites, never returned in responses | +| **Encrypted profiles** | Browser session state encrypted end-to-end | +| **Isolated execution** | Each auth flow runs in an isolated browser environment |