diff --git a/agents/auth/credentials.mdx b/agents/auth/credentials.mdx new file mode 100644 index 0000000..a412a79 --- /dev/null +++ b/agents/auth/credentials.mdx @@ -0,0 +1,177 @@ +--- +title: "Credentials" +--- + +Credentials enable fully automated authentication. Without credentials, users provide login info via the [Hosted UI](/agents/auth/hosted-ui) or [Programmatic](/agents/auth/programmatic) flow. With credentials saved, Kernel handles login automatically—both the first time and when sessions expire. + +## Save credentials during login + +Add `save_credential_as` to any invocation. The credentials entered during login are securely stored: + + +```typescript TypeScript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'my-login', +}); +``` + +```python Python +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, + save_credential_as="my-login", +) +``` + + +Once saved, the profile stays authenticated automatically. When the session expires, Kernel re-authenticates using the stored credentials—no user interaction needed. + +## Pre-store credentials + +For fully automated flows where no user is involved, create credentials upfront: + + +```typescript TypeScript +const credential = await kernel.credentials.create({ + name: 'my-netflix-login', + domain: 'netflix.com', + values: { + email: 'user@netflix.com', + password: 'secretpassword123', + }, +}); +``` + +```python Python +credential = await kernel.credentials.create( + name="my-netflix-login", + domain="netflix.com", + values={ + "email": "user@netflix.com", + "password": "secretpassword123", + }, +) +``` + + +Then link the credential to an auth agent: + + +```typescript TypeScript +const agent = await kernel.agents.auth.create({ + domain: 'netflix.com', + profile_name: 'my-profile', + credential_name: credential.name, +}); + +// Start invocation - logs in automatically using stored credentials +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); +``` + +```python Python +agent = await kernel.agents.auth.create( + domain="netflix.com", + profile_name="my-profile", + credential_name=credential.name, +) + +# Start invocation - logs in automatically using stored credentials +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, +) +``` + + +### 2FA with TOTP + +For sites with authenticator app 2FA, include `totp_secret` to fully automate login: + + +```typescript TypeScript +const credential = await kernel.credentials.create({ + name: 'my-login', + domain: 'github.com', + values: { + username: 'my-username', + password: 'my-password', + }, + totp_secret: 'JBSWY3DPEHPK3PXP', // From authenticator app setup +}); +``` + +```python Python +credential = await kernel.credentials.create( + name="my-login", + domain="github.com", + values={ + "username": "my-username", + "password": "my-password", + }, + totp_secret="JBSWY3DPEHPK3PXP", # From authenticator app setup +) +``` + + +### SSO / OAuth + +For sites with "Sign in with Google/GitHub/Microsoft", set `sso_provider` and include the OAuth provider in `allowed_domains`: + + +```typescript TypeScript +const credential = await kernel.credentials.create({ + name: 'my-google-login', + domain: 'accounts.google.com', + sso_provider: 'google', + values: { + email: 'user@gmail.com', + password: 'password', + }, +}); + +const agent = await kernel.agents.auth.create({ + domain: 'target-site.com', + profile_name: 'my-profile', + credential_name: credential.name, + allowed_domains: ['accounts.google.com', 'google.com'], +}); +``` + +```python Python +credential = await kernel.credentials.create( + name="my-google-login", + domain="accounts.google.com", + sso_provider="google", + values={ + "email": "user@gmail.com", + "password": "password", + }, +) + +agent = await kernel.agents.auth.create( + domain="target-site.com", + profile_name="my-profile", + credential_name=credential.name, + allowed_domains=["accounts.google.com", "google.com"], +) +``` + + +The workflow automatically clicks the matching SSO button and completes OAuth. + +## Security + +| Feature | Description | +|---------|-------------| +| **Encrypted at rest** | Values encrypted using per-organization keys | +| **Write-only** | Values cannot be retrieved via API after creation | +| **Never logged** | Values are never written to logs | +| **Never shared** | Values are never passed to LLMs | +| **Isolated execution** | Authentication runs in isolated browser environments | + +## Notes + +- The `values` object is flexible—store whatever fields the login form needs (`email`, `username`, `company_id`, etc.) +- Deleting a credential unlinks it from associated auth agents; they'll no longer auto-authenticate +- One credential per account—create separate credentials for different user accounts diff --git a/agents/auth/early-preview.mdx b/agents/auth/early-preview.mdx new file mode 100644 index 0000000..ac11852 --- /dev/null +++ b/agents/auth/early-preview.mdx @@ -0,0 +1,56 @@ +--- +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 + + + Redirect users to complete login themselves + + + Build custom auth flows with full control + + + Store credentials for automated re-auth + + + +## 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/3f4eb01bb73f679828e195a74f41214d69c01453/dist.tar.gz" + } +} +``` + +**Python (requirements.txt):** + +``` +kernel @ https://pkg.stainless.com/s/kernel-python/e941d0fb0a62cb8a1aad2424577c825bd6764df4/kernel-0.24.0-py3-none-any.whl +``` + +Or in pyproject.toml: + +```toml +[project] +dependencies = [ + "kernel @ https://pkg.stainless.com/s/kernel-python/e941d0fb0a62cb8a1aad2424577c825bd6764df4/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..b12782e --- /dev/null +++ b/agents/auth/hosted-ui.mdx @@ -0,0 +1,225 @@ +--- +title: "Hosted UI" +description: "The simplest way to create authenticated browser sessions" +--- + +Collect credentials via Kernel's hosted page, then use the authenticated session in your automations. This is the recommended approach for most applications. + +Use the Hosted UI when: +- You need users to provide their credentials +- You want the simplest integration with minimal code +- You want Kernel to handle 2FA and multi-step login flows + +## Getting started + +### 1. Create an Auth Agent + +An Auth Agent represents a login session for a specific website and profile. + + +```typescript TypeScript +const agent = await kernel.agents.auth.create({ + domain: 'linkedin.com', + profile_name: 'linkedin-profile', +}); +``` + +```python Python +agent = await kernel.agents.auth.create( + domain="linkedin.com", + profile_name="linkedin-profile", +) +``` + + +### 2. Start Authentication + +Create an invocation to get the hosted login URL. + + +```typescript TypeScript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); +``` + +```python Python +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, +) +``` + + +To enable automatic re-authentication when sessions expire, add `save_credential_as`: + + +```typescript TypeScript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'my-saved-creds', +}); +``` + +```python Python +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, + save_credential_as="my-saved-creds", +) +``` + + +### 3. Collect Credentials + +Send the user to the hosted login page: + + +```typescript TypeScript +window.location.href = invocation.hosted_url; +``` + +```python Python +# Return the URL to your frontend +print(f"Redirect to: {invocation.hosted_url}") +``` + + +The user will: +1. See the login page for the target website +2. Enter their credentials +3. Complete 2FA if needed + +### 4. Poll for Completion + +On your backend, poll until authentication completes: + + +```typescript TypeScript +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +while (state.status === 'IN_PROGRESS') { + await new Promise(r => setTimeout(r, 2000)); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +if (state.status === 'SUCCESS') { + console.log('Authentication successful!'); +} +``` + +```python Python +state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +while state.status == "IN_PROGRESS": + await asyncio.sleep(2) + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +if state.status == "SUCCESS": + print("Authentication successful!") +``` + + + +Poll every 2 seconds. The session expires after 5 minutes if not completed. + + +### 5. Use the Profile + +Create browsers with the profile—they're already logged in: + + +```typescript TypeScript +const browser = await kernel.browsers.create({ + profile: { name: 'linkedin-profile' }, + stealth: true, +}); +// Browser is logged into LinkedIn! +``` + +```python Python +browser = await kernel.browsers.create( + profile={"name": "linkedin-profile"}, + stealth=True, +) +# Browser is logged into LinkedIn! +``` + + + +Use `stealth: true` when creating browsers for authenticated sessions. + + + +## Complete Example + + +```typescript TypeScript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// Create auth agent +const agent = await kernel.agents.auth.create({ + domain: 'doordash.com', + profile_name: 'doordash-user-123', +}); + +// Start authentication +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); + +// Send user to hosted page +console.log('Login URL:', invocation.hosted_url); + +// Poll for completion +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +while (state.status === 'IN_PROGRESS') { + await new Promise(r => setTimeout(r, 2000)); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +if (state.status === 'SUCCESS') { + // Use the profile + const browser = await kernel.browsers.create({ + profile: { name: 'doordash-user-123' }, + stealth: true, + }); + // Browser is logged into DoorDash! +} +``` + +```python Python +from kernel import Kernel +import asyncio + +kernel = Kernel() + +# Create auth agent +agent = await kernel.agents.auth.create( + domain="doordash.com", + profile_name="doordash-user-123", +) + +# Start authentication +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, +) + +# Send user to hosted page +print(f"Login URL: {invocation.hosted_url}") + +# Poll for completion +state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) +while state.status == "IN_PROGRESS": + await asyncio.sleep(2) + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +if state.status == "SUCCESS": + # Use the profile + browser = await kernel.browsers.create( + profile={"name": "doordash-user-123"}, + stealth=True, + ) + # Browser is logged into DoorDash! +``` + diff --git a/agents/auth/overview.mdx b/agents/auth/overview.mdx new file mode 100644 index 0000000..2e24304 --- /dev/null +++ b/agents/auth/overview.mdx @@ -0,0 +1,130 @@ +--- +title: "Overview" +description: "Create authenticated browser sessions for your automations" +--- + +Agent Auth creates authenticated browser sessions for your automations—handling login flows, 2FA/OTP, and session persistence automatically. + +## How It Works + + + + An **Auth Agent** represents a login session for a specific website and profile. Create one for each domain + profile combination. + + +```typescript TypeScript +const agent = await kernel.agents.auth.create({ + domain: 'netflix.com', + profile_name: 'netflix-user-123', +}); +``` + +```python Python +agent = await kernel.agents.auth.create( + domain="netflix.com", + profile_name="netflix-user-123", +) +``` + + + + Start the login flow. Users provide credentials via the hosted page (or your own UI). + + +```typescript TypeScript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); + +// Send user to login page +console.log('Login URL:', invocation.hosted_url); + +// Poll until complete +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +while (state.status === 'IN_PROGRESS') { + await new Promise(r => setTimeout(r, 2000)); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +if (state.status === 'SUCCESS') { + console.log('Authenticated!'); +} +``` + +```python Python +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, +) + +# Send user to login page +print(f"Login URL: {invocation.hosted_url}") + +# Poll until complete +state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) +while state.status == "IN_PROGRESS": + await asyncio.sleep(2) + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +if state.status == "SUCCESS": + print("Authenticated!") +``` + + + + Create browsers with the saved profile—they're already logged in. + + +```typescript TypeScript +const browser = await kernel.browsers.create({ + profile: { name: 'netflix-user-123' }, + stealth: true, +}); +// Browser is logged into Netflix! +``` + +```python Python +browser = await kernel.browsers.create( + profile={"name": "netflix-user-123"}, + stealth=True, +) +# Browser is logged into Netflix! +``` + + + For fully automated flows, link [Credentials](/agents/auth/credentials) to enable re-authentication without user input. + + + +## Choose Your Integration + + + + **Best for:** Most applications + + Collect credentials via Kernel's hosted page. Simplest integration. + + + **Best for:** Custom UI or headless + + Build your own credential collection with polling and submit APIs. + + + +## Why Agent Auth? + +The most valuable workflows live behind logins. Agent Auth provides: + +- **Works on any website** - Login pages are handled automatically +- **SSO/OAuth support** - "Sign in with Google/GitHub/Microsoft" buttons work out of the box +- **2FA/OTP handling** - TOTP codes automated, SMS/email OTP via the UI +- **Session monitoring** - Automatic re-authentication when sessions expire +- **Secure by default** - Credentials encrypted at rest, never exposed in API responses + +## Security + +| Feature | Description | +|---------|-------------| +| **Encrypted credentials** | Values encrypted with per-organization keys | +| **No credential exposure** | Never returned in API responses or passed to LLMs | +| **Encrypted profiles** | Browser session state encrypted end-to-end | +| **Isolated execution** | Each login runs in an isolated browser environment | diff --git a/agents/auth/programmatic.mdx b/agents/auth/programmatic.mdx new file mode 100644 index 0000000..9c4922a --- /dev/null +++ b/agents/auth/programmatic.mdx @@ -0,0 +1,245 @@ +--- +title: "Programmatic Flow" +description: "Build your own credential collection UI with full control" +--- + +Build your own credential collection UI instead of using the hosted page. Poll for login fields, then submit credentials via the API. + +Use the Programmatic flow when: +- You need a custom credential collection UI that matches your app's design +- You're building headless/automated authentication +- You have credentials stored and want to authenticate without user interaction + +## How It Works + + + + Same as [Hosted UI](/agents/auth/hosted-ui) + + + Poll until `step` becomes `awaiting_input`, then submit credentials + + + If more fields appear (2FA code), submit again—same loop handles it + + + +## Getting started + +### 1. Create an Auth Agent + + +```typescript TypeScript +const agent = await kernel.agents.auth.create({ + domain: 'github.com', + profile_name: 'github-profile', +}); +``` + +```python Python +agent = await kernel.agents.auth.create( + domain="github.com", + profile_name="github-profile", +) +``` + + +### 2. Start Authentication + + +```typescript TypeScript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); +``` + +```python Python +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, +) +``` + + +To save credentials for automatic re-authentication: + + +```typescript TypeScript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'my-saved-creds', +}); +``` + +```python Python +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, + save_credential_as="my-saved-creds", +) +``` + + +### 3. Poll and Submit Credentials + +A single loop handles everything—initial login, 2FA, and completion: + + +```typescript TypeScript +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +while (state.status === 'IN_PROGRESS') { + // Submit when fields are ready (login or 2FA) + if (state.step === 'awaiting_input' && state.pending_fields?.length) { + const fieldValues = getCredentialsForFields(state.pending_fields); + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } + ); + } + + await new Promise(r => setTimeout(r, 2000)); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +if (state.status === 'SUCCESS') { + console.log('Authentication successful!'); +} +``` + +```python Python +state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +while state.status == "IN_PROGRESS": + # Submit when fields are ready (login or 2FA) + if state.step == "awaiting_input" and state.pending_fields: + field_values = get_credentials_for_fields(state.pending_fields) + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + field_values=field_values, + ) + + await asyncio.sleep(2) + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +if state.status == "SUCCESS": + print("Authentication successful!") +``` + + +The `pending_fields` array tells you what the login form needs: + +```typescript +// Example pending_fields for login +[{ name: 'username', type: 'text' }, { name: 'password', type: 'password' }] + +// Example pending_fields for 2FA +[{ name: 'otp', type: 'code' }] +``` + +## Complete Example + + +```typescript TypeScript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +// Create auth agent +const agent = await kernel.agents.auth.create({ + domain: 'github.com', + profile_name: 'github-profile', +}); + +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); + +// Single polling loop handles login + 2FA +let state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +while (state.status === 'IN_PROGRESS') { + if (state.step === 'awaiting_input' && state.pending_fields?.length) { + // Check what fields are needed + const fieldNames = state.pending_fields.map(f => f.name); + + if (fieldNames.includes('username')) { + // Initial login + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: { username: 'my-username', password: 'my-password' } } + ); + } else { + // 2FA or additional fields + const code = await promptUserForCode(); + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: { [state.pending_fields[0].name]: code } } + ); + } + } + + await new Promise(r => setTimeout(r, 2000)); + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +if (state.status === 'SUCCESS') { + console.log('Authentication successful!'); + + const browser = await kernel.browsers.create({ + profile: { name: 'github-profile' }, + stealth: true, + }); + // Browser is logged into GitHub! +} +``` + +```python Python +from kernel import Kernel +import asyncio + +kernel = Kernel() + +# Create auth agent +agent = await kernel.agents.auth.create( + domain="github.com", + profile_name="github-profile", +) + +invocation = await kernel.agents.auth.invocations.create( + auth_agent_id=agent.id, +) + +# Single polling loop handles login + 2FA +state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +while state.status == "IN_PROGRESS": + if state.step == "awaiting_input" and state.pending_fields: + # Check what fields are needed + field_names = [f["name"] for f in state.pending_fields] + + if "username" in field_names: + # Initial login + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + field_values={"username": "my-username", "password": "my-password"}, + ) + else: + # 2FA or additional fields + code = input("Enter code: ") + await kernel.agents.auth.invocations.submit( + invocation.invocation_id, + field_values={state.pending_fields[0]["name"]: code}, + ) + + await asyncio.sleep(2) + state = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) + +if state.status == "SUCCESS": + print("Authentication successful!") + + browser = await kernel.browsers.create( + profile={"name": "github-profile"}, + stealth=True, + ) + # Browser is logged into GitHub! +``` + diff --git a/docs.json b/docs.json index dedd1b8..6b01c22 100644 --- a/docs.json +++ b/docs.json @@ -98,6 +98,15 @@ "browsers/faq" ] }, + { + "group": "Agent Auth", + "pages": [ + "agents/auth/overview", + "agents/auth/hosted-ui", + "agents/auth/programmatic", + "agents/auth/credentials" + ] + }, { "group": "App Platform", "pages": [ @@ -182,7 +191,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; }