Skip to content
Merged
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
141 changes: 141 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"crates/browser-use-python-worker",
"crates/browser-use-runtime",
"crates/browser-use-protocol",
"crates/browser-use-secrets",
"crates/browser-use-store",
"crates/browser-use-tui",
]
Expand All @@ -22,6 +23,7 @@ license = "MIT"
version = "0.1.3"

[workspace.dependencies]
aes-gcm = "0.10"
anyhow = "1"
arboard = "3.6.1"
async-trait = "0.1"
Expand All @@ -39,6 +41,7 @@ image = { version = "0.25", default-features = false, features = ["png", "jpeg",
ratatui = { version = "0.30", default-features = false, features = ["crossterm_0_29"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
regex = "1"
rpassword = "7"
rustls = { version = "0.23", default-features = false, features = ["std", "aws_lc_rs"] }
rusqlite = { version = "0.39", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
Expand Down
1 change: 1 addition & 0 deletions crates/browser-use-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ browser-use-providers = { path = "../browser-use-providers" }
browser-use-protocol = { path = "../browser-use-protocol" }
browser-use-python-worker = { path = "../browser-use-python-worker" }
browser-use-runtime = { path = "../browser-use-runtime" }
browser-use-secrets = { path = "../browser-use-secrets" }
browser-use-store = { path = "../browser-use-store" }
anyhow.workspace = true
async-trait.workspace = true
Expand Down
13 changes: 12 additions & 1 deletion crates/browser-use-agent/src/entrypoint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2941,7 +2941,18 @@ async fn run_session_once_with_config_with_cancel(
cancel: CancellationToken,
runtime_handle: RuntimeHandle,
) -> anyhow::Result<()> {
let ctx = turn_ctx(&session_id, &config);
let mut ctx = turn_ctx(&session_id, &config);

// Tell the model which saved credentials exist (names only) and that it can
// use them securely, so it logs in via `<secret>` placeholders instead of
// refusing. Built once per session; secret values are never included.
if let Ok(store_guard) = store.lock() {
if let Some(block) =
crate::tools::handlers::secrets_admin::secrets_prompt_context(&store_guard)
{
ctx.base_instructions.push_str(&block);
}
}

// The single in-run conversation buffer, shared (by `Arc`) between the fused
// driver's `FusionRecorder` (which records the assistant message + dispatched
Expand Down
14 changes: 14 additions & 0 deletions crates/browser-use-agent/src/prompts/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ fn prompts_avoid_screenshots_for_text_heavy_extraction() {
assert!(script.contains("trust it and inspect/extract from the current page"));
}

#[test]
fn browser_script_prompt_guides_model_controlled_email_and_stable_selectors() {
let script = browser_script_tool_description();

assert!(script.contains("fill_input(selector, text, clear=True, timeout=3)"));
assert!(script.contains("current_datetime()"));
assert!(script.contains("email_inbox(limit=20, sent_after=None)"));
assert!(script.contains("email_inbox(sent_after=...)"));
assert!(script.contains("avoid brittle positional selectors"));
assert!(script.contains("input:nth-of-type(2)"));
assert!(script.contains("compare `timestamp`/`message_id` yourself"));
assert!(script.contains("verify the message `timestamp`"));
}

#[test]
fn dataset_prompt_enforces_timeboxed_finalization() {
let prompt = include_str!("../../../../prompts/dataset-case-user.md");
Expand Down
64 changes: 64 additions & 0 deletions crates/browser-use-agent/src/tools/handlers/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,10 +793,60 @@ fn dispatch_browser_preference_command_for_mode(
selected_browser_mode,
)?))
}
"secrets" | "secret" => Ok(Some(dispatch_secrets_command(store, &args)?)),
"domains" | "domain" => Ok(Some(dispatch_domains_command(store, &args)?)),
_ => Ok(None),
}
}

/// Model-facing `browser secrets …`. Read-only: the agent can discover which
/// placeholders exist (so it can call `secret("name")`), but setting/removing a
/// secret carries a value and must be done by the human via the CLI/TUI so the
/// value never enters the model context.
fn dispatch_secrets_command(store: &Store, args: &[String]) -> anyhow::Result<Value> {
use super::secrets_admin as sa;
match args.get(1).map(String::as_str) {
None | Some("list") | Some("--json") | Some("show") => {
let secrets = sa::list_secrets(store)?
.into_iter()
.map(|meta| {
json!({
"domain": meta.domain,
"name": meta.placeholder,
"kind": meta.kind.as_str(),
"allowed_domains": meta.allowed_domains,
})
})
.collect::<Vec<_>>();
Ok(json!({ "status": "ok", "secrets": secrets }))
}
Some("set" | "add" | "remove" | "rm" | "delete") => bail!(
"Secrets must be set or removed by the user so the value never enters the agent's \
context. Ask the user to run `browser-use-terminal secrets set --domain <domain> \
--name <name>` (add `--totp` for 2FA), or `secrets remove --domain <domain> \
--name <name>`."
),
Some(other) => bail!("unknown browser secrets command: {other}"),
}
}

/// Model-facing `browser domains …`. Read-only for the same reason the
/// navigation guard exists: the agent must not be able to widen its own policy.
fn dispatch_domains_command(store: &Store, args: &[String]) -> anyhow::Result<Value> {
use super::secrets_admin as sa;
match args.get(1).map(String::as_str) {
None | Some("list") | Some("--json") | Some("show") => {
let (allowed, denied) = sa::list_domains(store)?;
Ok(json!({ "status": "ok", "allowed": allowed, "denied": denied }))
}
Some("allow" | "deny" | "clear") => bail!(
"The navigation allow/deny policy must be changed by the user. Ask them to run \
`browser-use-terminal domains allow <domain>` or `domains deny <domain>`."
),
Some(other) => bail!("unknown browser domains command: {other}"),
}
}

fn dispatch_browser_preference(
store: &Store,
args: &[String],
Expand Down Expand Up @@ -3018,6 +3068,20 @@ impl ToolRuntime<BrowserRequest, ExecOutput> for BrowserTool {
)
.map_err(ToolError::Other)?;
}
// Re-resolve the secrets + nav policy on every run (fail closed)
// so secret/domain changes take effect mid-session. Cheap now
// that values live in an encrypted file, not the OS keychain.
if let Some(persistence) = &persistence {
let store = persistence.store.lock().map_err(|_| {
ToolError::Other(anyhow::anyhow!("store mutex poisoned"))
})?;
super::secrets_admin::install_script_security(&store, &session_id)
.map_err(|error| {
ToolError::Other(anyhow::anyhow!(
"failed to apply browser security policy: {error:#}"
))
})?;
}
let out = backend
.start_script(&session_id, &cwd, &artifact_dir, &script, timeout_secs)
.map_err(ToolError::Other)?;
Expand Down
Loading