diff --git a/apps/extension/src/transport/types.ts b/apps/extension/src/transport/types.ts index 4bc2d61..275d4e8 100644 --- a/apps/extension/src/transport/types.ts +++ b/apps/extension/src/transport/types.ts @@ -518,7 +518,13 @@ export interface HelpTarget { selector?: string; } -export type HelpOutcome = "continued" | "cancelled" | "timed_out" | "completed" | "navigated"; +export type HelpOutcome = + | "continued" + | "cancelled" + | "timed_out" + | "completed" + | "navigated" + | "disabled"; export interface HelpCompletionCondition { url_contains?: string; diff --git a/crates/bsk-cli/skill/SKILL.md b/crates/bsk-cli/skill/SKILL.md index 43d2417..0eecabf 100644 --- a/crates/bsk-cli/skill/SKILL.md +++ b/crates/bsk-cli/skill/SKILL.md @@ -219,6 +219,11 @@ which refs/selectors matched a live element. control. After a `continued` or `completed` result, issue a separate observation tool call (usually `bsk snapshot --session `) before using new refs or reasoning about the post-help page state. +#### Disabling request-help (unattended mode) +Set `BSK_REQUEST_HELP=off` on unattended servers: `bsk request-help` then +returns immediately with `outcome="disabled"` (no overlay, no waiting, +exit 0). Any other value keeps it enabled. If you get `disabled`, do not +retry — complete the task autonomously or stop gracefully. ### Recording — `bsk record` diff --git a/crates/bsk-cli/src/cli/human_loop.rs b/crates/bsk-cli/src/cli/human_loop.rs index deda217..25a110e 100644 --- a/crates/bsk-cli/src/cli/human_loop.rs +++ b/crates/bsk-cli/src/cli/human_loop.rs @@ -8,7 +8,7 @@ use std::time::Duration; use anyhow::Context; use bsk_protocol::Method; use bsk_protocol::tools::{ - HelpCompletionCriteria, HelpTarget, RequestHelpParams, RequestHelpResult, + HelpCompletionCriteria, HelpOutcome, HelpTarget, RequestHelpParams, RequestHelpResult, }; use clap::Args; @@ -16,6 +16,31 @@ use crate::cli::ensure_daemon::ensure_daemon; use crate::cli::error::{CliError, Format}; use crate::cli::navigate::parse_timeout_ms; +/// Environment variable that disables the blocking `request-help` +/// human-in-loop tool, for unattended / background-server deployments +/// where a blocking help overlay must never appear. +pub(crate) const REQUEST_HELP_ENV: &str = "BSK_REQUEST_HELP"; + +/// Note attached to the synthetic result returned when request-help is +/// disabled, so the agent knows why no overlay was shown. +pub(crate) const REQUEST_HELP_DISABLED_NOTE: &str = + "request-help disabled by BSK_REQUEST_HELP=off (unattended mode)"; + +/// Whether `request-help` is disabled via [`REQUEST_HELP_ENV`]. Only the +/// value `off` (case-insensitive, surrounding whitespace ignored) +/// disables it; unset or any other value keeps the default enabled +/// behaviour. +pub(crate) fn request_help_disabled() -> bool { + is_off_value(std::env::var(REQUEST_HELP_ENV).ok().as_deref()) +} + +/// Pure value check behind [`request_help_disabled`], kept separate so it +/// can be unit-tested without touching process env (parallel tests race +/// on `std::env::set_var`, which is also `unsafe` in edition 2024). +fn is_off_value(value: Option<&str>) -> bool { + value.is_some_and(|v| v.trim().eq_ignore_ascii_case("off")) +} + #[derive(Debug, Clone, Args)] pub struct RequestHelpArgs { #[arg(long)] @@ -72,6 +97,18 @@ pub fn parse_target(raw: &str) -> HelpTarget { } pub fn dispatch(args: RequestHelpArgs, format: Format) -> Result<(), CliError> { + // Disabled (`BSK_REQUEST_HELP=off`): return a synthetic `disabled` + // result immediately — no daemon startup, no overlay, no waiting. + if request_help_disabled() { + let result = RequestHelpResult { + outcome: HelpOutcome::Disabled, + completed_by: None, + note: Some(REQUEST_HELP_DISABLED_NOTE.into()), + tab_id: args.tab_id.unwrap_or(0), + resolved_targets: None, + }; + return render(&result, format); + } let info = ensure_daemon().context("ensure daemon is running")?; let targets: Vec = args.target.iter().map(|t| parse_target(t)).collect(); let completion_criteria = args @@ -156,4 +193,19 @@ mod tests { assert_eq!(parse_target("email").selector.as_deref(), Some("email")); assert!(parse_target("#login").ref_.is_none()); } + + #[test] + fn off_value_detection() { + assert!(is_off_value(Some("off"))); + assert!(is_off_value(Some("OFF"))); + assert!(is_off_value(Some("Off"))); + assert!(is_off_value(Some(" off "))); + assert!(is_off_value(Some("\toff\n"))); + assert!(!is_off_value(None)); + assert!(!is_off_value(Some(""))); + assert!(!is_off_value(Some("on"))); + assert!(!is_off_value(Some("0"))); + assert!(!is_off_value(Some("offx"))); + assert!(!is_off_value(Some("of f"))); + } } diff --git a/crates/bsk-cli/src/daemon/ipc.rs b/crates/bsk-cli/src/daemon/ipc.rs index 0efb977..f76cc0a 100644 --- a/crates/bsk-cli/src/daemon/ipc.rs +++ b/crates/bsk-cli/src/daemon/ipc.rs @@ -26,7 +26,9 @@ use bsk_protocol::system::{ BrowserListParams, BrowserStatusEntry, SessionStatusEntry, StatusParams, StatusResult, VersionSkewEntry, }; -use bsk_protocol::tools::{ReturnFailure, WaitMsParams, WaitMsResult}; +use bsk_protocol::tools::{ + HelpOutcome, RequestHelpResult, ReturnFailure, WaitMsParams, WaitMsResult, +}; use bsk_protocol::{ CancelParams, CancelResult, ErrorCode, Method, PingResult, ResponseBody, RpcError, RpcId, }; @@ -280,6 +282,19 @@ async fn handle_tool_dispatch( method: Method, params: Value, ) -> ResponseBody { + // `BSK_REQUEST_HELP=off` (unattended mode): never forward the + // blocking human-in-loop call to the extension; answer immediately + // with a synthetic `disabled` result. + if method == Method::ToolRequestHelp && crate::cli::human_loop::request_help_disabled() { + let result = RequestHelpResult { + outcome: HelpOutcome::Disabled, + completed_by: None, + note: Some(crate::cli::human_loop::REQUEST_HELP_DISABLED_NOTE.into()), + tab_id: params.get("tab_id").and_then(Value::as_i64).unwrap_or(0), + resolved_targets: None, + }; + return ResponseBody::Ok(serde_json::to_value(result).unwrap_or(Value::Null)); + } let session_id = match params.get("session_id").and_then(|v| v.as_str()) { Some(s) if !s.is_empty() => SessionId(s.to_string()), _ => { diff --git a/crates/bsk-protocol/schema/tool_request_help_result.json b/crates/bsk-protocol/schema/tool_request_help_result.json index 1b6ad4b..d7b7dc7 100644 --- a/crates/bsk-protocol/schema/tool_request_help_result.json +++ b/crates/bsk-protocol/schema/tool_request_help_result.json @@ -83,6 +83,13 @@ "enum": [ "navigated" ] + }, + { + "description": "request-help is disabled by configuration (`BSK_REQUEST_HELP=off`): the call returns immediately without ever showing the overlay.", + "type": "string", + "enum": [ + "disabled" + ] } ] }, diff --git a/crates/bsk-protocol/src/tools/human_loop.rs b/crates/bsk-protocol/src/tools/human_loop.rs index 7afbfc1..3b7374f 100644 --- a/crates/bsk-protocol/src/tools/human_loop.rs +++ b/crates/bsk-protocol/src/tools/human_loop.rs @@ -95,6 +95,9 @@ pub enum HelpOutcome { /// The page navigated while waiting (full reload or SPA URL change). /// Deprecated: navigation alone should not end a help request. Navigated, + /// request-help is disabled by configuration (`BSK_REQUEST_HELP=off`): + /// the call returns immediately without ever showing the overlay. + Disabled, } impl HelpOutcome { @@ -105,6 +108,7 @@ impl HelpOutcome { Self::TimedOut => "timed_out", Self::Completed => "completed", Self::Navigated => "navigated", + Self::Disabled => "disabled", } } } @@ -202,6 +206,10 @@ mod tests { serde_json::to_value(HelpOutcome::Navigated).unwrap(), json!("navigated") ); + assert_eq!( + serde_json::to_value(HelpOutcome::Disabled).unwrap(), + json!("disabled") + ); } #[test] diff --git a/skill/SKILL.md b/skill/SKILL.md index 43d2417..0eecabf 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -219,6 +219,11 @@ which refs/selectors matched a live element. control. After a `continued` or `completed` result, issue a separate observation tool call (usually `bsk snapshot --session `) before using new refs or reasoning about the post-help page state. +#### Disabling request-help (unattended mode) +Set `BSK_REQUEST_HELP=off` on unattended servers: `bsk request-help` then +returns immediately with `outcome="disabled"` (no overlay, no waiting, +exit 0). Any other value keeps it enabled. If you get `disabled`, do not +retry — complete the task autonomously or stop gracefully. ### Recording — `bsk record`