diff --git a/agent-auth/auto-login.mdx b/agent-auth/auto-login.mdx index 260100d..236a007 100644 --- a/agent-auth/auto-login.mdx +++ b/agent-auth/auto-login.mdx @@ -92,18 +92,12 @@ 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); +// Step 4: Poll for completion +const result = await pollForCompletion(invocation.invocation_id); - if (result.success) { - console.log('Auto-login successful!'); - } +// If already authenticated, status will immediately be SUCCESS +if (result.success) { + console.log('Auto-login successful!'); } // Step 5: Use the profile @@ -145,17 +139,12 @@ 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) +# Step 4: Poll for completion +result = await poll_for_completion(invocation.invocation_id) - if result["success"]: - print("Auto-login successful!") +# 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( @@ -187,6 +176,9 @@ async function pollForCompletion(invocationId: string) { if (status.status === 'SUCCESS') { return { success: true, status }; } + 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 }; } @@ -217,6 +209,8 @@ async def poll_for_completion(invocation_id: str): if status.status == "SUCCESS": return {"success": True, "status": status} + 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} @@ -240,6 +234,7 @@ async def poll_for_completion(invocation_id: str): | `awaiting_input` | Waiting for input (manual intervention needed) | | `submitting` | Currently submitting credentials | | `completed` | Login flow finished | +| `expired` | Invocation expired before completion | ## Credential Field Mapping @@ -288,19 +283,21 @@ 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, - {} + // Retrieve to see current pending fields + const state = await kernel.agents.auth.invocations.retrieve( + invocation.invocation_id ); + console.log('Pending fields:', state.pending_fields); - // Submit the missing value - const submit = await kernel.agents.auth.invocations.submit( + // Submit the missing value (async - returns immediately) + await kernel.agents.auth.invocations.submit( invocation.invocation_id, { field_values: { code: otpCode } } ); - if (submit.logged_in) { + // Poll for completion + const finalResult = await pollForCompletion(invocation.invocation_id); + if (finalResult.success) { console.log('Login completed!'); } } @@ -313,18 +310,21 @@ 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, + # Retrieve to see current pending fields + state = await kernel.agents.auth.invocations.retrieve( + invocation.invocation_id ) + print(f"Pending fields: {state.pending_fields}") - # Submit the missing value - submit = await kernel.agents.auth.invocations.submit( + # Submit the missing value (async - returns immediately) + await kernel.agents.auth.invocations.submit( invocation.invocation_id, field_values={"code": otp_code}, ) - if submit.logged_in: + # Poll for completion + final_result = await poll_for_completion(invocation.invocation_id) + if final_result["success"]: print("Login completed!") ``` diff --git a/agent-auth/credentials.mdx b/agent-auth/credentials.mdx index 4c8d90f..e47938c 100644 --- a/agent-auth/credentials.mdx +++ b/agent-auth/credentials.mdx @@ -40,8 +40,12 @@ Without stored credentials, every time a session expires, you need to redirect u ```typescript - // When session expires, trigger re-auth - const reauth = await kernel.agents.auth.reauth(agent.id); + // When session expires, create a new invocation + // With linked credentials, it will auto-fill and submit + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); + // Poll for completion - credentials are used automatically ``` @@ -113,7 +117,7 @@ There are two ways to link credentials to an Auth Agent: const agent = await kernel.agents.auth.create({ domain: 'netflix.com', profile_name: 'my-profile', - credential_id: credential.id, // Link the credential + credential_name: credential.name, // Link the credential }); ``` @@ -129,10 +133,50 @@ const invocation = await kernel.agents.auth.invocations.create({ ``` This approach: -- Creates the credential automatically when login succeeds +- 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. @@ -154,23 +198,28 @@ An Auth Agent `can_reauth` when: ### Triggering Re-authentication -When sessions expire, trigger re-authentication: +When sessions expire, create a new invocation. With linked credentials, the system will automatically use them: ```typescript -const reauth = await kernel.agents.auth.reauth(agent.id); +// Check agent status first +const agent = await kernel.agents.auth.retrieve(agentId); -switch (reauth.status) { - case 'REAUTH_STARTED': - console.log('Re-auth started, invocation:', reauth.invocation_id); +if (agent.status === 'NEEDS_AUTH') { + if (agent.can_reauth) { + // Create invocation - credentials will be used automatically + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.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; + const result = await pollForCompletion(invocation.invocation_id); + if (result.success) { + console.log('Re-auth successful!'); + } + } else { + console.log('Cannot auto re-auth - manual login required'); + // Redirect user to hosted_url for manual login + } } ``` @@ -370,7 +419,7 @@ async function setupAutomatedAuth() { const agent = await kernel.agents.auth.create({ domain: 'portal.acme.com', profile_name: 'acme-agent-profile', - credential_id: credential.id, + credential_name: credential.name, login_url: 'https://portal.acme.com/login', }); @@ -391,12 +440,12 @@ async function useAuthenticatedBrowser(agentId: string) { 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); - } + // Create invocation - credentials will be used automatically + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agentId, + }); + // Wait for re-auth to complete + await pollForCompletion(invocation.invocation_id); } else { throw new Error('Cannot re-auth - manual login required'); } diff --git a/agent-auth/hosted-ui.mdx b/agent-auth/hosted-ui.mdx index ebe4383..213ff7c 100644 --- a/agent-auth/hosted-ui.mdx +++ b/agent-auth/hosted-ui.mdx @@ -27,9 +27,10 @@ Use the Hosted UI when: }); ``` - + ```typescript - if (invocation.status !== 'ALREADY_AUTHENTICATED') { + const state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + if (state.status !== 'SUCCESS') { window.location.href = invocation.hosted_url; } ``` @@ -71,7 +72,9 @@ const invocation = await kernel.agents.auth.invocations.create({ }); // Step 3: Check if already logged in -if (invocation.status === 'ALREADY_AUTHENTICATED') { +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 { @@ -91,6 +94,9 @@ const pollForCompletion = async (invocationId: string) => { 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 }; } @@ -142,7 +148,9 @@ invocation = await kernel.agents.auth.invocations.create( ) # Step 3: Check if already logged in -if invocation.status == "ALREADY_AUTHENTICATED": +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}") @@ -157,6 +165,8 @@ async def poll_for_completion(invocation_id: str): 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} @@ -222,11 +232,11 @@ const invocation = await kernel.agents.auth.invocations.create({ | `invocation_id` | Unique ID for this auth attempt | | `hosted_url` | URL to redirect the user to | | `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. - + + +Poll the invocation immediately after creation. If `status` is already `SUCCESS`, the profile is already logged in—skip the redirect. + ### 3. Redirect the User @@ -249,22 +259,26 @@ The hosted UI will: On your backend, poll the invocation status until it completes: ```typescript -let status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); -while (status.status === 'IN_PROGRESS') { +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 - status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); } -switch (status.status) { +switch (state.status) { case 'SUCCESS': console.log('Authentication successful!'); break; + case 'FAILED': + console.error('Login failed:', state.error_message); + break; case 'EXPIRED': - console.log('User did not complete login in time'); + console.error('Invocation expired - user did not complete login in time'); break; case 'CANCELED': - console.log('Authentication was canceled'); + console.error('Authentication was canceled'); break; } ``` diff --git a/agent-auth/overview.mdx b/agent-auth/overview.mdx index 57f9103..5353286 100644 --- a/agent-auth/overview.mdx +++ b/agent-auth/overview.mdx @@ -26,16 +26,20 @@ Agent Auth logs users into any website and saves their authenticated browser ses auth_agent_id: agent.id, }); - // If already logged in, skip the flow - if (invocation.status === 'ALREADY_AUTHENTICATED') { - console.log('Session still valid!'); + // 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 statuses: `IN_PROGRESS` → `SUCCESS` | `EXPIRED` | `CANCELED` + **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 */} diff --git a/agent-auth/programmatic.mdx b/agent-auth/programmatic.mdx index e1bdb33..ace3421 100644 --- a/agent-auth/programmatic.mdx +++ b/agent-auth/programmatic.mdx @@ -1,23 +1,29 @@ --- title: "Programmatic Flow" -description: "Build custom authentication UIs with full control using discover and submit APIs" +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 the discover/submit APIs to drive the authentication. +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 the discover/submit flow +- 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](/agent-auth/auto-login) is simpler—it handles discover/submit automatically. Use Programmatic when you need custom control over each step. +**Have credentials stored?** If you just want automated login with pre-linked credentials, [Auto-Login](/agent-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 @@ -30,36 +36,57 @@ Use the Programmatic flow when: }); ``` - + ```typescript - const discover = await kernel.agents.auth.invocations.discover( - invocation.invocation_id, - {} - ); - // discover.fields contains the form fields + let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + while (state.step === 'initialized' || state.step === 'discovering') { + await sleep(2000); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + } + // state.pending_fields contains the form fields ``` ```typescript - const submit = await kernel.agents.auth.invocations.submit( + // Submit is async - returns immediately + 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' } } - ); + // Poll until step changes from 'submitting' + while (state.step === 'submitting' || state.step === 'awaiting_input') { + 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 + } + if (state.step === 'completed') { + // Success! + } } ``` +## 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 @@ -68,6 +95,9 @@ 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', @@ -78,55 +108,51 @@ const invocation = await kernel.agents.auth.invocations.create({ auth_agent_id: agent.id, }); -if (invocation.status === 'ALREADY_AUTHENTICATED') { +// Poll to check current state +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +// If already authenticated, status will immediately be SUCCESS +if (state.status === 'SUCCESS') { 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( + // Step 2: Poll until fields are discovered + 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: additionalValues } + { field_values: fieldValues } ); + + console.log('Submitted, waiting for result...'); } - - if (submitResponse.logged_in) { - console.log('Authentication successful!'); - } else if (submitResponse.error_message) { - console.error('Login failed:', submitResponse.error_message); - } + + // 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); } } @@ -146,7 +172,7 @@ function mapCredentialsToFields( 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')) { + } 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 || ''; @@ -159,6 +185,7 @@ function mapCredentialsToFields( ```python Python from kernel import Kernel +import asyncio kernel = Kernel() @@ -172,53 +199,51 @@ invocation = await kernel.agents.auth.invocations.create( auth_agent_id=agent.id, ) -if invocation.status == "ALREADY_AUTHENTICATED": +# Poll to check current state +state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +if state.status == "SUCCESS": 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}, + # 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_response = await kernel.agents.auth.invocations.submit( + + # Submit is async - returns immediately + await kernel.agents.auth.invocations.submit( invocation.invocation_id, - field_values=additional_values, + field_values=field_values, ) - - if submit_response.logged_in: - print("Authentication successful!") - elif submit_response.error_message: - print(f"Login failed: {submit_response.error_message}") + + 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): @@ -232,7 +257,7 @@ def map_credentials_to_fields(fields, credentials): 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: + 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", "") @@ -243,26 +268,28 @@ def map_credentials_to_fields(fields, credentials): ## Step-by-Step Breakdown -### 1. Discover Login Fields +### 1. Poll for Login Fields -Call `discover()` to navigate to the login page and extract form fields: +After creating an invocation, poll until `step` becomes `awaiting_input`: ```typescript -const discoverResponse = await kernel.agents.auth.invocations.discover( - invocation.invocation_id, - { login_url: 'https://example.com/login' } // Optional: override login URL -); +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +while (state.step === 'initialized' || state.step === 'discovering') { + await sleep(2000); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +console.log('Pending fields:', state.pending_fields); ``` -**Discover response:** +**Response when `step === 'awaiting_input'`:** ```json { - "success": true, - "logged_in": false, - "login_url": "https://identity.example.com/login", - "page_title": "Sign In - Example", - "fields": [ + "status": "IN_PROGRESS", + "step": "awaiting_input", + "pending_fields": [ { "name": "email", "type": "email", @@ -278,35 +305,19 @@ const discoverResponse = await kernel.agents.auth.invocations.discover( "required": true, "selector": "//input[@id='password']" } - ] + ], + "submitted_fields": null } ``` - -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: +Use the `name` from each pending field as the key when submitting: ```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 + 'email': 'user@example.com', // field.name → value + 'password': 'secretpassword' }; ``` @@ -319,89 +330,57 @@ const fieldValues = { | `password` | Password (masked) | ••••••••• | | `tel` | Phone number | +1-555-0123 | | `number` | Numeric input | 12345 | -| `code` | Verification code (OTP/2FA) | 123456 | +| `url` | URL input | https://example.com | +| `code` | Verification code | 123456 | +| `totp` | TOTP/Authenticator code | 123456 | ### 3. Submit Credentials -Submit the mapped field values to attempt login: +Submit is **async** - it returns immediately with `{ accepted: true }`. Poll to see the result: ```typescript -const submitResponse = await kernel.agents.auth.invocations.submit( +// Submit returns immediately +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" -} +// 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' ``` -**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']" - } - ] -} -``` +### 4. Handle Multi-Step Auth (2FA) -**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: +When additional authentication is needed (e.g., 2FA), the step goes back to `awaiting_input` with new `pending_fields`: ```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( +// 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: additionalValues } + { 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 discovered fields in a React component: +Here's an example of rendering pending fields in a React component: ```tsx -function LoginForm({ fields, onSubmit }) { +function LoginForm({ pendingFields, submittedFields, onSubmit }) { const [values, setValues] = useState>({}); return ( @@ -409,14 +388,19 @@ function LoginForm({ fields, onSubmit }) { e.preventDefault(); onSubmit(values); }}> - {fields.map((field) => ( + {submittedFields?.length > 0 && ( +

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

