add custom headers via --headers

This commit is contained in:
Chris Tate
2026-01-12 11:22:46 -06:00
parent 3cd0ab468f
commit 8c412197ad
7 changed files with 142 additions and 4 deletions
+36
View File
@@ -293,6 +293,7 @@ agent-browser snapshot -i -c -d 5 # Combine options
| Option | Description |
|--------|-------------|
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
| `--headers <json>` | Set HTTP headers scoped to the URL's origin |
| `--json` | JSON output (for agents) |
| `--full, -f` | Full page screenshot |
| `--name, -n` | Locator name filter |
@@ -387,6 +388,41 @@ agent-browser open example.com --headed
This opens a visible browser window instead of running headless.
## Authenticated Sessions
Use `--headers` to set HTTP headers for a specific origin, enabling authentication without login flows:
```bash
# Headers are scoped to api.example.com only
agent-browser open api.example.com --headers '{"Authorization": "Bearer <token>"}'
# Requests to api.example.com include the auth header
agent-browser snapshot -i --json
agent-browser click @e2
# Navigate to another domain - headers are NOT sent (safe!)
agent-browser open other-site.com
```
This is useful for:
- **Skipping login flows** - Authenticate via headers instead of UI
- **Switching users** - Start new sessions with different auth tokens
- **API testing** - Access protected endpoints directly
- **Security** - Headers are scoped to the origin, not leaked to other domains
To set headers for multiple origins, use `--headers` with each `open` command:
```bash
agent-browser open api.example.com --headers '{"Authorization": "Bearer token1"}'
agent-browser open api.acme.com --headers '{"Authorization": "Bearer token2"}'
```
For global headers (all domains), use `set headers`:
```bash
agent-browser set headers '{"X-Custom-Header": "value"}'
```
## Architecture
agent-browser uses a client-daemon architecture:
+9 -1
View File
@@ -80,7 +80,14 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
} else {
format!("https://{}", url)
};
Ok(json!({ "id": id, "action": "navigate", "url": url }))
let mut nav_cmd = json!({ "id": id, "action": "navigate", "url": url });
// If --headers flag is set, include headers (scoped to this origin)
if let Some(ref headers_json) = flags.headers {
if let Ok(headers) = serde_json::from_str::<serde_json::Value>(headers_json) {
nav_cmd["headers"] = headers;
}
}
Ok(nav_cmd)
}
"back" => Ok(json!({ "id": id, "action": "back" })),
"forward" => Ok(json!({ "id": id, "action": "forward" })),
@@ -886,6 +893,7 @@ mod tests {
full: false,
headed: false,
debug: false,
headers: None,
}
}
+11 -1
View File
@@ -6,6 +6,7 @@ pub struct Flags {
pub headed: bool,
pub debug: bool,
pub session: String,
pub headers: Option<String>,
}
pub fn parse_flags(args: &[String]) -> Flags {
@@ -15,6 +16,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
headed: false,
debug: false,
session: env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string()),
headers: None,
};
let mut i = 0;
@@ -30,6 +32,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--headers" => {
if let Some(h) = args.get(i + 1) {
flags.headers = Some(h.clone());
i += 1;
}
}
_ => {}
}
i += 1;
@@ -43,13 +51,15 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
// Global flags that should be stripped from command args
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
// Global flags that take a value (need to skip the next arg too)
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers"];
for arg in args.iter() {
if skip_next {
skip_next = false;
continue;
}
if arg == "--session" {
if GLOBAL_FLAGS_WITH_VALUE.contains(&arg.as_str()) {
skip_next = true;
continue;
}
+4
View File
@@ -162,12 +162,15 @@ Aliases: goto, navigate
Global Options:
--json Output as JSON
--session <name> Use specific session
--headers <json> Set HTTP headers (scoped to this origin)
--headed Show browser window
Examples:
agent-browser open example.com
agent-browser open https://github.com
agent-browser open localhost:3000
agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}'
# ^ Headers only sent to api.example.com, not other domains
"##,
"back" => r##"
agent-browser back - Navigate back in history
@@ -1186,6 +1189,7 @@ Snapshot Options:
Options:
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
--headers <json> HTTP headers scoped to URL's origin (for auth)
--json JSON output
--full, -f Full page screenshot
--headed Show browser window (not headless)
+6
View File
@@ -411,6 +411,12 @@ async function handleNavigate(
browser: BrowserManager
): Promise<Response<NavigateData>> {
const page = browser.getPage();
// If headers are provided, set up scoped headers for this origin
if (command.headers && Object.keys(command.headers).length > 0) {
await browser.setScopedHeaders(command.url, command.headers);
}
await page.goto(command.url, {
waitUntil: command.waitUntil ?? 'load',
});
+74 -2
View File
@@ -51,6 +51,7 @@ export class BrowserManager {
private isRecordingHar: boolean = false;
private refMap: RefMap = {};
private lastSnapshot: string = '';
private scopedHeaderRoutes: Map<string, (route: Route) => Promise<void>> = new Map();
/**
* Check if browser is launched
@@ -439,7 +440,7 @@ export class BrowserManager {
}
/**
* Set extra HTTP headers
* Set extra HTTP headers (global - all requests)
*/
async setExtraHeaders(headers: Record<string, string>): Promise<void> {
const context = this.contexts[0];
@@ -448,6 +449,76 @@ export class BrowserManager {
}
}
/**
* Set scoped HTTP headers (only for requests matching the origin)
* Uses route interception to add headers only to matching requests
*/
async setScopedHeaders(origin: string, headers: Record<string, string>): Promise<void> {
const page = this.getPage();
// Build URL pattern from origin (e.g., "api.example.com" -> "**://api.example.com/**")
// Handle both full URLs and just hostnames
let urlPattern: string;
try {
const url = new URL(origin.startsWith('http') ? origin : `https://${origin}`);
// Match any protocol, the host, and any path
urlPattern = `**://${url.host}/**`;
} catch {
// If parsing fails, treat as hostname pattern
urlPattern = `**://${origin}/**`;
}
// Remove existing route for this origin if any
const existingHandler = this.scopedHeaderRoutes.get(urlPattern);
if (existingHandler) {
await page.unroute(urlPattern, existingHandler);
}
// Create handler that adds headers to matching requests
const handler = async (route: Route) => {
const requestHeaders = route.request().headers();
await route.continue({
headers: {
...requestHeaders,
...headers,
},
});
};
// Store and register the route
this.scopedHeaderRoutes.set(urlPattern, handler);
await page.route(urlPattern, handler);
}
/**
* Clear scoped headers for an origin (or all if no origin specified)
*/
async clearScopedHeaders(origin?: string): Promise<void> {
const page = this.getPage();
if (origin) {
let urlPattern: string;
try {
const url = new URL(origin.startsWith('http') ? origin : `https://${origin}`);
urlPattern = `**://${url.host}/**`;
} catch {
urlPattern = `**://${origin}/**`;
}
const handler = this.scopedHeaderRoutes.get(urlPattern);
if (handler) {
await page.unroute(urlPattern, handler);
this.scopedHeaderRoutes.delete(urlPattern);
}
} else {
// Clear all scoped header routes
for (const [pattern, handler] of this.scopedHeaderRoutes) {
await page.unroute(pattern, handler);
}
this.scopedHeaderRoutes.clear();
}
}
/**
* Start tracing
*/
@@ -522,9 +593,10 @@ export class BrowserManager {
headless: options.headless ?? true,
});
// Create context with viewport
// Create context with viewport and optional headers
const context = await this.browser.newContext({
viewport: options.viewport ?? { width: 1280, height: 720 },
extraHTTPHeaders: options.headers,
});
// Set default timeout to 10 seconds (Playwright default is 30s)
+2
View File
@@ -12,12 +12,14 @@ export interface LaunchCommand extends BaseCommand {
headless?: boolean;
viewport?: { width: number; height: number };
browser?: 'chromium' | 'firefox' | 'webkit';
headers?: Record<string, string>;
}
export interface NavigateCommand extends BaseCommand {
action: 'navigate';
url: string;
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
headers?: Record<string, string>;
}
export interface ClickCommand extends BaseCommand {