feat(stealth): humanized typing cadence + --humanize flag (v3)

- Typing: type_text_into_active_context now uses variable, human-like
  inter-keystroke gaps from humanize::keystroke_delays when no explicit --delay
  is given (Fast/Human); Off stays instant. Explicit --delay still wins.
- CLI: `--humanize off|fast|human` surfaces AGENT_BROWSER_HUMANIZE so the
  session's daemon (a child that inherits this env) applies it, overriding the
  adaptive detector. Invalid values warn and are ignored.

Verified headless: `--humanize human` + type lands "hello world" correctly.
Deferred: in-bbox landing jitter (helper ready, needs bbox threaded) + wheel/drag
easing.
This commit is contained in:
leeguooooo
2026-06-11 19:40:32 +09:00
parent bab58991fe
commit a6f0193779
2 changed files with 31 additions and 4 deletions
+17
View File
@@ -248,6 +248,7 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
"--screenshot-format",
"--idle-timeout",
"--model",
"--humanize",
];
let mut i = 0;
while i < args.len() {
@@ -796,6 +797,21 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--humanize" => {
// Human-like input motion level (off|fast|human). Surface it as
// AGENT_BROWSER_HUMANIZE so the daemon — spawned as a child that
// inherits this process's env — picks it up and it overrides the
// adaptive detector. Applies when the session's daemon launches.
if let Some(s) = args.get(i + 1) {
match crate::native::humanize::HumanizeLevel::parse(s) {
Some(_) => std::env::set_var("AGENT_BROWSER_HUMANIZE", s),
None => eprintln!(
"warning: --humanize must be off|fast|human, got {s:?} (ignored)"
),
}
i += 1;
}
}
"--screenshot-dir" => {
if let Some(s) = args.get(i + 1) {
flags.screenshot_dir = Some(s.clone());
@@ -922,6 +938,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--screenshot-format",
"--idle-timeout",
"--model",
"--humanize",
];
let mut i = 0;
+14 -4
View File
@@ -350,9 +350,18 @@ pub async fn type_text_into_active_context(
text: &str,
delay_ms: Option<u64>,
) -> Result<(), String> {
let delay = delay_ms.unwrap_or(0);
// Per-character timing: an explicit `delay_ms` wins (caller asked for a
// fixed cadence); otherwise fall back to humanize — variable, human-like
// inter-keystroke gaps at Fast/Human, all-zero (instant) at Off.
let chars: Vec<char> = text.chars().collect();
let cadence: Vec<std::time::Duration> = match delay_ms {
Some(d) => vec![std::time::Duration::from_millis(d); chars.len()],
None => {
humanize::keystroke_delays(chars.len(), humanize::active_level(), humanize::next_seed())
}
};
for ch in text.chars() {
for (i, ch) in chars.into_iter().enumerate() {
if matches!(ch, '\n' | '\r' | '\t') {
let (key, code, key_code) = char_to_key_info(ch);
let text_str = key_text(&key);
@@ -404,8 +413,9 @@ pub async fn type_text_into_active_context(
.await?;
}
if delay > 0 {
tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
let gap = cadence[i];
if !gap.is_zero() {
tokio::time::sleep(gap).await;
}
}