+ )} + + {pendingFields.map((field) => (
setValues({ ...values, @@ -425,7 +409,9 @@ function LoginForm({ fields, onSubmit }) { />
))} - + ); } @@ -445,33 +431,34 @@ const invocation = await kernel.agents.auth.invocations.create({ // 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](/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 + 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 === 404) { + 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); @@ -483,6 +470,7 @@ try { - 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 diff --git a/agent-auth/session-monitoring.mdx b/agent-auth/session-monitoring.mdx index 026a381..da9ec64 100644 --- a/agent-auth/session-monitoring.mdx +++ b/agent-auth/session-monitoring.mdx @@ -92,7 +92,7 @@ Check `agent.can_reauth` to determine which option is available. If `true`, auto ### Option 1: Automated Re-auth (Requires Credentials) -If the Auth Agent has linked credentials and saved selectors, trigger automated re-auth: +If the Auth Agent has linked credentials and saved selectors, create a new invocation to trigger automated re-auth: ```typescript async function triggerReauth(agent) { @@ -102,22 +102,15 @@ async function triggerReauth(agent) { 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 }; + // Create invocation - credentials will be used automatically + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); - case 'CANNOT_REAUTH': - console.log('Cannot reauth:', reauth.message); - return await manualReauth(agent.id); - } + console.log('Re-auth started:', invocation.invocation_id); + + // Poll for completion + return await pollForCompletion(invocation.invocation_id); } ``` @@ -156,6 +149,11 @@ async function pollForCompletion(invocationId: string) { return { success: true }; } + if (status.status === 'FAILED') { + console.log('Re-auth failed:', status.error_message); + return { success: false, reason: 'FAILED', error: status.error_message }; + } + if (status.status === 'EXPIRED' || status.status === 'CANCELED') { console.log('Re-auth failed:', status.status); return { success: false, reason: status.status }; @@ -183,10 +181,10 @@ async function ensureAuthenticated(agentId: string) { // Need to re-auth if (agent.can_reauth) { - const reauth = await kernel.agents.auth.reauth(agentId); - if (reauth.status === 'REAUTH_STARTED') { - await pollForCompletion(reauth.invocation_id); - } + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agentId, + }); + await pollForCompletion(invocation.invocation_id); } else { throw new Error('Session expired and cannot auto-reauth'); } @@ -239,7 +237,10 @@ async function checkAllSessions() { // Auto-reauth those that can be automated for (const agent of canAutoReauth) { - await kernel.agents.auth.reauth(agent.id); + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + }); + // Optionally poll for completion } return { needsManualReauth }; @@ -296,13 +297,12 @@ class AuthSessionManager { } private async triggerReauth(agent: AuthAgent) { - const reauth = await kernel.agents.auth.reauth(agent.id); + // Create invocation - credentials will be used automatically + const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: 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); - } + await this.pollForCompletion(invocation.invocation_id); } private async pollForCompletion(invocationId: string, timeoutMs = 300000) { @@ -312,6 +312,7 @@ class AuthSessionManager { const status = await kernel.agents.auth.invocations.retrieve(invocationId); if (status.status === 'SUCCESS') return; + if (status.status === 'FAILED') throw new Error(`Re-auth failed: ${status.error_message}`); if (status.status === 'EXPIRED') throw new Error('Re-auth expired'); if (status.status === 'CANCELED') throw new Error('Re-auth canceled'); @@ -337,13 +338,6 @@ class AuthFailedError extends Error { } } -class CannotReauthError extends Error { - constructor(message: string, public agentId: string) { - super(message); - this.name = 'CannotReauthError'; - } -} - // Usage const sessionManager = new AuthSessionManager();