Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ agent-browser set offline [on|off] # Toggle offline mode
agent-browser set headers <json> # Extra HTTP headers
agent-browser set credentials <u> <p> # HTTP basic auth
agent-browser set media [dark|light] # Emulate color scheme
agent-browser set webauthn enable # Enable WebAuthn
agent-browser set webauthn add-virtual-authenticator # Add virtual authenticator
```

### Cookies & Storage
Expand Down
33 changes: 32 additions & 1 deletion cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,7 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
"credentials",
"auth",
"media",
"webauthn",
];

match rest.first().copied() {
Expand Down Expand Up @@ -1969,13 +1970,27 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
json!({ "id": id, "action": "emulatemedia", "colorScheme": color, "reducedMotion": reduced }),
)
}
Some("webauthn") => match rest.get(1).copied() {
Some("enable") => Ok(json!({ "id": id, "action": "webauthn_enable" })),
Some("add-virtual-authenticator") => {
Ok(json!({ "id": id, "action": "webauthn_add_virtual_authenticator" }))
}
Some(sub) => Err(ParseError::UnknownSubcommand {
subcommand: sub.to_string(),
valid_options: &["enable", "add-virtual-authenticator"],
}),
None => Err(ParseError::MissingArguments {
context: "set webauthn".to_string(),
usage: "set webauthn <enable|add-virtual-authenticator>",
}),
},
Some(sub) => Err(ParseError::UnknownSubcommand {
subcommand: sub.to_string(),
valid_options: VALID,
}),
None => Err(ParseError::MissingArguments {
context: "set".to_string(),
usage: "set <viewport|device|geo|offline|headers|credentials|media> [args...]",
usage: "set <viewport|device|geo|offline|headers|credentials|media|webauthn> [args...]",
}),
}
}
Expand Down Expand Up @@ -3045,6 +3060,22 @@ mod tests {
assert_eq!(cmd["reducedMotion"], "reduce");
}

#[test]
fn test_set_webauthn_enable() {
let cmd = parse_command(&args("set webauthn enable"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "webauthn_enable");
}

#[test]
fn test_set_webauthn_add_virtual_authenticator() {
let cmd = parse_command(
&args("set webauthn add-virtual-authenticator"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "webauthn_add_virtual_authenticator");
}

#[test]
fn test_find_first_no_value() {
let cmd = parse_command(&args("find first a click"), &default_flags()).unwrap();
Expand Down
16 changes: 16 additions & 0 deletions cli/src/native/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,10 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
"requests" => handle_requests(cmd, state).await,
"credentials" => handle_http_credentials(cmd, state).await,
"emulatemedia" => handle_set_media(cmd, state).await,
"webauthn_enable" => handle_webauthn_enable(state).await,
"webauthn_add_virtual_authenticator" => {
handle_webauthn_add_virtual_authenticator(state).await
}
"auth_save" => handle_auth_save(cmd).await,
"auth_login" => handle_auth_login(cmd, state).await,
"auth_list" => handle_credentials_list().await,
Expand Down Expand Up @@ -2880,6 +2884,18 @@ async fn handle_permissions(cmd: &Value, state: &DaemonState) -> Result<Value, S
Ok(json!({ "granted": permissions }))
}

async fn handle_webauthn_enable(state: &DaemonState) -> Result<Value, String> {
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
mgr.enable_webauthn().await?;
Ok(json!({ "enabled": true }))
}

async fn handle_webauthn_add_virtual_authenticator(state: &DaemonState) -> Result<Value, String> {
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
mgr.add_virtual_authenticator().await?;
Ok(json!({ "added": true }))
}

async fn handle_dialog(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
let accept = cmd
Expand Down
29 changes: 29 additions & 0 deletions cli/src/native/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,35 @@ impl BrowserManager {
Ok(())
}

pub async fn enable_webauthn(&self) -> Result<(), String> {
let session_id = self.active_session_id()?;
self.client
.send_command_no_params("WebAuthn.enable", Some(session_id))
.await?;
Ok(())
}

pub async fn add_virtual_authenticator(&self) -> Result<(), String> {
let session_id = self.active_session_id()?;
self.client
.send_command(
"WebAuthn.addVirtualAuthenticator",
Some(json!({
"options": {
"protocol": "ctap2",
"transport": "internal",
"hasResidentKey": true,
"hasUserVerification": true,
"isUserVerified": true,
}
})),
Some(session_id),
)
.await?;
Ok(())
}


pub async fn set_emulated_media(
&self,
media: Option<&str>,
Expand Down
2 changes: 2 additions & 0 deletions cli/src/native/parity_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ const DOCUMENTED_ACTIONS: &[&str] = &[
"unroute",
"requests",
"credentials",
"webauthn_enable",
"webauthn_add_virtual_authenticator",
"auth_save",
"auth_login",
"auth_list",
Expand Down
5 changes: 5 additions & 0 deletions cli/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,9 @@ Settings:
credentials <user> <pass> Set HTTP authentication
media [dark|light] Set color scheme preference
[reduced-motion] Enable reduced motion
webauthn enable Enable WebAuthn
webauthn add-virtual-authenticator
Add a virtual authenticator for passkeys

Global Options:
--json Output as JSON
Expand All @@ -1612,6 +1615,8 @@ Examples:
agent-browser set credentials admin secret123
agent-browser set media dark
agent-browser set media light reduced-motion
agent-browser set webauthn enable
agent-browser set webauthn add-virtual-authenticator
"##
}

Expand Down
2 changes: 2 additions & 0 deletions docs/src/app/commands/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ agent-browser set offline [on|off] # Toggle offline mode
agent-browser set headers <json> # Extra HTTP headers
agent-browser set credentials <u> <p> # HTTP basic auth
agent-browser set media [dark|light] # Emulate color scheme (persists for session)
agent-browser set webauthn enable # Enable WebAuthn
agent-browser set webauthn add-virtual-authenticator # Add virtual authenticator
```

