diff --git a/agents/auth/auto-login.mdx b/agents/auth/auto-login.mdx new file mode 100644 index 0000000..452b674 --- /dev/null +++ b/agents/auth/auto-login.mdx @@ -0,0 +1,340 @@ +--- +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](/agents/auth/hosted-ui) or [Programmatic](/agents/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 state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + // Poll until state.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, +}); + +// Step 4: Poll for completion +const result = await pollForCompletion(invocation.invocation_id); + +// If already authenticated, status will immediately be SUCCESS +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, +) + +# Step 4: Poll for completion +result = await poll_for_completion(invocation.invocation_id) + +# If already authenticated, status will immediately be SUCCESS +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 state = await kernel.agents.auth.invocations.retrieve(invocationId); + + console.log(`Status: ${state.status}, Step: ${state.step}`); + + if (state.status === 'SUCCESS') { + return { success: true, state }; + } + if (state.status === 'FAILED') { + return { success: false, reason: 'FAILED', error: state.error_message }; + } + if (state.status === 'EXPIRED' || state.status === 'CANCELED') { + return { success: false, reason: state.status }; + } + + // Check if manual input needed (e.g., MFA code not in credentials) + if (state.step === 'awaiting_input') { + return { success: false, reason: 'MANUAL_INPUT_REQUIRED', state }; + } + + 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: + state = await kernel.agents.auth.invocations.retrieve(invocation_id) + + print(f"Status: {state.status}, Step: {state.step}") + + if state.status == "SUCCESS": + return {"success": True, "state": state} + if state.status == "FAILED": + return {"success": False, "reason": "FAILED", "error": state.error_message} + if state.status in ("EXPIRED", "CANCELED"): + return {"success": False, "reason": state.status} + + # Check if manual input needed (e.g., MFA code not in credentials) + if state.step == "awaiting_input": + return {"success": False, "reason": "MANUAL_INPUT_REQUIRED", "state": state} + + 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 | +| `expired` | Invocation expired before completion | + +## 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', + phone: '+1234567890', // Optional, for sites that need it + }, + totp_secret: 'JBSWY3DPEHPK3PXP', // Optional, for 2FA sites +} +``` + +## 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 satisfy from your credentials (like a TOTP field without a `totp_secret`), it sets `step: 'awaiting_input'` and pauses. + +### Handling Paused Flows + +When auto-login pauses, check `pending_fields` and submit the missing values: + + +```typescript TypeScript +while (state.status === 'IN_PROGRESS') { + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + + if (state.step === 'awaiting_input' && state.pending_fields?.length) { + // Build values for the pending fields + const fieldValues: Record = {}; + for (const field of state.pending_fields) { + fieldValues[field.name] = await getValueFromUser(field); + } + + await kernel.agents.auth.invocations.submit(invocation.invocation_id, { + field_values: fieldValues, + }); + } + + await sleep(2000); +} +``` + +```python Python +while state.status == "IN_PROGRESS": + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + + if state.step == "awaiting_input" and len(state.pending_fields or []): + # Build values for the pending fields + field_values = {} + for field in state.pending_fields: + field_values[field.name] = await get_value_from_user(field) + + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + field_values=field_values, + ) + + await asyncio.sleep(2) +``` + + + +**2FA handling:** For TOTP-based 2FA, include a `totp_secret` on your credential to enable fully automated code generation. For SMS or email OTP, the flow will pause at `step: 'awaiting_input'` with the OTP field in `pending_fields`, allowing the user to enter the code via your UI. + + +## Using Proxies + +If the target site requires a specific IP or region, configure a proxy on the Auth Agent using `proxy: { proxy_id: 'your_proxy_id' }`. Use the same proxy when creating browsers afterward. + +See [Proxies](/proxies/overview) for setup details. + + +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/agents/auth/credentials.mdx b/agents/auth/credentials.mdx new file mode 100644 index 0000000..e6dbdd1 --- /dev/null +++ b/agents/auth/credentials.mdx @@ -0,0 +1,440 @@ +--- +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 +- **Encrypted at rest** - Values encrypted with per-organization keys, never stored in plaintext +- **Never exposed** - Submitted programmatically to target sites, never passed to LLMs or 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' }, + totp_secret: 'JBSWY3DPEHPK3PXP', // Optional: for sites with authenticator-based 2FA + }); + ``` + + + ```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](/agents/auth/auto-login) or [Hosted UI](/agents/auth/hosted-ui)). + + + With complete credentials, re-authentication happens automatically when sessions expire—no code required. + + + +## 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 (id, name, domain, created_at, updated_at, has_totp_secret) 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_name: credential.name, // 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 (or updates if it already exists) +- Links it to the Auth Agent +- Saves the form selectors for future re-auth + +#### Updating Existing Credentials + +If you use `save_credential_as` with the name of an existing credential, the submitted values are **merged** into that credential: + +```typescript +// 1. Create credential with just the identifier +const credential = await kernel.credentials.create({ + name: 'my-login', + domain: 'example.com', + values: { email: 'user@example.com' }, // No password yet +}); + +// 2. Create agent with the credential linked +const agent = await kernel.agents.auth.create({ + domain: 'example.com', + profile_name: 'my-profile', + credential_name: 'my-login', +}); + +// 3. During login, user provides the password manually +// Using save_credential_as with the SAME name updates the credential +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'my-login', // Same name as linked credential +}); + +// After successful login: +// - The credential now has BOTH email AND password +// - Future re-auth can use both values automatically +``` + +This is useful when: +- You have partial credentials (e.g., identifier only) and want to capture the password during login +- Users change their password and you want to save the new one +- You want to progressively build up credential values across multiple logins + + +TOTP codes are never saved to credentials—they're one-time codes generated from the TOTP secret. + + +## Using Stored Credentials + +Once credentials are linked and an initial login has completed (capturing form selectors), the Auth Agent can re-authenticate automatically when sessions expire—no code required. + +An Auth Agent can auto re-auth (`can_reauth: true`) 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) + +See [Session Monitoring](/agents/auth/session-monitoring) for details on how automatic re-authentication works. + +## 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 +// Retrieve by ID or name +const credential = await kernel.credentials.retrieve('my-netflix-login'); + +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 +// Update by ID or name +await kernel.credentials.update('my-netflix-login', { + name: 'updated-name', // Optional: update name + values: { // Optional: update values + email: 'newemail@example.com', + password: 'newpassword', + }, +}); +``` + +### Delete Credentials + +```typescript +// Delete by ID or name +await kernel.credentials.delete('my-netflix-login'); +``` + + +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 +// Get TOTP code by ID or name +const { code, expires_at } = await kernel.credentials.totpCode('my-login'); +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 | Auto-Login | User-Facing | Notes | +|------|------------|-------------|-------| +| **TOTP (Authenticator apps)** | ✅ | ✅ | Automated with `totp_secret`, or enter manually | +| **SMS OTP** | ❌ | ✅ | User receives text and enters code via UI | +| **Email OTP** | ❌ | ✅ | User receives email and enters code via UI | +| **Push notifications** | ❌ | ❌ | Not yet supported | +| **Hardware keys (FIDO/U2F)** | ❌ | ❌ | Requires physical connection to browser machine | + +- **Auto-Login**: Fully automated with stored credentials, no user present +- **User-Facing**: User enters values via [Hosted UI](/agents/auth/hosted-ui) or your [custom UI](/agents/auth/programmatic) + + +For SMS and email OTP, the field appears as a pending field. The user receives the code on their device, then enters it through your UI—no special configuration needed. + + +### 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(); + +// 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', + }, + totp_secret: 'JBSWY3DPEHPK3PXP', // Optional: for fully automated 2FA +}); + +// 2. Create auth agent with credential linked +const agent = await kernel.agents.auth.create({ + domain: 'portal.acme.com', + profile_name: 'acme-agent-profile', + credential_name: credential.name, + login_url: 'https://portal.acme.com/login', +}); + +// 3. Complete initial login to capture form selectors +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); + +// Poll for completion or redirect user to hosted_url +// After success, agent.can_reauth will be true + +// 4. Use the authenticated profile +const browser = await kernel.browsers.create({ + profile: { name: 'acme-agent-profile' }, + stealth: true, +}); + +// Browser is logged in! Run your automation... +``` + +Once set up, sessions are automatically refreshed when they expire. See [Session Monitoring](/agents/auth/session-monitoring) for details. + +## 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 | +| **No LLM exposure** | Credentials submitted programmatically to target sites, never passed to LLMs | +| **Isolated execution** | Each auth flow 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](/agents/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 a user-facing flow + +## Next Steps + + + + Fully automated login with pre-linked credentials + + + Automatic session health checks and re-auth + + diff --git a/agents/auth/early-preview.mdx b/agents/auth/early-preview.mdx new file mode 100644 index 0000000..129dcf6 --- /dev/null +++ b/agents/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/5cc0e0e48c515f5c6c88c8040442e095795b931f/dist.tar.gz" + } +} +``` + +**Python (requirements.txt):** + +``` +kernel @ https://pkg.stainless.com/s/kernel-python/0b2f621cd651357f8659a85eea24ac1e85d86fae/kernel-0.24.0-py3-none-any.whl +``` + +Or in pyproject.toml: + +```toml +[project] +dependencies = [ + "kernel @ https://pkg.stainless.com/s/kernel-python/0b2f621cd651357f8659a85eea24ac1e85d86fae/kernel-0.24.0-py3-none-any.whl" +] +``` + +## Support + +Questions or issues? Reach out to us on Slack! diff --git a/agents/auth/hosted-ui.mdx b/agents/auth/hosted-ui.mdx new file mode 100644 index 0000000..e5c3ea0 --- /dev/null +++ b/agents/auth/hosted-ui.mdx @@ -0,0 +1,395 @@ +--- +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 + const state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + if (state.status !== 'SUCCESS') { + 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 +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +if (state.status === 'SUCCESS') { + 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 === 'FAILED') { + return { success: false, reason: 'FAILED', error: status.error_message }; + } + 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 +state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +if state.status == "SUCCESS": + 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 == "FAILED": + return {"success": False, "reason": "FAILED", "error": status.error_message} + 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) | + + +Poll the invocation immediately after creation. If `status` is already `SUCCESS`, the profile is already logged in—skip the redirect. + + +### 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 state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +while (state.status === 'IN_PROGRESS') { + console.log(`Step: ${state.step}`); // discovering, awaiting_input, submitting, etc. + await new Promise(r => setTimeout(r, 2000)); // Poll every 2 seconds + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +switch (state.status) { + case 'SUCCESS': + console.log('Authentication successful!'); + break; + case 'FAILED': + console.error('Login failed:', state.error_message); + break; + case 'EXPIRED': + console.error('Invocation expired - user did not complete login in time'); + break; + case 'CANCELED': + console.error('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, +}); + +// Execute Playwright code - you're already logged in! +const response = await kernel.browsers.playwright.execute( + browser.session_id, + { + code: ` + await page.goto('https://example.com/dashboard'); + return await page.title(); + ` + } +); + +console.log(response.result); // "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 on the Auth Agent using `proxy: { proxy_id: 'your_proxy_id' }`. Use the same proxy when creating browsers afterward. + +See [Proxies](/proxies/overview) for setup details. + + +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](/agents/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 +``` + +## 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/agents/auth/overview.mdx b/agents/auth/overview.mdx new file mode 100644 index 0000000..ddc2d59 --- /dev/null +++ b/agents/auth/overview.mdx @@ -0,0 +1,102 @@ +--- +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](/agents/auth/programmatic) to build your own UI. + + ```typescript + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); + + // Poll to check if already authenticated + const state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + + if (state.status === 'SUCCESS') { + console.log('Already authenticated!'); + } else { + // Redirect user to complete login + console.log('Login URL:', invocation.hosted_url); + } + ``` + + **Invocation states:** + - `status`: `IN_PROGRESS` → `SUCCESS` | `EXPIRED` | `CANCELED` | `FAILED` + - `step`: `initialized` → `discovering` → `awaiting_input` ⇄ `submitting` → `completed` + + + {/* 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](/agents/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 polling and 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** - TOTP automated; SMS/email OTP via Hosted UI or custom UI +- **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 passed to LLMs | +| **Encrypted profiles** | Browser session state encrypted end-to-end | +| **Isolated execution** | Each auth flow runs in an isolated browser environment | diff --git a/agents/auth/programmatic.mdx b/agents/auth/programmatic.mdx new file mode 100644 index 0000000..64308df --- /dev/null +++ b/agents/auth/programmatic.mdx @@ -0,0 +1,469 @@ +--- +title: "Programmatic Flow" +description: "Build custom authentication UIs with full control using polling 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 polling + 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 each step of the 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](/agents/auth/auto-login) is simpler—it handles everything automatically. Use Programmatic when you need custom control over each step. + + +## How It Works + +The programmatic flow uses a **polling-based state machine**. After creating an invocation, you poll for state changes and respond accordingly: + +``` +initialized → discovering → awaiting_input ⇄ submitting → completed +``` + + + + ```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 + let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + while (state.status === 'IN_PROGRESS' && state.step !== 'awaiting_input') { + await sleep(2000); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + } + // state.pending_fields contains the form fields + ``` + + + ```typescript + // Submit is async - returns immediately + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: { email: 'user@example.com', password: 'secret' } } + ); + ``` + + + ```typescript + // Poll until complete + while (state.status === 'IN_PROGRESS') { + await sleep(2000); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + + if (state.step === 'awaiting_input' && state.pending_fields?.length) { + // New fields needed (e.g., 2FA) - submit again + } + } + // state.status is now SUCCESS, FAILED, EXPIRED, or CANCELED + ``` + + + +## Invocation State Machine + +The `step` field tracks where you are in the authentication flow: + +| Step | Description | What to Do | +|------|-------------|------------| +| `initialized` | Invocation just created | Wait for discovery | +| `discovering` | Navigating to login page, finding fields | Poll and wait | +| `awaiting_input` | Fields discovered, waiting for credentials | Read `pending_fields`, call submit | +| `submitting` | Processing submitted credentials | Poll and wait | +| `completed` | Authentication finished | Check `status` for SUCCESS/FAILED | +| `expired` | Invocation expired before completion | Create a new invocation | + +The `pending_fields` array shows which fields are currently needed. The `submitted_fields` array shows which fields have been successfully submitted. + +## Complete Example + + +```typescript TypeScript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// Helper function +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + +// 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, +}); + +// Step 2: Poll until complete +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +while (state.status === 'IN_PROGRESS') { + console.log(`Step: ${state.step}`); + + // Handle awaiting_input - submit credentials + if (state.step === 'awaiting_input' && state.pending_fields?.length) { + console.log('Fields needed:', state.pending_fields.map(f => f.name)); + console.log('Already submitted:', state.submitted_fields || []); + + // Map credentials to the pending fields + const fieldValues = mapCredentialsToFields(state.pending_fields, { + email: 'user@example.com', + password: 'secretpassword', + code: await promptUserForOTP(), // For 2FA fields + }); + + // Submit is async - returns immediately + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } + ); + + console.log('Submitted, waiting for result...'); + } + + // Poll every 2 seconds + await sleep(2000); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +// Check final result +if (state.status === 'SUCCESS') { + console.log('Authentication successful!'); + console.log('Submitted fields:', state.submitted_fields); +} else if (state.status === 'FAILED') { + console.error('Login failed:', state.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 === 'totp' || 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 +import asyncio + +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, +) + +# Step 2: Poll until complete +state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +while state.status == "IN_PROGRESS": + print(f"Step: {state.step}") + + # Handle awaiting_input - submit credentials + if state.step == "awaiting_input" and state.pending_fields: + print(f"Fields needed: {[f.name for f in state.pending_fields]}") + print(f"Already submitted: {state.submitted_fields or []}") + + # Map credentials to the pending fields + field_values = map_credentials_to_fields( + state.pending_fields, + { + "email": "user@example.com", + "password": "secretpassword", + "code": input("Enter 2FA code: ") if any(f.type == "totp" for f in state.pending_fields) else "", + }, + ) + + # Submit is async - returns immediately + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + field_values=field_values, + ) + + print("Submitted, waiting for result...") + + # Poll every 2 seconds + await asyncio.sleep(2) + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +# Check final result +if state.status == "SUCCESS": + print("Authentication successful!") + print(f"Submitted fields: {state.submitted_fields}") +elif state.status == "FAILED": + print(f"Login failed: {state.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 == "totp" or 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. Poll for Login Fields + +After creating an invocation, poll until `step` becomes `awaiting_input`: + +```typescript +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +while (state.status === 'IN_PROGRESS' && state.step !== 'awaiting_input') { + await sleep(2000); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +console.log('Pending fields:', state.pending_fields); +``` + +**Response when `step === 'awaiting_input'`:** + +```json +{ + "status": "IN_PROGRESS", + "step": "awaiting_input", + "pending_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']" + } + ], + "submitted_fields": null +} +``` + +### 2. Map Credentials to Fields + +Use the `name` from each pending field as the key when submitting: + +```typescript +const fieldValues = { + 'email': 'user@example.com', // field.name → value + 'password': 'secretpassword' +}; +``` + +**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 | +| `url` | URL input | https://example.com | +| `code` | Verification code | 123456 | +| `totp` | TOTP/Authenticator code | 123456 | + +### 3. Submit Credentials + +Submit is **async** - it returns immediately with `{ accepted: true }`. Poll to see the result: + +```typescript +// Submit returns immediately +await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } +); + +// Poll to see the result +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +console.log('Step after submit:', state.step); // 'submitting' → 'awaiting_input' or 'completed' +``` + +### 4. Handle Multi-Step Auth (2FA) + +When additional authentication is needed (e.g., 2FA), the step goes back to `awaiting_input` with new `pending_fields`: + +```typescript +// After submitting email/password... +state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +if (state.step === 'awaiting_input' && state.pending_fields?.length) { + console.log('Additional auth needed:', state.pending_fields); + // pending_fields might contain: [{ name: 'totp', type: 'totp', label: 'Verification Code' }] + + console.log('Already submitted:', state.submitted_fields); + // submitted_fields: ['email', 'password'] + + // Submit the 2FA code + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: { totp: '123456' } } + ); +} +``` + +The `submitted_fields` array tracks what's been successfully submitted across all steps. + +## Building a Custom Login UI + +Here's an example of rendering pending fields in a React component: + +```tsx +function LoginForm({ pendingFields, submittedFields, onSubmit }) { + const [values, setValues] = useState>({}); + + return ( +
{ + e.preventDefault(); + onSubmit(values); + }}> + {submittedFields?.length > 0 && ( +

✓ Submitted: {submittedFields.join(', ')}

+ )} + + {pendingFields.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 +``` + + +TOTP/2FA codes are **not saved** to credentials since they're one-time codes. Only persistent credentials (email, password, etc.) are saved. + + +See [Credentials](/agents/auth/credentials) for details on pre-storing credentials. + +## Error Handling + +```typescript +try { + const state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + + // Check for terminal states + if (state.status === 'FAILED') { + console.error('Login failed:', state.error_message); + } else if (state.status === 'EXPIRED') { + console.error('Invocation expired'); + } else if (state.status === 'CANCELED') { + console.error('Invocation was canceled'); + } + + // Submit errors + await kernel.agents.auth.invocations.submit(invocation.invocation_id, { field_values }); +} catch (error) { + if (error.status === 400 && error.code === 'submit_in_progress') { + // A submit is already being processed - just keep polling + console.log('Submit already in progress, waiting...'); + } else 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 +- TOTP codes are not saved to credentials (they're one-time) +- 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/agents/auth/session-monitoring.mdx b/agents/auth/session-monitoring.mdx new file mode 100644 index 0000000..fdd18ae --- /dev/null +++ b/agents/auth/session-monitoring.mdx @@ -0,0 +1,98 @@ +--- +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 It 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. If expired, attempts auto re-auth (if credentials are configured) + +```mermaid +flowchart TD + A[Auth Agent
status: AUTHENTICATED] --> B[Hourly Check] + B --> C[Load profile] + C --> D[Navigate to site] + D --> E[Detect login state] + E --> F{Still logged in?} + F -->|Yes| G[No change] + F -->|No| H[Attempt re-auth] +``` + +## What Happens When Sessions Expire + +When a session expires, Agent Auth checks if the Auth Agent can automatically re-authenticate: + +| Scenario | Outcome | +|----------|---------| +| Has credentials + saved selectors | Auto re-auth attempted. If successful, stays `AUTHENTICATED`. | +| Auto re-auth fails | Status changes to `NEEDS_AUTH` | +| No credentials or selectors | Status changes to `NEEDS_AUTH` | + +### Enabling Auto Re-auth + +To enable automatic re-authentication, you need: + +1. **Linked credentials** - Stored login values for the target site +2. **Saved selectors** - Form selectors captured from a previous successful login + +See [Credentials](/agents/auth/credentials) for setup details. + +### Handling NEEDS_AUTH + +When status is `NEEDS_AUTH`, auto re-auth either isn't configured or failed. You'll need to trigger a new login—this is the same flow as initial authentication: + +- **[Hosted UI](/agents/auth/hosted-ui)** - Redirect user to complete login +- **[Auto-Login](/agents/auth/auto-login)** - Fully automated with pre-linked credentials + + +Use `save_credential_as` during login to store credentials. This enables automatic re-authentication for future session expirations. + + +## Checking Auth Status + +Check the `status` and `last_auth_check_at` fields to see current state: + + +```typescript TypeScript +const agent = await kernel.agents.auth.retrieve(agentId); + +console.log('Status:', agent.status); // AUTHENTICATED or NEEDS_AUTH +console.log('Last check:', agent.last_auth_check_at); +``` + +```python Python +agent = await kernel.agents.auth.retrieve(agent_id) + +print(f"Status: {agent.status}") # AUTHENTICATED or NEEDS_AUTH +print(f"Last check: {agent.last_auth_check_at}") +``` + + +| Status | Description | +|--------|-------------| +| `AUTHENTICATED` | Session is valid and ready to use | +| `NEEDS_AUTH` | Session expired and auto re-auth failed or isn't configured | + +## Check Frequency + + +Auth checks run approximately **every hour** for authenticated agents. Checks are passive and don't modify the saved profile unless re-authentication occurs. + + +## Next Steps + + + + Store credentials to enable auto re-auth + + + Handle manual re-authentication + + 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" ] }, { diff --git a/style.css b/style.css index 003ae24..58e1f59 100644 --- a/style.css +++ b/style.css @@ -29,7 +29,7 @@ table { overflow: hidden !important; border: 1px solid rgb(var(--gray-950) / 0.1) !important; background-color: rgb(249, 250, 251) !important; - margin: 1.25rem 0 !important; + margin: 0 !important; display: table !important; }