diff --git a/README.md b/README.md index 2d25c9b..87b6619 100644 --- a/README.md +++ b/README.md @@ -334,6 +334,7 @@ agent-browser snapshot -i -c -d 5 # Combine options | `--exact` | Exact text match | | `--headed` | Show browser window (not headless) | | `--cdp ` | Connect via Chrome DevTools Protocol | +| `--ignore-https-errors` | Ignore HTTPS certificate errors (useful for self-signed certs) | | `--debug` | Debug output | ## Selectors diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 66c19bf..535aae1 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -1212,6 +1212,7 @@ mod tests { args: None, user_agent: None, provider: None, + ignore_https_errors: false, } } diff --git a/cli/src/connection.rs b/cli/src/connection.rs index 7221335..2625a5a 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -194,6 +194,7 @@ pub fn ensure_daemon( user_agent: Option<&str>, proxy: Option<&str>, proxy_bypass: Option<&str>, + ignore_https_errors: bool, ) -> Result { if is_daemon_running(session) && daemon_ready(session) { return Ok(DaemonResult { @@ -266,6 +267,10 @@ pub fn ensure_daemon( cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb); } + if ignore_https_errors { + cmd.env("AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "1"); + } + // Create new process group and session to fully detach unsafe { cmd.pre_exec(|| { @@ -321,6 +326,10 @@ pub fn ensure_daemon( cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb); } + if ignore_https_errors { + cmd.env("AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "1"); + } + // CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; const DETACHED_PROCESS: u32 = 0x00000008; diff --git a/cli/src/flags.rs b/cli/src/flags.rs index 713559a..63629f6 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -16,6 +16,7 @@ pub struct Flags { pub args: Option, pub user_agent: Option, pub provider: Option, + pub ignore_https_errors: bool, } pub fn parse_flags(args: &[String]) -> Flags { @@ -40,6 +41,7 @@ pub fn parse_flags(args: &[String]) -> Flags { args: env::var("AGENT_BROWSER_ARGS").ok(), user_agent: env::var("AGENT_BROWSER_USER_AGENT").ok(), provider: env::var("AGENT_BROWSER_PROVIDER").ok(), + ignore_https_errors: false, }; let mut i = 0; @@ -115,6 +117,7 @@ pub fn parse_flags(args: &[String]) -> Flags { i += 1; } } + "--ignore-https-errors" => flags.ignore_https_errors = true, _ => {} } i += 1; @@ -127,7 +130,7 @@ pub fn clean_args(args: &[String]) -> Vec { let mut skip_next = false; // Global flags that should be stripped from command args - const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"]; + const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug", "--ignore-https-errors"]; // Global flags that take a value (need to skip the next arg too) const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[ "--session", diff --git a/cli/src/main.rs b/cli/src/main.rs index 84d5d5d..cd8b9f5 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -200,6 +200,7 @@ fn main() { flags.user_agent.as_deref(), flags.proxy.as_deref(), flags.proxy_bypass.as_deref(), + flags.ignore_https_errors, ) { Ok(result) => result, Err(e) => { @@ -223,6 +224,7 @@ fn main() { flags.user_agent.as_ref().map(|_| "--user-agent"), flags.proxy.as_ref().map(|_| "--proxy"), flags.proxy_bypass.as_ref().map(|_| "--proxy-bypass"), + flags.ignore_https_errors.then(|| "--ignore-https-errors"), ] .into_iter() .flatten() @@ -235,6 +237,10 @@ fn main() { ignored_flags.join(", ") ); } + + if flags.ignore_https_errors { + eprintln!("{} --ignore-https-errors ignored: daemon already running. Use 'agent-browser close' first to restart with this option.", color::warning_indicator()); + } } // Validate mutually exclusive options @@ -261,7 +267,7 @@ fn main() { // Connect via CDP if --cdp flag is set // Accepts either a port number (e.g., "9222") or a full URL (e.g., "ws://..." or "wss://...") if let Some(ref cdp_value) = flags.cdp { - let launch_cmd = if cdp_value.starts_with("ws://") + let mut launch_cmd = if cdp_value.starts_with("ws://") || cdp_value.starts_with("wss://") || cdp_value.starts_with("http://") || cdp_value.starts_with("https://") @@ -317,6 +323,10 @@ fn main() { }) }; + if flags.ignore_https_errors { + launch_cmd["ignoreHTTPSErrors"] = json!(true); + } + let err = match send_command(launch_cmd, &flags.session) { Ok(resp) if resp.success => None, Ok(resp) => Some( @@ -401,6 +411,10 @@ fn main() { cmd_obj.insert("args".to_string(), json!(args_vec)); } + if flags.ignore_https_errors { + launch_cmd["ignoreHTTPSErrors"] = json!(true); + } + if let Err(e) = send_command(launch_cmd, &flags.session) { if !flags.json { eprintln!("{} Could not configure browser: {}", color::warning_indicator(), e); diff --git a/cli/src/output.rs b/cli/src/output.rs index 45eb8b5..2e34862 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -1535,6 +1535,7 @@ Options: e.g., --proxy "http://user:pass@127.0.0.1:7890" --proxy-bypass Bypass proxy for these hosts (or AGENT_BROWSER_PROXY_BYPASS) e.g., --proxy-bypass "localhost,*.internal.com" + --ignore-https-errors Ignore HTTPS certificate errors -p, --provider Cloud browser provider (or AGENT_BROWSER_PROVIDER env) --json JSON output --full, -f Full page screenshot diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index 5eda6a7..6344fcf 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -347,3 +347,10 @@ Usage: ./templates/authenticated-session.sh https://app.example.com/login ./templates/capture-workflow.sh https://example.com ./output ``` + +## HTTPS Certificate Errors + +For sites with self-signed or invalid certificates: +```bash +agent-browser open https://localhost:8443 --ignore-https-errors +``` diff --git a/src/browser.ts b/src/browser.ts index fb75d59..415da78 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -883,6 +883,7 @@ export class BrowserManager { extraHTTPHeaders: options.headers, userAgent: options.userAgent, ...(options.proxy && { proxy: options.proxy }), + ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false, } ); this.isPersistentContext = true; @@ -910,6 +911,7 @@ export class BrowserManager { extraHTTPHeaders: options.headers, userAgent: options.userAgent, ...(options.proxy && { proxy: options.proxy }), + ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false, }); } diff --git a/src/daemon.ts b/src/daemon.ts index 0f0fb08..fc9dc1c 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -248,6 +248,7 @@ export async function startDaemon(options?: { streamPort?: number }): Promise { const result = parseCommand(cmd({ id: '1', action: 'launch', cdpPort: 'invalid' })); expect(result.success).toBe(false); }); + + it('should parse launch with ignoreHTTPSErrors true', () => { + const result = parseCommand(cmd({ id: '1', action: 'launch', ignoreHTTPSErrors: true })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.ignoreHTTPSErrors).toBe(true); + } + }); + + it('should parse launch with ignoreHTTPSErrors false', () => { + const result = parseCommand(cmd({ id: '1', action: 'launch', ignoreHTTPSErrors: false })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.ignoreHTTPSErrors).toBe(false); + } + }); + + it('should reject launch with non-boolean ignoreHTTPSErrors', () => { + const result = parseCommand(cmd({ id: '1', action: 'launch', ignoreHTTPSErrors: 'true' })); + expect(result.success).toBe(false); + }); }); describe('mouse actions', () => { diff --git a/src/protocol.ts b/src/protocol.ts index 8f899eb..2bbed95 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -45,6 +45,7 @@ const launchSchema = baseCommandSchema.extend({ args: z.array(z.string()).optional(), userAgent: z.string().optional(), provider: z.string().optional(), + ignoreHTTPSErrors: z.boolean().optional(), }); const navigateSchema = baseCommandSchema.extend({ diff --git a/src/types.ts b/src/types.ts index 6fdcbb9..e2d5faf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,6 +27,7 @@ export interface LaunchCommand extends BaseCommand { args?: string[]; userAgent?: string; provider?: string; + ignoreHTTPSErrors?: boolean; } export interface NavigateCommand extends BaseCommand {