Use `--color-scheme` for persistent dark/light mode across all commands:
Expand Down
5 changes: 5 additions & 0 deletions skills/agent-browser/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ agent-browser diff screenshot --baseline before.png # Visual pixel diff
agent-browser diff url <url1> <url2> # Compare two pages
agent-browser diff url <url1> <url2> --wait-until networkidle # Custom wait strategy
agent-browser diff url <url1> <url2> --selector "#main" # Scope to element

# Passkeys

agent-browser set webauthn enable # Enable WebAuthn
agent-browser set webauthn add-virtual-authenticator # Add virtual authenticator for passkeys
```

## Common Patterns
Expand Down
2 changes: 2 additions & 0 deletions skills/agent-browser/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers
agent-browser set credentials user pass # HTTP basic auth (alias: auth)
agent-browser set media dark # Emulate color scheme
agent-browser set media light reduced-motion # Light mode + reduced motion
agent-browser set webauthn enable # Enable WebAuthn
agent-browser set webauthn add-virtual-authenticator # Add virtual authenticator
```

## Cookies and Storage
Expand Down
2 changes: 2 additions & 0 deletions src/action-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ describe('action-policy', () => {
expect(getActionCategory('session')).toBe('_internal');
expect(getActionCategory('auth_save')).toBe('_internal');
expect(getActionCategory('confirm')).toBe('_internal');
expect(getActionCategory('webauthn_enable')).toBe('_internal');
expect(getActionCategory('webauthn_add_virtual_authenticator')).toBe('_internal');
});

