diff --git a/agent-auth/auto-login.mdx b/agent-auth/auto-login.mdx new file mode 100644 index 0000000..260100d --- /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({ + 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({ + 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( + 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({ + 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( + 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 new file mode 100644 index 0000000..4c8d90f --- /dev/null +++ b/agent-auth/credentials.mdx @@ -0,0 +1,448 @@ +--- +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 + +## How Credentials Work + + + + ```typescript + const credential = await kernel.credentials.create({ + name: 'my-login', + domain: 'example.com', + values: { email: 'user@example.com', password: 'secret' }, + }); + ``` + + + ```typescript + const agent = await kernel.agents.auth.create({ + domain: 'example.com', + profile_name: 'my-profile', + credential_name: 'my-login', + }); + ``` + + + Run one successful authentication to save form selectors (via [Auto-Login](/agent-auth/auto-login) or [Hosted UI](/agent-auth/hosted-ui)). + + + ```typescript + // When session expires, trigger re-auth + const reauth = await kernel.agents.auth.reauth(agent.id); + ``` + + + +## 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 | +| `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. + + +## 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({ + 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; +} +``` + +Poll for completion, then use [Session Monitoring](/agent-auth/session-monitoring) to detect future expirations. + +## 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', +} +``` + +### With TOTP-based 2FA + +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 +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` + +#### 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. + + +#### 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 | + + +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 + +Some sites have additional fields (company ID, account number, etc.): + +```typescript +values: { + company_id: 'ACME123', + username: 'jsmith', + password: 'secretpassword', +} +``` + +### 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: + +```typescript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +async function setupAutomatedAuth() { + // 1. Create credential (with optional TOTP secret for 2FA sites) + const credential = await kernel.credentials.create({ + name: 'acme-portal-login', + domain: 'portal.acme.com', + values: { + 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 + const agent = await kernel.agents.auth.create({ + 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 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 | +| **Ephemeral TOTP codes** | Generated codes expire every 30 seconds and are only returned when needed | + +## 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 + + + + Fully automated login with pre-linked credentials + + + Automatic session health checks and re-auth + + diff --git a/agent-auth/early-preview.mdx b/agent-auth/early-preview.mdx new file mode 100644 index 0000000..502326b --- /dev/null +++ b/agent-auth/early-preview.mdx @@ -0,0 +1,62 @@ +--- +title: "Early Preview" +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:** + +```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" +] +``` + +## Support + +Questions or issues? Reach out to us on Slack! diff --git a/agent-auth/hosted-ui.mdx b/agent-auth/hosted-ui.mdx new file mode 100644 index 0000000..ebe4383 --- /dev/null +++ b/agent-auth/hosted-ui.mdx @@ -0,0 +1,391 @@ +--- +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 + +## How It Works + + + + ```typescript + const agent = await kernel.agents.auth.create({ + 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 + + +```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({ + 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( + 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({ + 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 | +|-----------|----------|-------------| +| `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 | +| `expires_at` | When the invocation expires (5 minutes) | +| `status` | If `ALREADY_AUTHENTICATED`, profile is already logged in—no redirect needed | + + +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 + +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({ + 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({ + 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..57f9103 --- /dev/null +++ b/agent-auth/overview.mdx @@ -0,0 +1,98 @@ +--- +title: "Overview" +description: "Secure authentication system - log users into any website and save authenticated browser sessions" +--- + +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: '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, + }); + + // 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` + + + {/* 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: 'netflix-user-123' }, + stealth: true, + }); + // Browser is logged into Netflix! + ``` + + See [Profiles](/browsers/profiles) to learn more about session persistence. + + + +## Choose Your Integration + + + + **Best for:** Fully automated flows + + Credentials are pre-linked. System handles everything automatically. + + + **Best for:** User-facing apps + + Redirect users to Kernel's hosted login page. + + + **Best for:** Custom UI or headless + + Build your own UI with discover/submit APIs. + + + +## Why Agent Auth? + +The most valuable workflows live behind logins. Agent Auth provides: + +- **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 + +## 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 | diff --git a/agent-auth/programmatic.mdx b/agent-auth/programmatic.mdx new file mode 100644 index 0000000..e1bdb33 --- /dev/null +++ b/agent-auth/programmatic.mdx @@ -0,0 +1,497 @@ +--- +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 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 + + + + ```typescript + const agent = await kernel.agents.auth.create({ + domain: 'example.com', + profile_name: 'my-profile', + }); + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); + ``` + + + ```typescript + const discover = await kernel.agents.auth.invocations.discover( + invocation.invocation_id, + {} + ); + // discover.fields contains the form fields + ``` + + + ```typescript + const submit = await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: { email: 'user@example.com', password: 'secret' } } + ); + ``` + + + ```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' } } + ); + } + ``` + + + +## 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({ + 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!'); +} else { + // Step 2: Discover login fields + const discoverResponse = await kernel.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 3: Map credentials to discovered fields + const fieldValues = mapCredentialsToFields(discoverResponse.fields, { + email: 'user@example.com', + password: 'secretpassword', + }); + + // Step 4: Submit credentials + let submitResponse = await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } + ); + + // Step 5: 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 kernel.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( + 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: Discover login fields + discover_response = await 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 3: Map credentials to discovered fields + field_values = map_credentials_to_fields( + discover_response.fields, + {"email": "user@example.com", "password": "secretpassword"}, + ) + + # Step 4: Submit credentials + submit_response = await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + field_values=field_values, + ) + + # Step 5: 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 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. Discover Login Fields + +Call `discover()` to navigate to the login page and extract form fields: + +```typescript +const discoverResponse = await kernel.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. + + +### 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: + +```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 | + +### 3. Submit Credentials + +Submit the mapped field values to attempt login: + +```typescript +const submitResponse = await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } +); +``` + +**Possible submit responses:** + +**Success:** +```json +{ + "success": true, + "logged_in": true, + "app_name": "My App", + "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" +} +``` + +### 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: + +```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 kernel.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 kernel.agents.auth.invocations.discover( + invocation.invocation_id, + {} + ); + + if (!discoverResponse.success) { + console.error('Discovery failed:', discoverResponse.error_message); + return; + } + + const submitResponse = await kernel.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 === 404) { + console.error('Invocation not found or expired'); + } else { + console.error('Unexpected error:', error.message); + } +} +``` + +## Security Considerations + +- Credentials submitted via `submit()` are sent directly to the target site +- Credentials are never logged or stored in plaintext +- 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..026a381 --- /dev/null +++ b/agent-auth/session-monitoring.mdx @@ -0,0 +1,404 @@ +--- +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 + +```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 + +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: + + +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: + +```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 + + + + 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. Use the `last_auth_check_at` field to see 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..df13dbe 100644 --- a/docs.json +++ b/docs.json @@ -98,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": [ @@ -182,7 +198,8 @@ "reference/cli/create", "reference/cli/auth", "reference/cli/browsers", - "reference/cli/apps" + "reference/cli/apps", + "reference/cli/mcp" ] }, {