it('should return eval for security-sensitive actions', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/action-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ const ACTION_CATEGORIES: Record<string, string> = {
input_mouse: '_internal',
input_keyboard: '_internal',
input_touch: '_internal',
webauthn_enable: '_internal',
webauthn_add_virtual_authenticator: '_internal',

auth_save: '_internal',
auth_login: '_internal',
Expand Down
22 changes: 22 additions & 0 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ import type {
TimezoneCommand,
LocaleCommand,
HttpCredentialsCommand,
WebAuthnEnableCommand,
WebAuthnAddVirtualAuthenticatorCommand,
MouseMoveCommand,
MouseDownCommand,
MouseUpCommand,
Expand Down Expand Up @@ -543,6 +545,10 @@ async function dispatchAction(command: Command, browser: BrowserManager): Promis
return await handleLocale(command, browser);
case 'credentials':
return await handleCredentials(command, browser);
case 'webauthn_enable':
return await handleWebAuthnEnable(command, browser);
case 'webauthn_add_virtual_authenticator':
return await handleWebAuthnAddVirtualAuthenticator(command, browser);
case 'mousemove':
return await handleMouseMove(command, browser);
case 'mousedown':
Expand Down Expand Up @@ -2349,6 +2355,22 @@ async function handleCredentials(
return successResponse(command.id, { set: true });
}

async function handleWebAuthnEnable(
command: WebAuthnEnableCommand,
browser: BrowserManager
): Promise<Response> {
await browser.enableWebAuthn();
return successResponse(command.id, { enabled: true });
}

async function handleWebAuthnAddVirtualAuthenticator(
command: WebAuthnAddVirtualAuthenticatorCommand,
browser: BrowserManager
): Promise<Response> {
await browser.addVirtualAuthenticator();
return successResponse(command.id, { added: true });
}

async function handleMouseMove(
command: MouseMoveCommand,
browser: BrowserManager
Expand Down
18 changes: 18 additions & 0 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,24 @@ export class BrowserManager {
await cdp.send('Emulation.clearDeviceMetricsOverride');
}

async enableWebAuthn(): Promise<void> {
const cdp = await this.getCDPSession();
await cdp.send('WebAuthn.enable');
}

async addVirtualAuthenticator(): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addVirtualAuthenticator method discards the authenticatorId returned by WebAuthn.addVirtualAuthenticator CDP call, making the ID unavailable for subsequent WebAuthn operations.

Fix on Vercel

const cdp = await this.getCDPSession();
await cdp.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
},
});
}

/**
* Get device descriptor
*/
Expand Down
10 changes: 10 additions & 0 deletions src/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,16 @@ describe('parseCommand', () => {
const result = parseCommand(cmd({ id: '1', action: 'offline', offline: true }));
expect(result.success).toBe(true);
});

it('should parse webauthn_enable', () => {
const result = parseCommand(cmd({ id: '1', action: 'webauthn_enable' }));
expect(result.success).toBe(true);
});

it('should parse webauthn_add_virtual_authenticator', () => {
const result = parseCommand(cmd({ id: '1', action: 'webauthn_add_virtual_authenticator' }));
expect(result.success).toBe(true);
});
});

describe('trace', () => {
Expand Down
10 changes: 10 additions & 0 deletions src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,14 @@ const credentialsSchema = baseCommandSchema.extend({
password: z.string(),
});

const webAuthnEnableSchema = baseCommandSchema.extend({
action: z.literal('webauthn_enable'),
});

const webAuthnAddVirtualAuthenticatorSchema = baseCommandSchema.extend({
action: z.literal('webauthn_add_virtual_authenticator'),
});

const mouseMoveSchema = baseCommandSchema.extend({
action: z.literal('mousemove'),
x: z.number(),
Expand Down Expand Up @@ -1038,6 +1046,8 @@ const commandSchema = z.discriminatedUnion('action', [
timezoneSchema,
localeSchema,
credentialsSchema,
webAuthnEnableSchema,
webAuthnAddVirtualAuthenticatorSchema,
mouseMoveSchema,
mouseDownSchema,
mouseUpSchema,
Expand Down
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,14 @@ export interface HttpCredentialsCommand extends BaseCommand {
password: string;
}

export interface WebAuthnEnableCommand extends BaseCommand {
action: 'webauthn_enable';
}

export interface WebAuthnAddVirtualAuthenticatorCommand extends BaseCommand {
action: 'webauthn_add_virtual_authenticator';
}

// Fine-grained mouse control
export interface MouseMoveCommand extends BaseCommand {
action: 'mousemove';
Expand Down Expand Up @@ -1004,6 +1012,8 @@ export type Command =
| TimezoneCommand
| LocaleCommand
| HttpCredentialsCommand
| WebAuthnEnableCommand
| WebAuthnAddVirtualAuthenticatorCommand
| MouseMoveCommand
| MouseDownCommand
| MouseUpCommand
Expand Down